#! /usr/bin/python
# Copyright Notice:
# Copyright 2019-2025 DMTF. All rights reserved.
# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md

"""
Systems Module

File : systems.py

Brief : This file contains the definitions and functionalities for interacting
        with the systems collection for a given Redfish service
"""

import warnings
import sys
from .collections import get_collection_ids
from .messages import verify_response
from .resets import reset_types
from . import config


class RedfishSystemNotFoundError(Exception):
    """
    Raised when a matching system cannot be found
    """

    pass


class RedfishSystemResetNotFoundError(Exception):
    """
    Raised when the reset action cannot be found
    """

    pass


class RedfishSystemBootNotFoundError(Exception):
    """
    Raised when the boot object cannot be found
    """

    pass


class RedfishSystemBiosNotFoundError(Exception):
    """
    Raised when the BIOS resource cannot be found
    """

    pass


class RedfishSystemBiosInvalidSettingsError(Exception):
    """
    Raised when the BIOS resource contains a settings object, but it's not rendered properly
    """

    pass


class RedfishSystemResetBiosNotFoundError(Exception):
    """
    Raised when the BIOS resource does not support the reset BIOS action
    """

    pass


class RedfishVirtualMediaNotFoundError(Exception):
    """
    Raised when a system does not have any virtual media available
    """

    pass


class RedfishNoAcceptableVirtualMediaError(Exception):
    """
    Raised when a system does not have virtual media available that meets criteria
    """

    pass


def get_system_ids(context):
    """
    Finds the system collection and returns all of the member's identifiers

    Args:
        context: The Redfish client object with an open session

    Returns:
        A list of identifiers of the members of the system collection
    """

    # Get the service root to find the system collection
    service_root = context.get("/redfish/v1/")
    if "Systems" not in service_root.dict:
        # No system collection
        raise RedfishSystemNotFoundError("Service does not contain a system collection")

    # Get the system collection and iterate through its collection
    return get_collection_ids(context, service_root.dict["Systems"]["@odata.id"])


def get_system(context, system_id=None):
    """
    Finds a system matching the given identifier and returns its resource

    Args:
        context: The Redfish client object with an open session
        system_id: The system to locate; if None, perform on the only system

    Returns:
        The system resource
    """

    system_uri_pattern = "/redfish/v1/Systems/{}"
    avail_systems = None

    # If given an identifier, get the system directly
    if system_id is not None:
        system = context.get(system_uri_pattern.format(system_id))
    # No identifier given; see if there's exactly one member
    else:
        avail_systems = get_system_ids(context)
        if len(avail_systems) == 1:
            system = context.get(system_uri_pattern.format(avail_systems[0]))
        else:
            raise RedfishSystemNotFoundError(
                "Service does not contain exactly one system; a target system needs to be specified: {}".format(
                    ", ".join(avail_systems)
                )
            )

    # Check the response and return the system if the response is good
    if system.status == 404:
        if avail_systems is None:
            avail_systems = get_system_ids(context)
        raise RedfishSystemNotFoundError(
            "Service does not contain a system called {}; valid systems: {}".format(system_id, ", ".join(avail_systems))
        )
    verify_response(system)
    return system


def get_system_boot(context, system_id=None):
    """
    Finds a system matching the given ID and returns its Boot object

    Args:
        context: The Redfish client object with an open session
        system_id: The system to locate; if None, perform on the only system

    Returns:
        The Boot object from the given system
    """

    system = get_system(context, system_id)
    boot_obj = system.dict.get("Boot")

    if config.__workarounds__:
        # Try getting boot information from the settings resource
        settings_uris = ["Settings", "SD"]
        for setting_ext in settings_uris:
            system_settings = context.get(system.dict["@odata.id"] + "/" + setting_ext)
            if system_settings.status == 200:
                if "Boot" in system_settings.dict:
                    if boot_obj is None:
                        # Boot property only exists in settings, which is not expected
                        warnings.warn(
                            "System '{}' only reports boot information in the settings resource.  Contact your vendor.".format(
                                system_id
                            )
                        )
                        boot_obj = system_settings.dict["Boot"]
                    else:
                        # Check the boot override properties; these are expected to only be in the active resource
                        boot_override_props = [
                            "BootSourceOverrideTarget",
                            "BootSourceOverrideEnabled",
                            "BootSourceOverrideMode",
                            "UefiTargetBootSourceOverride",
                            "BootNext",
                        ]
                        boot_ov_found = False
                        for prop in boot_override_props:
                            if prop in system_settings.dict["Boot"]:
                                # Boot override property found; copy it over
                                boot_ov_found = True
                                boot_obj[prop] = system_settings.dict["Boot"][prop]
                                if prop + "@Redfish.AllowableValues" in system_settings.dict["Boot"]:
                                    boot_obj[prop + "@Redfish.AllowableValues"] = system_settings.dict["Boot"][
                                        prop + "@Redfish.AllowableValues"
                                    ]
                        if boot_ov_found:
                            warnings.warn(
                                "System '{}' contains one or more boot override properties in the settings resource.  Contact your vendor.".format(
                                    system_id
                                )
                            )
                break

    if boot_obj is None:
        raise RedfishSystemBootNotFoundError("System '{}' does not contain the boot object".format(system.dict["Id"]))
    return boot_obj


def set_system_boot(
    context, system_id=None, ov_target=None, ov_enabled=None, ov_mode=None, ov_uefi_target=None, ov_boot_next=None
):
    """
    Finds a system matching the given ID and updates its Boot object

    Args:
        context: The Redfish client object with an open session
        system_id: The system to locate; if None, perform on the only system
        ov_target: The override target (BootSourceOverrideTarget)
        ov_enabled: The override enabled setting (BootSourceOverrideEnabled)
        ov_mode: The override mode setting (BootSourceOverrideMode)
        ov_uefi_target: The UEFI target for override (UefiTargetBootSourceOverride)
        ov_boot_next: The UEFI boot next for override (BootNext)

    Returns:
        The response of the PATCH
    """

    # Check that the values themselves are supported by the schema
    ov_target_values = [
        "None",
        "Pxe",
        "Floppy",
        "Cd",
        "Usb",
        "Hdd",
        "BiosSetup",
        "Utilities",
        "Diags",
        "UefiShell",
        "UefiTarget",
        "SDCard",
        "UefiHttp",
        "RemoteDrive",
        "UefiBootNext",
    ]
    if ov_target is not None:
        if ov_target not in ov_target_values:
            raise ValueError(
                "{} is not an allowable override target ({})".format(ov_target, ", ".join(ov_target_values))
            )
    ov_enabled_values = ["Disabled", "Once", "Continuous"]
    if ov_enabled is not None:
        if ov_enabled not in ov_enabled_values:
            raise ValueError(
                "{} is not an allowable override enabled ({})".format(ov_enabled, ", ".join(ov_enabled_values))
            )
    ov_mode_values = ["Legacy", "UEFI"]
    if ov_mode is not None:
        if ov_mode not in ov_mode_values:
            raise ValueError("{} is not an allowable override mode ({})".format(ov_mode, ", ".join(ov_mode_values)))

    # Locate the system
    system = get_system(context, system_id)

    # Build the payload
    payload = {"Boot": {}}
    if ov_target is not None:
        payload["Boot"]["BootSourceOverrideTarget"] = ov_target
    if ov_enabled is not None:
        payload["Boot"]["BootSourceOverrideEnabled"] = ov_enabled
    if ov_mode is not None:
        payload["Boot"]["BootSourceOverrideMode"] = ov_mode
    if ov_uefi_target is not None:
        payload["Boot"]["UefiTargetBootSourceOverride"] = ov_uefi_target
    if ov_boot_next is not None:
        payload["Boot"]["BootNext"] = ov_boot_next

    # Update the system
    headers = None
    etag = system.getheader("ETag")
    if etag is not None:
        headers = {"If-Match": etag}
    response = context.patch(system.dict["@odata.id"], body=payload, headers=headers)

    # Attempt workarounds if needed
    if config.__workarounds__ and response.status >= 400:
        # Try directing the request to the settings resource
        settings_uris = ["Settings", "SD"]
        for setting_ext in settings_uris:
            system_settings = context.get(system.dict["@odata.id"] + "/" + setting_ext)
            if system_settings.status == 200:
                headers = None
                etag = system_settings.getheader("ETag")
                if etag is not None:
                    headers = {"If-Match": etag}
                settings_response = context.patch(
                    system.dict["@odata.id"] + "/" + setting_ext, body=payload, headers=headers
                )
                if settings_response.status < 400:
                    # Workaround successful; swap out the response object to return
                    warnings.warn(
                        "System '{}' incorrectly required applying the boot override configuration to the settings resource.  Contact your vendor.".format(
                            system_id
                        )
                    )
                    response = settings_response
                    break

    verify_response(response)
    return response


def print_system_boot(boot):
    """
    Prints the contents of a Boot object

    Args:
        boot: The Boot object to print
    """

    print("")

    print("Boot Override Settings:")
    boot_properties = [
        "BootSourceOverrideTarget",
        "BootSourceOverrideEnabled",
        "BootSourceOverrideMode",
        "UefiTargetBootSourceOverride",
        "BootNext",
    ]
    boot_strings = ["Target", "Enabled", "Mode", "UEFI Target", "Boot Next"]
    for index, boot_property in enumerate(boot_properties):
        if boot_property in boot:
            out_string = "  {}: {}".format(boot_strings[index], boot[boot_property])
            allow_string = ""
            if boot_property + "@Redfish.AllowableValues" in boot:
                allow_string = "; Allowable Values: {}".format(
                    ", ".join(boot[boot_property + "@Redfish.AllowableValues"])
                )
            print(out_string + allow_string)

    print("")


def get_system_reset_info(context, system_id=None, system=None):
    """
    Finds a system matching the given ID and returns its reset info

    Args:
        context: The Redfish client object with an open session
        system_id: The system to locate; if None, perform on the only system
        system: Existing system resource to inspect for reset info

    Returns:
        The URI of the Reset action
        A list of parameter requirements from the Action Info
    """

    if system is None:
        system = get_system(context, system_id)

    # Check that there is a Reset action
    if "Actions" not in system.dict:
        raise RedfishSystemResetNotFoundError("System '{}' does not support the reset action".format(system.dict["Id"]))
    if "#ComputerSystem.Reset" not in system.dict["Actions"]:
        raise RedfishSystemResetNotFoundError("System '{}' does not support the reset action".format(system.dict["Id"]))

    # Extract the info about the Reset action
    reset_action = system.dict["Actions"]["#ComputerSystem.Reset"]
    reset_uri = reset_action["target"]

    if "@Redfish.ActionInfo" not in reset_action:
        # No action info; need to build this manually based on other annotations

        # Default parameter requirements
        reset_parameters = [
            {"Name": "ResetType", "Required": False, "DataType": "String", "AllowableValues": reset_types}
        ]

        # Get the AllowableValues from annotations
        for param in reset_parameters:
            if param["Name"] + "@Redfish.AllowableValues" in reset_action:
                param["AllowableValues"] = reset_action[param["Name"] + "@Redfish.AllowableValues"]
    else:
        # Get the action info and its parameter listing
        action_info = context.get(reset_action["@Redfish.ActionInfo"])
        reset_parameters = action_info.dict["Parameters"]

    return reset_uri, reset_parameters


def system_reset(context, system_id=None, reset_type=None):
    """
    Finds a system matching the given ID and performs a reset

    Args:
        context: The Redfish client object with an open session
        system_id: The system to locate; if None, perform on the only system
        reset_type: The type of reset to perform; if None, perform one of the common resets

    Returns:
        The response of the action
    """

    # Check that the values themselves are supported by the schema
    reset_type_values = reset_types
    if reset_type is not None:
        if reset_type not in reset_type_values:
            raise ValueError("{} is not an allowable reset type ({})".format(reset_type, ", ".join(reset_type_values)))

    # Locate the reset action
    reset_uri, reset_parameters = get_system_reset_info(context, system_id)

    # Build the payload
    if reset_type is None:
        for param in reset_parameters:
            if param["Name"] == "ResetType":
                if "GracefulRestart" in param["AllowableValues"]:
                    reset_type = "GracefulRestart"
                elif "ForceRestart" in param["AllowableValues"]:
                    reset_type = "ForceRestart"
                elif "PowerCycle" in param["AllowableValues"]:
                    reset_type = "PowerCycle"

    payload = {}
    if reset_type is not None:
        payload["ResetType"] = reset_type

    # Reset the system
    response = context.post(reset_uri, body=payload)
    try:
        verify_response(response)
    except Exception as e:
        additional_message = ""
        if response.status == 400:
            # Append the list of valid reset types to 400 Bad Request responses
            additional_message = "\nNo supported reset types listed"
            for param in reset_parameters:
                if param["Name"] == "ResetType" and "AllowableValues" in param:
                    additional_message = "\nSupported reset types: {}".format(", ".join(param["AllowableValues"]))
        raise type(e)(str(e) + additional_message).with_traceback(sys.exc_info()[2])
    return response


def get_virtual_media(context, system_id=None):
    """
    Finds the system matching the given ID and gets its virtual media

    Args:
        context: The Redfish client object with an open session
        system_id: The system to locate; if None, perform on the only system

    Returns:
        An array of dictionaries of the virtual media instances
    """

    # Get the virtual media collection
    virtual_media_collection = get_virtual_media_collection(context, system_id=system_id)

    # Iterate through the members and pull out each of the instances
    virtual_media_list = []
    for member in virtual_media_collection.dict["Members"]:
        virtual_media = context.get(member["@odata.id"])
        virtual_media_list.append(virtual_media.dict)
    return virtual_media_list


def print_virtual_media(virtual_media_list):
    """
    Prints the virtual media list into a table

    Args:
        virtual_media_list: The virtual media list to print
    """

    virtual_media_line_format = "  {:20s} | {}: {}"
    virtual_media_properties = ["Image", "MediaTypes", "ConnectedVia", "Inserted", "WriteProtected"]
    print("")
    for virtual_media in virtual_media_list:
        print(virtual_media_line_format.format(virtual_media["Id"], "ImageName", virtual_media.get("ImageName", "")))
        for property in virtual_media_properties:
            if property in virtual_media:
                prop_val = virtual_media[property]
                if isinstance(prop_val, list):
                    prop_val = ", ".join([i for i in prop_val if i is not None])
                print(virtual_media_line_format.format("", property, prop_val))
        print("")


def insert_virtual_media(
    context, image, system_id=None, media_id=None, media_types=None, inserted=None, write_protected=None
):
    """
    Finds the system matching the given ID and inserts virtual media

    Args:
        context: The Redfish client object with an open session
        image: The URI of the media to insert
        system_id: The system to locate; if None, perform on the only system
        media_id: The virtual media instance to insert; if None, perform on an appropriate instance
        media_types: A list of acceptable media types
        inserted: Indicates if the media is to be marked as inserted for the system
        write_protected: Indicates if the media is to be marked as write-protected for the system

    Returns:
        The response of the insert operation
    """

    # Set up acceptable media types based on the image URI if not specified
    if media_types is None:
        if image.lower().endswith(".iso"):
            media_types = ["CD", "DVD"]
        elif image.lower().endswith(".img"):
            media_types = ["USBStick"]
        elif image.lower().endswith(".bin"):
            media_types = ["USBStick"]

    # Get the virtual media collection
    virtual_media_collection = get_virtual_media_collection(context, system_id=system_id)

    # Scan the virtual media for an appropriate slot
    match = False
    for member in virtual_media_collection.dict["Members"]:
        media = context.get(member["@odata.id"])
        if media.dict["Image"] is not None:
            # In use; move on
            continue

        # Check for a match
        if media_id is not None:
            if media.dict["Id"] == media_id:
                # Identifier match
                match = True
        else:
            if media_types is None:
                # No preferred media type; automatic match
                match = True
            else:
                # Check if the preferred media type is in the reported list
                for type in media_types:
                    if type in media.dict["MediaTypes"]:
                        # Acceptable media type found
                        match = True

        # If a match was found, attempt to insert the media
        if match:
            payload = {"Image": image}
            if inserted:
                payload["Inserted"] = inserted
            if write_protected:
                payload["WriteProtected"] = write_protected
            try:
                # Preference for using the InsertMedia action
                response = context.post(media.dict["Actions"]["#VirtualMedia.InsertMedia"]["target"], body=payload)
            except Exception:
                # Fallback to PATCH method
                if "Inserted" not in payload:
                    payload["Inserted"] = True
                headers = None
                etag = media.getheader("ETag")
                if etag is not None:
                    headers = {"If-Match": etag}
                response = context.patch(media.dict["@odata.id"], body=payload, headers=headers)
            verify_response(response)
            return response

    # No matches found
    if media_id is not None:
        reason = "'{}' not found or is already in use".format(media_id)
    elif media_types is not None:
        reason = "No available slots of types {}".format(", ".join(media_types))
    else:
        reason = "No available slots"
    raise RedfishNoAcceptableVirtualMediaError("No acceptable virtual media: {}".format(reason))


def eject_virtual_media(context, media_id, system_id=None):
    """
    Finds the system matching the given ID and ejects virtual media

    Args:
        context: The Redfish client object with an open session
        media_id: The virtual media instance to eject
        system_id: The system to locate; if None, perform on the only system

    Returns:
        The response of the eject operation
    """

    # Get the virtual media collection
    virtual_media_collection = get_virtual_media_collection(context, system_id=system_id)

    # Scan the virtual media for the selected slot
    for member in virtual_media_collection.dict["Members"]:
        media = context.get(member["@odata.id"])
        if media.dict["Id"] == media_id:
            # Found the selected slot; eject it
            try:
                # Preference for using the EjectMedia action
                response = context.post(media.dict["Actions"]["#VirtualMedia.EjectMedia"]["target"], body={})
            except Exception:
                # Fallback to PATCH method
                payload = {"Image": None, "Inserted": False}
                headers = None
                etag = media.getheader("ETag")
                if etag is not None:
                    headers = {"If-Match": etag}
                response = context.patch(media.dict["@odata.id"], body=payload, headers=headers)
            verify_response(response)
            return response

    # No matches found
    raise RedfishNoAcceptableVirtualMediaError("No acceptable virtual media: '{}' not found".format(media_id))


def get_virtual_media_collection(context, system_id=None):
    """
    Finds the system matching the given ID and gets its virtual media collection

    Args:
        context: The Redfish client object with an open session
        system_id: The system to locate; if None, perform on the only system

    Returns:
        The virtual media collection
    """

    # Locate the system
    system = get_system(context, system_id)

    # Check if there is a VirtualMediaCollection; if not, try the older location in the Manager resource
    virtual_media_uri = None
    if "VirtualMedia" in system.dict:
        virtual_media_uri = system.dict["VirtualMedia"]["@odata.id"]
    else:
        if "Links" in system.dict:
            if "ManagedBy" in system.dict["Links"]:
                for manager in system.dict["Links"]["ManagedBy"]:
                    manager_resp = context.get(manager["@odata.id"])
                    if "VirtualMedia" in manager_resp.dict:
                        # Get the first manager that contains virtual media
                        virtual_media_uri = manager_resp.dict["VirtualMedia"]["@odata.id"]
                        break
    if virtual_media_uri is None:
        raise RedfishVirtualMediaNotFoundError("System '{}' does not support virtual media".format(system.dict["Id"]))

    # Get the VirtualMediaCollection
    return context.get(virtual_media_uri)


def get_system_bios(context, system_id=None):
    """
    Finds a system matching the given ID and gets the BIOS settings

    Args:
        context: The Redfish client object with an open session
        system_id: The system to locate; if None, perform on the only system

    Returns:
        A dictionary of the current BIOS attributes
        A dictionary of the BIOS attributes on the next reset
    """

    # Locate the system
    system = get_system(context, system_id)

    # Get the Bios resource
    if "Bios" not in system.dict:
        raise RedfishSystemBiosNotFoundError("System '{}' does not support representing BIOS".format(system.dict["Id"]))
    bios = context.get(system.dict["Bios"]["@odata.id"])
    current_settings = bios.dict["Attributes"]
    future_settings = bios.dict["Attributes"]

    # Get the Settings object if present
    if "@Redfish.Settings" in bios.dict:
        try:
            bios_settings = get_system_bios_settings(context, bios, system.dict["Id"])
            future_settings = bios_settings.dict["Attributes"]
        except Exception:
            if config.__workarounds__:
                warnings.warn(
                    "System '{}' BIOS resource contains the settings term, but no 'SettingsObject'.  Contact your vendor.  Workarounds exhausted for reading the settings data and falling back on using the active attributes.".format(
                        system_id
                    )
                )
            else:
                raise

    return current_settings, future_settings


def set_system_bios(context, settings, system_id=None):
    """
    Finds a system matching the given ID and sets the BIOS settings

    Args:
        context: The Redfish client object with an open session
        settings: The settings to apply to the system
        system_id: The system to locate; if None, perform on the only system

    Returns:
        The response of the PATCH
    """

    # Locate the system
    system = get_system(context, system_id)

    # Get the BIOS resource and determine if the settings need to be applied to the resource itself or the settings object
    if "Bios" not in system.dict:
        raise RedfishSystemBiosNotFoundError("System '{}' does not support representing BIOS".format(system.dict["Id"]))
    bios_uri = system.dict["Bios"]["@odata.id"]
    bios = context.get(bios_uri)
    etag = bios.getheader("ETag")
    if "@Redfish.Settings" in bios.dict:
        bios_settings = get_system_bios_settings(context, bios, system.dict["Id"])
        bios_uri = bios_settings.dict["@odata.id"]
        etag = bios_settings.getheader("ETag")

    # Update the settings
    payload = {"Attributes": settings}
    headers = None
    if etag is not None:
        headers = {"If-Match": etag}
    response = context.patch(bios_uri, body=payload, headers=headers)
    verify_response(response)
    return response


def get_system_bios_settings(context, bios, system_id):
    """
    Gets the settings resource for BIOS

    Args:
        context: The Redfish client object with an open session
        bios: The BIOS resource
        system_id: The system identifier

    Returns:
        The Settings resource for BIOS
    """

    if "SettingsObject" in bios.dict["@Redfish.Settings"]:
        bios_settings = context.get(bios.dict["@Redfish.Settings"]["SettingsObject"]["@odata.id"])
    else:
        if config.__workarounds__:
            warnings.warn(
                "System '{}' BIOS resource contains the settings term, but no 'SettingsObject'.  Contact your vendor.  Attempting workarounds...".format(
                    system_id
                )
            )
            settings_uris = ["Settings", "SD"]
            for setting_ext in settings_uris:
                bios_settings = context.get(bios.dict["@odata.id"] + "/" + setting_ext)
                if bios_settings.status == 200:
                    break
            try:
                verify_response(bios_settings)
            except Exception:
                raise RedfishSystemBiosInvalidSettingsError(
                    "System '{}' BIOS resource contains the settings term, but no 'SettingsObject'.  Workarounds exhausted.  Contact your vendor.".format(
                        system_id
                    )
                ) from None
        else:
            raise RedfishSystemBiosInvalidSettingsError(
                "System '{}' BIOS resource contains the settings term, but no 'SettingsObject'.  Contact your vendor, or retry with the '__workarounds__' flag.".format(
                    system_id
                )
            )

    return bios_settings


def print_system_bios(current_settings, future_settings):
    """
    Prints the system BIOS settings into a table

    Args:
        current_settings: A dictionary of the current BIOS attributes
        future_settings: A dictionary of the BIOS attributes on the next reset
    """

    print("")
    print("BIOS Settings:")

    bios_line_format = "  {:30s} | {:30s} | {:30s}"
    print(bios_line_format.format("Attribute Name", "Current Setting", "Future Setting"))
    for attribute, value in sorted(current_settings.items()):
        if attribute in future_settings:
            print(bios_line_format.format(attribute, str(current_settings[attribute]), str(future_settings[attribute])))
        else:
            print(
                bios_line_format.format(attribute, str(current_settings[attribute]), str(current_settings[attribute]))
            )

    print("")


def reset_system_bios(context, system_id=None):
    """
    Finds a system matching the given ID and resets the BIOS settings

    Args:
        context: The Redfish client object with an open session
        system_id: The system to locate; if None, perform on the only system

    Returns:
        The response of the reset operation
    """

    # Locate the system
    system = get_system(context, system_id)

    # Get the Bios resource
    if "Bios" not in system.dict:
        raise RedfishSystemBiosNotFoundError("System '{}' does not support representing BIOS".format(system.dict["Id"]))
    bios = context.get(system.dict["Bios"]["@odata.id"])

    # Locate the ResetBios action and invoke it
    if "Actions" not in bios.dict:
        raise RedfishSystemResetBiosNotFoundError(
            "System '{}' does not support the reset BIOS action".format(system.dict["Id"])
        )
    if "#Bios.ResetBios" not in bios.dict["Actions"]:
        raise RedfishSystemResetBiosNotFoundError(
            "System '{}' does not support the reset BIOS action".format(system.dict["Id"])
        )
    response = context.post(bios.dict["Actions"]["#Bios.ResetBios"]["target"], body={})
    verify_response(response)
    return response
