1 /* 2 // Copyright (c) 2018 Intel 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 #pragma once 17 18 #include "node.hpp" 19 #include "registries.hpp" 20 #include "registries/base_message_registry.hpp" 21 #include "registries/openbmc_message_registry.hpp" 22 23 #include <systemd/sd-journal.h> 24 25 #include <boost/algorithm/string/split.hpp> 26 #include <boost/beast/core/span.hpp> 27 #include <boost/container/flat_map.hpp> 28 #include <error_messages.hpp> 29 #include <filesystem> 30 #include <string_view> 31 #include <variant> 32 33 namespace redfish 34 { 35 36 constexpr char const *CrashdumpObject = "com.intel.crashdump"; 37 constexpr char const *CrashdumpPath = "/com/intel/crashdump"; 38 constexpr char const *CrashdumpOnDemandPath = "/com/intel/crashdump/OnDemand"; 39 constexpr char const *CrashdumpInterface = "com.intel.crashdump"; 40 constexpr char const *CrashdumpOnDemandInterface = 41 "com.intel.crashdump.OnDemand"; 42 constexpr char const *CrashdumpRawPECIInterface = 43 "com.intel.crashdump.SendRawPeci"; 44 45 namespace message_registries 46 { 47 static const Message *getMessageFromRegistry( 48 const std::string &messageKey, 49 const boost::beast::span<const MessageEntry> registry) 50 { 51 boost::beast::span<const MessageEntry>::const_iterator messageIt = 52 std::find_if(registry.cbegin(), registry.cend(), 53 [&messageKey](const MessageEntry &messageEntry) { 54 return !std::strcmp(messageEntry.first, 55 messageKey.c_str()); 56 }); 57 if (messageIt != registry.cend()) 58 { 59 return &messageIt->second; 60 } 61 62 return nullptr; 63 } 64 65 static const Message *getMessage(const std::string_view &messageID) 66 { 67 // Redfish MessageIds are in the form 68 // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find 69 // the right Message 70 std::vector<std::string> fields; 71 fields.reserve(4); 72 boost::split(fields, messageID, boost::is_any_of(".")); 73 std::string ®istryName = fields[0]; 74 std::string &messageKey = fields[3]; 75 76 // Find the right registry and check it for the MessageKey 77 if (std::string(base::header.registryPrefix) == registryName) 78 { 79 return getMessageFromRegistry( 80 messageKey, boost::beast::span<const MessageEntry>(base::registry)); 81 } 82 if (std::string(openbmc::header.registryPrefix) == registryName) 83 { 84 return getMessageFromRegistry( 85 messageKey, 86 boost::beast::span<const MessageEntry>(openbmc::registry)); 87 } 88 return nullptr; 89 } 90 } // namespace message_registries 91 92 namespace fs = std::filesystem; 93 94 using GetManagedPropertyType = boost::container::flat_map< 95 std::string, 96 sdbusplus::message::variant<std::string, bool, uint8_t, int16_t, uint16_t, 97 int32_t, uint32_t, int64_t, uint64_t, double>>; 98 99 using GetManagedObjectsType = boost::container::flat_map< 100 sdbusplus::message::object_path, 101 boost::container::flat_map<std::string, GetManagedPropertyType>>; 102 103 inline std::string translateSeverityDbusToRedfish(const std::string &s) 104 { 105 if (s == "xyz.openbmc_project.Logging.Entry.Level.Alert") 106 { 107 return "Critical"; 108 } 109 else if (s == "xyz.openbmc_project.Logging.Entry.Level.Critical") 110 { 111 return "Critical"; 112 } 113 else if (s == "xyz.openbmc_project.Logging.Entry.Level.Debug") 114 { 115 return "OK"; 116 } 117 else if (s == "xyz.openbmc_project.Logging.Entry.Level.Emergency") 118 { 119 return "Critical"; 120 } 121 else if (s == "xyz.openbmc_project.Logging.Entry.Level.Error") 122 { 123 return "Critical"; 124 } 125 else if (s == "xyz.openbmc_project.Logging.Entry.Level.Informational") 126 { 127 return "OK"; 128 } 129 else if (s == "xyz.openbmc_project.Logging.Entry.Level.Notice") 130 { 131 return "OK"; 132 } 133 else if (s == "xyz.openbmc_project.Logging.Entry.Level.Warning") 134 { 135 return "Warning"; 136 } 137 return ""; 138 } 139 140 static int getJournalMetadata(sd_journal *journal, 141 const std::string_view &field, 142 std::string_view &contents) 143 { 144 const char *data = nullptr; 145 size_t length = 0; 146 int ret = 0; 147 // Get the metadata from the requested field of the journal entry 148 ret = sd_journal_get_data(journal, field.data(), (const void **)&data, 149 &length); 150 if (ret < 0) 151 { 152 return ret; 153 } 154 contents = std::string_view(data, length); 155 // Only use the content after the "=" character. 156 contents.remove_prefix(std::min(contents.find("=") + 1, contents.size())); 157 return ret; 158 } 159 160 static int getJournalMetadata(sd_journal *journal, 161 const std::string_view &field, const int &base, 162 int &contents) 163 { 164 int ret = 0; 165 std::string_view metadata; 166 // Get the metadata from the requested field of the journal entry 167 ret = getJournalMetadata(journal, field, metadata); 168 if (ret < 0) 169 { 170 return ret; 171 } 172 contents = strtol(metadata.data(), nullptr, base); 173 return ret; 174 } 175 176 static bool getEntryTimestamp(sd_journal *journal, std::string &entryTimestamp) 177 { 178 int ret = 0; 179 uint64_t timestamp = 0; 180 ret = sd_journal_get_realtime_usec(journal, ×tamp); 181 if (ret < 0) 182 { 183 BMCWEB_LOG_ERROR << "Failed to read entry timestamp: " 184 << strerror(-ret); 185 return false; 186 } 187 time_t t = 188 static_cast<time_t>(timestamp / 1000 / 1000); // Convert from us to s 189 struct tm *loctime = localtime(&t); 190 char entryTime[64] = {}; 191 if (NULL != loctime) 192 { 193 strftime(entryTime, sizeof(entryTime), "%FT%T%z", loctime); 194 } 195 // Insert the ':' into the timezone 196 std::string_view t1(entryTime); 197 std::string_view t2(entryTime); 198 if (t1.size() > 2 && t2.size() > 2) 199 { 200 t1.remove_suffix(2); 201 t2.remove_prefix(t2.size() - 2); 202 } 203 entryTimestamp = std::string(t1) + ":" + std::string(t2); 204 return true; 205 } 206 207 static bool getSkipParam(crow::Response &res, const crow::Request &req, 208 long &skip) 209 { 210 char *skipParam = req.urlParams.get("$skip"); 211 if (skipParam != nullptr) 212 { 213 char *ptr = nullptr; 214 skip = std::strtol(skipParam, &ptr, 10); 215 if (*skipParam == '\0' || *ptr != '\0') 216 { 217 218 messages::queryParameterValueTypeError(res, std::string(skipParam), 219 "$skip"); 220 return false; 221 } 222 if (skip < 0) 223 { 224 225 messages::queryParameterOutOfRange(res, std::to_string(skip), 226 "$skip", "greater than 0"); 227 return false; 228 } 229 } 230 return true; 231 } 232 233 static constexpr const long maxEntriesPerPage = 1000; 234 static bool getTopParam(crow::Response &res, const crow::Request &req, 235 long &top) 236 { 237 char *topParam = req.urlParams.get("$top"); 238 if (topParam != nullptr) 239 { 240 char *ptr = nullptr; 241 top = std::strtol(topParam, &ptr, 10); 242 if (*topParam == '\0' || *ptr != '\0') 243 { 244 messages::queryParameterValueTypeError(res, std::string(topParam), 245 "$top"); 246 return false; 247 } 248 if (top < 1 || top > maxEntriesPerPage) 249 { 250 251 messages::queryParameterOutOfRange( 252 res, std::to_string(top), "$top", 253 "1-" + std::to_string(maxEntriesPerPage)); 254 return false; 255 } 256 } 257 return true; 258 } 259 260 static bool getUniqueEntryID(sd_journal *journal, std::string &entryID) 261 { 262 int ret = 0; 263 static uint64_t prevTs = 0; 264 static int index = 0; 265 // Get the entry timestamp 266 uint64_t curTs = 0; 267 ret = sd_journal_get_realtime_usec(journal, &curTs); 268 if (ret < 0) 269 { 270 BMCWEB_LOG_ERROR << "Failed to read entry timestamp: " 271 << strerror(-ret); 272 return false; 273 } 274 // If the timestamp isn't unique, increment the index 275 if (curTs == prevTs) 276 { 277 index++; 278 } 279 else 280 { 281 // Otherwise, reset it 282 index = 0; 283 } 284 // Save the timestamp 285 prevTs = curTs; 286 287 entryID = std::to_string(curTs); 288 if (index > 0) 289 { 290 entryID += "_" + std::to_string(index); 291 } 292 return true; 293 } 294 295 static bool getUniqueEntryID(const std::string &logEntry, std::string &entryID) 296 { 297 static uint64_t prevTs = 0; 298 static int index = 0; 299 // Get the entry timestamp 300 uint64_t curTs = 0; 301 std::tm timeStruct = {}; 302 std::istringstream entryStream(logEntry); 303 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S")) 304 { 305 curTs = std::mktime(&timeStruct); 306 } 307 // If the timestamp isn't unique, increment the index 308 if (curTs == prevTs) 309 { 310 index++; 311 } 312 else 313 { 314 // Otherwise, reset it 315 index = 0; 316 } 317 // Save the timestamp 318 prevTs = curTs; 319 320 entryID = std::to_string(curTs); 321 if (index > 0) 322 { 323 entryID += "_" + std::to_string(index); 324 } 325 return true; 326 } 327 328 static bool getTimestampFromID(crow::Response &res, const std::string &entryID, 329 uint64_t ×tamp, uint16_t &index) 330 { 331 if (entryID.empty()) 332 { 333 return false; 334 } 335 // Convert the unique ID back to a timestamp to find the entry 336 std::string_view tsStr(entryID); 337 338 auto underscorePos = tsStr.find("_"); 339 if (underscorePos != tsStr.npos) 340 { 341 // Timestamp has an index 342 tsStr.remove_suffix(tsStr.size() - underscorePos); 343 std::string_view indexStr(entryID); 344 indexStr.remove_prefix(underscorePos + 1); 345 std::size_t pos; 346 try 347 { 348 index = std::stoul(std::string(indexStr), &pos); 349 } 350 catch (std::invalid_argument) 351 { 352 messages::resourceMissingAtURI(res, entryID); 353 return false; 354 } 355 catch (std::out_of_range) 356 { 357 messages::resourceMissingAtURI(res, entryID); 358 return false; 359 } 360 if (pos != indexStr.size()) 361 { 362 messages::resourceMissingAtURI(res, entryID); 363 return false; 364 } 365 } 366 // Timestamp has no index 367 std::size_t pos; 368 try 369 { 370 timestamp = std::stoull(std::string(tsStr), &pos); 371 } 372 catch (std::invalid_argument) 373 { 374 messages::resourceMissingAtURI(res, entryID); 375 return false; 376 } 377 catch (std::out_of_range) 378 { 379 messages::resourceMissingAtURI(res, entryID); 380 return false; 381 } 382 if (pos != tsStr.size()) 383 { 384 messages::resourceMissingAtURI(res, entryID); 385 return false; 386 } 387 return true; 388 } 389 390 static bool 391 getRedfishLogFiles(std::vector<std::filesystem::path> &redfishLogFiles) 392 { 393 static const std::filesystem::path redfishLogDir = "/var/log"; 394 static const std::string redfishLogFilename = "redfish"; 395 396 // Loop through the directory looking for redfish log files 397 for (const std::filesystem::directory_entry &dirEnt : 398 std::filesystem::directory_iterator(redfishLogDir)) 399 { 400 // If we find a redfish log file, save the path 401 std::string filename = dirEnt.path().filename(); 402 if (boost::starts_with(filename, redfishLogFilename)) 403 { 404 redfishLogFiles.emplace_back(redfishLogDir / filename); 405 } 406 } 407 // As the log files rotate, they are appended with a ".#" that is higher for 408 // the older logs. Since we don't expect more than 10 log files, we 409 // can just sort the list to get them in order from newest to oldest 410 std::sort(redfishLogFiles.begin(), redfishLogFiles.end()); 411 412 return !redfishLogFiles.empty(); 413 } 414 415 class SystemLogServiceCollection : public Node 416 { 417 public: 418 template <typename CrowApp> 419 SystemLogServiceCollection(CrowApp &app) : 420 Node(app, "/redfish/v1/Systems/system/LogServices/") 421 { 422 entityPrivileges = { 423 {boost::beast::http::verb::get, {{"Login"}}}, 424 {boost::beast::http::verb::head, {{"Login"}}}, 425 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 426 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 427 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 428 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 429 } 430 431 private: 432 /** 433 * Functions triggers appropriate requests on DBus 434 */ 435 void doGet(crow::Response &res, const crow::Request &req, 436 const std::vector<std::string> ¶ms) override 437 { 438 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 439 // Collections don't include the static data added by SubRoute because 440 // it has a duplicate entry for members 441 asyncResp->res.jsonValue["@odata.type"] = 442 "#LogServiceCollection.LogServiceCollection"; 443 asyncResp->res.jsonValue["@odata.context"] = 444 "/redfish/v1/$metadata#LogServiceCollection.LogServiceCollection"; 445 asyncResp->res.jsonValue["@odata.id"] = 446 "/redfish/v1/Systems/system/LogServices"; 447 asyncResp->res.jsonValue["Name"] = "System Log Services Collection"; 448 asyncResp->res.jsonValue["Description"] = 449 "Collection of LogServices for this Computer System"; 450 nlohmann::json &logServiceArray = asyncResp->res.jsonValue["Members"]; 451 logServiceArray = nlohmann::json::array(); 452 logServiceArray.push_back( 453 {{"@odata.id", "/redfish/v1/Systems/system/LogServices/EventLog"}}); 454 #ifdef BMCWEB_ENABLE_REDFISH_CPU_LOG 455 logServiceArray.push_back( 456 {{"@odata.id", 457 "/redfish/v1/Systems/system/LogServices/Crashdump"}}); 458 #endif 459 asyncResp->res.jsonValue["Members@odata.count"] = 460 logServiceArray.size(); 461 } 462 }; 463 464 class EventLogService : public Node 465 { 466 public: 467 template <typename CrowApp> 468 EventLogService(CrowApp &app) : 469 Node(app, "/redfish/v1/Systems/system/LogServices/EventLog/") 470 { 471 entityPrivileges = { 472 {boost::beast::http::verb::get, {{"Login"}}}, 473 {boost::beast::http::verb::head, {{"Login"}}}, 474 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 475 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 476 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 477 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 478 } 479 480 private: 481 void doGet(crow::Response &res, const crow::Request &req, 482 const std::vector<std::string> ¶ms) override 483 { 484 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 485 486 asyncResp->res.jsonValue["@odata.id"] = 487 "/redfish/v1/Systems/system/LogServices/EventLog"; 488 asyncResp->res.jsonValue["@odata.type"] = 489 "#LogService.v1_1_0.LogService"; 490 asyncResp->res.jsonValue["@odata.context"] = 491 "/redfish/v1/$metadata#LogService.LogService"; 492 asyncResp->res.jsonValue["Name"] = "Event Log Service"; 493 asyncResp->res.jsonValue["Description"] = "System Event Log Service"; 494 asyncResp->res.jsonValue["Id"] = "Event Log"; 495 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 496 asyncResp->res.jsonValue["Entries"] = { 497 {"@odata.id", 498 "/redfish/v1/Systems/system/LogServices/EventLog/Entries"}}; 499 asyncResp->res.jsonValue["Actions"]["#LogService.ClearLog"] = { 500 501 {"target", "/redfish/v1/Systems/system/LogServices/EventLog/" 502 "Actions/LogService.ClearLog"}}; 503 } 504 }; 505 506 class EventLogClear : public Node 507 { 508 public: 509 EventLogClear(CrowApp &app) : 510 Node(app, "/redfish/v1/Systems/system/LogServices/EventLog/Actions/" 511 "LogService.ClearLog/") 512 { 513 entityPrivileges = { 514 {boost::beast::http::verb::get, {{"Login"}}}, 515 {boost::beast::http::verb::head, {{"Login"}}}, 516 {boost::beast::http::verb::patch, {{"ConfigureComponents"}}}, 517 {boost::beast::http::verb::put, {{"ConfigureComponents"}}}, 518 {boost::beast::http::verb::delete_, {{"ConfigureComponents"}}}, 519 {boost::beast::http::verb::post, {{"ConfigureComponents"}}}}; 520 } 521 522 private: 523 void doPost(crow::Response &res, const crow::Request &req, 524 const std::vector<std::string> ¶ms) override 525 { 526 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 527 528 // Clear the EventLog by deleting the log files 529 std::vector<std::filesystem::path> redfishLogFiles; 530 if (getRedfishLogFiles(redfishLogFiles)) 531 { 532 for (const std::filesystem::path &file : redfishLogFiles) 533 { 534 std::error_code ec; 535 std::filesystem::remove(file, ec); 536 } 537 } 538 539 // Reload rsyslog so it knows to start new log files 540 crow::connections::systemBus->async_method_call( 541 [asyncResp](const boost::system::error_code ec) { 542 if (ec) 543 { 544 BMCWEB_LOG_ERROR << "Failed to reload rsyslog: " << ec; 545 messages::internalError(asyncResp->res); 546 return; 547 } 548 549 messages::success(asyncResp->res); 550 }, 551 "org.freedesktop.systemd1", "/org/freedesktop/systemd1", 552 "org.freedesktop.systemd1.Manager", "ReloadUnit", "rsyslog.service", 553 "replace"); 554 } 555 }; 556 557 static int fillEventLogEntryJson(const std::string &logEntryID, 558 const std::string logEntry, 559 nlohmann::json &logEntryJson) 560 { 561 // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>" 562 // First get the Timestamp 563 size_t space = logEntry.find_first_of(" "); 564 if (space == std::string::npos) 565 { 566 return 1; 567 } 568 std::string timestamp = logEntry.substr(0, space); 569 // Then get the log contents 570 size_t entryStart = logEntry.find_first_not_of(" ", space); 571 if (entryStart == std::string::npos) 572 { 573 return 1; 574 } 575 std::string_view entry(logEntry); 576 entry.remove_prefix(entryStart); 577 // Use split to separate the entry into its fields 578 std::vector<std::string> logEntryFields; 579 boost::split(logEntryFields, entry, boost::is_any_of(","), 580 boost::token_compress_on); 581 // We need at least a MessageId to be valid 582 if (logEntryFields.size() < 1) 583 { 584 return 1; 585 } 586 std::string &messageID = logEntryFields[0]; 587 588 // Get the Message from the MessageRegistry 589 const message_registries::Message *message = 590 message_registries::getMessage(messageID); 591 592 std::string msg; 593 std::string severity; 594 if (message != nullptr) 595 { 596 msg = message->message; 597 severity = message->severity; 598 } 599 600 // Get the MessageArgs from the log if there are any 601 boost::beast::span<std::string> messageArgs; 602 if (logEntryFields.size() > 1) 603 { 604 std::string &messageArgsStart = logEntryFields[1]; 605 // If the first string is empty, assume there are no MessageArgs 606 std::size_t messageArgsSize = 0; 607 if (!messageArgsStart.empty()) 608 { 609 messageArgsSize = logEntryFields.size() - 1; 610 } 611 612 messageArgs = boost::beast::span(&messageArgsStart, messageArgsSize); 613 614 // Fill the MessageArgs into the Message 615 int i = 0; 616 for (const std::string &messageArg : messageArgs) 617 { 618 std::string argStr = "%" + std::to_string(++i); 619 size_t argPos = msg.find(argStr); 620 if (argPos != std::string::npos) 621 { 622 msg.replace(argPos, argStr.length(), messageArg); 623 } 624 } 625 } 626 627 // Get the Created time from the timestamp. The log timestamp is in RFC3339 628 // format which matches the Redfish format except for the fractional seconds 629 // between the '.' and the '+', so just remove them. 630 std::size_t dot = timestamp.find_first_of("."); 631 std::size_t plus = timestamp.find_first_of("+"); 632 if (dot != std::string::npos && plus != std::string::npos) 633 { 634 timestamp.erase(dot, plus - dot); 635 } 636 637 // Fill in the log entry with the gathered data 638 logEntryJson = { 639 {"@odata.type", "#LogEntry.v1_4_0.LogEntry"}, 640 {"@odata.context", "/redfish/v1/$metadata#LogEntry.LogEntry"}, 641 {"@odata.id", 642 "/redfish/v1/Systems/system/LogServices/EventLog/Entries/#" + 643 logEntryID}, 644 {"Name", "System Event Log Entry"}, 645 {"Id", logEntryID}, 646 {"Message", std::move(msg)}, 647 {"MessageId", std::move(messageID)}, 648 {"MessageArgs", std::move(messageArgs)}, 649 {"EntryType", "Event"}, 650 {"Severity", std::move(severity)}, 651 {"Created", std::move(timestamp)}}; 652 return 0; 653 } 654 655 class JournalEventLogEntryCollection : public Node 656 { 657 public: 658 template <typename CrowApp> 659 JournalEventLogEntryCollection(CrowApp &app) : 660 Node(app, "/redfish/v1/Systems/system/LogServices/EventLog/Entries/") 661 { 662 entityPrivileges = { 663 {boost::beast::http::verb::get, {{"Login"}}}, 664 {boost::beast::http::verb::head, {{"Login"}}}, 665 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 666 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 667 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 668 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 669 } 670 671 private: 672 void doGet(crow::Response &res, const crow::Request &req, 673 const std::vector<std::string> ¶ms) override 674 { 675 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 676 long skip = 0; 677 long top = maxEntriesPerPage; // Show max entries by default 678 if (!getSkipParam(asyncResp->res, req, skip)) 679 { 680 return; 681 } 682 if (!getTopParam(asyncResp->res, req, top)) 683 { 684 return; 685 } 686 // Collections don't include the static data added by SubRoute because 687 // it has a duplicate entry for members 688 asyncResp->res.jsonValue["@odata.type"] = 689 "#LogEntryCollection.LogEntryCollection"; 690 asyncResp->res.jsonValue["@odata.context"] = 691 "/redfish/v1/$metadata#LogEntryCollection.LogEntryCollection"; 692 asyncResp->res.jsonValue["@odata.id"] = 693 "/redfish/v1/Systems/system/LogServices/EventLog/Entries"; 694 asyncResp->res.jsonValue["Name"] = "System Event Log Entries"; 695 asyncResp->res.jsonValue["Description"] = 696 "Collection of System Event Log Entries"; 697 698 nlohmann::json &logEntryArray = asyncResp->res.jsonValue["Members"]; 699 logEntryArray = nlohmann::json::array(); 700 // Go through the log files and create a unique ID for each entry 701 std::vector<std::filesystem::path> redfishLogFiles; 702 getRedfishLogFiles(redfishLogFiles); 703 uint64_t entryCount = 0; 704 std::string logEntry; 705 706 // Oldest logs are in the last file, so start there and loop backwards 707 for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); 708 it++) 709 { 710 std::ifstream logStream(*it); 711 if (!logStream.is_open()) 712 { 713 continue; 714 } 715 716 while (std::getline(logStream, logEntry)) 717 { 718 entryCount++; 719 // Handle paging using skip (number of entries to skip from the 720 // start) and top (number of entries to display) 721 if (entryCount <= skip || entryCount > skip + top) 722 { 723 continue; 724 } 725 726 std::string idStr; 727 if (!getUniqueEntryID(logEntry, idStr)) 728 { 729 continue; 730 } 731 732 logEntryArray.push_back({}); 733 nlohmann::json &bmcLogEntry = logEntryArray.back(); 734 if (fillEventLogEntryJson(idStr, logEntry, bmcLogEntry) != 0) 735 { 736 messages::internalError(asyncResp->res); 737 return; 738 } 739 } 740 } 741 asyncResp->res.jsonValue["Members@odata.count"] = entryCount; 742 if (skip + top < entryCount) 743 { 744 asyncResp->res.jsonValue["Members@odata.nextLink"] = 745 "/redfish/v1/Systems/system/LogServices/EventLog/" 746 "Entries?$skip=" + 747 std::to_string(skip + top); 748 } 749 } 750 }; 751 752 class DBusEventLogEntryCollection : public Node 753 { 754 public: 755 template <typename CrowApp> 756 DBusEventLogEntryCollection(CrowApp &app) : 757 Node(app, "/redfish/v1/Systems/system/LogServices/EventLog/Entries/") 758 { 759 entityPrivileges = { 760 {boost::beast::http::verb::get, {{"Login"}}}, 761 {boost::beast::http::verb::head, {{"Login"}}}, 762 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 763 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 764 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 765 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 766 } 767 768 private: 769 void doGet(crow::Response &res, const crow::Request &req, 770 const std::vector<std::string> ¶ms) override 771 { 772 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 773 774 // Collections don't include the static data added by SubRoute because 775 // it has a duplicate entry for members 776 asyncResp->res.jsonValue["@odata.type"] = 777 "#LogEntryCollection.LogEntryCollection"; 778 asyncResp->res.jsonValue["@odata.context"] = 779 "/redfish/v1/$metadata#LogEntryCollection.LogEntryCollection"; 780 asyncResp->res.jsonValue["@odata.id"] = 781 "/redfish/v1/Systems/system/LogServices/EventLog/Entries"; 782 asyncResp->res.jsonValue["Name"] = "System Event Log Entries"; 783 asyncResp->res.jsonValue["Description"] = 784 "Collection of System Event Log Entries"; 785 786 // DBus implementation of EventLog/Entries 787 // Make call to Logging Service to find all log entry objects 788 crow::connections::systemBus->async_method_call( 789 [asyncResp](const boost::system::error_code ec, 790 GetManagedObjectsType &resp) { 791 if (ec) 792 { 793 // TODO Handle for specific error code 794 BMCWEB_LOG_ERROR 795 << "getLogEntriesIfaceData resp_handler got error " 796 << ec; 797 messages::internalError(asyncResp->res); 798 return; 799 } 800 nlohmann::json &entriesArray = 801 asyncResp->res.jsonValue["Members"]; 802 entriesArray = nlohmann::json::array(); 803 for (auto &objectPath : resp) 804 { 805 for (auto &interfaceMap : objectPath.second) 806 { 807 if (interfaceMap.first != 808 "xyz.openbmc_project.Logging.Entry") 809 { 810 BMCWEB_LOG_DEBUG << "Bailing early on " 811 << interfaceMap.first; 812 continue; 813 } 814 entriesArray.push_back({}); 815 nlohmann::json &thisEntry = entriesArray.back(); 816 uint32_t *id; 817 std::time_t timestamp; 818 std::string *severity, *message; 819 bool *resolved; 820 for (auto &propertyMap : interfaceMap.second) 821 { 822 if (propertyMap.first == "Id") 823 { 824 id = sdbusplus::message::variant_ns::get_if< 825 uint32_t>(&propertyMap.second); 826 if (id == nullptr) 827 { 828 messages::propertyMissing(asyncResp->res, 829 "Id"); 830 } 831 } 832 else if (propertyMap.first == "Timestamp") 833 { 834 const uint64_t *millisTimeStamp = 835 std::get_if<uint64_t>(&propertyMap.second); 836 if (millisTimeStamp == nullptr) 837 { 838 messages::propertyMissing(asyncResp->res, 839 "Timestamp"); 840 } 841 // Retrieve Created property with format: 842 // yyyy-mm-ddThh:mm:ss 843 std::chrono::milliseconds chronoTimeStamp( 844 *millisTimeStamp); 845 timestamp = 846 std::chrono::duration_cast< 847 std::chrono::seconds>(chronoTimeStamp) 848 .count(); 849 } 850 else if (propertyMap.first == "Severity") 851 { 852 severity = std::get_if<std::string>( 853 &propertyMap.second); 854 if (severity == nullptr) 855 { 856 messages::propertyMissing(asyncResp->res, 857 "Severity"); 858 } 859 } 860 else if (propertyMap.first == "Message") 861 { 862 message = std::get_if<std::string>( 863 &propertyMap.second); 864 if (message == nullptr) 865 { 866 messages::propertyMissing(asyncResp->res, 867 "Message"); 868 } 869 } 870 } 871 thisEntry = { 872 {"@odata.type", "#LogEntry.v1_4_0.LogEntry"}, 873 {"@odata.context", "/redfish/v1/" 874 "$metadata#LogEntry.LogEntry"}, 875 {"@odata.id", 876 "/redfish/v1/Systems/system/LogServices/EventLog/" 877 "Entries/" + 878 std::to_string(*id)}, 879 {"Name", "System Event Log Entry"}, 880 {"Id", std::to_string(*id)}, 881 {"Message", *message}, 882 {"EntryType", "Event"}, 883 {"Severity", 884 translateSeverityDbusToRedfish(*severity)}, 885 {"Created", crow::utility::getDateTime(timestamp)}}; 886 } 887 } 888 std::sort(entriesArray.begin(), entriesArray.end(), 889 [](const nlohmann::json &left, 890 const nlohmann::json &right) { 891 return (left["Id"] <= right["Id"]); 892 }); 893 asyncResp->res.jsonValue["Members@odata.count"] = 894 entriesArray.size(); 895 }, 896 "xyz.openbmc_project.Logging", "/xyz/openbmc_project/logging", 897 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); 898 } 899 }; 900 901 class DBusEventLogEntry : public Node 902 { 903 public: 904 DBusEventLogEntry(CrowApp &app) : 905 Node(app, 906 "/redfish/v1/Systems/system/LogServices/EventLog/Entries/<str>/", 907 std::string()) 908 { 909 entityPrivileges = { 910 {boost::beast::http::verb::get, {{"Login"}}}, 911 {boost::beast::http::verb::head, {{"Login"}}}, 912 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 913 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 914 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 915 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 916 } 917 918 private: 919 void doGet(crow::Response &res, const crow::Request &req, 920 const std::vector<std::string> ¶ms) override 921 { 922 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 923 if (params.size() != 1) 924 { 925 messages::internalError(asyncResp->res); 926 return; 927 } 928 const std::string &entryID = params[0]; 929 930 // DBus implementation of EventLog/Entries 931 // Make call to Logging Service to find all log entry objects 932 crow::connections::systemBus->async_method_call( 933 [asyncResp, entryID](const boost::system::error_code ec, 934 GetManagedPropertyType &resp) { 935 if (ec) 936 { 937 BMCWEB_LOG_ERROR 938 << "EventLogEntry (DBus) resp_handler got error " << ec; 939 messages::internalError(asyncResp->res); 940 return; 941 } 942 uint32_t *id; 943 std::time_t timestamp; 944 std::string *severity, *message; 945 bool *resolved; 946 for (auto &propertyMap : resp) 947 { 948 if (propertyMap.first == "Id") 949 { 950 id = std::get_if<uint32_t>(&propertyMap.second); 951 if (id == nullptr) 952 { 953 messages::propertyMissing(asyncResp->res, "Id"); 954 } 955 } 956 else if (propertyMap.first == "Timestamp") 957 { 958 const uint64_t *millisTimeStamp = 959 std::get_if<uint64_t>(&propertyMap.second); 960 if (millisTimeStamp == nullptr) 961 { 962 messages::propertyMissing(asyncResp->res, 963 "Timestamp"); 964 } 965 // Retrieve Created property with format: 966 // yyyy-mm-ddThh:mm:ss 967 std::chrono::milliseconds chronoTimeStamp( 968 *millisTimeStamp); 969 timestamp = 970 std::chrono::duration_cast<std::chrono::seconds>( 971 chronoTimeStamp) 972 .count(); 973 } 974 else if (propertyMap.first == "Severity") 975 { 976 severity = 977 std::get_if<std::string>(&propertyMap.second); 978 if (severity == nullptr) 979 { 980 messages::propertyMissing(asyncResp->res, 981 "Severity"); 982 } 983 } 984 else if (propertyMap.first == "Message") 985 { 986 message = std::get_if<std::string>(&propertyMap.second); 987 if (message == nullptr) 988 { 989 messages::propertyMissing(asyncResp->res, 990 "Message"); 991 } 992 } 993 } 994 asyncResp->res.jsonValue = { 995 {"@odata.type", "#LogEntry.v1_4_0.LogEntry"}, 996 {"@odata.context", "/redfish/v1/" 997 "$metadata#LogEntry.LogEntry"}, 998 {"@odata.id", 999 "/redfish/v1/Systems/system/LogServices/EventLog/" 1000 "Entries/" + 1001 std::to_string(*id)}, 1002 {"Name", "System Event Log Entry"}, 1003 {"Id", std::to_string(*id)}, 1004 {"Message", *message}, 1005 {"EntryType", "Event"}, 1006 {"Severity", translateSeverityDbusToRedfish(*severity)}, 1007 {"Created", crow::utility::getDateTime(timestamp)}}; 1008 }, 1009 "xyz.openbmc_project.Logging", 1010 "/xyz/openbmc_project/logging/entry/" + entryID, 1011 "org.freedesktop.DBus.Properties", "GetAll", 1012 "xyz.openbmc_project.Logging.Entry"); 1013 } 1014 }; 1015 1016 class BMCLogServiceCollection : public Node 1017 { 1018 public: 1019 template <typename CrowApp> 1020 BMCLogServiceCollection(CrowApp &app) : 1021 Node(app, "/redfish/v1/Managers/bmc/LogServices/") 1022 { 1023 entityPrivileges = { 1024 {boost::beast::http::verb::get, {{"Login"}}}, 1025 {boost::beast::http::verb::head, {{"Login"}}}, 1026 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 1027 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 1028 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 1029 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 1030 } 1031 1032 private: 1033 /** 1034 * Functions triggers appropriate requests on DBus 1035 */ 1036 void doGet(crow::Response &res, const crow::Request &req, 1037 const std::vector<std::string> ¶ms) override 1038 { 1039 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 1040 // Collections don't include the static data added by SubRoute because 1041 // it has a duplicate entry for members 1042 asyncResp->res.jsonValue["@odata.type"] = 1043 "#LogServiceCollection.LogServiceCollection"; 1044 asyncResp->res.jsonValue["@odata.context"] = 1045 "/redfish/v1/$metadata#LogServiceCollection.LogServiceCollection"; 1046 asyncResp->res.jsonValue["@odata.id"] = 1047 "/redfish/v1/Managers/bmc/LogServices"; 1048 asyncResp->res.jsonValue["Name"] = "Open BMC Log Services Collection"; 1049 asyncResp->res.jsonValue["Description"] = 1050 "Collection of LogServices for this Manager"; 1051 nlohmann::json &logServiceArray = asyncResp->res.jsonValue["Members"]; 1052 logServiceArray = nlohmann::json::array(); 1053 #ifdef BMCWEB_ENABLE_REDFISH_BMC_JOURNAL 1054 logServiceArray.push_back( 1055 {{"@odata.id", "/redfish/v1/Managers/bmc/LogServices/Journal"}}); 1056 #endif 1057 asyncResp->res.jsonValue["Members@odata.count"] = 1058 logServiceArray.size(); 1059 } 1060 }; 1061 1062 class BMCJournalLogService : public Node 1063 { 1064 public: 1065 template <typename CrowApp> 1066 BMCJournalLogService(CrowApp &app) : 1067 Node(app, "/redfish/v1/Managers/bmc/LogServices/Journal/") 1068 { 1069 entityPrivileges = { 1070 {boost::beast::http::verb::get, {{"Login"}}}, 1071 {boost::beast::http::verb::head, {{"Login"}}}, 1072 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 1073 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 1074 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 1075 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 1076 } 1077 1078 private: 1079 void doGet(crow::Response &res, const crow::Request &req, 1080 const std::vector<std::string> ¶ms) override 1081 { 1082 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 1083 asyncResp->res.jsonValue["@odata.type"] = 1084 "#LogService.v1_1_0.LogService"; 1085 asyncResp->res.jsonValue["@odata.id"] = 1086 "/redfish/v1/Managers/bmc/LogServices/Journal"; 1087 asyncResp->res.jsonValue["@odata.context"] = 1088 "/redfish/v1/$metadata#LogService.LogService"; 1089 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service"; 1090 asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service"; 1091 asyncResp->res.jsonValue["Id"] = "BMC Journal"; 1092 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 1093 asyncResp->res.jsonValue["Entries"] = { 1094 {"@odata.id", 1095 "/redfish/v1/Managers/bmc/LogServices/Journal/Entries"}}; 1096 } 1097 }; 1098 1099 static int fillBMCJournalLogEntryJson(const std::string &bmcJournalLogEntryID, 1100 sd_journal *journal, 1101 nlohmann::json &bmcJournalLogEntryJson) 1102 { 1103 // Get the Log Entry contents 1104 int ret = 0; 1105 1106 std::string_view msg; 1107 ret = getJournalMetadata(journal, "MESSAGE", msg); 1108 if (ret < 0) 1109 { 1110 BMCWEB_LOG_ERROR << "Failed to read MESSAGE field: " << strerror(-ret); 1111 return 1; 1112 } 1113 1114 // Get the severity from the PRIORITY field 1115 int severity = 8; // Default to an invalid priority 1116 ret = getJournalMetadata(journal, "PRIORITY", 10, severity); 1117 if (ret < 0) 1118 { 1119 BMCWEB_LOG_ERROR << "Failed to read PRIORITY field: " << strerror(-ret); 1120 return 1; 1121 } 1122 1123 // Get the Created time from the timestamp 1124 std::string entryTimeStr; 1125 if (!getEntryTimestamp(journal, entryTimeStr)) 1126 { 1127 return 1; 1128 } 1129 1130 // Fill in the log entry with the gathered data 1131 bmcJournalLogEntryJson = { 1132 {"@odata.type", "#LogEntry.v1_4_0.LogEntry"}, 1133 {"@odata.context", "/redfish/v1/$metadata#LogEntry.LogEntry"}, 1134 {"@odata.id", "/redfish/v1/Managers/bmc/LogServices/Journal/Entries/" + 1135 bmcJournalLogEntryID}, 1136 {"Name", "BMC Journal Entry"}, 1137 {"Id", bmcJournalLogEntryID}, 1138 {"Message", msg}, 1139 {"EntryType", "Oem"}, 1140 {"Severity", 1141 severity <= 2 ? "Critical" 1142 : severity <= 4 ? "Warning" : severity <= 7 ? "OK" : ""}, 1143 {"OemRecordFormat", "BMC Journal Entry"}, 1144 {"Created", std::move(entryTimeStr)}}; 1145 return 0; 1146 } 1147 1148 class BMCJournalLogEntryCollection : public Node 1149 { 1150 public: 1151 template <typename CrowApp> 1152 BMCJournalLogEntryCollection(CrowApp &app) : 1153 Node(app, "/redfish/v1/Managers/bmc/LogServices/Journal/Entries/") 1154 { 1155 entityPrivileges = { 1156 {boost::beast::http::verb::get, {{"Login"}}}, 1157 {boost::beast::http::verb::head, {{"Login"}}}, 1158 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 1159 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 1160 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 1161 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 1162 } 1163 1164 private: 1165 void doGet(crow::Response &res, const crow::Request &req, 1166 const std::vector<std::string> ¶ms) override 1167 { 1168 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 1169 static constexpr const long maxEntriesPerPage = 1000; 1170 long skip = 0; 1171 long top = maxEntriesPerPage; // Show max entries by default 1172 if (!getSkipParam(asyncResp->res, req, skip)) 1173 { 1174 return; 1175 } 1176 if (!getTopParam(asyncResp->res, req, top)) 1177 { 1178 return; 1179 } 1180 // Collections don't include the static data added by SubRoute because 1181 // it has a duplicate entry for members 1182 asyncResp->res.jsonValue["@odata.type"] = 1183 "#LogEntryCollection.LogEntryCollection"; 1184 asyncResp->res.jsonValue["@odata.id"] = 1185 "/redfish/v1/Managers/bmc/LogServices/Journal/Entries"; 1186 asyncResp->res.jsonValue["@odata.context"] = 1187 "/redfish/v1/$metadata#LogEntryCollection.LogEntryCollection"; 1188 asyncResp->res.jsonValue["@odata.id"] = 1189 "/redfish/v1/Managers/bmc/LogServices/Journal/Entries"; 1190 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries"; 1191 asyncResp->res.jsonValue["Description"] = 1192 "Collection of BMC Journal Entries"; 1193 asyncResp->res.jsonValue["@odata.id"] = 1194 "/redfish/v1/Managers/bmc/LogServices/BmcLog/Entries"; 1195 nlohmann::json &logEntryArray = asyncResp->res.jsonValue["Members"]; 1196 logEntryArray = nlohmann::json::array(); 1197 1198 // Go through the journal and use the timestamp to create a unique ID 1199 // for each entry 1200 sd_journal *journalTmp = nullptr; 1201 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 1202 if (ret < 0) 1203 { 1204 BMCWEB_LOG_ERROR << "failed to open journal: " << strerror(-ret); 1205 messages::internalError(asyncResp->res); 1206 return; 1207 } 1208 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 1209 journalTmp, sd_journal_close); 1210 journalTmp = nullptr; 1211 uint64_t entryCount = 0; 1212 SD_JOURNAL_FOREACH(journal.get()) 1213 { 1214 entryCount++; 1215 // Handle paging using skip (number of entries to skip from the 1216 // start) and top (number of entries to display) 1217 if (entryCount <= skip || entryCount > skip + top) 1218 { 1219 continue; 1220 } 1221 1222 std::string idStr; 1223 if (!getUniqueEntryID(journal.get(), idStr)) 1224 { 1225 continue; 1226 } 1227 1228 logEntryArray.push_back({}); 1229 nlohmann::json &bmcJournalLogEntry = logEntryArray.back(); 1230 if (fillBMCJournalLogEntryJson(idStr, journal.get(), 1231 bmcJournalLogEntry) != 0) 1232 { 1233 messages::internalError(asyncResp->res); 1234 return; 1235 } 1236 } 1237 asyncResp->res.jsonValue["Members@odata.count"] = entryCount; 1238 if (skip + top < entryCount) 1239 { 1240 asyncResp->res.jsonValue["Members@odata.nextLink"] = 1241 "/redfish/v1/Managers/bmc/LogServices/Journal/Entries?$skip=" + 1242 std::to_string(skip + top); 1243 } 1244 } 1245 }; 1246 1247 class BMCJournalLogEntry : public Node 1248 { 1249 public: 1250 BMCJournalLogEntry(CrowApp &app) : 1251 Node(app, "/redfish/v1/Managers/bmc/LogServices/Journal/Entries/<str>/", 1252 std::string()) 1253 { 1254 entityPrivileges = { 1255 {boost::beast::http::verb::get, {{"Login"}}}, 1256 {boost::beast::http::verb::head, {{"Login"}}}, 1257 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 1258 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 1259 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 1260 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 1261 } 1262 1263 private: 1264 void doGet(crow::Response &res, const crow::Request &req, 1265 const std::vector<std::string> ¶ms) override 1266 { 1267 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 1268 if (params.size() != 1) 1269 { 1270 messages::internalError(asyncResp->res); 1271 return; 1272 } 1273 const std::string &entryID = params[0]; 1274 // Convert the unique ID back to a timestamp to find the entry 1275 uint64_t ts = 0; 1276 uint16_t index = 0; 1277 if (!getTimestampFromID(asyncResp->res, entryID, ts, index)) 1278 { 1279 return; 1280 } 1281 1282 sd_journal *journalTmp = nullptr; 1283 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 1284 if (ret < 0) 1285 { 1286 BMCWEB_LOG_ERROR << "failed to open journal: " << strerror(-ret); 1287 messages::internalError(asyncResp->res); 1288 return; 1289 } 1290 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 1291 journalTmp, sd_journal_close); 1292 journalTmp = nullptr; 1293 // Go to the timestamp in the log and move to the entry at the index 1294 ret = sd_journal_seek_realtime_usec(journal.get(), ts); 1295 for (int i = 0; i <= index; i++) 1296 { 1297 sd_journal_next(journal.get()); 1298 } 1299 // Confirm that the entry ID matches what was requested 1300 std::string idStr; 1301 if (!getUniqueEntryID(journal.get(), idStr) || idStr != entryID) 1302 { 1303 messages::resourceMissingAtURI(asyncResp->res, entryID); 1304 return; 1305 } 1306 1307 if (fillBMCJournalLogEntryJson(entryID, journal.get(), 1308 asyncResp->res.jsonValue) != 0) 1309 { 1310 messages::internalError(asyncResp->res); 1311 return; 1312 } 1313 } 1314 }; 1315 1316 class CrashdumpService : public Node 1317 { 1318 public: 1319 template <typename CrowApp> 1320 CrashdumpService(CrowApp &app) : 1321 Node(app, "/redfish/v1/Systems/system/LogServices/Crashdump/") 1322 { 1323 entityPrivileges = { 1324 {boost::beast::http::verb::get, {{"Login"}}}, 1325 {boost::beast::http::verb::head, {{"Login"}}}, 1326 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 1327 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 1328 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 1329 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 1330 } 1331 1332 private: 1333 /** 1334 * Functions triggers appropriate requests on DBus 1335 */ 1336 void doGet(crow::Response &res, const crow::Request &req, 1337 const std::vector<std::string> ¶ms) override 1338 { 1339 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 1340 // Copy over the static data to include the entries added by SubRoute 1341 asyncResp->res.jsonValue["@odata.id"] = 1342 "/redfish/v1/Systems/system/LogServices/Crashdump"; 1343 asyncResp->res.jsonValue["@odata.type"] = 1344 "#LogService.v1_1_0.LogService"; 1345 asyncResp->res.jsonValue["@odata.context"] = 1346 "/redfish/v1/$metadata#LogService.LogService"; 1347 asyncResp->res.jsonValue["Name"] = "Open BMC Crashdump Service"; 1348 asyncResp->res.jsonValue["Description"] = "Crashdump Service"; 1349 asyncResp->res.jsonValue["Id"] = "Crashdump"; 1350 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 1351 asyncResp->res.jsonValue["MaxNumberOfRecords"] = 3; 1352 asyncResp->res.jsonValue["Entries"] = { 1353 {"@odata.id", 1354 "/redfish/v1/Systems/system/LogServices/Crashdump/Entries"}}; 1355 asyncResp->res.jsonValue["Actions"] = { 1356 {"Oem", 1357 {{"#Crashdump.OnDemand", 1358 {{"target", "/redfish/v1/Systems/system/LogServices/Crashdump/" 1359 "Actions/Oem/Crashdump.OnDemand"}}}}}}; 1360 1361 #ifdef BMCWEB_ENABLE_REDFISH_RAW_PECI 1362 asyncResp->res.jsonValue["Actions"]["Oem"].push_back( 1363 {"#Crashdump.SendRawPeci", 1364 {{"target", "/redfish/v1/Systems/system/LogServices/Crashdump/" 1365 "Actions/Oem/Crashdump.SendRawPeci"}}}); 1366 #endif 1367 } 1368 }; 1369 1370 class CrashdumpEntryCollection : public Node 1371 { 1372 public: 1373 template <typename CrowApp> 1374 CrashdumpEntryCollection(CrowApp &app) : 1375 Node(app, "/redfish/v1/Systems/system/LogServices/Crashdump/Entries/") 1376 { 1377 entityPrivileges = { 1378 {boost::beast::http::verb::get, {{"Login"}}}, 1379 {boost::beast::http::verb::head, {{"Login"}}}, 1380 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 1381 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 1382 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 1383 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 1384 } 1385 1386 private: 1387 /** 1388 * Functions triggers appropriate requests on DBus 1389 */ 1390 void doGet(crow::Response &res, const crow::Request &req, 1391 const std::vector<std::string> ¶ms) override 1392 { 1393 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 1394 // Collections don't include the static data added by SubRoute because 1395 // it has a duplicate entry for members 1396 auto getLogEntriesCallback = [asyncResp]( 1397 const boost::system::error_code ec, 1398 const std::vector<std::string> &resp) { 1399 if (ec) 1400 { 1401 if (ec.value() != 1402 boost::system::errc::no_such_file_or_directory) 1403 { 1404 BMCWEB_LOG_DEBUG << "failed to get entries ec: " 1405 << ec.message(); 1406 messages::internalError(asyncResp->res); 1407 return; 1408 } 1409 } 1410 asyncResp->res.jsonValue["@odata.type"] = 1411 "#LogEntryCollection.LogEntryCollection"; 1412 asyncResp->res.jsonValue["@odata.id"] = 1413 "/redfish/v1/Systems/system/LogServices/Crashdump/Entries"; 1414 asyncResp->res.jsonValue["@odata.context"] = 1415 "/redfish/v1/" 1416 "$metadata#LogEntryCollection.LogEntryCollection"; 1417 asyncResp->res.jsonValue["Name"] = "Open BMC Crashdump Entries"; 1418 asyncResp->res.jsonValue["Description"] = 1419 "Collection of Crashdump Entries"; 1420 nlohmann::json &logEntryArray = asyncResp->res.jsonValue["Members"]; 1421 logEntryArray = nlohmann::json::array(); 1422 for (const std::string &objpath : resp) 1423 { 1424 // Don't list the on-demand log 1425 if (objpath.compare(CrashdumpOnDemandPath) == 0) 1426 { 1427 continue; 1428 } 1429 std::size_t lastPos = objpath.rfind("/"); 1430 if (lastPos != std::string::npos) 1431 { 1432 logEntryArray.push_back( 1433 {{"@odata.id", "/redfish/v1/Systems/system/LogServices/" 1434 "Crashdump/Entries/" + 1435 objpath.substr(lastPos + 1)}}); 1436 } 1437 } 1438 asyncResp->res.jsonValue["Members@odata.count"] = 1439 logEntryArray.size(); 1440 }; 1441 crow::connections::systemBus->async_method_call( 1442 std::move(getLogEntriesCallback), 1443 "xyz.openbmc_project.ObjectMapper", 1444 "/xyz/openbmc_project/object_mapper", 1445 "xyz.openbmc_project.ObjectMapper", "GetSubTreePaths", "", 0, 1446 std::array<const char *, 1>{CrashdumpInterface}); 1447 } 1448 }; 1449 1450 std::string getLogCreatedTime(const nlohmann::json &Crashdump) 1451 { 1452 nlohmann::json::const_iterator cdIt = Crashdump.find("crashlog_data"); 1453 if (cdIt != Crashdump.end()) 1454 { 1455 nlohmann::json::const_iterator siIt = cdIt->find("SYSTEM_INFO"); 1456 if (siIt != cdIt->end()) 1457 { 1458 nlohmann::json::const_iterator tsIt = siIt->find("timestamp"); 1459 if (tsIt != siIt->end()) 1460 { 1461 const std::string *logTime = 1462 tsIt->get_ptr<const std::string *>(); 1463 if (logTime != nullptr) 1464 { 1465 return *logTime; 1466 } 1467 } 1468 } 1469 } 1470 BMCWEB_LOG_DEBUG << "failed to find log timestamp"; 1471 1472 return std::string(); 1473 } 1474 1475 class CrashdumpEntry : public Node 1476 { 1477 public: 1478 CrashdumpEntry(CrowApp &app) : 1479 Node(app, 1480 "/redfish/v1/Systems/system/LogServices/Crashdump/Entries/<str>/", 1481 std::string()) 1482 { 1483 entityPrivileges = { 1484 {boost::beast::http::verb::get, {{"Login"}}}, 1485 {boost::beast::http::verb::head, {{"Login"}}}, 1486 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 1487 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 1488 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 1489 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 1490 } 1491 1492 private: 1493 void doGet(crow::Response &res, const crow::Request &req, 1494 const std::vector<std::string> ¶ms) override 1495 { 1496 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 1497 if (params.size() != 1) 1498 { 1499 messages::internalError(asyncResp->res); 1500 return; 1501 } 1502 const uint8_t logId = std::atoi(params[0].c_str()); 1503 auto getStoredLogCallback = [asyncResp, logId]( 1504 const boost::system::error_code ec, 1505 const std::variant<std::string> &resp) { 1506 if (ec) 1507 { 1508 BMCWEB_LOG_DEBUG << "failed to get log ec: " << ec.message(); 1509 messages::internalError(asyncResp->res); 1510 return; 1511 } 1512 const std::string *log = std::get_if<std::string>(&resp); 1513 if (log == nullptr) 1514 { 1515 messages::internalError(asyncResp->res); 1516 return; 1517 } 1518 nlohmann::json j = nlohmann::json::parse(*log, nullptr, false); 1519 if (j.is_discarded()) 1520 { 1521 messages::internalError(asyncResp->res); 1522 return; 1523 } 1524 std::string t = getLogCreatedTime(j); 1525 asyncResp->res.jsonValue = { 1526 {"@odata.type", "#LogEntry.v1_4_0.LogEntry"}, 1527 {"@odata.context", "/redfish/v1/$metadata#LogEntry.LogEntry"}, 1528 {"@odata.id", 1529 "/redfish/v1/Systems/system/LogServices/Crashdump/Entries/" + 1530 std::to_string(logId)}, 1531 {"Name", "CPU Crashdump"}, 1532 {"Id", logId}, 1533 {"EntryType", "Oem"}, 1534 {"OemRecordFormat", "Intel Crashdump"}, 1535 {"Oem", {{"Intel", std::move(j)}}}, 1536 {"Created", std::move(t)}}; 1537 }; 1538 crow::connections::systemBus->async_method_call( 1539 std::move(getStoredLogCallback), CrashdumpObject, 1540 CrashdumpPath + std::string("/") + std::to_string(logId), 1541 "org.freedesktop.DBus.Properties", "Get", CrashdumpInterface, 1542 "Log"); 1543 } 1544 }; 1545 1546 class OnDemandCrashdump : public Node 1547 { 1548 public: 1549 OnDemandCrashdump(CrowApp &app) : 1550 Node(app, 1551 "/redfish/v1/Systems/system/LogServices/Crashdump/Actions/Oem/" 1552 "Crashdump.OnDemand/") 1553 { 1554 entityPrivileges = { 1555 {boost::beast::http::verb::get, {{"Login"}}}, 1556 {boost::beast::http::verb::head, {{"Login"}}}, 1557 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 1558 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 1559 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 1560 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 1561 } 1562 1563 private: 1564 void doPost(crow::Response &res, const crow::Request &req, 1565 const std::vector<std::string> ¶ms) override 1566 { 1567 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 1568 static std::unique_ptr<sdbusplus::bus::match::match> onDemandLogMatcher; 1569 1570 // Only allow one OnDemand Log request at a time 1571 if (onDemandLogMatcher != nullptr) 1572 { 1573 asyncResp->res.addHeader("Retry-After", "30"); 1574 messages::serviceTemporarilyUnavailable(asyncResp->res, "30"); 1575 return; 1576 } 1577 // Make this static so it survives outside this method 1578 static boost::asio::deadline_timer timeout(*req.ioService); 1579 1580 timeout.expires_from_now(boost::posix_time::seconds(30)); 1581 timeout.async_wait([asyncResp](const boost::system::error_code &ec) { 1582 onDemandLogMatcher = nullptr; 1583 if (ec) 1584 { 1585 // operation_aborted is expected if timer is canceled before 1586 // completion. 1587 if (ec != boost::asio::error::operation_aborted) 1588 { 1589 BMCWEB_LOG_ERROR << "Async_wait failed " << ec; 1590 } 1591 return; 1592 } 1593 BMCWEB_LOG_ERROR << "Timed out waiting for on-demand log"; 1594 1595 messages::internalError(asyncResp->res); 1596 }); 1597 1598 auto onDemandLogMatcherCallback = [asyncResp]( 1599 sdbusplus::message::message &m) { 1600 BMCWEB_LOG_DEBUG << "OnDemand log available match fired"; 1601 boost::system::error_code ec; 1602 timeout.cancel(ec); 1603 if (ec) 1604 { 1605 BMCWEB_LOG_ERROR << "error canceling timer " << ec; 1606 } 1607 sdbusplus::message::object_path objPath; 1608 boost::container::flat_map< 1609 std::string, boost::container::flat_map< 1610 std::string, std::variant<std::string>>> 1611 interfacesAdded; 1612 m.read(objPath, interfacesAdded); 1613 const std::string *log = std::get_if<std::string>( 1614 &interfacesAdded[CrashdumpInterface]["Log"]); 1615 if (log == nullptr) 1616 { 1617 messages::internalError(asyncResp->res); 1618 // Careful with onDemandLogMatcher. It is a unique_ptr to the 1619 // match object inside which this lambda is executing. Once it 1620 // is set to nullptr, the match object will be destroyed and the 1621 // lambda will lose its context, including res, so it needs to 1622 // be the last thing done. 1623 onDemandLogMatcher = nullptr; 1624 return; 1625 } 1626 nlohmann::json j = nlohmann::json::parse(*log, nullptr, false); 1627 if (j.is_discarded()) 1628 { 1629 messages::internalError(asyncResp->res); 1630 // Careful with onDemandLogMatcher. It is a unique_ptr to the 1631 // match object inside which this lambda is executing. Once it 1632 // is set to nullptr, the match object will be destroyed and the 1633 // lambda will lose its context, including res, so it needs to 1634 // be the last thing done. 1635 onDemandLogMatcher = nullptr; 1636 return; 1637 } 1638 std::string t = getLogCreatedTime(j); 1639 asyncResp->res.jsonValue = { 1640 {"@odata.type", "#LogEntry.v1_4_0.LogEntry"}, 1641 {"@odata.context", "/redfish/v1/$metadata#LogEntry.LogEntry"}, 1642 {"Name", "CPU Crashdump"}, 1643 {"EntryType", "Oem"}, 1644 {"OemRecordFormat", "Intel Crashdump"}, 1645 {"Oem", {{"Intel", std::move(j)}}}, 1646 {"Created", std::move(t)}}; 1647 // Careful with onDemandLogMatcher. It is a unique_ptr to the 1648 // match object inside which this lambda is executing. Once it is 1649 // set to nullptr, the match object will be destroyed and the lambda 1650 // will lose its context, including res, so it needs to be the last 1651 // thing done. 1652 onDemandLogMatcher = nullptr; 1653 }; 1654 onDemandLogMatcher = std::make_unique<sdbusplus::bus::match::match>( 1655 *crow::connections::systemBus, 1656 sdbusplus::bus::match::rules::interfacesAdded() + 1657 sdbusplus::bus::match::rules::argNpath(0, 1658 CrashdumpOnDemandPath), 1659 std::move(onDemandLogMatcherCallback)); 1660 1661 auto generateonDemandLogCallback = 1662 [asyncResp](const boost::system::error_code ec, 1663 const std::string &resp) { 1664 if (ec) 1665 { 1666 if (ec.value() == 1667 boost::system::errc::operation_not_supported) 1668 { 1669 messages::resourceInStandby(asyncResp->res); 1670 } 1671 else 1672 { 1673 messages::internalError(asyncResp->res); 1674 } 1675 boost::system::error_code timeoutec; 1676 timeout.cancel(timeoutec); 1677 if (timeoutec) 1678 { 1679 BMCWEB_LOG_ERROR << "error canceling timer " 1680 << timeoutec; 1681 } 1682 onDemandLogMatcher = nullptr; 1683 return; 1684 } 1685 }; 1686 crow::connections::systemBus->async_method_call( 1687 std::move(generateonDemandLogCallback), CrashdumpObject, 1688 CrashdumpPath, CrashdumpOnDemandInterface, "GenerateOnDemandLog"); 1689 } 1690 }; 1691 1692 class SendRawPECI : public Node 1693 { 1694 public: 1695 SendRawPECI(CrowApp &app) : 1696 Node(app, 1697 "/redfish/v1/Systems/system/LogServices/Crashdump/Actions/Oem/" 1698 "Crashdump.SendRawPeci/") 1699 { 1700 entityPrivileges = { 1701 {boost::beast::http::verb::get, {{"ConfigureComponents"}}}, 1702 {boost::beast::http::verb::head, {{"ConfigureComponents"}}}, 1703 {boost::beast::http::verb::patch, {{"ConfigureComponents"}}}, 1704 {boost::beast::http::verb::put, {{"ConfigureComponents"}}}, 1705 {boost::beast::http::verb::delete_, {{"ConfigureComponents"}}}, 1706 {boost::beast::http::verb::post, {{"ConfigureComponents"}}}}; 1707 } 1708 1709 private: 1710 void doPost(crow::Response &res, const crow::Request &req, 1711 const std::vector<std::string> ¶ms) override 1712 { 1713 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 1714 uint8_t clientAddress = 0; 1715 uint8_t readLength = 0; 1716 std::vector<uint8_t> peciCommand; 1717 if (!json_util::readJson(req, res, "ClientAddress", clientAddress, 1718 "ReadLength", readLength, "PECICommand", 1719 peciCommand)) 1720 { 1721 return; 1722 } 1723 1724 // Callback to return the Raw PECI response 1725 auto sendRawPECICallback = 1726 [asyncResp](const boost::system::error_code ec, 1727 const std::vector<uint8_t> &resp) { 1728 if (ec) 1729 { 1730 BMCWEB_LOG_DEBUG << "failed to send PECI command ec: " 1731 << ec.message(); 1732 messages::internalError(asyncResp->res); 1733 return; 1734 } 1735 asyncResp->res.jsonValue = {{"Name", "PECI Command Response"}, 1736 {"PECIResponse", resp}}; 1737 }; 1738 // Call the SendRawPECI command with the provided data 1739 crow::connections::systemBus->async_method_call( 1740 std::move(sendRawPECICallback), CrashdumpObject, CrashdumpPath, 1741 CrashdumpRawPECIInterface, "SendRawPeci", clientAddress, readLength, 1742 peciCommand); 1743 } 1744 }; 1745 1746 /** 1747 * DBusLogServiceActionsClear class supports POST method for ClearLog action. 1748 */ 1749 class DBusLogServiceActionsClear : public Node 1750 { 1751 public: 1752 DBusLogServiceActionsClear(CrowApp &app) : 1753 Node(app, "/redfish/v1/Systems/system/LogServices/EventLog/Actions/" 1754 "LogService.Reset") 1755 { 1756 entityPrivileges = { 1757 {boost::beast::http::verb::get, {{"Login"}}}, 1758 {boost::beast::http::verb::head, {{"Login"}}}, 1759 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 1760 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 1761 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 1762 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 1763 } 1764 1765 private: 1766 /** 1767 * Function handles POST method request. 1768 * The Clear Log actions does not require any parameter.The action deletes 1769 * all entries found in the Entries collection for this Log Service. 1770 */ 1771 void doPost(crow::Response &res, const crow::Request &req, 1772 const std::vector<std::string> ¶ms) override 1773 { 1774 BMCWEB_LOG_DEBUG << "Do delete all entries."; 1775 1776 auto asyncResp = std::make_shared<AsyncResp>(res); 1777 // Process response from Logging service. 1778 auto resp_handler = [asyncResp](const boost::system::error_code ec) { 1779 BMCWEB_LOG_DEBUG << "doClearLog resp_handler callback: Done"; 1780 if (ec) 1781 { 1782 // TODO Handle for specific error code 1783 BMCWEB_LOG_ERROR << "doClearLog resp_handler got error " << ec; 1784 asyncResp->res.result( 1785 boost::beast::http::status::internal_server_error); 1786 return; 1787 } 1788 1789 asyncResp->res.result(boost::beast::http::status::no_content); 1790 }; 1791 1792 // Make call to Logging service to request Clear Log 1793 crow::connections::systemBus->async_method_call( 1794 resp_handler, "xyz.openbmc_project.Logging", 1795 "/xyz/openbmc_project/logging", 1796 "xyz.openbmc_project.Collection.DeleteAll", "DeleteAll"); 1797 } 1798 }; 1799 } // namespace redfish 1800