ethernet_interface: Write an update marker to tmpfs

Currently, many BMC systems use JFFS2 as a filesystem for their RWFS.
JFFS2 doesn't support sub-second timestamps, and we sometimes have
instances of configuration writes happening within the same second
around a networkd reload. The second write is then ignored upon the next
networkd reload as the timestamp of the file hasn't changed!

Therefore, we leverage tmpfs which has higher precision timestamps as a
mechanism to guarantee networkd sees our updates without making extra
changes to networkd or writes to the RWFS.

Tested: Verified that during a gateway set operation (or any other
property write) the updated file is set with microsecond granularity.
Also verified that subsequent updates change the timestamp and it's set
right before networkd reloads for MAC updates.

Change-Id: Ibfd0463166223d0e4a6f7205d780f4de8590a0c1
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/src/ethernet_interface.cpp b/src/ethernet_interface.cpp
index 4b50894..1467240 100644
--- a/src/ethernet_interface.cpp
+++ b/src/ethernet_interface.cpp
@@ -7,7 +7,6 @@
 #include "system_queries.hpp"
 #include "util.hpp"
 
-#include <fcntl.h>
 #include <linux/rtnetlink.h>
 #include <net/if.h>
 #include <net/if_arp.h>
@@ -15,6 +14,7 @@
 
 #include <phosphor-logging/elog-errors.hpp>
 #include <phosphor-logging/lg2.hpp>
+#include <stdplus/fd/create.hpp>
 #include <stdplus/raw.hpp>
 #include <stdplus/str/cat.hpp>
 #include <stdplus/zstring.hpp>
@@ -664,6 +664,37 @@
     return value ? "true"sv : "false"sv;
 }
 
+static void writeUpdatedTime(const Manager& manager,
+                             const std::filesystem::path& netFile)
+{
+    // JFFS2 doesn't have the time granularity to deal with sub-second
+    // updates. Since we can have multiple file updates within a second
+    // around a reload, we need a location which gives that precision for
+    // future networkd detected reloads. TMPFS gives us this property.
+    if (manager.getConfDir() == "/etc/systemd/network"sv)
+    {
+        auto dir = stdplus::strCat(netFile.native(), ".d");
+        dir.replace(1, 3, "run"); // Replace /etc with /run
+        auto file = dir + "/updated.conf";
+        try
+        {
+            std::filesystem::create_directories(dir);
+            using namespace stdplus::fd;
+            futimens(
+                open(file,
+                     OpenFlags(OpenAccess::WriteOnly).set(OpenFlag::Create),
+                     0644)
+                    .get(),
+                nullptr);
+        }
+        catch (const std::exception& e)
+        {
+            lg2::error("Failed to write time updated file {FILE}: {ERROR}",
+                       "FILE", file, "ERROR", e.what());
+        }
+    }
+}
+
 void EthernetInterface::writeConfigurationFile()
 {
     config::Parser config;
@@ -782,6 +813,7 @@
                                         interfaceName());
     config.writeFile(path);
     lg2::info("Wrote networkd file: {CFG_FILE}", "CFG_FILE", path);
+    writeUpdatedTime(manager, path);
 }
 
 std::string EthernetInterface::macAddress([[maybe_unused]] std::string value)
@@ -818,8 +850,6 @@
         stdplus::fromStr<stdplus::EtherAddr>(MacAddressIntf::macAddress());
     if (newMAC != oldMAC)
     {
-        auto path = config::pathForIntfConf(manager.get().getConfDir(),
-                                            interface);
         // Update everything that depends on the MAC value
         for (const auto& [_, intf] : manager.get().interfaces)
         {
@@ -831,10 +861,12 @@
         MacAddressIntf::macAddress(validMAC);
 
         writeConfigurationFile();
-        manager.get().addReloadPreHook([interface, path]() {
+        manager.get().addReloadPreHook([interface, manager = manager]() {
             // The MAC and LLADDRs will only update if the NIC is already down
             system::setNICUp(interface, false);
-            utimensat(AT_FDCWD, path.c_str(), NULL, 0);
+            writeUpdatedTime(
+                manager,
+                config::pathForIntfConf(manager.get().getConfDir(), interface));
         });
         manager.get().reloadConfigs();
     }