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 20 #include <systemd/sd-journal.h> 21 22 #include <boost/container/flat_map.hpp> 23 #include <boost/utility/string_view.hpp> 24 #include <experimental/filesystem> 25 26 namespace redfish 27 { 28 29 constexpr char const *cpuLogObject = "com.intel.CpuDebugLog"; 30 constexpr char const *cpuLogPath = "/com/intel/CpuDebugLog"; 31 constexpr char const *cpuLogImmediatePath = "/com/intel/CpuDebugLog/Immediate"; 32 constexpr char const *cpuLogInterface = "com.intel.CpuDebugLog"; 33 constexpr char const *cpuLogImmediateInterface = 34 "com.intel.CpuDebugLog.Immediate"; 35 constexpr char const *cpuLogRawPECIInterface = 36 "com.intel.CpuDebugLog.SendRawPeci"; 37 38 namespace fs = std::experimental::filesystem; 39 40 static int getJournalMetadata(sd_journal *journal, 41 const boost::string_view &field, 42 boost::string_view &contents) 43 { 44 const char *data = nullptr; 45 size_t length = 0; 46 int ret = 0; 47 // Get the metadata from the requested field of the journal entry 48 ret = sd_journal_get_data(journal, field.data(), (const void **)&data, 49 &length); 50 if (ret < 0) 51 { 52 return ret; 53 } 54 contents = boost::string_view(data, length); 55 // Only use the content after the "=" character. 56 contents.remove_prefix(std::min(contents.find("=") + 1, contents.size())); 57 return ret; 58 } 59 60 static int getJournalMetadata(sd_journal *journal, 61 const boost::string_view &field, const int &base, 62 int &contents) 63 { 64 int ret = 0; 65 boost::string_view metadata; 66 // Get the metadata from the requested field of the journal entry 67 ret = getJournalMetadata(journal, field, metadata); 68 if (ret < 0) 69 { 70 return ret; 71 } 72 contents = strtol(metadata.data(), nullptr, base); 73 return ret; 74 } 75 76 static bool getEntryTimestamp(sd_journal *journal, std::string &entryTimestamp) 77 { 78 int ret = 0; 79 uint64_t timestamp = 0; 80 ret = sd_journal_get_realtime_usec(journal, ×tamp); 81 if (ret < 0) 82 { 83 BMCWEB_LOG_ERROR << "Failed to read entry timestamp: " 84 << strerror(-ret); 85 return false; 86 } 87 time_t t = 88 static_cast<time_t>(timestamp / 1000 / 1000); // Convert from us to s 89 struct tm *loctime = localtime(&t); 90 char entryTime[64] = {}; 91 if (NULL != loctime) 92 { 93 strftime(entryTime, sizeof(entryTime), "%FT%T%z", loctime); 94 } 95 // Insert the ':' into the timezone 96 boost::string_view t1(entryTime); 97 boost::string_view t2(entryTime); 98 if (t1.size() > 2 && t2.size() > 2) 99 { 100 t1.remove_suffix(2); 101 t2.remove_prefix(t2.size() - 2); 102 } 103 entryTimestamp = t1.to_string() + ":" + t2.to_string(); 104 return true; 105 } 106 107 static bool getSkipParam(crow::Response &res, const crow::Request &req, 108 long &skip) 109 { 110 char *skipParam = req.urlParams.get("$skip"); 111 if (skipParam != nullptr) 112 { 113 char *ptr = nullptr; 114 skip = std::strtol(skipParam, &ptr, 10); 115 if (*skipParam == '\0' || *ptr != '\0') 116 { 117 118 messages::queryParameterValueTypeError(res, std::string(skipParam), 119 "$skip"); 120 return false; 121 } 122 if (skip < 0) 123 { 124 125 messages::queryParameterOutOfRange(res, std::to_string(skip), 126 "$skip", "greater than 0"); 127 return false; 128 } 129 } 130 return true; 131 } 132 133 static constexpr const long maxEntriesPerPage = 1000; 134 static bool getTopParam(crow::Response &res, const crow::Request &req, 135 long &top) 136 { 137 char *topParam = req.urlParams.get("$top"); 138 if (topParam != nullptr) 139 { 140 char *ptr = nullptr; 141 top = std::strtol(topParam, &ptr, 10); 142 if (*topParam == '\0' || *ptr != '\0') 143 { 144 messages::queryParameterValueTypeError(res, std::string(topParam), 145 "$top"); 146 return false; 147 } 148 if (top < 1 || top > maxEntriesPerPage) 149 { 150 151 messages::queryParameterOutOfRange( 152 res, std::to_string(top), "$top", 153 "1-" + std::to_string(maxEntriesPerPage)); 154 return false; 155 } 156 } 157 return true; 158 } 159 160 static bool getUniqueEntryID(sd_journal *journal, std::string &entryID) 161 { 162 int ret = 0; 163 static uint64_t prevTs = 0; 164 static int index = 0; 165 // Get the entry timestamp 166 uint64_t curTs = 0; 167 ret = sd_journal_get_realtime_usec(journal, &curTs); 168 if (ret < 0) 169 { 170 BMCWEB_LOG_ERROR << "Failed to read entry timestamp: " 171 << strerror(-ret); 172 return false; 173 } 174 // If the timestamp isn't unique, increment the index 175 if (curTs == prevTs) 176 { 177 index++; 178 } 179 else 180 { 181 // Otherwise, reset it 182 index = 0; 183 } 184 // Save the timestamp 185 prevTs = curTs; 186 187 entryID = std::to_string(curTs); 188 if (index > 0) 189 { 190 entryID += "_" + std::to_string(index); 191 } 192 return true; 193 } 194 195 static bool getTimestampFromID(crow::Response &res, const std::string &entryID, 196 uint64_t ×tamp, uint16_t &index) 197 { 198 if (entryID.empty()) 199 { 200 return false; 201 } 202 // Convert the unique ID back to a timestamp to find the entry 203 boost::string_view tsStr(entryID); 204 205 auto underscorePos = tsStr.find("_"); 206 if (underscorePos != tsStr.npos) 207 { 208 // Timestamp has an index 209 tsStr.remove_suffix(tsStr.size() - underscorePos); 210 boost::string_view indexStr(entryID); 211 indexStr.remove_prefix(underscorePos + 1); 212 std::size_t pos; 213 try 214 { 215 index = std::stoul(indexStr.to_string(), &pos); 216 } 217 catch (std::invalid_argument) 218 { 219 messages::resourceMissingAtURI(res, entryID); 220 return false; 221 } 222 catch (std::out_of_range) 223 { 224 messages::resourceMissingAtURI(res, entryID); 225 return false; 226 } 227 if (pos != indexStr.size()) 228 { 229 messages::resourceMissingAtURI(res, entryID); 230 return false; 231 } 232 } 233 // Timestamp has no index 234 std::size_t pos; 235 try 236 { 237 timestamp = std::stoull(tsStr.to_string(), &pos); 238 } 239 catch (std::invalid_argument) 240 { 241 messages::resourceMissingAtURI(res, entryID); 242 return false; 243 } 244 catch (std::out_of_range) 245 { 246 messages::resourceMissingAtURI(res, entryID); 247 return false; 248 } 249 if (pos != tsStr.size()) 250 { 251 messages::resourceMissingAtURI(res, entryID); 252 return false; 253 } 254 return true; 255 } 256 257 class LogServiceCollection : public Node 258 { 259 public: 260 template <typename CrowApp> 261 LogServiceCollection(CrowApp &app) : 262 Node(app, "/redfish/v1/Managers/bmc/LogServices/") 263 { 264 // Collections use static ID for SubRoute to add to its parent, but only 265 // load dynamic data so the duplicate static members don't get displayed 266 Node::json["@odata.id"] = "/redfish/v1/Managers/bmc/LogServices"; 267 entityPrivileges = { 268 {boost::beast::http::verb::get, {{"Login"}}}, 269 {boost::beast::http::verb::head, {{"Login"}}}, 270 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 271 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 272 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 273 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 274 } 275 276 private: 277 /** 278 * Functions triggers appropriate requests on DBus 279 */ 280 void doGet(crow::Response &res, const crow::Request &req, 281 const std::vector<std::string> ¶ms) override 282 { 283 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 284 // Collections don't include the static data added by SubRoute because 285 // it has a duplicate entry for members 286 asyncResp->res.jsonValue["@odata.type"] = 287 "#LogServiceCollection.LogServiceCollection"; 288 asyncResp->res.jsonValue["@odata.context"] = 289 "/redfish/v1/" 290 "$metadata#LogServiceCollection.LogServiceCollection"; 291 asyncResp->res.jsonValue["@odata.id"] = 292 "/redfish/v1/Managers/bmc/LogServices"; 293 asyncResp->res.jsonValue["Name"] = "Open BMC Log Services Collection"; 294 asyncResp->res.jsonValue["Description"] = 295 "Collection of LogServices for this Manager"; 296 nlohmann::json &logserviceArray = asyncResp->res.jsonValue["Members"]; 297 logserviceArray = nlohmann::json::array(); 298 logserviceArray.push_back( 299 {{"@odata.id", "/redfish/v1/Managers/bmc/LogServices/BmcLog"}}); 300 #ifdef BMCWEB_ENABLE_REDFISH_CPU_LOG 301 logserviceArray.push_back( 302 {{"@odata.id", "/redfish/v1/Managers/bmc/LogServices/CpuLog"}}); 303 #endif 304 asyncResp->res.jsonValue["Members@odata.count"] = 305 logserviceArray.size(); 306 } 307 }; 308 309 class BMCLogService : public Node 310 { 311 public: 312 template <typename CrowApp> 313 BMCLogService(CrowApp &app) : 314 Node(app, "/redfish/v1/Managers/bmc/LogServices/BmcLog/") 315 { 316 // Set the id for SubRoute 317 Node::json["@odata.id"] = "/redfish/v1/Managers/bmc/LogServices/BmcLog"; 318 entityPrivileges = { 319 {boost::beast::http::verb::get, {{"Login"}}}, 320 {boost::beast::http::verb::head, {{"Login"}}}, 321 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 322 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 323 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 324 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 325 } 326 327 private: 328 void doGet(crow::Response &res, const crow::Request &req, 329 const std::vector<std::string> ¶ms) override 330 { 331 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 332 // Copy over the static data to include the entries added by SubRoute 333 asyncResp->res.jsonValue = Node::json; 334 asyncResp->res.jsonValue["@odata.type"] = 335 "#LogService.v1_1_0.LogService"; 336 asyncResp->res.jsonValue["@odata.context"] = 337 "/redfish/v1/$metadata#LogService.LogService"; 338 asyncResp->res.jsonValue["Name"] = "Open BMC Log Service"; 339 asyncResp->res.jsonValue["Description"] = "BMC Log Service"; 340 asyncResp->res.jsonValue["Id"] = "BMC Log"; 341 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 342 } 343 }; 344 345 static int fillBMCLogEntryJson(const std::string &bmcLogEntryID, 346 sd_journal *journal, 347 nlohmann::json &bmcLogEntryJson) 348 { 349 // Get the Log Entry contents 350 int ret = 0; 351 352 boost::string_view msg; 353 ret = getJournalMetadata(journal, "MESSAGE", msg); 354 if (ret < 0) 355 { 356 BMCWEB_LOG_ERROR << "Failed to read MESSAGE field: " << strerror(-ret); 357 return 1; 358 } 359 360 // Get the severity from the PRIORITY field 361 int severity = 8; // Default to an invalid priority 362 ret = getJournalMetadata(journal, "PRIORITY", 10, severity); 363 if (ret < 0) 364 { 365 BMCWEB_LOG_ERROR << "Failed to read PRIORITY field: " << strerror(-ret); 366 return 1; 367 } 368 369 // Get the Created time from the timestamp 370 std::string entryTimeStr; 371 if (!getEntryTimestamp(journal, entryTimeStr)) 372 { 373 return 1; 374 } 375 376 // Fill in the log entry with the gathered data 377 bmcLogEntryJson = { 378 {"@odata.type", "#LogEntry.v1_3_0.LogEntry"}, 379 {"@odata.context", "/redfish/v1/$metadata#LogEntry.LogEntry"}, 380 {"@odata.id", "/redfish/v1/Managers/bmc/LogServices/BmcLog/Entries/" + 381 bmcLogEntryID}, 382 {"Name", "BMC Journal Entry"}, 383 {"Id", bmcLogEntryID}, 384 {"Message", msg}, 385 {"EntryType", "Oem"}, 386 {"Severity", 387 severity <= 2 ? "Critical" 388 : severity <= 4 ? "Warning" : severity <= 7 ? "OK" : ""}, 389 {"OemRecordFormat", "Intel BMC Journal Entry"}, 390 {"Created", std::move(entryTimeStr)}}; 391 return 0; 392 } 393 394 class BMCLogEntryCollection : public Node 395 { 396 public: 397 template <typename CrowApp> 398 BMCLogEntryCollection(CrowApp &app) : 399 Node(app, "/redfish/v1/Managers/bmc/LogServices/BmcLog/Entries/") 400 { 401 // Collections use static ID for SubRoute to add to its parent, but only 402 // load dynamic data so the duplicate static members don't get displayed 403 Node::json["@odata.id"] = 404 "/redfish/v1/Managers/bmc/LogServices/BmcLog/Entries"; 405 entityPrivileges = { 406 {boost::beast::http::verb::get, {{"Login"}}}, 407 {boost::beast::http::verb::head, {{"Login"}}}, 408 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 409 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 410 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 411 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 412 } 413 414 private: 415 void doGet(crow::Response &res, const crow::Request &req, 416 const std::vector<std::string> ¶ms) override 417 { 418 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 419 static constexpr const long maxEntriesPerPage = 1000; 420 long skip = 0; 421 long top = maxEntriesPerPage; // Show max entries by default 422 if (!getSkipParam(asyncResp->res, req, skip)) 423 { 424 return; 425 } 426 if (!getTopParam(asyncResp->res, req, top)) 427 { 428 return; 429 } 430 // Collections don't include the static data added by SubRoute because 431 // it has a duplicate entry for members 432 asyncResp->res.jsonValue["@odata.type"] = 433 "#LogEntryCollection.LogEntryCollection"; 434 asyncResp->res.jsonValue["@odata.context"] = 435 "/redfish/v1/" 436 "$metadata#LogEntryCollection.LogEntryCollection"; 437 asyncResp->res.jsonValue["@odata.id"] = 438 "/redfish/v1/Managers/bmc/LogServices/BmcLog/Entries"; 439 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries"; 440 asyncResp->res.jsonValue["Description"] = 441 "Collection of BMC Journal Entries"; 442 nlohmann::json &logEntryArray = asyncResp->res.jsonValue["Members"]; 443 logEntryArray = nlohmann::json::array(); 444 445 // Go through the journal and use the timestamp to create a unique ID 446 // for each entry 447 sd_journal *journalTmp = nullptr; 448 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 449 if (ret < 0) 450 { 451 BMCWEB_LOG_ERROR << "failed to open journal: " << strerror(-ret); 452 messages::internalError(asyncResp->res); 453 return; 454 } 455 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 456 journalTmp, sd_journal_close); 457 journalTmp = nullptr; 458 uint64_t entryCount = 0; 459 SD_JOURNAL_FOREACH(journal.get()) 460 { 461 entryCount++; 462 // Handle paging using skip (number of entries to skip from the 463 // start) and top (number of entries to display) 464 if (entryCount <= skip || entryCount > skip + top) 465 { 466 continue; 467 } 468 469 std::string idStr; 470 if (!getUniqueEntryID(journal.get(), idStr)) 471 { 472 continue; 473 } 474 475 logEntryArray.push_back({}); 476 nlohmann::json &bmcLogEntry = logEntryArray.back(); 477 if (fillBMCLogEntryJson(idStr, journal.get(), bmcLogEntry) != 0) 478 { 479 messages::internalError(asyncResp->res); 480 return; 481 } 482 } 483 asyncResp->res.jsonValue["Members@odata.count"] = entryCount; 484 if (skip + top < entryCount) 485 { 486 asyncResp->res.jsonValue["Members@odata.nextLink"] = 487 "/redfish/v1/Managers/bmc/LogServices/BmcLog/Entries?$skip=" + 488 std::to_string(skip + top); 489 } 490 } 491 }; 492 493 class BMCLogEntry : public Node 494 { 495 public: 496 BMCLogEntry(CrowApp &app) : 497 Node(app, "/redfish/v1/Managers/bmc/LogServices/BmcLog/Entries/<str>/", 498 std::string()) 499 { 500 entityPrivileges = { 501 {boost::beast::http::verb::get, {{"Login"}}}, 502 {boost::beast::http::verb::head, {{"Login"}}}, 503 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 504 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 505 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 506 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 507 } 508 509 private: 510 void doGet(crow::Response &res, const crow::Request &req, 511 const std::vector<std::string> ¶ms) override 512 { 513 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 514 if (params.size() != 1) 515 { 516 messages::internalError(asyncResp->res); 517 return; 518 } 519 const std::string &entryID = params[0]; 520 // Convert the unique ID back to a timestamp to find the entry 521 uint64_t ts = 0; 522 uint16_t index = 0; 523 if (!getTimestampFromID(asyncResp->res, entryID, ts, index)) 524 { 525 return; 526 } 527 528 sd_journal *journalTmp = nullptr; 529 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 530 if (ret < 0) 531 { 532 BMCWEB_LOG_ERROR << "failed to open journal: " << strerror(-ret); 533 messages::internalError(asyncResp->res); 534 return; 535 } 536 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 537 journalTmp, sd_journal_close); 538 journalTmp = nullptr; 539 // Go to the timestamp in the log and move to the entry at the index 540 ret = sd_journal_seek_realtime_usec(journal.get(), ts); 541 for (int i = 0; i <= index; i++) 542 { 543 sd_journal_next(journal.get()); 544 } 545 if (fillBMCLogEntryJson(params[0], journal.get(), 546 asyncResp->res.jsonValue) != 0) 547 { 548 messages::internalError(asyncResp->res); 549 return; 550 } 551 } 552 }; 553 554 class CPULogService : public Node 555 { 556 public: 557 template <typename CrowApp> 558 CPULogService(CrowApp &app) : 559 Node(app, "/redfish/v1/Managers/bmc/LogServices/CpuLog/") 560 { 561 // Set the id for SubRoute 562 Node::json["@odata.id"] = "/redfish/v1/Managers/bmc/LogServices/CpuLog"; 563 entityPrivileges = { 564 {boost::beast::http::verb::get, {{"Login"}}}, 565 {boost::beast::http::verb::head, {{"Login"}}}, 566 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 567 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 568 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 569 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 570 } 571 572 private: 573 /** 574 * Functions triggers appropriate requests on DBus 575 */ 576 void doGet(crow::Response &res, const crow::Request &req, 577 const std::vector<std::string> ¶ms) override 578 { 579 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 580 // Copy over the static data to include the entries added by SubRoute 581 asyncResp->res.jsonValue = Node::json; 582 asyncResp->res.jsonValue["@odata.type"] = 583 "#LogService.v1_1_0.LogService"; 584 asyncResp->res.jsonValue["@odata.context"] = 585 "/redfish/v1/" 586 "$metadata#LogService.LogService"; 587 asyncResp->res.jsonValue["Name"] = "Open BMC CPU Log Service"; 588 asyncResp->res.jsonValue["Description"] = "CPU Log Service"; 589 asyncResp->res.jsonValue["Id"] = "CPU Log"; 590 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 591 asyncResp->res.jsonValue["MaxNumberOfRecords"] = 3; 592 asyncResp->res.jsonValue["Actions"] = { 593 {"Oem", 594 {{"#CpuLog.Immediate", 595 {{"target", 596 "/redfish/v1/Managers/bmc/LogServices/CpuLog/Actions/Oem/" 597 "CpuLog.Immediate"}}}}}}; 598 599 #ifdef BMCWEB_ENABLE_REDFISH_RAW_PECI 600 asyncResp->res.jsonValue["Actions"]["Oem"].push_back( 601 {"#CpuLog.SendRawPeci", 602 {{"target", 603 "/redfish/v1/Managers/bmc/LogServices/CpuLog/Actions/Oem/" 604 "CpuLog.SendRawPeci"}}}); 605 #endif 606 } 607 }; 608 609 class CPULogEntryCollection : public Node 610 { 611 public: 612 template <typename CrowApp> 613 CPULogEntryCollection(CrowApp &app) : 614 Node(app, "/redfish/v1/Managers/bmc/LogServices/CpuLog/Entries/") 615 { 616 // Collections use static ID for SubRoute to add to its parent, but only 617 // load dynamic data so the duplicate static members don't get displayed 618 Node::json["@odata.id"] = 619 "/redfish/v1/Managers/bmc/LogServices/CpuLog/Entries"; 620 entityPrivileges = { 621 {boost::beast::http::verb::get, {{"Login"}}}, 622 {boost::beast::http::verb::head, {{"Login"}}}, 623 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 624 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 625 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 626 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 627 } 628 629 private: 630 /** 631 * Functions triggers appropriate requests on DBus 632 */ 633 void doGet(crow::Response &res, const crow::Request &req, 634 const std::vector<std::string> ¶ms) override 635 { 636 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 637 // Collections don't include the static data added by SubRoute because 638 // it has a duplicate entry for members 639 auto getLogEntriesCallback = [asyncResp]( 640 const boost::system::error_code ec, 641 const std::vector<std::string> &resp) { 642 if (ec) 643 { 644 if (ec.value() != 645 boost::system::errc::no_such_file_or_directory) 646 { 647 BMCWEB_LOG_DEBUG << "failed to get entries ec: " 648 << ec.message(); 649 messages::internalError(asyncResp->res); 650 return; 651 } 652 } 653 asyncResp->res.jsonValue["@odata.type"] = 654 "#LogEntryCollection.LogEntryCollection"; 655 asyncResp->res.jsonValue["@odata.context"] = 656 "/redfish/v1/" 657 "$metadata#LogEntryCollection.LogEntryCollection"; 658 asyncResp->res.jsonValue["@odata.id"] = 659 "/redfish/v1/Managers/bmc/LogServices/CpuLog/Entries"; 660 asyncResp->res.jsonValue["Name"] = "Open BMC CPU Log Entries"; 661 asyncResp->res.jsonValue["Description"] = 662 "Collection of CPU Log Entries"; 663 nlohmann::json &logEntryArray = asyncResp->res.jsonValue["Members"]; 664 logEntryArray = nlohmann::json::array(); 665 for (const std::string &objpath : resp) 666 { 667 // Don't list the immediate log 668 if (objpath.compare(cpuLogImmediatePath) == 0) 669 { 670 continue; 671 } 672 std::size_t lastPos = objpath.rfind("/"); 673 if (lastPos != std::string::npos) 674 { 675 logEntryArray.push_back( 676 {{"@odata.id", "/redfish/v1/Managers/bmc/LogServices/" 677 "CpuLog/Entries/" + 678 objpath.substr(lastPos + 1)}}); 679 } 680 } 681 asyncResp->res.jsonValue["Members@odata.count"] = 682 logEntryArray.size(); 683 }; 684 crow::connections::systemBus->async_method_call( 685 std::move(getLogEntriesCallback), 686 "xyz.openbmc_project.ObjectMapper", 687 "/xyz/openbmc_project/object_mapper", 688 "xyz.openbmc_project.ObjectMapper", "GetSubTreePaths", "", 0, 689 std::array<const char *, 1>{cpuLogInterface}); 690 } 691 }; 692 693 std::string getLogCreatedTime(const nlohmann::json &cpuLog) 694 { 695 nlohmann::json::const_iterator metaIt = cpuLog.find("metadata"); 696 if (metaIt != cpuLog.end()) 697 { 698 nlohmann::json::const_iterator tsIt = metaIt->find("timestamp"); 699 if (tsIt != metaIt->end()) 700 { 701 const std::string *logTime = tsIt->get_ptr<const std::string *>(); 702 if (logTime != nullptr) 703 { 704 return *logTime; 705 } 706 } 707 } 708 BMCWEB_LOG_DEBUG << "failed to find log timestamp"; 709 710 return std::string(); 711 } 712 713 class CPULogEntry : public Node 714 { 715 public: 716 CPULogEntry(CrowApp &app) : 717 Node(app, "/redfish/v1/Managers/bmc/LogServices/CpuLog/Entries/<str>/", 718 std::string()) 719 { 720 entityPrivileges = { 721 {boost::beast::http::verb::get, {{"Login"}}}, 722 {boost::beast::http::verb::head, {{"Login"}}}, 723 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 724 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 725 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 726 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 727 } 728 729 private: 730 void doGet(crow::Response &res, const crow::Request &req, 731 const std::vector<std::string> ¶ms) override 732 { 733 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 734 if (params.size() != 1) 735 { 736 messages::internalError(asyncResp->res); 737 return; 738 } 739 const uint8_t logId = std::atoi(params[0].c_str()); 740 auto getStoredLogCallback = 741 [asyncResp, 742 logId](const boost::system::error_code ec, 743 const sdbusplus::message::variant<std::string> &resp) { 744 if (ec) 745 { 746 BMCWEB_LOG_DEBUG << "failed to get log ec: " 747 << ec.message(); 748 messages::internalError(asyncResp->res); 749 return; 750 } 751 const std::string *log = 752 mapbox::getPtr<const std::string>(resp); 753 if (log == nullptr) 754 { 755 messages::internalError(asyncResp->res); 756 return; 757 } 758 nlohmann::json j = nlohmann::json::parse(*log, nullptr, false); 759 if (j.is_discarded()) 760 { 761 messages::internalError(asyncResp->res); 762 return; 763 } 764 std::string t = getLogCreatedTime(j); 765 asyncResp->res.jsonValue = { 766 {"@odata.type", "#LogEntry.v1_3_0.LogEntry"}, 767 {"@odata.context", 768 "/redfish/v1/$metadata#LogEntry.LogEntry"}, 769 {"@odata.id", 770 "/redfish/v1/Managers/bmc/LogServices/CpuLog/Entries/" + 771 std::to_string(logId)}, 772 {"Name", "CPU Debug Log"}, 773 {"Id", logId}, 774 {"EntryType", "Oem"}, 775 {"OemRecordFormat", "Intel CPU Log"}, 776 {"Oem", {{"Intel", std::move(j)}}}, 777 {"Created", std::move(t)}}; 778 }; 779 crow::connections::systemBus->async_method_call( 780 std::move(getStoredLogCallback), cpuLogObject, 781 cpuLogPath + std::string("/") + std::to_string(logId), 782 "org.freedesktop.DBus.Properties", "Get", cpuLogInterface, "Log"); 783 } 784 }; 785 786 class ImmediateCPULog : public Node 787 { 788 public: 789 ImmediateCPULog(CrowApp &app) : 790 Node(app, "/redfish/v1/Managers/bmc/LogServices/CpuLog/Actions/Oem/" 791 "CpuLog.Immediate/") 792 { 793 entityPrivileges = { 794 {boost::beast::http::verb::get, {{"Login"}}}, 795 {boost::beast::http::verb::head, {{"Login"}}}, 796 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 797 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 798 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 799 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 800 } 801 802 private: 803 void doPost(crow::Response &res, const crow::Request &req, 804 const std::vector<std::string> ¶ms) override 805 { 806 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 807 static std::unique_ptr<sdbusplus::bus::match::match> 808 immediateLogMatcher; 809 810 // Only allow one Immediate Log request at a time 811 if (immediateLogMatcher != nullptr) 812 { 813 asyncResp->res.addHeader("Retry-After", "30"); 814 messages::serviceTemporarilyUnavailable(asyncResp->res, "30"); 815 return; 816 } 817 // Make this static so it survives outside this method 818 static boost::asio::deadline_timer timeout(*req.ioService); 819 820 timeout.expires_from_now(boost::posix_time::seconds(30)); 821 timeout.async_wait([asyncResp](const boost::system::error_code &ec) { 822 immediateLogMatcher = nullptr; 823 if (ec) 824 { 825 // operation_aborted is expected if timer is canceled before 826 // completion. 827 if (ec != boost::asio::error::operation_aborted) 828 { 829 BMCWEB_LOG_ERROR << "Async_wait failed " << ec; 830 } 831 return; 832 } 833 BMCWEB_LOG_ERROR << "Timed out waiting for immediate log"; 834 835 messages::internalError(asyncResp->res); 836 }); 837 838 auto immediateLogMatcherCallback = [asyncResp]( 839 sdbusplus::message::message &m) { 840 BMCWEB_LOG_DEBUG << "Immediate log available match fired"; 841 boost::system::error_code ec; 842 timeout.cancel(ec); 843 if (ec) 844 { 845 BMCWEB_LOG_ERROR << "error canceling timer " << ec; 846 } 847 sdbusplus::message::object_path objPath; 848 boost::container::flat_map< 849 std::string, 850 boost::container::flat_map< 851 std::string, sdbusplus::message::variant<std::string>>> 852 interfacesAdded; 853 m.read(objPath, interfacesAdded); 854 const std::string *log = mapbox::getPtr<const std::string>( 855 interfacesAdded[cpuLogInterface]["Log"]); 856 if (log == nullptr) 857 { 858 messages::internalError(asyncResp->res); 859 // Careful with immediateLogMatcher. It is a unique_ptr to the 860 // match object inside which this lambda is executing. Once it 861 // is set to nullptr, the match object will be destroyed and the 862 // lambda will lose its context, including res, so it needs to 863 // be the last thing done. 864 immediateLogMatcher = nullptr; 865 return; 866 } 867 nlohmann::json j = nlohmann::json::parse(*log, nullptr, false); 868 if (j.is_discarded()) 869 { 870 messages::internalError(asyncResp->res); 871 // Careful with immediateLogMatcher. It is a unique_ptr to the 872 // match object inside which this lambda is executing. Once it 873 // is set to nullptr, the match object will be destroyed and the 874 // lambda will lose its context, including res, so it needs to 875 // be the last thing done. 876 immediateLogMatcher = nullptr; 877 return; 878 } 879 std::string t = getLogCreatedTime(j); 880 asyncResp->res.jsonValue = { 881 {"@odata.type", "#LogEntry.v1_3_0.LogEntry"}, 882 {"@odata.context", "/redfish/v1/$metadata#LogEntry.LogEntry"}, 883 {"Name", "CPU Debug Log"}, 884 {"EntryType", "Oem"}, 885 {"OemRecordFormat", "Intel CPU Log"}, 886 {"Oem", {{"Intel", std::move(j)}}}, 887 {"Created", std::move(t)}}; 888 // Careful with immediateLogMatcher. It is a unique_ptr to the 889 // match object inside which this lambda is executing. Once it is 890 // set to nullptr, the match object will be destroyed and the lambda 891 // will lose its context, including res, so it needs to be the last 892 // thing done. 893 immediateLogMatcher = nullptr; 894 }; 895 immediateLogMatcher = std::make_unique<sdbusplus::bus::match::match>( 896 *crow::connections::systemBus, 897 sdbusplus::bus::match::rules::interfacesAdded() + 898 sdbusplus::bus::match::rules::argNpath(0, cpuLogImmediatePath), 899 std::move(immediateLogMatcherCallback)); 900 901 auto generateImmediateLogCallback = 902 [asyncResp](const boost::system::error_code ec, 903 const std::string &resp) { 904 if (ec) 905 { 906 if (ec.value() == 907 boost::system::errc::operation_not_supported) 908 { 909 messages::resourceInStandby(asyncResp->res); 910 } 911 else 912 { 913 messages::internalError(asyncResp->res); 914 } 915 boost::system::error_code timeoutec; 916 timeout.cancel(timeoutec); 917 if (timeoutec) 918 { 919 BMCWEB_LOG_ERROR << "error canceling timer " 920 << timeoutec; 921 } 922 immediateLogMatcher = nullptr; 923 return; 924 } 925 }; 926 crow::connections::systemBus->async_method_call( 927 std::move(generateImmediateLogCallback), cpuLogObject, cpuLogPath, 928 cpuLogImmediateInterface, "GenerateImmediateLog"); 929 } 930 }; 931 932 class SendRawPECI : public Node 933 { 934 public: 935 SendRawPECI(CrowApp &app) : 936 Node(app, "/redfish/v1/Managers/bmc/LogServices/CpuLog/Actions/Oem/" 937 "CpuLog.SendRawPeci/") 938 { 939 entityPrivileges = { 940 {boost::beast::http::verb::get, {{"ConfigureComponents"}}}, 941 {boost::beast::http::verb::head, {{"ConfigureComponents"}}}, 942 {boost::beast::http::verb::patch, {{"ConfigureComponents"}}}, 943 {boost::beast::http::verb::put, {{"ConfigureComponents"}}}, 944 {boost::beast::http::verb::delete_, {{"ConfigureComponents"}}}, 945 {boost::beast::http::verb::post, {{"ConfigureComponents"}}}}; 946 } 947 948 private: 949 void doPost(crow::Response &res, const crow::Request &req, 950 const std::vector<std::string> ¶ms) override 951 { 952 std::shared_ptr<AsyncResp> asyncResp = std::make_shared<AsyncResp>(res); 953 // Get the Raw PECI command from the request 954 nlohmann::json rawPECICmd; 955 if (!json_util::processJsonFromRequest(res, req, rawPECICmd)) 956 { 957 return; 958 } 959 // Get the Client Address from the request 960 nlohmann::json::const_iterator caIt = rawPECICmd.find("ClientAddress"); 961 if (caIt == rawPECICmd.end()) 962 { 963 messages::propertyMissing(asyncResp->res, "ClientAddress"); 964 return; 965 } 966 const uint64_t *ca = caIt->get_ptr<const uint64_t *>(); 967 if (ca == nullptr) 968 { 969 messages::propertyValueTypeError(asyncResp->res, caIt->dump(), 970 "ClientAddress"); 971 return; 972 } 973 // Get the Read Length from the request 974 const uint8_t clientAddress = static_cast<uint8_t>(*ca); 975 nlohmann::json::const_iterator rlIt = rawPECICmd.find("ReadLength"); 976 if (rlIt == rawPECICmd.end()) 977 { 978 messages::propertyMissing(asyncResp->res, "ReadLength"); 979 return; 980 } 981 const uint64_t *rl = rlIt->get_ptr<const uint64_t *>(); 982 if (rl == nullptr) 983 { 984 messages::propertyValueTypeError(asyncResp->res, rlIt->dump(), 985 "ReadLength"); 986 return; 987 } 988 // Get the PECI Command from the request 989 const uint32_t readLength = static_cast<uint32_t>(*rl); 990 nlohmann::json::const_iterator pcIt = rawPECICmd.find("PECICommand"); 991 if (pcIt == rawPECICmd.end()) 992 { 993 messages::propertyMissing(asyncResp->res, "PECICommand"); 994 return; 995 } 996 std::vector<uint8_t> peciCommand; 997 for (auto pc : *pcIt) 998 { 999 const uint64_t *val = pc.get_ptr<const uint64_t *>(); 1000 if (val == nullptr) 1001 { 1002 messages::propertyValueTypeError( 1003 asyncResp->res, pc.dump(), 1004 "PECICommand/" + std::to_string(peciCommand.size())); 1005 return; 1006 } 1007 peciCommand.push_back(static_cast<uint8_t>(*val)); 1008 } 1009 // Callback to return the Raw PECI response 1010 auto sendRawPECICallback = 1011 [asyncResp](const boost::system::error_code ec, 1012 const std::vector<uint8_t> &resp) { 1013 if (ec) 1014 { 1015 BMCWEB_LOG_DEBUG << "failed to send PECI command ec: " 1016 << ec.message(); 1017 messages::internalError(asyncResp->res); 1018 return; 1019 } 1020 asyncResp->res.jsonValue = {{"Name", "PECI Command Response"}, 1021 {"PECIResponse", resp}}; 1022 }; 1023 // Call the SendRawPECI command with the provided data 1024 crow::connections::systemBus->async_method_call( 1025 std::move(sendRawPECICallback), cpuLogObject, cpuLogPath, 1026 cpuLogRawPECIInterface, "SendRawPeci", clientAddress, readLength, 1027 peciCommand); 1028 } 1029 }; 1030 1031 } // namespace redfish 1032