예제 #1
0
class NetAppOntapLUN(object):
    ''' create, modify, delete LUN '''
    def __init__(self):

        self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
        self.argument_spec.update(
            dict(
                state=dict(required=False,
                           type='str',
                           choices=['present', 'absent'],
                           default='present'),
                name=dict(required=True, type='str'),
                from_name=dict(required=False, type='str'),
                size=dict(type='int'),
                size_unit=dict(default='gb',
                               choices=[
                                   'bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb',
                                   'eb', 'zb', 'yb'
                               ],
                               type='str'),
                force_resize=dict(default=False, type='bool'),
                force_remove=dict(default=False, type='bool'),
                force_remove_fenced=dict(default=False, type='bool'),
                flexvol_name=dict(type='str'),
                vserver=dict(required=True, type='str'),
                os_type=dict(required=False, type='str', aliases=['ostype']),
                qos_policy_group=dict(required=False, type='str'),
                space_reserve=dict(required=False, type='bool', default=True),
                space_allocation=dict(required=False,
                                      type='bool',
                                      default=False),
                use_exact_size=dict(required=False, type='bool', default=True),
                san_application_template=dict(
                    type='dict',
                    options=dict(
                        use_san_application=dict(type='bool', default=True),
                        name=dict(required=True, type='str'),
                        igroup_name=dict(type='str'),
                        lun_count=dict(type='int'),
                        protection_type=dict(
                            type='dict',
                            options=dict(local_policy=dict(type='str'), )),
                        storage_service=dict(
                            type='str',
                            choices=['value', 'performance', 'extreme']),
                        tiering=dict(
                            type='dict',
                            options=dict(
                                control=dict(type='str',
                                             choices=[
                                                 'required', 'best_effort',
                                                 'disallowed'
                                             ]),
                                policy=dict(type='str',
                                            choices=[
                                                'all', 'auto', 'none',
                                                'snapshot-only'
                                            ]),
                                object_stores=dict(
                                    type='list', elements='str')  # create only
                            )),
                    ))))

        self.module = AnsibleModule(argument_spec=self.argument_spec,
                                    supports_check_mode=True)

        # set up state variables
        self.na_helper = NetAppModule()
        self.parameters = self.na_helper.set_parameters(self.module.params)
        if self.parameters.get('size') is not None:
            self.parameters['size'] *= netapp_utils.POW2_BYTE_MAP[
                self.parameters['size_unit']]

        if HAS_NETAPP_LIB is False:
            self.module.fail_json(
                msg="the python NetApp-Lib module is required")
        else:
            self.server = netapp_utils.setup_na_ontap_zapi(
                module=self.module, vserver=self.parameters['vserver'])

        # REST API for application/applications if needed
        self.rest_api, self.rest_app = self.setup_rest_application()

    def setup_rest_application(self):
        use_application_template = self.na_helper.safe_get(
            self.parameters,
            ['san_application_template', 'use_san_application'])
        rest_api, rest_app = None, None
        if use_application_template:
            if self.parameters.get('flexvol_name') is not None:
                self.module.fail_json(
                    msg=
                    "'flexvol_name' option is not supported when san_application_template is present"
                )
            rest_api = netapp_utils.OntapRestAPI(self.module)
            name = self.na_helper.safe_get(
                self.parameters, ['san_application_template', 'name'],
                allow_sparse_dict=False)
            rest_app = RestApplication(rest_api, self.parameters['vserver'],
                                       name)
        elif self.parameters.get('flexvol_name') is None:
            self.module.fail_json(
                msg=
                "flexvol_name option is required when san_application_template is not present"
            )
        return rest_api, rest_app

    def get_luns(self, lun_path=None):
        """
        Return list of LUNs matching vserver and volume names.

        :return: list of LUNs in XML format.
        :rtype: list
        """
        luns = []
        tag = None
        if lun_path is None and self.parameters.get('flexvol_name') is None:
            return luns

        query_details = netapp_utils.zapi.NaElement('lun-info')
        query_details.add_new_child('vserver', self.parameters['vserver'])
        if lun_path is not None:
            query_details.add_new_child('lun_path', lun_path)
        else:
            query_details.add_new_child('volume',
                                        self.parameters['flexvol_name'])
        query = netapp_utils.zapi.NaElement('query')
        query.add_child_elem(query_details)

        while True:
            lun_info = netapp_utils.zapi.NaElement('lun-get-iter')
            lun_info.add_child_elem(query)
            if tag:
                lun_info.add_new_child('tag', tag, True)

            result = self.server.invoke_successfully(lun_info, True)
            if result.get_child_by_name('num-records') and int(
                    result.get_child_content('num-records')) >= 1:
                attr_list = result.get_child_by_name('attributes-list')
                luns.extend(attr_list.get_children())
            tag = result.get_child_content('next-tag')
            if tag is None:
                break
        return luns

    def get_lun_details(self, lun):
        """
        Extract LUN details, from XML to python dict

        :return: Details about the lun
        :rtype: dict
        """
        return_value = dict()
        return_value['size'] = int(lun.get_child_content('size'))
        bool_attr_map = {
            'is-space-alloc-enabled': 'space_allocation',
            'is-space-reservation-enabled': 'space_reserve'
        }
        for attr in bool_attr_map:
            value = lun.get_child_content(attr)
            if value is not None:
                return_value[
                    bool_attr_map[attr]] = self.na_helper.get_value_for_bool(
                        True, value)
        str_attr_map = {
            'name': 'name',
            'path': 'path',
            'qos-policy-group': 'qos_policy_group',
            'multiprotocol-type': 'os_type'
        }
        for attr in str_attr_map:
            value = lun.get_child_content(attr)
            if value is not None:
                return_value[str_attr_map[attr]] = value

        # Find out if the lun is attached
        attached_to = None
        lun_id = None
        if lun.get_child_content('mapped') == 'true':
            lun_map_list = netapp_utils.zapi.NaElement.create_node_with_children(
                'lun-map-list-info', **{'path': lun.get_child_content('path')})
            result = self.server.invoke_successfully(lun_map_list,
                                                     enable_tunneling=True)
            igroups = result.get_child_by_name('initiator-groups')
            if igroups:
                for igroup_info in igroups.get_children():
                    igroup = igroup_info.get_child_content(
                        'initiator-group-name')
                    attached_to = igroup
                    lun_id = igroup_info.get_child_content('lun-id')

        return_value.update({'attached_to': attached_to, 'lun_id': lun_id})
        return return_value

    def find_lun(self, luns, name, lun_path=None):
        """
        Return lun record matching name or path

        :return: lun record
        :rtype: XML or None if not found
        """
        for lun in luns:
            path = lun.get_child_content('path')
            if lun_path is not None:
                if lun_path == path:
                    return lun
            else:
                if name == path:
                    return lun
                _rest, _splitter, found_name = path.rpartition('/')
                if found_name == name:
                    return lun
        return None

    def get_lun(self, name, lun_path=None):
        """
        Return details about the LUN

        :return: Details about the lun
        :rtype: dict
        """
        luns = self.get_luns(lun_path)
        lun = self.find_lun(luns, name, lun_path)
        if lun is not None:
            return self.get_lun_details(lun)
        return None

    def get_luns_from_app(self):
        app_details, error = self.rest_app.get_application_details()
        self.fail_on_error(error)
        if app_details is not None:
            app_details['paths'] = self.get_lun_paths_from_app()
        return app_details

    def get_lun_paths_from_app(self):
        """Get luns path for SAN application"""
        backing_storage, error = self.rest_app.get_application_component_backing_storage(
        )
        self.fail_on_error(error)
        # {'luns': [{'path': '/vol/ansibleLUN/ansibleLUN_1', ...
        if backing_storage is not None:
            return [lun['path'] for lun in backing_storage.get('luns', [])]
        return None

    def get_lun_path_from_backend(self, name):
        """returns lun path matching name if found in backing_storage
           retruns None if not found
        """
        lun_paths = self.get_lun_paths_from_app()
        match = "/%s" % name
        for path in lun_paths:
            if path.endswith(match):
                return path
        return None

    def create_san_app_component(self):
        '''Create SAN application component'''
        required_options = ('name', 'size')
        for option in required_options:
            if self.parameters.get(option) is None:
                self.module.fail_json(
                    msg='Error: "%s" is required to create san application.' %
                    option)

        application_component = dict(
            name=self.parameters['name'],
            total_size=self.parameters['size'],
            lun_count=1  # default value, may be overriden below
        )
        for attr in ('igroup_name', 'lun_count', 'storage_service'):
            value = self.na_helper.safe_get(self.parameters,
                                            ['san_application_template', attr])
            if value is not None:
                application_component[attr] = value
        for attr in ('os_type', 'qos_policy_group'):
            value = self.na_helper.safe_get(self.parameters, [attr])
            if value is not None:
                if attr == 'qos_policy_group':
                    attr = 'qos'
                    value = dict(policy=dict(name=value))
                application_component[attr] = value
        tiering = self.na_helper.safe_get(
            self.parameters, ['nas_application_template', 'tiering'])
        if tiering is not None:
            application_component['tiering'] = dict()
            for attr in ('control', 'policy', 'object_stores'):
                value = tiering.get(attr)
                if attr == 'object_stores' and value is not None:
                    value = [dict(name=x) for x in value]
                if value is not None:
                    application_component['tiering'][attr] = value
        return application_component

    def create_san_app_body(self):
        '''Create body for san template'''
        # TODO:
        # Should we support new_igroups?
        # It may raise idempotency issues if the REST call fails if the igroup already exists.
        # And we already have na_ontap_igroups.
        san = {
            'application_components': [self.create_san_app_component()],
        }
        for attr in ('protection_type', ):
            value = self.na_helper.safe_get(self.parameters,
                                            ['san_application_template', attr])
            if value is not None:
                # we expect value to be a dict, but maybe an empty dict
                value = self.na_helper.filter_out_none_entries(value)
                if value:
                    san[attr] = value
        for attr in ('os_type', ):
            value = self.na_helper.safe_get(self.parameters, [attr])
            if value is not None:
                san[attr] = value
        body, error = self.rest_app.create_application_body('san', san)
        return body, error

    def create_san_application(self):
        '''Use REST application/applications san template to create one or more LUNs'''
        body, error = self.create_san_app_body()
        self.fail_on_error(error)
        dummy, error = self.rest_app.create_application(body)
        self.fail_on_error(error)

    def delete_san_application(self):
        '''Use REST application/applications san template to delete one or more LUNs'''
        dummy, error = self.rest_app.delete_application()
        self.fail_on_error(error)

    def create_lun(self):
        """
        Create LUN with requested name and size
        """
        path = '/vol/%s/%s' % (self.parameters['flexvol_name'],
                               self.parameters['name'])
        options = {
            'path': path,
            'size': str(self.parameters['size']),
            'space-reservation-enabled': str(self.parameters['space_reserve']),
            'space-allocation-enabled':
            str(self.parameters['space_allocation']),
            'use-exact-size': str(self.parameters['use_exact_size'])
        }
        if self.parameters.get('os_type') is not None:
            options['ostype'] = self.parameters['os_type']
        if self.parameters.get('qos_policy_group') is not None:
            options['qos-policy-group'] = self.parameters['qos_policy_group']
        lun_create = netapp_utils.zapi.NaElement.create_node_with_children(
            'lun-create-by-size', **options)

        try:
            self.server.invoke_successfully(lun_create, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            self.module.fail_json(
                msg="Error provisioning lun %s of size %s: %s" %
                (self.parameters['name'], self.parameters['size'],
                 to_native(exc)),
                exception=traceback.format_exc())

    def delete_lun(self, path):
        """
        Delete requested LUN
        """
        lun_delete = netapp_utils.zapi.NaElement.create_node_with_children(
            'lun-destroy', **{
                'path': path,
                'force': str(self.parameters['force_remove']),
                'destroy-fenced-lun':
                str(self.parameters['force_remove_fenced'])
            })

        try:
            self.server.invoke_successfully(lun_delete, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            self.module.fail_json(msg="Error deleting lun %s: %s" %
                                  (path, to_native(exc)),
                                  exception=traceback.format_exc())

    def resize_lun(self, path):
        """
        Resize requested LUN.

        :return: True if LUN was actually re-sized, false otherwise.
        :rtype: bool
        """
        lun_resize = netapp_utils.zapi.NaElement.create_node_with_children(
            'lun-resize', **{
                'path': path,
                'size': str(self.parameters['size']),
                'force': str(self.parameters['force_resize'])
            })
        try:
            self.server.invoke_successfully(lun_resize, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            if to_native(exc.code) == "9042":
                # Error 9042 denotes the new LUN size being the same as the
                # old LUN size. This happens when there's barely any difference
                # in the two sizes. For example, from 8388608 bytes to
                # 8194304 bytes. This should go away if/when the default size
                # requested/reported to/from the controller is changed to a
                # larger unit (MB/GB/TB).
                return False
            else:
                self.module.fail_json(msg="Error resizing lun %s: %s" %
                                      (path, to_native(exc)),
                                      exception=traceback.format_exc())

        return True

    def set_lun_value(self, path, key, value):
        key_to_zapi = dict(qos_policy_group=('lun-set-qos-policy-group',
                                             'qos-policy-group'),
                           space_allocation=('lun-set-space-alloc', 'enable'),
                           space_reserve=('lun-set-space-reservation-info',
                                          'enable'))
        if key in key_to_zapi:
            zapi, option = key_to_zapi[key]
        else:
            self.module.fail_json(msg="option %s cannot be modified to %s" %
                                  (key, value))
        options = dict(path=path)
        if option == 'enable':
            options[option] = self.na_helper.get_value_for_bool(False, value)
        else:
            options[option] = value

        lun_set = netapp_utils.zapi.NaElement.create_node_with_children(
            zapi, **options)
        try:
            self.server.invoke_successfully(lun_set, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            self.module.fail_json(msg="Error setting lun option %s: %s" %
                                  (key, to_native(exc)),
                                  exception=traceback.format_exc())
        return

    def modify_lun(self, path, modify):
        """
        update LUN properties (except size or name)
        """
        for key, value in modify.items():
            self.set_lun_value(path, key, value)

    def rename_lun(self, path, new_path):
        """
        rename LUN
        """
        lun_move = netapp_utils.zapi.NaElement.create_node_with_children(
            'lun-move', **{
                'path': path,
                'new-path': new_path
            })
        try:
            self.server.invoke_successfully(lun_move, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            self.module.fail_json(msg="Error moving lun %s: %s" %
                                  (path, to_native(exc)),
                                  exception=traceback.format_exc())

    def fail_on_error(self, error, stack=False):
        if error is None:
            return
        elements = dict(msg="Error: %s" % error)
        if stack:
            elements['stack'] = traceback.format_stack()
        self.module.fail_json(**elements)

    def apply(self):
        results = dict()
        warnings = list()
        netapp_utils.ems_log_event("na_ontap_lun", self.server)
        app_cd_action = None
        if self.rest_app:
            app_current, error = self.rest_app.get_application_uuid()
            self.fail_on_error(error)
            app_cd_action = self.na_helper.get_cd_action(
                app_current, self.parameters)
            if app_cd_action == 'create' and self.parameters.get(
                    'size') is None:
                self.module.fail_json(
                    msg="size is a required parameter for create.")

        # For LUNs created using a SAN application, we're getting lun paths from the backing storage
        lun_path, from_lun_path = None, None
        from_name = self.parameters.get('from_name')
        if self.rest_app and app_cd_action is None and app_current:
            lun_path = self.get_lun_path_from_backend(self.parameters['name'])
            if from_name is not None:
                from_lun_path = self.get_lun_path_from_backend(from_name)

        if app_cd_action is None:
            # actions at LUN level
            current = self.get_lun(self.parameters['name'], lun_path)
            if current is not None and lun_path is None:
                lun_path = current['path']
            cd_action = self.na_helper.get_cd_action(current, self.parameters)
            modify, rename = None, None
            if cd_action == 'create' and from_name is not None:
                # create by renaming existing LUN, if it really exists
                old_lun = self.get_lun(from_name, from_lun_path)
                rename = self.na_helper.is_rename_action(old_lun, current)
                if rename is None:
                    self.module.fail_json(
                        msg="Error renaming lun: %s does not exist" %
                        from_name)
                if rename:
                    current = old_lun
                    if from_lun_path is None:
                        from_lun_path = current['path']
                    head, _sep, tail = from_lun_path.rpartition(from_name)
                    if tail:
                        self.module.fail_json(
                            msg=
                            "Error renaming lun: %s does not match lun_path %s"
                            % (from_name, from_lun_path))
                    lun_path = head + self.parameters['name']
                    results['renamed'] = True
                    cd_action = None
            if cd_action == 'create' and self.parameters.get('size') is None:
                self.module.fail_json(
                    msg="size is a required parameter for create.")
            if cd_action is None and self.parameters['state'] == 'present':
                # we already handled rename if required
                current.pop('name', None)
                modify = self.na_helper.get_modified_attributes(
                    current, self.parameters)
                results['modify'] = dict(modify)
            if cd_action and self.rest_app and app_cd_action is None and app_current:
                msg = 'This module does not support %s a LUN by name %s a SAN application.' %\
                      ('adding', 'to') if cd_action == 'create' else ('removing', 'from')
                warnings.append(msg)
                cd_action = None
                self.na_helper.changed = False

        if self.na_helper.changed and not self.module.check_mode:
            if app_cd_action == 'create':
                self.create_san_application()
            elif app_cd_action == 'delete':
                self.rest_app.delete_application()
            elif cd_action == 'create':
                self.create_lun()
            elif cd_action == 'delete':
                self.delete_lun(lun_path)
            else:
                if rename:
                    self.rename_lun(from_lun_path, lun_path)
                size_changed = False
                if modify and 'size' in modify:
                    # Ensure that size was actually changed. Please
                    # read notes in 'resize_lun' function for details.
                    size_changed = self.resize_lun(lun_path)
                    modify.pop('size')
                if modify:
                    self.modify_lun(lun_path, modify)
                if not modify and not rename:
                    # size may not have changed
                    self.na_helper.changed = size_changed

        results['changed'] = self.na_helper.changed
        self.module.exit_json(**results)
class NetAppOntapLUN(object):
    ''' create, modify, delete LUN '''
    def __init__(self):

        self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
        self.argument_spec.update(dict(
            state=dict(required=False, type='str', choices=['present', 'absent'], default='present'),
            name=dict(required=True, type='str'),
            from_name=dict(required=False, type='str'),
            size=dict(type='int'),
            size_unit=dict(default='gb',
                           choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb',
                                    'pb', 'eb', 'zb', 'yb'], type='str'),
            comment=dict(required=False, type='str'),
            force_resize=dict(default=False, type='bool'),
            force_remove=dict(default=False, type='bool'),
            force_remove_fenced=dict(default=False, type='bool'),
            flexvol_name=dict(type='str'),
            vserver=dict(required=True, type='str'),
            os_type=dict(required=False, type='str', aliases=['ostype']),
            qos_policy_group=dict(required=False, type='str'),
            qos_adaptive_policy_group=dict(required=False, type='str'),
            space_reserve=dict(required=False, type='bool', default=True),
            space_allocation=dict(required=False, type='bool', default=False),
            use_exact_size=dict(required=False, type='bool', default=True),
            san_application_template=dict(type='dict', options=dict(
                use_san_application=dict(type='bool', default=True),
                name=dict(required=True, type='str'),
                igroup_name=dict(type='str'),
                lun_count=dict(type='int'),
                protection_type=dict(type='dict', options=dict(
                    local_policy=dict(type='str'),
                )),
                storage_service=dict(type='str', choices=['value', 'performance', 'extreme']),
                tiering=dict(type='dict', options=dict(
                    control=dict(type='str', choices=['required', 'best_effort', 'disallowed']),
                    policy=dict(type='str', choices=['all', 'auto', 'none', 'snapshot-only']),
                    object_stores=dict(type='list', elements='str')     # create only
                )),
                total_size=dict(type='int'),
                total_size_unit=dict(choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb',
                                              'pb', 'eb', 'zb', 'yb'], type='str'),
                scope=dict(type='str', choices=['application', 'auto', 'lun'], default='auto'),
            ))
        ))

        self.module = AnsibleModule(
            argument_spec=self.argument_spec,
            supports_check_mode=True,
            mutually_exclusive=[('qos_policy_group', 'qos_adaptive_policy_group')]
        )

        # set up state variables
        self.na_helper = NetAppModule()
        self.parameters = self.na_helper.set_parameters(self.module.params)

        if self.parameters.get('size') is not None:
            self.parameters['size'] *= netapp_utils.POW2_BYTE_MAP[self.parameters['size_unit']]
        if self.na_helper.safe_get(self.parameters, ['san_application_template', 'total_size']) is not None:
            unit = self.na_helper.safe_get(self.parameters, ['san_application_template', 'total_size_unit'])
            if unit is None:
                unit = self.parameters['size_unit']
            self.parameters['san_application_template']['total_size'] *= netapp_utils.POW2_BYTE_MAP[unit]

        self.warnings = list()
        self.debug = dict()
        # self.debug['got'] = 'empty'     # uncomment to enable collecting data

        if HAS_NETAPP_LIB is False:
            self.module.fail_json(msg="the python NetApp-Lib module is required")
        else:
            self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver'])

        # REST API for application/applications if needed
        self.rest_api, self.rest_app = self.setup_rest_application()

    def setup_rest_application(self):
        use_application_template = self.na_helper.safe_get(self.parameters, ['san_application_template', 'use_san_application'])
        rest_api, rest_app = None, None
        if use_application_template:
            if self.parameters.get('flexvol_name') is not None:
                self.module.fail_json(msg="'flexvol_name' option is not supported when san_application_template is present")
            rest_api = netapp_utils.OntapRestAPI(self.module)
            name = self.na_helper.safe_get(self.parameters, ['san_application_template', 'name'], allow_sparse_dict=False)
            rest_app = RestApplication(rest_api, self.parameters['vserver'], name)
        elif self.parameters.get('flexvol_name') is None:
            self.module.fail_json(msg="flexvol_name option is required when san_application_template is not present")
        return rest_api, rest_app

    def get_luns(self, lun_path=None):
        """
        Return list of LUNs matching vserver and volume names.

        :return: list of LUNs in XML format.
        :rtype: list
        """
        luns = []
        tag = None
        if lun_path is None and self.parameters.get('flexvol_name') is None:
            return luns

        query_details = netapp_utils.zapi.NaElement('lun-info')
        query_details.add_new_child('vserver', self.parameters['vserver'])
        if lun_path is not None:
            query_details.add_new_child('lun_path', lun_path)
        else:
            query_details.add_new_child('volume', self.parameters['flexvol_name'])
        query = netapp_utils.zapi.NaElement('query')
        query.add_child_elem(query_details)

        while True:
            lun_info = netapp_utils.zapi.NaElement('lun-get-iter')
            lun_info.add_child_elem(query)
            if tag:
                lun_info.add_new_child('tag', tag, True)

            result = self.server.invoke_successfully(lun_info, True)
            if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1:
                attr_list = result.get_child_by_name('attributes-list')
                luns.extend(attr_list.get_children())
            tag = result.get_child_content('next-tag')
            if tag is None:
                break
        return luns

    def get_lun_details(self, lun):
        """
        Extract LUN details, from XML to python dict

        :return: Details about the lun
        :rtype: dict
        """
        return_value = dict()
        return_value['size'] = int(lun.get_child_content('size'))
        bool_attr_map = {
            'is-space-alloc-enabled': 'space_allocation',
            'is-space-reservation-enabled': 'space_reserve'
        }
        for attr in bool_attr_map:
            value = lun.get_child_content(attr)
            if value is not None:
                return_value[bool_attr_map[attr]] = self.na_helper.get_value_for_bool(True, value)
        str_attr_map = {
            'comment': 'comment',
            'multiprotocol-type': 'os_type',
            'name': 'name',
            'path': 'path',
            'qos-policy-group': 'qos_policy_group',
            'qos-adaptive-policy-group': 'qos_adaptive_policy_group',
        }
        for attr in str_attr_map:
            value = lun.get_child_content(attr)
            if value is None and attr in ('comment', 'qos-policy-group', 'qos-adaptive-policy-group'):
                value = ''
            if value is not None:
                return_value[str_attr_map[attr]] = value

        # Find out if the lun is attached
        attached_to = None
        lun_id = None
        if lun.get_child_content('mapped') == 'true':
            lun_map_list = netapp_utils.zapi.NaElement.create_node_with_children(
                'lun-map-list-info', **{'path': lun.get_child_content('path')})
            result = self.server.invoke_successfully(
                lun_map_list, enable_tunneling=True)
            igroups = result.get_child_by_name('initiator-groups')
            if igroups:
                for igroup_info in igroups.get_children():
                    igroup = igroup_info.get_child_content(
                        'initiator-group-name')
                    attached_to = igroup
                    lun_id = igroup_info.get_child_content('lun-id')

        return_value.update({
            'attached_to': attached_to,
            'lun_id': lun_id
        })
        return return_value

    def find_lun(self, luns, name, lun_path=None):
        """
        Return lun record matching name or path

        :return: lun record
        :rtype: XML or None if not found
        """
        for lun in luns:
            path = lun.get_child_content('path')
            if lun_path is not None:
                if lun_path == path:
                    return lun
            else:
                if name == path:
                    return lun
                _rest, _splitter, found_name = path.rpartition('/')
                if found_name == name:
                    return lun
        return None

    def get_lun(self, name, lun_path=None):
        """
        Return details about the LUN

        :return: Details about the lun
        :rtype: dict
        """
        luns = self.get_luns(lun_path)
        lun = self.find_lun(luns, name, lun_path)
        if lun is not None:
            return self.get_lun_details(lun)
        return None

    def get_luns_from_app(self):
        app_details, error = self.rest_app.get_application_details()
        self.fail_on_error(error)
        if app_details is not None:
            app_details['paths'] = self.get_lun_paths_from_app()
        return app_details

    def get_lun_paths_from_app(self):
        """Get luns path for SAN application"""
        backing_storage, error = self.rest_app.get_application_component_backing_storage()
        self.fail_on_error(error)
        # {'luns': [{'path': '/vol/ansibleLUN/ansibleLUN_1', ...
        if backing_storage is not None:
            return [lun['path'] for lun in backing_storage.get('luns', [])]
        return None

    def get_lun_path_from_backend(self, name):
        """returns lun path matching name if found in backing_storage
           retruns None if not found
        """
        lun_paths = self.get_lun_paths_from_app()
        match = "/%s" % name
        for path in lun_paths:
            if path.endswith(match):
                return path
        return None

    def create_san_app_component(self, modify):
        '''Create SAN application component'''
        if modify:
            required_options = ['name']
            action = 'modify'
            if 'lun_count' in modify:
                required_options.append('total_size')
        else:
            required_options = ('name', 'total_size')
            action = 'create'
        for option in required_options:
            if self.parameters.get(option) is None:
                self.module.fail_json(msg="Error: '%s' is required to %s a san application." % (option, action))

        application_component = dict(name=self.parameters['name'])
        if not modify:
            application_component['lun_count'] = 1      # default value for create, may be overriden below

        for attr in ('igroup_name', 'lun_count', 'storage_service'):
            if not modify or attr in modify:
                value = self.na_helper.safe_get(self.parameters, ['san_application_template', attr])
                if value is not None:
                    application_component[attr] = value
        for attr in ('os_type', 'qos_policy_group', 'qos_adaptive_policy_group', 'total_size'):
            if not modify or attr in modify:
                value = self.na_helper.safe_get(self.parameters, [attr])
                if value is not None:
                    # only one of them can be present at most
                    if attr in ('qos_policy_group', 'qos_adaptive_policy_group'):
                        attr = 'qos'
                        value = dict(policy=dict(name=value))
                    application_component[attr] = value
        tiering = self.na_helper.safe_get(self.parameters, ['nas_application_template', 'tiering'])
        if tiering is not None and not modify:
            application_component['tiering'] = dict()
            for attr in ('control', 'policy', 'object_stores'):
                value = tiering.get(attr)
                if attr == 'object_stores' and value is not None:
                    value = [dict(name=x) for x in value]
                if value is not None:
                    application_component['tiering'][attr] = value
        return application_component

    def create_san_app_body(self, modify=None):
        '''Create body for san template'''
        # TODO:
        # Should we support new_igroups?
        # It may raise idempotency issues if the REST call fails if the igroup already exists.
        # And we already have na_ontap_igroups.
        san = {
            'application_components': [self.create_san_app_component(modify)],
        }
        for attr in ('protection_type',):
            if not modify or attr in modify:
                value = self.na_helper.safe_get(self.parameters, ['san_application_template', attr])
                if value is not None:
                    # we expect value to be a dict, but maybe an empty dict
                    value = self.na_helper.filter_out_none_entries(value)
                    if value:
                        san[attr] = value
        for attr in ('os_type',):
            if not modify:      # not supported for modify operation, but required at applicaiton component level
                value = self.na_helper.safe_get(self.parameters, [attr])
                if value is not None:
                    san[attr] = value
        body, error = self.rest_app.create_application_body('san', san)
        return body, error

    def create_san_application(self):
        '''Use REST application/applications san template to create one or more LUNs'''
        body, error = self.create_san_app_body()
        self.fail_on_error(error)
        dummy, error = self.rest_app.create_application(body)
        self.fail_on_error(error)

    def modify_san_application(self, modify):
        '''Use REST application/applications san template to add one or more LUNs'''
        body, error = self.create_san_app_body(modify)
        self.fail_on_error(error)
        # these cannot be present when using PATCH
        body.pop('name')
        body.pop('svm')
        body.pop('smart_container')
        dummy, error = self.rest_app.patch_application(body)
        self.fail_on_error(error)

    def convert_to_san_application(self, scope):
        '''First convert volume to smart container using POST
           Second modify app to add new luns using PATCH
        '''
        # dummy modify, so that we don't fill in the body
        modify = dict(dummy='dummy')
        body, error = self.create_san_app_body(modify)
        self.fail_on_error(error)
        dummy, error = self.rest_app.create_application(body)
        self.fail_on_error(error)
        app_current, error = self.rest_app.get_application_uuid()
        self.fail_on_error(error)
        if app_current is None:
            self.module.fail_json('Error: failed to create smart container for %s' % self.parameters['name'])
        app_modify, app_modify_warning = self.app_changes(scope)
        if app_modify_warning is not None:
            self.warnings.append(app_modify_warning)
        if app_modify:
            self.modify_san_application(app_modify)

    def delete_san_application(self):
        '''Use REST application/applications san template to delete one or more LUNs'''
        dummy, error = self.rest_app.delete_application()
        self.fail_on_error(error)

    def create_lun(self):
        """
        Create LUN with requested name and size
        """
        path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name'])
        options = {'path': path,
                   'size': str(self.parameters['size']),
                   'space-reservation-enabled': str(self.parameters['space_reserve']),
                   'space-allocation-enabled': str(self.parameters['space_allocation']),
                   'use-exact-size': str(self.parameters['use_exact_size'])}
        if self.parameters.get('comment') is not None:
            options['comment'] = self.parameters['comment']
        if self.parameters.get('os_type') is not None:
            options['ostype'] = self.parameters['os_type']
        if self.parameters.get('qos_policy_group') is not None:
            options['qos-policy-group'] = self.parameters['qos_policy_group']
        if self.parameters.get('qos_adaptive_policy_group') is not None:
            options['qos-adaptive-policy-group'] = self.parameters['qos_adaptive_policy_group']
        lun_create = netapp_utils.zapi.NaElement.create_node_with_children(
            'lun-create-by-size', **options)

        try:
            self.server.invoke_successfully(lun_create, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            self.module.fail_json(msg="Error provisioning lun %s of size %s: %s"
                                  % (self.parameters['name'], self.parameters['size'], to_native(exc)),
                                  exception=traceback.format_exc())

    def delete_lun(self, path):
        """
        Delete requested LUN
        """
        lun_delete = netapp_utils.zapi.NaElement.create_node_with_children(
            'lun-destroy', **{'path': path,
                              'force': str(self.parameters['force_remove']),
                              'destroy-fenced-lun':
                                  str(self.parameters['force_remove_fenced'])})

        try:
            self.server.invoke_successfully(lun_delete, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            self.module.fail_json(msg="Error deleting lun %s: %s" % (path, to_native(exc)),
                                  exception=traceback.format_exc())

    def resize_lun(self, path):
        """
        Resize requested LUN.

        :return: True if LUN was actually re-sized, false otherwise.
        :rtype: bool
        """
        lun_resize = netapp_utils.zapi.NaElement.create_node_with_children(
            'lun-resize', **{'path': path,
                             'size': str(self.parameters['size']),
                             'force': str(self.parameters['force_resize'])})
        try:
            self.server.invoke_successfully(lun_resize, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            if to_native(exc.code) == "9042":
                # Error 9042 denotes the new LUN size being the same as the
                # old LUN size. This happens when there's barely any difference
                # in the two sizes. For example, from 8388608 bytes to
                # 8194304 bytes. This should go away if/when the default size
                # requested/reported to/from the controller is changed to a
                # larger unit (MB/GB/TB).
                return False
            else:
                self.module.fail_json(msg="Error resizing lun %s: %s" % (path, to_native(exc)),
                                      exception=traceback.format_exc())

        return True

    def set_lun_value(self, path, key, value):
        key_to_zapi = dict(
            comment=('lun-set-comment', 'comment'),
            # The same ZAPI is used for both QOS attributes
            qos_policy_group=('lun-set-qos-policy-group', 'qos-policy-group'),
            qos_adaptive_policy_group=('lun-set-qos-policy-group', 'qos-adaptive-policy-group'),
            space_allocation=('lun-set-space-alloc', 'enable'),
            space_reserve=('lun-set-space-reservation-info', 'enable')
        )
        if key in key_to_zapi:
            zapi, option = key_to_zapi[key]
        else:
            self.module.fail_json(msg="option %s cannot be modified to %s" % (key, value))
        options = dict(path=path)
        if option == 'enable':
            options[option] = self.na_helper.get_value_for_bool(False, value)
        else:
            options[option] = value

        lun_set = netapp_utils.zapi.NaElement.create_node_with_children(zapi, **options)
        try:
            self.server.invoke_successfully(lun_set, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            self.module.fail_json(msg="Error setting lun option %s: %s" % (key, to_native(exc)),
                                  exception=traceback.format_exc())
        return

    def modify_lun(self, path, modify):
        """
        update LUN properties (except size or name)
        """
        for key, value in modify.items():
            self.set_lun_value(path, key, value)

    def rename_lun(self, path, new_path):
        """
        rename LUN
        """
        lun_move = netapp_utils.zapi.NaElement.create_node_with_children(
            'lun-move', **{'path': path,
                           'new-path': new_path})
        try:
            self.server.invoke_successfully(lun_move, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as exc:
            self.module.fail_json(msg="Error moving lun %s: %s" % (path, to_native(exc)),
                                  exception=traceback.format_exc())

    def fail_on_error(self, error, stack=False):
        if error is None:
            return
        elements = dict(msg="Error: %s" % error)
        if stack:
            elements['stack'] = traceback.format_stack()
        self.module.fail_json(**elements)

    def set_total_size(self, validate):
        # fix total_size attribute, report error if total_size is missing (or size is missing)
        attr = 'total_size'
        value = self.na_helper.safe_get(self.parameters, ['san_application_template', attr])
        if value is not None or not validate:
            self.parameters[attr] = value
            return
        lun_count = self.na_helper.safe_get(self.parameters, ['san_application_template', 'lun_count'])
        value = self.parameters.get('size')
        if value is not None and (lun_count is None or lun_count == 1):
            self.parameters[attr] = value
            return
        self.module.fail_json("Error: 'total_size' is a required SAN application template attribute when creating a LUN application")

    def validate_app_create(self):
        # fix total_size attribute
        self.set_total_size(validate=True)

    def validate_app_changes(self, modify, warning):
        errors = list()
        for key in modify:
            if key not in ('lun_count', 'total_size'):
                errors.append("Error: the following application parameter cannot be modified: %s: %s."
                              % (key, modify[key]))
        if 'lun_count' in modify:
            for attr in ('total_size', 'os_type', 'igroup_name'):
                value = self.parameters.get(attr)
                if value is None:
                    value = self.na_helper.safe_get(self.parameters['san_application_template'], [attr])
                if value is None:
                    errors.append('Error: %s is a required parameter when increasing lun_count.' % attr)
                else:
                    modify[attr] = value
            if warning:
                errors.append('Error: %s' % warning)
        if errors:
            self.module.fail_json(msg='\n'.join(errors))
        if 'total_size' in modify:
            self.set_total_size(validate=False)
            if warning:
                # can't change total_size, let's ignore it
                self.warnings.append(warning)
                modify.pop('total_size')

    def app_changes(self, scope):
        # find and validate app changes
        app_current, error = self.rest_app.get_application_details('san')
        self.fail_on_error(error)
        # save application name, as it is overriden in the flattening operation
        app_name = app_current['name']
        # there is an issue with total_size not reflecting the real total_size, and some additional overhead
        provisioned_size = self.na_helper.safe_get(app_current, ['statistics', 'space', 'provisioned'])
        if provisioned_size is None:
            provisioned_size = 0
        if self.debug:
            self.debug['app_current'] = app_current             # will be updated below as it is mutable
            self.debug['got'] = copy.deepcopy(app_current)      # fixed copy
        # flatten
        app_current = app_current['san']                                # app template
        app_current.update(app_current['application_components'][0])    # app component
        del app_current['application_components']
        # if component name does not match, assume a change at LUN level
        comp_name = app_current['name']
        if comp_name != self.parameters['name']:
            msg = "desired component/volume name: %s does not match existing component name: %s" % (self.parameters['name'], comp_name)
            if scope == 'application':
                self.module.fail_json(msg='Error: ' + msg + ".  scope=%s" % scope)
            return None, msg + ".  scope=%s, assuming 'lun' scope." % scope
        # restore app name
        app_current['name'] = app_name

        # ready to compare, except for a quirk in size handling
        total_size = app_current['total_size']
        desired = dict(self.parameters['san_application_template'])
        desired_size = desired.get('total_size')

        warning = None
        if desired_size is not None:
            if desired_size < total_size:
                self.module.fail_json("Error: can't reduce size: total_size=%d, provisioned=%d, requested=%d"
                                      % (total_size, provisioned_size, desired_size))
            elif desired_size > total_size and desired_size < provisioned_size:
                # we can't increase, but we can't say it is a problem, as the size is already bigger!
                warning = "requested size is too small: total_size=%d, provisioned=%d, requested=%d" % (total_size, provisioned_size, desired_size)

        # preserve change state before calling modify in case an ignorable total_size change is the only change
        changed = self.na_helper.changed
        app_modify = self.na_helper.get_modified_attributes(app_current, desired)
        self.validate_app_changes(app_modify, warning)
        if not app_modify:
            self.na_helper.changed = changed
            app_modify = None
        return app_modify, None

    def apply(self):
        results = dict()
        netapp_utils.ems_log_event("na_ontap_lun", self.server)
        app_cd_action, app_modify, lun_cd_action, lun_modify, lun_rename = None, None, None, None, None
        app_modify_warning = None
        actions = list()
        if self.rest_app:
            scope = self.na_helper.safe_get(self.parameters, ['san_application_template', 'scope'])
            app_current, error = self.rest_app.get_application_uuid()
            self.fail_on_error(error)
            if scope == 'lun' and app_current is None:
                self.module.fail_json('Application not found: %s.  scope=%s.' %
                                      (self.na_helper.safe_get(self.parameters, ['san_application_template', 'name']), scope))
        else:
            # no application template, fall back to LUN only
            scope = 'lun'

        if self.rest_app and scope != 'lun':
            app_cd_action = self.na_helper.get_cd_action(app_current, self.parameters)
            if app_cd_action == 'create':
                # check if target volume already exists
                cp_volume_name = self.parameters['name']
                volume, error = rest_volume.get_volume(self.rest_api, self.parameters['vserver'], cp_volume_name)
                self.fail_on_error(error)
                if volume is not None:
                    if scope == 'application':
                        # volume already exists, but not as part of this application
                        app_cd_action = 'convert'
                    else:
                        # default name already in use, ask user to clarify intent
                        msg = "Error: volume '%s' already exists.  Please use a different group name, or use 'application' scope.  scope=%s"
                        self.module.fail_json(msg=msg % (cp_volume_name, scope))
            if app_cd_action is not None:
                actions.append('app_%s' % app_cd_action)
            if app_cd_action == 'create':
                self.validate_app_create()
            if app_cd_action is None and app_current is not None:
                app_modify, app_modify_warning = self.app_changes(scope)
                if app_modify:
                    actions.append('app_modify')
                    results['app_modify'] = dict(app_modify)

        if app_cd_action is None and scope != 'application':
            # actions at LUN level
            lun_path, from_lun_path = None, None
            from_name = self.parameters.get('from_name')
            if self.rest_app and app_current:
                # For LUNs created using a SAN application, we're getting lun paths from the backing storage
                lun_path = self.get_lun_path_from_backend(self.parameters['name'])
                if from_name is not None:
                    from_lun_path = self.get_lun_path_from_backend(from_name)
            current = self.get_lun(self.parameters['name'], lun_path)
            if current is not None and lun_path is None:
                lun_path = current['path']
            lun_cd_action = self.na_helper.get_cd_action(current, self.parameters)
            if lun_cd_action == 'create' and from_name is not None:
                # create by renaming existing LUN, if it really exists
                old_lun = self.get_lun(from_name, from_lun_path)
                lun_rename = self.na_helper.is_rename_action(old_lun, current)
                if lun_rename is None:
                    self.module.fail_json(msg="Error renaming lun: %s does not exist" % from_name)
                if lun_rename:
                    current = old_lun
                    if from_lun_path is None:
                        from_lun_path = current['path']
                    head, _sep, tail = from_lun_path.rpartition(from_name)
                    if tail:
                        self.module.fail_json(msg="Error renaming lun: %s does not match lun_path %s" % (from_name, from_lun_path))
                    lun_path = head + self.parameters['name']
                    lun_cd_action = None
                    actions.append('lun_rename')
                    app_modify_warning = None       # reset warning as we found a match
            if lun_cd_action is not None:
                actions.append('lun_%s' % lun_cd_action)
            if lun_cd_action is None and self.parameters['state'] == 'present':
                # we already handled rename if required
                current.pop('name', None)
                lun_modify = self.na_helper.get_modified_attributes(current, self.parameters)
                if lun_modify:
                    actions.append('lun_modify')
                    results['lun_modify'] = dict(lun_modify)
                    app_modify_warning = None       # reset warning as we found a match
            if lun_cd_action and self.rest_app and app_current:
                msg = 'This module does not support %s a LUN by name %s a SAN application.' %\
                    ('adding', 'to') if lun_cd_action == 'create' else ('removing', 'from')
                if scope == 'auto':
                    # ignore LUN not found, as name can be a group name
                    self.warnings.append(msg + ".  scope=%s, assuming 'application'" % scope)
                    if not app_modify:
                        self.na_helper.changed = False
                elif scope == 'lun':
                    self.module.fail_json(msg=msg + ".  scope=%s." % scope)
                lun_cd_action = None
            if lun_cd_action == 'create' and self.parameters.get('size') is None:
                self.module.fail_json(msg="size is a required parameter for create.")

        if self.na_helper.changed and not self.module.check_mode:
            if app_cd_action == 'create':
                self.create_san_application()
            elif app_cd_action == 'convert':
                self.convert_to_san_application(scope)
            elif app_cd_action == 'delete':
                self.rest_app.delete_application()
            elif lun_cd_action == 'create':
                self.create_lun()
            elif lun_cd_action == 'delete':
                self.delete_lun(lun_path)
            else:
                if app_modify:
                    self.modify_san_application(app_modify)
                if lun_rename:
                    self.rename_lun(from_lun_path, lun_path)
                size_changed = False
                if lun_modify and 'size' in lun_modify:
                    # Ensure that size was actually changed. Please
                    # read notes in 'resize_lun' function for details.
                    size_changed = self.resize_lun(lun_path)
                    lun_modify.pop('size')
                if lun_modify:
                    self.modify_lun(lun_path, lun_modify)
                if not lun_modify and not lun_rename and not app_modify:
                    # size may not have changed
                    self.na_helper.changed = size_changed

        if app_modify_warning:
            self.warnings.append(app_modify_warning)
        results['changed'] = self.na_helper.changed
        results['actions'] = actions
        if self.warnings:
            results['warnings'] = self.warnings
        results.update(self.debug)
        self.module.exit_json(**results)
예제 #3
0
class NetAppONTAPFlexCache(object):
    """
    Class with FlexCache methods
    """
    def __init__(self):

        self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
        self.argument_spec.update(
            dict(
                state=dict(required=False,
                           type='str',
                           choices=['present', 'absent'],
                           default='present'),
                origin_volume=dict(required=False, type='str'),  # origins[0]
                origin_vserver=dict(required=False, type='str'),  # origins[0]
                origin_cluster=dict(required=False, type='str'),  # origins[0]
                auto_provision_as=dict(required=False,
                                       type='str'),  # ignored with REST
                name=dict(required=True, type='str', aliases=['volume']),
                junction_path=dict(required=False,
                                   type='str',
                                   aliases=['path']),
                size=dict(required=False, type='int'),
                size_unit=dict(default='gb',
                               choices=[
                                   'bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb',
                                   'eb', 'zb', 'yb'
                               ],
                               type='str'),
                vserver=dict(required=True, type='str'),
                aggr_list=dict(required=False,
                               type='list',
                               elements='str',
                               aliases=['aggregates']),
                aggr_list_multiplier=dict(
                    required=False,
                    type='int',
                    aliases=['constituents_per_aggregate']),
                force_offline=dict(required=False, type='bool', default=False),
                force_unmount=dict(required=False, type='bool', default=False),
                time_out=dict(required=False, type='int', default=180),
                prepopulate=dict(required=False,
                                 type='dict',
                                 options=dict(
                                     dir_paths=dict(required=True,
                                                    type='list',
                                                    elements='str'),
                                     exclude_dir_paths=dict(required=False,
                                                            type='list',
                                                            elements='str'),
                                     recurse=dict(required=False, type='bool'),
                                     force_prepopulate_if_already_created=dict(
                                         required=False,
                                         type='bool',
                                         default=True),
                                 ))))

        self.module = AnsibleModule(argument_spec=self.argument_spec,
                                    mutually_exclusive=[
                                        ('aggr_list', 'auto_provision_as'),
                                    ],
                                    supports_check_mode=True)

        self.na_helper = NetAppModule(self.module)
        self.parameters = self.na_helper.set_parameters(self.module.params)
        if self.parameters.get('size'):
            self.parameters['size'] = self.parameters['size'] * \
                netapp_utils.POW2_BYTE_MAP[self.parameters['size_unit']]
        # setup later if required
        self.origin_server = None

        self.rest_api = netapp_utils.OntapRestAPI(self.module)
        self.use_rest = self.rest_api.is_rest()

        ontap_98_options = ['prepopulate']
        if not self.rest_api.meets_rest_minimum_version(
                self.use_rest, 9, 8) and any(x in self.parameters
                                             for x in ontap_98_options):
            self.module.fail_json(msg='Error: %s' %
                                  self.rest_api.options_require_ontap_version(
                                      ontap_98_options, version='9.8'))

        if 'prepopulate' in self.parameters:
            # sanitize the dictionary, as Ansible fills everything with None values
            self.parameters[
                'prepopulate'] = self.na_helper.filter_out_none_entries(
                    self.parameters['prepopulate'])
            ontap_99_options = ['exclude_dir_paths']
            if not self.rest_api.meets_rest_minimum_version(
                    self.use_rest, 9, 9) and any(
                        x in self.parameters['prepopulate']
                        for x in ontap_99_options):
                options = ['prepopulate: ' + x for x in ontap_99_options]
                self.module.fail_json(
                    msg='Error: %s' %
                    self.rest_api.options_require_ontap_version(options,
                                                                version='9.9'))
            if not self.parameters['prepopulate']:
                # remove entry if the dict is empty
                del self.parameters['prepopulate']

        if not self.use_rest:
            if not netapp_utils.has_netapp_lib():
                self.module.fail_json(
                    msg=netapp_utils.netapp_lib_is_required())
            else:
                self.server = netapp_utils.setup_na_ontap_zapi(
                    module=self.module, vserver=self.parameters['vserver'])

    def add_parameter_to_dict(self, adict, name, key=None, tostr=False):
        ''' add defined parameter (not None) to adict using key '''
        if key is None:
            key = name
        if self.parameters.get(name) is not None:
            if tostr:
                adict[key] = str(self.parameters.get(name))
            else:
                adict[key] = self.parameters.get(name)

    def get_job(self, jobid, server):
        """
        Get job details by id
        """
        job_get = netapp_utils.zapi.NaElement('job-get')
        job_get.add_new_child('job-id', jobid)
        try:
            result = server.invoke_successfully(job_get, enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as error:
            if to_native(error.code) == "15661":
                # Not found
                return None
            self.module.fail_json(msg='Error fetching job info: %s' %
                                  to_native(error),
                                  exception=traceback.format_exc())
        results = dict()
        job_info = result.get_child_by_name('attributes').get_child_by_name(
            'job-info')
        results = {
            'job-progress': job_info['job-progress'],
            'job-state': job_info['job-state']
        }
        if job_info.get_child_by_name('job-completion') is not None:
            results['job-completion'] = job_info['job-completion']
        else:
            results['job-completion'] = None
        return results

    def check_job_status(self, jobid):
        """
        Loop until job is complete
        """
        server = self.server
        sleep_time = 5
        time_out = self.parameters['time_out']
        while time_out > 0:
            results = self.get_job(jobid, server)
            # If running as cluster admin, the job is owned by cluster vserver
            # rather than the target vserver.
            if results is None and server == self.server:
                results = netapp_utils.get_cserver(self.server)
                server = netapp_utils.setup_na_ontap_zapi(module=self.module,
                                                          vserver=results)
                continue
            if results is None:
                error = 'cannot locate job with id: %d' % jobid
                break
            if results['job-state'] in ('queued', 'running'):
                time.sleep(sleep_time)
                time_out -= sleep_time
                continue
            if results['job-state'] in ('success', 'failure'):
                break
            else:
                self.module.fail_json(msg='Unexpected job status in: %s' %
                                      repr(results))

        if results is not None:
            if results['job-state'] == 'success':
                error = None
            elif results['job-state'] in ('queued', 'running'):
                error = 'job completion exceeded expected timer of: %s seconds' % \
                        self.parameters['time_out']
            else:
                if results['job-completion'] is not None:
                    error = results['job-completion']
                else:
                    error = results['job-progress']
        return error

    def flexcache_get_iter(self):
        """
        Compose NaElement object to query current FlexCache relation
        """
        options = {'volume': self.parameters['name']}
        self.add_parameter_to_dict(options, 'origin_volume', 'origin-volume')
        self.add_parameter_to_dict(options, 'origin_vserver', 'origin-vserver')
        self.add_parameter_to_dict(options, 'origin_cluster', 'origin-cluster')
        flexcache_info = netapp_utils.zapi.NaElement.create_node_with_children(
            'flexcache-info', **options)
        query = netapp_utils.zapi.NaElement('query')
        query.add_child_elem(flexcache_info)
        flexcache_get_iter = netapp_utils.zapi.NaElement('flexcache-get-iter')
        flexcache_get_iter.add_child_elem(query)
        return flexcache_get_iter

    def flexcache_get(self):
        """
        Get current FlexCache relations
        :return: Dictionary of current FlexCache details if query successful, else None
        """
        fields = 'svm,name,uuid,path'
        if self.use_rest:
            flexcache, error = rest_flexcache.get_flexcache(
                self.rest_api,
                self.parameters['vserver'],
                self.parameters['name'],
                fields=fields)
            self.na_helper.fail_on_error(error)
            if flexcache is None:
                return None
            return dict(vserver=flexcache['svm']['name'],
                        name=flexcache['name'],
                        uuid=flexcache['uuid'],
                        junction_path=flexcache.get('path'))

        flexcache_get_iter = self.flexcache_get_iter()
        flex_info = dict()
        try:
            result = self.server.invoke_successfully(flexcache_get_iter,
                                                     enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg='Error fetching FlexCache info: %s' %
                                  to_native(error),
                                  exception=traceback.format_exc())
        if result.get_child_by_name('num-records') and \
                int(result.get_child_content('num-records')) == 1:
            flexcache_info = result.get_child_by_name('attributes-list') \
                                   .get_child_by_name('flexcache-info')
            flex_info['origin_cluster'] = flexcache_info.get_child_content(
                'origin-cluster')
            flex_info['origin_volume'] = flexcache_info.get_child_content(
                'origin-volume')
            flex_info['origin_vserver'] = flexcache_info.get_child_content(
                'origin-vserver')
            flex_info['size'] = flexcache_info.get_child_content('size')
            flex_info['name'] = flexcache_info.get_child_content('volume')
            flex_info['vserver'] = flexcache_info.get_child_content('vserver')

            return flex_info
        if result.get_child_by_name('num-records') and \
                int(result.get_child_content('num-records')) > 1:
            msg = 'Multiple records found for %s:' % self.parameters['name']
            self.module.fail_json(msg='Error fetching FlexCache info: %s' %
                                  msg)
        return None

    def flexcache_rest_create_body(self, mappings):
        ''' maps self.parameters to REST API body attributes, using mappings to identify fields to add '''
        body = dict()
        for key, value in mappings.items():
            if key in self.parameters:
                if key == 'aggr_list':
                    body[value] = [
                        dict(name=aggr) for aggr in self.parameters[key]
                    ]
                else:
                    body[value] = self.parameters[key]
            elif key == 'origins':
                # this is an artificial key, to match the REST list of dict structure
                origin = dict(
                    volume=dict(name=self.parameters['origin_volume']),
                    svm=dict(name=self.parameters['origin_vserver']))
                if 'origin_cluster' in self.parameters:
                    origin['cluster'] = dict(
                        name=self.parameters['origin_cluster'])
                body[value] = [origin]
        return body

    def flexcache_rest_create(self):
        ''' use POST to create a FlexCache '''
        mappings = dict(name='name',
                        vserver='svm.name',
                        junction_path='path',
                        size='size',
                        aggr_list='aggregates',
                        aggr_list_multiplier='constituents_per_aggregate',
                        origins='origins',
                        prepopulate='prepopulate')
        body = self.flexcache_rest_create_body(mappings)
        response, error = rest_flexcache.post_flexcache(
            self.rest_api, body, timeout=self.parameters['time_out'])
        self.na_helper.fail_on_error(error)
        return response

    def flexcache_rest_modify(self, uuid):
        ''' use PATCH to start prepopulating a FlexCache '''
        mappings = dict(  # name cannot be set, though swagger example shows it
            prepopulate='prepopulate')
        body = self.flexcache_rest_create_body(mappings)
        response, error = rest_flexcache.patch_flexcache(
            self.rest_api, uuid, body)
        self.na_helper.fail_on_error(error)
        return response

    def flexcache_create_async(self):
        """
        Create a FlexCache relationship
        """
        options = {
            'origin-volume': self.parameters['origin_volume'],
            'origin-vserver': self.parameters['origin_vserver'],
            'volume': self.parameters['name']
        }
        self.add_parameter_to_dict(options, 'junction_path', 'junction-path')
        self.add_parameter_to_dict(options, 'auto_provision_as',
                                   'auto-provision-as')
        self.add_parameter_to_dict(options, 'size', 'size', tostr=True)
        if self.parameters.get('aggr_list') and self.parameters.get(
                'aggr_list_multiplier'):
            self.add_parameter_to_dict(options,
                                       'aggr_list_multiplier',
                                       'aggr-list-multiplier',
                                       tostr=True)
        flexcache_create = netapp_utils.zapi.NaElement.create_node_with_children(
            'flexcache-create-async', **options)
        if self.parameters.get('aggr_list'):
            aggregates = netapp_utils.zapi.NaElement('aggr-list')
            for aggregate in self.parameters['aggr_list']:
                aggregates.add_new_child('aggr-name', aggregate)
            flexcache_create.add_child_elem(aggregates)
        try:
            result = self.server.invoke_successfully(flexcache_create,
                                                     enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg='Error creating FlexCache %s' %
                                  to_native(error),
                                  exception=traceback.format_exc())
        results = dict()
        for key in ('result-status', 'result-jobid'):
            if result.get_child_by_name(key):
                results[key] = result[key]
        return results

    def flexcache_create(self):
        """
        Create a FlexCache relationship
        Check job status
        """
        if self.use_rest:
            return self.flexcache_rest_create()

        results = self.flexcache_create_async()
        status = results.get('result-status')
        if status == 'in_progress' and 'result-jobid' in results:
            if self.parameters['time_out'] == 0:
                # asynchronous call, assuming success!
                return
            error = self.check_job_status(results['result-jobid'])
            if error is None:
                return
            else:
                self.module.fail_json(msg='Error when creating flexcache: %s' %
                                      error)
        self.module.fail_json(
            msg='Unexpected error when creating flexcache: results is: %s' %
            repr(results))

    def flexcache_delete_async(self):
        """
        Delete FlexCache relationship at destination cluster
        """
        options = {'volume': self.parameters['name']}
        flexcache_delete = netapp_utils.zapi.NaElement.create_node_with_children(
            'flexcache-destroy-async', **options)
        try:
            result = self.server.invoke_successfully(flexcache_delete,
                                                     enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg='Error deleting FlexCache : %s' %
                                  (to_native(error)),
                                  exception=traceback.format_exc())
        results = dict()
        for key in ('result-status', 'result-jobid'):
            if result.get_child_by_name(key):
                results[key] = result[key]
        return results

    def rest_offline_volume(self, current):
        """
        Offline the volume using REST PATCH method.
        """
        response = None
        uuid = current.get('uuid')
        if uuid is None:
            error = 'Error, no uuid in current: %s' % str(current)
            self.na_helper.fail_on_error(error)
        body = dict(state='offline')
        response, error = rest_volume.patch_volume(self.rest_api, uuid, body)
        self.na_helper.fail_on_error(error)
        return response

    def volume_offline(self, current):
        """
        Offline FlexCache volume at destination cluster
        """
        if self.use_rest:
            self.rest_offline_volume(current)
        else:
            options = {'name': self.parameters['name']}
            xml = netapp_utils.zapi.NaElement.create_node_with_children(
                'volume-offline', **options)
            try:
                self.server.invoke_successfully(xml, enable_tunneling=True)
            except netapp_utils.zapi.NaApiError as error:
                self.module.fail_json(
                    msg='Error offlining FlexCache volume: %s' %
                    (to_native(error)),
                    exception=traceback.format_exc())

    def rest_mount_volume(self, current, path):
        """
        Mount the volume using REST PATCH method.
        If path is empty string, unmount the volume.
        """
        response = None
        uuid = current.get('uuid')
        if uuid is None:
            error = 'Error, no uuid in current: %s' % str(current)
            self.na_helper.fail_on_error(error)
        body = dict(nas=dict(path=path))
        response, error = rest_volume.patch_volume(self.rest_api, uuid, body)
        self.na_helper.fail_on_error(error)
        return response

    def rest_unmount_volume(self, current):
        """
        Unmount the volume using REST PATCH method.
        """
        response = None
        if current.get('junction_path'):
            response = self.rest_mount_volume(current, '')
        return response

    def volume_unmount(self, current):
        """
        Unmount FlexCache volume at destination cluster
        """
        if self.use_rest:
            self.rest_unmount_volume(current)
        else:
            options = {'volume-name': self.parameters['name']}
            xml = netapp_utils.zapi.NaElement.create_node_with_children(
                'volume-unmount', **options)
            try:
                self.server.invoke_successfully(xml, enable_tunneling=True)
            except netapp_utils.zapi.NaApiError as error:
                self.module.fail_json(
                    msg='Error unmounting FlexCache volume: %s' %
                    (to_native(error)),
                    exception=traceback.format_exc())

    def flexcache_rest_delete(self, current):
        """
        Delete the flexcache using REST DELETE method.
        """
        response = None
        uuid = current.get('uuid')
        if uuid is None:
            error = 'Error, no uuid in current: %s' % str(current)
            self.na_helper.fail_on_error(error)
        rto = netapp_utils.get_feature(self.module,
                                       'flexcache_delete_return_timeout')
        response, error = rest_flexcache.delete_flexcache(
            self.rest_api,
            uuid,
            timeout=self.parameters['time_out'],
            return_timeout=rto)
        self.na_helper.fail_on_error(error)
        return response

    def flexcache_delete(self, current):
        """
        Delete FlexCache relationship at destination cluster
        Check job status
        """
        if self.parameters['force_unmount']:
            self.volume_unmount(current)
        if self.parameters['force_offline']:
            self.volume_offline(current)
        if self.use_rest:
            return self.flexcache_rest_delete(current)
        results = self.flexcache_delete_async()
        status = results.get('result-status')
        if status == 'in_progress' and 'result-jobid' in results:
            if self.parameters['time_out'] == 0:
                # asynchronous call, assuming success!
                return None
            error = self.check_job_status(results['result-jobid'])
            if error is not None:
                self.module.fail_json(msg='Error when deleting flexcache: %s' %
                                      error)
            return None
        self.module.fail_json(
            msg='Unexpected error when deleting flexcache: results is: %s' %
            repr(results))

    def check_parameters(self, cd_action):
        """
        Validate parameters and fail if one or more required params are missing
        """
        if cd_action != 'create':
            return
        missings = list()
        expected = ('origin_volume', 'origin_vserver')
        if self.parameters['state'] == 'present':
            for param in expected:
                if not self.parameters.get(param):
                    missings.append(param)
        if missings:
            plural = 's' if len(missings) > 1 else ''
            msg = 'Missing parameter%s: %s' % (plural, ', '.join(missings))
            self.module.fail_json(msg=msg)

    def apply(self):
        """
        Apply action to FlexCache
        """
        if not self.use_rest:
            netapp_utils.ems_log_event("na_ontap_flexcache", self.server)
        current = self.flexcache_get()
        cd_action = self.na_helper.get_cd_action(current, self.parameters)
        modify, mount_unmount = None, None
        prepopulate_if_already_created = None

        if self.parameters[
                'state'] == 'present' and 'prepopulate' in self.parameters:
            prepopulate_if_already_created = self.parameters[
                'prepopulate'].pop('force_prepopulate_if_already_created')

        if cd_action is None:
            modify = self.na_helper.get_modified_attributes(
                current, self.parameters)
            if modify:
                if self.use_rest:
                    mount_unmount = modify.pop('junction_path', None)
                if modify:
                    self.module.fail_json(
                        'FlexCache properties cannot be modified by this module.  modify: %s'
                        % str(modify))
            if current and prepopulate_if_already_created:
                # force a prepopulate action
                modify = dict(prepopulate=self.parameters['prepopulate'])
                self.na_helper.changed = True
                self.module.warn(
                    'na_ontap_flexcache is not idempotent when prepopulate is present and force_prepopulate_if_already_created=true'
                )
                if mount_unmount == '' or current['junction_path'] == '':
                    self.module.warn(
                        'prepopulate requires the FlexCache volume to be mounted'
                    )
        self.check_parameters(cd_action)
        response = None
        if self.na_helper.changed and not self.module.check_mode:
            if cd_action == 'create':
                response = self.flexcache_create()
            elif cd_action == 'delete':
                response = self.flexcache_delete(current)
            else:
                if mount_unmount is not None:
                    # mount first, as this is required for prepopulate to succeed (or fail for unmount)
                    self.rest_mount_volume(current, mount_unmount)
                if modify:
                    response = self.flexcache_rest_modify(current['uuid'])
        self.module.exit_json(changed=self.na_helper.changed,
                              response=response,
                              modify=modify)