Skip to content

Commit f55ba59

Browse files
authored
Merge pull request #152 from MetricsHub/fix/real-bmc-robustness
Real-BMC robustness: iLO 5 session revocation, iDRAC 8 retry pauses, one-way tags, OEM states (#145 #146 #147 #150)
2 parents fdd1738 + 49c6232 commit f55ba59

32 files changed

Lines changed: 640 additions & 131 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The BMC must have IPMI over LAN enabled and an account with the User privilege:
2020

2121
## Upgrading
2222

23-
Version 1.2.03 makes the `protected` fields of the protocol classes (`AbstractIpmiRunner`, `MessageHandler`, `IpmiLanMessage`, `ConfidentialityAlgorithm`, `IntegrityAlgorithm`) `private`. Subclasses must use the new `protected` accessors instead; see [Upgrading from 1.2.02](https://metricshub.org/ipmi-java/upgrading.html#upgrading-from-1-2-02) for the list. The `IpmiClient` API is unchanged. The Full, Compact and Event-Only sensor records now share the `AbstractSensorRecord` superclass, and commands can check responses with `IpmiCommandCoder.validateResponse()`; both are described on the same page. The user name and password are now encoded in UTF-8 whatever the platform charset, and the BMC key is used as raw bytes; as a result, `AuthenticationAlgorithm.getKeyExchangeAuthenticationCode()` and `checkKeyExchangeAuthenticationCode()` take the key and password as `byte[]` instead of `String`. `UdpMessenger.getSentPackets()` is removed, and the `CONST1`/`CONST2` constants of `IntegrityAlgorithm` and `ConfidentialityAesCbc128` are now `private`. `IpmiConnector.closeConnection()` releases the connection, whose handle then throws `IllegalStateException`; the keep-alive is actually sent with the default configuration, as a `Connection.KeepAlive` request whose reply and timeout are not reported to the listeners; and `Constants.TIMEOUT`, which nothing reads, is deprecated.
23+
Version 1.2.03 makes the `protected` fields of the protocol classes (`AbstractIpmiRunner`, `MessageHandler`, `IpmiLanMessage`, `ConfidentialityAlgorithm`, `IntegrityAlgorithm`) `private`. Subclasses must use the new `protected` accessors instead; see [Upgrading from 1.2.02](https://metricshub.org/ipmi-java/upgrading.html#upgrading-from-1-2-02) for the list. The `IpmiClient` API is unchanged. The Full, Compact and Event-Only sensor records now share the `AbstractSensorRecord` superclass, and commands can check responses with `IpmiCommandCoder.validateResponse()`; both are described on the same page. The user name and password are now encoded in UTF-8 whatever the platform charset, and the BMC key is used as raw bytes; as a result, `AuthenticationAlgorithm.getKeyExchangeAuthenticationCode()` and `checkKeyExchangeAuthenticationCode()` take the key and password as `byte[]` instead of `String`. `UdpMessenger.getSentPackets()` is removed, and the `CONST1`/`CONST2` constants of `IntegrityAlgorithm` and `ConfidentialityAesCbc128` are now `private`. `IpmiConnector.closeConnection()` releases the connection, whose handle then throws `IllegalStateException`; the keep-alive is actually sent with the default configuration, as a one-way Get Device ID whose reply and timeout are not reported to the listeners; a one-way IPMI message keeps its tag reserved until its reply or its timeout; and `Constants.TIMEOUT`, which nothing reads, is deprecated.
2424

2525
## Build instructions
2626

‎src/main/java/org/metricshub/ipmi/client/runner/GetSensorsRunner.java‎

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,15 @@
4545
import org.metricshub.ipmi.core.coding.payload.lan.IPMIException;
4646
import org.metricshub.ipmi.core.coding.protocol.AuthenticationType;
4747
import org.metricshub.ipmi.core.common.TypeConverter;
48+
import org.slf4j.Logger;
49+
import org.slf4j.LoggerFactory;
4850

4951
/**
5052
* Get Full And Compact Sensor records
5153
*/
5254
public class GetSensorsRunner extends AbstractIpmiRunner<List<Sensor>> {
5355

54-
private static final int OEM_EVENT_READING_TYPE = 127;
56+
private static final Logger LOGGER = LoggerFactory.getLogger(GetSensorsRunner.class);
5557

5658
public GetSensorsRunner(IpmiClientConfiguration ipmiConfiguration) {
5759
super(ipmiConfiguration);
@@ -88,12 +90,9 @@ public List<Sensor> call() throws Exception {
8890
// Only Full and Compact sensor records have a reading associated
8991
// with them (see IPMI specification for details)
9092
if (sensorRecord instanceof FullSensorRecord || sensorRecord instanceof CompactSensorRecord) {
91-
int recordReadingId = TypeConverter
92-
.byteToInt(((AbstractSensorRecord) sensorRecord).getSensorNumber());
93-
9493
// If our record has got a reading associated, we get request
9594
// for it
96-
GetSensorReadingResponseData data = getSensorRecordReading(recordReadingId);
95+
GetSensorReadingResponseData data = getSensorRecordReading((AbstractSensorRecord) sensorRecord);
9796

9897
// Build the states e.g. deviceName=OK|deviceName=Device Present
9998
String states = buildStates(data, sensorRecord);
@@ -146,12 +145,13 @@ static String buildStates(final GetSensorReadingResponseData data, final SensorR
146145
final AbstractSensorRecord record = (AbstractSensorRecord) sensorRecord;
147146
final String deviceName = record.getName();
148147

149-
if (record.getEventReadingType() == OEM_EVENT_READING_TYPE) {
150-
return buildOemState(data.getRaw(), deviceName);
151-
}
152-
153148
final List<ReadingType> events = data.getStatesAsserted(record.getSensorType(), record.getEventReadingType());
154149

150+
// Like any discrete sensor, an OEM sensor with no state asserted reports no state
151+
if (ReadingType.isOem(record.getEventReadingType())) {
152+
return events.isEmpty() ? Utils.EMPTY : buildOemState(data.getRaw(), deviceName);
153+
}
154+
155155
return appendReadingTypes(events, deviceName);
156156

157157
} catch (Exception e) {
@@ -160,7 +160,7 @@ static String buildStates(final GetSensorReadingResponseData data, final SensorR
160160
}
161161

162162
/**
163-
* Build the state for oem event reading type (0x7f)
163+
* Build the state of an OEM event/reading type (70h-7Fh)
164164
*
165165
* @param raw a byte array of the raw IPMI command data
166166
* @param deviceName the name of the device
@@ -210,32 +210,36 @@ private static String createStateEntry(final String deviceName, final ReadingTyp
210210
}
211211

212212
/**
213-
* Using the given reading id run the GetSensorReading request to get reading data
213+
* Run the GetSensorReading request of a sensor record. A reading the BMC does not provide (DataNotPresent) or
214+
* refuses with another completion code costs that sensor its reading, not the whole walk.
214215
*
215-
* @param recordReadingId the reading identifier of the sensor record
216-
* @return {@link GetSensorReadingResponseData} instance
217-
* @throws Exception at sendMessage or if the error completion code is not DataNotPresent
216+
* @param sensorRecord the Full or Compact sensor record
217+
* @return {@link GetSensorReadingResponseData} instance, or <code>null</code> when the BMC returned no reading
218+
* @throws Exception at sendMessage when the BMC does not answer
218219
*/
219-
private GetSensorReadingResponseData getSensorRecordReading(final int recordReadingId) throws Exception {
220+
GetSensorReadingResponseData getSensorRecordReading(final AbstractSensorRecord sensorRecord)
221+
throws Exception {
222+
int sensorNumber = TypeConverter.byteToInt(sensorRecord.getSensorNumber());
220223
try {
221-
// If we have a reading id means the reading data (e.g. temperature) is potentially available so let's perform the
222-
// re
223-
if (recordReadingId >= 0) {
224-
return (GetSensorReadingResponseData) getConnector()
225-
.sendMessage(
226-
getHandle(),
227-
new GetSensorReading(
228-
IpmiVersion.V20,
229-
getHandle().getCipherSuite(),
230-
AuthenticationType.RMCPPlus,
231-
recordReadingId));
232-
233-
}
224+
return (GetSensorReadingResponseData) getConnector()
225+
.sendMessage(
226+
getHandle(),
227+
new GetSensorReading(
228+
IpmiVersion.V20,
229+
getHandle().getCipherSuite(),
230+
AuthenticationType.RMCPPlus,
231+
sensorNumber));
234232
} catch (IPMIException e) {
235233
if (e.getCompletionCode() != CompletionCode.DataNotPresent) {
236-
throw e;
234+
LOGGER
235+
.warn(
236+
"Failed to read sensor {} ({}) on {}: {}",
237+
sensorNumber,
238+
sensorRecord.getName(),
239+
getIpmiConfiguration().getHostname(),
240+
e.getMessage());
237241
}
242+
return null;
238243
}
239-
return null;
240244
}
241245
}

‎src/main/java/org/metricshub/ipmi/core/api/sync/IpmiConnector.java‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.metricshub.ipmi.core.coding.commands.PrivilegeLevel;
3030
import org.metricshub.ipmi.core.coding.commands.ResponseData;
3131
import org.metricshub.ipmi.core.coding.commands.session.GetChannelAuthenticationCapabilitiesResponseData;
32+
import org.metricshub.ipmi.core.coding.payload.CompletionCode;
3233
import org.metricshub.ipmi.core.coding.payload.lan.IPMIException;
3334
import org.metricshub.ipmi.core.coding.protocol.PayloadType;
3435
import org.metricshub.ipmi.core.coding.security.CipherSuite;
@@ -402,20 +403,15 @@ private ResponseData sendThroughAsyncConnector(
402403
ResponseData responseData = null;
403404

404405
int tries = 0;
405-
int tag = -1;
406406
boolean messageSent = false;
407407

408408
while (!messageSent) {
409409
try {
410410
++tries;
411411

412-
if (tag >= 0) {
413-
tag = asyncConnector.retry(connectionHandle, tag, request.getSupportedPayloadType());
414-
}
415-
416-
if (tag < 0) {
417-
tag = asyncConnector.sendMessage(connectionHandle, request, !waitForResponse);
418-
}
412+
// A fresh tag for every try: the request that timed out or was answered left the queue, or is about
413+
// to, and a reply to the same tag would then be dropped as an orphan
414+
int tag = asyncConnector.sendMessage(connectionHandle, request, !waitForResponse);
419415

420416
logger.debug("Sending message with tag {}, try {}", tag, tries);
421417

@@ -429,27 +425,36 @@ private ResponseData sendThroughAsyncConnector(
429425
} catch (IPMIException e) {
430426
handleErrorResponse(tries, e);
431427
} catch (Exception e) {
432-
handleRetriesWhenException(tries, e);
428+
// No reply in time: the BMC already had the whole message timeout, the request is sent again at once
429+
handleRetriesWhenException(tries, e, false);
433430
}
434431
}
435432

436433
return responseData;
437434
}
438435

439-
private void handleRetriesWhenException(int tries, Exception e) throws Exception {
436+
/**
437+
* Throws the exception when the request was already sent again {@link #retries} times (retries + 1 tries),
438+
* otherwise lets the caller send it again, after a random pause of up to {@link #idleTime} ms if asked.
439+
*/
440+
private void handleRetriesWhenException(int tries, Exception e, boolean pause) throws Exception {
440441
if (tries > retries) {
441442
throw e;
442-
} else {
443+
}
444+
if (pause) {
443445
long sleepTime = (random.nextLong() % (idleTime / 2)) + (idleTime / 2);
444-
445446
Thread.sleep(sleepTime);
446-
logger.warn("Receiving message failed, retrying", e);
447447
}
448+
logger.warn("Receiving message failed, retrying: {}: {}", e.getClass().getSimpleName(), e.getMessage());
448449
}
449450

451+
/**
452+
* Retries a request the BMC answered with a transient completion code: after a pause when the BMC says it is busy,
453+
* at once after a timeout on its side (C3h), where it already waited for the device it could not reach.
454+
*/
450455
private void handleErrorResponse(int tries, IPMIException e) throws Exception {
451456
if (e.getCompletionCode().isTransient()) {
452-
handleRetriesWhenException(tries, e);
457+
handleRetriesWhenException(tries, e, e.getCompletionCode() != CompletionCode.Timeout);
453458
} else {
454459
throw e;
455460
}

‎src/main/java/org/metricshub/ipmi/core/coding/commands/CommandCodes.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public final class CommandCodes {
3939
*/
4040
public static final byte GET_CHASSIS_STATUS = 0x01;
4141

42+
/**
43+
* An IPMI code for Get Device ID command (Application network function)
44+
*/
45+
public static final byte GET_DEVICE_ID = 0x01;
46+
4247
/**
4348
* An IPMI code for Chassis Control command
4449
*/

‎src/main/java/org/metricshub/ipmi/core/coding/commands/fru/record/ChassisType.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ public static ChassisType parseInt(int value) {
168168
case LAPTOP:
169169
return LapTop;
170170
default:
171-
logger.error("Invalid value: " + value);
171+
logger.debug("Invalid value: {}", value);
172172
return Other;
173173
}
174174
}

‎src/main/java/org/metricshub/ipmi/core/coding/commands/fru/record/FruMultiRecordType.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public static FruMultiRecordType parseInt(int value) {
8888
case EXTENDEDCOMPATIBILITYRECORD:
8989
return ExtendedCompatibilityRecord;
9090
default:
91-
logger.error("Invalid value: " + value);
91+
logger.debug("Invalid value: {}", value);
9292
return Unspecified;
9393
}
9494
}

‎src/main/java/org/metricshub/ipmi/core/coding/commands/fru/record/ManagementAccessRecordType.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static ManagementAccessRecordType parseInt(int value) {
6767
case COMPONENTMANAGEMENTURL:
6868
return ComponentManagementURL;
6969
default:
70-
logger.error("Invalid value: " + value);
70+
logger.debug("Invalid value: {}", value);
7171
return Unspecified;
7272
}
7373
}

‎src/main/java/org/metricshub/ipmi/core/coding/commands/sdr/record/DeviceType.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public static DeviceType parseInt(int value) {
172172
case EEPROM24C02:
173173
return Eeprom24C02;
174174
default:
175-
logger.error("Invalid value: " + value);
175+
logger.debug("Invalid value: {}", value);
176176
return Other;
177177
}
178178
}

‎src/main/java/org/metricshub/ipmi/core/coding/commands/sdr/record/EntityId.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ public static EntityId parseInt(int value) {
351351
case BASEBOARD:
352352
return Baseboard;
353353
default:
354-
logger.error("Invalid value: " + value);
354+
logger.debug("Invalid value: {}", value);
355355
return Other;
356356
}
357357
}

‎src/main/java/org/metricshub/ipmi/core/coding/commands/sdr/record/ReadingType.java‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,7 +1119,19 @@ public int getCode() {
11191119
}
11201120

11211121
/**
1122-
* Determines type of discrete sensor reading.
1122+
* Tells whether an event/reading type is OEM: the IPMI 2.0 specification (Table 42-1) reserves {@code 70h} to
1123+
* {@code 7Fh} for OEM use, and defines none of their states.
1124+
*
1125+
* @param eventReadingType the event/reading type code of a sensor record
1126+
* @return true for an OEM event/reading type
1127+
*/
1128+
public static boolean isOem(int eventReadingType) {
1129+
return eventReadingType >= 0x70 && eventReadingType <= 0x7f;
1130+
}
1131+
1132+
/**
1133+
* Determines type of discrete sensor reading. The states of an OEM event/reading type or of an OEM sensor type are
1134+
* {@link #UnknownOEMEvent}.
11231135
*
11241136
* @param sensorType
11251137
* - {@link SensorType} of the sensor
@@ -1132,7 +1144,7 @@ public int getCode() {
11321144
*/
11331145
public static ReadingType parseInt(SensorType sensorType, int eventReadingType, int offset) {
11341146

1135-
if (sensorType == SensorType.Oem) {
1147+
if (sensorType == SensorType.Oem || isOem(eventReadingType)) {
11361148
return UnknownOEMEvent;
11371149
}
11381150

@@ -1670,10 +1682,8 @@ public static ReadingType parseInt(SensorType sensorType, int eventReadingType,
16701682
case MONITORASICIC:
16711683
return MonitorAsicIc;
16721684
default:
1673-
logger
1674-
.warn(
1675-
"Invalid value: " + value + " (" + Integer.toHexString(value)
1676-
+ ") for sensor " + sensorType);
1685+
// BMCs assert states the reading type does not define (HP iLO sets bits 6 and 7 of generic types)
1686+
logger.debug("Invalid value: {} ({}) for sensor {}", value, Integer.toHexString(value), sensorType);
16771687
return Unknown;
16781688
}
16791689
}

0 commit comments

Comments
 (0)