system: Network autoconfiguration

Add a czechlight-network:dhcp-client leaf into
ietf-interfaces:interfaces/interface/ietf-ip:ipv4 container which
indicates whether the implementation starts an DHCP client on IPv4.

For IPv6, we use
ietf-interfaces:interfaces/interface/ietf-ip:ipv6/autoconf container.
The value of autoconf/create-global-addresses leaf is mapped to
networkd's IPv6AcceptRA config value [1].

The 'DHCP=' option in systemd.network [2] allows multiple values
managing DHCPv4 and DHCPv6. We chose to ignore DHCPv6 because it will
(or won't) be triggered by router advertisement (RA) *regardless* of
this parameter [2]. It is sufficient to have RA on (which is managed by
the ipv6/autoconf leaf). In case RA is on but no routers are found,
systemd-network starts the DHCP client anyway [1]. If RA is off, then
no IPv6 autoconfiguration happens.

[1] https://systemd.network/systemd.network.html#IPv6AcceptRA=
[2] https://systemd.network/systemd.network.html#DHCP=

Change-Id: Ia885ae644d368987b6101f828bddfd0fc4e969dd
diff --git a/src/system/IETFInterfacesConfig.cpp b/src/system/IETFInterfacesConfig.cpp
index 9ac681a..61c68b9 100644
--- a/src/system/IETFInterfacesConfig.cpp
+++ b/src/system/IETFInterfacesConfig.cpp
@@ -127,7 +127,19 @@
                 configValues["Network"].push_back("LinkLocalAddressing=no");
             }
 
-            configValues["Network"].push_back("DHCP=no"); // temporarily disabled
+            // network autoconfiguration
+            if (auto node = utils::getUniqueSubtree(linkEntry, "ietf-ip:ipv6/ietf-ip:autoconf/ietf-ip:create-global-addresses"); protocolEnabled(linkEntry, "ipv6") && utils::getValueAsString(node.value()) == "true"s) {
+                configValues["Network"].push_back("IPv6AcceptRA=true");
+            } else {
+                configValues["Network"].push_back("IPv6AcceptRA=false");
+            }
+
+            if (auto node = utils::getUniqueSubtree(linkEntry, "ietf-ip:ipv4/czechlight-network:dhcp-client"); protocolEnabled(linkEntry, "ipv4") && utils::getValueAsString(node.value()) == "true"s) {
+                configValues["Network"].push_back("DHCP=ipv4");
+            } else {
+                configValues["Network"].push_back("DHCP=no");
+            }
+
             configValues["Network"].push_back("LLDP=true");
             configValues["Network"].push_back("EmitLLDP=nearest-bridge");
 
diff --git a/tests/sysrepo_system-ietfinterfaces-sudo.cpp b/tests/sysrepo_system-ietfinterfaces-sudo.cpp
index e61bd33..c520b29 100644
--- a/tests/sysrepo_system-ietfinterfaces-sudo.cpp
+++ b/tests/sysrepo_system-ietfinterfaces-sudo.cpp
@@ -113,6 +113,7 @@
         {"/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']", ""},
         {"/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']/ip", "::ffff:192.0.2.1"},
         {"/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']/prefix-length", "128"},
+        {"/ietf-ip:ipv6/autoconf", ""},
         {"/name", IFACE},
         {"/oper-status", "down"},
         {"/phys-address", LINK_MAC},
@@ -198,6 +199,7 @@
 
         iproute2_exec_and_wait(WAIT_BRIDGE, "link", "set", "dev", IFACE_BRIDGE, "up");
         expectedBridge["/ietf-ip:ipv6"] = "";
+        expectedBridge["/ietf-ip:ipv6/autoconf"] = "";
         expectedBridge["/ietf-ip:ipv6/address[ip='fe80::22:22ff:fe22:2222']"] = "";
         expectedBridge["/ietf-ip:ipv6/address[ip='fe80::22:22ff:fe22:2222']/ip"] = "fe80::22:22ff:fe22:2222";
         expectedBridge["/ietf-ip:ipv6/address[ip='fe80::22:22ff:fe22:2222']/prefix-length"] = "64";
@@ -229,6 +231,7 @@
         expectedIface.erase("/ietf-ip:ipv4/address[ip='192.0.2.1']/ip");
         expectedIface.erase("/ietf-ip:ipv4/address[ip='192.0.2.1']/prefix-length");
         expectedIface.erase("/ietf-ip:ipv4");
+        expectedIface.erase("/ietf-ip:ipv6/autoconf");
         expectedIface.erase("/ietf-ip:ipv6");
         REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", SR_DS_OPERATIONAL) == expectedIface);
         REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE_BRIDGE + "']", SR_DS_OPERATIONAL) == expectedBridge);
diff --git a/tests/sysrepo_system-ietfinterfaces.cpp b/tests/sysrepo_system-ietfinterfaces.cpp
index 2f0469d..dea8177 100644
--- a/tests/sysrepo_system-ietfinterfaces.cpp
+++ b/tests/sysrepo_system-ietfinterfaces.cpp
@@ -37,7 +37,7 @@
         lo.erase(it);
     }
 
-    REQUIRE(lo == std::map<std::string, std::string> {
+    REQUIRE(lo == std::map<std::string, std::string>{
                 {"/name", "lo"},
                 {"/type", "iana-if-type:softwareLoopback"},
                 {"/phys-address", "00:00:00:00:00:00"},
@@ -47,6 +47,7 @@
                 {"/ietf-ip:ipv4/address[ip='127.0.0.1']/ip", "127.0.0.1"},
                 {"/ietf-ip:ipv4/address[ip='127.0.0.1']/prefix-length", "8"},
                 {"/ietf-ip:ipv6", ""},
+                {"/ietf-ip:ipv6/autoconf", ""},
                 {"/ietf-ip:ipv6/address[ip='::1']", ""},
                 {"/ietf-ip:ipv6/address[ip='::1']/ip", "::1"},
                 {"/ietf-ip:ipv6/address[ip='::1']/prefix-length", "128"},
@@ -167,24 +168,27 @@
             client->apply_changes();
         }
 
-        SECTION("Enabled IPv4 protocol must have at least one IP")
+        SECTION("Enabled IPv4 protocol must have at least one IP or the autoconfiguration must be on")
         {
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/enabled", "true");
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:enabled", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:enabled", "false");
             REQUIRE_THROWS_AS(client->apply_changes(), sysrepo::sysrepo_exception);
         }
 
-        SECTION("Enabled IPv6 protocol must have at least one IP")
+        SECTION("Enabled IPv6 protocol must have at least one IP or the autoconfiguration must be on")
         {
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/enabled", "true");
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:enabled", "false");
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:enabled", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:autoconf/ietf-ip:create-global-addresses", "false");
             REQUIRE_THROWS_AS(client->apply_changes(), sysrepo::sysrepo_exception);
         }
     }
 
     std::string expectedContents;
+
     SECTION("Setting IPs to eth0")
     {
         const auto expectedFilePath = fakeConfigDir / "eth0.network";
@@ -195,6 +199,7 @@
         {
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/description", "Hello world");
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/ietf-ip:prefix-length", "24");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
             expectedContents = R"([Match]
 Name=eth0
 
@@ -202,6 +207,7 @@
 Description=Hello world
 Address=192.0.2.1/24
 LinkLocalAddressing=no
+IPv6AcceptRA=false
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -212,6 +218,7 @@
         {
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/ietf-ip:prefix-length", "24");
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.2']/ietf-ip:prefix-length", "24");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
             client->delete_item("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6");
             expectedContents = R"([Match]
 Name=eth0
@@ -220,6 +227,7 @@
 Address=192.0.2.1/24
 Address=192.0.2.2/24
 LinkLocalAddressing=no
+IPv6AcceptRA=false
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -230,12 +238,14 @@
         {
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/ietf-ip:prefix-length", "24");
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:address[ip='2001:db8::1']/ietf-ip:prefix-length", "32");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
             expectedContents = R"([Match]
 Name=eth0
 
 [Network]
 Address=192.0.2.1/24
 Address=2001:db8::1/32
+IPv6AcceptRA=true
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -246,6 +256,7 @@
         {
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/ietf-ip:prefix-length", "24");
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:address[ip='2001:db8::1']/ietf-ip:prefix-length", "32");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
             client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/enabled", "false");
             expectedContents = R"([Match]
 Name=eth0
@@ -253,6 +264,7 @@
 [Network]
 Address=192.0.2.1/24
 LinkLocalAddressing=no
+IPv6AcceptRA=false
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -282,6 +294,7 @@
 [Network]
 Address=192.0.2.1/24
 LinkLocalAddressing=no
+IPv6AcceptRA=false
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -291,6 +304,7 @@
 
 [Network]
 Address=2001:db8::1/32
+IPv6AcceptRA=true
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -298,6 +312,7 @@
 
         client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/type", "iana-if-type:ethernetCsmacd");
         client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/ietf-ip:prefix-length", "24");
+        client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
         client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth1']/type", "iana-if-type:ethernetCsmacd");
         client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth1']/ietf-ip:ipv6/ietf-ip:address[ip='2001:db8::1']/ietf-ip:prefix-length", "32");
 
@@ -328,6 +343,7 @@
 
 [Network]
 LinkLocalAddressing=no
+IPv6AcceptRA=false
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -338,6 +354,7 @@
 
 [Network]
 Bridge=br0
+IPv6AcceptRA=false
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -348,6 +365,7 @@
 
 [Network]
 Bridge=br0
+IPv6AcceptRA=false
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -379,12 +397,14 @@
         // assign an IPv4 address to br0
         client->set_item_str("/ietf-interfaces:interfaces/interface[name='br0']/enabled", "true");
         client->set_item_str("/ietf-interfaces:interfaces/interface[name='br0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/ietf-ip:prefix-length", "24");
+        client->set_item_str("/ietf-interfaces:interfaces/interface[name='br0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
         expectedContentsBr0 = R"([Match]
 Name=br0
 
 [Network]
 Address=192.0.2.1/24
 LinkLocalAddressing=no
+IPv6AcceptRA=false
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -407,6 +427,7 @@
 [Network]
 Address=192.0.2.1/24
 Address=2001:db8::1/32
+IPv6AcceptRA=true
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -430,6 +451,7 @@
 
 [Network]
 Address=2001:db8::2/32
+IPv6AcceptRA=true
 DHCP=no
 LLDP=true
 EmitLLDP=nearest-bridge
@@ -501,4 +523,136 @@
             REQUIRE(!std::filesystem::exists(expectedFilePathEth0));
         }
     }
+
+    SECTION("Network autoconfiguration")
+    {
+        const auto expectedFilePath = fakeConfigDir / "eth0.network";
+
+        client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/type", "iana-if-type:ethernetCsmacd");
+
+        SECTION("IPv4 on with address, IPv6 disabled, DHCPv4 off, RA off")
+        {
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/ietf-ip:prefix-length", "24"); // in case DHCP is disabled an IP must be present
+
+            expectedContents = R"([Match]
+Name=eth0
+
+[Network]
+Address=192.0.2.1/24
+LinkLocalAddressing=no
+IPv6AcceptRA=false
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+        }
+
+        SECTION("IPv4 on with address, IPv6 disabled, DHCPv4 on, RA on")
+        {
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/ietf-ip:prefix-length", "24"); // in case DHCP is disabled an IP must be present
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:enabled", "false");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:autoconf/ietf-ip:create-global-addresses", "true");
+
+            expectedContents = R"([Match]
+Name=eth0
+
+[Network]
+Address=192.0.2.1/24
+LinkLocalAddressing=no
+IPv6AcceptRA=false
+DHCP=ipv4
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+        }
+
+        SECTION("IPv4 disabled, IPv6 enabled, DHCPv4 on, RA on")
+        {
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:enabled", "false");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:enabled", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:autoconf/ietf-ip:create-global-addresses", "true");
+
+            expectedContents = R"([Match]
+Name=eth0
+
+[Network]
+IPv6AcceptRA=true
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+        }
+
+        SECTION("IPv4 enabled, IPv6 enabled, DHCPv4 on, RA on")
+        {
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:enabled", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:enabled", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:autoconf/ietf-ip:create-global-addresses", "true");
+
+            expectedContents = R"([Match]
+Name=eth0
+
+[Network]
+IPv6AcceptRA=true
+DHCP=ipv4
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+        }
+
+        SECTION("IPv4 enabled, IPv6 enabled, DHCPv4 off, RA on")
+        {
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:enabled", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/prefix-length", "24");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:enabled", "true");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:autoconf/ietf-ip:create-global-addresses", "true");
+
+            expectedContents = R"([Match]
+Name=eth0
+
+[Network]
+Address=192.0.2.1/24
+IPv6AcceptRA=true
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+        }
+
+        SECTION("IPv4 disabled, IPv6 disabled, DHCPv4 off, RA off")
+        {
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/ietf-ip:address[ip='192.0.2.1']/prefix-length", "24");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4/czechlight-network:dhcp-client", "false");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:address[ip='2001:db8::1']/prefix-length", "32");
+            client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:autoconf/ietf-ip:create-global-addresses", "false");
+
+            expectedContents = R"([Match]
+Name=eth0
+
+[Network]
+Address=192.0.2.1/24
+Address=2001:db8::1/32
+IPv6AcceptRA=false
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+        }
+
+        REQUIRE_CALL(fake, cb(std::vector<std::string>{"eth0"})).IN_SEQUENCE(seq1);
+        client->apply_changes();
+        REQUIRE(std::filesystem::exists(expectedFilePath));
+        REQUIRE(velia::utils::readFileToString(expectedFilePath) == expectedContents);
+
+        // reset the contents
+        client->delete_item("/ietf-interfaces:interfaces/interface[name='eth0']");
+        REQUIRE_CALL(fake, cb(std::vector<std::string>{"eth0"})).IN_SEQUENCE(seq1);
+        client->apply_changes();
+        REQUIRE(!std::filesystem::exists(expectedFilePath));
+    }
 }
diff --git a/yang/czechlight-network@2021-02-22.yang b/yang/czechlight-network@2021-02-22.yang
index 2d67e8b..7c352aa 100644
--- a/yang/czechlight-network@2021-02-22.yang
+++ b/yang/czechlight-network@2021-02-22.yang
@@ -104,25 +104,32 @@
 		}
 	}
 
+    augment /if:interfaces/if:interface/ip:ipv4 {
+        description "Add the IPv4 autoconfiguration option (DHCP client).";
+
+        leaf dhcp-client {
+            type boolean;
+            default true;
+            description "Enable DHCP client.";
+        }
+    }
+
     deviation /if:interfaces/if:interface/ip:ipv4 {
         deviate add {
-            must 'ip:enabled = "false" or count(ip:address) > 0' {
-                error-message "There must always be at least one IPv4 address unless the protocol is disabled.";
+            must 'ip:enabled = "false" or count(ip:address) > 0 or cla-network:dhcp-client = "true"' {
+                error-message "There must always be at least one IPv4 address or the autoconfiguration must be turned on unless the protocol is disabled.";
             }
         }
     }
 
     deviation /if:interfaces/if:interface/ip:ipv6 {
         deviate add {
-            must 'ip:enabled = "false" or count(ip:address) > 0' {
-                error-message "There must always be at least one IPv6 address unless the protocol is disabled.";
+            must 'ip:enabled = "false" or count(ip:address) > 0 or ip:autoconf/ip:create-global-addresses = "true"' {
+                error-message "There must always be at least one IPv6 address or the autoconfiguration must be turned on unless the protocol is disabled.";
             }
         }
     }
 
-    // IPv6 address autoconfiguration is not supported
-    deviation /if:interfaces/if:interface/ip:ipv6/ip:autoconf { deviate not-supported; }
-
     identity dhcp {
         base rt:routing-protocol;
         description "Identity for route installed by DHCP.";