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