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(required=True, type='str'), vserver=dict(required=True, type='str'), ostype=dict(required=False, type='str', default='image'), 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), )) 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']) def get_luns(self): """ Return list of LUNs matching vserver and volume names. :return: list of LUNs in XML format. :rtype: list """ luns = [] tag = None while True: lun_info = netapp_utils.zapi.NaElement('lun-get-iter') if tag: lun_info.add_new_child('tag', tag, True) query_details = netapp_utils.zapi.NaElement('lun-info') query_details.add_new_child('vserver', self.parameters['vserver']) query_details.add_new_child('volume', self.parameters['flexvol_name']) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) lun_info.add_child_elem(query) 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, name, lun): """ Extract LUN details, from XML to python dict :return: Details about the lun :rtype: dict """ return_value = dict(name=name) 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 = { 'qos-policy-group': 'qos_policy_group', 'multiprotocol-type': 'ostype' } 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 get_lun(self, name): """ Return details about the LUN :return: Details about the lun :rtype: dict """ return_value = dict() luns = self.get_luns() # The LUNs have been extracted. # Find the specified lun and extract details. for lun in luns: path = lun.get_child_content('path') _rest, _splitter, found_name = path.rpartition('/') if found_name == name: return_value = self.get_lun_details(found_name, lun) break return return_value if return_value else None 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']), 'ostype': self.parameters['ostype'], '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('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): """ Delete requested LUN """ path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name']) 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): """ Resize requested LUN. :return: True if LUN was actually re-sized, false otherwise. :rtype: bool """ path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name']) 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, 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)) path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name']) 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, modify): """ update LUN properties (except size or name) """ for key, value in modify.items(): self.set_lun_value(key, value) def rename_lun(self): """ rename LUN """ path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['from_name']) new_path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name']) 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 apply(self): results = dict() netapp_utils.ems_log_event("na_ontap_lun", self.server) current = self.get_lun(self.parameters['name']) cd_action = self.na_helper.get_cd_action(current, self.parameters) modify, rename = None, None from_name = self.parameters.get('from_name') 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) 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" % self.parameters['from_name']) if rename: current = old_lun cd_action = None if cd_action is None and self.parameters['state'] == 'present': modify = self.na_helper.get_modified_attributes(current, self.parameters) results['modify'] = dict(modify) if 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 cd_action == 'create': self.create_lun() elif cd_action == 'delete': self.delete_lun() else: if rename: self.rename_lun() size_changed = False if 'size' in modify: # Ensure that size was actually changed. Please # read notes in 'resize_lun' function for details. size_changed = self.resize_lun() modify.pop('size') if modify: # we already handled rename if required modify.pop('name', None) self.modify_lun(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 NetAppOntapNetRoutes(object): """ Create, Modifies and Destroys a Net Route """ def __init__(self): """ Initialize the Ontap Net Route class """ self.use_rest = False 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'), vserver=dict(required=True, type='str'), destination=dict(required=True, type='str'), gateway=dict(required=True, type='str'), metric=dict(required=False, type='int'), from_destination=dict(required=False, type='str', default=None), from_gateway=dict(required=False, type='str', default=None), from_metric=dict(required=False, type='int', default=None), )) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.restApi = OntapRestAPI(self.module) # some attributes are not supported in earlier REST implementation unsupported_rest_properties = ['metric', 'from_metric'] used_unsupported_rest_properties = [ x for x in unsupported_rest_properties if x in self.parameters ] self.use_rest, error = self.restApi.is_rest( used_unsupported_rest_properties) if error is not None: self.module.fail_json(msg=error) if not self.use_rest: 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']) return def create_net_route(self, current_metric=None): """ Creates a new Route """ if self.use_rest: api = "network/ip/routes" params = { 'gateway': self.parameters['gateway'], 'svm': self.parameters['vserver'] } if self.parameters.get('destination') is not None: d = self.parameters['destination'].split('/') params['destination'] = {'address': d[0], 'netmask': d[1]} __, error = self.restApi.post(api, params) if error: self.module.fail_json(msg=error) else: route_obj = netapp_utils.zapi.NaElement('net-routes-create') route_obj.add_new_child("destination", self.parameters['destination']) route_obj.add_new_child("gateway", self.parameters['gateway']) if current_metric is None and self.parameters.get( 'metric') is not None: metric = self.parameters['metric'] else: metric = current_metric # Metric can be None, Can't set metric to none if metric is not None: route_obj.add_new_child("metric", str(metric)) try: self.server.invoke_successfully(route_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error creating net route: %s' % (to_native(error)), exception=traceback.format_exc()) def delete_net_route(self, params): """ Deletes a given Route """ if self.use_rest: uuid = params['uuid'] api = "network/ip/routes/" + uuid data = None message, error = self.restApi.delete(api, data) if error: self.module.fail_json(msg=error) else: route_obj = netapp_utils.zapi.NaElement('net-routes-destroy') if params is None: params = self.parameters route_obj.add_new_child("destination", params['destination']) route_obj.add_new_child("gateway", params['gateway']) try: self.server.invoke_successfully(route_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error deleting net route: %s' % (to_native(error)), exception=traceback.format_exc()) def modify_net_route(self, current, desired): """ Modify a net route Since we cannot modify a route, we are deleting the existing route, and creating a new one. """ if self.use_rest: if desired.get('destination') is not None: d = desired['destination'].split('/') if d[0] != current['destination']['address'] or d[ 1] != current['destination']['netmask']: self.na_helper.changed = True self.parameters['destination'] = desired['destination'] else: self.parameters['destination'] = '%s/%s' % ( current['destination']['address'], current['destination']['netmask']) if desired.get('gateway') is not None: if desired['gateway'] != current['gateway']: self.na_helper.changed = True self.parameters['gateway'] = desired['gateway'] else: self.parameters['gateway'] = current['gateway'] if not self.na_helper.changed or self.module.check_mode: return params = { 'destination': '%s/%s' % (current['destination']['address'], current['destination']['netmask']), 'gateway': current['gateway'] } target = self.get_net_route(params) self.delete_net_route(target) self.create_net_route() return else: # return if there is nothing to change for key, val in desired.items(): if val != current[key]: self.na_helper.changed = True break if not self.na_helper.changed or self.module.check_mode: return # delete and re-create with new params self.delete_net_route(current) route_obj = netapp_utils.zapi.NaElement('net-routes-create') for attribute in ['metric', 'destination', 'gateway']: if desired.get(attribute) is not None: value = desired[attribute] else: value = current[attribute] route_obj.add_new_child(attribute, str(value)) try: result = self.server.invoke_successfully(route_obj, True) except netapp_utils.zapi.NaApiError as error: # restore the old route, create the route with the existing metric self.create_net_route(current['metric']) # return if desired route already exists if to_native(error.code) == '13001': return # Invalid value specified for any of the attributes self.module.fail_json(msg='Error modifying net route: %s' % (to_native(error)), exception=traceback.format_exc()) def get_net_route(self, params=None): """ Checks to see if a route exist or not :return: NaElement object if a route exists, None otherwise """ if params is not None: # we need either destination or gateway to fetch desired route if params.get('destination') is None and params.get( 'gateway') is None: return None if self.use_rest: api = "network/ip/routes" data = {'fields': 'destination,gateway,svm'} message, error = self.restApi.get(api, data) if error: self.module.fail_json(msg=error) if len(message.keys()) == 0: return None elif 'records' in message and len(message['records']) == 0: return None elif 'records' not in message: error = "Unexpected response in get_net_route from %s: %s" % ( api, repr(message)) self.module.fail_json(msg=error) if params is None: params = self.parameters else: if params.get('destination') is None: params['destination'] = self.parameters['destination'] if params.get('gateway') is None: params['gateway'] = self.parameters['gateway'] params['vserver'] = self.parameters['vserver'] for record in message['records']: if record['gateway'] == params['gateway'] and \ record['destination']['address'] == params['destination'].split('/')[0] and \ record.get('svm') and record['svm']['name'] == params['vserver']: return record return None else: current = None route_obj = netapp_utils.zapi.NaElement('net-routes-get') for attr in ['destination', 'gateway']: if params and params.get(attr) is not None: value = params[attr] else: value = self.parameters[attr] route_obj.add_new_child(attr, value) try: result = self.server.invoke_successfully(route_obj, True) if result.get_child_by_name('attributes') is not None: route_info = result.get_child_by_name( 'attributes').get_child_by_name('net-vs-routes-info') current = { 'destination': route_info.get_child_content('destination'), 'gateway': route_info.get_child_content('gateway'), 'metric': int(route_info.get_child_content('metric')) } except netapp_utils.zapi.NaApiError as error: # Error 13040 denotes a route doesn't exist. if to_native(error.code) == "15661": return None self.module.fail_json(msg='Error fetching net route: %s' % (to_native(error)), exception=traceback.format_exc()) return current @staticmethod def is_modify_action(current, desired): """ Get desired action to be applied for net routes Destination and gateway are unique params for a route and cannot be duplicated So if a route with desired destination or gateway exists already, we don't try to modify :param current: current details :param desired: desired details :return: create / delete / modify / None """ if current is None and desired is None: # this is invalid # cannot modify a non existent resource return None if current is None and desired is not None: # idempotency or duplication # we need not create return False if current is not None and desired is not None: # we can't modify an ambiguous route (idempotency/duplication) return False return True def get_params_to_be_modified(self, current): """ Get parameters and values that need to be modified :param current: current details :return: dict(), None """ if current is None: return None desired = dict() if self.parameters.get('new_destination') is not None and \ self.parameters['new_destination'] != current['destination']: desired['destination'] = self.parameters['new_destination'] if self.parameters.get('new_gateway') is not None and \ self.parameters['new_gateway'] != current['gateway']: desired['gateway'] = self.parameters['new_gateway'] if self.parameters.get('new_metric') is not None and \ self.parameters['new_metric'] != current['metric']: desired['metric'] = self.parameters['new_metric'] return desired def apply(self): """ Run Module based on play book """ if not self.use_rest: netapp_utils.ems_log_event("na_ontap_net_routes", self.server) current = self.get_net_route() modify, cd_action = None, None if self.use_rest: modify_params = { 'gateway': self.parameters.get('from_gateway'), 'destination': self.parameters.get('from_destination') } if any(modify_params.values()): # destination and gateway combination is unique, and is considered like a id. so modify destination # or gateway is considered a rename action. old_params = self.get_net_route(modify_params) modify = self.na_helper.is_rename_action(old_params, current) if modify is None: self.module.fail_json( msg="Error modifying: route %s does not exist" % self.parameters['from_destination']) else: cd_action = self.na_helper.get_cd_action( current, self.parameters) else: modify_params = { 'destination': self.parameters.get('from_destination'), 'gateway': self.parameters.get('from_gateway'), 'metric': self.parameters.get('from_metric') } # if any from_* param is present in playbook, check for modify action if any(modify_params.values()): # destination and gateway combination is unique, and is considered like a id. so modify destination # or gateway is considered a rename action. metric is considered an attribute of the route so it is # considered as modify. if modify_params.get('metric') is not None: modify = True old_params = current else: # get parameters that are eligible for modify old_params = self.get_net_route(modify_params) modify = self.na_helper.is_rename_action( old_params, current) if modify is None: self.module.fail_json( msg="Error modifying: route %s does not exist" % self.parameters['from_destination']) else: cd_action = self.na_helper.get_cd_action( current, self.parameters) if cd_action == 'create': if not self.module.check_mode: self.create_net_route() elif cd_action == 'delete': if not self.module.check_mode: self.delete_net_route(current) elif modify: desired = {} for key, value in old_params.items(): desired[key] = value for key, value in modify_params.items(): if value is not None: desired[key] = self.parameters.get(key) self.modify_net_route(old_params, desired) self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapQTree(object): '''Class with qtree operations''' def __init__(self): self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( state=dict(required=False, choices=['present', 'absent'], default='present'), name=dict(required=True, type='str'), from_name=dict(required=False, type='str'), flexvol_name=dict(type='str'), vserver=dict(required=True, type='str'), export_policy=dict(required=False, type='str'), security_style=dict(required=False, choices=['unix', 'ntfs', 'mixed']), oplocks=dict(required=False, choices=['enabled', 'disabled']), unix_permissions=dict(required=False, type='str'), )) self.module = AnsibleModule( argument_spec=self.argument_spec, required_if=[ ('state', 'present', ['flexvol_name']) ], supports_check_mode=True ) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) 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']) def get_qtree(self, name=None): """ Checks if the qtree exists. :param: name : qtree name :return: Details about the qtree False if qtree is not found :rtype: bool """ if name is None: name = self.parameters['name'] qtree_list_iter = netapp_utils.zapi.NaElement('qtree-list-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-info', **{'vserver': self.parameters['vserver'], 'volume': self.parameters['flexvol_name'], 'qtree': name}) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) qtree_list_iter.add_child_elem(query) result = self.server.invoke_successfully(qtree_list_iter, enable_tunneling=True) return_q = None if (result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1): return_q = {'export_policy': result['attributes-list']['qtree-info']['export-policy'], 'unix_permissions': result['attributes-list']['qtree-info']['mode'], 'oplocks': result['attributes-list']['qtree-info']['oplocks'], 'security_style': result['attributes-list']['qtree-info']['security-style']} return return_q def create_qtree(self): """ Create a qtree """ options = {'qtree': self.parameters['name'], 'volume': self.parameters['flexvol_name']} if self.parameters.get('export_policy'): options['export-policy'] = self.parameters['export_policy'] if self.parameters.get('security_style'): options['security-style'] = self.parameters['security_style'] if self.parameters.get('oplocks'): options['oplocks'] = self.parameters['oplocks'] if self.parameters.get('unix_permissions'): options['mode'] = self.parameters['unix_permissions'] qtree_create = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-create', **options) try: self.server.invoke_successfully(qtree_create, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error provisioning qtree %s: %s" % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_qtree(self): """ Delete a qtree """ path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name']) qtree_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-delete', **{'qtree': path}) try: self.server.invoke_successfully(qtree_delete, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error deleting qtree %s: %s" % (path, to_native(error)), exception=traceback.format_exc()) def rename_qtree(self): """ Rename a qtree """ path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['from_name']) new_path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name']) qtree_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-rename', **{'qtree': path, 'new-qtree-name': new_path}) try: self.server.invoke_successfully(qtree_rename, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error renaming qtree %s: %s" % (self.parameters['from_name'], to_native(error)), exception=traceback.format_exc()) def modify_qtree(self): """ Modify a qtree """ options = {'qtree': self.parameters['name'], 'volume': self.parameters['flexvol_name']} if self.parameters.get('export_policy'): options['export-policy'] = self.parameters['export_policy'] if self.parameters.get('security_style'): options['security-style'] = self.parameters['security_style'] if self.parameters.get('oplocks'): options['oplocks'] = self.parameters['oplocks'] if self.parameters.get('unix_permissions'): options['mode'] = self.parameters['unix_permissions'] qtree_modify = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-modify', **options) try: self.server.invoke_successfully(qtree_modify, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error modifying qtree %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def apply(self): '''Call create/delete/modify/rename operations''' netapp_utils.ems_log_event("na_ontap_qtree", self.server) current = self.get_qtree() rename, cd_action, modify = None, None, None if self.parameters.get('from_name'): rename = self.na_helper.is_rename_action(self.get_qtree(self.parameters['from_name']), current) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action is None and self.parameters['state'] == 'present': modify = self.na_helper.get_modified_attributes(current, self.parameters) if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_qtree() if cd_action == 'create': self.create_qtree() elif cd_action == 'delete': self.delete_qtree() elif modify: self.modify_qtree() self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapIgroup(object): 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', default=None), ostype=dict(required=False, type='str'), initiator_group_type=dict(required=False, type='str', choices=['fcp', 'iscsi', 'mixed']), initiators=dict(required=False, type='list', elements='str', aliases=['initiator']), vserver=dict(required=True, type='str'), force_remove_initiator=dict(required=False, type='bool', default=False), bind_portset=dict(required=False, type='str'))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) if self.module.params.get('initiators') is not None: self.parameters['initiators'] = [ self.na_helper.sanitize_wwn(initiator) for initiator in self.module.params['initiators'] ] 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']) def get_igroup(self, name): """ Return details about the igroup :param: name : Name of the igroup :return: Details about the igroup. None if not found. :rtype: dict """ igroup_info = netapp_utils.zapi.NaElement('igroup-get-iter') attributes = dict( query={ 'initiator-group-info': { 'initiator-group-name': name, 'vserver': self.parameters['vserver'] } }) igroup_info.translate_struct(attributes) result, current = None, None try: result = self.server.invoke_successfully(igroup_info, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error fetching igroup info %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) if result.get_child_by_name('num-records') and int( result.get_child_content('num-records')) >= 1: igroup = result.get_child_by_name( 'attributes-list').get_child_by_name('initiator-group-info') initiators = [] if igroup.get_child_by_name('initiators'): current_initiators = igroup['initiators'].get_children() for initiator in current_initiators: initiators.append(initiator['initiator-name']) current = {'initiators': initiators} return current def add_initiators(self): """ Add the list of initiators to igroup :return: None """ # don't add if initiators is empty string if self.parameters.get('initiators') == [ '' ] or self.parameters.get('initiators') is None: return for initiator in self.parameters['initiators']: self.modify_initiator(initiator, 'igroup-add') def remove_initiators(self, initiators): """ Removes all existing initiators from igroup :return: None """ for initiator in initiators: self.modify_initiator(initiator, 'igroup-remove') def modify_initiator(self, initiator, zapi): """ Add or remove an initiator to/from an igroup """ options = { 'initiator-group-name': self.parameters['name'], 'initiator': initiator } igroup_modify = netapp_utils.zapi.NaElement.create_node_with_children( zapi, **options) try: self.server.invoke_successfully(igroup_modify, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error modifying igroup initiator %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def create_igroup(self): """ Create the igroup. """ options = {'initiator-group-name': self.parameters['name']} if self.parameters.get('ostype') is not None: options['os-type'] = self.parameters['ostype'] if self.parameters.get('initiator_group_type') is not None: options['initiator-group-type'] = self.parameters[ 'initiator_group_type'] if self.parameters.get('bind_portset') is not None: options['bind-portset'] = self.parameters['bind_portset'] igroup_create = netapp_utils.zapi.NaElement.create_node_with_children( 'igroup-create', **options) try: self.server.invoke_successfully(igroup_create, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error provisioning igroup %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) self.add_initiators() def delete_igroup(self): """ Delete the igroup. """ igroup_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'igroup-destroy', **{ 'initiator-group-name': self.parameters['name'], 'force': 'true' if self.parameters['force_remove_initiator'] else 'false' }) try: self.server.invoke_successfully(igroup_delete, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error deleting igroup %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def rename_igroup(self): """ Rename the igroup. """ igroup_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'igroup-rename', **{ 'initiator-group-name': self.parameters['from_name'], 'initiator-group-new-name': str(self.parameters['name']) }) try: self.server.invoke_successfully(igroup_rename, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error renaming igroup %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def autosupport_log(self): netapp_utils.ems_log_event("na_ontap_igroup", self.server) def apply(self): self.autosupport_log() current = self.get_igroup(self.parameters['name']) # rename and create are mutually exclusive rename, cd_action, modify = None, None, None if self.parameters.get('from_name'): rename = self.na_helper.is_rename_action( self.get_igroup(self.parameters['from_name']), current) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action is None and self.parameters['state'] == 'present': modify = self.na_helper.get_modified_attributes( current, self.parameters) if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_igroup() elif cd_action == 'create': self.create_igroup() elif cd_action == 'delete': self.delete_igroup() if modify: self.remove_initiators(current['initiators']) self.add_initiators() self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapSVM(object): def __init__(self): self.use_rest = False 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'), root_volume=dict(type='str'), root_volume_aggregate=dict(type='str'), root_volume_security_style=dict( type='str', choices=['unix', 'ntfs', 'mixed', 'unified']), allowed_protocols=dict(type='list', elements='str'), aggr_list=dict(type='list', elements='str'), ipspace=dict(type='str', required=False), snapshot_policy=dict(type='str', required=False), language=dict(type='str', required=False), subtype=dict(type='str', choices=[ 'default', 'dp_destination', 'sync_source', 'sync_destination' ]), comment=dict(type="str", required=False))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) # Ontap documentation uses C.UTF-8, but actually stores as c.utf_8. if 'language' in self.parameters and self.parameters['language'].lower( ) == 'c.utf-8': self.parameters['language'] = 'c.utf_8' self.restApi = OntapRestAPI(self.module) # with REST, to force synchronous operations self.timeout = self.restApi.timeout # root volume not supported with rest api unsupported_rest_properties = [ 'root_volume', 'root_volume_aggregate', 'root_volume_security_style' ] used_unsupported_rest_properties = [ x for x in unsupported_rest_properties if x in self.parameters ] self.use_rest, error = self.restApi.is_rest( used_unsupported_rest_properties) if error is not None: self.module.fail_json(msg=error) if not self.use_rest: 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) @staticmethod def clean_up_output(vserver_details): vserver_details['root_volume'] = None vserver_details['root_volume_aggregate'] = None vserver_details['root_volume_security_style'] = None vserver_details['aggr_list'] = [] for aggr in vserver_details['aggregates']: vserver_details['aggr_list'].append(aggr['name']) vserver_details.pop('aggregates') vserver_details['ipspace'] = vserver_details['ipspace']['name'] vserver_details['snapshot_policy'] = vserver_details[ 'snapshot_policy']['name'] vserver_details['allowed_protocols'] = [] if 'cifs' in vserver_details: if vserver_details['cifs']['enabled']: vserver_details['allowed_protocols'].append('cifs') vserver_details.pop('cifs') if 'fcp' in vserver_details: if vserver_details['fcp']['enabled']: vserver_details['allowed_protocols'].append('fcp') vserver_details.pop('fcp') if 'issi' in vserver_details: if vserver_details['iscsi']['enabled']: vserver_details['allowed_protocols'].append('iscsi') vserver_details.pop('iscsi') if 'nvme' in vserver_details: if vserver_details['nvme']['enabled']: vserver_details['allowed_protocols'].append('nvme') vserver_details.pop('nvme') if 'nfs' in vserver_details: if vserver_details['nfs']['enabled']: vserver_details['allowed_protocols'].append('nfs') vserver_details.pop('nfs') return vserver_details def get_vserver(self, vserver_name=None): """ Checks if vserver exists. :return: vserver object if vserver found None if vserver is not found :rtype: object/None """ if vserver_name is None: vserver_name = self.parameters['name'] if self.use_rest: api = 'svm/svms' params = { 'fields': 'subtype,aggregates,language,snapshot_policy,ipspace,comment,nfs,cifs,fcp,iscsi,nvme' } message, error = self.restApi.get(api, params) if error: self.module.fail_json(msg=error) if len(message.keys()) == 0: return None elif 'records' in message and len(message['records']) == 0: return None elif 'records' not in message: error = "Unexpected response in get_net_route from %s: %s" % ( api, repr(message)) self.module.fail_json(msg=error) vserver_details = None for record in message['records']: if record['name'] == vserver_name: vserver_details = copy.deepcopy(record) break if vserver_details is None: return None return self.clean_up_output(vserver_details) else: vserver_info = netapp_utils.zapi.NaElement('vserver-get-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'vserver-info', **{'vserver-name': vserver_name}) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) vserver_info.add_child_elem(query) result = self.server.invoke_successfully(vserver_info, enable_tunneling=False) vserver_details = None if (result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1): attributes_list = result.get_child_by_name('attributes-list') vserver_info = attributes_list.get_child_by_name( 'vserver-info') aggr_list = list() ''' vserver aggr-list can be empty by default''' get_list = vserver_info.get_child_by_name('aggr-list') if get_list is not None: aggregates = get_list.get_children() for aggr in aggregates: aggr_list.append(aggr.get_content()) protocols = list() '''allowed-protocols is not empty for data SVM, but is for node SVM''' allowed_protocols = vserver_info.get_child_by_name( 'allowed-protocols') if allowed_protocols is not None: get_protocols = allowed_protocols.get_children() for protocol in get_protocols: protocols.append(protocol.get_content()) vserver_details = { 'name': vserver_info.get_child_content('vserver-name'), 'root_volume': vserver_info.get_child_content('root-volume'), 'root_volume_aggregate': vserver_info.get_child_content('root-volume-aggregate'), 'root_volume_security_style': vserver_info.get_child_content( 'root-volume-security-style'), 'subtype': vserver_info.get_child_content('vserver-subtype'), 'aggr_list': aggr_list, 'language': vserver_info.get_child_content('language'), 'snapshot_policy': vserver_info.get_child_content('snapshot-policy'), 'allowed_protocols': protocols, 'ipspace': vserver_info.get_child_content('ipspace'), 'comment': vserver_info.get_child_content('comment') } return vserver_details def create_vserver(self): if self.use_rest: api = 'svm/svms' params = {'name': self.parameters['name']} if self.parameters.get('language'): params['language'] = self.parameters['language'] if self.parameters.get('ipspace'): params['ipspace'] = self.parameters['ipspace'] if self.parameters.get('snapshot_policy'): params['snapshot_policy'] = self.parameters['snapshot_policy'] if self.parameters.get('subtype'): params['subtype'] = self.parameters['subtype'] if self.parameters.get('comment'): params['comment'] = self.parameters['comment'] if self.parameters.get('aggr_list'): params['aggregates'] = [] for aggr in self.parameters['aggr_list']: params['aggregates'].append({'name': aggr}) if self.parameters.get('allowed_protocols'): for protocol in self.parameters['allowed_protocols']: params[protocol] = {'enabled': 'true'} # for a sync operation data = {'return_timeout': self.timeout} __, error = self.restApi.post(api, params, data) if error: self.module.fail_json(msg=error) else: options = {'vserver-name': self.parameters['name']} self.add_parameter_to_dict(options, 'root_volume', 'root-volume') self.add_parameter_to_dict(options, 'root_volume_aggregate', 'root-volume-aggregate') self.add_parameter_to_dict(options, 'root_volume_security_style', 'root-volume-security-style') self.add_parameter_to_dict(options, 'language', 'language') self.add_parameter_to_dict(options, 'ipspace', 'ipspace') self.add_parameter_to_dict(options, 'snapshot_policy', 'snapshot-policy') self.add_parameter_to_dict(options, 'subtype', 'vserver-subtype') self.add_parameter_to_dict(options, 'comment', 'comment') vserver_create = netapp_utils.zapi.NaElement.create_node_with_children( 'vserver-create', **options) try: self.server.invoke_successfully(vserver_create, enable_tunneling=False) except netapp_utils.zapi.NaApiError as e: self.module.fail_json(msg='Error provisioning SVM %s: %s' % (self.parameters['name'], to_native(e)), exception=traceback.format_exc()) # add allowed-protocols, aggr-list after creation, # since vserver-create doesn't allow these attributes during creation options = dict() for key in ('allowed_protocols', 'aggr_list'): if self.parameters.get(key): options[key] = self.parameters[key] if options: self.modify_vserver(options) def delete_vserver(self, current=None): if self.use_rest: if current is None: self.module.fail_json( msg='Internal error, expecting SVM object in delete') api = 'svm/svms/%s' % current['uuid'] params = {} # for a sync operation data = {'return_timeout': self.timeout} __, error = self.restApi.delete(api, params, data) if error: self.module.fail_json(msg=error) else: vserver_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'vserver-destroy', **{'vserver-name': self.parameters['name']}) try: self.server.invoke_successfully(vserver_delete, enable_tunneling=False) except netapp_utils.zapi.NaApiError as e: self.module.fail_json(msg='Error deleting SVM %s: %s' % (self.parameters['name'], to_native(e)), exception=traceback.format_exc()) def rename_vserver(self, current=None): if self.use_rest: if current is None: self.module.fail_json( msg='Internal error, expecting SVM object in rename') api = 'svm/svms/%s' % current['uuid'] params = {'name': self.parameters['name']} # for a sync operation data = {'return_timeout': self.timeout} __, error = self.restApi.patch(api, params, data) if error: self.module.fail_json(msg=error) else: vserver_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'vserver-rename', **{ 'vserver-name': self.parameters['from_name'], 'new-name': self.parameters['name'] }) try: self.server.invoke_successfully(vserver_rename, enable_tunneling=False) except netapp_utils.zapi.NaApiError as e: self.module.fail_json( msg='Error renaming SVM %s: %s' % (self.parameters['from_name'], to_native(e)), exception=traceback.format_exc()) def modify_vserver(self, modify, current=None): ''' Modify vserver. :param modify: list of modify attributes :param current: with rest, SVM object to modify ''' if self.use_rest: if current is None: self.module.fail_json( msg='Internal error, expecting SVM object in modify') api = 'svm/svms/%s' % current['uuid'] for attribute in modify: if attribute == 'snapshot_policy' or attribute == 'allowed_protocols' or attribute == 'aggr_list': self.module.fail_json( msg='REST API does not support modify of %s' % attribute) # for a sync operation data = {'return_timeout': self.timeout} __, error = self.restApi.patch(api, modify, data) if error: self.module.fail_json(msg=error) else: vserver_modify = netapp_utils.zapi.NaElement('vserver-modify') vserver_modify.add_new_child('vserver-name', self.parameters['name']) for attribute in modify: if attribute == 'language': vserver_modify.add_new_child('language', self.parameters['language']) if attribute == 'snapshot_policy': vserver_modify.add_new_child( 'snapshot-policy', self.parameters['snapshot_policy']) if attribute == 'comment': vserver_modify.add_new_child('comment', self.parameters['comment']) if attribute == 'allowed_protocols': allowed_protocols = netapp_utils.zapi.NaElement( 'allowed-protocols') for protocol in self.parameters['allowed_protocols']: allowed_protocols.add_new_child('protocol', protocol) vserver_modify.add_child_elem(allowed_protocols) if attribute == 'aggr_list': aggregates = netapp_utils.zapi.NaElement('aggr-list') for aggr in self.parameters['aggr_list']: aggregates.add_new_child('aggr-name', aggr) vserver_modify.add_child_elem(aggregates) try: self.server.invoke_successfully(vserver_modify, enable_tunneling=False) except netapp_utils.zapi.NaApiError as e: self.module.fail_json(msg='Error modifying SVM %s: %s' % (self.parameters['name'], to_native(e)), exception=traceback.format_exc()) def add_parameter_to_dict(self, adict, name, key=None, tostr=False): ''' add defined parameter (not None) to adict using key. :param adict: a dictionary. :param name: name in self.parameters. :param key: key in adict. :param tostr: boolean. ''' 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 apply(self): '''Call create/modify/delete operations.''' if not self.use_rest: self.asup_log_for_cserver("na_ontap_svm") current = self.get_vserver() cd_action, rename = None, None if self.parameters.get('from_name'): old_svm = self.get_vserver(self.parameters['from_name']) rename = self.na_helper.is_rename_action(old_svm, current) if rename is None: self.module.fail_json( msg='Error renaming SVM %s: no SVM with from_name %s.' % (self.parameters['name'], self.parameters['from_name'])) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = self.na_helper.get_modified_attributes( current, self.parameters) for attribute in modify: if attribute in [ 'root_volume', 'root_volume_aggregate', 'root_volume_security_style', 'subtype', 'ipspace' ]: self.module.fail_json( msg='Error modifying SVM %s: can not modify %s.' % (self.parameters['name'], attribute)) if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_vserver(old_svm) # If rename is True, cd_action is None, but modify could be true or false. if cd_action == 'create': self.create_vserver() elif cd_action == 'delete': self.delete_vserver(current) elif modify: self.modify_vserver(modify, current) self.module.exit_json(changed=self.na_helper.changed) def asup_log_for_cserver(self, event_name): """ Fetch admin vserver for the given cluster Create and Autosupport log event with the given module name :param event_name: Name of the event log :return: None """ results = netapp_utils.get_cserver(self.server) cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) netapp_utils.ems_log_event(event_name, cserver)
class NetAppOntapIgroup(object): """Create/Delete/Rename Igroups and Modify initiators list""" 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', default=None), os_type=dict(required=False, type='str', aliases=['ostype']), igroups=dict(required=False, type='list', elements='str'), initiator_group_type=dict(required=False, type='str', choices=['fcp', 'iscsi', 'mixed'], aliases=['protocol']), initiators=dict(required=False, type='list', elements='str', aliases=['initiator']), vserver=dict(required=True, type='str'), force_remove_initiator=dict(required=False, type='bool', default=False, aliases=['allow_delete_while_mapped']), bind_portset=dict(required=False, type='str') )) self.module = AnsibleModule( argument_spec=self.argument_spec, supports_check_mode=True, mutually_exclusive=[('igroups', 'initiators')] ) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.rest_modify_zapi_to_rest = dict( # initiator_group_type (protocol) cannot be changed after create bind_portset='portset', name='name', os_type='os_type' ) if self.module.params.get('initiators') is not None: self.parameters['initiators'] = [self.na_helper.sanitize_wwn(initiator) for initiator in self.module.params['initiators']] self.rest_api = OntapRestAPI(self.module) self.use_rest = self.rest_api.is_rest() def too_old_for_rest(minimum_generation, minimum_major): return self.use_rest and self.rest_api.get_ontap_version() < (minimum_generation, minimum_major) ontap_99_options = ['bind_portset'] if too_old_for_rest(9, 9) and any(x in self.parameters for x in ontap_99_options): self.module.warn('Warning: falling back to ZAPI: %s' % self.rest_api.options_require_ontap_version(ontap_99_options, version='9.9')) self.use_rest = False ontap_99_options = ['igroups'] if not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 9) and any(x in self.parameters for x in ontap_99_options): self.module.fail_json(msg='Error: %s' % self.rest_api.options_require_ontap_version(ontap_99_options, version='9.9')) if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 9): if 'igroups' in self.parameters: # we may need to remove existing initiators self.parameters['initiators'] = list() elif 'initiators' in self.parameters: # we may need to remove existing igroups self.parameters['igroups'] = list() if not self.use_rest: 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']) 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 get_igroup_rest(self, name): api = "protocols/san/igroups" fields = 'name,uuid,svm,initiators,os_type,protocol' if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 9): fields += ',igroups' query = dict(name=name, fields=fields) query['svm.name'] = self.parameters['vserver'] response, error = self.rest_api.get(api, query) igroup, error = rrh.check_for_0_or_1_records(api, response, error) self.fail_on_error(error) if igroup: try: igroup_details = dict( name=igroup['name'], uuid=igroup['uuid'], vserver=igroup['svm']['name'], os_type=igroup['os_type'], initiator_group_type=igroup['protocol'], name_to_uuid=dict() ) except KeyError as exc: self.module.fail_json(msg='Error: unexpected igroup body: %s, KeyError on %s' % (str(igroup), str(exc))) igroup_details['name_to_key'] = dict() for attr in ('igroups', 'initiators'): if attr in igroup: igroup_details[attr] = [item['name'] for item in igroup[attr]] # for initiators, there is no uuid, so we're using name as the key igroup_details['name_to_uuid'][attr] = dict((item['name'], item.get('uuid', item['name'])) for item in igroup[attr]) else: igroup_details[attr] = [] igroup_details['name_to_uuid'][attr] = dict() return igroup_details return None def get_igroup(self, name): """ Return details about the igroup :param: name : Name of the igroup :return: Details about the igroup. None if not found. :rtype: dict """ if self.use_rest: return self.get_igroup_rest(name) igroup_info = netapp_utils.zapi.NaElement('igroup-get-iter') attributes = dict(query={'initiator-group-info': {'initiator-group-name': name, 'vserver': self.parameters['vserver']}}) igroup_info.translate_struct(attributes) current = None try: result = self.server.invoke_successfully(igroup_info, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error fetching igroup info %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1: igroup_info = result.get_child_by_name('attributes-list') initiator_group_info = igroup_info.get_child_by_name('initiator-group-info') initiators = [] if initiator_group_info.get_child_by_name('initiators'): current_initiators = initiator_group_info['initiators'].get_children() for initiator in current_initiators: initiators.append(initiator['initiator-name']) current = { 'initiators': initiators, # place holder, not used for ZAPI 'name_to_uuid': dict(initiators=dict()) } zapi_to_params = { 'vserver': 'vserver', 'initiator-group-os-type': 'os_type', 'initiator-group-portset-name': 'bind_portset', 'initiator-group-type': 'initiator_group_type' } for attr in zapi_to_params: value = igroup_info.get_child_content(attr) if value is not None: current[zapi_to_params[attr]] = value return current def check_what_is_valid(self, what): if self.use_rest and what in ('igroups', 'initiators'): return if what == 'initiators': return raise KeyError('what=%s' % what) def add_initiators_or_igroups_rest(self, uuid, what, names): self.check_what_is_valid(what) api = "protocols/san/igroups/%s/%s" % (uuid, what) records = [dict(name=name) for name in names] body = dict(records=records) dummy, error = self.rest_api.post(api, body) self.fail_on_error(error) def add_initiators_or_igroups(self, uuid, what, current_names): """ Add the list of desired initiators to igroup unless they are already set :return: None """ self.check_what_is_valid(what) # don't add if initiators/igroups is empty string if self.parameters.get(what) == [''] or self.parameters.get(what) is None: return names_to_add = [name for name in self.parameters[what] if name not in current_names] if self.use_rest and uuid is not None and names_to_add: self.add_initiators_or_igroups_rest(uuid, what, names_to_add) else: for name in names_to_add: self.modify_initiator(name, 'igroup-add') def delete_initiator_or_igroup_rest(self, uuid, what, name): self.check_what_is_valid(what) api = "protocols/san/igroups/%s/%s/%s" % (uuid, what, name) dummy, error = self.rest_api.delete(api) self.fail_on_error(error) def remove_initiators_or_igroups(self, uuid, what, current_names, mapping): """ Removes current names from igroup unless they are still desired :return: None """ self.check_what_is_valid(what) for name in current_names: if name not in self.parameters.get(what, list()): if self.use_rest: self.delete_initiator_or_igroup_rest(uuid, what, mapping[name]) else: self.modify_initiator(name, 'igroup-remove') def modify_initiator(self, initiator, zapi): """ Add or remove an initiator to/from an igroup """ options = {'initiator-group-name': self.parameters['name'], 'initiator': initiator} igroup_modify = netapp_utils.zapi.NaElement.create_node_with_children(zapi, **options) try: self.server.invoke_successfully(igroup_modify, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error modifying igroup initiator %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def create_igroup_rest(self): api = "protocols/san/igroups" body = dict( name=self.parameters['name'], os_type=self.parameters['os_type']) body['svm'] = dict(name=self.parameters['vserver']) mapping = dict( initiator_group_type='protocol', bind_portset='portset', igroups='igroups', initiators='initiators' ) for option in mapping: value = self.parameters.get(option) if value is not None: if option in ('igroups', 'initiators'): # we may have an empty list, ignore it value = [dict(name=name) for name in value] if value else None if value is not None: body[mapping[option]] = value dummy, error = self.rest_api.post(api, body) self.fail_on_error(error) def create_igroup(self): """ Create the igroup. """ if self.use_rest: self.create_igroup_rest() return options = {'initiator-group-name': self.parameters['name']} if self.parameters.get('os_type') is not None: options['os-type'] = self.parameters['os_type'] if self.parameters.get('initiator_group_type') is not None: options['initiator-group-type'] = self.parameters['initiator_group_type'] if self.parameters.get('bind_portset') is not None: options['bind-portset'] = self.parameters['bind_portset'] igroup_create = netapp_utils.zapi.NaElement.create_node_with_children( 'igroup-create', **options) try: self.server.invoke_successfully(igroup_create, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error provisioning igroup %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) self.add_initiators_or_igroups(None, 'initiators', []) def modify_igroup_rest(self, uuid, modify): api = "protocols/san/igroups/%s" % uuid body = dict() for option in modify: if option not in self.rest_modify_zapi_to_rest: self.module.fail_json(msg='Error: modifying %s is not supported in REST' % option) body[self.rest_modify_zapi_to_rest[option]] = modify[option] if body: dummy, error = self.rest_api.patch(api, body) self.fail_on_error(error) def delete_igroup_rest(self, uuid): api = "protocols/san/igroups/%s" % uuid if self.parameters['force_remove_initiator']: query = dict(allow_delete_while_mapped=True) else: query = None dummy, error = self.rest_api.delete(api, params=query) self.fail_on_error(error) def delete_igroup(self, uuid): """ Delete the igroup. """ if self.use_rest: self.delete_igroup_rest(uuid) return igroup_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'igroup-destroy', **{'initiator-group-name': self.parameters['name'], 'force': 'true' if self.parameters['force_remove_initiator'] else 'false'}) try: self.server.invoke_successfully(igroup_delete, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error deleting igroup %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def rename_igroup(self): """ Rename the igroup. """ if self.use_rest: self.module.fail_json('Internal error, should not call rename, but use modify') igroup_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'igroup-rename', **{'initiator-group-name': self.parameters['from_name'], 'initiator-group-new-name': str(self.parameters['name'])}) try: self.server.invoke_successfully(igroup_rename, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error renaming igroup %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def report_error_in_modify(self, modify, context): if modify: if len(modify) > 1: tag = 'any of ' else: tag = '' self.module.fail_json(msg='Error: modifying %s %s is not supported in %s' % (tag, str(modify), context)) def validate_modify(self, modify): """Identify options that cannot be modified for REST or ZAPI """ if not modify: return modify_local = dict(modify) modify_local.pop('igroups', None) modify_local.pop('initiators', None) if not self.use_rest: self.report_error_in_modify(modify_local, 'ZAPI') return for option in modify: if option in self.rest_modify_zapi_to_rest: modify_local.pop(option) self.report_error_in_modify(modify_local, 'REST') def autosupport_log(self): if not self.use_rest: netapp_utils.ems_log_event("na_ontap_igroup", self.server) def is_rename_action(self, cd_action, current): old = self.get_igroup(self.parameters['from_name']) rename = self.na_helper.is_rename_action(old, current) if rename is None: self.module.fail_json(msg='Error: igroup with from_name=%s not found' % self.parameters.get('from_name')) if rename: current = old cd_action = None return cd_action, rename, current def apply(self): self.autosupport_log() uuid = None rename, modify = None, None current = self.get_igroup(self.parameters['name']) cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action == 'create' and self.parameters.get('from_name'): cd_action, rename, current = self.is_rename_action(cd_action, current) if cd_action is None and self.parameters['state'] == 'present': modify = self.na_helper.get_modified_attributes(current, self.parameters) # a change in name is handled in rename for ZAPI, but REST can use modify if self.use_rest: rename = False else: modify.pop('name', None) if current and self.use_rest: uuid = current['uuid'] if cd_action == 'create' and self.use_rest and 'os_type' not in self.parameters: self.module.fail_json(msg='Error: os_type is a required parameter when creating an igroup with REST') self.validate_modify(modify) if self.na_helper.changed and not self.module.check_mode: if rename: self.rename_igroup() elif cd_action == 'create': self.create_igroup() elif cd_action == 'delete': self.delete_igroup(uuid) if modify: for attr in ('igroups', 'initiators'): if attr in current: # we need to remove everything first self.remove_initiators_or_igroups(uuid, attr, current[attr], current['name_to_uuid'][attr]) for attr in ('igroups', 'initiators'): if attr in current: self.add_initiators_or_igroups(uuid, attr, current[attr]) modify.pop(attr, None) if modify: self.modify_igroup_rest(uuid, modify) self.module.exit_json(changed=self.na_helper.changed, current=current, modify=modify)
class NetAppOntapQuotaPolicy(object): """ Create, rename or delete a quota policy """ def __init__(self): """ Initialize the ONTAP quota policy class """ self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update( dict( state=dict(required=False, choices=['present', 'absent'], default='present'), vserver=dict(required=True, type='str'), name=dict(required=True, type='str'), from_name=dict(required=False, type='str'), )) self.module = AnsibleModule(argument_spec=self.argument_spec, required_if=[('state', 'present', ['name', 'vserver'])], supports_check_mode=True) # set up variables self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) 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']) def get_quota_policy(self, policy_name=None): if policy_name is None: policy_name = self.parameters['name'] return_value = None quota_policy_get_iter = netapp_utils.zapi.NaElement( 'quota-policy-get-iter') quota_policy_info = netapp_utils.zapi.NaElement('quota-policy-info') quota_policy_info.add_new_child('policy-name', policy_name) quota_policy_info.add_new_child('vserver', self.parameters['vserver']) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(quota_policy_info) quota_policy_get_iter.add_child_elem(query) try: result = self.server.invoke_successfully(quota_policy_get_iter, True) if result.get_child_by_name('attributes-list'): quota_policy_attributes = result['attributes-list'][ 'quota-policy-info'] return_value = {'name': quota_policy_attributes['policy-name']} except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error fetching quota policy %s: %s' % (policy_name, to_native(error)), exception=traceback.format_exc()) return return_value def create_quota_policy(self): """ Creates a new quota policy """ quota_policy_obj = netapp_utils.zapi.NaElement("quota-policy-create") quota_policy_obj.add_new_child("policy-name", self.parameters['name']) quota_policy_obj.add_new_child("vserver", self.parameters['vserver']) try: self.server.invoke_successfully(quota_policy_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error creating quota policy %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_quota_policy(self): """ Deletes a quota policy """ quota_policy_obj = netapp_utils.zapi.NaElement("quota-policy-delete") quota_policy_obj.add_new_child("policy-name", self.parameters['name']) try: self.server.invoke_successfully(quota_policy_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error deleting quota policy %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def rename_quota_policy(self): """ Rename a quota policy """ quota_policy_obj = netapp_utils.zapi.NaElement("quota-policy-rename") quota_policy_obj.add_new_child("policy-name", self.parameters['from_name']) quota_policy_obj.add_new_child("vserver", self.parameters['vserver']) quota_policy_obj.add_new_child("new-policy-name", self.parameters['name']) try: self.server.invoke_successfully(quota_policy_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error renaming quota policy %s: %s' % (self.parameters['from_name'], to_native(error)), exception=traceback.format_exc()) def apply(self): netapp_utils.ems_log_event("na_ontap_efficiency_policy", self.server) current = self.get_quota_policy() # rename and create are mutually exclusive rename, cd_action = None, None if self.parameters.get('from_name'): rename = self.na_helper.is_rename_action( self.get_quota_policy(self.parameters['from_name']), current) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) if self.na_helper.changed: if self.module.check_mode: pass else: if cd_action == 'create': self.create_quota_policy() elif cd_action == 'delete': self.delete_quota_policy() elif rename: self.rename_quota_policy() self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapQosPolicyGroup(object): """ Create, delete, modify and rename a policy group. """ def __init__(self): """ Initialize the Ontap qos policy group class. """ 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'), vserver=dict(required=True, type='str'), max_throughput=dict(required=False, type='str'), min_throughput=dict(required=False, type='str'), is_shared=dict(required=False, type='bool'), force=dict(required=False, type='bool', default=False))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) 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) def get_policy_group(self, policy_group_name=None): """ Return details of a policy group. :param policy_group_name: policy group name :return: policy group details. :rtype: dict. """ if policy_group_name is None: policy_group_name = self.parameters['name'] policy_group_get_iter = netapp_utils.zapi.NaElement( 'qos-policy-group-get-iter') policy_group_info = netapp_utils.zapi.NaElement( 'qos-policy-group-info') policy_group_info.add_new_child('policy-group', policy_group_name) policy_group_info.add_new_child('vserver', self.parameters['vserver']) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(policy_group_info) policy_group_get_iter.add_child_elem(query) result = self.server.invoke_successfully(policy_group_get_iter, True) policy_group_detail = None if result.get_child_by_name('num-records') and int( result.get_child_content('num-records')) == 1: policy_info = result.get_child_by_name( 'attributes-list').get_child_by_name('qos-policy-group-info') policy_group_detail = { 'name': policy_info.get_child_content('policy-group'), 'vserver': policy_info.get_child_content('vserver'), 'max_throughput': policy_info.get_child_content('max-throughput'), 'min_throughput': policy_info.get_child_content('min-throughput'), 'is_shared': self.na_helper.get_value_for_bool( True, policy_info.get_child_content('is-shared')) } return policy_group_detail def create_policy_group(self): """ create a policy group name. """ policy_group = netapp_utils.zapi.NaElement('qos-policy-group-create') policy_group.add_new_child('policy-group', self.parameters['name']) policy_group.add_new_child('vserver', self.parameters['vserver']) if self.parameters.get('max_throughput'): policy_group.add_new_child('max-throughput', self.parameters['max_throughput']) if self.parameters.get('min_throughput'): policy_group.add_new_child('min-throughput', self.parameters['min_throughput']) if self.parameters.get('is_shared') is not None: policy_group.add_new_child( 'is-shared', self.na_helper.get_value_for_bool( False, self.parameters['is_shared'])) try: self.server.invoke_successfully(policy_group, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error creating qos policy group %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_policy_group(self, policy_group=None): """ delete an existing policy group. :param policy_group: policy group name. """ if policy_group is None: policy_group = self.parameters['name'] policy_group_obj = netapp_utils.zapi.NaElement( 'qos-policy-group-delete') policy_group_obj.add_new_child('policy-group', policy_group) if self.parameters.get('force'): policy_group_obj.add_new_child('force', str(self.parameters['force'])) try: self.server.invoke_successfully(policy_group_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error deleting qos policy group %s: %s' % (policy_group, to_native(error)), exception=traceback.format_exc()) def modify_policy_group(self): """ Modify policy group. """ policy_group_obj = netapp_utils.zapi.NaElement( 'qos-policy-group-modify') policy_group_obj.add_new_child('policy-group', self.parameters['name']) if self.parameters.get('max_throughput'): policy_group_obj.add_new_child('max-throughput', self.parameters['max_throughput']) if self.parameters.get('min_throughput'): policy_group_obj.add_new_child('min-throughput', self.parameters['min_throughput']) try: self.server.invoke_successfully(policy_group_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error modifying qos policy group %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def rename_policy_group(self): """ Rename policy group name. """ rename_obj = netapp_utils.zapi.NaElement('qos-policy-group-rename') rename_obj.add_new_child('new-name', self.parameters['name']) rename_obj.add_new_child('policy-group-name', self.parameters['from_name']) try: self.server.invoke_successfully(rename_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error renaming qos policy group %s: %s' % (self.parameters['from_name'], to_native(error)), exception=traceback.format_exc()) def modify_helper(self, modify): """ helper method to modify policy group. :param modify: modified attributes. """ if 'is_shared' in modify: self.module.fail_json( msg='Error cannot modify is_shared attribute.') if any([ attribute in modify for attribute in ['max_throughput', 'min_throughput'] ]): self.modify_policy_group() def apply(self): """ Run module based on playbook """ self.asup_log_for_cserver("na_ontap_qos_policy_group") current = self.get_policy_group() rename, cd_action = None, None cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action == 'create' and self.parameters.get('from_name'): # create policy by renaming an existing one old_policy = self.get_policy_group(self.parameters['from_name']) rename = self.na_helper.is_rename_action(old_policy, current) if rename: current = old_policy cd_action = None if rename is None: self.module.fail_json( msg='Error renaming qos policy group: cannot find %s' % self.parameters['from_name']) modify = self.na_helper.get_modified_attributes( current, self.parameters) if self.na_helper.changed and not self.module.check_mode: if rename: self.rename_policy_group() if cd_action == 'create': self.create_policy_group() elif cd_action == 'delete': self.delete_policy_group() elif modify: self.modify_helper(modify) self.module.exit_json(changed=self.na_helper.changed) def asup_log_for_cserver(self, event_name): """ Fetch admin vserver for the given cluster Create and Autosupport log event with the given module name :param event_name: Name of the event log :return: None """ results = netapp_utils.get_cserver(self.server) cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) netapp_utils.ems_log_event(event_name, cserver)
class NetAppOntapSnapshot(object): """ Creates, modifies, and deletes a Snapshot """ 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'), from_name=dict(required=False, type='str'), snapshot=dict(required=True, type="str"), volume=dict(required=True, type="str"), async_bool=dict(required=False, type="bool", default=False), comment=dict(required=False, type="str"), snapmirror_label=dict(required=False, type="str"), ignore_owners=dict(required=False, type="bool", default=False), snapshot_instance_uuid=dict(required=False, type="str"), vserver=dict(required=True, type="str"), )) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) 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']) return def get_snapshot(self, snapshot_name=None): """ Checks to see if a snapshot exists or not :return: Return True if a snapshot exists, False if it doesn't """ if snapshot_name is None: snapshot_name = self.parameters['snapshot'] snapshot_obj = netapp_utils.zapi.NaElement("snapshot-get-iter") desired_attr = netapp_utils.zapi.NaElement("desired-attributes") snapshot_info = netapp_utils.zapi.NaElement('snapshot-info') comment = netapp_utils.zapi.NaElement('comment') snapmirror_label = netapp_utils.zapi.NaElement('snapmirror-label') # add more desired attributes that are allowed to be modified snapshot_info.add_child_elem(comment) snapshot_info.add_child_elem(snapmirror_label) desired_attr.add_child_elem(snapshot_info) snapshot_obj.add_child_elem(desired_attr) # compose query query = netapp_utils.zapi.NaElement("query") snapshot_info_obj = netapp_utils.zapi.NaElement("snapshot-info") snapshot_info_obj.add_new_child("name", snapshot_name) snapshot_info_obj.add_new_child("volume", self.parameters['volume']) snapshot_info_obj.add_new_child("vserver", self.parameters['vserver']) query.add_child_elem(snapshot_info_obj) snapshot_obj.add_child_elem(query) result = self.server.invoke_successfully(snapshot_obj, True) return_value = None if result.get_child_by_name('num-records') and \ int(result.get_child_content('num-records')) == 1: attributes_list = result.get_child_by_name('attributes-list') snap_info = attributes_list.get_child_by_name('snapshot-info') return_value = {'comment': snap_info.get_child_content('comment')} if snap_info.get_child_by_name('snapmirror-label'): return_value['snapmirror_label'] = snap_info.get_child_content( 'snapmirror-label') else: return_value['snapmirror_label'] = None return return_value def create_snapshot(self): """ Creates a new snapshot """ snapshot_obj = netapp_utils.zapi.NaElement("snapshot-create") # set up required variables to create a snapshot snapshot_obj.add_new_child("snapshot", self.parameters['snapshot']) snapshot_obj.add_new_child("volume", self.parameters['volume']) # Set up optional variables to create a snapshot if self.parameters.get('async_bool'): snapshot_obj.add_new_child("async", str(self.parameters['async_bool'])) if self.parameters.get('comment'): snapshot_obj.add_new_child("comment", self.parameters['comment']) if self.parameters.get('snapmirror_label'): snapshot_obj.add_new_child("snapmirror-label", self.parameters['snapmirror_label']) try: self.server.invoke_successfully(snapshot_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error creating snapshot %s: %s' % (self.parameters['snapshot'], to_native(error)), exception=traceback.format_exc()) def delete_snapshot(self): """ Deletes an existing snapshot """ snapshot_obj = netapp_utils.zapi.NaElement("snapshot-delete") # Set up required variables to delete a snapshot snapshot_obj.add_new_child("snapshot", self.parameters['snapshot']) snapshot_obj.add_new_child("volume", self.parameters['volume']) # set up optional variables to delete a snapshot if self.parameters.get('ignore_owners'): snapshot_obj.add_new_child("ignore-owners", str(self.parameters['ignore_owners'])) if self.parameters.get('snapshot_instance_uuid'): snapshot_obj.add_new_child( "snapshot-instance-uuid", self.parameters['snapshot_instance_uuid']) try: self.server.invoke_successfully(snapshot_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error deleting snapshot %s: %s' % (self.parameters['snapshot'], to_native(error)), exception=traceback.format_exc()) def modify_snapshot(self): """ Modify an existing snapshot :return: """ snapshot_obj = netapp_utils.zapi.NaElement("snapshot-modify-iter") # Create query object, this is the existing object query = netapp_utils.zapi.NaElement("query") snapshot_info_obj = netapp_utils.zapi.NaElement("snapshot-info") snapshot_info_obj.add_new_child("name", self.parameters['snapshot']) snapshot_info_obj.add_new_child("vserver", self.parameters['vserver']) query.add_child_elem(snapshot_info_obj) snapshot_obj.add_child_elem(query) # this is what we want to modify in the snapshot object attributes = netapp_utils.zapi.NaElement("attributes") snapshot_info_obj = netapp_utils.zapi.NaElement("snapshot-info") snapshot_info_obj.add_new_child("name", self.parameters['snapshot']) if self.parameters.get('comment'): snapshot_info_obj.add_new_child("comment", self.parameters['comment']) if self.parameters.get('snapmirror_label'): snapshot_info_obj.add_new_child( "snapmirror-label", self.parameters['snapmirror_label']) attributes.add_child_elem(snapshot_info_obj) snapshot_obj.add_child_elem(attributes) try: self.server.invoke_successfully(snapshot_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error modifying snapshot %s: %s' % (self.parameters['snapshot'], to_native(error)), exception=traceback.format_exc()) def rename_snapshot(self): """ Rename the sanpshot """ snapshot_obj = netapp_utils.zapi.NaElement("snapshot-rename") # set up required variables to rename a snapshot snapshot_obj.add_new_child("current-name", self.parameters['from_name']) snapshot_obj.add_new_child("new-name", self.parameters['snapshot']) snapshot_obj.add_new_child("volume", self.parameters['volume']) try: self.server.invoke_successfully(snapshot_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error renaming snapshot %s to %s: %s' % (self.parameters['from_name'], self.parameters['snapshot'], to_native(error)), exception=traceback.format_exc()) def apply(self): """ Check to see which play we should run """ current = self.get_snapshot() netapp_utils.ems_log_event("na_ontap_snapshot", self.server) rename, cd_action = None, None modify = {} if self.parameters.get('from_name'): current_old_name = self.get_snapshot(self.parameters['from_name']) rename = self.na_helper.is_rename_action(current_old_name, current) modify = self.na_helper.get_modified_attributes( current_old_name, self.parameters) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action is None: modify = self.na_helper.get_modified_attributes( current, self.parameters) if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_snapshot() if cd_action == 'create': self.create_snapshot() elif cd_action == 'delete': self.delete_snapshot() elif modify: self.modify_snapshot() self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapAggregate(object): ''' object initialize and class methods ''' def __init__(self): self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update( dict(name=dict(required=True, type='str'), disks=dict(required=False, type='list'), disk_count=dict(required=False, type='int', default=None), disk_size=dict(required=False, type='int'), disk_type=dict(required=False, choices=[ 'ATA', 'BSAS', 'FCAL', 'FSAS', 'LUN', 'MSATA', 'SAS', 'SSD', 'VMDISK' ]), from_name=dict(required=False, type='str'), mirror_disks=dict(required=False, type='list'), nodes=dict(required=False, type='list'), is_mirrored=dict(required=False, type='bool'), raid_size=dict(required=False, type='int'), raid_type=dict(required=False, choices=['raid4', 'raid_dp', 'raid_tec']), service_state=dict(required=False, choices=['online', 'offline']), spare_pool=dict(required=False, choices=['Pool0', 'Pool1']), state=dict(required=False, choices=['present', 'absent'], default='present'), unmount_volumes=dict(required=False, type='bool'), wait_for_online=dict(required=False, type='bool', default=False), time_out=dict(required=False, type='int', default=100), object_store_name=dict(required=False, type='str'), snaplock_type=dict( required=False, type='str', choices=['compliance', 'enterprise', 'non_snaplock']))) self.module = AnsibleModule(argument_spec=self.argument_spec, required_if=[ ('service_state', 'offline', ['unmount_volumes']), ], mutually_exclusive=[ ('is_mirrored', 'disks'), ('is_mirrored', 'mirror_disks'), ('is_mirrored', 'spare_pool'), ('spare_pool', 'disks') ], supports_check_mode=True) self.na_helper = NetAppModule() self.using_vserver_msg = None # This module should be run as cluster admin self.parameters = self.na_helper.set_parameters(self.module.params) if self.parameters.get( 'mirror_disks' ) is not None and self.parameters.get('disks') is None: self.module.fail_json( mgs="mirror_disks require disks options to be set") 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) def aggr_get_iter(self, name): """ Return aggr-get-iter query results :param name: Name of the aggregate :return: NaElement if aggregate found, None otherwise """ aggr_get_iter = netapp_utils.zapi.NaElement('aggr-get-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-attributes', **{'aggregate-name': name}) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) aggr_get_iter.add_child_elem(query) result = None try: result = self.server.invoke_successfully(aggr_get_iter, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: # Error 13040 denotes an aggregate not being found. if to_native(error.code) == "13040": pass else: msg = to_native(error) if self.using_vserver_msg is not None: msg += '. Added info: %s.' % self.using_vserver_msg self.module.fail_json(msg=msg, exception=traceback.format_exc()) return result def get_aggr(self, name=None): """ Fetch details if aggregate exists. :param name: Name of the aggregate to be fetched :return: Dictionary of current details if aggregate found None if aggregate is not found """ if name is None: name = self.parameters['name'] aggr_get = self.aggr_get_iter(name) if (aggr_get and aggr_get.get_child_by_name('num-records') and int(aggr_get.get_child_content('num-records')) >= 1): current_aggr = dict() attr = aggr_get.get_child_by_name( 'attributes-list').get_child_by_name('aggr-attributes') current_aggr['service_state'] = attr.get_child_by_name( 'aggr-raid-attributes').get_child_content('state') return current_aggr return None def object_store_get_iter(self): """ Return aggr-object-store-get query results :return: NaElement if object-store for given aggregate found, None otherwise """ object_store_get_iter = netapp_utils.zapi.NaElement( 'aggr-object-store-get-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'object-store-information', **{ 'object-store-name': self.parameters.get('object_store_name'), 'aggregate': self.parameters.get('name') }) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) object_store_get_iter.add_child_elem(query) result = None try: result = self.server.invoke_successfully(object_store_get_iter, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) return result def get_object_store(self): """ Fetch details if object store attached to the given aggregate exists. :return: Dictionary of current details if object store attached to the given aggregate is found None if object store is not found """ object_store_get = self.object_store_get_iter() if (object_store_get and object_store_get.get_child_by_name('num-records') and int(object_store_get.get_child_content('num-records')) >= 1): current_object_store = dict() attr = object_store_get.get_child_by_name('attributes-list').\ get_child_by_name('object-store-information') current_object_store['object_store_name'] = attr.get_child_content( 'object-store-name') return current_object_store return None def aggregate_online(self): """ Set state of an offline aggregate to online :return: None """ online_aggr = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-online', **{ 'aggregate': self.parameters['name'], 'force-online': 'true' }) try: self.server.invoke_successfully(online_aggr, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error changing the state of aggregate %s to %s: %s' % (self.parameters['name'], self.parameters['service_state'], to_native(error)), exception=traceback.format_exc()) def aggregate_offline(self): """ Set state of an online aggregate to offline :return: None """ offline_aggr = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-offline', **{ 'aggregate': self.parameters['name'], 'force-offline': 'false', 'unmount-volumes': str(self.parameters['unmount_volumes']) }) try: self.server.invoke_successfully(offline_aggr, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error changing the state of aggregate %s to %s: %s' % (self.parameters['name'], self.parameters['service_state'], to_native(error)), exception=traceback.format_exc()) def create_aggr(self): """ Create aggregate :return: None """ if not self.parameters.get('disk_count'): self.module.fail_json(msg='Error provisioning aggregate %s: \ disk_count is required' % self.parameters['name']) options = { 'aggregate': self.parameters['name'], 'disk-count': str(self.parameters['disk_count']) } if self.parameters.get('disk_type'): options['disk-type'] = self.parameters['disk_type'] if self.parameters.get('raid_size'): options['raid-size'] = str(self.parameters['raid_size']) if self.parameters.get('raid_type'): options['raid-type'] = self.parameters['raid_type'] if self.parameters.get('disk_size'): options['disk-size'] = str(self.parameters['disk_size']) if self.parameters.get('is_mirrored'): options['is-mirrored'] = str(self.parameters['is_mirrored']) if self.parameters.get('spare_pool'): options['spare-pool'] = self.parameters['spare_pool'] if self.parameters.get('raid_type'): options['raid-type'] = self.parameters['raid_type'] if self.parameters.get('snaplock_type'): options['snaplock-type'] = self.parameters['snaplock_type'] aggr_create = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-create', **options) if self.parameters.get('nodes'): nodes_obj = netapp_utils.zapi.NaElement('nodes') aggr_create.add_child_elem(nodes_obj) for node in self.parameters['nodes']: nodes_obj.add_new_child('node-name', node) if self.parameters.get('disks'): disks_obj = netapp_utils.zapi.NaElement('disk-info') for disk in self.parameters.get('disks'): disks_obj.add_new_child('name', disk) aggr_create.add_child_elem(disks_obj) if self.parameters.get('mirror_disks'): mirror_disks_obj = netapp_utils.zapi.NaElement('disk-info') for disk in self.parameters.get('mirror_disks'): mirror_disks_obj.add_new_child('name', disk) aggr_create.add_child_elem(mirror_disks_obj) try: self.server.invoke_successfully(aggr_create, enable_tunneling=False) if self.parameters.get('wait_for_online'): # round off time_out retries = (self.parameters['time_out'] + 5) / 10 current = self.get_aggr() status = None if current is None else current['service_state'] while status != 'online' and retries > 0: time.sleep(10) retries = retries - 1 current = self.get_aggr() status = None if current is None else current[ 'service_state'] except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error provisioning aggregate %s: %s" % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_aggr(self): """ Delete aggregate. :return: None """ aggr_destroy = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-destroy', **{'aggregate': self.parameters['name']}) try: self.server.invoke_successfully(aggr_destroy, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error removing aggregate %s: %s" % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def rename_aggregate(self): """ Rename aggregate. """ aggr_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-rename', **{ 'aggregate': self.parameters['from_name'], 'new-aggregate-name': self.parameters['name'] }) try: self.server.invoke_successfully(aggr_rename, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg="Error renaming aggregate %s: %s" % (self.parameters['from_name'], to_native(error)), exception=traceback.format_exc()) def modify_aggr(self, modify): """ Modify state of the aggregate :param modify: dictionary of parameters to be modified :return: None """ if modify['service_state'] == 'offline': self.aggregate_offline() elif modify['service_state'] == 'online': self.aggregate_online() def attach_object_store_to_aggr(self): """ Attach object store to aggregate. :return: None """ attach_object_store = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-object-store-attach', **{ 'aggregate': self.parameters['name'], 'object-store-name': self.parameters['object_store_name'] }) try: self.server.invoke_successfully(attach_object_store, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg="Error attaching object store %s to aggregate %s: %s" % (self.parameters['object_store_name'], self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def asup_log_for_cserver(self, event_name): """ Fetch admin vserver for the given cluster Create and Autosupport log event with the given module name :param event_name: Name of the event log :return: None """ cserver = netapp_utils.get_cserver(self.server) if cserver is None: server = self.server self.using_vserver_msg = netapp_utils.ERROR_MSG['no_cserver'] event_name += ':error_no_cserver' else: server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=cserver) netapp_utils.ems_log_event(event_name, server) def apply(self): """ Apply action to the aggregate :return: None """ self.asup_log_for_cserver("na_ontap_aggregate") object_store_cd_action = None current = self.get_aggr() # rename and create are mutually exclusive rename, cd_action, object_store_current = None, None, None if self.parameters.get('from_name'): rename = self.na_helper.is_rename_action( self.get_aggr(self.parameters['from_name']), current) if rename is None: self.module.fail_json( msg="Error renaming: aggregate %s does not exist" % self.parameters['from_name']) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = self.na_helper.get_modified_attributes( current, self.parameters) if self.parameters.get( 'object_store_name') and cd_action is None and rename is None: object_store_current = self.get_object_store() object_store_cd_action = self.na_helper.get_cd_action( object_store_current, self.parameters.get('object_store_name')) if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_aggregate() elif cd_action == 'create': self.create_aggr() elif cd_action == 'delete': self.delete_aggr() else: if modify: self.modify_aggr(modify) if object_store_cd_action is not None: self.attach_object_store_to_aggr() self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapNetRoutes(object): """ Create, Modifies and Destroys a Net Route """ def __init__(self): """ Initialize the Ontap Net Route class """ self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update( dict( state=dict(required=False, choices=['present', 'absent'], default='present'), vserver=dict(required=True, type='str'), destination=dict(required=True, type='str'), gateway=dict(required=True, type='str'), metric=dict(required=False, type='str'), from_destination=dict(required=False, type='str', default=None), from_gateway=dict(required=False, type='str', default=None), from_metric=dict(required=False, type='str', default=None), )) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) 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']) return def create_net_route(self, current_metric=None): """ Creates a new Route """ route_obj = netapp_utils.zapi.NaElement('net-routes-create') route_obj.add_new_child("destination", self.parameters['destination']) route_obj.add_new_child("gateway", self.parameters['gateway']) if current_metric is None and self.parameters.get( 'metric') is not None: metric = self.parameters['metric'] else: metric = current_metric # Metric can be None, Can't set metric to none if metric is not None: route_obj.add_new_child("metric", metric) try: self.server.invoke_successfully(route_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error creating net route: %s' % (to_native(error)), exception=traceback.format_exc()) def delete_net_route(self, params=None): """ Deletes a given Route """ route_obj = netapp_utils.zapi.NaElement('net-routes-destroy') if params is None: params = self.parameters route_obj.add_new_child("destination", params['destination']) route_obj.add_new_child("gateway", params['gateway']) try: self.server.invoke_successfully(route_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error deleting net route: %s' % (to_native(error)), exception=traceback.format_exc()) def modify_net_route(self, current, desired): """ Modify a net route """ # return if there is nothing to change for key, val in desired.items(): if val != current[key]: self.na_helper.changed = True break if not self.na_helper.changed: return # delete and re-create with new params self.delete_net_route(current) route_obj = netapp_utils.zapi.NaElement('net-routes-create') for attribute in ['metric', 'destination', 'gateway']: if desired.get(attribute) is not None: value = desired[attribute] else: value = current[attribute] route_obj.add_new_child(attribute, value) try: result = self.server.invoke_successfully(route_obj, True) except netapp_utils.zapi.NaApiError as error: # restore the old route, create the route with the existing metric self.create_net_route(current['metric']) # return if desired route already exists if to_native(error.code) == '13001': return # Invalid value specified for any of the attributes self.module.fail_json(msg='Error modifying net route: %s' % (to_native(error)), exception=traceback.format_exc()) def get_net_route(self, params=None): """ Checks to see if a route exist or not :return: NaElement object if a route exists, None otherwise """ if params is not None: # we need at least on of the new_destination or new_gateway to fetch desired route if params.get('destination') is None and params.get( 'gateway') is None: return None current = None route_obj = netapp_utils.zapi.NaElement('net-routes-get') for attr in ['destination', 'gateway']: if params and params.get(attr) is not None: value = params[attr] else: value = self.parameters[attr] route_obj.add_new_child(attr, value) try: result = self.server.invoke_successfully(route_obj, True) if result.get_child_by_name('attributes') is not None: route_info = result.get_child_by_name( 'attributes').get_child_by_name('net-vs-routes-info') current = { 'destination': route_info.get_child_content('destination'), 'gateway': route_info.get_child_content('gateway'), 'metric': route_info.get_child_content('metric') } except netapp_utils.zapi.NaApiError as error: # Error 13040 denotes a route doesn't exist. if to_native(error.code) == "15661": return None self.module.fail_json(msg='Error fetching net route: %s' % (to_native(error)), exception=traceback.format_exc()) return current def is_modify_action(self, current, desired): """ Get desired action to be applied for net routes Destination and gateway are unique params for a route and cannot be duplicated So if a route with desired destination or gateway exists already, we don't try to modify :param current: current details :param desired: desired details :return: create / delete / modify / None """ if current is None and desired is None: # this is invalid # cannot modify a non existent resource return None if current is None and desired is not None: # idempotency or duplication # we need not create return False if current is not None and desired is not None: # we can't modify an ambiguous route (idempotency/duplication) return False return True def get_params_to_be_modified(self, current): """ Get parameters and values that need to be modified :param current: current details :return: dict(), None """ if current is None: return None desired = dict() if self.parameters.get('new_destination') is not None and \ self.parameters['new_destination'] != current['destination']: desired['destination'] = self.parameters['new_destination'] if self.parameters.get('new_gateway') is not None and \ self.parameters['new_gateway'] != current['gateway']: desired['gateway'] = self.parameters['new_gateway'] if self.parameters.get('new_metric') is not None and \ self.parameters['new_metric'] != current['metric']: desired['metric'] = self.parameters['new_metric'] return desired def apply(self): """ Run Module based on play book """ netapp_utils.ems_log_event("na_ontap_net_routes", self.server) current = self.get_net_route() modify, cd_action = None, None modify_params = { 'destination': self.parameters.get('from_destination'), 'gateway': self.parameters.get('from_gateway'), 'metric': self.parameters.get('from_metric') } # if any from_* param is present in playbook, check for modify action if any(modify_params.values()): # destination and gateway combination is unique, and is considered like a id. so modify destination # or gateway is considered a rename action. metric is considered an attribute of the route so it is # considered as modify. if modify_params.get('metric') is not None: modify = True old_params = current else: # get parameters that are eligible for modify old_params = self.get_net_route(modify_params) modify = self.na_helper.is_rename_action(old_params, current) if modify is None: self.module.fail_json( msg="Error modifying: route %s does not exist" % self.parameters['from_destination']) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action == 'create': self.create_net_route() elif cd_action == 'delete': self.delete_net_route() elif modify: desired = {} for key, value in old_params.items(): desired[key] = value for key, value in modify_params.items(): if value is not None: desired[key] = self.parameters.get(key) self.modify_net_route(old_params, desired) self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapBroadcastDomain(object): """ Create, Modifies and Destroys a Broadcast domain """ def __init__(self): """ Initialize the ONTAP Broadcast Domain class """ 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', aliases=["broadcast_domain"]), ipspace=dict(required=False, type='str'), mtu=dict(required=False, type='str'), ports=dict(required=False, type='list', elements='str'), from_name=dict(required=False, type='str'), )) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) 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) return def get_broadcast_domain(self, broadcast_domain=None): """ Return details about the broadcast domain :param broadcast_domain: specific broadcast domain to get. :return: Details about the broadcast domain. None if not found. :rtype: dict """ if broadcast_domain is None: broadcast_domain = self.parameters['name'] domain_get_iter = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-get-iter') broadcast_domain_info = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-info') broadcast_domain_info.add_new_child('broadcast-domain', broadcast_domain) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(broadcast_domain_info) domain_get_iter.add_child_elem(query) result = self.server.invoke_successfully(domain_get_iter, True) domain_exists = None # check if broadcast_domain exists if result.get_child_by_name('num-records') and \ int(result.get_child_content('num-records')) == 1: domain_info = result.get_child_by_name('attributes-list').\ get_child_by_name('net-port-broadcast-domain-info') domain_name = domain_info.get_child_content('broadcast-domain') domain_mtu = domain_info.get_child_content('mtu') domain_ipspace = domain_info.get_child_content('ipspace') domain_ports = domain_info.get_child_by_name('ports') if domain_ports is not None: ports = [ port.get_child_content('port') for port in domain_ports.get_children() ] else: ports = [] domain_exists = { 'domain-name': domain_name, 'mtu': domain_mtu, 'ipspace': domain_ipspace, 'ports': ports } return domain_exists def create_broadcast_domain(self): """ Creates a new broadcast domain """ domain_obj = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-create') domain_obj.add_new_child("broadcast-domain", self.parameters['name']) if self.parameters.get('ipspace'): domain_obj.add_new_child("ipspace", self.parameters['ipspace']) if self.parameters.get('mtu'): domain_obj.add_new_child("mtu", self.parameters['mtu']) if self.parameters.get('ports'): ports_obj = netapp_utils.zapi.NaElement('ports') domain_obj.add_child_elem(ports_obj) for port in self.parameters['ports']: ports_obj.add_new_child('net-qualified-port-name', port) try: self.server.invoke_successfully(domain_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error creating broadcast domain %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_broadcast_domain(self, broadcast_domain=None): """ Deletes a broadcast domain """ if broadcast_domain is None: broadcast_domain = self.parameters['name'] domain_obj = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-destroy') domain_obj.add_new_child("broadcast-domain", broadcast_domain) if self.parameters.get('ipspace'): domain_obj.add_new_child("ipspace", self.parameters['ipspace']) try: self.server.invoke_successfully(domain_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error deleting broadcast domain %s: %s' % (broadcast_domain, to_native(error)), exception=traceback.format_exc()) def modify_broadcast_domain(self): """ Modifies ipspace and mtu options of a broadcast domain """ domain_obj = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-modify') domain_obj.add_new_child("broadcast-domain", self.parameters['name']) if self.parameters.get('mtu'): domain_obj.add_new_child("mtu", self.parameters['mtu']) if self.parameters.get('ipspace'): domain_obj.add_new_child("ipspace", self.parameters['ipspace']) try: self.server.invoke_successfully(domain_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error modifying broadcast domain %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def split_broadcast_domain(self): """ split broadcast domain """ domain_obj = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-split') domain_obj.add_new_child("broadcast-domain", self.parameters['from_name']) domain_obj.add_new_child("new-broadcast-domain", self.parameters['name']) if self.parameters.get('ports'): ports_obj = netapp_utils.zapi.NaElement('ports') domain_obj.add_child_elem(ports_obj) for port in self.parameters['ports']: ports_obj.add_new_child('net-qualified-port-name', port) if self.parameters.get('ipspace'): domain_obj.add_new_child("ipspace", self.parameters['ipspace']) try: self.server.invoke_successfully(domain_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error splitting broadcast domain %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) if len(self.get_broadcast_domain_ports( self.parameters['from_name'])) == 0: self.delete_broadcast_domain(self.parameters['from_name']) def modify_redirect(self, modify): """ :param modify: modify attributes. """ for attribute in modify.keys(): if attribute == 'mtu': self.modify_broadcast_domain() if attribute == 'ports': self.modify_broadcast_domain_ports() def get_modify_attributes(self, current, split): """ :param current: current state. :param split: True or False of split action. :return: list of modified attributes. """ modify = None if self.parameters['state'] == 'present': # split already handled ipspace and ports. if self.parameters.get('from_name'): current = self.get_broadcast_domain( self.parameters['from_name']) if split: modify = self.na_helper.get_modified_attributes( current, self.parameters) if modify.get('ipspace'): del modify['ipspace'] if modify.get('ports'): del modify['ports'] # ipspace can not be modified. else: modify = self.na_helper.get_modified_attributes( current, self.parameters) if modify.get('ipspace'): self.module.fail_json( msg= 'A domain ipspace can not be modified after the domain has been created.', exception=traceback.format_exc()) return modify def modify_broadcast_domain_ports(self): """ compare current and desire ports. Call add or remove ports methods if needed. :return: None. """ current_ports = self.get_broadcast_domain_ports() expect_ports = self.parameters['ports'] # if want to remove all ports, simply delete the broadcast domain. if len(expect_ports) == 0: self.delete_broadcast_domain() return ports_to_remove = list(set(current_ports) - set(expect_ports)) ports_to_add = list(set(expect_ports) - set(current_ports)) if len(ports_to_add) > 0: self.add_broadcast_domain_ports(ports_to_add) if len(ports_to_remove) > 0: self.delete_broadcast_domain_ports(ports_to_remove) def add_broadcast_domain_ports(self, ports): """ Creates new broadcast domain ports """ domain_obj = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-add-ports') domain_obj.add_new_child("broadcast-domain", self.parameters['name']) if self.parameters.get('ipspace'): domain_obj.add_new_child("ipspace", self.parameters['ipspace']) if ports: ports_obj = netapp_utils.zapi.NaElement('ports') domain_obj.add_child_elem(ports_obj) for port in ports: ports_obj.add_new_child('net-qualified-port-name', port) try: self.server.invoke_successfully(domain_obj, True) return True except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error creating port for broadcast domain %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_broadcast_domain_ports(self, ports): """ Deletes broadcast domain ports :param: ports to be deleted. """ domain_obj = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-remove-ports') domain_obj.add_new_child("broadcast-domain", self.parameters['name']) if self.parameters.get('ipspace'): domain_obj.add_new_child("ipspace", self.parameters['ipspace']) if ports: ports_obj = netapp_utils.zapi.NaElement('ports') domain_obj.add_child_elem(ports_obj) for port in ports: ports_obj.add_new_child('net-qualified-port-name', port) try: self.server.invoke_successfully(domain_obj, True) return True except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error deleting port for broadcast domain %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def get_broadcast_domain_ports(self, broadcast_domain=None): """ Return details about the broadcast domain ports. :return: Details about the broadcast domain ports. None if not found. :rtype: list """ if broadcast_domain is None: broadcast_domain = self.parameters['name'] domain_get_iter = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-get-iter') broadcast_domain_info = netapp_utils.zapi.NaElement( 'net-port-broadcast-domain-info') broadcast_domain_info.add_new_child('broadcast-domain', broadcast_domain) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(broadcast_domain_info) domain_get_iter.add_child_elem(query) result = self.server.invoke_successfully(domain_get_iter, True) ports = [] if result.get_child_by_name('num-records') and \ int(result.get_child_content('num-records')) == 1: domain_info = result.get_child_by_name( 'attributes-list').get_child_by_name( 'net-port-broadcast-domain-info') domain_ports = domain_info.get_child_by_name('ports') if domain_ports is not None: ports = [ port.get_child_content('port') for port in domain_ports.get_children() ] return ports def apply(self): """ Run Module based on play book """ self.asup_log_for_cserver("na_ontap_broadcast_domain") current = self.get_broadcast_domain() cd_action, split = None, None cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action == 'create': # either create new domain or split domain. if self.parameters.get('from_name'): split = self.na_helper.is_rename_action( self.get_broadcast_domain(self.parameters['from_name']), current) if split is None: self.module.fail_json( msg='A domain can not be split if it does not exist.', exception=traceback.format_exc()) if split: cd_action = None modify = self.get_modify_attributes(current, split) if self.na_helper.changed: if self.module.check_mode: pass else: if split: self.split_broadcast_domain() if cd_action == 'create': self.create_broadcast_domain() elif cd_action == 'delete': self.delete_broadcast_domain() elif modify: self.modify_redirect(modify) self.module.exit_json(changed=self.na_helper.changed) def asup_log_for_cserver(self, event_name): """ Fetch admin vserver for the given cluster Create and Autosupport log event with the given module name :param event_name: Name of the event log :return: None """ results = netapp_utils.get_cserver(self.server) cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) netapp_utils.ems_log_event(event_name, cserver)
class NetAppOntapSVM(object): def __init__(self): self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update( dict(state=dict(required=False, choices=['present', 'absent'], default='present'), name=dict(required=True, type='str'), from_name=dict(required=False, type='str'), root_volume=dict(type='str'), root_volume_aggregate=dict(type='str'), root_volume_security_style=dict( type='str', choices=['unix', 'ntfs', 'mixed', 'unified']), allowed_protocols=dict(type='list'), aggr_list=dict(type='list'), ipspace=dict(type='str', required=False), snapshot_policy=dict(type='str', required=False), language=dict(type='str', required=False), subtype=dict(choices=[ 'default', 'dp_destination', 'sync_source', 'sync_destination' ]), comment=dict(type="str", required=False))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) 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) def get_vserver(self, vserver_name=None): """ Checks if vserver exists. :return: vserver object if vserver found None if vserver is not found :rtype: object/None """ if vserver_name is None: vserver_name = self.parameters['name'] vserver_info = netapp_utils.zapi.NaElement('vserver-get-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'vserver-info', **{'vserver-name': vserver_name}) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) vserver_info.add_child_elem(query) result = self.server.invoke_successfully(vserver_info, enable_tunneling=False) vserver_details = None if (result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1): attributes_list = result.get_child_by_name('attributes-list') vserver_info = attributes_list.get_child_by_name('vserver-info') aggr_list = list() ''' vserver aggr-list can be empty by default''' get_list = vserver_info.get_child_by_name('aggr-list') if get_list is not None: aggregates = get_list.get_children() for aggr in aggregates: aggr_list.append(aggr.get_content()) protocols = list() '''allowed-protocols is not empty for data SVM, but is for node SVM''' allowed_protocols = vserver_info.get_child_by_name( 'allowed-protocols') if allowed_protocols is not None: get_protocols = allowed_protocols.get_children() for protocol in get_protocols: protocols.append(protocol.get_content()) vserver_details = { 'name': vserver_info.get_child_content('vserver-name'), 'root_volume': vserver_info.get_child_content('root-volume'), 'root_volume_aggregate': vserver_info.get_child_content('root-volume-aggregate'), 'root_volume_security_style': vserver_info.get_child_content('root-volume-security-style'), 'subtype': vserver_info.get_child_content('vserver-subtype'), 'aggr_list': aggr_list, 'language': vserver_info.get_child_content('language'), 'snapshot_policy': vserver_info.get_child_content('snapshot-policy'), 'allowed_protocols': protocols, 'ipspace': vserver_info.get_child_content('ipspace'), 'comment': vserver_info.get_child_content('comment') } return vserver_details def create_vserver(self): options = {'vserver-name': self.parameters['name']} self.add_parameter_to_dict(options, 'root_volume', 'root-volume') self.add_parameter_to_dict(options, 'root_volume_aggregate', 'root-volume-aggregate') self.add_parameter_to_dict(options, 'root_volume_security_style', 'root-volume-security-style') self.add_parameter_to_dict(options, 'language', 'language') self.add_parameter_to_dict(options, 'ipspace', 'ipspace') self.add_parameter_to_dict(options, 'snapshot_policy', 'snapshot-policy') self.add_parameter_to_dict(options, 'subtype', 'vserver-subtype') self.add_parameter_to_dict(options, 'comment', 'comment') vserver_create = netapp_utils.zapi.NaElement.create_node_with_children( 'vserver-create', **options) try: self.server.invoke_successfully(vserver_create, enable_tunneling=False) except netapp_utils.zapi.NaApiError as e: self.module.fail_json(msg='Error provisioning SVM %s: %s' % (self.parameters['name'], to_native(e)), exception=traceback.format_exc()) # add allowed-protocols, aggr-list after creation, # since vserver-create doesn't allow these attributes during creation options = dict() for key in ('allowed_protocols', 'aggr_list'): if self.parameters.get(key): options[key] = self.parameters[key] if options: self.modify_vserver(options) def delete_vserver(self): vserver_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'vserver-destroy', **{'vserver-name': self.parameters['name']}) try: self.server.invoke_successfully(vserver_delete, enable_tunneling=False) except netapp_utils.zapi.NaApiError as e: self.module.fail_json(msg='Error deleting SVM %s: %s' % (self.parameters['name'], to_native(e)), exception=traceback.format_exc()) def rename_vserver(self): vserver_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'vserver-rename', **{ 'vserver-name': self.parameters['from_name'], 'new-name': self.parameters['name'] }) try: self.server.invoke_successfully(vserver_rename, enable_tunneling=False) except netapp_utils.zapi.NaApiError as e: self.module.fail_json(msg='Error renaming SVM %s: %s' % (self.parameters['from_name'], to_native(e)), exception=traceback.format_exc()) def modify_vserver(self, modify): ''' Modify vserver. :param modify: list of modify attributes ''' vserver_modify = netapp_utils.zapi.NaElement('vserver-modify') vserver_modify.add_new_child('vserver-name', self.parameters['name']) for attribute in modify: if attribute == 'language': vserver_modify.add_new_child('language', self.parameters['language']) if attribute == 'snapshot_policy': vserver_modify.add_new_child( 'snapshot_policy', self.parameters['snapshot_policy']) if attribute == 'comment': vserver_modify.add_new_child('comment', self.parameters['comment']) if attribute == 'allowed_protocols': allowed_protocols = netapp_utils.zapi.NaElement( 'allowed-protocols') for protocol in self.parameters['allowed_protocols']: allowed_protocols.add_new_child('protocol', protocol) vserver_modify.add_child_elem(allowed_protocols) if attribute == 'aggr_list': aggregates = netapp_utils.zapi.NaElement('aggr-list') for aggr in self.parameters['aggr_list']: aggregates.add_new_child('aggr-name', aggr) vserver_modify.add_child_elem(aggregates) try: self.server.invoke_successfully(vserver_modify, enable_tunneling=False) except netapp_utils.zapi.NaApiError as e: self.module.fail_json(msg='Error modifying SVM %s: %s' % (self.parameters['name'], to_native(e)), exception=traceback.format_exc()) def add_parameter_to_dict(self, adict, name, key=None, tostr=False): ''' add defined parameter (not None) to adict using key. :param adict: a dictionary. :param name: name in self.parameters. :param key: key in adict. :param tostr: boolean. ''' 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 apply(self): '''Call create/modify/delete operations.''' self.asup_log_for_cserver("na_ontap_svm") current = self.get_vserver() cd_action, rename = None, None if self.parameters.get('from_name'): rename = self.na_helper.is_rename_action( self.get_vserver(self.parameters['from_name']), current) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = self.na_helper.get_modified_attributes( current, self.parameters) for attribute in modify: if attribute in [ 'root_volume', 'root_volume_aggregate', 'root_volume_security_style', 'subtype', 'ipspace' ]: self.module.fail_json( msg='Error modifying SVM %s: can not modify %s.' % (self.parameters['name'], attribute)) if attribute == 'language': # Ontap documentation uses C.UTF-8, but actually stores as c.utf_8. if self.parameters['language'].lower() == 'c.utf-8': self.parameters['language'] = 'c.utf_8' if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_vserver() # If rename is True, cd_action is None, but modify could be true or false. if cd_action == 'create': self.create_vserver() elif cd_action == 'delete': self.delete_vserver() elif modify: self.modify_vserver(modify) self.module.exit_json(changed=self.na_helper.changed) def asup_log_for_cserver(self, event_name): """ Fetch admin vserver for the given cluster Create and Autosupport log event with the given module name :param event_name: Name of the event log :return: None """ results = netapp_utils.get_cserver(self.server) cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) netapp_utils.ems_log_event(event_name, cserver)
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 NetAppOntapAdaptiveQosPolicyGroup(object): """ Create, delete, modify and rename a policy group. """ def __init__(self): """ Initialize the Ontap qos policy group class. """ 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'), vserver=dict(required=True, type='str'), absolute_min_iops=dict(required=False, type='str'), expected_iops=dict(required=False, type='str'), peak_iops=dict(required=False, type='str'), peak_iops_allocation=dict(choices=['allocated_space', 'used_space'], default='used_space'), force=dict(required=False, type='bool', default=False) )) self.module = AnsibleModule( argument_spec=self.argument_spec, supports_check_mode=True ) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) 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) def get_policy_group(self, policy_group_name=None): """ Return details of a policy group. :param policy_group_name: policy group name :return: policy group details. :rtype: dict. """ if policy_group_name is None: policy_group_name = self.parameters['name'] policy_group_get_iter = netapp_utils.zapi.NaElement('qos-adaptive-policy-group-get-iter') policy_group_info = netapp_utils.zapi.NaElement('qos-adaptive-policy-group-info') policy_group_info.add_new_child('policy-group', policy_group_name) policy_group_info.add_new_child('vserver', self.parameters['vserver']) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(policy_group_info) policy_group_get_iter.add_child_elem(query) result = self.server.invoke_successfully(policy_group_get_iter, True) policy_group_detail = None if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) == 1: policy_info = result.get_child_by_name('attributes-list').get_child_by_name('qos-adaptive-policy-group-info') policy_group_detail = { 'name': policy_info.get_child_content('policy-group'), 'vserver': policy_info.get_child_content('vserver'), 'absolute_min_iops': policy_info.get_child_content('absolute-min-iops'), 'expected_iops': policy_info.get_child_content('expected-iops'), 'peak_iops': policy_info.get_child_content('peak-iops'), 'peak_iops_allocation': policy_info.get_child_content('peak-iops-allocation') } return policy_group_detail def create_policy_group(self): """ create a policy group name. """ policy_group = netapp_utils.zapi.NaElement('qos-adaptive-policy-group-create') policy_group.add_new_child('policy-group', self.parameters['name']) policy_group.add_new_child('vserver', self.parameters['vserver']) if self.parameters.get('absolute_min_iops'): policy_group.add_new_child('absolute-min-iops', self.parameters['absolute_min_iops']) if self.parameters.get('expected_iops'): policy_group.add_new_child('expected-iops', self.parameters['expected_iops']) if self.parameters.get('peak_iops'): policy_group.add_new_child('peak-iops', self.parameters['peak_iops']) if self.parameters.get('peak_iops_allocation'): policy_group.add_new_child('peak-iops-allocation', self.parameters['peak_iops_allocation']) try: self.server.invoke_successfully(policy_group, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error creating adaptive qos policy group %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_policy_group(self, policy_group=None): """ delete an existing policy group. :param policy_group: policy group name. """ if policy_group is None: policy_group = self.parameters['name'] policy_group_obj = netapp_utils.zapi.NaElement('qos-adaptive-policy-group-delete') policy_group_obj.add_new_child('policy-group', policy_group) if self.parameters.get('force'): policy_group_obj.add_new_child('force', str(self.parameters['force'])) try: self.server.invoke_successfully(policy_group_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error deleting adaptive qos policy group %s: %s' % (policy_group, to_native(error)), exception=traceback.format_exc()) def modify_policy_group(self): """ Modify policy group. """ policy_group_obj = netapp_utils.zapi.NaElement('qos-adaptive-policy-group-modify') policy_group_obj.add_new_child('policy-group', self.parameters['name']) if self.parameters.get('absolute_min_iops'): policy_group_obj.add_new_child('absolute-min-iops', self.parameters['absolute_min_iops']) if self.parameters.get('expected_iops'): policy_group_obj.add_new_child('expected-iops', self.parameters['expected_iops']) if self.parameters.get('peak_iops'): policy_group_obj.add_new_child('peak-iops', self.parameters['peak_iops']) if self.parameters.get('peak_iops_allocation'): policy_group_obj.add_new_child('peak-iops-allocation', self.parameters['peak_iops_allocation']) try: self.server.invoke_successfully(policy_group_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error modifying adaptive qos policy group %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def rename_policy_group(self): """ Rename policy group name. """ rename_obj = netapp_utils.zapi.NaElement('qos-adaptive-policy-group-rename') rename_obj.add_new_child('new-name', self.parameters['name']) rename_obj.add_new_child('policy-group-name', self.parameters['from_name']) try: self.server.invoke_successfully(rename_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error renaming adaptive qos policy group %s: %s' % (self.parameters['from_name'], to_native(error)), exception=traceback.format_exc()) def modify_helper(self, modify): """ helper method to modify policy group. :param modify: modified attributes. """ for attribute in modify.keys(): if attribute in ['absolute_min_iops', 'expected_iops', 'peak_iops', 'peak_iops_allocation']: self.modify_policy_group() def apply(self): """ Run module based on playbook """ self.autosupport_log("na_ontap_qos_policy_group") current = self.get_policy_group() rename, cd_action = None, None if self.parameters.get('from_name'): rename = self.na_helper.is_rename_action(self.get_policy_group(self.parameters['from_name']), current) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = self.na_helper.get_modified_attributes(current, self.parameters) if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_policy_group() if cd_action == 'create': self.create_policy_group() elif cd_action == 'delete': self.delete_policy_group() elif modify: self.modify_helper(modify) self.module.exit_json(changed=self.na_helper.changed) def autosupport_log(self, event_name): """ Create a log event against the provided vserver """ server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver']) netapp_utils.ems_log_event(event_name, server)
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 NetAppOntapAggregate(object): ''' object initialize and class methods ''' def __init__(self): self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( name=dict(required=True, type='str'), disks=dict(required=False, type='list', elements='str'), disk_count=dict(required=False, type='int', default=None), disk_size=dict(required=False, type='int'), disk_size_with_unit=dict(required=False, type='str'), disk_type=dict(required=False, choices=['ATA', 'BSAS', 'FCAL', 'FSAS', 'LUN', 'MSATA', 'SAS', 'SSD', 'VMDISK']), from_name=dict(required=False, type='str'), mirror_disks=dict(required=False, type='list', elements='str'), nodes=dict(required=False, type='list', elements='str'), is_mirrored=dict(required=False, type='bool'), raid_size=dict(required=False, type='int'), raid_type=dict(required=False, choices=['raid4', 'raid_dp', 'raid_tec', 'raid_0']), service_state=dict(required=False, choices=['online', 'offline']), spare_pool=dict(required=False, choices=['Pool0', 'Pool1']), state=dict(required=False, choices=['present', 'absent'], default='present'), unmount_volumes=dict(required=False, type='bool'), wait_for_online=dict(required=False, type='bool', default=False), time_out=dict(required=False, type='int', default=100), object_store_name=dict(required=False, type='str'), snaplock_type=dict(required=False, type='str', choices=['compliance', 'enterprise', 'non_snaplock']), ignore_pool_checks=dict(required=False, type='bool') )) self.module = AnsibleModule( argument_spec=self.argument_spec, required_if=[ ('service_state', 'offline', ['unmount_volumes']), ], mutually_exclusive=[ ('is_mirrored', 'disks'), ('is_mirrored', 'mirror_disks'), ('is_mirrored', 'spare_pool'), ('spare_pool', 'disks'), ('disk_count', 'disks'), ('disk_size', 'disk_size_with_unit') ], supports_check_mode=True ) self.na_helper = NetAppModule() self.using_vserver_msg = None # This module should be run as cluster admin self.parameters = self.na_helper.set_parameters(self.module.params) if self.parameters.get('mirror_disks') is not None and self.parameters.get('disks') is None: self.module.fail_json(msg="mirror_disks require disks options to be set") 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) def aggr_get_iter(self, name): """ Return aggr-get-iter query results :param name: Name of the aggregate :return: NaElement if aggregate found, None otherwise """ aggr_get_iter = netapp_utils.zapi.NaElement('aggr-get-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-attributes', **{'aggregate-name': name}) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) aggr_get_iter.add_child_elem(query) result = None try: result = self.server.invoke_successfully(aggr_get_iter, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: # Error 13040 denotes an aggregate not being found. if to_native(error.code) == "13040": pass else: msg = to_native(error) if self.using_vserver_msg is not None: msg += '. Added info: %s.' % self.using_vserver_msg self.module.fail_json(msg=msg, exception=traceback.format_exc()) return result def get_aggr(self, name=None): """ Fetch details if aggregate exists. :param name: Name of the aggregate to be fetched :return: Dictionary of current details if aggregate found None if aggregate is not found """ if name is None: name = self.parameters['name'] aggr_get = self.aggr_get_iter(name) if (aggr_get and aggr_get.get_child_by_name('num-records') and int(aggr_get.get_child_content('num-records')) >= 1): current_aggr = dict() attr = aggr_get.get_child_by_name('attributes-list').get_child_by_name('aggr-attributes') current_aggr['service_state'] = attr.get_child_by_name('aggr-raid-attributes').get_child_content('state') if attr.get_child_by_name('aggr-raid-attributes').get_child_content('disk-count'): current_aggr['disk_count'] = int(attr.get_child_by_name('aggr-raid-attributes').get_child_content('disk-count')) return current_aggr return None def disk_get_iter(self, name): """ Return storage-disk-get-iter query results Filter disk list by aggregate name, and only reports disk-name and plex-name :param name: Name of the aggregate :return: NaElement """ disk_get_iter = netapp_utils.zapi.NaElement('storage-disk-get-iter') query_details = { 'query': { 'storage-disk-info': { 'disk-raid-info': { 'disk-aggregate-info': { 'aggregate-name': name } } } } } disk_get_iter.translate_struct(query_details) attributes = { 'desired-attributes': { 'storage-disk-info': { 'disk-name': None, 'disk-raid-info': { 'disk_aggregate_info': { 'plex-name': None } } } } } disk_get_iter.translate_struct(attributes) result = None try: result = self.server.invoke_successfully(disk_get_iter, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) return result def get_aggr_disks(self, name): """ Fetch disks that are used for this aggregate. :param name: Name of the aggregate to be fetched :return: list of tuples (disk-name, plex-name) empty list if aggregate is not found """ disks = list() aggr_get = self.disk_get_iter(name) if (aggr_get and aggr_get.get_child_by_name('num-records') and int(aggr_get.get_child_content('num-records')) >= 1): attr = aggr_get.get_child_by_name('attributes-list') disks = [(disk_info.get_child_content('disk-name'), disk_info.get_child_by_name('disk-raid-info').get_child_by_name('disk-aggregate-info').get_child_content('plex-name')) for disk_info in attr.get_children()] return disks def object_store_get_iter(self, name): """ Return aggr-object-store-get query results :return: NaElement if object-store for given aggregate found, None otherwise """ object_store_get_iter = netapp_utils.zapi.NaElement('aggr-object-store-get-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'object-store-information', **{'object-store-name': self.parameters.get('object_store_name'), 'aggregate': name}) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) object_store_get_iter.add_child_elem(query) result = None try: result = self.server.invoke_successfully(object_store_get_iter, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) return result def get_object_store(self, name): """ Fetch details if object store attached to the given aggregate exists. :return: Dictionary of current details if object store attached to the given aggregate is found None if object store is not found """ object_store_get = self.object_store_get_iter(name) if (object_store_get and object_store_get.get_child_by_name('num-records') and int(object_store_get.get_child_content('num-records')) >= 1): current_object_store = dict() attr = object_store_get.get_child_by_name('attributes-list').\ get_child_by_name('object-store-information') current_object_store['object_store_name'] = attr.get_child_content('object-store-name') return current_object_store return None def aggregate_online(self): """ Set state of an offline aggregate to online :return: None """ online_aggr = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-online', **{'aggregate': self.parameters['name'], 'force-online': 'true'}) try: self.server.invoke_successfully(online_aggr, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error changing the state of aggregate %s to %s: %s' % (self.parameters['name'], self.parameters['service_state'], to_native(error)), exception=traceback.format_exc()) def aggregate_offline(self): """ Set state of an online aggregate to offline :return: None """ offline_aggr = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-offline', **{'aggregate': self.parameters['name'], 'force-offline': 'false', 'unmount-volumes': str(self.parameters['unmount_volumes'])}) try: self.server.invoke_successfully(offline_aggr, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error changing the state of aggregate %s to %s: %s' % (self.parameters['name'], self.parameters['service_state'], to_native(error)), exception=traceback.format_exc()) @staticmethod def get_disks_or_mirror_disks_object(name, disks): ''' create ZAPI object for disks or mirror_disks ''' disks_obj = netapp_utils.zapi.NaElement(name) for disk in disks: disk_info_obj = netapp_utils.zapi.NaElement('disk-info') disk_info_obj.add_new_child('name', disk) disks_obj.add_child_elem(disk_info_obj) return disks_obj def create_aggr(self): """ Create aggregate :return: None """ options = {'aggregate': self.parameters['name']} if self.parameters.get('disk_count'): options['disk-count'] = str(self.parameters['disk_count']) if self.parameters.get('disk_type'): options['disk-type'] = self.parameters['disk_type'] if self.parameters.get('raid_size'): options['raid-size'] = str(self.parameters['raid_size']) if self.parameters.get('raid_type'): options['raid-type'] = self.parameters['raid_type'] if self.parameters.get('disk_size'): options['disk-size'] = str(self.parameters['disk_size']) if self.parameters.get('disk_size_with_unit'): options['disk-size-with-unit'] = str(self.parameters['disk_size_with_unit']) if self.parameters.get('is_mirrored'): options['is-mirrored'] = str(self.parameters['is_mirrored']) if self.parameters.get('spare_pool'): options['spare-pool'] = self.parameters['spare_pool'] if self.parameters.get('raid_type'): options['raid-type'] = self.parameters['raid_type'] if self.parameters.get('snaplock_type'): options['snaplock-type'] = self.parameters['snaplock_type'] if self.parameters.get('ignore_pool_checks'): options['ignore-pool-checks'] = str(self.parameters['ignore_pool_checks']) aggr_create = netapp_utils.zapi.NaElement.create_node_with_children('aggr-create', **options) if self.parameters.get('nodes'): nodes_obj = netapp_utils.zapi.NaElement('nodes') aggr_create.add_child_elem(nodes_obj) for node in self.parameters['nodes']: nodes_obj.add_new_child('node-name', node) if self.parameters.get('disks'): aggr_create.add_child_elem(self.get_disks_or_mirror_disks_object('disks', self.parameters.get('disks'))) if self.parameters.get('mirror_disks'): aggr_create.add_child_elem(self.get_disks_or_mirror_disks_object('mirror-disks', self.parameters.get('mirror_disks'))) try: self.server.invoke_successfully(aggr_create, enable_tunneling=False) if self.parameters.get('wait_for_online'): # round off time_out retries = (self.parameters['time_out'] + 5) / 10 current = self.get_aggr() status = None if current is None else current['service_state'] while status != 'online' and retries > 0: time.sleep(10) retries = retries - 1 current = self.get_aggr() status = None if current is None else current['service_state'] else: current = self.get_aggr() if current is not None and current.get('disk_count') != self.parameters.get('disk_count'): self.module.exit_json(changed=self.na_helper.changed, warnings="Aggregate created with mismatched disk_count: created %s not %s" % (current.get('disk_count'), self.parameters.get('disk_count'))) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error provisioning aggregate %s: %s" % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_aggr(self): """ Delete aggregate. :return: None """ aggr_destroy = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-destroy', **{'aggregate': self.parameters['name']}) try: self.server.invoke_successfully(aggr_destroy, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error removing aggregate %s: %s" % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def rename_aggregate(self): """ Rename aggregate. """ aggr_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-rename', **{'aggregate': self.parameters['from_name'], 'new-aggregate-name': self.parameters['name']}) try: self.server.invoke_successfully(aggr_rename, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error renaming aggregate %s: %s" % (self.parameters['from_name'], to_native(error)), exception=traceback.format_exc()) def modify_aggr(self, modify): """ Modify state of the aggregate :param modify: dictionary of parameters to be modified :return: None """ if modify.get('service_state') == 'offline': self.aggregate_offline() else: disk_size = 0 disk_size_with_unit = None if modify.get('service_state') == 'online': self.aggregate_online() if modify.get('disk_size'): disk_size = modify.get('disk_size') if modify.get('disk_size_with_unit'): disk_size_with_unit = modify.get('disk_size_with_unit') if modify.get('disk_count'): self.add_disks(modify['disk_count'], disk_size=disk_size, disk_size_with_unit=disk_size_with_unit) if modify.get('disks_to_add') or modify.get('mirror_disks_to_add'): self.add_disks(0, modify.get('disks_to_add'), modify.get('mirror_disks_to_add')) def attach_object_store_to_aggr(self): """ Attach object store to aggregate. :return: None """ attach_object_store = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-object-store-attach', **{'aggregate': self.parameters['name'], 'object-store-name': self.parameters['object_store_name']}) try: self.server.invoke_successfully(attach_object_store, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error attaching object store %s to aggregate %s: %s" % (self.parameters['object_store_name'], self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def add_disks(self, count=0, disks=None, mirror_disks=None, disk_size=0, disk_size_with_unit=None): """ Add additional disks to aggregate. :return: None """ options = {'aggregate': self.parameters['name']} if count: options['disk-count'] = str(count) if disks and self.parameters.get('ignore_pool_checks'): options['ignore-pool-checks'] = str(self.parameters['ignore_pool_checks']) if disk_size: options['disk-size'] = str(disk_size) if disk_size_with_unit: options['disk-size-with-unit'] = disk_size_with_unit aggr_add = netapp_utils.zapi.NaElement.create_node_with_children( 'aggr-add', **options) if disks: aggr_add.add_child_elem(self.get_disks_or_mirror_disks_object('disks', disks)) if mirror_disks: aggr_add.add_child_elem(self.get_disks_or_mirror_disks_object('mirror-disks', mirror_disks)) try: self.server.invoke_successfully(aggr_add, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error adding additional disks to aggregate %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def asup_log_for_cserver(self, event_name): """ Fetch admin vserver for the given cluster Create and Autosupport log event with the given module name :param event_name: Name of the event log :return: None """ cserver = netapp_utils.get_cserver(self.server) if cserver is None: server = self.server self.using_vserver_msg = netapp_utils.ERROR_MSG['no_cserver'] event_name += ':error_no_cserver' else: server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=cserver) netapp_utils.ems_log_event(event_name, server) def map_plex_to_primary_and_mirror(self, plex_disks, disks, mirror_disks): ''' we have N plexes, and disks, and maybe mirror_disks we're trying to find which plex is used for disks, and which one, if applicable, for mirror_disks :return: a tuple with the names of the two plexes (disks_plex, mirror_disks_plex) the second one can be None ''' disks_plex = None mirror_disks_plex = None error = None for plex in plex_disks: common = set(plex_disks[plex]).intersection(set(disks)) if common: if disks_plex is None: disks_plex = plex else: error = 'found overlapping plexes: %s and %s' % (disks_plex, plex) if mirror_disks is not None: common = set(plex_disks[plex]).intersection(set(mirror_disks)) if common: if mirror_disks_plex is None: mirror_disks_plex = plex else: error = 'found overlapping mirror plexes: %s and %s' % (mirror_disks_plex, plex) if error is None: # make sure we found a match if disks_plex is None: error = 'cannot not match disks with current aggregate disks' if mirror_disks is not None and mirror_disks_plex is None: if error is not None: error += ', and ' error = 'cannot not match mirror_disks with current aggregate disks' if error: self.module.fail_json(msg="Error mapping disks for aggregate %s: %s. Found: %s" % (self.parameters['name'], error, str(plex_disks))) return disks_plex, mirror_disks_plex def get_disks_to_add(self, aggr_name, disks, mirror_disks): ''' Get list of disks used by the aggregate, as primary and mirror. Report error if: the plexes in use cannot be matched with user inputs (we expect some overlap) the user request requires some disks to be removed (not supported) : return: a tuple of two lists of disks: disks_to_add, mirror_disks_to_add ''' # let's see if we need to add disks disks_in_use = self.get_aggr_disks(aggr_name) # we expect a list of tuples (disk_name, plex_name), if there is a mirror, we should have 2 plexes # let's get a list of disks for each plex plex_disks = dict() for disk_name, plex_name in disks_in_use: plex_disks.setdefault(plex_name, []).append(disk_name) # find who is who disks_plex, mirror_disks_plex = self.map_plex_to_primary_and_mirror(plex_disks, disks, mirror_disks) # Now that we know what is which, find what needs to be removed (error), and what needs to be added disks_to_remove = [disk for disk in plex_disks[disks_plex] if disk not in disks] if mirror_disks_plex: disks_to_remove.extend([disk for disk in plex_disks[mirror_disks_plex] if disk not in mirror_disks]) if disks_to_remove: error = 'these disks cannot be removed: %s' % str(disks_to_remove) self.module.fail_json(msg="Error removing disks is not supported. Aggregate %s: %s. In use: %s" % (aggr_name, error, str(plex_disks))) # finally, what's to be added disks_to_add = [disk for disk in disks if disk not in plex_disks[disks_plex]] mirror_disks_to_add = list() if mirror_disks_plex: mirror_disks_to_add = [disk for disk in mirror_disks if disk not in plex_disks[mirror_disks_plex]] if mirror_disks_to_add and not disks_to_add: self.module.fail_json(msg="Error cannot add mirror disks %s without adding disks for aggregate %s. In use: %s" % (str(mirror_disks_to_add), aggr_name, str(plex_disks))) if disks_to_add or mirror_disks_to_add: self.na_helper.changed = True return disks_to_add, mirror_disks_to_add def apply(self): """ Apply action to the aggregate :return: None """ self.asup_log_for_cserver("na_ontap_aggregate") object_store_cd_action = None aggr_name = self.parameters['name'] current = self.get_aggr() # rename and create are mutually exclusive rename, cd_action, object_store_current = None, None, None if self.parameters.get('from_name'): old_aggr = self.get_aggr(self.parameters['from_name']) rename = self.na_helper.is_rename_action(old_aggr, current) if rename is None: self.module.fail_json(msg="Error renaming: aggregate %s does not exist" % self.parameters['from_name']) if rename: current = old_aggr aggr_name = self.parameters['from_name'] else: cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = self.na_helper.get_modified_attributes(current, self.parameters) if cd_action is None and self.parameters.get('disks') and current is not None: modify['disks_to_add'], modify['mirror_disks_to_add'] = \ self.get_disks_to_add(aggr_name, self.parameters['disks'], self.parameters.get('mirror_disks')) if modify.get('disk_count'): if int(modify['disk_count']) < int(current['disk_count']): self.module.fail_json(msg="specified disk_count is less than current disk_count. Only adding_disk is allowed.") else: modify['disk_count'] = modify['disk_count'] - current['disk_count'] if self.parameters.get('object_store_name'): object_store_current = None if current: object_store_current = self.get_object_store(aggr_name) object_store_cd_action = self.na_helper.get_cd_action(object_store_current, self.parameters.get('object_store_name')) if object_store_cd_action is None and object_store_current is not None\ and object_store_current['object_store_name'] != self.parameters.get('object_store_name'): self.module.fail_json(msg='Error: object store %s is already associated with aggregate %s.' % (object_store_current['object_store_name'], aggr_name)) if self.na_helper.changed and not self.module.check_mode: if cd_action == 'create': self.create_aggr() elif cd_action == 'delete': self.delete_aggr() else: if rename: self.rename_aggregate() if modify: self.modify_aggr(modify) if object_store_cd_action == 'create': self.attach_object_store_to_aggr() self.module.exit_json(changed=self.na_helper.changed)
class NetAppONTAPExportPolicy(object): """ Class with export policy methods """ def __init__(self): self.use_rest = False 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', default=None), vserver=dict(required=True, type='str') )) self.module = AnsibleModule( argument_spec=self.argument_spec, supports_check_mode=True ) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.restApi = OntapRestAPI(self.module) if self.restApi.is_rest(): self.use_rest = True else: 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']) def get_export_policy(self, name=None, uuid=None): """ Return details about the export-policy :param: name : Name of the export-policy :return: Details about the export-policy. None if not found. :rtype: dict """ if name is None: name = self.parameters['name'] if self.use_rest: params = {'fields': 'name', 'name': name, 'svm.uuid': uuid} api = 'protocols/nfs/export-policies/' message, error = self.restApi.get(api, params) if error is not None: self.module.fail_json(msg="Error on fetching export policy: %s" % error) if message['num_records'] > 0: return {'policy-name': message['records'][0]['name']} else: return None else: export_policy_iter = netapp_utils.zapi.NaElement('export-policy-get-iter') export_policy_info = netapp_utils.zapi.NaElement('export-policy-info') export_policy_info.add_new_child('policy-name', name) export_policy_info.add_new_child('vserver', self.parameters['vserver']) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(export_policy_info) export_policy_iter.add_child_elem(query) result = self.server.invoke_successfully(export_policy_iter, True) return_value = None # check if query returns the expected export-policy if result.get_child_by_name('num-records') and \ int(result.get_child_content('num-records')) == 1: export_policy = result.get_child_by_name('attributes-list').get_child_by_name('export-policy-info').get_child_by_name('policy-name') return_value = { 'policy-name': export_policy } return return_value def create_export_policy(self, uuid=None): """ Creates an export policy """ if self.use_rest: params = {'name': self.parameters['name'], 'svm.uuid': uuid} api = 'protocols/nfs/export-policies' message, error = self.restApi.post(api, params) if error is not None: self.module.fail_json(msg="Error on creating export policy: %s" % error) else: export_policy_create = netapp_utils.zapi.NaElement.create_node_with_children( 'export-policy-create', **{'policy-name': self.parameters['name']}) try: self.server.invoke_successfully(export_policy_create, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error on creating export-policy %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_export_policy(self, policy_id=None): """ Delete export-policy """ if self.use_rest: params = {} api = 'protocols/nfs/export-policies/' + str(policy_id) message, error = self.restApi.delete(api, params) if error is not None: self.module.fail_json(msg=" Error on deleting export policy: %s" % error) else: export_policy_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'export-policy-destroy', **{'policy-name': self.parameters['name'], }) try: self.server.invoke_successfully(export_policy_delete, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error on deleting export-policy %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def rename_export_policy(self, policy_id=None): """ Rename the export-policy. """ if self.use_rest: params = {'name': self.parameters['name']} api = 'protocols/nfs/export-policies/' + str(policy_id) message, error = self.restApi.patch(api, params) if error is not None: self.module.fail_json(msg="Error on renaming export policy: %s" % error) else: export_policy_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'export-policy-rename', **{'policy-name': self.parameters['from_name'], 'new-policy-name': self.parameters['name']}) try: self.server.invoke_successfully(export_policy_rename, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error on renaming export-policy %s:%s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def get_export_policy_id(self, name=None): """ Get a export policy's id :return: id of the export policy """ if name is None: name = self.parameters['name'] params = {'fields': 'id', 'svm.name': self.parameters['vserver'], 'name': name } api = 'protocols/nfs/export-policies' message, error = self.restApi.get(api, params) if error is not None: self.module.fail_json(msg="%s" % error) if message['num_records'] == 0: return None else: return message['records'][0]['id'] def get_export_policy_svm_uuid(self): """ Get a svm's uuid :return: uuid of the svm """ params = {'svm.name': self.parameters['vserver']} api = 'protocols/nfs/export-policies' message, error = self.restApi.get(api, params) if error is not None: self.module.fail_json(msg="%s" % error) return message['records'][0]['svm']['uuid'] def apply(self): """ Apply action to export-policy """ policy_id, uuid = None, None cd_action, rename = None, None if not self.use_rest: netapp_utils.ems_log_event("na_ontap_export_policy", self.server) if self.use_rest: uuid = self.get_export_policy_svm_uuid() if self.parameters.get('from_name'): policy_id = self.get_export_policy_id(self.parameters['from_name']) else: policy_id = self.get_export_policy_id() current = self.get_export_policy(uuid=uuid) if self.parameters.get('from_name'): rename = self.na_helper.is_rename_action(self.get_export_policy(self.parameters['from_name']), current) if rename is None: self.module.fail_json(msg="Error renaming: export policy %s does not exist" % self.parameters['from_name']) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_export_policy(policy_id=policy_id) elif cd_action == 'create': self.create_export_policy(uuid=uuid) elif cd_action == 'delete': self.delete_export_policy(policy_id=policy_id) self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapQTree(object): '''Class with qtree operations''' def __init__(self): self.use_rest = False 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'), flexvol_name=dict(required=True, type='str'), vserver=dict(required=True, type='str'), export_policy=dict(required=False, type='str'), security_style=dict(required=False, type='str', choices=['unix', 'ntfs', 'mixed']), oplocks=dict(required=False, type='str', choices=['enabled', 'disabled']), unix_permissions=dict(required=False, type='str'), force_delete=dict(required=False, type='bool', default=True), wait_for_completion=dict(required=False, type='bool', default=True), time_out=dict(required=False, type='int', default=180), )) self.module = AnsibleModule( argument_spec=self.argument_spec, required_if=[ ('state', 'present', ['flexvol_name']) ], supports_check_mode=True ) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.rest_api = OntapRestAPI(self.module) if self.rest_api.is_rest(): self.use_rest = True else: 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']) def get_qtree(self, name=None): """ Checks if the qtree exists. :param: name : qtree name :return: Details about the qtree False if qtree is not found :rtype: bool """ if name is None: name = self.parameters['name'] if self.use_rest: api = "storage/qtrees" query = {'fields': 'export_policy,unix_permissions,security_style,volume', 'svm.name': self.parameters['vserver'], 'volume': self.parameters['flexvol_name'], 'name': name} message, error = self.rest_api.get(api, query) if error: self.module.fail_json(msg=error) if len(message.keys()) == 0: return None elif 'records' in message and len(message['records']) == 0: return None elif 'records' not in message: error = "Unexpected response in get_qtree from %s: %s" % (api, repr(message)) self.module.fail_json(msg=error) return message['records'][0] else: qtree_list_iter = netapp_utils.zapi.NaElement('qtree-list-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-info', **{'vserver': self.parameters['vserver'], 'volume': self.parameters['flexvol_name'], 'qtree': name}) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) qtree_list_iter.add_child_elem(query) result = self.server.invoke_successfully(qtree_list_iter, enable_tunneling=True) return_q = None if (result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1): return_q = {'export_policy': result['attributes-list']['qtree-info']['export-policy'], 'oplocks': result['attributes-list']['qtree-info']['oplocks'], 'security_style': result['attributes-list']['qtree-info']['security-style']} if result['attributes-list']['qtree-info'].get_child_by_name('mode'): return_q['unix_permissions'] = result['attributes-list']['qtree-info']['mode'] else: return_q['unix_permissions'] = '' return return_q def create_qtree(self): """ Create a qtree """ if self.use_rest: api = "storage/qtrees" body = {'name': self.parameters['name'], 'volume': {'name': self.parameters['flexvol_name']}, 'svm': {'name': self.parameters['vserver']}} if self.parameters.get('export_policy'): body['export_policy'] = self.parameters['export_policy'] if self.parameters.get('security_style'): body['security_style'] = self.parameters['security_style'] if self.parameters.get('unix_permissions'): body['unix_permissions'] = self.parameters['unix_permissions'] __, error = self.rest_api.post(api, body) if error: self.module.fail_json(msg=error) else: options = {'qtree': self.parameters['name'], 'volume': self.parameters['flexvol_name']} if self.parameters.get('export_policy'): options['export-policy'] = self.parameters['export_policy'] if self.parameters.get('security_style'): options['security-style'] = self.parameters['security_style'] if self.parameters.get('oplocks'): options['oplocks'] = self.parameters['oplocks'] if self.parameters.get('unix_permissions'): options['mode'] = self.parameters['unix_permissions'] qtree_create = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-create', **options) try: self.server.invoke_successfully(qtree_create, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error provisioning qtree %s: %s" % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_qtree(self, current): """ Delete a qtree """ if self.use_rest: uuid = current['volume']['uuid'] qid = str(current['id']) api = "storage/qtrees/%s/%s" % (uuid, qid) query = {'return_timeout': 3} response, error = self.rest_api.delete(api, params=query) if error: self.module.fail_json(msg=error) if 'job' in response and self.parameters['wait_for_completion']: message, error = self.rest_api.wait_on_job(response['job'], timeout=self.parameters['time_out'], increment=10) if error: self.module.fail_json(msg="%s" % error) else: path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name']) options = {'qtree': path} if self.parameters['force_delete']: options['force'] = "true" qtree_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-delete', **options) try: self.server.invoke_successfully(qtree_delete, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error deleting qtree %s: %s" % (path, to_native(error)), exception=traceback.format_exc()) def rename_qtree(self, current): """ Rename a qtree """ if self.use_rest: body = {'name': self.parameters['name']} uuid = current['volume']['uuid'] qid = str(current['id']) api = "storage/qtrees/%s/%s" % (uuid, qid) dummy, error = self.rest_api.patch(api, body) if error: self.module.fail_json(msg=error) else: path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['from_name']) new_path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name']) qtree_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-rename', **{'qtree': path, 'new-qtree-name': new_path}) try: self.server.invoke_successfully(qtree_rename, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error renaming qtree %s: %s" % (self.parameters['from_name'], to_native(error)), exception=traceback.format_exc()) def modify_qtree(self, current): """ Modify a qtree """ if self.use_rest: now = datetime.datetime.now() body = {} if self.parameters.get('security_style'): body['security_style'] = self.parameters['security_style'] if self.parameters.get('unix_permissions'): body['unix_permissions'] = self.parameters['unix_permissions'] if self.parameters.get('export_policy'): body['export_policy'] = {'name': self.parameters['export_policy']} uuid = current['volume']['uuid'] qid = str(current['id']) api = "storage/qtrees/%s/%s" % (uuid, qid) timeout = 120 query = {'return_timeout': timeout} dummy, error = self.rest_api.patch(api, body, query) later = datetime.datetime.now() time_elapsed = later - now # modify will not return any error if return_timeout is 0, so we set it to 120 seconds as default if time_elapsed.seconds > (timeout - 1): self.module.fail_json(msg="Too long to run") if error: self.module.fail_json(msg=error) else: options = {'qtree': self.parameters['name'], 'volume': self.parameters['flexvol_name']} if self.parameters.get('export_policy'): options['export-policy'] = self.parameters['export_policy'] if self.parameters.get('security_style'): options['security-style'] = self.parameters['security_style'] if self.parameters.get('oplocks'): options['oplocks'] = self.parameters['oplocks'] if self.parameters.get('unix_permissions'): options['mode'] = self.parameters['unix_permissions'] qtree_modify = netapp_utils.zapi.NaElement.create_node_with_children( 'qtree-modify', **options) try: self.server.invoke_successfully(qtree_modify, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error modifying qtree %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def apply(self): '''Call create/delete/modify/rename operations''' if not self.use_rest: netapp_utils.ems_log_event("na_ontap_qtree", self.server) current = self.get_qtree() rename, cd_action, modify = None, None, None if self.parameters.get('from_name'): from_qtree = self.get_qtree(self.parameters['from_name']) rename = self.na_helper.is_rename_action(from_qtree, current) if rename is None: self.module.fail_json(msg='Error renaming: qtree %s does not exist' % self.parameters['from_name']) if rename: current = from_qtree else: cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action is None and self.parameters['state'] == 'present': if self.parameters.get('security_style') and self.parameters['security_style'] != current['security_style']: modify = True if self.parameters.get('unix_permissions') and \ self.parameters['unix_permissions'] != str(current['unix_permissions']): modify = True # rest and zapi handle export policy differently if self.use_rest: if self.parameters.get('export_policy') and \ self.parameters['export_policy'] != current['export_policy']['name']: modify = True else: if self.parameters.get('export_policy') and \ self.parameters['export_policy'] != current['export_policy']: modify = True if self.use_rest and cd_action == 'delete' and not self.parameters['force_delete']: self.module.fail_json(msg='Error: force_delete option is not supported for REST, unless set to true.') if modify: self.na_helper.changed = True if self.na_helper.changed: if self.module.check_mode: pass else: if cd_action == 'create': self.create_qtree() elif cd_action == 'delete': self.delete_qtree(current) else: if rename: self.rename_qtree(current) if modify: self.modify_qtree(current) self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapIpspace(object): '''Class with ipspace operations''' def __init__(self): self.use_rest = False 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'), )) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.rest_api = OntapRestAPI(self.module) if self.rest_api.is_rest(): self.use_rest = True else: 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) return def ipspace_get_iter(self, name): """ Return net-ipspaces-get-iter query results :param name: Name of the ipspace :return: NaElement if ipspace found, None otherwise """ ipspace_get_iter = netapp_utils.zapi.NaElement('net-ipspaces-get-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'net-ipspaces-info', **{'ipspace': name}) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) ipspace_get_iter.add_child_elem(query) try: result = self.server.invoke_successfully(ipspace_get_iter, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: # Error 14636 denotes an ipspace does not exist # Error 13073 denotes an ipspace not found if to_native(error.code) == "14636" or to_native( error.code) == "13073": return None else: self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) return result def get_ipspace(self, name=None): """ Fetch details if ipspace exists :param name: Name of the ipspace to be fetched :return: Dictionary of current details if ipspace found None if ipspace is not found """ if name is None: name = self.parameters['name'] if self.use_rest: api = 'network/ipspaces' params = None message, error = self.rest_api.get(api, params) if error: self.module.fail_json(msg=error) if len(message.keys()) == 0: return None elif 'records' in message and len(message['records']) == 0: return None elif 'records' not in message: error = "Unexpected response from %s: %s" % (api, repr(message)) self.module.fail_json(msg=error) for record in message['records']: if record['name'] == name: return record return None else: ipspace_get = self.ipspace_get_iter(name) if (ipspace_get and ipspace_get.get_child_by_name('num-records') and int(ipspace_get.get_child_content('num-records')) >= 1): current_ipspace = dict() attr_list = ipspace_get.get_child_by_name('attributes-list') attr = attr_list.get_child_by_name('net-ipspaces-info') current_ipspace['name'] = attr.get_child_content('ipspace') return current_ipspace return None def create_ipspace(self): """ Create ipspace :return: None """ if self.use_rest: api = 'network/ipspaces' params = {'name': self.parameters['name']} dummy, error = self.rest_api.post(api, params) if error: self.module.fail_json(msg=error) else: ipspace_create = netapp_utils.zapi.NaElement.create_node_with_children( 'net-ipspaces-create', **{'ipspace': self.parameters['name']}) try: self.server.invoke_successfully(ipspace_create, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg="Error provisioning ipspace %s: %s" % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_ipspace(self): """ Destroy ipspace :return: None """ if self.use_rest: current = self.get_ipspace() if current is not None: uuid = current['uuid'] api = 'network/ipspaces/' + uuid dummy, error = self.rest_api.delete(api) if error: self.module.fail_json(msg=error) else: ipspace_destroy = netapp_utils.zapi.NaElement.create_node_with_children( 'net-ipspaces-destroy', **{'ipspace': self.parameters['name']}) try: self.server.invoke_successfully(ipspace_destroy, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg="Error removing ipspace %s: %s" % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def rename_ipspace(self): """ Rename an ipspace :return: Nothing """ if self.use_rest: current = self.get_ipspace(self.parameters['from_name']) if current is None: self.module.fail_json(msg="Error renaming ipspace %s" % (self.parameters['from_name'])) uuid = current['uuid'] api = 'network/ipspaces/' + uuid params = {'name': self.parameters['name']} dummy, error = self.rest_api.patch(api, params) if error: self.module.fail_json(msg=error) else: ipspace_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'net-ipspaces-rename', **{ 'ipspace': self.parameters['from_name'], 'new-name': self.parameters['name'] }) try: self.server.invoke_successfully(ipspace_rename, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg="Error renaming ipspace %s: %s" % (self.parameters['from_name'], to_native(error)), exception=traceback.format_exc()) def apply(self): """ Apply action to the ipspace :return: Nothing """ current = self.get_ipspace() # rename and create are mutually exclusive rename, cd_action = None, None if self.parameters.get('from_name'): rename = self.na_helper.is_rename_action( self.get_ipspace(self.parameters['from_name']), current) if rename is None: self.module.fail_json( msg="Error renaming: ipspace %s does not exist" % self.parameters['from_name']) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_ipspace() elif cd_action == 'create': self.create_ipspace() elif cd_action == 'delete': self.delete_ipspace() self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapSubnet(object): """ Create, Modifies and Destroys a subnet """ def __init__(self): """ Initialize the ONTAP Subnet class """ self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update( dict(state=dict(required=False, choices=['present', 'absent'], default='present'), name=dict(required=True, type='str'), from_name=dict(required=False, type='str'), broadcast_domain=dict(required=False, type='str'), gateway=dict(required=False, type='str'), ip_ranges=dict(required=False, type='list'), ipspace=dict(required=False, type='str'), subnet=dict(required=False, type='str'))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) 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) return def get_subnet(self, name=None): """ Return details about the subnet :param: name : Name of the subnet :return: Details about the subnet. None if not found. :rtype: dict """ if name is None: name = self.parameters.get('name') subnet_iter = netapp_utils.zapi.NaElement('net-subnet-get-iter') subnet_info = netapp_utils.zapi.NaElement('net-subnet-info') subnet_info.add_new_child('subnet-name', name) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(subnet_info) subnet_iter.add_child_elem(query) result = self.server.invoke_successfully(subnet_iter, True) return_value = None # check if query returns the expected subnet if result.get_child_by_name('num-records') and \ int(result.get_child_content('num-records')) == 1: subnet_attributes = result.get_child_by_name( 'attributes-list').get_child_by_name('net-subnet-info') broadcast_domain = subnet_attributes.get_child_content( 'broadcast-domain') gateway = subnet_attributes.get_child_content('gateway') ipspace = subnet_attributes.get_child_content('ipspace') subnet = subnet_attributes.get_child_content('subnet') name = subnet_attributes.get_child_content('subnet-name') ip_ranges = [] if subnet_attributes.get_child_by_name('ip-ranges'): range_obj = subnet_attributes.get_child_by_name( 'ip-ranges').get_children() for elem in range_obj: ip_ranges.append(elem.get_content()) return_value = { 'name': name, 'broadcast_domain': broadcast_domain, 'gateway': gateway, 'ip_ranges': ip_ranges, 'ipspace': ipspace, 'subnet': subnet } return return_value def create_subnet(self): """ Creates a new subnet """ options = { 'subnet-name': self.parameters.get('name'), 'broadcast-domain': self.parameters.get('broadcast_domain'), 'subnet': self.parameters.get('subnet') } subnet_create = netapp_utils.zapi.NaElement.create_node_with_children( 'net-subnet-create', **options) if self.parameters.get('gateway'): subnet_create.add_new_child('gateway', self.parameters.get('gateway')) if self.parameters.get('ip_ranges'): subnet_ips = netapp_utils.zapi.NaElement('ip-ranges') subnet_create.add_child_elem(subnet_ips) for ip_range in self.parameters.get('ip_ranges'): subnet_ips.add_new_child('ip-range', ip_range) if self.parameters.get('ipspace'): subnet_create.add_new_child('ipspace', self.parameters.get('ipspace')) try: self.server.invoke_successfully(subnet_create, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error creating subnet %s: %s' % (self.parameters.get('name'), to_native(error)), exception=traceback.format_exc()) def delete_subnet(self): """ Deletes a subnet """ subnet_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'net-subnet-destroy', **{'subnet-name': self.parameters.get('name')}) try: self.server.invoke_successfully(subnet_delete, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error deleting subnet %s: %s' % (self.parameters.get('name'), to_native(error)), exception=traceback.format_exc()) def modify_subnet(self): """ Modifies a subnet """ options = {'subnet-name': self.parameters.get('name')} subnet_modify = netapp_utils.zapi.NaElement.create_node_with_children( 'net-subnet-modify', **options) if self.parameters.get('gateway'): subnet_modify.add_new_child('gateway', self.parameters.get('gateway')) if self.parameters.get('ip_ranges'): subnet_ips = netapp_utils.zapi.NaElement('ip-ranges') subnet_modify.add_child_elem(subnet_ips) for ip_range in self.parameters.get('ip_ranges'): subnet_ips.add_new_child('ip-range', ip_range) if self.parameters.get('ipspace'): subnet_modify.add_new_child('ipspace', self.parameters.get('ipspace')) if self.parameters.get('subnet'): subnet_modify.add_new_child('subnet', self.parameters.get('subnet')) try: self.server.invoke_successfully(subnet_modify, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error modifying subnet %s: %s' % (self.parameters.get('name'), to_native(error)), exception=traceback.format_exc()) def rename_subnet(self): """ TODO """ options = { 'subnet-name': self.parameters.get('from_name'), 'new-name': self.parameters.get('name') } subnet_rename = netapp_utils.zapi.NaElement.create_node_with_children( 'net-subnet-rename', **options) if self.parameters.get('ipspace'): subnet_rename.add_new_child('ipspace', self.parameters.get('ipspace')) try: self.server.invoke_successfully(subnet_rename, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error renaming subnet %s: %s' % (self.parameters.get('name'), to_native(error)), exception=traceback.format_exc()) def apply(self): '''Apply action to subnet''' results = netapp_utils.get_cserver(self.server) cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) netapp_utils.ems_log_event("na_ontap_net_subnet", cserver) current = self.get_subnet() cd_action, rename = None, None if self.parameters.get('from_name'): rename = self.na_helper.is_rename_action( self.get_subnet(self.parameters.get('from_name')), current) if rename is None: self.module.fail_json( msg="Error renaming: subnet %s does not exist" % self.parameters.get('from_name')) else: cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = self.na_helper.get_modified_attributes( current, self.parameters) for attribute in modify: if attribute in ['broadcast_domain']: self.module.fail_json( msg= 'Error modifying subnet %s: cannot modify broadcast_domain parameter.' % self.parameters.get('name')) if self.na_helper.changed: if self.module.check_mode: pass else: if rename: self.rename_subnet() # If rename is True, cd_action is NOne but modify could be true if cd_action == 'create': for attribute in ['subnet', 'broadcast_domain']: if not self.parameters.get(attribute): self.module.fail_json( msg='Error - missing required arguments: %s.' % attribute) self.create_subnet() elif cd_action == 'delete': self.delete_subnet() elif modify: self.modify_subnet() self.module.exit_json(changed=self.na_helper.changed)