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)
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)