diff --git a/README.md b/README.md index 227afd3..6810492 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/main/java/org/metricshub/ipmi/cli/CliArguments.java b/src/main/java/org/metricshub/ipmi/cli/CliArguments.java index 91e7119..38a6efc 100644 --- a/src/main/java/org/metricshub/ipmi/cli/CliArguments.java +++ b/src/main/java/org/metricshub/ipmi/cli/CliArguments.java @@ -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"); } } diff --git a/src/main/java/org/metricshub/ipmi/cli/IpmiCli.java b/src/main/java/org/metricshub/ipmi/cli/IpmiCli.java index 6af31e2..0a55d98 100644 --- a/src/main/java/org/metricshub/ipmi/cli/IpmiCli.java +++ b/src/main/java/org/metricshub/ipmi/cli/IpmiCli.java @@ -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); @@ -1379,7 +1385,8 @@ static String usage() { + " -k, --bmc-key BMC key (Kg) of a two-key login, in hexadecimal (at most 20 bytes)\n" + " -kf, --bmc-key-file Read the hexadecimal BMC key from a file\n" + " -P, --port UDP port of the BMC (default: 623)\n" - + " -L, --privilege user, operator or administrator (default: the lowest the subcommand needs)\n" + + " -L, --privilege 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 [,..] One ID: use exactly this suite; several: preference order" + " (default: 17,3,8,16,2,7)\n" + " -t, --timeout Deadline of the subcommand (default: 120000); for sol, of its setup\n" diff --git a/src/main/java/org/metricshub/ipmi/client/IpmiClient.java b/src/main/java/org/metricshub/ipmi/client/IpmiClient.java index a17089f..434c4f7 100644 --- a/src/main/java/org/metricshub/ipmi/client/IpmiClient.java +++ b/src/main/java/org/metricshub/ipmi/client/IpmiClient.java @@ -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; @@ -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; @@ -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; } /** @@ -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); } @@ -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. */ + private PrivilegeLevel ceiling = PrivilegeLevel.Administrator; private boolean lost; private Link(IpmiConnector connector, ConnectionHandle handle, Session session, String host, int tries) { @@ -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; } /** * 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; @@ -1086,7 +1104,111 @@ R sendOnce(IpmiCommandCoder 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 exchange(IpmiCommandCoder 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; + } + 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 attempt(IpmiCommandCoder command, boolean once) { String name = command.getClass().getSimpleName(); try { return once ? connector.sendMessage(handle, command, 0) : connector.sendMessage(handle, command); @@ -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. + *

+ * {@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; diff --git a/src/main/java/org/metricshub/ipmi/core/api/async/IpmiAsyncConnector.java b/src/main/java/org/metricshub/ipmi/core/api/async/IpmiAsyncConnector.java index 814a85d..968f042 100644 --- a/src/main/java/org/metricshub/ipmi/core/api/async/IpmiAsyncConnector.java +++ b/src/main/java/org/metricshub/ipmi/core/api/async/IpmiAsyncConnector.java @@ -31,6 +31,7 @@ import org.metricshub.ipmi.core.coding.commands.ResponseData; import org.metricshub.ipmi.core.coding.commands.session.GetChannelAuthenticationCapabilitiesResponseData; import org.metricshub.ipmi.core.coding.commands.session.SetSessionPrivilegeLevel; +import org.metricshub.ipmi.core.coding.payload.CompletionCode; import org.metricshub.ipmi.core.coding.payload.IpmiPayload; import org.metricshub.ipmi.core.coding.payload.lan.IPMIException; import org.metricshub.ipmi.core.coding.protocol.PayloadType; @@ -333,7 +334,10 @@ public Session openSession( * Establishes the session with the remote host, with the password as bytes: the caller can then clear them * once the session is open, which an immutable {@link String} does not allow. When the requested privilege is * Operator or Administrator, the session is raised to it with Set Session Privilege Level right after the - * handshake (IPMI 2.0 section 22.18: every session starts at User); a refusal closes the session. + * handshake (IPMI 2.0 section 22.18: every session starts at User); a refusal closes the session. For + * {@link PrivilegeLevel#MaximumAvailable}, the session is requested at Administrator, then at Operator and at User + * when the BMC refuses the role to the account, and set to User right after the handshake: the caller raises it + * when a command needs more. * * @param connectionHandle * - {@link ConnectionHandle} associated with the remote host. @@ -361,28 +365,28 @@ public Session openSession( byte[] password, byte[] bmcKey) throws Exception { - int sessionId = 0; - int tries = 0; - boolean succeded = false; - connectionHandle.setUser(username); connectionHandle.setPasswordBytes(password); connectionHandle.setBmcKey(bmcKey); - while (tries <= retries && !succeded) { + // MaximumAvailable: the role 0 of the specification (highest level matching the proposed algorithms, section + // 13.17) is refused at Open Session by iLO 5, iDRAC and Cisco IMC BMCs, and leaves a session that IMM, Supermicro + // and iRMC BMCs will not set to any level (D4h), so the levels are requested from the highest down, and a role + // the BMC refuses to the account is tried one level lower + PrivilegeLevel[] roles = connectionHandle.getPrivilegeLevel() == PrivilegeLevel.MaximumAvailable ? + new PrivilegeLevel[] + { PrivilegeLevel.Administrator, PrivilegeLevel.Operator, PrivilegeLevel.User } : + new PrivilegeLevel[] + { connectionHandle.getPrivilegeLevel() }; + int sessionId = 0; + for (int i = 0; i < roles.length; i++) { try { - ++tries; - sessionId = connectionManager - .startSession( - connectionHandle.getHandle(), - connectionHandle.getCipherSuite(), - connectionHandle.getPrivilegeLevel(), - username, - password, - bmcKey); - succeded = true; - } catch (Exception e) { - if (tries > retries || !isRetriable(e)) { + sessionId = startSession(connectionHandle, roles[i], username, password, bmcKey); + break; + } catch (IPMIException e) { + CompletionCode code = e.getCompletionCode(); + if (i == roles.length - 1 + || code != CompletionCode.UnauthorizedRole && code != CompletionCode.InvalidRole) { throw e; } } @@ -399,17 +403,53 @@ public Session openSession( } /** - * Raises the new session to the privilege requested for it when that is Operator or Administrator: the BMC starts - * every session at User whatever the maximum requested in RAKP Message 1 (IPMI 2.0 section 22.18). Sent again, like - * a handshake step, when no reply comes or the BMC is busy; any failure closes the session. + * Open Session and RAKP Messages 1 to 4 with the given role, sent again when no reply comes or the BMC is busy. + * + * @return the console session ID + */ + private int startSession( + ConnectionHandle connectionHandle, + PrivilegeLevel role, + String username, + byte[] password, + byte[] bmcKey) + throws Exception { + int tries = 0; + while (true) { + try { + ++tries; + return connectionManager + .startSession( + connectionHandle.getHandle(), + connectionHandle.getCipherSuite(), + role, + username, + password, + bmcKey); + } catch (Exception e) { + if (tries > retries || !isRetriable(e)) { + throw e; + } + } + } + } + + /** + * Sets the new session to the privilege requested for it: raised to Operator or Administrator, as the BMC starts + * every session at User whatever the maximum requested in RAKP Message 1 (IPMI 2.0 section 22.18); set to User for + * {@link PrivilegeLevel#MaximumAvailable}, as some BMCs (IMM, iDRAC 6, iRMC, GIGABYTE, Supermicro) start the + * session at the requested maximum instead, which for the highest level available is the limit of the account. + * Sent again, like a handshake step, when no reply comes or the BMC is busy; any failure closes the session. * * @throws SessionException when the BMC answers with an error completion code, with the {@link IPMIException} as * cause */ private void raisePrivilege(ConnectionHandle connectionHandle) throws Exception { PrivilegeLevel level = connectionHandle.getPrivilegeLevel(); - if (level != PrivilegeLevel.Operator && level != PrivilegeLevel.Administrator) { - // User: nothing to raise. Callback cannot be changed (section 22.18), MaximumAvailable names no level + if (level == PrivilegeLevel.MaximumAvailable) { + level = PrivilegeLevel.User; + } else if (level != PrivilegeLevel.Operator && level != PrivilegeLevel.Administrator) { + // User: nothing to raise. Callback cannot be changed (section 22.18) return; } MessageListener listener = new MessageListener(connectionHandle); @@ -432,7 +472,7 @@ private void raisePrivilege(ConnectionHandle connectionHandle) throws Exception closeSessionQuietly(connectionHandle); if (e instanceof IPMIException) { throw new SessionException( - "The BMC refused to raise the session to the " + level + " privilege: " + "The BMC refused to set the session to the " + level + " privilege: " + SetSessionPrivilegeLevel.describeRefusal((IPMIException) e), e); } diff --git a/src/main/java/org/metricshub/ipmi/core/api/sol/SerialOverLan.java b/src/main/java/org/metricshub/ipmi/core/api/sol/SerialOverLan.java index 48fd31f..ee29ba7 100644 --- a/src/main/java/org/metricshub/ipmi/core/api/sol/SerialOverLan.java +++ b/src/main/java/org/metricshub/ipmi/core/api/sol/SerialOverLan.java @@ -119,6 +119,8 @@ public class SerialOverLan implements Closeable { * The connection of the session where the payload was last activated, or null, and the instance activated there. */ private ConnectionHandle activatedConnection; + /** The level the console raised a MaximumAvailable session of the payload to, or null. */ + private PrivilegeLevel raisedTo; private int activatedInstance; /** @@ -378,11 +380,12 @@ private int activatePayload(IpmiConnector ipmiConnector, Session payloadSession) } /** - * Sends Activate Payload, and once more after raising the privilege of a session opened with - * {@link PrivilegeLevel#MaximumAvailable} when it is too low, or after deactivating the payload when a lost reply - * left it active in this session. {@code openSession()} raised a session opened at Operator or Administrator, and a - * User session cannot go higher; the session the console opens on the port of the payload has the level of the - * given session, so a MaximumAvailable one is raised here too. + * Sends Activate Payload, and again after raising the privilege of a session opened with + * {@link PrivilegeLevel#MaximumAvailable} when it is too low (to Operator, then to Administrator, as far as the + * account allows), or after deactivating the payload when a lost reply left it active in this session. + * {@code openSession()} raised a session opened at Operator or Administrator, and a User session cannot go higher; + * the session the console opens on the port of the payload has the level of the given session, so a + * MaximumAvailable one is raised here too. */ private ActivateSolPayloadResponseData getActivatePayloadResponse( IpmiConnector ipmiConnector, @@ -390,16 +393,20 @@ private ActivateSolPayloadResponseData getActivatePayloadResponse( ActivateSolPayload activatePayload) throws Exception { ConnectionHandle connectionHandle = payloadSession.getConnectionHandle(); - boolean raised = connectionHandle.getPrivilegeLevel() != PrivilegeLevel.MaximumAvailable; + boolean onDemand = connectionHandle.getPrivilegeLevel() == PrivilegeLevel.MaximumAvailable; + PrivilegeLevel level = PrivilegeLevel.User; boolean deactivated = false; while (true) { try { return ipmiConnector.sendMessage(connectionHandle, activatePayload); } catch (IPMIException e) { boolean alreadyActive = !deactivated && e.getRawCode() == PAYLOAD_ALREADY_ACTIVE; - if (!raised && e.getCompletionCode() == CompletionCode.InsufficentPrivilege) { - raiseSessionPrivileges(ipmiConnector, connectionHandle); - raised = true; + if (onDemand + && level != PrivilegeLevel.Administrator + && e.getCompletionCode() == CompletionCode.InsufficentPrivilege) { + level = level == PrivilegeLevel.User ? PrivilegeLevel.Operator : PrivilegeLevel.Administrator; + ipmiConnector.sendMessage(connectionHandle, new SetSessionPrivilegeLevel(level)); + raisedTo = level; } else if (alreadyActive && isActiveInSession(ipmiConnector, payloadSession)) { // The reply to Activate Payload was lost, and the request sent again found the payload active in this // session: the port and the sizes of the payload are only in that reply, which a new activation gives @@ -458,18 +465,14 @@ private int getFirstAvailablePayloadInstance(IpmiConnector ipmiConnector, Connec } /** - * Raises a session opened with {@link PrivilegeLevel#MaximumAvailable}, which still runs at User (IPMI 2.0 section - * 22.18), to Administrator. + * Returns the privilege the console raised the session of the payload to, when that session was opened with + * {@link PrivilegeLevel#MaximumAvailable} and the BMC refused to activate the payload at its level: the owner of the + * session (see {@link #getConnectionHandle()}) runs it at that level from then on. * - * @param ipmiConnector - * {@link IpmiConnector} that will be used for communication - * @param connectionHandle - * {@link ConnectionHandle} representing single connection to managed system. - * @throws Exception - * If any exception occurred during communication. + * @return Operator or Administrator, or null when the console did not raise the session */ - private void raiseSessionPrivileges(IpmiConnector ipmiConnector, ConnectionHandle connectionHandle) throws Exception { - ipmiConnector.sendMessage(connectionHandle, new SetSessionPrivilegeLevel(PrivilegeLevel.Administrator)); + public PrivilegeLevel getRaisedPrivilegeLevel() { + return raisedTo; } /** diff --git a/src/main/java/org/metricshub/ipmi/core/coding/commands/session/SetSessionPrivilegeLevel.java b/src/main/java/org/metricshub/ipmi/core/coding/commands/session/SetSessionPrivilegeLevel.java index 2a8e3bb..a6fdd3c 100644 --- a/src/main/java/org/metricshub/ipmi/core/coding/commands/session/SetSessionPrivilegeLevel.java +++ b/src/main/java/org/metricshub/ipmi/core/coding/commands/session/SetSessionPrivilegeLevel.java @@ -82,6 +82,16 @@ public SetSessionPrivilegeLevel(PrivilegeLevel privilegeLevel) { this.privilegeLevel = privilegeLevel; } + /** + * Returns the level the command requests. + * + * @return the privilege level, {@link PrivilegeLevel#MaximumAvailable} when the command only returns the present + * level of the session + */ + public PrivilegeLevel getPrivilegeLevel() { + return privilegeLevel; + } + @Override public byte getCommandCode() { return CommandCodes.SET_SESSION_PRIVILEGE_LEVEL; diff --git a/src/site/markdown/cli.md b/src/site/markdown/cli.md index 4fd7efd..c3fbfb6 100644 --- a/src/site/markdown/cli.md +++ b/src/site/markdown/cli.md @@ -91,7 +91,7 @@ either option can be repeated or given a comma-separated list. | `-k, --bmc-key ` | BMC key (Kg) of a two-key login, in hexadecimal (`0x` optional), at most 20 bytes. | | `-kf, --bmc-key-file ` | Read the hexadecimal BMC key from a file. | | `-P, --port ` | UDP port of the BMC. Default: 623. | -| `-L, --privilege ` | `user`, `operator` or `administrator` (case-insensitive). Default: the lowest level the subcommand needs (see [below](#default-privilege-per-subcommand)). | +| `-L, --privilege ` | `user`, `operator`, `administrator` or `auto` (case-insensitive). Default: the lowest level the subcommand needs (see [below](#default-privilege-per-subcommand)). | | `-C, --cipher-suite [,...]` | One ID: use exactly this cipher suite, without discovery. Several: the preference order among the suites the BMC offers. Default: `17,3,8,16,2,7`. Suites without integrity (0, 1, 6, 15) are refused: their replies could be forged. | | `-t, --timeout ` | Deadline of the subcommand, in milliseconds. Default: 120000. For `sol`, it bounds the session setup and the payload activation only. | | `--message-timeout ` | Per-message timeout. Default: 5000. | @@ -119,9 +119,16 @@ for the **least** privilege that works (unlike | `power on`, `off`, `cycle`, `reset`, `soft`, `diag`, `identify` | `operator` | | `sol`, `raw` | `administrator` | +`-L auto` requests the highest level the account is allowed, runs the session at `user` and +raises it only when the BMC refuses a command for insufficient privilege (`D4h`), to `operator` +then `administrator`, as far as the account allows +([Raising the session on demand](configuration.html#raising-the-session-on-demand)): the same +command line works on BMCs that need different levels for the same command, and a read-only +account still logs in. + `sensors` at the `user` level prints the sensors of other controllers than the BMC (power supplies, fans, RAID adapter behind an HPE iLO, for instance) without reading on the BMCs that serve them at -a higher level, with the `D4h` problem in `--long` and `--json`: give `-L operator`, or +a higher level, with the `D4h` problem in `--long` and `--json`: give `-L auto`, `-L operator`, or `-L administrator` on some BMCs ([Sensor owners](sensors.html#sensor-owners)). A BMC that refuses a command at the session's level answers `D4h`: the CLI exits with 77 and says diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index acd090c..8f09483 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -91,10 +91,33 @@ operation with `IpmiAuthenticationException`. | `User` (default) | Every read operation: `chassisStatus()`, `sensors()`, `frus()`, `sel()`, but for the sensors of other controllers than the BMC on most BMCs | | `Operator` | The control operations: `chassisControl()`, `chassisIdentify()`, `chassisIdentifyForever()`; the sensors of other controllers than the BMC, which the BMC reaches with Send Message ([Sensor owners](sensors.html#sensor-owners)), Administrator on some BMCs | | `Administrator` | Serial over LAN on most BMCs, the configuration commands | +| `MaximumAvailable` | Everything the account is allowed: the session runs at User and is raised on demand (below) | The account must be allowed that level on the LAN channel ([Preparing the BMC](preparing-the-bmc.html#creating-the-account)). +#### Raising the session on demand + +`privilege(PrivilegeLevel.MaximumAvailable)` requests the highest level the account is allowed: +Administrator at Open Session and RAKP Message 1, then Operator and User when the BMC refuses +the role to the account (the role `0h` of the specification, "highest level matching the +proposed algorithms", is refused by HPE iLO 5, Dell iDRAC and Cisco IMC BMCs, and leaves a +session that Lenovo IMM, Supermicro and Fujitsu iRMC BMCs will not set to any level). The +session is set to **User** right after the handshake (some BMCs start it at the requested +maximum instead), and raised only when the BMC refuses a command for insufficient privilege +(`D4h`): with Set Session Privilege Level to Operator, then to Administrator, and the refused +command, which the BMC did not carry out, is sent again. `sol().open()` raises the session the same way when the BMC refuses to activate the payload at its level. A level the BMC refuses (`80h`, the level is not available for this user, or +`81h`, it exceeds the limit of the channel or of the user) is the ceiling of the session, +remembered for its lifetime, and the `D4h` reaches the caller as it does at a fixed level +(`IpmiCompletionCodeException`, or the `problem()` of a sensor). + +This is the mode for a monitoring account on a fleet of BMCs that differ: `sensors()` reads the +sensors of other controllers than the BMC ([Sensor owners](sensors.html#sensor-owners)) at the +User level on an HPE iLO 5, after one raise to Operator on most BMCs, and after a second raise to +Administrator on an AMI-based BMC, each with the same client and without running the whole +session at the highest level of the account. A session the BMC revoked answers `D4h` to every +command, Set Session Privilege Level included: the raise is then refused once and not asked again. + ### Cipher suites `cipherSuites(int...)` lists the suites the client may use, by order of preference: before opening diff --git a/src/site/markdown/preparing-the-bmc.md b/src/site/markdown/preparing-the-bmc.md index a69f61c..65eea8b 100644 --- a/src/site/markdown/preparing-the-bmc.md +++ b/src/site/markdown/preparing-the-bmc.md @@ -24,7 +24,7 @@ only, and works whether the server is powered on or off, as long as the BMC has | IPMI 2.0 (RMCP+) | The client only opens RMCP+ sessions. IPMI 1.5-only BMCs (RMCP with MD2/MD5 or straight password authentication) are not supported: the client refuses them at once with `IpmiClientException: The BMC ... does not support IPMI 2.0 / RMCP+`. | | UDP port 623 | Open from the machine running the client to the BMC. The port can be changed with [`setPort()`](configuration.html#host-and-port). | | An account | Enabled, with a password, allowed to log in over IPMI on the LAN channel. | -| Privilege level | **User** for everything `IpmiClient` reads, but for the [sensors of other controllers](sensors.html#sensor-owners) than the BMC, which most BMCs serve at the Operator level; **Operator** for the control commands; **Administrator** for [Serial over LAN](serial-over-lan.html). | +| Privilege level | **User** for everything `IpmiClient` reads, but for the [sensors of other controllers](sensors.html#sensor-owners) than the BMC, which most BMCs serve at the Operator level; **Operator** for the control commands; **Administrator** for [Serial over LAN](serial-over-lan.html). A client built with `privilege(PrivilegeLevel.MaximumAvailable)` uses whatever the account is allowed, raising the session only when a command needs more ([Raising the session on demand](configuration.html#raising-the-session-on-demand)). | | A supported cipher suite | Suite **3** or **17** in practice; see [Cipher suites](#cipher-suites). | ## Enabling IPMI over LAN diff --git a/src/site/markdown/sensors.md b/src/site/markdown/sensors.md index e9ec1b2..eca4a8f 100644 --- a/src/site/markdown/sensors.md +++ b/src/site/markdown/sensors.md @@ -63,7 +63,10 @@ BMCs allow it at the User level (HPE iLO 5), others only at the Administrator le GIGABYTE BMC). A client below the level the BMC requires gets the sensors of the BMC as usual, and those of the other controllers without reading, their `problem()` saying `Get Sensor Reading through the BMC to the controller at 2Ch on channel 6 failed: ... (completion code D4h)` -([Privilege level](configuration.html#privilege-level)). +([Privilege level](configuration.html#privilege-level)). A client built with +`privilege(PrivilegeLevel.MaximumAvailable)` raises the session to the level the BMC requires on +that first `D4h`, as far as the account allows, and reads these sensors with the same code on +every BMC ([Raising the session on demand](configuration.html#raising-the-session-on-demand)). ### Selecting sensors diff --git a/src/site/markdown/troubleshooting.md b/src/site/markdown/troubleshooting.md index 9960202..70bf776 100644 --- a/src/site/markdown/troubleshooting.md +++ b/src/site/markdown/troubleshooting.md @@ -81,7 +81,10 @@ The BMC refused a command at the session's privilege level, which the client req and sets right after it: the command needs a higher level than the requested one. The default **User** level is enough for everything the client reads; `send()` of a control command (Chassis Control, chassis identify) needs `privilege(PrivilegeLevel.Operator)` on the builder, and Serial over LAN -`Administrator` on most BMCs. If a BMC refuses a read command at the User level, check: +`Administrator` on most BMCs; `privilege(PrivilegeLevel.MaximumAvailable)` raises the session +on that `D4h`, as far as the account allows +([Raising the session on demand](configuration.html#raising-the-session-on-demand)). If a BMC +refuses a read command at the User level, check: * the privilege of the account on the LAN channel (`ipmitool channel getaccess 1 `), * the maximum privilege of the cipher suite in use (`Cipher Suite Priv Max` in @@ -127,7 +130,7 @@ MD5-128 integrity): choose a suite whose `isSupported()` is `true`. | `Fru.problems()` says `Cannot read FRU ` or `Cannot decode FRU ` | The BMC refused Get FRU Inventory Area Info for that FRU, or its data is neither in the IPMI FRU format nor the SPD data of a DDR3, DDR4 or DDR5 memory module nor the processor information ROM of an Intel Xeon processor (the SPD data of a DDR2 or LPDDR module, and a processor information ROM of another data format revision than 7, are reported as not decoded). The other FRUs are still returned. | | `Fru.problems()` says `The info area at offset is truncated` | The read stopped before the end of that area (see above): the complete areas of the FRU are still returned. | | `Sensor.problem()` says `The sensor belongs to system software` | The record names system software, the BIOS for instance, as the owner of the sensor: no controller holds its reading ([Sensor owners](sensors.html#sensor-owners)). `ipmiutil` shows it as not available. | -| `Sensor.problem()` says `... through the BMC to the controller at ... failed` or `No reply from the controller at ...` | The sensor belongs to another controller than the BMC, which the BMC forwards the request to with Send Message. `D4h`: the session needs a higher privilege for Send Message, Operator or, on some BMCs, Administrator; `83h` (NAK on write): no controller answered at that address; no reply: the controller is absent or does not answer ([Sensor owners](sensors.html#sensor-owners)). | +| `Sensor.problem()` says `... through the BMC to the controller at ... failed` or `No reply from the controller at ...` | The sensor belongs to another controller than the BMC, which the BMC forwards the request to with Send Message. `D4h`: the session needs a higher privilege for Send Message, Operator or, on some BMCs, Administrator (`privilege(PrivilegeLevel.MaximumAvailable)` raises it there when the account has the level); `83h` (NAK on write): no controller answered at that address; no reply: the controller is absent or does not answer ([Sensor owners](sensors.html#sensor-owners)). | | A sensor known to `ipmitool` is not returned | Only Full and Compact sensor records are read, and shared Compact records are not expanded ([#100](https://github.com/metricshub/ipmi-java/issues/100)). A sensor whose reading the BMC flags as unavailable or not scanned (`ipmitool` shows `na` or `disabled`) is returned without reading or states, with `problem()` set. | | Negative processor temperatures (`CPU1 DTS = -44.0`) | Not an error: Intel *Digital Thermal Sensor* readings are the margin below the maximum junction temperature. | diff --git a/src/site/markdown/upgrading.md b/src/site/markdown/upgrading.md index a790820..c1c6a47 100644 --- a/src/site/markdown/upgrading.md +++ b/src/site/markdown/upgrading.md @@ -7,6 +7,16 @@ description: What changes when upgrading the IPMI Java Client — from 2.0.00, f ## Upgrading from 2.0.00 +* `privilege(PrivilegeLevel.MaximumAvailable)` is accepted by the builder, which 2.0.00 refused: + the session requests the highest level the account is allowed, runs at User, and is raised + with Set Session Privilege Level only when the BMC refuses a command for insufficient + privilege (`D4h`), to Operator then Administrator, the refused command being sent again; a + level the BMC refuses is the ceiling of the session + ([Raising the session on demand](configuration.html#raising-the-session-on-demand)). `ipmicli` + takes `-L auto`. In the protocol core, `openSession()` of a connection created with + `MaximumAvailable` now sets the session to User right after the handshake, where 2.0.00 left + it at whatever level the BMC started it (the requested maximum on IMM, iDRAC 6, iRMC, GIGABYTE + and Supermicro BMCs); a `SetSessionPrivilegeLevel` sent after it raises the session as before. * `Sensor.name()` (and `getName()` of the Full, Compact and Event-Only sensor records) is never `null`: a Compact record that ends before its ID string gives the empty name, as an ID string of length 0 does. 2.0.00 returned `null`, which made `ipmicli sensors` fail with a diff --git a/src/test/java/org/metricshub/ipmi/cli/CliArgumentsTest.java b/src/test/java/org/metricshub/ipmi/cli/CliArgumentsTest.java index f6d3020..f36fe16 100644 --- a/src/test/java/org/metricshub/ipmi/cli/CliArgumentsTest.java +++ b/src/test/java/org/metricshub/ipmi/cli/CliArgumentsTest.java @@ -257,7 +257,7 @@ void usageErrors() throws Exception { assertEquals("--port must be a number", usageError("--port", "six")); assertEquals("-t must be between 1 and 2147483647", usageError("-t", "0")); assertEquals("--retries must be between 0 and 2147483647", usageError("--retries", "-1")); - assertEquals("-L must be user, operator or administrator", usageError("-L", "ADMIN")); + assertEquals("-L must be user, operator, administrator or auto", usageError("-L", "ADMIN")); assertEquals("-C must be between 0 and 19", usageError("-C", "17,20")); assertEquals("-C must be a number", usageError("-C", "")); assertEquals("-C must be a number", usageError("-C", "17,")); diff --git a/src/test/java/org/metricshub/ipmi/cli/IpmiCliTest.java b/src/test/java/org/metricshub/ipmi/cli/IpmiCliTest.java index fab06f0..28ec558 100644 --- a/src/test/java/org/metricshub/ipmi/cli/IpmiCliTest.java +++ b/src/test/java/org/metricshub/ipmi/cli/IpmiCliTest.java @@ -266,6 +266,17 @@ void powerActionsAndIdentify() throws Exception { assertTrue(user.err.contains("use -L operator or -L administrator"), user.err); assertEquals("off\n", login(bmc, "power", "status").out, "the refused command did nothing"); + // -L auto: User, raised to Operator on the D4h; the ceiling of the account has its own hint + assertEquals(0, login(bmc, "-L", "auto", "power", "on").code); + assertEquals("on\n", login(bmc, "power", "status").out); + bmc.privilegeLimit(FakeRmcpPlusBmc.USER); + Result capped = login(bmc, "-L", "auto", "power", "off"); + assertEquals(IpmiCli.EXIT_AUTHENTICATION, capped.code, "D4h at the highest level of the account"); + assertTrue(capped.err.contains("at the highest level of the account"), capped.err); + assertFalse(capped.err.contains("-L operator"), capped.err); + assertEquals("on\n", login(bmc, "power", "status").out, "the refused command did nothing"); + bmc.privilegeLimit(ADMINISTRATOR); + assertEquals(0, login(bmc, "identify").code); assertArrayEquals(new byte[] { 15 }, bmc.getChassisIdentify(), "the default interval of the specification"); assertEquals(0, login(bmc, "identify", "5").code); diff --git a/src/test/java/org/metricshub/ipmi/client/IpmiClientTest.java b/src/test/java/org/metricshub/ipmi/client/IpmiClientTest.java index 6e75c10..afbc87f 100644 --- a/src/test/java/org/metricshub/ipmi/client/IpmiClientTest.java +++ b/src/test/java/org/metricshub/ipmi/client/IpmiClientTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -67,6 +68,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; import java.util.stream.Collectors; @@ -80,6 +82,7 @@ import org.metricshub.ipmi.core.coding.commands.sdr.record.OemRecord; import org.metricshub.ipmi.core.coding.commands.sdr.record.ReadingType; import org.metricshub.ipmi.core.coding.commands.sdr.record.SensorType; +import org.metricshub.ipmi.core.coding.commands.session.SetSessionPrivilegeLevel; import org.metricshub.ipmi.core.coding.commands.sdr.record.SensorUnit; import org.metricshub.ipmi.core.coding.commands.sel.EventDirection; import org.metricshub.ipmi.core.coding.commands.sel.SelRecordType; @@ -1283,6 +1286,30 @@ private static void holdsDeviceSdrsOnly(FakeRmcpPlusBmc bmc) { bmc.handle(NETFN_APP, GET_DEVICE_ID, data -> reply); } + @Test + @Timeout(30) + void aMaximumAvailableClientRaisesItsSessionForTheSolConsole() throws Exception { + try (FakeRmcpPlusBmc bmc = bmc()) { + bmc.enableSol(bmc.getPort(), 255, ADMINISTRATOR); + try (IpmiClient client = client(bmc).privilege(PrivilegeLevel.MaximumAvailable).build()) { + try (SolConsole console = client.sol().open()) { + assertEquals(ADMINISTRATOR, bmc.getPrivilege()); + assertEquals( + 3, + bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), + "User, then Operator and Administrator by the console"); + } + client.chassisControl(PowerCommand.PowerDown); + assertEquals( + 3, + bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), + "the client knows the level the console reached: no raise"); + assertFalse(bmc.isPowerOn()); + } + assertEquals(Collections.emptyList(), bmc.getErrors()); + } + } + @Test @Timeout(30) void aSolConsoleEndsWhenItsSessionIsDropped() throws Exception { @@ -1426,8 +1453,8 @@ void builderValidation() { assertThrows(IllegalArgumentException.class, () -> builder.retries(-1)); assertThrows(NullPointerException.class, () -> builder.credentials(null, new char[0])); assertThrows(NullPointerException.class, () -> builder.privilege(null)); - assertThrows(IllegalArgumentException.class, () -> builder.privilege(PrivilegeLevel.MaximumAvailable)); assertThrows(IllegalArgumentException.class, () -> builder.privilege(PrivilegeLevel.Callback)); + assertSame(builder, builder.privilege(PrivilegeLevel.MaximumAvailable), "raised on demand"); assertThrows(IllegalArgumentException.class, () -> builder.keepAlive(Duration.ofNanos(1))); assertThrows( IllegalArgumentException.class, @@ -1567,7 +1594,7 @@ void aBusyBmcThatCannotRaiseTheSessionIsNotAnAuthenticationFailure() throws Exce try (IpmiClient client = client(bmc).privilege(PrivilegeLevel.Operator).build()) { IpmiClientException busy = assertThrows(IpmiClientException.class, client::chassisStatus); assertEquals(IpmiClientException.class, busy.getClass()); - assertTrue(busy.getMessage().contains("could not raise the session"), busy.getMessage()); + assertTrue(busy.getMessage().contains("could not set the session"), busy.getMessage()); assertTrue(busy.getMessage().contains("(completion code C0h)"), busy.getMessage()); assertEquals(2, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), "busy is sent again"); } @@ -1616,6 +1643,145 @@ void aPrivilegeTheBmcRefusesFailsTheOperation() throws Exception { } } + @Test + @Timeout(30) + void maximumAvailableRunsAtUserAndRaisesTheSessionOnDemand() throws Exception { + try (FakeRmcpPlusBmc bmc = bmc()) { + // A BMC that starts the session at the requested maximum, and forwards Send Message at Administrator only + bmc.startsAtMaximum(true); + bmc.sendMessagePrivilege(ADMINISTRATOR); + byte[] sensor = compactSensorRecord( + 1, + 7, + EntityId.Processor.getCode(), + 1, + SensorType.Processor.getCode(), + 0x6f, + "S"); + new FakeSdrRepository().add(withOwner(sensor, 0x2c, 6, 0)).install(bmc); + bmc.satellite(6, 0x2c, NETFN_SENSOR, GET_SENSOR_READING, data -> new byte[] { 0x00, 0x30, 0x40, 0x00 }); + try (IpmiClient client = client(bmc).privilege(PrivilegeLevel.MaximumAvailable).build()) { + assertTrue(client.chassisStatus().powerOn()); + assertEquals(FakeRmcpPlusBmc.USER, bmc.getPrivilege(), "set to User right after the handshake"); + assertEquals(1, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL)); + + // Chassis Control sent raw: its D4h is returned, not thrown, and raises the session all the same + RawResponse raw = client.raw(0x00, 0x02).data(0x00).execute(); + assertEquals(CompletionCode.Ok, raw.completionCode(), "refused at User, sent again at Operator"); + assertEquals(0, bmc.getChassisControl()); + assertEquals(FakeRmcpPlusBmc.OPERATOR, bmc.getPrivilege()); + assertEquals(2, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL)); + assertEquals(2, bmc.getCount(NETFN_CHASSIS, CHASSIS_CONTROL)); + + client.chassisControl(PowerCommand.PowerUp); + assertEquals(1, bmc.getChassisControl()); + assertEquals(3, bmc.getCount(NETFN_CHASSIS, CHASSIS_CONTROL), "no D4h at Operator: sent once"); + assertEquals(2, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL)); + + SensorList sensors = client.sensors().execute(); + assertEquals(0x30, sensors.get(0).rawReading().getAsInt(), "read from its controller at Administrator"); + assertFalse(sensors.get(0).problem().isPresent(), sensors.get(0).problem().toString()); + assertEquals(ADMINISTRATOR, bmc.getPrivilege()); + assertEquals(3, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL)); + assertEquals(2, bmc.getCount(NETFN_APP, SEND_MESSAGE), "refused at Operator, sent again at Administrator"); + + // A Set Session Privilege Level the caller sends itself is tracked: User again, raised on the next D4h + client.send(new SetSessionPrivilegeLevel(PrivilegeLevel.User)); + assertEquals(FakeRmcpPlusBmc.USER, bmc.getPrivilege()); + client.chassisControl(PowerCommand.PowerDown); + assertEquals( + FakeRmcpPlusBmc.OPERATOR, + bmc.getPrivilege(), + "raised from User, not from the level reached before"); + assertEquals(5, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL)); + // A raw one too, whose reply gives the new level + assertEquals(CompletionCode.Ok, client.raw(0x06, 0x3b).data(0x04).execute().completionCode()); + assertEquals(ADMINISTRATOR, bmc.getPrivilege()); + client.sensors().execute(); + assertEquals(6, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), "at Administrator already: no raise"); + assertEquals(3, bmc.getCount(NETFN_APP, SEND_MESSAGE)); + + // Nothing above Administrator: the D4h reaches the caller + bmc.handle(NETFN_CHASSIS, CHASSIS_IDENTIFY, data -> new byte[] { (byte) 0xd4 }); + IpmiCompletionCodeException refused = assertThrows( + IpmiCompletionCodeException.class, + () -> client.chassisIdentify(5)); + assertEquals(CompletionCode.InsufficentPrivilege, refused.completionCode()); + assertEquals(6, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL)); + assertEquals(1, bmc.getCount(NETFN_CHASSIS, CHASSIS_IDENTIFY), "not sent again"); + assertEquals(1, bmc.getCount(OPEN_SESSION_REQUEST), "one session throughout"); + } + awaitClose(bmc); + assertEquals(Collections.emptyList(), bmc.getErrors()); + } + } + + @Test + @Timeout(30) + void theLevelTheBmcRefusesIsTheCeilingOfTheSession() throws Exception { + try (FakeRmcpPlusBmc bmc = bmc()) { + bmc.privilegeLimit(FakeRmcpPlusBmc.USER); + try (IpmiClient client = client(bmc).privilege(PrivilegeLevel.MaximumAvailable).build()) { + IpmiCompletionCodeException refused = assertThrows( + IpmiCompletionCodeException.class, + () -> client.chassisControl(PowerCommand.PowerDown)); + assertEquals( + CompletionCode.InsufficentPrivilege, + refused.completionCode(), + "the D4h, not the 81h of the raise"); + assertEquals(3, bmc.getCount(FakeRmcpPlusBmc.RAKP_1), "logged in at User after two refused roles"); + assertEquals(2, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), "User, then Operator refused (81h)"); + assertEquals(1, bmc.getCount(NETFN_CHASSIS, CHASSIS_CONTROL), "not sent again"); + assertTrue(bmc.isPowerOn()); + + assertThrows(IpmiCompletionCodeException.class, () -> client.chassisIdentify(5)); + assertEquals(2, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), "the ceiling is remembered"); + assertEquals(FakeRmcpPlusBmc.USER, bmc.getPrivilege()); + } + awaitClose(bmc); + assertEquals(Collections.emptyList(), bmc.getErrors()); + } + try (FakeRmcpPlusBmc bmc = bmc()) { + // A session the BMC revoked answers D4h to everything, Set Session Privilege Level included: asked once + try (IpmiClient client = client(bmc).privilege(PrivilegeLevel.MaximumAvailable).build()) { + assertTrue(client.chassisStatus().powerOn()); + bmc.handle(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL, data -> new byte[] { (byte) 0xd4 }); + bmc.handle(NETFN_CHASSIS, GET_CHASSIS_STATUS, data -> new byte[] { (byte) 0xd4 }); + for (int i = 0; i < 2; i++) { + IpmiCompletionCodeException refused = assertThrows(IpmiCompletionCodeException.class, client::chassisStatus); + assertEquals(CompletionCode.InsufficentPrivilege, refused.completionCode()); + } + assertEquals(2, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), "User, then one refused raise"); + assertEquals(3, bmc.getCount(NETFN_CHASSIS, GET_CHASSIS_STATUS)); + } + assertEquals(Collections.emptyList(), bmc.getErrors()); + } + try (FakeRmcpPlusBmc bmc = bmc()) { + // A busy BMC (C0h) fails the raise, thrown as such, and the raise is tried again at the next D4h: no ceiling + AtomicBoolean busy = new AtomicBoolean(true); + bmc + .handle(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL, data -> busy.get() ? new byte[] + { (byte) 0xc0 } : bmc.setSessionPrivilegeLevel(data)); + try (IpmiClient client = client(bmc).privilege(PrivilegeLevel.MaximumAvailable).build()) { + IpmiClientException failed = assertThrows(IpmiClientException.class, client::chassisStatus); + assertTrue(failed.getMessage().contains("could not set the session"), failed.getMessage()); + busy.set(false); + assertTrue(client.chassisStatus().powerOn(), "logged in on the next operation"); + busy.set(true); + IpmiCompletionCodeException refused = assertThrows( + IpmiCompletionCodeException.class, + () -> client.chassisControl(PowerCommand.PowerDown)); + assertEquals(CompletionCode.NodeBusy, refused.completionCode(), "the failure of the raise, not the D4h"); + assertTrue(bmc.isPowerOn()); + busy.set(false); + client.chassisControl(PowerCommand.PowerDown); + assertFalse(bmc.isPowerOn(), "raised at the next D4h"); + assertEquals(FakeRmcpPlusBmc.OPERATOR, bmc.getPrivilege()); + } + assertEquals(Collections.emptyList(), bmc.getErrors()); + } + } + @Test @Timeout(30) void chassisControlAndIdentifyNeedTheOperatorPrivilege() throws Exception { diff --git a/src/test/java/org/metricshub/ipmi/core/api/sol/SerialOverLanTest.java b/src/test/java/org/metricshub/ipmi/core/api/sol/SerialOverLanTest.java index e33ebbd..90fd0ff 100644 --- a/src/test/java/org/metricshub/ipmi/core/api/sol/SerialOverLanTest.java +++ b/src/test/java/org/metricshub/ipmi/core/api/sol/SerialOverLanTest.java @@ -33,10 +33,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.metricshub.ipmi.core.api.async.ConnectionHandle; +import org.metricshub.ipmi.core.api.sol.SOLException; import org.metricshub.ipmi.core.api.sync.IpmiConnector; import org.metricshub.ipmi.core.coding.commands.PrivilegeLevel; import org.metricshub.ipmi.core.coding.commands.chassis.GetChassisStatus; import org.metricshub.ipmi.core.coding.commands.chassis.GetChassisStatusResponseData; +import org.metricshub.ipmi.core.coding.commands.session.SetSessionPrivilegeLevel; +import org.metricshub.ipmi.core.coding.payload.lan.IPMIException; import org.metricshub.ipmi.core.coding.payload.sol.SolOperation; import org.metricshub.ipmi.core.coding.payload.sol.SolStatus; import org.metricshub.ipmi.core.coding.security.CipherSuite; @@ -746,12 +749,46 @@ void onlyASessionOpenedAtMaximumAvailableIsRaisedByTheConsole() throws Exception connector.closeSession(session.getConnectionHandle()); awaitClose(bmc); - // A session opened with MaximumAvailable still runs at User: the console raises it after the D4h + // A session opened with MaximumAvailable is set to User by openSession(): the console raises it after + // the D4h Session unraised = openSession(connector, bmc, null, PrivilegeLevel.MaximumAvailable); - assertEquals(1, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL)); + assertEquals(2, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), "set to User"); new SerialOverLan(connector, unraised).close(); - assertEquals(2, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL)); + assertEquals(3, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), "raised by the console"); + assertEquals(3, bmc.getCount(NETFN_APP, FakeRmcpPlusBmc.ACTIVATE_PAYLOAD)); + assertEquals(FakeRmcpPlusBmc.OPERATOR, bmc.getPrivilege(), "raised to the level the payload needs, no further"); + assertEquals(Collections.emptyList(), bmc.getErrors()); + } finally { + connector.tearDown(); + } + } + } + + @Test + @Timeout(30) + void theConsoleRaisesAMaximumAvailableSessionAsFarAsTheAccountAllows() throws Exception { + try (FakeRmcpPlusBmc bmc = new FakeRmcpPlusBmc(SUITES, USER, utf8(PASSWORD), null)) { + bmc.enableSol(bmc.getPort(), PAYLOAD_SIZE, ADMINISTRATOR); + IpmiConnector connector = new IpmiConnector(0, 0); + try { + // Operator, refused, then Administrator + Session session = openSession(connector, bmc, null, PrivilegeLevel.MaximumAvailable); + SerialOverLan console = new SerialOverLan(connector, session); + assertEquals(PrivilegeLevel.Administrator, console.getRaisedPrivilegeLevel()); assertEquals(3, bmc.getCount(NETFN_APP, FakeRmcpPlusBmc.ACTIVATE_PAYLOAD)); + assertEquals(3, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), "User, Operator, Administrator"); + console.close(); + connector.closeSession(session.getConnectionHandle()); + awaitClose(bmc); + + // An account limited to Operator: the raise to Administrator is refused, and the console fails on it + bmc.privilegeLimit(FakeRmcpPlusBmc.OPERATOR); + Session limited = openSession(connector, bmc, null, PrivilegeLevel.MaximumAvailable); + SOLException refused = assertThrows(SOLException.class, () -> new SerialOverLan(connector, limited)); + assertEquals( + SetSessionPrivilegeLevel.LEVEL_ABOVE_LIMIT, + ((IPMIException) refused.getCause()).getRawCode(), + refused.getMessage()); assertEquals(Collections.emptyList(), bmc.getErrors()); } finally { connector.tearDown(); diff --git a/src/test/java/org/metricshub/ipmi/core/api/sync/RmcpPlusSessionTest.java b/src/test/java/org/metricshub/ipmi/core/api/sync/RmcpPlusSessionTest.java index a192a85..24325b4 100644 --- a/src/test/java/org/metricshub/ipmi/core/api/sync/RmcpPlusSessionTest.java +++ b/src/test/java/org/metricshub/ipmi/core/api/sync/RmcpPlusSessionTest.java @@ -28,6 +28,7 @@ import org.metricshub.ipmi.core.coding.commands.chassis.ChassisIdentify; import org.metricshub.ipmi.core.coding.commands.chassis.GetChassisStatusResponseData; import org.metricshub.ipmi.core.coding.commands.session.GetSessionInfo; +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.security.CipherSuite; @@ -409,6 +410,38 @@ void aRefusedPrivilegeFailsOpenSessionAndClosesTheSession() throws Exception { } } + @Test + @Timeout(30) + void openSessionSetsAMaximumAvailableSessionToUser() throws Exception { + try (FakeRmcpPlusBmc bmc = new FakeRmcpPlusBmc(XCC_SUITES, USER, utf8(PASSWORD), null)) { + // A BMC that starts the session at the requested maximum, and an account limited to Operator + bmc.startsAtMaximum(true); + bmc.privilegeLimit(FakeRmcpPlusBmc.OPERATOR); + IpmiConnector connector = new IpmiConnector(0, 0); + try { + ConnectionHandle handle = openSession(connector, bmc, 17, PASSWORD, null, PrivilegeLevel.MaximumAvailable); + assertEquals(2, bmc.getCount(FakeRmcpPlusBmc.RAKP_1), "Administrator refused to the account, then Operator"); + assertEquals(1, bmc.getCount(NETFN_APP, SET_SESSION_PRIVILEGE_LEVEL), "set to User after the handshake"); + assertEquals(FakeRmcpPlusBmc.USER, bmc.getPrivilege()); + int sessionId = connector + .getExistingSessionForCriteria(bmc.getAddress(), bmc.getPort(), USER) + .getManagedSystemSessionId(); + assertEquals(2, connector.sendMessage(handle, new GetSessionInfo(sessionId)).getPrivilegeLevel()); + + // The caller raises it when a command needs more, up to the limit of the account + connector.sendMessage(handle, new SetSessionPrivilegeLevel(PrivilegeLevel.Operator)); + assertEquals(FakeRmcpPlusBmc.OPERATOR, bmc.getPrivilege()); + IPMIException refused = assertThrows( + IPMIException.class, + () -> connector.sendMessage(handle, new SetSessionPrivilegeLevel(PrivilegeLevel.Administrator))); + assertEquals(SetSessionPrivilegeLevel.LEVEL_ABOVE_LIMIT, refused.getRawCode()); + assertEquals(Collections.emptyList(), bmc.getErrors()); + } finally { + connector.tearDown(); + } + } + } + @Test @Timeout(30) void aLostReplyToSetSessionPrivilegeLevelIsSentAgain() throws Exception { diff --git a/src/test/java/org/metricshub/ipmi/core/transport/FakeRmcpPlusBmc.java b/src/test/java/org/metricshub/ipmi/core/transport/FakeRmcpPlusBmc.java index b5b86df..308fc10 100644 --- a/src/test/java/org/metricshub/ipmi/core/transport/FakeRmcpPlusBmc.java +++ b/src/test/java/org/metricshub/ipmi/core/transport/FakeRmcpPlusBmc.java @@ -215,6 +215,12 @@ public enum Fault { private volatile int requestLun; private volatile Session session; private volatile SocketAddress console; + /** The highest privilege of the account: what a RAKP Message 1 role of 0 (highest available) gets. */ + private volatile int privilegeLimit = ADMINISTRATOR; + /** Starts the session at its maximum instead of User, as an IMM, iDRAC 6, iRMC, GIGABYTE or Supermicro does. */ + private volatile boolean startsAtMaximum; + /** The privilege Send Message to another controller needs: User like an iLO 5 by default, Administrator on AMI. */ + private volatile int sendMessagePrivilege = USER; private volatile boolean powerOn = true; /** The chassis control of the last Chassis Control the BMC accepted, -1 before the first. */ @@ -406,6 +412,43 @@ public void bridgeReply(BridgeReply reply) { bridgeReply = reply; } + /** + * Sets the highest privilege of the account, {@link #ADMINISTRATOR} by default: a RAKP Message 1 role of 0 gets + * it, and Set Session Privilege Level above it is refused with {@code 81h}. + * + * @param limit the privilege level + */ + public void privilegeLimit(int limit) { + privilegeLimit = limit; + } + + /** + * Starts every session at the maximum requested in RAKP Message 1 instead of User, as an IMM, an iDRAC 6, an iRMC, + * a GIGABYTE or a Supermicro BMC does (against IPMI 2.0 section 22.18). + * + * @param enabled whether the session starts at its maximum + */ + public void startsAtMaximum(boolean enabled) { + startsAtMaximum = enabled; + } + + /** + * Sets the privilege Send Message to another controller needs; {@link #USER} by default, like an HPE iLO 5, where + * an AMI-based BMC needs {@link #ADMINISTRATOR}. + * + * @param privilege the privilege level + */ + public void sendMessagePrivilege(int privilege) { + sendMessagePrivilege = privilege; + } + + /** + * @return the privilege level of the current session, as the BMC holds it + */ + public int getPrivilege() { + return session.privilege; + } + /** * @return the IPMB requests the BMC received in Send Message requests, in order */ @@ -806,7 +849,14 @@ private byte[] rakp2(byte[] request) throws GeneralSecurityException { } current.rm = Arrays.copyOfRange(request, 8, 24); // RoleM: the maximum privilege requested, 0 for the highest one the user has - current.maxPrivilege = (request[24] & 0x0f) == 0 ? ADMINISTRATOR : request[24] & 0x0f; + int role = request[24] & 0x0f; + if (role > privilegeLimit) { + return out.put((byte) 0x0a).putShort((short) 0).putInt(current.consoleId).array(); // unauthorized role + } + current.maxPrivilege = role == 0 ? privilegeLimit : role; + if (startsAtMaximum) { + current.privilege = current.maxPrivilege; + } current.rc = new byte[16]; random.nextBytes(current.rc); current.identity = concat(new byte[] { request[24], request[27] }, name); @@ -1107,8 +1157,11 @@ private byte[] getSessionInfo(byte[] data) { /** * Set Session Privilege Level (IPMI 2.0 table 22-24): up to the privilege requested in RAKP Message 1. + * + * @param data the request data + * @return the reply */ - private byte[] setSessionPrivilegeLevel(byte[] data) { + public byte[] setSessionPrivilegeLevel(byte[] data) { Session current = session; int requested = data[0] & 0x0f; if (requested > current.maxPrivilege) { @@ -1236,6 +1289,9 @@ private byte[] sendMessage(byte[] data) { errors.add("Bad Send Message request: " + hex(data)); return new byte[] { (byte) 0xcc }; // invalid data field } + if (session.privilege < sendMessagePrivilege) { + return new byte[] { (byte) 0xd4 }; // insufficient privilege + } bridgedRequests.add(request); int channel = data[0] & 0x0f; int address = request[0] & 0xff;