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 "health.hpp" 19 #include "led.hpp" 20 21 #include <app.hpp> 22 #include <boost/container/flat_map.hpp> 23 #include <dbus_utility.hpp> 24 #include <registries/privilege_registry.hpp> 25 #include <sdbusplus/asio/property.hpp> 26 #include <utils/collection.hpp> 27 28 namespace redfish 29 { 30 31 /** 32 * @brief Retrieves chassis state properties over dbus 33 * 34 * @param[in] aResp - Shared pointer for completing asynchronous calls. 35 * 36 * @return None. 37 */ 38 inline void getChassisState(std::shared_ptr<bmcweb::AsyncResp> aResp) 39 { 40 // crow::connections::systemBus->async_method_call( 41 sdbusplus::asio::getProperty<std::string>( 42 *crow::connections::systemBus, "xyz.openbmc_project.State.Chassis", 43 "/xyz/openbmc_project/state/chassis0", 44 "xyz.openbmc_project.State.Chassis", "CurrentPowerState", 45 [aResp{std::move(aResp)}](const boost::system::error_code ec, 46 const std::string& chassisState) { 47 if (ec) 48 { 49 BMCWEB_LOG_DEBUG << "DBUS response error " << ec; 50 messages::internalError(aResp->res); 51 return; 52 } 53 54 BMCWEB_LOG_DEBUG << "Chassis state: " << chassisState; 55 // Verify Chassis State 56 if (chassisState == 57 "xyz.openbmc_project.State.Chassis.PowerState.On") 58 { 59 aResp->res.jsonValue["PowerState"] = "On"; 60 aResp->res.jsonValue["Status"]["State"] = "Enabled"; 61 } 62 else if (chassisState == 63 "xyz.openbmc_project.State.Chassis.PowerState.Off") 64 { 65 aResp->res.jsonValue["PowerState"] = "Off"; 66 aResp->res.jsonValue["Status"]["State"] = "StandbyOffline"; 67 } 68 }); 69 } 70 71 /** 72 * DBus types primitives for several generic DBus interfaces 73 * TODO(Pawel) consider move this to separate file into boost::dbus 74 */ 75 using ManagedObjectsType = std::vector<std::pair< 76 sdbusplus::message::object_path, 77 std::vector<std::pair< 78 std::string, 79 std::vector<std::pair<std::string, dbus::utility::DbusVariantType>>>>>>; 80 81 using PropertiesType = 82 boost::container::flat_map<std::string, dbus::utility::DbusVariantType>; 83 84 inline void getIntrusionByService(std::shared_ptr<bmcweb::AsyncResp> aResp, 85 const std::string& service, 86 const std::string& objPath) 87 { 88 BMCWEB_LOG_DEBUG << "Get intrusion status by service \n"; 89 90 sdbusplus::asio::getProperty<std::string>( 91 *crow::connections::systemBus, service, objPath, 92 "xyz.openbmc_project.Chassis.Intrusion", "Status", 93 [aResp{std::move(aResp)}](const boost::system::error_code ec, 94 const std::string& value) { 95 if (ec) 96 { 97 // do not add err msg in redfish response, because this is not 98 // mandatory property 99 BMCWEB_LOG_ERROR << "DBUS response error " << ec << "\n"; 100 return; 101 } 102 103 aResp->res.jsonValue["PhysicalSecurity"] = { 104 {"IntrusionSensorNumber", 1}, {"IntrusionSensor", value}}; 105 }); 106 } 107 108 /** 109 * Retrieves physical security properties over dbus 110 */ 111 inline void getPhysicalSecurityData(std::shared_ptr<bmcweb::AsyncResp> aResp) 112 { 113 crow::connections::systemBus->async_method_call( 114 [aResp{std::move(aResp)}]( 115 const boost::system::error_code ec, 116 const std::vector<std::pair< 117 std::string, 118 std::vector<std::pair<std::string, std::vector<std::string>>>>>& 119 subtree) { 120 if (ec) 121 { 122 // do not add err msg in redfish response, because this is not 123 // mandatory property 124 BMCWEB_LOG_INFO << "DBUS error: no matched iface " << ec 125 << "\n"; 126 return; 127 } 128 // Iterate over all retrieved ObjectPaths. 129 for (const auto& object : subtree) 130 { 131 for (const auto& service : object.second) 132 { 133 getIntrusionByService(aResp, service.first, object.first); 134 return; 135 } 136 } 137 }, 138 "xyz.openbmc_project.ObjectMapper", 139 "/xyz/openbmc_project/object_mapper", 140 "xyz.openbmc_project.ObjectMapper", "GetSubTree", 141 "/xyz/openbmc_project/Intrusion", 1, 142 std::array<const char*, 1>{"xyz.openbmc_project.Chassis.Intrusion"}); 143 } 144 145 /** 146 * ChassisCollection derived class for delivering Chassis Collection Schema 147 * Functions triggers appropriate requests on DBus 148 */ 149 inline void requestRoutesChassisCollection(App& app) 150 { 151 BMCWEB_ROUTE(app, "/redfish/v1/Chassis/") 152 .privileges(redfish::privileges::getChassisCollection) 153 .methods(boost::beast::http::verb::get)( 154 [](const crow::Request&, 155 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 156 asyncResp->res.jsonValue["@odata.type"] = 157 "#ChassisCollection.ChassisCollection"; 158 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/Chassis"; 159 asyncResp->res.jsonValue["Name"] = "Chassis Collection"; 160 161 collection_util::getCollectionMembers( 162 asyncResp, "/redfish/v1/Chassis", 163 {"xyz.openbmc_project.Inventory.Item.Board", 164 "xyz.openbmc_project.Inventory.Item.Chassis"}); 165 }); 166 } 167 168 inline void 169 getChassisLocationCode(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 170 const std::string& connectionName, 171 const std::string& path) 172 { 173 sdbusplus::asio::getProperty<std::string>( 174 *crow::connections::systemBus, connectionName, path, 175 "xyz.openbmc_project.Inventory.Decorator.LocationCode", "LocationCode", 176 [asyncResp](const boost::system::error_code ec, 177 const std::string& property) { 178 if (ec) 179 { 180 BMCWEB_LOG_DEBUG << "DBUS response error for Location"; 181 messages::internalError(asyncResp->res); 182 return; 183 } 184 185 asyncResp->res 186 .jsonValue["Location"]["PartLocation"]["ServiceLabel"] = 187 property; 188 }); 189 } 190 191 inline void getChassisUUID(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 192 const std::string& connectionName, 193 const std::string& path) 194 { 195 sdbusplus::asio::getProperty<std::string>( 196 *crow::connections::systemBus, connectionName, path, 197 "xyz.openbmc_project.Common.UUID", "UUID", 198 [asyncResp](const boost::system::error_code ec, 199 const std::string& chassisUUID) { 200 if (ec) 201 { 202 BMCWEB_LOG_DEBUG << "DBUS response error for UUID"; 203 messages::internalError(asyncResp->res); 204 return; 205 } 206 asyncResp->res.jsonValue["UUID"] = chassisUUID; 207 }); 208 } 209 210 /** 211 * Chassis override class for delivering Chassis Schema 212 * Functions triggers appropriate requests on DBus 213 */ 214 inline void requestRoutesChassis(App& app) 215 { 216 BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/") 217 .privileges(redfish::privileges::getChassis) 218 .methods( 219 boost::beast::http::verb::get)([](const crow::Request&, 220 const std::shared_ptr< 221 bmcweb::AsyncResp>& asyncResp, 222 const std::string& chassisId) { 223 const std::array<const char*, 2> interfaces = { 224 "xyz.openbmc_project.Inventory.Item.Board", 225 "xyz.openbmc_project.Inventory.Item.Chassis"}; 226 227 crow::connections::systemBus->async_method_call( 228 [asyncResp, chassisId(std::string(chassisId))]( 229 const boost::system::error_code ec, 230 const crow::openbmc_mapper::GetSubTreeType& subtree) { 231 if (ec) 232 { 233 messages::internalError(asyncResp->res); 234 return; 235 } 236 // Iterate over all retrieved ObjectPaths. 237 for (const std::pair< 238 std::string, 239 std::vector<std::pair<std::string, 240 std::vector<std::string>>>>& 241 object : subtree) 242 { 243 const std::string& path = object.first; 244 const std::vector< 245 std::pair<std::string, std::vector<std::string>>>& 246 connectionNames = object.second; 247 248 sdbusplus::message::object_path objPath(path); 249 if (objPath.filename() != chassisId) 250 { 251 continue; 252 } 253 254 auto health = 255 std::make_shared<HealthPopulate>(asyncResp); 256 257 sdbusplus::asio::getProperty<std::vector<std::string>>( 258 *crow::connections::systemBus, 259 "xyz.openbmc_project.ObjectMapper", 260 path + "/all_sensors", 261 "xyz.openbmc_project.Association", "endpoints", 262 [health](const boost::system::error_code ec2, 263 const std::vector<std::string>& resp) { 264 if (ec2) 265 { 266 return; // no sensors = no failures 267 } 268 health->inventory = resp; 269 }); 270 271 health->populate(); 272 273 if (connectionNames.empty()) 274 { 275 BMCWEB_LOG_ERROR << "Got 0 Connection names"; 276 continue; 277 } 278 279 asyncResp->res.jsonValue["@odata.type"] = 280 "#Chassis.v1_16_0.Chassis"; 281 asyncResp->res.jsonValue["@odata.id"] = 282 "/redfish/v1/Chassis/" + chassisId; 283 asyncResp->res.jsonValue["Name"] = "Chassis Collection"; 284 asyncResp->res.jsonValue["ChassisType"] = "RackMount"; 285 asyncResp->res.jsonValue["Actions"]["#Chassis.Reset"] = 286 {{"target", "/redfish/v1/Chassis/" + chassisId + 287 "/Actions/Chassis.Reset"}, 288 {"@Redfish.ActionInfo", "/redfish/v1/Chassis/" + 289 chassisId + 290 "/ResetActionInfo"}}; 291 asyncResp->res.jsonValue["PCIeDevices"] = { 292 {"@odata.id", 293 "/redfish/v1/Systems/system/PCIeDevices"}}; 294 295 const std::string& connectionName = 296 connectionNames[0].first; 297 298 const std::vector<std::string>& interfaces2 = 299 connectionNames[0].second; 300 const std::array<const char*, 2> hasIndicatorLed = { 301 "xyz.openbmc_project.Inventory.Item.Panel", 302 "xyz.openbmc_project.Inventory.Item.Board.Motherboard"}; 303 304 const std::string assetTagInterface = 305 "xyz.openbmc_project.Inventory.Decorator.AssetTag"; 306 if (std::find(interfaces2.begin(), interfaces2.end(), 307 assetTagInterface) != interfaces2.end()) 308 { 309 sdbusplus::asio::getProperty<std::string>( 310 *crow::connections::systemBus, connectionName, 311 path, assetTagInterface, "AssetTag", 312 [asyncResp, chassisId(std::string(chassisId))]( 313 const boost::system::error_code ec, 314 const std::string& property) { 315 if (ec) 316 { 317 BMCWEB_LOG_DEBUG 318 << "DBus response error for AssetTag"; 319 messages::internalError(asyncResp->res); 320 return; 321 } 322 asyncResp->res.jsonValue["AssetTag"] = 323 property; 324 }); 325 } 326 327 for (const char* interface : hasIndicatorLed) 328 { 329 if (std::find(interfaces2.begin(), 330 interfaces2.end(), 331 interface) != interfaces2.end()) 332 { 333 getIndicatorLedState(asyncResp); 334 getLocationIndicatorActive(asyncResp); 335 break; 336 } 337 } 338 339 crow::connections::systemBus->async_method_call( 340 [asyncResp, chassisId(std::string(chassisId))]( 341 const boost::system::error_code /*ec2*/, 342 const std::vector< 343 std::pair<std::string, 344 dbus::utility::DbusVariantType>>& 345 propertiesList) { 346 for (const std::pair< 347 std::string, 348 dbus::utility::DbusVariantType>& 349 property : propertiesList) 350 { 351 // Store DBus properties that are also 352 // Redfish properties with same name and a 353 // string value 354 const std::string& propertyName = 355 property.first; 356 if ((propertyName == "PartNumber") || 357 (propertyName == "SerialNumber") || 358 (propertyName == "Manufacturer") || 359 (propertyName == "Model") || 360 (propertyName == "SparePartNumber")) 361 { 362 const std::string* value = 363 std::get_if<std::string>( 364 &property.second); 365 if (value == nullptr) 366 { 367 BMCWEB_LOG_ERROR 368 << "Null value returned for " 369 << propertyName; 370 messages::internalError( 371 asyncResp->res); 372 return; 373 } 374 // SparePartNumber is optional on D-Bus 375 // so skip if it is empty 376 if (propertyName == "SparePartNumber") 377 { 378 if (value->empty()) 379 { 380 continue; 381 } 382 } 383 asyncResp->res.jsonValue[propertyName] = 384 *value; 385 } 386 } 387 asyncResp->res.jsonValue["Name"] = chassisId; 388 asyncResp->res.jsonValue["Id"] = chassisId; 389 #ifdef BMCWEB_ALLOW_DEPRECATED_POWER_THERMAL 390 asyncResp->res.jsonValue["Thermal"] = { 391 {"@odata.id", "/redfish/v1/Chassis/" + 392 chassisId + "/Thermal"}}; 393 // Power object 394 asyncResp->res.jsonValue["Power"] = { 395 {"@odata.id", "/redfish/v1/Chassis/" + 396 chassisId + "/Power"}}; 397 #endif 398 // SensorCollection 399 asyncResp->res.jsonValue["Sensors"] = { 400 {"@odata.id", "/redfish/v1/Chassis/" + 401 chassisId + "/Sensors"}}; 402 asyncResp->res.jsonValue["Status"] = { 403 {"State", "Enabled"}, 404 }; 405 406 asyncResp->res 407 .jsonValue["Links"]["ComputerSystems"] = { 408 {{"@odata.id", 409 "/redfish/v1/Systems/system"}}}; 410 asyncResp->res.jsonValue["Links"]["ManagedBy"] = 411 {{{"@odata.id", 412 "/redfish/v1/Managers/bmc"}}}; 413 getChassisState(asyncResp); 414 }, 415 connectionName, path, 416 "org.freedesktop.DBus.Properties", "GetAll", 417 "xyz.openbmc_project.Inventory.Decorator.Asset"); 418 419 for (const auto& interface : interfaces2) 420 { 421 if (interface == "xyz.openbmc_project.Common.UUID") 422 { 423 getChassisUUID(asyncResp, connectionName, path); 424 } 425 else if ( 426 interface == 427 "xyz.openbmc_project.Inventory.Decorator.LocationCode") 428 { 429 getChassisLocationCode(asyncResp, 430 connectionName, path); 431 } 432 } 433 434 return; 435 } 436 437 // Couldn't find an object with that name. return an error 438 messages::resourceNotFound( 439 asyncResp->res, "#Chassis.v1_16_0.Chassis", chassisId); 440 }, 441 "xyz.openbmc_project.ObjectMapper", 442 "/xyz/openbmc_project/object_mapper", 443 "xyz.openbmc_project.ObjectMapper", "GetSubTree", 444 "/xyz/openbmc_project/inventory", 0, interfaces); 445 446 getPhysicalSecurityData(asyncResp); 447 }); 448 449 BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/") 450 .privileges(redfish::privileges::patchChassis) 451 .methods( 452 boost::beast::http::verb:: 453 patch)([](const crow::Request& req, 454 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 455 const std::string& param) { 456 std::optional<bool> locationIndicatorActive; 457 std::optional<std::string> indicatorLed; 458 459 if (param.empty()) 460 { 461 return; 462 } 463 464 if (!json_util::readJson( 465 req, asyncResp->res, "LocationIndicatorActive", 466 locationIndicatorActive, "IndicatorLED", indicatorLed)) 467 { 468 return; 469 } 470 471 // TODO (Gunnar): Remove IndicatorLED after enough time has passed 472 if (!locationIndicatorActive && !indicatorLed) 473 { 474 return; // delete this when we support more patch properties 475 } 476 if (indicatorLed) 477 { 478 asyncResp->res.addHeader( 479 boost::beast::http::field::warning, 480 "299 - \"IndicatorLED is deprecated. Use LocationIndicatorActive instead.\""); 481 } 482 483 const std::array<const char*, 2> interfaces = { 484 "xyz.openbmc_project.Inventory.Item.Board", 485 "xyz.openbmc_project.Inventory.Item.Chassis"}; 486 487 const std::string& chassisId = param; 488 489 crow::connections::systemBus->async_method_call( 490 [asyncResp, chassisId, locationIndicatorActive, indicatorLed]( 491 const boost::system::error_code ec, 492 const crow::openbmc_mapper::GetSubTreeType& subtree) { 493 if (ec) 494 { 495 messages::internalError(asyncResp->res); 496 return; 497 } 498 499 // Iterate over all retrieved ObjectPaths. 500 for (const std::pair< 501 std::string, 502 std::vector<std::pair<std::string, 503 std::vector<std::string>>>>& 504 object : subtree) 505 { 506 const std::string& path = object.first; 507 const std::vector< 508 std::pair<std::string, std::vector<std::string>>>& 509 connectionNames = object.second; 510 511 sdbusplus::message::object_path objPath(path); 512 if (objPath.filename() != chassisId) 513 { 514 continue; 515 } 516 517 if (connectionNames.empty()) 518 { 519 BMCWEB_LOG_ERROR << "Got 0 Connection names"; 520 continue; 521 } 522 523 const std::vector<std::string>& interfaces3 = 524 connectionNames[0].second; 525 526 const std::array<const char*, 2> hasIndicatorLed = { 527 "xyz.openbmc_project.Inventory.Item.Panel", 528 "xyz.openbmc_project.Inventory.Item.Board.Motherboard"}; 529 bool indicatorChassis = false; 530 for (const char* interface : hasIndicatorLed) 531 { 532 if (std::find(interfaces3.begin(), 533 interfaces3.end(), 534 interface) != interfaces3.end()) 535 { 536 indicatorChassis = true; 537 break; 538 } 539 } 540 if (locationIndicatorActive) 541 { 542 if (indicatorChassis) 543 { 544 setLocationIndicatorActive( 545 asyncResp, *locationIndicatorActive); 546 } 547 else 548 { 549 messages::propertyUnknown( 550 asyncResp->res, "LocationIndicatorActive"); 551 } 552 } 553 if (indicatorLed) 554 { 555 if (indicatorChassis) 556 { 557 setIndicatorLedState(asyncResp, *indicatorLed); 558 } 559 else 560 { 561 messages::propertyUnknown(asyncResp->res, 562 "IndicatorLED"); 563 } 564 } 565 return; 566 } 567 568 messages::resourceNotFound( 569 asyncResp->res, "#Chassis.v1_14_0.Chassis", chassisId); 570 }, 571 "xyz.openbmc_project.ObjectMapper", 572 "/xyz/openbmc_project/object_mapper", 573 "xyz.openbmc_project.ObjectMapper", "GetSubTree", 574 "/xyz/openbmc_project/inventory", 0, interfaces); 575 }); 576 } 577 578 inline void 579 doChassisPowerCycle(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 580 { 581 const char* busName = "xyz.openbmc_project.ObjectMapper"; 582 const char* path = "/xyz/openbmc_project/object_mapper"; 583 const char* interface = "xyz.openbmc_project.ObjectMapper"; 584 const char* method = "GetSubTreePaths"; 585 586 const std::array<const char*, 1> interfaces = { 587 "xyz.openbmc_project.State.Chassis"}; 588 589 // Use mapper to get subtree paths. 590 crow::connections::systemBus->async_method_call( 591 [asyncResp](const boost::system::error_code ec, 592 const std::vector<std::string>& chassisList) { 593 if (ec) 594 { 595 BMCWEB_LOG_DEBUG << "[mapper] Bad D-Bus request error: " << ec; 596 messages::internalError(asyncResp->res); 597 return; 598 } 599 600 const char* processName = "xyz.openbmc_project.State.Chassis"; 601 const char* interfaceName = "xyz.openbmc_project.State.Chassis"; 602 const char* destProperty = "RequestedPowerTransition"; 603 const std::string propertyValue = 604 "xyz.openbmc_project.State.Chassis.Transition.PowerCycle"; 605 std::string objectPath = 606 "/xyz/openbmc_project/state/chassis_system0"; 607 608 /* Look for system reset chassis path */ 609 if ((std::find(chassisList.begin(), chassisList.end(), 610 objectPath)) == chassisList.end()) 611 { 612 /* We prefer to reset the full chassis_system, but if it doesn't 613 * exist on some platforms, fall back to a host-only power reset 614 */ 615 objectPath = "/xyz/openbmc_project/state/chassis0"; 616 } 617 618 crow::connections::systemBus->async_method_call( 619 [asyncResp](const boost::system::error_code ec) { 620 // Use "Set" method to set the property value. 621 if (ec) 622 { 623 BMCWEB_LOG_DEBUG << "[Set] Bad D-Bus request error: " 624 << ec; 625 messages::internalError(asyncResp->res); 626 return; 627 } 628 629 messages::success(asyncResp->res); 630 }, 631 processName, objectPath, "org.freedesktop.DBus.Properties", 632 "Set", interfaceName, destProperty, 633 dbus::utility::DbusVariantType{propertyValue}); 634 }, 635 busName, path, interface, method, "/", 0, interfaces); 636 } 637 638 /** 639 * ChassisResetAction class supports the POST method for the Reset 640 * action. 641 * Function handles POST method request. 642 * Analyzes POST body before sending Reset request data to D-Bus. 643 */ 644 645 inline void requestRoutesChassisResetAction(App& app) 646 { 647 BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/Actions/Chassis.Reset/") 648 .privileges(redfish::privileges::postChassis) 649 .methods(boost::beast::http::verb::post)( 650 [](const crow::Request& req, 651 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 652 const std::string&) { 653 BMCWEB_LOG_DEBUG << "Post Chassis Reset."; 654 655 std::string resetType; 656 657 if (!json_util::readJson(req, asyncResp->res, "ResetType", 658 resetType)) 659 { 660 return; 661 } 662 663 if (resetType != "PowerCycle") 664 { 665 BMCWEB_LOG_DEBUG << "Invalid property value for ResetType: " 666 << resetType; 667 messages::actionParameterNotSupported( 668 asyncResp->res, resetType, "ResetType"); 669 670 return; 671 } 672 doChassisPowerCycle(asyncResp); 673 }); 674 } 675 676 /** 677 * ChassisResetActionInfo derived class for delivering Chassis 678 * ResetType AllowableValues using ResetInfo schema. 679 */ 680 inline void requestRoutesChassisResetActionInfo(App& app) 681 { 682 BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ResetActionInfo/") 683 .privileges(redfish::privileges::getActionInfo) 684 .methods(boost::beast::http::verb::get)( 685 [](const crow::Request&, 686 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 687 const std::string& chassisId) 688 689 { 690 asyncResp->res.jsonValue = { 691 {"@odata.type", "#ActionInfo.v1_1_2.ActionInfo"}, 692 {"@odata.id", 693 "/redfish/v1/Chassis/" + chassisId + "/ResetActionInfo"}, 694 {"Name", "Reset Action Info"}, 695 {"Id", "ResetActionInfo"}, 696 {"Parameters", 697 {{{"Name", "ResetType"}, 698 {"Required", true}, 699 {"DataType", "String"}, 700 {"AllowableValues", {"PowerCycle"}}}}}}; 701 }); 702 } 703 704 } // namespace redfish 705