xref: /openbmc/bmcweb/redfish-core/src/subscription.cpp (revision f2656d1b941f52d20e44fa216a8f09627ab96aca)
1 /*
2 Copyright (c) 2020 Intel Corporation
3 
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7 
8       http://www.apache.org/licenses/LICENSE-2.0
9 
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15 */
16 #include "subscription.hpp"
17 
18 #include "dbus_singleton.hpp"
19 #include "event_log.hpp"
20 #include "event_logs_object_type.hpp"
21 #include "event_matches_filter.hpp"
22 #include "event_service_store.hpp"
23 #include "filter_expr_executor.hpp"
24 #include "heartbeat_messages.hpp"
25 #include "http_client.hpp"
26 #include "http_response.hpp"
27 #include "logging.hpp"
28 #include "metric_report.hpp"
29 #include "server_sent_event.hpp"
30 #include "ssl_key_handler.hpp"
31 #include "utils/time_utils.hpp"
32 
33 #include <boost/asio/error.hpp>
34 #include <boost/asio/io_context.hpp>
35 #include <boost/asio/steady_timer.hpp>
36 #include <boost/beast/http/field.hpp>
37 #include <boost/beast/http/fields.hpp>
38 #include <boost/beast/http/verb.hpp>
39 #include <boost/system/errc.hpp>
40 #include <boost/url/format.hpp>
41 #include <boost/url/url_view_base.hpp>
42 #include <nlohmann/json.hpp>
43 
44 #include <algorithm>
45 #include <chrono>
46 #include <cstdint>
47 #include <cstdlib>
48 #include <ctime>
49 #include <format>
50 #include <functional>
51 #include <memory>
52 #include <span>
53 #include <string>
54 #include <string_view>
55 #include <utility>
56 #include <vector>
57 
58 namespace redfish
59 {
60 
Subscription(std::shared_ptr<persistent_data::UserSubscription> userSubIn,const boost::urls::url_view_base & url,boost::asio::io_context & ioc)61 Subscription::Subscription(
62     std::shared_ptr<persistent_data::UserSubscription> userSubIn,
63     const boost::urls::url_view_base& url, boost::asio::io_context& ioc) :
64     userSub{std::move(userSubIn)},
65     policy(std::make_shared<crow::ConnectionPolicy>()), hbTimer(ioc)
66 {
67     userSub->destinationUrl = url;
68     client.emplace(ioc, policy);
69     // Subscription constructor
70     policy->invalidResp = retryRespHandler;
71 }
72 
Subscription(crow::sse_socket::Connection & connIn)73 Subscription::Subscription(crow::sse_socket::Connection& connIn) :
74     userSub{std::make_shared<persistent_data::UserSubscription>()},
75     sseConn(&connIn), hbTimer(crow::connections::systemBus->get_io_context())
76 {}
77 
78 // callback for subscription sendData
resHandler(const crow::Response & res)79 void Subscription::resHandler(const crow::Response& res)
80 {
81     BMCWEB_LOG_DEBUG("Response handled with return code: {}", res.resultInt());
82 
83     if (!client)
84     {
85         BMCWEB_LOG_ERROR(
86             "Http client wasn't filled but http client callback was called.");
87         return;
88     }
89 
90     if (userSub->retryPolicy != "TerminateAfterRetries")
91     {
92         return;
93     }
94     if (client->isTerminated())
95     {
96         hbTimer.cancel();
97         if (deleter)
98         {
99             BMCWEB_LOG_INFO("Subscription {} is deleted after MaxRetryAttempts",
100                             userSub->id);
101             deleter();
102         }
103     }
104 }
105 
sendHeartbeatEvent()106 void Subscription::sendHeartbeatEvent()
107 {
108     // send the heartbeat message
109     nlohmann::json eventMessage = messages::redfishServiceFunctional();
110 
111     std::string heartEventId = std::to_string(eventSeqNum);
112     eventMessage["EventId"] = heartEventId;
113     eventMessage["EventTimestamp"] = time_utils::getDateTimeOffsetNow().first;
114     eventMessage["OriginOfCondition"] =
115         std::format("/redfish/v1/EventService/Subscriptions/{}", userSub->id);
116     eventMessage["MemberId"] = "0";
117 
118     nlohmann::json::array_t eventRecord;
119     eventRecord.emplace_back(std::move(eventMessage));
120 
121     nlohmann::json msgJson;
122     msgJson["@odata.type"] = "#Event.v1_4_0.Event";
123     msgJson["Name"] = "Heartbeat";
124     msgJson["Id"] = heartEventId;
125     msgJson["Events"] = std::move(eventRecord);
126 
127     std::string strMsg =
128         msgJson.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
129     sendEventToSubscriber(std::move(strMsg));
130     eventSeqNum++;
131 }
132 
scheduleNextHeartbeatEvent()133 void Subscription::scheduleNextHeartbeatEvent()
134 {
135     hbTimer.expires_after(std::chrono::minutes(userSub->hbIntervalMinutes));
136     hbTimer.async_wait(
137         std::bind_front(&Subscription::onHbTimeout, this, weak_from_this()));
138 }
139 
heartbeatParametersChanged()140 void Subscription::heartbeatParametersChanged()
141 {
142     hbTimer.cancel();
143 
144     if (userSub->sendHeartbeat)
145     {
146         scheduleNextHeartbeatEvent();
147     }
148 }
149 
onHbTimeout(const std::weak_ptr<Subscription> & weakSelf,const boost::system::error_code & ec)150 void Subscription::onHbTimeout(const std::weak_ptr<Subscription>& weakSelf,
151                                const boost::system::error_code& ec)
152 {
153     if (ec == boost::asio::error::operation_aborted)
154     {
155         BMCWEB_LOG_DEBUG("heartbeat timer async_wait is aborted");
156         return;
157     }
158     if (ec == boost::system::errc::operation_canceled)
159     {
160         BMCWEB_LOG_DEBUG("heartbeat timer async_wait canceled");
161         return;
162     }
163     if (ec)
164     {
165         BMCWEB_LOG_CRITICAL("heartbeat timer async_wait failed: {}", ec);
166         return;
167     }
168 
169     std::shared_ptr<Subscription> self = weakSelf.lock();
170     if (!self)
171     {
172         BMCWEB_LOG_CRITICAL("onHbTimeout failed on Subscription");
173         return;
174     }
175 
176     // Timer expired.
177     sendHeartbeatEvent();
178 
179     // reschedule heartbeat timer
180     scheduleNextHeartbeatEvent();
181 }
182 
sendEventToSubscriber(std::string && msg)183 bool Subscription::sendEventToSubscriber(std::string&& msg)
184 {
185     persistent_data::EventServiceConfig eventServiceConfig =
186         persistent_data::EventServiceStore::getInstance()
187             .getEventServiceConfig();
188     if (!eventServiceConfig.enabled)
189     {
190         return false;
191     }
192 
193     if (client)
194     {
195         boost::beast::http::fields httpHeadersCopy(userSub->httpHeaders);
196         httpHeadersCopy.set(boost::beast::http::field::content_type,
197                             "application/json");
198         client->sendDataWithCallback(
199             std::move(msg), userSub->destinationUrl,
200             static_cast<ensuressl::VerifyCertificate>(
201                 userSub->verifyCertificate),
202             httpHeadersCopy, boost::beast::http::verb::post,
203             std::bind_front(&Subscription::resHandler, this));
204         return true;
205     }
206 
207     if (sseConn != nullptr)
208     {
209         eventSeqNum++;
210         sseConn->sendSseEvent(std::to_string(eventSeqNum), msg);
211     }
212     return true;
213 }
214 
sendTestEventLog(TestEvent & testEvent)215 bool Subscription::sendTestEventLog(TestEvent& testEvent)
216 {
217     nlohmann::json::array_t logEntryArray;
218     nlohmann::json& logEntryJson = logEntryArray.emplace_back();
219 
220     if (testEvent.eventGroupId)
221     {
222         logEntryJson["EventGroupId"] = *testEvent.eventGroupId;
223     }
224 
225     if (testEvent.eventId)
226     {
227         logEntryJson["EventId"] = *testEvent.eventId;
228     }
229 
230     if (testEvent.eventTimestamp)
231     {
232         logEntryJson["EventTimestamp"] = *testEvent.eventTimestamp;
233     }
234 
235     if (testEvent.originOfCondition)
236     {
237         logEntryJson["OriginOfCondition"]["@odata.id"] =
238             *testEvent.originOfCondition;
239     }
240     if (testEvent.severity)
241     {
242         logEntryJson["Severity"] = *testEvent.severity;
243     }
244 
245     if (testEvent.message)
246     {
247         logEntryJson["Message"] = *testEvent.message;
248     }
249 
250     if (testEvent.resolution)
251     {
252         logEntryJson["Resolution"] = *testEvent.resolution;
253     }
254 
255     if (testEvent.messageId)
256     {
257         logEntryJson["MessageId"] = *testEvent.messageId;
258     }
259 
260     if (testEvent.messageArgs)
261     {
262         logEntryJson["MessageArgs"] = *testEvent.messageArgs;
263     }
264     // MemberId is 0 : since we are sending one event record.
265     logEntryJson["MemberId"] = "0";
266 
267     nlohmann::json msg;
268     msg["@odata.type"] = "#Event.v1_4_0.Event";
269     msg["Id"] = std::to_string(eventSeqNum);
270     msg["Name"] = "Event Log";
271     msg["Events"] = logEntryArray;
272 
273     std::string strMsg =
274         msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
275     return sendEventToSubscriber(std::move(strMsg));
276 }
277 
filterAndSendEventLogs(const std::vector<EventLogObjectsType> & eventRecords)278 void Subscription::filterAndSendEventLogs(
279     const std::vector<EventLogObjectsType>& eventRecords)
280 {
281     nlohmann::json::array_t logEntryArray;
282     for (const EventLogObjectsType& logEntry : eventRecords)
283     {
284         std::vector<std::string_view> messageArgsView(
285             logEntry.messageArgs.begin(), logEntry.messageArgs.end());
286 
287         nlohmann::json::object_t bmcLogEntry;
288         if (event_log::formatEventLogEntry(
289                 logEntry.id, logEntry.messageId, messageArgsView,
290                 logEntry.timestamp, userSub->customText, bmcLogEntry) != 0)
291         {
292             BMCWEB_LOG_DEBUG("Read eventLog entry failed");
293             continue;
294         }
295 
296         if (!eventMatchesFilter(*userSub, bmcLogEntry, ""))
297         {
298             BMCWEB_LOG_DEBUG("Event {} did not match the filter",
299                              nlohmann::json(bmcLogEntry).dump());
300             continue;
301         }
302 
303         if (filter)
304         {
305             if (!memberMatches(bmcLogEntry, *filter))
306             {
307                 BMCWEB_LOG_DEBUG("Filter didn't match");
308                 continue;
309             }
310         }
311 
312         logEntryArray.emplace_back(std::move(bmcLogEntry));
313     }
314 
315     if (logEntryArray.empty())
316     {
317         BMCWEB_LOG_DEBUG("No log entries available to be transferred.");
318         return;
319     }
320 
321     nlohmann::json msg;
322     msg["@odata.type"] = "#Event.v1_4_0.Event";
323     msg["Id"] = std::to_string(eventSeqNum);
324     msg["Name"] = "Event Log";
325     msg["Events"] = std::move(logEntryArray);
326     std::string strMsg =
327         msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
328     sendEventToSubscriber(std::move(strMsg));
329     eventSeqNum++;
330 }
331 
filterAndSendReports(const std::string & reportId,const telemetry::TimestampReadings & var)332 void Subscription::filterAndSendReports(const std::string& reportId,
333                                         const telemetry::TimestampReadings& var)
334 {
335     boost::urls::url mrdUri = boost::urls::format(
336         "/redfish/v1/TelemetryService/MetricReportDefinitions/{}", reportId);
337 
338     // Empty list means no filter. Send everything.
339     if (!userSub->metricReportDefinitions.empty())
340     {
341         if (std::ranges::find(userSub->metricReportDefinitions,
342                               mrdUri.buffer()) ==
343             userSub->metricReportDefinitions.end())
344         {
345             return;
346         }
347     }
348 
349     nlohmann::json msg;
350     if (!telemetry::fillReport(msg, reportId, var))
351     {
352         BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus "
353                          "Report with id {}",
354                          reportId);
355         return;
356     }
357 
358     // Context is set by user during Event subscription and it must be
359     // set for MetricReport response.
360     if (!userSub->customText.empty())
361     {
362         msg["Context"] = userSub->customText;
363     }
364 
365     std::string strMsg =
366         msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
367     sendEventToSubscriber(std::move(strMsg));
368 }
369 
updateRetryConfig(uint32_t retryAttempts,uint32_t retryTimeoutInterval)370 void Subscription::updateRetryConfig(uint32_t retryAttempts,
371                                      uint32_t retryTimeoutInterval)
372 {
373     if (policy == nullptr)
374     {
375         BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set");
376         return;
377     }
378     policy->maxRetryAttempts = retryAttempts;
379     policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval);
380 }
381 
getEventSeqNum() const382 uint64_t Subscription::getEventSeqNum() const
383 {
384     return eventSeqNum;
385 }
386 
matchSseId(const crow::sse_socket::Connection & thisConn)387 bool Subscription::matchSseId(const crow::sse_socket::Connection& thisConn)
388 {
389     return &thisConn == sseConn;
390 }
391 
392 // Check used to indicate what response codes are valid as part of our retry
393 // policy.  2XX is considered acceptable
retryRespHandler(unsigned int respCode)394 boost::system::error_code Subscription::retryRespHandler(unsigned int respCode)
395 {
396     BMCWEB_LOG_DEBUG("Checking response code validity for SubscriptionEvent");
397     if ((respCode < 200) || (respCode >= 300))
398     {
399         return boost::system::errc::make_error_code(
400             boost::system::errc::result_out_of_range);
401     }
402 
403     // Return 0 if the response code is valid
404     return boost::system::errc::make_error_code(boost::system::errc::success);
405 }
406 
407 } // namespace redfish
408