network_manager: Async query all interface states

This only queries the state during startup and relies on matches to keep
up with changes from the systemd-networkd daemon.

Change-Id: I30776d443c39a52a91826ad1b515da60c7ecf641
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/src/ethernet_interface.cpp b/src/ethernet_interface.cpp
index 4f71a46..0408219 100644
--- a/src/ethernet_interface.cpp
+++ b/src/ethernet_interface.cpp
@@ -18,7 +18,6 @@
 #include <filesystem>
 #include <phosphor-logging/elog-errors.hpp>
 #include <phosphor-logging/log.hpp>
-#include <sdbusplus/bus/match.hpp>
 #include <stdplus/raw.hpp>
 #include <stdplus/zstring.hpp>
 #include <string>
@@ -76,8 +75,7 @@
                                      const InterfaceInfo& info,
                                      std::string_view objRoot,
                                      const config::Parser& config,
-                                     bool emitSignal,
-                                     std::optional<bool> enabled) :
+                                     bool emitSignal, bool enabled) :
     EthernetInterface(bus, manager, info, makeObjPath(objRoot, *info.name),
                       config, emitSignal, enabled)
 {
@@ -87,8 +85,7 @@
                                      const InterfaceInfo& info,
                                      std::string&& objPath,
                                      const config::Parser& config,
-                                     bool emitSignal,
-                                     std::optional<bool> enabled) :
+                                     bool emitSignal, bool enabled) :
     Ifaces(bus, objPath.c_str(),
            emitSignal ? Ifaces::action::defer_emit
                       : Ifaces::action::emit_no_signals),
@@ -99,7 +96,7 @@
     EthernetInterfaceIntf::dhcp4(dhcpVal.v4);
     EthernetInterfaceIntf::dhcp6(dhcpVal.v6);
     EthernetInterfaceIntf::ipv6AcceptRA(getIPv6AcceptRA(config));
-    EthernetInterfaceIntf::nicEnabled(enabled ? *enabled : queryNicEnabled());
+    EthernetInterfaceIntf::nicEnabled(enabled);
     {
         const auto& gws = manager.getRouteTable().getDefaultGateway();
         auto it = gws.find(ifIdx);
@@ -435,91 +432,6 @@
     }));
 }
 
-bool EthernetInterface::queryNicEnabled() const
-{
-    constexpr auto svc = "org.freedesktop.network1";
-    constexpr auto intf = "org.freedesktop.network1.Link";
-    constexpr auto prop = "AdministrativeState";
-    char* rpath;
-    sd_bus_path_encode("/org/freedesktop/network1/link",
-                       std::to_string(ifIdx).c_str(), &rpath);
-    std::string path(rpath);
-    free(rpath);
-
-    // Store / Parser for the AdministrativeState return value
-    std::optional<bool> ret;
-    auto cb = [&](std::string_view state) {
-        if (state != "initialized")
-        {
-            ret = state != "unmanaged";
-        }
-    };
-
-    // Build a matcher before making the property call to ensure we
-    // can eventually get the value.
-    sdbusplus::bus::match_t match(
-        bus,
-        fmt::format("type='signal',sender='{}',path='{}',interface='{}',member="
-                    "'PropertiesChanged',arg0='{}',",
-                    svc, path, PROPERTY_INTERFACE, intf)
-            .c_str(),
-        [&](sdbusplus::message_t& m) {
-            std::string intf;
-            std::unordered_map<std::string, std::variant<std::string>> values;
-            try
-            {
-                m.read(intf, values);
-                auto it = values.find(prop);
-                // Ignore properties that aren't AdministrativeState
-                if (it != values.end())
-                {
-                    cb(std::get<std::string>(it->second));
-                }
-            }
-            catch (const std::exception& e)
-            {
-                log<level::ERR>(
-                    fmt::format(
-                        "AdministrativeState match parsing failed on {}: {}",
-                        interfaceName(), e.what())
-                        .c_str(),
-                    entry("INTERFACE=%s", interfaceName().c_str()),
-                    entry("ERROR=%s", e.what()));
-            }
-        });
-
-    // Actively call for the value in case the interface is already configured
-    auto method =
-        bus.new_method_call(svc, path.c_str(), PROPERTY_INTERFACE, METHOD_GET);
-    method.append(intf, prop);
-    try
-    {
-        auto reply = bus.call(method);
-        std::variant<std::string> state;
-        reply.read(state);
-        cb(std::get<std::string>(state));
-    }
-    catch (const std::exception& e)
-    {
-        log<level::ERR>(
-            fmt::format("Failed to get AdministrativeState on {}: {}",
-                        interfaceName(), e.what())
-                .c_str(),
-            entry("INTERFACE=%s", interfaceName().c_str()),
-            entry("ERROR=%s", e.what()));
-    }
-
-    // The interface is not yet configured by systemd-networkd, wait until it
-    // signals us a valid state.
-    while (!ret)
-    {
-        bus.wait();
-        bus.process_discard();
-    }
-
-    return *ret;
-}
-
 bool EthernetInterface::nicEnabled(bool value)
 {
     if (value == EthernetInterfaceIntf::nicEnabled())
diff --git a/src/ethernet_interface.hpp b/src/ethernet_interface.hpp
index ee83fe2..a2fb409 100644
--- a/src/ethernet_interface.hpp
+++ b/src/ethernet_interface.hpp
@@ -79,12 +79,12 @@
      *  @param[in] vlan - The id of the vlan if configured
      *  @param[in] emitSignal - true if the object added signal needs to be
      *                          send.
-     *  @param[in] enabled - Override the lookup of nicEnabled
+     *  @param[in] enabled - Determine if systemd-networkd is managing this link
      */
     EthernetInterface(sdbusplus::bus_t& bus, Manager& manager,
                       const InterfaceInfo& info, std::string_view objRoot,
-                      const config::Parser& config, bool emitSignal = true,
-                      std::optional<bool> enabled = std::nullopt);
+                      const config::Parser& config, bool emitSignal,
+                      bool enabled);
 
     /** @brief Network Manager object. */
     Manager& manager;
@@ -265,18 +265,13 @@
     EthernetInterface(sdbusplus::bus_t& bus, Manager& manager,
                       const InterfaceInfo& info, std::string&& objPath,
                       const config::Parser& config, bool emitSignal,
-                      std::optional<bool> enabled);
+                      bool enabled);
 
     /** @brief Determines if the address is manually assigned
      *  @param[in] origin - The origin entry of the IP::Address
      *  @returns true/false value if the address is static
      */
     bool originIsManuallyAssigned(IP::AddressOrigin origin);
-
-    /** @brief Determines if the NIC is enabled in systemd
-     *  @returns true/false value if the NIC is enabled
-     */
-    bool queryNicEnabled() const;
 };
 
 } // namespace network
diff --git a/src/network_manager.cpp b/src/network_manager.cpp
index beb677a..f862e28 100644
--- a/src/network_manager.cpp
+++ b/src/network_manager.cpp
@@ -11,6 +11,7 @@
 #include <fstream>
 #include <phosphor-logging/elog-errors.hpp>
 #include <phosphor-logging/log.hpp>
+#include <sdbusplus/message.hpp>
 #include <xyz/openbmc_project/Common/error.hpp>
 
 constexpr char SYSTEMD_BUSNAME[] = "org.freedesktop.systemd1";
@@ -33,13 +34,79 @@
 using namespace sdbusplus::xyz::openbmc_project::Common::Error;
 using Argument = xyz::openbmc_project::Common::InvalidArgument;
 
+static constexpr const char enabledMatch[] =
+    "type='signal',sender='org.freedesktop.network1',path_namespace='/org/"
+    "freedesktop/network1/"
+    "link',interface='org.freedesktop.DBus.Properties',member='"
+    "PropertiesChanged',arg0='org.freedesktop.network1.Link',";
+
 Manager::Manager(sdbusplus::bus_t& bus, const char* objPath,
                  const fs::path& confDir) :
     details::VLANCreateIface(bus, objPath,
                              details::VLANCreateIface::action::defer_emit),
-    bus(bus), objectPath(objPath)
+    bus(bus), objectPath(objPath),
+    systemdNetworkdEnabledMatch(
+        bus, enabledMatch, [&](sdbusplus::message_t& m) {
+            std::string intf;
+            std::unordered_map<std::string, std::variant<std::string>> values;
+            try
+            {
+                m.read(intf, values);
+                auto it = values.find("AdministrativeState");
+                if (it == values.end())
+                {
+                    return;
+                }
+                const std::string_view obj = m.get_path();
+                auto sep = obj.rfind('/');
+                if (sep == obj.npos || sep + 3 > obj.size())
+                {
+                    throw std::invalid_argument("Invalid obj path");
+                }
+                auto ifidx = DecodeInt<unsigned, 10>{}(obj.substr(sep + 3));
+                const auto& state = std::get<std::string>(it->second);
+                handleAdminState(state, ifidx);
+            }
+            catch (const std::exception& e)
+            {
+                log<level::ERR>(
+                    fmt::format("AdministrativeState match parsing failed: {}",
+                                e.what())
+                        .c_str(),
+                    entry("ERROR=%s", e.what()));
+            }
+        })
 {
     setConfDir(confDir);
+    std::vector<
+        std::tuple<int32_t, std::string, sdbusplus::message::object_path>>
+        links;
+    try
+    {
+        auto rsp =
+            bus.new_method_call("org.freedesktop.network1",
+                                "/org/freedesktop/network1",
+                                "org.freedesktop.network1.Manager", "ListLinks")
+                .call();
+        rsp.read(links);
+    }
+    catch (const sdbusplus::exception::SdBusError& e)
+    {
+        // Any failures are systemd-network not being ready
+    }
+    for (const auto& link : links)
+    {
+        unsigned ifidx = std::get<0>(link);
+        auto obj = fmt::format("/org/freedesktop/network1/link/_3{}", ifidx);
+        auto req =
+            bus.new_method_call("org.freedesktop.network1", obj.c_str(),
+                                "org.freedesktop.DBus.Properties", "Get");
+        req.append("org.freedesktop.network1.Link", "AdministrativeState");
+        auto rsp = req.call();
+        std::variant<std::string> val;
+        rsp.read(val);
+        handleAdminState(std::get<std::string>(val), ifidx);
+    }
 }
 
 void Manager::setConfDir(const fs::path& dir)
@@ -57,24 +124,36 @@
     }
 }
 
+void Manager::addInterface(InterfaceInfo& info, bool enabled)
+{
+    config::Parser config(config::pathForIntfConf(confDir, *info.name));
+    auto intf = std::make_unique<EthernetInterface>(
+        bus, *this, info, objectPath, config, true, enabled);
+    intf->createIPAddressObjects();
+    intf->createStaticNeighborObjects();
+    intf->loadNameServers(config);
+    intf->loadNTPServers(config);
+    auto ptr = intf.get();
+    interfaces.emplace(std::move(*info.name), std::move(intf));
+    interfacesByIdx.emplace(info.idx, ptr);
+}
+
 void Manager::createInterfaces()
 {
     // clear all the interfaces first
     interfaces.clear();
     interfacesByIdx.clear();
-    for (auto& interface : system::getInterfaces())
+    for (auto& info : system::getInterfaces())
     {
-        config::Parser config(
-            config::pathForIntfConf(confDir, *interface.name));
-        auto intf = std::make_unique<EthernetInterface>(bus, *this, interface,
-                                                        objectPath, config);
-        intf->createIPAddressObjects();
-        intf->createStaticNeighborObjects();
-        intf->loadNameServers(config);
-        intf->loadNTPServers(config);
-        auto ptr = intf.get();
-        interfaces.emplace(std::move(*interface.name), std::move(intf));
-        interfacesByIdx.emplace(interface.idx, ptr);
+        auto it = systemdNetworkdEnabled.find(info.idx);
+        if (it != systemdNetworkdEnabled.end())
+        {
+            addInterface(info, it->second);
+        }
+        else
+        {
+            undiscoveredIntfInfo.insert_or_assign(info.idx, std::move(info));
+        }
     }
 }
 
@@ -223,5 +302,30 @@
     }
 }
 
+void Manager::handleAdminState(std::string_view state, unsigned ifidx)
+{
+    if (state == "initialized" || state == "linger")
+    {
+        systemdNetworkdEnabled.erase(ifidx);
+    }
+    else
+    {
+        bool managed = state != "unmanaged";
+        systemdNetworkdEnabled.insert_or_assign(ifidx, managed);
+        if (auto it = undiscoveredIntfInfo.find(ifidx);
+            it != undiscoveredIntfInfo.end())
+        {
+            auto info = std::move(it->second);
+            undiscoveredIntfInfo.erase(it);
+            addInterface(info, managed);
+        }
+        else if (auto it = interfacesByIdx.find(ifidx);
+                 it != interfacesByIdx.end())
+        {
+            it->second->EthernetInterfaceIntf::nicEnabled(managed);
+        }
+    }
+}
+
 } // namespace network
 } // namespace phosphor
diff --git a/src/network_manager.hpp b/src/network_manager.hpp
index f265439..f7eb6bd 100644
--- a/src/network_manager.hpp
+++ b/src/network_manager.hpp
@@ -10,6 +10,7 @@
 #include <function2/function2.hpp>
 #include <memory>
 #include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
 #include <string>
 #include <string_view>
 #include <vector>
@@ -42,7 +43,6 @@
 class Manager : public details::VLANCreateIface
 {
   public:
-    Manager() = delete;
     Manager(const Manager&) = delete;
     Manager& operator=(const Manager&) = delete;
     Manager(Manager&&) = delete;
@@ -63,6 +63,9 @@
      */
     void writeToConfigurationFile();
 
+    /** @brief Adds a single interface to the interface map */
+    void addInterface(InterfaceInfo& info, bool enabled);
+
     /** @brief Fetch the interface and the ipaddress details
      *         from the system and create the ethernet interraces
      *         dbus object.
@@ -170,8 +173,18 @@
     /** @brief The routing table */
     route::Table routeTable;
 
+    /** @brief Map of interface info for undiscovered interfaces */
+    std::unordered_map<unsigned, InterfaceInfo> undiscoveredIntfInfo;
+
+    /** @brief Map of enabled interfaces */
+    std::unordered_map<unsigned, bool> systemdNetworkdEnabled;
+    sdbusplus::bus::match_t systemdNetworkdEnabledMatch;
+
     /** @brief List of hooks to execute during the next reload */
     std::vector<fu2::unique_function<void()>> reloadPreHooks;
+
+    /** @brief Handles the recipt of an adminstrative state string */
+    void handleAdminState(std::string_view state, unsigned ifidx);
 };
 
 } // namespace network