View Javadoc
1   package org.metricshub.ipmi.client.runner;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * IPMI Java Client
6    * ჻჻჻჻჻჻
7    * Copyright 2023 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 java.net.InetAddress;
26  import java.util.List;
27  import java.util.concurrent.Callable;
28  
29  import org.metricshub.ipmi.client.IpmiClientConfiguration;
30  import org.metricshub.ipmi.core.api.async.ConnectionHandle;
31  import org.metricshub.ipmi.core.api.sync.IpmiConnector;
32  import org.metricshub.ipmi.core.coding.commands.IpmiVersion;
33  import org.metricshub.ipmi.core.coding.commands.PrivilegeLevel;
34  import org.metricshub.ipmi.core.coding.commands.sdr.GetSdr;
35  import org.metricshub.ipmi.core.coding.commands.sdr.GetSdrResponseData;
36  import org.metricshub.ipmi.core.coding.commands.sdr.record.SensorRecord;
37  import org.metricshub.ipmi.core.coding.payload.CompletionCode;
38  import org.metricshub.ipmi.core.coding.payload.lan.IPMIException;
39  import org.metricshub.ipmi.core.coding.protocol.AuthenticationType;
40  import org.metricshub.ipmi.core.coding.security.CipherSuite;
41  import org.metricshub.ipmi.core.common.TypeConverter;
42  import org.metricshub.ipmi.core.connection.Connection;
43  
44  /**
45   * This abstract class implements common features required by FRUs, Sensor and Chassis Status runners.
46   * 
47   * @param <T> Represent the data type managed by the runner 
48   */
49  public abstract class AbstractIpmiRunner<T> implements AutoCloseable, Callable<T> {
50  
51  	private static final int DEFAULT_LOCAL_UDP_PORT = 0;
52  
53  	/**
54  	 * This is the value of Last Record ID (FFFFh). In order to retrieve the full set of SDR records, client must repeat reading SDR records
55  	 * until MAX_REPO_RECORD_ID is returned as next record ID. For further information see section 33.12 of the IPMI specification ver. 2.0
56  	 */
57  	protected static final int MAX_REPO_RECORD_ID =  65535; 
58  
59  	/**
60  	 * Size of the initial GetSdr message to get record header and size
61  	 */
62  	protected static final int INITIAL_CHUNK_SIZE = 8;
63  
64  	/**
65  	 * Chunk size depending on buffer size of the IPMI server. Bigger values will improve performance. If server is returning "Cannot return
66  	 * number of requested data bytes." error during GetSdr command, CHUNK_SIZE should be decreased.
67  	 */
68  	protected static final int CHUNK_SIZE = 16;
69  
70  	/**
71  	 * Size of SDR record header
72  	 */
73  	protected static final int HEADER_SIZE = 5;
74  
75  	protected IpmiClientConfiguration ipmiConfiguration;
76  
77  
78  	protected IpmiConnector connector;
79  	protected ConnectionHandle handle;
80  
81  	protected int nextRecId;
82  
83  	protected AbstractIpmiRunner(IpmiClientConfiguration ipmiConfiguration) {
84  		this.ipmiConfiguration = ipmiConfiguration;
85  	}
86  
87  	/**
88  	 * Create the {@link IpmiConnector} instance, perform the authentication if required then start the session. <br>
89  	 * This method will instantiate the internal fields: <em></em>
90  	 * 
91  	 * @throws Exception If an error occurs when starting the session
92  	 */
93  	protected void startSession() throws Exception {
94  		// Create the connector, specify port that will be used to communicate
95  		// with the remote host. The UDP layer starts listening at this port, so
96  		// no 2 connectors can work at the same time on the same port.
97  		connector = new IpmiConnector(DEFAULT_LOCAL_UDP_PORT, ipmiConfiguration.getPingPeriod());
98  
99  		// Should we perform the authentication
100 		if (!ipmiConfiguration.isSkipAuth()) {
101 			authenticate();
102 		} else {
103 			handle = connector.createConnection(InetAddress.getByName(ipmiConfiguration.getHostname()),
104 					ipmiConfiguration.getPort(), Connection.getDefaultCipherSuite(), PrivilegeLevel.User);
105 		}
106 
107 		// Start the session, provide user name and password, and optionally the
108 		// BMC key (only if the remote host has two-key authentication enabled,
109 		// otherwise this parameter should be null)
110 		connector.openSession(handle, ipmiConfiguration.getUsername(),
111 				String.valueOf(ipmiConfiguration.getPassword()), ipmiConfiguration.getBmcKey());
112 	}
113 
114 	/**
115 	 * Authenticate IPMI
116 	 * 
117 	 * @throws Exception If the authentication fails
118 	 */
119 	public void authenticate() throws Exception  {
120 		// Create the connection and get the handle, specify IP address of the
121 		// remote host. The connection is being registered in ConnectionManager,
122 		// the handle will be needed to identify it among other connections
123 		// (target IP address isn't enough, since we can handle multiple
124 		// connections to the same host)
125 		handle = connector.createConnection(InetAddress.getByName(ipmiConfiguration.getHostname()),
126 				ipmiConfiguration.getPort());
127 
128 		// Get available cipher suites list via getAvailableCipherSuites and
129 		// pick one of them that will be used further in the session.
130 		CipherSuite cs = getAvailableCipherSuite();
131 
132 		// Provide chosen cipher suite and privilege level to the remote host.
133 		// From now on, your connection handle will contain these information.
134 		connector.getChannelAuthenticationCapabilities(handle, cs, PrivilegeLevel.User);
135 	}
136 
137 	/**
138 	 * Get the available cipher suite. Get the last available if many cipher suites coexist.<br>
139 	 * 
140 	 * @return {@link CipherSuite} instance
141 	 * @throws Exception when sending message to the managed system fails or suites not found
142 	 */
143 	protected CipherSuite getAvailableCipherSuite() throws Exception {
144 	
145 		// Get cipher suites supported by the remote host
146 		List<CipherSuite> suites = connector.getAvailableCipherSuites(handle);
147 
148 		if (suites == null || suites.isEmpty()) {
149 			throw new Exception("Cannot get the available cipher suites.");
150 		}
151 
152 		// Return the cipher suite based on the available suites length
153 		if (suites.size() > 3) {
154 			return suites.get(3);
155 		} else if (suites.size() > 2) {
156 			return suites.get(2);
157 		} else if (suites.size() > 1) {
158 			return suites.get(1);
159 		}
160 
161 		return suites.get(0);
162 	}
163 
164 	@Override
165 	public void close() {
166 		if (handle != null) {
167 			// Close the session
168 			try {
169 				connector.closeSession(handle);
170 			} catch (Exception e) {
171 				// Ignore
172 			}
173 		}
174 
175 		// Close connection manager and release the listener port.
176 		connector.tearDown();
177 	}
178 
179 	/**
180 	 * Using the reservation id, get the {@link SensorRecord} instance by running a GetSdr IPMI request.<br>
181 	 * When the {@link SensorRecord} cannot be fetched using one request we try a second method, see <em>getSensorViaChunks</em>
182 	 * 
183 	 * @param reservationId The reservation identifier that needs to be sent to the BMC so that it handles correctly the request
184 	 * @return {@link SensorRecord} instance
185 	 * @throws Exception at sendMessage or if the error completion code is CannotRespond or UnspecifiedError
186 	 */
187 	protected SensorRecord getSensorData(int reservationId) throws Exception {
188 		try {
189 			// BMC capabilities are limited - that means that sometimes the
190 			// record size exceeds maximum size of the message. Since we don't
191 			// know what is the size of the record, we try to get whole one first
192 			GetSdrResponseData data = (GetSdrResponseData) connector.sendMessage(handle,
193 					new GetSdr(IpmiVersion.V20, handle.getCipherSuite(), AuthenticationType.RMCPPlus, reservationId, nextRecId));
194 
195 			// If getting whole record succeeded we create SensorRecord from
196 			// received data...
197 			SensorRecord sensorDataToPopulate = SensorRecord.populateSensorRecord(data.getSensorRecordData());
198 
199 			// ... and update the ID of the next record
200 			nextRecId = data.getNextRecordId();
201 			return sensorDataToPopulate;
202 
203 		} catch (IPMIException e) {
204 
205 			// The following error codes mean that record is too large to be
206 			// sent in one chunk. This means we need to split the data in
207 			// smaller parts.
208 			if (e.getCompletionCode() != CompletionCode.CannotRespond && e.getCompletionCode() != CompletionCode.UnspecifiedError) {
209 				throw e;
210 			}
211 
212 			return getSensorViaChunks(reservationId);
213 
214 		} catch (Exception e) {
215 			throw e;
216 		}
217 	}
218 
219 	/**
220 	 * Get SDR (sensor data record) by chunks of {@link #CHUNK_SIZE} bytes. We get the full record size from the first request, then
221 	 * we query the IPMI interface to get the remaining parts.
222 	 * 
223 	 * @param reservationId The reservation identifier that needs to be sent to the BMC so that it handles correctly the request
224 	 * @return {@link SensorRecord} instance
225 	 * @throws Exception if one of the sendMessage calls fails
226 	 */
227 	protected SensorRecord getSensorViaChunks(int reservationId) throws Exception {
228 		// First we get the header of the record to find out its size.
229 		GetSdrResponseData data = (GetSdrResponseData) connector.sendMessage(handle, new GetSdr(IpmiVersion.V20, handle.getCipherSuite(),
230 				AuthenticationType.RMCPPlus, reservationId, nextRecId, 0, INITIAL_CHUNK_SIZE));
231 
232 		// The record size is 5th byte of the record. It does not take
233 		// into account the size of the header, so we need to add it.
234 		int recSize = TypeConverter.byteToInt(data.getSensorRecordData()[4]) + HEADER_SIZE;
235 		int read = INITIAL_CHUNK_SIZE;
236 
237 		byte[] bytes = new byte[recSize];
238 
239 		System.arraycopy(data.getSensorRecordData(), 0, bytes, 0, data.getSensorRecordData().length);
240 
241 		// We get the rest of the record in chunks (watch out for
242 		// exceeding the record size, since this will result in BMC's
243 		// error.
244 		while (read < recSize) {
245 
246 			int bytesToRead = CHUNK_SIZE;
247 			if (recSize - read < bytesToRead) {
248 				bytesToRead = recSize - read;
249 			}
250 
251 			GetSdrResponseData part = (GetSdrResponseData) connector.sendMessage(handle, new GetSdr(IpmiVersion.V20, handle.getCipherSuite(),
252 					AuthenticationType.RMCPPlus, reservationId, nextRecId, read, bytesToRead));
253 
254 			// Append the new bytes
255 			System.arraycopy(part.getSensorRecordData(), 0, bytes, read, bytesToRead);
256 
257 			read += bytesToRead;
258 		}
259 
260 		// Finally we populate the sensor record with the gathered
261 		// data...
262 		SensorRecord sensorDataToPopulate = SensorRecord.populateSensorRecord(bytes);
263 
264 		// ... and update the ID of the next record
265 		nextRecId = data.getNextRecordId();
266 
267 		return sensorDataToPopulate;
268 	}
269 }