Enable Fist Boot Set MAC Feature for Network Service

- This commit enables the Network Service to listen for
  the Added inventory objects & set the MAC address respectively
  on the ethernet interfaces on the first boot.

- An Early start to Inventory Manager service can also drop the
  Objects Added Signal, in those cases the Network Serice would
  read the Inventory Objects and sets the MAC during First Boot
  and does not register for Signal matchers to Wait for the Objects
  to be Added.

- Through this commit, Network Service can now also set the MAC
  addresses at Runtime(if the Inventory Objects are Added at Runtime).
  This would help in case of MultiNode systems with Concurrent
  maintainence.

- The Mapping of the ethernet Interfaces on Network Service and
  the Inventory objects are configured by the administator via a
  machine specific config.json file.

Tested By :

- Clear the VPD and Boot the System and observed that Network
  service registers a Signal which listens on the Object Added
  on the Inventory Manager.

- Force Collect the VPD , and that should trigger a MATCH on the
  Network service there by setting the MAC Addresses based in the
  Signals received on various interfaces.

- Once the MAC is read from the VPD and set on the repective Interface,
  a restart of Network Service should not set the MAC Address again.

Signed-off-by: Manojkiran Eda <manojkiran.eda@gmail.com>
Change-Id: If06e79e6381933e58ad552bbd7dbb61fe852384b
diff --git a/network_manager.cpp b/network_manager.cpp
index a718808..c55c9bb 100644
--- a/network_manager.cpp
+++ b/network_manager.cpp
@@ -13,6 +13,7 @@
 
 #include <algorithm>
 #include <bitset>
+#include <filesystem>
 #include <fstream>
 #include <map>
 #include <phosphor-logging/elog-errors.hpp>
@@ -23,6 +24,7 @@
 constexpr char SYSTEMD_BUSNAME[] = "org.freedesktop.systemd1";
 constexpr char SYSTEMD_PATH[] = "/org/freedesktop/systemd1";
 constexpr char SYSTEMD_INTERFACE[] = "org.freedesktop.systemd1.Manager";
+constexpr auto FirstBootFile = "/var/lib/network/firstBoot_";
 
 namespace phosphor
 {
@@ -208,6 +210,39 @@
     restartTimers();
 }
 
+#if SYNC_MAC_FROM_INVENTORY
+void Manager::setFistBootMACOnInterface(
+    const std::pair<std::string, std::string>& inventoryEthPair)
+{
+    for (const auto& interface : interfaces)
+    {
+        if (interface.first == inventoryEthPair.first)
+        {
+            auto returnMAC =
+                interface.second->mACAddress(inventoryEthPair.second);
+            if (returnMAC == inventoryEthPair.second)
+            {
+                log<level::INFO>("Set the MAC on "),
+                    entry("interface : ", interface.first.c_str()),
+                    entry("MAC : ", inventoryEthPair.second.c_str());
+                std::error_code ec;
+                if (std::filesystem::is_directory("/var/lib/network", ec))
+                {
+                    std::ofstream persistentFile(FirstBootFile +
+                                                 interface.first);
+                }
+                break;
+            }
+            else
+            {
+                log<level::INFO>("MAC is Not Set on ethernet Interface");
+            }
+        }
+    }
+}
+
+#endif
+
 void Manager::restartTimers()
 {
     using namespace std::chrono;
diff --git a/network_manager.hpp b/network_manager.hpp
index 80d017e..c9f95b3 100644
--- a/network_manager.hpp
+++ b/network_manager.hpp
@@ -112,6 +112,20 @@
     /** @brief restart the network timers. */
     void restartTimers();
 
+    /** @brief This function gets the MAC address from the VPD and
+     *  sets it on the corresponding ethernet interface during first
+     *  Boot, once it sets the MAC from VPD, it creates a file named
+     *  firstBoot under /var/lib to make sure we dont run this function
+     *  again.
+     *
+     *  @param[in] ethPair - Its a pair of ethernet interface name & the
+     * corresponding MAC Address from the VPD
+     *
+     *  return - NULL
+     */
+    void setFistBootMACOnInterface(
+        const std::pair<std::string, std::string>& ethPair);
+
     /** @brief Restart the systemd unit
      *  @param[in] unit - systemd unit name which needs to be
      *                    restarted.
diff --git a/network_manager_main.cpp b/network_manager_main.cpp
index fedb73d..eaa970a 100644
--- a/network_manager_main.cpp
+++ b/network_manager_main.cpp
@@ -7,11 +7,15 @@
 
 #include <linux/netlink.h>
 
+#include <filesystem>
+#include <fstream>
 #include <functional>
 #include <memory>
+#include <nlohmann/json.hpp>
 #include <phosphor-logging/elog-errors.hpp>
 #include <phosphor-logging/log.hpp>
 #include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
 #include <sdbusplus/server/manager.hpp>
 #include <sdeventplus/event.hpp>
 #include <xyz/openbmc_project/Common/error.hpp>
@@ -21,11 +25,20 @@
 using phosphor::logging::level;
 using phosphor::logging::log;
 using sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure;
+using DbusObjectPath = std::string;
+using DbusInterface = std::string;
+using PropertyValue = std::string;
 
 constexpr char NETWORK_CONF_DIR[] = "/etc/systemd/network";
 
 constexpr char DEFAULT_OBJPATH[] = "/xyz/openbmc_project/network";
 
+constexpr auto firstBootPath = "/var/lib/network/firstBoot_";
+constexpr auto configFile = "/usr/share/network/config.json";
+
+constexpr auto invNetworkIntf =
+    "xyz.openbmc_project.Inventory.Item.NetworkInterface";
+
 namespace phosphor
 {
 namespace network
@@ -35,6 +48,189 @@
 std::unique_ptr<Timer> refreshObjectTimer = nullptr;
 std::unique_ptr<Timer> restartTimer = nullptr;
 
+#if SYNC_MAC_FROM_INVENTORY
+std::unique_ptr<sdbusplus::bus::match::match> EthInterfaceMatch = nullptr;
+std::vector<std::string> first_boot_status;
+
+bool setInventoryMACOnSystem(sdbusplus::bus::bus& bus,
+                             const nlohmann::json& configJson,
+                             const std::string& intfname)
+{
+    try
+    {
+        auto inventoryMAC = mac_address::getfromInventory(bus, intfname);
+        if (!mac_address::toString(inventoryMAC).empty())
+        {
+            log<level::INFO>("Mac Address in Inventory on "),
+                entry("Interface : ", intfname.c_str()),
+                entry("MAC Address :",
+                      (mac_address::toString(inventoryMAC)).c_str());
+            manager->setFistBootMACOnInterface(std::make_pair(
+                intfname.c_str(), mac_address::toString(inventoryMAC)));
+            first_boot_status.push_back(intfname.c_str());
+            bool status = true;
+            for (const auto& keys : configJson.items())
+            {
+                if (!(std::find(first_boot_status.begin(),
+                                first_boot_status.end(),
+                                keys.key()) != first_boot_status.end()))
+                {
+                    log<level::INFO>("Interface MAC is NOT set from VPD"),
+                        entry("INTERFACE", keys.key().c_str());
+                    status = false;
+                }
+            }
+            if (status)
+            {
+                log<level::INFO>("Removing the match for ethernet interfaces");
+                phosphor::network::EthInterfaceMatch = nullptr;
+            }
+        }
+        else
+        {
+            log<level::INFO>("Nothing is present in Inventory");
+            return false;
+        }
+    }
+    catch (const std::exception& e)
+    {
+        log<level::ERR>("Exception occurred during getting of MAC "
+                        "address from Inventory");
+        return false;
+    }
+    return true;
+}
+
+// register the macthes to be monitored from inventory manager
+void registerSignals(sdbusplus::bus::bus& bus, const nlohmann::json& configJson)
+{
+    log<level::INFO>("Registering the Inventory Signals Matcher");
+
+    static std::unique_ptr<sdbusplus::bus::match::match> MacAddressMatch;
+
+    auto callback = [&](sdbusplus::message::message& m) {
+        std::map<DbusObjectPath,
+                 std::map<DbusInterface, std::variant<PropertyValue>>>
+            interfacesProperties;
+
+        sdbusplus::message::object_path objPath;
+        std::pair<std::string, std::string> ethPair;
+        m.read(objPath, interfacesProperties);
+
+        for (const auto& pattern : configJson.items())
+        {
+            if (objPath.str.find(pattern.value()) != std::string::npos)
+            {
+                for (auto& interface : interfacesProperties)
+                {
+                    if (interface.first == invNetworkIntf)
+                    {
+                        for (const auto& property : interface.second)
+                        {
+                            if (property.first == "MACAddress")
+                            {
+                                ethPair = std::make_pair(
+                                    pattern.key(),
+                                    std::get<std::string>(property.second));
+                                break;
+                            }
+                        }
+                        break;
+                    }
+                }
+                if (!(ethPair.first.empty() || ethPair.second.empty()))
+                {
+                    manager->setFistBootMACOnInterface(ethPair);
+                }
+            }
+        }
+    };
+
+    MacAddressMatch = std::make_unique<sdbusplus::bus::match::match>(
+        bus,
+        "interface='org.freedesktop.DBus.ObjectManager',type='signal',"
+        "member='InterfacesAdded',path='/xyz/openbmc_project/"
+        "inventory'",
+        callback);
+}
+
+void watchEthernetInterface(sdbusplus::bus::bus& bus,
+                            const nlohmann::json& configJson)
+{
+    auto mycallback = [&](sdbusplus::message::message& m) {
+        std::map<DbusObjectPath,
+                 std::map<DbusInterface, std::variant<PropertyValue>>>
+            interfacesProperties;
+
+        sdbusplus::message::object_path objPath;
+        std::pair<std::string, std::string> ethPair;
+        m.read(objPath, interfacesProperties);
+        for (const auto& interfaces : interfacesProperties)
+        {
+            if (interfaces.first ==
+                "xyz.openbmc_project.Network.EthernetInterface")
+            {
+                for (const auto& property : interfaces.second)
+                {
+                    if (property.first == "InterfaceName")
+                    {
+                        std::string infname =
+                            std::get<std::string>(property.second);
+
+                        if (configJson.find(infname) == configJson.end())
+                        {
+                            // ethernet interface not found in configJSON
+                            // check if it is not sit0 interface, as it is
+                            // expected.
+                            if (infname != "sit0")
+                            {
+                                log<level::ERR>(
+                                    "Wrong Interface Name in Config Json");
+                            }
+                        }
+                        else
+                        {
+                            if (!phosphor::network::setInventoryMACOnSystem(
+                                    bus, configJson, infname))
+                            {
+                                phosphor::network::registerSignals(bus,
+                                                                   configJson);
+                                phosphor::network::EthInterfaceMatch = nullptr;
+                            }
+                        }
+                        break;
+                    }
+                }
+                break;
+            }
+        }
+    };
+    // Incase if phosphor-inventory-manager started early and the VPD is already
+    // collected by the time network service has come up, better to check the
+    // VPD directly and set the MAC Address on the respective Interface.
+
+    bool registeredSignals = false;
+    for (const auto& interfaceString : configJson.items())
+    {
+        if (!std::filesystem::exists(firstBootPath + interfaceString.key()) &&
+            !registeredSignals)
+        {
+
+            log<level::INFO>(
+                "First boot file is not present, check VPD for MAC");
+            phosphor::network::EthInterfaceMatch = std::make_unique<
+                sdbusplus::bus::match::match>(
+                bus,
+                "interface='org.freedesktop.DBus.ObjectManager',type='signal',"
+                "member='InterfacesAdded',path='/xyz/openbmc_project/network'",
+                mycallback);
+            registeredSignals = true;
+        }
+    }
+}
+
+#endif
+
 /** @brief refresh the network objects. */
 void refreshObjects()
 {
@@ -134,5 +330,11 @@
     // RTNETLINK event handler
     phosphor::network::rtnetlink::Server svr(eventPtr, smartSock);
 
+#if SYNC_MAC_FROM_INVENTORY
+    std::ifstream in(configFile);
+    nlohmann::json configJson;
+    in >> configJson;
+    phosphor::network::watchEthernetInterface(bus, configJson);
+#endif
     sd_event_loop(eventPtr.get());
 }
diff --git a/util.cpp b/util.cpp
index fbe576d..c880fd6 100644
--- a/util.cpp
+++ b/util.cpp
@@ -12,8 +12,9 @@
 #include <cstdlib>
 #include <cstring>
 #include <experimental/filesystem>
-#include <iostream>
+#include <fstream>
 #include <list>
+#include <nlohmann/json.hpp>
 #include <phosphor-logging/elog-errors.hpp>
 #include <phosphor-logging/log.hpp>
 #include <stdexcept>
@@ -508,6 +509,7 @@
 constexpr auto mapperIntf = "xyz.openbmc_project.ObjectMapper";
 constexpr auto propIntf = "org.freedesktop.DBus.Properties";
 constexpr auto methodGet = "Get";
+constexpr auto configFile = "/usr/share/network/config.json";
 
 using DbusObjectPath = std::string;
 using DbusService = std::string;
@@ -523,6 +525,17 @@
 ether_addr getfromInventory(sdbusplus::bus::bus& bus,
                             const std::string& intfName)
 {
+
+    std::string interfaceName = intfName;
+
+#if SYNC_MAC_FROM_INVENTORY
+    // load the config JSON from the Read Only Path
+    std::ifstream in(configFile);
+    nlohmann::json configJson;
+    in >> configJson;
+    interfaceName = configJson[intfName];
+#endif
+
     std::vector<DbusInterface> interfaces;
     interfaces.emplace_back(invNetworkIntf);
 
@@ -564,9 +577,11 @@
         // interface name
         for (auto const& object : objectTree)
         {
-            log<level::INFO>("interface", entry("INT=%s", intfName.c_str()));
+            log<level::INFO>("interface",
+                             entry("INT=%s", interfaceName.c_str()));
             log<level::INFO>("object", entry("OBJ=%s", object.first.c_str()));
-            if (std::string::npos != object.first.find(intfName))
+
+            if (std::string::npos != object.first.find(interfaceName.c_str()))
             {
                 objPath = object.first;
                 service = object.second.begin()->first;
@@ -577,7 +592,7 @@
         if (objPath.empty())
         {
             log<level::ERR>("Can't find the object for the interface",
-                            entry("intfName=%s", intfName.c_str()));
+                            entry("intfName=%s", interfaceName.c_str()));
             elog<InternalFailure>();
         }
     }