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