#!/usr/bin/python
# Copyright: (c) 2020, Dell Technologies

# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt)

"""Ansible module for managing nfs export on Unity"""

from __future__ import absolute_import, division, print_function
__metaclass__ = type

DOCUMENTATION = r"""
---
module: nfs
version_added: '1.1.0'
short_description: Manage NFS export on Unity storage system
description:
- Managing NFS export on Unity storage system includes-
  Create new NFS export,
  Modify NFS export attributes,
  Display NFS export details,
  Delete NFS export.

extends_documentation_fragment:
  -  dellemc.unity.unity

author:
- Vivek Soni (@v-soni11) <ansible.team@dell.com>

options:
  nfs_export_name:
    description:
    - Name of the nfs export.
    - Mandatory for create operation.
    - Specify either I(nfs_export_name) or I(nfs_export_id) (but not both) for any
      operation.
    type: str
  nfs_export_id:
    description:
    - ID of the nfs export.
    - This is a unique ID generated by Unity storage system.
    type: str
  filesystem_name:
    description:
    - Name of the filesystem for which NFS export will be created.
    - Either filesystem or snapshot is required for creation of the NFS.
    - If I(filesystem_name) is specified, then I(nas_server) is required to uniquely
      identify the filesystem.
    - If filesystem parameter is provided, then snapshot cannot be specified.
    type: str
  filesystem_id:
    description:
    - ID of the filesystem.
    - This is a unique ID generated by Unity storage system.
    type: str
  snapshot_name:
    description:
    - Name of the snapshot for which NFS export will be created.
    - Either filesystem or snapshot is required for creation of the NFS
      export.
    - If snapshot parameter is provided, then filesystem cannot be specified.
    type: str
  snapshot_id:
    description:
    - ID of the snapshot.
    - This is a unique ID generated by Unity storage system.
    type: str
  nas_server_name:
    description:
    - Name of the NAS server on which filesystem will be hosted.
    type: str
  nas_server_id:
    description:
    - ID of the NAS server on which filesystem will be hosted.
    type: str
  path:
    description:
    - Local path to export relative to the NAS server root.
    - With NFS, each export of a file_system or file_snap must have a unique
      local path.
    - Mandatory while creating NFS export.
    type: str
  description:
    description:
    - Description of the NFS export.
    - Optional parameter when creating a NFS export.
    - To modify description, pass the new value in I(description) field.
    - To remove description, pass the empty value in I(description) field.
    type: str
  host_state:
    description:
    - Define whether the hosts can access the NFS export.
    - Required when adding or removing access of hosts from the export.
    type: str
    choices: ['present-in-export', 'absent-in-export']
  anonymous_uid:
    description:
    - Specifies the user ID of the anonymous account.
    - If not specified at the time of creation, it will be set to 4294967294.
    type: int
  anonymous_gid:
    description:
    - Specifies the group ID of the anonymous account.
    - If not specified at the time of creation, it will be set to 4294967294.
    type: int
  state:
    description:
    - State variable to determine whether NFS export will exist or not.
    required: true
    type: str
    choices: ['absent', 'present']
  default_access:
    description:
    - Default access level for all hosts that can access the NFS export.
    - For hosts that need different access than the default,
      they can be configured by adding to the list.
    - If I(default_access) is not mentioned during creation, then NFS export will
      be created with C(NO_ACCESS).
    type: str
    choices: ['NO_ACCESS', 'READ_ONLY', 'READ_WRITE', 'ROOT',
              'READ_ONLY_ROOT']
  min_security:
    description:
    - NFS enforced security type for users accessing a NFS export.
    - If not specified at the time of creation, it will be set to C(SYS).
    type: str
    choices: ['SYS', 'KERBEROS', 'KERBEROS_WITH_INTEGRITY',
              'KERBEROS_WITH_ENCRYPTION']
  adv_host_mgmt_enabled:
    description:
    - If C(false), allows you to specify hosts without first having to register them.
    - Mandatory while adding access hosts.
    type: bool
  no_access_hosts:
    description:
    - Hosts with no access to the NFS export.
    - List of dictionaries. Each dictionary will have any of the keys from
      I(host_name), I(host_id), I(subnet), I(netgroup), I(domain) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(true) then the accepted keys are I(host_name), I(host_id) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(false) then the accepted keys are I(host_name), I(subnet), I(netgroup), I(domain) and I(ip_address).
    type: list
    elements: dict
    suboptions:
      host_name:
        description:
        - Name of the host.
        type: str
      host_id:
        description:
        - ID of the host.
        type: str
      ip_address:
        description:
        - IP address of the host.
        type: str
      subnet:
        description:
        - Subnet can be an 'IP address/netmask' or 'IP address/prefix length'.
        type: str
      netgroup:
        description:
        - Netgroup that is defined in NIS or the local netgroup file.
        type: str
      domain:
        description:
        - DNS domain, where all NFS clients in the domain are included in the host list.
        type: str
  read_only_hosts:
    description:
    - Hosts with read-only access to the NFS export.
    - List of dictionaries. Each dictionary will have any of the keys from
      I(host_name), I(host_id), I(subnet), I(netgroup), I(domain) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(true) then the accepted keys are I(host_name), I(host_id) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(false) then the accepted keys are I(host_name), I(subnet), I(netgroup), I(domain) and I(ip_address).
    type: list
    elements: dict
    suboptions:
      host_name:
        description:
        - Name of the host.
        type: str
      host_id:
        description:
        - ID of the host.
        type: str
      ip_address:
        description:
        - IP address of the host.
        type: str
      subnet:
        description:
        - Subnet can be an 'IP address/netmask' or 'IP address/prefix length'.
        type: str
      netgroup:
        description:
        - Netgroup that is defined in NIS or the local netgroup file.
        type: str
      domain:
        description:
        - DNS domain, where all NFS clients in the domain are included in the host list.
        type: str
  read_only_root_hosts:
    description:
    - Hosts with read-only for root user access to the NFS export.
    - List of dictionaries. Each dictionary will have any of the keys from
      I(host_name), I(host_id), I(subnet), I(netgroup), I(domain) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(true) then the accepted keys are I(host_name), I(host_id) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(false) then the accepted keys are I(host_name), I(subnet), I(netgroup), I(domain) and I(ip_address).
    type: list
    elements: dict
    suboptions:
      host_name:
        description:
        - Name of the host.
        type: str
      host_id:
        description:
        - ID of the host.
        type: str
      ip_address:
        description:
        - IP address of the host.
        type: str
      subnet:
        description:
        - Subnet can be an 'IP address/netmask' or 'IP address/prefix length'.
        type: str
      netgroup:
        description:
        - Netgroup that is defined in NIS or the local netgroup file.
        type: str
      domain:
        description:
        - DNS domain, where all NFS clients in the domain are included in the host list.
        type: str
  read_write_hosts:
    description:
    - Hosts with read and write access to the NFS export.
    - List of dictionaries. Each dictionary will have any of the keys from
      I(host_name), I(host_id), I(subnet), I(netgroup), I(domain) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(true) then the accepted keys are I(host_name), I(host_id) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(false) then the accepted keys are I(host_name), I(subnet), I(netgroup), I(domain) and I(ip_address).
    type: list
    elements: dict
    suboptions:
      host_name:
        description:
        - Name of the host.
        type: str
      host_id:
        description:
        - ID of the host.
        type: str
      ip_address:
        description:
        - IP address of the host.
        type: str
      subnet:
        description:
        - Subnet can be an 'IP address/netmask' or 'IP address/prefix length'.
        type: str
      netgroup:
        description:
        - Netgroup that is defined in NIS or the local netgroup file.
        type: str
      domain:
        description:
        - DNS domain, where all NFS clients in the domain are included in the host list.
        type: str
  read_write_root_hosts:
    description:
    - Hosts with read and write for root user access to the NFS export.
    - List of dictionaries. Each dictionary will have any of the keys from
      I(host_name), I(host_id), I(subnet), I(netgroup), I(domain) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(true) then the accepted keys are I(host_name), I(host_id) and I(ip_address).
    - If I(adv_host_mgmt_enabled) is C(false) then the accepted keys are I(host_name), I(subnet), I(netgroup), I(domain) and I(ip_address).
    type: list
    elements: dict
    suboptions:
      host_name:
        description:
        - Name of the host.
        type: str
      host_id:
        description:
        - ID of the host.
        type: str
      ip_address:
        description:
        - IP address of the host.
        type: str
      subnet:
        description:
        - Subnet can be an 'IP address/netmask' or 'IP address/prefix length'.
        type: str
      netgroup:
        description:
        - Netgroup that is defined in NIS or the local netgroup file.
        type: str
      domain:
        description:
        - DNS domain, where all NFS clients in the domain are included in the host list.
        type: str
notes:
- The I(check_mode) is not supported.
"""

EXAMPLES = r"""
- name: Create nfs export from filesystem
  dellemc.unity.nfs:
    unispherehost: "{{unispherehost}}"
    username: "{{username}}"
    password: "{{password}}"
    validate_certs: "{{validate_certs}}"
    nfs_export_name: "ansible_nfs_from_fs"
    path: '/'
    filesystem_id: "fs_377"
    state: "present"

- name: Create nfs export from snapshot
  dellemc.unity.nfs:
    unispherehost: "{{unispherehost}}"
    username: "{{username}}"
    password: "{{password}}"
    validate_certs: "{{validate_certs}}"
    nfs_export_name: "ansible_nfs_from_snap"
    path: '/'
    snapshot_name: "ansible_fs_snap"
    state: "present"

- name: Modify nfs export
  dellemc.unity.nfs:
    unispherehost: "{{unispherehost}}"
    username: "{{username}}"
    password: "{{password}}"
    validate_certs: "{{validate_certs}}"
    nfs_export_name: "ansible_nfs_from_fs"
    nas_server_id: "nas_3"
    description: ""
    default_access: "READ_ONLY_ROOT"
    anonymous_gid: 4294967290
    anonymous_uid: 4294967290
    state: "present"

- name: Add host in nfs export with adv_host_mgmt_enabled as true
  dellemc.unity.nfs:
    unispherehost: "{{unispherehost}}"
    username: "{{username}}"
    password: "{{password}}"
    validate_certs: "{{validate_certs}}"
    nfs_export_name: "ansible_nfs_from_fs"
    filesystem_id: "fs_377"
    adv_host_mgmt_enabled: true
    no_access_hosts:
      - host_id: "Host_1"
    read_only_hosts:
      - host_id: "Host_2"
    read_only_root_hosts:
      - host_name: "host_name1"
    read_write_hosts:
      - host_name: "host_name2"
    read_write_root_hosts:
      - ip_address: "1.1.1.1"
    host_state: "present-in-export"
    state: "present"

- name: Remove host in nfs export with adv_host_mgmt_enabled as true
  dellemc.unity.nfs:
    unispherehost: "{{unispherehost}}"
    username: "{{username}}"
    password: "{{password}}"
    validate_certs: "{{validate_certs}}"
    nfs_export_name: "ansible_nfs_from_fs"
    filesystem_id: "fs_377"
    adv_host_mgmt_enabled: true
    no_access_hosts:
      - host_id: "Host_1"
    read_only_hosts:
      - host_id: "Host_2"
    read_only_root_hosts:
      - host_name: "host_name1"
    read_write_hosts:
      - host_name: "host_name2"
    read_write_root_hosts:
      - ip_address: "1.1.1.1"
    host_state: "absent-in-export"
    state: "present"

- name: Add host in nfs export with adv_host_mgmt_enabled as false
  dellemc.unity.nfs:
    unispherehost: "{{unispherehost}}"
    username: "{{username}}"
    password: "{{password}}"
    validate_certs: "{{validate_certs}}"
    nfs_export_name: "ansible_nfs_from_fs"
    filesystem_id: "fs_377"
    adv_host_mgmt_enabled: false
    no_access_hosts:
    - domain: "google.com"
    read_only_hosts:
    - netgroup: "netgroup_admin"
    read_only_root_hosts:
    - host_name: "host5"
    read_write_hosts:
    - subnet: "168.159.57.4/255.255.255.0"
    read_write_root_hosts:
    - ip_address: "10.255.2.4"
    host_state: "present-in-export"
    state: "present"

- name: Remove host in nfs export with adv_host_mgmt_enabled as false
  dellemc.unity.nfs:
    unispherehost: "{{unispherehost}}"
    username: "{{username}}"
    password: "{{password}}"
    validate_certs: "{{validate_certs}}"
    nfs_export_name: "ansible_nfs_from_fs"
    filesystem_id: "fs_377"
    adv_host_mgmt_enabled: false
    no_access_hosts:
    - domain: "google.com"
    read_only_hosts:
    - netgroup: "netgroup_admin"
    read_only_root_hosts:
    - host_name: "host5"
    read_write_hosts:
    - subnet: "168.159.57.4/255.255.255.0"
    read_write_root_hosts:
    - ip_address: "10.255.2.4"
    host_state: "absent-in-export"
    state: "present"

- name: Get nfs details
  dellemc.unity.nfs:
    unispherehost: "{{unispherehost}}"
    username: "{{username}}"
    password: "{{password}}"
    validate_certs: "{{validate_certs}}"
    nfs_export_id: "NFSShare_291"
    state: "present"

- name: Delete nfs export by nfs name
  dellemc.unity.nfs:
    unispherehost: "{{unispherehost}}"
    username: "{{username}}"
    password: "{{password}}"
    validate_certs: "{{validate_certs}}"
    nfs_export_name: "ansible_nfs_name"
    nas_server_name: "ansible_nas_name"
    state: "absent"
"""

RETURN = r"""
changed:
  description: Whether or not the resource has changed.
  returned: always
  type: bool
  sample: "false"

nfs_share_details:
  description: Details of the nfs export.
  returned: When nfs export exists.
  type: dict
  contains:
    anonymous_uid:
      description: User ID of the anonymous account
      type: int
    anonymous_gid:
      description: Group ID of the anonymous account
      type: int
    default_access:
      description: Default access level for all hosts that can access export
      type: str
    description:
      description: Description about the nfs export
      type: str
    id:
      description: ID of the nfs export
      type: str
    min_security:
      description: NFS enforced security type for users accessing an export
      type: str
    name:
      description: Name of the nfs export
      type: str
    no_access_hosts_string:
      description: Hosts with no access to the nfs export
      type: str
    read_only_hosts_string:
      description: Hosts with read-only access to the nfs export
      type: str
    read_only_root_hosts_string:
      description: Hosts with read-only for root user access to the nfs export
      type: str
    read_write_hosts_string:
      description: Hosts with read and write access to the nfs export
      type: str
    read_write_root_hosts_string:
      description: Hosts with read and write for root user access to export
      type: str
    type:
      description: NFS export type. i.e. filesystem or snapshot
      type: str
    export_paths:
      description: Export paths that can be used to mount and access export
      type: list
    filesystem:
      description: Details of the filesystem on which nfs export is present
      type: dict
      contains:
        UnityFileSystem:
          description: filesystem details
          type: dict
          contains:
            id:
              description: ID of the filesystem
              type: str
            name:
              description: Name of the filesystem
              type: str
    nas_server:
      description: Details of the nas server
      type: dict
      contains:
        UnityNasServer:
          description: NAS server details
          type: dict
          contains:
            id:
              description: ID of the nas server
              type: str
            name:
              description: Name of the nas server
              type: str
  sample: {
      'anonymous_gid': 4294967294,
      'anonymous_uid': 4294967294,
      'creation_time': '2022-03-09 15:05:34.720000+00:00',
      'default_access': 'NFSShareDefaultAccessEnum.NO_ACCESS',
      'description': '',
      'export_option': 1,
      'export_paths': [
      '**.***.**.**:/dummy-share-123'
      ],
      'filesystem': {
      'UnityFileSystem': {
          'id': 'fs_id_1',
          'name': 'fs_name_1'
      }
      },
      'host_accesses': None,
      'id': 'NFSShare_14393',
      'is_read_only': None,
      'min_security': 'NFSShareSecurityEnum.SYS',
      'modification_time': '2022-04-25 08:12:28.179000+00:00',
      'name': 'dummy-share-123',
      'nfs_owner_username': None,
      'no_access_hosts': None,
      'no_access_hosts_string': 'host1,**.***.*.*',
      'path': '/',
      'read_only_hosts': None,
      'read_only_hosts_string': '',
      'read_only_root_access_hosts': None,
      'read_only_root_hosts_string': '',
      'read_write_hosts': None,
      'read_write_hosts_string': '',
      'read_write_root_hosts_string': '',
      'role': 'NFSShareRoleEnum.PRODUCTION',
      'root_access_hosts': None,
      'snap': None,
      'type': 'NFSTypeEnum.NFS_SHARE',
      'existed': true,
      'nas_server': {
      'UnityNasServer': {
          'id': 'nas_id_1',
          'name': 'dummy_nas_server'
      }
      }
    }
"""

import re
import traceback

try:
    from ipaddress import ip_network, IPv4Network, IPv6Network
    HAS_IPADDRESS, IP_ADDRESS_IMP_ERR = True, None
except ImportError:
    HAS_IPADDRESS, IP_ADDRESS_IMP_ERR = False, traceback.format_exc()

from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \
    import utils

LOG = utils.get_logger('nfs')

DEFAULT_ACCESS_LIST = ['NO_ACCESS', 'READ_ONLY', 'READ_WRITE', 'ROOT',
                       'READ_ONLY_ROOT']
MIN_SECURITY_LIST = ['SYS', 'KERBEROS', 'KERBEROS_WITH_INTEGRITY',
                     'KERBEROS_WITH_ENCRYPTION']
HOST_DICT = dict(type='list', required=False, elements='dict',
                 options=dict(host_name=dict(),
                              host_id=dict(),
                              ip_address=dict(),
                              subnet=dict(),
                              netgroup=dict(),
                              domain=dict()))
HOST_STATE_LIST = ['present-in-export', 'absent-in-export']
STATE_LIST = ['present', 'absent']

application_type = "Ansible/1.7.1"


class NFS(object):
    """Class with nfs export operations"""

    def __init__(self):
        """ Define all parameters required by this module"""

        self.module_params = utils.get_unity_management_host_parameters()
        self.module_params.update(get_nfs_parameters())

        mutually_exclusive = [['nfs_export_id', 'nas_server_id'],
                              ['nfs_export_id', 'nas_server_name'],
                              ['filesystem_id', 'filesystem_name',
                               'snapshot_id', 'snapshot_name'],
                              ['nas_server_id', 'nas_server_name']]
        required_one_of = [['nfs_export_id', 'nfs_export_name']]

        """ initialize the ansible module """
        self.module = AnsibleModule(
            argument_spec=self.module_params, supports_check_mode=False,
            mutually_exclusive=mutually_exclusive,
            required_one_of=required_one_of)
        utils.ensure_required_libs(self.module)

        if not HAS_IPADDRESS:
            self.module.fail_json(msg=missing_required_lib("ipaddress"),
                                  exception=IP_ADDRESS_IMP_ERR)

        self.unity = utils.get_unity_unisphere_connection(self.module.params,
                                                          application_type)
        self.cli = self.unity._cli

        self.is_given_nfs_for_fs = None
        if self.module.params['filesystem_name'] or \
           self.module.params['filesystem_id']:
            self.is_given_nfs_for_fs = True
        elif self.module.params['snapshot_name'] or \
                self.module.params['snapshot_id']:
            self.is_given_nfs_for_fs = False

        # Contain hosts input & output parameters
        self.host_param_mapping = {
            'no_access_hosts': 'no_access_hosts_string',
            'read_only_hosts': 'read_only_hosts_string',
            'read_only_root_hosts': 'read_only_root_hosts_string',
            'read_write_hosts': 'read_write_hosts_string',
            'read_write_root_hosts': 'read_write_root_hosts_string'
        }

        # Default_access mapping. keys are giving by user & values are
        # accepted by SDK
        self.default_access = {'READ_ONLY_ROOT': 'RO_ROOT'}

        LOG.info('Got the unity instance for provisioning on Unity')

    def validate_host_access_data(self, host_dict):
        """
        Validate host access data
        :param host_dict: Host access data
        :return None
        """
        fqdn_pat = re.compile(r'(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}'
                              r'[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)')

        if host_dict.get('host_name'):
            version = get_ip_version(host_dict.get('host_name'))
            if version in (4, 6):
                msg = "IP4/IP6: %s given in host_name instead " \
                    "of name" % host_dict.get('host_name')
                LOG.error(msg)
                self.module.fail_json(msg=msg)

        if host_dict.get('ip_address'):
            ip_or_fqdn = host_dict.get('ip_address')
            version = get_ip_version(ip_or_fqdn)
            # validate its FQDN or not
            if version == 0 and not fqdn_pat.match(ip_or_fqdn):
                msg = "%s is not a valid FQDN" % ip_or_fqdn
                LOG.error(msg)
                self.module.fail_json(msg=msg)

        if host_dict.get('subnet'):
            subnet = host_dict.get('subnet')
            subnet_info = subnet.split("/")
            if len(subnet_info) != 2:
                msg = "Subnet should be in format 'IP address/netmask' or 'IP address/prefix length'"
                LOG.error(msg)
                self.module.fail_json(msg=msg)

    def validate_adv_host_mgmt_enabled_check(self, host_dict):
        """
        Validate adv_host_mgmt_enabled check
        :param host_dict: Host access data
        :return None
        """
        host_dict_keys_set = set(host_dict.keys())
        adv_host_mgmt_enabled_true_set = {'host_name', 'host_id', 'ip_address'}
        adv_host_mgmt_enabled_false_set = {'host_name', 'subnet', 'domain', 'netgroup', 'ip_address'}
        adv_host_mgmt_enabled_true_diff = host_dict_keys_set - adv_host_mgmt_enabled_true_set
        adv_host_mgmt_enabled_false_diff = host_dict_keys_set - adv_host_mgmt_enabled_false_set
        if self.module.params['adv_host_mgmt_enabled'] and adv_host_mgmt_enabled_true_diff != set():
            msg = "If 'adv_host_mgmt_enabled' is true then host access should only have  %s" % adv_host_mgmt_enabled_true_set
            LOG.error(msg)
            self.module.fail_json(msg=msg)
        elif not self.module.params['adv_host_mgmt_enabled'] and adv_host_mgmt_enabled_false_diff != set():
            msg = "If 'adv_host_mgmt_enabled' is false then host access should only have  %s" % adv_host_mgmt_enabled_false_set
            LOG.error(msg)
            self.module.fail_json(msg=msg)

    def validate_host_access_input_params(self):
        """
        Validate host access params
        :return None
        """
        for param in list(self.host_param_mapping.keys()):
            if self.module.params[param] and (not self.module.params[
                    'host_state'] or self.module.params[
                    'adv_host_mgmt_enabled'] is None):
                msg = "'host_state' and 'adv_host_mgmt_enabled' is required along with: %s" % param
                LOG.error(msg)
                self.module.fail_json(msg=msg)
            elif self.module.params[param]:
                for host_dict in self.module.params[param]:
                    host_dict = {k: v for k, v in host_dict.items() if v}
                    self.validate_adv_host_mgmt_enabled_check(host_dict)
                    self.validate_host_access_data(host_dict)

    def validate_module_attributes(self):
        """
        Validate module attributes
        :return None
        """
        param_list = ['nfs_export_name', 'nfs_export_id', 'filesystem_name',
                      'filesystem_id', 'nas_server_id',
                      'snapshot_name', 'snapshot_id', 'path']

        for param in param_list:
            if self.module.params[param] and \
               len(self.module.params[param].strip()) == 0:
                msg = "Please provide valid value for: %s" % param
                LOG.error(msg)
                self.module.fail_json(msg=msg)

    def validate_input(self):
        """ Validate input parameters """

        if self.module.params['nfs_export_name'] and \
                not self.module.params['snapshot_name'] and \
                not self.module.params['snapshot_id']:
            if ((self.module.params['filesystem_name']) and
                (not self.module.params['nas_server_id'] and
                 not self.module.params['nas_server_name'])):
                msg = "Please provide nas server id or name along with " \
                      "filesystem name and nfs name"
                LOG.error(msg)
                self.module.fail_json(msg=msg)

            if ((not self.module.params['nas_server_id']) and
                (not self.module.params['nas_server_name']) and
                    (not self.module.params['filesystem_id'])):
                msg = "Please provide either nas server id/name or " \
                      "filesystem id"
                LOG.error(msg)
                self.module.fail_json(msg=msg)
        self.validate_module_attributes()
        self.validate_host_access_input_params()

    def get_nfs_id_or_name(self):
        """ Provide nfs_export_id or nfs_export_name user given value

        :return: value provided by user in nfs_export_id/nfs_export_name
        :rtype: str
        """
        if self.module.params['nfs_export_id']:
            return self.module.params['nfs_export_id']
        return self.module.params['nfs_export_name']

    def get_nas_from_given_input(self):
        """ Get nas server object

        :return: nas server object
        :rtype: UnityNasServer
        """
        LOG.info("Getting nas server details")
        if not self.module.params['nas_server_id'] and not \
                self.module.params['nas_server_name']:
            return None
        id_or_name = self.module.params['nas_server_id'] if \
            self.module.params['nas_server_id'] else self.module.params[
            'nas_server_name']
        try:
            nas = self.unity.get_nas_server(
                _id=self.module.params['nas_server_id'],
                name=self.module.params['nas_server_name'])
        except utils.UnityResourceNotFoundError as e:
            # In case of incorrect name
            msg = "Given nas server not found error: %s" % str(e)
            LOG.error(msg)
            self.module.fail_json(msg=msg)
        except utils.HTTPClientError as e:
            if e.http_status == 401:
                msg = "Failed to get nas server: %s due to incorrect " \
                      "username/password error: %s" % (id_or_name, str(e))
            else:
                msg = "Failed to get nas server: %s error: %s" % (
                    id_or_name, str(e))
            LOG.error(msg)
            self.module.fail_json(msg=msg)
        except Exception as e:
            msg = "Failed to get nas server: %s error: %s" % (
                id_or_name, str(e))
            LOG.error(msg)
            self.module.fail_json(msg=msg)

        if nas and not nas.existed:
            # In case of incorrect id, sdk return nas object whose attribute
            # existed=false, instead of raising UnityResourceNotFoundError
            msg = "Please check nas details it does not exists"
            LOG.error(msg)
            self.module.fail_json(msg=msg)

        LOG.info("Got nas server details")
        return nas

    def get_nfs_share(self, id=None, name=None):
        """ Get the nfs export

        :return: nfs_export object if nfs exists else None
        :rtype: UnityNfsShare or None
        """
        try:
            if not id and not name:
                msg = "Please give nfs id/name"
                LOG.error(msg)
                self.module.fail_json(msg=msg)

            id_or_name = id if id else name
            LOG.info("Getting nfs export: %s", id_or_name)
            if id:
                # Get nfs details from nfs ID
                if self.is_given_nfs_for_fs:
                    nfs = self.unity.get_nfs_share(
                        _id=id, filesystem=self.fs_obj)
                elif self.is_given_nfs_for_fs is False:
                    # nfs from snap
                    nfs = self.unity.get_nfs_share(_id=id, snap=self.snap_obj)
                else:
                    nfs = self.unity.get_nfs_share(_id=id)
            else:
                # Get nfs details from nfs name
                if self.is_given_nfs_for_fs:
                    nfs = self.unity.get_nfs_share(
                        name=name, filesystem=self.fs_obj)
                elif self.is_given_nfs_for_fs is False:
                    # nfs from snap
                    nfs = self.unity.get_nfs_share(
                        name=name, snap=self.snap_obj)
                else:
                    nfs = self.unity.get_nfs_share(name=name)

            if isinstance(nfs, utils.UnityNfsShareList):
                # This block will be executed, when we are trying to get nfs
                # details using nfs name & nas server.
                nfs_list = nfs
                LOG.info("Multiple nfs export with same name: %s "
                         "found", id_or_name)
                if self.nas_obj:
                    for n in nfs_list:
                        if n.filesystem.nas_server == self.nas_obj:
                            return n
                    msg = "Multiple nfs share with same name: %s found. " \
                          "Given nas server is not correct. Please check"
                else:
                    msg = "Multiple nfs share with same name: %s found. " \
                          "Please give nas server"
            else:
                # nfs is instance of UnityNfsShare class
                if nfs and nfs.existed:
                    if self.nas_obj and nfs.filesystem.nas_server != \
                       self.nas_obj:
                        msg = "nfs found but nas details given is incorrect"
                        LOG.error(msg)
                        self.module.fail_json(msg=msg)
                    LOG.info("Successfully got nfs share for: %s", id_or_name)
                    return nfs
                elif nfs and not nfs.existed:
                    # in case of incorrect id, sdk returns nfs object whose
                    # attribute existed=False
                    msg = "Please check incorrect nfs id is given"
                else:
                    msg = "Failed to get nfs share: %s" % id_or_name
        except utils.UnityResourceNotFoundError as e:
            msg = "NFS share: %(id_or_name)s not found " \
                  "error: %(err)s" % {'id_or_name': id_or_name, 'err': str(e)}
            LOG.info(str(msg))
            return None
        except utils.HTTPClientError as e:
            if e.http_status == 401:
                msg = "Failed to get nfs share: %s due to incorrect " \
                      "username/password error: %s" % (id_or_name, str(e))
            else:
                msg = "Failed to get nfs share: %s error: %s" % (id_or_name,
                                                                 str(e))
        except utils.StoropsConnectTimeoutError as e:
            msg = "Failed to get nfs share: %s check unispherehost IP: %s " \
                  "error: %s" % (id_or_name,
                                 self.module.params['nfs_export_id'], str(e))
        except Exception as e:
            msg = "Failed to get nfs share: %s error: %s" % (id_or_name,
                                                             str(e))
        LOG.error(msg)
        self.module.fail_json(msg=msg)

    def delete_nfs_share(self, nfs_obj):
        """ Delete nfs share

        :param nfs: NFS share obj
        :type nfs: UnityNfsShare
        :return: None
        """
        try:
            LOG.info("Deleting nfs share: %s", self.get_nfs_id_or_name())
            nfs_obj.delete()
            LOG.info("Deleted nfs share")
        except Exception as e:
            msg = "Failed to delete nfs share, error: %s" % str(e)
            LOG.error(msg)
            self.module.fail_json(msg=msg)

    def get_filesystem(self):
        """ Get filesystem obj

        :return: filesystem obj
        :rtype: UnityFileSystem
        """
        if self.module.params['filesystem_id']:
            id_or_name = self.module.params['filesystem_id']
        elif self.module.params['filesystem_name']:
            id_or_name = self.module.params['filesystem_name']
        else:
            msg = "Please provide filesystem ID/name, to get filesystem"
            LOG.error(msg)
            self.module.fail_json(msg=msg)

        try:
            if self.module.params['filesystem_name']:
                if not self.nas_obj:
                    err_msg = "NAS Server is required to get the filesystem"
                    LOG.error(err_msg)
                    self.module.fail_json(msg=err_msg)
                LOG.info("Getting filesystem by name: %s", id_or_name)
                fs_obj = self.unity.get_filesystem(
                    name=self.module.params['filesystem_name'],
                    nas_server=self.nas_obj)
            elif self.module.params['filesystem_id']:
                LOG.info("Getting filesystem by ID: %s", id_or_name)
                fs_obj = self.unity.get_filesystem(
                    _id=self.module.params['filesystem_id'])
        except utils.UnityResourceNotFoundError as e:
            msg = "Filesystem: %s not found error: %s" % (
                id_or_name, str(e))
            LOG.error(msg)
            self.module.fail_json(msg=msg)
        except utils.HTTPClientError as e:
            if e.http_status == 401:
                msg = "Failed to get filesystem due to incorrect " \
                      "username/password error: %s" % str(e)
            else:
                msg = "Failed to get filesystem error: %s" % str(e)
            LOG.error(msg)
        except Exception as e:
            msg = "Failed to get filesystem: %s error: %s" % (
                id_or_name, str(e))
            LOG.error(msg)
            self.module.fail_json(msg=msg)

        if fs_obj and fs_obj.existed:
            LOG.info("Got the filesystem: %s", id_or_name)
            return fs_obj
        else:
            msg = "Filesystem: %s does not exists" % id_or_name
            LOG.error(msg)
            self.module.fail_json(msg=msg)

    def get_snapshot(self):
        """ Get snapshot obj

        :return: Snapshot obj
        :rtype: UnitySnap
        """
        if self.module.params['snapshot_id']:
            id_or_name = self.module.params['snapshot_id']
        elif self.module.params['snapshot_name']:
            id_or_name = self.module.params['snapshot_name']
        else:
            msg = "Please provide snapshot ID/name, to get snapshot"
            LOG.error(msg)
            self.module.fail_json(msg=msg)

        LOG.info("Getting snapshot: %s", id_or_name)
        try:
            if id_or_name:
                snap_obj = self.unity.get_snap(
                    _id=self.module.params['snapshot_id'],
                    name=self.module.params['snapshot_name'])
            else:
                msg = "Failed to get the snapshot. Please provide snapshot " \
                      "details"
                LOG.error(msg)
                self.module.fail_json(msg=msg)
        except utils.UnityResourceNotFoundError as e:
            msg = "Failed to get snapshot: %s error: %s" % (id_or_name,
                                                            str(e))
            LOG.error(msg)
            self.module.fail_json(msg=msg)
        except utils.HTTPClientError as e:
            if e.http_status == 401:
                msg = "Failed to get snapshot due to incorrect " \
                      "username/password error: %s" % str(e)
            else:
                msg = "Failed to get snapshot error: %s" % str(e)
            LOG.error(msg)
        except Exception as e:
            msg = "Failed to get snapshot: %s error: %s" % (id_or_name,
                                                            str(e))
            LOG.error(msg)
            self.module.fail_json(msg=msg)

        if snap_obj and snap_obj.existed:
            LOG.info("Successfully got the snapshot: %s", id_or_name)
            return snap_obj
        else:
            msg = "Snapshot: %s does not exists" % id_or_name
            LOG.error(msg)
            self.module.fail_json(msg=msg)

    def get_host_obj(self, host_id=None, host_name=None, ip_address=None):
        """
        Get host object
        :param host_id: ID of the host
        :param host_name: Name of the host
        :param ip_address: Network address of the host
        :return: Host object
        :rtype: object
        """
        try:
            host_obj = None
            host = None
            if host_id:
                host = host_id
                host_obj = self.unity.get_host(_id=host_id)
            elif host_name:
                host = host_name
                host_obj = self.unity.get_host(name=host_name)
            elif ip_address:
                host = ip_address
                host_obj = self.unity.get_host(address=ip_address)

            if host_obj and host_obj.existed:
                LOG.info("Successfully got host: %s", host_obj.name)
                return host_obj
            else:
                msg = f'Host : {host} does not exists'
                LOG.error(msg)
                self.module.fail_json(msg=msg)

        except Exception as e:
            msg = f'Failed to get host {host}, error: {e}'
            LOG.error(msg)
            self.module.fail_json(msg=msg)

    def get_host_access_string_value(self, host_dict):
        """
        Form host access string
        :host_dict Host access type info
        :return Host access data in string
        """
        if host_dict.get("host_id"):
            return self.get_host_obj(host_id=(host_dict.get("host_id"))).name + ','
        elif host_dict.get("host_name"):
            return host_dict.get(
                "host_name") + ','
        elif host_dict.get("ip_address"):
            return host_dict.get(
                "ip_address") + ','
        elif host_dict.get("subnet"):
            return host_dict.get(
                "subnet") + ','
        elif host_dict.get("domain"):
            return "*." + host_dict.get(
                "domain") + ','
        elif host_dict.get("netgroup"):
            return "@" + host_dict.get(
                "netgroup") + ','

    def get_host_obj_value(self, host_dict):
        """
        Form host access value using host object
        :host_dict Host access type info
        :return Host object
        """
        if host_dict.get("host_id"):
            return self.get_host_obj(host_id=host_dict.get("host_id"))
        elif host_dict.get("host_name"):
            return self.get_host_obj(host_name=host_dict.get("host_name"))
        elif host_dict.get("ip_address"):
            return self.get_host_obj(ip_address=host_dict.get("ip_address"))

    def format_host_dict_for_adv_mgmt(self):
        """
        Form host access for advance management
        :return: Formatted Host access type info
        :rtype: dict
        """
        result_host = {}
        for param in list(self.host_param_mapping.keys()):
            if self.module.params[param]:
                result_host[param] = []
                for host_dict in self.module.params[param]:
                    result_host[param].append(self.get_host_obj_value(host_dict))

        if 'read_only_root_hosts' in result_host:
            result_host['read_only_root_access_hosts'] = result_host.pop('read_only_root_hosts')
        if 'read_write_root_hosts' in result_host:
            result_host['root_access_hosts'] = result_host.pop('read_write_root_hosts')
        return result_host

    def format_host_dict_for_non_adv_mgmt(self):
        """
        Form host access for non advance management option
        :return: Formatted Host access type info
        :rtype: dict
        """
        result_host = {}
        for param in list(self.host_param_mapping.keys()):
            if self.module.params[param]:
                result_host[param] = ''
                for host_dict in self.module.params[param]:
                    result_host[param] += self.get_host_access_string_value(host_dict)

        if result_host != {}:
            # Since we are supporting HOST STRING parameters instead of HOST
            # parameters, so lets change given input HOST parameter name to
            # HOST STRING parameter name and strip trailing ','
            result_host = {self.host_param_mapping[k]: v[:-1] for k, v in result_host.items()}
        return result_host

    def get_host_dict_from_pb(self):
        """ Traverse all given hosts params and provides with host dict,
            which has respective host str param name with its value
            required by SDK

        :return: dict with key named as respective host str param name & value
                required by SDK
        :rtype: dict
        """
        LOG.info("Getting host parameters")
        result_host = {}
        if self.module.params['host_state']:
            if not self.module.params['adv_host_mgmt_enabled']:
                result_host = self.format_host_dict_for_non_adv_mgmt()
            else:
                result_host = self.format_host_dict_for_adv_mgmt()
        return result_host

    def get_adv_param_from_pb(self):
        """ Provide all the advance parameters named as required by SDK

        :return: all given advanced parameters
        :rtype: dict
        """
        param = {}
        LOG.info("Getting all given advance parameter")
        host_dict = self.get_host_dict_from_pb()
        if host_dict:
            param.update(host_dict)

        fields = ('description', 'anonymous_uid', 'anonymous_gid')
        for field in fields:
            if self.module.params[field] is not None:
                param[field] = self.module.params[field]

        if self.module.params['min_security'] and self.module.params[
                'min_security'] in utils.NFSShareSecurityEnum.__members__:
            LOG.info("Getting min_security object from NFSShareSecurityEnum")
            param['min_security'] = utils.NFSShareSecurityEnum[
                self.module.params['min_security']]

        if self.module.params['default_access']:
            param['default_access'] = self.get_default_access()

        LOG.info("Successfully got advance parameter: %s", param)
        return param

    def get_default_access(self):
        LOG.info("Getting default_access object from "
                 "NFSShareDefaultAccessEnum")
        default_access = self.default_access.get(
            self.module.params['default_access'],
            self.module.params['default_access'])
        try:
            return utils.NFSShareDefaultAccessEnum[default_access]
        except KeyError as e:
            msg = "default_access: %s not found error: %s" % (
                default_access, str(e))
            LOG.error(msg)
            self.module.fail_json(msg)

    def correct_payload_as_per_sdk(self, payload, nfs_details=None):
        """ Correct payload keys as required by SDK

        :param payload: Payload used for create/modify operation
        :type payload: dict
        :param nfs_details: NFS details
        :type nfs_details: dict
        :return: Payload required by SDK
        :rtype: dict
        """
        ouput_host_param = self.host_param_mapping.values()
        if set(payload.keys()) & set(ouput_host_param):
            if not nfs_details or (nfs_details and nfs_details['export_option'] != 1):
                payload['export_option'] = 1
            if 'read_write_root_hosts_string' in payload:
                # SDK have param named 'root_access_hosts_string' instead of
                # 'read_write_root_hosts_string'
                payload['root_access_hosts_string'] = payload.pop(
                    'read_write_root_hosts_string')

        return payload

    def create_nfs_share_from_filesystem(self):
        """ Create nfs share from given filesystem

        :return: nfs_share object
        :rtype: UnityNfsShare
        """

        name = self.module.params['nfs_export_name']
        path = self.module.params['path']

        if not name or not path:
            msg = "Please provide name and path both for create"
            LOG.error(msg)
            self.module.fail_json(msg=msg)

        param = self.get_adv_param_from_pb()
        if 'default_access' in param:
            # create nfs from FILESYSTEM take 'share_access' as param in SDK
            param['share_access'] = param.pop('default_access')
            LOG.info("Param name: 'share_access' is used instead of "
                     "'default_access' in SDK so changed")

        param = self.correct_payload_as_per_sdk(param)

        LOG.info("Creating nfs share from filesystem with param: %s", param)
        try:
            nfs_obj = utils.UnityNfsShare.create(
                cli=self.cli, name=name, fs=self.fs_obj, path=path, **param)
            LOG.info("Successfully created nfs share: %s", nfs_obj)
            return nfs_obj
        except utils.UnityNfsShareNameExistedError as e:
            LOG.error(str(e))
            self.module.fail_json(msg=str(e))
        except Exception as e:
            msg = "Failed to create nfs share: %s error: %s" % (name, str(e))
            LOG.error(msg)
            self.module.fail_json(msg=msg)

    def create_nfs_share_from_snapshot(self):
        """ Create nfs share from given snapshot

        :return: nfs_share object
        :rtype: UnityNfsShare
        """

        name = self.module.params['nfs_export_name']
        path = self.module.params['path']

        if not name or not path:
            msg = "Please provide name and path both for create"
            LOG.error(msg)
            self.module.fail_json(msg=msg)

        param = self.get_adv_param_from_pb()

        param = self.correct_payload_as_per_sdk(param)

        LOG.info("Creating nfs share from snap with param: %s", param)
        try:
            nfs_obj = utils.UnityNfsShare.create_from_snap(
                cli=self.cli, name=name, snap=self.snap_obj, path=path, **param)
            LOG.info("Successfully created nfs share: %s", nfs_obj)
            return nfs_obj
        except utils.UnityNfsShareNameExistedError as e:
            LOG.error(str(e))
            self.module.fail_json(msg=str(e))
        except Exception as e:
            msg = "Failed to create nfs share: %s error: %s" % (name, str(e))
            LOG.error(msg)
            self.module.fail_json(msg=msg)

    def create_nfs_share(self):
        """ Create nfs share from either filesystem/snapshot

        :return: nfs_share object
        :rtype: UnityNfsShare
        """
        if self.is_given_nfs_for_fs:
            # Share to be created from filesystem
            return self.create_nfs_share_from_filesystem()
        elif self.is_given_nfs_for_fs is False:
            # Share to be created from snapshot
            return self.create_nfs_share_from_snapshot()
        else:
            msg = "Please provide filesystem or filesystem snapshot to create NFS export"
            LOG.error(msg)
            self.module.fail_json(msg=msg)

    def convert_host_str_to_list(self, host_str):
        """ Convert host_str which have comma separated hosts to host_list with
            ip4/ip6 host obj if IP4/IP6 like string found

        :param host_str: hosts str separated by comma
        :return: hosts list, which may contains IP4/IP6 object if given in
                host_str
        :rytpe: list
        """
        if not host_str:
            LOG.debug("Empty host_str given")
            return []

        host_list = []
        try:
            for h in host_str.split(","):
                version = get_ip_version(h)
                if version == 4:
                    h = u'{0}'.format(h)
                    h = IPv4Network(h, strict=False)
                elif version == 6:
                    h = u'{0}'.format(h)
                    h = IPv6Network(h, strict=False)
                host_list.append(h)
        except Exception as e:
            msg = "Error while converting host_str: %s to list error: %s" % (
                host_str, str(e))
            LOG.error(msg)
            self.module.fail_json(msg=msg)
        return host_list

    def add_host_dict_for_adv(self, existing_host_dict, new_host_dict):
        """ Compares & adds up new hosts with the existing ones and provide
            the final consolidated hosts for advance host management

        :param existing_host_dict: All hosts params details which are
            associated with existing nfs which to be modified
        :type existing_host_dict: dict
        :param new_host_dict: All hosts param details which are to be added
        :type new_host_dict: dict
        :return: consolidated hosts params details which contains newly added
            hosts along with the existing ones
        :rtype: dict
        """
        modify_host_dict = {}
        for host_access_key in existing_host_dict:
            LOG.debug("Checking for param: %s", host_access_key)
            new_host_obj_list = new_host_dict[host_access_key]
            if new_host_obj_list and not existing_host_dict[host_access_key]:
                # Existing nfs host is empty so lets directly add
                # new_host_str as it is
                LOG.debug("Existing nfs host key: %s is empty, so lets add new host given value as it is", host_access_key)
                modify_host_dict[host_access_key] = new_host_obj_list
                continue

            existing_host_obj_list = [self.get_host_obj(host_id=existing_host_dict['UnityHost']['id'])
                                      for existing_host_dict in existing_host_dict[host_access_key]['UnityHostList']]

            if not new_host_obj_list:
                LOG.debug("Nothing to add as no host given")
                continue

            existing_set = set(host.id for host in existing_host_obj_list)
            actual_to_add = [new_host for new_host in new_host_obj_list if new_host.id not in existing_set]

            if not actual_to_add:
                LOG.debug("All host given to be added is already added")
                continue

            # Lets extends actual_to_add list, which is new with existing
            actual_to_add.extend(existing_host_obj_list)
            modify_host_dict[host_access_key] = actual_to_add

        return modify_host_dict

    def add_host_dict_for_non_adv(self, existing_host_dict, new_host_dict):
        """ Compares & adds up new hosts with the existing ones and provide
            the final consolidated hosts for non advance host management

        :param existing_host_dict: All hosts params details which are
            associated with existing nfs which to be modified
        :type existing_host_dict: dict
        :param new_host_dict: All hosts param details which are to be added
        :type new_host_dict: dict
        :return: consolidated hosts params details which contains newly added
            hosts along with the existing ones
        :rtype: dict
        """
        modify_host_dict = {}
        for host_access_key in existing_host_dict:
            LOG.debug("Checking add host for param: %s", host_access_key)
            existing_host_str = existing_host_dict[host_access_key]
            existing_host_list = self.convert_host_str_to_list(
                existing_host_str)

            new_host_str = new_host_dict[host_access_key]
            new_host_list = self.convert_host_str_to_list(
                new_host_str)

            if not new_host_list:
                LOG.debug("Nothing to add as no host given")
                continue

            if new_host_list and not existing_host_list:
                # Existing nfs host is empty so lets directly add
                # new_host_str as it is
                LOG.debug("Existing nfs host key: %s is empty, so lets add new host given value as it is", host_access_key)
                modify_host_dict[host_access_key] = new_host_str
                continue

            actual_to_add = list(set(new_host_list) - set(existing_host_list))
            if not actual_to_add:
                LOG.debug("All host given to be added is already added")
                continue

            # Lets extends actual_to_add list, which is new with existing
            actual_to_add.extend(existing_host_list)

            # Since SDK takes host_str as ',' separated instead of list, so
            # lets convert str to list
            # Note: explicity str() needed here to convert IP4/IP6 object
            modify_host_dict[host_access_key] = ",".join(str(v) for v in actual_to_add)
        return modify_host_dict

    def remove_host_dict_for_adv(self, existing_host_dict, new_host_dict):
        """ Compares & remove new hosts from the existing ones and provide
            the remaining hosts for advance host management

        :param existing_host_dict: All hosts params details which are
            associated with existing nfs which to be modified
        :type existing_host_dict: dict
        :param new_host_dict: All hosts param details which are to be removed
        :type new_host_dict: dict
        :return: existing hosts params details from which given new hosts are
            removed
        :rtype: dict
        """
        modify_host_dict = {}
        for host_access_key in existing_host_dict:
            LOG.debug("Checking host for param: %s", host_access_key)
            if not existing_host_dict[host_access_key]:
                # existing list is already empty, so nothing to remove
                LOG.debug("Existing list is already empty, so nothing to remove")
                continue

            existing_host_obj_list = [self.get_host_obj(host_id=existing_host_dict['UnityHost']['id'])
                                      for existing_host_dict in existing_host_dict[host_access_key]['UnityHostList']]
            new_host_obj_list = new_host_dict[host_access_key]

            if new_host_obj_list == []:
                LOG.debug("Nothing to remove as no host given")
                continue

            unique_new_host_list = [new_host.id for new_host in new_host_obj_list]
            if len(new_host_obj_list) > len(set(unique_new_host_list)):
                msg = f'Duplicate host given: {unique_new_host_list} in host param: {host_access_key}'
                LOG.error(msg)
                self.module.fail_json(msg=msg)

            unique_existing_host_list = [host.id for host in existing_host_obj_list]
            actual_to_remove = list(set(unique_new_host_list) & set(
                unique_existing_host_list))
            if not actual_to_remove:
                continue

            final_host_list = [existing_host for existing_host in existing_host_obj_list if existing_host.id not in unique_new_host_list]

            modify_host_dict[host_access_key] = final_host_list

        return modify_host_dict

    def remove_host_dict_for_non_adv(self, existing_host_dict, new_host_dict):
        """ Compares & remove new hosts from the existing ones and provide
            the remaining hosts for non advance host management

        :param existing_host_dict: All hosts params details which are
            associated with existing nfs which to be modified
        :type existing_host_dict: dict
        :param new_host_dict: All hosts param details which are to be removed
        :type new_host_dict: dict
        :return: existing hosts params details from which given new hosts are
            removed
        :rtype: dict
        """
        modify_host_dict = {}

        for host_access_key in existing_host_dict:
            LOG.debug("Checking remove host for param: %s", host_access_key)
            existing_host_str = existing_host_dict[host_access_key]
            existing_host_list = self.convert_host_str_to_list(
                existing_host_str)

            new_host_str = new_host_dict[host_access_key]
            new_host_list = self.convert_host_str_to_list(
                new_host_str)

            if not new_host_list:
                LOG.debug("Nothing to remove as no host given")
                continue

            if len(new_host_list) > len(set(new_host_list)):
                msg = "Duplicate host given: %s in host param: %s" % (
                    new_host_list, host_access_key)
                LOG.error(msg)
                self.module.fail_json(msg=msg)

            if new_host_list and not existing_host_list:
                # existing list is already empty, so nothing to remove
                LOG.debug("Existing list is already empty, so nothing to remove")
                continue

            actual_to_remove = list(set(new_host_list) & set(
                existing_host_list))
            if not actual_to_remove:
                continue

            final_host_list = list(set(existing_host_list) - set(
                actual_to_remove))

            # Since SDK takes host_str as ',' separated instead of list, so
            # lets convert str to list
            # Note: explicity str() needed here to convert IP4/IP6 object
            modify_host_dict[host_access_key] = ",".join(str(v) for v in final_host_list)

        return modify_host_dict

    def add_host(self, existing_host_dict, new_host_dict):
        """ Compares & adds up new hosts with the existing ones and provide
            the final consolidated hosts

        :param existing_host_dict: All hosts params details which are
            associated with existing nfs which to be modified
        :type existing_host_dict: dict
        :param new_host_dict: All hosts param details which are to be added
        :type new_host_dict: dict
        :return: consolidated hosts params details which contains newly added
            hosts along with the existing ones
        :rtype: dict
        """
        if self.module.params['adv_host_mgmt_enabled']:
            modify_host_dict = self.add_host_dict_for_adv(existing_host_dict, new_host_dict)
        else:
            modify_host_dict = self.add_host_dict_for_non_adv(existing_host_dict, new_host_dict)

        return modify_host_dict

    def remove_host(self, existing_host_dict, new_host_dict):
        """ Compares & remove new hosts from the existing ones and provide
            the remaining hosts

        :param existing_host_dict: All hosts params details which are
            associated with existing nfs which to be modified
        :type existing_host_dict: dict
        :param new_host_dict: All hosts param details which are to be removed
        :type new_host_dict: dict
        :return: existing hosts params details from which given new hosts are
            removed
        :rtype: dict
        """
        if self.module.params['adv_host_mgmt_enabled']:
            modify_host_dict = self.remove_host_dict_for_adv(existing_host_dict, new_host_dict)
        else:
            modify_host_dict = self.remove_host_dict_for_non_adv(existing_host_dict, new_host_dict)

        return modify_host_dict

    def modify_nfs_share(self, nfs_obj):
        """ Modify given nfs share

        :param nfs_obj: NFS share obj
        :type nfs_obj: UnityNfsShare
        :return: tuple(bool, nfs_obj)
            - bool: indicates whether nfs_obj is modified or not
            - nfs_obj: same nfs_obj if not modified else modified nfs_obj
        :rtype: tuple
        """
        modify_param = {}
        LOG.info("Modifying nfs share")

        nfs_details = nfs_obj._get_properties()
        fields = ('description', 'anonymous_uid', 'anonymous_gid')
        for field in fields:
            if self.module.params[field] is not None and \
                    self.module.params[field] != nfs_details[field]:
                modify_param[field] = self.module.params[field]

        if self.module.params['min_security'] and self.module.params[
                'min_security'] != nfs_obj.min_security.name:
            modify_param['min_security'] = utils.NFSShareSecurityEnum[
                self.module.params['min_security']]

        if self.module.params['default_access']:
            default_access = self.get_default_access()
            if default_access != nfs_obj.default_access:
                modify_param['default_access'] = default_access

        new_host_dict = self.get_host_dict_from_pb()
        if new_host_dict:
            try:
                if is_nfs_have_host_with_host_obj(nfs_details) and not self.module.params['adv_host_mgmt_enabled']:
                    msg = "Modification of nfs host is restricted using adv_host_mgmt_enabled as false since nfs " \
                          "already have host added using host obj"
                    LOG.error(msg)
                    self.module.fail_json(msg=msg)
                elif is_nfs_have_host_with_host_string(nfs_details) and self.module.params['adv_host_mgmt_enabled']:
                    msg = "Modification of nfs host is restricted using adv_host_mgmt_enabled as true since nfs " \
                          "already have host added without host obj"
                    LOG.error(msg)
                    self.module.fail_json(msg=msg)
                LOG.info("Extracting same given param from nfs")
                existing_host_dict = {k: nfs_details[k] for k in new_host_dict}
            except KeyError as e:
                msg = "Failed to extract key-value from current nfs: %s" % \
                      str(e)
                LOG.error(msg)
                self.module.fail_json(msg=msg)

            if self.module.params['host_state'] == HOST_STATE_LIST[0]:
                # present-in-export
                LOG.info("Getting host to be added")
                modify_host_dict = self.add_host(existing_host_dict, new_host_dict)
            else:
                # absent-in-export
                LOG.info("Getting host to be removed")
                modify_host_dict = self.remove_host(existing_host_dict, new_host_dict)

            if modify_host_dict:
                modify_param.update(modify_host_dict)

        if not modify_param:
            LOG.info("Existing nfs attribute value is same as given input, "
                     "so returning same nfs object - idempotency case")
            return False, nfs_obj

        modify_param = self.correct_payload_as_per_sdk(
            modify_param, nfs_details)

        try:
            resp = nfs_obj.modify(**modify_param)
            resp.raise_if_err()
        except Exception as e:
            msg = "Failed to modify nfs error: %s" % str(e)
            LOG.error(msg)
            self.module.fail_json(msg=msg)

        return True, self.get_nfs_share(id=nfs_obj.id)

    def perform_module_operation(self):
        """ Perform different actions on nfs based on user parameter
            chosen in playbook """

        changed = False
        nfs_share_details = {}

        self.validate_input()

        self.nas_obj = None
        if self.module.params['nas_server_id'] or self.module.params[
                'nas_server_name']:
            self.nas_obj = self.get_nas_from_given_input()

        self.fs_obj = None
        self.snap_obj = None
        if self.is_given_nfs_for_fs:
            self.fs_obj = self.get_filesystem()
        elif self.is_given_nfs_for_fs is False:
            self.snap_obj = self.get_snapshot()

        # Get nfs Share
        nfs_obj = self.get_nfs_share(
            id=self.module.params['nfs_export_id'],
            name=self.module.params['nfs_export_name']
        )

        # Delete nfs Share
        if self.module.params['state'] == STATE_LIST[1]:
            if nfs_obj:
                # delete_nfs_share() does not return any value
                # In case of successful delete, lets nfs_obj set None
                # to avoid fetching and displaying attribute
                nfs_obj = self.delete_nfs_share(nfs_obj)
                changed = True
        elif not nfs_obj:
            # create
            nfs_obj = self.create_nfs_share()
            changed = True
        else:
            # modify
            changed, nfs_obj = self.modify_nfs_share(nfs_obj)

        # Get display attributes
        if self.module.params['state'] and nfs_obj:
            nfs_share_details = get_nfs_share_display_attrs(nfs_obj)

        result = {"changed": changed,
                  "nfs_share_details": nfs_share_details}
        self.module.exit_json(**result)


def get_nfs_share_display_attrs(nfs_obj):
    """ Provide nfs share attributes for display

    :param nfs: NFS share obj
    :type nfs: UnityNfsShare
    :return: nfs_share_details
    :rtype: dict
    """
    LOG.info("Getting nfs share details from nfs share object")
    nfs_share_details = nfs_obj._get_properties()

    # Adding filesystem_name to nfs_share_details
    LOG.info("Updating filesystem details")
    nfs_share_details['filesystem']['UnityFileSystem']['name'] = \
        nfs_obj.filesystem.name
    if 'id' not in nfs_share_details['filesystem']['UnityFileSystem']:
        nfs_share_details['filesystem']['UnityFileSystem']['id'] = \
            nfs_obj.filesystem.id

    # Adding nas server details
    LOG.info("Updating nas server details")
    nas_details = nfs_obj.filesystem._get_properties()['nas_server']
    nas_details['UnityNasServer']['name'] = \
        nfs_obj.filesystem.nas_server.name
    nfs_share_details['nas_server'] = nas_details

    # Adding snap.id & snap.name if nfs_obj is for snap
    if is_nfs_obj_for_snap(nfs_obj):
        LOG.info("Updating snap details")
        nfs_share_details['snap']['UnitySnap']['id'] = nfs_obj.snap.id
        nfs_share_details['snap']['UnitySnap']['name'] = nfs_obj.snap.name

    LOG.info("Successfully updated nfs share details")
    return nfs_share_details


def is_nfs_have_host_with_host_obj(nfs_details):
    """ Check whether nfs host is already added using host obj

    :param nfs_details: nfs details
    :return: True if nfs have host already added with host obj else False
    :rtype: bool
    """
    host_obj_params = ('no_access_hosts', 'read_only_hosts',
                       'read_only_root_access_hosts', 'read_write_hosts',
                       'root_access_hosts')
    for host_obj_param in host_obj_params:
        if nfs_details.get(host_obj_param):
            return True
    return False


def is_nfs_have_host_with_host_string(nfs_details):
    """ Check whether nfs host is already added using host by string method

    :param nfs_details: nfs details
    :return: True if nfs have host already added with host string method else False
    :rtype: bool
    """
    host_obj_params = (
        'no_access_hosts_string',
        'read_only_hosts_string',
        'read_only_root_hosts_string',
        'read_write_hosts_string',
        'read_write_root_hosts_string'
    )
    for host_obj_param in host_obj_params:
        if nfs_details.get(host_obj_param):
            return True
    return False


def get_ip_version(val):
    try:
        val = u'{0}'.format(val)
        ip = ip_network(val, strict=False)
        return ip.version
    except ValueError:
        return 0


def is_nfs_obj_for_fs(nfs_obj):
    """ Check whether the nfs_obj if for filesystem

    :param nfs_obj: NFS share object
    :return: True if nfs_obj is of filesystem type
    :rtype: bool
    """
    if nfs_obj.type == utils.NFSTypeEnum.NFS_SHARE:
        return True
    return False


def is_nfs_obj_for_snap(nfs_obj):
    """ Check whether the nfs_obj if for snapshot

    :param nfs_obj: NFS share object
    :return: True if nfs_obj is of snapshot type
    :rtype: bool
    """
    if nfs_obj.type == utils.NFSTypeEnum.NFS_SNAPSHOT:
        return True
    return False


def get_nfs_parameters():
    """ Provides parameters required for the NFS share module on Unity """

    return dict(
        nfs_export_name=dict(required=False, type='str'),
        nfs_export_id=dict(required=False, type='str'),
        filesystem_id=dict(required=False, type='str'),
        filesystem_name=dict(required=False, type='str'),
        snapshot_id=dict(required=False, type='str'),
        snapshot_name=dict(required=False, type='str'),
        nas_server_id=dict(required=False, type='str'),
        nas_server_name=dict(required=False, type='str'),
        path=dict(required=False, type='str', no_log=True),
        description=dict(required=False, type='str'),
        default_access=dict(required=False, type='str',
                            choices=DEFAULT_ACCESS_LIST),
        min_security=dict(required=False, type='str',
                          choices=MIN_SECURITY_LIST),
        adv_host_mgmt_enabled=dict(required=False, type='bool', default=None),
        no_access_hosts=HOST_DICT,
        read_only_hosts=HOST_DICT,
        read_only_root_hosts=HOST_DICT,
        read_write_hosts=HOST_DICT,
        read_write_root_hosts=HOST_DICT,
        host_state=dict(required=False, type='str', choices=HOST_STATE_LIST),
        anonymous_uid=dict(required=False, type='int'),
        anonymous_gid=dict(required=False, type='int'),
        state=dict(required=True, type='str', choices=STATE_LIST)
    )


def main():
    """ Create UnityNFS object and perform action on it
        based on user input from playbook"""
    obj = NFS()
    obj.perform_module_operation()


if __name__ == '__main__':
    main()
