class NetAppOntapSecurityCertificates(object): ''' object initialize and class methods ''' def __init__(self): self.use_rest = False self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( common_name=dict(required=False, type='str'), name=dict(required=False, type='str'), state=dict(required=False, choices=['present', 'absent'], default='present'), type=dict(required=False, choices=['client', 'server', 'client_ca', 'server_ca', 'root_ca']), svm=dict(required=False, type='str', aliases=['vserver']), public_certificate=dict(required=False, type='str'), private_key=dict(required=False, type='str'), signing_request=dict(required=False, type='str'), expiry_time=dict(required=False, type='str'), key_size=dict(required=False, type='int'), hash_function=dict(required=False, type='str'), intermediate_certificates=dict(required=False, type='list', elements='str'), ignore_name_if_not_supported=dict(required=False, type='bool', default=True) )) 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.parameters.get('name') is None: if self.parameters.get('common_name') is None or self.parameters.get('type') is None: error = "'name' or ('common_name' and 'type') are required parameters." self.module.fail_json(msg=error) # ONTAP 9.6 and 9.7 do not support name. We'll change this to True if we detect an issue. self.ignore_name_param = False # API should be used for ONTAP 9.6 or higher self.rest_api = OntapRestAPI(self.module) if self.rest_api.is_rest(): self.use_rest = True else: self.module.fail_json(msg=self.rest_api.requires_ontap_9_6('na_ontap_security_certificates')) def get_certificate(self): """ Fetch uuid if certificate exists. NOTE: because of a bug in ONTAP 9.6 and 9.7, name is not supported. We are falling back to using common_name and type, but unicity is not guaranteed. :return: Dictionary if certificate with same name is found None if not found """ error = "'name' or ('common_name', 'type') are required." for key in ('name', 'common_name'): if self.parameters.get(key) is None: continue data = {'fields': 'uuid', key: self.parameters[key], } if self.parameters.get('svm') is not None: data['svm.name'] = self.parameters['svm'] else: data['scope'] = 'cluster' if key == 'common_name': if self.parameters.get('type') is not None: data['type'] = self.parameters['type'] else: error = "When using 'common_name', 'type' is required." break api = "security/certificates" message, error = self.rest_api.get(api, data) if error: try: name_not_supported_error = (key == 'name') and (error['message'] == 'Unexpected argument "name".') except (KeyError, TypeError): name_not_supported_error = False if name_not_supported_error: if self.parameters['ignore_name_if_not_supported'] and self.parameters.get('common_name') is not None: # let's attempt a retry using common_name self.ignore_name_param = True continue error = "ONTAP 9.6 and 9.7 do not support 'name'. Use 'common_name' and 'type' as a work-around." # report success, or any other error as is break if error: self.module.fail_json(msg='Error calling API: %s - %s' % (api, error)) if len(message['records']) == 1: return message['records'][0] if len(message['records']) > 1: error = 'Duplicate records with same common_name are preventing safe operations: %s' % repr(message) self.module.fail_json(msg=error) return None def create_or_install_certificate(self): """ Create or install certificate :return: message (should be empty dict) """ required_keys = ['type', 'common_name'] optional_keys = ['public_certificate', 'private_key', 'expiry_time', 'key_size', 'hash_function'] if not self.ignore_name_param: optional_keys.append('name') # special key: svm if not set(required_keys).issubset(set(self.parameters.keys())): self.module.fail_json(msg='Error creating or installing certificate: one or more of the following options are missing: %s' % (', '.join(required_keys))) data = dict() if self.parameters.get('svm') is not None: data['svm'] = {'name': self.parameters['svm']} for key in required_keys + optional_keys: if self.parameters.get(key) is not None: data[key] = self.parameters[key] api = "security/certificates" message, error = self.rest_api.post(api, data) if error: if self.parameters.get('svm') is None and error.get('target') == 'uuid': error['target'] = 'cluster' if error.get('message') == 'duplicate entry': error['message'] += '. Same certificate may already exist under a different name.' self.module.fail_json(msg="Error creating or installing certificate: %s" % error) return message def sign_certificate(self, uuid): """ sign certificate :return: a dictionary with key "public_certificate" """ api = "security/certificates/%s/sign" % uuid data = {'signing_request': self.parameters['signing_request']} optional_keys = ['expiry_time', 'hash_function'] for key in optional_keys: if self.parameters.get(key) is not None: data[key] = self.parameters[key] message, error = self.rest_api.post(api, data) if error: self.module.fail_json(msg="Error signing certificate: %s" % error) return message def delete_certificate(self, uuid): """ Delete certificate :return: message (should be empty dict) """ api = "security/certificates/%s" % uuid message, error = self.rest_api.delete(api) if error: self.module.fail_json(msg="Error deleting certificate: %s" % error) return message def apply(self): """ Apply action to create/install/sign/delete certificate :return: None """ # TODO: add telemetry for REST current = self.get_certificate() cd_action = self.na_helper.get_cd_action(current, self.parameters) message = None if self.parameters.get('signing_request') is not None: error = None if self.parameters['state'] == 'absent': error = "'signing_request' is not supported with 'state' set to 'absent'" elif current is None: scope = 'cluster' if self.parameters.get('svm') is None else "svm: %s" % self.parameters.get('svm') error = "signing certificate with name '%s' not found on %s" % (self.parameters.get('name'), scope) elif cd_action is not None: error = "'signing_request' is exclusive with other actions: create, install, delete" if error is not None: self.module.fail_json(msg=error) self.na_helper.changed = True if self.na_helper.changed: if self.module.check_mode: pass else: if cd_action == 'create': message = self.create_or_install_certificate() elif cd_action == 'delete': message = self.delete_certificate(current['uuid']) elif self.parameters.get('signing_request') is not None: message = self.sign_certificate(current['uuid']) results = {'changed': self.na_helper.changed} if message: results['ontap_info'] = message self.module.exit_json(**results)
class NetAppONTAPMetroCluster(object): ''' ONTAP metrocluster operations ''' 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.rest_api = OntapRestAPI(self.module) self.use_rest = self.rest_api.is_rest() if not self.use_rest: self.module.fail_json( msg=self.rest_api.requires_ontap_9_6('na_ontap_metrocluster')) def get_metrocluster(self): attrs = None api = 'cluster/metrocluster' options = {'fields': '*'} message, error = self.rest_api.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.rest_api.post(api, data, options) if error is not None: self.module.fail_json(msg="%s" % error) message, error = self.rest_api.wait_on_job(message['job'], self.parameters['hostname']) if error: self.module.fail_json(msg="%s" % error) 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 NetAppOntapMccipMediator(object): """ Mediator object for Add/Remove/Display """ 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'), mediator_address=dict(required=True, type='str'), mediator_user=dict(required=True, type='str'), mediator_password=dict(required=True, type='str', no_log=True), )) 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) self.use_rest = self.rest_api.is_rest() if not self.use_rest: self.module.fail_json( msg=self.rest_api.requires_ontap_9_6('na_ontap_mcc_mediator')) def add_mediator(self): """ Adds an ONTAP Mediator to MCC configuration """ api = 'cluster/mediators' params = { 'ip_address': self.parameters['mediator_address'], 'password': self.parameters['mediator_password'], 'user': self.parameters['mediator_user'] } dummy, error = self.rest_api.post(api, params) if error: self.module.fail_json(msg=error) def remove_mediator(self, current_uuid): """ Removes the ONTAP Mediator from MCC configuration """ api = 'cluster/mediators/%s' % current_uuid params = { 'ip_address': self.parameters['mediator_address'], 'password': self.parameters['mediator_password'], 'user': self.parameters['mediator_user'], 'uuid': current_uuid } dummy, error = self.rest_api.delete(api, params) if error: self.module.fail_json(msg=error) def get_mediator(self): """ Determine if the MCC configuration has added an ONTAP Mediator """ api = "cluster/mediators" message, error = self.rest_api.get(api, None) if error: self.module.fail_json(msg=error) if message['num_records'] > 0: return message['records'][0]['uuid'] return None def apply(self): """ Apply action to MCC Mediator """ current = self.get_mediator() 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.add_mediator() elif cd_action == 'delete': self.remove_mediator(current) self.module.exit_json(changed=self.na_helper.changed)
class NetAppOntapWwpnAlias(object): ''' ONTAP WWPN alias 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'), wwpn=dict(required=False, type='str'), vserver=dict(required=True, type='str'))) self.module = AnsibleModule(argument_spec=self.argument_spec, required_if=[('state', 'present', ['wwpn']) ], supports_check_mode=True) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) # REST API should be used for ONTAP 9.6 or higher. self.rest_api = OntapRestAPI(self.module) if self.rest_api.is_rest(): self.use_rest = True else: self.module.fail_json( msg=self.rest_api.requires_ontap_9_6('na_ontap_wwpn_alias')) def get_alias(self, uuid): params = { 'fields': 'alias,wwpn', 'alias': self.parameters['name'], 'svm.uuid': uuid } api = 'network/fc/wwpn-aliases' message, error = self.rest_api.get(api, params) if error is not None: self.module.fail_json(msg="Error on fetching wwpn alias: %s" % error) if message['num_records'] > 0: return { 'name': message['records'][0]['alias'], 'wwpn': message['records'][0]['wwpn'], } else: return None def create_alias(self, uuid, is_modify=False): params = { 'alias': self.parameters['name'], 'wwpn': self.parameters['wwpn'], 'svm.uuid': uuid } api = 'network/fc/wwpn-aliases' dummy, error = self.rest_api.post(api, params) if error is not None: if is_modify: self.module.fail_json( msg= "Error on modifying wwpn alias when trying to re-create alias: %s." % error) else: self.module.fail_json(msg="Error on creating wwpn alias: %s." % error) def delete_alias(self, uuid, is_modify=False): api = 'network/fc/wwpn-aliases/%s/%s' % (uuid, self.parameters['name']) dummy, error = self.rest_api.delete(api) if error is not None: if is_modify: self.module.fail_json( msg= "Error on modifying wwpn alias when trying to delete alias: %s." % error) else: self.module.fail_json(msg="Error on deleting wwpn alias: %s." % error) def get_svm_uuid(self): """ Get a svm's UUID :return: uuid of the svm. """ params = {'fields': 'uuid', 'name': self.parameters['vserver']} api = "svm/svms" message, error = self.rest_api.get(api, params) if error is not None: self.module.fail_json(msg="Error on fetching svm uuid: %s" % error) return message['records'][0]['uuid'] def apply(self): cd_action, uuid, modify = None, None, None uuid = self.get_svm_uuid() current = self.get_alias(uuid) 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 cd_action == 'create': self.create_alias(uuid) elif cd_action == 'delete': self.delete_alias(uuid) elif modify: self.delete_alias(uuid, is_modify=True) self.create_alias(uuid, is_modify=True) self.module.exit_json(changed=self.na_helper.changed)