#!/usr/bin/python
# Copyright (c) 2022, Masayoshi Mizuma <msys.mizuma@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

DOCUMENTATION = r"""
author:
  - Masayoshi Mizuma (@mizumm)
module: pmem
short_description: Configure Intel Optane Persistent Memory modules
version_added: 4.5.0
description:
  - This module allows Configuring Intel Optane Persistent Memory modules (PMem) using C(ipmctl) and C(ndctl) command line
    tools.
requirements:
  - C(ipmctl) and C(ndctl) command line tools
  - xmltodict
extends_documentation_fragment:
  - community.general._attributes
attributes:
  check_mode:
    support: none
  diff_mode:
    support: none
options:
  appdirect:
    description:
      - Percentage of the total capacity to use in AppDirect Mode (V(0)-V(100)).
      - Create AppDirect capacity utilizing hardware interleaving across the requested PMem modules if applicable given the
        specified target.
      - Total of O(appdirect), O(memorymode) and O(reserved) must be V(100).
    type: int
  appdirect_interleaved:
    description:
      - Create AppDirect capacity that is interleaved any other PMem modules.
    type: bool
    default: true
  memorymode:
    description:
      - Percentage of the total capacity to use in Memory Mode (V(0)-V(100)).
    type: int
  reserved:
    description:
      - Percentage of the capacity to reserve (V(0)-V(100)). O(reserved) is not mapped into the system physical address space
        and is presented as reserved capacity with Show Device and Show Memory Resources Commands.
      - O(reserved) is set automatically if this is not configured.
    type: int
  socket:
    description:
      - This enables to set the configuration for each socket by using the socket ID.
      - Total of O(appdirect), O(memorymode) and O(reserved) must be V(100) within one socket.
    type: list
    elements: dict
    suboptions:
      id:
        description: The socket ID of the PMem module.
        type: int
        required: true
      appdirect:
        description:
          - Percentage of the total capacity to use in AppDirect Mode (V(0)-V(100)) within the socket ID.
        type: int
        required: true
      appdirect_interleaved:
        description:
          - Create AppDirect capacity that is interleaved any other PMem modules within the socket ID.
        type: bool
        default: true
      memorymode:
        description:
          - Percentage of the total capacity to use in Memory Mode (V(0)-V(100)) within the socket ID.
        type: int
        required: true
      reserved:
        description:
          - Percentage of the capacity to reserve (V(0)-V(100)) within the socket ID.
        type: int
  namespace:
    description:
      - This enables to set the configuration for the namespace of the PMem.
    type: list
    elements: dict
    suboptions:
      mode:
        description:
          - The mode of namespace. The detail of the mode is in the man page of ndctl-create-namespace.
        type: str
        required: true
        choices: ['raw', 'sector', 'fsdax', 'devdax']
      type:
        description:
          - The type of namespace. The detail of the type is in the man page of ndctl-create-namespace.
        type: str
        choices: ['pmem', 'blk']
      size:
        description:
          - The size of namespace. This option supports the suffixes V(k) or V(K) or V(KB) for KiB, V(m) or V(M) or V(MB)
            for MiB, V(g) or V(G) or V(GB) for GiB and V(t) or V(T) or V(TB) for TiB.
          - This option is required if multiple namespaces are configured.
          - If this option is not set, all of the available space of a region is configured.
        type: str
  namespace_append:
    description:
      - Enable to append the new namespaces to the system.
      - The default is V(false) so the all existing namespaces not listed in O(namespace) are removed.
    type: bool
    default: false
"""

RETURN = r"""
reboot_required:
  description: Indicates that the system reboot is required to complete the PMem configuration.
  returned: success
  type: bool
  sample: true
result:
  description:
    - Shows the value of AppDirect, Memory Mode and Reserved size in bytes.
    - If O(socket) argument is provided, shows the values in each socket with C(socket) which contains the socket ID.
    - If O(namespace) argument is provided, shows the detail of each namespace.
  returned: success
  type: list
  elements: dict
  contains:
    appdirect:
      description: AppDirect size in bytes.
      type: int
    memorymode:
      description: Memory Mode size in bytes.
      type: int
    reserved:
      description: Reserved size in bytes.
      type: int
    socket:
      description: The socket ID to be configured.
      type: int
    namespace:
      description: The list of the detail of namespace.
      type: list
  sample:
    [
      {
        "appdirect": 111669149696,
        "memorymode": 970662608896,
        "reserved": 3626500096,
        "socket": 0
      },
      {
        "appdirect": 111669149696,
        "memorymode": 970662608896,
        "reserved": 3626500096,
        "socket": 1
      }
    ]
"""

EXAMPLES = r"""
- name: Configure the Pmem as AppDirect 10, Memory Mode 70, and the Reserved 20 percent.
  community.general.pmem:
    appdirect: 10
    memorymode: 70

- name: Configure the Pmem as AppDirect 10, Memory Mode 80, and the Reserved 10 percent.
  community.general.pmem:
    appdirect: 10
    memorymode: 80
    reserved: 10

- name: Configure the Pmem as AppDirect with not interleaved 10, Memory Mode 70, and the Reserved 20 percent.
  community.general.pmem:
    appdirect: 10
    appdirect_interleaved: false
    memorymode: 70

- name: Configure the Pmem each socket.
  community.general.pmem:
    socket:
      - id: 0
        appdirect: 10
        appdirect_interleaved: false
        memorymode: 70
        reserved: 20
      - id: 1
        appdirect: 10
        memorymode: 80
        reserved: 10

- name: Configure the two namespaces.
  community.general.pmem:
    namespace:
      - size: 1GB
        type: pmem
        mode: raw
      - size: 320MB
        type: pmem
        mode: sector
"""

import json
import traceback

from ansible.module_utils.basic import AnsibleModule, human_to_bytes, missing_required_lib

XMLTODICT_LIBRARY_IMPORT_ERROR: str | None
try:
    import xmltodict
except ImportError:
    HAS_XMLTODICT_LIBRARY = False
    XMLTODICT_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
    HAS_XMLTODICT_LIBRARY = True
    XMLTODICT_LIBRARY_IMPORT_ERROR = None


class PersistentMemory:
    def __init__(self):
        module = AnsibleModule(
            argument_spec=dict(
                appdirect=dict(type="int"),
                appdirect_interleaved=dict(type="bool", default=True),
                memorymode=dict(type="int"),
                reserved=dict(type="int"),
                socket=dict(
                    type="list",
                    elements="dict",
                    options=dict(
                        id=dict(required=True, type="int"),
                        appdirect=dict(required=True, type="int"),
                        appdirect_interleaved=dict(type="bool", default=True),
                        memorymode=dict(required=True, type="int"),
                        reserved=dict(type="int"),
                    ),
                ),
                namespace=dict(
                    type="list",
                    elements="dict",
                    options=dict(
                        mode=dict(required=True, type="str", choices=["raw", "sector", "fsdax", "devdax"]),
                        type=dict(type="str", choices=["pmem", "blk"]),
                        size=dict(type="str"),
                    ),
                ),
                namespace_append=dict(type="bool", default=False),
            ),
            required_together=(["appdirect", "memorymode"],),
            required_one_of=(["appdirect", "memorymode", "socket", "namespace"],),
            mutually_exclusive=(
                ["appdirect", "socket"],
                ["memorymode", "socket"],
                ["appdirect", "namespace"],
                ["memorymode", "namespace"],
                ["socket", "namespace"],
                ["appdirect", "namespace_append"],
                ["memorymode", "namespace_append"],
                ["socket", "namespace_append"],
            ),
        )

        if not HAS_XMLTODICT_LIBRARY:
            module.fail_json(msg=missing_required_lib("xmltodict"), exception=XMLTODICT_LIBRARY_IMPORT_ERROR)

        self.ipmctl_exec = module.get_bin_path("ipmctl", True)
        self.ndctl_exec = module.get_bin_path("ndctl", True)

        self.appdirect = module.params["appdirect"]
        self.interleaved = module.params["appdirect_interleaved"]
        self.memmode = module.params["memorymode"]
        self.reserved = module.params["reserved"]
        self.socket = module.params["socket"]
        self.namespace = module.params["namespace"]
        self.namespace_append = module.params["namespace_append"]

        self.module = module
        self.changed = False
        self.result = []

    def pmem_run_command(self, command, returnCheck=True):
        # in case command[] has number
        cmd = [str(part) for part in command]

        self.module.log(msg=f"pmem_run_command: execute: {cmd}")

        rc, out, err = self.module.run_command(cmd)

        self.module.log(msg=f"pmem_run_command: result: {out}")

        if returnCheck and rc != 0:
            self.module.fail_json(msg=f"Error while running: {cmd}", rc=rc, out=out, err=err)

        return out

    def pmem_run_ipmctl(self, command, returnCheck=True):
        command = [self.ipmctl_exec] + command

        return self.pmem_run_command(command, returnCheck)

    def pmem_run_ndctl(self, command, returnCheck=True):
        command = [self.ndctl_exec] + command

        return self.pmem_run_command(command, returnCheck)

    def pmem_is_dcpmm_installed(self):
        # To check this system has dcpmm
        command = ["show", "-system", "-capabilities"]
        return self.pmem_run_ipmctl(command)

    def pmem_get_region_align_size(self, region):
        aligns = []
        for rg in region:
            if rg["align"] not in aligns:
                aligns.append(rg["align"])

        return aligns

    def pmem_get_available_region_size(self, region):
        available_size = []
        for rg in region:
            available_size.append(rg["available_size"])

        return available_size

    def pmem_get_available_region_type(self, region):
        types = []
        for rg in region:
            if rg["type"] not in types:
                types.append(rg["type"])

        return types

    def pmem_argument_check(self):
        def namespace_check(self):
            command = ["list", "-R"]
            out = self.pmem_run_ndctl(command)
            if not out:
                return "Available region(s) is not in this system."
            region = json.loads(out)

            aligns = self.pmem_get_region_align_size(region)
            if len(aligns) != 1:
                return "Not supported the regions whose alignment size is different."

            available_size = self.pmem_get_available_region_size(region)
            types = self.pmem_get_available_region_type(region)
            for ns in self.namespace:
                if ns["size"]:
                    try:
                        size_byte = human_to_bytes(ns["size"])
                    except ValueError:
                        return "The format of size: NNN TB|GB|MB|KB|T|G|M|K|B"

                    if size_byte % aligns[0] != 0:
                        return f"size: {ns['size']} should be align with {aligns[0]}"

                    is_space_enough = False
                    for i, avail in enumerate(available_size):
                        if avail > size_byte:
                            available_size[i] -= size_byte
                            is_space_enough = True
                            break

                    if is_space_enough is False:
                        return f"There is not available region for size: {ns['size']}"

                    ns["size_byte"] = size_byte

                elif len(self.namespace) != 1:
                    return "size option is required to configure multiple namespaces"

                if ns["type"] not in types:
                    return f"type {ns['type']} is not supported in this system. Supported type: {types}"

            return None

        def percent_check(self, appdirect, memmode, reserved=None):
            if appdirect is None or (appdirect < 0 or appdirect > 100):
                return "appdirect percent should be from 0 to 100."
            if memmode is None or (memmode < 0 or memmode > 100):
                return "memorymode percent should be from 0 to 100."

            if reserved is None:
                if appdirect + memmode > 100:
                    return "Total percent should be less equal 100."
            else:
                if reserved < 0 or reserved > 100:
                    return "reserved percent should be from 0 to 100."
                if appdirect + memmode + reserved != 100:
                    return "Total percent should be 100."

        def socket_id_check(self):
            command = ["show", "-o", "nvmxml", "-socket"]
            out = self.pmem_run_ipmctl(command)
            sockets_dict = xmltodict.parse(out, dict_constructor=dict)["SocketList"]["Socket"]
            socket_ids = []
            for sl in sockets_dict:
                socket_ids.append(int(sl["SocketID"], 16))

            for skt in self.socket:
                if skt["id"] not in socket_ids:
                    return f"Invalid socket number: {skt['id']}"

            return None

        if self.namespace:
            return namespace_check(self)
        elif self.socket is None:
            return percent_check(self, self.appdirect, self.memmode, self.reserved)
        else:
            ret = socket_id_check(self)
            if ret is not None:
                return ret

            for skt in self.socket:
                ret = percent_check(self, skt["appdirect"], skt["memorymode"], skt["reserved"])
                if ret is not None:
                    return ret

            return None

    def pmem_remove_namespaces(self):
        command = ["list", "-N"]
        out = self.pmem_run_ndctl(command)

        # There's nothing namespaces in this system. Nothing to do.
        if not out:
            return

        namespaces = json.loads(out)

        # Disable and destroy all namespaces
        for ns in namespaces:
            command = ["disable-namespace", ns["dev"]]
            self.pmem_run_ndctl(command)

            command = ["destroy-namespace", ns["dev"]]
            self.pmem_run_ndctl(command)

        return

    def pmem_delete_goal(self):
        # delete the goal request
        command = ["delete", "-goal"]
        self.pmem_run_ipmctl(command)

    def pmem_init_env(self):
        if self.namespace is None or (self.namespace and self.namespace_append is False):
            self.pmem_remove_namespaces()
        if self.namespace is None:
            self.pmem_delete_goal()

    def pmem_get_capacity(self, skt=None):
        command = ["show", "-d", "Capacity", "-u", "B", "-o", "nvmxml", "-dimm"]
        if skt:
            command += ["-socket", skt["id"]]
        out = self.pmem_run_ipmctl(command)

        dimm_list = xmltodict.parse(out, dict_constructor=dict)["DimmList"]["Dimm"]
        capacity = 0
        for entry in dimm_list:
            for key, v in entry.items():
                if key == "Capacity":
                    capacity += int(v.split()[0])

        return capacity

    def pmem_create_memory_allocation(self, skt=None):
        def build_ipmctl_creation_opts(self, skt=None):
            ipmctl_opts = []

            if skt:
                appdirect = skt["appdirect"]
                memmode = skt["memorymode"]
                reserved = skt["reserved"]
                socket_id = skt["id"]
                ipmctl_opts += ["-socket", socket_id]
            else:
                appdirect = self.appdirect
                memmode = self.memmode
                reserved = self.reserved

            if reserved is None:
                res = 100 - memmode - appdirect
                ipmctl_opts += [f"memorymode={memmode}", f"reserved={res}"]
            else:
                ipmctl_opts += [f"memorymode={memmode}", f"reserved={reserved}"]

            if self.interleaved:
                ipmctl_opts += ["PersistentMemoryType=AppDirect"]
            else:
                ipmctl_opts += ["PersistentMemoryType=AppDirectNotInterleaved"]

            return ipmctl_opts

        def is_allocation_good(self, ipmctl_out, command):
            ignore_error = "Do you want to continue? [y/n] Error: Invalid data input."

            errmsg = ""
            rc = True
            for line in ipmctl_out.splitlines():
                if line.startswith("WARNING"):
                    errmsg = f"{line} (command: {command})"
                    rc = False
                    break
                elif "Error" in line:
                    if not line.startswith(ignore_error):
                        errmsg = f"{line} (command: {command})"
                        rc = False
                        break

            return rc, errmsg

        def get_allocation_result(self, goal, skt=None):
            ret = {"appdirect": 0, "memorymode": 0}

            if skt:
                ret["socket"] = skt["id"]

            out = xmltodict.parse(goal, dict_constructor=dict)["ConfigGoalList"]["ConfigGoal"]
            for entry in out:
                # Probably it is a bug of ipmctl to show the socket goal
                # which isn't specified by the -socket option.
                # Anyway, filter the noise out here:
                if skt and skt["id"] != int(entry["SocketID"], 16):
                    continue

                for key, v in entry.items():
                    if key == "MemorySize":
                        ret["memorymode"] += int(v.split()[0])
                    elif key == "AppDirect1Size" or key == "AapDirect2Size":
                        ret["appdirect"] += int(v.split()[0])

            capacity = self.pmem_get_capacity(skt)
            ret["reserved"] = capacity - ret["appdirect"] - ret["memorymode"]

            return ret

        reboot_required = False

        ipmctl_opts = build_ipmctl_creation_opts(self, skt)

        # First, do dry run ipmctl create command to check the error and warning.
        command = ["create", "-goal"] + ipmctl_opts
        out = self.pmem_run_ipmctl(command, returnCheck=False)
        rc, errmsg = is_allocation_good(self, out, command)
        if rc is False:
            return reboot_required, {}, errmsg

        # Run actual creation here
        command = ["create", "-u", "B", "-o", "nvmxml", "-force", "-goal"] + ipmctl_opts
        goal = self.pmem_run_ipmctl(command)
        ret = get_allocation_result(self, goal, skt)
        reboot_required = True

        return reboot_required, ret, ""

    def pmem_config_namespaces(self, namespace):
        command = ["create-namespace", "-m", namespace["mode"]]
        if namespace["type"]:
            command += ["-t", namespace["type"]]
        if "size_byte" in namespace:
            command += ["-s", namespace["size_byte"]]

        self.pmem_run_ndctl(command)

        return None


def main():
    pmem = PersistentMemory()

    pmem.pmem_is_dcpmm_installed()

    error = pmem.pmem_argument_check()
    if error:
        pmem.module.fail_json(msg=error)

    pmem.pmem_init_env()
    pmem.changed = True

    if pmem.namespace:
        for ns in pmem.namespace:
            pmem.pmem_config_namespaces(ns)

        command = ["list", "-N"]
        out = pmem.pmem_run_ndctl(command)
        all_ns = json.loads(out)

        pmem.result = all_ns
        reboot_required = False
    elif pmem.socket is None:
        reboot_required, ret, errmsg = pmem.pmem_create_memory_allocation()
        if errmsg:
            pmem.module.fail_json(msg=errmsg)
        pmem.result.append(ret)
    else:
        for skt in pmem.socket:
            skt_reboot_required, skt_ret, skt_errmsg = pmem.pmem_create_memory_allocation(skt)

            if skt_errmsg:
                pmem.module.fail_json(msg=skt_errmsg)

            if skt_reboot_required:
                reboot_required = True

            pmem.result.append(skt_ret)

    pmem.module.exit_json(changed=pmem.changed, reboot_required=reboot_required, result=pmem.result)


if __name__ == "__main__":
    main()
