class ElementSWAccessGroup(object): """ Element Software Volume Access Group """ def __init__(self): self.argument_spec = netapp_utils.ontap_sf_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'), name=dict(required=True, aliases=["src_access_group_id"], type='str'), initiators=dict(required=False, type='list', elements='str'), volumes=dict(required=False, type='list', elements='str'), account_id=dict(required=False, type='str'), virtual_network_id=dict(required=False, type='int'), virtual_network_tags=dict(required=False, type='list', elements='str'), attributes=dict(required=False, type='dict'), )) self.module = AnsibleModule(argument_spec=self.argument_spec, required_if=[('state', 'present', ['account_id'])], supports_check_mode=True) input_params = self.module.params # Set up state variables self.state = input_params['state'] self.from_name = input_params['from_name'] self.access_group_name = input_params['name'] self.initiators = input_params['initiators'] self.volumes = input_params['volumes'] self.account_id = input_params['account_id'] self.virtual_network_id = input_params['virtual_network_id'] self.virtual_network_tags = input_params['virtual_network_tags'] self.attributes = input_params['attributes'] if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the SolidFire Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes if self.attributes is not None: self.attributes.update( self.elementsw_helper.set_element_attributes( source='na_elementsw_access_group')) else: self.attributes = self.elementsw_helper.set_element_attributes( source='na_elementsw_access_group') def get_access_group(self, name): """ Get Access Group :description: Get Access Group object for a given name :return: object (Group object) :rtype: object (Group object) """ access_groups_list = self.sfe.list_volume_access_groups() group_obj = None for group in access_groups_list.volume_access_groups: # Check and get access_group object for a given name if str(group.volume_access_group_id) == name: group_obj = group elif group.name == name: group_obj = group return group_obj def get_account_id(self): # Validate account id # Return account_id if found, None otherwise try: account_id = self.elementsw_helper.account_exists(self.account_id) return account_id except solidfire.common.ApiServerError: return None def get_volume_ids(self): # Validate volume_ids # Return volume ids if found, fail if not found volume_ids = [] for volume in self.volumes: volume_id = self.elementsw_helper.volume_exists( volume, self.account_id) if volume_id: volume_ids.append(volume_id) else: self.module.fail_json( msg='Specified volume %s does not exist' % volume) return volume_ids def create_access_group(self): """ Create the Access Group """ try: self.sfe.create_volume_access_group( name=self.access_group_name, initiators=self.initiators, volumes=self.volumes, virtual_network_id=self.virtual_network_id, virtual_network_tags=self.virtual_network_tags, attributes=self.attributes) except Exception as e: self.module.fail_json( msg="Error creating volume access group %s: %s" % (self.access_group_name, to_native(e)), exception=traceback.format_exc()) def delete_access_group(self): """ Delete the Access Group """ try: self.sfe.delete_volume_access_group( volume_access_group_id=self.group_id) except Exception as e: self.module.fail_json( msg="Error deleting volume access group %s: %s" % (self.access_group_name, to_native(e)), exception=traceback.format_exc()) def update_access_group(self): """ Update the Access Group if the access_group already exists """ try: self.sfe.modify_volume_access_group( volume_access_group_id=self.group_id, virtual_network_id=self.virtual_network_id, virtual_network_tags=self.virtual_network_tags, initiators=self.initiators, volumes=self.volumes, attributes=self.attributes) except Exception as e: self.module.fail_json( msg="Error updating volume access group %s: %s" % (self.access_group_name, to_native(e)), exception=traceback.format_exc()) def rename_access_group(self): """ Rename the Access Group to the new name """ try: self.sfe.modify_volume_access_group( volume_access_group_id=self.from_group_id, virtual_network_id=self.virtual_network_id, virtual_network_tags=self.virtual_network_tags, name=self.access_group_name, initiators=self.initiators, volumes=self.volumes, attributes=self.attributes) except Exception as e: self.module.fail_json( msg="Error updating volume access group %s: %s" % (self.from_name, to_native(e)), exception=traceback.format_exc()) def apply(self): """ Process the access group operation on the Element Software Cluster """ changed = False action = None input_account_id = self.account_id if self.account_id is not None: self.account_id = self.get_account_id() if self.state == 'present' and self.volumes is not None: if self.account_id: self.volumes = self.get_volume_ids() else: self.module.fail_json( msg='Error: Specified account id "%s" does not exist.' % str(input_account_id)) group_detail = self.get_access_group(self.access_group_name) if group_detail is not None: # If access group found self.group_id = group_detail.volume_access_group_id if self.state == "absent": action = 'delete' changed = True else: # If state - present, check for any parameter of exising group needs modification. if self.volumes is not None and len(self.volumes) > 0: # Compare the volume list if not group_detail.volumes: # If access group does not have any volume attached action = 'update' changed = True else: for volumeID in group_detail.volumes: if volumeID not in self.volumes: action = 'update' changed = True break elif self.initiators is not None and group_detail.initiators != self.initiators: action = 'update' changed = True elif self.virtual_network_id is not None or self.virtual_network_tags is not None: action = 'update' changed = True else: # access_group does not exist if self.state == "present" and self.from_name is not None: group_detail = self.get_access_group(self.from_name) if group_detail is not None: # If resource pointed by from_name exists, rename the access_group to name self.from_group_id = group_detail.volume_access_group_id action = 'rename' changed = True else: # If resource pointed by from_name does not exists, error out self.module.fail_json(msg="Resource does not exist : %s" % self.from_name) elif self.state == "present": # If from_name is not defined, Create from scratch. action = 'create' changed = True if changed and not self.module.check_mode: if action == 'create': self.create_access_group() elif action == 'rename': self.rename_access_group() elif action == 'update': self.update_access_group() elif action == 'delete': self.delete_access_group() self.module.exit_json(changed=changed)
class ElementSWAccessGroupVolumes(object): """ Element Software Access Group Volumes """ def __init__(self): self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update( dict( state=dict(required=False, type='str', choices=['present', 'absent'], default='present'), access_group=dict(required=True, type='str'), volumes=dict(required=True, type='list', elements='str'), account_id=dict(required=True, type='str'), )) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) input_params = self.module.params # Set up state variables self.state = input_params['state'] self.access_group_name = input_params['access_group'] self.volumes = input_params['volumes'] self.account_id = input_params['account_id'] if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the SolidFire Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes self.attributes = self.elementsw_helper.set_element_attributes( source='na_elementsw_access_group') def get_access_group(self, name): """ Get Access Group :description: Get Access Group object for a given name :return: object (Group object) :rtype: object (Group object) """ access_groups_list = self.sfe.list_volume_access_groups() group_obj = None for group in access_groups_list.volume_access_groups: # Check and get access_group object for a given name if str(group.volume_access_group_id) == name: group_obj = group elif group.name == name: group_obj = group return group_obj def get_account_id(self): # Validate account id # Return account_id if found, None otherwise try: account_id = self.elementsw_helper.account_exists(self.account_id) return account_id except solidfire.common.ApiServerError: return None def get_volume_ids(self): # Validate volume_ids # Return volume ids if found, fail if not found volume_ids = [] for volume in self.volumes: volume_id = self.elementsw_helper.volume_exists( volume, self.account_id) if volume_id: volume_ids.append(volume_id) else: self.module.fail_json( msg='Error: Specified volume %s does not exist' % volume) return volume_ids def update_access_group(self, volumes): """ Update the Access Group if the access_group already exists """ try: self.sfe.modify_volume_access_group( volume_access_group_id=self.group_id, volumes=volumes) except Exception as e: self.module.fail_json( msg="Error updating volume access group %s: %s" % (self.access_group_name, to_native(e)), exception=traceback.format_exc()) def apply(self): """ Process the volume add/remove operations for the access group on the Element Software Cluster """ changed = False input_account_id = self.account_id if self.account_id is not None: self.account_id = self.get_account_id() if self.account_id is None: self.module.fail_json( msg='Error: Specified account id "%s" does not exist.' % str(input_account_id)) # get volume data self.volume_ids = self.get_volume_ids() group_detail = self.get_access_group(self.access_group_name) if group_detail is None: self.module.fail_json( msg= 'Error: Specified access group "%s" does not exist for account id: %s.' % (self.access_group_name, str(input_account_id))) self.group_id = group_detail.volume_access_group_id volumes = group_detail.volumes # compare expected list of volumes to existing one if self.state == "absent": # remove volumes if present in access group volumes = [ vol for vol in group_detail.volumes if vol not in self.volume_ids ] else: # add volumes if not already present volumes = [ vol for vol in self.volume_ids if vol not in group_detail.volumes ] volumes.extend(group_detail.volumes) # update if there is a change if len(volumes) != len(group_detail.volumes): if not self.module.check_mode: self.update_access_group(volumes) changed = True self.module.exit_json(changed=changed)
class NetAppElementSWAdminUser(object): """ Class to set, modify and delete admin users on ElementSW box """ def __init__(self): """ Initialize the NetAppElementSWAdminUser class. """ self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update( dict(state=dict(required=True, choices=['present', 'absent']), element_username=dict(required=True, type='str'), element_password=dict(required=False, type='str', no_log=True), acceptEula=dict(required=False, type='bool'), access=dict(required=False, type='list'))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) param = self.module.params # set up state variables self.state = param['state'] self.element_username = param['element_username'] self.element_password = param['element_password'] self.acceptEula = param['acceptEula'] self.access = param['access'] if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the SolidFire Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes self.attributes = self.elementsw_helper.set_element_attributes( source='na_elementsw_admin_users') def does_admin_user_exist(self): """ Checks to see if an admin user exists or not :return: True if the user exist, False if it dose not exist """ admins_list = self.sfe.list_cluster_admins() for admin in admins_list.cluster_admins: if admin.username == self.element_username: return True return False def get_admin_user(self): """ Get the admin user object :return: the admin user object """ admins_list = self.sfe.list_cluster_admins() for admin in admins_list.cluster_admins: if admin.username == self.element_username: return admin return None def modify_admin_user(self): """ Modify a admin user. If a password is set the user will be modified as there is no way to compare a new password with an existing one :return: if a user was modified or not """ changed = False admin_user = self.get_admin_user() if self.access is not None and len(self.access) > 0: for access in self.access: if access not in admin_user.access: changed = True if changed: self.sfe.modify_cluster_admin( cluster_admin_id=admin_user.cluster_admin_id, access=self.access, password=self.element_password, attributes=self.attributes) return changed def add_admin_user(self): """ Add's a new admin user to the element cluster :return: nothing """ self.sfe.add_cluster_admin(username=self.element_username, password=self.element_password, access=self.access, accept_eula=self.acceptEula, attributes=self.attributes) def delete_admin_user(self): """ Deletes an existing admin user from the element cluster :return: nothing """ admin_user = self.get_admin_user() self.sfe.remove_cluster_admin( cluster_admin_id=admin_user.cluster_admin_id) def apply(self): """ determines which method to call to set, delete or modify admin users :return: """ changed = False if self.state == "present": if self.does_admin_user_exist(): changed = self.modify_admin_user() else: self.add_admin_user() changed = True else: if self.does_admin_user_exist(): self.delete_admin_user() changed = True self.module.exit_json(changed=changed)
class ElementOSVolumeClone(object): """ Contains methods to parse arguments, derive details of Element Software objects and send requests to Element OS via the Solidfire SDK """ def __init__(self): """ Parse arguments, setup state variables, check paramenters and ensure SDK is installed """ self._size_unit_map = netapp_utils.SF_BYTE_MAP self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update( dict( name=dict(required=True), src_volume_id=dict(required=True), src_snapshot_id=dict(), account_id=dict(required=True), attributes=dict(type='dict', default=None), size=dict(type='int'), size_unit=dict(default='gb', choices=[ 'bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb' ], type='str'), access=dict(type='str', default=None, choices=[ 'readOnly', 'readWrite', 'locked', 'replicationTarget' ]), )) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) parameters = self.module.params # set up state variables self.name = parameters['name'] self.src_volume_id = parameters['src_volume_id'] self.src_snapshot_id = parameters['src_snapshot_id'] self.account_id = parameters['account_id'] self.attributes = parameters['attributes'] self.size_unit = parameters['size_unit'] if parameters['size'] is not None: self.size = parameters['size'] * \ self._size_unit_map[self.size_unit] else: self.size = None self.access = parameters['access'] if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the SolidFire Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes if self.attributes is not None: self.attributes.update( self.elementsw_helper.set_element_attributes( source='na_elementsw_volume_clone')) else: self.attributes = self.elementsw_helper.set_element_attributes( source='na_elementsw_volume_clone') def get_account_id(self): """ Return account id if found """ try: # Update and return self.account_id self.account_id = self.elementsw_helper.account_exists( self.account_id) return self.account_id except Exception as err: self.module.fail_json(msg="Error: account_id %s does not exist" % self.account_id, exception=to_native(err)) def get_snapshot_id(self): """ Return snapshot details if found """ src_snapshot = self.elementsw_helper.get_snapshot( self.src_snapshot_id, self.src_volume_id) # Update and return self.src_snapshot_id if src_snapshot is not None: self.src_snapshot_id = src_snapshot.snapshot_id # Return src_snapshot return self.src_snapshot_id return None def get_src_volume_id(self): """ Return volume id if found """ src_vol_id = self.elementsw_helper.volume_exists( self.src_volume_id, self.account_id) if src_vol_id is not None: # Update and return self.volume_id self.src_volume_id = src_vol_id # Return src_volume_id return self.src_volume_id return None def clone_volume(self): """Clone Volume from source""" try: self.sfe.clone_volume(volume_id=self.src_volume_id, name=self.name, new_account_id=self.account_id, new_size=self.size, access=self.access, snapshot_id=self.src_snapshot_id, attributes=self.attributes) except Exception as err: self.module.fail_json(msg="Error creating clone %s of size %s" % (self.name, self.size), exception=to_native(err)) def apply(self): """Perform pre-checks, call functions and exit""" changed = False result_message = "" if self.get_account_id() is None: self.module.fail_json(msg="Account id not found: %s" % (self.account_id)) # there is only one state. other operations # are part of the volume module # ensure that a volume with the clone name # isn't already present if self.elementsw_helper.volume_exists(self.name, self.account_id) is None: # check for the source volume if self.get_src_volume_id() is not None: # check for a valid snapshot if self.src_snapshot_id and not self.get_snapshot_id(): self.module.fail_json(msg="Snapshot id not found: %s" % (self.src_snapshot_id)) # change required changed = True else: self.module.fail_json(msg="Volume id not found %s" % (self.src_volume_id)) if changed: if self.module.check_mode: result_message = "Check mode, skipping changes" else: self.clone_volume() result_message = "Volume cloned" self.module.exit_json(changed=changed, msg=result_message)
class ElementSWCluster(object): """ Element Software Initialize node with ownership for cluster formation """ def __init__(self): self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update( dict(management_virtual_ip=dict(required=True, type='str'), storage_virtual_ip=dict(required=True, type='str'), replica_count=dict(required=False, type='str', default='2'), cluster_admin_username=dict(required=False, type='str'), cluster_admin_password=dict(required=False, type='str', no_log=True), accept_eula=dict(required=False, type='bool'), nodes=dict(required=True, type=list), attributes=dict(required=False, type='dict', default=None))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) input_params = self.module.params self.management_virtual_ip = input_params['management_virtual_ip'] self.storage_virtual_ip = input_params['storage_virtual_ip'] self.replica_count = input_params['replica_count'] self.accept_eula = input_params.get('accept_eula') self.attributes = input_params.get('attributes') self.nodes = input_params['nodes'] self.cluster_admin_username = input_params.get( 'cluster_admin_username') self.cluster_admin_password = input_params.get( 'cluster_admin_password') if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the SolidFire Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes if self.attributes is not None: self.attributes.update( self.elementsw_helper.set_element_attributes( source='na_elementsw_cluster')) else: self.attributes = self.elementsw_helper.set_element_attributes( source='na_elementsw_cluster') def create_cluster(self): """ Create Cluster """ options = { 'mvip': self.management_virtual_ip, 'svip': self.storage_virtual_ip, 'rep_count': self.replica_count, 'accept_eula': self.accept_eula, 'nodes': self.nodes, 'attributes': self.attributes } if self.cluster_admin_username is not None: options['username'] = self.cluster_admin_username if self.cluster_admin_password is not None: options['password'] = self.cluster_admin_password try: self.sfe.create_cluster(**options) except Exception as exception_object: self.module.fail_json(msg='Error create cluster %s' % (to_native(exception_object)), exception=traceback.format_exc()) def check_connection(self): """ Check connections to mvip, svip address. :description: To test connection to given IP addressed for mvip and svip :rtype: bool """ try: mvip_test = self.sfe.test_connect_mvip( mvip=self.management_virtual_ip) svip_test = self.sfe.test_connect_svip( svip=self.storage_virtual_ip) if mvip_test.details.connected and svip_test.details.connected: return True else: return False except Exception as e: return False def apply(self): """ Check connection and initialize node with cluster ownership """ changed = False result_message = None if self.module.supports_check_mode and self.accept_eula: if self.check_connection(): self.create_cluster() changed = True else: self.module.fail_json( msg='Error connecting mvip and svip address') else: result_message = "Skipping changes, No change requested" self.module.exit_json(changed=changed, msg=result_message)
class ElementSWQosPolicy(object): """ Element Software QOS Policy """ def __init__(self): self.argument_spec = netapp_utils.ontap_sf_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'), qos=dict(required=False, type='dict'), )) 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) self.qos_policy_id = None if HAS_SF_SDK is False: self.module.fail_json(msg="Unable to import the SolidFire Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_qos_policy') def get_qos_policy(self, name): """ Get QOS Policy """ policy, error = self.elementsw_helper.get_qos_policy(name) if error is not None: self.module.fail_json(msg=error, exception=traceback.format_exc()) return policy def create_qos_policy(self, name, qos): """ Create the QOS Policy """ try: self.sfe.create_qos_policy(name=name, qos=qos) except (solidfire.common.ApiServerError, solidfire.common.ApiConnectionError) as exc: self.module.fail_json(msg="Error creating qos policy: %s: %s" % (name, to_native(exc)), exception=traceback.format_exc()) def update_qos_policy(self, qos_policy_id, modify, name=None): """ Update the QOS Policy if the policy already exists """ options = dict( qos_policy_id=qos_policy_id ) if name is not None: options['name'] = name if 'qos' in modify: options['qos'] = modify['qos'] try: self.sfe.modify_qos_policy(**options) except (solidfire.common.ApiServerError, solidfire.common.ApiConnectionError) as exc: self.module.fail_json(msg="Error updating qos policy: %s: %s" % (self.parameters['from_name'] if name is not None else self.parameters['name'], to_native(exc)), exception=traceback.format_exc()) def delete_qos_policy(self, qos_policy_id): """ Delete the QOS Policy """ try: self.sfe.delete_qos_policy(qos_policy_id=qos_policy_id) except (solidfire.common.ApiServerError, solidfire.common.ApiConnectionError) as exc: self.module.fail_json(msg="Error deleting qos policy: %s: %s" % (self.parameters['name'], to_native(exc)), exception=traceback.format_exc()) def apply(self): """ Process the create/delete/rename/modify actions for qos policy on the Element Software Cluster """ modify = dict() current = self.get_qos_policy(self.parameters['name']) qos_policy_id = None if current is None else current['qos_policy_id'] cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = self.na_helper.get_modified_attributes(current, self.parameters) if cd_action == 'create' and self.parameters.get('from_name') is not None: from_qos_policy = self.get_qos_policy(self.parameters['from_name']) if from_qos_policy is None: self.module.fail_json(msg="Error renaming qos policy, no existing policy with name/id: %s" % self.parameters['from_name']) cd_action = 'rename' qos_policy_id = from_qos_policy['qos_policy_id'] self.na_helper.changed = True modify = self.na_helper.get_modified_attributes(from_qos_policy, self.parameters) if cd_action == 'create' and 'qos' not in self.parameters: self.module.fail_json(msg="Error creating qos policy: %s, 'qos:' option is required" % self.parameters['name']) if not self.module.check_mode: if cd_action == 'create': self.create_qos_policy(self.parameters['name'], self.parameters['qos']) elif cd_action == 'delete': self.delete_qos_policy(qos_policy_id) elif cd_action == 'rename': self.update_qos_policy(qos_policy_id, modify, name=self.parameters['name']) elif modify: self.update_qos_policy(qos_policy_id, modify) self.module.exit_json(changed=self.na_helper.changed)
class ElementOSSnapshotRestore(object): """ Element OS Restore from snapshot """ def __init__(self): self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update( dict(account_id=dict(required=True, type='str'), src_volume_id=dict(required=True, type='str'), dest_volume_name=dict(required=True, type='str'), src_snapshot_id=dict(required=True, type='str'))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) input_params = self.module.params self.account_id = input_params['account_id'] self.src_volume_id = input_params['src_volume_id'] self.dest_volume_name = input_params['dest_volume_name'] self.src_snapshot_id = input_params['src_snapshot_id'] if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the SolidFire Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes self.attributes = self.elementsw_helper.set_element_attributes( source='na_elementsw_snapshot_restore') def get_account_id(self): """ Get account id if found """ try: # Update and return self.account_id self.account_id = self.elementsw_helper.account_exists( self.account_id) return self.account_id except Exception as err: self.module.fail_json(msg="Error: account_id %s does not exist" % self.account_id, exception=to_native(err)) def get_snapshot_id(self): """ Return snapshot details if found """ src_snapshot = self.elementsw_helper.get_snapshot( self.src_snapshot_id, self.src_volume_id) # Update and return self.src_snapshot_id if src_snapshot: self.src_snapshot_id = src_snapshot.snapshot_id # Return self.src_snapshot_id return self.src_snapshot_id return None def restore_snapshot(self): """ Restore Snapshot to Volume """ try: self.sfe.clone_volume(volume_id=self.src_volume_id, name=self.dest_volume_name, snapshot_id=self.src_snapshot_id, attributes=self.attributes) except Exception as exception_object: self.module.fail_json(msg='Error restore snapshot %s' % (to_native(exception_object)), exception=traceback.format_exc()) def apply(self): """ Check, process and initiate restore snapshot to volume operation """ changed = False result_message = None snapshot_detail = None self.get_account_id() src_vol_id = self.elementsw_helper.volume_exists( self.src_volume_id, self.account_id) if src_vol_id is not None: # Update self.src_volume_id self.src_volume_id = src_vol_id if self.get_snapshot_id() is not None: # Addressing idempotency by comparing volume does not exist with same volume name if self.elementsw_helper.volume_exists( self.dest_volume_name, self.account_id) is None: self.restore_snapshot() changed = True else: result_message = "No changes requested, Skipping changes" else: self.module.fail_json(msg="Snapshot id not found %s" % self.src_snapshot_id) else: self.module.fail_json(msg="Volume id not found %s" % self.src_volume_id) self.module.exit_json(changed=changed, msg=result_message)
class ElementSWAccount(object): """ Element SW Account """ def __init__(self): self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update( dict( state=dict(required=True, choices=['present', 'absent']), element_username=dict(required=True, aliases=["account_id"], type='str'), from_name=dict(required=False, default=None), initiator_secret=dict(required=False, type='str'), target_secret=dict(required=False, type='str'), attributes=dict(required=False, type='dict'), status=dict(required=False, type='str'), )) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) params = self.module.params # set up state variables self.state = params.get('state') self.element_username = params.get('element_username') self.from_name = params.get('from_name') self.initiator_secret = params.get('initiator_secret') self.target_secret = params.get('target_secret') self.attributes = params.get('attributes') self.status = params.get('status') if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the Element SW Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes if self.attributes is not None: self.attributes.update( self.elementsw_helper.set_element_attributes( source='na_elementsw_account')) else: self.attributes = self.elementsw_helper.set_element_attributes( source='na_elementsw_account') def get_account(self, username): """ Get Account :description: Get Account object from account id or name :return: Details about the account. None if not found. :rtype: object (Account object) """ account_list = self.sfe.list_accounts() for account in account_list.accounts: # Check and get account object for a given name if str(account.account_id) == username: return account elif account.username == username: return account return None def create_account(self): """ Create the Account """ try: self.sfe.add_account(username=self.element_username, initiator_secret=self.initiator_secret, target_secret=self.target_secret, attributes=self.attributes) except Exception as e: self.module.fail_json(msg='Error creating account %s: %s' % (self.element_username, to_native(e)), exception=traceback.format_exc()) def delete_account(self): """ Delete the Account """ try: self.sfe.remove_account(account_id=self.account_id) except Exception as e: self.module.fail_json(msg='Error deleting account %s: %s' % (self.account_id, to_native(e)), exception=traceback.format_exc()) def rename_account(self): """ Rename the Account """ try: self.sfe.modify_account(account_id=self.account_id, username=self.element_username, status=self.status, initiator_secret=self.initiator_secret, target_secret=self.target_secret, attributes=self.attributes) except Exception as e: self.module.fail_json(msg='Error renaming account %s: %s' % (self.account_id, to_native(e)), exception=traceback.format_exc()) def update_account(self): """ Update the Account if account already exists """ try: self.sfe.modify_account(account_id=self.account_id, status=self.status, initiator_secret=self.initiator_secret, target_secret=self.target_secret, attributes=self.attributes) except Exception as e: self.module.fail_json(msg='Error updating account %s: %s' % (self.account_id, to_native(e)), exception=traceback.format_exc()) def apply(self): """ Process the account operation on the Element OS Cluster """ changed = False update_account = False account_detail = self.get_account(self.element_username) if account_detail is None and self.state == 'present': changed = True elif account_detail is not None: # If account found self.account_id = account_detail.account_id if self.state == 'absent': changed = True else: # If state - present, check for any parameter of exising account needs modification. if account_detail.username is not None and self.element_username is not None and \ account_detail.username != self.element_username: update_account = True changed = True elif account_detail.status is not None and self.status is not None \ and account_detail.status != self.status: update_account = True changed = True elif account_detail.initiator_secret is not None and self.initiator_secret is not None \ and account_detail.initiator_secret != self.initiator_secret: update_account = True changed = True elif account_detail.target_secret is not None and self.target_secret is not None \ and account_detail.target_secret != self.target_secret: update_account = True changed = True elif account_detail.attributes is not None and self.attributes is not None \ and account_detail.attributes != self.attributes: update_account = True changed = True if changed: if self.module.check_mode: # Skipping the changes pass else: if self.state == 'present': if update_account: self.update_account() else: if self.from_name is not None: # If from_name is defined account_exists = self.get_account(self.from_name) if account_exists is not None: # If resource pointed by from_name exists, rename the account to name self.account_id = account_exists.account_id self.rename_account() else: # If resource pointed by from_name does not exists, error out self.module.fail_json( msg="Resource does not exist : %s" % self.from_name) else: # If from_name is not defined, create from scratch. self.create_account() elif self.state == 'absent': self.delete_account() self.module.exit_json(changed=changed)
class ElementSWVolume(object): """ Contains methods to parse arguments, derive details of ElementSW objects and send requests to ElementOS via the ElementSW SDK """ def __init__(self): """ Parse arguments, setup state variables, check paramenters and ensure SDK is installed """ self._size_unit_map = netapp_utils.SF_BYTE_MAP self.argument_spec = netapp_utils.ontap_sf_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'), account_id=dict(required=True), enable512e=dict(required=False, type='bool', aliases=['enable512emulation']), qos=dict(required=False, type='dict', default=None), qos_policy_name=dict(required=False, type='str', default=None), attributes=dict(required=False, type='dict', default=None), size=dict(type='int'), size_unit=dict(default='gb', choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'], type='str'), access=dict(required=False, type='str', default=None, choices=['readOnly', 'readWrite', 'locked', 'replicationTarget']), )) self.module = AnsibleModule( argument_spec=self.argument_spec, required_if=[ ('state', 'present', ['size', 'enable512e']) ], mutually_exclusive=[ ('qos', 'qos_policy_name'), ], supports_check_mode=True ) param = self.module.params # set up state variables self.state = param['state'] self.name = param['name'] self.account_id = param['account_id'] self.enable512e = param['enable512e'] self.qos = param['qos'] self.qos_policy_name = param['qos_policy_name'] self.attributes = param['attributes'] self.access = param['access'] self.size_unit = param['size_unit'] if param['size'] is not None: self.size = param['size'] * self._size_unit_map[self.size_unit] else: self.size = None if HAS_SF_SDK is False: self.module.fail_json(msg="Unable to import the ElementSW Python SDK") else: try: self.sfe = netapp_utils.create_sf_connection(module=self.module) except solidfire.common.ApiServerError: self.module.fail_json(msg="Unable to create the connection") self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes if self.attributes is not None: self.attributes.update(self.elementsw_helper.set_element_attributes(source='na_elementsw_volume')) else: self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_volume') def get_account_id(self): """ Return account id if found """ try: # Update and return self.account_id self.account_id = self.elementsw_helper.account_exists(self.account_id) except Exception as err: self.module.fail_json(msg="Error: account_id %s does not exist" % self.account_id, exception=to_native(err)) return self.account_id def get_qos_policy(self, name): """ Get QOS Policy """ policy, error = self.elementsw_helper.get_qos_policy(name) if error is not None: self.module.fail_json(msg=error) return policy def get_volume(self): """ Return volume details if found """ # Get volume details volume_id = self.elementsw_helper.volume_exists(self.name, self.account_id) if volume_id is not None: # Return volume_details volume_details = self.elementsw_helper.get_volume(volume_id) if volume_details is not None: return volume_details return None def create_volume(self, qos_policy_id): """ Create Volume :return: True if created, False if fails """ options = dict( name=self.name, account_id=self.account_id, total_size=self.size, enable512e=self.enable512e, attributes=self.attributes ) if qos_policy_id is not None: options['qos_policy_id'] = qos_policy_id if self.qos is not None: options['qos'] = self.qos try: self.sfe.create_volume(**options) except Exception as err: self.module.fail_json(msg="Error provisioning volume: %s of size: %s" % (self.name, self.size), exception=to_native(err)) def delete_volume(self, volume_id): """ Delete and purge the volume using volume id :return: Success : True , Failed : False """ try: self.sfe.delete_volume(volume_id=volume_id) self.sfe.purge_deleted_volume(volume_id=volume_id) # Delete method will delete and also purge the volume instead of moving the volume state to inactive. except Exception as err: # Throwing the exact error message instead of generic error message self.module.fail_json(msg='Error deleting volume: %s, %s' % (str(volume_id), to_native(err)), exception=to_native(err)) def update_volume(self, volume_id, qos_policy_id): """ Update the volume with the specified param :return: Success : True, Failed : False """ options = dict( attributes=self.attributes ) if self.access is not None: options['access'] = self.access if self.account_id is not None: options['account_id'] = self.account_id if self.qos is not None: options['qos'] = self.qos if qos_policy_id is not None: options['qos_policy_id'] = qos_policy_id if self.size is not None: options['total_size'] = self.size try: self.sfe.modify_volume(volume_id, **options) except Exception as err: # Throwing the exact error message instead of generic error message self.module.fail_json(msg='Error updating volume: %s, %s' % (str(volume_id), to_native(err)), exception=to_native(err)) def apply(self): # Perform pre-checks, call functions and exit changed = False qos_policy_id = None action = None self.get_account_id() volume_detail = self.get_volume() if self.state == 'present' and self.qos_policy_name is not None: policy = self.get_qos_policy(self.qos_policy_name) if policy is None: error = 'Cannot find qos policy with name/id: %s' % self.qos_policy_name self.module.fail_json(msg=error) qos_policy_id = policy['qos_policy_id'] if volume_detail: volume_id = volume_detail.volume_id if self.state == 'absent': action = 'delete' elif self.state == 'present': # Checking all the params for update operation if self.access is not None and volume_detail.access != self.access: action = 'update' if self.account_id is not None and volume_detail.account_id != self.account_id: action = 'update' if qos_policy_id is not None and volume_detail.qos_policy_id != qos_policy_id: # volume_detail.qos_policy_id may be None if no policy is associated with the volume action = 'update' if self.qos is not None and volume_detail.qos_policy_id is not None: # remove qos_policy action = 'update' if self.qos is not None: # Actual volume_detail.qos has ['burst_iops', 'burst_time', 'curve', 'max_iops', 'min_iops'] keys. # As only minOPS, maxOPS, burstOPS is important to consider, checking only these values. volume_qos = vars(volume_detail.qos) if volume_qos['min_iops'] != self.qos['minIOPS'] or volume_qos['max_iops'] != self.qos['maxIOPS'] \ or volume_qos['burst_iops'] != self.qos['burstIOPS']: action = 'update' if self.size is not None and volume_detail.total_size is not None and volume_detail.total_size != self.size: size_difference = abs(float(volume_detail.total_size - self.size)) # Change size only if difference is bigger than 0.001 if size_difference / self.size > 0.001: action = 'update' if self.attributes is not None and volume_detail.attributes != self.attributes: action = 'update' elif self.state == 'present': action = 'create' result_message = "" if action is not None: changed = True if self.module.check_mode: result_message = "Check mode, skipping changes" else: if action == 'create': self.create_volume(qos_policy_id) result_message = "Volume created" elif action == 'update': self.update_volume(volume_id, qos_policy_id) result_message = "Volume updated" elif action == 'delete': self.delete_volume(volume_id) result_message = "Volume deleted" self.module.exit_json(changed=changed, msg=result_message)
class ElementOSSnapshot(object): """ Element OS Snapshot Manager """ def __init__(self): self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update( dict(state=dict(required=False, choices=['present', 'absent'], default='present'), account_id=dict(required=True, type='str'), name=dict(required=False, type='str'), src_volume_id=dict(required=True, type='str'), retention=dict(required=False, type='str'), src_snapshot_id=dict(required=False, type='str'), enable_remote_replication=dict(required=False, type='bool'), expiration_time=dict(required=False, type='str'), snap_mirror_label=dict(required=False, type='str'))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) input_params = self.module.params self.state = input_params['state'] self.name = input_params['name'] self.account_id = input_params['account_id'] self.src_volume_id = input_params['src_volume_id'] self.src_snapshot_id = input_params['src_snapshot_id'] self.retention = input_params['retention'] self.properties_provided = False self.expiration_time = input_params['expiration_time'] if input_params['expiration_time'] is not None: self.properties_provided = True self.enable_remote_replication = input_params[ 'enable_remote_replication'] if input_params['enable_remote_replication'] is not None: self.properties_provided = True self.snap_mirror_label = input_params['snap_mirror_label'] if input_params['snap_mirror_label'] is not None: self.properties_provided = True if self.state == 'absent' and self.src_snapshot_id is None: self.module.fail_json( msg="Please provide required parameter : snapshot_id") if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the SolidFire Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # add telemetry attributes self.attributes = self.elementsw_helper.set_element_attributes( source='na_elementsw_snapshot') def get_account_id(self): """ Return account id if found """ try: # Update and return self.account_id self.account_id = self.elementsw_helper.account_exists( self.account_id) return self.account_id except Exception as err: self.module.fail_json(msg="Error: account_id %s does not exist" % self.account_id, exception=to_native(err)) def get_src_volume_id(self): """ Return volume id if found """ src_vol_id = self.elementsw_helper.volume_exists( self.src_volume_id, self.account_id) if src_vol_id is not None: # Update and return self.volume_id self.src_volume_id = src_vol_id # Return src_volume_id return self.src_volume_id return None def get_snapshot(self, name=None): """ Return snapshot details if found """ src_snapshot = None if name is not None: src_snapshot = self.elementsw_helper.get_snapshot( name, self.src_volume_id) elif self.src_snapshot_id is not None: src_snapshot = self.elementsw_helper.get_snapshot( self.src_snapshot_id, self.src_volume_id) if src_snapshot is not None: # Update self.src_snapshot_id self.src_snapshot_id = src_snapshot.snapshot_id # Return src_snapshot return src_snapshot def create_snapshot(self): """ Create Snapshot """ try: self.sfe.create_snapshot( volume_id=self.src_volume_id, snapshot_id=self.src_snapshot_id, name=self.name, enable_remote_replication=self.enable_remote_replication, retention=self.retention, snap_mirror_label=self.snap_mirror_label, attributes=self.attributes) except Exception as exception_object: self.module.fail_json(msg='Error creating snapshot %s' % (to_native(exception_object)), exception=traceback.format_exc()) def modify_snapshot(self): """ Modify Snapshot Properties """ try: self.sfe.modify_snapshot( snapshot_id=self.src_snapshot_id, expiration_time=self.expiration_time, enable_remote_replication=self.enable_remote_replication, snap_mirror_label=self.snap_mirror_label) except Exception as exception_object: self.module.fail_json(msg='Error modify snapshot %s' % (to_native(exception_object)), exception=traceback.format_exc()) def delete_snapshot(self): """ Delete Snapshot """ try: self.sfe.delete_snapshot(snapshot_id=self.src_snapshot_id) except Exception as exception_object: self.module.fail_json(msg='Error delete snapshot %s' % (to_native(exception_object)), exception=traceback.format_exc()) def apply(self): """ Check, process and initiate snapshot operation """ changed = False result_message = None self.get_account_id() # Dont proceed if source volume is not found if self.get_src_volume_id() is None: self.module.fail_json(msg="Volume id not found %s" % self.src_volume_id) # Get snapshot details using source volume snapshot_detail = self.get_snapshot() if snapshot_detail: if self.properties_provided: if self.expiration_time != snapshot_detail.expiration_time: changed = True else: # To preserve value in case parameter expiration_time is not defined/provided. self.expiration_time = snapshot_detail.expiration_time if self.enable_remote_replication != snapshot_detail.enable_remote_replication: changed = True else: # To preserve value in case parameter enable_remote_Replication is not defined/provided. self.enable_remote_replication = snapshot_detail.enable_remote_replication if self.snap_mirror_label != snapshot_detail.snap_mirror_label: changed = True else: # To preserve value in case parameter snap_mirror_label is not defined/provided. self.snap_mirror_label = snapshot_detail.snap_mirror_label if self.account_id is None or self.src_volume_id is None or self.module.check_mode: changed = False result_message = "Check mode, skipping changes" elif self.state == 'absent' and snapshot_detail is not None: self.delete_snapshot() changed = True elif self.state == 'present' and snapshot_detail is not None: if changed: self.modify_snapshot() # Modify Snapshot properties elif not self.properties_provided: if self.name is not None: snapshot = self.get_snapshot(self.name) # If snapshot with name already exists return without performing any action if snapshot is None: self.create_snapshot( ) # Create Snapshot using parent src_snapshot_id changed = True else: self.create_snapshot() changed = True elif self.state == 'present': if self.name is not None: snapshot = self.get_snapshot(self.name) # If snapshot with name already exists return without performing any action if snapshot is None: self.create_snapshot( ) # Create Snapshot using parent src_snapshot_id changed = True else: self.create_snapshot() changed = True else: changed = False result_message = "No changes requested, skipping changes" self.module.exit_json(changed=changed, msg=result_message)
class ElementSWVlan(object): """ class to handle VLAN operations """ def __init__(self): """ Setup Ansible parameters and ElementSW connection """ self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update( dict(state=dict(required=False, choices=['present', 'absent'], default='present'), name=dict(required=False, type='str'), vlan_tag=dict(required=True, type='str'), svip=dict(required=False, type='str'), netmask=dict(required=False, type='str'), gateway=dict(required=False, type='str'), namespace=dict(required=False, type='bool'), attributes=dict(required=False, type='dict'), address_blocks=dict(required=False, type='list'))) self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True) if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the SolidFire Python SDK") else: self.elem = netapp_utils.create_sf_connection(module=self.module) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.elementsw_helper = NaElementSWModule(self.elem) # add telemetry attributes if self.parameters.get('attributes') is not None: self.parameters['attributes'].update( self.elementsw_helper.set_element_attributes( source='na_elementsw_vlan')) else: self.parameters[ 'attributes'] = self.elementsw_helper.set_element_attributes( source='na_elementsw_vlan') def validate_keys(self): """ Validate if all required keys are present before creating """ required_keys = ['address_blocks', 'svip', 'netmask', 'name'] if all(item in self.parameters.keys() for item in required_keys) is False: self.module.fail_json( msg= "One or more required fields %s for creating VLAN is missing" % required_keys) addr_blk_fields = ['start', 'size'] for address in self.parameters['address_blocks']: if 'start' not in address or 'size' not in address: self.module.fail_json( msg= "One or more required fields %s for address blocks is missing" % addr_blk_fields) def create_network(self): """ Add VLAN """ try: self.validate_keys() create_params = self.parameters.copy() for key in [ 'username', 'hostname', 'password', 'state', 'vlan_tag' ]: del create_params[key] self.elem.add_virtual_network( virtual_network_tag=self.parameters['vlan_tag'], **create_params) except solidfire.common.ApiServerError as err: self.module.fail_json(msg="Error creating VLAN %s" % self.parameters['vlan_tag'], exception=to_native(err)) def delete_network(self): """ Remove VLAN """ try: self.elem.remove_virtual_network( virtual_network_tag=self.parameters['vlan_tag']) except solidfire.common.ApiServerError as err: self.module.fail_json(msg="Error deleting VLAN %s" % self.parameters['vlan_tag'], exception=to_native(err)) def modify_network(self, modify): """ Modify the VLAN """ try: self.elem.modify_virtual_network( virtual_network_tag=self.parameters['vlan_tag'], **modify) except solidfire.common.ApiServerError as err: self.module.fail_json(msg="Error modifying VLAN %s" % self.parameters['vlan_tag'], exception=to_native(err)) def get_network_details(self): """ Check existing VLANs :return: vlan details if found, None otherwise :type: dict """ vlans = self.elem.list_virtual_networks( virtual_network_tag=self.parameters['vlan_tag']) vlan_details = dict() for vlan in vlans.virtual_networks: if vlan is not None: vlan_details['name'] = vlan.name vlan_details['address_blocks'] = list() for address in vlan.address_blocks: vlan_details['address_blocks'].append({ 'start': address.start, 'size': address.size }) vlan_details['svip'] = vlan.svip vlan_details['gateway'] = vlan.gateway vlan_details['netmask'] = vlan.netmask vlan_details['namespace'] = vlan.namespace vlan_details['attributes'] = dict() for key in vlan.attributes.__dict__.keys(): vlan_details['attributes'][key] = vlan.attributes.key return vlan_details return None def apply(self): """ Call create / delete / modify vlan methods """ network = self.get_network_details() # calling helper to determine action cd_action = self.na_helper.get_cd_action(network, self.parameters) modify = self.na_helper.get_modified_attributes( network, self.parameters) if cd_action == "create": self.create_network() elif cd_action == "delete": self.delete_network() elif modify: self.modify_network(modify) self.module.exit_json(changed=self.na_helper.changed)
class ElementSWBackup(object): ''' class to handle backup operations ''' def __init__(self): """ Setup Ansible parameters and SolidFire connection """ self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update( dict(src_volume_id=dict(aliases=['volume_id'], required=True, type='str'), dest_hostname=dict(required=False, type='str'), dest_username=dict(required=False, type='str'), dest_password=dict(required=False, type='str', no_log=True), dest_volume_id=dict(required=True, type='str'), format=dict(required=False, choices=['native', 'uncompressed'], default='native'), script=dict(required=False, type='str'), script_parameters=dict(required=False, type='dict'))) self.module = AnsibleModule( argument_spec=self.argument_spec, required_together=[['script', 'script_parameters']], supports_check_mode=True) if HAS_SF_SDK is False: self.module.fail_json( msg="Unable to import the SolidFire Python SDK") # If destination cluster details are not specified , set the destination to be the same as the source if self.module.params["dest_hostname"] is None: self.module.params["dest_hostname"] = self.module.params[ "hostname"] if self.module.params["dest_username"] is None: self.module.params["dest_username"] = self.module.params[ "username"] if self.module.params["dest_password"] is None: self.module.params["dest_password"] = self.module.params[ "password"] params = self.module.params # establish a connection to both source and destination elementsw clusters self.src_connection = netapp_utils.create_sf_connection(self.module) self.module.params["username"] = params["dest_username"] self.module.params["password"] = params["dest_password"] self.module.params["hostname"] = params["dest_hostname"] self.dest_connection = netapp_utils.create_sf_connection(self.module) self.elementsw_helper = NaElementSWModule(self.src_connection) # add telemetry attributes self.attributes = self.elementsw_helper.set_element_attributes( source='na_elementsw_backup') def apply(self): """ Apply backup creation logic """ self.create_backup() self.module.exit_json(changed=True) def create_backup(self): """ Create backup """ # Start volume write on destination cluster try: write_obj = self.dest_connection.start_bulk_volume_write( volume_id=self.module.params["dest_volume_id"], format=self.module.params["format"], attributes=self.attributes) write_key = write_obj.key except solidfire.common.ApiServerError as err: self.module.fail_json( msg="Error starting bulk write on destination cluster", exception=to_native(err)) # Set script parameters if not passed by user # These parameters are equivalent to the options used when a backup is executed via the GUI if self.module.params["script"] is None and self.module.params[ "script_parameters"] is None: self.module.params["script"] = 'bv_internal.py' self.module.params["script_parameters"] = { "write": { "mvip": self.module.params["dest_hostname"], "username": self.module.params["dest_username"], "password": self.module.params["dest_password"], "key": write_key, "endpoint": "solidfire", "format": self.module.params["format"] }, "range": { "lba": 0, "blocks": 244224 } } # Start volume read on source cluster try: read_obj = self.src_connection.start_bulk_volume_read( self.module.params["src_volume_id"], self.module.params["format"], script=self.module.params["script"], script_parameters=self.module.params["script_parameters"], attributes=self.attributes) except solidfire.common.ApiServerError as err: self.module.fail_json( msg="Error starting bulk read on source cluster", exception=to_native(err)) # Poll job status until it has completed # SF will automatically timeout if the job is not successful after certain amount of time completed = False while completed is not True: # Sleep between polling iterations to reduce api load time.sleep(2) try: result = self.src_connection.get_async_result( read_obj.async_handle, True) except solidfire.common.ApiServerError as err: self.module.fail_json(msg="Unable to check backup job status", exception=to_native(err)) if result["status"] != 'running': completed = True if 'error' in result: self.module.fail_json(msg=result['error']['message'])
class ElementSWCluster(object): """ Element Software Initialize node with ownership for cluster formation """ def __init__(self): self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update(dict( management_virtual_ip=dict(required=True, type='str'), storage_virtual_ip=dict(required=True, type='str'), replica_count=dict(required=False, type='int', default=2), cluster_admin_username=dict(required=False, type='str'), cluster_admin_password=dict(required=False, type='str', no_log=True), accept_eula=dict(required=False, type='bool'), nodes=dict(required=True, type='list', elements='str'), attributes=dict(required=False, type='dict', default=None), timeout=dict(required=False, type='int', default=100), fail_if_cluster_already_exists_with_larger_ensemble=dict(required=False, type='bool', default=True), )) self.module = AnsibleModule( argument_spec=self.argument_spec, supports_check_mode=True ) input_params = self.module.params self.management_virtual_ip = input_params['management_virtual_ip'] self.storage_virtual_ip = input_params['storage_virtual_ip'] self.replica_count = input_params['replica_count'] self.accept_eula = input_params.get('accept_eula') self.attributes = input_params.get('attributes') self.nodes = input_params['nodes'] self.cluster_admin_username = input_params['username'] if input_params.get('cluster_admin_username') is None else input_params['cluster_admin_username'] self.cluster_admin_password = input_params['password'] if input_params.get('cluster_admin_password') is None else input_params['cluster_admin_password'] self.fail_if_cluster_already_exists_with_larger_ensemble = input_params['fail_if_cluster_already_exists_with_larger_ensemble'] self.debug = list() if HAS_SF_SDK is False: self.module.fail_json(msg="Unable to import the SolidFire Python SDK") # 442 for node APIs, 443 (default) for cluster APIs for role, port in [('node', 442), ('cluster', 443)]: try: # even though username/password should be optional, create_sf_connection fails if not set conn = netapp_utils.create_sf_connection(module=self.module, raise_on_connection_error=True, port=port, timeout=input_params['timeout']) if role == 'node': self.sfe_node = conn self.debug.append('created node cx: port sent: %d, port read: %s' % (port, str(self.sfe_node._port))) else: self.sfe_cluster = conn self.debug.append('created cluster cx: port sent: %d, port read: %s' % (port, str(self.sfe_cluster._port))) except netapp_utils.solidfire.common.ApiConnectionError as exc: if str(exc) == "Bad Credentials": msg = 'Most likely the cluster is already created.' msg += ' Make sure to use valid %s credentials for username and password.' % 'node' if port == 442 else 'cluster' msg += ' Even though credentials are not required for the first create, they are needed to check whether the cluster already exists.' msg += ' Cluster reported: %s' % repr(exc) else: msg = 'Failed to create connection: %s' % repr(exc) self.module.fail_json(msg=msg) except Exception as exc: self.module.fail_json(msg='Failed to connect: %s' % repr(exc)) self.elementsw_helper = NaElementSWModule(self.sfe_cluster) # add telemetry attributes if self.attributes is not None: self.attributes.update(self.elementsw_helper.set_element_attributes(source='na_elementsw_cluster')) else: self.attributes = self.elementsw_helper.set_element_attributes(source='na_elementsw_cluster') def get_cluster_info(self): """ Get Cluster Info - using node API """ try: info = self.sfe_node.get_config() self.debug.append(repr(info.config.cluster)) return info.config.cluster except Exception as exc: self.debug.append("port: %s, %s" % (str(self.sfe_node._port), repr(exc))) return None def check_cluster_exists(self): """ validate if cluster exists with list of nodes error out if something is found but with different nodes return a tuple (found, info) found is True if found, False if not found """ info = self.get_cluster_info() if info is None: return False ensemble = getattr(info, 'ensemble', None) if not ensemble: return False # format is 'id:IP' nodes = [x.split(':', 1)[1] for x in ensemble] current_ensemble_nodes = set(nodes) if ensemble else set() requested_nodes = set(self.nodes) if self.nodes else set() extra_ensemble_nodes = current_ensemble_nodes - requested_nodes # TODO: the cluster may have more nodes than what is reported in ensemble: # nodes_not_in_ensemble = requested_nodes - current_ensemble_nodes # So it's OK to find some missing nodes, but not very deterministic. # eg some kind of backup nodes could be in nodes_not_in_ensemble. if extra_ensemble_nodes and self.fail_if_cluster_already_exists_with_larger_ensemble: msg = 'Error: found existing cluster with more nodes in ensemble. Cluster: %s, extra nodes: %s' %\ (getattr(info, 'cluster', 'not found'), extra_ensemble_nodes) msg += '. Cluster info: %s' % repr(info) self.module.fail_json(msg=msg) if extra_ensemble_nodes: self.debug.append("Extra ensemble nodes: %s" % extra_ensemble_nodes) nodes_not_in_ensemble = requested_nodes - current_ensemble_nodes if nodes_not_in_ensemble: self.debug.append("Extra requested nodes not in ensemble: %s" % nodes_not_in_ensemble) return True def create_cluster(self): """ Create Cluster """ options = { 'mvip': self.management_virtual_ip, 'svip': self.storage_virtual_ip, 'rep_count': self.replica_count, 'accept_eula': self.accept_eula, 'nodes': self.nodes, 'attributes': self.attributes, 'username': self.cluster_admin_username, 'password': self.cluster_admin_password } return_msg = 'created' try: # does not work as node even though documentation says otherwise # running as node, this error is reported: 500 xUnknownAPIMethod method=CreateCluster self.sfe_cluster.create_cluster(**options) except netapp_utils.solidfire.common.ApiServerError as exc: # not sure how this can happen, but the cluster may already exists if 'xClusterAlreadyCreated' not in str(exc.message): self.module.fail_json(msg='Error creating cluster %s' % to_native(exc), exception=traceback.format_exc()) return_msg = 'already_exists: %s' % str(exc.message) except Exception as exc: self.module.fail_json(msg='Error creating cluster %s' % to_native(exc), exception=traceback.format_exc()) return return_msg def apply(self): """ Check connection and initialize node with cluster ownership """ changed = False result_message = None exists = self.check_cluster_exists() if exists: result_message = "cluster already exists" else: changed = True if not self.module.check_mode: result_message = self.create_cluster() if result_message.startswith('already_exists:'): changed = False self.module.exit_json(changed=changed, msg=result_message, debug=self.debug)
class ElementSWInitiators(object): """ Element Software Manage Element SW initiators """ def __init__(self): self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() self.argument_spec.update(dict( initiators=dict( type='list', elements='dict', options=dict( name=dict(type='str', required=True), alias=dict(type='str', default=None), initiator_id=dict(type='int', default=None), volume_access_group_id=dict(type='int', default=None), attributes=dict(type='dict', default=None), ) ), state=dict(choices=['present', 'absent'], default='present'), )) 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.debug = list() if HAS_SF_SDK is False: self.module.fail_json(msg="Unable to import the SolidFire Python SDK") else: self.sfe = netapp_utils.create_sf_connection(module=self.module) self.elementsw_helper = NaElementSWModule(self.sfe) # iterate over each user-provided initiator for initiator in self.parameters.get('initiators'): # add telemetry attributes if 'attributes' in initiator and initiator['attributes']: initiator['attributes'].update(self.elementsw_helper.set_element_attributes(source='na_elementsw_initiators')) else: initiator['attributes'] = self.elementsw_helper.set_element_attributes(source='na_elementsw_initiators') def compare_initiators(self, user_initiator, existing_initiator): """ compare user input initiator with existing dict :return: True if matched, False otherwise """ if user_initiator is None or existing_initiator is None: return False changed = False for param in user_initiator: # lookup initiator_name instead of name if param == 'name': if user_initiator['name'] == existing_initiator['initiator_name']: pass elif param == 'initiator_id': # can't change the key pass elif user_initiator[param] == existing_initiator[param]: pass else: self.debug.append('Initiator: %s. Changed: %s from: %s to %s' % (user_initiator['name'], param, str(existing_initiator[param]), str(user_initiator[param]))) changed = True return changed def initiator_to_dict(self, initiator_obj): """ converts initiator class object to dict :return: reconstructed initiator dict """ known_params = ['initiator_name', 'alias', 'initiator_id', 'volume_access_groups', 'volume_access_group_id', 'attributes'] initiator_dict = {} # missing parameter cause error # so assign defaults for param in known_params: initiator_dict[param] = getattr(initiator_obj, param, None) if initiator_dict['volume_access_groups'] is not None: if len(initiator_dict['volume_access_groups']) == 1: initiator_dict['volume_access_group_id'] = initiator_dict['volume_access_groups'][0] elif len(initiator_dict['volume_access_groups']) > 1: self.module.fail_json(msg="Only 1 access group is supported, found: %s" % repr(initiator_obj)) del initiator_dict['volume_access_groups'] return initiator_dict def find_initiator(self, id=None, name=None): """ find a specific initiator :return: initiator dict """ initiator_details = None if self.all_existing_initiators is None: return initiator_details for initiator in self.all_existing_initiators: # if name is provided or # if id is provided if name is not None: if initiator.initiator_name == name: initiator_details = self.initiator_to_dict(initiator) elif id is not None: if initiator.initiator_id == id: initiator_details = self.initiator_to_dict(initiator) else: # if neither id nor name provided # return everything initiator_details = self.all_existing_initiators return initiator_details @staticmethod def rename_key(obj, old_name, new_name): obj[new_name] = obj.pop(old_name) def create_initiator(self, initiator): """ create initiator """ # SF SDK is using camelCase for this one self.rename_key(initiator, 'volume_access_group_id', 'volumeAccessGroupID') # create_initiators needs an array initiator_list = [initiator] try: self.sfe.create_initiators(initiator_list) except Exception as exception_object: self.module.fail_json(msg='Error creating initiator %s' % (to_native(exception_object)), exception=traceback.format_exc()) def delete_initiator(self, initiator): """ delete initiator """ # delete_initiators needs an array initiator_id_array = [initiator] try: self.sfe.delete_initiators(initiator_id_array) except Exception as exception_object: self.module.fail_json(msg='Error deleting initiator %s' % (to_native(exception_object)), exception=traceback.format_exc()) def modify_initiator(self, initiator, existing_initiator): """ modify initiator """ # create the new initiator dict # by merging old and new values merged_initiator = existing_initiator.copy() # can't change the key del initiator['initiator_id'] merged_initiator.update(initiator) # we MUST create an object before sending # the new initiator to modify_initiator initiator_object = ModifyInitiator(initiator_id=merged_initiator['initiator_id'], alias=merged_initiator['alias'], volume_access_group_id=merged_initiator['volume_access_group_id'], attributes=merged_initiator['attributes']) initiator_list = [initiator_object] try: self.sfe.modify_initiators(initiators=initiator_list) except Exception as exception_object: self.module.fail_json(msg='Error modifying initiator: %s' % (to_native(exception_object)), exception=traceback.format_exc()) def apply(self): """ configure initiators """ changed = False result_message = None # get all user provided initiators input_initiators = self.parameters.get('initiators') # get all initiators # store in a cache variable self.all_existing_initiators = self.sfe.list_initiators().initiators # iterate over each user-provided initiator for in_initiator in input_initiators: if self.parameters.get('state') == 'present': # check if initiator_id is provided and exists if 'initiator_id' in in_initiator and in_initiator['initiator_id'] is not None and \ self.find_initiator(id=in_initiator['initiator_id']) is not None: if self.compare_initiators(in_initiator, self.find_initiator(id=in_initiator['initiator_id'])): changed = True result_message = 'modifying initiator(s)' self.modify_initiator(in_initiator, self.find_initiator(id=in_initiator['initiator_id'])) # otherwise check if name is provided and exists elif 'name' in in_initiator and in_initiator['name'] is not None and self.find_initiator(name=in_initiator['name']) is not None: if self.compare_initiators(in_initiator, self.find_initiator(name=in_initiator['name'])): changed = True result_message = 'modifying initiator(s)' self.modify_initiator(in_initiator, self.find_initiator(name=in_initiator['name'])) # this is a create op if initiator doesn't exist else: changed = True result_message = 'creating initiator(s)' self.create_initiator(in_initiator) elif self.parameters.get('state') == 'absent': # delete_initiators only processes ids # so pass ids of initiators to method if 'name' in in_initiator and in_initiator['name'] is not None and \ self.find_initiator(name=in_initiator['name']) is not None: changed = True result_message = 'deleting initiator(s)' self.delete_initiator(self.find_initiator(name=in_initiator['name'])['initiator_id']) elif 'initiator_id' in in_initiator and in_initiator['initiator_id'] is not None and \ self.find_initiator(id=in_initiator['initiator_id']) is not None: changed = True result_message = 'deleting initiator(s)' self.delete_initiator(in_initiator['initiator_id']) if self.module.check_mode is True: result_message = "Check mode, skipping changes" if self.debug: result_message += ". %s" % self.debug self.module.exit_json(changed=changed, msg=result_message)