Enabling Ethernet Interfaces, and Ethernet Interfaces Collection Schemas

Commit provides Schemas for:
* Single Eth Interface (GET)
* Eth Interface Collection (GET)

Both are using EthernetProvider, which delivers data
available from xyz.openbmc_project.Network

It was tested:
* Regression (no regression in web server behaviour)
* Memory Leaks (no leaks detected via valgrind)
* Compiling multiple arch (x86, ASPEED)
* Service Validator pass all available schemas

Change-Id: Idee321cb294f48b59f7740512bbe416cf10236c0
Signed-off-by: Rapkiewicz, Pawel <pawel.rapkiewicz@intel.com>
Signed-off-by: Ed Tanous <ed.tanous@intel.com>
diff --git a/redfish-core/lib/ethernet.hpp b/redfish-core/lib/ethernet.hpp
new file mode 100644
index 0000000..68e6ebb
--- /dev/null
+++ b/redfish-core/lib/ethernet.hpp
@@ -0,0 +1,497 @@
+/*
+// 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 "node.hpp"
+#include <boost/container/flat_map.hpp>
+
+namespace redfish {
+
+/**
+ * DBus types primitives for several generic DBus interfaces
+ * TODO(Pawel) consider move this to separate file into boost::dbus
+ */
+using PropertiesMapType =
+    boost::container::flat_map<std::string, dbus::dbus_variant>;
+
+using GetManagedObjectsType = boost::container::flat_map<
+    dbus::object_path,
+    boost::container::flat_map<std::string, PropertiesMapType>>;
+
+using GetAllPropertiesType = PropertiesMapType;
+
+/**
+ * Structure for keeping IPv4 data required by Redfish
+ * TODO(Pawel) consider change everything to ptr, or to non-ptr values.
+ */
+struct IPv4AddressData {
+  const std::string *address;
+  const std::string *domain;
+  const std::string *gateway;
+  std::string netmask;
+  std::string origin;
+  bool global;
+};
+
+/**
+ * Structure for keeping basic single Ethernet Interface information
+ * available from DBus
+ */
+struct EthernetInterfaceData {
+  const unsigned int *speed;
+  const bool *auto_neg;
+  const std::string *hostname;
+  const std::string *default_gateway;
+  const std::string *mac_address;
+};
+
+/**
+ * OnDemandEthernetProvider
+ * Ethernet provider class that retrieves data directly from dbus, before seting
+ * it into JSON output. This does not cache any data.
+ *
+ * TODO(Pawel)
+ * This perhaps shall be different file, which has to be chosen on compile time
+ * depending on OEM needs
+ */
+class OnDemandEthernetProvider {
+ private:
+  // Consts that may have influence on EthernetProvider performance/memory usage
+  const size_t MAX_IPV4_ADDRESSES_PER_INTERFACE = 10;
+
+  // Helper function that allows to extract GetAllPropertiesType from
+  // GetManagedObjectsType, based on object path, and interface name
+  const PropertiesMapType *extractInterfaceProperties(
+      const dbus::object_path &objpath, const std::string &interface,
+      const GetManagedObjectsType &dbus_data) {
+    const auto &dbus_obj = dbus_data.find(objpath);
+    if (dbus_obj != dbus_data.end()) {
+      const auto &iface = dbus_obj->second.find(interface);
+      if (iface != dbus_obj->second.end()) {
+        return &iface->second;
+      }
+    }
+    return nullptr;
+  }
+
+  // Helper Wrapper that does inline object_path conversion from string
+  // into dbus::object_path type
+  inline const PropertiesMapType *extractInterfaceProperties(
+      const std::string &objpath, const std::string &interface,
+      const GetManagedObjectsType &dbus_data) {
+    const auto &dbus_obj = dbus::object_path{objpath};
+    return extractInterfaceProperties(dbus_obj, interface, dbus_data);
+  }
+
+  // Helper function that allows to get pointer to the property from
+  // GetAllPropertiesType native, or extracted by GetAllPropertiesType
+  template <typename T>
+  inline const T *extractProperty(const PropertiesMapType &properties,
+                                  const std::string &name) {
+    const auto &property = properties.find(name);
+    if (property != properties.end()) {
+      return boost::get<T>(&property->second);
+    }
+    return nullptr;
+  }
+  // TODO(Pawel) Consider to move the above functions to dbus
+  // generic_interfaces.hpp
+
+  // Helper function that extracts data from several dbus objects and several
+  // interfaces required by single ethernet interface instance
+  void extractEthernetInterfaceData(const std::string &ethiface_id,
+                                    const GetManagedObjectsType &dbus_data,
+                                    EthernetInterfaceData &eth_data) {
+    // Extract data that contains MAC Address
+    const PropertiesMapType *mac_properties = extractInterfaceProperties(
+        "/xyz/openbmc_project/network/" + ethiface_id,
+        "xyz.openbmc_project.Network.MACAddress", dbus_data);
+
+    if (mac_properties != nullptr) {
+      eth_data.mac_address =
+          extractProperty<std::string>(*mac_properties, "MACAddress");
+    }
+
+    // Extract data that contains link information (auto negotiation and speed)
+    const PropertiesMapType *eth_properties = extractInterfaceProperties(
+        "/xyz/openbmc_project/network/" + ethiface_id,
+        "xyz.openbmc_project.Network.EthernetInterface", dbus_data);
+
+    if (eth_properties != nullptr) {
+      eth_data.auto_neg = extractProperty<bool>(*eth_properties, "AutoNeg");
+      eth_data.speed = extractProperty<unsigned int>(*eth_properties, "Speed");
+    }
+
+    // Extract data that contains network config (HostName and DefaultGW)
+    const PropertiesMapType *config_properties = extractInterfaceProperties(
+        "/xyz/openbmc_project/network/config",
+        "xyz.openbmc_project.Network.SystemConfiguration", dbus_data);
+
+    if (config_properties != nullptr) {
+      eth_data.hostname =
+          extractProperty<std::string>(*config_properties, "HostName");
+      eth_data.default_gateway =
+          extractProperty<std::string>(*config_properties, "DefaultGateway");
+    }
+  }
+
+  // Helper function that changes bits netmask notation (i.e. /24)
+  // into full dot notation
+  inline std::string getNetmask(unsigned int bits) {
+    uint32_t value = 0xffffffff << (32 - bits);
+    std::string netmask = std::to_string((value >> 24) & 0xff) + "." +
+                          std::to_string((value >> 16) & 0xff) + "." +
+                          std::to_string((value >> 8) & 0xff) + "." +
+                          std::to_string(value & 0xff);
+    return netmask;
+  }
+
+  // Helper function that extracts data for single ethernet ipv4 address
+  void extractIPv4Data(const std::string &ethiface_id,
+                       const GetManagedObjectsType &dbus_data,
+                       std::vector<IPv4AddressData> &ipv4_config) {
+    // Since there might be several IPv4 configurations aligned with
+    // single ethernet interface, loop over all of them
+    for (auto &objpath : dbus_data) {
+      // Check if propper patter for object path appears
+      if (boost::starts_with(
+              objpath.first.value,
+              "/xyz/openbmc_project/network/" + ethiface_id + "/ipv4/")) {
+        // and get approrpiate interface
+        const auto &interface =
+            objpath.second.find("xyz.openbmc_project.Network.IP");
+        if (interface != objpath.second.end()) {
+          // Make a properties 'shortcut', to make everything more readable
+          const PropertiesMapType &properties = interface->second;
+          // Instance IPv4AddressData structure, and set as appropriate
+          IPv4AddressData ipv4_address;
+          // IPv4 address
+          ipv4_address.address =
+              extractProperty<std::string>(properties, "Address");
+          // IPv4 gateway
+          ipv4_address.gateway =
+              extractProperty<std::string>(properties, "Gateway");
+
+          // Origin is kind of DBus object so fetch pointer...
+          const std::string *origin =
+              extractProperty<std::string>(properties, "Origin");
+          if (origin != nullptr) {
+            // ... and get everything after last dot
+            int last = origin->rfind(".");
+            if (last != std::string::npos) {
+              ipv4_address.origin = origin->substr(last + 1);
+            }
+          }
+
+          // Netmask is presented as PrefixLength
+          const auto *mask =
+              extractProperty<uint8_t>(properties, "PrefixLength");
+          if (mask != nullptr) {
+            // convert it to the string
+            ipv4_address.netmask = getNetmask(*mask);
+          }
+
+          // Attach IPv4 only if address is present
+          if (ipv4_address.address != nullptr) {
+            // Check if given addres is local, or global
+            if (boost::starts_with(*ipv4_address.address, "169.254")) {
+              ipv4_address.global = false;
+            } else {
+              ipv4_address.global = true;
+            }
+            ipv4_config.emplace_back(std::move(ipv4_address));
+          }
+        }
+      }
+    }
+  }
+
+ public:
+  /**
+   * Function that retrieves all properties for given Ethernet Interface Object
+   * from EntityManager Network Manager
+   * @param ethiface_id a eth interface id to query on DBus
+   * @param callback a function that shall be called to convert Dbus output into
+   * JSON
+   */
+  template <typename CallbackFunc>
+  void getEthernetIfaceData(const std::string &ethiface_id,
+                            CallbackFunc &&callback) {
+    crow::connections::system_bus->async_method_call(
+        [
+          this, ethiface_id{std::move(ethiface_id)},
+          callback{std::move(callback)}
+        ](const boost::system::error_code error_code,
+          const GetManagedObjectsType &resp) {
+
+          EthernetInterfaceData eth_data;
+          std::vector<IPv4AddressData> ipv4_data;
+          ipv4_data.reserve(MAX_IPV4_ADDRESSES_PER_INTERFACE);
+
+          if (error_code) {
+            // Something wrong on DBus, the error_code is not important at this
+            // moment, just return success=false, and empty output. Since size
+            // of vector may vary depending on information from Network Manager,
+            // and empty output could not be treated same way as error.
+            callback(false, eth_data, ipv4_data);
+            return;
+          }
+
+          extractEthernetInterfaceData(ethiface_id, resp, eth_data);
+          extractIPv4Data(ethiface_id, resp, ipv4_data);
+
+          // Fix global GW
+          for (IPv4AddressData &ipv4 : ipv4_data) {
+            if ((ipv4.global) &&
+                ((ipv4.gateway == nullptr) || (*ipv4.gateway == "0.0.0.0"))) {
+              ipv4.gateway = eth_data.default_gateway;
+            }
+          }
+
+          // Finally make a callback with usefull data
+          callback(true, eth_data, ipv4_data);
+        },
+        {"xyz.openbmc_project.Network", "/xyz/openbmc_project/network",
+         "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"});
+  };
+
+  /**
+   * Function that retrieves all Ethernet Interfaces available through Network
+   * Manager
+   * @param callback a function that shall be called to convert Dbus output into
+   * JSON.
+   */
+  template <typename CallbackFunc>
+  void getEthernetIfaceList(CallbackFunc &&callback) {
+    crow::connections::system_bus->async_method_call(
+        [ this, callback{std::move(callback)} ](
+            const boost::system::error_code error_code,
+            const GetManagedObjectsType &resp) {
+          // Callback requires vector<string> to retrieve all available ethernet
+          // interfaces
+          std::vector<std::string> iface_list;
+          iface_list.reserve(resp.size());
+          if (error_code) {
+            // Something wrong on DBus, the error_code is not important at this
+            // moment, just return success=false, and empty output. Since size
+            // of vector may vary depending on information from Network Manager,
+            // and empty output could not be treated same way as error.
+            callback(false, iface_list);
+            return;
+          }
+
+          // Iterate over all retrieved ObjectPaths.
+          for (auto &objpath : resp) {
+            // And all interfaces available for certain ObjectPath.
+            for (auto &interface : objpath.second) {
+              // If interface is xyz.openbmc_project.Network.EthernetInterface,
+              // this is what we're looking for.
+              if (interface.first ==
+                  "xyz.openbmc_project.Network.EthernetInterface") {
+                // Cut out everyting until last "/", ...
+                const std::string &iface_id = objpath.first.value;
+                std::size_t last_pos = iface_id.rfind("/");
+                if (last_pos != std::string::npos) {
+                  // and put it into output vector.
+                  iface_list.emplace_back(iface_id.substr(last_pos + 1));
+                }
+              }
+            }
+          }
+          // Finally make a callback with usefull data
+          callback(true, iface_list);
+        },
+        {"xyz.openbmc_project.Network", "/xyz/openbmc_project/network",
+         "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"});
+  };
+};
+
+/**
+ * EthernetCollection derived class for delivering Ethernet Collection Schema
+ */
+class EthernetCollection : public Node {
+ public:
+  template <typename CrowApp>
+  // TODO(Pawel) Remove line from below, where we assume that there is only one
+  // manager called openbmc This shall be generic, but requires to update
+  // GetSubroutes method
+  EthernetCollection(CrowApp &app)
+      : Node(app, "/redfish/v1/Managers/openbmc/EthernetInterfaces/") {
+    Node::json["@odata.type"] =
+        "#EthernetInterfaceCollection.EthernetInterfaceCollection";
+    Node::json["@odata.context"] =
+        "/redfish/v1/"
+        "$metadata#EthernetInterfaceCollection.EthernetInterfaceCollection";
+    Node::json["@odata.id"] = "/redfish/v1/Managers/openbmc/EthernetInterfaces";
+    Node::json["Name"] = "Ethernet Network Interface Collection";
+    Node::json["Description"] =
+        "Collection of EthernetInterfaces for this Manager";
+
+    entityPrivileges = {{crow::HTTPMethod::GET, {{"Login"}}},
+                        {crow::HTTPMethod::HEAD, {{"Login"}}},
+                        {crow::HTTPMethod::PATCH, {{"ConfigureComponents"}}},
+                        {crow::HTTPMethod::PUT, {{"ConfigureComponents"}}},
+                        {crow::HTTPMethod::DELETE, {{"ConfigureComponents"}}},
+                        {crow::HTTPMethod::POST, {{"ConfigureComponents"}}}};
+  }
+
+ private:
+  /**
+   * Functions triggers appropriate requests on DBus
+   */
+  void doGet(crow::response &res, const crow::request &req,
+             const std::vector<std::string> &params) override {
+    // TODO(Pawel) this shall be parametrized call to get EthernetInterfaces for
+    // any Manager, not only hardcoded 'openbmc'.
+    std::string manager_id = "openbmc";
+
+    // Get eth interface list, and call the below callback for JSON preparation
+    ethernet_provider.getEthernetIfaceList(
+        [&, manager_id{std::move(manager_id)} ](
+            const bool &success, const std::vector<std::string> &iface_list) {
+          if (success) {
+            nlohmann::json iface_array = nlohmann::json::array();
+            for (const std::string &iface_item : iface_list) {
+              iface_array.push_back(
+                  {{"@odata.id", "/redfish/v1/Managers/" + manager_id +
+                                     "/EthernetInterfaces/" + iface_item}});
+            }
+            Node::json["Members"] = iface_array;
+            Node::json["Members@odata.count"] = iface_array.size();
+            Node::json["@odata.id"] =
+                "/redfish/v1/Managers/" + manager_id + "/EthernetInterfaces";
+            res.json_value = Node::json;
+          } else {
+            // No success, best what we can do is return INTERNALL ERROR
+            res.code = static_cast<int>(HttpRespCode::INTERNAL_ERROR);
+          }
+          res.end();
+        });
+  }
+
+  // Ethernet Provider object
+  // TODO(Pawel) consider move it to singleton
+  OnDemandEthernetProvider ethernet_provider;
+};
+
+/**
+ * EthernetInterface derived class for delivering Ethernet Schema
+ */
+class EthernetInterface : public Node {
+ public:
+  /*
+   * Default Constructor
+   */
+  template <typename CrowApp>
+  // TODO(Pawel) Remove line from below, where we assume that there is only one
+  // manager called openbmc This shall be generic, but requires to update
+  // GetSubroutes method
+  EthernetInterface(CrowApp &app)
+      : Node(app, "/redfish/v1/Managers/openbmc/EthernetInterfaces/<str>/",
+             std::string()) {
+    Node::json["@odata.type"] = "#EthernetInterface.v1_2_0.EthernetInterface";
+    Node::json["@odata.context"] =
+        "/redfish/v1/$metadata#EthernetInterface.EthernetInterface";
+    Node::json["Name"] = "Manager Ethernet Interface";
+    Node::json["Description"] = "Management Network Interface";
+
+    entityPrivileges = {{crow::HTTPMethod::GET, {{"Login"}}},
+                        {crow::HTTPMethod::HEAD, {{"Login"}}},
+                        {crow::HTTPMethod::PATCH, {{"ConfigureComponents"}}},
+                        {crow::HTTPMethod::PUT, {{"ConfigureComponents"}}},
+                        {crow::HTTPMethod::DELETE, {{"ConfigureComponents"}}},
+                        {crow::HTTPMethod::POST, {{"ConfigureComponents"}}}};
+  }
+
+ private:
+  /**
+   * Functions triggers appropriate requests on DBus
+   */
+  void doGet(crow::response &res, const crow::request &req,
+             const std::vector<std::string> &params) override {
+    // TODO(Pawel) this shall be parametrized call (two params) to get
+    // EthernetInterfaces for any Manager, not only hardcoded 'openbmc'.
+    // Check if there is required param, truly entering this shall be
+    // impossible.
+    if (params.size() != 1) {
+      res.code = static_cast<int>(HttpRespCode::INTERNAL_ERROR);
+      res.end();
+      return;
+    }
+
+    const std::string &iface_id = params[0];
+
+    // Get single eth interface data, and call the below callback for JSON
+    // preparation
+    ethernet_provider.getEthernetIfaceData(
+        iface_id, [&, iface_id](const bool &success,
+                                const EthernetInterfaceData &eth_data,
+                                const std::vector<IPv4AddressData> &ipv4_data) {
+          if (success) {
+            // Copy JSON object to avoid race condition
+            nlohmann::json json_response(Node::json);
+
+            // Fill out obvious data...
+            json_response["Id"] = iface_id;
+            json_response["@odata.id"] =
+                "/redfish/v1/Managers/openbmc/EthernetInterfaces/" + iface_id;
+
+            // ... then the one from DBus, regarding eth iface...
+            if (eth_data.speed != nullptr)
+              json_response["SpeedMbps"] = *eth_data.speed;
+
+            if (eth_data.mac_address != nullptr)
+              json_response["MACAddress"] = *eth_data.mac_address;
+
+            if (eth_data.hostname != nullptr)
+              json_response["HostName"] = *eth_data.hostname;
+
+            // ... at last, check if there are IPv4 data and prepare appropriate
+            // collection
+            if (ipv4_data.size() > 0) {
+              nlohmann::json ipv4_array = nlohmann::json::array();
+              for (auto &ipv4_config : ipv4_data) {
+                nlohmann::json json_ipv4;
+                if (ipv4_config.address != nullptr) {
+                  json_ipv4["Address"] = *ipv4_config.address;
+                  if (ipv4_config.gateway != nullptr)
+                    json_ipv4["Gateway"] = *ipv4_config.gateway;
+
+                  json_ipv4["AddressOrigin"] = ipv4_config.origin;
+                  json_ipv4["SubnetMask"] = ipv4_config.netmask;
+
+                  ipv4_array.push_back(json_ipv4);
+                }
+              }
+              json_response["IPv4Addresses"] = ipv4_array;
+            }
+            res.json_value = std::move(json_response);
+          } else {
+            // ... otherwise return error
+            // TODO(Pawel)consider distinguish between non existing object, and
+            // other errors
+            res.code = static_cast<int>(HttpRespCode::NOT_FOUND);
+          }
+          res.end();
+        });
+  }
+
+  // Ethernet Provider object
+  // TODO(Pawel) consider move it to singleton
+  OnDemandEthernetProvider ethernet_provider;
+};
+
+}  // namespace redfish