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