1 #pragma once
2 
3 #include "app.hpp"
4 #include "error_messages.hpp"
5 #include "generated/enums/log_entry.hpp"
6 #include "query.hpp"
7 #include "registries/base_message_registry.hpp"
8 #include "registries/privilege_registry.hpp"
9 #include "utils/time_utils.hpp"
10 
11 #include <systemd/sd-journal.h>
12 
13 #include <boost/beast/http/verb.hpp>
14 
15 #include <array>
16 #include <memory>
17 #include <string>
18 #include <string_view>
19 
20 namespace redfish
21 {
22 // Entry is formed like "BootID_timestamp" or "BootID_timestamp_index"
23 inline bool
24     getTimestampFromID(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
25                        std::string_view entryIDStrView, sd_id128_t& bootID,
26                        uint64_t& timestamp, uint64_t& index)
27 {
28     // Convert the unique ID back to a bootID + timestamp to find the entry
29     auto underscore1Pos = entryIDStrView.find('_');
30     if (underscore1Pos == std::string_view::npos)
31     {
32         // EntryID has no bootID or timestamp
33         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
34         return false;
35     }
36 
37     // EntryID has bootID + timestamp
38 
39     // Convert entryIDViewString to BootID
40     // NOTE: bootID string which needs to be null-terminated for
41     // sd_id128_from_string()
42     std::string bootIDStr(entryIDStrView.substr(0, underscore1Pos));
43     if (sd_id128_from_string(bootIDStr.c_str(), &bootID) < 0)
44     {
45         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
46         return false;
47     }
48 
49     // Get the timestamp from entryID
50     entryIDStrView.remove_prefix(underscore1Pos + 1);
51 
52     auto [timestampEnd, tstampEc] = std::from_chars(
53         entryIDStrView.begin(), entryIDStrView.end(), timestamp);
54     if (tstampEc != std::errc())
55     {
56         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
57         return false;
58     }
59     entryIDStrView = std::string_view(
60         timestampEnd,
61         static_cast<size_t>(std::distance(timestampEnd, entryIDStrView.end())));
62     if (entryIDStrView.empty())
63     {
64         index = 0U;
65         return true;
66     }
67     // Timestamp might include optional index, if two events happened at the
68     // same "time".
69     if (entryIDStrView[0] != '_')
70     {
71         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
72         return false;
73     }
74     entryIDStrView.remove_prefix(1);
75     auto [ptr, indexEc] = std::from_chars(entryIDStrView.begin(),
76                                           entryIDStrView.end(), index);
77     if (indexEc != std::errc() || ptr != entryIDStrView.end())
78     {
79         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
80         return false;
81     }
82     if (index <= 1)
83     {
84         // Indexes go directly from no postfix (handled above) to _2
85         // so if we ever see _0 or _1, it's incorrect
86         messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
87         return false;
88     }
89 
90     // URI indexes are one based, journald is zero based
91     index -= 1;
92     return true;
93 }
94 
95 inline std::string getUniqueEntryID(uint64_t index, uint64_t curTs,
96                                     sd_id128_t& curBootID)
97 {
98     // make entryID as <bootID>_<timestamp>[_<index>]
99     std::array<char, SD_ID128_STRING_MAX> bootIDStr{};
100     sd_id128_to_string(curBootID, bootIDStr.data());
101     std::string postfix;
102     if (index > 0)
103     {
104         postfix = std::format("_{}", index + 1);
105     }
106     return std::format("{}_{}{}", bootIDStr.data(), curTs, postfix);
107 }
108 
109 inline int getJournalMetadata(sd_journal* journal, std::string_view field,
110                               std::string_view& contents)
111 {
112     const char* data = nullptr;
113     size_t length = 0;
114     int ret = 0;
115     // Get the metadata from the requested field of the journal entry
116     // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
117     const void** dataVoid = reinterpret_cast<const void**>(&data);
118 
119     ret = sd_journal_get_data(journal, field.data(), dataVoid, &length);
120     if (ret < 0)
121     {
122         return ret;
123     }
124     contents = std::string_view(data, length);
125     // Only use the content after the "=" character.
126     contents.remove_prefix(std::min(contents.find('=') + 1, contents.size()));
127     return ret;
128 }
129 
130 inline int getJournalMetadataInt(sd_journal* journal, std::string_view field,
131                                  const int& base, long int& contents)
132 {
133     int ret = 0;
134     std::string_view metadata;
135     // Get the metadata from the requested field of the journal entry
136     ret = getJournalMetadata(journal, field, metadata);
137     if (ret < 0)
138     {
139         return ret;
140     }
141     contents = strtol(metadata.data(), nullptr, base);
142     return ret;
143 }
144 
145 inline bool getEntryTimestamp(sd_journal* journal, std::string& entryTimestamp)
146 {
147     int ret = 0;
148     uint64_t timestamp = 0;
149     ret = sd_journal_get_realtime_usec(journal, &timestamp);
150     if (ret < 0)
151     {
152         BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret));
153         return false;
154     }
155     entryTimestamp = redfish::time_utils::getDateTimeUintUs(timestamp);
156     return true;
157 }
158 
159 inline bool
160     fillBMCJournalLogEntryJson(const std::string& bmcJournalLogEntryID,
161                                sd_journal* journal,
162                                nlohmann::json::object_t& bmcJournalLogEntryJson)
163 {
164     // Get the Log Entry contents
165     std::string message;
166     std::string_view syslogID;
167     int ret = getJournalMetadata(journal, "SYSLOG_IDENTIFIER", syslogID);
168     if (ret < 0)
169     {
170         BMCWEB_LOG_DEBUG("Failed to read SYSLOG_IDENTIFIER field: {}",
171                          strerror(-ret));
172     }
173     if (!syslogID.empty())
174     {
175         message += std::string(syslogID) + ": ";
176     }
177 
178     std::string_view msg;
179     ret = getJournalMetadata(journal, "MESSAGE", msg);
180     if (ret < 0)
181     {
182         BMCWEB_LOG_ERROR("Failed to read MESSAGE field: {}", strerror(-ret));
183         return false;
184     }
185     message += std::string(msg);
186 
187     // Get the severity from the PRIORITY field
188     long int severity = 8; // Default to an invalid priority
189     ret = getJournalMetadataInt(journal, "PRIORITY", 10, severity);
190     if (ret < 0)
191     {
192         BMCWEB_LOG_DEBUG("Failed to read PRIORITY field: {}", strerror(-ret));
193     }
194 
195     // Get the Created time from the timestamp
196     std::string entryTimeStr;
197     if (!getEntryTimestamp(journal, entryTimeStr))
198     {
199         return false;
200     }
201 
202     // Fill in the log entry with the gathered data
203     bmcJournalLogEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
204     bmcJournalLogEntryJson["@odata.id"] = boost::urls::format(
205         "/redfish/v1/Managers/{}/LogServices/Journal/Entries/{}",
206         BMCWEB_REDFISH_MANAGER_URI_NAME, bmcJournalLogEntryID);
207     bmcJournalLogEntryJson["Name"] = "BMC Journal Entry";
208     bmcJournalLogEntryJson["Id"] = bmcJournalLogEntryID;
209     bmcJournalLogEntryJson["Message"] = std::move(message);
210     bmcJournalLogEntryJson["EntryType"] = "Oem";
211     log_entry::EventSeverity severityEnum = log_entry::EventSeverity::OK;
212     if (severity <= 2)
213     {
214         severityEnum = log_entry::EventSeverity::Critical;
215     }
216     else if (severity <= 4)
217     {
218         severityEnum = log_entry::EventSeverity::Warning;
219     }
220 
221     bmcJournalLogEntryJson["Severity"] = severityEnum;
222     bmcJournalLogEntryJson["OemRecordFormat"] = "BMC Journal Entry";
223     bmcJournalLogEntryJson["Created"] = std::move(entryTimeStr);
224     return true;
225 }
226 
227 inline void handleManagersLogServiceJournalGet(
228     App& app, const crow::Request& req,
229     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
230     const std::string& managerId)
231 {
232     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
233     {
234         return;
235     }
236 
237     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
238     {
239         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
240         return;
241     }
242 
243     asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService";
244     asyncResp->res.jsonValue["@odata.id"] =
245         boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal",
246                             BMCWEB_REDFISH_MANAGER_URI_NAME);
247     asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service";
248     asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service";
249     asyncResp->res.jsonValue["Id"] = "Journal";
250     asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull";
251 
252     std::pair<std::string, std::string> redfishDateTimeOffset =
253         redfish::time_utils::getDateTimeOffsetNow();
254     asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first;
255     asyncResp->res.jsonValue["DateTimeLocalOffset"] =
256         redfishDateTimeOffset.second;
257 
258     asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format(
259         "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
260         BMCWEB_REDFISH_MANAGER_URI_NAME);
261 }
262 
263 struct JournalReadState
264 {
265     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal;
266     uint64_t index = 0;
267     sd_id128_t prevBootID{};
268     uint64_t prevTs = 0;
269 };
270 
271 inline void
272     readJournalEntries(uint64_t topEntryCount,
273                        const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
274                        JournalReadState&& readState)
275 {
276     nlohmann::json& logEntry = asyncResp->res.jsonValue["Members"];
277     nlohmann::json::array_t* logEntryArray =
278         logEntry.get_ptr<nlohmann::json::array_t*>();
279     if (logEntryArray == nullptr)
280     {
281         messages::internalError(asyncResp->res);
282         return;
283     }
284 
285     // The Journal APIs unfortunately do blocking calls to the filesystem, and
286     // can be somewhat expensive.  Short of creating our own io_uring based
287     // implementation of sd-journal, which would be difficult, the best thing we
288     // can do is to only parse a certain number of entries at a time.  The
289     // current chunk size is selected arbitrarily to ensure that we're not
290     // trying to process thousands of entries at the same time.
291     // The implementation will process the number of entries, then return
292     // control to the io_context to let other operations continue.
293     size_t segmentCountRemaining = 10;
294 
295     // Reset the unique ID on the first entry
296     for (uint64_t entryCount = logEntryArray->size();
297          entryCount < topEntryCount; entryCount++)
298     {
299         if (segmentCountRemaining == 0)
300         {
301             boost::asio::post(crow::connections::systemBus->get_io_context(),
302                               [asyncResp, topEntryCount,
303                                readState = std::move(readState)]() mutable {
304                 readJournalEntries(topEntryCount, asyncResp,
305                                    std::move(readState));
306             });
307             return;
308         }
309 
310         // Get the entry timestamp
311         sd_id128_t curBootID{};
312         uint64_t curTs = 0;
313         int ret = sd_journal_get_monotonic_usec(readState.journal.get(), &curTs,
314                                                 &curBootID);
315         if (ret < 0)
316         {
317             BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}",
318                              strerror(-ret));
319             messages::internalError(asyncResp->res);
320             return;
321         }
322 
323         // If the timestamp isn't unique on the same boot, increment the index
324         bool sameBootIDs = sd_id128_equal(curBootID, readState.prevBootID) != 0;
325         if (sameBootIDs && (curTs == readState.prevTs))
326         {
327             readState.index++;
328         }
329         else
330         {
331             // Otherwise, reset it
332             readState.index = 0;
333         }
334 
335         // Save the bootID
336         readState.prevBootID = curBootID;
337 
338         // Save the timestamp
339         readState.prevTs = curTs;
340 
341         std::string idStr = getUniqueEntryID(readState.index, curTs, curBootID);
342 
343         nlohmann::json::object_t bmcJournalLogEntry;
344         if (!fillBMCJournalLogEntryJson(idStr, readState.journal.get(),
345                                         bmcJournalLogEntry))
346         {
347             messages::internalError(asyncResp->res);
348             return;
349         }
350         logEntryArray->emplace_back(std::move(bmcJournalLogEntry));
351 
352         ret = sd_journal_next(readState.journal.get());
353         if (ret < 0)
354         {
355             messages::internalError(asyncResp->res);
356             return;
357         }
358         if (ret == 0)
359         {
360             break;
361         }
362         segmentCountRemaining--;
363     }
364 }
365 
366 inline void handleManagersJournalLogEntryCollectionGet(
367     App& app, const crow::Request& req,
368     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
369     const std::string& managerId)
370 {
371     query_param::QueryCapabilities capabilities = {
372         .canDelegateTop = true,
373         .canDelegateSkip = true,
374     };
375     query_param::Query delegatedQuery;
376     if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp,
377                                                   delegatedQuery, capabilities))
378     {
379         return;
380     }
381 
382     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
383     {
384         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
385         return;
386     }
387 
388     size_t skip = delegatedQuery.skip.value_or(0);
389     size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
390 
391     // Collections don't include the static data added by SubRoute
392     // because it has a duplicate entry for members
393     asyncResp->res.jsonValue["@odata.type"] =
394         "#LogEntryCollection.LogEntryCollection";
395     asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
396         "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
397         BMCWEB_REDFISH_MANAGER_URI_NAME);
398     asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries";
399     asyncResp->res.jsonValue["Description"] =
400         "Collection of BMC Journal Entries";
401     asyncResp->res.jsonValue["Members"] = nlohmann::json::array_t();
402 
403     // Go through the journal and use the timestamp to create a
404     // unique ID for each entry
405     sd_journal* journalTmp = nullptr;
406     int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
407     if (ret < 0)
408     {
409         BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret));
410         messages::internalError(asyncResp->res);
411         return;
412     }
413 
414     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
415         journalTmp, sd_journal_close);
416     journalTmp = nullptr;
417 
418     // Seek to the end
419     if (sd_journal_seek_tail(journal.get()) < 0)
420     {
421         messages::internalError(asyncResp->res);
422         return;
423     }
424 
425     // Get the last entry
426     if (sd_journal_previous(journal.get()) < 0)
427     {
428         messages::internalError(asyncResp->res);
429         return;
430     }
431 
432     // Get the last sequence number
433     uint64_t endSeqNum = 0;
434 #if SYSTEMD_VERSION >= 254
435     {
436         if (sd_journal_get_seqnum(journal.get(), &endSeqNum, nullptr) < 0)
437         {
438             messages::internalError(asyncResp->res);
439             return;
440         }
441     }
442 #endif
443 
444     // Seek to the beginning
445     if (sd_journal_seek_head(journal.get()) < 0)
446     {
447         messages::internalError(asyncResp->res);
448         return;
449     }
450 
451     // Get the first entry
452     if (sd_journal_next(journal.get()) < 0)
453     {
454         messages::internalError(asyncResp->res);
455         return;
456     }
457 
458     // Get the first sequence number
459     uint64_t startSeqNum = 0;
460 #if SYSTEMD_VERSION >= 254
461     {
462         if (sd_journal_get_seqnum(journal.get(), &startSeqNum, nullptr) < 0)
463         {
464             messages::internalError(asyncResp->res);
465             return;
466         }
467     }
468 #endif
469 
470     BMCWEB_LOG_DEBUG("journal Sequence IDs start:{} end:{}", startSeqNum,
471                      endSeqNum);
472 
473     // Add 1 to account for the last entry
474     uint64_t totalEntries = endSeqNum - startSeqNum + 1;
475     asyncResp->res.jsonValue["Members@odata.count"] = totalEntries;
476     if (skip + top < totalEntries)
477     {
478         asyncResp->res.jsonValue["Members@odata.nextLink"] =
479             boost::urls::format(
480                 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}",
481                 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top));
482     }
483     uint64_t index = 0;
484     sd_id128_t curBootID{};
485     uint64_t curTs = 0;
486     if (skip > 0)
487     {
488         if (sd_journal_next_skip(journal.get(), skip) < 0)
489         {
490             messages::internalError(asyncResp->res);
491             return;
492         }
493 
494         // Get the entry timestamp
495         ret = sd_journal_get_monotonic_usec(journal.get(), &curTs, &curBootID);
496         if (ret < 0)
497         {
498             BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}",
499                              strerror(-ret));
500             messages::internalError(asyncResp->res);
501             return;
502         }
503 
504         uint64_t endChunkSeqNum = 0;
505 #if SYSTEMD_VERSION >= 254
506         {
507             if (sd_journal_get_seqnum(journal.get(), &endChunkSeqNum, nullptr) <
508                 0)
509             {
510                 messages::internalError(asyncResp->res);
511                 return;
512             }
513         }
514 #endif
515 
516         // Seek to the first entry with the same timestamp and boot
517         ret = sd_journal_seek_monotonic_usec(journal.get(), curBootID, curTs);
518         if (ret < 0)
519         {
520             BMCWEB_LOG_ERROR("Failed to seek: {}", strerror(-ret));
521             messages::internalError(asyncResp->res);
522             return;
523         }
524         if (sd_journal_next(journal.get()) < 0)
525         {
526             messages::internalError(asyncResp->res);
527             return;
528         }
529         uint64_t startChunkSeqNum = 0;
530 #if SYSTEMD_VERSION >= 254
531         {
532             if (sd_journal_get_seqnum(journal.get(), &startChunkSeqNum,
533                                       nullptr) < 0)
534             {
535                 messages::internalError(asyncResp->res);
536                 return;
537             }
538         }
539 #endif
540 
541         // Get the difference between the start and end.  Most of the time this
542         // will be 0
543         BMCWEB_LOG_DEBUG("start={} end={}", startChunkSeqNum, endChunkSeqNum);
544         index = endChunkSeqNum - startChunkSeqNum;
545         if (index > endChunkSeqNum)
546         {
547             // Detect underflows.  Should never happen.
548             messages::internalError(asyncResp->res);
549             return;
550         }
551         if (index > 0)
552         {
553             BMCWEB_LOG_DEBUG("index = {}", index);
554             if (sd_journal_next_skip(journal.get(), index) < 0)
555             {
556                 messages::internalError(asyncResp->res);
557                 return;
558             }
559         }
560     }
561     // If this is the first entry of this series, reset the timestamps so the
562     // Index doesn't increment
563     if (index == 0)
564     {
565         curBootID = {};
566         curTs = 0;
567     }
568     else
569     {
570         index -= 1;
571     }
572     BMCWEB_LOG_DEBUG("Index was {}", index);
573     readJournalEntries(top, asyncResp,
574                        {std::move(journal), index, curBootID, curTs});
575 }
576 
577 inline void handleManagersJournalEntriesLogEntryGet(
578     App& app, const crow::Request& req,
579     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
580     const std::string& managerId, const std::string& entryID)
581 {
582     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
583     {
584         return;
585     }
586 
587     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
588     {
589         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
590         return;
591     }
592 
593     // Convert the unique ID back to a timestamp to find the entry
594     sd_id128_t bootID{};
595     uint64_t ts = 0;
596     uint64_t index = 0;
597     if (!getTimestampFromID(asyncResp, entryID, bootID, ts, index))
598     {
599         return;
600     }
601 
602     sd_journal* journalTmp = nullptr;
603     int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
604     if (ret < 0)
605     {
606         BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret));
607         messages::internalError(asyncResp->res);
608         return;
609     }
610     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
611         journalTmp, sd_journal_close);
612     journalTmp = nullptr;
613     // Go to the timestamp in the log and move to the entry at the
614     // index tracking the unique ID
615     ret = sd_journal_seek_monotonic_usec(journal.get(), bootID, ts);
616     if (ret < 0)
617     {
618         BMCWEB_LOG_ERROR("failed to seek to an entry in journal{}",
619                          strerror(-ret));
620         messages::internalError(asyncResp->res);
621         return;
622     }
623 
624     if (sd_journal_next_skip(journal.get(), index + 1) < 0)
625     {
626         messages::internalError(asyncResp->res);
627         return;
628     }
629 
630     std::string idStr = getUniqueEntryID(index, ts, bootID);
631 
632     nlohmann::json::object_t bmcJournalLogEntry;
633     if (!fillBMCJournalLogEntryJson(entryID, journal.get(), bmcJournalLogEntry))
634     {
635         messages::internalError(asyncResp->res);
636         return;
637     }
638     asyncResp->res.jsonValue.update(bmcJournalLogEntry);
639 }
640 
641 inline void requestRoutesBMCJournalLogService(App& app)
642 {
643     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/")
644         .privileges(redfish::privileges::getLogService)
645         .methods(boost::beast::http::verb::get)(
646             std::bind_front(handleManagersLogServiceJournalGet, std::ref(app)));
647 
648     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/")
649         .privileges(redfish::privileges::getLogEntryCollection)
650         .methods(boost::beast::http::verb::get)(std::bind_front(
651             handleManagersJournalLogEntryCollectionGet, std::ref(app)));
652 
653     BMCWEB_ROUTE(
654         app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/")
655         .privileges(redfish::privileges::getLogEntry)
656         .methods(boost::beast::http::verb::get)(std::bind_front(
657             handleManagersJournalEntriesLogEntryGet, std::ref(app)));
658 }
659 } // namespace redfish
660