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.util.ArrayList;
26  import java.util.List;
27  import java.util.stream.Collectors;
28  
29  import org.metricshub.ipmi.client.IpmiClientConfiguration;
30  import org.metricshub.ipmi.client.model.Fru;
31  import org.metricshub.ipmi.core.coding.commands.IpmiVersion;
32  import org.metricshub.ipmi.core.coding.commands.fru.BaseUnit;
33  import org.metricshub.ipmi.core.coding.commands.fru.GetFruInventoryAreaInfo;
34  import org.metricshub.ipmi.core.coding.commands.fru.GetFruInventoryAreaInfoResponseData;
35  import org.metricshub.ipmi.core.coding.commands.fru.ReadFruData;
36  import org.metricshub.ipmi.core.coding.commands.fru.ReadFruDataResponseData;
37  import org.metricshub.ipmi.core.coding.commands.fru.record.BoardInfo;
38  import org.metricshub.ipmi.core.coding.commands.fru.record.ChassisInfo;
39  import org.metricshub.ipmi.core.coding.commands.fru.record.FruRecord;
40  import org.metricshub.ipmi.core.coding.commands.fru.record.ProductInfo;
41  import org.metricshub.ipmi.core.coding.commands.sdr.ReserveSdrRepository;
42  import org.metricshub.ipmi.core.coding.commands.sdr.ReserveSdrRepositoryResponseData;
43  import org.metricshub.ipmi.core.coding.commands.sdr.record.CompactSensorRecord;
44  import org.metricshub.ipmi.core.coding.commands.sdr.record.EntityId;
45  import org.metricshub.ipmi.core.coding.commands.sdr.record.FruDeviceLocatorRecord;
46  import org.metricshub.ipmi.core.coding.commands.sdr.record.SensorRecord;
47  import org.metricshub.ipmi.core.coding.payload.CompletionCode;
48  import org.metricshub.ipmi.core.coding.payload.lan.IPMIException;
49  import org.metricshub.ipmi.core.coding.protocol.AuthenticationType;
50  
51  /**
52   * Get FRU information
53   */
54  public class GetFrusRunner extends AbstractIpmiRunner<List<Fru>> {
55  
56  	/**
57  	 * Id of the built-in, default FRU
58  	 */
59  	private static final int DEFAULT_FRU_ID = 0;
60  
61  	/**
62  	 * Size of data transmitted in single ReadFru command. Bigger values will improve performance. If server is returning "Invalid data field in
63  	 * Request." error during ReadFru command, FRU_READ_PACKET_SIZE should be decreased.
64  	 */
65  	private static final int FRU_READ_PACKET_SIZE = 16;
66  
67  	private boolean systemBoardFruUpdated = false;
68  
69  	public GetFrusRunner(IpmiClientConfiguration ipmiConfiguration) {
70  		super(ipmiConfiguration);
71  	}
72  
73  	@Override
74  	public List<Fru> call() throws Exception {
75  		final List<Fru> result = new ArrayList<>();
76  
77  		super.startSession();
78  
79  		// Id 0 indicates first record in SDR. Next IDs can be retrieved from
80  		// records - they are organized in a list and there is no BMC command to
81  		// get all of them.
82  		nextRecId = 0;
83  
84  		// Some BMCs allow getting sensor records without reservation, so we try
85  		// to do it that way first
86  		int reservationId = 0;
87  		int lastReservationId = -1;
88  
89  		List<FruRecord> systemBoardFruRecords = getFruRecords(DEFAULT_FRU_ID);
90  
91  		// We get sensor data until we encounter ID = 65535 which means that
92  		// this record is the last one.
93  		while (nextRecId < MAX_REPO_RECORD_ID) {
94  
95  			SensorRecord sensorRecord = null;
96  
97  			try {
98  				// Populate the sensor record and get ID of the next record in
99  				// repository (see #getSensorData for details).
100 				sensorRecord = super.getSensorData(reservationId);
101 
102 				processFruRecord(result, sensorRecord, systemBoardFruRecords);
103 
104 			} catch (IPMIException e) {
105 
106 				// If getting sensor data failed, we check if it already failed
107 				// with this reservation ID, so that we avoid the infinite loop.
108 				if (lastReservationId == reservationId || e.getCompletionCode() != CompletionCode.ReservationCanceled) {
109 					throw e;
110 				}
111 
112 				lastReservationId = reservationId;
113 
114 				// If the cause of the failure was canceling of the
115 				// reservation, we get new reservationId and retry. This can
116 				// happen many times during getting all sensors, since BMC can't
117 				// manage parallel sessions and invalidates old one if new one
118 				// appears.
119 				reservationId = ((ReserveSdrRepositoryResponseData) connector.sendMessage(handle,
120 						new ReserveSdrRepository(IpmiVersion.V20, handle.getCipherSuite(), AuthenticationType.RMCPPlus))).getReservationId();
121 			}
122 
123 		}
124 
125 		return result;
126 	}
127 
128 	/**
129 	 * Process the given sensor record and create the system board FRU record. The new {@link Fru} is added to th FRU list <code>result</code>
130 	 * 
131 	 * @param result                List of {@link Fru} instance to append
132 	 * @param sensorRecord          The sensor record to process
133 	 * @param systemBoardFruRecords The system board Fru records
134 	 * @throws Exception
135 	 */
136 	private void processFruRecord(final List<Fru> result, final SensorRecord sensorRecord, final List<FruRecord> systemBoardFruRecords) throws Exception {
137 		try {
138 			// Process the FRU record
139 			Fru fru = null;
140 			if (sensorRecord instanceof FruDeviceLocatorRecord) {
141 				FruDeviceLocatorRecord fruLocator = (FruDeviceLocatorRecord) sensorRecord;
142 
143 				if (fruLocator.isLogical()) {
144 					List<FruRecord> fruRecords = getFruRecords(fruLocator.getDeviceId());
145 
146 					if (!fruRecords.isEmpty()) {
147 						fru = new Fru(fruLocator, fruRecords);
148 					}
149 				}
150 			} else if (!systemBoardFruRecords.isEmpty()
151 					&& !systemBoardFruUpdated
152 					&& sensorRecord instanceof CompactSensorRecord
153 					&& ((CompactSensorRecord) sensorRecord).getEntityId().equals(EntityId.SystemBoard)) {
154 
155 				// Since we can only access the SystemBoard components,
156 				// we need to build the FruDeviceLocatorRecord for SystemBoard instance. 
157 
158 				// OK this can be one of the SystemBoard sensors
159 				CompactSensorRecord compactSensorRecord = (CompactSensorRecord) sensorRecord;
160 
161 				BoardInfo boardInfo = systemBoardFruRecords.stream()
162 						.filter(BoardInfo.class::isInstance)
163 						.map(BoardInfo.class::cast)
164 						.findFirst()
165 						.orElse(null);
166 
167 				if (boardInfo != null) {
168 
169 					// Create the Fru locator
170 					FruDeviceLocatorRecord locator = new FruDeviceLocatorRecord();
171 					locator.setFruEntityId(EntityId.SystemBoard.getCode());
172 					locator.setFruEntityInstance(compactSensorRecord.getEntityInstanceNumber());
173 					locator.setName(boardInfo.getBoardProductName() + " " + compactSensorRecord.getEntityInstanceNumber());
174 
175 					fru = new Fru(locator, systemBoardFruRecords);
176 
177 					// OK, now we are good!
178 					systemBoardFruUpdated = true;
179 
180 				}
181 			}
182 
183 			// Add the Fru instance
184 			if (fru != null) {
185 				result.add(fru);
186 			}
187 
188 		} catch (IPMIException e) {
189 			// Nothing can be done
190 		}
191 
192 	}
193 
194 	/**
195 	 * Get the FRU records for the given FRU identifier <code>fruId</code>
196 	 * 
197 	 * @param fruId The unique identifier of the FRU
198 	 * @return new List of {@link FruRecord} instances
199 	 * @throws Exception
200 	 */
201 	private List<FruRecord> getFruRecords(int fruId) throws Exception {
202 		List<ReadFruDataResponseData> fruData = new ArrayList<>();
203 
204 		// get the FRU Inventory Area info
205 		GetFruInventoryAreaInfoResponseData info = (GetFruInventoryAreaInfoResponseData) connector.sendMessage(handle,
206 				new GetFruInventoryAreaInfo(IpmiVersion.V20, handle.getCipherSuite(), AuthenticationType.RMCPPlus, fruId));
207 
208 		int size = info.getFruInventoryAreaSize();
209 		BaseUnit unit = info.getFruUnit();
210 
211 		// since the size of single FRU entry can exceed maximum size of the
212 		// message sent via IPMI, it has to be read in chunks
213 		for (int i = 0; i < size; i += FRU_READ_PACKET_SIZE) {
214 			int fruReadPacketSize = FRU_READ_PACKET_SIZE;
215 			if (i + fruReadPacketSize > size) {
216 				fruReadPacketSize = size % FRU_READ_PACKET_SIZE;
217 			}
218 			try {
219 				// get single package od FRU data
220 				ReadFruDataResponseData data = (ReadFruDataResponseData) connector.sendMessage(handle,
221 						new ReadFruData(IpmiVersion.V20, handle.getCipherSuite(), AuthenticationType.RMCPPlus, fruId, unit, i, fruReadPacketSize));
222 
223 				fruData.add(data);
224 
225 			} catch (Exception e) {
226 				// Nothing can be done
227 			}
228 		}
229 
230 		try {
231 			// after collecting all the data, we can combine and parse it
232 			return ReadFruData.decodeFruData(fruData).stream()
233 					.filter(fruRecord -> fruRecord instanceof BoardInfo || fruRecord instanceof ChassisInfo || fruRecord instanceof ProductInfo)
234 					.collect(Collectors.toList());
235 
236 		} catch (Exception e) {
237 			// Nothing can be done
238 		}
239 
240 		return new ArrayList<>();
241 	}
242 }