#!/usr/bin/python
# -*- coding: utf-8 -*-

#
# Dell OpenManage Ansible Modules
# Version 9.3.0
# Copyright (C) 2021-2025 Dell Inc. or its subsidiaries. All Rights Reserved.

# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#

from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

DOCUMENTATION = """
---
module: ome_device_group
short_description: Add or remove device(s) from a static device group on OpenManage Enterprise
version_added: "3.3.0"
description: This module allows to add or remove device(s) from a static device group on OpenManage Enterprise.
extends_documentation_fragment:
  - dellemc.openmanage.oment_auth_options
options:
  state:
    type: str
    description:
      - C(present) allows to add the device(s) to a static device group.
      - C(absent) allows to remove the device(s) from a static device group.
    choices: [present, absent]
    default: present
  name:
    type: str
    description:
      - Name of the static group.
      - I(name) is mutually exclusive with I(group_id).
  group_id:
    type: int
    description:
      - ID of the static device.
      - I(group_id) is mutually exclusive with I(name).
  device_ids:
    type: list
    elements: int
    description:
      - List of ID(s) of the device(s) to be added or removed from the device group.
      - I(device_ids) is mutually exclusive with I(device_service_tags) and I(ip_addresses).
  device_service_tags:
    type: list
    elements: str
    description:
      - List of service tag(s) of the device(s) to be added or removed from the device group.
      - I(device_service_tags) is mutually exclusive with I(device_ids) and I(ip_addresses).
  ip_addresses:
    type: list
    elements: str
    description:
      - List of IPs of the device(s) to be added or removed from the device group.
      - I(ip_addresses) is mutually exclusive with I(device_ids) and I(device_service_tags).
      - "Supported  IP address range formats:"
      - "    - 192.35.0.1"
      - "    - 10.36.0.0-192.36.0.255"
      - "    - 192.37.0.0/24"
      - "    - fe80::ffff:ffff:ffff:ffff"
      - "    - fe80::ffff:192.0.2.0/125"
      - "    - fe80::ffff:ffff:ffff:1111-fe80::ffff:ffff:ffff:ffff"
      - C(NOTE) Hostname is not supported.
      - C(NOTE) I(ip_addresses) requires python's netaddr packages to work on IP Addresses.
      - C(NOTE) This module reports success even if one of the IP addresses provided in the I(ip_addresses) list is
       available in OpenManage Enterprise.The module reports failure only if none of the IP addresses provided in the
        list are available in OpenManage Enterprise.
requirements:
  - "python >= 3.9.6"
  - "netaddr >= 0.7.19"
author:
  - "Felix Stephen (@felixs88)"
  - "Sajna Shetty(@Sajna-Shetty)"
  - "Abhishek Sinha (@Abhishek-Dell)"
notes:
  - Run this module from a system that has direct access to Dell OpenManage Enterprise.
  - This module supports C(check_mode).
"""

EXAMPLES = """
---
- name: Add devices to a static device group by using the group name and device IDs
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    name: "Storage Services"
    device_ids:
      - 11111
      - 11112
      - 11113

- name: Add devices to a static device group by using the group name and device service tags
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    name: "Storage Services"
    device_service_tags:
      - GHRT2RL
      - KJHDF3S
      - LKIJNG6

- name: Add devices to a static device group by using the group ID and device service tags
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    group_id: 12345
    device_service_tags:
      - GHRT2RL
      - KJHDF3S

- name: Add devices to a static device group by using the group name and IPv4 addresses
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    name: "Storage Services"
    ip_addresses:
      - 192.35.0.1
      - 192.35.0.5

- name: Add devices to a static device group by using the group ID and IPv6 addresses
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    group_id: 12345
    ip_addresses:
      - fe80::ffff:ffff:ffff:ffff
      - fe80::ffff:ffff:ffff:2222

- name: Add devices to a static device group by using the group ID and supported IPv4 and IPv6 address formats.
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    group_id: 12345
    ip_addresses:
      - 192.35.0.1
      - 10.36.0.0-192.36.0.255
      - 192.37.0.0/24
      - fe80::ffff:ffff:ffff:ffff
      - ::ffff:192.0.2.0/125
      - fe80::ffff:ffff:ffff:1111-fe80::ffff:ffff:ffff:ffff

- name: Remove devices from a static device group by using the group name and device IDs
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    state: "absent"
    name: "Storage Services"
    device_ids:
      - 11111
      - 11112
      - 11113

- name: Remove devices from a static device group by using the group name and device service tags
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    state: "absent"
    name: "Storage Services"
    device_service_tags:
      - GHRT2RL
      - KJHDF3S
      - LKIJNG6

- name: Remove devices from a static device group by using the group ID and device service tags
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    state: "absent"
    group_id: 12345
    device_service_tags:
      - GHRT2RL
      - KJHDF3S

- name: Remove devices from a static device group by using the group name and IPv4 addresses
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    state: "absent"
    name: "Storage Services"
    ip_addresses:
      - 192.35.0.1
      - 192.35.0.5

- name: Remove devices from a static device group by using the group ID and IPv6 addresses
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    state: "absent"
    group_id: 12345
    ip_addresses:
      - fe80::ffff:ffff:ffff:ffff
      - fe80::ffff:ffff:ffff:2222

- name: Remove devices from a static device group by using the group ID and supported IPv4 and IPv6 address formats.
  dellemc.openmanage.ome_device_group:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    state: "absent"
    group_id: 12345
    ip_addresses:
      - 192.35.0.1
      - 10.36.0.0-192.36.0.255
      - 192.37.0.0/24
      - fe80::ffff:ffff:ffff:ffff
      - ::ffff:192.0.2.0/125
      - fe80::ffff:ffff:ffff:1111-fe80::ffff:ffff:ffff:ffff
"""


RETURN = """
---
msg:
  type: str
  description: Overall status of the device group settings.
  returned: always
  sample:
  - "Successfully added member(s) to the device group."
group_id:
  type: int
  description: ID of the group.
  returned: success
  sample: 21078
ip_addresses_added:
  type: list
  description: IP Addresses which are added to the device group.
  returned: success
  sample: 21078
error_info:
  description: Details of the HTTP Error.
  returned: on HTTP error
  type: dict
  sample: {
    "error": {
      "code": "Base.1.0.GeneralError",
      "message": "A general error has occurred. See ExtendedInfo for more information.",
      "@Message.ExtendedInfo": [
        {
          "MessageId": "GEN1234",
          "RelatedProperties": [],
          "Message": "Unable to process the request because an error occurred.",
          "MessageArgs": [],
          "Severity": "Critical",
          "Resolution": "Retry the operation. If the issue persists, contact your system administrator."
        }
      ]
    }
  }
"""

import json
from ssl import SSLError
from ansible_collections.dellemc.openmanage.plugins.module_utils.ome import RestOME, OmeAnsibleModule
from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
from ansible.module_utils.urls import ConnectionError

try:
    from netaddr import IPAddress, IPNetwork, IPRange
    from netaddr.core import AddrFormatError

    HAS_NETADDR = True
except ImportError:
    HAS_NETADDR = False

GROUP_URI = "GroupService/Groups"
DEVICE_URI = "DeviceService/Devices"
ADD_MEMBER_URI = "GroupService/Actions/GroupService.AddMemberDevices"
REMOVE_MEMBER_URI = "GroupService/Actions/GroupService.RemoveMemberDevices"
ADD_STATIC_GROUP_MESSAGE = "Devices can be added only to the static device groups created using OpenManage Enterprise."
REMOVE_STATIC_GROUP_MESSAGE = "Devices can be removed only from the static device groups created using OpenManage Enterprise."
NETADDR_ERROR = "The module requires python's netaddr be installed on the ansible controller to work on IP Addresses."
INVALID_IP_FORMAT = "The format {0} of the IP address provided is not supported or invalid."
IP_NOT_EXISTS = "The IP addresses provided do not exist in OpenManage Enterprise."


def validate_group(group_resp, module, identifier, identifier_val):
    if not group_resp:
        module.fail_json(msg="Unable to complete the operation because the entered "
                             "target group {identifier} '{val}' is invalid.".format(identifier=identifier,
                                                                                    val=identifier_val))
    system_groups = group_resp["TypeId"]
    membership_id = group_resp["MembershipTypeId"]
    if system_groups != 3000 or (system_groups == 3000 and membership_id == 24):
        msg = ADD_STATIC_GROUP_MESSAGE if module.params.get("state", "present") == "present" else \
            REMOVE_STATIC_GROUP_MESSAGE
        module.fail_json(msg=msg)


def get_group_id(rest_obj, module):
    group_name = module.params.get("name")
    group_id = module.params.get("group_id")
    if group_name is not None:
        group_resp = rest_obj.invoke_request("GET", GROUP_URI,
                                             query_param={"$filter": "Name eq '{0}'".format(group_name)})
        value = group_resp.json_data.get("value")
        if value:
            value = value[0]
        else:
            value = []
        validate_group(value, module, "name", group_name)
        group_id = value["Id"]

    else:
        uri = GROUP_URI + "(" + str(group_id) + ")"
        try:
            group_resp = rest_obj.invoke_request("GET", uri)
            validate_group(group_resp.json_data, module, "Id", group_id)
        except HTTPError:
            validate_group({}, module, "Id", group_id)
    return group_id


def get_all_ips(ip_addresses, module):
    ip_addresses_list = []
    for ip in ip_addresses:
        try:
            if "/" in ip:
                cidr_list = IPNetwork(ip)
                ip_addresses_list.append(cidr_list)
            elif "-" in ip and ip.count("-") == 1:
                range_addr = ip.split("-")
                range_list = IPRange(range_addr[0], range_addr[1])
                ip_addresses_list.append(range_list)
            else:
                single_ip = IPAddress(ip)
                ip_addresses_list.append(single_ip)
        except (AddrFormatError, ValueError):
            module.fail_json(msg=INVALID_IP_FORMAT.format(ip))
    return ip_addresses_list


def get_device_id_from_ip(ip_addresses, device_list, module):
    ip_map = dict(
        [(each_device["DeviceManagement"][0]["NetworkAddress"], each_device["Id"]) for each_device in device_list
         if each_device["DeviceManagement"]])
    device_id_list_map = {}
    for available_ip, device_id in ip_map.items():
        for ip_formats in ip_addresses:
            if isinstance(ip_formats, IPAddress):
                try:
                    ome_ip = IPAddress(available_ip)
                except AddrFormatError:
                    ome_ip = IPAddress(available_ip.replace(']', '').replace('[', ''))
                if ome_ip == ip_formats:
                    device_id_list_map.update({device_id: str(ip_formats)})
            if not isinstance(ip_formats, IPAddress):
                try:
                    ome_ip = IPAddress(available_ip)
                except AddrFormatError:
                    ome_ip = IPAddress(available_ip.replace(']', '').replace('[', ''))
                if ome_ip in ip_formats:
                    device_id_list_map.update({device_id: str(ome_ip)})
    if len(device_id_list_map) == 0:
        module.fail_json(msg=IP_NOT_EXISTS)
    return device_id_list_map


def get_device_id(rest_obj, module):
    device_id_list = module.params.get("device_ids")
    device_tag_list = module.params.get("device_service_tags")
    ip_addresses = module.params.get("ip_addresses")
    device_list = rest_obj.get_all_report_details(DEVICE_URI)
    invalid, each_device_list, each_tag_to_id = [], [], []
    if device_id_list or device_tag_list:
        if device_id_list:
            key = "Id"
            each_device_list = device_id_list
        elif device_tag_list:
            key = "DeviceServiceTag"
            each_device_list = device_tag_list

        for each in each_device_list:
            each_device = list(filter(lambda d: d[key] in [each], device_list["report_list"]))
            if key == "DeviceServiceTag" and each_device:
                each_tag_to_id.append(each_device[0]["Id"])
            if not each_device:
                invalid.append(str(each))
        if invalid:
            value = "id" if key == "Id" else "service tag"
            module.fail_json(msg="Unable to complete the operation because the entered "
                                 "target device {0}(s) '{1}' are invalid.".format(value, ",".join(set(invalid))))
        if each_tag_to_id:
            each_device_list = each_tag_to_id
    else:
        all_ips = get_all_ips(ip_addresses, module)
        each_device_list = get_device_id_from_ip(all_ips, device_list["report_list"], module)
        key = "IPAddresses"
    return each_device_list, key


def add_member_to_group(module, rest_obj, group_id, device_id, key):
    group_device = rest_obj.get_all_report_details("{0}({1})/Devices".format(GROUP_URI, group_id))
    device_exists, device_not_exists, added_ips = [], [], []
    if key != "IPAddresses":
        for each in device_id:
            each_device = list(filter(lambda d: d["Id"] in [each], group_device["report_list"]))
            if each_device:
                tag_or_id = each_device[0][key] if key == "DeviceServiceTag" else each
                device_exists.append(str(tag_or_id))
            else:
                device_not_exists.append(each)
    else:
        already_existing_id = []
        for device in group_device["report_list"]:
            if device["Id"] in device_id:
                device_exists.append(device_id[device["Id"]])
                already_existing_id.append(device["Id"])
        device_not_exists = list(set(device_id.keys()) - set(already_existing_id))
        added_ips = [ip for d_id, ip in device_id.items() if d_id in device_not_exists]
    if module.check_mode and device_not_exists:
        module.exit_json(msg="Changes found to be applied.", changed=True, group_id=group_id)
    elif module.check_mode and not device_not_exists:
        module.exit_json(msg="No changes found to be applied.", group_id=group_id)

    if device_exists and not device_not_exists:
        module.exit_json(
            msg="No changes found to be applied.",
            group_id=group_id
        )
    payload = {"GroupId": group_id, "MemberDeviceIds": device_not_exists}
    response = rest_obj.invoke_request("POST", ADD_MEMBER_URI, data=payload)
    return response, added_ips


def get_current_member_of_group(rest_obj, group_id):
    group_device = rest_obj.get_all_report_details("{0}({1})/Devices".format(GROUP_URI, group_id))

    device_id_list = [each["Id"] for each in group_device["report_list"]]
    return device_id_list


def remove_member_from_group(module, rest_obj, group_id, device_id, current_device_list):
    payload_device_list = [each_id for each_id in device_id if each_id in current_device_list]

    if module.check_mode and payload_device_list:
        module.exit_json(msg="Changes found to be applied.", changed=True, group_id=group_id)

    if not payload_device_list:
        module.exit_json(msg="No changes found to be applied.", group_id=group_id)

    payload = {"GroupId": group_id, "MemberDeviceIds": payload_device_list}
    response = rest_obj.invoke_request("POST", REMOVE_MEMBER_URI, data=payload)
    return response


def main():
    specs = {
        "name": {"type": "str"},
        "group_id": {"type": "int"},
        "state": {"required": False, "type": "str", "choices": ["present", "absent"], "default": "present"},
        "device_service_tags": {"required": False, "type": "list", "elements": 'str'},
        "device_ids": {"required": False, "type": "list", "elements": 'int'},
        "ip_addresses": {"required": False, "type": "list", "elements": 'str'},
    }

    module = OmeAnsibleModule(
        argument_spec=specs,
        required_if=[
            ["state", "present", ("device_ids", "device_service_tags", "ip_addresses"), True],
        ],
        mutually_exclusive=[
            ("name", "group_id"),
            ("device_ids", "device_service_tags", "ip_addresses"),
        ],
        required_one_of=[("name", "group_id"),
                         ("device_ids", "device_service_tags", "ip_addresses")],
        supports_check_mode=True
    )

    try:
        if module.params.get("ip_addresses") and not HAS_NETADDR:
            module.fail_json(msg=NETADDR_ERROR)
        with RestOME(module.params, req_session=True) as rest_obj:
            group_id = get_group_id(rest_obj, module)
            device_id, key = get_device_id(rest_obj, module)
            if module.params["state"] == "present":
                response, added_ips = add_member_to_group(module, rest_obj, group_id, device_id, key)
                if added_ips:
                    module.exit_json(msg="Successfully added member(s) to the device group.",
                                     group_id=group_id, changed=True, ip_addresses_added=added_ips)
                module.exit_json(msg="Successfully added member(s) to the device group.",
                                 group_id=group_id, changed=True)
            else:
                current_device_list = get_current_member_of_group(rest_obj, group_id)
                remove_member_from_group(module, rest_obj, group_id, device_id, current_device_list)
                module.exit_json(msg="Successfully removed member(s) from the device group.", changed=True)
    except HTTPError as err:
        module.fail_json(msg=str(err), error_info=json.load(err))
    except URLError as err:
        module.exit_json(msg=str(err), unreachable=True)
    except (IOError, ValueError, SSLError, TypeError, ConnectionError, AttributeError,
            IndexError, KeyError, OSError) as err:
        module.fail_json(msg=str(err))


if __name__ == '__main__':
    main()
