diff --git a/README.md b/README.md index 6d84005..86afc19 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ * [Via Virtualenv](#via-virtualenv) * [Via RPM System Package](#via-rpm-system-package) * [Common Operations](#common-operations) + * [Use Environment Variables for Secrets](#using-environment-variables-for-secrets) * [Enforcing an OpenStack Director-style interface order](#enforcing-an-openstack-director-style-interface-order) * [Enforcing a Foreman-style interface order](#enforcing-a-foreman-style-interface-order) * [Enforcing a Custom interface order](#enforcing-a-custom-interface-order) @@ -197,8 +198,19 @@ Badfish happily runs in a container image using Podman or Docker (likely, but no podman pull quay.io/quads/badfish ``` ```bash -podman run -it --rm --dns $DNS_IP quay.io/quads/badfish -H $HOST -u $USER -p $PASS --reboot-only +export BADFISH_USERNAME="my_username" +export BADFISH_PASSWORD="my_password" +podman run -it --rm --dns 1.2.3.4 quay.io/quads/badfish -H mgmt-host01.example.com --reboot-only ``` + +If you want to pass your user/pass variables only in podman: + +```bash +BADFISH_USERNAME=my_username BADFISH_PASSWORD=my_password \ + podman run -it --env BADFISH_USERNAME --env BADFISH_PASSWORD --rm --dns 1.2.3.4 \ + quay.io/quads/badfish -H mgmt-host01.example.com --reboot-only +``` + > [!IMPORTANT] > > If you are running Badfish against a host inside a VPN to an address without public resolution you must specify your VPN DNS server ip address with `--dns` @@ -216,15 +228,18 @@ podman run -it --rm --dns $DNS_IP quay.io/quads/badfish -H $HOST -u $USER -p $PA While we strongly recommend using the [podman](#via-podman) method of calling Badfish inside a virtual environment you can still do it directly from the repository via virtualenv but you would need to prepend the call to Badfish with the setting of the `PYTHONPATH` environment variable pointing at the path of your Badfish repository. -``` +```bash virtualenv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txt -PYTHONPATH={BADFISH_REPO_PATH} python3 src/badfish/main.py -h ``` -We will likely add more libaries in the future and [can't guarantee](https://github.com/redhat-performance/JetSki/issues/186#issuecomment-982666646) these will be visible within your virtualenv without more symlinks or workarounds. +* You'll need to set `PYTHONPATH` to the BADFISHREPO/src to run this way: + +```bash +PYTHONPATH="./src" python3 src/badfish/main.py -h +``` ### Via RPM System Package If you choose to install Badfish via RPM package then it'll be located in `/usr/bin/badfish` and you don't need to do much else beyond know the correct command syntax for your required operations. @@ -242,17 +257,25 @@ curl https://raw.githubusercontent.com/redhat-performance/badfish/master/config ## Common Operations +### Use Environment Variables for Secrets +> [!IMPORTANT] +> While you can pass `-u` and `-p` for user and password env variables are more secure. +> +> `export BADFISH_USERNAME="my_username"` +> +> `export BADFISH_PASSWORD="my_password"` + ### Enforcing an OpenStack Director-style interface order In our performance/scale R&D environments TripleO-based OpenStack deployments require a specific 10/25/40GbE NIC to be the primary boot device for PXE, followed by disk, and then followed by the rest of the interfaces. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass -i config/idrac_interfaces.yml -t director +badfish -H mgmt-your-server.example.com -i config/idrac_interfaces.yml -t director ``` ### Enforcing a Foreman-style interface order Foreman and Red Hat Satellite (as of 6.x based on Foreman) require managed systems to first always PXE from the interface that is Foreman-managed (DHCP/PXE). If the system is not set to build it will simply boot to local disk. In our setup we utilize a specific NIC for this interface based on system type. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass -i config/idrac_interfaces.yml -t foreman +badfish -H mgmt-your-server.example.com -i config/idrac_interfaces.yml -t foreman ``` ### Enforcing a Custom interface order @@ -293,19 +316,19 @@ src/main.py --host-list /tmp/hosts -u root -p password -i config/idrac_interface ### Forcing a one time boot to a specific device To force systems to perform a one-time boot to a specific device on the next subsequent reboot you can use the ```--boot-to``` option and pass as an argument the device you want the one-time boot to be set to. This will change the one time boot BIOS attributes OneTimeBootMode and OneTimeBootSeqDev and on the next reboot it will attempt to PXE boot or boot from that interface string. You can obtain the device list via the `--check-boot` directive below. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --boot-to NIC.Integrated.1-3-1 +badfish -H mgmt-your-server.example.com --boot-to NIC.Integrated.1-3-1 ``` ### Forcing a one time boot to a specific mac address To force systems to perform a one-time boot to a specific mac address on the next subsequent reboot you can use the ```--boot-to-mac``` option and pass as an argument the device mac address for a specific NIC that you want the one-time boot to be set to. This will change the one time boot BIOS attributes OneTimeBootMode and OneTimeBootSeqDev and on the next reboot it will attempt to PXE boot or boot from that interface. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --boot-to-mac A9:BB:4B:50:CA:54 +badfish -H mgmt-your-server.example.com --boot-to-mac A9:BB:4B:50:CA:54 ``` ### Forcing a one time boot to a specific type To force systems to perform a one-time boot to a specific type on the next subsequent reboot you can use the ```--boot-to-type``` option and pass as an argument the device type, as defined on the iDRAC interfaces yaml, that you want the one-time boot to be set to. For this action you must also include the path to your interfaces yaml. This will change the one time boot BIOS attributes OneTimeBootMode and OneTimeBootSeqDev and on the next reboot it will attempt to PXE boot or boot from the first interface defined for that host type on the interfaces yaml file. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass -i config/idrac_interfaces.yml --boot-to-type foreman +badfish -H mgmt-your-server.example.com -i config/idrac_interfaces.yml --boot-to-type foreman ``` **Note** `--boot-to`, `--boot-to-type`, and `--boot-to-mac` require you to manually perform a reboot action, these simply just batch what the system will boot to on the next boot. For this you can use either `--power-cycle` or `--reboot-only`. @@ -315,31 +338,31 @@ To force systems to perform a one-time boot to PXE, simply pass the `--pxe` flag For Dell systems please use either `--boot-to`, `--boot-to-mac` or `--boot-to-type` for temporary PXE to a specific interface or change the boot order permanently to achieve your desired effect. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass -i config/idrac_interfaces.yml --pxe +badfish -H mgmt-your-server.example.com -i config/idrac_interfaces.yml --pxe ``` ### Rebooting a system In certain cases you might need to only reboot the host, for this case we included the ```--reboot-only``` flag which will force a GracefulRestart on the target host. Note that this option is not to be used with any other option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --reboot-only +badfish -H mgmt-your-server.example.com --reboot-only ``` ### Power cycling a system For a hard reset you can use ```--power-cycle``` flag which will run a ForceOff instruction on the target host. Note that this option is not to be used with any other option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --power-cycle +badfish -H mgmt-your-server.example.com --power-cycle ``` ### Power State Control You can also turn a server on or off by using options `--power-on` and `--power-off` respectively. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --power-on +badfish -H mgmt-your-server.example.com --power-on ``` ### Check Power State For checking the current power state of a server you can run badfish with the `--power-state` option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --power-state +badfish -H mgmt-your-server.example.com --power-state ``` Partial Output: ``` @@ -359,7 +382,7 @@ Partial Output: ### Resetting iDRAC For the replacement of `racadm racreset`, the optional argument `--racreset` was added. When this argument is passed to ```badfish```, a graceful restart is triggered on the iDRAC itself. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --racreset +badfish -H mgmt-your-server.example.com --racreset ``` > [!NOTE] > Dell specific command, for Supermicro servers there is an equivalent of `--bmc-reset` @@ -367,7 +390,7 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --racreset ### Resetting BMC For the replacement of `ipmitool bmc reset` or `ipmiutil reset`, the optional argument `--bmc-reset` was added. When this argument is passed to ```badfish```, a graceful restart is triggered on the BMC itself. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --bmc-reset +badfish -H mgmt-your-server.example.com --bmc-reset ``` > [!NOTE] > Supermicro specific command, for Dell servers there is an equivalent of `--racreset` @@ -378,127 +401,127 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --bmc-reset You can restore BIOS default settings by calling Badfish with the option `--factory-reset`. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --factory-reset +badfish -H mgmt-your-server.example.com --factory-reset ``` ### Check current boot order To check the current boot order of a specific host you can use the ```--check-boot``` option which will return an ordered list of boot devices. Additionally you can pass the ```-i``` option which will in turn print on screen what type of host does the current boot order match as those defined on the iDRAC interfaces yaml. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass -i config/idrac_interfaces.yml --check-boot +badfish -H mgmt-your-server.example.com -i config/idrac_interfaces.yml --check-boot ``` ### Toggle boot device If you would like to enable or disable a boot device you can use ```--toggle-boot-device``` argument which takes the device name as input and will toggle the `Enabled` state from True to False and vice versa. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --toggle-boot-device NIC.Integrated.1-3-1``` +badfish -H mgmt-your-server.example.com --toggle-boot-device NIC.Integrated.1-3-1``` ``` ### Variable number of retries At certain points during the execution of ```badfish``` the program might come across a non responsive resources and will automatically retry to establish connection. We have included a default value of 15 retries after failed attempts but this can be customized via the ```--retries``` optional argument which takes as input an integer with the number of desired retries. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass -i config/idrac_interfaces.yml -t foreman --retries 20 +badfish -H mgmt-your-server.example.com -i config/idrac_interfaces.yml -t foreman --retries 20 ``` ### Firmware inventory If you would like to get a detailed list of all the devices supported by iDRAC you can run ```badfish``` with the ```--firware-inventory``` option which will return a list of devices with additional device info. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --firmware-inventory +badfish -H mgmt-your-server.example.com --firmware-inventory ``` ### Delta of firmware inventories If you would like to get a delta between firmware inventories of two servers, you can do so with the `--delta` option. This option takes a second host address as its argument. Only the firmware that's on both servers and has different versions will get displayed. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --firmware-inventory --delta mgmt-your-other-server.example.com +badfish -H mgmt-your-server.example.com --firmware-inventory --delta mgmt-your-other-server.example.com ``` ### Clear Job Queue If you would like to clear all the jobs that are queued on the remote iDRAC you can run ```badfish``` with the ```--clear-jobs``` option which query for all active jobs in the iDRAC queue and will post a request to clear the queue. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --clear-jobs +badfish -H mgmt-your-server.example.com --clear-jobs ``` You can also force the clearing of Dell iDRAC job queues by passing the `--force` option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --clear-jobs --force +badfish -H mgmt-your-server.example.com --clear-jobs --force ``` ### List Job Queue If you would like to list all active jobs that are queued on the remote iDRAC you can run ```badfish``` with the ```--ls-jobs``` option which query for all active jobs in the iDRAC queue and will return a list with all active items. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --ls-jobs +badfish -H mgmt-your-server.example.com --ls-jobs ``` ### Check Job Status If you would like to the status of an existing LifeCycle controller job you can run ```badfish``` with the ```--check-job``` option and passing the job id which can be obtained via ```--ls-jobs```. This will return a detail of the specific job with status and percentage of completion. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --check-job JID_340568202796 +badfish -H mgmt-your-server.example.com --check-job JID_340568202796 ``` ### Set Bios Password If you would like to set the bios password you can run ```badfish``` with the ```--set-bios-password``` option and passing the new password with ```--new-password```. If a password is already set you must pass this with ```--old-password``` otherwise optional. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --set-bios-password --new-password new_pass --old-password old_pass +badfish -H mgmt-your-server.example.com -u root --set-bios-password --new-password new_pass --old-password old_pass ``` ### Remove Bios Password If you would like to remove the bios password you can run ```badfish``` with the ```--remove-bios-password``` option and passing the existing password with ```--old-password```. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --remove-bios-password --old-password old_pass +badfish -H mgmt-your-server.example.com -u root --remove-bios-password --old-password old_pass ``` ### List Network Interfaces For getting a list of network interfaces with individual metadata for each you can run ```badfish``` with the ```--ls-interfaces``` option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --ls-interfaces +badfish -H mgmt-your-server.example.com --ls-interfaces ``` ### List Memory For getting a detailed list of memory devices you can run ```badfish``` with the ```--ls-memory``` option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --ls-memory +badfish -H mgmt-your-server.example.com --ls-memory ``` ### List Processors For getting a detailed list of processors you can run ```badfish``` with the ```--ls-processors``` option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --ls-processors +badfish -H mgmt-your-server.example.com --ls-processors ``` ### List Serial Number or Service Tag For getting the system's serial number or on Dell servers the service tag (equivalent to `racadm getsvctag`) you can run ```badfish``` with the ```--ls-serial``` option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --ls-serial +badfish -H mgmt-your-server.example.com --ls-serial ``` ### Check Virtual Media If you would like to check for any active virtual media you can run ```badfish``` with the ```--check-virtual-media``` option which query for all active virtual devices. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --check-virtual-media +badfish -H mgmt-your-server.example.com --check-virtual-media ``` ### Mount Virtual Media If you would like to mount an ISO from network you can run ```badfish``` with the ```--mount-virtual-media``` option which post a request for mounting the ISO virtual media (Virtual CD). Full address to the ISO is needed as an argument. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --mount-virtual-media http://storage.example.com/folder/linux.iso +badfish -H mgmt-your-server.example.com --mount-virtual-media http://storage.example.com/folder/linux.iso ``` ### Unmount Virtual Media If you would like to unmount all active virtual media you can run ```badfish``` with the ```--unmount-virtual-media``` option which post a request for unmounting all active virtual devices. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --unmount-virtual-media +badfish -H mgmt-your-server.example.com --unmount-virtual-media ``` ### Boot to Virtual Media If you would like to boot to virtual media (Virtual CD) you can run ```badfish``` with the ```--boot-to-virtual-media``` option which sets the onetime next boot device to virtual CD. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --boot-to-virtual-media +badfish -H mgmt-your-server.example.com --boot-to-virtual-media ``` ### Check Remote Image If you would like to check the attach status of a remote ISO in DellOSDeployment service you can run ```badfish``` with the ```--check-remote-image``` option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --check-remote-image +badfish -H mgmt-your-server.example.com --check-remote-image ``` > [!NOTE] > This is only supported on DELL devices. @@ -506,7 +529,7 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --check-remote-image ### Boot to Remote Image If you would like to boot to a remote ISO on NFS with DellOSDeployment service you can run ```badfish``` with the ```--boot-remote-image``` option which will attach the image and reboot the server to it. Expects the NFS path to the ISO as the argument. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --boot-remote-image nfs-storage.example.com:/mnt/folder/linux.iso +badfish -H mgmt-your-server.example.com --boot-remote-image nfs-storage.example.com:/mnt/folder/linux.iso ``` > [!NOTE] > This is only supported on DELL devices. @@ -514,7 +537,7 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --boot-remote-image ### Detach Remote Image If you would like to detach an ISO from DellOSDeployment service you can run ```badfish``` with the ```--detach-remote-image``` option. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --detach-remote-image +badfish -H mgmt-your-server.example.com --detach-remote-image ``` > [!NOTE] > This is only supported on DELL devices. @@ -522,7 +545,7 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --detach-remote-imag ### Get SRIOV mode For checking if the global SRIOV mode is enabled you can use ```--get-sriov``` ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --get-sriov +badfish -H mgmt-your-server.example.com --get-sriov ``` > [!NOTE] > This is only supported on DELL devices. @@ -531,11 +554,11 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --get-sriov For changing the mode of the SRIOV glabal BIOS attribute, we have included 2 new arguments. In case the setting is in disabled mode, you can enable it by passing ```--enable-sriov``` ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --enable-sriov +badfish -H mgmt-your-server.example.com --enable-sriov ``` On the contrary, if you would like to disable the SRIOV mode, you can now pass ```--disable-sriov``` ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --disable-sriov +badfish -H mgmt-your-server.example.com --disable-sriov ``` > [!NOTE] > This is only supported on DELL devices. @@ -543,13 +566,13 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --disable-sriov ### Get FQDDs for all nics To get a list of all FQDDs for all NICs on the server you can run badfish with ```--get-nic-fqdds```. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --get-nic-fqdds +badfish -H mgmt-your-server.example.com --get-nic-fqdds ``` ### Get NIC attributes To get a list of all NIC attributes we can potentially modify (some might be set as read-only), you can run badfish with ```--get-nic-attribute``` passing the desired FQDD and this will return a list off all NIC attributes with their current value set. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --get-nic-attribute NIC.Integrated.1-1-1 +badfish -H mgmt-your-server.example.com --get-nic-attribute NIC.Integrated.1-1-1 ``` ### Set NIC attribute @@ -558,25 +581,25 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --get-nic-attribute To change the value of a NIC attribute you can use ```--set-nic-attribute``` with the desired FQDD, passing both ```--attribute``` and desired ```--value```. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --set-nic-attribute NIC.Integrated.1-1-1 --attribute LegacyBootProto --value PXE +badfish -H mgmt-your-server.example.com --set-nic-attribute NIC.Integrated.1-1-1 --attribute LegacyBootProto --value PXE ``` ### Get BIOS attributes To get a list of all BIOS attributes we can potentially modify (some might be set as read-only), you can run badfish with ```--get-bios-attribute``` alone and this will return a list off all BIOS attributes with their current value set. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --get-bios-attribute +badfish -H mgmt-your-server.example.com --get-bios-attribute ``` ### Get specific BIOS attribute In case you would like to get a more detailed view on the parameters for a BIOS attribute you can run ```--get-bios-attribute``` including the specific name of the attribute via ```--attribute```. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --get-bios-attribute --attribute ProcC1E +badfish -H mgmt-your-server.example.com --get-bios-attribute --attribute ProcC1E ``` ### Set BIOS attribute To change the value of a bios attribute you can use ```--set-bios-attribute``` passing both ```--attribute``` and ```--value```. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --set-bios-attribute --attribute ProcC1E --value Enabled +badfish -H mgmt-your-server.example.com --set-bios-attribute --attribute ProcC1E --value Enabled ``` > [!NOTE] > You can get the list of allowed values you can pass for that attribute by looking at the attribute details via ```--get-bios-attribute``` for that specific one. @@ -590,15 +613,15 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --set-bios-attribute #### Querying bootmode * First determine what bootmode state your server is using before proceeding. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --get-bios-attribute --attribute BootMode +badfish -H mgmt-your-server.example.com --get-bios-attribute --attribute BootMode ``` #### Setting UEFI mode ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --set-bios-attribute --attribute BootMode --value Uefi +badfish -H mgmt-your-server.example.com --set-bios-attribute --attribute BootMode --value Uefi ``` ### Setting BIOS mode ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --set-bios-attribute --attribute BootMode --value Bios +badfish -H mgmt-your-server.example.com --set-bios-attribute --attribute BootMode --value Bios ``` > [!IMPORTANT] @@ -608,13 +631,13 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --set-bios-attribute ### Get server screenshot If you would like to get a screenshot with the current state of the server you can now run badfish with ```--screenshot``` which will capture this and store it in the current directory in png format. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --screenshot +badfish -H mgmt-your-server.example.com --screenshot ``` ### Targets for server configuration profile If you want to get a list of allowed targets for SCP export or import you can get that with the `--get-scp-targets` command, takes either `Export` or `Import` as an argument. ``` -badfish -H mgmt-your-server.example.com -u root -p yourpass --get-scp-targets (Export | Import) +badfish -H mgmt-your-server.example.com --get-scp-targets (Export | Import) ``` > [!NOTE] > This is only supported on Dell devices. @@ -623,7 +646,7 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --get-scp-targets (E ### Export server configuration profile If you would like to export a SCP as a JSON file for either some specific targets or all of them, you can run badfish with `--export-scp` and specify a path where the config should be saved to with its argument. Targets can be specified with `--scp-targets` flag that takes a comma separated list of targets as an argument. Read only arguments can be included with the `--scp-include-read-only` flag. ``` -badfish -H mgmt-your-server.example.com -u root -p yourpass --export-scp "./" --scp-targets IDRAC,BIOS --scp-include-read-only +badfish -H mgmt-your-server.example.com --export-scp "./" --scp-targets IDRAC,BIOS --scp-include-read-only ``` > [!NOTE] > This is only supported on Dell devices. @@ -631,7 +654,7 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --export-scp "./" -- ### Import server configuration profile If you would like to import a SCP in a JSON file for either some specific targets or all of them, you can run badfish with `--import-scp`. Targets can be specified with `--scp-targets` flag that takes a comma separated list of targets as an argument. Command will reboot the server and return it to a state at the launch start of import. ``` -badfish -H mgmt-your-server.example.com -u root -p yourpass --import-scp "./example_export.json" --scp-targets IDRAC,BIOS +badfish -H mgmt-your-server.example.com --import-scp "./example_export.json" --scp-targets IDRAC,BIOS ``` > [!NOTE] > This is only supported on Dell devices. @@ -639,19 +662,19 @@ badfish -H mgmt-your-server.example.com -u root -p yourpass --import-scp "./exam ### Bulk actions via text file with list of hosts In the case you would like to execute a common badfish action on a list of hosts, you can pass the optional argument ```--host-list``` in place of ```-H``` with the path to a text file with the hosts you would like to action upon and any addtional arguments defining a common action for all these hosts. ```bash -badfish --host-list /tmp/bad-hosts -u root -p yourpass --clear-jobs +badfish --host-list /tmp/bad-hosts --clear-jobs ``` ### Verbose output If you would like to see a more detailed output on console you can use the ```--verbose``` option and get a additional debug logs. > [!NOTE] this is the default log level for the ```--log``` argument. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass -i config/idrac_interfaces.yml -t foreman --verbose +badfish -H mgmt-your-server.example.com -i config/idrac_interfaces.yml -t foreman --verbose ``` ### Log to file If you would like to log the output of ```badfish``` you can use the ```--log``` option and pass the path to where you want ```badfish``` to log it's output to. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass -i config/idrac_interfaces.yml -t foreman --log /tmp/bad.log +badfish -H mgmt-your-server.example.com -i config/idrac_interfaces.yml -t foreman --log /tmp/bad.log ``` ### Formatted output @@ -663,7 +686,7 @@ If you would like to easier query some information listed by badfish, you can te - `--check-virtual-media` - `--power-state`. ```bash -badfish -H mgmt-your-server.example.com -u root -p yourpass --output json/yaml --firmware-inventory +badfish -H mgmt-your-server.example.com --output json/yaml --firmware-inventory ``` ## iDRAC and Data Format diff --git a/setup.py b/setup.py index 446deea..3f4b6da 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,4 @@ else: raise RuntimeError("Unable to find version string in src/badfish/__init__.py") -setuptools.setup( - version=current_version -) +setuptools.setup(version=current_version) diff --git a/src/badfish/helpers/parser.py b/src/badfish/helpers/parser.py index 9629729..f267b8a 100644 --- a/src/badfish/helpers/parser.py +++ b/src/badfish/helpers/parser.py @@ -10,8 +10,8 @@ def create_parser(): allow_abbrev=False, ) parser.add_argument("-H", "--host", help="iDRAC host address") - parser.add_argument("-u", help="iDRAC username", required=True) - parser.add_argument("-p", help="iDRAC password", required=True) + parser.add_argument("-u", help="iDRAC username") + parser.add_argument("-p", help="iDRAC password") parser.add_argument("-i", help="Path to iDRAC interfaces yaml", default=None) parser.add_argument("-t", help="Type of host as defined on iDRAC interfaces yaml") parser.add_argument("-l", "--log", help="Optional argument for logging results to a file") diff --git a/src/badfish/main.py b/src/badfish/main.py index 2b129ca..37b57be 100755 --- a/src/badfish/main.py +++ b/src/badfish/main.py @@ -382,9 +382,15 @@ async def find_session_uri(self): if not response: raise BadfishException(f"Failed to communicate with {self.host}") + if response.status == 401: + raise BadfishException(f"Failed to authenticate. Verify your credentials for {self.host}") + raw = await response.text("utf-8", "ignore") data = json.loads(raw.strip()) - redfish_version = int(data["RedfishVersion"].replace(".", "")) + try: + redfish_version = int(data["RedfishVersion"].replace(".", "")) + except KeyError: + raise BadfishException("Was unable to get Redfish Version. Please verify credentials/host.") session_uri = None if redfish_version >= 160: session_uri = "/redfish/v1/SessionService/Sessions" @@ -2405,7 +2411,7 @@ async def set_nic_attribute(self, fqdd, attribute, value): ) self.logger.debug(uri) except (IndexError, ValueError): - self.logger.error("Invalid FQDD suplied.") + self.logger.error("Invalid FQDD supplied.") return False headers = {"content-type": "application/json"} @@ -2450,8 +2456,19 @@ async def set_nic_attribute(self, fqdd, attribute, value): async def execute_badfish(_host, _args, logger, format_handler=None): - _username = _args["u"] - _password = _args["p"] + _username = _args.get("u") or os.environ.get("BADFISH_USERNAME") + _password = _args.get("p") or os.environ.get("BADFISH_PASSWORD") + + if _args.get("p"): + logger.warning( + "Passing secrets via command line arguments can be unsafe. " + "Consider using environment variables (BADFISH_USERNAME, BADFISH_PASSWORD)." + ) + + if not _username or not _password: + logger.error("Missing credentials. Please provide credentials via CLI arguments or environment variables.") + return _host, False + host_type = _args["t"] interfaces_path = _args["i"] force = _args["force"] diff --git a/tests/config.py b/tests/config.py index 155dfcb..d803a92 100644 --- a/tests/config.py +++ b/tests/config.py @@ -924,8 +924,7 @@ 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. @@ -933,9 +932,7 @@ def render_device_dict(index, device): - 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` """ @@ -1010,13 +1007,10 @@ 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. @@ -1039,13 +1033,10 @@ 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" @@ -2347,3 +2338,5 @@ def render_device_dict(index, device): {RESPONSE_UNSOPPORTED_IDRAC_VERSION} {RESPONSE_NIC_ATTR_GET_ERROR} """ +MANAGER_INSTANCE_RESP = '{"Jobs":{"@odata.id":"/redfish/v1/Managers/iDRAC.Embedded.1/Jobs"}}' +JOBS_RESP = '{"Members":[]}' diff --git a/tests/test_async_loop.py b/tests/test_async_loop.py index 016bd63..07fa3b7 100644 --- a/tests/test_async_loop.py +++ b/tests/test_async_loop.py @@ -4,15 +4,15 @@ class TestAsyncioFix(unittest.TestCase): - @patch('badfish.main.execute_badfish') - @patch('badfish.main.BadfishLogger') - @patch('badfish.main.parse_arguments') - @patch('asyncio.set_event_loop') - @patch('asyncio.new_event_loop') - @patch('asyncio.get_event_loop') - def test_main_handles_no_event_loop(self, mock_get_loop, mock_new_loop, - mock_set_loop, mock_parse_args, - mock_logger, mock_execute): + @patch("badfish.main.execute_badfish") + @patch("badfish.main.BadfishLogger") + @patch("badfish.main.parse_arguments") + @patch("asyncio.set_event_loop") + @patch("asyncio.new_event_loop") + @patch("asyncio.get_event_loop") + def test_main_handles_no_event_loop( + self, mock_get_loop, mock_new_loop, mock_set_loop, mock_parse_args, mock_logger, mock_execute + ): mock_get_loop.side_effect = RuntimeError("No event loop") mock_loop_instance = MagicMock() @@ -20,9 +20,13 @@ def test_main_handles_no_event_loop(self, mock_get_loop, mock_new_loop, mock_loop_instance.run_until_complete.return_value = ("localhost", True) mock_parse_args.return_value = { - "verbose": False, "host": "localhost", "delta": None, - "firmware_inventory": None, "host_list": None, "log": None, - "output": None + "verbose": False, + "host": "localhost", + "delta": None, + "firmware_inventory": None, + "host_list": None, + "log": None, + "output": None, } main() @@ -32,24 +36,28 @@ def test_main_handles_no_event_loop(self, mock_get_loop, mock_new_loop, mock_set_loop.assert_called_once_with(mock_loop_instance) mock_loop_instance.run_until_complete.assert_called() - @patch('badfish.main.execute_badfish') - @patch('badfish.main.BadfishLogger') - @patch('badfish.main.parse_arguments') - @patch('asyncio.set_event_loop') - @patch('asyncio.new_event_loop') - @patch('asyncio.get_event_loop') - def test_main_uses_existing_loop(self, mock_get_loop, mock_new_loop, - mock_set_loop, mock_parse_args, - mock_logger, mock_execute): + @patch("badfish.main.execute_badfish") + @patch("badfish.main.BadfishLogger") + @patch("badfish.main.parse_arguments") + @patch("asyncio.set_event_loop") + @patch("asyncio.new_event_loop") + @patch("asyncio.get_event_loop") + def test_main_uses_existing_loop( + self, mock_get_loop, mock_new_loop, mock_set_loop, mock_parse_args, mock_logger, mock_execute + ): existing_loop = MagicMock() mock_get_loop.return_value = existing_loop mock_get_loop.side_effect = None existing_loop.run_until_complete.return_value = ("localhost", True) mock_parse_args.return_value = { - "verbose": False, "host": "localhost", "delta": None, - "firmware_inventory": None, "host_list": None, "log": None, - "output": None + "verbose": False, + "host": "localhost", + "delta": None, + "firmware_inventory": None, + "host_list": None, + "log": None, + "output": None, } main() diff --git a/tests/test_base.py b/tests/test_base.py index 3417052..ebf7875 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,4 +1,5 @@ import sys +import os from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest @@ -49,25 +50,35 @@ def capture_wrap(self): yield def badfish_call( - self, - mock_host=config.MOCK_HOST, - mock_user=config.MOCK_USER, - mock_pass=config.MOCK_PASS, + self, mock_host=config.MOCK_HOST, mock_user=config.MOCK_USER, mock_pass=config.MOCK_PASS, use_cli_secrets=False ): argv = [] + env_vars = os.environ.copy() if mock_host is not None: argv.extend(("-H", mock_host)) - if mock_user is not None: - argv.extend(("-u", mock_user)) - if mock_pass is not None: - argv.extend(("-p", mock_pass)) - - argv.extend(self.args) - try: - main(argv) - except BadfishException: - pass + + if use_cli_secrets: + # Legacy behavior: Pass secrets via CLI args to test warning logic + if mock_user is not None: + argv.extend(("-u", mock_user)) + if mock_pass is not None: + argv.extend(("-p", mock_pass)) + argv.extend(self.args) + else: + # Default behavior for tests: Use Env Vars to suppress warnings + if mock_user is not None: + env_vars["BADFISH_USERNAME"] = mock_user + if mock_pass is not None: + env_vars["BADFISH_PASSWORD"] = mock_pass + argv.extend(self.args) + + with patch.dict(os.environ, env_vars): + try: + main(argv) + except BadfishException: + pass + out, err = self._capsys.readouterr() return out, err diff --git a/tests/test_execution.py b/tests/test_execution.py index cb63813..9c3a0b2 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -13,8 +13,11 @@ RESPONSE_INIT_SYSTEMS_RESOURCE_NOT_FOUND, ROOT_RESP, SUCCESSFUL_HOST_LIST, + SYS_RESP, WRONG_BADFISH_EXECUTION, WRONG_BADFISH_EXECUTION_HOST_LIST, + MANAGER_INSTANCE_RESP, + JOBS_RESP, ) from tests.test_base import TestBase @@ -82,6 +85,21 @@ def test_host_list_extras(self, mock_get, mock_post, mock_delete): class TestInitialization(TestBase): args = ["--ls-jobs"] + @patch("aiohttp.ClientSession.delete") + @patch("aiohttp.ClientSession.post") + @patch("aiohttp.ClientSession.get") + def test_cli_secrets_warning(self, mock_get, mock_post, mock_delete): + """Test that passing credentials via CLI triggers a warning.""" + responses = [ROOT_RESP] * 4 + [SYS_RESP, MAN_RESP, MANAGER_INSTANCE_RESP, JOBS_RESP] + self.set_mock_response(mock_get, 200, responses) + self.set_mock_response(mock_post, 200, "OK") + self.set_mock_response(mock_delete, 200, "OK") + + # Explicitly use CLI secrets to trigger the warning + _, err = self.badfish_call(use_cli_secrets=True) + + assert "Passing secrets via command line arguments can be unsafe" in err + @patch("aiohttp.ClientSession.delete") @patch("aiohttp.ClientSession.post") @patch("aiohttp.ClientSession.get") diff --git a/tests/test_hosts_file.py b/tests/test_hosts_file.py index 551646a..5453a62 100644 --- a/tests/test_hosts_file.py +++ b/tests/test_hosts_file.py @@ -24,10 +24,7 @@ def test_hosts_good(self): for call in badfish_mock.await_args_list: _host, _args, _logger, _fh = call[0] assert _host == config.MOCK_HOST - assert _args["host_list"] == self.mock_hosts_good_path - assert _args["u"] == config.MOCK_USER - assert _args["p"] == config.MOCK_PASS def test_hosts_non_existent(self): self.args = [self.option_arg, "non/existent/file"] @@ -61,7 +58,4 @@ def test_hosts_bad(self): for call in badfish_mock.await_args_list: _host, _args, _logger, _fh = call[0] assert _host == config.MOCK_HOST - assert _args["host_list"] == self.mock_hosts_garbled_path - assert _args["u"] == config.MOCK_USER - assert _args["p"] == config.MOCK_PASS diff --git a/tests/test_main_coverage.py b/tests/test_main_coverage.py index e69de29..83f3960 100644 --- a/tests/test_main_coverage.py +++ b/tests/test_main_coverage.py @@ -0,0 +1,174 @@ +import pytest +from unittest.mock import patch, MagicMock, AsyncMock +import logging +from collections import defaultdict +import json + +# CORRECT IMPORT: Must match the application's runtime context (PYTHONPATH=src) +# Using 'src.badfish' here would cause isinstance checks to fail. +from badfish.main import execute_badfish, Badfish +from badfish.helpers.exceptions import BadfishException + + +@pytest.fixture +def mock_args(): + """Returns a dictionary that returns None for missing keys.""" + return defaultdict(lambda: None) + + +@pytest.mark.asyncio +async def test_missing_credentials(): + host = "test_host" + args = {} + logger = MagicMock(spec=logging.Logger) + format_handler = None + + with patch("os.environ.get", side_effect=lambda k: None): + result = await execute_badfish(host, args, logger, format_handler) + + assert result == (host, False) + logger.error.assert_called_once_with( + "Missing credentials. Please provide credentials via CLI arguments " + "or environment variables." + ) + + +@pytest.mark.asyncio +async def test_init_401_unauthorized(mock_args): + """Test that a 401 response raises a clean BadfishException.""" + host = "test_host" + mock_args.update({"u": "user", "p": "pass", "retries": 1}) + logger = MagicMock(spec=logging.Logger) + + # Mock response for HTTPClient.get_request + mock_response = MagicMock() + mock_response.status = 401 + 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: + mock_get.return_value = mock_response + + result = await execute_badfish(host, mock_args, logger, None) + + assert result == (host, False) + + # Verify logger was called with a BadfishException containing expected msg + args, _ = logger.error.call_args + exception_obj = args[0] + + assert isinstance(exception_obj, BadfishException) + assert ( + str(exception_obj) + == f"Failed to authenticate. Verify your credentials for {host}" + ) + + +@pytest.mark.asyncio +async def test_init_key_error_missing_version(mock_args): + """Test that missing RedfishVersion key raises a clean BadfishException.""" + host = "test_host" + mock_args.update({"u": "user", "p": "pass", "retries": 1}) + logger = MagicMock(spec=logging.Logger) + + # Mock response for HTTPClient.get_request (Success 200 but bad payload) + mock_response = MagicMock() + mock_response.status = 200 + # Payload missing "RedfishVersion" + 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: + mock_get.return_value = mock_response + + result = await execute_badfish(host, mock_args, logger, None) + + assert result == (host, False) + + # Verify logger was called with a BadfishException containing expected msg + args, _ = logger.error.call_args + exception_obj = args[0] + + assert isinstance(exception_obj, BadfishException) + assert ( + str(exception_obj) + == "Was unable to get Redfish Version. Please verify credentials/host." + ) + + +@pytest.mark.asyncio +async def test_init_no_response_from_host(mock_args): + """Test that no response from host raises BadfishException (L383).""" + host = "test_host" + 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: + mock_get.return_value = None # L383 condition + + result = await execute_badfish(host, mock_args, logger, None) + + assert result == (host, False) + args, _ = logger.error.call_args + assert isinstance(args[0], BadfishException) + assert str(args[0]) == f"Failed to communicate with {host}" + + +@pytest.mark.asyncio +async def test_find_session_uri_fallback(): + """Test fallback logic when session URI check fails (L400).""" + host = "test_host" + logger = MagicMock(spec=logging.Logger) + + bf = Badfish(host, "user", "pass", logger, 1) + bf.http_client = MagicMock() + + # 1. Root response: Version 1.0.0 ( < 160) -> sets uri to /redfish/v1/Sessions + root_data = {"RedfishVersion": "1.0.0"} + root_resp = MagicMock() + root_resp.status = 200 + root_resp.text = AsyncMock(return_value=json.dumps(root_data)) + + # 2. Check response: 404 (Not 200) -> Should fallback to /redfish/v1/SessionService/Sessions + check_resp = MagicMock() + check_resp.status = 404 + + bf.http_client.get_request = AsyncMock(side_effect=[root_resp, check_resp]) + + uri = await bf.find_session_uri() + + assert uri == "/redfish/v1/SessionService/Sessions" + + +@pytest.mark.asyncio +async def test_set_nic_attribute_invalid_fqdd_exception(): + """Test handling of invalid FQDD string operation raising exception (L2417-2419).""" + host = "test_host" + logger = MagicMock(spec=logging.Logger) + + bf = Badfish(host, "user", "pass", logger, 1) + + # Bypass get_nic_attribute_info check + bf.get_nic_attribute_info = AsyncMock( + return_value={ + "Type": "String", + "CurrentValue": "Old", + "MaxLength": 10, + "MinLength": 1, + } + ) + + # Mock fqdd object to raise ValueError on split + # This simulates the except (IndexError, ValueError) block catch + mock_fqdd = MagicMock() + mock_fqdd.split.side_effect = ValueError("Mock Split Error") + + await bf.set_nic_attribute(mock_fqdd, "Attr", "NewVal") + + logger.error.assert_any_call("Invalid FQDD supplied.") diff --git a/tests/test_nic_attributes.py b/tests/test_nic_attributes.py index b9c4e9f..f938988 100644 --- a/tests/test_nic_attributes.py +++ b/tests/test_nic_attributes.py @@ -83,7 +83,7 @@ def test_get_nic_fqdds_unsupported(self, mock_get, mock_post, mock_delete): self.option_arg, ] _, err = self.badfish_call() - assert err == RESPONSE_VENDOR_UNSUPPORTED + '\n' + assert err == RESPONSE_VENDOR_UNSUPPORTED + "\n" @patch("aiohttp.ClientSession.delete") @patch("aiohttp.ClientSession.post") @@ -161,7 +161,11 @@ def test_get_nic_attr_list_invalid(self, mock_get, mock_post, mock_delete): @patch("aiohttp.ClientSession.post") @patch("aiohttp.ClientSession.get") def test_get_nic_attr_info_ok(self, mock_get, mock_post, mock_delete): - responses = INIT_RESP + [GET_FW_VERSION, GET_NIC_ATTR_REGISTRY, GET_NIC_ATTR_LIST] + responses = INIT_RESP + [ + GET_FW_VERSION, + GET_NIC_ATTR_REGISTRY, + GET_NIC_ATTR_LIST, + ] self.set_mock_response(mock_get, 200, responses) self.set_mock_response(mock_post, 200, "OK") self.set_mock_response(mock_delete, 200, "OK") @@ -174,7 +178,6 @@ def test_get_nic_attr_info_ok(self, mock_get, mock_post, mock_delete): @patch("aiohttp.ClientSession.get") @patch("badfish.main.Badfish.get_idrac_fw_version") def test_get_nic_attr_fw_bad(self, mock_get_fw, mock_get, mock_post, mock_delete): - async def fake_get_fw(): # Emit via Badfish logger name to match formatting from logging import getLogger