blob: 77fbf5c3a676ce113e3a01d92064d88be13b6e59 [file] [log] [blame]
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +02001/*
2 * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
3 *
4 * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
5 *
6*/
7
8#include "trompeloeil_doctest.h"
9#include <boost/algorithm/string/join.hpp>
10#include <boost/process.hpp>
11#include <boost/process/extend.hpp>
12#include <cstdlib>
13#include <netlink/route/addr.h>
14#include <regex>
15#include <sys/wait.h>
16#include <thread>
17#include "pretty_printers.h"
18#include "system/IETFInterfaces.h"
19#include "test_log_setup.h"
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020020#include "test_vars.h"
Tomáš Peckac164ca62024-01-24 13:38:03 +010021#include "tests/sysrepo-helpers/common.h"
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020022#include "utils/exec.h"
23
24using namespace std::chrono_literals;
25using namespace std::string_literals;
26
27namespace {
28
29const auto IFACE = "czechlight0"s;
30const auto LINK_MAC = "02:02:02:02:02:02"s;
Jan Kundrát09fc7012023-01-17 00:25:55 +010031const auto WAIT = 500ms;
Tomáš Pecka9e2291b2021-07-14 20:39:30 +020032const auto WAIT_BRIDGE = 2500ms;
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020033
34template <class... Args>
35void iproute2_run(const Args... args_)
36{
37 namespace bp = boost::process;
38 auto logger = spdlog::get("main");
39
40 bp::ipstream stdoutStream;
41 bp::ipstream stderrStream;
42
43 std::vector<std::string> args = {IPROUTE2_EXECUTABLE, args_...};
44
Tomáš Peckafd5ec7b2023-02-02 18:48:37 +010045 logger->trace("exec: {}", boost::algorithm::join(args, " "));
46 bp::child c(boost::process::args = std::move(args), bp::std_out > stdoutStream, bp::std_err > stderrStream);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020047 c.wait();
Tomáš Peckafd5ec7b2023-02-02 18:48:37 +010048 logger->trace("{} exited", IPROUTE2_EXECUTABLE);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020049
50 if (c.exit_code() != 0) {
51 std::istreambuf_iterator<char> begin(stderrStream), end;
52 std::string stderrOutput(begin, end);
Tomáš Peckafd5ec7b2023-02-02 18:48:37 +010053 logger->critical("{} ended with a non-zero exit code. stderr: {}", IPROUTE2_EXECUTABLE, stderrOutput);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020054
Tomáš Peckafd5ec7b2023-02-02 18:48:37 +010055 throw std::runtime_error(IPROUTE2_EXECUTABLE + " returned non-zero exit code "s + std::to_string(c.exit_code()));
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020056 }
57}
58
59template <class... Args>
Tomáš Peckaf9108932021-06-01 10:19:36 +020060void iproute2_exec_and_wait(const auto& wait, const Args... args_)
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020061{
62 iproute2_run(args_...);
Tomáš Peckaf9108932021-06-01 10:19:36 +020063 std::this_thread::sleep_for(wait); // wait for velia to process and publish the change
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020064}
65
66
67template <class T>
68void nlCacheForeachWrapper(nl_cache* cache, std::function<void(T*)> cb)
69{
70 nl_cache_foreach(
71 cache, [](nl_object* obj, void* data) {
72 auto& cb = *static_cast<std::function<void(T*)>*>(data);
73 auto link = reinterpret_cast<T*>(obj);
74 cb(link);
75 },
76 &cb);
77}
78
Václav Kubernát7efd6d52021-11-09 01:31:11 +010079auto dataFromSysrepoNoStatistics(sysrepo::Session session, const std::string& xpath, sysrepo::Datastore datastore)
Tomáš Peckaf9108932021-06-01 10:19:36 +020080{
81 auto res = dataFromSysrepo(session, xpath, datastore);
Tomáš Peckaf9108932021-06-01 10:19:36 +020082 REQUIRE(res.erase("/statistics/in-octets") == 1);
83 REQUIRE(res.erase("/statistics/in-errors") == 1);
84 REQUIRE(res.erase("/statistics/in-discards") == 1);
85 REQUIRE(res.erase("/statistics/out-octets") == 1);
86 REQUIRE(res.erase("/statistics/out-errors") == 1);
87 REQUIRE(res.erase("/statistics/out-discards") == 1);
88 return res;
89}
90
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +020091}
92
93TEST_CASE("Test ietf-interfaces and ietf-routing")
94{
95 TEST_SYSREPO_INIT_LOGS;
96 TEST_SYSREPO_INIT;
97 TEST_SYSREPO_INIT_CLIENT;
98
99 auto network = std::make_shared<velia::system::IETFInterfaces>(srSess);
100
Tomáš Peckaf9108932021-06-01 10:19:36 +0200101 iproute2_exec_and_wait(WAIT, "link", "add", IFACE, "address", LINK_MAC, "type", "dummy");
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200102
Tomáš Peckaf9108932021-06-01 10:19:36 +0200103 iproute2_exec_and_wait(WAIT, "addr", "add", "192.0.2.1/24", "dev", IFACE); // from TEST-NET-1 (RFC 5737)
104 iproute2_exec_and_wait(WAIT, "addr", "add", "::ffff:192.0.2.1", "dev", IFACE);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200105
106 std::map<std::string, std::string> initialExpected{
107 {"/ietf-ip:ipv4", ""},
108 {"/ietf-ip:ipv4/address[ip='192.0.2.1']", ""},
109 {"/ietf-ip:ipv4/address[ip='192.0.2.1']/ip", "192.0.2.1"},
110 {"/ietf-ip:ipv4/address[ip='192.0.2.1']/prefix-length", "24"},
111 {"/ietf-ip:ipv6", ""},
112 {"/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']", ""},
113 {"/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']/ip", "::ffff:192.0.2.1"},
114 {"/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']/prefix-length", "128"},
Tomáš Peckabd43a942021-08-04 10:12:49 +0200115 {"/ietf-ip:ipv6/autoconf", ""},
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200116 {"/name", IFACE},
117 {"/oper-status", "down"},
118 {"/phys-address", LINK_MAC},
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100119 {"/statistics", ""},
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200120 {"/type", "iana-if-type:ethernetCsmacd"},
121 };
122
123 SECTION("Change physical address")
124 {
125 const auto LINK_MAC_CHANGED = "02:44:44:44:44:44"s;
126
Tomáš Peckaf9108932021-06-01 10:19:36 +0200127 iproute2_exec_and_wait(WAIT, "link", "set", IFACE, "address", LINK_MAC_CHANGED);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200128
129 std::map<std::string, std::string> expected = initialExpected;
130 expected["/phys-address"] = LINK_MAC_CHANGED;
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100131 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expected);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200132 }
133
134 SECTION("Add and remove IP addresses")
135 {
Tomáš Peckaf9108932021-06-01 10:19:36 +0200136 iproute2_exec_and_wait(WAIT, "addr", "add", "192.0.2.6/24", "dev", IFACE);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200137 std::map<std::string, std::string> expected = initialExpected;
138 expected["/ietf-ip:ipv4/address[ip='192.0.2.6']"] = "";
139 expected["/ietf-ip:ipv4/address[ip='192.0.2.6']/ip"] = "192.0.2.6";
140 expected["/ietf-ip:ipv4/address[ip='192.0.2.6']/prefix-length"] = "24";
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100141 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expected);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200142
Tomáš Peckaf9108932021-06-01 10:19:36 +0200143 iproute2_exec_and_wait(WAIT, "addr", "del", "192.0.2.6/24", "dev", IFACE);
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100144 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == initialExpected);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200145 }
146
147 SECTION("IPv6 LL gained when device up")
148 {
Tomáš Peckaf9108932021-06-01 10:19:36 +0200149 iproute2_exec_and_wait(WAIT, "link", "set", "dev", IFACE, "up");
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200150
151 {
152 std::map<std::string, std::string> expected = initialExpected;
153 expected["/ietf-ip:ipv6/address[ip='fe80::2:2ff:fe02:202']"] = "";
154 expected["/ietf-ip:ipv6/address[ip='fe80::2:2ff:fe02:202']/ip"] = "fe80::2:2ff:fe02:202";
155 expected["/ietf-ip:ipv6/address[ip='fe80::2:2ff:fe02:202']/prefix-length"] = "64";
156 expected["/oper-status"] = "unknown";
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100157 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expected);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200158 }
159
Tomáš Peckaf9108932021-06-01 10:19:36 +0200160 iproute2_exec_and_wait(WAIT, "link", "set", "dev", IFACE, "down"); // this discards all addresses, i.e., the link-local address and the ::ffff:192.0.2.1 address
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200161 {
162 std::map<std::string, std::string> expected = initialExpected;
163 expected.erase("/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']");
164 expected.erase("/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']/ip");
165 expected.erase("/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']/prefix-length");
166 expected["/oper-status"] = "down";
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100167 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expected);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200168 }
169 }
170
Tomáš Peckaf9108932021-06-01 10:19:36 +0200171 SECTION("Add a bridge")
172 {
173 const auto IFACE_BRIDGE = "czechlight_br0"s;
174 const auto MAC_BRIDGE = "02:22:22:22:22:22";
175
176 std::map<std::string, std::string> expectedIface = initialExpected;
177 std::map<std::string, std::string> expectedBridge{
178 {"/name", "czechlight_br0"},
179 {"/oper-status", "down"},
180 {"/phys-address", MAC_BRIDGE},
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100181 {"/statistics", ""},
Tomáš Peckaf9108932021-06-01 10:19:36 +0200182 {"/type", "iana-if-type:bridge"},
183 };
184
185 iproute2_exec_and_wait(WAIT, "link", "add", "name", IFACE_BRIDGE, "address", MAC_BRIDGE, "type", "bridge");
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100186 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expectedIface);
187 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE_BRIDGE + "']", sysrepo::Datastore::Operational) == expectedBridge);
Tomáš Peckaf9108932021-06-01 10:19:36 +0200188
189 iproute2_exec_and_wait(WAIT, "link", "set", "dev", IFACE, "master", IFACE_BRIDGE);
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100190 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expectedIface);
191 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE_BRIDGE + "']", sysrepo::Datastore::Operational) == expectedBridge);
Tomáš Peckaf9108932021-06-01 10:19:36 +0200192
193 iproute2_exec_and_wait(WAIT, "link", "set", "dev", IFACE, "up");
Tomáš Peckaee9fbaa2021-12-14 15:39:06 +0100194 iproute2_exec_and_wait(WAIT, "addr", "flush", "dev", IFACE); // sometimes, addresses are preserved even when enslaved
Tomáš Peckaf9108932021-06-01 10:19:36 +0200195 expectedIface["/oper-status"] = "unknown";
Tomáš Peckaee9fbaa2021-12-14 15:39:06 +0100196 expectedIface.erase("/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']");
197 expectedIface.erase("/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']/ip");
198 expectedIface.erase("/ietf-ip:ipv6/address[ip='::ffff:192.0.2.1']/prefix-length");
199 expectedIface.erase("/ietf-ip:ipv4/address[ip='192.0.2.1']");
200 expectedIface.erase("/ietf-ip:ipv4/address[ip='192.0.2.1']/ip");
201 expectedIface.erase("/ietf-ip:ipv4/address[ip='192.0.2.1']/prefix-length");
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100202 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expectedIface);
203 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE_BRIDGE + "']", sysrepo::Datastore::Operational) == expectedBridge);
Tomáš Peckaf9108932021-06-01 10:19:36 +0200204
205 iproute2_exec_and_wait(WAIT_BRIDGE, "link", "set", "dev", IFACE_BRIDGE, "up");
Tomáš Pecka9e2291b2021-07-14 20:39:30 +0200206 expectedBridge["/ietf-ip:ipv6"] = "";
Tomáš Peckabd43a942021-08-04 10:12:49 +0200207 expectedBridge["/ietf-ip:ipv6/autoconf"] = "";
Tomáš Pecka9e2291b2021-07-14 20:39:30 +0200208 expectedBridge["/ietf-ip:ipv6/address[ip='fe80::22:22ff:fe22:2222']"] = "";
209 expectedBridge["/ietf-ip:ipv6/address[ip='fe80::22:22ff:fe22:2222']/ip"] = "fe80::22:22ff:fe22:2222";
210 expectedBridge["/ietf-ip:ipv6/address[ip='fe80::22:22ff:fe22:2222']/prefix-length"] = "64";
Tomáš Peckaf9108932021-06-01 10:19:36 +0200211 expectedBridge["/oper-status"] = "up";
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100212 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expectedIface);
213 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE_BRIDGE + "']", sysrepo::Datastore::Operational) == expectedBridge);
Tomáš Peckaf9108932021-06-01 10:19:36 +0200214
215 iproute2_exec_and_wait(WAIT_BRIDGE, "link", "set", "dev", IFACE_BRIDGE, "down");
Tomáš Pecka9e2291b2021-07-14 20:39:30 +0200216 expectedBridge.erase("/ietf-ip:ipv6/address[ip='fe80::22:22ff:fe22:2222']");
217 expectedBridge.erase("/ietf-ip:ipv6/address[ip='fe80::22:22ff:fe22:2222']/ip");
218 expectedBridge.erase("/ietf-ip:ipv6/address[ip='fe80::22:22ff:fe22:2222']/prefix-length");
Tomáš Peckaf9108932021-06-01 10:19:36 +0200219 expectedBridge["/oper-status"] = "down";
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100220 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expectedIface);
221 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE_BRIDGE + "']", sysrepo::Datastore::Operational) == expectedBridge);
Tomáš Peckaf9108932021-06-01 10:19:36 +0200222
223 iproute2_exec_and_wait(WAIT, "link", "set", "dev", IFACE, "down");
Tomáš Peckaf9108932021-06-01 10:19:36 +0200224 expectedIface["/oper-status"] = "down";
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100225 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expectedIface);
226 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE_BRIDGE + "']", sysrepo::Datastore::Operational) == expectedBridge);
Tomáš Peckaf9108932021-06-01 10:19:36 +0200227 iproute2_exec_and_wait(WAIT, "link", "set", "dev", IFACE, "nomaster");
Tomáš Peckaf9108932021-06-01 10:19:36 +0200228 expectedIface.erase("/ietf-ip:ipv4");
Tomáš Peckabd43a942021-08-04 10:12:49 +0200229 expectedIface.erase("/ietf-ip:ipv6/autoconf");
Tomáš Peckaf9108932021-06-01 10:19:36 +0200230 expectedIface.erase("/ietf-ip:ipv6");
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100231 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE + "']", sysrepo::Datastore::Operational) == expectedIface);
232 REQUIRE(dataFromSysrepoNoStatistics(client, "/ietf-interfaces:interfaces/interface[name='" + IFACE_BRIDGE + "']", sysrepo::Datastore::Operational) == expectedBridge);
Tomáš Peckaf9108932021-06-01 10:19:36 +0200233 }
234
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200235 SECTION("Add and remove routes")
236 {
Tomáš Peckaf9108932021-06-01 10:19:36 +0200237 iproute2_exec_and_wait(WAIT, "link", "set", "dev", IFACE, "up");
238 iproute2_exec_and_wait(WAIT, "route", "add", "198.51.100.0/24", "dev", IFACE);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200239 std::this_thread::sleep_for(WAIT);
240
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100241 auto data = dataFromSysrepo(client, "/ietf-routing:routing", sysrepo::Datastore::Operational);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200242 REQUIRE(data["/control-plane-protocols"] == "");
243 REQUIRE(data["/interfaces"] == "");
244 REQUIRE(data["/ribs"] == "");
245
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100246 data = dataFromSysrepo(client, "/ietf-routing:routing/ribs/rib[name='ipv4-master']", sysrepo::Datastore::Operational);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200247 REQUIRE(data["/name"] == "ipv4-master");
248
249 auto findRouteIndex = [&data](const std::string& prefix) {
250 std::smatch match;
251 std::regex regex(R"(route\[(\d+)\])");
252 size_t length = 0;
253 for (const auto& [key, value] : data) {
254 if (std::regex_search(key, match, regex)) {
255 length = std::max(std::stoul(match[1]), length);
256 }
257 }
258
259 for (size_t i = 1; i <= length; i++) {
260 const auto keyPrefix = "/routes/route["s + std::to_string(i) + "]";
261 if (data[keyPrefix + "/ietf-ipv4-unicast-routing:destination-prefix"] == prefix)
262 return i;
263 }
264
265 return size_t{0};
266 };
267
268 {
269 auto routeIdx = findRouteIndex("198.51.100.0/24");
270 REQUIRE(routeIdx > 0);
271 REQUIRE(data["/routes/route["s + std::to_string(routeIdx) + "]/next-hop/outgoing-interface"] == IFACE);
Jan Kundrát6141d582023-09-27 11:23:38 +0200272 REQUIRE(data["/routes/route["s + std::to_string(routeIdx) + "]/source-protocol"] == "ietf-routing:static");
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200273 }
274 {
275 auto routeIdx = findRouteIndex("192.0.2.0/24");
276 REQUIRE(routeIdx > 0);
277 REQUIRE(data["/routes/route["s + std::to_string(routeIdx) + "]/next-hop/outgoing-interface"] == IFACE);
Jan Kundrát6141d582023-09-27 11:23:38 +0200278 REQUIRE(data["/routes/route["s + std::to_string(routeIdx) + "]/source-protocol"] == "ietf-routing:direct");
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200279 }
280
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100281 data = dataFromSysrepo(client, "/ietf-routing:routing/ribs/rib[name='ipv6-master']", sysrepo::Datastore::Operational);
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200282 REQUIRE(data["/name"] == "ipv6-master");
283
Tomáš Peckaf9108932021-06-01 10:19:36 +0200284 iproute2_exec_and_wait(WAIT, "route", "del", "198.51.100.0/24");
285 iproute2_exec_and_wait(WAIT, "link", "set", IFACE, "down");
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200286 }
287
Tomáš Peckaf9108932021-06-01 10:19:36 +0200288 iproute2_exec_and_wait(WAIT, "link", "del", IFACE, "type", "dummy"); // Executed later again by ctest fixture cleanup just for sure. It remains here because of doctest sections: The interface needs to be setup again.
Tomáš Pecka9fa5f6a2021-04-13 11:36:11 +0200289}