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