diff --git a/docs/modules/idrac_license.rst b/docs/modules/idrac_license.rst new file mode 100644 index 000000000..d4441a3ab --- /dev/null +++ b/docs/modules/idrac_license.rst @@ -0,0 +1,432 @@ +.. _idrac_license_module: + + +idrac_license -- Configure iDRAC licenses +========================================= + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- + +This module allows to import, export and delete licenses on iDRAC. + + + +Requirements +------------ +The below requirements are needed on the host that executes this module. + +- python \>= 3.9.6 + + + +Parameters +---------- + + license_id (optional, str, None) + Entitlement ID of the license that is to be imported, exported or deleted. + + \ :emphasis:`license\_id`\ is required when \ :emphasis:`delete`\ is \ :literal:`true`\ or \ :emphasis:`export`\ is \ :literal:`true`\ . + + + delete (optional, bool, False) + Delete the license from the iDRAC. + + When \ :emphasis:`delete`\ is \ :literal:`true`\ , then \ :emphasis:`license\_id`\ is required. + + \ :emphasis:`delete`\ is mutually exclusive with \ :emphasis:`export`\ and \ :emphasis:`import`\ . + + + export (optional, bool, False) + Export the license from the iDRAC. + + When \ :emphasis:`export`\ is \ :literal:`true`\ , \ :emphasis:`license\_id`\ and \ :emphasis:`share\_parameters`\ is required. + + \ :emphasis:`export`\ is mutually exclusive with \ :emphasis:`delete`\ and \ :emphasis:`import`\ . + + + import (optional, bool, False) + Import the license from the iDRAC. + + When \ :emphasis:`import`\ is \ :literal:`true`\ , \ :emphasis:`share\_parameters`\ is required. + + \ :emphasis:`import`\ is mutually exclusive with \ :emphasis:`delete`\ and \ :emphasis:`export`\ . + + + share_parameters (optional, dict, None) + Parameters that are required for the import and export operation of a license. + + \ :emphasis:`share\_parameters`\ is required when \ :emphasis:`export`\ or \ :emphasis:`import`\ is \ :literal:`true`\ . + + + share_type (optional, str, local) + Share type of the network share. + + \ :literal:`local`\ uses local path for \ :emphasis:`import`\ and \ :emphasis:`export`\ operation. + + \ :literal:`nfs`\ uses NFS share for \ :emphasis:`import`\ and \ :emphasis:`export`\ operation. + + \ :literal:`cifs`\ uses CIFS share for \ :emphasis:`import`\ and \ :emphasis:`export`\ operation. + + \ :literal:`http`\ uses HTTP share for \ :emphasis:`import`\ and \ :emphasis:`export`\ operation. + + \ :literal:`https`\ uses HTTPS share for \ :emphasis:`import`\ and \ :emphasis:`export`\ operation. + + + file_name (optional, str, None) + License file name for \ :emphasis:`import`\ and \ :emphasis:`export`\ operation. + + \ :emphasis:`file\_name`\ is required when \ :emphasis:`import`\ is \ :literal:`true`\ . + + For the \ :emphasis:`import`\ operation, when \ :emphasis:`share\_type`\ is \ :literal:`local`\ , the supported extensions for \ :emphasis:`file\_name`\ are '.txt' and '.xml'. For other share types, the supported extension is '.xml' + + + ip_address (optional, str, None) + IP address of the network share. + + \ :emphasis:`ip\_address`\ is required when \ :emphasis:`share\_type`\ is \ :literal:`nfs`\ , \ :literal:`cifs`\ , \ :literal:`http`\ or \ :literal:`https`\ . + + + share_name (optional, str, None) + Network share or local path of the license file. + + + workgroup (optional, str, None) + Workgroup of the network share. + + \ :emphasis:`workgroup`\ is applicable only when \ :emphasis:`share\_type`\ is \ :literal:`cifs`\ . + + + username (optional, str, None) + Username of the network share. + + \ :emphasis:`username`\ is required when \ :emphasis:`share\_type`\ is \ :literal:`cifs`\ . + + + password (optional, str, None) + Password of the network share. + + \ :emphasis:`password`\ is required when \ :emphasis:`share\_type`\ is \ :literal:`cifs`\ . + + + ignore_certificate_warning (optional, str, off) + Ignores the certificate warning while connecting to Share and is only applicable when \ :emphasis:`share\_type`\ is \ :literal:`https`\ . + + \ :literal:`off`\ ignores the certificate warning. + + \ :literal:`on`\ does not ignore the certificate warning. + + + proxy_support (optional, str, off) + Specifies if proxy is to be used or not. + + \ :literal:`off`\ does not use proxy settings. + + \ :literal:`default\_proxy`\ uses the default proxy settings. + + \ :literal:`parameters\_proxy`\ uses the specified proxy settings. \ :emphasis:`proxy\_server`\ is required when \ :emphasis:`proxy\_support`\ is \ :literal:`parameters\_proxy`\ . + + \ :emphasis:`proxy\_support`\ is only applicable when \ :emphasis:`share\_type`\ is \ :literal:`https`\ or \ :literal:`https`\ . + + + proxy_type (optional, str, http) + The proxy type of the proxy server. + + \ :literal:`http`\ to select HTTP proxy. + + \ :literal:`socks`\ to select SOCKS proxy. + + \ :emphasis:`proxy\_type`\ is only applicable when \ :emphasis:`share\_type`\ is \ :literal:`https`\ or \ :literal:`https`\ and when \ :emphasis:`proxy\_support`\ is \ :literal:`parameters\_proxy`\ . + + + proxy_server (optional, str, None) + The IP address of the proxy server. + + \ :emphasis:`proxy\_server`\ is required when \ :emphasis:`proxy\_support`\ is \ :literal:`parameters\_proxy`\ . + + \ :emphasis:`proxy\_server`\ is only applicable when \ :emphasis:`share\_type`\ is \ :literal:`https`\ or \ :literal:`https`\ and when \ :emphasis:`proxy\_support`\ is \ :literal:`parameters\_proxy`\ . + + + proxy_port (optional, int, 80) + The port of the proxy server. + + \ :emphasis:`proxy\_port`\ is only applicable when \ :emphasis:`share\_type`\ is \ :literal:`https`\ or \ :literal:`https`\ and when \ :emphasis:`proxy\_support`\ is \ :literal:`parameters\_proxy`\ . + + + proxy_username (optional, str, None) + The username of the proxy server. + + \ :emphasis:`proxy\_username`\ is only applicable when \ :emphasis:`share\_type`\ is \ :literal:`https`\ or \ :literal:`https`\ and when \ :emphasis:`proxy\_support`\ is \ :literal:`parameters\_proxy`\ . + + + proxy_password (optional, str, None) + The password of the proxy server. + + \ :emphasis:`proxy\_password`\ is only applicable when \ :emphasis:`share\_type`\ is \ :literal:`https`\ or \ :literal:`https`\ and when \ :emphasis:`proxy\_support`\ is \ :literal:`parameters\_proxy`\ . + + + + resource_id (optional, str, None) + Id of the resource. + + If the value for resource ID is not provided, the module picks the first resource ID available from the list of system resources returned by the iDRAC. + + + idrac_ip (True, str, None) + iDRAC IP Address. + + + idrac_user (True, str, None) + iDRAC username. + + + idrac_password (True, str, None) + iDRAC user password. + + + idrac_port (optional, int, 443) + iDRAC port. + + + validate_certs (optional, bool, True) + If \ :literal:`false`\ , the SSL certificates will not be validated. + + Configure \ :literal:`false`\ only on personally controlled sites where self-signed certificates are used. + + Prior to collection version \ :literal:`5.0.0`\ , the \ :emphasis:`validate\_certs`\ is \ :literal:`false`\ by default. + + + ca_path (optional, path, None) + The Privacy Enhanced Mail (PEM) file that contains a CA certificate to be used for the validation. + + + timeout (optional, int, 30) + The socket level timeout in seconds. + + + + + +Notes +----- + +.. note:: + - Run this module from a system that has direct access to Dell iDRAC. + - This module supports only iDRAC9 and above. + - This module supports IPv4 and IPv6 addresses. + - This module does not support \ :literal:`check\_mode`\ . + - When \ :emphasis:`share\_type`\ is \ :literal:`local`\ for \ :emphasis:`import`\ and \ :emphasis:`export`\ operations, job\_details are not displayed. + + + + +Examples +-------- + +.. code-block:: yaml+jinja + + + --- + - name: Export a license from iDRAC to local + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "local" + share_name: "/path/to/share" + file_name: "license_file" + + - name: Export a license from iDRAC to NFS share + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "nfs" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + + - name: Export a license from iDRAC to CIFS share + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "cifs" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + username: "username" + password: "password" + workgroup: "workgroup" + + - name: Export a license from iDRAC to HTTP share via proxy + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "http" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + username: "username" + password: "password" + proxy_support: "parameters_proxy" + proxy_type: socks + proxy_server: "192.168.0.2" + proxy_port: 1080 + proxy_username: "proxy_username" + proxy_password: "proxy_password" + + - name: Export a license from iDRAC to HTTPS share + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "https" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + username: "username" + password: "password" + ignore_certificate_warning: "on" + + - name: Import a license to iDRAC from local + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: local + share_name: "/path/to/share" + + - name: Import a license to iDRAC from NFS share + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: nfs + ip_address: "192.168.0.1" + share_name: "/path/to/share" + + - name: Import a license to iDRAC from CIFS share + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: cifs + ip_address: "192.168.0.1" + share_name: "/path/to/share" + username: "username" + password: "password" + + - name: Import a license to iDRAC from HTTP share + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: http + ip_address: "192.168.0.1" + share_name: "/path/to/share" + username: "username" + password: "password" + + - name: Import a license to iDRAC from HTTPS share via proxy + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: https + ip_address: "192.168.0.1" + share_name: "/path/to/share" + username: "username" + password: "password" + proxy_support: "parameters_proxy" + proxy_server: "192.168.0.2" + proxy_port: 808 + proxy_username: "proxy_username" + proxy_password: "proxy_password" + + - name: Delete a License from iDRAC + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENCE_123" + delete: true + + + +Return Values +------------- + +msg (always, str, Successfully exported the license.) + Status of the license operation. + + +job_details (For import and export operations, dict, {'ActualRunningStartTime': '2024-01-09T05:16:19', 'ActualRunningStopTime': '2024-01-09T05:16:19', 'CompletionTime': '2024-01-09T05:16:19', 'Description': 'Job Instance', 'EndTime': None, 'Id': 'JID_XXXXXXXXX', 'JobState': 'Completed', 'JobType': 'LicenseExport', 'Message': 'The command was successful.', 'MessageArgs': [], 'MessageId': 'LIC900', 'Name': 'Export: License', 'PercentComplete': 100, 'StartTime': '2024-01-09T05:16:19', 'TargetSettingsURI': None}) + Returns the output for status of the job. + + +error_info (on HTTP error, dict, {'error': {'code': 'Base.1.8.GeneralError', 'message': 'A general error has occurred. See ExtendedInfo for more information.', '@Message.ExtendedInfo': [{'MessageId': 'Base.1.8.AccessDenied', 'Message': 'The authentication credentials included with this request are missing or invalid.', 'MessageArgs': [], 'RelatedProperties': [], 'Severity': 'Critical', 'Resolution': 'Attempt to ensure that the URI is correct and that the service has the appropriate credentials.'}]}}) + Details of the HTTP Error. + + + + + +Status +------ + + + + + +Authors +~~~~~~~ + +- Rajshekar P(@rajshekarp87) + diff --git a/playbooks/idrac/idrac_license.yml b/playbooks/idrac/idrac_license.yml new file mode 100644 index 000000000..2304fa5b6 --- /dev/null +++ b/playbooks/idrac/idrac_license.yml @@ -0,0 +1,183 @@ +--- +- name: Dell OpenManage Ansible iDRAC License Management. + hosts: idrac + gather_facts: false + + tasks: + - name: Export a license from iDRAC to local + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "local" + share_name: "/path/to/share" + file_name: "license_file" + delegate_to: localhost + + - name: Export a license from iDRAC to NFS share + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "nfs" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + delegate_to: localhost + + - name: Export a license from iDRAC to CIFS share + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "cifs" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + username: "username" + password: "password" + workgroup: "workgroup" + delegate_to: localhost + + - name: Export a license from iDRAC to HTTP share via proxy + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "http" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + username: "username" + password: "password" + proxy_support: "parameters_proxy" + proxy_type: socks + proxy_server: "192.168.0.2" + proxy_port: 1080 + proxy_username: "proxy_username" + proxy_password: "proxy_password" + delegate_to: localhost + + - name: Export a license from iDRAC to HTTPS share + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "https" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + username: "username" + password: "password" + ignore_certificate_warning: "on" + delegate_to: localhost + + - name: Import a license to iDRAC from local + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: local + share_name: "/path/to/share" + delegate_to: localhost + + - name: Import a license to iDRAC from NFS share + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: nfs + ip_address: "192.168.0.1" + share_name: "/path/to/share" + delegate_to: localhost + + - name: Import a license to iDRAC from CIFS share + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: cifs + ip_address: "192.168.0.1" + share_name: "/path/to/share" + username: "username" + password: "password" + delegate_to: localhost + + - name: Import a license to iDRAC from HTTP share + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: http + ip_address: "192.168.0.1" + share_name: "/path/to/share" + username: "username" + password: "password" + delegate_to: localhost + + - name: Import a license to iDRAC from HTTPS share via proxy + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: https + ip_address: "192.168.0.1" + share_name: "/path/to/share" + username: "username" + password: "password" + proxy_support: "parameters_proxy" + proxy_server: "192.168.0.2" + proxy_port: 808 + proxy_username: "proxy_username" + proxy_password: "proxy_password" + delegate_to: localhost + + - name: Delete a License from iDRAC + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENCE_123" + delete: true + delegate_to: localhost diff --git a/plugins/README.md b/plugins/README.md index 20d792c2e..7711a1d84 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -30,11 +30,13 @@ Here are the list of modules and module_utils supported by Dell. ├── idrac_certificates.py ├── idrac_firmware.py ├── idrac_firmware_info.py + ├── idrac_license.py ├── idrac_lifecycle_controller_job_status_info.py ├── idrac_lifecycle_controller_jobs.py ├── idrac_lifecycle_controller_logs.py ├── idrac_lifecycle_controller_status_info.py ├── idrac_network.py + ├── idrac_network_attributes.py ├── idrac_os_deployment.py ├── idrac_redfish_storage_controller.py ├── idrac_reset.py diff --git a/plugins/modules/idrac_license.py b/plugins/modules/idrac_license.py new file mode 100644 index 000000000..565c61cd4 --- /dev/null +++ b/plugins/modules/idrac_license.py @@ -0,0 +1,1118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# +# Dell OpenManage Ansible Modules +# Version 8.7.0 +# Copyright (C) 2024 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: idrac_license +short_description: Configure iDRAC licenses +version_added: "8.7.0" +description: + - This module allows to import, export and delete licenses on iDRAC. +extends_documentation_fragment: + - dellemc.openmanage.idrac_auth_options +options: + license_id: + description: + - Entitlement ID of the license that is to be imported, exported or deleted. + - I(license_id) is required when I(delete) is C(true) or I(export) is C(true). + type: str + aliases: ['entitlement_id'] + delete: + description: + - Delete the license from the iDRAC. + - When I(delete) is C(true), then I(license_id) is required. + - I(delete) is mutually exclusive with I(export) and I(import). + type: bool + default: false + export: + description: + - Export the license from the iDRAC. + - When I(export) is C(true), I(license_id) and I(share_parameters) is required. + - I(export) is mutually exclusive with I(delete) and I(import). + type: bool + default: false + import: + description: + - Import the license from the iDRAC. + - When I(import) is C(true), I(share_parameters) is required. + - I(import) is mutually exclusive with I(delete) and I(export). + type: bool + default: false + share_parameters: + description: + - Parameters that are required for the import and export operation of a license. + - I(share_parameters) is required when I(export) or I(import) is C(true). + type: dict + suboptions: + share_type: + description: + - Share type of the network share. + - C(local) uses local path for I(import) and I(export) operation. + - C(nfs) uses NFS share for I(import) and I(export) operation. + - C(cifs) uses CIFS share for I(import) and I(export) operation. + - C(http) uses HTTP share for I(import) and I(export) operation. + - C(https) uses HTTPS share for I(import) and I(export) operation. + type: str + choices: [local, nfs, cifs, http, https] + default: local + file_name: + description: + - License file name for I(import) and I(export) operation. + - I(file_name) is required when I(import) is C(true). + - For the I(import) operation, when I(share_type) is C(local), the supported extensions for I(file_name) are '.txt' and '.xml'. + For other share types, the supported extension is '.xml' + type: str + ip_address: + description: + - IP address of the network share. + - I(ip_address) is required when I(share_type) is C(nfs), C(cifs), C(http) or C(https). + type: str + share_name: + description: + - Network share or local path of the license file. + type: str + workgroup: + description: + - Workgroup of the network share. + - I(workgroup) is applicable only when I(share_type) is C(cifs). + type: str + username: + description: + - Username of the network share. + - I(username) is required when I(share_type) is C(cifs). + type: str + password: + description: + - Password of the network share. + - I(password) is required when I(share_type) is C(cifs). + type: str + ignore_certificate_warning: + description: + - Ignores the certificate warning while connecting to Share and is only applicable when I(share_type) is C(https). + - C(off) ignores the certificate warning. + - C(on) does not ignore the certificate warning. + type: str + choices: ["off", "on"] + default: "off" + proxy_support: + description: + - Specifies if proxy is to be used or not. + - C(off) does not use proxy settings. + - C(default_proxy) uses the default proxy settings. + - C(parameters_proxy) uses the specified proxy settings. I(proxy_server) is required when I(proxy_support) is C(parameters_proxy). + - I(proxy_support) is only applicable when I(share_type) is C(https) or C(https). + type: str + choices: ["off", "default_proxy", "parameters_proxy"] + default: "off" + proxy_type: + description: + - The proxy type of the proxy server. + - C(http) to select HTTP proxy. + - C(socks) to select SOCKS proxy. + - I(proxy_type) is only applicable when I(share_type) is C(https) or C(https) and when I(proxy_support) is C(parameters_proxy). + type: str + choices: [http, socks] + default: http + proxy_server: + description: + - The IP address of the proxy server. + - I(proxy_server) is required when I(proxy_support) is C(parameters_proxy). + - I(proxy_server) is only applicable when I(share_type) is C(https) or C(https) and when I(proxy_support) is C(parameters_proxy). + type: str + proxy_port: + description: + - The port of the proxy server. + - I(proxy_port) is only applicable when I(share_type) is C(https) or C(https) and when I(proxy_support) is C(parameters_proxy). + type: int + default: 80 + proxy_username: + description: + - The username of the proxy server. + - I(proxy_username) is only applicable when I(share_type) is C(https) or C(https) and when I(proxy_support) is C(parameters_proxy). + type: str + proxy_password: + description: + - The password of the proxy server. + - I(proxy_password) is only applicable when I(share_type) is C(https) or C(https) and when I(proxy_support) is C(parameters_proxy). + type: str + resource_id: + type: str + description: + - Id of the resource. + - If the value for resource ID is not provided, the module picks the first resource ID available from the list of system resources returned by the iDRAC. +requirements: + - "python >= 3.9.6" +author: + - "Rajshekar P(@rajshekarp87)" +notes: + - Run this module from a system that has direct access to Dell iDRAC. + - This module supports only iDRAC9 and above. + - This module supports IPv4 and IPv6 addresses. + - This module does not support C(check_mode). + - When I(share_type) is C(local) for I(import) and I(export) operations, job_details are not displayed. +""" + +EXAMPLES = r""" +--- +- name: Export a license from iDRAC to local + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "local" + share_name: "/path/to/share" + file_name: "license_file" + +- name: Export a license from iDRAC to NFS share + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "nfs" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + +- name: Export a license from iDRAC to CIFS share + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "cifs" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + username: "username" + password: "password" + workgroup: "workgroup" + +- name: Export a license from iDRAC to HTTP share via proxy + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "http" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + username: "username" + password: "password" + proxy_support: "parameters_proxy" + proxy_type: socks + proxy_server: "192.168.0.2" + proxy_port: 1080 + proxy_username: "proxy_username" + proxy_password: "proxy_password" + +- name: Export a license from iDRAC to HTTPS share + dellemc.openmanage.idrac_license: + idrac_ip: "192.168.0.1" + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENSE_123" + export: true + share_parameters: + share_type: "https" + share_name: "/path/to/share" + file_name: "license_file" + ip_address: "192.168.0.1" + username: "username" + password: "password" + ignore_certificate_warning: "on" + +- name: Import a license to iDRAC from local + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: local + share_name: "/path/to/share" + +- name: Import a license to iDRAC from NFS share + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: nfs + ip_address: "192.168.0.1" + share_name: "/path/to/share" + +- name: Import a license to iDRAC from CIFS share + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: cifs + ip_address: "192.168.0.1" + share_name: "/path/to/share" + username: "username" + password: "password" + +- name: Import a license to iDRAC from HTTP share + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: http + ip_address: "192.168.0.1" + share_name: "/path/to/share" + username: "username" + password: "password" + +- name: Import a license to iDRAC from HTTPS share via proxy + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + import: true + share_parameters: + file_name: "license_file_name.xml" + share_type: https + ip_address: "192.168.0.1" + share_name: "/path/to/share" + username: "username" + password: "password" + proxy_support: "parameters_proxy" + proxy_server: "192.168.0.2" + proxy_port: 808 + proxy_username: "proxy_username" + proxy_password: "proxy_password" + +- name: Delete a License from iDRAC + dellemc.openmanage.idrac_license: + idrac_ip: 198.162.0.1 + idrac_user: "username" + idrac_password: "password" + ca_path: "/path/to/ca_cert.pem" + license_id: "LICENCE_123" + delete: true +""" + +RETURN = r''' +--- +msg: + type: str + description: Status of the license operation. + returned: always + sample: "Successfully exported the license." +job_details: + description: Returns the output for status of the job. + returned: For import and export operations + type: dict + sample: { + "ActualRunningStartTime": "2024-01-09T05:16:19", + "ActualRunningStopTime": "2024-01-09T05:16:19", + "CompletionTime": "2024-01-09T05:16:19", + "Description": "Job Instance", + "EndTime": null, + "Id": "JID_XXXXXXXXX", + "JobState": "Completed", + "JobType": "LicenseExport", + "Message": "The command was successful.", + "MessageArgs": [], + "MessageId": "LIC900", + "Name": "Export: License", + "PercentComplete": 100, + "StartTime": "2024-01-09T05:16:19", + "TargetSettingsURI": null + } +error_info: + description: Details of the HTTP Error. + returned: on HTTP error + type: dict + sample: { + "error": { + "code": "Base.1.8.GeneralError", + "message": "A general error has occurred. See ExtendedInfo for more information.", + "@Message.ExtendedInfo": [ + { + "MessageId": "Base.1.8.AccessDenied", + "Message": "The authentication credentials included with this request are missing or invalid.", + "MessageArgs": [], + "RelatedProperties": [], + "Severity": "Critical", + "Resolution": "Attempt to ensure that the URI is correct and that the service has the appropriate credentials." + } + ] + } + } +''' + + +import json +import os +import base64 +from urllib.error import HTTPError, URLError +from ansible_collections.dellemc.openmanage.plugins.module_utils.idrac_redfish import iDRACRedfishAPI, idrac_auth_params +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import ConnectionError, SSLValidationError +from ansible.module_utils.compat.version import LooseVersion +from ansible_collections.dellemc.openmanage.plugins.module_utils.utils import ( + get_idrac_firmware_version, get_dynamic_uri, get_manager_res_id, + validate_and_get_first_resource_id_uri, remove_key, idrac_redfish_job_tracking) + +REDFISH = "/redfish/v1" +MANAGERS_URI = "/redfish/v1/Managers" +IDRAC_JOB_URI = "{res_uri}/Jobs/{job_id}" + +OEM = "Oem" +MANUFACTURER = "Dell" +LICENSE_MANAGEMENT_SERVICE = "DellLicenseManagementService" +ACTIONS = "Actions" +EXPORT_LOCAL = "#DellLicenseManagementService.ExportLicense" +EXPORT_NETWORK_SHARE = "#DellLicenseManagementService.ExportLicenseToNetworkShare" +IMPORT_LOCAL = "#DellLicenseManagementService.ImportLicense" +IMPORT_NETWORK_SHARE = "#DellLicenseManagementService.ImportLicenseFromNetworkShare" +ODATA = "@odata.id" +ODATA_REGEX = "(.*?)@odata" + +INVALID_LICENSE_MSG = "License with ID '{license_id}' does not exist on the iDRAC." +SUCCESS_EXPORT_MSG = "Successfully exported the license." +SUCCESS_DELETE_MSG = "Successfully deleted the license." +SUCCESS_IMPORT_MSG = "Successfully imported the license." +FAILURE_MSG = "Unable to '{operation}' the license with id '{license_id}' as it does not exist." +FAILURE_IMPORT_MSG = "Unable to import the license." +NO_FILE_MSG = "License file not found." +UNSUPPORTED_FIRMWARE_MSG = "iDRAC firmware version is not supported." +NO_OPERATION_SKIP_MSG = "Task is skipped as none of import, export or delete is specified." +INVALID_FILE_MSG = "File extension is invalid. Supported extensions for local 'share_type' " \ + "are: .txt and .xml, and for network 'share_type' is: .xml." +INVALID_DIRECTORY_MSG = "Provided directory path '{path}' is not valid." +INSUFFICIENT_DIRECTORY_PERMISSION_MSG = "Provided directory path '{path}' is not writable. " \ + "Please check if the directory has appropriate permissions" +MISSING_FILE_NAME_PARAMETER_MSG = "Missing required parameter 'file_name'." + +PROXY_SUPPORT = {"off": "Off", "default_proxy": "DefaultProxy", "parameters_proxy": "ParametersProxy"} + + +class License(): + def __init__(self, idrac, module): + """ + Initializes the class instance with the provided idrac and module parameters. + + :param idrac: The idrac parameter. + :type idrac: Any + :param module: The module parameter. + :type module: Any + """ + self.idrac = idrac + self.module = module + + def execute(self): + """ + Executes the function with the given module. + + :param module: The module to execute. + :type module: Any + :return: None + """ + + def check_license_id(self, license_id): + """ + Check the license ID for a given operation. + + :param self: The object instance. + :param module: The Ansible module. + :param license_id: The ID of the license to check. + :param operation: The operation to perform. + :return: The response from the license URL. + """ + license_uri = self.get_license_url() + license_url = license_uri + f"/{license_id}" + try: + response = self.idrac.invoke_request(license_url, 'GET') + return response + except Exception: + self.module.exit_json(msg=INVALID_LICENSE_MSG.format(license_id=license_id), skipped=True) + + def get_license_url(self): + """ + Retrieves the license URL for the current user. + + :return: The license URL as a string. + """ + v1_resp = get_dynamic_uri(self.idrac, REDFISH) + license_service_url = v1_resp.get('LicenseService', {}).get(ODATA, {}) + license_service_resp = get_dynamic_uri(self.idrac, license_service_url) + license_url = license_service_resp.get('Licenses', {}).get(ODATA, {}) + return license_url + + def get_job_status(self, license_job_response): + """ + Get the status of a job. + + Args: + module (object): The module object. + license_job_response (object): The response object for the license job. + + Returns: + dict: The job details. + """ + res_uri = validate_and_get_first_resource_id_uri(self.module, self.idrac, MANAGERS_URI) + job_tracking_uri = license_job_response.headers.get("Location") + job_id = job_tracking_uri.split("/")[-1] + job_uri = IDRAC_JOB_URI.format(job_id=job_id, res_uri=res_uri[0]) + job_failed, msg, job_dict, wait_time = idrac_redfish_job_tracking(self.idrac, job_uri) + job_dict = remove_key(job_dict, regex_pattern=ODATA_REGEX) + if job_failed: + self.module.exit_json( + msg=job_dict.get('Message'), + failed=True, + job_details=job_dict) + return job_dict + + def get_share_details(self): + """ + Retrieves the share details from the given module. + + Args: + module (object): The module object containing the share parameters. + + Returns: + dict: A dictionary containing the share details with the following keys: + - IPAddress (str): The IP address of the share. + - ShareName (str): The name of the share. + - UserName (str): The username for accessing the share. + - Password (str): The password for accessing the share. + """ + share_details = {} + share_details["IPAddress"] = self.module.params.get('share_parameters').get('ip_address') + share_details["ShareName"] = self.module.params.get('share_parameters').get('share_name') + share_details["UserName"] = self.module.params.get('share_parameters').get('username') + share_details["Password"] = self.module.params.get('share_parameters').get('password') + return share_details + + def get_proxy_details(self): + """ + Retrieves the proxy details based on the provided module parameters. + + Args: + self: The instance of the class. + module: The module object containing the parameters. + + Returns: + dict: A dictionary containing the proxy details. + """ + proxy_details = {} + proxy_details["ShareType"] = self.module.params.get('share_parameters').get('share_type').upper() + share_details = self.get_share_details() + proxy_details.update(share_details) + proxy_details["IgnoreCertWarning"] = self.module.params.get('share_parameters').get('ignore_certificate_warning').capitalize() + if self.module.params.get('share_parameters').get('proxy_support') == "parameters_proxy": + proxy_details["ProxySupport"] = PROXY_SUPPORT[self.module.params.get('share_parameters').get('proxy_support')] + proxy_details["ProxyType"] = self.module.params.get('share_parameters').get('proxy_type').upper() + proxy_details["ProxyServer"] = self.module.params.get('share_parameters').get('proxy_server') + proxy_details["ProxyPort"] = str(self.module.params.get('share_parameters').get('proxy_port')) + if self.module.params.get('share_parameters').get('proxy_username') and self.module.params.get('share_parameters').get('proxy_password'): + proxy_details["ProxyUname"] = self.module.params.get('share_parameters').get('proxy_username') + proxy_details["ProxyPasswd"] = self.module.params.get('share_parameters').get('proxy_password') + return proxy_details + + +class DeleteLicense(License): + def execute(self): + """ + Executes the delete operation for a given license ID. + + Args: + module (object): The Ansible module object. + + Returns: + object: The response object from the delete operation. + """ + license_id = self.module.params.get('license_id') + self.check_license_id(license_id) + license_url = self.get_license_url() + delete_license_url = license_url + f"/{license_id}" + delete_license_response = self.idrac.invoke_request(delete_license_url, 'DELETE') + status = delete_license_response.status_code + if status == 204: + self.module.exit_json(msg=SUCCESS_DELETE_MSG, changed=True) + else: + self.module.exit_json(msg=FAILURE_MSG.format(operation="delete", license_id=license_id), failed=True) + + +class ExportLicense(License): + STATUS_SUCCESS = [200, 202] + + def execute(self): + """ + Executes the export operation for a given license ID. + + :param module: The Ansible module object. + :type module: AnsibleModule + + :return: The response from the export operation. + :rtype: Response + """ + share_type = self.module.params.get('share_parameters').get('share_type') + license_id = self.module.params.get('license_id') + self.check_license_id(license_id) + export_license_url = self.__get_export_license_url() + job_status = {} + if share_type == "local": + export_license_response = self.__export_license_local(export_license_url) + elif share_type in ["http", "https"]: + export_license_response = self.__export_license_http(export_license_url) + job_status = self.get_job_status(export_license_response) + elif share_type == "cifs": + export_license_response = self.__export_license_cifs(export_license_url) + job_status = self.get_job_status(export_license_response) + elif share_type == "nfs": + export_license_response = self.__export_license_nfs(export_license_url) + job_status = self.get_job_status(export_license_response) + status = export_license_response.status_code + if status in self.STATUS_SUCCESS: + self.module.exit_json(msg=SUCCESS_EXPORT_MSG, changed=True, job_details=job_status) + else: + self.module.exit_json(msg=FAILURE_MSG.format(operation="export", license_id=license_id), failed=True, job_details=job_status) + + def __export_license_local(self, export_license_url): + """ + Export the license to a local directory. + + Args: + module (object): The Ansible module object. + export_license_url (str): The URL for exporting the license. + + Returns: + object: The license status after exporting. + """ + payload = {} + payload["EntitlementID"] = self.module.params.get('license_id') + path = self.module.params.get('share_parameters').get('share_name') + if not (os.path.exists(path) or os.path.isdir(path)): + self.module.exit_json(msg=INVALID_DIRECTORY_MSG.format(path=path), failed=True) + if not os.access(path, os.W_OK): + self.module.exit_json(msg=INSUFFICIENT_DIRECTORY_PERMISSION_MSG.format(path=path), failed=True) + license_name = self.module.params.get('share_parameters').get('file_name') + if license_name: + license_file_name = f"{license_name}_iDRAC_license.txt" + else: + license_file_name = f"{self.module.params['license_id']}_iDRAC_license.txt" + license_status = self.idrac.invoke_request(export_license_url, "POST", data=payload) + license_data = license_status.json_data + license_file = license_data.get("LicenseFile") + file_name = os.path.join(path, license_file_name) + with open(file_name, "w") as fp: + fp.writelines(license_file) + return license_status + + def __export_license_http(self, export_license_url): + """ + Export the license using the HTTP protocol. + + Args: + module (object): The module object. + export_license_url (str): The URL for exporting the license. + + Returns: + str: The export status. + """ + payload = {} + payload["EntitlementID"] = self.module.params.get('license_id') + proxy_details = self.get_proxy_details() + payload.update(proxy_details) + export_status = self.__export_license(payload, export_license_url) + return export_status + + def __export_license_cifs(self, export_license_url): + """ + Export the license using CIFS share type. + + Args: + module (object): The Ansible module object. + export_license_url (str): The URL for exporting the license. + + Returns: + str: The export status. + """ + payload = {} + payload["EntitlementID"] = self.module.params.get('license_id') + payload["ShareType"] = "CIFS" + if self.module.params.get('share_parameters').get('workgroup'): + payload["Workgroup"] = self.module.params.get('share_parameters').get('workgroup') + share_details = self.get_share_details() + payload.update(share_details) + export_status = self.__export_license(payload, export_license_url) + return export_status + + def __export_license_nfs(self, export_license_url): + """ + Export the license using NFS share type. + + Args: + module (object): The Ansible module object. + export_license_url (str): The URL for exporting the license. + + Returns: + dict: The export status of the license. + """ + payload = {} + payload["EntitlementID"] = self.module.params.get('license_id') + payload["ShareType"] = "NFS" + payload["IPAddress"] = self.module.params.get('share_parameters').get('ip_address') + payload["ShareName"] = self.module.params.get('share_parameters').get('share_name') + export_status = self.__export_license(payload, export_license_url) + return export_status + + def __get_export_license_url(self): + """ + Get the export license URL. + + :param module: The module object. + :type module: object + :return: The export license URL. + :rtype: str + """ + uri, error_msg = validate_and_get_first_resource_id_uri( + self.module, self.idrac, MANAGERS_URI) + if error_msg: + self.module.exit_json(msg=error_msg, failed=True) + resp = get_dynamic_uri(self.idrac, uri) + url = resp.get('Links', {}).get(OEM, {}).get(MANUFACTURER, {}).get(LICENSE_MANAGEMENT_SERVICE, {}).get(ODATA, {}) + action_resp = get_dynamic_uri(self.idrac, url) + license_service = EXPORT_LOCAL if self.module.params.get('share_parameters').get('share_type') == "local" else EXPORT_NETWORK_SHARE + export_url = action_resp.get(ACTIONS, {}).get(license_service, {}).get('target', {}) + return export_url + + def __export_license(self, payload, export_license_url): + """ + Export the license to a file. + + Args: + module (object): The Ansible module object. + payload (dict): The payload containing the license information. + export_license_url (str): The URL for exporting the license. + + Returns: + dict: The license status after exporting. + """ + license_name = self.module.params.get('share_parameters').get('file_name') + if license_name: + license_file_name = f"{license_name}_iDRAC_license.xml" + else: + license_file_name = f"{self.module.params['license_id']}_iDRAC_license.xml" + payload["FileName"] = license_file_name + license_status = self.idrac.invoke_request(export_license_url, "POST", data=payload) + return license_status + + +class ImportLicense(License): + STATUS_SUCCESS = [200, 202] + + def execute(self): + """ + Executes the import license process based on the given module parameters. + + Args: + module (object): The Ansible module object. + + Returns: + object: The response object from the import license API call. + """ + if not self.module.params.get('share_parameters').get('file_name'): + self.module.exit_json(msg=MISSING_FILE_NAME_PARAMETER_MSG, failed=True) + share_type = self.module.params.get('share_parameters').get('share_type') + self.__check_file_extension() + import_license_url = self.__get_import_license_url() + resource_id = get_manager_res_id(self.idrac) + job_status = {} + if share_type == "local": + import_license_response = self.__import_license_local(import_license_url, resource_id) + elif share_type in ["http", "https"]: + import_license_response = self.__import_license_http(import_license_url, resource_id) + job_status = self.get_job_status(import_license_response) + elif share_type == "cifs": + import_license_response = self.__import_license_cifs(import_license_url, resource_id) + job_status = self.get_job_status(import_license_response) + elif share_type == "nfs": + import_license_response = self.__import_license_nfs(import_license_url, resource_id) + job_status = self.get_job_status(import_license_response) + status = import_license_response.status_code + if status in self.STATUS_SUCCESS: + self.module.exit_json(msg=SUCCESS_IMPORT_MSG, changed=True, job_details=job_status) + else: + self.module.exit_json(msg=FAILURE_IMPORT_MSG, failed=True, job_details=job_status) + + def __import_license_local(self, import_license_url, resource_id): + """ + Import a license locally. + + Args: + module (object): The Ansible module object. + import_license_url (str): The URL for importing the license. + resource_id (str): The ID of the resource. + + Returns: + dict: The import status of the license. + """ + payload = {} + path = self.module.params.get('share_parameters').get('share_name') + if not (os.path.exists(path) or os.path.isdir(path)): + self.module.exit_json(msg=INVALID_DIRECTORY_MSG.format(path=path), failed=True) + file_path = self.module.params.get('share_parameters').get('share_name') + "/" + self.module.params.get('share_parameters').get('file_name') + file_exits = os.path.exists(file_path) + if file_exits: + with open(file_path, "rb") as cert: + cert_content = cert.read() + read_file = base64.encodebytes(cert_content).decode('ascii') + else: + self.module.exit_json(msg=NO_FILE_MSG, failed=True) + payload["LicenseFile"] = read_file + payload["FQDD"] = resource_id + payload["ImportOptions"] = "Force" + try: + import_status = self.idrac.invoke_request(import_license_url, "POST", data=payload) + except HTTPError as err: + filter_err = remove_key(json.load(err), regex_pattern=ODATA_REGEX) + message_details = filter_err.get('error').get('@Message.ExtendedInfo')[0] + message_id = message_details.get('MessageId') + if 'LIC018' in message_id: + self.module.exit_json(msg=message_details.get('Message'), skipped=True) + else: + self.module.exit_json(msg=message_details.get('Message'), error_info=filter_err, failed=True) + return import_status + + def __import_license_http(self, import_license_url, resource_id): + """ + Imports a license using HTTP. + + Args: + module (object): The Ansible module object. + import_license_url (str): The URL for importing the license. + resource_id (str): The ID of the resource. + + Returns: + object: The import status. + """ + payload = {} + payload["LicenseName"] = self.module.params.get('share_parameters').get('file_name') + payload["FQDD"] = resource_id + payload["ImportOptions"] = "Force" + proxy_details = self.get_proxy_details() + payload.update(proxy_details) + import_status = self.idrac.invoke_request(import_license_url, "POST", data=payload) + return import_status + + def __import_license_cifs(self, import_license_url, resource_id): + """ + Imports a license using CIFS share type. + + Args: + self (object): The instance of the class. + module (object): The Ansible module object. + import_license_url (str): The URL for importing the license. + resource_id (str): The ID of the resource. + + Returns: + object: The import status of the license. + """ + payload = {} + payload["ShareType"] = "CIFS" + payload["LicenseName"] = self.module.params.get('share_parameters').get('file_name') + payload["FQDD"] = resource_id + payload["ImportOptions"] = "Force" + if self.module.params.get('share_parameters').get('workgroup'): + payload["Workgroup"] = self.module.params.get('share_parameters').get('workgroup') + share_details = self.get_share_details() + payload.update(share_details) + import_status = self.idrac.invoke_request(import_license_url, "POST", data=payload) + return import_status + + def __import_license_nfs(self, import_license_url, resource_id): + """ + Import a license from an NFS share. + + Args: + module (object): The Ansible module object. + import_license_url (str): The URL for importing the license. + resource_id (str): The ID of the resource. + + Returns: + dict: The import status of the license. + """ + payload = {} + payload["ShareType"] = "NFS" + payload["IPAddress"] = self.module.params.get('share_parameters').get('ip_address') + payload["ShareName"] = self.module.params.get('share_parameters').get('share_name') + payload["LicenseName"] = self.module.params.get('share_parameters').get('file_name') + payload["FQDD"] = resource_id + payload["ImportOptions"] = "Force" + import_status = self.idrac.invoke_request(import_license_url, "POST", data=payload) + return import_status + + def __check_file_extension(self): + """ + Check if the file extension of the given file name is valid. + + :param module: The Ansible module object. + :type module: AnsibleModule + + :return: None + """ + share_type = self.module.params.get('share_parameters').get('share_type') + file_name = self.module.params.get('share_parameters').get('file_name') + valid_extensions = {".txt", ".xml"} if share_type == "local" else {".xml"} + file_extension = any(file_name.lower().endswith(ext) for ext in valid_extensions) + if not file_extension: + self.module.exit_json(msg=INVALID_FILE_MSG, failed=True) + + def __get_import_license_url(self): + """ + Get the import license URL. + + :param module: The module object. + :type module: object + :return: The import license URL. + :rtype: str + """ + uri, error_msg = validate_and_get_first_resource_id_uri( + self.module, self.idrac, MANAGERS_URI) + if error_msg: + self.module.exit_json(msg=error_msg, failed=True) + resp = get_dynamic_uri(self.idrac, uri) + url = resp.get('Links', {}).get(OEM, {}).get(MANUFACTURER, {}).get(LICENSE_MANAGEMENT_SERVICE, {}).get(ODATA, {}) + action_resp = get_dynamic_uri(self.idrac, url) + license_service = IMPORT_LOCAL if self.module.params.get('share_parameters').get('share_type') == "local" else IMPORT_NETWORK_SHARE + import_url = action_resp.get(ACTIONS, {}).get(license_service, {}).get('target', {}) + return import_url + + def get_job_status(self, license_job_response): + res_uri = validate_and_get_first_resource_id_uri(self.module, self.idrac, MANAGERS_URI) + job_tracking_uri = license_job_response.headers.get("Location") + job_id = job_tracking_uri.split("/")[-1] + job_uri = IDRAC_JOB_URI.format(job_id=job_id, res_uri=res_uri[0]) + job_failed, msg, job_dict, wait_time = idrac_redfish_job_tracking(self.idrac, job_uri) + job_dict = remove_key(job_dict, regex_pattern=ODATA_REGEX) + if job_failed: + if job_dict.get('MessageId') == 'LIC018': + self.module.exit_json(msg=job_dict.get('Message'), skipped=True, job_details=job_dict) + else: + self.module.exit_json( + msg=job_dict.get('Message'), + failed=True, + job_details=job_dict) + return job_dict + + +class LicenseType: + _license_classes = { + "import": ImportLicense, + "export": ExportLicense, + "delete": DeleteLicense, + } + + @staticmethod + def license_operation(idrac, module): + """ + Perform a license operation based on the given parameters. + + :param idrac: The IDRAC object. + :type idrac: IDRAC + :param module: The Ansible module object. + :type module: AnsibleModule + :return: The license class object based on the license type. + :rtype: LicenseType + """ + license_type = next((param for param in ["import", "export", "delete"] if module.params[param]), None) + if not license_type: + module.exit_json(msg=NO_OPERATION_SKIP_MSG, skipped=True) + license_class = LicenseType._license_classes.get(license_type) + return license_class(idrac, module) + + +def main(): + """ + Main function that serves as the entry point for the program. + + This function retrieves the argument specification using the `get_argument_spec` function and updates it with the `idrac_auth_params`. + It then creates an `AnsibleModule` object with the updated argument specification, specifying the mutually exclusive arguments, + required arguments if conditions are met, and setting `supports_check_mode` to `False`. + + The function then attempts to establish a connection with the iDRAC Redfish API using the `iDRACRedfishAPI` class. + It retrieves the iDRAC firmware version using the `get_idrac_firmware_version` function and checks if it is less than or equal to '3.0'. + If it is, the function exits with a message indicating that the iDRAC firmware version is not supported and sets `failed` to `True`. + + If the iDRAC firmware version is supported, the function creates a `LicenseType` object using the `license_operation` method of the + `LicenseType` class and calls the `execute` method on the `license_obj` object, passing in the `module` object. + + If an `HTTPError` occurs, the function loads the error response as JSON, removes a specific key using a regular expression pattern, + and exits with the error message, the filtered error information, and sets `failed` to `True`. + + If a `URLError` occurs, the function exits with the error message and sets `unreachable` to `True`. + + If any of the following errors occur: `SSLValidationError`, `ConnectionError`, `TypeError`, `ValueError`, or `OSError`, the function + exits with the error message and sets `failed` to `True`. + + Parameters: + None + + Returns: + None + """ + specs = get_argument_spec() + specs.update(idrac_auth_params) + module = AnsibleModule( + argument_spec=specs, + mutually_exclusive=[("import", "export", "delete")], + required_if=[ + ["import", True, ("share_parameters",)], + ["export", True, ("license_id", "share_parameters",)], + ["delete", True, ("license_id",)] + ], + supports_check_mode=False + ) + + try: + with iDRACRedfishAPI(module.params) as idrac: + idrac_firmware_version = get_idrac_firmware_version(idrac) + if LooseVersion(idrac_firmware_version) <= '3.0': + module.exit_json(msg=UNSUPPORTED_FIRMWARE_MSG, failed=True) + license_obj = LicenseType.license_operation(idrac, module) + if license_obj: + license_obj.execute() + except HTTPError as err: + filter_err = remove_key(json.load(err), regex_pattern=ODATA_REGEX) + module.exit_json(msg=str(err), error_info=filter_err, failed=True) + except URLError as err: + module.exit_json(msg=str(err), unreachable=True) + except (SSLValidationError, ConnectionError, TypeError, ValueError, OSError) as err: + module.exit_json(msg=str(err), failed=True) + + +def get_argument_spec(): + """ + Returns a dictionary containing the argument spec for the get_argument_spec function. + The argument spec is a dictionary that defines the parameters and their types and options for the function. + The dictionary has the following keys: + - "license_id": A string representing the license ID. + - "delete": A boolean representing whether to delete the license. + - "export": A boolean representing whether to export the license. + - "import": A boolean representing whether to import the license. + - "share_parameters": A dictionary representing the share parameters. + - "type": A string representing the share type. + - "options": A dictionary representing the options for the share parameters. + - "share_type": A string representing the share type. + - "file_name": A string representing the file name. + - "ip_address": A string representing the IP address. + - "share_name": A string representing the share name. + - "workgroup": A string representing the workgroup. + - "username": A string representing the username. + - "password": A string representing the password. + - "ignore_certificate_warning": A string representing whether to ignore certificate warnings. + - "proxy_support": A string representing the proxy support. + - "proxy_type": A string representing the proxy type. + - "proxy_server": A string representing the proxy server. + - "proxy_port": A integer representing the proxy port. + - "proxy_username": A string representing the proxy username. + - "proxy_password": A string representing the proxy password. + - "required_if": A list of lists representing the required conditions for the share parameters. + - "required_together": A list of lists representing the required conditions for the share parameters. + - "resource_id": A string representing the resource ID. + """ + return { + "license_id": {"type": 'str', "aliases": ['entitlement_id']}, + "delete": {"type": 'bool', "default": False}, + "export": {"type": 'bool', "default": False}, + "import": {"type": 'bool', "default": False}, + "share_parameters": { + "type": 'dict', + "options": { + "share_type": { + "type": 'str', + "default": 'local', + "choices": ['local', 'nfs', 'cifs', 'http', 'https'] + }, + "file_name": {"type": 'str'}, + "ip_address": {"type": 'str'}, + "share_name": {"type": 'str'}, + "workgroup": {"type": 'str'}, + "username": {"type": 'str'}, + "password": {"type": 'str', "no_log": True}, + "ignore_certificate_warning": { + "type": 'str', + "default": "off", + "choices": ["off", "on"] + }, + "proxy_support": { + "type": 'str', + "default": "off", + "choices": ["off", "default_proxy", "parameters_proxy"] + }, + "proxy_type": { + "type": 'str', + "default": 'http', + "choices": ['http', 'socks'] + }, + "proxy_server": {"type": 'str'}, + "proxy_port": {"type": 'int', "default": 80}, + "proxy_username": {"type": 'str'}, + "proxy_password": {"type": 'str', "no_log": True} + }, + "required_if": [ + ["share_type", "local", ["share_name"]], + ["share_type", "nfs", ["ip_address", "share_name"]], + ["share_type", "cifs", ["ip_address", "share_name", "username", "password"]], + ["share_type", "http", ["ip_address", "share_name"]], + ["share_type", "https", ["ip_address", "share_name"]], + ["proxy_support", "parameters_proxy", ["proxy_server"]] + ], + "required_together": [ + ("username", "password"), + ("proxy_username", "proxy_password") + ] + }, + "resource_id": {"type": 'str'} + } + + +if __name__ == '__main__': + main() diff --git a/tests/unit/plugins/modules/test_idrac_license.py b/tests/unit/plugins/modules/test_idrac_license.py new file mode 100644 index 000000000..a07cc1eb1 --- /dev/null +++ b/tests/unit/plugins/modules/test_idrac_license.py @@ -0,0 +1,746 @@ +# -*- coding: utf-8 -*- + +# +# Dell OpenManage Ansible Modules +# Version 8.7.0 +# Copyright (C) 2024 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 + +from io import StringIO +import json +import tempfile +import os + +import pytest +from urllib.error import HTTPError, URLError +from ansible.module_utils.urls import ConnectionError, SSLValidationError +from ansible.module_utils._text import to_text +from ansible_collections.dellemc.openmanage.plugins.modules import idrac_license +from ansible_collections.dellemc.openmanage.tests.unit.plugins.modules.common import FakeAnsibleModule +from mock import MagicMock +from ansible_collections.dellemc.openmanage.plugins.modules.idrac_license import main + +MODULE_PATH = 'ansible_collections.dellemc.openmanage.plugins.modules.idrac_license.' +MODULE_UTILS_PATH = 'ansible_collections.dellemc.openmanage.plugins.module_utils.utils.' + +INVALID_LICENSE_MSG = "License with ID '{license_id}' does not exist on the iDRAC." +SUCCESS_EXPORT_MSG = "Successfully exported the license." +SUCCESS_DELETE_MSG = "Successfully deleted the license." +SUCCESS_IMPORT_MSG = "Successfully imported the license." +FAILURE_MSG = "Unable to '{operation}' the license with id '{license_id}' as it does not exist." +FAILURE_IMPORT_MSG = "Unable to import the license." +NO_FILE_MSG = "License file not found." +UNSUPPORTED_FIRMWARE_MSG = "iDRAC firmware version is not supported." +NO_OPERATION_SKIP_MSG = "Task is skipped as none of import, export or delete is specified." +INVALID_FILE_MSG = "File extension is invalid. Supported extensions for local 'share_type' " \ + "are: .txt and .xml, and for network 'share_type' is: .xml." +INVALID_DIRECTORY_MSG = "Provided directory path '{path}' is not valid." +INSUFFICIENT_DIRECTORY_PERMISSION_MSG = "Provided directory path '{path}' is not writable. " \ + "Please check if the directory has appropriate permissions" +MISSING_FILE_NAME_PARAMETER_MSG = "Missing required parameter 'file_name'." +REDFISH = "/redfish/v1" + +LIC_GET_LICENSE_URL = "License.get_license_url" +REDFISH_LICENSE_URL = "/redfish/v1/license" +REDFISH_BASE_API = '/redfish/v1/api' +MANAGER_URI_ONE = "/redfish/v1/managers/1" +API_ONE = "/local/action" +EXPORT_URL_MOCK = '/redfish/v1/export_license' +IMPORT_URL_MOCK = '/redfish/v1/import_license' +API_INVOKE_MOCKER = "iDRACRedfishAPI.invoke_request" +ODATA = "@odata.id" +IDRAC_ID = "iDRAC.Embedded.1" +LIC_FILE_NAME = 'test_lic.txt' +HTTPS_PATH = "https://testhost.com" +HTTP_ERROR = "http error message" +APPLICATION_JSON = "application/json" + + +class TestLicense(FakeAnsibleModule): + module = idrac_license + + @pytest.fixture + def idrac_license_mock(self): + idrac_obj = MagicMock() + return idrac_obj + + @pytest.fixture + def idrac_connection_license_mock(self, mocker, idrac_license_mock): + idrac_conn_mock = mocker.patch(MODULE_PATH + 'iDRACRedfishAPI', + return_value=idrac_license_mock) + idrac_conn_mock.return_value.__enter__.return_value = idrac_license_mock + return idrac_conn_mock + + def test_check_license_id(self, idrac_default_args, idrac_connection_license_mock, + idrac_license_mock, mocker): + mocker.patch(MODULE_PATH + LIC_GET_LICENSE_URL, + return_value=REDFISH_LICENSE_URL) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=False) + lic_obj = self.module.License( + idrac_connection_license_mock, f_module) + + idr_obj = MagicMock() + idr_obj.json_data = {"license_id": "1234"} + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + return_value=idr_obj) + data = lic_obj.check_license_id(license_id="1234") + assert data.json_data == {"license_id": "1234"} + + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + side_effect=HTTPError(HTTPS_PATH, 400, + HTTP_ERROR, + {"accept-type": APPLICATION_JSON}, + StringIO("json_str"))) + with pytest.raises(Exception) as exc: + lic_obj.check_license_id(license_id="1234") + assert exc.value.args[0] == INVALID_LICENSE_MSG.format(license_id="1234") + + def test_get_license_url(self, idrac_default_args, idrac_connection_license_mock, mocker): + v1_resp = {"LicenseService": {ODATA: "/redfish/v1/LicenseService"}, + "Licenses": {ODATA: "/redfish/v1/LicenseService/Licenses"}} + mocker.patch(MODULE_PATH + "get_dynamic_uri", + return_value=v1_resp) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=False) + lic_obj = self.module.License( + idrac_connection_license_mock, f_module) + data = lic_obj.get_license_url() + assert data == "/redfish/v1/LicenseService/Licenses" + + def test_get_job_status_success(self, mocker, idrac_license_mock): + # Mocking necessary objects and functions + module_mock = self.get_module_mock() + license_job_response_mock = mocker.MagicMock() + license_job_response_mock.headers.get.return_value = "HTTPS_PATH/job_tracking/12345" + + mocker.patch(MODULE_PATH + "remove_key", return_value={"job_details": "mocked_job_details"}) + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", return_value=[MANAGER_URI_ONE]) + + # Creating an instance of the class + obj_under_test = self.module.License(idrac_license_mock, module_mock) + + # Mocking the idrac_redfish_job_tracking function to simulate a successful job tracking + mocker.patch(MODULE_PATH + "idrac_redfish_job_tracking", return_value=(False, "mocked_message", {"job_details": "mocked_job_details"}, 0)) + + # Calling the method under test + result = obj_under_test.get_job_status(license_job_response_mock) + + # Assertions + assert result == {"job_details": "mocked_job_details"} + + def test_get_job_status_failure(self, mocker, idrac_license_mock): + # Mocking necessary objects and functions + module_mock = self.get_module_mock() + license_job_response_mock = mocker.MagicMock() + license_job_response_mock.headers.get.return_value = "HTTPS_PATH/job_tracking/12345" + + mocker.patch(MODULE_PATH + "remove_key", return_value={"Message": "None"}) + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", return_value=[MANAGER_URI_ONE]) + + # Creating an instance of the class + obj_under_test = self.module.License(idrac_license_mock, module_mock) + + # Mocking the idrac_redfish_job_tracking function to simulate a failed job tracking + mocker.patch(MODULE_PATH + "idrac_redfish_job_tracking", return_value=(True, "None", {"Message": "None"}, 0)) + + # Mocking module.exit_json + exit_json_mock = mocker.patch.object(module_mock, "exit_json") + + # Calling the method under test + result = obj_under_test.get_job_status(license_job_response_mock) + + # Assertions + exit_json_mock.assert_called_once_with(msg="None", failed=True, job_details={"Message": "None"}) + assert result == {"Message": "None"} + + def test_get_share_details(self, idrac_connection_license_mock): + # Create a mock module object + module_mock = MagicMock() + module_mock.params.get.return_value = { + 'ip_address': 'XX.XX.XX.XX', + 'share_name': 'my_share', + 'username': 'my_user', + 'password': 'my_password' + } + + # Create an instance of the License class + lic_obj = self.module.License(idrac_connection_license_mock, module_mock) + + # Call the get_share_details method + result = lic_obj.get_share_details() + + # Assert the result + assert result == { + 'IPAddress': 'XX.XX.XX.XX', + 'ShareName': 'my_share', + 'UserName': 'my_user', + 'Password': 'my_password' + } + + def test_get_proxy_details(self, idrac_connection_license_mock): + # Create a mock module object + module_mock = MagicMock() + module_mock.params.get.return_value = { + 'ip_address': 'XX.XX.XX.XX', + 'share_name': 'my_share', + 'username': 'my_user', + 'password': 'my_password', + 'share_type': 'http', + 'ignore_certificate_warning': 'off', + 'proxy_support': 'parameters_proxy', + 'proxy_type': 'http', + 'proxy_server': 'proxy.example.com', + 'proxy_port': 8080, + 'proxy_username': 'my_username', + 'proxy_password': 'my_password' + } + + # Create an instance of the License class + lic_obj = self.module.License(idrac_connection_license_mock, module_mock) + + # Call the get_proxy_details method + result = lic_obj.get_proxy_details() + + # Define the expected result + expected_result = { + 'IPAddress': 'XX.XX.XX.XX', + 'ShareName': 'my_share', + 'UserName': 'my_user', + 'Password': 'my_password', + 'ShareType': 'HTTP', + 'IgnoreCertWarning': 'Off', + 'ProxySupport': 'ParametersProxy', + 'ProxyType': 'HTTP', + 'ProxyServer': 'proxy.example.com', + 'ProxyPort': '8080', + 'ProxyUname': 'my_username', + 'ProxyPasswd': 'my_password' + } + + # Assert the result + assert result == expected_result + + +class TestDeleteLicense: + @pytest.fixture + def delete_license_mock(self): + delete_license_obj = MagicMock() + return delete_license_obj + + @pytest.fixture + def idrac_connection_license_mock(self, mocker, delete_license_mock): + idrac_conn_mock = mocker.patch(MODULE_PATH + 'iDRACRedfishAPI', + return_value=delete_license_mock) + idrac_conn_mock.return_value.__enter__.return_value = delete_license_mock + return idrac_conn_mock + + def test_execute_delete_license_success(self, mocker, idrac_connection_license_mock): + mocker.patch(MODULE_PATH + LIC_GET_LICENSE_URL, + return_value=REDFISH_LICENSE_URL) + f_module = MagicMock() + f_module.params = {'license_id': '1234'} + delete_license_obj = idrac_license.DeleteLicense(idrac_connection_license_mock, f_module) + delete_license_obj.idrac.invoke_request.return_value.status_code = 204 + delete_license_obj.execute() + f_module.exit_json.assert_called_once_with(msg=SUCCESS_DELETE_MSG, changed=True) + + def test_execute_delete_license_failure(self, mocker, idrac_connection_license_mock): + mocker.patch(MODULE_PATH + LIC_GET_LICENSE_URL, + return_value=REDFISH_LICENSE_URL) + f_module = MagicMock() + f_module.params = {'license_id': '5678'} + delete_license_obj = idrac_license.DeleteLicense(idrac_connection_license_mock, f_module) + delete_license_obj.idrac.invoke_request.return_value.status_code = 404 + delete_license_obj.execute() + f_module.exit_json.assert_called_once_with(msg=FAILURE_MSG.format(operation="delete", license_id="5678"), failed=True) + + +class TestExportLicense(FakeAnsibleModule): + module = idrac_license + + @pytest.fixture + def idrac_license_mock(self): + idrac_obj = MagicMock() + return idrac_obj + + @pytest.fixture + def idrac_connection_license_mock(self, mocker, idrac_license_mock): + idrac_conn_mock = mocker.patch(MODULE_PATH + 'iDRACRedfishAPI', + return_value=idrac_license_mock) + idrac_conn_mock.return_value.__enter__.return_value = idrac_license_mock + return idrac_conn_mock + + def test_export_license_local(self, idrac_default_args, idrac_connection_license_mock, mocker): + tmp_path = tempfile.gettempdir() + export_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'share_name': str(tmp_path), + 'file_name': 'test_lic' + } + } + idr_obj = MagicMock() + idr_obj.json_data = {"license_id": "1234", "LicenseFile": "test_license_content"} + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + return_value=idr_obj) + idrac_default_args.update(export_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + export_license_obj = self.module.ExportLicense(idrac_connection_license_mock, f_module) + result = export_license_obj._ExportLicense__export_license_local(EXPORT_URL_MOCK) + assert result.json_data == {'LicenseFile': 'test_license_content', 'license_id': '1234'} + assert os.path.exists(f"{tmp_path}/test_lic_iDRAC_license.txt") + if os.path.exists(f"{tmp_path}/test_lic_iDRAC_license.txt"): + os.remove(f"{tmp_path}/test_lic_iDRAC_license.txt") + + export_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'share_name': str(tmp_path), + } + } + idrac_default_args.update(export_params) + result = export_license_obj._ExportLicense__export_license_local(EXPORT_URL_MOCK) + assert result.json_data == {'LicenseFile': 'test_license_content', 'license_id': '1234'} + assert os.path.exists(f"{tmp_path}/test_license_id_iDRAC_license.txt") + if os.path.exists(f"{tmp_path}/test_license_id_iDRAC_license.txt"): + os.remove(f"{tmp_path}/test_license_id_iDRAC_license.txt") + + def test_export_license_http(self, idrac_default_args, idrac_connection_license_mock, mocker): + export_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic', + 'share_type': 'http', + 'ignore_certificate_warning': 'off' + } + } + idr_obj = MagicMock() + idr_obj.json_data = {"license_id": "1234", "LicenseFile": "test_license_content"} + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + return_value=idr_obj) + idrac_default_args.update(export_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + export_license_obj = self.module.ExportLicense(idrac_connection_license_mock, f_module) + result = export_license_obj._ExportLicense__export_license_http(EXPORT_URL_MOCK) + assert result.json_data == {'LicenseFile': 'test_license_content', 'license_id': '1234'} + + def test_export_license_cifs(self, idrac_default_args, idrac_connection_license_mock, mocker): + export_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic', + 'share_type': 'cifs', + 'ignore_certificate_warning': 'off', + 'workgroup': "mydomain" + } + } + idr_obj = MagicMock() + idr_obj.json_data = {"license_id": "1234", "LicenseFile": "test_license_content"} + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + return_value=idr_obj) + idrac_default_args.update(export_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + export_license_obj = self.module.ExportLicense(idrac_connection_license_mock, f_module) + result = export_license_obj._ExportLicense__export_license_cifs(EXPORT_URL_MOCK) + assert result.json_data == {'LicenseFile': 'test_license_content', 'license_id': '1234'} + + def test_export_license_nfs(self, idrac_default_args, idrac_connection_license_mock, mocker): + export_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic', + 'share_type': 'nfs', + 'ignore_certificate_warning': 'off' + } + } + idr_obj = MagicMock() + idr_obj.json_data = {"license_id": "1234", "LicenseFile": "test_license_content"} + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + return_value=idr_obj) + idrac_default_args.update(export_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + export_license_obj = self.module.ExportLicense(idrac_connection_license_mock, f_module) + result = export_license_obj._ExportLicense__export_license_nfs(EXPORT_URL_MOCK) + assert result.json_data == {'LicenseFile': 'test_license_content', 'license_id': '1234'} + + def test_get_export_license_url(self, idrac_default_args, idrac_connection_license_mock, mocker): + export_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic', + 'share_type': 'local', + 'ignore_certificate_warning': 'off' + } + } + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", + return_value=(REDFISH, None)) + mocker.patch(MODULE_PATH + "get_dynamic_uri", + return_value={"Links": {"Oem": {"Dell": {"DellLicenseManagementService": {ODATA: "/LicenseService"}}}}, + "Actions": {"#DellLicenseManagementService.ExportLicense": {"target": API_ONE}}}) + idrac_default_args.update(export_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + export_license_obj = self.module.ExportLicense(idrac_connection_license_mock, f_module) + result = export_license_obj._ExportLicense__get_export_license_url() + assert result == API_ONE + + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", + return_value=(REDFISH, "error")) + with pytest.raises(Exception) as exc: + export_license_obj._ExportLicense__get_export_license_url() + assert exc.value.args[0] == "error" + + def test_execute(self, idrac_default_args, idrac_connection_license_mock, mocker): + share_type = 'local' + export_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic', + 'share_type': share_type + } + } + idrac_default_args.update(export_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + mocker.patch(MODULE_PATH + "License.check_license_id") + mocker.patch(MODULE_PATH + "ExportLicense._ExportLicense__get_export_license_url", + return_value="/License/url") + mocker.patch(MODULE_PATH + "ExportLicense.get_job_status", + return_value={"JobId": "JID1234"}) + idr_obj = MagicMock() + idr_obj.status_code = 200 + + mocker.patch(MODULE_PATH + "ExportLicense._ExportLicense__export_license_local", + return_value=idr_obj) + export_license_obj = self.module.ExportLicense(idrac_connection_license_mock, f_module) + with pytest.raises(Exception) as exc: + export_license_obj.execute() + assert exc.value.args[0] == SUCCESS_EXPORT_MSG + + export_params.get('share_parameters')["share_type"] = "http" + mocker.patch(MODULE_PATH + "ExportLicense._ExportLicense__export_license_http", + return_value=idr_obj) + with pytest.raises(Exception) as exc: + export_license_obj.execute() + assert exc.value.args[0] == SUCCESS_EXPORT_MSG + + export_params.get('share_parameters')["share_type"] = "cifs" + mocker.patch(MODULE_PATH + "ExportLicense._ExportLicense__export_license_cifs", + return_value=idr_obj) + with pytest.raises(Exception) as exc: + export_license_obj.execute() + assert exc.value.args[0] == SUCCESS_EXPORT_MSG + + export_params.get('share_parameters')["share_type"] = "nfs" + mocker.patch(MODULE_PATH + "ExportLicense._ExportLicense__export_license_nfs", + return_value=idr_obj) + with pytest.raises(Exception) as exc: + export_license_obj.execute() + assert exc.value.args[0] == SUCCESS_EXPORT_MSG + + export_params.get('share_parameters')["share_type"] = "https" + idr_obj.status_code = 400 + mocker.patch(MODULE_PATH + "ExportLicense._ExportLicense__export_license_http", + return_value=idr_obj) + with pytest.raises(Exception) as exc: + export_license_obj.execute() + assert exc.value.args[0] == FAILURE_MSG.format(operation="export", license_id="test_license_id") + + +class TestImportLicense(FakeAnsibleModule): + module = idrac_license + + @pytest.fixture + def idrac_license_mock(self): + idrac_obj = MagicMock() + return idrac_obj + + @pytest.fixture + def idrac_connection_license_mock(self, mocker, idrac_license_mock): + idrac_conn_mock = mocker.patch(MODULE_PATH + 'iDRACRedfishAPI', + return_value=idrac_license_mock) + idrac_conn_mock.return_value.__enter__.return_value = idrac_license_mock + return idrac_conn_mock + + def test_execute(self, idrac_default_args, idrac_connection_license_mock, mocker): + share_type = 'local' + import_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic.xml', + 'share_type': share_type + } + } + idrac_default_args.update(import_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + mocker.patch(MODULE_PATH + "ImportLicense._ImportLicense__get_import_license_url", + return_value="/License/url") + mocker.patch(MODULE_PATH + "get_manager_res_id", + return_value=IDRAC_ID) + mocker.patch(MODULE_PATH + "ImportLicense.get_job_status", + return_value={"JobId": "JID1234"}) + idr_obj = MagicMock() + idr_obj.status_code = 200 + + mocker.patch(MODULE_PATH + "ImportLicense._ImportLicense__import_license_local", + return_value=idr_obj) + import_license_obj = self.module.ImportLicense(idrac_connection_license_mock, f_module) + with pytest.raises(Exception) as exc: + import_license_obj.execute() + assert exc.value.args[0] == SUCCESS_IMPORT_MSG + + import_params.get('share_parameters')["share_type"] = "http" + mocker.patch(MODULE_PATH + "ImportLicense._ImportLicense__import_license_http", + return_value=idr_obj) + with pytest.raises(Exception) as exc: + import_license_obj.execute() + assert exc.value.args[0] == SUCCESS_IMPORT_MSG + + import_params.get('share_parameters')["share_type"] = "cifs" + mocker.patch(MODULE_PATH + "ImportLicense._ImportLicense__import_license_cifs", + return_value=idr_obj) + with pytest.raises(Exception) as exc: + import_license_obj.execute() + assert exc.value.args[0] == SUCCESS_IMPORT_MSG + + import_params.get('share_parameters')["share_type"] = "nfs" + mocker.patch(MODULE_PATH + "ImportLicense._ImportLicense__import_license_nfs", + return_value=idr_obj) + with pytest.raises(Exception) as exc: + import_license_obj.execute() + assert exc.value.args[0] == SUCCESS_IMPORT_MSG + + import_params.get('share_parameters')["share_type"] = "https" + idr_obj.status_code = 400 + mocker.patch(MODULE_PATH + "ImportLicense._ImportLicense__import_license_http", + return_value=idr_obj) + with pytest.raises(Exception) as exc: + import_license_obj.execute() + assert exc.value.args[0] == FAILURE_IMPORT_MSG + + def test_import_license_local(self, idrac_default_args, idrac_connection_license_mock, mocker): + tmp_path = tempfile.gettempdir() + import_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'share_name': 'doesnotexistpath', + 'file_name': LIC_FILE_NAME + } + } + idrac_default_args.update(import_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + import_license_obj = self.module.ImportLicense(idrac_connection_license_mock, f_module) + with pytest.raises(Exception) as exc: + import_license_obj._ImportLicense__import_license_local(EXPORT_URL_MOCK, IDRAC_ID) + assert exc.value.args[0] == INVALID_DIRECTORY_MSG.format(path='doesnotexistpath') + + import_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'share_name': str(tmp_path), + 'file_name': LIC_FILE_NAME + } + } + file_name = os.path.join(tmp_path, LIC_FILE_NAME) + with open(file_name, "w") as fp: + fp.writelines("license_file") + idr_obj = MagicMock() + idr_obj.json_data = {"license_id": "1234", "LicenseFile": "test_license_content"} + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + return_value=idr_obj) + idrac_default_args.update(import_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + import_license_obj = self.module.ImportLicense(idrac_connection_license_mock, f_module) + result = import_license_obj._ImportLicense__import_license_local(EXPORT_URL_MOCK, IDRAC_ID) + assert result.json_data == {'LicenseFile': 'test_license_content', 'license_id': '1234'} + assert os.path.exists(file_name) + + json_str = to_text(json.dumps({"error": {'@Message.ExtendedInfo': [ + { + 'MessageId': "LIC018", + "Message": "Already imported" + } + ]}})) + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + side_effect=HTTPError(HTTPS_PATH, 400, HTTP_ERROR, + {"accept-type": APPLICATION_JSON}, StringIO(json_str))) + with pytest.raises(Exception) as exc: + import_license_obj._ImportLicense__import_license_local(EXPORT_URL_MOCK, IDRAC_ID) + assert exc.value.args[0] == "Already imported" + + if os.path.exists(file_name): + os.remove(file_name) + + def test_import_license_http(self, idrac_default_args, idrac_connection_license_mock, mocker): + import_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic', + 'share_type': 'http', + 'ignore_certificate_warning': 'off' + } + } + idr_obj = MagicMock() + idr_obj.json_data = {"license_id": "1234", "LicenseFile": "test_license_content"} + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + return_value=idr_obj) + idrac_default_args.update(import_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + import_license_obj = self.module.ImportLicense(idrac_connection_license_mock, f_module) + result = import_license_obj._ImportLicense__import_license_http(IMPORT_URL_MOCK, IDRAC_ID) + assert result.json_data == {'LicenseFile': 'test_license_content', 'license_id': '1234'} + + def test_import_license_cifs(self, idrac_default_args, idrac_connection_license_mock, mocker): + import_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic', + 'share_type': 'cifs', + 'ignore_certificate_warning': 'off', + 'workgroup': 'mydomain' + } + } + idr_obj = MagicMock() + idr_obj.json_data = {"license_id": "1234", "LicenseFile": "test_license_content"} + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + return_value=idr_obj) + idrac_default_args.update(import_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + import_license_obj = self.module.ImportLicense(idrac_connection_license_mock, f_module) + result = import_license_obj._ImportLicense__import_license_cifs(IMPORT_URL_MOCK, IDRAC_ID) + assert result.json_data == {'LicenseFile': 'test_license_content', 'license_id': '1234'} + + def test_import_license_nfs(self, idrac_default_args, idrac_connection_license_mock, mocker): + import_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic', + 'share_type': 'nfs', + 'ignore_certificate_warning': 'off', + 'workgroup': 'mydomain' + } + } + idr_obj = MagicMock() + idr_obj.json_data = {"license_id": "1234", "LicenseFile": "test_license_content"} + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, + return_value=idr_obj) + idrac_default_args.update(import_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + import_license_obj = self.module.ImportLicense(idrac_connection_license_mock, f_module) + result = import_license_obj._ImportLicense__import_license_nfs(IMPORT_URL_MOCK, IDRAC_ID) + assert result.json_data == {'LicenseFile': 'test_license_content', 'license_id': '1234'} + + def test_get_import_license_url(self, idrac_default_args, idrac_connection_license_mock, mocker): + export_params = { + 'license_id': 'test_license_id', + 'share_parameters': { + 'file_name': 'test_lic', + 'share_type': 'local', + 'ignore_certificate_warning': 'off' + } + } + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", + return_value=(REDFISH, None)) + mocker.patch(MODULE_PATH + "get_dynamic_uri", + return_value={"Links": {"Oem": {"Dell": {"DellLicenseManagementService": {ODATA: "/LicenseService"}}}}, + "Actions": {"#DellLicenseManagementService.ImportLicense": {"target": API_ONE}}}) + idrac_default_args.update(export_params) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + import_license_obj = self.module.ImportLicense(idrac_connection_license_mock, f_module) + result = import_license_obj._ImportLicense__get_import_license_url() + assert result == API_ONE + + def test_get_job_status(self, idrac_default_args, idrac_connection_license_mock, mocker): + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", return_value=[MANAGER_URI_ONE]) + lic_job_resp_obj = MagicMock() + lic_job_resp_obj.headers = {"Location": "idrac_internal"} + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + import_license_obj = self.module.ImportLicense(idrac_connection_license_mock, f_module) + + mocker.patch(MODULE_PATH + "idrac_redfish_job_tracking", return_value=(False, "None", {"JobId": "JID1234"}, 0)) + result = import_license_obj.get_job_status(lic_job_resp_obj) + assert result == {"JobId": "JID1234"} + + mocker.patch(MODULE_PATH + "idrac_redfish_job_tracking", return_value=(True, "None", {"Message": "Got LIC018", + "MessageId": "LIC018"}, 0)) + with pytest.raises(Exception) as exc: + import_license_obj.get_job_status(lic_job_resp_obj) + assert exc.value.args[0] == "Got LIC018" + + mocker.patch(MODULE_PATH + "idrac_redfish_job_tracking", return_value=(True, "None", {"Message": "Got LIC019", + "MessageId": "LIC019"}, 0)) + with pytest.raises(Exception) as exc: + import_license_obj.get_job_status(lic_job_resp_obj) + assert exc.value.args[0] == "Got LIC019" + + +class TestLicenseType(FakeAnsibleModule): + module = idrac_license + + @pytest.fixture + def idrac_license_mock(self): + idrac_obj = MagicMock() + return idrac_obj + + @pytest.fixture + def idrac_connection_license_mock(self, mocker, idrac_license_mock): + idrac_conn_mock = mocker.patch(MODULE_PATH + 'iDRACRedfishAPI', + return_value=idrac_license_mock) + idrac_conn_mock.return_value.__enter__.return_value = idrac_license_mock + return idrac_conn_mock + + def test_license_operation(self, idrac_default_args, idrac_connection_license_mock, mocker): + idrac_default_args.update({"import": False, "export": False, "delete": True}) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + lic_class = self.module.LicenseType.license_operation(idrac_connection_license_mock, f_module) + assert isinstance(lic_class, self.module.DeleteLicense) + + idrac_default_args.update({"import": False, "export": True, "delete": False}) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + lic_class = self.module.LicenseType.license_operation(idrac_connection_license_mock, f_module) + assert isinstance(lic_class, self.module.ExportLicense) + + idrac_default_args.update({"import": True, "export": False, "delete": False}) + f_module = self.get_module_mock(params=idrac_default_args, check_mode=False) + lic_class = self.module.LicenseType.license_operation(idrac_connection_license_mock, f_module) + assert isinstance(lic_class, self.module.ImportLicense) + + @pytest.mark.parametrize("exc_type", + [URLError, HTTPError, SSLValidationError, ConnectionError, TypeError, ValueError]) + def test_idrac_license_main_exception_handling_case(self, exc_type, mocker, idrac_default_args, idrac_connection_license_mock): + idrac_default_args.update({"delete": True, "license_id": "1234"}) + json_str = to_text(json.dumps({"data": "out"})) + if exc_type in [HTTPError, SSLValidationError]: + mocker.patch(MODULE_PATH + "get_idrac_firmware_version", + side_effect=exc_type(HTTPS_PATH, 400, + HTTP_ERROR, + {"accept-type": APPLICATION_JSON}, + StringIO(json_str))) + else: + mocker.patch(MODULE_PATH + "get_idrac_firmware_version", + side_effect=exc_type('test')) + result = self._run_module(idrac_default_args) + if exc_type == URLError: + assert result['unreachable'] is True + else: + assert result['failed'] is True + assert 'msg' in result + + def test_main(self, mocker): + module_mock = mocker.MagicMock() + idrac_mock = mocker.MagicMock() + license_mock = mocker.MagicMock() + + # Mock the necessary functions and objects + mocker.patch(MODULE_PATH + 'get_argument_spec', return_value={}) + mocker.patch(MODULE_PATH + 'idrac_auth_params', {}) + mocker.patch(MODULE_PATH + 'AnsibleModule', return_value=module_mock) + mocker.patch(MODULE_PATH + 'iDRACRedfishAPI', return_value=idrac_mock) + mocker.patch(MODULE_PATH + 'get_idrac_firmware_version', return_value='3.1') + mocker.patch(MODULE_PATH + 'LicenseType.license_operation', return_value=license_mock) + main() + mocker.patch(MODULE_PATH + 'get_idrac_firmware_version', return_value='2.9') + main()