class NetAppONTAPMetroCluster(object): def __init__(self): self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( state=dict(choices=['present'], default='present'), dr_pairs=dict(required=True, type='list', elements='dict', options=dict( node_name=dict(required=True, type='str'), partner_node_name=dict(required=True, type='str') )), partner_cluster_name=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) self.use_rest = self.restApi.is_rest() if not self.use_rest: self.module.fail_json(msg="na_ontap_metrocluster only supports REST API") def get_metrocluster(self): attrs = None api = 'cluster/metrocluster' options = {'fields': '*'} message, error = self.restApi.get(api, options) if error: self.module.fail_json(msg=error) if message is not None: local = message['local'] if local['configuration_state'] != "not_configured": attrs = { 'configuration_state': local['configuration_state'], 'partner_cluster_reachable': local['partner_cluster_reachable'], 'partner_cluster_name': local['cluster']['name'] } return attrs def create_metrocluster(self): api = 'cluster/metrocluster' options = {} dr_pairs = [] for pair in self.parameters['dr_pairs']: dr_pairs.append({'node': {'name': pair['node_name']}, 'partner': {'name': pair['partner_node_name']}}) partner_cluster = {'name': self.parameters['partner_cluster_name']} data = {'dr_pairs': dr_pairs, 'partner_cluster': partner_cluster} message, error = self.restApi.post(api, data, options) if error is not None: self.module.fail_json(msg="%s" % error) self.restApi.wait_on_job(message['job'], self.parameters['hostname']) def apply(self): current = self.get_metrocluster() 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_metrocluster() # Since there is no modify or delete, we will return no change else: self.module.fail_json(msg="Modify and Delete currently not support in API") 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 NetAppONTAPGatherInfo(object): '''Class with gather info methods''' def __init__(self): """ Parse arguments, setup state variables, check paramenters and ensure request module is installed """ self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update( dict(state=dict(type='str', choices=['info'], default='info', required=False), gather_subset=dict(default=['all'], type='list', elements='str', required=False), max_records=dict(type='int', default=1024, required=False), fields=dict(type='list', elements='str', required=False), parameters=dict(type='dict', required=False))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) # set up variables self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.fields = list() self.rest_api = OntapRestAPI(self.module) def validate_ontap_version(self): """ Method to validate the ONTAP version """ api = 'cluster' data = {'fields': ['version']} ontap_version, error = self.rest_api.get(api, data) if error: self.module.fail_json(msg=error) return ontap_version def get_subset_info(self, gather_subset_info): """ Gather ONTAP information for the given subset using REST APIs Input for REST APIs call : (api, data) return gathered_ontap_info """ api = gather_subset_info['api_call'] if gather_subset_info.pop('post', False): self.run_post(gather_subset_info) data = { 'max_records': self.parameters['max_records'], 'fields': self.fields } # allow for passing in any additional rest api fields if self.parameters.get('parameters'): for each in self.parameters['parameters']: data[each] = self.parameters['parameters'][each] gathered_ontap_info, error = self.rest_api.get(api, data) if error: # Fail the module if error occurs from REST APIs call if int(error.get('code', 0)) == 6: self.module.fail_json( msg="%s user is not authorized to make %s api call" % (self.parameters.get('username'), api)) # if Aggr recommender can't make a recommendation it will fail with the following error code. # We don't want to fail elif int( error.get('code', 0) ) == 19726344 and "No recommendation can be made for this cluster" in error.get( 'message'): return error.get('message') # If the API doesn't exist (using an older system) we don't want to fail elif int(error.get('code', 0)) == 3: return error.get('message') else: self.module.fail_json(msg=error) else: return gathered_ontap_info return None def run_post(self, gather_subset_info): api = gather_subset_info['api_call'] post_return, error = self.rest_api.post(api, None) if error: return None message, error = self.rest_api.wait_on_job(post_return['job'], increment=5) if error: self.module.fail_json(msg="%s" % error) def get_next_records(self, api): """ Gather next set of ONTAP information for the specified api Input for REST APIs call : (api, data) return gather_subset_info """ data = {} gather_subset_info, error = self.rest_api.get(api, data) if error: self.module.fail_json(msg=error) return gather_subset_info def convert_subsets(self): """ Convert an info to the REST API """ info_to_rest_mapping = { "aggregate_info": "storage/aggregates", "application_info": "application/applications", "application_template_info": "application/templates", "autosupport_config_info": "support/autosupport", "autosupport_messages_history": "support/autosupport/messages", "broadcast_domains_info": "network/ethernet/broadcast-domains", "cifs_services_info": "protocols/cifs/services", "cifs_share_info": "protocols/cifs/shares", "cloud_targets_info": "cloud/targets", "cluster_chassis_info": "cluster/chassis", "cluster_jobs_info": "cluster/jobs", "cluster_metrocluster_diagnostics": "cluster/metrocluster/diagnostics", "cluster_metrics_info": "cluster/metrics", "cluster_node_info": "cluster/nodes", "cluster_peer_info": "cluster/peers", "cluster_schedules": "cluster/schedules", "cluster_software_history": "cluster/software/history", "cluster_software_packages": "cluster/software/packages", "disk_info": "storage/disks", "initiator_groups_info": "protocols/san/igroups", "ip_interfaces_info": "network/ip/interfaces", "ip_routes_info": "network/ip/routes", "ip_service_policies": "network/ip/service-policies", "network_ipspaces_info": "network/ipspaces", "network_ports_info": "network/ethernet/ports", "ontap_system_version": "cluster/software", "san_fc_logins_info": "network/fc/logins", "san_fc_wppn-aliases": "network/fc/wwpn-aliases", "san_fcp_services": "protocols/san/fcp/services", "san_iscsi_credentials": "protocols/san/iscsi/credentials", "san_iscsi_services": "protocols/san/iscsi/services", "san_lun_maps": "protocols/san/lun-maps", "storage_flexcaches_info": "storage/flexcache/flexcaches", "storage_flexcaches_origin_info": "storage/flexcache/origins", "storage_luns_info": "storage/luns", "storage_NVMe_namespaces": "storage/namespaces", "storage_ports_info": "storage/ports", "storage_qos_policies": "storage/qos/policies", "storage_qtrees_config": "storage/qtrees", "storage_quota_reports": "storage/quota/reports", "storage_quota_policy_rules": "storage/quota/rules", "storage_shelves_config": "storage/shelves", "storage_snapshot_policies": "storage/snapshot-policies", "support_ems_config": "support/ems", "support_ems_events": "support/ems/events", "support_ems_filters": "support/ems/filters", "svm_dns_config_info": "name-services/dns", "svm_ldap_config_info": "name-services/ldap", "svm_name_mapping_config_info": "name-services/name-mappings", "svm_nis_config_info": "name-services/nis", "svm_peers_info": "svm/peers", "svm_peer-permissions_info": "svm/peer-permissions", "vserver_info": "svm/svms", "volume_info": "storage/volumes" } # Add rest API names as there info version, also make sure we don't add a duplicate subsets = [] for subset in self.parameters['gather_subset']: if subset in info_to_rest_mapping: if info_to_rest_mapping[subset] not in subsets: subsets.append(info_to_rest_mapping[subset]) else: if subset not in subsets: subsets.append(subset) return subsets def apply(self): """ Perform pre-checks, call functions and exit """ result_message = dict() # Validating ONTAP version self.validate_ontap_version() # Defining gather_subset and appropriate api_call get_ontap_subset_info = { 'application/applications': { 'api_call': 'application/applications', }, 'application/templates': { 'api_call': 'application/templates', }, 'cloud/targets': { 'api_call': 'cloud/targets', }, 'cluster/chassis': { 'api_call': 'cluster/chassis', }, 'cluster/jobs': { 'api_call': 'cluster/jobs', }, 'cluster/metrocluster/diagnostics': { 'api_call': 'cluster/metrocluster/diagnostics', 'post': True }, 'cluster/metrics': { 'api_call': 'cluster/metrics', }, 'cluster/nodes': { 'api_call': 'cluster/nodes', }, 'cluster/peers': { 'api_call': 'cluster/peers', }, 'cluster/schedules': { 'api_call': 'cluster/schedules', }, 'cluster/software': { 'api_call': 'cluster/software', }, 'cluster/software/history': { 'api_call': 'cluster/software/history', }, 'cluster/software/packages': { 'api_call': 'cluster/software/packages', }, 'name-services/dns': { 'api_call': 'name-services/dns', }, 'name-services/ldap': { 'api_call': 'name-services/ldap', }, 'name-services/name-mappings': { 'api_call': 'name-services/name-mappings', }, 'name-services/nis': { 'api_call': 'name-services/nis', }, 'network/ethernet/broadcast-domains': { 'api_call': 'network/ethernet/broadcast-domains', }, 'network/ethernet/ports': { 'api_call': 'network/ethernet/ports', }, 'network/fc/logins': { 'api_call': 'network/fc/logins', }, 'network/fc/wwpn-aliases': { 'api_call': 'network/fc/wwpn-aliases', }, 'network/ip/interfaces': { 'api_call': 'network/ip/interfaces', }, 'network/ip/routes': { 'api_call': 'network/ip/routes', }, 'network/ip/service-policies': { 'api_call': 'network/ip/service-policies', }, 'network/ipspaces': { 'api_call': 'network/ipspaces', }, 'protocols/cifs/services': { 'api_call': 'protocols/cifs/services', }, 'protocols/cifs/shares': { 'api_call': 'protocols/cifs/shares', }, 'protocols/san/fcp/services': { 'api_call': 'protocols/san/fcp/services', }, 'protocols/san/igroups': { 'api_call': 'protocols/san/igroups', }, 'protocols/san/iscsi/credentials': { 'api_call': 'protocols/san/iscsi/credentials', }, 'protocols/san/iscsi/services': { 'api_call': 'protocols/san/iscsi/services', }, 'protocols/san/lun-maps': { 'api_call': 'protocols/san/lun-maps', }, 'storage/aggregates': { 'api_call': 'storage/aggregates', }, 'storage/disks': { 'api_call': 'storage/disks', }, 'storage/flexcache/flexcaches': { 'api_call': 'storage/flexcache/flexcaches', }, 'storage/flexcache/origins': { 'api_call': 'storage/flexcache/origins', }, 'storage/luns': { 'api_call': 'storage/luns', }, 'storage/namespaces': { 'api_call': 'storage/namespaces', }, 'storage/ports': { 'api_call': 'storage/ports', }, 'storage/qos/policies': { 'api_call': 'storage/qos/policies', }, 'storage/qtrees': { 'api_call': 'storage/qtrees', }, 'storage/quota/reports': { 'api_call': 'storage/quota/reports', }, 'storage/quota/rules': { 'api_call': 'storage/quota/rules', }, 'storage/shelves': { 'api_call': 'storage/shelves', }, 'storage/snapshot-policies': { 'api_call': 'storage/snapshot-policies', }, 'storage/volumes': { 'api_call': 'storage/volumes', }, 'support/autosupport': { 'api_call': 'support/autosupport', }, 'support/autosupport/messages': { 'api_call': 'support/autosupport/messages', }, 'support/ems': { 'api_call': 'support/ems', }, 'support/ems/events': { 'api_call': 'support/ems/events', }, 'support/ems/filters': { 'api_call': 'support/ems/filters', }, 'svm/peers': { 'api_call': 'svm/peers', }, 'svm/peer-permissions': { 'api_call': 'svm/peer-permissions', }, 'svm/svms': { 'api_call': 'svm/svms', } } if 'all' in self.parameters['gather_subset']: # If all in subset list, get the information of all subsets self.parameters['gather_subset'] = sorted( get_ontap_subset_info.keys()) length_of_subsets = len(self.parameters['gather_subset']) if self.parameters.get('fields') is not None: # If multiple fields specified to return, convert list to string self.fields = ','.join(self.parameters.get('fields')) if self.fields != '*' and length_of_subsets > 1: # Restrict gather subsets to one subset if fields section is list_of_fields self.module.fail_json( msg="Error: fields: %s, only one subset will be allowed." % self.parameters.get('fields')) converted_subsets = self.convert_subsets() for subset in converted_subsets: try: # Verify whether the supported subset passed specified_subset = get_ontap_subset_info[subset] except KeyError: self.module.fail_json( msg= "Specified subset %s is not found, supported subsets are %s" % (subset, list(get_ontap_subset_info.keys()))) result_message[subset] = self.get_subset_info(specified_subset) if result_message[subset] is not None: if isinstance(result_message[subset], dict): while result_message[subset]['_links'].get('next'): # Get all the set of records if next link found in subset_info for the specified subset next_api = result_message[subset]['_links']['next'][ 'href'] gathered_subset_info = self.get_next_records( next_api.replace('/api', '')) # Update the subset info for the specified subset result_message[subset][ '_links'] = gathered_subset_info['_links'] result_message[subset]['records'].extend( gathered_subset_info['records']) # metrocluster doesn't have a records field, so we need to skip this if result_message[subset].get('records') is not None: # Getting total number of records result_message[subset]['num_records'] = len( result_message[subset]['records']) self.module.exit_json(changed='False', state=self.parameters['state'], ontap_info=result_message)
class NetAppOntapSnapMirrorPolicy(object): """ Create, Modifies and Destroys a SnapMirror policy """ def __init__(self): """ Initialize the Ontap SnapMirror policy class """ self.use_rest = False 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'), policy_name=dict(required=True, type='str'), comment=dict(required=False, type='str'), policy_type=dict(required=False, type='str', choices=[ 'vault', 'async_mirror', 'mirror_vault', 'strict_sync_mirror', 'sync_mirror' ]), tries=dict(required=False, type='str'), transfer_priority=dict(required=False, type='str', choices=['low', 'normal']), common_snapshot_schedule=dict(required=False, type='str'), ignore_atime=dict(required=False, type='bool'), is_network_compression_enabled=dict(required=False, type='bool'), owner=dict(required=False, type='str', choices=['cluster_admin', 'vserver_admin']), restart=dict(required=False, type='str', choices=['always', 'never', 'default']), snapmirror_label=dict(required=False, type="list", elements="str"), keep=dict(required=False, type="list", elements="int"), prefix=dict(required=False, type="list", elements="str"), schedule=dict(required=False, type="list", elements="str"), )) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) # set up variables self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) # API should be used for ONTAP 9.6 or higher, Zapi for lower version self.restApi = OntapRestAPI(self.module) # some attributes are not supported in earlier REST implementation unsupported_rest_properties = [ 'owner', 'restart', 'transfer_priority', 'tries', 'ignore_atime', 'common_snapshot_schedule' ] 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: 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']) def get_snapmirror_policy(self): if self.use_rest: data = { 'fields': 'uuid,name,svm.name,comment,network_compression_enabled,type,retention', 'name': self.parameters['policy_name'], 'svm.name': self.parameters['vserver'] } api = "snapmirror/policies" message, error = self.restApi.get(api, data) if error: self.module.fail_json(msg=error) if len(message['records']) != 0: return_value = { 'uuid': message['records'][0]['uuid'], 'vserver': message['records'][0]['svm']['name'], 'policy_name': message['records'][0]['name'], 'comment': '', 'is_network_compression_enabled': message['records'][0]['network_compression_enabled'], 'snapmirror_label': list(), 'keep': list(), 'prefix': list(), 'schedule': list() } if 'type' in message['records'][0]: policy_type = message['records'][0]['type'] if policy_type == 'async': policy_type = 'async_mirror' elif policy_type == 'sync': policy_type = 'sync_mirror' return_value['policy_type'] = policy_type if 'comment' in message['records'][0]: return_value['comment'] = message['records'][0]['comment'] if 'retention' in message['records'][0]: for rule in message['records'][0]['retention']: return_value['snapmirror_label'].append(rule['label']) return_value['keep'].append(int(rule['count'])) if rule['prefix'] == '-': return_value['prefix'].append('') else: return_value['prefix'].append(rule['prefix']) if rule['creation_schedule']['name'] == '-': return_value['schedule'].append('') else: return_value['schedule'].append( rule['creation_schedule']['name']) return return_value return None else: return_value = None snapmirror_policy_get_iter = netapp_utils.zapi.NaElement( 'snapmirror-policy-get-iter') snapmirror_policy_info = netapp_utils.zapi.NaElement( 'snapmirror-policy-info') snapmirror_policy_info.add_new_child( 'policy-name', self.parameters['policy_name']) snapmirror_policy_info.add_new_child('vserver', self.parameters['vserver']) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(snapmirror_policy_info) snapmirror_policy_get_iter.add_child_elem(query) try: result = self.server.invoke_successfully( snapmirror_policy_get_iter, True) if result.get_child_by_name('attributes-list'): snapmirror_policy_attributes = result['attributes-list'][ 'snapmirror-policy-info'] return_value = { 'policy_name': snapmirror_policy_attributes['policy-name'], 'tries': snapmirror_policy_attributes['tries'], 'transfer_priority': snapmirror_policy_attributes['transfer-priority'], 'is_network_compression_enabled': self.na_helper.get_value_for_bool( True, snapmirror_policy_attributes[ 'is-network-compression-enabled']), 'restart': snapmirror_policy_attributes['restart'], 'ignore_atime': self.na_helper.get_value_for_bool( True, snapmirror_policy_attributes['ignore-atime']), 'vserver': snapmirror_policy_attributes['vserver-name'], 'comment': '', 'snapmirror_label': list(), 'keep': list(), 'prefix': list(), 'schedule': list() } if snapmirror_policy_attributes.get_child_content( 'comment') is not None: return_value['comment'] = snapmirror_policy_attributes[ 'comment'] if snapmirror_policy_attributes.get_child_content( 'type') is not None: return_value[ 'policy_type'] = snapmirror_policy_attributes[ 'type'] if snapmirror_policy_attributes.get_child_by_name( 'snapmirror-policy-rules'): for rule in snapmirror_policy_attributes[ 'snapmirror-policy-rules'].get_children(): # Ignore builtin rules if rule.get_child_content('snapmirror-label') == "sm_created" or \ rule.get_child_content('snapmirror-label') == "all_source_snapshots": continue return_value['snapmirror_label'].append( rule.get_child_content('snapmirror-label')) return_value['keep'].append( int(rule.get_child_content('keep'))) prefix = rule.get_child_content('prefix') if prefix is None or prefix == '-': prefix = '' return_value['prefix'].append(prefix) schedule = rule.get_child_content('schedule') if schedule is None or schedule == '-': schedule = '' return_value['schedule'].append(schedule) except netapp_utils.zapi.NaApiError as error: if 'NetApp API failed. Reason - 13001:' in to_native(error): # Policy does not exist pass else: self.module.fail_json( msg='Error getting snapmirror policy %s: %s' % (self.parameters['policy_name'], to_native(error)), exception=traceback.format_exc()) return return_value def validate_parameters(self): """ Validate snapmirror policy rules :return: None """ # For snapmirror policy rules, 'snapmirror_label' is required. if 'snapmirror_label' in self.parameters: # Check size of 'snapmirror_label' list is 0-10. Can have zero rules. # Take builtin 'sm_created' rule into account for 'mirror_vault'. if (('policy_type' in self.parameters and self.parameters['policy_type'] == 'mirror_vault' and len(self.parameters['snapmirror_label']) > 9) or len(self.parameters['snapmirror_label']) > 10): self.module.fail_json( msg="Error: A SnapMirror Policy can have up to a maximum of " "10 rules (including builtin rules), with a 'keep' value " "representing the maximum number of Snapshot copies for each rule" ) # 'keep' must be supplied as long as there is at least one snapmirror_label if len(self.parameters['snapmirror_label'] ) > 0 and 'keep' not in self.parameters: self.module.fail_json( msg="Error: Missing 'keep' parameter. When specifying the " "'snapmirror_label' parameter, the 'keep' parameter must " "also be supplied") # Make sure other rule values match same number of 'snapmirror_label' values. for rule_parameter in ['keep', 'prefix', 'schedule']: if rule_parameter in self.parameters: if len(self.parameters['snapmirror_label']) > len( self.parameters[rule_parameter]): self.module.fail_json( msg="Error: Each 'snapmirror_label' value must have " "an accompanying '%s' value" % rule_parameter) if len(self.parameters[rule_parameter]) > len( self.parameters['snapmirror_label']): self.module.fail_json( msg= "Error: Each '%s' value must have an accompanying " "'snapmirror_label' value" % rule_parameter) else: # 'snapmirror_label' not supplied. # Bail out if other rule parameters have been supplied. for rule_parameter in ['keep', 'prefix', 'schedule']: if rule_parameter in self.parameters: self.module.fail_json( msg="Error: Missing 'snapmirror_label' parameter. When " "specifying the '%s' parameter, the 'snapmirror_label' " "parameter must also be supplied" % rule_parameter) # Schedule must be supplied if prefix is supplied. if 'prefix' in self.parameters and 'schedule' not in self.parameters: self.module.fail_json( msg="Error: Missing 'schedule' parameter. When " "specifying the 'prefix' parameter, the 'schedule' " "parameter must also be supplied") def create_snapmirror_policy(self): """ Creates a new storage efficiency policy """ self.validate_parameters() if self.use_rest: data = { 'name': self.parameters['policy_name'], 'svm': { 'name': self.parameters['vserver'] } } if 'policy_type' in self.parameters.keys(): if 'async_mirror' in self.parameters['policy_type']: data['type'] = 'async' elif 'sync_mirror' in self.parameters['policy_type']: data['type'] = 'sync' data['sync_type'] = 'sync' else: self.module.fail_json( msg= 'policy type in REST only supports options async_mirror or sync_mirror, given %s' % (self.parameters['policy_type'])) data = self.create_snapmirror_policy_obj_for_rest( data, data['type']) else: data = self.create_snapmirror_policy_obj_for_rest(data) api = "snapmirror/policies" response, error = self.restApi.post(api, data) if error: self.module.fail_json(msg=error) if 'job' in response: self.restApi.wait_on_job(response['job'], increment=5) else: snapmirror_policy_obj = netapp_utils.zapi.NaElement( "snapmirror-policy-create") snapmirror_policy_obj.add_new_child("policy-name", self.parameters['policy_name']) if 'policy_type' in self.parameters.keys(): snapmirror_policy_obj.add_new_child( "type", self.parameters['policy_type']) snapmirror_policy_obj = self.create_snapmirror_policy_obj( snapmirror_policy_obj) try: self.server.invoke_successfully(snapmirror_policy_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error creating snapmirror policy %s: %s' % (self.parameters['policy_name'], to_native(error)), exception=traceback.format_exc()) def create_snapmirror_policy_obj(self, snapmirror_policy_obj): if 'comment' in self.parameters.keys(): snapmirror_policy_obj.add_new_child("comment", self.parameters['comment']) if 'common_snapshot_schedule' in self.parameters.keys( ) and 'sync_mirror' in self.parameters['policy_type']: snapmirror_policy_obj.add_new_child( "common-snapshot-schedule", self.parameters['common_snapshot_schedule']) if 'ignore_atime' in self.parameters.keys(): snapmirror_policy_obj.add_new_child( "ignore-atime", self.na_helper.get_value_for_bool( False, self.parameters['ignore_atime'])) if 'is_network_compression_enabled' in self.parameters.keys(): snapmirror_policy_obj.add_new_child( "is-network-compression-enabled", self.na_helper.get_value_for_bool( False, self.parameters['is_network_compression_enabled'])) if 'owner' in self.parameters.keys(): snapmirror_policy_obj.add_new_child("owner", self.parameters['owner']) if 'restart' in self.parameters.keys(): snapmirror_policy_obj.add_new_child("restart", self.parameters['restart']) if 'transfer_priority' in self.parameters.keys(): snapmirror_policy_obj.add_new_child( "transfer-priority", self.parameters['transfer_priority']) if 'tries' in self.parameters.keys(): snapmirror_policy_obj.add_new_child("tries", self.parameters['tries']) return snapmirror_policy_obj def create_snapmirror_policy_obj_for_rest(self, snapmirror_policy_obj, policy_type=None): if 'comment' in self.parameters.keys(): snapmirror_policy_obj["comment"] = self.parameters['comment'] if 'is_network_compression_enabled' in self.parameters: if policy_type == 'async': snapmirror_policy_obj[ "network_compression_enabled"] = self.parameters[ 'is_network_compression_enabled'] elif policy_type == 'sync': self.module.fail_json( msg= "Input parameter network_compression_enabled is not valid for SnapMirror policy type sync" ) return snapmirror_policy_obj def create_snapmirror_policy_retention_obj_for_rest(self, rules=None): """ Create SnapMirror policy retention REST object. :param list rules: e.g. [{'snapmirror_label': 'daily', 'keep': 7, 'prefix': 'daily', 'schedule': 'daily'}, ... ] :return: List of retention REST objects. e.g. [{'label': 'daily', 'count': 7, 'prefix': 'daily', 'creation_schedule': {'name': 'daily'}}, ... ] """ snapmirror_policy_retention_objs = list() if rules is not None: for rule in rules: retention = { 'label': rule['snapmirror_label'], 'count': str(rule['keep']) } if 'prefix' in rule and rule['prefix'] != '': retention['prefix'] = rule['prefix'] if 'schedule' in rule and rule['schedule'] != '': retention['creation_schedule'] = {'name': rule['schedule']} snapmirror_policy_retention_objs.append(retention) return snapmirror_policy_retention_objs def delete_snapmirror_policy(self, uuid=None): """ Deletes a snapmirror policy """ if self.use_rest: api = "snapmirror/policies" data = {'uuid': uuid} dummy, error = self.restApi.delete(api, data) if error: self.module.fail_json(msg=error) else: snapmirror_policy_obj = netapp_utils.zapi.NaElement( "snapmirror-policy-delete") snapmirror_policy_obj.add_new_child("policy-name", self.parameters['policy_name']) try: self.server.invoke_successfully(snapmirror_policy_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error deleting snapmirror policy %s: %s' % (self.parameters['policy_name'], to_native(error)), exception=traceback.format_exc()) def modify_snapmirror_policy(self, uuid=None, policy_type=None): """ Modifies a snapmirror policy """ if self.use_rest: api = "snapmirror/policies/" + uuid data = self.create_snapmirror_policy_obj_for_rest( dict(), policy_type) dummy, error = self.restApi.patch(api, data) if error: self.module.fail_json(msg=error) else: snapmirror_policy_obj = netapp_utils.zapi.NaElement( "snapmirror-policy-modify") snapmirror_policy_obj = self.create_snapmirror_policy_obj( snapmirror_policy_obj) # Only modify snapmirror policy if a specific snapmirror policy attribute needs # modifying. It may be that only snapmirror policy rules are being modified. if snapmirror_policy_obj.get_children(): snapmirror_policy_obj.add_new_child( "policy-name", self.parameters['policy_name']) try: self.server.invoke_successfully(snapmirror_policy_obj, True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error modifying snapmirror policy %s: %s' % (self.parameters['policy_name'], to_native(error)), exception=traceback.format_exc()) def identify_new_snapmirror_policy_rules(self, current=None): """ Identify new rules that should be added. :return: List of new rules to be added e.g. [{'snapmirror_label': 'daily', 'keep': 7, 'prefix': '', 'schedule': ''}, ... ] """ new_rules = list() if 'snapmirror_label' in self.parameters: for snapmirror_label in self.parameters['snapmirror_label']: snapmirror_label = snapmirror_label.strip() # Construct new rule. prefix and schedule are optional. snapmirror_label_index = self.parameters[ 'snapmirror_label'].index(snapmirror_label) rule = dict({ 'snapmirror_label': snapmirror_label, 'keep': self.parameters['keep'][snapmirror_label_index] }) if 'prefix' in self.parameters: rule['prefix'] = self.parameters['prefix'][ snapmirror_label_index] else: rule['prefix'] = '' if 'schedule' in self.parameters: rule['schedule'] = self.parameters['schedule'][ snapmirror_label_index] else: rule['schedule'] = '' if current is not None and 'snapmirror_label' in current: if snapmirror_label not in current['snapmirror_label']: # Rule doesn't exist. Add new rule. new_rules.append(rule) else: # No current or any rules. Add new rule. new_rules.append(rule) return new_rules def identify_obsolete_snapmirror_policy_rules(self, current=None): """ Identify existing rules that should be deleted :return: List of rules to be deleted e.g. [{'snapmirror_label': 'daily', 'keep': 7, 'prefix': '', 'schedule': ''}, ... ] """ obsolete_rules = list() if 'snapmirror_label' in self.parameters: if current is not None and 'snapmirror_label' in current: # Iterate existing rules. for snapmirror_label in current['snapmirror_label']: snapmirror_label = snapmirror_label.strip() if snapmirror_label not in [ item.strip() for item in self.parameters['snapmirror_label'] ]: # Existing rule isn't in parameters. Delete existing rule. current_snapmirror_label_index = current[ 'snapmirror_label'].index(snapmirror_label) rule = dict({ 'snapmirror_label': snapmirror_label, 'keep': current['keep'][current_snapmirror_label_index], 'prefix': current['prefix'][current_snapmirror_label_index], 'schedule': current['schedule'][current_snapmirror_label_index] }) obsolete_rules.append(rule) return obsolete_rules def identify_modified_snapmirror_policy_rules(self, current=None): """ Identify self.parameters rules that will be modified or not. :return: List of 'modified' rules and a list of 'unmodified' rules e.g. [{'snapmirror_label': 'daily', 'keep': 7, 'prefix': '', 'schedule': ''}, ... ] """ modified_rules = list() unmodified_rules = list() if 'snapmirror_label' in self.parameters: for snapmirror_label in self.parameters['snapmirror_label']: snapmirror_label = snapmirror_label.strip() if current is not None and 'snapmirror_label' in current: if snapmirror_label in current['snapmirror_label']: # Rule exists. Identify whether it requires modification or not. modified = False rule = dict() rule['snapmirror_label'] = snapmirror_label # Get indexes of current and supplied rule. current_snapmirror_label_index = current[ 'snapmirror_label'].index(snapmirror_label) snapmirror_label_index = self.parameters[ 'snapmirror_label'].index(snapmirror_label) # Check if keep modified if self.parameters['keep'][ snapmirror_label_index] != current['keep'][ current_snapmirror_label_index]: modified = True rule['keep'] = self.parameters['keep'][ snapmirror_label_index] else: rule['keep'] = current['keep'][ current_snapmirror_label_index] # Check if prefix modified if 'prefix' in self.parameters: if self.parameters['prefix'][ snapmirror_label_index] != current[ 'prefix'][ current_snapmirror_label_index]: modified = True rule['prefix'] = self.parameters['prefix'][ snapmirror_label_index] else: rule['prefix'] = current['prefix'][ current_snapmirror_label_index] else: rule['prefix'] = current['prefix'][ current_snapmirror_label_index] # Check if schedule modified if 'schedule' in self.parameters: if self.parameters['schedule'][ snapmirror_label_index] != current[ 'schedule'][ current_snapmirror_label_index]: modified = True rule['schedule'] = self.parameters['schedule'][ snapmirror_label_index] else: rule['schedule'] = current['schedule'][ current_snapmirror_label_index] else: rule['schedule'] = current['schedule'][ current_snapmirror_label_index] if modified: modified_rules.append(rule) else: unmodified_rules.append(rule) return modified_rules, unmodified_rules def identify_snapmirror_policy_rules_with_schedule(self, rules=None): """ Identify rules that are using a schedule or not. At least one non-schedule rule must be added to a policy before schedule rules are added. :return: List of rules with schedules and a list of rules without schedules e.g. [{'snapmirror_label': 'daily', 'keep': 7, 'prefix': 'daily', 'schedule': 'daily'}, ... ], [{'snapmirror_label': 'weekly', 'keep': 5, 'prefix': '', 'schedule': ''}, ... ] """ schedule_rules = list() non_schedule_rules = list() if rules is not None: for rule in rules: if 'schedule' in rule: schedule_rules.append(rule) else: non_schedule_rules.append(rule) return schedule_rules, non_schedule_rules def modify_snapmirror_policy_rules(self, current=None, uuid=None): """ Modify existing rules in snapmirror policy :return: None """ self.validate_parameters() # Need 'snapmirror_label' to add/modify/delete rules if 'snapmirror_label' not in self.parameters: return obsolete_rules = self.identify_obsolete_snapmirror_policy_rules( current) new_rules = self.identify_new_snapmirror_policy_rules(current) modified_rules, unmodified_rules = self.identify_modified_snapmirror_policy_rules( current) if self.use_rest: api = "snapmirror/policies/" + uuid data = {'retention': list()} # As rule 'prefix' can't be unset, have to delete existing rules first. # Builtin rules remain. dummy, error = self.restApi.patch(api, data) if error: self.module.fail_json(msg=error) # Re-add desired rules. rules = unmodified_rules + modified_rules + new_rules data[ 'retention'] = self.create_snapmirror_policy_retention_obj_for_rest( rules) if len(data['retention']) > 0: dummy, error = self.restApi.patch(api, data) if error: self.module.fail_json(msg=error) else: delete_rules = obsolete_rules + modified_rules add_schedule_rules, add_non_schedule_rules = self.identify_snapmirror_policy_rules_with_schedule( new_rules + modified_rules) # Delete rules no longer required or modified rules that will be re-added. for rule in delete_rules: options = { 'policy-name': self.parameters['policy_name'], 'snapmirror-label': rule['snapmirror_label'] } self.modify_snapmirror_policy_rule( options, 'snapmirror-policy-remove-rule') # Add rules. At least one non-schedule rule must exist before # a rule with a schedule can be added, otherwise zapi will complain. for rule in add_non_schedule_rules + add_schedule_rules: options = { 'policy-name': self.parameters['policy_name'], 'snapmirror-label': rule['snapmirror_label'], 'keep': str(rule['keep']) } if 'prefix' in rule and rule['prefix'] != '': options['prefix'] = rule['prefix'] if 'schedule' in rule and rule['schedule'] != '': options['schedule'] = rule['schedule'] self.modify_snapmirror_policy_rule( options, 'snapmirror-policy-add-rule') def modify_snapmirror_policy_rule(self, options, zapi): """ Add, modify or remove a rule to/from a snapmirror policy """ snapmirror_obj = netapp_utils.zapi.NaElement.create_node_with_children( zapi, **options) try: self.server.invoke_successfully(snapmirror_obj, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json( msg='Error modifying snapmirror policy rule %s: %s' % (self.parameters['policy_name'], to_native(error)), exception=traceback.format_exc()) def asup_log_for_cserver(self): 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_snapmirror_policy", cserver) def apply(self): uuid = None if not self.use_rest: self.asup_log_for_cserver() current, modify = self.get_snapmirror_policy(), None cd_action = self.na_helper.get_cd_action(current, self.parameters) if current and 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 cd_action == 'create': self.create_snapmirror_policy() if self.use_rest: current = self.get_snapmirror_policy() uuid = current['uuid'] self.modify_snapmirror_policy_rules(current, uuid) else: self.modify_snapmirror_policy_rules(current) elif cd_action == 'delete': if self.use_rest: uuid = current['uuid'] self.delete_snapmirror_policy(uuid) elif modify: if self.use_rest: uuid = current['uuid'] self.modify_snapmirror_policy(uuid, current['policy_type']) self.modify_snapmirror_policy_rules(current, uuid) else: self.modify_snapmirror_policy() self.modify_snapmirror_policy_rules(current) self.module.exit_json(changed=self.na_helper.changed)