/* Copyright (c) 2018 Intel Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #pragma once #include "account_service.hpp" #include "app.hpp" #include "async_resp.hpp" #include "credential_pipe.hpp" #include "dbus_utility.hpp" #include "generated/enums/virtual_media.hpp" #include "query.hpp" #include "registries/privilege_registry.hpp" #include "utils/json_utils.hpp" #include #include #include #include #include #include namespace redfish { enum class VmMode { Invalid, Legacy, Proxy }; inline VmMode parseObjectPathAndGetMode( const sdbusplus::message::object_path& itemPath, const std::string& resName) { std::string thisPath = itemPath.filename(); BMCWEB_LOG_DEBUG("Filename: {}, ThisPath: {}", itemPath.str, thisPath); if (thisPath.empty()) { return VmMode::Invalid; } if (thisPath != resName) { return VmMode::Invalid; } auto mode = itemPath.parent_path(); auto type = mode.parent_path(); if (mode.filename().empty() || type.filename().empty()) { return VmMode::Invalid; } if (type.filename() != "VirtualMedia") { return VmMode::Invalid; } std::string modeStr = mode.filename(); if (modeStr == "Legacy") { return VmMode::Legacy; } if (modeStr == "Proxy") { return VmMode::Proxy; } return VmMode::Invalid; } using CheckItemHandler = std::function&, const std::pair&)>; inline void findAndParseObject(const std::string& service, const std::string& resName, const std::shared_ptr& asyncResp, CheckItemHandler&& handler) { sdbusplus::message::object_path path("/xyz/openbmc_project/VirtualMedia"); dbus::utility::getManagedObjects( service, path, [service, resName, asyncResp, handler = std::move(handler)]( const boost::system::error_code& ec, const dbus::utility::ManagedObjectType& subtree) { if (ec) { BMCWEB_LOG_DEBUG("DBUS response error"); return; } for (const auto& item : subtree) { VmMode mode = parseObjectPathAndGetMode(item.first, resName); if (mode != VmMode::Invalid) { handler(service, resName, asyncResp, item); return; } } BMCWEB_LOG_DEBUG("Parent item not found"); asyncResp->res.result(boost::beast::http::status::not_found); }); } /** * @brief Function extracts transfer protocol name from URI. */ inline std::string getTransferProtocolTypeFromUri(const std::string& imageUri) { boost::system::result url = boost::urls::parse_uri(imageUri); if (!url) { return "None"; } std::string_view scheme = url->scheme(); if (scheme == "smb") { return "CIFS"; } if (scheme == "https") { return "HTTPS"; } return "None"; } /** * @brief Read all known properties from VM object interfaces */ inline void vmParseInterfaceObject(const dbus::utility::DBusInterfacesMap& interfaces, const std::shared_ptr& asyncResp) { for (const auto& [interface, values] : interfaces) { if (interface == "xyz.openbmc_project.VirtualMedia.MountPoint") { for (const auto& [property, value] : values) { if (property == "EndpointId") { const std::string* endpointIdValue = std::get_if(&value); if (endpointIdValue == nullptr) { continue; } if (!endpointIdValue->empty()) { // Proxy mode asyncResp->res .jsonValue["Oem"]["OpenBMC"]["WebSocketEndpoint"] = *endpointIdValue; asyncResp->res.jsonValue["TransferProtocolType"] = "OEM"; } } if (property == "ImageURL") { const std::string* imageUrlValue = std::get_if(&value); if (imageUrlValue != nullptr && !imageUrlValue->empty()) { std::filesystem::path filePath = *imageUrlValue; if (!filePath.has_filename()) { // this will handle https share, which not // necessarily has to have filename given. asyncResp->res.jsonValue["ImageName"] = ""; } else { asyncResp->res.jsonValue["ImageName"] = filePath.filename(); } asyncResp->res.jsonValue["Image"] = *imageUrlValue; asyncResp->res.jsonValue["TransferProtocolType"] = getTransferProtocolTypeFromUri(*imageUrlValue); asyncResp->res.jsonValue["ConnectedVia"] = virtual_media::ConnectedVia::URI; } } if (property == "WriteProtected") { const bool* writeProtectedValue = std::get_if(&value); if (writeProtectedValue != nullptr) { asyncResp->res.jsonValue["WriteProtected"] = *writeProtectedValue; } } } } if (interface == "xyz.openbmc_project.VirtualMedia.Process") { for (const auto& [property, value] : values) { if (property == "Active") { const bool* activeValue = std::get_if(&value); if (activeValue == nullptr) { BMCWEB_LOG_DEBUG("Value Active not found"); return; } asyncResp->res.jsonValue["Inserted"] = *activeValue; if (*activeValue) { asyncResp->res.jsonValue["ConnectedVia"] = virtual_media::ConnectedVia::Applet; } } } } } } /** * @brief Fill template for Virtual Media Item. */ inline nlohmann::json vmItemTemplate(const std::string& name, const std::string& resName) { nlohmann::json item; item["@odata.id"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}", name, resName); item["@odata.type"] = "#VirtualMedia.v1_3_0.VirtualMedia"; item["Name"] = "Virtual Removable Media"; item["Id"] = resName; item["WriteProtected"] = true; item["ConnectedVia"] = virtual_media::ConnectedVia::NotConnected; item["MediaTypes"] = nlohmann::json::array_t({"CD", "USBStick"}); item["TransferMethod"] = virtual_media::TransferMethod::Stream; item["Oem"]["OpenBMC"]["@odata.type"] = "#OpenBMCVirtualMedia.v1_0_0.VirtualMedia"; item["Oem"]["OpenBMC"]["@odata.id"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}#/Oem/OpenBMC", name, resName); return item; } /** * @brief Fills collection data */ inline void getVmResourceList(std::shared_ptr asyncResp, const std::string& service, const std::string& name) { BMCWEB_LOG_DEBUG("Get available Virtual Media resources."); sdbusplus::message::object_path objPath( "/xyz/openbmc_project/VirtualMedia"); dbus::utility::getManagedObjects( service, objPath, [name, asyncResp{std::move(asyncResp)}]( const boost::system::error_code& ec, const dbus::utility::ManagedObjectType& subtree) { if (ec) { BMCWEB_LOG_DEBUG("DBUS response error"); return; } nlohmann::json& members = asyncResp->res.jsonValue["Members"]; members = nlohmann::json::array(); for (const auto& object : subtree) { nlohmann::json item; std::string path = object.first.filename(); if (path.empty()) { continue; } item["@odata.id"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}", name, path); members.emplace_back(std::move(item)); } asyncResp->res.jsonValue["Members@odata.count"] = members.size(); }); } inline void afterGetVmData(const std::string& name, const std::string& /*service*/, const std::string& resName, const std::shared_ptr& asyncResp, const std::pair& item) { VmMode mode = parseObjectPathAndGetMode(item.first, resName); if (mode == VmMode::Invalid) { return; } asyncResp->res.jsonValue = vmItemTemplate(name, resName); // Check if dbus path is Legacy type if (mode == VmMode::Legacy) { asyncResp->res.jsonValue["Actions"]["#VirtualMedia.InsertMedia"] ["target"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}/Actions/VirtualMedia.InsertMedia", name, resName); } vmParseInterfaceObject(item.second, asyncResp); asyncResp->res.jsonValue["Actions"]["#VirtualMedia.EjectMedia"] ["target"] = boost::urls::format( "/redfish/v1/Managers/{}/VirtualMedia/{}/Actions/VirtualMedia.EjectMedia", name, resName); } /** * @brief Fills data for specific resource */ inline void getVmData(const std::shared_ptr& asyncResp, const std::string& service, const std::string& name, const std::string& resName) { BMCWEB_LOG_DEBUG("Get Virtual Media resource data."); findAndParseObject(service, resName, asyncResp, std::bind_front(afterGetVmData, name)); } /** * @brief Transfer protocols supported for InsertMedia action. * */ enum class TransferProtocol { https, smb, invalid }; /** * @brief Function extracts transfer protocol type from URI. * */ inline std::optional getTransferProtocolFromUri(const boost::urls::url_view_base& imageUri) { std::string_view scheme = imageUri.scheme(); if (scheme == "smb") { return TransferProtocol::smb; } if (scheme == "https") { return TransferProtocol::https; } if (!scheme.empty()) { return TransferProtocol::invalid; } return {}; } /** * @brief Function convert transfer protocol from string param. * */ inline std::optional getTransferProtocolFromParam( const std::optional& transferProtocolType) { if (!transferProtocolType) { return {}; } if (*transferProtocolType == "CIFS") { return TransferProtocol::smb; } if (*transferProtocolType == "HTTPS") { return TransferProtocol::https; } return TransferProtocol::invalid; } /** * @brief Function extends URI with transfer protocol type. * */ inline std::string getUriWithTransferProtocol( const std::string& imageUri, const TransferProtocol& transferProtocol) { if (transferProtocol == TransferProtocol::smb) { return "smb://" + imageUri; } if (transferProtocol == TransferProtocol::https) { return "https://" + imageUri; } return imageUri; } struct InsertMediaActionParams { std::optional imageUrl; std::optional userName; std::optional password; std::optional transferMethod; std::optional transferProtocolType; std::optional writeProtected = true; std::optional inserted; }; /** * @brief Function transceives data with dbus directly. * * All BMC state properties will be retrieved before sending reset request. */ inline void doMountVmLegacy(const std::shared_ptr& asyncResp, const std::string& service, const std::string& name, const std::string& imageUrl, bool rw, std::string&& userName, std::string&& password) { int fd = -1; std::shared_ptr secretPipe; if (!userName.empty() || !password.empty()) { // Payload must contain data + NULL delimiters constexpr const size_t secretLimit = 1024; if (userName.size() + password.size() + 2 > secretLimit) { BMCWEB_LOG_ERROR("Credentials too long to handle"); messages::unrecognizedRequestBody(asyncResp->res); return; } // Open pipe secretPipe = std::make_shared( crow::connections::systemBus->get_io_context()); fd = secretPipe->releaseFd(); // Pass secret over pipe secretPipe->asyncWrite( std::move(userName), std::move(password), [asyncResp, secretPipe](const boost::system::error_code& ec, std::size_t) { if (ec) { BMCWEB_LOG_ERROR("Failed to pass secret: {}", ec); messages::internalError(asyncResp->res); } }); } dbus::utility::DbusVariantType unixFd( std::in_place_type, fd); sdbusplus::message::object_path path( "/xyz/openbmc_project/VirtualMedia/Legacy"); path /= name; crow::connections::systemBus->async_method_call( [asyncResp, secretPipe](const boost::system::error_code& ec, bool success) { if (ec) { BMCWEB_LOG_ERROR("Bad D-Bus request error: {}", ec); messages::internalError(asyncResp->res); return; } if (!success) { BMCWEB_LOG_ERROR("Service responded with error"); messages::internalError(asyncResp->res); } }, service, path.str, "xyz.openbmc_project.VirtualMedia.Legacy", "Mount", imageUrl, rw, unixFd); } /** * @brief Function validate parameters of insert media request. * */ inline void validateParams(const std::shared_ptr& asyncResp, const std::string& service, const std::string& resName, InsertMediaActionParams& actionParams) { BMCWEB_LOG_DEBUG("Validation started"); // required param imageUrl must not be empty if (!actionParams.imageUrl) { BMCWEB_LOG_ERROR("Request action parameter Image is empty."); messages::propertyValueFormatError(asyncResp->res, "", "Image"); return; } // optional param inserted must be true if (actionParams.inserted && !*actionParams.inserted) { BMCWEB_LOG_ERROR( "Request action optional parameter Inserted must be true."); messages::actionParameterNotSupported(asyncResp->res, "Inserted", "InsertMedia"); return; } // optional param transferMethod must be stream if (actionParams.transferMethod && (*actionParams.transferMethod != "Stream")) { BMCWEB_LOG_ERROR("Request action optional parameter " "TransferMethod must be Stream."); messages::actionParameterNotSupported(asyncResp->res, "TransferMethod", "InsertMedia"); return; } boost::system::result url = boost::urls::parse_uri(*actionParams.imageUrl); if (!url) { messages::actionParameterValueFormatError( asyncResp->res, *actionParams.imageUrl, "Image", "InsertMedia"); return; } std::optional uriTransferProtocolType = getTransferProtocolFromUri(*url); std::optional paramTransferProtocolType = getTransferProtocolFromParam(actionParams.transferProtocolType); // ImageUrl does not contain valid protocol type if (uriTransferProtocolType && *uriTransferProtocolType == TransferProtocol::invalid) { BMCWEB_LOG_ERROR("Request action parameter ImageUrl must " "contain specified protocol type from list: " "(smb, https)."); messages::resourceAtUriInUnknownFormat(asyncResp->res, *url); return; } // transferProtocolType should contain value from list if (paramTransferProtocolType && *paramTransferProtocolType == TransferProtocol::invalid) { BMCWEB_LOG_ERROR("Request action parameter TransferProtocolType " "must be provided with value from list: " "(CIFS, HTTPS)."); messages::propertyValueNotInList( asyncResp->res, actionParams.transferProtocolType.value_or(""), "TransferProtocolType"); return; } // valid transfer protocol not provided either with URI nor param if (!uriTransferProtocolType && !paramTransferProtocolType) { BMCWEB_LOG_ERROR("Request action parameter ImageUrl must " "contain specified protocol type or param " "TransferProtocolType must be provided."); messages::resourceAtUriInUnknownFormat(asyncResp->res, *url); return; } // valid transfer protocol provided both with URI and param if (paramTransferProtocolType && uriTransferProtocolType) { // check if protocol is the same for URI and param if (*paramTransferProtocolType != *uriTransferProtocolType) { BMCWEB_LOG_ERROR("Request action parameter " "TransferProtocolType must contain the " "same protocol type as protocol type " "provided with param imageUrl."); messages::actionParameterValueTypeError( asyncResp->res, actionParams.transferProtocolType.value_or(""), "TransferProtocolType", "InsertMedia"); return; } } // validation passed, add protocol to URI if needed if (!uriTransferProtocolType && paramTransferProtocolType) { actionParams.imageUrl = getUriWithTransferProtocol( *actionParams.imageUrl, *paramTransferProtocolType); } if (!actionParams.userName) { actionParams.userName = ""; } if (!actionParams.password) { actionParams.password = ""; } doMountVmLegacy(asyncResp, service, resName, *actionParams.imageUrl, !(actionParams.writeProtected.value_or(false)), std::move(*actionParams.userName), std::move(*actionParams.password)); } /** * @brief Function transceives data with dbus directly. * * All BMC state properties will be retrieved before sending reset request. */ inline void doEjectAction(const std::shared_ptr& asyncResp, const std::string& service, const std::string& name, bool legacy) { // Legacy mount requires parameter with image if (legacy) { crow::connections::systemBus->async_method_call( [asyncResp](const boost::system::error_code& ec) { if (ec) { BMCWEB_LOG_ERROR("Bad D-Bus request error: {}", ec); messages::internalError(asyncResp->res); return; } }, service, "/xyz/openbmc_project/VirtualMedia/Legacy/" + name, "xyz.openbmc_project.VirtualMedia.Legacy", "Unmount"); } else // proxy { crow::connections::systemBus->async_method_call( [asyncResp](const boost::system::error_code& ec) { if (ec) { BMCWEB_LOG_ERROR("Bad D-Bus request error: {}", ec); messages::internalError(asyncResp->res); return; } }, service, "/xyz/openbmc_project/VirtualMedia/Proxy/" + name, "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount"); } } inline void handleManagersVirtualMediaActionInsertPost( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& name, const std::string& resName) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } constexpr std::string_view action = "VirtualMedia.InsertMedia"; if (name != "bmc") { messages::resourceNotFound(asyncResp->res, action, resName); return; } InsertMediaActionParams actionParams; // Read obligatory parameters (url of image) if (!json_util::readJsonAction( req, asyncResp->res, "Image", actionParams.imageUrl, "WriteProtected", actionParams.writeProtected, "UserName", actionParams.userName, "Password", actionParams.password, "Inserted", actionParams.inserted, "TransferMethod", actionParams.transferMethod, "TransferProtocolType", actionParams.transferProtocolType)) { return; } dbus::utility::getDbusObject( "/xyz/openbmc_project/VirtualMedia", {}, [asyncResp, action, actionParams, resName](const boost::system::error_code& ec, const dbus::utility::MapperGetObject& getObjectType) mutable { if (ec) { BMCWEB_LOG_ERROR("ObjectMapper::GetObject call failed: {}", ec); messages::resourceNotFound(asyncResp->res, action, resName); return; } std::string service = getObjectType.begin()->first; BMCWEB_LOG_DEBUG("GetObjectType: {}", service); sdbusplus::message::object_path path( "/xyz/openbmc_project/VirtualMedia"); dbus::utility::getManagedObjects( service, path, [service, resName, action, actionParams, asyncResp]( const boost::system::error_code& ec2, const dbus::utility::ManagedObjectType& subtree) mutable { if (ec2) { // Not possible in proxy mode BMCWEB_LOG_DEBUG("InsertMedia not " "allowed in proxy mode"); messages::resourceNotFound(asyncResp->res, action, resName); return; } for (const auto& object : subtree) { VmMode mode = parseObjectPathAndGetMode(object.first, resName); if (mode == VmMode::Legacy) { validateParams(asyncResp, service, resName, actionParams); return; } } BMCWEB_LOG_DEBUG("Parent item not found"); messages::resourceNotFound(asyncResp->res, "VirtualMedia", resName); }); }); } inline void handleManagersVirtualMediaActionEject( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& managerName, const std::string& resName) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } constexpr std::string_view action = "VirtualMedia.EjectMedia"; if (managerName != "bmc") { messages::resourceNotFound(asyncResp->res, action, resName); return; } dbus::utility::getDbusObject( "/xyz/openbmc_project/VirtualMedia", {}, [asyncResp, action, resName](const boost::system::error_code& ec2, const dbus::utility::MapperGetObject& getObjectType) { if (ec2) { BMCWEB_LOG_ERROR("ObjectMapper::GetObject call failed: {}", ec2); messages::internalError(asyncResp->res); return; } std::string service = getObjectType.begin()->first; BMCWEB_LOG_DEBUG("GetObjectType: {}", service); sdbusplus::message::object_path path( "/xyz/openbmc_project/VirtualMedia"); dbus::utility::getManagedObjects( service, path, [resName, service, action, asyncResp](const boost::system::error_code& ec, const dbus::utility::ManagedObjectType& subtree) { if (ec) { BMCWEB_LOG_ERROR("ObjectMapper : No Service found"); messages::resourceNotFound(asyncResp->res, action, resName); return; } for (const auto& object : subtree) { VmMode mode = parseObjectPathAndGetMode(object.first, resName); if (mode != VmMode::Invalid) { doEjectAction(asyncResp, service, resName, mode == VmMode::Legacy); return; } } BMCWEB_LOG_DEBUG("Parent item not found"); messages::resourceNotFound(asyncResp->res, "VirtualMedia", resName); }); }); } inline void handleManagersVirtualMediaCollectionGet( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& name) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } if (name != "bmc") { messages::resourceNotFound(asyncResp->res, "VirtualMedia", name); return; } asyncResp->res.jsonValue["@odata.type"] = "#VirtualMediaCollection.VirtualMediaCollection"; asyncResp->res.jsonValue["Name"] = "Virtual Media Services"; asyncResp->res.jsonValue["@odata.id"] = boost::urls::format("/redfish/v1/Managers/{}/VirtualMedia", name); dbus::utility::getDbusObject( "/xyz/openbmc_project/VirtualMedia", {}, [asyncResp, name](const boost::system::error_code& ec, const dbus::utility::MapperGetObject& getObjectType) { if (ec) { BMCWEB_LOG_ERROR("ObjectMapper::GetObject call failed: {}", ec); messages::internalError(asyncResp->res); return; } std::string service = getObjectType.begin()->first; BMCWEB_LOG_DEBUG("GetObjectType: {}", service); getVmResourceList(asyncResp, service, name); }); } inline void handleVirtualMediaGet(crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& name, const std::string& resName) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } if (name != "bmc") { messages::resourceNotFound(asyncResp->res, "VirtualMedia", resName); return; } dbus::utility::getDbusObject( "/xyz/openbmc_project/VirtualMedia", {}, [asyncResp, name, resName](const boost::system::error_code& ec, const dbus::utility::MapperGetObject& getObjectType) { if (ec) { BMCWEB_LOG_ERROR("ObjectMapper::GetObject call failed: {}", ec); messages::internalError(asyncResp->res); return; } std::string service = getObjectType.begin()->first; BMCWEB_LOG_DEBUG("GetObjectType: {}", service); getVmData(asyncResp, service, name, resName); }); } inline void requestNBDVirtualMediaRoutes(App& app) { BMCWEB_ROUTE( app, "/redfish/v1/Managers//VirtualMedia//Actions/VirtualMedia.InsertMedia") .privileges(redfish::privileges::postVirtualMedia) .methods(boost::beast::http::verb::post)(std::bind_front( handleManagersVirtualMediaActionInsertPost, std::ref(app))); BMCWEB_ROUTE( app, "/redfish/v1/Managers//VirtualMedia//Actions/VirtualMedia.EjectMedia") .privileges(redfish::privileges::postVirtualMedia) .methods(boost::beast::http::verb::post)(std::bind_front( handleManagersVirtualMediaActionEject, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/Managers//VirtualMedia/") .privileges(redfish::privileges::getVirtualMediaCollection) .methods(boost::beast::http::verb::get)(std::bind_front( handleManagersVirtualMediaCollectionGet, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/Managers//VirtualMedia//") .privileges(redfish::privileges::getVirtualMedia) .methods(boost::beast::http::verb::get)( std::bind_front(handleVirtualMediaGet, std::ref(app))); } } // namespace redfish