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 }