xref: /openbmc/bmcweb/features/redfish/src/subscription.cpp (revision 5ffbc9ae424658c15c2b6d93b86b752f09d2b6dc)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 // SPDX-FileCopyrightText: Copyright 2020 Intel Corporation
4 #include "subscription.hpp"
5 
6 #include "dbus_singleton.hpp"
7 #include "event_log.hpp"
8 #include "event_logs_object_type.hpp"
9 #include "event_matches_filter.hpp"
10 #include "event_service_store.hpp"
11 #include "filter_expr_executor.hpp"
12 #include "heartbeat_messages.hpp"
13 #include "http_client.hpp"
14 #include "http_response.hpp"
15 #include "logging.hpp"
16 #include "metric_report.hpp"
17 #include "server_sent_event.hpp"
18 #include "ssl_key_handler.hpp"
19 #include "utils/time_utils.hpp"
20 
21 #include <boost/asio/error.hpp>
22 #include <boost/asio/io_context.hpp>
23 #include <boost/asio/steady_timer.hpp>
24 #include <boost/beast/http/field.hpp>
25 #include <boost/beast/http/fields.hpp>
26 #include <boost/beast/http/verb.hpp>
27 #include <boost/system/errc.hpp>
28 #include <boost/url/format.hpp>
29 #include <boost/url/url_view_base.hpp>
30 #include <nlohmann/json.hpp>
31 
32 #include <algorithm>
33 #include <chrono>
34 #include <cstdint>
35 #include <cstdlib>
36 #include <ctime>
37 #include <format>
38 #include <functional>
39 #include <memory>
40 #include <span>
41 #include <string>
42 #include <string_view>
43 #include <utility>
44 #include <vector>
45 
46 namespace redfish
47 {
48 
49 Subscription::Subscription(
50     std::shared_ptr<persistent_data::UserSubscription> userSubIn,
51     const boost::urls::url_view_base& url, boost::asio::io_context& ioc) :
52     userSub{std::move(userSubIn)},
53     policy(std::make_shared<crow::ConnectionPolicy>()), hbTimer(ioc)
54 {
55     userSub->destinationUrl = url;
56     client.emplace(ioc, policy);
57     // Subscription constructor
58     policy->invalidResp = retryRespHandler;
59 }
60 
61 Subscription::Subscription(crow::sse_socket::Connection& connIn) :
62     userSub{std::make_shared<persistent_data::UserSubscription>()},
63     sseConn(&connIn), hbTimer(crow::connections::systemBus->get_io_context())
64 {}
65 
66 // callback for subscription sendData
67 void Subscription::resHandler(const crow::Response& res)
68 {
69     BMCWEB_LOG_DEBUG("Response handled with return code: {}", res.resultInt());
70 
71     if (!client)
72     {
73         BMCWEB_LOG_ERROR(
74             "Http client wasn't filled but http client callback was called.");
75         return;
76     }
77 
78     if (userSub->retryPolicy != "TerminateAfterRetries")
79     {
80         return;
81     }
82     if (client->isTerminated())
83     {
84         hbTimer.cancel();
85         if (deleter)
86         {
87             BMCWEB_LOG_INFO("Subscription {} is deleted after MaxRetryAttempts",
88                             userSub->id);
89             deleter();
90         }
91     }
92 }
93 
94 void Subscription::sendHeartbeatEvent()
95 {
96     // send the heartbeat message
97     nlohmann::json eventMessage = messages::redfishServiceFunctional();
98     eventMessage["EventTimestamp"] = time_utils::getDateTimeOffsetNow().first;
99     eventMessage["OriginOfCondition"] =
100         std::format("/redfish/v1/EventService/Subscriptions/{}", userSub->id);
101     eventMessage["MemberId"] = "0";
102 
103     nlohmann::json::array_t eventRecord;
104     eventRecord.emplace_back(std::move(eventMessage));
105 
106     nlohmann::json msgJson;
107     msgJson["@odata.type"] = "#Event.v1_4_0.Event";
108     msgJson["Name"] = "Heartbeat";
109     msgJson["Events"] = std::move(eventRecord);
110 
111     std::string strMsg =
112         msgJson.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
113 
114     // Note, eventId here is always zero, because this is a a per subscription
115     // event and doesn't have an "ID"
116     uint64_t eventId = 0;
117     sendEventToSubscriber(eventId, std::move(strMsg));
118 }
119 
120 void Subscription::scheduleNextHeartbeatEvent()
121 {
122     hbTimer.expires_after(std::chrono::minutes(userSub->hbIntervalMinutes));
123     hbTimer.async_wait(
124         std::bind_front(&Subscription::onHbTimeout, this, weak_from_this()));
125 }
126 
127 void Subscription::heartbeatParametersChanged()
128 {
129     hbTimer.cancel();
130 
131     if (userSub->sendHeartbeat)
132     {
133         scheduleNextHeartbeatEvent();
134     }
135 }
136 
137 void Subscription::onHbTimeout(const std::weak_ptr<Subscription>& weakSelf,
138                                const boost::system::error_code& ec)
139 {
140     if (ec == boost::asio::error::operation_aborted)
141     {
142         BMCWEB_LOG_DEBUG("heartbeat timer async_wait is aborted");
143         return;
144     }
145     if (ec == boost::system::errc::operation_canceled)
146     {
147         BMCWEB_LOG_DEBUG("heartbeat timer async_wait canceled");
148         return;
149     }
150     if (ec)
151     {
152         BMCWEB_LOG_CRITICAL("heartbeat timer async_wait failed: {}", ec);
153         return;
154     }
155 
156     std::shared_ptr<Subscription> self = weakSelf.lock();
157     if (!self)
158     {
159         BMCWEB_LOG_CRITICAL("onHbTimeout failed on Subscription");
160         return;
161     }
162 
163     // Timer expired.
164     sendHeartbeatEvent();
165 
166     // reschedule heartbeat timer
167     scheduleNextHeartbeatEvent();
168 }
169 
170 bool Subscription::sendEventToSubscriber(uint64_t eventId, std::string&& msg)
171 {
172     persistent_data::EventServiceConfig eventServiceConfig =
173         persistent_data::EventServiceStore::getInstance()
174             .getEventServiceConfig();
175     if (!eventServiceConfig.enabled)
176     {
177         return false;
178     }
179 
180     if (client)
181     {
182         boost::beast::http::fields httpHeadersCopy(userSub->httpHeaders);
183         httpHeadersCopy.set(boost::beast::http::field::content_type,
184                             "application/json");
185         client->sendDataWithCallback(
186             std::move(msg), userSub->destinationUrl,
187             static_cast<ensuressl::VerifyCertificate>(
188                 userSub->verifyCertificate),
189             httpHeadersCopy, boost::beast::http::verb::post,
190             std::bind_front(&Subscription::resHandler, this));
191         return true;
192     }
193 
194     if (sseConn != nullptr)
195     {
196         sseConn->sendSseEvent(std::to_string(eventId), msg);
197     }
198     return true;
199 }
200 
201 void Subscription::filterAndSendEventLogs(
202     uint64_t eventId, const std::vector<EventLogObjectsType>& eventRecords)
203 {
204     nlohmann::json::array_t logEntryArray;
205     for (const EventLogObjectsType& logEntry : eventRecords)
206     {
207         BMCWEB_LOG_DEBUG("Processing logEntry: {}, {} '{}'", logEntry.id,
208                          logEntry.timestamp, logEntry.messageId);
209         std::vector<std::string_view> messageArgsView(
210             logEntry.messageArgs.begin(), logEntry.messageArgs.end());
211 
212         nlohmann::json::object_t bmcLogEntry;
213         if (event_log::formatEventLogEntry(
214                 eventId, logEntry.id, logEntry.messageId, messageArgsView,
215                 logEntry.timestamp, userSub->customText, bmcLogEntry) != 0)
216         {
217             BMCWEB_LOG_WARNING("Read eventLog entry failed");
218             continue;
219         }
220 
221         if (!eventMatchesFilter(*userSub, bmcLogEntry, ""))
222         {
223             BMCWEB_LOG_DEBUG("Event {} did not match the filter",
224                              nlohmann::json(bmcLogEntry).dump());
225             continue;
226         }
227 
228         if (filter)
229         {
230             if (!memberMatches(bmcLogEntry, *filter))
231             {
232                 BMCWEB_LOG_DEBUG("Filter didn't match");
233                 continue;
234             }
235         }
236 
237         logEntryArray.emplace_back(std::move(bmcLogEntry));
238         eventId++;
239     }
240 
241     if (logEntryArray.empty())
242     {
243         BMCWEB_LOG_DEBUG("No log entries available to be transferred.");
244         return;
245     }
246 
247     nlohmann::json msg;
248     msg["@odata.type"] = "#Event.v1_4_0.Event";
249     msg["Id"] = std::to_string(eventId);
250     msg["Name"] = "Event Log";
251     msg["Events"] = std::move(logEntryArray);
252     std::string strMsg =
253         msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
254     sendEventToSubscriber(eventId, std::move(strMsg));
255 }
256 
257 void Subscription::filterAndSendReports(uint64_t eventId,
258                                         const std::string& reportId,
259                                         const telemetry::TimestampReadings& var)
260 {
261     boost::urls::url mrdUri = boost::urls::format(
262         "/redfish/v1/TelemetryService/MetricReportDefinitions/{}", reportId);
263 
264     // Empty list means no filter. Send everything.
265     if (!userSub->metricReportDefinitions.empty())
266     {
267         if (std::ranges::find(userSub->metricReportDefinitions,
268                               mrdUri.buffer()) ==
269             userSub->metricReportDefinitions.end())
270         {
271             return;
272         }
273     }
274 
275     nlohmann::json msg;
276     if (!telemetry::fillReport(msg, reportId, var))
277     {
278         BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus "
279                          "Report with id {}",
280                          reportId);
281         return;
282     }
283 
284     // Context is set by user during Event subscription and it must be
285     // set for MetricReport response.
286     if (!userSub->customText.empty())
287     {
288         msg["Context"] = userSub->customText;
289     }
290 
291     std::string strMsg =
292         msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
293     sendEventToSubscriber(eventId, std::move(strMsg));
294 }
295 
296 void Subscription::updateRetryConfig(uint32_t retryAttempts,
297                                      uint32_t retryTimeoutInterval)
298 {
299     if (policy == nullptr)
300     {
301         BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set");
302         return;
303     }
304     policy->maxRetryAttempts = retryAttempts;
305     policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval);
306 }
307 
308 bool Subscription::matchSseId(const crow::sse_socket::Connection& thisConn)
309 {
310     return &thisConn == sseConn;
311 }
312 
313 // Check used to indicate what response codes are valid as part of our retry
314 // policy.  2XX is considered acceptable
315 boost::system::error_code Subscription::retryRespHandler(unsigned int respCode)
316 {
317     BMCWEB_LOG_DEBUG("Checking response code validity for SubscriptionEvent");
318     if ((respCode < 200) || (respCode >= 300))
319     {
320         return boost::system::errc::make_error_code(
321             boost::system::errc::result_out_of_range);
322     }
323 
324     // Return 0 if the response code is valid
325     return boost::system::errc::make_error_code(boost::system::errc::success);
326 }
327 
328 } // namespace redfish
329