system: Bridge support for ietf-interfaces config
Introduce a possibility to configure an interface to be enslaved by
another interface.
Our interfaces are not changeable, so the only possible value for a
master interface is a br0 interface, the only bridge in the system.
Change-Id: Ie19192bfed3404781ef4f727695928e50537dd7a
diff --git a/src/system/IETFInterfacesConfig.cpp b/src/system/IETFInterfacesConfig.cpp
index f776d78..421fd85 100644
--- a/src/system/IETFInterfacesConfig.cpp
+++ b/src/system/IETFInterfacesConfig.cpp
@@ -116,8 +116,15 @@
}
// systemd-networkd auto-generates IPv6 link-layer addresses https://www.freedesktop.org/software/systemd/man/systemd.network.html#LinkLocalAddressing=
- // disable this behaviour if ipv6 is disabled
- if (!protocolEnabled(linkEntry, "ipv6")) {
+ // disable this behaviour when IPv6 is disabled or when link enslaved
+ bool isSlave = false;
+
+ if (auto set = linkEntry->find_path("czechlight-network:bridge"); set->number() > 0) {
+ configValues["Network"].push_back("Bridge="s + getValueAsString(set->data().front()));
+ isSlave = true;
+ }
+
+ if (!protocolEnabled(linkEntry, "ipv6") && !isSlave) {
configValues["Network"].push_back("LinkLocalAddressing=no");
}
diff --git a/tests/sysrepo_system-ietfinterfaces.cpp b/tests/sysrepo_system-ietfinterfaces.cpp
index 27c301a..9e7accb 100644
--- a/tests/sysrepo_system-ietfinterfaces.cpp
+++ b/tests/sysrepo_system-ietfinterfaces.cpp
@@ -177,7 +177,7 @@
std::filesystem::remove_all(fakeConfigDir);
std::filesystem::create_directories(fakeConfigDir);
- auto network = std::make_shared<velia::system::IETFInterfacesConfig>(srSess, fakeConfigDir, std::vector<std::string>{"eth0", "eth1"}, [&fake](const std::vector<std::string>& updatedInterfaces) { fake.cb(updatedInterfaces); });
+ auto network = std::make_shared<velia::system::IETFInterfacesConfig>(srSess, fakeConfigDir, std::vector<std::string>{"br0", "eth0", "eth1"}, [&fake](const std::vector<std::string>& updatedInterfaces) { fake.cb(updatedInterfaces); });
std::string expectedContents;
@@ -267,11 +267,12 @@
REQUIRE(!std::filesystem::exists(expectedFilePath));
}
- SECTION("Setting IPs to eth0 and eth1")
+ SECTION("Two links")
{
const auto expectedFilePathEth0 = fakeConfigDir / "eth0.network";
const auto expectedFilePathEth1 = fakeConfigDir / "eth1.network";
- const std::string expectedContentsEth0 = R"([Match]
+
+ std::string expectedContentsEth0 = R"([Match]
Name=eth0
[Network]
@@ -281,7 +282,7 @@
LLDP=true
EmitLLDP=nearest-bridge
)";
- const std::string expectedContentsEth1 = R"([Match]
+ std::string expectedContentsEth1 = R"([Match]
Name=eth1
[Network]
@@ -311,4 +312,189 @@
REQUIRE(!std::filesystem::exists(expectedFilePathEth0));
REQUIRE(!std::filesystem::exists(expectedFilePathEth1));
}
+
+ SECTION("Setup a bridge br0 over eth0 and eth1")
+ {
+ const auto expectedFilePathBr0 = fakeConfigDir / "br0.network";
+ const auto expectedFilePathEth0 = fakeConfigDir / "eth0.network";
+ const auto expectedFilePathEth1 = fakeConfigDir / "eth1.network";
+
+ std::string expectedContentsBr0 = R"([Match]
+Name=br0
+
+[Network]
+LinkLocalAddressing=no
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+
+ std::string expectedContentsEth0 = R"([Match]
+Name=eth0
+
+[Network]
+Bridge=br0
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+
+ std::string expectedContentsEth1 = R"([Match]
+Name=eth1
+
+[Network]
+Bridge=br0
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+
+ // create br0 bridge over eth0 and eth1 with no IP
+ client->set_item_str("/ietf-interfaces:interfaces/interface[name='br0']/enabled", "false");
+ client->set_item_str("/ietf-interfaces:interfaces/interface[name='br0']/type", "iana-if-type:bridge");
+
+ 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']/czechlight-network:bridge", "br0");
+ client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv6/ietf-ip:enabled", "false");
+ client->delete_item("/ietf-interfaces:interfaces/interface[name='eth0']/ietf-ip:ipv4");
+
+ 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']/czechlight-network:bridge", "br0");
+ client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth1']/ietf-ip:ipv4/ietf-ip:enabled", "false");
+ client->delete_item("/ietf-interfaces:interfaces/interface[name='eth1']/ietf-ip:ipv6");
+
+ REQUIRE_CALL(fake, cb(std::vector<std::string>{"br0", "eth0", "eth1"})).IN_SEQUENCE(seq1);
+ client->apply_changes();
+ REQUIRE(std::filesystem::exists(expectedFilePathBr0));
+ REQUIRE(std::filesystem::exists(expectedFilePathEth0));
+ REQUIRE(std::filesystem::exists(expectedFilePathEth1));
+ REQUIRE(velia::utils::readFileToString(expectedFilePathBr0) == expectedContentsBr0);
+ REQUIRE(velia::utils::readFileToString(expectedFilePathEth0) == expectedContentsEth0);
+ REQUIRE(velia::utils::readFileToString(expectedFilePathEth1) == expectedContentsEth1);
+
+ // 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");
+ expectedContentsBr0 = R"([Match]
+Name=br0
+
+[Network]
+Address=192.0.2.1/24
+LinkLocalAddressing=no
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+
+ REQUIRE_CALL(fake, cb(std::vector<std::string>{"br0"})).IN_SEQUENCE(seq1);
+ client->apply_changes();
+ REQUIRE(std::filesystem::exists(expectedFilePathBr0));
+ REQUIRE(std::filesystem::exists(expectedFilePathEth0));
+ REQUIRE(std::filesystem::exists(expectedFilePathEth1));
+ REQUIRE(velia::utils::readFileToString(expectedFilePathBr0) == expectedContentsBr0);
+ REQUIRE(velia::utils::readFileToString(expectedFilePathEth0) == expectedContentsEth0);
+ REQUIRE(velia::utils::readFileToString(expectedFilePathEth1) == expectedContentsEth1);
+
+ // assign also an IPv6 address to br0
+ client->set_item_str("/ietf-interfaces:interfaces/interface[name='br0']/ietf-ip:ipv6/ietf-ip:address[ip='2001:db8::1']/ietf-ip:prefix-length", "32");
+ expectedContentsBr0 = R"([Match]
+Name=br0
+
+[Network]
+Address=192.0.2.1/24
+Address=2001:db8::1/32
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+
+ REQUIRE_CALL(fake, cb(std::vector<std::string>{"br0"})).IN_SEQUENCE(seq1);
+ client->apply_changes();
+ REQUIRE(std::filesystem::exists(expectedFilePathBr0));
+ REQUIRE(std::filesystem::exists(expectedFilePathEth0));
+ REQUIRE(std::filesystem::exists(expectedFilePathEth1));
+ REQUIRE(velia::utils::readFileToString(expectedFilePathBr0) == expectedContentsBr0);
+ REQUIRE(velia::utils::readFileToString(expectedFilePathEth0) == expectedContentsEth0);
+ REQUIRE(velia::utils::readFileToString(expectedFilePathEth1) == expectedContentsEth1);
+
+ // remove eth1 from bridge
+ client->delete_item("/ietf-interfaces:interfaces/interface[name='eth1']/czechlight-network:bridge");
+ client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth1']/ietf-ip:ipv6/ietf-ip:address[ip='2001:db8::2']/ietf-ip:prefix-length", "32");
+
+ expectedContentsEth1 = R"([Match]
+Name=eth1
+
+[Network]
+Address=2001:db8::2/32
+DHCP=no
+LLDP=true
+EmitLLDP=nearest-bridge
+)";
+
+ REQUIRE_CALL(fake, cb(std::vector<std::string>{"eth1"})).IN_SEQUENCE(seq1);
+ client->apply_changes();
+ REQUIRE(std::filesystem::exists(expectedFilePathBr0));
+ REQUIRE(std::filesystem::exists(expectedFilePathEth0));
+ REQUIRE(std::filesystem::exists(expectedFilePathEth1));
+ REQUIRE(velia::utils::readFileToString(expectedFilePathBr0) == expectedContentsBr0);
+ REQUIRE(velia::utils::readFileToString(expectedFilePathEth0) == expectedContentsEth0);
+ REQUIRE(velia::utils::readFileToString(expectedFilePathEth1) == expectedContentsEth1);
+
+ // reset the contents
+ client->delete_item("/ietf-interfaces:interfaces/interface[name='br0']");
+ client->delete_item("/ietf-interfaces:interfaces/interface[name='eth0']");
+ client->delete_item("/ietf-interfaces:interfaces/interface[name='eth1']");
+ REQUIRE_CALL(fake, cb(std::vector<std::string>{"br0", "eth0", "eth1"})).IN_SEQUENCE(seq1);
+ client->apply_changes();
+ REQUIRE(!std::filesystem::exists(expectedFilePathBr0));
+ REQUIRE(!std::filesystem::exists(expectedFilePathEth0));
+ REQUIRE(!std::filesystem::exists(expectedFilePathEth1));
+ }
+
+ SECTION("Slave interface and enabled/disabled IP protocols")
+ {
+ const auto expectedFilePathBr0 = fakeConfigDir / "br0.network";
+ const auto expectedFilePathEth0 = fakeConfigDir / "eth0.network";
+
+ client->set_item_str("/ietf-interfaces:interfaces/interface[name='br0']/type", "iana-if-type:bridge");
+ 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='eth0']/type", "iana-if-type:ethernetCsmacd");
+ client->set_item_str("/ietf-interfaces:interfaces/interface[name='eth0']/czechlight-network:bridge", "br0");
+
+ SECTION("Can't be a slave when IPv4 enabled")
+ {
+ 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");
+ REQUIRE_THROWS_AS(client->apply_changes(), sysrepo::sysrepo_exception);
+ }
+
+ SECTION("Can't be a slave when IPv6 enabled")
+ {
+ 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");
+ REQUIRE_THROWS_AS(client->apply_changes(), sysrepo::sysrepo_exception);
+ }
+
+ SECTION("Can't be a slave when both IPv4 and IPv6 enabled")
+ {
+ 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");
+ REQUIRE_THROWS_AS(client->apply_changes(), sysrepo::sysrepo_exception);
+ }
+
+ SECTION("Can be a slave when addresses present but protocol is disabled")
+ {
+ 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/enabled", "false");
+
+ REQUIRE_CALL(fake, cb(std::vector<std::string>{"br0", "eth0"})).IN_SEQUENCE(seq1);
+ client->apply_changes();
+
+ // reset the contents
+ client->delete_item("/ietf-interfaces:interfaces/interface[name='br0']");
+ client->delete_item("/ietf-interfaces:interfaces/interface[name='eth0']");
+ REQUIRE_CALL(fake, cb(std::vector<std::string>{"br0", "eth0"})).IN_SEQUENCE(seq1);
+ client->apply_changes();
+ REQUIRE(!std::filesystem::exists(expectedFilePathBr0));
+ REQUIRE(!std::filesystem::exists(expectedFilePathEth0));
+ }
+ }
}
diff --git a/yang/czechlight-network@2021-02-22.yang b/yang/czechlight-network@2021-02-22.yang
index fbdd6fb..2d67e8b 100644
--- a/yang/czechlight-network@2021-02-22.yang
+++ b/yang/czechlight-network@2021-02-22.yang
@@ -73,16 +73,36 @@
}
}
+ augment /if:interfaces/if:interface {
+ description "Add the option to add this link to a bridge.";
+
+ leaf bridge {
+ must '. = "br0"' {
+ error-message "br0 is the only available bridge interface.";
+ }
+
+ when '(not(../ip:ipv4) or ../ip:ipv4/ip:enabled[.="false"]) and
+ (not(../ip:ipv6) or ../ip:ipv6/ip:enabled[.="false"])' {
+ description "IP protocols must be disabled for enslaved link.";
+ }
+
+ type if:interface-ref;
+ mandatory false;
+ description "The name of the bridge to add the link to.";
+ }
+ }
+
// Make it hard to accidentally lose connection by removing ipv4/ipv6 presence containers or all IP addresses
deviation /if:interfaces/if:interface {
deviate add {
must 'if:enabled = "false" or
ip:ipv4/ip:enabled = "true" or
- ip:ipv6/ip:enabled = "true"' {
+ ip:ipv6/ip:enabled = "true" or
+ cla-network:bridge' {
error-message "There must always be at least one protocol active unless the interface is disabled.";
}
- }
- }
+ }
+ }
deviation /if:interfaces/if:interface/ip:ipv4 {
deviate add {