Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions src/badfish/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ def __init__(

def _log_table(self, table):
buf = StringIO()
tmp = Console(file=buf, highlight=False, force_terminal=self.console.is_terminal, no_color=self.console.no_color)
tmp = Console(
file=buf, highlight=False, force_terminal=self.console.is_terminal, no_color=self.console.no_color
)
tmp.print(table)
self.logger.info(buf.getvalue().rstrip("\n"), extra={"is_table": True})

Expand Down Expand Up @@ -1099,7 +1101,9 @@ async def reboot_server(self, graceful=True):
host_down = await self.polling_host_state("Off")

if not host_down:
self.logger.warning("Unable to graceful shutdown the server, will perform forced shutdown now.")
self.logger.warning(
"Unable to graceful shutdown the server, will perform forced shutdown now."
)
await self.send_reset("ForceOff")
else:
await self.send_reset("ForceOff")
Expand All @@ -1115,7 +1119,9 @@ async def reboot_server(self, graceful=True):

async def reset_idrac(self, wait=False):
if self.vendor != "Dell":
self.logger.warning("Vendor isn't a Dell, if you are trying this on a Supermicro, use --bmc-reset instead.")
self.logger.warning(
"Vendor isn't a Dell, if you are trying this on a Supermicro, use --bmc-reset instead."
)
return False
self.logger.debug("Running reset iDRAC.")
_reset_types = await self.get_reset_types(manager=True)
Expand Down Expand Up @@ -1503,7 +1509,9 @@ async def get_virtual_media_config(self):
"members": [],
}
if data["Oem"].get("Supermicro"):
vm_path.update({"config": data["Oem"].get("Supermicro").get("VirtualMediaConfig").get("@odata.id")})
vm_path.update(
{"config": data["Oem"].get("Supermicro").get("VirtualMediaConfig").get("@odata.id")}
)
else:
vm_path.update({"config": data["Oem"].get("VirtualMediaConfig").get("@odata.id")})
if vm_path["count"] > 0:
Expand Down Expand Up @@ -1655,7 +1663,8 @@ async def boot_to_virtual_media(self):
await self.boot_to("Optical.iDRACVirtual.1-1", True)
else:
self.logger.error(
"Command failed to set next onetime boot to virtual media. " "No virtual optical media boot device."
"Command failed to set next onetime boot to virtual media. "
"No virtual optical media boot device."
)
return False
return True
Expand Down Expand Up @@ -2809,7 +2818,9 @@ async def set_nic_attribute(self, fqdd, attribute, value):
response = await self.patch_request(uri, payload, headers)
status_code = response.status
if status_code in [200, 202]:
self.logger.info("Patch command to set network attribute values and create next reboot job PASSED.")
self.logger.info(
"Patch command to set network attribute values and create next reboot job PASSED."
)

# Extract job ID from response headers (DMTF Redfish DSP0266 §13.6)
# HTTP 202 indicates async operation with job creation
Expand Down
21 changes: 15 additions & 6 deletions tests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,15 +924,18 @@ def render_device_dict(index, device):
BIOS_PASS_SET_MISS_ARG = """\
- ERROR - Missing argument: `--new-password`
"""
BIOS_PASS_RM_GOOD = """\
BIOS_PASS_RM_GOOD = (
"""\
- INFO - Command passed to set BIOS password.
- WARNING - Host will now be rebooted for changes to take place.
- INFO - Command passed to On server, code return is 200.
- INFO - JobID: %s
- INFO - Name: Task
- INFO - Message: Job completed successfully.
- INFO - PercentComplete: 100
""" % JOB_ID
"""
% JOB_ID
)
BIOS_PASS_RM_MISS_ARG = """\
- ERROR - Missing argument: `--old-password`
"""
Expand Down Expand Up @@ -1007,10 +1010,13 @@ def render_device_dict(index, device):
- INFO - Polling for host state: Not Down
- INFO - Command passed to On server, code return is 200.
"""
BIOS_SET_BAD_VALUE = """\
BIOS_SET_BAD_VALUE = (
"""\
- WARNING - List of accepted values for '%s': ['Enabled', 'Disabled']
- ERROR - Value not accepted
""" % ATTRIBUTE_OK
"""
% ATTRIBUTE_OK
)
BIOS_SET_BAD_ATTR = """\
- WARNING - Could not retrieve Bios Attributes.
- ERROR - NotThere not found. Please check attribute name.
Expand All @@ -1033,10 +1039,13 @@ def render_device_dict(index, device):
- INFO - WarningText: None
- INFO - WriteOnly: False
"""
BIOS_GET_ONE_BAD = """\
BIOS_GET_ONE_BAD = (
"""\
- WARNING - Could not retrieve Bios Attributes.
- ERROR - Unable to locate the Bios attribute: %s
""" % ATTRIBUTE_BAD
"""
% ATTRIBUTE_BAD
)
NEXT_BOOT_PXE_OK = '- INFO - PATCH command passed to set next boot onetime boot device to: "Pxe".\n'
NEXT_BOOT_PXE_BAD = (
"- ERROR - Command failed, error code is 400.\n" "- ERROR - Error reading response from host.\n"
Expand Down
11 changes: 8 additions & 3 deletions tests/test_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ def test_host_list_extras(self, mock_get, mock_post, mock_delete):
self.set_mock_response(mock_delete, 200, "OK")
_, err = self.badfish_call(mock_host=None)
# When Members array is empty, init() catches the exception and logs as WARNING
assert err.count("- WARNING - Could not find system resource: ComputerSystem's Members array is either empty or missing") == 3
assert (
err.count(
"- WARNING - Could not find system resource: ComputerSystem's Members array is either empty or missing"
)
== 3
)
assert err.count("- INFO - ************************************************") == 3
assert "[badfish.helpers.logger] - INFO - RESULTS:" in err
assert err.count("f01-h01-000-r630.host.io: FAILED") == 3
Expand Down Expand Up @@ -142,7 +147,7 @@ def test_find_systems_resource_unauthorized(self, mock_get, mock_post, mock_dele
_, err = self.badfish_call()
# When Members array is empty or missing, init() catches the exception and logs as WARNING
assert "- WARNING - Could not find system resource:" in err
assert ("ComputerSystem's Members array" in err or "Authorization Error" in err)
assert "ComputerSystem's Members array" in err or "Authorization Error" in err

@patch("aiohttp.ClientSession.delete")
@patch("aiohttp.ClientSession.post")
Expand All @@ -155,4 +160,4 @@ def test_find_systems_resource_not_found(self, mock_get, mock_post, mock_delete)
_, err = self.badfish_call()
# When Members array is empty or missing, init() catches the exception and logs as WARNING
assert "- WARNING - Could not find system resource:" in err
assert ("Systems resource not found" in err or "ComputerSystem's Members array" in err)
assert "Systems resource not found" in err or "ComputerSystem's Members array" in err
12 changes: 8 additions & 4 deletions tests/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,8 @@ async def test_get_raw_client_connector_certificate_error_raises(mock_get):
client = HTTPClient("host", "u", "p", logger, insecure=False)
# Simulate ClientConnectorCertificateError
mock_get.side_effect = aiohttp.ClientConnectorCertificateError(
connection_key=MagicMock(), certificate_error=ssl.SSLError("cert error"))
connection_key=MagicMock(), certificate_error=ssl.SSLError("cert error")
)
with pytest.raises(BadfishException, match="SSL certificate verification failed"):
await client.get_raw("https://x")

Expand All @@ -373,7 +374,8 @@ async def test_post_request_client_connector_certificate_error_raises(mock_post)
logger = DummyLogger()
client = HTTPClient("host", "u", "p", logger, insecure=False)
mock_post.side_effect = aiohttp.ClientConnectorCertificateError(
connection_key=MagicMock(), certificate_error=ssl.SSLError("cert error"))
connection_key=MagicMock(), certificate_error=ssl.SSLError("cert error")
)
with pytest.raises(BadfishException, match="SSL certificate verification failed"):
await client.post_request("https://x", {}, {})

Expand Down Expand Up @@ -403,7 +405,8 @@ async def test_patch_request_client_connector_certificate_error_raises(mock_patc
logger = DummyLogger()
client = HTTPClient("host", "u", "p", logger, insecure=False)
mock_patch.side_effect = aiohttp.ClientConnectorCertificateError(
connection_key=MagicMock(), certificate_error=ssl.SSLError("cert error"))
connection_key=MagicMock(), certificate_error=ssl.SSLError("cert error")
)
with pytest.raises(BadfishException, match="SSL certificate verification failed"):
await client.patch_request("https://x", {}, {})

Expand All @@ -424,7 +427,8 @@ async def test_delete_request_client_connector_certificate_error_raises(mock_del
logger = DummyLogger()
client = HTTPClient("host", "u", "p", logger, insecure=False)
mock_delete.side_effect = aiohttp.ClientConnectorCertificateError(
connection_key=MagicMock(), certificate_error=ssl.SSLError("cert error"))
connection_key=MagicMock(), certificate_error=ssl.SSLError("cert error")
)
with pytest.raises(BadfishException, match="SSL certificate verification failed"):
await client.delete_request("https://x", {})

Expand Down
25 changes: 6 additions & 19 deletions tests/test_main_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ async def test_missing_credentials():

assert result == (host, False)
logger.error.assert_called_once_with(
"Missing credentials. Please provide credentials via CLI arguments "
"or environment variables."
"Missing credentials. Please provide credentials via CLI arguments " "or environment variables."
)


Expand All @@ -46,9 +45,7 @@ async def test_init_401_unauthorized(mock_args):
mock_response.text = AsyncMock(return_value='{"error": "Unauthorized"}')

# Patch 'badfish' (not src.badfish) to match the import above
with patch(
"badfish.main.HTTPClient.get_request", new_callable=AsyncMock
) as mock_get:
with patch("badfish.main.HTTPClient.get_request", new_callable=AsyncMock) as mock_get:
mock_get.return_value = mock_response

result = await execute_badfish(host, mock_args, logger, None)
Expand All @@ -60,10 +57,7 @@ async def test_init_401_unauthorized(mock_args):
exception_obj = args[0]

assert isinstance(exception_obj, BadfishException)
assert (
str(exception_obj)
== f"Failed to authenticate. Verify your credentials for {host}"
)
assert str(exception_obj) == f"Failed to authenticate. Verify your credentials for {host}"


@pytest.mark.asyncio
Expand All @@ -80,9 +74,7 @@ async def test_init_key_error_missing_version(mock_args):
mock_response.text = AsyncMock(return_value='{"OtherKey": "Value"}')

# Patch 'badfish' (not src.badfish) to match the import above
with patch(
"badfish.main.HTTPClient.get_request", new_callable=AsyncMock
) as mock_get:
with patch("badfish.main.HTTPClient.get_request", new_callable=AsyncMock) as mock_get:
mock_get.return_value = mock_response

result = await execute_badfish(host, mock_args, logger, None)
Expand All @@ -94,10 +86,7 @@ async def test_init_key_error_missing_version(mock_args):
exception_obj = args[0]

assert isinstance(exception_obj, BadfishException)
assert (
str(exception_obj)
== "Was unable to get Redfish Version. Please verify credentials/host."
)
assert str(exception_obj) == "Was unable to get Redfish Version. Please verify credentials/host."


@pytest.mark.asyncio
Expand All @@ -107,9 +96,7 @@ async def test_init_no_response_from_host(mock_args):
mock_args.update({"u": "user", "p": "pass", "retries": 1})
logger = MagicMock(spec=logging.Logger)

with patch(
"badfish.main.HTTPClient.get_request", new_callable=AsyncMock
) as mock_get:
with patch("badfish.main.HTTPClient.get_request", new_callable=AsyncMock) as mock_get:
mock_get.return_value = None # L383 condition

result = await execute_badfish(host, mock_args, logger, None)
Expand Down
27 changes: 13 additions & 14 deletions tests/test_nic_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,13 +843,14 @@ def test_set_nic_attr_vf_limit_valid_value_no_warning(self, mock_get, mock_post,
GET_NIC_ATTR_REGISTRY_WITH_VF,
GET_NIC_ATTR_LIST_WITH_VF,
)

# Create updated list with NumberVFAdvertised set to 48
import json

updated_attrs = json.loads(GET_NIC_ATTR_LIST_WITH_VF)
updated_attrs["Attributes"]["NumberVFAdvertised"] = "48"
GET_NIC_ATTR_LIST_VF_48 = json.dumps(updated_attrs)

responses = INIT_RESP + [
GET_FW_VERSION,
GET_NIC_ATTR_REGISTRY_WITH_VF,
Expand All @@ -867,7 +868,7 @@ def test_set_nic_attr_vf_limit_valid_value_no_warning(self, mock_get, mock_post,
self.set_mock_response(mock_get, 200, responses)
self.set_mock_response(mock_post, 200, "OK")
self.set_mock_response(mock_delete, 200, "OK")

patch_headers = {"Location": "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/JID_498218641680"}
self.set_mock_response(mock_patch, 202, "OK", headers=patch_headers)

Expand Down Expand Up @@ -943,7 +944,7 @@ def test_set_nic_attr_vf_limit_exception_handling(self, mock_get, mock_post, moc
GET_NIC_ATTR_REGISTRY_WITH_VF,
GET_NIC_ATTR_LIST_WITH_VF,
)

# NIC attrs with non-integer NumberPCIFunctionsEnabled to trigger ValueError in int() conversion
ATTRS_WITH_INVALID_FUNCTION_COUNT = """
{
Expand All @@ -956,7 +957,7 @@ def test_set_nic_attr_vf_limit_exception_handling(self, mock_get, mock_post, moc
}
}
"""

responses = INIT_RESP + [
GET_FW_VERSION,
GET_NIC_ATTR_REGISTRY_WITH_VF,
Expand All @@ -973,7 +974,7 @@ def test_set_nic_attr_vf_limit_exception_handling(self, mock_get, mock_post, moc
self.set_mock_response(mock_get, 200, responses)
self.set_mock_response(mock_post, 200, "OK")
self.set_mock_response(mock_delete, 200, "OK")

patch_headers = {"Location": "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/JID_498218641680"}
self.set_mock_response(mock_patch, 202, "OK", headers=patch_headers)

Expand All @@ -990,7 +991,6 @@ def test_set_nic_attr_vf_limit_exception_handling(self, mock_get, mock_post, moc
# Exception caught at line 2568, proceeds with attempt
assert "Attempting to set NumberVFAdvertised" not in err # No warning due to exception


@patch("aiohttp.ClientSession.patch")
@patch("aiohttp.ClientSession.delete")
@patch("aiohttp.ClientSession.post")
Expand All @@ -999,7 +999,7 @@ def test_set_nic_attr_nonexistent_attribute(self, mock_get, mock_post, mock_dele
"""Test set_nic_attribute when attribute doesn't exist (L2522-2523)"""
# Return empty registry that won't match the requested attribute
empty_registry = '{"RegistryEntries": {"Attributes": []}}'

responses = INIT_RESP + [
GET_FW_VERSION,
empty_registry, # Empty registry means attribute won't be found
Expand Down Expand Up @@ -1043,7 +1043,7 @@ def test_monitor_verify_job_failed_value_mismatch(self, mock_get, mock_post, moc
self.set_mock_response(mock_get, 200, responses)
self.set_mock_response(mock_post, 200, "OK")
self.set_mock_response(mock_delete, 200, "OK")

patch_headers = {"Location": "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/JID_498218641680"}
self.set_mock_response(mock_patch, 202, "OK", headers=patch_headers)

Expand All @@ -1067,7 +1067,7 @@ def test_monitor_verify_final_check_returns_none(self, mock_get, mock_post, mock
"""Test _monitor_and_verify_attribute_job when final verification returns None (L1023-1024)"""
# Create a response that will cause get_nic_attribute_info to fail
empty_or_invalid_response = '{"Attributes": {}}' # Missing the requested attribute

responses = INIT_RESP + [
GET_FW_VERSION,
GET_NIC_ATTR_REGISTRY,
Expand All @@ -1083,7 +1083,7 @@ def test_monitor_verify_final_check_returns_none(self, mock_get, mock_post, mock
self.set_mock_response(mock_get, 200, responses)
self.set_mock_response(mock_post, 200, "OK")
self.set_mock_response(mock_delete, 200, "OK")

patch_headers = {"Location": "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/JID_498218641680"}
self.set_mock_response(mock_patch, 202, "OK", headers=patch_headers)

Expand Down Expand Up @@ -1182,7 +1182,6 @@ def test_set_nic_attr_virt_mode_sriov_numbervf(self, mock_get, mock_post, mock_d
# Should NOT have the NONE error
assert "Cannot set NumberVFAdvertised when VirtualizationMode is NONE" not in err


@patch("aiohttp.ClientSession.patch")
@patch("aiohttp.ClientSession.delete")
@patch("aiohttp.ClientSession.post")
Expand All @@ -1198,7 +1197,7 @@ def test_set_nic_attr_catches_patch_exception(self, mock_get, mock_post, mock_de
self.set_mock_response(mock_get, 200, responses)
self.set_mock_response(mock_post, 200, "OK")
self.set_mock_response(mock_delete, 200, "OK")

# Make PATCH context manager raise exception
mock_patch.return_value.__aenter__.side_effect = ValueError("Test exception")

Expand All @@ -1212,4 +1211,4 @@ def test_set_nic_attr_catches_patch_exception(self, mock_get, mock_post, mock_de
]
_, err = self.badfish_call()
# Should catch and log error (may be "Failed to communicate" or "Was unable to set")
assert ("Was unable to set" in err or "Failed to communicate" in err or "ERROR" in err)
assert "Was unable to set" in err or "Failed to communicate" in err or "ERROR" in err
1 change: 0 additions & 1 deletion tests/test_reset_idrac.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,3 @@ def test_reset_idrac_with_wait_delayed(self, mock_get, mock_post, mock_delete):
assert "Polling for iDRAC" in err
assert "iDRAC is ready" in err
assert "iDRAC is now responsive" in err

Loading