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