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

#
# Dell OpenManage Ansible Modules
# Version 9.3.0
# Copyright (C) 2019-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 = r'''
---
module: ome_firmware_baseline
short_description: Create, modify, or delete a firmware baseline on OpenManage Enterprise or OpenManage Enterprise Modular
description: This module allows to create, modify, or delete a firmware baseline on OpenManage Enterprise or OpenManage Enterprise Modular.
version_added: "2.0.0"
author:
  - "Jagadeesh N V(@jagadeeshnv)"
  - "Kritika Bhateja (@Kritika-Bhateja-03)"
extends_documentation_fragment:
  - dellemc.openmanage.ome_auth_options
options:
  state:
    description:
      - C(present) creates or modifies a baseline.
      - C(absent) deletes an existing baseline.
    choices:
      - present
      - absent
    default: present
    type: str
    version_added: 3.4.0
  baseline_name:
    type: str
    description:
      - Name of the the baseline.
      - This option is mutually exclusive with I(baseline_id).
  baseline_id:
    type: int
    description:
      - ID of the existing baseline.
      - This option is mutually exclusive with I(baseline_name).
    version_added: 3.4.0
  new_baseline_name:
    description: New name of the baseline.
    type: str
    version_added: 3.4.0
  baseline_description:
    type: str
    description:
      - Description for the baseline being created.
  catalog_name:
    type: str
    description:
      - Name of the catalog to be associated with the baseline.
  downgrade_enabled:
    type: bool
    description:
      - Indicates whether firmware downgrade is allowed for the devices in the baseline.
      - This value will be set to C(true) by default, if not provided during baseline creation.
  is_64_bit:
    type: bool
    description:
      - Indicates if the repository contains 64-bit DUPs.
      - This value will be set to C(true) by default, if not provided during baseline creation.
  device_ids:
    type: list
    elements: int
    description:
      - List of device IDs.
      - This option is mutually exclusive with I(device_service_tags) and I(device_group_names).
  device_service_tags:
    type: list
    elements: str
    description:
      - List of device service tags.
      - This option is mutually exclusive with I(device_ids) and I(device_group_names).
  device_group_names:
    type: list
    elements: str
    description:
      - List of group names.
      - This option is mutually exclusive with I(device_ids) and I(device_service_tags).
  job_wait:
    description:
      - Provides the option to wait for job completion.
      - This option is applicable when I(state) is C(present).
    type: bool
    default: true
    version_added: 3.4.0
  job_wait_timeout:
    description:
      - The maximum wait time of I(job_wait) in seconds. The job is tracked only for this duration.
      - This option is applicable when I(job_wait) is C(true).
    type: int
    default: 600
    version_added: 3.4.0
  filter_no_reboot_required:
    description:
      - Select only components with no reboot required allows to create a
        firmware/driver baseline that consists of only the components of the
        target devices that don't require a reboot of the target devices.
    type: bool
    version_added: 8.1.0

requirements:
    - "python >= 3.9.6"
notes:
    - Run this module from a system that has direct access to Dell OpenManage Enterprise or OpenManage Enterprise Modular.
    - I(device_group_names) option is not applicable for OpenManage Enterprise Modular.
    - This module supports C(check_mode).
'''

EXAMPLES = r'''
---
- name: Create baseline for device IDs
  dellemc.openmanage.ome_firmware_baseline:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    baseline_name: "baseline_name"
    baseline_description: "baseline_description"
    catalog_name: "catalog_name"
    device_ids:
      - 1010
      - 2020

- name: Create baseline for device IDs with no reboot required
  dellemc.openmanage.ome_firmware_baseline:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    baseline_name: "baseline_name"
    baseline_description: "baseline_description"
    catalog_name: "catalog_name"
    filter_no_reboot_required: true
    device_ids:
      - 1010
      - 2020

- name: Create baseline for servicetags
  dellemc.openmanage.ome_firmware_baseline:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    baseline_name: "baseline_name"
    baseline_description: "baseline_description"
    catalog_name: "catalog_name"
    device_service_tags:
      - "SVCTAG1"
      - "SVCTAG2"

- name: Create baseline for servicetags with no reboot required
  dellemc.openmanage.ome_firmware_baseline:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    baseline_name: "baseline_name"
    baseline_description: "baseline_description"
    catalog_name: "catalog_name"
    filter_no_reboot_required: true
    device_service_tags:
      - "SVCTAG1"
      - "SVCTAG2"

- name: Create baseline for device groups without job tracking
  dellemc.openmanage.ome_firmware_baseline:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    baseline_name: "baseline_name"
    baseline_description: "baseline_description"
    catalog_name: "catalog_name"
    device_group_names:
      - "Group1"
      - "Group2"
    job_wait: false

- name: Modify an existing baseline
  dellemc.openmanage.ome_firmware_baseline:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    baseline_name: "existing_baseline_name"
    new_baseline_name: "new_baseline_name"
    baseline_description: "new baseline_description"
    catalog_name: "catalog_other"
    device_group_names:
      - "Group3"
      - "Group4"
      - "Group5"
    downgrade_enabled: false
    is_64_bit: true

- name: Modify no reboot filter in existing baseline
  dellemc.openmanage.ome_firmware_baseline:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    baseline_name: "existing_baseline_name"
    new_baseline_name: "new_baseline_name"
    filter_no_reboot_required: true

- name: Delete a baseline
  dellemc.openmanage.ome_firmware_baseline:
    hostname: "192.168.0.1"
    username: "username"
    password: "password"
    ca_path: "/path/to/ca_cert.pem"
    state: absent
    baseline_name: "baseline_name"
'''

RETURN = r'''
---
msg:
  description: Overall status of the firmware baseline operation.
  returned: always
  type: str
  sample: "Successfully created the firmware baseline."
baseline_status:
  description: Details of the baseline status.
  returned: success
  type: dict
  sample: {
    "CatalogId": 123,
    "Description": "BASELINE DESCRIPTION",
    "DeviceComplianceReports": [],
    "DowngradeEnabled": true,
    "FilterNoRebootRequired": true,
    "Id": 23,
    "Is64Bit": true,
    "Name": "my_baseline",
    "RepositoryId": 123,
    "RepositoryName": "catalog123",
    "RepositoryType": "HTTP",
    "Targets": [
        {
            "Id": 10083,
            "Type": {
                "Id": 1000,
                "Name": "DEVICE"
            }
        },
        {
            "Id": 10076,
            "Type": {
                "Id": 1000,
                "Name": "DEVICE"
            }
        }
    ],
    "TaskId": 11235,
    "TaskStatusId": 2060
  }
job_id:
  description: Job ID of the baseline task.
  returned: When baseline job is in running state
  type: int
  sample: 10123
baseline_id:
  description: ID of the deleted baseline.
  returned: When I(state) is C(absent)
  type: int
  sample: 10123
error_info:
  type: dict
  description: Details of http error.
  returned: on http error
  sample:  {
    "error": {
      "@Message.ExtendedInfo": [
        {
          "Message": "Unable to retrieve baseline list either because the device ID(s) entered are invalid",
          "Resolution": "Make sure the entered device ID(s) are valid and retry the operation.",
          "Severity": "Critical"
        }
      ],
      "code": "Base.1.0.GeneralError",
      "message": "A general error has occurred. See ExtendedInfo for more information."
    }
  }
'''

BASELINE_URI = "UpdateService/Baselines"
ID_BASELINE_URI = "UpdateService/Baselines({Id})"
DELETE_BASELINE_URI = "UpdateService/Actions/UpdateService.RemoveBaselines"
CATALOG_URI = "UpdateService/Catalogs"
BASELINE_JOB_RUNNING = "Firmware baseline '{name}' with ID {id} is running. Please retry after job completion."
BASELINE_DEL_SUCCESS = "Successfully deleted the firmware baseline."
NO_CHANGES_MSG = "No changes found to be applied."
CHANGES_FOUND = "Changes found to be applied."
INVALID_BASELINE_ID = "Invalid baseline ID provided."
BASELINE_TRIGGERED = "Successfully triggered the firmware baseline task."
NO_CATALOG_MESSAGE = "Catalog name not provided for baseline creation."
NO_TARGETS_MESSAGE = "Targets not specified for baseline creation."
CATALOG_STATUS_MESSAGE = "Unable to create the firmware baseline as the catalog is in {status} status."
BASELINE_UPDATED = "Successfully {op} the firmware baseline."
SETTLING_TIME = 3
JOB_POLL_INTERVAL = 10
GROUP_ID = 6000


import json
import time
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, SSLValidationError
from ansible.module_utils.common.dict_transformations import recursive_diff


def get_baseline_from_name(rest_obj, baseline):
    resp = rest_obj.get_all_items_with_pagination(BASELINE_URI)
    baselines_list = resp.get("value")
    bsln = baseline
    for d in baselines_list:
        if d['Name'] == baseline.get('Name'):
            bsln = d
            break
    nlist = list(bsln)
    for k in nlist:
        if str(k).lower().startswith('@odata'):
            bsln.pop(k)
    return bsln


def check_existing_baseline(module, rest_obj):
    baseline_id = module.params.get("baseline_id")
    srch_key = "Name"
    srch_val = module.params.get("baseline_name")
    if baseline_id:
        srch_key = "Id"
        srch_val = module.params.get("baseline_id")
    baseline_cfgs = []
    resp = rest_obj.get_all_items_with_pagination(BASELINE_URI)
    baselines = resp.get("value")
    for d in baselines:
        if d[srch_key] == srch_val:
            baseline_cfgs.append(d)
            if baseline_id:
                break
    return baseline_cfgs


def get_catrepo_ids(module, cat_name, rest_obj):
    if cat_name is not None:
        resp_data = rest_obj.get_all_items_with_pagination(CATALOG_URI)
        values = resp_data["value"]
        if values:
            for catalog in values:
                repo = catalog.get("Repository")
                if repo.get("Name") == cat_name:
                    if catalog.get('Status') != 'Completed':
                        module.fail_json(msg=CATALOG_STATUS_MESSAGE.format(status=catalog.get('Status')))
                    return catalog.get("Id"), repo.get("Id")
    return None, None


def get_dev_ids(module, rest_obj, param, devkey):
    paramlist = module.params[param]
    resp_data = rest_obj.get_all_items_with_pagination("DeviceService/Devices")
    values = resp_data["value"]
    targets = []
    if values:
        devlist = values
        device_resp = {device[devkey]: device for device in devlist}
        for st in paramlist:
            if st in device_resp:
                djson = device_resp[st]
                target = {}
                device_type = {}
                device_type['Id'] = djson['Type']
                device_type['Name'] = "DEVICE"
                target['Id'] = djson['Id']
                target['Type'] = device_type
                targets.append(target)
            else:
                module.fail_json(msg="Unable to complete the operation because the entered target"
                                     " {0} '{1}' is invalid.".format(devkey, st))
    return targets


def get_group_ids(module, rest_obj):
    grp_name_list = module.params.get("device_group_names")
    resp_data = rest_obj.get_all_items_with_pagination("GroupService/Groups")
    values = resp_data["value"]
    targets = []
    if values:
        grplist = values
        device_resp = {str(grp['Name']): grp for grp in grplist}
        for st in grp_name_list:
            if st in device_resp:
                djson = device_resp[st]
                target = {}
                device_type = {}
                device_type['Id'] = GROUP_ID
                device_type['Name'] = "GROUP"
                target['Id'] = djson['Id']
                target['Type'] = device_type
                targets.append(target)
            else:
                module.fail_json(msg="Unable to complete the operation because the entered target"
                                     " Group Name '{0}' is invalid.".format(st))
    return targets


def get_target_list(module, rest_obj):
    target_list = None
    if module.params.get("device_service_tags"):
        target_list = get_dev_ids(module, rest_obj, "device_service_tags", "DeviceServiceTag")
    elif module.params.get("device_group_names"):
        target_list = get_group_ids(module, rest_obj)
    elif module.params.get("device_ids"):
        target_list = get_dev_ids(module, rest_obj, "device_ids", "Id")
    return target_list


def exit_baseline(module, rest_obj, baseline, op):
    msg = BASELINE_TRIGGERED
    time.sleep(SETTLING_TIME)
    try:
        bsln = get_baseline_from_name(rest_obj, baseline)
    except Exception:
        bsln = baseline
    if module.params.get("job_wait"):
        job_failed, job_message = rest_obj.job_tracking(
            baseline.get('TaskId'), job_wait_sec=module.params["job_wait_timeout"], sleep_time=JOB_POLL_INTERVAL)
        if job_failed is True:
            module.fail_json(msg=job_message, baseline_status=bsln)
        msg = BASELINE_UPDATED.format(op=op)
    module.exit_json(msg=msg, baseline_status=bsln, changed=True)


def _get_baseline_payload(module, rest_obj):
    cat_name = module.params.get("catalog_name")
    cat_id, repo_id = get_catrepo_ids(module, cat_name, rest_obj)
    if cat_id is None or repo_id is None:
        module.fail_json(msg="No Catalog with name {0} found".format(cat_name))
    targets = get_target_list(module, rest_obj)
    if targets is None:
        module.fail_json(msg=NO_TARGETS_MESSAGE)
    baseline_name = module.params.get("baseline_name")
    baseline_payload = {
        "Name": baseline_name,
        "CatalogId": cat_id,
        "RepositoryId": repo_id,
        "Targets": targets
    }
    baseline_payload['Description'] = module.params.get("baseline_description")
    baseline_payload['FilterNoRebootRequired'] = module.params.get("filter_no_reboot_required")
    de = module.params.get("downgrade_enabled")
    baseline_payload['DowngradeEnabled'] = de if de is not None else True
    sfb = module.params.get("is_64_bit")
    baseline_payload['Is64Bit'] = sfb if sfb is not None else True
    return baseline_payload


def create_baseline(module, rest_obj):
    myparams = module.params
    if not any([myparams.get("device_ids"), myparams.get("device_service_tags"), myparams.get("device_group_names")]):
        module.fail_json(msg=NO_TARGETS_MESSAGE)
    if not myparams.get("catalog_name"):
        module.fail_json(msg=NO_CATALOG_MESSAGE)
    payload = _get_baseline_payload(module, rest_obj)
    if module.check_mode:
        module.exit_json(msg=CHANGES_FOUND, changed=True)
    resp = rest_obj.invoke_request("POST", BASELINE_URI, data=payload)
    exit_baseline(module, rest_obj, resp.json_data, 'created')


def update_modify_payload(module, rest_obj, modify_payload, current_baseline):
    paylist = ['Name', "CatalogId", "RepositoryId", 'Description', 'DowngradeEnabled', 'Is64Bit',
               'FilterNoRebootRequired']
    diff_tuple = recursive_diff(modify_payload, current_baseline)
    diff = 0
    payload = {item: current_baseline.get(item) for item in paylist}
    try:
        if diff_tuple and diff_tuple[0]:
            diff += 1
            payload.update(diff_tuple[0])
        payload['Targets'] = current_baseline.get('Targets', [])
        inp_targets_list = get_target_list(module, rest_obj)
        if inp_targets_list:
            inp_target_dict = {item['Id']: item['Type']['Id'] for item in inp_targets_list}
            cur_target_dict = {item['Id']: item['Type']['Id'] for item in current_baseline.get('Targets', [])}
            diff_tuple = recursive_diff(inp_target_dict, cur_target_dict)
            if diff_tuple:
                diff += 1
                payload['Targets'] = inp_targets_list
        if diff == 0:
            module.exit_json(msg=NO_CHANGES_MSG)
        payload['Id'] = current_baseline['Id']
    except (IndexError, TypeError) as err:
        module.fail_json(msg=str(err))
    return payload


def modify_baseline(module, rest_obj, baseline_list):
    d = baseline_list[0]
    if d["TaskStatusId"] == 2050:
        module.fail_json(msg=BASELINE_JOB_RUNNING.format(name=d["Name"], id=d["Id"]), job_id=d['TaskId'])
    mparam = module.params
    current_baseline = baseline_list[0]
    modify_payload = {}
    if mparam.get('catalog_name'):
        cat_id, repo_id = get_catrepo_ids(module, mparam.get('catalog_name'), rest_obj)
        if cat_id is None or repo_id is None:
            module.fail_json(msg="No Catalog with name {0} found".format(mparam.get('catalog_name')))
        modify_payload["CatalogId"] = cat_id
        modify_payload["RepositoryId"] = repo_id
    if mparam.get('new_baseline_name'):
        modify_payload['Name'] = mparam.get('new_baseline_name')
    if mparam.get("baseline_description"):
        modify_payload['Description'] = mparam.get("baseline_description")
    if module.params.get("downgrade_enabled") is not None:
        modify_payload['DowngradeEnabled'] = module.params.get("downgrade_enabled")
    if module.params.get("is_64_bit") is not None:
        modify_payload['Is64Bit'] = module.params.get("is_64_bit")
    if module.params.get("filter_no_reboot_required") is not None:
        modify_payload['FilterNoRebootRequired'] = module.params.get("filter_no_reboot_required")
    payload = update_modify_payload(module, rest_obj, modify_payload, current_baseline)
    if module.check_mode:
        module.exit_json(msg=CHANGES_FOUND, changed=True)
    resp = rest_obj.invoke_request("PUT", ID_BASELINE_URI.format(Id=str(payload["Id"])), data=payload)
    exit_baseline(module, rest_obj, resp.json_data, 'modified')


def delete_baseline(module, rest_obj, baseline_list):
    delete_ids = []
    d = baseline_list[0]
    if d["TaskStatusId"] == 2050:
        module.fail_json(msg=BASELINE_JOB_RUNNING.format(name=d["Name"], id=d["Id"]), job_id=d['TaskId'])
    delete_ids.append(d["Id"])
    delete_payload = {"BaselineIds": delete_ids}
    if module.check_mode:
        module.exit_json(msg=CHANGES_FOUND, changed=True)
    rest_obj.invoke_request('POST', DELETE_BASELINE_URI, data=delete_payload)
    module.exit_json(msg=BASELINE_DEL_SUCCESS, changed=True, baseline_id=delete_ids[0])


def main():
    specs = {
        "state": {"default": "present", "choices": ['present', 'absent']},
        "baseline_name": {"type": 'str'},
        "baseline_id": {"type": 'int'},
        "baseline_description": {"type": 'str'},
        "new_baseline_name": {"type": 'str'},
        "catalog_name": {"type": 'str'},
        "downgrade_enabled": {"type": 'bool'},
        "is_64_bit": {"type": 'bool'},
        "device_ids": {"type": 'list', "elements": 'int'},
        "device_service_tags": {"type": 'list', "elements": 'str'},
        "device_group_names": {"type": 'list', "elements": 'str'},
        "job_wait": {"type": 'bool', "default": True},
        "job_wait_timeout": {"type": 'int', "default": 600},
        "filter_no_reboot_required": {"type": 'bool'}
    }

    module = OmeAnsibleModule(
        argument_spec=specs,
        mutually_exclusive=[
            ('device_ids', 'device_service_tags', 'device_group_names'),
            ('baseline_name', 'baseline_id')
        ],
        required_one_of=[('baseline_name', 'baseline_id')],
        supports_check_mode=True)

    try:
        with RestOME(module.params, req_session=True) as rest_obj:
            baseline_list = check_existing_baseline(module, rest_obj)
            if module.params.get('state') == 'absent':
                if baseline_list:
                    delete_baseline(module, rest_obj, baseline_list)
                module.exit_json(msg=NO_CHANGES_MSG)
            else:
                if baseline_list:
                    modify_baseline(module, rest_obj, baseline_list)
                else:
                    if module.params.get('baseline_id'):
                        module.fail_json(msg=INVALID_BASELINE_ID)
                    create_baseline(module, rest_obj)
    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, TypeError, ConnectionError, SSLValidationError, OSError) as err:
        module.fail_json(msg=str(err))


if __name__ == '__main__':
    main()
