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