#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type

DOCUMENTATION = '''
---
module: xcc_redfish_command
short_description: Manages Lenovo Out-Of-Band controllers using Redfish APIs
version_added: 2.4.0
description:
  - Builds Redfish URIs locally and sends them to remote OOB controllers to
    perform an action or get information back or update a configuration attribute.
  - Manages virtual media.
  - Supports getting information back via GET method.
  - Supports updating a configuration attribute via PATCH method.
  - Supports performing an action via POST method.
options:
  category:
    required: true
    description:
      - Category to execute on OOB controller.
    type: str
  command:
    required: true
    description:
      - List of commands to execute on OOB controller.
    type: list
    elements: str
  baseuri:
    required: true
    description:
      - Base URI of OOB controller.
    type: str
  username:
    description:
      - Username for authentication with OOB controller.
    type: str
  password:
    description:
      - Password for authentication with OOB controller.
    type: str
  auth_token:
    description:
      - Security token for authentication with OOB controller
    type: str
  timeout:
    description:
      - Timeout in seconds for URL requests to OOB controller.
    default: 10
    type: int
  resource_id:
    required: false
    description:
      - The ID of the System, Manager or Chassis to modify.
    type: str
  virtual_media:
    required: false
    description:
      - The options for VirtualMedia commands.
    type: dict
    suboptions:
      media_types:
        description:
          - The list of media types appropriate for the image.
        type: list
        elements: str
      image_url:
        description:
          - The URL of the image to insert or eject.
        type: str
      inserted:
        description:
          - Indicates if the image is treated as inserted on command completion.
        type: bool
        default: true
      write_protected:
        description:
          - Indicates if the media is treated as write-protected.
        type: bool
        default: true
      username:
        description:
          - The username for accessing the image URL.
        type: str
      password:
        description:
          - The password for accessing the image URL.
        type: str
      transfer_protocol_type:
        description:
          - The network protocol to use with the image.
        type: str
      transfer_method:
        description:
          - The transfer method to use with the image.
        type: str
  resource_uri:
    required: false
    description:
      - The resource uri to get or patch or post.
    type: str
  request_body:
    required: false
    description:
      - The request body to patch or post.
    type: dict

author: "Yuyan Pan (@panyy3)"
'''

EXAMPLES = '''
  - name: Insert Virtual Media
    community.general.xcc_redfish_command:
      category: Manager
      command: VirtualMediaInsert
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      virtual_media:
        image_url: "http://example.com/images/SomeLinux-current.iso"
        media_types:
          - CD
          - DVD
      resource_id: "1"

  - name: Eject Virtual Media
    community.general.xcc_redfish_command:
      category: Manager
      command: VirtualMediaEject
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      virtual_media:
        image_url: "http://example.com/images/SomeLinux-current.iso"
      resource_id: "1"

  - name: Eject all Virtual Media
    community.general.xcc_redfish_command:
      category: Manager
      command: VirtualMediaEject
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      resource_id: "1"

  - name: Get ComputeSystem Oem property SystemStatus via GetResource command
    community.general.xcc_redfish_command:
      category: Raw
      command: GetResource
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      resource_uri: "/redfish/v1/Systems/1"
    register: result
  - ansible.builtin.debug:
      msg: "{{ result.redfish_facts.data.Oem.Lenovo.SystemStatus }}"

  - name: Get Oem DNS setting via GetResource command
    community.general.xcc_redfish_command:
      category: Raw
      command: GetResource
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      resource_uri: "/redfish/v1/Managers/1/NetworkProtocol/Oem/Lenovo/DNS"
    register: result

  - name: Print fetched information
    ansible.builtin.debug:
      msg: "{{ result.redfish_facts.data }}"

  - name: Get Lenovo FoD key collection resource via GetCollectionResource command
    community.general.xcc_redfish_command:
      category: Raw
      command: GetCollectionResource
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      resource_uri: "/redfish/v1/Managers/1/Oem/Lenovo/FoD/Keys"
    register: result

  - name: Print fetched information
    ansible.builtin.debug:
      msg: "{{ result.redfish_facts.data_list }}"

  - name: Update ComputeSystem property AssetTag via PatchResource command
    community.general.xcc_redfish_command:
      category: Raw
      command: PatchResource
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      resource_uri: "/redfish/v1/Systems/1"
      request_body:
        AssetTag: "new_asset_tag"

  - name: Perform BootToBIOSSetup action via PostResource command
    community.general.xcc_redfish_command:
      category: Raw
      command: PostResource
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      resource_uri: "/redfish/v1/Systems/1/Actions/Oem/LenovoComputerSystem.BootToBIOSSetup"
      request_body: {}

  - name: Perform SecureBoot.ResetKeys action via PostResource command
    community.general.xcc_redfish_command:
      category: Raw
      command: PostResource
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      resource_uri: "/redfish/v1/Systems/1/SecureBoot/Actions/SecureBoot.ResetKeys"
      request_body:
        ResetKeysType: DeleteAllKeys

  - name: Create session
    community.general.redfish_command:
      category: Sessions
      command: CreateSession
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
    register: result

  - name: Update Manager DateTimeLocalOffset property using security token for auth
    community.general.xcc_redfish_command:
      category: Raw
      command: PatchResource
      baseuri: "{{ baseuri }}"
      auth_token: "{{ result.session.token }}"
      resource_uri: "/redfish/v1/Managers/1"
      request_body:
        DateTimeLocalOffset: "+08:00"

  - 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 }}"
'''

RETURN = '''
msg:
    description: A message related to the performed action(s).
    returned: when failure or action/update success
    type: str
    sample: "Action was successful"
redfish_facts:
    description: Resource content.
    returned: when command == GetResource or command == GetCollectionResource
    type: dict
    sample: '{
        "redfish_facts": {
            "data": {
                "@odata.etag": "\"3179bf00d69f25a8b3c\"",
                "@odata.id": "/redfish/v1/Managers/1/NetworkProtocol/Oem/Lenovo/DNS",
                "@odata.type": "#LenovoDNS.v1_0_0.LenovoDNS",
                "DDNS": [
                    {
                        "DDNSEnable": true,
                        "DomainName": "",
                        "DomainNameSource": "DHCP"
                    }
                ],
                "DNSEnable": true,
                "Description": "This resource is used to represent a DNS resource for a Redfish implementation.",
                "IPv4Address1": "10.103.62.178",
                "IPv4Address2": "0.0.0.0",
                "IPv4Address3": "0.0.0.0",
                "IPv6Address1": "::",
                "IPv6Address2": "::",
                "IPv6Address3": "::",
                "Id": "LenovoDNS",
                "PreferredAddresstype": "IPv4"
            },
            "ret": true
        }
    }'
'''

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.general.plugins.module_utils.redfish_utils import RedfishUtils


class XCCRedfishUtils(RedfishUtils):
    @staticmethod
    def _find_empty_virt_media_slot(resources, media_types,
                                    media_match_strict=True):
        for uri, data in resources.items():
            # check MediaTypes
            if 'MediaTypes' in data and media_types:
                if not set(media_types).intersection(set(data['MediaTypes'])):
                    continue
            else:
                if media_match_strict:
                    continue
            if 'RDOC' in uri:
                continue
            # if ejected, 'Inserted' should be False and 'ImageName' cleared
            if (not data.get('Inserted', False) and
                    not data.get('ImageName')):
                return uri, data
        return None, None

    def virtual_media_eject_one(self, image_url):
        # locate and read the VirtualMedia resources
        response = self.get_request(self.root_uri + self.manager_uri)
        if response['ret'] is False:
            return response
        data = response['data']
        if 'VirtualMedia' not in data:
            return {'ret': False, 'msg': "VirtualMedia resource not found"}
        virt_media_uri = data["VirtualMedia"]["@odata.id"]
        response = self.get_request(self.root_uri + virt_media_uri)
        if response['ret'] is False:
            return response
        data = response['data']
        virt_media_list = []
        for member in data[u'Members']:
            virt_media_list.append(member[u'@odata.id'])
        resources, headers = self._read_virt_media_resources(virt_media_list)

        # find the VirtualMedia resource to eject
        uri, data, eject = self._find_virt_media_to_eject(resources, image_url)
        if uri and eject:
            if ('Actions' not in data or
                    '#VirtualMedia.EjectMedia' not in data['Actions']):
                # try to eject via PATCH if no EjectMedia action found
                h = headers[uri]
                if 'allow' in h:
                    methods = [m.strip() for m in h.get('allow').split(',')]
                    if 'PATCH' not in methods:
                        # if Allow header present and PATCH missing, return error
                        return {'ret': False,
                                'msg': "%s action not found and PATCH not allowed"
                                       % '#VirtualMedia.EjectMedia'}
                return self.virtual_media_eject_via_patch(uri)
            else:
                # POST to the EjectMedia Action
                action = data['Actions']['#VirtualMedia.EjectMedia']
                if 'target' not in action:
                    return {'ret': False,
                            'msg': "target URI property missing from Action "
                                   "#VirtualMedia.EjectMedia"}
                action_uri = action['target']
                # empty payload for Eject action
                payload = {}
                # POST to action
                response = self.post_request(self.root_uri + action_uri,
                                             payload)
                if response['ret'] is False:
                    return response
                return {'ret': True, 'changed': True,
                        'msg': "VirtualMedia ejected"}
        elif uri and not eject:
            # already ejected: return success but changed=False
            return {'ret': True, 'changed': False,
                    'msg': "VirtualMedia image '%s' already ejected" %
                           image_url}
        else:
            # return failure (no resources matching image_url found)
            return {'ret': False, 'changed': False,
                    'msg': "No VirtualMedia resource found with image '%s' "
                           "inserted" % image_url}

    def virtual_media_eject(self, options):
        if options:
            image_url = options.get('image_url')
            if image_url:  # eject specified one media
                return self.virtual_media_eject_one(image_url)

        # eject all inserted media when no image_url specified
        # read all the VirtualMedia resources
        response = self.get_request(self.root_uri + self.manager_uri)
        if response['ret'] is False:
            return response
        data = response['data']
        if 'VirtualMedia' not in data:
            return {'ret': False, 'msg': "VirtualMedia resource not found"}
        virt_media_uri = data["VirtualMedia"]["@odata.id"]
        response = self.get_request(self.root_uri + virt_media_uri)
        if response['ret'] is False:
            return response
        data = response['data']
        virt_media_list = []
        for member in data[u'Members']:
            virt_media_list.append(member[u'@odata.id'])
        resources, headers = self._read_virt_media_resources(virt_media_list)

        # eject all inserted media one by one
        ejected_media_list = []
        for uri, data in resources.items():
            if data.get('Image') and data.get('Inserted', True):
                returndict = self.virtual_media_eject_one(data.get('Image'))
                if not returndict['ret']:
                    return returndict
                ejected_media_list.append(data.get('Image'))

        if len(ejected_media_list) == 0:
            # no media inserted: return success but changed=False
            return {'ret': True, 'changed': False,
                    'msg': "No VirtualMedia image inserted"}
        else:
            return {'ret': True, 'changed': True,
                    'msg': "VirtualMedia %s ejected" % str(ejected_media_list)}

    def raw_get_resource(self, resource_uri):
        if resource_uri is None:
            return {'ret': False, 'msg': "resource_uri is missing"}
        response = self.get_request(self.root_uri + resource_uri)
        if response['ret'] is False:
            return response
        data = response['data']
        return {'ret': True, 'data': data}

    def raw_get_collection_resource(self, resource_uri):
        if resource_uri is None:
            return {'ret': False, 'msg': "resource_uri is missing"}
        response = self.get_request(self.root_uri + resource_uri)
        if response['ret'] is False:
            return response
        if 'Members' not in response['data']:
            return {'ret': False, 'msg': "Specified resource_uri doesn't have Members property"}
        member_list = [i['@odata.id'] for i in response['data'].get('Members', [])]

        # get member resource one by one
        data_list = []
        for member_uri in member_list:
            uri = self.root_uri + member_uri
            response = self.get_request(uri)
            if response['ret'] is False:
                return response
            data = response['data']
            data_list.append(data)

        return {'ret': True, 'data_list': data_list}

    def raw_patch_resource(self, resource_uri, request_body):
        if resource_uri is None:
            return {'ret': False, 'msg': "resource_uri is missing"}
        if request_body is None:
            return {'ret': False, 'msg': "request_body is missing"}
        # check whether resource_uri existing or not
        response = self.get_request(self.root_uri + resource_uri)
        if response['ret'] is False:
            return response
        original_etag = response['data']['@odata.etag']

        # check validity of keys in request_body
        data = response['data']
        for key in request_body.keys():
            if key not in data:
                return {'ret': False, 'msg': "Key %s not found. Supported key list: %s" % (key, str(data.keys()))}

        # perform patch
        response = self.patch_request(self.root_uri + resource_uri, request_body)
        if response['ret'] is False:
            return response

        # check whether changed or not
        current_etag = ''
        if 'data' in response and '@odata.etag' in response['data']:
            current_etag = response['data']['@odata.etag']
        if current_etag != original_etag:
            return {'ret': True, 'changed': True}
        else:
            return {'ret': True, 'changed': False}

    def raw_post_resource(self, resource_uri, request_body):
        if resource_uri is None:
            return {'ret': False, 'msg': "resource_uri is missing"}
        if '/Actions/' not in resource_uri:
            return {'ret': False, 'msg': "Bad uri %s. Keyword /Actions/ should be included in uri" % resource_uri}
        if request_body is None:
            return {'ret': False, 'msg': "request_body is missing"}
        # get action base uri data for further checking
        action_base_uri = resource_uri.split('/Actions/')[0]
        response = self.get_request(self.root_uri + action_base_uri)
        if response['ret'] is False:
            return response
        if 'Actions' not in response['data']:
            return {'ret': False, 'msg': "Actions property not found in %s" % action_base_uri}

        # check resouce_uri with target uri found in action base uri data
        action_found = False
        action_info_uri = None
        action_target_uri_list = []
        for key in response['data']['Actions'].keys():
            if action_found:
                break
            if not key.startswith('#'):
                continue
            if 'target' in response['data']['Actions'][key]:
                if resource_uri == response['data']['Actions'][key]['target']:
                    action_found = True
                    if '@Redfish.ActionInfo' in response['data']['Actions'][key]:
                        action_info_uri = response['data']['Actions'][key]['@Redfish.ActionInfo']
                else:
                    action_target_uri_list.append(response['data']['Actions'][key]['target'])
        if not action_found and 'Oem' in response['data']['Actions']:
            for key in response['data']['Actions']['Oem'].keys():
                if action_found:
                    break
                if not key.startswith('#'):
                    continue
                if 'target' in response['data']['Actions']['Oem'][key]:
                    if resource_uri == response['data']['Actions']['Oem'][key]['target']:
                        action_found = True
                        if '@Redfish.ActionInfo' in response['data']['Actions']['Oem'][key]:
                            action_info_uri = response['data']['Actions']['Oem'][key]['@Redfish.ActionInfo']
                    else:
                        action_target_uri_list.append(response['data']['Actions']['Oem'][key]['target'])

        if not action_found:
            return {'ret': False,
                    'msg': 'Specified resource_uri is not a supported action target uri, please specify a supported target uri instead. Supported uri: %s'
                    % (str(action_target_uri_list))}

        # check request_body with parameter name defined by @Redfish.ActionInfo
        if action_info_uri is not None:
            response = self.get_request(self.root_uri + action_info_uri)
            if response['ret'] is False:
                return response
            for key in request_body.keys():
                key_found = False
                for para in response['data']['Parameters']:
                    if key == para['Name']:
                        key_found = True
                        break
                if not key_found:
                    return {'ret': False,
                            'msg': 'Invalid property %s found in request_body. Please refer to @Redfish.ActionInfo Parameters: %s'
                            % (key, str(response['data']['Parameters']))}

        # perform post
        response = self.post_request(self.root_uri + resource_uri, request_body)
        if response['ret'] is False:
            return response
        return {'ret': True, 'changed': True}


# More will be added as module features are expanded
CATEGORY_COMMANDS_ALL = {
    "Manager": ["VirtualMediaInsert",
                "VirtualMediaEject"],
    "Raw": ["GetResource",
            "GetCollectionResource",
            "PatchResource",
            "PostResource"]
}


def main():
    result = {}
    module = AnsibleModule(
        argument_spec=dict(
            category=dict(required=True),
            command=dict(required=True, type='list', elements='str'),
            baseuri=dict(required=True),
            username=dict(),
            password=dict(no_log=True),
            auth_token=dict(no_log=True),
            timeout=dict(type='int', default=10),
            resource_id=dict(),
            virtual_media=dict(
                type='dict',
                options=dict(
                    media_types=dict(type='list', elements='str', default=[]),
                    image_url=dict(),
                    inserted=dict(type='bool', default=True),
                    write_protected=dict(type='bool', default=True),
                    username=dict(),
                    password=dict(no_log=True),
                    transfer_protocol_type=dict(),
                    transfer_method=dict(),
                )
            ),
            resource_uri=dict(),
            request_body=dict(
                type='dict',
            ),
        ),
        required_together=[
            ('username', 'password'),
        ],
        required_one_of=[
            ('username', 'auth_token'),
        ],
        mutually_exclusive=[
            ('username', 'auth_token'),
        ],
        supports_check_mode=False
    )

    category = module.params['category']
    command_list = module.params['command']

    # admin credentials used for authentication
    creds = {'user': module.params['username'],
             'pswd': module.params['password'],
             'token': module.params['auth_token']}

    # timeout
    timeout = module.params['timeout']

    # System, Manager or Chassis ID to modify
    resource_id = module.params['resource_id']

    # VirtualMedia options
    virtual_media = module.params['virtual_media']

    # resource_uri
    resource_uri = module.params['resource_uri']

    # request_body
    request_body = module.params['request_body']

    # Build root URI
    root_uri = "https://" + module.params['baseuri']
    rf_utils = XCCRedfishUtils(creds, root_uri, timeout, module, resource_id=resource_id, data_modification=True)

    # Check that Category is valid
    if category not in CATEGORY_COMMANDS_ALL:
        module.fail_json(msg=to_native("Invalid Category '%s'. Valid Categories = %s" % (category, CATEGORY_COMMANDS_ALL.keys())))

    # Check that all commands are valid
    for cmd in command_list:
        # Fail if even one command given is invalid
        if cmd not in CATEGORY_COMMANDS_ALL[category]:
            module.fail_json(msg=to_native("Invalid Command '%s'. Valid Commands = %s" % (cmd, CATEGORY_COMMANDS_ALL[category])))

    # Organize by Categories / Commands
    if category == "Manager":
        # execute only if we find a Manager service resource
        result = rf_utils._find_managers_resource()
        if result['ret'] is False:
            module.fail_json(msg=to_native(result['msg']))

        for command in command_list:
            if command == 'VirtualMediaInsert':
                result = rf_utils.virtual_media_insert(virtual_media)
            elif command == 'VirtualMediaEject':
                result = rf_utils.virtual_media_eject(virtual_media)
    elif category == "Raw":
        for command in command_list:
            if command == 'GetResource':
                result = rf_utils.raw_get_resource(resource_uri)
            elif command == 'GetCollectionResource':
                result = rf_utils.raw_get_collection_resource(resource_uri)
            elif command == 'PatchResource':
                result = rf_utils.raw_patch_resource(resource_uri, request_body)
            elif command == 'PostResource':
                result = rf_utils.raw_post_resource(resource_uri, request_body)

    # Return data back or fail with proper message
    if result['ret'] is True:
        if command == 'GetResource' or command == 'GetCollectionResource':
            module.exit_json(redfish_facts=result)
        else:
            changed = result.get('changed', True)
            msg = result.get('msg', 'Action was successful')
            module.exit_json(changed=changed, msg=msg)
    else:
        module.fail_json(msg=to_native(result['msg']))


if __name__ == '__main__':
    main()
