102c1e29fSAlexander Hansen /* 202c1e29fSAlexander Hansen Copyright (c) 2020 Intel Corporation 302c1e29fSAlexander Hansen 402c1e29fSAlexander Hansen Licensed under the Apache License, Version 2.0 (the "License"); 502c1e29fSAlexander Hansen you may not use this file except in compliance with the License. 602c1e29fSAlexander Hansen You may obtain a copy of the License at 702c1e29fSAlexander Hansen 802c1e29fSAlexander Hansen http://www.apache.org/licenses/LICENSE-2.0 902c1e29fSAlexander Hansen 1002c1e29fSAlexander Hansen Unless required by applicable law or agreed to in writing, software 1102c1e29fSAlexander Hansen distributed under the License is distributed on an "AS IS" BASIS, 1202c1e29fSAlexander Hansen WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1302c1e29fSAlexander Hansen See the License for the specific language governing permissions and 1402c1e29fSAlexander Hansen limitations under the License. 1502c1e29fSAlexander Hansen */ 1602c1e29fSAlexander Hansen #include "subscription.hpp" 1702c1e29fSAlexander Hansen 18*fb546105SMyung Bae #include "dbus_singleton.hpp" 19b80ba2e4SAlexander Hansen #include "event_log.hpp" 2002c1e29fSAlexander Hansen #include "event_logs_object_type.hpp" 2102c1e29fSAlexander Hansen #include "event_matches_filter.hpp" 2202c1e29fSAlexander Hansen #include "event_service_store.hpp" 2302c1e29fSAlexander Hansen #include "filter_expr_executor.hpp" 2402c1e29fSAlexander Hansen #include "generated/enums/log_entry.hpp" 25*fb546105SMyung Bae #include "heartbeat_messages.hpp" 2602c1e29fSAlexander Hansen #include "http_client.hpp" 2702c1e29fSAlexander Hansen #include "http_response.hpp" 2802c1e29fSAlexander Hansen #include "logging.hpp" 2902c1e29fSAlexander Hansen #include "metric_report.hpp" 3002c1e29fSAlexander Hansen #include "server_sent_event.hpp" 3102c1e29fSAlexander Hansen #include "ssl_key_handler.hpp" 3202c1e29fSAlexander Hansen #include "utils/time_utils.hpp" 3302c1e29fSAlexander Hansen 34*fb546105SMyung Bae #include <boost/asio/error.hpp> 3502c1e29fSAlexander Hansen #include <boost/asio/io_context.hpp> 36*fb546105SMyung Bae #include <boost/asio/steady_timer.hpp> 3702c1e29fSAlexander Hansen #include <boost/beast/http/verb.hpp> 3802c1e29fSAlexander Hansen #include <boost/system/errc.hpp> 3902c1e29fSAlexander Hansen #include <boost/url/format.hpp> 4002c1e29fSAlexander Hansen #include <boost/url/url_view_base.hpp> 4102c1e29fSAlexander Hansen #include <nlohmann/json.hpp> 4202c1e29fSAlexander Hansen 4302c1e29fSAlexander Hansen #include <algorithm> 44*fb546105SMyung Bae #include <chrono> 4502c1e29fSAlexander Hansen #include <cstdint> 4602c1e29fSAlexander Hansen #include <cstdlib> 4702c1e29fSAlexander Hansen #include <ctime> 4802c1e29fSAlexander Hansen #include <format> 4902c1e29fSAlexander Hansen #include <functional> 5002c1e29fSAlexander Hansen #include <memory> 5102c1e29fSAlexander Hansen #include <span> 5202c1e29fSAlexander Hansen #include <string> 5302c1e29fSAlexander Hansen #include <string_view> 5402c1e29fSAlexander Hansen #include <utility> 5502c1e29fSAlexander Hansen #include <vector> 5602c1e29fSAlexander Hansen 5702c1e29fSAlexander Hansen namespace redfish 5802c1e29fSAlexander Hansen { 5902c1e29fSAlexander Hansen 6002c1e29fSAlexander Hansen Subscription::Subscription( 6102c1e29fSAlexander Hansen std::shared_ptr<persistent_data::UserSubscription> userSubIn, 6202c1e29fSAlexander Hansen const boost::urls::url_view_base& url, boost::asio::io_context& ioc) : 6302c1e29fSAlexander Hansen userSub{std::move(userSubIn)}, 64*fb546105SMyung Bae policy(std::make_shared<crow::ConnectionPolicy>()), hbTimer(ioc) 6502c1e29fSAlexander Hansen { 6602c1e29fSAlexander Hansen userSub->destinationUrl = url; 6702c1e29fSAlexander Hansen client.emplace(ioc, policy); 6802c1e29fSAlexander Hansen // Subscription constructor 6902c1e29fSAlexander Hansen policy->invalidResp = retryRespHandler; 7002c1e29fSAlexander Hansen } 7102c1e29fSAlexander Hansen 7202c1e29fSAlexander Hansen Subscription::Subscription(crow::sse_socket::Connection& connIn) : 7302c1e29fSAlexander Hansen userSub{std::make_shared<persistent_data::UserSubscription>()}, 74*fb546105SMyung Bae sseConn(&connIn), hbTimer(crow::connections::systemBus->get_io_context()) 7502c1e29fSAlexander Hansen {} 7602c1e29fSAlexander Hansen 7702c1e29fSAlexander Hansen // callback for subscription sendData 7802c1e29fSAlexander Hansen void Subscription::resHandler(const std::shared_ptr<Subscription>& /*unused*/, 7902c1e29fSAlexander Hansen const crow::Response& res) 8002c1e29fSAlexander Hansen { 8102c1e29fSAlexander Hansen BMCWEB_LOG_DEBUG("Response handled with return code: {}", res.resultInt()); 8202c1e29fSAlexander Hansen 8302c1e29fSAlexander Hansen if (!client) 8402c1e29fSAlexander Hansen { 8502c1e29fSAlexander Hansen BMCWEB_LOG_ERROR( 8602c1e29fSAlexander Hansen "Http client wasn't filled but http client callback was called."); 8702c1e29fSAlexander Hansen return; 8802c1e29fSAlexander Hansen } 8902c1e29fSAlexander Hansen 9002c1e29fSAlexander Hansen if (userSub->retryPolicy != "TerminateAfterRetries") 9102c1e29fSAlexander Hansen { 9202c1e29fSAlexander Hansen return; 9302c1e29fSAlexander Hansen } 9402c1e29fSAlexander Hansen if (client->isTerminated()) 9502c1e29fSAlexander Hansen { 96*fb546105SMyung Bae hbTimer.cancel(); 9702c1e29fSAlexander Hansen if (deleter) 9802c1e29fSAlexander Hansen { 9902c1e29fSAlexander Hansen BMCWEB_LOG_INFO("Subscription {} is deleted after MaxRetryAttempts", 10002c1e29fSAlexander Hansen userSub->id); 10102c1e29fSAlexander Hansen deleter(); 10202c1e29fSAlexander Hansen } 10302c1e29fSAlexander Hansen } 10402c1e29fSAlexander Hansen } 10502c1e29fSAlexander Hansen 106*fb546105SMyung Bae void Subscription::sendHeartbeatEvent() 107*fb546105SMyung Bae { 108*fb546105SMyung Bae // send the heartbeat message 109*fb546105SMyung Bae nlohmann::json eventMessage = messages::redfishServiceFunctional(); 110*fb546105SMyung Bae 111*fb546105SMyung Bae std::string heartEventId = std::to_string(eventSeqNum); 112*fb546105SMyung Bae eventMessage["EventId"] = heartEventId; 113*fb546105SMyung Bae eventMessage["EventTimestamp"] = time_utils::getDateTimeOffsetNow().first; 114*fb546105SMyung Bae eventMessage["OriginOfCondition"] = 115*fb546105SMyung Bae std::format("/redfish/v1/EventService/Subscriptions/{}", userSub->id); 116*fb546105SMyung Bae eventMessage["MemberId"] = "0"; 117*fb546105SMyung Bae 118*fb546105SMyung Bae nlohmann::json::array_t eventRecord; 119*fb546105SMyung Bae eventRecord.emplace_back(std::move(eventMessage)); 120*fb546105SMyung Bae 121*fb546105SMyung Bae nlohmann::json msgJson; 122*fb546105SMyung Bae msgJson["@odata.type"] = "#Event.v1_4_0.Event"; 123*fb546105SMyung Bae msgJson["Name"] = "Heartbeat"; 124*fb546105SMyung Bae msgJson["Id"] = heartEventId; 125*fb546105SMyung Bae msgJson["Events"] = std::move(eventRecord); 126*fb546105SMyung Bae 127*fb546105SMyung Bae std::string strMsg = 128*fb546105SMyung Bae msgJson.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 129*fb546105SMyung Bae sendEventToSubscriber(std::move(strMsg)); 130*fb546105SMyung Bae eventSeqNum++; 131*fb546105SMyung Bae } 132*fb546105SMyung Bae 133*fb546105SMyung Bae void Subscription::scheduleNextHeartbeatEvent() 134*fb546105SMyung Bae { 135*fb546105SMyung Bae hbTimer.expires_after(std::chrono::minutes(userSub->hbIntervalMinutes)); 136*fb546105SMyung Bae hbTimer.async_wait( 137*fb546105SMyung Bae std::bind_front(&Subscription::onHbTimeout, this, weak_from_this())); 138*fb546105SMyung Bae } 139*fb546105SMyung Bae 140*fb546105SMyung Bae void Subscription::heartbeatParametersChanged() 141*fb546105SMyung Bae { 142*fb546105SMyung Bae hbTimer.cancel(); 143*fb546105SMyung Bae 144*fb546105SMyung Bae if (userSub->sendHeartbeat) 145*fb546105SMyung Bae { 146*fb546105SMyung Bae scheduleNextHeartbeatEvent(); 147*fb546105SMyung Bae } 148*fb546105SMyung Bae } 149*fb546105SMyung Bae 150*fb546105SMyung Bae void Subscription::onHbTimeout(const std::weak_ptr<Subscription>& weakSelf, 151*fb546105SMyung Bae const boost::system::error_code& ec) 152*fb546105SMyung Bae { 153*fb546105SMyung Bae if (ec == boost::asio::error::operation_aborted) 154*fb546105SMyung Bae { 155*fb546105SMyung Bae BMCWEB_LOG_DEBUG("heartbeat timer async_wait is aborted"); 156*fb546105SMyung Bae return; 157*fb546105SMyung Bae } 158*fb546105SMyung Bae if (ec == boost::system::errc::operation_canceled) 159*fb546105SMyung Bae { 160*fb546105SMyung Bae BMCWEB_LOG_DEBUG("heartbeat timer async_wait canceled"); 161*fb546105SMyung Bae return; 162*fb546105SMyung Bae } 163*fb546105SMyung Bae if (ec) 164*fb546105SMyung Bae { 165*fb546105SMyung Bae BMCWEB_LOG_CRITICAL("heartbeat timer async_wait failed: {}", ec); 166*fb546105SMyung Bae return; 167*fb546105SMyung Bae } 168*fb546105SMyung Bae 169*fb546105SMyung Bae std::shared_ptr<Subscription> self = weakSelf.lock(); 170*fb546105SMyung Bae if (!self) 171*fb546105SMyung Bae { 172*fb546105SMyung Bae BMCWEB_LOG_CRITICAL("onHbTimeout failed on Subscription"); 173*fb546105SMyung Bae return; 174*fb546105SMyung Bae } 175*fb546105SMyung Bae 176*fb546105SMyung Bae // Timer expired. 177*fb546105SMyung Bae sendHeartbeatEvent(); 178*fb546105SMyung Bae 179*fb546105SMyung Bae // reschedule heartbeat timer 180*fb546105SMyung Bae scheduleNextHeartbeatEvent(); 181*fb546105SMyung Bae } 182*fb546105SMyung Bae 18302c1e29fSAlexander Hansen bool Subscription::sendEventToSubscriber(std::string&& msg) 18402c1e29fSAlexander Hansen { 18502c1e29fSAlexander Hansen persistent_data::EventServiceConfig eventServiceConfig = 18602c1e29fSAlexander Hansen persistent_data::EventServiceStore::getInstance() 18702c1e29fSAlexander Hansen .getEventServiceConfig(); 18802c1e29fSAlexander Hansen if (!eventServiceConfig.enabled) 18902c1e29fSAlexander Hansen { 19002c1e29fSAlexander Hansen return false; 19102c1e29fSAlexander Hansen } 19202c1e29fSAlexander Hansen 19302c1e29fSAlexander Hansen if (client) 19402c1e29fSAlexander Hansen { 19502c1e29fSAlexander Hansen client->sendDataWithCallback( 19602c1e29fSAlexander Hansen std::move(msg), userSub->destinationUrl, 19702c1e29fSAlexander Hansen static_cast<ensuressl::VerifyCertificate>( 19802c1e29fSAlexander Hansen userSub->verifyCertificate), 19902c1e29fSAlexander Hansen userSub->httpHeaders, boost::beast::http::verb::post, 20002c1e29fSAlexander Hansen std::bind_front(&Subscription::resHandler, this, 20102c1e29fSAlexander Hansen shared_from_this())); 20202c1e29fSAlexander Hansen return true; 20302c1e29fSAlexander Hansen } 20402c1e29fSAlexander Hansen 20502c1e29fSAlexander Hansen if (sseConn != nullptr) 20602c1e29fSAlexander Hansen { 20702c1e29fSAlexander Hansen eventSeqNum++; 20802c1e29fSAlexander Hansen sseConn->sendSseEvent(std::to_string(eventSeqNum), msg); 20902c1e29fSAlexander Hansen } 21002c1e29fSAlexander Hansen return true; 21102c1e29fSAlexander Hansen } 21202c1e29fSAlexander Hansen 21302c1e29fSAlexander Hansen bool Subscription::sendTestEventLog() 21402c1e29fSAlexander Hansen { 21502c1e29fSAlexander Hansen nlohmann::json::array_t logEntryArray; 21602c1e29fSAlexander Hansen nlohmann::json& logEntryJson = logEntryArray.emplace_back(); 21702c1e29fSAlexander Hansen 21802c1e29fSAlexander Hansen logEntryJson["EventId"] = "TestID"; 21902c1e29fSAlexander Hansen logEntryJson["Severity"] = log_entry::EventSeverity::OK; 22002c1e29fSAlexander Hansen logEntryJson["Message"] = "Generated test event"; 22102c1e29fSAlexander Hansen logEntryJson["MessageId"] = "OpenBMC.0.2.TestEventLog"; 22202c1e29fSAlexander Hansen // MemberId is 0 : since we are sending one event record. 22302c1e29fSAlexander Hansen logEntryJson["MemberId"] = "0"; 22402c1e29fSAlexander Hansen logEntryJson["MessageArgs"] = nlohmann::json::array(); 22502c1e29fSAlexander Hansen logEntryJson["EventTimestamp"] = 22602c1e29fSAlexander Hansen redfish::time_utils::getDateTimeOffsetNow().first; 22702c1e29fSAlexander Hansen logEntryJson["Context"] = userSub->customText; 22802c1e29fSAlexander Hansen 22902c1e29fSAlexander Hansen nlohmann::json msg; 23002c1e29fSAlexander Hansen msg["@odata.type"] = "#Event.v1_4_0.Event"; 23102c1e29fSAlexander Hansen msg["Id"] = std::to_string(eventSeqNum); 23202c1e29fSAlexander Hansen msg["Name"] = "Event Log"; 23302c1e29fSAlexander Hansen msg["Events"] = logEntryArray; 23402c1e29fSAlexander Hansen 23502c1e29fSAlexander Hansen std::string strMsg = 23602c1e29fSAlexander Hansen msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 23702c1e29fSAlexander Hansen return sendEventToSubscriber(std::move(strMsg)); 23802c1e29fSAlexander Hansen } 23902c1e29fSAlexander Hansen 24002c1e29fSAlexander Hansen void Subscription::filterAndSendEventLogs( 24102c1e29fSAlexander Hansen const std::vector<EventLogObjectsType>& eventRecords) 24202c1e29fSAlexander Hansen { 24302c1e29fSAlexander Hansen nlohmann::json::array_t logEntryArray; 24402c1e29fSAlexander Hansen for (const EventLogObjectsType& logEntry : eventRecords) 24502c1e29fSAlexander Hansen { 24602c1e29fSAlexander Hansen std::vector<std::string_view> messageArgsView( 24702c1e29fSAlexander Hansen logEntry.messageArgs.begin(), logEntry.messageArgs.end()); 24802c1e29fSAlexander Hansen 24902c1e29fSAlexander Hansen nlohmann::json::object_t bmcLogEntry; 25002c1e29fSAlexander Hansen if (event_log::formatEventLogEntry( 25102c1e29fSAlexander Hansen logEntry.id, logEntry.messageId, messageArgsView, 25202c1e29fSAlexander Hansen logEntry.timestamp, userSub->customText, bmcLogEntry) != 0) 25302c1e29fSAlexander Hansen { 25402c1e29fSAlexander Hansen BMCWEB_LOG_DEBUG("Read eventLog entry failed"); 25502c1e29fSAlexander Hansen continue; 25602c1e29fSAlexander Hansen } 25702c1e29fSAlexander Hansen 25802c1e29fSAlexander Hansen if (!eventMatchesFilter(*userSub, bmcLogEntry, "")) 25902c1e29fSAlexander Hansen { 26002c1e29fSAlexander Hansen BMCWEB_LOG_DEBUG("Event {} did not match the filter", 26102c1e29fSAlexander Hansen nlohmann::json(bmcLogEntry).dump()); 26202c1e29fSAlexander Hansen continue; 26302c1e29fSAlexander Hansen } 26402c1e29fSAlexander Hansen 26502c1e29fSAlexander Hansen if (filter) 26602c1e29fSAlexander Hansen { 26702c1e29fSAlexander Hansen if (!memberMatches(bmcLogEntry, *filter)) 26802c1e29fSAlexander Hansen { 26902c1e29fSAlexander Hansen BMCWEB_LOG_DEBUG("Filter didn't match"); 27002c1e29fSAlexander Hansen continue; 27102c1e29fSAlexander Hansen } 27202c1e29fSAlexander Hansen } 27302c1e29fSAlexander Hansen 27402c1e29fSAlexander Hansen logEntryArray.emplace_back(std::move(bmcLogEntry)); 27502c1e29fSAlexander Hansen } 27602c1e29fSAlexander Hansen 27702c1e29fSAlexander Hansen if (logEntryArray.empty()) 27802c1e29fSAlexander Hansen { 27902c1e29fSAlexander Hansen BMCWEB_LOG_DEBUG("No log entries available to be transferred."); 28002c1e29fSAlexander Hansen return; 28102c1e29fSAlexander Hansen } 28202c1e29fSAlexander Hansen 28302c1e29fSAlexander Hansen nlohmann::json msg; 28402c1e29fSAlexander Hansen msg["@odata.type"] = "#Event.v1_4_0.Event"; 28502c1e29fSAlexander Hansen msg["Id"] = std::to_string(eventSeqNum); 28602c1e29fSAlexander Hansen msg["Name"] = "Event Log"; 28702c1e29fSAlexander Hansen msg["Events"] = std::move(logEntryArray); 28802c1e29fSAlexander Hansen std::string strMsg = 28902c1e29fSAlexander Hansen msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 29002c1e29fSAlexander Hansen sendEventToSubscriber(std::move(strMsg)); 29102c1e29fSAlexander Hansen eventSeqNum++; 29202c1e29fSAlexander Hansen } 29302c1e29fSAlexander Hansen 29402c1e29fSAlexander Hansen void Subscription::filterAndSendReports(const std::string& reportId, 29502c1e29fSAlexander Hansen const telemetry::TimestampReadings& var) 29602c1e29fSAlexander Hansen { 29702c1e29fSAlexander Hansen boost::urls::url mrdUri = boost::urls::format( 29802c1e29fSAlexander Hansen "/redfish/v1/TelemetryService/MetricReportDefinitions/{}", reportId); 29902c1e29fSAlexander Hansen 30002c1e29fSAlexander Hansen // Empty list means no filter. Send everything. 30102c1e29fSAlexander Hansen if (!userSub->metricReportDefinitions.empty()) 30202c1e29fSAlexander Hansen { 30302c1e29fSAlexander Hansen if (std::ranges::find(userSub->metricReportDefinitions, 30402c1e29fSAlexander Hansen mrdUri.buffer()) == 30502c1e29fSAlexander Hansen userSub->metricReportDefinitions.end()) 30602c1e29fSAlexander Hansen { 30702c1e29fSAlexander Hansen return; 30802c1e29fSAlexander Hansen } 30902c1e29fSAlexander Hansen } 31002c1e29fSAlexander Hansen 31102c1e29fSAlexander Hansen nlohmann::json msg; 31202c1e29fSAlexander Hansen if (!telemetry::fillReport(msg, reportId, var)) 31302c1e29fSAlexander Hansen { 31402c1e29fSAlexander Hansen BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus " 31502c1e29fSAlexander Hansen "Report with id {}", 31602c1e29fSAlexander Hansen reportId); 31702c1e29fSAlexander Hansen return; 31802c1e29fSAlexander Hansen } 31902c1e29fSAlexander Hansen 32002c1e29fSAlexander Hansen // Context is set by user during Event subscription and it must be 32102c1e29fSAlexander Hansen // set for MetricReport response. 32202c1e29fSAlexander Hansen if (!userSub->customText.empty()) 32302c1e29fSAlexander Hansen { 32402c1e29fSAlexander Hansen msg["Context"] = userSub->customText; 32502c1e29fSAlexander Hansen } 32602c1e29fSAlexander Hansen 32702c1e29fSAlexander Hansen std::string strMsg = 32802c1e29fSAlexander Hansen msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 32902c1e29fSAlexander Hansen sendEventToSubscriber(std::move(strMsg)); 33002c1e29fSAlexander Hansen } 33102c1e29fSAlexander Hansen 33202c1e29fSAlexander Hansen void Subscription::updateRetryConfig(uint32_t retryAttempts, 33302c1e29fSAlexander Hansen uint32_t retryTimeoutInterval) 33402c1e29fSAlexander Hansen { 33502c1e29fSAlexander Hansen if (policy == nullptr) 33602c1e29fSAlexander Hansen { 33702c1e29fSAlexander Hansen BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set"); 33802c1e29fSAlexander Hansen return; 33902c1e29fSAlexander Hansen } 34002c1e29fSAlexander Hansen policy->maxRetryAttempts = retryAttempts; 34102c1e29fSAlexander Hansen policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval); 34202c1e29fSAlexander Hansen } 34302c1e29fSAlexander Hansen 34402c1e29fSAlexander Hansen uint64_t Subscription::getEventSeqNum() const 34502c1e29fSAlexander Hansen { 34602c1e29fSAlexander Hansen return eventSeqNum; 34702c1e29fSAlexander Hansen } 34802c1e29fSAlexander Hansen 34902c1e29fSAlexander Hansen bool Subscription::matchSseId(const crow::sse_socket::Connection& thisConn) 35002c1e29fSAlexander Hansen { 35102c1e29fSAlexander Hansen return &thisConn == sseConn; 35202c1e29fSAlexander Hansen } 35302c1e29fSAlexander Hansen 35402c1e29fSAlexander Hansen // Check used to indicate what response codes are valid as part of our retry 35502c1e29fSAlexander Hansen // policy. 2XX is considered acceptable 35602c1e29fSAlexander Hansen boost::system::error_code Subscription::retryRespHandler(unsigned int respCode) 35702c1e29fSAlexander Hansen { 35802c1e29fSAlexander Hansen BMCWEB_LOG_DEBUG("Checking response code validity for SubscriptionEvent"); 35902c1e29fSAlexander Hansen if ((respCode < 200) || (respCode >= 300)) 36002c1e29fSAlexander Hansen { 36102c1e29fSAlexander Hansen return boost::system::errc::make_error_code( 36202c1e29fSAlexander Hansen boost::system::errc::result_out_of_range); 36302c1e29fSAlexander Hansen } 36402c1e29fSAlexander Hansen 36502c1e29fSAlexander Hansen // Return 0 if the response code is valid 36602c1e29fSAlexander Hansen return boost::system::errc::make_error_code(boost::system::errc::success); 36702c1e29fSAlexander Hansen } 36802c1e29fSAlexander Hansen 36902c1e29fSAlexander Hansen } // namespace redfish 370