1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation 4 #pragma once 5 6 #include "bmcweb_config.h" 7 8 #include "app.hpp" 9 #include "async_resp.hpp" 10 #include "dbus_singleton.hpp" 11 #include "dbus_utility.hpp" 12 #include "error_messages.hpp" 13 #include "generated/enums/resource.hpp" 14 #include "http_request.hpp" 15 #include "logging.hpp" 16 #include "privileges.hpp" 17 #include "query.hpp" 18 #include "redfish_util.hpp" 19 #include "registries/privilege_registry.hpp" 20 #include "utils/dbus_utils.hpp" 21 #include "utils/json_utils.hpp" 22 #include "utils/stl_utils.hpp" 23 24 #include <unistd.h> 25 26 #include <boost/beast/http/field.hpp> 27 #include <boost/beast/http/status.hpp> 28 #include <boost/beast/http/verb.hpp> 29 #include <boost/system/error_code.hpp> 30 #include <boost/url/format.hpp> 31 #include <sdbusplus/message/native_types.hpp> 32 33 #include <array> 34 #include <cstddef> 35 #include <functional> 36 #include <memory> 37 #include <optional> 38 #include <string> 39 #include <string_view> 40 #include <tuple> 41 #include <utility> 42 #include <variant> 43 #include <vector> 44 45 namespace redfish 46 { 47 48 void getNTPProtocolEnabled(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp); 49 std::string getHostName(); 50 51 static constexpr std::string_view sshServiceName = "dropbear"; 52 static constexpr std::string_view httpsServiceName = "bmcweb"; 53 static constexpr std::string_view ipmiServiceName = "phosphor-ipmi-net"; 54 55 // Mapping from Redfish NetworkProtocol key name to backend service that hosts 56 // that protocol. 57 static constexpr std::array<std::pair<std::string_view, std::string_view>, 3> 58 networkProtocolToDbus = {{{"SSH", sshServiceName}, 59 {"HTTPS", httpsServiceName}, 60 {"IPMI", ipmiServiceName}}}; 61 62 inline void extractNTPServersAndDomainNamesData( 63 const dbus::utility::ManagedObjectType& dbusData, 64 std::vector<std::string>& ntpData, std::vector<std::string>& dynamicNtpData, 65 std::vector<std::string>& dnData) 66 { 67 for (const auto& obj : dbusData) 68 { 69 for (const auto& ifacePair : obj.second) 70 { 71 if (ifacePair.first != 72 "xyz.openbmc_project.Network.EthernetInterface") 73 { 74 continue; 75 } 76 77 for (const auto& propertyPair : ifacePair.second) 78 { 79 if (propertyPair.first == "StaticNTPServers") 80 { 81 const std::vector<std::string>* ntpServers = 82 std::get_if<std::vector<std::string>>( 83 &propertyPair.second); 84 if (ntpServers != nullptr) 85 { 86 ntpData.insert(ntpData.end(), ntpServers->begin(), 87 ntpServers->end()); 88 } 89 } 90 else if (propertyPair.first == "NTPServers") 91 { 92 const std::vector<std::string>* dynamicNtpServers = 93 std::get_if<std::vector<std::string>>( 94 &propertyPair.second); 95 if (dynamicNtpServers != nullptr) 96 { 97 dynamicNtpData = *dynamicNtpServers; 98 } 99 } 100 else if (propertyPair.first == "DomainName") 101 { 102 const std::vector<std::string>* domainNames = 103 std::get_if<std::vector<std::string>>( 104 &propertyPair.second); 105 if (domainNames != nullptr) 106 { 107 dnData.insert(dnData.end(), domainNames->begin(), 108 domainNames->end()); 109 } 110 } 111 } 112 } 113 } 114 stl_utils::removeDuplicate(ntpData); 115 stl_utils::removeDuplicate(dnData); 116 } 117 118 template <typename CallbackFunc> 119 void getEthernetIfaceData(CallbackFunc&& callback) 120 { 121 sdbusplus::message::object_path path("/xyz/openbmc_project/network"); 122 dbus::utility::getManagedObjects( 123 "xyz.openbmc_project.Network", path, 124 [callback = std::forward<CallbackFunc>(callback)]( 125 const boost::system::error_code& ec, 126 const dbus::utility::ManagedObjectType& dbusData) { 127 std::vector<std::string> ntpServers; 128 std::vector<std::string> dynamicNtpServers; 129 std::vector<std::string> domainNames; 130 131 if (ec) 132 { 133 callback(false, ntpServers, dynamicNtpServers, domainNames); 134 return; 135 } 136 137 extractNTPServersAndDomainNamesData(dbusData, ntpServers, 138 dynamicNtpServers, domainNames); 139 140 callback(true, ntpServers, dynamicNtpServers, domainNames); 141 }); 142 } 143 144 inline void afterNetworkPortRequest( 145 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 146 const boost::system::error_code& ec, 147 const std::vector<std::tuple<std::string, std::string, bool>>& socketData) 148 { 149 if (ec) 150 { 151 messages::internalError(asyncResp->res); 152 return; 153 } 154 for (const auto& data : socketData) 155 { 156 const std::string& socketPath = get<0>(data); 157 const std::string& protocolName = get<1>(data); 158 bool isProtocolEnabled = get<2>(data); 159 160 asyncResp->res.jsonValue[protocolName]["ProtocolEnabled"] = 161 isProtocolEnabled; 162 asyncResp->res.jsonValue[protocolName]["Port"] = nullptr; 163 getPortNumber(socketPath, [asyncResp, protocolName]( 164 const boost::system::error_code& ec2, 165 int portNumber) { 166 if (ec2) 167 { 168 messages::internalError(asyncResp->res); 169 return; 170 } 171 asyncResp->res.jsonValue[protocolName]["Port"] = portNumber; 172 }); 173 } 174 } 175 176 inline void getNetworkData(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 177 const crow::Request& req) 178 { 179 if (req.session == nullptr) 180 { 181 messages::internalError(asyncResp->res); 182 return; 183 } 184 185 asyncResp->res.addHeader( 186 boost::beast::http::field::link, 187 "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/NetworkProtocol.json>; rel=describedby"); 188 asyncResp->res.jsonValue["@odata.type"] = 189 "#ManagerNetworkProtocol.v1_9_0.ManagerNetworkProtocol"; 190 asyncResp->res.jsonValue["@odata.id"] = 191 boost::urls::format("/redfish/v1/Managers/{}/NetworkProtocol", 192 BMCWEB_REDFISH_MANAGER_URI_NAME); 193 asyncResp->res.jsonValue["Id"] = "NetworkProtocol"; 194 asyncResp->res.jsonValue["Name"] = "Manager Network Protocol"; 195 asyncResp->res.jsonValue["Description"] = "Manager Network Service"; 196 asyncResp->res.jsonValue["Status"]["Health"] = resource::Health::OK; 197 asyncResp->res.jsonValue["Status"]["HealthRollup"] = resource::Health::OK; 198 asyncResp->res.jsonValue["Status"]["State"] = resource::State::Enabled; 199 200 // HTTP is Mandatory attribute as per OCP Baseline Profile - v1.0.0, 201 // but from security perspective it is not recommended to use. 202 // Hence using protocolEnabled as false to make it OCP and security-wise 203 // compliant 204 asyncResp->res.jsonValue["HTTP"]["Port"] = nullptr; 205 asyncResp->res.jsonValue["HTTP"]["ProtocolEnabled"] = false; 206 207 // The ProtocolEnabled of the following protocols is determined by 208 // inspecting the state of associated systemd sockets. If these protocols 209 // have been disabled, then the systemd socket unit files will not be found 210 // and the protocols will not be returned in this Redfish query. Set some 211 // defaults to ensure something is always returned. 212 for (const auto& nwkProtocol : networkProtocolToDbus) 213 { 214 asyncResp->res.jsonValue[nwkProtocol.first]["Port"] = nullptr; 215 asyncResp->res.jsonValue[nwkProtocol.first]["ProtocolEnabled"] = false; 216 } 217 218 std::string hostName = getHostName(); 219 220 asyncResp->res.jsonValue["HostName"] = hostName; 221 222 getNTPProtocolEnabled(asyncResp); 223 224 getEthernetIfaceData([hostName, asyncResp]( 225 const bool& success, 226 const std::vector<std::string>& ntpServers, 227 const std::vector<std::string>& dynamicNtpServers, 228 const std::vector<std::string>& domainNames) { 229 if (!success) 230 { 231 messages::resourceNotFound(asyncResp->res, "ManagerNetworkProtocol", 232 "NetworkProtocol"); 233 return; 234 } 235 asyncResp->res.jsonValue["NTP"]["NTPServers"] = ntpServers; 236 asyncResp->res.jsonValue["NTP"]["NetworkSuppliedServers"] = 237 dynamicNtpServers; 238 if (!hostName.empty()) 239 { 240 std::string fqdn = hostName; 241 if (!domainNames.empty()) 242 { 243 fqdn += "."; 244 fqdn += domainNames[0]; 245 } 246 asyncResp->res.jsonValue["FQDN"] = std::move(fqdn); 247 } 248 }); 249 250 Privileges effectiveUserPrivileges = 251 redfish::getUserPrivileges(*req.session); 252 253 // /redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates is 254 // something only ConfigureManager can access then only display when 255 // the user has permissions ConfigureManager 256 if (isOperationAllowedWithPrivileges({{"ConfigureManager"}}, 257 effectiveUserPrivileges)) 258 { 259 asyncResp->res.jsonValue["HTTPS"]["Certificates"]["@odata.id"] = 260 boost::urls::format( 261 "/redfish/v1/Managers/{}/NetworkProtocol/HTTPS/Certificates", 262 BMCWEB_REDFISH_MANAGER_URI_NAME); 263 } 264 265 getPortStatusAndPath(std::span(networkProtocolToDbus), 266 std::bind_front(afterNetworkPortRequest, asyncResp)); 267 } // namespace redfish 268 269 inline void afterSetNTP(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 270 const boost::system::error_code& ec) 271 { 272 if (ec) 273 { 274 BMCWEB_LOG_DEBUG("Failed to set elapsed time. DBUS response error {}", 275 ec); 276 messages::internalError(asyncResp->res); 277 return; 278 } 279 asyncResp->res.result(boost::beast::http::status::no_content); 280 } 281 282 inline void handleNTPProtocolEnabled( 283 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, bool ntpEnabled) 284 { 285 bool interactive = false; 286 auto callback = [asyncResp](const boost::system::error_code& ec) { 287 afterSetNTP(asyncResp, ec); 288 }; 289 crow::connections::systemBus->async_method_call( 290 std::move(callback), "org.freedesktop.timedate1", 291 "/org/freedesktop/timedate1", "org.freedesktop.timedate1", "SetNTP", 292 ntpEnabled, interactive); 293 } 294 295 // Redfish states that ip addresses can be 296 // string, to set a value 297 // null, to delete the value 298 // object_t, empty json object, to ignore the value 299 using IpAddress = 300 std::variant<std::string, nlohmann::json::object_t, std::nullptr_t>; 301 302 inline void 303 handleNTPServersPatch(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 304 const std::vector<IpAddress>& ntpServerObjects, 305 std::vector<std::string> currentNtpServers) 306 { 307 std::vector<std::string>::iterator currentNtpServer = 308 currentNtpServers.begin(); 309 for (size_t index = 0; index < ntpServerObjects.size(); index++) 310 { 311 const IpAddress& ntpServer = ntpServerObjects[index]; 312 if (std::holds_alternative<std::nullptr_t>(ntpServer)) 313 { 314 // Can't delete an item that doesn't exist 315 if (currentNtpServer == currentNtpServers.end()) 316 { 317 messages::propertyValueNotInList( 318 asyncResp->res, "null", 319 "NTP/NTPServers/" + std::to_string(index)); 320 321 return; 322 } 323 currentNtpServer = currentNtpServers.erase(currentNtpServer); 324 continue; 325 } 326 const nlohmann::json::object_t* ntpServerObject = 327 std::get_if<nlohmann::json::object_t>(&ntpServer); 328 if (ntpServerObject != nullptr) 329 { 330 if (!ntpServerObject->empty()) 331 { 332 messages::propertyValueNotInList( 333 asyncResp->res, *ntpServerObject, 334 "NTP/NTPServers/" + std::to_string(index)); 335 return; 336 } 337 // Can't retain an item that doesn't exist 338 if (currentNtpServer == currentNtpServers.end()) 339 { 340 messages::propertyValueOutOfRange( 341 asyncResp->res, *ntpServerObject, 342 "NTP/NTPServers/" + std::to_string(index)); 343 344 return; 345 } 346 // empty objects should leave the NtpServer unmodified 347 currentNtpServer++; 348 continue; 349 } 350 351 const std::string* ntpServerStr = std::get_if<std::string>(&ntpServer); 352 if (ntpServerStr == nullptr) 353 { 354 messages::internalError(asyncResp->res); 355 return; 356 } 357 if (currentNtpServer == currentNtpServers.end()) 358 { 359 // if we're at the end of the list, append to the end 360 currentNtpServers.push_back(*ntpServerStr); 361 currentNtpServer = currentNtpServers.end(); 362 continue; 363 } 364 *currentNtpServer = *ntpServerStr; 365 currentNtpServer++; 366 } 367 368 // Any remaining array elements should be removed 369 currentNtpServers.erase(currentNtpServer, currentNtpServers.end()); 370 371 constexpr std::array<std::string_view, 1> ethInterfaces = { 372 "xyz.openbmc_project.Network.EthernetInterface"}; 373 dbus::utility::getSubTree( 374 "/xyz/openbmc_project", 0, ethInterfaces, 375 [asyncResp, currentNtpServers]( 376 const boost::system::error_code& ec, 377 const dbus::utility::MapperGetSubTreeResponse& subtree) { 378 if (ec) 379 { 380 BMCWEB_LOG_WARNING("D-Bus error: {}, {}", ec, ec.message()); 381 messages::internalError(asyncResp->res); 382 return; 383 } 384 385 for (const auto& [objectPath, serviceMap] : subtree) 386 { 387 for (const auto& [service, interfaces] : serviceMap) 388 { 389 for (const auto& interface : interfaces) 390 { 391 if (interface != 392 "xyz.openbmc_project.Network.EthernetInterface") 393 { 394 continue; 395 } 396 397 setDbusProperty(asyncResp, "NTP/NTPServers/", service, 398 objectPath, interface, 399 "StaticNTPServers", currentNtpServers); 400 } 401 } 402 } 403 }); 404 } 405 406 inline void 407 handleProtocolEnabled(const bool protocolEnabled, 408 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 409 const std::string& netBasePath) 410 { 411 constexpr std::array<std::string_view, 1> interfaces = { 412 "xyz.openbmc_project.Control.Service.Attributes"}; 413 dbus::utility::getSubTree( 414 "/xyz/openbmc_project/control/service", 0, interfaces, 415 [protocolEnabled, asyncResp, 416 netBasePath](const boost::system::error_code& ec, 417 const dbus::utility::MapperGetSubTreeResponse& subtree) { 418 if (ec) 419 { 420 messages::internalError(asyncResp->res); 421 return; 422 } 423 424 for (const auto& entry : subtree) 425 { 426 if (entry.first.starts_with(netBasePath)) 427 { 428 setDbusProperty( 429 asyncResp, "IPMI/ProtocolEnabled", 430 entry.second.begin()->first, entry.first, 431 "xyz.openbmc_project.Control.Service.Attributes", 432 "Running", protocolEnabled); 433 setDbusProperty( 434 asyncResp, "IPMI/ProtocolEnabled", 435 entry.second.begin()->first, entry.first, 436 "xyz.openbmc_project.Control.Service.Attributes", 437 "Enabled", protocolEnabled); 438 } 439 } 440 }); 441 } 442 443 inline std::string getHostName() 444 { 445 std::string hostName; 446 447 std::array<char, HOST_NAME_MAX> hostNameCStr{}; 448 if (gethostname(hostNameCStr.data(), hostNameCStr.size()) == 0) 449 { 450 hostName = hostNameCStr.data(); 451 } 452 return hostName; 453 } 454 455 inline void 456 getNTPProtocolEnabled(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 457 { 458 dbus::utility::getProperty<bool>( 459 "org.freedesktop.timedate1", "/org/freedesktop/timedate1", 460 "org.freedesktop.timedate1", "NTP", 461 [asyncResp](const boost::system::error_code& ec, bool enabled) { 462 if (ec) 463 { 464 BMCWEB_LOG_WARNING( 465 "Failed to get NTP status, assuming not supported"); 466 return; 467 } 468 469 asyncResp->res.jsonValue["NTP"]["ProtocolEnabled"] = enabled; 470 }); 471 } 472 473 inline std::string encodeServiceObjectPath(std::string_view serviceName) 474 { 475 sdbusplus::message::object_path objPath( 476 "/xyz/openbmc_project/control/service"); 477 objPath /= serviceName; 478 return objPath.str; 479 } 480 481 inline void handleBmcNetworkProtocolHead( 482 crow::App& app, const crow::Request& req, 483 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 484 { 485 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 486 { 487 return; 488 } 489 asyncResp->res.addHeader( 490 boost::beast::http::field::link, 491 "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby"); 492 } 493 494 inline void handleManagersNetworkProtocolPatch( 495 App& app, const crow::Request& req, 496 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 497 const std::string& managerId) 498 { 499 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 500 { 501 return; 502 } 503 504 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 505 { 506 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 507 return; 508 } 509 510 std::optional<std::string> newHostName; 511 512 std::optional<std::vector<IpAddress>> ntpServerObjects; 513 std::optional<bool> ntpEnabled; 514 std::optional<bool> ipmiEnabled; 515 std::optional<bool> sshEnabled; 516 517 if (!json_util::readJsonPatch( 518 req, asyncResp->res, // 519 "HostName", newHostName, // 520 "NTP/NTPServers", ntpServerObjects, // 521 "NTP/ProtocolEnabled", ntpEnabled, // 522 "IPMI/ProtocolEnabled", ipmiEnabled, // 523 "SSH/ProtocolEnabled", sshEnabled // 524 )) 525 { 526 return; 527 } 528 529 asyncResp->res.result(boost::beast::http::status::no_content); 530 if (newHostName) 531 { 532 messages::propertyNotWritable(asyncResp->res, "HostName"); 533 return; 534 } 535 536 if (ntpEnabled) 537 { 538 handleNTPProtocolEnabled(asyncResp, *ntpEnabled); 539 } 540 if (ntpServerObjects) 541 { 542 getEthernetIfaceData( 543 [asyncResp, ntpServerObjects]( 544 const bool success, std::vector<std::string>& currentNtpServers, 545 const std::vector<std::string>& /*dynamicNtpServers*/, 546 const std::vector<std::string>& /*domainNames*/) { 547 if (!success) 548 { 549 messages::internalError(asyncResp->res); 550 return; 551 } 552 handleNTPServersPatch(asyncResp, *ntpServerObjects, 553 std::move(currentNtpServers)); 554 }); 555 } 556 557 if (ipmiEnabled) 558 { 559 handleProtocolEnabled( 560 *ipmiEnabled, asyncResp, 561 encodeServiceObjectPath(std::string(ipmiServiceName) + '@')); 562 } 563 564 if (sshEnabled) 565 { 566 handleProtocolEnabled(*sshEnabled, asyncResp, 567 encodeServiceObjectPath(sshServiceName)); 568 } 569 } 570 571 inline void handleManagersNetworkProtocolHead( 572 App& app, const crow::Request& req, 573 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 574 const std::string& managerId) 575 { 576 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 577 { 578 return; 579 } 580 asyncResp->res.addHeader( 581 boost::beast::http::field::link, 582 "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby"); 583 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 584 { 585 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 586 return; 587 } 588 } 589 590 inline void handleManagersNetworkProtocolGet( 591 App& app, const crow::Request& req, 592 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 593 const std::string& managerId) 594 { 595 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 596 { 597 return; 598 } 599 asyncResp->res.addHeader( 600 boost::beast::http::field::link, 601 "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby"); 602 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 603 { 604 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 605 return; 606 } 607 608 getNetworkData(asyncResp, req); 609 } 610 611 inline void requestRoutesNetworkProtocol(App& app) 612 { 613 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/NetworkProtocol/") 614 .privileges(redfish::privileges::patchManagerNetworkProtocol) 615 .methods(boost::beast::http::verb::patch)( 616 std::bind_front(handleManagersNetworkProtocolPatch, std::ref(app))); 617 618 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/NetworkProtocol/") 619 .privileges(redfish::privileges::headManagerNetworkProtocol) 620 .methods(boost::beast::http::verb::head)( 621 std::bind_front(handleManagersNetworkProtocolHead, std::ref(app))); 622 623 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/NetworkProtocol/") 624 .privileges(redfish::privileges::getManagerNetworkProtocol) 625 .methods(boost::beast::http::verb::get)( 626 std::bind_front(handleManagersNetworkProtocolGet, std::ref(app))); 627 } 628 629 } // namespace redfish 630