ethernet_interface: Support DHCP4/6 Properties

We are splitting the DHCPEnabled option into 3 new options to reflect
all of the desired states (DHCP4, DHCP6, IPv6AcceptRA). We will
still support DHCPEnabled until all of the old users have transitioned
away. This commit makes DHCPEnabled a view / modifier of the other
options.

Tested: Toggled the DHCPEnabled settings on a BMC and verified that the
state updated correctly along with the other properties to affects.
Verified that changing the individual properties updates the DHCPEnabled
property to the correct enum state.

Change-Id: I2cef15d7eaf80c8d717a11d090f61ca5d275821a
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/src/ethernet_interface.cpp b/src/ethernet_interface.cpp
index c222af1..6f078e3 100644
--- a/src/ethernet_interface.cpp
+++ b/src/ethernet_interface.cpp
@@ -47,12 +47,6 @@
 constexpr auto RESOLVED_SERVICE_PATH = "/org/freedesktop/resolve1/link/";
 constexpr auto METHOD_GET = "Get";
 
-std::map<EthernetInterface::DHCPConf, std::string> mapDHCPToSystemd = {
-    {EthernetInterface::DHCPConf::both, "true"},
-    {EthernetInterface::DHCPConf::v4, "ipv4"},
-    {EthernetInterface::DHCPConf::v6, "ipv6"},
-    {EthernetInterface::DHCPConf::none, "false"}};
-
 static stdplus::Fd& getIFSock()
 {
     using namespace stdplus::fd;
@@ -74,7 +68,9 @@
     auto intfName = objPath.substr(objPath.rfind("/") + 1);
     std::replace(intfName.begin(), intfName.end(), '_', '.');
     interfaceName(intfName);
-    EthernetInterfaceIntf::dhcpEnabled(getDHCPValue(config));
+    auto dhcpVal = getDHCPValue(config);
+    EthernetInterfaceIntf::dhcp4(dhcpVal.v4);
+    EthernetInterfaceIntf::dhcp6(dhcpVal.v6);
     EthernetInterfaceIntf::ipv6AcceptRA(getIPv6AcceptRA(config));
     EthernetInterfaceIntf::nicEnabled(enabled ? *enabled : queryNicEnabled());
     const auto& gatewayList = manager.getRouteTable().getDefaultGateway();
@@ -142,40 +138,16 @@
     throw std::runtime_error("Invalid addr type");
 }
 
-void EthernetInterface::disableDHCP(IP::Protocol protocol)
-{
-    DHCPConf dhcpState = EthernetInterfaceIntf::dhcpEnabled();
-    if (dhcpState == EthernetInterface::DHCPConf::both)
-    {
-        if (protocol == IP::Protocol::IPv4)
-        {
-            dhcpEnabled(EthernetInterface::DHCPConf::v6);
-        }
-        else if (protocol == IP::Protocol::IPv6)
-        {
-            dhcpEnabled(EthernetInterface::DHCPConf::v4);
-        }
-    }
-    else if ((dhcpState == EthernetInterface::DHCPConf::v4) &&
-             (protocol == IP::Protocol::IPv4))
-    {
-        dhcpEnabled(EthernetInterface::DHCPConf::none);
-    }
-    else if ((dhcpState == EthernetInterface::DHCPConf::v6) &&
-             (protocol == IP::Protocol::IPv6))
-    {
-        dhcpEnabled(EthernetInterface::DHCPConf::none);
-    }
-}
-
 bool EthernetInterface::dhcpIsEnabled(IP::Protocol family)
 {
-    const auto cur = EthernetInterfaceIntf::dhcpEnabled();
-    return cur == EthernetInterface::DHCPConf::both ||
-           (family == IP::Protocol::IPv6 &&
-            cur == EthernetInterface::DHCPConf::v6) ||
-           (family == IP::Protocol::IPv4 &&
-            cur == EthernetInterface::DHCPConf::v4);
+    switch (family)
+    {
+        case IP::Protocol::IPv6:
+            return dhcp6();
+        case IP::Protocol::IPv4:
+            return dhcp4();
+    }
+    throw std::logic_error("Unreachable");
 }
 
 bool EthernetInterface::originIsManuallyAssigned(IP::AddressOrigin origin)
@@ -268,9 +240,17 @@
 {
     if (dhcpIsEnabled(protType))
     {
-        log<level::INFO>("DHCP enabled on the interface"),
+        log<level::INFO>("DHCP enabled on the interface, disabling"),
             entry("INTERFACE=%s", interfaceName().c_str());
-        disableDHCP(protType);
+        switch (protType)
+        {
+            case IP::Protocol::IPv4:
+                dhcp4(false);
+                break;
+            case IP::Protocol::IPv6:
+                dhcp6(false);
+                break;
+        }
         // Delete the IP address object and that reloads the networkd
         // to allow the same IP address to be set as Static IP
         deleteObject(ipaddress);
@@ -540,32 +520,69 @@
 
 bool EthernetInterface::ipv6AcceptRA(bool value)
 {
-    if (value == EthernetInterfaceIntf::ipv6AcceptRA())
+    if (ipv6AcceptRA() != EthernetInterfaceIntf::ipv6AcceptRA(value))
     {
-        return value;
+        writeConfigurationFile();
+        manager.reloadConfigs();
     }
-    EthernetInterfaceIntf::ipv6AcceptRA(value);
+    return value;
+}
 
-    writeConfigurationFile();
-    manager.reloadConfigs();
+bool EthernetInterface::dhcp4(bool value)
+{
+    if (dhcp4() != EthernetInterfaceIntf::dhcp4(value))
+    {
+        writeConfigurationFile();
+        manager.reloadConfigs();
+    }
+    return value;
+}
 
+bool EthernetInterface::dhcp6(bool value)
+{
+    if (dhcp6() != EthernetInterfaceIntf::dhcp6(value))
+    {
+        writeConfigurationFile();
+        manager.reloadConfigs();
+    }
     return value;
 }
 
 EthernetInterface::DHCPConf EthernetInterface::dhcpEnabled(DHCPConf value)
 {
-    if (value == EthernetInterfaceIntf::dhcpEnabled())
+    auto old4 = EthernetInterfaceIntf::dhcp4();
+    auto new4 = EthernetInterfaceIntf::dhcp4(value == DHCPConf::v4 ||
+                                             value == DHCPConf::v4v6stateless ||
+                                             value == DHCPConf::both);
+    auto old6 = EthernetInterfaceIntf::dhcp6();
+    auto new6 = EthernetInterfaceIntf::dhcp6(value == DHCPConf::v6 ||
+                                             value == DHCPConf::both);
+    auto oldra = EthernetInterfaceIntf::ipv6AcceptRA();
+    auto newra = EthernetInterfaceIntf::ipv6AcceptRA(
+        value == DHCPConf::v6stateless || value == DHCPConf::v4v6stateless ||
+        value == DHCPConf::v6 || value == DHCPConf::both);
+
+    if (old4 != new4 || old6 != new6 || oldra != newra)
     {
-        return value;
+        writeConfigurationFile();
+        manager.reloadConfigs();
     }
-    EthernetInterfaceIntf::dhcpEnabled(value);
-
-    writeConfigurationFile();
-    manager.reloadConfigs();
-
     return value;
 }
 
+EthernetInterface::DHCPConf EthernetInterface::dhcpEnabled() const
+{
+    if (dhcp6())
+    {
+        return dhcp4() ? DHCPConf::both : DHCPConf::v6;
+    }
+    else if (dhcp4())
+    {
+        return ipv6AcceptRA() ? DHCPConf::v4v6stateless : DHCPConf::v4;
+    }
+    return ipv6AcceptRA() ? DHCPConf::v6stateless : DHCPConf::none;
+}
+
 bool EthernetInterface::linkUp() const
 {
     bool value = EthernetInterfaceIntf::linkUp();
@@ -982,10 +999,9 @@
 #else
         lla.emplace_back("no");
 #endif
-        network["IPv6AcceptRA"].emplace_back(
-            EthernetInterfaceIntf::ipv6AcceptRA() ? "true" : "false");
-        network["DHCP"].emplace_back(
-            mapDHCPToSystemd[EthernetInterfaceIntf::dhcpEnabled()]);
+        network["IPv6AcceptRA"].emplace_back(ipv6AcceptRA() ? "true" : "false");
+        network["DHCP"].emplace_back(dhcp4() ? (dhcp6() ? "true" : "ipv4")
+                                             : (dhcp6() ? "ipv6" : "false"));
         {
             auto& vlans = network["VLAN"];
             for (const auto& intf : vlanInterfaces)
@@ -1023,7 +1039,7 @@
         }
         {
             auto& gateways = network["Gateway"];
-            if (!dhcpIsEnabled(IP::Protocol::IPv4))
+            if (!dhcp4())
             {
                 auto gateway = EthernetInterfaceIntf::defaultGateway();
                 if (!gateway.empty())
@@ -1032,7 +1048,7 @@
                 }
             }
 
-            if (!dhcpIsEnabled(IP::Protocol::IPv6))
+            if (!dhcp6())
             {
                 auto gateway6 = EthernetInterfaceIntf::defaultGateway6();
                 if (!gateway6.empty())
@@ -1043,7 +1059,7 @@
         }
     }
     config.map["IPv6AcceptRA"].emplace_back()["DHCPv6Client"].emplace_back(
-        dhcpIsEnabled(IP::Protocol::IPv6) ? "true" : "false");
+        dhcp6() ? "true" : "false");
     {
         auto& neighbors = config.map["Neighbor"];
         for (const auto& sneighbor : staticNeighbors)
diff --git a/src/ethernet_interface.hpp b/src/ethernet_interface.hpp
index e0f2987..e81a108 100644
--- a/src/ethernet_interface.hpp
+++ b/src/ethernet_interface.hpp
@@ -159,13 +159,12 @@
     }
 
     /** Set value of DHCPEnabled */
+    DHCPConf dhcpEnabled() const override;
     DHCPConf dhcpEnabled(DHCPConf value) override;
-
-    /** @brief Selectively disables DHCP
-     *  @param[in] protocol - The IPv4 or IPv6 protocol to return to static
-     *                        addressing mode
-     */
-    void disableDHCP(IP::Protocol protocol);
+    using EthernetInterfaceIntf::dhcp4;
+    bool dhcp4(bool value) override;
+    using EthernetInterfaceIntf::dhcp6;
+    bool dhcp6(bool value) override;
 
     /** Retrieve Link State */
     bool linkUp() const override;
@@ -189,6 +188,7 @@
      *
      */
     bool ipv6AcceptRA(bool value) override;
+    using EthernetInterfaceIntf::ipv6AcceptRA;
 
     /** @brief sets the NTP servers.
      *  @param[in] value - vector of NTP servers.
@@ -230,7 +230,6 @@
      */
     std::string defaultGateway6(std::string gateway) override;
 
-    using EthernetInterfaceIntf::dhcpEnabled;
     using EthernetInterfaceIntf::interfaceName;
     using EthernetInterfaceIntf::linkUp;
     using EthernetInterfaceIntf::mtu;
diff --git a/src/util.cpp b/src/util.cpp
index 7f55e2e..54028af 100644
--- a/src/util.cpp
+++ b/src/util.cpp
@@ -397,9 +397,9 @@
     return ret.value_or(def);
 }
 
-EthernetInterfaceIntf::DHCPConf getDHCPValue(const config::Parser& config)
+DHCPVal getDHCPValue(const config::Parser& config)
 {
-    constexpr auto def = EthernetInterfaceIntf::DHCPConf::both;
+    constexpr auto def = DHCPVal{.v4 = true, .v6 = true};
 
     const auto value = config.map.getLastValueString("Network", "DHCP");
     if (value == nullptr)
@@ -413,11 +413,11 @@
     }
     if (config::icaseeq(*value, "ipv4"))
     {
-        return EthernetInterfaceIntf::DHCPConf::v4;
+        return DHCPVal{.v4 = true, .v6 = false};
     }
     if (config::icaseeq(*value, "ipv6"))
     {
-        return EthernetInterfaceIntf::DHCPConf::v6;
+        return DHCPVal{.v4 = false, .v6 = true};
     }
     auto ret = config::parseBool(*value);
     if (!ret.has_value())
@@ -429,8 +429,8 @@
                            entry("VALUE=%s", value->c_str()));
         return def;
     }
-    return *ret ? EthernetInterfaceIntf::DHCPConf::both
-                : EthernetInterfaceIntf::DHCPConf::none;
+    return *ret ? DHCPVal{.v4 = true, .v6 = true}
+                : DHCPVal{.v4 = false, .v6 = false};
 }
 
 namespace mac_address
diff --git a/src/util.hpp b/src/util.hpp
index c4e1b60..055c684 100644
--- a/src/util.hpp
+++ b/src/util.hpp
@@ -132,7 +132,11 @@
 /** @brief read the DHCP value from the configuration file
  *  @param[in] config - The parsed configuration.
  */
-EthernetInterfaceIntf::DHCPConf getDHCPValue(const config::Parser& config);
+struct DHCPVal
+{
+    bool v4, v6;
+};
+DHCPVal getDHCPValue(const config::Parser& config);
 
 namespace internal
 {
diff --git a/test/test_ethernet_interface.cpp b/test/test_ethernet_interface.cpp
index 0893147..0775756 100644
--- a/test/test_ethernet_interface.cpp
+++ b/test/test_ethernet_interface.cpp
@@ -201,5 +201,49 @@
     EXPECT_EQ(interface.defaultGateway6(), "");
 }
 
+TEST_F(TestEthernetInterface, DHCPEnabled)
+{
+    EXPECT_CALL(manager, reloadConfigs()).WillRepeatedly(testing::Return());
+
+    using DHCPConf = EthernetInterfaceIntf::DHCPConf;
+    auto test = [&](DHCPConf conf, bool dhcp4, bool dhcp6, bool ra) {
+        EXPECT_EQ(conf, interface.dhcpEnabled());
+        EXPECT_EQ(dhcp4, interface.dhcp4());
+        EXPECT_EQ(dhcp6, interface.dhcp6());
+        EXPECT_EQ(ra, interface.ipv6AcceptRA());
+    };
+    test(DHCPConf::both, /*dhcp4=*/true, /*dhcp6=*/true, /*ra=*/true);
+
+    auto set_test = [&](DHCPConf conf, bool dhcp4, bool dhcp6, bool ra) {
+        EXPECT_EQ(conf, interface.dhcpEnabled(conf));
+        test(conf, dhcp4, dhcp6, ra);
+    };
+    set_test(DHCPConf::none, /*dhcp4=*/false, /*dhcp6=*/false, /*ra=*/false);
+    set_test(DHCPConf::v4, /*dhcp4=*/true, /*dhcp6=*/false, /*ra=*/false);
+    set_test(DHCPConf::v6stateless, /*dhcp4=*/false, /*dhcp6=*/false,
+             /*ra=*/true);
+    set_test(DHCPConf::v6, /*dhcp4=*/false, /*dhcp6=*/true, /*ra=*/true);
+    set_test(DHCPConf::v4v6stateless, /*dhcp4=*/true, /*dhcp6=*/false,
+             /*ra=*/true);
+    set_test(DHCPConf::both, /*dhcp4=*/true, /*dhcp6=*/true, /*ra=*/true);
+
+    auto ind_test = [&](DHCPConf conf, bool dhcp4, bool dhcp6, bool ra) {
+        EXPECT_EQ(dhcp4, interface.dhcp4(dhcp4));
+        EXPECT_EQ(dhcp6, interface.dhcp6(dhcp6));
+        EXPECT_EQ(ra, interface.ipv6AcceptRA(ra));
+        test(conf, dhcp4, dhcp6, ra);
+    };
+    ind_test(DHCPConf::none, /*dhcp4=*/false, /*dhcp6=*/false, /*ra=*/false);
+    ind_test(DHCPConf::v4, /*dhcp4=*/true, /*dhcp6=*/false, /*ra=*/false);
+    ind_test(DHCPConf::v6stateless, /*dhcp4=*/false, /*dhcp6=*/false,
+             /*ra=*/true);
+    ind_test(DHCPConf::v6, /*dhcp4=*/false, /*dhcp6=*/true, /*ra=*/false);
+    set_test(DHCPConf::v6, /*dhcp4=*/false, /*dhcp6=*/true, /*ra=*/true);
+    ind_test(DHCPConf::v4v6stateless, /*dhcp4=*/true, /*dhcp6=*/false,
+             /*ra=*/true);
+    ind_test(DHCPConf::both, /*dhcp4=*/true, /*dhcp6=*/true, /*ra=*/false);
+    set_test(DHCPConf::both, /*dhcp4=*/true, /*dhcp6=*/true, /*ra=*/true);
+}
+
 } // namespace network
 } // namespace phosphor