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
handleManagersLogServiceJournalGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & managerId)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
readJournalEntries(uint64_t topEntryCount,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,JournalReadState && readState)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
handleManagersJournalLogEntryCollectionGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & managerId)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
handleManagersJournalEntriesLogEntryGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & managerId,const std::string & entryID)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
requestRoutesBMCJournalLogService(App & app)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