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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ try (IpmiClient bmc = IpmiClient.builder("bmc.example.com").credentials("monitor
}
```

A client owns one RMCP+ session, opened by the first operation and closed by `close()`: always use it in a try-with-resources block. The BMC must have IPMI over LAN enabled and an account with the User privilege, Operator on most BMCs for the sensors of other controllers than the BMC (power supplies, fans, RAID adapter...), which the BMC reaches with Send Message: see [Preparing the BMC](https://metricshub.org/ipmi-java/preparing-the-bmc.html).
A client owns one RMCP+ session, opened by the first operation and closed by `close()`: always use it in a try-with-resources block. The BMC must have IPMI over LAN enabled and an account with the User privilege, Operator on most BMCs for the sensors of other controllers than the BMC (power supplies, fans, RAID adapter...), which the BMC reaches with Send Message: see [Preparing the BMC](https://metricshub.org/ipmi-java/preparing-the-bmc.html). `privilege(PrivilegeLevel.MaximumAvailable)` runs the session at User and raises it only when a command needs more, as far as the account allows ([Raising the session on demand](https://metricshub.org/ipmi-java/configuration.html#raising-the-session-on-demand)).

## Command-line client

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/metricshub/ipmi/cli/CliArguments.java
Original file line number Diff line number Diff line change
Expand Up @@ -548,8 +548,10 @@ private static PrivilegeLevel parsePrivilege(String value, String option) throws
return PrivilegeLevel.Operator;
case "administrator":
return PrivilegeLevel.Administrator;
case "auto":
return PrivilegeLevel.MaximumAvailable;
default:
throw new CliUsageException(option + " must be user, operator or administrator");
throw new CliUsageException(option + " must be user, operator, administrator or auto");
}
}

Expand Down
23 changes: 15 additions & 8 deletions src/main/java/org/metricshub/ipmi/cli/IpmiCli.java
Original file line number Diff line number Diff line change
Expand Up @@ -202,19 +202,25 @@ static int run(

/**
* Reports an error completion code: exit code 77 for {@code D4h}, with the {@code -L} to use above the privilege of
* the session, or none at the Administrator level, where the BMC restricts the command; 70 for the others.
* the session, or none at the Administrator level, where the BMC restricts the command, or at {@code -L auto}, where
* the account has not the level; 70 for the others.
*/
private static int completionCodeFailure(IpmiCompletionCodeException e, PrintStream err, PrivilegeLevel privilege) {
String message = e.getMessage();
if (e.completionCode() == CompletionCode.InsufficentPrivilege) {
// The message of the library ends with a hint for Java callers: replace it with the CLI's
int hint = message.indexOf(". The command requires");
diagnostic(
err,
(hint > 0 ? message.substring(0, hint) : message) + (privilege == PrivilegeLevel.Administrator ?
": the BMC refuses the command even at the Administrator level (it restricts or disables it)" :
": the session has not the privilege the command needs, use "
+ (privilege == PrivilegeLevel.Operator ? "" : "-L operator or ") + "-L administrator"));
String reason;
if (privilege == PrivilegeLevel.Administrator) {
reason = ": the BMC refuses the command even at the Administrator level (it restricts or disables it)";
} else if (privilege == PrivilegeLevel.MaximumAvailable) {
reason = ": the BMC refuses the command at the highest level of the account (the account has not the level"
+ " the command needs, or the BMC restricts or disables the command)";
} else {
reason = ": the session has not the privilege the command needs, use "
+ (privilege == PrivilegeLevel.Operator ? "" : "-L operator or ") + "-L administrator";
}
diagnostic(err, (hint > 0 ? message.substring(0, hint) : message) + reason);
return EXIT_AUTHENTICATION;
}
diagnostic(err, message);
Expand Down Expand Up @@ -1379,7 +1385,8 @@ static String usage() {
+ " -k, --bmc-key <hex> BMC key (Kg) of a two-key login, in hexadecimal (at most 20 bytes)\n"
+ " -kf, --bmc-key-file <file> Read the hexadecimal BMC key from a file\n"
+ " -P, --port <port> UDP port of the BMC (default: 623)\n"
+ " -L, --privilege <level> user, operator or administrator (default: the lowest the subcommand needs)\n"
+ " -L, --privilege <level> user, operator, administrator or auto (default: the lowest the subcommand\n"
+ " needs); auto runs at user and raises the session when a command needs more\n"
+ " -C, --cipher-suite <id>[,..] One ID: use exactly this suite; several: preference order"
+ " (default: 17,3,8,16,2,7)\n"
+ " -t, --timeout <ms> Deadline of the subcommand (default: 120000); for sol, of its setup\n"
Expand Down
155 changes: 142 additions & 13 deletions src/main/java/org/metricshub/ipmi/client/IpmiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.metricshub.ipmi.core.api.sol.SOLException;
import org.metricshub.ipmi.core.api.sol.SerialOverLan;
import org.metricshub.ipmi.core.api.sync.IpmiConnector;
import org.metricshub.ipmi.core.coding.commands.CommandCodes;
import org.metricshub.ipmi.core.coding.commands.IpmiCommandCoder;
import org.metricshub.ipmi.core.coding.commands.PrivilegeLevel;
import org.metricshub.ipmi.core.coding.commands.ResponseData;
Expand Down Expand Up @@ -81,6 +82,7 @@
import org.metricshub.ipmi.core.coding.commands.session.SetSessionPrivilegeLevel;
import org.metricshub.ipmi.core.coding.payload.CompletionCode;
import org.metricshub.ipmi.core.coding.payload.lan.IPMIException;
import org.metricshub.ipmi.core.coding.payload.lan.NetworkFunction;
import org.metricshub.ipmi.core.coding.payload.lan.IpmiLanConstants;
import org.metricshub.ipmi.core.coding.protocol.PayloadType;
import org.metricshub.ipmi.core.coding.security.CipherSuite;
Expand Down Expand Up @@ -641,14 +643,21 @@ SolConsole openSol(Duration deadline) {
* console that was killed, which some BMCs keep active for good) is deactivated and the activation tried again.
*/
private SerialOverLan activateSol() throws SOLException, SessionException {
SerialOverLan sol;
try {
return new SerialOverLan(link.connector, link.session);
sol = new SerialOverLan(link.connector, link.session);
} catch (SOLException e) {
if (e.getCause() != null || !reclaimStaleSol(e)) {
throw e;
}
return new SerialOverLan(link.connector, link.session);
sol = new SerialOverLan(link.connector, link.session);
}
if (sol.getRaisedPrivilegeLevel() != null && sol.getConnectionHandle() == link.handle) {
// The console raised the session of the client (not one of its own on the port of the payload): the
// session runs at that level, and the next D4h is raised from there
link.note(sol.getRaisedPrivilegeLevel());
}
return sol;
}

/**
Expand Down Expand Up @@ -898,19 +907,20 @@ private IpmiClientException translateHandshake(Exception e) {
if (e instanceof SessionException && e.getCause() instanceof IPMIException) {
// openSession() logged in, then closed the session the BMC would not raise to the requested privilege
IPMIException refusal = (IPMIException) e.getCause();
PrivilegeLevel level = privilege == PrivilegeLevel.MaximumAvailable ? PrivilegeLevel.User : privilege;
int code = refusal.getRawCode();
if (code == SetSessionPrivilegeLevel.LEVEL_NOT_AVAILABLE
|| code == SetSessionPrivilegeLevel.LEVEL_ABOVE_LIMIT
|| isAuthenticationCode(refusal.getCompletionCode())) {
return new IpmiAuthenticationException(
"The BMC " + host + " refused the " + privilege + " privilege to " + user + ": "
"The BMC " + host + " refused the " + level + " privilege to " + user + ": "
+ SetSessionPrivilegeLevel.describeRefusal(refusal),
refusal.getCompletionCode(),
e);
}
// A busy BMC, or another failure than a refusal of the level
return new IpmiClientException(
"The BMC " + host + " could not raise the session to the " + privilege + " privilege: "
"The BMC " + host + " could not set the session to the " + level + " privilege: "
+ SetSessionPrivilegeLevel.describeRefusal(refusal),
e);
}
Expand Down Expand Up @@ -1024,6 +1034,12 @@ private static final class Link implements Exchange {
private final Session session;
private final String host;
private final int tries;
/** Raises the session on a D4h: the builder asked for {@link PrivilegeLevel#MaximumAvailable}. */
private final boolean onDemand;
/** The level the session runs at when raised on demand: User right after the handshake. */
private PrivilegeLevel level = PrivilegeLevel.User;
/** The highest level the BMC lets the session be raised to: Administrator until it refuses one. */
Comment thread
bertysentry marked this conversation as resolved.
private PrivilegeLevel ceiling = PrivilegeLevel.Administrator;
private boolean lost;

private Link(IpmiConnector connector, ConnectionHandle handle, Session session, String host, int tries) {
Expand All @@ -1032,13 +1048,15 @@ private Link(IpmiConnector connector, ConnectionHandle handle, Session session,
this.session = session;
this.host = host;
this.tries = tries;
this.onDemand = handle.getPrivilegeLevel() == PrivilegeLevel.MaximumAvailable;
Comment thread
bertysentry marked this conversation as resolved.
}

/**
* Opens the session: cipher suite discovery unless the builder fixed the suite, Get Channel Authentication
* Capabilities, then {@link IpmiConnector#openSession(ConnectionHandle, String, byte[], byte[])}: Open Session,
* RAKP 1 to 4 and, when the builder asked for more than User, Set Session Privilege Level (IPMI 2.0 section
* 22.18: every session starts at User).
* RAKP 1 to 4 and Set Session Privilege Level, to the level the builder asked for when that is more than User
* (IPMI 2.0 section 22.18: every session starts at User), to User for {@link PrivilegeLevel#MaximumAvailable}
* (some BMCs start the session at the requested maximum instead).
*/
static Link open(IpmiClient client, IpmiConnector connector, InetAddress address) {
ConnectionHandle handle = null;
Expand Down Expand Up @@ -1086,7 +1104,111 @@ <R extends ResponseData> R sendOnce(IpmiCommandCoder<R> command) {
return exchange(command, true);
}

/**
* Exchanges the command, and again at a higher privilege when the BMC refuses it for insufficient privilege
* (D4h) and the session is raised on demand: the BMC did not carry a refused command out, so even a command sent
* once is safely sent again. A raw request returns its D4h instead of throwing it, and is raised the same way.
*/
private <R extends ResponseData> R exchange(IpmiCommandCoder<R> command, boolean once) {
while (true) {
try {
R response = attempt(command, once);
if (response instanceof RawResponse
&& ((RawResponse) response).completionCode() == CompletionCode.InsufficentPrivilege
&& raise()) {
continue;
}
if (onDemand) {
learn(command, response);
}
return response;
} catch (IpmiCompletionCodeException e) {
if (e.completionCode() != CompletionCode.InsufficentPrivilege || !raise()) {
throw e;
}
}
}
}

/**
* Raises a session raised on demand to the next level, Operator then Administrator, and tells whether it was:
* not when the session runs at a fixed level, at Administrator, or at the ceiling the BMC set. A refusal of the
* level is the ceiling: 80h or 81h when the account or the channel stops there (IPMI 2.0 table 22-24), and D4h,
* as a session the BMC revoked answers it to everything, Set Session Privilege Level included, and must not be
* asked again at every command. Any other error (a busy BMC) is the failure of the raise, thrown as such and
* tried again at the next D4h.
*
* @throws IpmiCompletionCodeException when the raise fails with another code than a refusal of the level
*/
private boolean raise() {
PrivilegeLevel next = level == PrivilegeLevel.User ?
PrivilegeLevel.Operator :
level == PrivilegeLevel.Operator ? PrivilegeLevel.Administrator : null;
if (!onDemand || next == null || next.compareTo(ceiling) > 0) {
return false;
}
try {
attempt(new SetSessionPrivilegeLevel(next), false);
} catch (IpmiCompletionCodeException e) {
int code = e.rawCompletionCode();
if (code != SetSessionPrivilegeLevel.LEVEL_NOT_AVAILABLE
&& code != SetSessionPrivilegeLevel.LEVEL_ABOVE_LIMIT
&& e.completionCode() != CompletionCode.InsufficentPrivilege) {
throw e;
}
ceiling = level;
return false;
Comment thread
bertysentry marked this conversation as resolved.
}
level = next;
return true;
}

/**
* Keeps the level of a session raised on demand in step with a Set Session Privilege Level the caller sent
* itself, typed or raw: the BMC runs the session at the level it accepted, whatever the level this link reached.
*/
private void learn(IpmiCommandCoder<?> command, ResponseData response) {
PrivilegeLevel set = null;
if (command instanceof SetSessionPrivilegeLevel) {
set = ((SetSessionPrivilegeLevel) command).getPrivilegeLevel();
} else
if (response instanceof RawResponse
&& command.getNetworkFunctionCode() == NetworkFunction.ApplicationRequest.getCode()
&& command.getCommandCode() == CommandCodes.SET_SESSION_PRIVILEGE_LEVEL
&& ((RawResponse) response).completionCode() == CompletionCode.Ok
&& ((RawResponse) response).data().length > 0) {
// The reply gives the new level of the session (IPMI 2.0 table 22-24)
switch (((RawResponse) response).data()[0] & 0x0f) {
case 2:
set = PrivilegeLevel.User;
break;
case 3:
set = PrivilegeLevel.Operator;
break;
case 4:
set = PrivilegeLevel.Administrator;
break;
default:
break;
}
}
if (set == PrivilegeLevel.User || set == PrivilegeLevel.Operator || set == PrivilegeLevel.Administrator) {
note(set);
}
}

/**
* Records the level the session runs at, set by the caller or by the SOL console: a level the BMC accepted is
* within the ceiling.
*/
void note(PrivilegeLevel set) {
level = set;
if (set.compareTo(ceiling) > 0) {
ceiling = set;
}
}

private <R extends ResponseData> R attempt(IpmiCommandCoder<R> command, boolean once) {
String name = command.getClass().getSimpleName();
try {
return once ? connector.sendMessage(handle, command, 0) : connector.sendMessage(handle, command);
Expand Down Expand Up @@ -1201,19 +1323,26 @@ public Builder bmcKey(byte[] key) {
* Privilege Level right after the handshake (every session starts at User, IPMI 2.0 section 22.18). User by
* default, enough for every read operation; Operator for the power control; Administrator for Serial over
* LAN on most BMCs.
* <p>
* {@code MaximumAvailable} requests the highest level the account is allowed, then runs the session at User and
* raises it only when the BMC refuses a command for insufficient privilege ({@code D4h}): to Operator, then to
* Administrator, and the refused command, which the BMC did not carry out, is sent again. A level the BMC refuses
* ({@code 80h}, {@code 81h}) is the ceiling of the session, remembered for its lifetime, and the {@code D4h}
* reaches the caller as it does at a fixed level. The sensors of other controllers than the BMC, which some BMCs
* serve at the Operator or Administrator level only, are then read with any account that has the level, without
* running the whole session at it.
*
* @param level the privilege level: User, Operator or Administrator ({@code MaximumAvailable} is refused, as
* the session could not be raised to it, and {@code Callback}, at which no operation works)
* @param level the privilege level: User, Operator, Administrator or MaximumAvailable ({@code Callback}, at
* which no operation works, is refused)
* @return this builder
* @throws IllegalArgumentException for {@code MaximumAvailable} and {@code Callback}
* @throws IllegalArgumentException for {@code Callback}
*/
public Builder privilege(PrivilegeLevel level) {
Objects.requireNonNull(level, "level");
if (level != PrivilegeLevel.User && level != PrivilegeLevel.Operator && level != PrivilegeLevel.Administrator) {
// MaximumAvailable: Set Session Privilege Level needs a level, and the highest one the user has is not
// known here. Callback: no command of the client works at that level
if (level == PrivilegeLevel.Callback) {
// No command of the client works at that level
throw new IllegalArgumentException(
"Choose the privilege level of the session: User, Operator or Administrator");
"Choose the privilege level of the session: User, Operator, Administrator or MaximumAvailable");
}
this.privilege = level;
return this;
Expand Down
Loading
Loading