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 }