#!/usr/bin/python # -*- coding: utf-8 -*- # # Dell EMC OpenManage Ansible Modules # Version 3.6.0 # Copyright (C) 2021 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_diagnostics short_description: Export technical support logs(TSR) to network share location version_added: "3.6.0" description: This module allows to export SupportAssist collection logs from OpenManage Enterprise and OpenManage Enterprise Modular and application logs from OpenManage Enterprise Modular to a CIFS or NFS share. extends_documentation_fragment: - dellemc.openmanage.ome_auth_options options: device_ids: type: list description: - List of target device IDs. - This is applicable for C(support_assist_collection) logs. - This option is mutually exclusive with I(device_service_tags) and I(device_group_name). elements: int device_service_tags: type: list description: - List of target identifier. - This is applicable for C(support_assist_collection) logs. - This option is mutually exclusive with I(device_ids) and I(device_group_name). elements: str device_group_name: type: str description: - Name of the device group to export C(support_assist_collection) logs of all devices within the group. - This is applicable for C(support_assist_collection) logs. - This option is not applicable for OpenManage Enterprise Modular. - This option is mutually exclusive with I(device_ids) and I(device_service_tags). log_type: type: str description: - C(application) is applicable for OpenManage Enterprise Modular to export the application log bundle. - C(support_assist_collection) is applicable for one or more devices to export SupportAssist logs. - C(support_assist_collection) supports both OpenManage Enterprise and OpenManage Enterprise Modular. - C(support_assist_collection) does not support export of C(OS_LOGS) from OpenManage Enterprise. If tried to export, the tasks will complete with errors, and the module fails. choices: [application, support_assist_collection] default: support_assist_collection mask_sensitive_info: type: bool description: - Select this option to mask the personal identification information such as IPAddress, DNS, alert destination, email, gateway, inet6, MacAddress, netmask etc. - This option is applicable for C(application) of I(log_type). default: False log_selectors: type: list description: - By default, the SupportAssist logs contains only hardware logs. To collect additional logs such as OS logs or RAID logs, specify these option in the choices list. - If not provided the default hardware log will be exported. - C(OS_LOGS) to collect OS Logs. - C(RAID_LOGS) to collect RAID controller logs. - This option is applicable only for C(support_assist_collection) of I(log_type). choices: [OS_LOGS, RAID_LOGS] elements: str share_address: type: str required: True description: Network share IP address. share_name: type: str required: True description: - Network share path. - Filename is auto generated and should not be provided as part of I(share_name). share_type: type: str required: True description: Network share type choices: [NFS, CIFS] share_user: type: str description: - Network share username. - This option is applicable for C(CIFS) of I(share_type). share_password: type: str description: - Network share password - This option is applicable for C(CIFS) of I(share_type). share_domain: type: str description: - Network share domain name. - This option is applicable for C(CIFS) if I(share_type). job_wait: type: bool description: - Whether to wait for the Job completion or not. - The maximum wait time is I(job_wait_timeout). default: True job_wait_timeout: type: int description: - The maximum wait time of I(job_wait) in minutes. - This option is applicable I(job_wait) is true. default: 60 test_connection: type: bool description: - Test the availability of the network share location. - I(job_wait) and I(job_wait_timeout) options are not applicable for I(test_connection). default: False requirements: - "python >= 2.7.17" author: - "Felix Stephen (@felixs88)" """ EXAMPLES = r""" --- - name: Export application log using CIFS share location dellemc.openmanage.ome_diagnostics: hostname: "192.168.0.1" username: "username" password: "password" share_type: CIFS share_address: "192.168.0.2" share_user: share_username share_password: share_password share_name: cifs_share log_type: application mask_sensitive_info: false test_connection: true - name: Export application log using NFS share location dellemc.openmanage.ome_diagnostics: hostname: "192.168.0.1" username: "username" password: "password" share_address: "192.168.0.3" share_type: NFS share_name: nfs_share log_type: application mask_sensitive_info: true test_connection: true - name: Export SupportAssist log using CIFS share location dellemc.openmanage.ome_diagnostics: hostname: "192.168.0.1" username: "username" password: "password" share_address: "192.168.0.3" share_user: share_username share_password: share_password share_name: cifs_share share_type: CIFS log_type: support_assist_collection device_ids: [10011, 10022] log_selectors: [OS_LOGS] test_connection: true - name: Export SupportAssist log using NFS share location dellemc.openmanage.ome_diagnostics: hostname: "192.168.0.1" username: "username" password: "password" share_address: "192.168.0.3" share_type: NFS share_name: nfs_share log_type: support_assist_collection device_group_name: group_name test_connection: true """ RETURN = r""" --- msg: type: str description: "Overall status of the export log." returned: always sample: "Export log job completed successfully." jog_status: type: dict description: Details of the export log operation status. returned: success sample: { "Builtin": false, "CreatedBy": "root", "Editable": true, "EndTime": None, "Id": 12778, "JobDescription": "Export device log", "JobName": "Export Log", "JobStatus": {"Id": 2080, "Name": "New"}, "JobType": {"Id": 18, "Internal": false, "Name": "DebugLogs_Task"}, "LastRun": "2021-07-06 10:52:50.519", "LastRunStatus": {"Id": 2060, "Name": "Completed"}, "NextRun": None, "Schedule": "startnow", "StartTime": None, "State": "Enabled", "UpdatedBy": None, "UserGenerated": true, "Visible": true, "Params": [ {"JobId": 12778, "Key": "maskSensitiveInfo", "Value": "FALSE"}, {"JobId": 12778, "Key": "password", "Value": "tY86w7q92u0QzvykuF0gQQ"}, {"JobId": 12778, "Key": "userName", "Value": "administrator"}, {"JobId": 12778, "Key": "shareName", "Value": "iso"}, {"JobId": 12778, "Key": "OPERATION_NAME", "Value": "EXTRACT_LOGS"}, {"JobId": 12778, "Key": "shareType", "Value": "CIFS"}, {"JobId": 12778, "Key": "shareAddress", "Value": "100.96.32.142"} ], "Targets": [{"Data": "", "Id": 10053, "JobId": 12778, "TargetType": {"Id": 1000, "Name": "DEVICE"}}] } 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 import re from ssl import SSLError from ansible.module_utils.basic import AnsibleModule from ansible_collections.dellemc.openmanage.plugins.module_utils.ome import RestOME from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError from ansible.module_utils.urls import ConnectionError, SSLValidationError LOG_SELECTOR = {"OS_LOGS": 1, "RAID_LOGS": 2} JOB_URI = "JobService/Jobs" GROUP_URI = "GroupService/Groups" GROUP_DEVICE_URI = "GroupService/Groups({0})/Devices" DEVICE_URI = "DeviceService/Devices" DOMAIN_URI = "ManagementDomainService/Domains" EXE_HISTORY_URI = "JobService/Jobs({0})/ExecutionHistories" def group_validation(module, rest_obj): group_name, group_device = module.params.get('device_group_name'), [] query_param = {"$filter": "Name eq '{0}'".format(group_name)} group_resp = rest_obj.invoke_request("GET", GROUP_URI, query_param=query_param) group = group_resp.json_data["value"] if group: group_id = group[0]["Id"] resp = rest_obj.invoke_request("GET", GROUP_DEVICE_URI.format(group_id)) device_group_resp = resp.json_data["value"] if device_group_resp: for device in device_group_resp: if device["Type"] == 1000: group_device.append(device["Id"]) else: module.fail_json(msg="There are no device(s) present in this group.") else: module.fail_json(msg="Unable to complete the operation because the entered target " "device group name '{0}' is invalid.".format(group_name)) if not group_device: module.fail_json(msg="The requested group '{0}' does not contain devices that " "support export log.".format(group_name)) return group_device def device_validation(module, rest_obj): device_lst, invalid_lst, other_types = [], [], [] devices, tags = module.params.get("device_ids"), module.params.get("device_service_tags") all_device = rest_obj.get_all_report_details(DEVICE_URI) key = "Id" if devices is not None else "DeviceServiceTag" value = "id" if key == "Id" else "service tag" req_device = devices if devices is not None else tags for each in req_device: device = list(filter(lambda d: d[key] in [each], all_device["report_list"])) if device and device[0]["Type"] == 1000: device_lst.append(device[0]["Id"]) elif device and not device[0]["Type"] == 1000: other_types.append(str(each)) else: invalid_lst.append(str(each)) if invalid_lst: module.fail_json(msg="Unable to complete the operation because the entered " "target device {0}(s) '{1}' are invalid.".format(value, ",".join(set(invalid_lst)))) if not device_lst and other_types: module.fail_json(msg="The requested device {0}(s) '{1}' are " "not applicable for export log.".format(value, ",".join(set(other_types)))) return device_lst def extract_log_operation(module, rest_obj, device_lst=None): payload_params, target_params = [], [] log_type = module.params["log_type"] if log_type == "application": resp = rest_obj.invoke_request("GET", DEVICE_URI, query_param={"$filter": "Type eq 2000"}) resp_data = resp.json_data["value"] if resp_data: for dev in resp_data: target_params.append({"Id": dev["Id"], "Data": "", "TargetType": {"Id": dev["Type"], "Name": "CHASSIS"}}) else: module.fail_json(msg="There is no device(s) available to export application log.") else: for device in device_lst: target_params.append({"Id": device, "Data": "", "TargetType": {"Id": 1000, "Name": "DEVICE"}}) payload_params.append({"Key": "shareAddress", "Value": module.params["share_address"]}) payload_params.append({"Key": "shareType", "Value": module.params["share_type"]}) payload_params.append({"Key": "OPERATION_NAME", "Value": "EXTRACT_LOGS"}) if module.params.get("share_name") is not None: payload_params.append({"Key": "shareName", "Value": module.params["share_name"]}) if module.params.get("share_user") is not None: payload_params.append({"Key": "userName", "Value": module.params["share_user"]}) if module.params.get("share_password") is not None: payload_params.append({"Key": "password", "Value": module.params["share_password"]}) if module.params.get("share_domain") is not None: payload_params.append({"Key": "domainName", "Value": module.params["share_domain"]}) if module.params.get("mask_sensitive_info") is not None and log_type == "application": payload_params.append({"Key": "maskSensitiveInfo", "Value": str(module.params["mask_sensitive_info"]).upper()}) if module.params.get("log_selectors") is not None and log_type == "support_assist_collection": log_lst = [LOG_SELECTOR[i] for i in module.params["log_selectors"]] log_lst.sort() log_selector = ",".join(map(str, log_lst)) payload_params.append({"Key": "logSelector", "Value": "0,{0}".format(log_selector)}) response = rest_obj.job_submission("Export Log", "Export device log", target_params, payload_params, {"Id": 18, "Name": "DebugLogs_Task"}) return response def check_domain_service(module, rest_obj): try: rest_obj.invoke_request("GET", DOMAIN_URI, api_timeout=5) except HTTPError as err: err_message = json.load(err) if err_message["error"]["@Message.ExtendedInfo"][0]["MessageId"] == "CGEN1006": module.fail_json(msg="Export log operation is not supported on the specified system.") return def find_failed_jobs(resp, rest_obj): msg, fail = "Export log job completed with errors.", False history = rest_obj.invoke_request("GET", EXE_HISTORY_URI.format(resp["Id"])) if history.json_data["value"]: hist = history.json_data["value"][0] history_details = rest_obj.invoke_request( "GET", "{0}({1})/ExecutionHistoryDetails".format(EXE_HISTORY_URI.format(resp["Id"]), hist["Id"]) ) for hd in history_details.json_data["value"]: if not re.findall(r"Job status for JID_\d+ is Completed with Errors.", hd["Value"]): fail = True break else: fail = False return msg, fail def main(): module = AnsibleModule( argument_spec={ "hostname": {"required": True, "type": "str"}, "username": {"required": True, "type": "str"}, "password": {"required": True, "type": "str", "no_log": True}, "port": {"required": False, "type": "int", "default": 443}, "device_ids": {"required": False, "type": "list", "elements": "int"}, "device_service_tags": {"required": False, "type": "list", "elements": "str"}, "device_group_name": {"required": False, "type": "str"}, "log_type": {"required": False, "type": "str", "default": "support_assist_collection", "choices": ["support_assist_collection", "application"]}, "mask_sensitive_info": {"required": False, "type": "bool", "default": False}, "log_selectors": {"required": False, "type": "list", "choices": ["RAID_LOGS", "OS_LOGS"], "elements": "str"}, "share_address": {"required": True, "type": "str"}, "share_name": {"required": True, "type": "str"}, "share_type": {"required": True, "type": "str", "choices": ["NFS", "CIFS"]}, "share_user": {"required": False, "type": "str"}, "share_password": {"required": False, "type": "str", "no_log": True}, "share_domain": {"required": False, "type": "str"}, "job_wait": {"required": False, "type": "bool", "default": True}, "job_wait_timeout": {"required": False, "type": "int", "default": 60}, "test_connection": {"required": False, "type": "bool", "default": False}, }, required_if=[ ['log_type', 'application', ['mask_sensitive_info']], ['log_type', 'support_assist_collection', ['device_ids', 'device_service_tags', 'device_group_name'], True], ['share_type', 'CIFS', ['share_user', 'share_password']] ], mutually_exclusive=[('device_ids', 'device_service_tags', 'device_group_name')], supports_check_mode=True ) try: with RestOME(module.params, req_session=True) as rest_obj: # checking the domain service if module.params["log_type"] == "application": check_domain_service(module, rest_obj) # checking any existing running job job_allowed, job_lst = rest_obj.check_existing_job_state("DebugLogs_Task") if not job_allowed: module.fail_json(msg="An export log job is already running. Wait for the job to finish.") # test network connection if module.params["test_connection"]: conn_resp = rest_obj.test_network_connection(module.params["share_address"], module.params["share_name"], module.params["share_type"], module.params["share_user"], module.params["share_password"], module.params["share_domain"]) job_failed, job_message = rest_obj.job_tracking(conn_resp.json_data["Id"], job_wait_sec=5, sleep_time=5) if job_failed: module.fail_json(msg="Unable to access the share. Ensure that the share address, share name, " "share domain, and share credentials provided are correct.") # validation for device id/tag/group valid_device = [] if module.params["log_type"] == "support_assist_collection" and \ module.params.get("device_group_name") is not None: valid_device = group_validation(module, rest_obj) elif module.params["log_type"] == "support_assist_collection" and \ module.params.get("device_group_name") is None: valid_device = device_validation(module, rest_obj) # extract log job operation response = extract_log_operation(module, rest_obj, device_lst=valid_device) message = "Export log job submitted successfully." if module.params["job_wait"]: seconds = module.params["job_wait_timeout"] * 60 job_failed, job_message = rest_obj.job_tracking(response.json_data["Id"], job_wait_sec=seconds, sleep_time=5) message = "Export log job completed successfully." if job_message == "The job is not complete after {0} seconds.".format(seconds): module.fail_json( msg="The export job is not complete because it has exceeded the configured timeout period.", job_status=response.json_data ) if job_failed: message, failed_job = find_failed_jobs(response.json_data, rest_obj) if failed_job: module.fail_json(msg=message, job_status=response.json_data) response = rest_obj.invoke_request("GET", "{0}({1})".format(JOB_URI, response.json_data["Id"])) resp = response.json_data if resp: resp = rest_obj.strip_substr_dict(resp) module.exit_json(msg=message, job_status=resp) 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, SSLError, ConnectionError, SSLValidationError) as err: module.fail_json(msg=str(err)) if __name__ == '__main__': main()