diff --git a/changelogs/fragments/2027-add-redfish-session-create-delete-authenticate.yml b/changelogs/fragments/2027-add-redfish-session-create-delete-authenticate.yml new file mode 100644 index 0000000000..b5c22b9502 --- /dev/null +++ b/changelogs/fragments/2027-add-redfish-session-create-delete-authenticate.yml @@ -0,0 +1,2 @@ +minor_changes: + - redfish_* modules, redfish_utils module utils - add support for Redfish session create, delete, and authenticate (https://github.com/ansible-collections/community.general/issues/1975). diff --git a/plugins/module_utils/redfish_utils.py b/plugins/module_utils/redfish_utils.py index 67a17bd8d1..d8cc4061f8 100644 --- a/plugins/module_utils/redfish_utils.py +++ b/plugins/module_utils/redfish_utils.py @@ -38,13 +38,34 @@ class RedfishUtils(object): self.data_modification = data_modification self._init_session() + def _auth_params(self, headers): + """ + Return tuple of required authentication params based on the presence + of a token in the self.creds dict. If using a token, set the + X-Auth-Token header in the `headers` param. + + :param headers: dict containing headers to send in request + :return: tuple of username, password and force_basic_auth + """ + if self.creds.get('token'): + username = None + password = None + force_basic_auth = False + headers['X-Auth-Token'] = self.creds['token'] + else: + username = self.creds['user'] + password = self.creds['pswd'] + force_basic_auth = True + return username, password, force_basic_auth + # The following functions are to send GET/POST/PATCH/DELETE requests def get_request(self, uri): + req_headers = dict(GET_HEADERS) + username, password, basic_auth = self._auth_params(req_headers) try: - resp = open_url(uri, method="GET", headers=GET_HEADERS, - url_username=self.creds['user'], - url_password=self.creds['pswd'], - force_basic_auth=True, validate_certs=False, + resp = open_url(uri, method="GET", headers=req_headers, + url_username=username, url_password=password, + force_basic_auth=basic_auth, validate_certs=False, follow_redirects='all', use_proxy=True, timeout=self.timeout) data = json.loads(to_native(resp.read())) @@ -65,14 +86,16 @@ class RedfishUtils(object): return {'ret': True, 'data': data, 'headers': headers} def post_request(self, uri, pyld): + req_headers = dict(POST_HEADERS) + username, password, basic_auth = self._auth_params(req_headers) try: resp = open_url(uri, data=json.dumps(pyld), - headers=POST_HEADERS, method="POST", - url_username=self.creds['user'], - url_password=self.creds['pswd'], - force_basic_auth=True, validate_certs=False, + headers=req_headers, method="POST", + url_username=username, url_password=password, + force_basic_auth=basic_auth, validate_certs=False, follow_redirects='all', use_proxy=True, timeout=self.timeout) + headers = dict((k.lower(), v) for (k, v) in resp.info().items()) except HTTPError as e: msg = self._get_extended_message(e) return {'ret': False, @@ -86,10 +109,10 @@ class RedfishUtils(object): except Exception as e: return {'ret': False, 'msg': "Failed POST request to '%s': '%s'" % (uri, to_text(e))} - return {'ret': True, 'resp': resp} + return {'ret': True, 'headers': headers, 'resp': resp} def patch_request(self, uri, pyld): - headers = PATCH_HEADERS + req_headers = dict(PATCH_HEADERS) r = self.get_request(uri) if r['ret']: # Get etag from etag header or @odata.etag property @@ -97,15 +120,13 @@ class RedfishUtils(object): if not etag: etag = r['data'].get('@odata.etag') if etag: - # Make copy of headers and add If-Match header - headers = dict(headers) - headers['If-Match'] = etag + req_headers['If-Match'] = etag + username, password, basic_auth = self._auth_params(req_headers) try: resp = open_url(uri, data=json.dumps(pyld), - headers=headers, method="PATCH", - url_username=self.creds['user'], - url_password=self.creds['pswd'], - force_basic_auth=True, validate_certs=False, + headers=req_headers, method="PATCH", + url_username=username, url_password=password, + force_basic_auth=basic_auth, validate_certs=False, follow_redirects='all', use_proxy=True, timeout=self.timeout) except HTTPError as e: @@ -124,13 +145,14 @@ class RedfishUtils(object): return {'ret': True, 'resp': resp} def delete_request(self, uri, pyld=None): + req_headers = dict(DELETE_HEADERS) + username, password, basic_auth = self._auth_params(req_headers) try: data = json.dumps(pyld) if pyld else None resp = open_url(uri, data=data, - headers=DELETE_HEADERS, method="DELETE", - url_username=self.creds['user'], - url_password=self.creds['pswd'], - force_basic_auth=True, validate_certs=False, + headers=req_headers, method="DELETE", + url_username=username, url_password=password, + force_basic_auth=basic_auth, validate_certs=False, follow_redirects='all', use_proxy=True, timeout=self.timeout) except HTTPError as e: @@ -1192,6 +1214,54 @@ class RedfishUtils(object): return {'ret': True, 'changed': True, 'msg': "Clear all sessions successfully"} + def create_session(self): + if not self.creds.get('user') or not self.creds.get('pswd'): + return {'ret': False, 'msg': + 'Must provide the username and password parameters for ' + 'the CreateSession command'} + + payload = { + 'UserName': self.creds['user'], + 'Password': self.creds['pswd'] + } + response = self.post_request(self.root_uri + self.sessions_uri, payload) + if response['ret'] is False: + return response + + headers = response['headers'] + if 'x-auth-token' not in headers: + return {'ret': False, 'msg': + 'The service did not return the X-Auth-Token header in ' + 'the response from the Sessions collection POST'} + + if 'location' not in headers: + self.module.warn( + 'The service did not return the Location header for the ' + 'session URL in the response from the Sessions collection ' + 'POST') + session_uri = None + else: + session_uri = urlparse(headers.get('location')).path + + session = dict() + session['token'] = headers.get('x-auth-token') + session['uri'] = session_uri + return {'ret': True, 'changed': True, 'session': session, + 'msg': 'Session created successfully'} + + def delete_session(self, session_uri): + if not session_uri: + return {'ret': False, 'msg': + 'Must provide the session_uri parameter for the ' + 'DeleteSession command'} + + response = self.delete_request(self.root_uri + session_uri) + if response['ret'] is False: + return response + + return {'ret': True, 'changed': True, + 'msg': 'Session deleted successfully'} + def get_firmware_update_capabilities(self): result = {} response = self.get_request(self.root_uri + self.update_uri) diff --git a/plugins/modules/remote_management/redfish/idrac_redfish_command.py b/plugins/modules/remote_management/redfish/idrac_redfish_command.py index 49e12e811a..a637d15631 100644 --- a/plugins/modules/remote_management/redfish/idrac_redfish_command.py +++ b/plugins/modules/remote_management/redfish/idrac_redfish_command.py @@ -33,15 +33,18 @@ options: - Base URI of OOB controller type: str username: - required: true description: - User for authentication with OOB controller type: str password: - required: true description: - Password for authentication with OOB controller type: str + auth_token: + description: + - Security token for authentication with OOB controller + type: str + version_added: 2.3.0 timeout: description: - Timeout in seconds for URL requests to OOB controller @@ -137,11 +140,21 @@ def main(): category=dict(required=True), command=dict(required=True, type='list', elements='str'), baseuri=dict(required=True), - username=dict(required=True), - password=dict(required=True, no_log=True), + username=dict(), + password=dict(no_log=True), + auth_token=dict(no_log=True), timeout=dict(type='int', default=10), resource_id=dict() ), + required_together=[ + ('username', 'password'), + ], + required_one_of=[ + ('username', 'auth_token'), + ], + mutually_exclusive=[ + ('username', 'auth_token'), + ], supports_check_mode=False ) @@ -150,7 +163,8 @@ def main(): # admin credentials used for authentication creds = {'user': module.params['username'], - 'pswd': module.params['password']} + 'pswd': module.params['password'], + 'token': module.params['auth_token']} # timeout timeout = module.params['timeout'] diff --git a/plugins/modules/remote_management/redfish/idrac_redfish_config.py b/plugins/modules/remote_management/redfish/idrac_redfish_config.py index a3525fa9c8..e27ef6a2a6 100644 --- a/plugins/modules/remote_management/redfish/idrac_redfish_config.py +++ b/plugins/modules/remote_management/redfish/idrac_redfish_config.py @@ -36,15 +36,18 @@ options: - Base URI of iDRAC type: str username: - required: true description: - User for authentication with iDRAC type: str password: - required: true description: - Password for authentication with iDRAC type: str + auth_token: + description: + - Security token for authentication with OOB controller + type: str + version_added: 2.3.0 manager_attributes: required: false description: @@ -232,12 +235,22 @@ def main(): category=dict(required=True), command=dict(required=True, type='list', elements='str'), baseuri=dict(required=True), - username=dict(required=True), - password=dict(required=True, no_log=True), + username=dict(), + password=dict(no_log=True), + auth_token=dict(no_log=True), manager_attributes=dict(type='dict', default={}), timeout=dict(type='int', default=10), resource_id=dict() ), + required_together=[ + ('username', 'password'), + ], + required_one_of=[ + ('username', 'auth_token'), + ], + mutually_exclusive=[ + ('username', 'auth_token'), + ], supports_check_mode=False ) @@ -246,7 +259,8 @@ def main(): # admin credentials used for authentication creds = {'user': module.params['username'], - 'pswd': module.params['password']} + 'pswd': module.params['password'], + 'token': module.params['auth_token']} # timeout timeout = module.params['timeout'] diff --git a/plugins/modules/remote_management/redfish/idrac_redfish_info.py b/plugins/modules/remote_management/redfish/idrac_redfish_info.py index 42a5efcba9..65fbd5a58b 100644 --- a/plugins/modules/remote_management/redfish/idrac_redfish_info.py +++ b/plugins/modules/remote_management/redfish/idrac_redfish_info.py @@ -37,15 +37,18 @@ options: - Base URI of iDRAC controller type: str username: - required: true description: - User for authentication with iDRAC controller type: str password: - required: true description: - Password for authentication with iDRAC controller type: str + auth_token: + description: + - Security token for authentication with OOB controller + type: str + version_added: 2.3.0 timeout: description: - Timeout in seconds for URL requests to OOB controller @@ -174,10 +177,20 @@ def main(): category=dict(required=True), command=dict(required=True, type='list', elements='str'), baseuri=dict(required=True), - username=dict(required=True), - password=dict(required=True, no_log=True), + username=dict(), + password=dict(no_log=True), + auth_token=dict(no_log=True), timeout=dict(type='int', default=10) ), + required_together=[ + ('username', 'password'), + ], + required_one_of=[ + ('username', 'auth_token'), + ], + mutually_exclusive=[ + ('username', 'auth_token'), + ], supports_check_mode=False ) is_old_facts = module._name in ('idrac_redfish_facts', 'community.general.idrac_redfish_facts') @@ -191,7 +204,8 @@ def main(): # admin credentials used for authentication creds = {'user': module.params['username'], - 'pswd': module.params['password']} + 'pswd': module.params['password'], + 'token': module.params['auth_token']} # timeout timeout = module.params['timeout'] diff --git a/plugins/modules/remote_management/redfish/redfish_command.py b/plugins/modules/remote_management/redfish/redfish_command.py index 9e23fd4626..a2f290d16a 100644 --- a/plugins/modules/remote_management/redfish/redfish_command.py +++ b/plugins/modules/remote_management/redfish/redfish_command.py @@ -35,15 +35,23 @@ options: - Base URI of OOB controller type: str username: - required: true description: - Username for authentication with OOB controller type: str password: - required: true description: - Password for authentication with OOB controller type: str + auth_token: + description: + - Security token for authentication with OOB controller + type: str + version_added: 2.3.0 + session_uri: + description: + - URI of the session resource + type: str + version_added: 2.3.0 id: required: false aliases: [ account_id ] @@ -284,15 +292,6 @@ EXAMPLES = ''' category: Systems command: DisableBootOverride - - name: Set chassis indicator LED to blink - community.general.redfish_command: - category: Chassis - command: IndicatorLedBlink - resource_id: 1U - baseuri: "{{ baseuri }}" - username: "{{ username }}" - password: "{{ password }}" - - name: Add user community.general.redfish_command: category: Accounts @@ -414,6 +413,31 @@ EXAMPLES = ''' password: "{{ password }}" timeout: 20 + - name: Create session + community.general.redfish_command: + category: Sessions + command: CreateSession + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + register: result + + - name: Set chassis indicator LED to blink using security token for auth + community.general.redfish_command: + category: Chassis + command: IndicatorLedBlink + resource_id: 1U + baseuri: "{{ baseuri }}" + auth_token: "{{ result.session.token }}" + + - name: Delete session using security token created by CreateSesssion above + community.general.redfish_command: + category: Sessions + command: DeleteSession + baseuri: "{{ baseuri }}" + auth_token: "{{ result.session.token }}" + session_uri: "{{ result.session.uri }}" + - name: Clear Sessions community.general.redfish_command: category: Sessions @@ -538,7 +562,7 @@ CATEGORY_COMMANDS_ALL = { "Accounts": ["AddUser", "EnableUser", "DeleteUser", "DisableUser", "UpdateUserRole", "UpdateUserPassword", "UpdateUserName", "UpdateAccountServiceProperties"], - "Sessions": ["ClearSessions"], + "Sessions": ["ClearSessions", "CreateSession", "DeleteSession"], "Manager": ["GracefulRestart", "ClearLogs", "VirtualMediaInsert", "VirtualMediaEject", "PowerOn", "PowerForceOff", "PowerForceRestart", "PowerGracefulRestart", "PowerGracefulShutdown", "PowerReboot"], @@ -553,8 +577,10 @@ def main(): category=dict(required=True), command=dict(required=True, type='list', elements='str'), baseuri=dict(required=True), - username=dict(required=True), - password=dict(required=True, no_log=True), + username=dict(), + password=dict(no_log=True), + auth_token=dict(no_log=True), + session_uri=dict(), id=dict(aliases=["account_id"]), new_username=dict(aliases=["account_username"]), new_password=dict(aliases=["account_password"], no_log=True), @@ -590,6 +616,15 @@ def main(): ) ) ), + required_together=[ + ('username', 'password'), + ], + required_one_of=[ + ('username', 'auth_token'), + ], + mutually_exclusive=[ + ('username', 'auth_token'), + ], supports_check_mode=False ) @@ -598,7 +633,8 @@ def main(): # admin credentials used for authentication creds = {'user': module.params['username'], - 'pswd': module.params['password']} + 'pswd': module.params['password'], + 'token': module.params['auth_token']} # user to add/modify/delete user = {'account_id': module.params['id'], @@ -712,6 +748,10 @@ def main(): for command in command_list: if command == "ClearSessions": result = rf_utils.clear_sessions() + elif command == "CreateSession": + result = rf_utils.create_session() + elif command == "DeleteSession": + result = rf_utils.delete_session(module.params['session_uri']) elif category == "Manager": # execute only if we find a Manager service resource @@ -748,7 +788,9 @@ def main(): if result['ret'] is True: del result['ret'] changed = result.get('changed', True) - module.exit_json(changed=changed, msg='Action was successful') + session = result.get('session', dict()) + module.exit_json(changed=changed, session=session, + msg='Action was successful') else: module.fail_json(msg=to_native(result['msg'])) diff --git a/plugins/modules/remote_management/redfish/redfish_config.py b/plugins/modules/remote_management/redfish/redfish_config.py index ecc170437b..5c1df16c4e 100644 --- a/plugins/modules/remote_management/redfish/redfish_config.py +++ b/plugins/modules/remote_management/redfish/redfish_config.py @@ -34,15 +34,18 @@ options: - Base URI of OOB controller type: str username: - required: true description: - User for authentication with OOB controller type: str password: - required: true description: - Password for authentication with OOB controller type: str + auth_token: + description: + - Security token for authentication with OOB controller + type: str + version_added: 2.3.0 bios_attributes: required: false description: @@ -219,8 +222,9 @@ def main(): category=dict(required=True), command=dict(required=True, type='list', elements='str'), baseuri=dict(required=True), - username=dict(required=True), - password=dict(required=True, no_log=True), + username=dict(), + password=dict(no_log=True), + auth_token=dict(no_log=True), bios_attributes=dict(type='dict', default={}), timeout=dict(type='int', default=10), boot_order=dict(type='list', elements='str', default=[]), @@ -235,6 +239,15 @@ def main(): default={} ) ), + required_together=[ + ('username', 'password'), + ], + required_one_of=[ + ('username', 'auth_token'), + ], + mutually_exclusive=[ + ('username', 'auth_token'), + ], supports_check_mode=False ) @@ -243,7 +256,8 @@ def main(): # admin credentials used for authentication creds = {'user': module.params['username'], - 'pswd': module.params['password']} + 'pswd': module.params['password'], + 'token': module.params['auth_token']} # timeout timeout = module.params['timeout'] diff --git a/plugins/modules/remote_management/redfish/redfish_info.py b/plugins/modules/remote_management/redfish/redfish_info.py index 7bf209b7f6..782115d464 100644 --- a/plugins/modules/remote_management/redfish/redfish_info.py +++ b/plugins/modules/remote_management/redfish/redfish_info.py @@ -37,15 +37,18 @@ options: - Base URI of OOB controller type: str username: - required: true description: - User for authentication with OOB controller type: str password: - required: true description: - Password for authentication with OOB controller type: str + auth_token: + description: + - Security token for authentication with OOB controller + type: str + version_added: 2.3.0 timeout: description: - Timeout in seconds for URL requests to OOB controller @@ -301,10 +304,20 @@ def main(): category=dict(type='list', elements='str', default=['Systems']), command=dict(type='list', elements='str'), baseuri=dict(required=True), - username=dict(required=True), - password=dict(required=True, no_log=True), + username=dict(), + password=dict(no_log=True), + auth_token=dict(no_log=True), timeout=dict(type='int', default=10) ), + required_together=[ + ('username', 'password'), + ], + required_one_of=[ + ('username', 'auth_token'), + ], + mutually_exclusive=[ + ('username', 'auth_token'), + ], supports_check_mode=False ) is_old_facts = module._name in ('redfish_facts', 'community.general.redfish_facts') @@ -315,7 +328,8 @@ def main(): # admin credentials used for authentication creds = {'user': module.params['username'], - 'pswd': module.params['password']} + 'pswd': module.params['password'], + 'token': module.params['auth_token']} # timeout timeout = module.params['timeout']