1 package org.metricshub.ipmi.core.api.async;
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.messages.IpmiError;
26 import org.metricshub.ipmi.core.api.async.messages.IpmiResponse;
27 import org.metricshub.ipmi.core.api.async.messages.IpmiResponseData;
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.IpmiPayload;
33 import org.metricshub.ipmi.core.coding.protocol.PayloadType;
34 import org.metricshub.ipmi.core.coding.security.CipherSuite;
35 import org.metricshub.ipmi.core.common.PropertiesManager;
36 import org.metricshub.ipmi.core.connection.Connection;
37 import org.metricshub.ipmi.core.connection.ConnectionException;
38 import org.metricshub.ipmi.core.connection.ConnectionListener;
39 import org.metricshub.ipmi.core.connection.ConnectionManager;
40 import org.metricshub.ipmi.core.connection.Session;
41 import org.metricshub.ipmi.core.connection.SessionManager;
42
43 import java.io.FileNotFoundException;
44 import java.io.IOException;
45 import java.net.InetAddress;
46 import java.util.ArrayList;
47 import java.util.List;
48
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 /**
53 * <p>
54 * Asynchronous API for connecting to BMC via IPMI.
55 * </p>
56 * Creating connection consists of the following steps:
57 * <ul>
58 * <li>Create {@link Connection} and get associated with it
59 * {@link ConnectionHandle} via {@link #createConnection(InetAddress, int)}</li>
60 * <li>Get {@link CipherSuite}s that are available for the connection via
61 * {@link #getAvailableCipherSuites(ConnectionHandle)}</li>
62 * <li>Pick {@link CipherSuite} and {@link PrivilegeLevel} that will be used
63 * during session and get
64 * {@link GetChannelAuthenticationCapabilitiesResponseData} to find out allowed
65 * authentication options via
66 * {@link #getChannelAuthenticationCapabilities(ConnectionHandle, CipherSuite, PrivilegeLevel)}</li>
67 * <li>Provide username, password and (if the BMC needs it) the BMC Kg key and
68 * start session via
69 * {@link #openSession(ConnectionHandle, String, String, byte[])}</li>
70 * </ul>
71 * <br>
72 * <p>
73 * To send message register for receiving answers via
74 * {@link #registerListener(IpmiResponseListener)} and send message via
75 * {@link #sendMessage(ConnectionHandle, PayloadCoder, boolean)}
76 * </p>
77 * <br>
78 * <p>
79 * To close session call {@link #closeSession(ConnectionHandle)}
80 * </p>
81 * <br>
82 */
83 public class IpmiAsyncConnector implements ConnectionListener {
84 public static final String FAILED_TO_RECEIVE_ANSWER_CAUSE_MESSAGE = "Failed to receive answer, cause:";
85 private ConnectionManager connectionManager;
86 private SessionManager sessionManager;
87 private int retries;
88 private final List<IpmiResponseListener> responseListeners;
89 private final List<InboundMessageListener> inboundMessageListeners;
90
91 private static Logger logger = LoggerFactory.getLogger(IpmiAsyncConnector.class);
92
93 /**
94 * Starts {@link IpmiAsyncConnector} and initiates the
95 * {@link ConnectionManager} at the given port. The wildcard IP address will
96 * be used.
97 *
98 * @param port
99 * - the port that will be used by {@link IpmiAsyncConnector} to
100 * communicate with the remote hosts.
101 * @throws IOException
102 * when properties file was not found
103 */
104 public IpmiAsyncConnector(int port) throws IOException {
105 responseListeners = new ArrayList<IpmiResponseListener>();
106 inboundMessageListeners = new ArrayList<InboundMessageListener>();
107 connectionManager = new ConnectionManager(port);
108 sessionManager = new SessionManager();
109 loadProperties();
110 }
111
112 /**
113 * Starts {@link IpmiAsyncConnector} and initiates the
114 * {@link ConnectionManager} at the given port and IP interface.
115 *
116 * @param port
117 * - the port that will be used by {@link IpmiAsyncConnector} to
118 * communicate with the remote hosts.
119 * @param address
120 * - the IP address that will be used by
121 * {@link IpmiAsyncConnector} to communicate with the remote
122 * hosts.
123 * @throws IOException
124 * when properties file was not found
125 */
126 public IpmiAsyncConnector(int port, InetAddress address) throws IOException {
127 responseListeners = new ArrayList<IpmiResponseListener>();
128 inboundMessageListeners = new ArrayList<InboundMessageListener>();
129 connectionManager = new ConnectionManager(port, address);
130 sessionManager = new SessionManager();
131 loadProperties();
132 }
133
134 /**
135 * Starts {@link IpmiAsyncConnector} and initiates the {@link ConnectionManager}
136 * at the given port and ping period.
137 *
138 * @param port the port that will be used by {@link IpmiAsyncConnector} to
139 * communicate with the remote hosts.
140 * @param pingPeriod the period between sending keep-alive messages to the
141 * remote host.
142 * @throws IOException When ConnectionManager cannot be created due to an IO
143 * error.
144 */
145 public IpmiAsyncConnector(int port, long pingPeriod) throws IOException {
146 responseListeners = new ArrayList<>();
147 inboundMessageListeners = new ArrayList<>();
148 connectionManager = new ConnectionManager(port, pingPeriod);
149 sessionManager = new SessionManager();
150 loadProperties();
151 }
152
153 /**
154 * Loads properties from the properties file.
155 */
156 private void loadProperties() {
157 retries = Integer.parseInt(PropertiesManager.getInstance().getProperty("retries"));
158 }
159
160 /**
161 * Creates connection to the remote host.
162 *
163 * @param address
164 * - {@link InetAddress} of the remote host
165 * @return handle to the connection to the remote host
166 * @throws IOException
167 * when properties file was not found
168 * @throws FileNotFoundException
169 * when properties file was not found
170 */
171 public ConnectionHandle createConnection(InetAddress address, int port)
172 throws IOException {
173 int handle = connectionManager.createConnection(address, port);
174 connectionManager.getConnection(handle).registerListener(this);
175 return new ConnectionHandle(handle, address, port);
176 }
177
178 /**
179 * Creates connection to the remote host, with pre set {@link CipherSuite} and {@link PrivilegeLevel}, skipping the
180 * getAvailableCipherSuites and getChannelAuthenticationCapabilities phases.
181 * @param address
182 * - {@link InetAddress} of the remote host
183 * @return handle to the connection to the remote host
184 * @throws IOException
185 * when properties file was not found
186 * @throws FileNotFoundException
187 * when properties file was not found
188 */
189 public ConnectionHandle createConnection(InetAddress address, int port, CipherSuite cipherSuite, PrivilegeLevel privilegeLevel)
190 throws IOException {
191 int handle = connectionManager.createConnection(address, port, true);
192 connectionManager.getConnection(handle).registerListener(this);
193
194 ConnectionHandle connectionHandle = new ConnectionHandle(handle, address, port);
195 connectionHandle.setCipherSuite(cipherSuite);
196 connectionHandle.setPrivilegeLevel(privilegeLevel);
197
198 return connectionHandle;
199 }
200
201 /**
202 * Gets {@link CipherSuite}s available for the connection with the remote
203 * host.
204 *
205 * @param connectionHandle
206 * {@link ConnectionHandle} to the connection created before
207 * @see #createConnection(InetAddress, int)
208 * @return list of the {@link CipherSuite}s that are allowed during the
209 * connection
210 * @throws Exception
211 * when sending message to the managed system fails
212 */
213 public List<CipherSuite> getAvailableCipherSuites(
214 ConnectionHandle connectionHandle) throws Exception {
215 int tries = 0;
216 List<CipherSuite> result = null;
217 while (tries <= retries && result == null) {
218 try {
219 ++tries;
220 result = connectionManager
221 .getAvailableCipherSuites(connectionHandle.getHandle());
222 } catch (Exception e) {
223 logger.warn(FAILED_TO_RECEIVE_ANSWER_CAUSE_MESSAGE, e);
224 if (tries > retries) {
225 throw e;
226 }
227 }
228 }
229 return result;
230 }
231
232 /**
233 * Gets the authentication capabilities for the connection with the remote
234 * host.
235 *
236 * @param connectionHandle
237 * - {@link ConnectionHandle} associated with the host
238 * @param cipherSuite
239 * - {@link CipherSuite} that will be used during the connection
240 * @param requestedPrivilegeLevel
241 * - {@link PrivilegeLevel} that is requested for the session
242 * @return - {@link GetChannelAuthenticationCapabilitiesResponseData}
243 * @throws ConnectionException
244 * when connection is in the state that does not allow to
245 * perform this operation.
246 * @throws Exception
247 * when sending message to the managed system fails
248 */
249 public GetChannelAuthenticationCapabilitiesResponseData getChannelAuthenticationCapabilities(
250 ConnectionHandle connectionHandle, CipherSuite cipherSuite,
251 PrivilegeLevel requestedPrivilegeLevel) throws Exception {
252 int tries = 0;
253 GetChannelAuthenticationCapabilitiesResponseData result = null;
254 while (tries <= retries && result == null) {
255 try {
256 ++tries;
257 result = connectionManager
258 .getChannelAuthenticationCapabilities(
259 connectionHandle.getHandle(), cipherSuite,
260 requestedPrivilegeLevel);
261 connectionHandle.setCipherSuite(cipherSuite);
262 connectionHandle.setPrivilegeLevel(requestedPrivilegeLevel);
263 } catch (Exception e) {
264 logger.warn(FAILED_TO_RECEIVE_ANSWER_CAUSE_MESSAGE, e);
265 if (tries > retries) {
266 throw e;
267 }
268 }
269 }
270 return result;
271 }
272
273 /**
274 * Establishes the session with the remote host.
275 *
276 * @param connectionHandle
277 * - {@link ConnectionHandle} associated with the remote host.
278 * @param username
279 * - the username
280 * @param password
281 * - password matching the username
282 * @param bmcKey
283 * - the key that should be provided if the two-key
284 * authentication is enabled, null otherwise.
285 * @throws ConnectionException
286 * when connection is in the state that does not allow to
287 * perform this operation.
288 * @throws Exception
289 * when sending message to the managed system or initializing
290 * one of the cipherSuite's algorithms fails
291 */
292 public Session openSession(ConnectionHandle connectionHandle, String username,
293 String password, byte[] bmcKey) throws Exception {
294 Session session = null;
295 int tries = 0;
296 boolean succeded = false;
297
298 connectionHandle.setUser(username);
299 connectionHandle.setPassword(password);
300
301 while (tries <= retries && !succeded) {
302 try {
303 ++tries;
304 int sessionId = connectionManager.startSession(connectionHandle.getHandle(),
305 connectionHandle.getCipherSuite(),
306 connectionHandle.getPrivilegeLevel(), username,
307 password, bmcKey);
308
309 session = sessionManager.registerSession(sessionId, connectionHandle);
310
311 succeded = true;
312 } catch (Exception e) {
313 logger.warn(FAILED_TO_RECEIVE_ANSWER_CAUSE_MESSAGE, e);
314 if (tries > retries) {
315 throw e;
316 }
317 }
318 }
319
320 return session;
321 }
322
323 /**
324 * Returns session already bound to given connection handle fulfilling given criteria.
325 *
326 * @param remoteAddress
327 * IP addres of the managed system
328 * @param remotePort
329 * UDP port of the managed system
330 * @param user
331 * IPMI user for whom the connection is established
332 * @return session object fulfilling given criteria, or null if no session was registered for such connection.
333 */
334 public Session getExistingSessionForCriteria(InetAddress remoteAddress, int remotePort, String user) {
335 return sessionManager.getSessionForCriteria(remoteAddress, remotePort, user);
336 }
337
338 /**
339 * Closes the session with the remote host if it is currently in open state.
340 *
341 * @param connectionHandle
342 * - {@link ConnectionHandle} associated with the remote host.
343 * @throws ConnectionException
344 * when connection is in the state that does not allow to
345 * perform this operation.
346 * @throws Exception
347 * when sending message to the managed system or initializing
348 * one of the cipherSuite's algorithms fails
349 */
350 public void closeSession(ConnectionHandle connectionHandle)
351 throws Exception {
352 if (!connectionManager.getConnection(connectionHandle.getHandle())
353 .isSessionValid()) {
354 return;
355 }
356 int tries = 0;
357 boolean succeded = false;
358 while (tries <= retries && !succeded) {
359 try {
360 ++tries;
361 connectionManager.getConnection(connectionHandle.getHandle())
362 .closeSession();
363 sessionManager.unregisterSession(connectionHandle);
364 succeded = true;
365 } catch (Exception e) {
366 logger.warn(FAILED_TO_RECEIVE_ANSWER_CAUSE_MESSAGE, e);
367 if (tries > retries) {
368 throw e;
369 }
370 }
371 }
372 return;
373 }
374
375 /**
376 * Sends the IPMI message to the remote host.
377 *
378 * @param connectionHandle
379 * - {@link ConnectionHandle} associated with the remote host.
380 * @param request
381 * - {@link PayloadCoder} containing the request to be sent
382 * @param isOneWay
383 * - tells whether this message is one way (needs response) or not.
384 * @return ID of the message that will be also attached to the response to
385 * pair request with response if queue was not full and message was
386 * sent, -1 if sending of the message failed.
387 *
388 * @throws ConnectionException
389 * when connection is in the state that does not allow to
390 * perform this operation.
391 * @throws Exception
392 * when sending message to the managed system or initializing
393 * one of the cipherSuite's algorithms fails
394 */
395 public int sendMessage(ConnectionHandle connectionHandle,
396 PayloadCoder request, boolean isOneWay) throws Exception {
397 int tries = 0;
398 int tag = -1;
399 while (tries <= retries && tag < 0) {
400 try {
401 ++tries;
402 while (tag < 0) {
403 tag = connectionManager.getConnection(
404 connectionHandle.getHandle()).sendMessage(
405 request, isOneWay);
406 if (tag < 0) {
407 Thread.sleep(10); // tag < 0 means that MessageQueue is
408 // full so we need to wait and retry
409 }
410 }
411 logger.debug("Sending message with tag " + tag + ", try "
412 + tries);
413 } catch (IllegalArgumentException e) {
414 throw e;
415 } catch (Exception e) {
416 logger.warn("Failed to send message, cause:", e);
417 if (tries > retries) {
418 throw e;
419 }
420 }
421 }
422 return tag;
423 }
424
425 /**
426 * Attempts to retry sending a message.
427 *
428 * @param connectionHandle
429 * - {@link ConnectionHandle} associated with the remote host.
430 * @param tag
431 * - tag of the message to retry
432 * @param messagePayloadType
433 * - {@link PayloadType} of the message that should be retried
434 * @return new tag if message was retried, -1 if operation failed
435 * @throws ConnectionException
436 * when connection isn't in state where sending commands is
437 * allowed
438 */
439 public int retry(ConnectionHandle connectionHandle, int tag, PayloadType messagePayloadType) throws ConnectionException {
440 return connectionManager.getConnection(connectionHandle.getHandle()).retry(tag, messagePayloadType);
441 }
442
443 /**
444 * Registers the listener so it will be notified of incoming messages.
445 *
446 * @param listener
447 * {@link IpmiResponseListener} to processResponse
448 */
449 public void registerListener(IpmiResponseListener listener) {
450 synchronized (responseListeners) {
451 responseListeners.add(listener);
452 }
453 }
454
455 /**
456 * Unregisters the listener so it will no longer receive notifications of
457 * received answers.
458 *
459 * @param listener
460 * - the {@link IpmiResponseListener} to unregister
461 */
462 public void unregisterListener(IpmiResponseListener listener) {
463 synchronized (responseListeners) {
464 responseListeners.remove(listener);
465 }
466 }
467
468 /**
469 * Registers the listener for incoming messages.
470 *
471 * @param listener
472 * the {@link InboundMessageListener} to register.
473 */
474 public void registerIncomingPayloadListener(InboundMessageListener listener) {
475 synchronized (inboundMessageListeners) {
476 inboundMessageListeners.add(listener);
477 }
478 }
479
480 /**
481 * Unregisters the listener so it will no longer receive notifications of received messages.
482 *
483 * @param listener
484 * the {@link InboundMessageListener} to unregister.
485 */
486 public void unregisterIncomingPayloadListener(InboundMessageListener listener) {
487 synchronized (inboundMessageListeners) {
488 inboundMessageListeners.remove(listener);
489 }
490 }
491
492 @Override
493 public void processResponse(ResponseData responseData, int handle, int tag, Exception exception) {
494 IpmiResponse response = null;
495 Connection connection = connectionManager.getConnection(handle);
496
497 if (responseData == null || exception != null) {
498 Exception notNullException = exception != null ? exception : new Exception("Empty response");
499
500 response = new IpmiError(notNullException, tag, new ConnectionHandle(
501 handle, connection.getRemoteMachineAddress(), connection.getRemoteMachinePort()));
502 } else {
503 response = new IpmiResponseData(responseData, tag,
504 new ConnectionHandle(handle, connection.getRemoteMachineAddress(), connection.getRemoteMachinePort()));
505
506 }
507 synchronized (responseListeners) {
508 for (IpmiResponseListener listener : responseListeners) {
509 if (listener != null) {
510 listener.notify(response);
511 }
512 }
513 }
514 }
515
516 @Override
517 public void processRequest(IpmiPayload payload) {
518 for (InboundMessageListener listener : inboundMessageListeners) {
519 if (listener.isPayloadSupported(payload)) {
520 listener.notify(payload);
521 }
522 }
523 }
524
525 /**
526 * Closes the connection with the given handle
527 */
528 public void closeConnection(ConnectionHandle handle) {
529 connectionManager.getConnection(handle.getHandle()).unregisterListener(
530 this);
531 connectionManager.closeConnection(handle.getHandle());
532 }
533
534 /**
535 * Finalizes the connector and closes all connections.
536 */
537 public void tearDown() {
538 connectionManager.close();
539 }
540
541 /**
542 * Changes the timeout value for connection with the given handle.
543 * @param handle
544 * - {@link ConnectionHandle} associated with the remote host.
545 * @param timeout
546 * - new timeout value in ms
547 */
548 public void setTimeout(ConnectionHandle handle, int timeout) {
549 connectionManager.getConnection(handle.getHandle()).setTimeout(timeout);
550 }
551
552 }