1 /** 2 * Copyright © 2022 IBM Corporation 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 #include "fan_error.hpp" 17 18 #include "logging.hpp" 19 #include "sdbusplus.hpp" 20 21 #include <systemd/sd-journal.h> 22 23 #include <nlohmann/json.hpp> 24 #include <xyz/openbmc_project/Logging/Create/server.hpp> 25 26 #include <filesystem> 27 28 namespace phosphor::fan::monitor 29 { 30 31 using FFDCFormat = 32 sdbusplus::xyz::openbmc_project::Logging::server::Create::FFDCFormat; 33 using FFDCFiles = std::vector< 34 std::tuple<FFDCFormat, uint8_t, uint8_t, sdbusplus::message::unix_fd>>; 35 using json = nlohmann::json; 36 37 const auto loggingService = "xyz.openbmc_project.Logging"; 38 const auto loggingPath = "/xyz/openbmc_project/logging"; 39 const auto loggingCreateIface = "xyz.openbmc_project.Logging.Create"; 40 41 namespace fs = std::filesystem; 42 using namespace phosphor::fan::util; 43 44 /** 45 * @class JournalCloser 46 * 47 * Automatically closes the journal when the object goes out of scope. 48 */ 49 class JournalCloser 50 { 51 public: 52 // Specify which compiler-generated methods we want 53 JournalCloser() = delete; 54 JournalCloser(const JournalCloser&) = delete; 55 JournalCloser(JournalCloser&&) = delete; 56 JournalCloser& operator=(const JournalCloser&) = delete; 57 JournalCloser& operator=(JournalCloser&&) = delete; 58 59 JournalCloser(sd_journal* journal) : journal{journal} 60 {} 61 62 ~JournalCloser() 63 { 64 sd_journal_close(journal); 65 } 66 67 private: 68 sd_journal* journal{nullptr}; 69 }; 70 71 FFDCFile::FFDCFile(const fs::path& name) : 72 _fd(open(name.c_str(), O_RDONLY)), _name(name) 73 { 74 if (_fd() == -1) 75 { 76 auto e = errno; 77 getLogger().log(fmt::format("Could not open FFDC file {}. errno {}", 78 _name.string(), e)); 79 } 80 } 81 82 void FanError::commit(const json& jsonFFDC, bool isPowerOffError) 83 { 84 FFDCFiles ffdc; 85 auto ad = getAdditionalData(isPowerOffError); 86 87 // Add the Logger contents as FFDC 88 auto logFile = makeLogFFDCFile(); 89 if (logFile && (logFile->fd() != -1)) 90 { 91 ffdc.emplace_back(FFDCFormat::Text, 0x01, 0x01, logFile->fd()); 92 } 93 94 // Add the passed in JSON as FFDC 95 auto ffdcFile = makeJsonFFDCFile(jsonFFDC); 96 if (ffdcFile && (ffdcFile->fd() != -1)) 97 { 98 ffdc.emplace_back(FFDCFormat::JSON, 0x01, 0x01, ffdcFile->fd()); 99 } 100 101 // add the previous systemd journal entries as FFDC 102 auto serviceFFDC = makeJsonFFDCFile(getJournalEntries(25)); 103 if (serviceFFDC && serviceFFDC->fd() != -1) 104 { 105 ffdc.emplace_back(FFDCFormat::JSON, 0x01, 0x01, serviceFFDC->fd()); 106 } 107 108 try 109 { 110 auto sev = _severity; 111 112 // If this is a power off, change severity to Critical 113 if (isPowerOffError) 114 { 115 using namespace sdbusplus::xyz::openbmc_project::Logging::server; 116 sev = convertForMessage(Entry::Level::Critical); 117 } 118 SDBusPlus::callMethod(loggingService, loggingPath, loggingCreateIface, 119 "CreateWithFFDCFiles", _errorName, sev, ad, ffdc); 120 } 121 catch (const DBusError& e) 122 { 123 getLogger().log( 124 fmt::format("Call to create a {} error for fan {} failed: {}", 125 _errorName, _fanName, e.what()), 126 Logger::error); 127 } 128 } 129 130 std::map<std::string, std::string> 131 FanError::getAdditionalData(bool isPowerOffError) 132 { 133 std::map<std::string, std::string> ad; 134 135 ad.emplace("_PID", std::to_string(getpid())); 136 137 if (!_fanName.empty()) 138 { 139 ad.emplace("CALLOUT_INVENTORY_PATH", _fanName); 140 } 141 142 if (!_sensorName.empty()) 143 { 144 ad.emplace("FAN_SENSOR", _sensorName); 145 } 146 147 // If this is a power off, specify that it's a power 148 // fault and a system termination. This is used by some 149 // implementations for service reasons. 150 if (isPowerOffError) 151 { 152 ad.emplace("SEVERITY_DETAIL", "SYSTEM_TERM"); 153 } 154 155 return ad; 156 } 157 158 std::unique_ptr<FFDCFile> FanError::makeLogFFDCFile() 159 { 160 try 161 { 162 auto logFile = getLogger().saveToTempFile(); 163 return std::make_unique<FFDCFile>(logFile); 164 } 165 catch (const std::exception& e) 166 { 167 log<level::ERR>( 168 fmt::format("Could not save log contents in FFDC. Error msg: {}", 169 e.what()) 170 .c_str()); 171 } 172 return nullptr; 173 } 174 175 std::unique_ptr<FFDCFile> FanError::makeJsonFFDCFile(const json& ffdcData) 176 { 177 char tmpFile[] = "/tmp/fanffdc.XXXXXX"; 178 auto fd = mkstemp(tmpFile); 179 if (fd != -1) 180 { 181 auto jsonString = ffdcData.dump(); 182 183 auto rc = write(fd, jsonString.data(), jsonString.size()); 184 close(fd); 185 if (rc != -1) 186 { 187 fs::path jsonFile{tmpFile}; 188 return std::make_unique<FFDCFile>(jsonFile); 189 } 190 else 191 { 192 getLogger().log("Failed call to write JSON FFDC file"); 193 } 194 } 195 else 196 { 197 auto e = errno; 198 getLogger().log(fmt::format("Failed called to mkstemp, errno = {}", e), 199 Logger::error); 200 } 201 return nullptr; 202 } 203 204 nlohmann::json FanError::getJournalEntries(int numLines) const 205 { 206 // Sleep 100ms; otherwise recent journal entries sometimes not available 207 using namespace std::chrono_literals; 208 std::this_thread::sleep_for(100ms); 209 210 std::vector<std::string> entries; 211 212 // Open the journal 213 sd_journal* journal; 214 int rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY); 215 if (rc < 0) 216 { 217 // Build one line string containing field values 218 entries.push_back("[Internal error: sd_journal_open(), rc=" + 219 std::string(strerror(rc)) + "]"); 220 return json(entries); 221 } 222 223 // Create object to automatically close journal 224 JournalCloser closer{journal}; 225 226 std::string field{"SYSLOG_IDENTIFIER"}; 227 std::vector<std::string> executables{"systemd"}; 228 229 entries.reserve(2 * numLines); 230 231 for (const auto& executable : executables) 232 { 233 // Add match so we only loop over entries with specified field value 234 std::string match{field + '=' + executable}; 235 rc = sd_journal_add_match(journal, match.c_str(), 0); 236 if (rc < 0) 237 { 238 // Build one line string containing field values 239 entries.push_back("[Internal error: sd_journal_add_match(), rc=" + 240 std::string(strerror(rc)) + "]"); 241 242 break; 243 } 244 245 int count{1}; 246 247 std::string syslogID, pid, message, timeStamp; 248 249 // Loop through journal entries from newest to oldest 250 SD_JOURNAL_FOREACH_BACKWARDS(journal) 251 { 252 // Get relevant journal entry fields 253 timeStamp = getTimeStamp(journal); 254 syslogID = getFieldValue(journal, "SYSLOG_IDENTIFIER"); 255 pid = getFieldValue(journal, "_PID"); 256 message = getFieldValue(journal, "MESSAGE"); 257 258 // Build one line string containing field values 259 entries.push_back(timeStamp + " " + syslogID + "[" + pid + 260 "]: " + message); 261 262 // Stop after number of lines was read 263 if (count++ >= numLines) 264 { 265 break; 266 } 267 } 268 } 269 270 // put the journal entries in chronological order 271 std::reverse(entries.begin(), entries.end()); 272 273 return json(entries); 274 } 275 276 std::string FanError::getTimeStamp(sd_journal* journal) const 277 { 278 // Get realtime (wallclock) timestamp of current journal entry. The 279 // timestamp is in microseconds since the epoch. 280 uint64_t usec{0}; 281 int rc = sd_journal_get_realtime_usec(journal, &usec); 282 if (rc < 0) 283 { 284 return "[Internal error: sd_journal_get_realtime_usec(), rc=" + 285 std::string(strerror(rc)) + "]"; 286 } 287 288 // Convert to number of seconds since the epoch 289 time_t secs = usec / 1000000; 290 291 // Convert seconds to tm struct required by strftime() 292 struct tm* timeStruct = localtime(&secs); 293 if (timeStruct == nullptr) 294 { 295 return "[Internal error: localtime() returned nullptr]"; 296 } 297 298 // Convert tm struct into a date/time string 299 char timeStamp[80]; 300 strftime(timeStamp, sizeof(timeStamp), "%b %d %H:%M:%S", timeStruct); 301 302 return timeStamp; 303 } 304 305 std::string FanError::getFieldValue(sd_journal* journal, 306 const std::string& field) const 307 { 308 std::string value{}; 309 310 // Get field data from current journal entry 311 const void* data{nullptr}; 312 size_t length{0}; 313 int rc = sd_journal_get_data(journal, field.c_str(), &data, &length); 314 if (rc < 0) 315 { 316 if (-rc == ENOENT) 317 { 318 // Current entry does not include this field; return empty value 319 return value; 320 } 321 else 322 { 323 return "[Internal error: sd_journal_get_data() rc=" + 324 std::string(strerror(rc)) + "]"; 325 } 326 } 327 328 // Get value from field data. Field data in format "FIELD=value". 329 std::string dataString{static_cast<const char*>(data), length}; 330 std::string::size_type pos = dataString.find('='); 331 if ((pos != std::string::npos) && ((pos + 1) < dataString.size())) 332 { 333 // Value is substring after the '=' 334 value = dataString.substr(pos + 1); 335 } 336 337 return value; 338 } 339 340 } // namespace phosphor::fan::monitor 341