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 }