xref: /openbmc/bmcweb/redfish-core/src/subscription.cpp (revision 4ac78946f01a016727af5a24489dd44ee37273a6)
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 std::shared_ptr<Subscription> &,const crow::Response & res)79 void Subscription::resHandler(const std::shared_ptr<Subscription>& /*unused*/,
80                               const crow::Response& res)
81 {
82     BMCWEB_LOG_DEBUG("Response handled with return code: {}", res.resultInt());
83 
84     if (!client)
85     {
86         BMCWEB_LOG_ERROR(
87             "Http client wasn't filled but http client callback was called.");
88         return;
89     }
90 
91     if (userSub->retryPolicy != "TerminateAfterRetries")
92     {
93         return;
94     }
95     if (client->isTerminated())
96     {
97         hbTimer.cancel();
98         if (deleter)
99         {
100             BMCWEB_LOG_INFO("Subscription {} is deleted after MaxRetryAttempts",
101                             userSub->id);
102             deleter();
103         }
104     }
105 }
106 
sendHeartbeatEvent()107 void Subscription::sendHeartbeatEvent()
108 {
109     // send the heartbeat message
110     nlohmann::json eventMessage = messages::redfishServiceFunctional();
111 
112     std::string heartEventId = std::to_string(eventSeqNum);
113     eventMessage["EventId"] = heartEventId;
114     eventMessage["EventTimestamp"] = time_utils::getDateTimeOffsetNow().first;
115     eventMessage["OriginOfCondition"] =
116         std::format("/redfish/v1/EventService/Subscriptions/{}", userSub->id);
117     eventMessage["MemberId"] = "0";
118 
119     nlohmann::json::array_t eventRecord;
120     eventRecord.emplace_back(std::move(eventMessage));
121 
122     nlohmann::json msgJson;
123     msgJson["@odata.type"] = "#Event.v1_4_0.Event";
124     msgJson["Name"] = "Heartbeat";
125     msgJson["Id"] = heartEventId;
126     msgJson["Events"] = std::move(eventRecord);
127 
128     std::string strMsg =
129         msgJson.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
130     sendEventToSubscriber(std::move(strMsg));
131     eventSeqNum++;
132 }
133 
scheduleNextHeartbeatEvent()134 void Subscription::scheduleNextHeartbeatEvent()
135 {
136     hbTimer.expires_after(std::chrono::minutes(userSub->hbIntervalMinutes));
137     hbTimer.async_wait(
138         std::bind_front(&Subscription::onHbTimeout, this, weak_from_this()));
139 }
140 
heartbeatParametersChanged()141 void Subscription::heartbeatParametersChanged()
142 {
143     hbTimer.cancel();
144 
145     if (userSub->sendHeartbeat)
146     {
147         scheduleNextHeartbeatEvent();
148     }
149 }
150 
onHbTimeout(const std::weak_ptr<Subscription> & weakSelf,const boost::system::error_code & ec)151 void Subscription::onHbTimeout(const std::weak_ptr<Subscription>& weakSelf,
152                                const boost::system::error_code& ec)
153 {
154     if (ec == boost::asio::error::operation_aborted)
155     {
156         BMCWEB_LOG_DEBUG("heartbeat timer async_wait is aborted");
157         return;
158     }
159     if (ec == boost::system::errc::operation_canceled)
160     {
161         BMCWEB_LOG_DEBUG("heartbeat timer async_wait canceled");
162         return;
163     }
164     if (ec)
165     {
166         BMCWEB_LOG_CRITICAL("heartbeat timer async_wait failed: {}", ec);
167         return;
168     }
169 
170     std::shared_ptr<Subscription> self = weakSelf.lock();
171     if (!self)
172     {
173         BMCWEB_LOG_CRITICAL("onHbTimeout failed on Subscription");
174         return;
175     }
176 
177     // Timer expired.
178     sendHeartbeatEvent();
179 
180     // reschedule heartbeat timer
181     scheduleNextHeartbeatEvent();
182 }
183 
sendEventToSubscriber(std::string && msg)184 bool Subscription::sendEventToSubscriber(std::string&& msg)
185 {
186     persistent_data::EventServiceConfig eventServiceConfig =
187         persistent_data::EventServiceStore::getInstance()
188             .getEventServiceConfig();
189     if (!eventServiceConfig.enabled)
190     {
191         return false;
192     }
193 
194     if (client)
195     {
196         boost::beast::http::fields httpHeadersCopy(userSub->httpHeaders);
197         httpHeadersCopy.set(boost::beast::http::field::content_type,
198                             "application/json");
199         client->sendDataWithCallback(
200             std::move(msg), userSub->destinationUrl,
201             static_cast<ensuressl::VerifyCertificate>(
202                 userSub->verifyCertificate),
203             httpHeadersCopy, boost::beast::http::verb::post,
204             std::bind_front(&Subscription::resHandler, this,
205                             shared_from_this()));
206         return true;
207     }
208 
209     if (sseConn != nullptr)
210     {
211         eventSeqNum++;
212         sseConn->sendSseEvent(std::to_string(eventSeqNum), msg);
213     }
214     return true;
215 }
216 
sendTestEventLog(TestEvent & testEvent)217 bool Subscription::sendTestEventLog(TestEvent& testEvent)
218 {
219     nlohmann::json::array_t logEntryArray;
220     nlohmann::json& logEntryJson = logEntryArray.emplace_back();
221 
222     if (testEvent.eventGroupId)
223     {
224         logEntryJson["EventGroupId"] = *testEvent.eventGroupId;
225     }
226 
227     if (testEvent.eventId)
228     {
229         logEntryJson["EventId"] = *testEvent.eventId;
230     }
231 
232     if (testEvent.eventTimestamp)
233     {
234         logEntryJson["EventTimestamp"] = *testEvent.eventTimestamp;
235     }
236 
237     if (testEvent.originOfCondition)
238     {
239         logEntryJson["OriginOfCondition"]["@odata.id"] =
240             *testEvent.originOfCondition;
241     }
242     if (testEvent.severity)
243     {
244         logEntryJson["Severity"] = *testEvent.severity;
245     }
246 
247     if (testEvent.message)
248     {
249         logEntryJson["Message"] = *testEvent.message;
250     }
251 
252     if (testEvent.resolution)
253     {
254         logEntryJson["Resolution"] = *testEvent.resolution;
255     }
256 
257     if (testEvent.messageId)
258     {
259         logEntryJson["MessageId"] = *testEvent.messageId;
260     }
261 
262     if (testEvent.messageArgs)
263     {
264         logEntryJson["MessageArgs"] = *testEvent.messageArgs;
265     }
266     // MemberId is 0 : since we are sending one event record.
267     logEntryJson["MemberId"] = "0";
268 
269     nlohmann::json msg;
270     msg["@odata.type"] = "#Event.v1_4_0.Event";
271     msg["Id"] = std::to_string(eventSeqNum);
272     msg["Name"] = "Event Log";
273     msg["Events"] = logEntryArray;
274 
275     std::string strMsg =
276         msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
277     return sendEventToSubscriber(std::move(strMsg));
278 }
279 
filterAndSendEventLogs(const std::vector<EventLogObjectsType> & eventRecords)280 void Subscription::filterAndSendEventLogs(
281     const std::vector<EventLogObjectsType>& eventRecords)
282 {
283     nlohmann::json::array_t logEntryArray;
284     for (const EventLogObjectsType& logEntry : eventRecords)
285     {
286         std::vector<std::string_view> messageArgsView(
287             logEntry.messageArgs.begin(), logEntry.messageArgs.end());
288 
289         nlohmann::json::object_t bmcLogEntry;
290         if (event_log::formatEventLogEntry(
291                 logEntry.id, logEntry.messageId, messageArgsView,
292                 logEntry.timestamp, userSub->customText, bmcLogEntry) != 0)
293         {
294             BMCWEB_LOG_DEBUG("Read eventLog entry failed");
295             continue;
296         }
297 
298         if (!eventMatchesFilter(*userSub, bmcLogEntry, ""))
299         {
300             BMCWEB_LOG_DEBUG("Event {} did not match the filter",
301                              nlohmann::json(bmcLogEntry).dump());
302             continue;
303         }
304 
305         if (filter)
306         {
307             if (!memberMatches(bmcLogEntry, *filter))
308             {
309                 BMCWEB_LOG_DEBUG("Filter didn't match");
310                 continue;
311             }
312         }
313 
314         logEntryArray.emplace_back(std::move(bmcLogEntry));
315     }
316 
317     if (logEntryArray.empty())
318     {
319         BMCWEB_LOG_DEBUG("No log entries available to be transferred.");
320         return;
321     }
322 
323     nlohmann::json msg;
324     msg["@odata.type"] = "#Event.v1_4_0.Event";
325     msg["Id"] = std::to_string(eventSeqNum);
326     msg["Name"] = "Event Log";
327     msg["Events"] = std::move(logEntryArray);
328     std::string strMsg =
329         msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
330     sendEventToSubscriber(std::move(strMsg));
331     eventSeqNum++;
332 }
333 
filterAndSendReports(const std::string & reportId,const telemetry::TimestampReadings & var)334 void Subscription::filterAndSendReports(const std::string& reportId,
335                                         const telemetry::TimestampReadings& var)
336 {
337     boost::urls::url mrdUri = boost::urls::format(
338         "/redfish/v1/TelemetryService/MetricReportDefinitions/{}", reportId);
339 
340     // Empty list means no filter. Send everything.
341     if (!userSub->metricReportDefinitions.empty())
342     {
343         if (std::ranges::find(userSub->metricReportDefinitions,
344                               mrdUri.buffer()) ==
345             userSub->metricReportDefinitions.end())
346         {
347             return;
348         }
349     }
350 
351     nlohmann::json msg;
352     if (!telemetry::fillReport(msg, reportId, var))
353     {
354         BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus "
355                          "Report with id {}",
356                          reportId);
357         return;
358     }
359 
360     // Context is set by user during Event subscription and it must be
361     // set for MetricReport response.
362     if (!userSub->customText.empty())
363     {
364         msg["Context"] = userSub->customText;
365     }
366 
367     std::string strMsg =
368         msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
369     sendEventToSubscriber(std::move(strMsg));
370 }
371 
updateRetryConfig(uint32_t retryAttempts,uint32_t retryTimeoutInterval)372 void Subscription::updateRetryConfig(uint32_t retryAttempts,
373                                      uint32_t retryTimeoutInterval)
374 {
375     if (policy == nullptr)
376     {
377         BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set");
378         return;
379     }
380     policy->maxRetryAttempts = retryAttempts;
381     policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval);
382 }
383 
getEventSeqNum() const384 uint64_t Subscription::getEventSeqNum() const
385 {
386     return eventSeqNum;
387 }
388 
matchSseId(const crow::sse_socket::Connection & thisConn)389 bool Subscription::matchSseId(const crow::sse_socket::Connection& thisConn)
390 {
391     return &thisConn == sseConn;
392 }
393 
394 // Check used to indicate what response codes are valid as part of our retry
395 // policy.  2XX is considered acceptable
retryRespHandler(unsigned int respCode)396 boost::system::error_code Subscription::retryRespHandler(unsigned int respCode)
397 {
398     BMCWEB_LOG_DEBUG("Checking response code validity for SubscriptionEvent");
399     if ((respCode < 200) || (respCode >= 300))
400     {
401         return boost::system::errc::make_error_code(
402             boost::system::errc::result_out_of_range);
403     }
404 
405     // Return 0 if the response code is valid
406     return boost::system::errc::make_error_code(boost::system::errc::success);
407 }
408 
409 } // namespace redfish
410