1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include "bmcweb_config.h" 6 7 #include "app.hpp" 8 #include "async_resp.hpp" 9 #include "dbus_singleton.hpp" 10 #include "error_messages.hpp" 11 #include "http_request.hpp" 12 #include "logging.hpp" 13 #include "query.hpp" 14 #include "registries/privilege_registry.hpp" 15 #include "utility.hpp" 16 #include "utils/etag_utils.hpp" 17 #include "utils/journal_utils.hpp" 18 #include "utils/query_param.hpp" 19 #include "utils/time_utils.hpp" 20 21 #include <systemd/sd-journal.h> 22 23 #include <boost/asio/post.hpp> 24 #include <boost/beast/http/verb.hpp> 25 #include <boost/url/format.hpp> 26 27 #include <cstddef> 28 #include <cstdint> 29 #include <functional> 30 #include <memory> 31 #include <string> 32 #include <string_view> 33 #include <utility> 34 35 namespace redfish 36 { 37 38 inline void handleManagersLogServiceJournalGet( 39 App& app, const crow::Request& req, 40 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 41 const std::string& managerId) 42 { 43 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 44 { 45 return; 46 } 47 48 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 49 { 50 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 51 return; 52 } 53 54 asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService"; 55 asyncResp->res.jsonValue["@odata.id"] = 56 boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal", 57 BMCWEB_REDFISH_MANAGER_URI_NAME); 58 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service"; 59 asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service"; 60 asyncResp->res.jsonValue["Id"] = "Journal"; 61 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 62 63 std::pair<std::string, std::string> redfishDateTimeOffset = 64 redfish::time_utils::getDateTimeOffsetNow(); 65 asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first; 66 asyncResp->res.jsonValue["DateTimeLocalOffset"] = 67 redfishDateTimeOffset.second; 68 69 asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format( 70 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 71 BMCWEB_REDFISH_MANAGER_URI_NAME); 72 73 etag_utils::setEtagOmitDateTimeHandler(asyncResp); 74 } 75 76 struct JournalReadState 77 { 78 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal; 79 }; 80 81 inline void readJournalEntries( 82 uint64_t topEntryCount, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 83 JournalReadState&& readState) 84 { 85 nlohmann::json& logEntry = asyncResp->res.jsonValue["Members"]; 86 nlohmann::json::array_t* logEntryArray = 87 logEntry.get_ptr<nlohmann::json::array_t*>(); 88 if (logEntryArray == nullptr) 89 { 90 messages::internalError(asyncResp->res); 91 return; 92 } 93 94 // The Journal APIs unfortunately do blocking calls to the filesystem, and 95 // can be somewhat expensive. Short of creating our own io_uring based 96 // implementation of sd-journal, which would be difficult, the best thing we 97 // can do is to only parse a certain number of entries at a time. The 98 // current chunk size is selected arbitrarily to ensure that we're not 99 // trying to process thousands of entries at the same time. 100 // The implementation will process the number of entries, then return 101 // control to the io_context to let other operations continue. 102 size_t segmentCountRemaining = 10; 103 104 // Reset the unique ID on the first entry 105 for (uint64_t entryCount = logEntryArray->size(); 106 entryCount < topEntryCount; entryCount++) 107 { 108 if (segmentCountRemaining == 0) 109 { 110 boost::asio::post(crow::connections::systemBus->get_io_context(), 111 [asyncResp, topEntryCount, 112 readState = std::move(readState)]() mutable { 113 readJournalEntries(topEntryCount, asyncResp, 114 std::move(readState)); 115 }); 116 return; 117 } 118 119 nlohmann::json::object_t bmcJournalLogEntry; 120 if (!fillBMCJournalLogEntryJson(readState.journal.get(), 121 bmcJournalLogEntry)) 122 { 123 messages::internalError(asyncResp->res); 124 return; 125 } 126 logEntryArray->emplace_back(std::move(bmcJournalLogEntry)); 127 128 int ret = sd_journal_next(readState.journal.get()); 129 if (ret < 0) 130 { 131 messages::internalError(asyncResp->res); 132 return; 133 } 134 if (ret == 0) 135 { 136 break; 137 } 138 segmentCountRemaining--; 139 } 140 } 141 142 inline void handleManagersJournalLogEntryCollectionGet( 143 App& app, const crow::Request& req, 144 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 145 const std::string& managerId) 146 { 147 query_param::QueryCapabilities capabilities = { 148 .canDelegateTop = true, 149 .canDelegateSkip = true, 150 }; 151 query_param::Query delegatedQuery; 152 if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp, 153 delegatedQuery, capabilities)) 154 { 155 return; 156 } 157 158 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 159 { 160 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 161 return; 162 } 163 164 size_t skip = delegatedQuery.skip.value_or(0); 165 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 166 167 // Collections don't include the static data added by SubRoute 168 // because it has a duplicate entry for members 169 asyncResp->res.jsonValue["@odata.type"] = 170 "#LogEntryCollection.LogEntryCollection"; 171 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( 172 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 173 BMCWEB_REDFISH_MANAGER_URI_NAME); 174 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries"; 175 asyncResp->res.jsonValue["Description"] = 176 "Collection of BMC Journal Entries"; 177 asyncResp->res.jsonValue["Members"] = nlohmann::json::array_t(); 178 179 // Go through the journal and use the timestamp to create a 180 // unique ID for each entry 181 sd_journal* journalTmp = nullptr; 182 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 183 if (ret < 0) 184 { 185 BMCWEB_LOG_ERROR("failed to open journal: {}", ret); 186 messages::internalError(asyncResp->res); 187 return; 188 } 189 190 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 191 journalTmp, sd_journal_close); 192 journalTmp = nullptr; 193 194 // Seek to the end 195 if (sd_journal_seek_tail(journal.get()) < 0) 196 { 197 messages::internalError(asyncResp->res); 198 return; 199 } 200 201 // Get the last entry 202 if (sd_journal_previous(journal.get()) < 0) 203 { 204 messages::internalError(asyncResp->res); 205 return; 206 } 207 208 // Get the last sequence number 209 uint64_t endSeqNum = 0; 210 #if LIBSYSTEMD_VERSION >= 254 211 { 212 if (sd_journal_get_seqnum(journal.get(), &endSeqNum, nullptr) < 0) 213 { 214 messages::internalError(asyncResp->res); 215 return; 216 } 217 } 218 #endif 219 220 // Seek to the beginning 221 if (sd_journal_seek_head(journal.get()) < 0) 222 { 223 messages::internalError(asyncResp->res); 224 return; 225 } 226 227 // Get the first entry 228 if (sd_journal_next(journal.get()) < 0) 229 { 230 messages::internalError(asyncResp->res); 231 return; 232 } 233 234 // Get the first sequence number 235 uint64_t startSeqNum = 0; 236 #if LIBSYSTEMD_VERSION >= 254 237 { 238 if (sd_journal_get_seqnum(journal.get(), &startSeqNum, nullptr) < 0) 239 { 240 messages::internalError(asyncResp->res); 241 return; 242 } 243 } 244 #endif 245 246 BMCWEB_LOG_DEBUG("journal Sequence IDs start:{} end:{}", startSeqNum, 247 endSeqNum); 248 249 // Add 1 to account for the last entry 250 uint64_t totalEntries = endSeqNum - startSeqNum + 1; 251 asyncResp->res.jsonValue["Members@odata.count"] = totalEntries; 252 if (skip + top < totalEntries) 253 { 254 asyncResp->res.jsonValue["Members@odata.nextLink"] = 255 boost::urls::format( 256 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}", 257 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top)); 258 } 259 uint64_t index = 0; 260 if (skip > 0) 261 { 262 if (sd_journal_next_skip(journal.get(), skip) < 0) 263 { 264 messages::internalError(asyncResp->res); 265 return; 266 } 267 } 268 BMCWEB_LOG_DEBUG("Index was {}", index); 269 readJournalEntries(top, asyncResp, {std::move(journal)}); 270 } 271 272 inline void handleManagersJournalEntriesLogEntryGet( 273 App& app, const crow::Request& req, 274 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 275 const std::string& managerId, const std::string& entryID) 276 { 277 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 278 { 279 return; 280 } 281 282 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 283 { 284 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 285 return; 286 } 287 288 sd_journal* journalTmp = nullptr; 289 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 290 if (ret < 0) 291 { 292 BMCWEB_LOG_ERROR("failed to open journal: {}", ret); 293 messages::internalError(asyncResp->res); 294 return; 295 } 296 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 297 journalTmp, sd_journal_close); 298 journalTmp = nullptr; 299 300 std::string cursor; 301 if (!crow::utility::base64Decode(entryID, cursor)) 302 { 303 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 304 return; 305 } 306 307 // Go to the cursor in the log 308 ret = sd_journal_seek_cursor(journal.get(), cursor.c_str()); 309 if (ret < 0) 310 { 311 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 312 return; 313 } 314 315 if (sd_journal_next(journal.get()) < 0) 316 { 317 messages::internalError(asyncResp->res); 318 return; 319 } 320 321 ret = sd_journal_test_cursor(journal.get(), cursor.c_str()); 322 if (ret == 0) 323 { 324 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 325 return; 326 } 327 if (ret < 0) 328 { 329 messages::internalError(asyncResp->res); 330 return; 331 } 332 333 nlohmann::json::object_t bmcJournalLogEntry; 334 if (!fillBMCJournalLogEntryJson(journal.get(), bmcJournalLogEntry)) 335 { 336 messages::internalError(asyncResp->res); 337 return; 338 } 339 asyncResp->res.jsonValue.update(bmcJournalLogEntry); 340 } 341 342 inline void requestRoutesBMCJournalLogService(App& app) 343 { 344 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/") 345 .privileges(redfish::privileges::getLogService) 346 .methods(boost::beast::http::verb::get)( 347 std::bind_front(handleManagersLogServiceJournalGet, std::ref(app))); 348 349 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/") 350 .privileges(redfish::privileges::getLogEntryCollection) 351 .methods(boost::beast::http::verb::get)(std::bind_front( 352 handleManagersJournalLogEntryCollectionGet, std::ref(app))); 353 354 BMCWEB_ROUTE( 355 app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/") 356 .privileges(redfish::privileges::getLogEntry) 357 .methods(boost::beast::http::verb::get)(std::bind_front( 358 handleManagersJournalEntriesLogEntryGet, std::ref(app))); 359 } 360 } // namespace redfish 361