xref: /openbmc/bmcweb/features/redfish/lib/manager_logservices_journal.hpp (revision 08fad5d9dc59323a8916ff97a035221621047d8c)
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