View Javadoc
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 }