#!/usr/bin/env python

# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from ansible.module_utils.basic import AnsibleModule

DOCUMENTATION = '''
---
module: OVS DPDK PMD CPU's check
short_description: Run PMD CPU's from all the NUMA nodes check
description:
    - Run PMD CPU's from all the NUMA nodes check
options:
    pmd_cpu_mask:
        required: true
        description:
           - The pmd cpu mask value
        type: str
author: "Jaganathan Palanisamy"
'''

EXAMPLES = '''
- hosts: ComputeOvsDpdk
  vars:
    pmd_cpu_mask: "1010010000000001"
  tasks:
  - name: Run PMD CPU's check
    become: true
    ovs_dpdk_pmd_cpus_check: pmd_cpu_mask={{ pmad_cpu_mask }}
'''


def get_cpus_list_from_mask_value(mask_val):
    """Gets CPU's list from the mask value

    :return: comma separated CPU's list
    """
    mask_val = mask_val.strip('\\"')
    cpus_list = []
    int_mask_val = int(mask_val, 16)
    bin_mask_val = bin(int_mask_val)
    bin_mask_val = str(bin_mask_val).replace('0b', '')
    rev_bin_mask_val = bin_mask_val[::-1]
    thread = 0
    for bin_val in rev_bin_mask_val:
        if bin_val == '1':
            cpus_list.append(thread)
        thread += 1
    return ','.join([str(cpu) for cpu in cpus_list])


# Gets the distinct numa nodes, physical and logical cpus info
# for all numa nodes.
def get_nodes_cores_info(module):
    dict_cpus = {}
    numa_nodes = []
    cmd = "sudo lscpu -p=NODE,CORE,CPU"
    result = module.run_command(cmd)
    if (not result or (result[0] != 0) or not (str(result[1]).strip(' '))):
        err = "Unable to determine physical and logical cpus."
        module.fail_json(msg=err)
    else:
        for line in str(result[1]).split('\n'):
            if (line.strip(' ') and not line.strip(' ').startswith('#')):
                cpu_info = line.strip(' ').split(',')
                try:
                    node = int(cpu_info[0])
                    cpu = int(cpu_info[1])
                    thread = int(cpu_info[2])
                    if node not in numa_nodes:
                        numa_nodes.append(node)
                    # CPU and NUMA node together forms a unique value,
                    # as cpu is specific to a NUMA node
                    # NUMA node id and cpu id tuple is used for unique key
                    key = node, cpu
                    if key in dict_cpus:
                        if thread not in dict_cpus[key]['thread_siblings']:
                            dict_cpus[key]['thread_siblings'].append(thread)
                    else:
                        cpu_item = {}
                        cpu_item['thread_siblings'] = [thread]
                        cpu_item['cpu'] = cpu
                        cpu_item['numa_node'] = node
                        dict_cpus[key] = cpu_item
                except (IndexError, ValueError):
                    err = "Unable to determine physical and logical cpus."
                    module.fail_json(msg=err)
    return (numa_nodes, list(dict_cpus.values()))


def validate_pmd_cpus(module, pmd_cpu_mask):
    pmd_cpus = get_cpus_list_from_mask_value(pmd_cpu_mask)
    pmd_cpu_list = pmd_cpus.split(',')
    cpus = []
    numa_nodes = []
    numa_nodes, cpus = get_nodes_cores_info(module)
    valid_numa_nodes = {}
    for numa_node in numa_nodes:
        valid_numa_nodes[str(numa_node)] = False
        for cpu in cpus:
            if cpu['numa_node'] == numa_node:
                if True in [int(pmd_cpu) in cpu['thread_siblings']
                            for pmd_cpu in pmd_cpu_list]:
                    valid_numa_nodes[str(numa_node)] = True
    invalid_numa_nodes = [node for node, val in valid_numa_nodes.items()
                          if not val]
    if invalid_numa_nodes:
        failed_nodes = ','.join(invalid_numa_nodes)
        err = ("Invalid PMD CPU's, cpu is not used from "
               "NUMA node(s): %(node)s." % {'node': failed_nodes})
        module.fail_json(msg=err)
    else:
        module.exit_json(msg="PMD CPU's configured correctly.")


def main():
    module = AnsibleModule(argument_spec=dict(
        pmd_cpu_mask=dict(required=True, type='str'),
    ))
    validate_pmd_cpus(module,
                      module.params.get('pmd_cpu_mask'))


if __name__ == '__main__':
    main()
