1 package org.metricshub.ipmi.core.api.sync; 2 3 /*- 4 * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ 5 * IPMI Java Client 6 * ჻჻჻჻჻჻ 7 * Copyright 2023 Verax Systems, MetricsHub 8 * ჻჻჻჻჻჻ 9 * This program is free software: you can redistribute it and/or modify 10 * it under the terms of the GNU Lesser General Public License as 11 * published by the Free Software Foundation, either version 3 of the 12 * License, or (at your option) any later version. 13 * 14 * This program is distributed in the hope that it will be useful, 15 * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 * GNU General Lesser Public License for more details. 18 * 19 * You should have received a copy of the GNU General Lesser Public 20 * License along with this program. If not, see 21 * <http://www.gnu.org/licenses/lgpl-3.0.html>. 22 * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ 23 */ 24 25 import org.metricshub.ipmi.core.api.async.ConnectionHandle; 26 import org.metricshub.ipmi.core.api.async.InboundMessageListener; 27 import org.metricshub.ipmi.core.api.async.IpmiAsyncConnector; 28 import org.metricshub.ipmi.core.coding.PayloadCoder; 29 import org.metricshub.ipmi.core.coding.commands.PrivilegeLevel; 30 import org.metricshub.ipmi.core.coding.commands.ResponseData; 31 import org.metricshub.ipmi.core.coding.commands.session.GetChannelAuthenticationCapabilitiesResponseData; 32 import org.metricshub.ipmi.core.coding.payload.CompletionCode; 33 import org.metricshub.ipmi.core.coding.payload.lan.IPMIException; 34 import org.metricshub.ipmi.core.coding.protocol.PayloadType; 35 import org.metricshub.ipmi.core.coding.security.CipherSuite; 36 import org.metricshub.ipmi.core.common.Constants; 37 import org.metricshub.ipmi.core.common.PropertiesManager; 38 import org.metricshub.ipmi.core.connection.Connection; 39 import org.metricshub.ipmi.core.connection.ConnectionException; 40 import org.metricshub.ipmi.core.connection.ConnectionManager; 41 import org.metricshub.ipmi.core.connection.Session; 42 43 import org.slf4j.Logger; 44 import org.slf4j.LoggerFactory; 45 46 import java.io.FileNotFoundException; 47 import java.io.IOException; 48 import java.net.InetAddress; 49 import java.util.List; 50 import java.util.Random; 51 52 /** 53 * <p> Synchronous API for connecting to BMC via IPMI. </p> <br>Creating connection consists of the following steps: 54 * <ul> 55 * <li>Create {@link Connection} and get associated with it {@link ConnectionHandle} via 56 * {@link #createConnection(InetAddress)}</li> <li>Get {@link CipherSuite}s that are available for the connection via 57 * {@link #getAvailableCipherSuites(ConnectionHandle)}</li> <li>Pick {@link CipherSuite} and {@link PrivilegeLevel} that will 58 * be used during session and get {@link GetChannelAuthenticationCapabilitiesResponseData} to find out allowed 59 * authentication options via 60 * {@link #getChannelAuthenticationCapabilities(ConnectionHandle, CipherSuite, PrivilegeLevel)} </li><li>Provide username, 61 * password and (if the BMC needs it) the BMC Kg key and start session via 62 * {@link #openSession(ConnectionHandle, String, String, byte[])} </li> 63 * </ul> 64 * <br> 65 * <p> Send message register via 66 * {@link #sendMessage(ConnectionHandle, PayloadCoder)} </p> <br> <p> To close session call 67 * {@link #closeSession(ConnectionHandle)} </p> <p> When done with work, clean up via {@link #tearDown()} </p> <br> 68 */ 69 public class IpmiConnector { 70 71 private static Logger logger = LoggerFactory.getLogger(IpmiConnector.class); 72 73 private IpmiAsyncConnector asyncConnector; 74 75 private int retries; 76 77 private int idleTime; 78 79 private Random random = new Random(System.currentTimeMillis()); 80 81 /** 82 * Starts {@link IpmiConnector} and initiates the {@link ConnectionManager} at the given port. Wildcard IP address 83 * will be used. 84 * @param port 85 * - the port that will be used by {@link IpmiAsyncConnector} to communicate with the remote hosts. 86 * @throws FileNotFoundException 87 * when properties file was not found 88 * @throws IOException 89 * when properties file was not found 90 */ 91 public IpmiConnector(int port) throws IOException { 92 asyncConnector = new IpmiAsyncConnector(port); 93 loadProperties(); 94 } 95 96 /** 97 * Starts {@link IpmiConnector} and initiates the {@link ConnectionManager} at the given port and IP interface. 98 * @param port 99 * - the port that will be used by {@link IpmiAsyncConnector} to communicate with the remote hosts. 100 * @param address 101 * - the IP address that will be used by {@link IpmiAsyncConnector} to communicate with the remote hosts. 102 * @throws FileNotFoundException 103 * when properties file was not found 104 * @throws IOException 105 * when properties file was not found 106 */ 107 public IpmiConnector(int port, InetAddress address) throws IOException { 108 asyncConnector = new IpmiAsyncConnector(port, address); 109 loadProperties(); 110 } 111 112 /** 113 * Starts {@link IpmiConnector} and initiates the {@link ConnectionManager} at 114 * the given port. Wildcard IP address will be used. 115 * 116 * @param port the port that will be used by {@link IpmiAsyncConnector} to 117 * communicate with the remote hosts. 118 * @param pingPeriod the period in milliseconds used to send the keep alive 119 * messages.<br> 120 * 0 If keep-alive messages should be disabled. 121 * @throws IOException When IpmiAsyncConnector cannot be created. 122 */ 123 public IpmiConnector(int port, long pingPeriod) throws IOException { 124 asyncConnector = new IpmiAsyncConnector(port, pingPeriod); 125 loadProperties(); 126 } 127 128 /** 129 * Loads properties from the properties file. 130 */ 131 private void loadProperties() { 132 PropertiesManager manager = PropertiesManager.getInstance(); 133 retries = Integer.parseInt(manager.getProperty("retries")); 134 idleTime = Integer.parseInt(manager.getProperty("idleTime")); 135 } 136 137 /** 138 * Creates connection to the remote host on default IPMI port. 139 * @param address 140 * - {@link InetAddress} of the remote host 141 * @return handle to the connection to the remote host 142 * @throws IOException 143 * when properties file was not found 144 * @throws FileNotFoundException 145 * when properties file was not found 146 */ 147 public ConnectionHandle createConnection(InetAddress address) throws IOException { 148 return createConnection(address, Constants.IPMI_PORT); 149 } 150 151 /** 152 * Creates connection to the remote host on specified port. 153 * @param address 154 * - {@link InetAddress} of the remote host 155 * @param port 156 * - remote UDP port 157 * @return handle to the connection to the remote host 158 * @throws IOException 159 * when properties file was not found 160 * @throws FileNotFoundException 161 * when properties file was not found 162 */ 163 public ConnectionHandle createConnection(InetAddress address, int port) throws IOException { 164 return asyncConnector.createConnection(address, port); 165 } 166 167 /** 168 * Creates connection to the remote host, with pre set {@link CipherSuite} and {@link PrivilegeLevel}, skipping the 169 * getAvailableCipherSuites and getChannelAuthenticationCapabilities phases. 170 * @param address 171 * - {@link InetAddress} of the remote host 172 * @return handle to the connection to the remote host 173 * @throws IOException 174 * when properties file was not found 175 * @throws FileNotFoundException 176 * when properties file was not found 177 */ 178 public ConnectionHandle createConnection(InetAddress address, CipherSuite cipherSuite, PrivilegeLevel privilegeLevel) 179 throws IOException { 180 return createConnection(address, Constants.IPMI_PORT, cipherSuite, privilegeLevel); 181 } 182 183 /** 184 * Creates connection to the remote host, with pre set {@link CipherSuite} and {@link PrivilegeLevel}, skipping the 185 * getAvailableCipherSuites and getChannelAuthenticationCapabilities phases. 186 * @param address 187 * - {@link InetAddress} of the remote host 188 * @param port 189 * - remote UDP port 190 * @return handle to the connection to the remote host 191 * @throws IOException 192 * when properties file was not found 193 * @throws FileNotFoundException 194 * when properties file was not found 195 */ 196 public ConnectionHandle createConnection(InetAddress address, int port, CipherSuite cipherSuite, PrivilegeLevel privilegeLevel) 197 throws IOException { 198 return asyncConnector.createConnection(address, port, cipherSuite, privilegeLevel); 199 } 200 201 /** 202 * Gets {@link CipherSuite}s available for the connection with the remote host. 203 * @param connectionHandle 204 * {@link ConnectionHandle} to the connection created before 205 * @see #createConnection(InetAddress) 206 * @return list of the {@link CipherSuite}s that are allowed during the connection 207 * @throws Exception 208 * when sending message to the managed system fails 209 */ 210 public List<CipherSuite> getAvailableCipherSuites(ConnectionHandle connectionHandle) throws Exception { 211 return asyncConnector.getAvailableCipherSuites(connectionHandle); 212 } 213 214 /** 215 * Gets the authentication capabilities for the connection with the remote host. 216 * @param connectionHandle 217 * - {@link ConnectionHandle} associated with the host 218 * @param cipherSuite 219 * - {@link CipherSuite} that will be used during the connection 220 * @param requestedPrivilegeLevel 221 * - {@link PrivilegeLevel} that is requested for the session 222 * @return - {@link GetChannelAuthenticationCapabilitiesResponseData} 223 * @throws ConnectionException 224 * when connection is in the state that does not allow to perform this operation. 225 * @throws Exception 226 * when sending message to the managed system fails 227 */ 228 public GetChannelAuthenticationCapabilitiesResponseData getChannelAuthenticationCapabilities( 229 ConnectionHandle connectionHandle, CipherSuite cipherSuite, PrivilegeLevel requestedPrivilegeLevel) 230 throws Exception { 231 return asyncConnector.getChannelAuthenticationCapabilities(connectionHandle, cipherSuite, 232 requestedPrivilegeLevel); 233 } 234 235 /** 236 * Establishes the session with the remote host. 237 * @param connectionHandle 238 * - {@link ConnectionHandle} associated with the remote host. 239 * @param username 240 * - the username 241 * @param password 242 * - password matching the username 243 * @param bmcKey 244 * - the key that should be provided if the two-key authentication is enabled, null otherwise. 245 * 246 * @return object representing newly created {@link Session} 247 * 248 * @throws ConnectionException 249 * when connection is in the state that does not allow to perform this operation. 250 * @throws Exception 251 * when sending message to the managed system or initializing one of the cipherSuite's algorithms fails 252 */ 253 public Session openSession(ConnectionHandle connectionHandle, String username, String password, byte[] bmcKey) 254 throws Exception { 255 return asyncConnector.openSession(connectionHandle, username, password, bmcKey); 256 } 257 258 /** 259 * Returns session already bound to given connection handle fulfilling given criteria. 260 * 261 * @param remoteAddress 262 * IP addres of the managed system 263 * @param remotePort 264 * UDP port of the managed system 265 * @param user 266 * IPMI user for whom the connection is established 267 * @return session object fulfilling given criteria, or null if no session was registered for such connection. 268 */ 269 public Session getExistingSessionForCriteria(InetAddress remoteAddress, int remotePort, String user) { 270 return asyncConnector.getExistingSessionForCriteria(remoteAddress, remotePort, user); 271 } 272 273 /** 274 * Closes the session with the remote host if it is currently in open state. 275 * @param connectionHandle 276 * - {@link ConnectionHandle} associated with the remote host. 277 * @throws ConnectionException 278 * when connection is in the state that does not allow to perform this operation. 279 * @throws Exception 280 * when sending message to the managed system or initializing one of the cipherSuite's algorithms fails 281 */ 282 public void closeSession(ConnectionHandle connectionHandle) throws Exception { 283 asyncConnector.closeSession(connectionHandle); 284 } 285 286 /** 287 * Sends the IPMI message to the remote host. 288 * @param connectionHandle 289 * - {@link ConnectionHandle} associated with the remote host. 290 * @param request 291 * - {@link PayloadCoder} containing the request to be sent 292 * @return {@link ResponseData} for the <b>request</b> 293 * @throws ConnectionException 294 * when connection is in the state that does not allow to perform this operation. 295 * @throws Exception 296 * when sending message to the managed system or initializing one of the cipherSuite's algorithms fails 297 */ 298 public ResponseData sendMessage(ConnectionHandle connectionHandle, PayloadCoder request) throws Exception { 299 return sendMessage(connectionHandle, request, true); 300 } 301 302 /** 303 * Sends the IPMI message to the remote host and doesn't wait for any response. 304 * @param connectionHandle 305 * - {@link ConnectionHandle} associated with the remote host. 306 * @param request 307 * - {@link PayloadCoder} containing the request to be sent 308 * @throws ConnectionException 309 * when connection is in the state that does not allow to perform this operation. 310 * @throws Exception 311 * when sending message to the managed system or initializing one of the cipherSuite's algorithms fails 312 */ 313 public void sendOneWayMessage(ConnectionHandle connectionHandle, PayloadCoder request) throws Exception { 314 sendMessage(connectionHandle, request, false); 315 } 316 317 /** 318 * Re-sends message with given tag having given {@link PayloadType}, using passed {@link ConnectionHandle}. 319 * 320 * @param connectionHandle 321 * - {@link ConnectionHandle} associated with the remote host. 322 * @param tag 323 * - tag of the message to retry 324 * @param messagePayloadType 325 * - {@link PayloadType} of the message that should be retried 326 * @return {@link ResponseData} for the re-sent request or null if message could not be resent. 327 * @throws Exception 328 * when sending message to the managed system fails 329 */ 330 public ResponseData retryMessage(ConnectionHandle connectionHandle, byte tag, PayloadType messagePayloadType) throws Exception { 331 MessageListener listener = new MessageListener(connectionHandle); 332 asyncConnector.registerListener(listener); 333 334 int retryResult = asyncConnector.retry(connectionHandle, tag, messagePayloadType); 335 336 ResponseData data = retryResult != -1 ? listener.waitForAnswer(tag) : null; 337 338 asyncConnector.unregisterListener(listener); 339 340 return data; 341 } 342 343 private ResponseData sendMessage(ConnectionHandle connectionHandle, PayloadCoder request, boolean waitForResponse) throws Exception { 344 MessageListener listener = new MessageListener(connectionHandle); 345 346 if (waitForResponse) { 347 asyncConnector.registerListener(listener); 348 } 349 350 ResponseData data = sendThroughAsyncConnector(request, connectionHandle, listener, waitForResponse); 351 352 if (waitForResponse) { 353 asyncConnector.unregisterListener(listener); 354 } 355 356 return data; 357 } 358 359 private ResponseData sendThroughAsyncConnector(PayloadCoder request, ConnectionHandle connectionHandle, 360 MessageListener listener, boolean waitForResponse) throws Exception { 361 ResponseData responseData = null; 362 363 int tries = 0; 364 int tag = -1; 365 boolean messageSent = false; 366 367 while (!messageSent) { 368 try { 369 ++tries; 370 371 if (tag >= 0) { 372 tag = asyncConnector.retry(connectionHandle, tag, request.getSupportedPayloadType()); 373 } 374 375 if (tag < 0){ 376 tag = asyncConnector.sendMessage(connectionHandle, request, !waitForResponse); 377 } 378 379 logger.debug("Sending message with tag {}, try {}", tag, tries); 380 381 if (waitForResponse) { 382 responseData = listener.waitForAnswer(tag); 383 } 384 385 messageSent = true; 386 } catch (IllegalArgumentException e) { 387 throw e; 388 } catch (IPMIException e) { 389 handleErrorResponse(tries, e); 390 } catch (Exception e) { 391 handleRetriesWhenException(tries, e); 392 } 393 } 394 395 return responseData; 396 } 397 398 private void handleRetriesWhenException(int tries, Exception e) throws Exception { 399 if (tries > retries) { 400 throw e; 401 } else { 402 long sleepTime = (random.nextLong() % (idleTime / 2)) + (idleTime / 2); 403 404 Thread.sleep(sleepTime); 405 logger.warn("Receiving message failed, retrying", e); 406 } 407 } 408 409 private void handleErrorResponse(int tries, IPMIException e) throws Exception { 410 if (e.getCompletionCode() == CompletionCode.InitializationInProgress 411 || e.getCompletionCode() == CompletionCode.InsufficientResources 412 || e.getCompletionCode() == CompletionCode.NodeBusy 413 || e.getCompletionCode() == CompletionCode.Timeout) { 414 415 handleRetriesWhenException(tries, e); 416 } else { 417 throw e; 418 } 419 } 420 421 /** 422 * Registers {@link InboundMessageListener} that will react on any request sent from remote system to the application. 423 * 424 * @param listener 425 * listener to be registered. 426 */ 427 public void registerIncomingMessageListener(InboundMessageListener listener) { 428 asyncConnector.registerIncomingPayloadListener(listener); 429 } 430 431 /** 432 * Closes the connection with the given handle 433 */ 434 public void closeConnection(ConnectionHandle handle) { 435 asyncConnector.closeConnection(handle); 436 } 437 438 /** 439 * Finalizes the connector and closes all connections. 440 */ 441 public void tearDown() { 442 asyncConnector.tearDown(); 443 } 444 445 /** 446 * Changes the timeout value for connection with the given handle. 447 * @param handle 448 * - {@link ConnectionHandle} associated with the remote host. 449 * @param timeout 450 * - new timeout value in ms 451 */ 452 public void setTimeout(ConnectionHandle handle, int timeout) { 453 asyncConnector.setTimeout(handle, timeout); 454 } 455 456 /** 457 * Returns configured number of retries. 458 * 459 * @return number of retries when message could not be sent 460 */ 461 public int getRetries() { 462 return retries; 463 } 464 }