system: Configure eth1 interface (on running system)

This commit is a part of implementation of network configuration for eth1
interface through Sysrepo and velia-system. It implements
/czechlight-system:networking/standalone-eth1 presence container in
running datastore.

If the presence container is missing, the network configuration for eth1
interface resets to the "old" default configuration of eth1, i.e., add
it to the br0 bridge.
If the container is present, the interface is removed from the bridge and
its IP address is obtained via DHCP.

The network is managed by systemd-networkd thus we implement the changes
via systemd-networkd network files [1]. The files can be placed in /etc,
/run and /usr directories (and they take precedence in this order). We
place the new network file in the /run directory and reload network
configuration via networkctl reload.

There is a slight catch in removing the interface from a bridge.
Apparently, systemd-networkd does not remove an interface from a bridge
if the new configuration does not contain Bridge settings [2].
However, bringing the interface down and then reloading the new
configuration apparently works.

[1] https://www.freedesktop.org/software/systemd/man/systemd.network.html
[2] https://github.com/systemd/systemd/issues/8190

Change-Id: I8940985a88903c17ea61e55aced67b17fba4656a
diff --git a/src/main-system.cpp b/src/main-system.cpp
index 77f5e4f..d74c45c 100644
--- a/src/main-system.cpp
+++ b/src/main-system.cpp
@@ -6,9 +6,11 @@
 #include "main.h"
 #include "system/Firmware.h"
 #include "system/Authentication.h"
+#include "system/Network.h"
 #include "system_vars.h"
 #include "system/IETFSystem.h"
 #include "utils/exceptions.h"
+#include "utils/exec.h"
 #include "utils/journal.h"
 #include "utils/log-init.h"
 
@@ -82,6 +84,30 @@
 
         auto dbusConnection = sdbus::createConnection(); // second connection for RAUC (for calling methods).
         dbusConnection->enterEventLoopAsync();
+
+        // initialize czechlight-system:networking
+        const std::filesystem::path runtimeNetworkDirectory("/run/systemd/network");
+        std::filesystem::create_directories(runtimeNetworkDirectory);
+        auto sysrepoNetworkRunning = velia::system::Network(srSess, runtimeNetworkDirectory, [](const std::vector<std::string>& reconfiguredInterfaces) {
+            auto log = spdlog::get("system");
+
+            /* Bring all the updated interfaces down (they will later be brought up by executing `networkctl reload`).
+             *
+             * This is required when transitioning from bridge to DHCP configuration. systemd-networkd apparently does not reset many
+             * interface properties when reconfiguring the interface into new "bridge-less" configuration (the interface stays in the
+             * bridge and it also does not obtain link local address).
+             *
+             * This doesn't seem to be required when transitioning from DHCP to bridge configuration. It's just a "precaution" because
+             * there might be hidden some caveats that I am unable to see now (some leftover setting). Bringing the interface
+             * down seems to reset the interface (and it is something we can afford in the interface reconfiguration process).
+             */
+            for (const auto& interfaceName : reconfiguredInterfaces) {
+                velia::utils::execAndWait(log, NETWORKCTL_EXECUTABLE, {"down", interfaceName}, "");
+            }
+
+            velia::utils::execAndWait(log, NETWORKCTL_EXECUTABLE, {"reload"}, "");
+        });
+
         auto sysrepoFirmware = velia::system::Firmware(srConn, *g_dbusConnection, *dbusConnection);
 
         auto srSess2 = std::make_shared<sysrepo::Session>(srConn);
diff --git a/src/system/Network.cpp b/src/system/Network.cpp
new file mode 100644
index 0000000..cb8254a
--- /dev/null
+++ b/src/system/Network.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ *
+ */
+
+#include <fmt/core.h>
+#include "Network.h"
+#include "utils/io.h"
+#include "utils/log.h"
+#include "utils/sysrepo.h"
+
+using namespace std::literals;
+using namespace fmt::literals;
+
+namespace {
+
+const auto CZECHLIGHT_SYSTEM_MODULE_NAME = "czechlight-system"s;
+const auto CZECHLIGHT_SYSTEM_STANDALONE_ETH1 = "/"s + CZECHLIGHT_SYSTEM_MODULE_NAME + ":networking/standalone-eth1"s;
+
+const std::string NETWORK_FILE_CONTENT_TEMPLATE = R"([Match]
+Name=eth1
+
+[Network]
+{setting}
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+
+std::map<std::string, std::string> getNetworkConfiguration(std::shared_ptr<::sysrepo::Session> session, velia::Log log)
+{
+    if (session->get_data(CZECHLIGHT_SYSTEM_STANDALONE_ETH1.c_str()) == nullptr) { // the presence container is missing, bridge eth1
+        log->debug("Container eth1-standalone not present. Generating bridge configuration for eth1.");
+        return {{"eth1", fmt::format(NETWORK_FILE_CONTENT_TEMPLATE, "setting"_a = "Bridge=br0")}};
+    } else {
+        log->debug("Container eth1-standalone is present. Generating DHCP configuration for eth1.");
+        return {{"eth1", fmt::format(NETWORK_FILE_CONTENT_TEMPLATE, "setting"_a = "DHCP=yes")}};
+    }
+}
+
+}
+
+namespace velia::system {
+
+Network::Network(std::shared_ptr<::sysrepo::Session> srSess, std::filesystem::path networkConfigDirectory, std::function<void(const std::vector<std::string>&)> networkReloadCallback)
+    : m_log(spdlog::get("system"))
+    , m_srSubscribe(std::make_shared<sysrepo::Subscribe>(srSess))
+{
+    utils::ensureModuleImplemented(srSess, CZECHLIGHT_SYSTEM_MODULE_NAME, "2021-01-13");
+
+    m_srSubscribe->module_change_subscribe(
+        CZECHLIGHT_SYSTEM_MODULE_NAME.c_str(),
+        [&, networkConfigDirectory = std::move(networkConfigDirectory), networkReloadCallback = std::move(networkReloadCallback)](sysrepo::S_Session session, [[maybe_unused]] const char* module_name, [[maybe_unused]] const char* xpath, [[maybe_unused]] sr_event_t event, [[maybe_unused]] uint32_t request_id) {
+            auto config = getNetworkConfiguration(session, m_log);
+            for (const auto& [interface, networkFileContents] : config) {
+                velia::utils::safeWriteFile(networkConfigDirectory / (interface + ".network"), networkFileContents);
+            }
+
+            std::vector<std::string> interfaces;
+            std::transform(config.begin(), config.end(), std::back_inserter(interfaces), [](const auto& kv) { return kv.first; });
+            networkReloadCallback(interfaces);
+
+            return SR_ERR_OK;
+        },
+        CZECHLIGHT_SYSTEM_STANDALONE_ETH1.c_str(),
+        0,
+        SR_SUBSCR_DONE_ONLY | SR_SUBSCR_ENABLED);
+}
+
+}
diff --git a/src/system/Network.h b/src/system/Network.h
new file mode 100644
index 0000000..8115101
--- /dev/null
+++ b/src/system/Network.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
+ *
+ */
+#pragma once
+
+#include <filesystem>
+#include <sysrepo-cpp/Session.hpp>
+#include "utils/log-fwd.h"
+
+namespace velia::system {
+
+class Network {
+public:
+    Network(std::shared_ptr<::sysrepo::Session> srSess, std::filesystem::path networkConfigDirectory, std::function<void(const std::vector<std::string>&)> networkReloadCallback);
+
+private:
+    velia::Log m_log;
+    std::shared_ptr<::sysrepo::Subscribe> m_srSubscribe;
+};
+}
diff --git a/src/system/system_vars.h.in b/src/system/system_vars.h.in
index 8098a2e..b2953db 100644
--- a/src/system/system_vars.h.in
+++ b/src/system/system_vars.h.in
@@ -8,3 +8,4 @@
 #define SSH_KEYGEN_EXECUTABLE "@SSH_KEYGEN_EXECUTABLE@"
 #define CHPASSWD_EXECUTABLE "@CHPASSWD_EXECUTABLE@"
 #define SYSTEMCTL_EXECUTABLE "@SYSTEMCTL_EXECUTABLE@"
+#define NETWORKCTL_EXECUTABLE "@NETWORKCTL_EXECUTABLE@"