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 					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 
127 		// Get available cipher suites list via getAvailableCipherSuites and
128 		// pick one of them that will be used further in the session.
129 		CipherSuite cs = getAvailableCipherSuite();
130 
131 		// Provide chosen cipher suite and privilege level to the remote host.
132 		// From now on, your connection handle will contain these information.
133 		connector.getChannelAuthenticationCapabilities(handle, cs, PrivilegeLevel.User);
134 	}
135 
136 	/**
137 	 * Get the available cipher suite. Get the last available if many cipher suites coexist.<br>
138 	 * 
139 	 * @return {@link CipherSuite} instance
140 	 * @throws Exception when sending message to the managed system fails or suites not found
141 	 */
142 	protected CipherSuite getAvailableCipherSuite() throws Exception {
143 	
144 		// Get cipher suites supported by the remote host
145 		List<CipherSuite> suites = connector.getAvailableCipherSuites(handle);
146 
147 		if (suites == null || suites.isEmpty()) {
148 			throw new Exception("Cannot get the available cipher suites.");
149 		}
150 
151 		// Return the cipher suite based on the available suites length
152 		if (suites.size() > 3) {
153 			return suites.get(3);
154 		} else if (suites.size() > 2) {
155 			return suites.get(2);
156 		} else if (suites.size() > 1) {
157 			return suites.get(1);
158 		}
159 
160 		return suites.get(0);
161 	}
162 
163 	@Override
164 	public void close() {
165 		if (handle != null) {
166 			// Close the session
167 			try {
168 				connector.closeSession(handle);
169 			} catch (Exception e) {
170 				// Ignore
171 			}
172 		}
173 
174 		// Close connection manager and release the listener port.
175 		connector.tearDown();
176 	}
177 
178 	/**
179 	 * Using the reservation id, get the {@link SensorRecord} instance by running a GetSdr IPMI request.<br>
180 	 * When the {@link SensorRecord} cannot be fetched using one request we try a second method, see <em>getSensorViaChunks</em>
181 	 * 
182 	 * @param reservationId The reservation identifier that needs to be sent to the BMC so that it handles correctly the request
183 	 * @return {@link SensorRecord} instance
184 	 * @throws Exception at sendMessage or if the error completion code is CannotRespond or UnspecifiedError
185 	 */
186 	protected SensorRecord getSensorData(int reservationId) throws Exception {
187 		try {
188 			// BMC capabilities are limited - that means that sometimes the
189 			// record size exceeds maximum size of the message. Since we don't
190 			// know what is the size of the record, we try to get whole one first
191 			GetSdrResponseData data = (GetSdrResponseData) connector.sendMessage(handle,
192 					new GetSdr(IpmiVersion.V20, handle.getCipherSuite(), AuthenticationType.RMCPPlus, reservationId, nextRecId));
193 
194 			// If getting whole record succeeded we create SensorRecord from
195 			// received data...
196 			SensorRecord sensorDataToPopulate = SensorRecord.populateSensorRecord(data.getSensorRecordData());
197 
198 			// ... and update the ID of the next record
199 			nextRecId = data.getNextRecordId();
200 			return sensorDataToPopulate;
201 
202 		} catch (IPMIException e) {
203 
204 			// The following error codes mean that record is too large to be
205 			// sent in one chunk. This means we need to split the data in
206 			// smaller parts.
207 			if (e.getCompletionCode() != CompletionCode.CannotRespond && e.getCompletionCode() != CompletionCode.UnspecifiedError) {
208 				throw e;
209 			}
210 
211 			return getSensorViaChunks(reservationId);
212 
213 		} catch (Exception e) {
214 			throw e;
215 		}
216 	}
217 
218 	/**
219 	 * Get SDR (sensor data record) by chunks of {@link #CHUNK_SIZE} bytes. We get the full record size from the first request, then
220 	 * we query the IPMI interface to get the remaining parts.
221 	 * 
222 	 * @param reservationId The reservation identifier that needs to be sent to the BMC so that it handles correctly the request
223 	 * @return {@link SensorRecord} instance
224 	 * @throws Exception if one of the sendMessage calls fails
225 	 */
226 	protected SensorRecord getSensorViaChunks(int reservationId) throws Exception {
227 		// First we get the header of the record to find out its size.
228 		GetSdrResponseData data = (GetSdrResponseData) connector.sendMessage(handle, new GetSdr(IpmiVersion.V20, handle.getCipherSuite(),
229 				AuthenticationType.RMCPPlus, reservationId, nextRecId, 0, INITIAL_CHUNK_SIZE));
230 
231 		// The record size is 5th byte of the record. It does not take
232 		// into account the size of the header, so we need to add it.
233 		int recSize = TypeConverter.byteToInt(data.getSensorRecordData()[4]) + HEADER_SIZE;
234 		int read = INITIAL_CHUNK_SIZE;
235 
236 		byte[] bytes = new byte[recSize];
237 
238 		System.arraycopy(data.getSensorRecordData(), 0, bytes, 0, data.getSensorRecordData().length);
239 
240 		// We get the rest of the record in chunks (watch out for
241 		// exceeding the record size, since this will result in BMC's
242 		// error.
243 		while (read < recSize) {
244 
245 			int bytesToRead = CHUNK_SIZE;
246 			if (recSize - read < bytesToRead) {
247 				bytesToRead = recSize - read;
248 			}
249 
250 			GetSdrResponseData part = (GetSdrResponseData) connector.sendMessage(handle, new GetSdr(IpmiVersion.V20, handle.getCipherSuite(),
251 					AuthenticationType.RMCPPlus, reservationId, nextRecId, read, bytesToRead));
252 
253 			// Append the new bytes
254 			System.arraycopy(part.getSensorRecordData(), 0, bytes, read, bytesToRead);
255 
256 			read += bytesToRead;
257 		}
258 
259 		// Finally we populate the sensor record with the gathered
260 		// data...
261 		SensorRecord sensorDataToPopulate = SensorRecord.populateSensorRecord(bytes);
262 
263 		// ... and update the ID of the next record
264 		nextRecId = data.getNextRecordId();
265 
266 		return sensorDataToPopulate;
267 	}
268 }