Skip to content

Commit ae3e12b

Browse files
authored
Merge pull request #519 from redhat-performance/development
feature and fixes sync from development branch
2 parents 898d400 + 9f3523c commit ae3e12b

File tree

11 files changed

+369
-130
lines changed

11 files changed

+369
-130
lines changed

README.md

Lines changed: 82 additions & 59 deletions
Large diffs are not rendered by default.

setup.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,4 @@
1313
else:
1414
raise RuntimeError("Unable to find version string in src/badfish/__init__.py")
1515

16-
setuptools.setup(
17-
version=current_version
18-
)
16+
setuptools.setup(version=current_version)

src/badfish/helpers/parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ def create_parser():
1010
allow_abbrev=False,
1111
)
1212
parser.add_argument("-H", "--host", help="iDRAC host address")
13-
parser.add_argument("-u", help="iDRAC username", required=True)
14-
parser.add_argument("-p", help="iDRAC password", required=True)
13+
parser.add_argument("-u", help="iDRAC username")
14+
parser.add_argument("-p", help="iDRAC password")
1515
parser.add_argument("-i", help="Path to iDRAC interfaces yaml", default=None)
1616
parser.add_argument("-t", help="Type of host as defined on iDRAC interfaces yaml")
1717
parser.add_argument("-l", "--log", help="Optional argument for logging results to a file")

src/badfish/main.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,15 @@ async def find_session_uri(self):
382382
if not response:
383383
raise BadfishException(f"Failed to communicate with {self.host}")
384384

385+
if response.status == 401:
386+
raise BadfishException(f"Failed to authenticate. Verify your credentials for {self.host}")
387+
385388
raw = await response.text("utf-8", "ignore")
386389
data = json.loads(raw.strip())
387-
redfish_version = int(data["RedfishVersion"].replace(".", ""))
390+
try:
391+
redfish_version = int(data["RedfishVersion"].replace(".", ""))
392+
except KeyError:
393+
raise BadfishException("Was unable to get Redfish Version. Please verify credentials/host.")
388394
session_uri = None
389395
if redfish_version >= 160:
390396
session_uri = "/redfish/v1/SessionService/Sessions"
@@ -2405,7 +2411,7 @@ async def set_nic_attribute(self, fqdd, attribute, value):
24052411
)
24062412
self.logger.debug(uri)
24072413
except (IndexError, ValueError):
2408-
self.logger.error("Invalid FQDD suplied.")
2414+
self.logger.error("Invalid FQDD supplied.")
24092415
return False
24102416

24112417
headers = {"content-type": "application/json"}
@@ -2450,8 +2456,19 @@ async def set_nic_attribute(self, fqdd, attribute, value):
24502456

24512457

24522458
async def execute_badfish(_host, _args, logger, format_handler=None):
2453-
_username = _args["u"]
2454-
_password = _args["p"]
2459+
_username = _args.get("u") or os.environ.get("BADFISH_USERNAME")
2460+
_password = _args.get("p") or os.environ.get("BADFISH_PASSWORD")
2461+
2462+
if _args.get("p"):
2463+
logger.warning(
2464+
"Passing secrets via command line arguments can be unsafe. "
2465+
"Consider using environment variables (BADFISH_USERNAME, BADFISH_PASSWORD)."
2466+
)
2467+
2468+
if not _username or not _password:
2469+
logger.error("Missing credentials. Please provide credentials via CLI arguments or environment variables.")
2470+
return _host, False
2471+
24552472
host_type = _args["t"]
24562473
interfaces_path = _args["i"]
24572474
force = _args["force"]

tests/config.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -924,18 +924,15 @@ def render_device_dict(index, device):
924924
BIOS_PASS_SET_MISS_ARG = """\
925925
- ERROR - Missing argument: `--new-password`
926926
"""
927-
BIOS_PASS_RM_GOOD = (
928-
"""\
927+
BIOS_PASS_RM_GOOD = """\
929928
- INFO - Command passed to set BIOS password.
930929
- WARNING - Host will now be rebooted for changes to take place.
931930
- INFO - Command passed to On server, code return is 200.
932931
- INFO - JobID: %s
933932
- INFO - Name: Task
934933
- INFO - Message: Job completed successfully.
935934
- INFO - PercentComplete: 100
936-
"""
937-
% JOB_ID
938-
)
935+
""" % JOB_ID
939936
BIOS_PASS_RM_MISS_ARG = """\
940937
- ERROR - Missing argument: `--old-password`
941938
"""
@@ -1010,13 +1007,10 @@ def render_device_dict(index, device):
10101007
- INFO - Polling for host state: Not Down
10111008
- INFO - Command passed to On server, code return is 200.
10121009
"""
1013-
BIOS_SET_BAD_VALUE = (
1014-
"""\
1010+
BIOS_SET_BAD_VALUE = """\
10151011
- WARNING - List of accepted values for '%s': ['Enabled', 'Disabled']
10161012
- ERROR - Value not accepted
1017-
"""
1018-
% ATTRIBUTE_OK
1019-
)
1013+
""" % ATTRIBUTE_OK
10201014
BIOS_SET_BAD_ATTR = """\
10211015
- WARNING - Could not retrieve Bios Attributes.
10221016
- ERROR - NotThere not found. Please check attribute name.
@@ -1039,13 +1033,10 @@ def render_device_dict(index, device):
10391033
- INFO - WarningText: None
10401034
- INFO - WriteOnly: False
10411035
"""
1042-
BIOS_GET_ONE_BAD = (
1043-
"""\
1036+
BIOS_GET_ONE_BAD = """\
10441037
- WARNING - Could not retrieve Bios Attributes.
10451038
- ERROR - Unable to locate the Bios attribute: %s
1046-
"""
1047-
% ATTRIBUTE_BAD
1048-
)
1039+
""" % ATTRIBUTE_BAD
10491040
NEXT_BOOT_PXE_OK = '- INFO - PATCH command passed to set next boot onetime boot device to: "Pxe".\n'
10501041
NEXT_BOOT_PXE_BAD = (
10511042
"- ERROR - Command failed, error code is 400.\n" "- ERROR - Error reading response from host.\n"
@@ -2347,3 +2338,5 @@ def render_device_dict(index, device):
23472338
{RESPONSE_UNSOPPORTED_IDRAC_VERSION}
23482339
{RESPONSE_NIC_ATTR_GET_ERROR}
23492340
"""
2341+
MANAGER_INSTANCE_RESP = '{"Jobs":{"@odata.id":"/redfish/v1/Managers/iDRAC.Embedded.1/Jobs"}}'
2342+
JOBS_RESP = '{"Members":[]}'

tests/test_async_loop.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,29 @@
44

55

66
class TestAsyncioFix(unittest.TestCase):
7-
@patch('badfish.main.execute_badfish')
8-
@patch('badfish.main.BadfishLogger')
9-
@patch('badfish.main.parse_arguments')
10-
@patch('asyncio.set_event_loop')
11-
@patch('asyncio.new_event_loop')
12-
@patch('asyncio.get_event_loop')
13-
def test_main_handles_no_event_loop(self, mock_get_loop, mock_new_loop,
14-
mock_set_loop, mock_parse_args,
15-
mock_logger, mock_execute):
7+
@patch("badfish.main.execute_badfish")
8+
@patch("badfish.main.BadfishLogger")
9+
@patch("badfish.main.parse_arguments")
10+
@patch("asyncio.set_event_loop")
11+
@patch("asyncio.new_event_loop")
12+
@patch("asyncio.get_event_loop")
13+
def test_main_handles_no_event_loop(
14+
self, mock_get_loop, mock_new_loop, mock_set_loop, mock_parse_args, mock_logger, mock_execute
15+
):
1616
mock_get_loop.side_effect = RuntimeError("No event loop")
1717

1818
mock_loop_instance = MagicMock()
1919
mock_new_loop.return_value = mock_loop_instance
2020
mock_loop_instance.run_until_complete.return_value = ("localhost", True)
2121

2222
mock_parse_args.return_value = {
23-
"verbose": False, "host": "localhost", "delta": None,
24-
"firmware_inventory": None, "host_list": None, "log": None,
25-
"output": None
23+
"verbose": False,
24+
"host": "localhost",
25+
"delta": None,
26+
"firmware_inventory": None,
27+
"host_list": None,
28+
"log": None,
29+
"output": None,
2630
}
2731

2832
main()
@@ -32,24 +36,28 @@ def test_main_handles_no_event_loop(self, mock_get_loop, mock_new_loop,
3236
mock_set_loop.assert_called_once_with(mock_loop_instance)
3337
mock_loop_instance.run_until_complete.assert_called()
3438

35-
@patch('badfish.main.execute_badfish')
36-
@patch('badfish.main.BadfishLogger')
37-
@patch('badfish.main.parse_arguments')
38-
@patch('asyncio.set_event_loop')
39-
@patch('asyncio.new_event_loop')
40-
@patch('asyncio.get_event_loop')
41-
def test_main_uses_existing_loop(self, mock_get_loop, mock_new_loop,
42-
mock_set_loop, mock_parse_args,
43-
mock_logger, mock_execute):
39+
@patch("badfish.main.execute_badfish")
40+
@patch("badfish.main.BadfishLogger")
41+
@patch("badfish.main.parse_arguments")
42+
@patch("asyncio.set_event_loop")
43+
@patch("asyncio.new_event_loop")
44+
@patch("asyncio.get_event_loop")
45+
def test_main_uses_existing_loop(
46+
self, mock_get_loop, mock_new_loop, mock_set_loop, mock_parse_args, mock_logger, mock_execute
47+
):
4448
existing_loop = MagicMock()
4549
mock_get_loop.return_value = existing_loop
4650
mock_get_loop.side_effect = None
4751
existing_loop.run_until_complete.return_value = ("localhost", True)
4852

4953
mock_parse_args.return_value = {
50-
"verbose": False, "host": "localhost", "delta": None,
51-
"firmware_inventory": None, "host_list": None, "log": None,
52-
"output": None
54+
"verbose": False,
55+
"host": "localhost",
56+
"delta": None,
57+
"firmware_inventory": None,
58+
"host_list": None,
59+
"log": None,
60+
"output": None,
5361
}
5462

5563
main()

tests/test_base.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import os
23
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
34

45
import pytest
@@ -49,25 +50,35 @@ def capture_wrap(self):
4950
yield
5051

5152
def badfish_call(
52-
self,
53-
mock_host=config.MOCK_HOST,
54-
mock_user=config.MOCK_USER,
55-
mock_pass=config.MOCK_PASS,
53+
self, mock_host=config.MOCK_HOST, mock_user=config.MOCK_USER, mock_pass=config.MOCK_PASS, use_cli_secrets=False
5654
):
5755
argv = []
56+
env_vars = os.environ.copy()
5857

5958
if mock_host is not None:
6059
argv.extend(("-H", mock_host))
61-
if mock_user is not None:
62-
argv.extend(("-u", mock_user))
63-
if mock_pass is not None:
64-
argv.extend(("-p", mock_pass))
65-
66-
argv.extend(self.args)
67-
try:
68-
main(argv)
69-
except BadfishException:
70-
pass
60+
61+
if use_cli_secrets:
62+
# Legacy behavior: Pass secrets via CLI args to test warning logic
63+
if mock_user is not None:
64+
argv.extend(("-u", mock_user))
65+
if mock_pass is not None:
66+
argv.extend(("-p", mock_pass))
67+
argv.extend(self.args)
68+
else:
69+
# Default behavior for tests: Use Env Vars to suppress warnings
70+
if mock_user is not None:
71+
env_vars["BADFISH_USERNAME"] = mock_user
72+
if mock_pass is not None:
73+
env_vars["BADFISH_PASSWORD"] = mock_pass
74+
argv.extend(self.args)
75+
76+
with patch.dict(os.environ, env_vars):
77+
try:
78+
main(argv)
79+
except BadfishException:
80+
pass
81+
7182
out, err = self._capsys.readouterr()
7283
return out, err
7384

tests/test_execution.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
RESPONSE_INIT_SYSTEMS_RESOURCE_NOT_FOUND,
1414
ROOT_RESP,
1515
SUCCESSFUL_HOST_LIST,
16+
SYS_RESP,
1617
WRONG_BADFISH_EXECUTION,
1718
WRONG_BADFISH_EXECUTION_HOST_LIST,
19+
MANAGER_INSTANCE_RESP,
20+
JOBS_RESP,
1821
)
1922
from tests.test_base import TestBase
2023

@@ -82,6 +85,21 @@ def test_host_list_extras(self, mock_get, mock_post, mock_delete):
8285
class TestInitialization(TestBase):
8386
args = ["--ls-jobs"]
8487

88+
@patch("aiohttp.ClientSession.delete")
89+
@patch("aiohttp.ClientSession.post")
90+
@patch("aiohttp.ClientSession.get")
91+
def test_cli_secrets_warning(self, mock_get, mock_post, mock_delete):
92+
"""Test that passing credentials via CLI triggers a warning."""
93+
responses = [ROOT_RESP] * 4 + [SYS_RESP, MAN_RESP, MANAGER_INSTANCE_RESP, JOBS_RESP]
94+
self.set_mock_response(mock_get, 200, responses)
95+
self.set_mock_response(mock_post, 200, "OK")
96+
self.set_mock_response(mock_delete, 200, "OK")
97+
98+
# Explicitly use CLI secrets to trigger the warning
99+
_, err = self.badfish_call(use_cli_secrets=True)
100+
101+
assert "Passing secrets via command line arguments can be unsafe" in err
102+
85103
@patch("aiohttp.ClientSession.delete")
86104
@patch("aiohttp.ClientSession.post")
87105
@patch("aiohttp.ClientSession.get")

tests/test_hosts_file.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ def test_hosts_good(self):
2424
for call in badfish_mock.await_args_list:
2525
_host, _args, _logger, _fh = call[0]
2626
assert _host == config.MOCK_HOST
27-
2827
assert _args["host_list"] == self.mock_hosts_good_path
29-
assert _args["u"] == config.MOCK_USER
30-
assert _args["p"] == config.MOCK_PASS
3128

3229
def test_hosts_non_existent(self):
3330
self.args = [self.option_arg, "non/existent/file"]
@@ -61,7 +58,4 @@ def test_hosts_bad(self):
6158
for call in badfish_mock.await_args_list:
6259
_host, _args, _logger, _fh = call[0]
6360
assert _host == config.MOCK_HOST
64-
6561
assert _args["host_list"] == self.mock_hosts_garbled_path
66-
assert _args["u"] == config.MOCK_USER
67-
assert _args["p"] == config.MOCK_PASS

0 commit comments

Comments
 (0)