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