Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
# dellemc.openmanage collections Plugins Directory
Here are the list of modules and module_utils supported by Dell.
```
├── doc_fragments
├── idrac_auth_options.py
├── network_share_options.py
├── ome_auth_options.py
├── omem_auth_options.py
├── oment_auth_options.py
└── redfish_auth_options.py
├── module_utils
├── dellemc_idrac.py
├── idrac_redfish.py
├── ome.py
├── redfish.py
└── utils.py
└── modules
├── dellemc_configure_idrac_eventing.py
├── dellemc_configure_idrac_services.py
├── dellemc_get_firmware_inventory.py
├── dellemc_get_system_inventory.py
├── dellemc_idrac_lc_attributes.py
├── dellemc_idrac_storage_volume.py
├── dellemc_system_lockdown_mode.py
├── idrac_attributes.py
├── idrac_bios.py
├── idrac_boot.py
├── idrac_certificates.py
├── idrac_firmware.py
├── idrac_firmware_info.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_os_deployment.py
├── idrac_redfish_storage_controller.py
├── idrac_reset.py
├── idrac_server_config_profile.py
├── idrac_syslog.py
├── idrac_system_info.py
├── idrac_timezone_ntp.py
├── idrac_user.py
├── idrac_virtual_media.py
├── ome_active_directory.py
├── ome_application_alerts_smtp.py
├── ome_application_alerts_syslog.py
├── ome_application_certificate.py
├── ome_application_console_preferences.py
├── ome_application_network_address.py
├── ome_application_network_proxy.py
├── ome_application_network_settings.py
├── ome_application_network_time.py
├── ome_application_network_webserver.py
├── ome_application_security_settings.py
├── ome_chassis_slots.py
├── ome_configuration_compliance_baseline.py
├── ome_configuration_compliance_info.py
├── ome_device_group.py
├── ome_device_info.py
├── ome_device_local_access_configuration.py
├── ome_device_location.py
├── ome_device_mgmt_network.py
├── ome_device_network_services.py
├── ome_device_power_settings.py
├── ome_device_quick_deploy.py
├── ome_devices.py
├── ome_diagnostics.py
├── ome_discovery.py
├── ome_domain_user_groups.py
├── ome_firmware.py
├── ome_firmware_baseline.py
├── ome_firmware_baseline_compliance_info.py
├── ome_firmware_baseline_info.py
├── ome_firmware_catalog.py
├── ome_groups.py
├── ome_identity_pool.py
├── ome_job_info.py
├── ome_network_port_breakout.py
├── ome_network_vlan.py
├── ome_network_vlan_info.py
├── ome_powerstate.py
├── ome_profile.py
├── ome_server_interface_profile_info.py
├── ome_server_interface_profiles.py
├── ome_smart_fabric.py
├── ome_smart_fabric_uplink.py
├── ome_template.py
├── ome_template_identity_pool.py
├── ome_template_info.py
├── ome_template_network_vlan.py
├── ome_user.py
├── ome_user_info.py
├── redfish_event_subscription.py
├── redfish_firmware.py
├── redfish_powerstate.py
└── redfish_storage_volume.py
```

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
#
# Dell EMC OpenManage Ansible Modules
# Version 5.0.1
# Copyright (C) 2020-2022 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
class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
idrac_ip:
required: True
type: str
description: iDRAC IP Address.
idrac_user:
required: True
type: str
description: iDRAC username.
idrac_password:
required: True
type: str
description: iDRAC user password.
aliases: ['idrac_pwd']
idrac_port:
type: int
description: iDRAC port.
default: 443
validate_certs:
description:
- If C(False), the SSL certificates will not be validated.
- Configure C(False) only on personally controlled sites where self-signed certificates are used.
- Prior to collection version C(5.0.0), the I(validate_certs) is C(False) by default.
type: bool
default: True
version_added: 5.0.0
ca_path:
description:
- The Privacy Enhanced Mail (PEM) file that contains a CA certificate to be used for the validation.
type: path
version_added: 5.0.0
timeout:
description: The socket level timeout in seconds.
type: int
default: 30
version_added: 5.0.0
'''

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
#
# Dell EMC OpenManage Ansible Modules
# Version 3.0.0
# Copyright (C) 2020-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
class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
share_name:
required: True
type: str
description: Network share or a local path.
share_user:
type: str
description: Network share user name. Use the format 'user@domain' or 'domain\\user' if user is part of a domain.
This option is mandatory for CIFS share.
share_password:
type: str
description: Network share user password. This option is mandatory for CIFS share.
aliases: ['share_pwd']
share_mnt:
type: str
description: Local mount path of the network share with read-write permission for ansible user.
This option is mandatory for network shares.
'''

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#
# Dell EMC OpenManage Ansible Modules
# Version 5.0.1
# Copyright (C) 2020-2022 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
class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
hostname:
description: OpenManage Enterprise or OpenManage Enterprise Modular IP address or hostname.
type: str
required: True
username:
description: OpenManage Enterprise or OpenManage Enterprise Modular username.
type: str
required: True
password:
description: OpenManage Enterprise or OpenManage Enterprise Modular password.
type: str
required: True
port:
description: OpenManage Enterprise or OpenManage Enterprise Modular HTTPS port.
type: int
default: 443
validate_certs:
description:
- If C(False), the SSL certificates will not be validated.
- Configure C(False) only on personally controlled sites where self-signed certificates are used.
- Prior to collection version C(5.0.0), the I(validate_certs) is C(False) by default.
type: bool
default: True
version_added: 5.0.0
ca_path:
description:
- The Privacy Enhanced Mail (PEM) file that contains a CA certificate to be used for the validation.
type: path
version_added: 5.0.0
timeout:
description: The socket level timeout in seconds.
type: int
default: 30
version_added: 5.0.0
'''

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#
# Dell EMC OpenManage Ansible Modules
# Version 5.0.1
# Copyright (C) 2020-2022 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
class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
hostname:
description: OpenManage Enterprise Modular IP address or hostname.
type: str
required: True
username:
description: OpenManage Enterprise Modular username.
type: str
required: True
password:
description: OpenManage Enterprise Modular password.
type: str
required: True
port:
description: OpenManage Enterprise Modular HTTPS port.
type: int
default: 443
validate_certs:
description:
- If C(False), the SSL certificates will not be validated.
- Configure C(False) only on personally controlled sites where self-signed certificates are used.
- Prior to collection version C(5.0.0), the I(validate_certs) is C(False) by default.
type: bool
default: True
version_added: 5.0.0
ca_path:
description:
- The Privacy Enhanced Mail (PEM) file that contains a CA certificate to be used for the validation.
type: path
version_added: 5.0.0
timeout:
description: The socket level timeout in seconds.
type: int
default: 30
version_added: 5.0.0
'''

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#
# Dell EMC OpenManage Ansible Modules
# Version 5.0.1
# Copyright (C) 2020-2022 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
class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
hostname:
description: OpenManage Enterprise IP address or hostname.
type: str
required: True
username:
description: OpenManage Enterprise username.
type: str
required: True
password:
description: OpenManage Enterprise password.
type: str
required: True
port:
description: OpenManage Enterprise HTTPS port.
type: int
default: 443
validate_certs:
description:
- If C(False), the SSL certificates will not be validated.
- Configure C(False) only on personally controlled sites where self-signed certificates are used.
- Prior to collection version C(5.0.0), the I(validate_certs) is C(False) by default.
type: bool
default: True
version_added: 5.0.0
ca_path:
description:
- The Privacy Enhanced Mail (PEM) file that contains a CA certificate to be used for the validation.
type: path
version_added: 5.0.0
timeout:
description: The socket level timeout in seconds.
type: int
default: 30
version_added: 5.0.0
'''

View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#
# Dell EMC OpenManage Ansible Modules
# Version 5.0.1
# Copyright (C) 2020-2022 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
class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
baseuri:
description: "IP address of the target out-of-band controller. For example- <ipaddress>:<port>."
type: str
required: True
username:
description: Username of the target out-of-band controller.
type: str
required: True
password:
description: Password of the target out-of-band controller.
type: str
required: True
validate_certs:
description:
- If C(False), the SSL certificates will not be validated.
- Configure C(False) only on personally controlled sites where self-signed certificates are used.
- Prior to collection version C(5.0.0), the I(validate_certs) is C(False) by default.
type: bool
default: True
version_added: 5.0.0
ca_path:
description:
- The Privacy Enhanced Mail (PEM) file that contains a CA certificate to be used for the validation.
type: path
version_added: 5.0.0
timeout:
description: The socket level timeout in seconds.
type: int
default: 30
version_added: 5.0.0
'''

View File

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Dell EMC OpenManage Ansible Modules
# Version 5.0.1
# Copyright (C) 2019-2022 Dell Inc. or its subsidiaries. All Rights Reserved.
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
try:
from omsdk.sdkinfra import sdkinfra
from omsdk.sdkcreds import UserCredentials
from omsdk.sdkfile import FileOnShare, file_share_manager
from omsdk.sdkprotopref import ProtoPreference, ProtocolEnum
from omsdk.http.sdkwsmanbase import WsManOptions
HAS_OMSDK = True
except ImportError:
HAS_OMSDK = False
idrac_auth_params = {
"idrac_ip": {"required": True, "type": 'str'},
"idrac_user": {"required": True, "type": 'str'},
"idrac_password": {"required": True, "type": 'str', "aliases": ['idrac_pwd'], "no_log": True},
"idrac_port": {"required": False, "default": 443, "type": 'int'},
"validate_certs": {"type": "bool", "default": True},
"ca_path": {"type": "path"},
"timeout": {"type": "int", "default": 30},
}
class iDRACConnection:
def __init__(self, module_params):
if not HAS_OMSDK:
raise ImportError("Dell EMC OMSDK library is required for this module")
self.idrac_ip = module_params['idrac_ip']
self.idrac_user = module_params['idrac_user']
self.idrac_pwd = module_params['idrac_password']
self.idrac_port = module_params['idrac_port']
if not all((self.idrac_ip, self.idrac_user, self.idrac_pwd)):
raise ValueError("hostname, username and password required")
self.handle = None
self.creds = UserCredentials(self.idrac_user, self.idrac_pwd)
self.validate_certs = module_params.get("validate_certs", False)
self.ca_path = module_params.get("ca_path")
verify_ssl = False
if self.validate_certs is True:
if self.ca_path is None:
self.ca_path = self._get_omam_ca_env()
verify_ssl = self.ca_path
timeout = module_params.get("timeout", 30)
if not timeout or type(timeout) != int:
timeout = 30
self.pOp = WsManOptions(port=self.idrac_port, read_timeout=timeout, verify_ssl=verify_ssl)
self.sdk = sdkinfra()
if self.sdk is None:
msg = "Could not initialize iDRAC drivers."
raise RuntimeError(msg)
def __enter__(self):
self.sdk.importPath()
protopref = ProtoPreference(ProtocolEnum.WSMAN)
protopref.include_only(ProtocolEnum.WSMAN)
self.handle = self.sdk.get_driver(self.sdk.driver_enum.iDRAC, self.idrac_ip, self.creds,
protopref=protopref, pOptions=self.pOp)
if self.handle is None:
msg = "Unable to communicate with iDRAC {0}. This may be due to one of the following: " \
"Incorrect username or password, unreachable iDRAC IP or " \
"a failure in TLS/SSL handshake.".format(self.idrac_ip)
raise RuntimeError(msg)
return self.handle
def __exit__(self, exc_type, exc_val, exc_tb):
self.handle.disconnect()
return False
def _get_omam_ca_env(self):
"""Check if the value is set in REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE or OMAM_CA_BUNDLE or True as ssl has to
be validated from omsdk with single param and is default to false in omsdk"""
return (os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("CURL_CA_BUNDLE")
or os.environ.get("OMAM_CA_BUNDLE") or True)

View File

@@ -0,0 +1,377 @@
# -*- coding: utf-8 -*-
# Dell EMC OpenManage Ansible Modules
# Version 5.5.0
# Copyright (C) 2019-2022 Dell Inc. or its subsidiaries. All Rights Reserved.
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
import re
import time
import os
from ansible.module_utils.urls import open_url, ConnectionError, SSLValidationError
from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
from ansible.module_utils.six.moves.urllib.parse import urlencode
idrac_auth_params = {
"idrac_ip": {"required": True, "type": 'str'},
"idrac_user": {"required": True, "type": 'str'},
"idrac_password": {"required": True, "type": 'str', "aliases": ['idrac_pwd'], "no_log": True},
"idrac_port": {"required": False, "default": 443, "type": 'int'},
"validate_certs": {"type": "bool", "default": True},
"ca_path": {"type": "path"},
"timeout": {"type": "int", "default": 30},
}
SESSION_RESOURCE_COLLECTION = {
"SESSION": "/redfish/v1/Sessions",
"SESSION_ID": "/redfish/v1/Sessions/{Id}",
}
MANAGER_URI = "/redfish/v1/Managers/iDRAC.Embedded.1"
EXPORT_URI = "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ExportSystemConfiguration"
IMPORT_URI = "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration"
IMPORT_PREVIEW = "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfigurationPreview"
class OpenURLResponse(object):
"""Handles HTTPResponse"""
def __init__(self, resp):
self.body = None
self.resp = resp
if self.resp:
self.body = self.resp.read()
@property
def json_data(self):
try:
return json.loads(self.body)
except ValueError:
raise ValueError("Unable to parse json")
@property
def status_code(self):
return self.resp.getcode()
@property
def success(self):
status = self.status_code
return status >= 200 & status <= 299
@property
def headers(self):
return self.resp.headers
@property
def reason(self):
return self.resp.reason
class iDRACRedfishAPI(object):
"""REST api for iDRAC modules."""
def __init__(self, module_params, req_session=False):
self.ipaddress = module_params['idrac_ip']
self.username = module_params['idrac_user']
self.password = module_params['idrac_password']
self.port = module_params['idrac_port']
self.validate_certs = module_params.get("validate_certs", False)
self.ca_path = module_params.get("ca_path")
self.timeout = module_params.get("timeout", 30)
self.use_proxy = module_params.get("use_proxy", True)
self.req_session = req_session
self.session_id = None
self.protocol = 'https'
self._headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
def _get_url(self, uri):
return "{0}://{1}:{2}{3}".format(self.protocol, self.ipaddress, self.port, uri)
def _build_url(self, path, query_param=None):
"""builds complete url"""
url = path
base_uri = self._get_url(url)
if path:
url = base_uri
if query_param:
url += "?{0}".format(urlencode(query_param))
return url
def _url_common_args_spec(self, method, api_timeout, headers=None):
"""Creates an argument common spec"""
req_header = self._headers
if headers:
req_header.update(headers)
if api_timeout is None:
api_timeout = self.timeout
if self.ca_path is None:
self.ca_path = self._get_omam_ca_env()
url_kwargs = {
"method": method,
"validate_certs": self.validate_certs,
"ca_path": self.ca_path,
"use_proxy": self.use_proxy,
"headers": req_header,
"timeout": api_timeout,
"follow_redirects": 'all',
}
return url_kwargs
def _args_without_session(self, path, method, api_timeout, headers=None):
"""Creates an argument spec in case of basic authentication"""
req_header = self._headers
if headers:
req_header.update(headers)
url_kwargs = self._url_common_args_spec(method, api_timeout, headers=headers)
if not (path == SESSION_RESOURCE_COLLECTION["SESSION"] and method == 'POST'):
url_kwargs["url_username"] = self.username
url_kwargs["url_password"] = self.password
url_kwargs["force_basic_auth"] = True
return url_kwargs
def _args_with_session(self, method, api_timeout, headers=None):
"""Creates an argument spec, in case of authentication with session"""
url_kwargs = self._url_common_args_spec(method, api_timeout, headers=headers)
url_kwargs["force_basic_auth"] = False
return url_kwargs
def invoke_request(self, uri, method, data=None, query_param=None, headers=None, api_timeout=None, dump=True):
try:
if 'X-Auth-Token' in self._headers:
url_kwargs = self._args_with_session(method, api_timeout, headers=headers)
else:
url_kwargs = self._args_without_session(uri, method, api_timeout, headers=headers)
if data and dump:
data = json.dumps(data)
url = self._build_url(uri, query_param=query_param)
resp = open_url(url, data=data, **url_kwargs)
resp_data = OpenURLResponse(resp)
except (HTTPError, URLError, SSLValidationError, ConnectionError) as err:
raise err
return resp_data
def __enter__(self):
"""Creates sessions by passing it to header"""
if self.req_session:
payload = {'UserName': self.username,
'Password': self.password}
path = SESSION_RESOURCE_COLLECTION["SESSION"]
resp = self.invoke_request(path, 'POST', data=payload)
if resp and resp.success:
self.session_id = resp.json_data.get("Id")
self._headers["X-Auth-Token"] = resp.headers.get('X-Auth-Token')
else:
msg = "Could not create the session"
raise ConnectionError(msg)
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Deletes a session id, which is in use for request"""
if self.session_id:
path = SESSION_RESOURCE_COLLECTION["SESSION_ID"].format(Id=self.session_id)
self.invoke_request(path, 'DELETE')
return False
@property
def get_server_generation(self):
"""
This method fetches the connected server generation.
:return: 14, 4.11.11.11
"""
model, firmware_version = None, None
response = self.invoke_request(MANAGER_URI, 'GET')
if response.status_code == 200:
generation = int(re.search(r"\d+(?=G)", response.json_data["Model"]).group())
firmware_version = response.json_data["FirmwareVersion"]
return generation, firmware_version
def wait_for_job_complete(self, task_uri, job_wait=False):
"""
This function wait till the job completion.
:param task_uri: uri to track job.
:param job_wait: True or False decide whether to wait till the job completion.
:return: object
"""
response = None
while job_wait:
try:
response = self.invoke_request(task_uri, "GET")
if response.json_data.get("TaskState") == "Running":
time.sleep(10)
else:
break
except ValueError:
response = response.body
break
return response
def wait_for_job_completion(self, job_uri, job_wait=False, reboot=False, apply_update=False):
"""
This function wait till the job completion.
:param job_uri: uri to track job.
:param job_wait: True or False decide whether to wait till the job completion.
:return: object
"""
time.sleep(5)
response = self.invoke_request(job_uri, "GET")
while job_wait:
response = self.invoke_request(job_uri, "GET")
if response.json_data.get("PercentComplete") == 100 and \
response.json_data.get("JobState") == "Completed":
break
if response.json_data.get("JobState") == "Starting" and not reboot and apply_update:
break
time.sleep(30)
return response
def export_scp(self, export_format=None, export_use=None, target=None,
job_wait=False, share=None):
"""
This method exports system configuration details from the system.
:param export_format: XML or JSON.
:param export_use: Default or Clone or Replace.
:param target: IDRAC or NIC or ALL or BIOS or RAID.
:param job_wait: True or False decide whether to wait till the job completion.
:return: exported data in requested format.
"""
payload = {"ExportFormat": export_format, "ExportUse": export_use,
"ShareParameters": {"Target": target}}
if share is None:
share = {}
if share.get("share_ip") is not None:
payload["ShareParameters"]["IPAddress"] = share["share_ip"]
if share.get("share_name") is not None and share.get("share_name"):
payload["ShareParameters"]["ShareName"] = share["share_name"]
if share.get("share_type") is not None:
payload["ShareParameters"]["ShareType"] = share["share_type"]
if share.get("file_name") is not None:
payload["ShareParameters"]["FileName"] = share["file_name"]
if share.get("username") is not None:
payload["ShareParameters"]["Username"] = share["username"]
if share.get("password") is not None:
payload["ShareParameters"]["Password"] = share["password"]
response = self.invoke_request(EXPORT_URI, "POST", data=payload)
if response.status_code == 202 and job_wait:
task_uri = response.headers["Location"]
response = self.wait_for_job_complete(task_uri, job_wait=job_wait)
return response
def import_scp_share(self, shutdown_type=None, host_powerstate=None, job_wait=True,
target=None, import_buffer=None, share=None):
"""
This method imports system configuration using share.
:param shutdown_type: graceful
:param host_powerstate: on
:param file_name: import.xml
:param job_wait: True
:param target: iDRAC
:param share: dictionary which has all the share details.
:return: json response
"""
payload = {"ShutdownType": shutdown_type, "EndHostPowerState": host_powerstate,
"ShareParameters": {"Target": target}}
if import_buffer is not None:
payload["ImportBuffer"] = import_buffer
if share is None:
share = {}
if share.get("share_ip") is not None:
payload["ShareParameters"]["IPAddress"] = share["share_ip"]
if share.get("share_name") is not None and share.get("share_name"):
payload["ShareParameters"]["ShareName"] = share["share_name"]
if share.get("share_type") is not None:
payload["ShareParameters"]["ShareType"] = share["share_type"]
if share.get("file_name") is not None:
payload["ShareParameters"]["FileName"] = share["file_name"]
if share.get("username") is not None:
payload["ShareParameters"]["Username"] = share["username"]
if share.get("password") is not None:
payload["ShareParameters"]["Password"] = share["password"]
response = self.invoke_request(IMPORT_URI, "POST", data=payload)
if response.status_code == 202 and job_wait:
task_uri = response.headers["Location"]
response = self.wait_for_job_complete(task_uri, job_wait=job_wait)
return response
def import_preview(self, import_buffer=None, target=None, share=None, job_wait=False):
payload = {"ShareParameters": {"Target": target}}
if import_buffer is not None:
payload["ImportBuffer"] = import_buffer
if share is None:
share = {}
if share.get("share_ip") is not None:
payload["ShareParameters"]["IPAddress"] = share["share_ip"]
if share.get("share_name") is not None and share.get("share_name"):
payload["ShareParameters"]["ShareName"] = share["share_name"]
if share.get("share_type") is not None:
payload["ShareParameters"]["ShareType"] = share["share_type"]
if share.get("file_name") is not None:
payload["ShareParameters"]["FileName"] = share["file_name"]
if share.get("username") is not None:
payload["ShareParameters"]["Username"] = share["username"]
if share.get("password") is not None:
payload["ShareParameters"]["Password"] = share["password"]
response = self.invoke_request(IMPORT_PREVIEW, "POST", data=payload)
if response.status_code == 202 and job_wait:
task_uri = response.headers["Location"]
response = self.wait_for_job_complete(task_uri, job_wait=job_wait)
return response
def import_scp(self, import_buffer=None, target=None, job_wait=False):
"""
This method imports system configuration details to the system.
:param import_buffer: import buffer payload content xml or json format
:param target: IDRAC or NIC or ALL or BIOS or RAID.
:param job_wait: True or False decide whether to wait till the job completion.
:return: json response
"""
payload = {"ImportBuffer": import_buffer, "ShareParameters": {"Target": target}}
response = self.invoke_request(IMPORT_URI, "POST", data=payload)
if response.status_code == 202 and job_wait:
task_uri = response.headers["Location"]
response = self.wait_for_job_complete(task_uri, job_wait=job_wait)
return response
def get_idrac_local_account_attr(self, idrac_attribues, fqdd=None):
"""
This method filtered from all the user attributes from the given idrac attributes.
:param idrac_attribues: all the idrac attribues in json data format.
:return: user attributes in dictionary format
"""
user_attr = None
if "SystemConfiguration" in idrac_attribues:
sys_config = idrac_attribues.get("SystemConfiguration")
for comp in sys_config.get("Components"):
if comp.get("FQDD") == fqdd:
attributes = comp.get("Attributes")
break
user_attr = dict([(attr["Name"], attr["Value"]) for attr in attributes if attr["Name"].startswith("Users.")])
return user_attr
def _get_omam_ca_env(self):
"""Check if the value is set in REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE or OMAM_CA_BUNDLE or returns None"""
return os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("CURL_CA_BUNDLE") or os.environ.get("OMAM_CA_BUNDLE")

View File

@@ -0,0 +1,399 @@
# -*- coding: utf-8 -*-
# Dell EMC OpenManage Ansible Modules
# Version 5.0.1
# Copyright (C) 2019-2022 Dell Inc. or its subsidiaries. All Rights Reserved.
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
import os
import time
from ansible.module_utils.urls import open_url, ConnectionError, SSLValidationError
from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
from ansible.module_utils.six.moves.urllib.parse import urlencode
ome_auth_params = {
"hostname": {"required": True, "type": "str"},
"username": {"required": True, "type": "str"},
"password": {"required": True, "type": "str", "no_log": True},
"port": {"type": "int", "default": 443},
"validate_certs": {"type": "bool", "default": True},
"ca_path": {"type": "path"},
"timeout": {"type": "int", "default": 30},
}
SESSION_RESOURCE_COLLECTION = {
"SESSION": "SessionService/Sessions",
"SESSION_ID": "SessionService/Sessions('{Id}')",
}
JOB_URI = "JobService/Jobs({job_id})"
JOB_SERVICE_URI = "JobService/Jobs"
class OpenURLResponse(object):
"""Handles HTTPResponse"""
def __init__(self, resp):
self.body = None
self.resp = resp
if self.resp:
self.body = self.resp.read()
@property
def json_data(self):
try:
return json.loads(self.body)
except ValueError:
raise ValueError("Unable to parse json")
@property
def status_code(self):
return self.resp.getcode()
@property
def success(self):
return self.status_code in (200, 201, 202, 204)
@property
def token_header(self):
return self.resp.headers.get('X-Auth-Token')
class RestOME(object):
"""Handles OME API requests"""
def __init__(self, module_params=None, req_session=False):
self.module_params = module_params
self.hostname = self.module_params["hostname"]
self.username = self.module_params["username"]
self.password = self.module_params["password"]
self.port = self.module_params["port"]
self.validate_certs = self.module_params.get("validate_certs", True)
self.ca_path = self.module_params.get("ca_path")
self.timeout = self.module_params.get("timeout", 30)
self.req_session = req_session
self.session_id = None
self.protocol = 'https'
self._headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
def _get_base_url(self):
"""builds base url"""
return '{0}://{1}:{2}/api'.format(self.protocol, self.hostname, self.port)
def _build_url(self, path, query_param=None):
"""builds complete url"""
url = path
base_uri = self._get_base_url()
if path:
url = '{0}/{1}'.format(base_uri, path)
if query_param:
"""Ome filtering does not work as expected when '+' is passed,
urlencode will encode spaces as '+' so replace it to '%20'"""
url += "?{0}".format(urlencode(query_param).replace('+', '%20'))
return url
def _url_common_args_spec(self, method, api_timeout, headers=None):
"""Creates an argument common spec"""
req_header = self._headers
if headers:
req_header.update(headers)
if api_timeout is None:
api_timeout = self.timeout
if self.ca_path is None:
self.ca_path = self._get_omam_ca_env()
url_kwargs = {
"method": method,
"validate_certs": self.validate_certs,
"ca_path": self.ca_path,
"use_proxy": True,
"headers": req_header,
"timeout": api_timeout,
"follow_redirects": 'all',
}
return url_kwargs
def _args_without_session(self, method, api_timeout, headers=None):
"""Creates an argument spec in case of basic authentication"""
req_header = self._headers
if headers:
req_header.update(headers)
url_kwargs = self._url_common_args_spec(method, api_timeout, headers=headers)
url_kwargs["url_username"] = self.username
url_kwargs["url_password"] = self.password
url_kwargs["force_basic_auth"] = True
return url_kwargs
def _args_with_session(self, method, api_timeout, headers=None):
"""Creates an argument spec, in case of authentication with session"""
url_kwargs = self._url_common_args_spec(method, api_timeout, headers=headers)
url_kwargs["force_basic_auth"] = False
return url_kwargs
def invoke_request(self, method, path, data=None, query_param=None, headers=None,
api_timeout=None, dump=True):
"""
Sends a request through open_url
Returns :class:`OpenURLResponse` object.
:arg method: HTTP verb to use for the request
:arg path: path to request without query parameter
:arg data: (optional) Payload to send with the request
:arg query_param: (optional) Dictionary of query parameter to send with request
:arg headers: (optional) Dictionary of HTTP Headers to send with the
request
:arg api_timeout: (optional) How long to wait for the server to send
data before giving up
:arg dump: (Optional) boolean value for dumping payload data.
:returns: OpenURLResponse
"""
try:
if 'X-Auth-Token' in self._headers:
url_kwargs = self._args_with_session(method, api_timeout, headers=headers)
else:
url_kwargs = self._args_without_session(method, api_timeout, headers=headers)
if data and dump:
data = json.dumps(data)
url = self._build_url(path, query_param=query_param)
resp = open_url(url, data=data, **url_kwargs)
resp_data = OpenURLResponse(resp)
except (HTTPError, URLError, SSLValidationError, ConnectionError) as err:
raise err
return resp_data
def __enter__(self):
"""Creates sessions by passing it to header"""
if self.req_session:
payload = {'UserName': self.username,
'Password': self.password,
'SessionType': 'API', }
path = SESSION_RESOURCE_COLLECTION["SESSION"]
resp = self.invoke_request('POST', path, data=payload)
if resp and resp.success:
self.session_id = resp.json_data.get("Id")
self._headers["X-Auth-Token"] = resp.token_header
else:
msg = "Could not create the session"
raise ConnectionError(msg)
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Deletes a session id, which is in use for request"""
if self.session_id:
path = SESSION_RESOURCE_COLLECTION["SESSION_ID"].format(Id=self.session_id)
self.invoke_request('DELETE', path)
return False
def get_all_report_details(self, uri):
"""
This implementation mainly dependent on '@odata.count' value.
Currently first request without query string, always returns total number of available
reports in '@odata.count'.
"""
try:
resp = self.invoke_request('GET', uri)
data = resp.json_data
report_list = data["value"]
total_count = data['@odata.count']
remaining_count = total_count - len(report_list)
first_page_count = len(report_list)
while remaining_count > 0:
resp = self.invoke_request('GET', uri,
query_param={"$top": first_page_count, "$skip": len(report_list)})
data = resp.json_data
value = data["value"]
report_list.extend(value)
remaining_count = remaining_count - len(value)
return {"resp_obj": resp, "report_list": report_list}
except (URLError, HTTPError, SSLValidationError, ConnectionError, TypeError, ValueError) as err:
raise err
def get_job_type_id(self, jobtype_name):
"""This provides an ID of the job type."""
job_type_id = None
resp = self.invoke_request('GET', "JobService/JobTypes")
data = resp.json_data["value"]
for each in data:
if each["Name"] == jobtype_name:
job_type_id = each["Id"]
break
return job_type_id
def get_device_id_from_service_tag(self, service_tag):
"""
:param service_tag: service tag of the device
:return: dict
Id: int: device id
value: dict: device id details
not_found_msg: str: message if service tag not found
"""
device_id = None
query = "DeviceServiceTag eq '{0}'".format(service_tag)
response = self.invoke_request("GET", "DeviceService/Devices", query_param={"$filter": query})
value = response.json_data.get("value", [])
device_info = {}
if value:
device_info = value[0]
device_id = device_info["Id"]
return {"Id": device_id, "value": device_info}
def get_all_items_with_pagination(self, uri):
"""
This implementation mainly to get all available items from ome for pagination
supported GET uri
:param uri: uri which supports pagination
:return: dict.
"""
try:
resp = self.invoke_request('GET', uri)
data = resp.json_data
total_items = data.get("value", [])
total_count = data.get('@odata.count', 0)
next_link = data.get('@odata.nextLink', '')
while next_link:
resp = self.invoke_request('GET', next_link.split('/api')[-1])
data = resp.json_data
value = data["value"]
next_link = data.get('@odata.nextLink', '')
total_items.extend(value)
return {"total_count": total_count, "value": total_items}
except (URLError, HTTPError, SSLValidationError, ConnectionError, TypeError, ValueError) as err:
raise err
def get_device_type(self):
"""
Returns device type map where as key is type and value is type name
eg: {1000: "SERVER", 2000: "CHASSIS", 4000: "NETWORK_IOM", "8000": "STORAGE_IOM", 3000: "STORAGE"}
:return: dict, first item dict gives device type map
"""
device_map = {}
response = self.invoke_request("GET", "DeviceService/DeviceType")
if response.json_data.get("value"):
device_map = dict([(item["DeviceType"], item["Name"]) for item in response.json_data["value"]])
return device_map
def get_job_info(self, job_id):
try:
job_status_map = {
2020: "Scheduled", 2030: "Queued", 2040: "Starting", 2050: "Running", 2060: "Completed",
2070: "Failed", 2090: "Warning", 2080: "New", 2100: "Aborted", 2101: "Paused", 2102: "Stopped",
2103: "Canceled"
}
failed_job_status = [2070, 2090, 2100, 2101, 2102, 2103]
job_url = JOB_URI.format(job_id=job_id)
job_resp = self.invoke_request('GET', job_url)
job_dict = job_resp.json_data
job_status = job_dict['LastRunStatus']['Id']
if job_status in [2060, 2020]:
job_failed = False
message = "Job {0} successfully.".format(job_status_map[job_status])
exit_poll = True
return exit_poll, job_failed, message
elif job_status in failed_job_status:
exit_poll = True
job_failed = True
message = "Job is in {0} state, and is not completed.".format(job_status_map[job_status])
return exit_poll, job_failed, message
return False, False, None
except HTTPError:
job_failed = True
message = "Unable to track the job status of {0}.".format(job_id)
exit_poll = True
return exit_poll, job_failed, message
def job_tracking(self, job_id, job_wait_sec=600, sleep_time=60):
"""
job_id: job id
job_wait_sec: Maximum time to wait to fetch the final job details in seconds
sleep_time: Maximum time to sleep in seconds in each job details fetch
"""
max_sleep_time = job_wait_sec
sleep_interval = sleep_time
while max_sleep_time:
if max_sleep_time > sleep_interval:
max_sleep_time = max_sleep_time - sleep_interval
else:
sleep_interval = max_sleep_time
max_sleep_time = 0
time.sleep(sleep_interval)
exit_poll, job_failed, job_message = self.get_job_info(job_id)
if exit_poll is True:
return job_failed, job_message
return True, "The job is not complete after {0} seconds.".format(job_wait_sec)
def strip_substr_dict(self, odata_dict, chkstr='@odata.'):
cp = odata_dict.copy()
klist = cp.keys()
for k in klist:
if chkstr in str(k).lower():
odata_dict.pop(k)
return odata_dict
def job_submission(self, job_name, job_desc, targets, params, job_type,
schedule="startnow", state="Enabled"):
job_payload = {"JobName": job_name, "JobDescription": job_desc,
"Schedule": schedule, "State": state, "Targets": targets,
"Params": params, "JobType": job_type}
response = self.invoke_request("POST", JOB_SERVICE_URI, data=job_payload)
return response
def test_network_connection(self, share_address, share_path, share_type,
share_user=None, share_password=None, share_domain=None):
job_type = {"Id": 56, "Name": "ValidateNWFileShare_Task"}
params = [
{"Key": "checkPathOnly", "Value": "false"},
{"Key": "shareType", "Value": share_type},
{"Key": "ShareNetworkFilePath", "Value": share_path},
{"Key": "shareAddress", "Value": share_address},
{"Key": "testShareWriteAccess", "Value": "true"}
]
if share_user is not None:
params.append({"Key": "UserName", "Value": share_user})
if share_password is not None:
params.append({"Key": "Password", "Value": share_password})
if share_domain is not None:
params.append({"Key": "domainName", "Value": share_domain})
job_response = self.job_submission("Validate Share", "Validate Share", [], params, job_type)
return job_response
def check_existing_job_state(self, job_type_name):
query_param = {"$filter": "LastRunStatus/Id eq 2030 or LastRunStatus/Id eq 2040 or LastRunStatus/Id eq 2050"}
job_resp = self.invoke_request("GET", JOB_SERVICE_URI, query_param=query_param)
job_lst = job_resp.json_data["value"] if job_resp.json_data.get("value") is not None else []
for job in job_lst:
if job["JobType"]["Name"] == job_type_name:
job_allowed = False
available_jobs = job
break
else:
job_allowed = True
available_jobs = job_lst
return job_allowed, available_jobs
def _get_omam_ca_env(self):
"""Check if the value is set in REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE or OMAM_CA_BUNDLE or returns None"""
return os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("CURL_CA_BUNDLE") or os.environ.get("OMAM_CA_BUNDLE")

View File

@@ -0,0 +1,219 @@
# -*- coding: utf-8 -*-
# Dell EMC OpenManage Ansible Modules
# Version 5.0.1
# Copyright (C) 2019-2022 Dell Inc. or its subsidiaries. All Rights Reserved.
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
import os
from ansible.module_utils.urls import open_url, ConnectionError, SSLValidationError
from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
from ansible.module_utils.six.moves.urllib.parse import urlencode
redfish_auth_params = {
"baseuri": {"required": True, "type": "str"},
"username": {"required": True, "type": "str"},
"password": {"required": True, "type": "str", "no_log": True},
"validate_certs": {"type": "bool", "default": True},
"ca_path": {"type": "path"},
"timeout": {"type": "int", "default": 30},
}
SESSION_RESOURCE_COLLECTION = {
"SESSION": "/redfish/v1/Sessions",
"SESSION_ID": "/redfish/v1/Sessions/{Id}",
}
class OpenURLResponse(object):
"""Handles HTTPResponse"""
def __init__(self, resp):
self.body = None
self.resp = resp
if self.resp:
self.body = self.resp.read()
@property
def json_data(self):
try:
return json.loads(self.body)
except ValueError:
raise ValueError("Unable to parse json")
@property
def status_code(self):
return self.resp.getcode()
@property
def success(self):
status = self.status_code
return status >= 200 & status <= 299
@property
def headers(self):
return self.resp.headers
@property
def reason(self):
return self.resp.reason
class Redfish(object):
"""Handles iDRAC Redfish API requests"""
def __init__(self, module_params=None, req_session=False):
self.module_params = module_params
self.hostname = self.module_params["baseuri"]
self.username = self.module_params["username"]
self.password = self.module_params["password"]
self.validate_certs = self.module_params.get("validate_certs", True)
self.ca_path = self.module_params.get("ca_path")
self.timeout = self.module_params.get("timeout", 30)
self.use_proxy = self.module_params.get("use_proxy", True)
self.req_session = req_session
self.session_id = None
self.protocol = 'https'
self.root_uri = '/redfish/v1/'
self._headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
def _get_base_url(self):
"""builds base url"""
return '{0}://{1}'.format(self.protocol, self.hostname)
def _build_url(self, path, query_param=None):
"""builds complete url"""
url = path
base_uri = self._get_base_url()
if path:
url = base_uri + path
if query_param:
url += "?{0}".format(urlencode(query_param))
return url
def _url_common_args_spec(self, method, api_timeout, headers=None):
"""Creates an argument common spec"""
req_header = self._headers
if headers:
req_header.update(headers)
if api_timeout is None:
api_timeout = self.timeout
if self.ca_path is None:
self.ca_path = self._get_omam_ca_env()
url_kwargs = {
"method": method,
"validate_certs": self.validate_certs,
"ca_path": self.ca_path,
"use_proxy": self.use_proxy,
"headers": req_header,
"timeout": api_timeout,
"follow_redirects": 'all',
}
return url_kwargs
def _args_without_session(self, path, method, api_timeout, headers=None):
"""Creates an argument spec in case of basic authentication"""
req_header = self._headers
if headers:
req_header.update(headers)
url_kwargs = self._url_common_args_spec(method, api_timeout, headers=headers)
if not (path == SESSION_RESOURCE_COLLECTION["SESSION"] and method == 'POST'):
url_kwargs["url_username"] = self.username
url_kwargs["url_password"] = self.password
url_kwargs["force_basic_auth"] = True
return url_kwargs
def _args_with_session(self, method, api_timeout, headers=None):
"""Creates an argument spec, in case of authentication with session"""
url_kwargs = self._url_common_args_spec(method, api_timeout, headers=headers)
url_kwargs["force_basic_auth"] = False
return url_kwargs
def invoke_request(self, method, path, data=None, query_param=None, headers=None,
api_timeout=None, dump=True):
"""
Sends a request through open_url
Returns :class:`OpenURLResponse` object.
:arg method: HTTP verb to use for the request
:arg path: path to request without query parameter
:arg data: (optional) Payload to send with the request
:arg query_param: (optional) Dictionary of query parameter to send with request
:arg headers: (optional) Dictionary of HTTP Headers to send with the
request
:arg api_timeout: (optional) How long to wait for the server to send
data before giving up
:arg dump: (Optional) boolean value for dumping payload data.
:returns: OpenURLResponse
"""
try:
if 'X-Auth-Token' in self._headers:
url_kwargs = self._args_with_session(method, api_timeout, headers=headers)
else:
url_kwargs = self._args_without_session(path, method, api_timeout, headers=headers)
if data and dump:
data = json.dumps(data)
url = self._build_url(path, query_param=query_param)
resp = open_url(url, data=data, **url_kwargs)
resp_data = OpenURLResponse(resp)
except (HTTPError, URLError, SSLValidationError, ConnectionError) as err:
raise err
return resp_data
def __enter__(self):
"""Creates sessions by passing it to header"""
if self.req_session:
payload = {'UserName': self.username,
'Password': self.password}
path = SESSION_RESOURCE_COLLECTION["SESSION"]
resp = self.invoke_request('POST', path, data=payload)
if resp and resp.success:
self.session_id = resp.json_data.get("Id")
self._headers["X-Auth-Token"] = resp.headers.get('X-Auth-Token')
else:
msg = "Could not create the session"
raise ConnectionError(msg)
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Deletes a session id, which is in use for request"""
if self.session_id:
path = SESSION_RESOURCE_COLLECTION["SESSION_ID"].format(Id=self.session_id)
self.invoke_request('DELETE', path)
return False
def strip_substr_dict(self, odata_dict, chkstr='@odata.'):
cp = odata_dict.copy()
klist = cp.keys()
for k in klist:
if chkstr in str(k).lower():
odata_dict.pop(k)
return odata_dict
def _get_omam_ca_env(self):
"""Check if the value is set in REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE or OMAM_CA_BUNDLE or returns None"""
return os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("CURL_CA_BUNDLE") or os.environ.get("OMAM_CA_BUNDLE")

View File

@@ -0,0 +1,350 @@
# -*- coding: utf-8 -*-
# Dell OpenManage Ansible Modules
# Version 6.1.0
# Copyright (C) 2022 Dell Inc. or its subsidiaries. All Rights Reserved.
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
CHANGES_MSG = "Changes found to be applied."
NO_CHANGES_MSG = "No changes found to be applied."
RESET_UNTRACK = "iDRAC reset is in progress. Until the iDRAC is reset, the changes would not apply."
RESET_SUCCESS = "iDRAC has been reset successfully."
RESET_FAIL = "Unable to reset the iDRAC. For changes to reflect, manually reset the iDRAC."
SYSTEM_ID = "System.Embedded.1"
MANAGER_ID = "iDRAC.Embedded.1"
SYSTEMS_URI = "/redfish/v1/Systems"
MANAGERS_URI = "/redfish/v1/Managers"
IDRAC_RESET_URI = "/redfish/v1/Managers/{res_id}/Actions/Manager.Reset"
SYSTEM_RESET_URI = "/redfish/v1/Systems/{res_id}/Actions/ComputerSystem.Reset"
MANAGER_JOB_URI = "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs?$expand=*($levels=1)"
MANAGER_JOB_ID_URI = "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/{0}"
import time
from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
def strip_substr_dict(odata_dict, chkstr='@odata.', case_sensitive=False):
'''
:param odata_dict: the dict to be stripped of unwanted keys
:param chkstr: the substring to be checked among the keys
:param case_sensitive: should the match be case sensitive or not
:return: dict
'''
cp = odata_dict.copy()
klist = cp.keys()
if not case_sensitive:
chkstr = chkstr.lower()
for k in klist:
if case_sensitive:
lk = k
else:
lk = str(k).lower()
if chkstr in lk:
odata_dict.pop(k, None)
return odata_dict
def job_tracking(rest_obj, job_uri, max_job_wait_sec=600, job_state_var=('LastRunStatus', 'Id'),
job_complete_states=(2060, 2020, 2090), job_fail_states=(2070, 2101, 2102, 2103),
job_running_states=(2050, 2040, 2030, 2100),
sleep_interval_secs=10, max_unresponsive_wait=30, initial_wait=1):
'''
:param rest_obj: the rest_obj either of the below
ansible_collections.dellemc.openmanage.plugins.module_utils.ome.RestOME
:param job_uri: the uri to fetch the job response dict
:param max_job_wait_sec: max time the job will wait
:param job_state_var: The nested dict traversal path
:param job_complete_states:
:param job_fail_states:
:param job_running_states:
:param sleep_interval_secs:
:param max_unresponsive_wait:
:param initial_wait:
:return:
'''
# ome_job_status_map = {
# 2020: "Scheduled", 2030: "Queued", 2040: "Starting", 2050: "Running", 2060: "completed successfully",
# 2070: "Failed", 2090: "completed with errors", 2080: "New", 2100: "Aborted", 2101: "Paused", 2102: "Stopped",
# 2103: "Canceled"
# }
# ensure job states are mutually exclusive
max_retries = max_job_wait_sec // sleep_interval_secs
unresp = max_unresponsive_wait // sleep_interval_secs
loop_ctr = 0
job_failed = True
job_dict = {}
wait_time = 0
if set(job_complete_states) & set(job_fail_states):
return job_failed, "Overlapping job states found.", job_dict, wait_time
msg = "Job tracking started."
time.sleep(initial_wait)
while loop_ctr < max_retries:
loop_ctr += 1
try:
job_resp = rest_obj.invoke_request('GET', job_uri)
job_dict = job_resp.json_data
job_status = job_dict
for x in job_state_var:
job_status = job_status.get(x, {})
if job_status in job_complete_states:
job_failed = False
msg = "Job tracking completed."
loop_ctr = max_retries
elif job_status in job_fail_states:
job_failed = True
msg = "Job is in Failed state."
loop_ctr = max_retries
if job_running_states:
if job_status in job_running_states:
time.sleep(sleep_interval_secs)
wait_time = wait_time + sleep_interval_secs
else:
time.sleep(sleep_interval_secs)
wait_time = wait_time + sleep_interval_secs
except Exception as err:
if unresp:
time.sleep(sleep_interval_secs)
wait_time = wait_time + sleep_interval_secs
else:
job_failed = True
msg = "Exception in job tracking " + str(err)
break
unresp = unresp - 1
return job_failed, msg, job_dict, wait_time
def idrac_redfish_job_tracking(
rest_obj, job_uri, max_job_wait_sec=600, job_state_var='JobState',
job_complete_states=("Completed", "Downloaded", "CompletedWithErrors", "RebootCompleted"),
job_fail_states=("Failed", "RebootFailed", "Unknown"),
job_running_states=("Running", "RebootPending", "Scheduling", "Scheduled", "Downloading", "Waiting", "Paused",
"New", "PendingActivation", "ReadyForExecution"),
sleep_interval_secs=10, max_unresponsive_wait=30, initial_wait=1):
# idrac_redfish_job_sates = [ "New", "Scheduled", "Running", "Completed", "Downloading", "Downloaded",
# "Scheduling", "ReadyForExecution", "Waiting", "Paused", "Failed", "CompletedWithErrors", "RebootPending",
# "RebootFailed", "RebootCompleted", "PendingActivation", "Unknown"]
max_retries = max_job_wait_sec // sleep_interval_secs
unresp = max_unresponsive_wait // sleep_interval_secs
loop_ctr = 0
job_failed = True
job_dict = {}
wait_time = 0
if set(job_complete_states) & set(job_fail_states):
return job_failed, "Overlapping job states found.", job_dict, wait_time
msg = "Job tracking started."
time.sleep(initial_wait)
while loop_ctr < max_retries:
loop_ctr += 1
try:
job_resp = rest_obj.invoke_request(job_uri, 'GET')
job_dict = job_resp.json_data
job_status = job_dict
job_status = job_status.get(job_state_var, "Unknown")
if job_status in job_running_states:
time.sleep(sleep_interval_secs)
wait_time = wait_time + sleep_interval_secs
elif job_status in job_complete_states:
job_failed = False
msg = "Job tracking completed."
loop_ctr = max_retries
elif job_status in job_fail_states:
job_failed = True
msg = "Job is in {0} state.".format(job_status)
loop_ctr = max_retries
else: # unrecognised states, just wait
time.sleep(sleep_interval_secs)
wait_time = wait_time + sleep_interval_secs
except Exception as err:
if unresp:
time.sleep(sleep_interval_secs)
wait_time = wait_time + sleep_interval_secs
else:
job_failed = True
msg = "Exception in job tracking " + str(err)
break
unresp = unresp - 1
return job_failed, msg, job_dict, wait_time
def get_rest_items(rest_obj, uri="DeviceService/Devices", key="Id", value="Identifier", selector="value"):
item_dict = {}
resp = rest_obj.get_all_items_with_pagination(uri)
if resp.get(selector):
item_dict = dict((item.get(key), item.get(value)) for item in resp[selector])
return item_dict
def get_item_and_list(rest_obj, name, uri, key='Name', value='value'):
resp = rest_obj.invoke_request('GET', uri)
tlist = []
if resp.success and resp.json_data.get(value):
tlist = resp.json_data.get(value, [])
for xtype in tlist:
if xtype.get(key, "") == name:
return xtype, tlist
return {}, tlist
def apply_diff_key(src, dest, klist):
diff_cnt = 0
for k in klist:
v = src.get(k)
if v is not None and v != dest.get(k):
dest[k] = v
diff_cnt = diff_cnt + 1
return diff_cnt
def wait_for_job_completion(redfish_obj, uri, job_wait=True, wait_timeout=120, sleep_time=10):
max_sleep_time = wait_timeout
sleep_interval = sleep_time
if job_wait:
while max_sleep_time:
if max_sleep_time > sleep_interval:
max_sleep_time = max_sleep_time - sleep_interval
else:
sleep_interval = max_sleep_time
max_sleep_time = 0
time.sleep(sleep_interval)
job_resp = redfish_obj.invoke_request("GET", uri)
if job_resp.json_data.get("PercentComplete") == 100:
time.sleep(10)
return job_resp, ""
else:
job_resp = redfish_obj.invoke_request("GET", uri)
time.sleep(10)
return job_resp, ""
return {}, "The job is not complete after {0} seconds.".format(wait_timeout)
def wait_after_idrac_reset(idrac, wait_time_sec, interval=30):
time.sleep(interval // 2)
msg = RESET_UNTRACK
wait = wait_time_sec
track_failed = True
while wait > 0:
try:
idrac.invoke_request(MANAGERS_URI, 'GET')
time.sleep(interval // 2)
msg = RESET_SUCCESS
track_failed = False
break
except Exception:
time.sleep(interval)
wait = wait - interval
return track_failed, msg
# Can this be in idrac_redfish???
def reset_idrac(idrac_restobj, wait_time_sec=300, res_id=MANAGER_ID, interval=30):
track_failed = True
reset_msg = "iDRAC reset triggered successfully."
try:
resp = idrac_restobj.invoke_request(IDRAC_RESET_URI.format(res_id=res_id), 'POST',
data={"ResetType": "GracefulRestart"})
if wait_time_sec:
track_failed, reset_msg = wait_after_idrac_reset(idrac_restobj, wait_time_sec, interval)
reset = True
except Exception:
reset = False
reset_msg = RESET_FAIL
return reset, track_failed, reset_msg
def get_manager_res_id(idrac):
try:
resp = idrac.invoke_request(MANAGERS_URI, "GET")
membs = resp.json_data.get("Members")
res_uri = membs[0].get('@odata.id')
res_id = res_uri.split("/")[-1]
except HTTPError:
res_id = MANAGER_ID
return res_id
def wait_for_idrac_job_completion(idrac, uri, job_wait=True, wait_timeout=120, sleep_time=10):
max_sleep_time = wait_timeout
sleep_interval = sleep_time
job_msg = "The job is not complete after {0} seconds.".format(wait_timeout)
if job_wait:
while max_sleep_time:
if max_sleep_time > sleep_interval:
max_sleep_time = max_sleep_time - sleep_interval
else:
sleep_interval = max_sleep_time
max_sleep_time = 0
time.sleep(sleep_interval)
job_resp = idrac.invoke_request(uri, "GET")
if job_resp.json_data.get("PercentComplete") == 100:
time.sleep(10)
return job_resp, ""
if job_resp.json_data.get("JobState") == "RebootFailed":
time.sleep(10)
return job_resp, job_msg
else:
job_resp = idrac.invoke_request(uri, "GET")
time.sleep(10)
return job_resp, ""
return {}, "The job is not complete after {0} seconds.".format(wait_timeout)
def idrac_system_reset(idrac, res_id, payload=None, job_wait=True, wait_time_sec=300, interval=30):
track_failed, reset, job_resp = True, False, {}
reset_msg = RESET_UNTRACK
try:
idrac.invoke_request(SYSTEM_RESET_URI.format(res_id=res_id), 'POST', data=payload)
time.sleep(10)
if wait_time_sec:
resp = idrac.invoke_request(MANAGER_JOB_URI, "GET")
job = list(filter(lambda d: d["JobState"] in ["RebootPending"], resp.json_data["Members"]))
if job:
job_resp, msg = wait_for_idrac_job_completion(idrac, MANAGER_JOB_ID_URI.format(job[0]["Id"]),
job_wait=job_wait, wait_timeout=wait_time_sec)
if "job is not complete" in msg:
reset, reset_msg = False, msg
if not msg:
reset = True
except Exception:
reset = False
reset_msg = RESET_FAIL
return reset, track_failed, reset_msg, job_resp
def get_system_res_id(idrac):
res_id = SYSTEM_ID
error_msg = ""
try:
resp = idrac.invoke_request(SYSTEMS_URI, "GET")
except HTTPError:
error_msg = "Unable to complete the request because the resource URI " \
"does not exist or is not implemented."
else:
member = resp.json_data.get("Members")
res_uri = member[0].get('@odata.id')
res_id = res_uri.split("/")[-1]
return res_id, error_msg

Some files were not shown because too many files have changed in this diff Show More