From 091ab12692f2c44ce9cac6439c022b32dedc1cb6 Mon Sep 17 00:00:00 2001 From: Bill Dodd <billdodd@gmail.com> Date: Tue, 3 Sep 2019 20:44:33 -0500 Subject: [PATCH] Fix issues and limitations in account mgmt commands (#58441) * fix issues and limitations in account mgmt commands * fix pep8 and example yaml errors * remove new option and update option aliases * find next empty slot when adding user via PATCH --- lib/ansible/module_utils/redfish_utils.py | 249 +++++++++++++++--- .../redfish/redfish_command.py | 103 ++++++-- 2 files changed, 297 insertions(+), 55 deletions(-) diff --git a/lib/ansible/module_utils/redfish_utils.py b/lib/ansible/module_utils/redfish_utils.py index cf80172b770..b79f1df8769 100644 --- a/lib/ansible/module_utils/redfish_utils.py +++ b/lib/ansible/module_utils/redfish_utils.py @@ -43,7 +43,8 @@ class RedfishUtils(object): msg = self._get_extended_message(e) return {'ret': False, 'msg': "HTTP Error %s on GET request to '%s', extended message: '%s'" - % (e.code, uri, msg)} + % (e.code, uri, msg), + 'status': e.code} except URLError as e: return {'ret': False, 'msg': "URL Error on GET request to '%s': '%s'" % (uri, e.reason)} @@ -66,7 +67,8 @@ class RedfishUtils(object): msg = self._get_extended_message(e) return {'ret': False, 'msg': "HTTP Error %s on POST request to '%s', extended message: '%s'" - % (e.code, uri, msg)} + % (e.code, uri, msg), + 'status': e.code} except URLError as e: return {'ret': False, 'msg': "URL Error on POST request to '%s': '%s'" % (uri, e.reason)} @@ -100,7 +102,8 @@ class RedfishUtils(object): msg = self._get_extended_message(e) return {'ret': False, 'msg': "HTTP Error %s on PATCH request to '%s', extended message: '%s'" - % (e.code, uri, msg)} + % (e.code, uri, msg), + 'status': e.code} except URLError as e: return {'ret': False, 'msg': "URL Error on PATCH request to '%s': '%s'" % (uri, e.reason)} @@ -110,9 +113,10 @@ class RedfishUtils(object): 'msg': "Failed PATCH request to '%s': '%s'" % (uri, to_text(e))} return {'ret': True, 'resp': resp} - def delete_request(self, uri, pyld): + def delete_request(self, uri, pyld=None): try: - resp = open_url(uri, data=json.dumps(pyld), + 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'], @@ -123,7 +127,8 @@ class RedfishUtils(object): msg = self._get_extended_message(e) return {'ret': False, 'msg': "HTTP Error %s on DELETE request to '%s', extended message: '%s'" - % (e.code, uri, msg)} + % (e.code, uri, msg), + 'status': e.code} except URLError as e: return {'ret': False, 'msg': "URL Error on DELETE request to '%s': '%s'" % (uri, e.reason)} @@ -670,6 +675,60 @@ class RedfishUtils(object): return response return {'ret': True, 'changed': True} + def _find_account_uri(self, username=None, acct_id=None): + if not any((username, acct_id)): + return {'ret': False, 'msg': + 'Must provide either account_id or account_username'} + + response = self.get_request(self.root_uri + self.accounts_uri) + if response['ret'] is False: + return response + data = response['data'] + + uris = [a.get('@odata.id') for a in data.get('Members', []) if + a.get('@odata.id')] + for uri in uris: + response = self.get_request(self.root_uri + uri) + if response['ret'] is False: + continue + data = response['data'] + headers = response['headers'] + if username: + if username == data.get('UserName'): + return {'ret': True, 'data': data, + 'headers': headers, 'uri': uri} + if acct_id: + if acct_id == data.get('Id'): + return {'ret': True, 'data': data, + 'headers': headers, 'uri': uri} + + return {'ret': False, 'no_match': True, 'msg': + 'No account with the given account_id or account_username found'} + + def _find_empty_account_slot(self): + response = self.get_request(self.root_uri + self.accounts_uri) + if response['ret'] is False: + return response + data = response['data'] + + uris = [a.get('@odata.id') for a in data.get('Members', []) if + a.get('@odata.id')] + if uris: + # first slot may be reserved, so move to end of list + uris += [uris.pop(0)] + for uri in uris: + response = self.get_request(self.root_uri + uri) + if response['ret'] is False: + continue + data = response['data'] + headers = response['headers'] + if data.get('UserName') == "" and not data.get('Enabled', True): + return {'ret': True, 'data': data, + 'headers': headers, 'uri': uri} + + return {'ret': False, 'no_match': True, 'msg': + 'No empty account slot found'} + def list_users(self): result = {} # listing all users has always been slower than other operations, why? @@ -684,7 +743,7 @@ class RedfishUtils(object): result['ret'] = True data = response['data'] - for users in data[u'Members']: + for users in data.get('Members', []): user_list.append(users[u'@odata.id']) # user_list[] are URIs # for each user, get details @@ -703,54 +762,184 @@ class RedfishUtils(object): result["entries"] = users_results return result + def add_user_via_patch(self, user): + if user.get('account_id'): + # If Id slot specified, use it + response = self._find_account_uri(acct_id=user.get('account_id')) + else: + # Otherwise find first empty slot + response = self._find_empty_account_slot() + + if not response['ret']: + return response + uri = response['uri'] + payload = {} + if user.get('account_username'): + payload['UserName'] = user.get('account_username') + if user.get('account_password'): + payload['Password'] = user.get('account_password') + if user.get('account_roleid'): + payload['RoleId'] = user.get('account_roleid') + response = self.patch_request(self.root_uri + uri, payload) + if response['ret'] is False: + return response + return {'ret': True} + def add_user(self, user): - uri = self.root_uri + self.accounts_uri + "/" + user['userid'] - username = {'UserName': user['username']} - pswd = {'Password': user['userpswd']} - roleid = {'RoleId': user['userrole']} - enabled = {'Enabled': True} - for payload in username, pswd, roleid, enabled: - response = self.patch_request(uri, payload) - if response['ret'] is False: + if not user.get('account_username'): + return {'ret': False, 'msg': + 'Must provide account_username for AddUser command'} + + response = self._find_account_uri(username=user.get('account_username')) + if response['ret']: + # account_username already exists, nothing to do + return {'ret': True, 'changed': False} + + response = self.get_request(self.root_uri + self.accounts_uri) + if not response['ret']: + return response + headers = response['headers'] + + if 'allow' in headers: + methods = [m.strip() for m in headers.get('allow').split(',')] + if 'POST' not in methods: + # if Allow header present and POST not listed, add via PATCH + return self.add_user_via_patch(user) + + payload = {} + if user.get('account_username'): + payload['UserName'] = user.get('account_username') + if user.get('account_password'): + payload['Password'] = user.get('account_password') + if user.get('account_roleid'): + payload['RoleId'] = user.get('account_roleid') + + response = self.post_request(self.root_uri + self.accounts_uri, payload) + if not response['ret']: + if response.get('status') == 405: + # if POST returned a 405, try to add via PATCH + return self.add_user_via_patch(user) + else: return response return {'ret': True} def enable_user(self, user): - uri = self.root_uri + self.accounts_uri + "/" + user['userid'] + response = self._find_account_uri(username=user.get('account_username'), + acct_id=user.get('account_id')) + if not response['ret']: + return response + uri = response['uri'] + data = response['data'] + + if data.get('Enabled'): + # account already enabled, nothing to do + return {'ret': True, 'changed': False} + payload = {'Enabled': True} - response = self.patch_request(uri, payload) + response = self.patch_request(self.root_uri + uri, payload) + if response['ret'] is False: + return response + return {'ret': True} + + def delete_user_via_patch(self, user, uri=None, data=None): + if not uri: + response = self._find_account_uri(username=user.get('account_username'), + acct_id=user.get('account_id')) + if not response['ret']: + return response + uri = response['uri'] + data = response['data'] + + if data and data.get('UserName') == '' and not data.get('Enabled', False): + # account UserName already cleared, nothing to do + return {'ret': True, 'changed': False} + + payload = {'UserName': ''} + if 'Enabled' in data: + payload['Enabled'] = False + response = self.patch_request(self.root_uri + uri, payload) if response['ret'] is False: return response return {'ret': True} def delete_user(self, user): - uri = self.root_uri + self.accounts_uri + "/" + user['userid'] - payload = {'UserName': ""} - response = self.patch_request(uri, payload) - if response['ret'] is False: - return response + response = self._find_account_uri(username=user.get('account_username'), + acct_id=user.get('account_id')) + if not response['ret']: + if response.get('no_match'): + # account does not exist, nothing to do + return {'ret': True, 'changed': False} + else: + # some error encountered + return response + + uri = response['uri'] + headers = response['headers'] + data = response['data'] + + if 'allow' in headers: + methods = [m.strip() for m in headers.get('allow').split(',')] + if 'DELETE' not in methods: + # if Allow header present and DELETE not listed, del via PATCH + return self.delete_user_via_patch(user, uri=uri, data=data) + + response = self.delete_request(self.root_uri + uri) + if not response['ret']: + if response.get('status') == 405: + # if DELETE returned a 405, try to delete via PATCH + return self.delete_user_via_patch(user, uri=uri, data=data) + else: + return response return {'ret': True} def disable_user(self, user): - uri = self.root_uri + self.accounts_uri + "/" + user['userid'] + response = self._find_account_uri(username=user.get('account_username'), + acct_id=user.get('account_id')) + if not response['ret']: + return response + uri = response['uri'] + data = response['data'] + + if not data.get('Enabled'): + # account already disabled, nothing to do + return {'ret': True, 'changed': False} + payload = {'Enabled': False} - response = self.patch_request(uri, payload) + response = self.patch_request(self.root_uri + uri, payload) if response['ret'] is False: return response return {'ret': True} def update_user_role(self, user): - uri = self.root_uri + self.accounts_uri + "/" + user['userid'] - payload = {'RoleId': user['userrole']} - response = self.patch_request(uri, payload) + if not user.get('account_roleid'): + return {'ret': False, 'msg': + 'Must provide account_roleid for UpdateUserRole command'} + + response = self._find_account_uri(username=user.get('account_username'), + acct_id=user.get('account_id')) + if not response['ret']: + return response + uri = response['uri'] + data = response['data'] + + if data.get('RoleId') == user.get('account_roleid'): + # account already has RoleId , nothing to do + return {'ret': True, 'changed': False} + + payload = {'RoleId': user.get('account_roleid')} + response = self.patch_request(self.root_uri + uri, payload) if response['ret'] is False: return response return {'ret': True} def update_user_password(self, user): - uri = self.root_uri + self.accounts_uri + "/" + user['userid'] - payload = {'Password': user['userpswd']} - response = self.patch_request(uri, payload) + response = self._find_account_uri(username=user.get('account_username'), + acct_id=user.get('account_id')) + if not response['ret']: + return response + uri = response['uri'] + payload = {'Password': user['account_password']} + response = self.patch_request(self.root_uri + uri, payload) if response['ret'] is False: return response return {'ret': True} diff --git a/lib/ansible/modules/remote_management/redfish/redfish_command.py b/lib/ansible/modules/remote_management/redfish/redfish_command.py index bc06f621e60..f16e486edb0 100644 --- a/lib/ansible/modules/remote_management/redfish/redfish_command.py +++ b/lib/ansible/modules/remote_management/redfish/redfish_command.py @@ -41,7 +41,7 @@ options: username: required: true description: - - User for authentication with OOB controller + - Username for authentication with OOB controller type: str version_added: "2.8" password: @@ -51,26 +51,30 @@ options: type: str id: required: false + aliases: [ account_id ] description: - - ID of user to add/delete/modify + - ID of account to delete/modify type: str version_added: "2.8" new_username: required: false + aliases: [ account_username ] description: - - name of user to add/delete/modify + - Username of account to add/delete/modify type: str version_added: "2.8" new_password: required: false + aliases: [ account_password ] description: - - password of user to add/delete/modify + - New password of account to add/modify type: str version_added: "2.8" roleid: required: false + aliases: [ account_roleid ] description: - - role of user to add/delete/modify + - Role of account to add/modify type: str version_added: "2.8" bootdevice: @@ -146,6 +150,55 @@ EXAMPLES = ''' username: "{{ username }}" password: "{{ password }}" + - name: Add user + redfish_command: + category: Accounts + command: AddUser + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + new_username: "{{ new_username }}" + new_password: "{{ new_password }}" + roleid: "{{ roleid }}" + + - name: Add user using new option aliases + redfish_command: + category: Accounts + command: AddUser + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + account_username: "{{ account_username }}" + account_password: "{{ account_password }}" + account_roleid: "{{ account_roleid }}" + + - name: Delete user + redfish_command: + category: Accounts + command: DeleteUser + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + account_username: "{{ account_username }}" + + - name: Disable user + redfish_command: + category: Accounts + command: DisableUser + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + account_username: "{{ account_username }}" + + - name: Enable user + redfish_command: + category: Accounts + command: EnableUser + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + account_username: "{{ account_username }}" + - name: Add and enable user redfish_command: category: Accounts @@ -153,20 +206,10 @@ EXAMPLES = ''' baseuri: "{{ baseuri }}" username: "{{ username }}" password: "{{ password }}" - id: "{{ id }}" new_username: "{{ new_username }}" new_password: "{{ new_password }}" roleid: "{{ roleid }}" - - name: Disable and delete user - redfish_command: - category: Accounts - command: ["DisableUser", "DeleteUser"] - baseuri: "{{ baseuri }}" - username: "{{ username }}" - password: "{{ password }}" - id: "{{ id }}" - - name: Update user password redfish_command: category: Accounts @@ -174,8 +217,18 @@ EXAMPLES = ''' baseuri: "{{ baseuri }}" username: "{{ username }}" password: "{{ password }}" - id: "{{ id }}" - new_password: "{{ new_password }}" + account_username: "{{ account_username }}" + account_password: "{{ account_password }}" + + - name: Update user role + redfish_command: + category: Accounts + command: UpdateUserRole + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + account_username: "{{ account_username }}" + roleid: "{{ roleid }}" - name: Clear Manager Logs with a timeout of 20 seconds redfish_command: @@ -220,10 +273,10 @@ def main(): baseuri=dict(required=True), username=dict(required=True), password=dict(required=True, no_log=True), - id=dict(), - new_username=dict(), - new_password=dict(no_log=True), - roleid=dict(), + id=dict(aliases=["account_id"]), + new_username=dict(aliases=["account_username"]), + new_password=dict(aliases=["account_password"], no_log=True), + roleid=dict(aliases=["account_roleid"]), bootdevice=dict(), timeout=dict(type='int', default=10), uefi_target=dict(), @@ -240,10 +293,10 @@ def main(): 'pswd': module.params['password']} # user to add/modify/delete - user = {'userid': module.params['id'], - 'username': module.params['new_username'], - 'userpswd': module.params['new_password'], - 'userrole': module.params['roleid']} + user = {'account_id': module.params['id'], + 'account_username': module.params['new_username'], + 'account_password': module.params['new_password'], + 'account_roleid': module.params['roleid']} # timeout timeout = module.params['timeout']