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