Commit a5abebaa authored by Tomek Mrugalski's avatar Tomek Mrugalski 🛰
Browse files

[5357] Parser for shared-networks in DHCPv6 implemented.

parent b9c54a65
...@@ -89,7 +89,6 @@ public: ...@@ -89,7 +89,6 @@ public:
cfg->setDhcp4o6Port(dhcp4o6_port); cfg->setDhcp4o6Port(dhcp4o6_port);
} }
/// @brief Copies subnets from shared networks to regular subnets container /// @brief Copies subnets from shared networks to regular subnets container
/// ///
/// @param from pointer to shared networks container (copy from here) /// @param from pointer to shared networks container (copy from here)
...@@ -156,6 +155,13 @@ public: ...@@ -156,6 +155,13 @@ public:
sharedNetworksSanityChecks(const SharedNetwork4Collection& networks, sharedNetworksSanityChecks(const SharedNetwork4Collection& networks,
ConstElementPtr json) { ConstElementPtr json) {
/// @todo: in case of errors, use json to extract line numbers.
if (!json) {
// No json? That means that the shared-networks was never specified
// in the config.
return;
}
// Used for names uniqueness checks. // Used for names uniqueness checks.
std::set<string> names; std::set<string> names;
...@@ -315,6 +321,8 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set, ...@@ -315,6 +321,8 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set,
parser.parse(cfg_option_def, option_defs); parser.parse(cfg_option_def, option_defs);
} }
// This parser is used in several places, so it should be available
// early.
Dhcp4ConfigParser global_parser; Dhcp4ConfigParser global_parser;
// Make parsers grouping. // Make parsers grouping.
...@@ -427,9 +435,10 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set, ...@@ -427,9 +435,10 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set,
/// as well. /// as well.
SharedNetworks4ListParser parser; SharedNetworks4ListParser parser;
CfgSharedNetworks4Ptr cfg = srv_cfg->getCfgSharedNetworks4(); CfgSharedNetworks4Ptr cfg = srv_cfg->getCfgSharedNetworks4();
parser.parse(cfg, config_pair.second); parser.parse(cfg, config_pair.second);
// We also need to put the subnets it contains into normal
// subnets list.
global_parser.copySubnets4(srv_cfg->getCfgSubnets4(), cfg); global_parser.copySubnets4(srv_cfg->getCfgSubnets4(), cfg);
continue; continue;
} }
......
...@@ -604,6 +604,13 @@ public: ...@@ -604,6 +604,13 @@ public:
return (ReturnType()); return (ReturnType());
} }
/// @brief Checks if specified subnet is part of the collection
///
/// @param col collection of subnets to be inspected
/// @param subnet text notation (e.g. 192.0.2.0/24)
/// @param t1 expected renew-timer value
/// @param t2 expected rebind-timer value
/// @param valid expected valid-lifetime value
void void
checkSubnet(const Subnet4Collection& col, std::string subnet, checkSubnet(const Subnet4Collection& col, std::string subnet,
uint32_t t1, uint32_t t2, uint32_t valid) { uint32_t t1, uint32_t t2, uint32_t valid) {
...@@ -5066,17 +5073,15 @@ TEST_F(Dhcp4ParserTest, sharedNetworksName) { ...@@ -5066,17 +5073,15 @@ TEST_F(Dhcp4ParserTest, sharedNetworksName) {
// Now verify that the shared network was indeed configured. // Now verify that the shared network was indeed configured.
CfgSharedNetworks4Ptr cfg_net = CfgMgr::instance().getStagingCfg() CfgSharedNetworks4Ptr cfg_net = CfgMgr::instance().getStagingCfg()
->getCfgSharedNetworks4(); ->getCfgSharedNetworks4();
ASSERT_TRUE(cfg_net); ASSERT_TRUE(cfg_net);
const SharedNetwork4Collection* nets = cfg_net->getAll(); const SharedNetwork4Collection* nets = cfg_net->getAll();
ASSERT_TRUE(nets); ASSERT_TRUE(nets);
ASSERT_EQ(1, nets->size()); ASSERT_EQ(1, nets->size());
SharedNetwork4Ptr net = *(nets->begin()); SharedNetwork4Ptr net = *(nets->begin());
ASSERT_TRUE(net); ASSERT_TRUE(net);
EXPECT_EQ("foo", net->getName()); EXPECT_EQ("foo", net->getName());
// Verify that there are no subnets in this shared-network
const Subnet4Collection * subs = net->getAllSubnets(); const Subnet4Collection * subs = net->getAllSubnets();
ASSERT_TRUE(subs); ASSERT_TRUE(subs);
EXPECT_EQ(0, subs->size()); EXPECT_EQ(0, subs->size());
......
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
#include <dhcpsrv/parsers/ifaces_config_parser.h> #include <dhcpsrv/parsers/ifaces_config_parser.h>
#include <dhcpsrv/parsers/option_data_parser.h> #include <dhcpsrv/parsers/option_data_parser.h>
#include <dhcpsrv/parsers/simple_parser6.h> #include <dhcpsrv/parsers/simple_parser6.h>
#include <dhcpsrv/parsers/shared_networks_list_parser.h>
#include <hooks/hooks_parser.h> #include <hooks/hooks_parser.h>
#include <log/logger_support.h> #include <log/logger_support.h>
#include <util/encode/hex.h> #include <util/encode/hex.h>
...@@ -129,7 +130,12 @@ public: ...@@ -129,7 +130,12 @@ public:
} }
}; };
/// @brief Parser that takes care of global DHCPv6 parameters. /// @brief Parser that takes care of global DHCPv6 parameters and utility
/// functions that work on global level.
///
/// This class is a collection of utility method that either handle
/// global parameters (see @ref parse), or conducts operations on
/// global level (see @ref sanityChecks and @ref copySubnets6).
/// ///
/// See @ref parse method for a list of supported parameters. /// See @ref parse method for a list of supported parameters.
class Dhcp6ConfigParser : public isc::data::SimpleParser { class Dhcp6ConfigParser : public isc::data::SimpleParser {
...@@ -158,6 +164,134 @@ public: ...@@ -158,6 +164,134 @@ public:
uint16_t dhcp4o6_port = getUint16(global, "dhcp4o6-port"); uint16_t dhcp4o6_port = getUint16(global, "dhcp4o6-port");
srv_config->setDhcp4o6Port(dhcp4o6_port); srv_config->setDhcp4o6Port(dhcp4o6_port);
} }
/// @brief Copies subnets from shared networks to regular subnets container
///
/// @param from pointer to shared networks container (copy from here)
/// @param dest pointer to cfg subnets6 (copy to here)
/// @throw BadValue if any pointer is missing
/// @throw DhcpConfigError if there are duplicates (or other subnet defects)
void
copySubnets6(const CfgSubnets6Ptr dest, const CfgSharedNetworks6Ptr from) {
if (!dest || !from) {
isc_throw(BadValue, "Unable to copy subnets: at least one pointer is null");
}
const SharedNetwork6Collection* networks = from->getAll();
if (!networks) {
// Nothing to copy. Technically, it should return a pointer to empty
// container, but let's handle null pointer as well.
return;
}
// Let's go through all the networks one by one
for (auto net = networks->begin(); net != networks->end(); ++net) {
// For each network go through all the subnets in it.
const Subnet6Collection* subnets = (*net)->getAllSubnets();
if (!subnets) {
// Shared network without subnets it weird, but we decided to
// accept such configurations.
continue;
}
// For each subnet, add it to a list of regular subnets.
for (auto subnet = subnets->begin(); subnet != subnets->end(); ++subnet) {
dest->add(*subnet);
}
}
}
/// @brief Conducts global sanity checks
///
/// This method is very simply now, but more sanity checks are expected
/// in the future.
///
/// @param cfg - the parsed structure
/// @param global global Dhcp4 scope
/// @throw DhcpConfigError in case of issues found
void
sanityChecks(SrvConfigPtr cfg, ConstElementPtr global) {
/// Shared network sanity checks
const SharedNetwork6Collection* networks = cfg->getCfgSharedNetworks6()->getAll();
if (networks) {
sharedNetworksSanityChecks(*networks, global->get("shared-networks"));
}
}
/// @brief Sanity checks for shared networks
///
/// This method verifies if there are no issues with shared networks.
/// @param networks pointer to shared networks being checked
/// @param json shared-networks element
/// @throw DhcpConfigError if issues are encountered
void
sharedNetworksSanityChecks(const SharedNetwork6Collection& networks,
ConstElementPtr json) {
/// @todo: in case of errors, use json to extract line numbers.
if (!json) {
// No json? That means that the shared-networks was never specified
// in the config.
return;
}
// Used for names uniqueness checks.
std::set<string> names;
// Let's go through all the networks one by one
for (auto net = networks.begin(); net != networks.end(); ++net) {
string txt;
// Let's check if all subnets have either the same interface
// or don't have the interface specified at all.
string iface = (*net)->getIface();
const Subnet6Collection* subnets = (*net)->getAllSubnets();
if (subnets) {
// For each subnet, add it to a list of regular subnets.
for (auto subnet = subnets->begin(); subnet != subnets->end(); ++subnet) {
if (iface.empty()) {
iface = (*subnet)->getIface();
continue;
}
if ((*subnet)->getIface().empty()) {
continue;
}
if (iface != (*subnet)->getIface()) {
isc_throw(DhcpConfigError, "Subnet " << (*subnet)->toText()
<< " has specified interface " << (*subnet)->getIface()
<< ", but earlier subnet in the same shared-network"
<< " or the shared-network itself used " << iface);
}
// Let's collect the subnets in case we later find out the
// subnet doesn't have a mandatory name.
txt += (*subnet)->toText() + " ";
}
}
// Next, let's check name of the shared network.
if ((*net)->getName().empty()) {
isc_throw(DhcpConfigError, "Shared-network with subnets "
<< txt << " is missing mandatory 'name' parameter");
}
// Is it unique?
if (names.find((*net)->getName()) != names.end()) {
isc_throw(DhcpConfigError, "A shared-network with "
"name " << (*net)->getName() << " defined twice.");
}
names.insert((*net)->getName());
}
}
}; };
} // anonymous namespace } // anonymous namespace
...@@ -270,6 +404,10 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set, ...@@ -270,6 +404,10 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set,
parser.parse(cfg_option_def, option_defs); parser.parse(cfg_option_def, option_defs);
} }
// This parser is used in several places, so it should be available
// early.
Dhcp6ConfigParser global_parser;
BOOST_FOREACH(config_pair, values_map) { BOOST_FOREACH(config_pair, values_map) {
// In principle we could have the following code structured as a series // In principle we could have the following code structured as a series
// of long if else if clauses. That would give a marginal performance // of long if else if clauses. That would give a marginal performance
...@@ -375,19 +513,26 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set, ...@@ -375,19 +513,26 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set,
} }
if (config_pair.first == "subnet6") { if (config_pair.first == "subnet6") {
SrvConfigPtr srv_cfg = CfgMgr::instance().getStagingCfg();
Subnets6ListConfigParser subnets_parser; Subnets6ListConfigParser subnets_parser;
// parse() returns number of subnets parsed. We may log it one day. // parse() returns number of subnets parsed. We may log it one day.
subnets_parser.parse(srv_cfg, config_pair.second); subnets_parser.parse(srv_config, config_pair.second);
continue; continue;
} }
if (config_pair.first == "shared-networks") { if (config_pair.first == "shared-networks") {
/// @todo We need to create instance of SharedNetworks4ListParser /// We need to create instance of SharedNetworks4ListParser
/// and parse the list of the shared networks into the /// and parse the list of the shared networks into the
/// CfgSharedNetworks4 object. One additional step is then to /// CfgSharedNetworks4 object. One additional step is then to
/// add subnets from the CfgSharedNetworks6 into CfgSubnets6 /// add subnets from the CfgSharedNetworks6 into CfgSubnets6
/// as well. /// as well.
SharedNetworks6ListParser parser;
CfgSharedNetworks6Ptr cfg = srv_config->getCfgSharedNetworks6();
parser.parse(cfg, config_pair.second);
// We also need to put the subnets it contains into normal
// subnets list.
global_parser.copySubnets6(srv_config->getCfgSubnets6(), cfg);
continue; continue;
} }
...@@ -417,9 +562,13 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set, ...@@ -417,9 +562,13 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set,
} }
// Apply global options in the staging config. // Apply global options in the staging config.
Dhcp6ConfigParser global_parser;
global_parser.parse(srv_config, mutable_cfg); global_parser.parse(srv_config, mutable_cfg);
// This method conducts final sanity checks and tweaks. In particular,
// it checks that there is no conflict between plain subnets and those
// defined as part of shared networks.
global_parser.sanityChecks(srv_config, mutable_cfg);
} catch (const isc::Exception& ex) { } catch (const isc::Exception& ex) {
LOG_ERROR(dhcp6_logger, DHCP6_PARSER_FAIL) LOG_ERROR(dhcp6_logger, DHCP6_PARSER_FAIL)
.arg(config_pair.first).arg(ex.what()); .arg(config_pair.first).arg(ex.what());
......
...@@ -280,6 +280,62 @@ public: ...@@ -280,6 +280,62 @@ public:
} }
} }
/// @brief Convenience method for running configuration
///
/// This method does not throw, but signals errors using gtest macros.
///
/// @param config text to be parsed as JSON
/// @param expected_code expected code (see cc/command_interpreter.h)
/// @param exp_error expected text error (check skipped if empty)
void configure(std::string config, int expected_code,
std::string exp_error = "") {
ConstElementPtr json;
ASSERT_NO_THROW(json = parseDHCP6(config, true));
ConstElementPtr status;
EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
ASSERT_TRUE(status);
int rcode;
ConstElementPtr comment = parseAnswer(rcode, status);
EXPECT_EQ(expected_code, rcode);
string text;
ASSERT_NO_THROW(text = comment->stringValue());
if (expected_code != rcode) {
std::cout << "Reported status: " << text << std::endl;
}
if ((rcode != 0)) {
if (!exp_error.empty()) {
EXPECT_EQ(exp_error, text);
}
}
}
/// @brief Checks if specified subnet is part of the collection
///
/// @param col collection of subnets to be inspected
/// @param subnet text notation (e.g. 192.0.2.0/24)
/// @param t1 expected renew-timer value
/// @param t2 expected rebind-timer value
/// @param preferred expected preferred-lifetime value
/// @param valid expected valid-lifetime value
void
checkSubnet(const Subnet6Collection& col, std::string subnet,
uint32_t t1, uint32_t t2, uint32_t pref, uint32_t valid) {
const auto& index = col.get<SubnetPrefixIndexTag>();
auto subnet_it = index.find(subnet);
ASSERT_NE(subnet_it, index.cend());
Subnet6Ptr s = *subnet_it;
EXPECT_EQ(t1, s->getT1());
EXPECT_EQ(t2, s->getT2());
EXPECT_EQ(pref, s->getPreferred());
EXPECT_EQ(valid, s->getValid());
}
/// @brief Returns an interface configuration used by the most of the /// @brief Returns an interface configuration used by the most of the
/// unit tests. /// unit tests.
std::string genIfaceConfig() const { std::string genIfaceConfig() const {
...@@ -5060,9 +5116,9 @@ TEST_F(Dhcp6ParserTest, invalidClientClassDictionary) { ...@@ -5060,9 +5116,9 @@ TEST_F(Dhcp6ParserTest, invalidClientClassDictionary) {
" \"bogus\": \"bad\" \n" " \"bogus\": \"bad\" \n"
" } \n" " } \n"
"], \n" "], \n"
"\"subnet4\": [ { \n" "\"subnet6\": [ { \n"
" \"pools\": [ { \"pool\": \"192.0.2.1 - 192.0.2.100\" } ], \n" " \"pools\": [ { \"pool\": \"2001:db8::1 - 2001:db8::ffff\" } ], \n"
" \"subnet\": \"192.0.2.0/24\" \n" " \"subnet\": \"2001:db8::/64\" \n"
" } ] \n" " } ] \n"
"} \n"; "} \n";
...@@ -5303,4 +5359,256 @@ TEST_F(Dhcp6ParserTest, outsideSubnetPool) { ...@@ -5303,4 +5359,256 @@ TEST_F(Dhcp6ParserTest, outsideSubnetPool) {
EXPECT_EQ(expected, text); EXPECT_EQ(expected, text);
} }
// Test verifies that empty shared networks are accepted.
TEST_F(Dhcp6ParserTest, sharedNetworksEmpty) {
string config = "{\n"
"\"valid-lifetime\": 4000, \n"
"\"rebind-timer\": 2000, \n"
"\"renew-timer\": 1000, \n"
"\"subnet6\": [ { \n"
" \"subnet\": \"2001:db8::/48\" \n"
" } ],\n"
"\"shared-networks\": [ ]\n"
"} \n";
configure(config, CONTROL_RESULT_SUCCESS, "");
}
// Test verifies that if a shared network is defined, it at least has to have
// a name.
TEST_F(Dhcp6ParserTest, sharedNetworksNoName) {
string config = "{\n"
"\"valid-lifetime\": 4000, \n"
"\"rebind-timer\": 2000, \n"
"\"renew-timer\": 1000, \n"
"\"subnet6\": [ { \n"
" \"subnet\": \"2001:db8::/48\" \n"
" } ],\n"
"\"shared-networks\": [ { } ]\n"
"} \n";
EXPECT_THROW(parseDHCP6(config, true), Dhcp6ParseError);
}
// Test verifies that empty shared networks are accepted.
TEST_F(Dhcp6ParserTest, sharedNetworksEmptyName) {
string config = "{\n"
"\"valid-lifetime\": 4000, \n"
"\"rebind-timer\": 2000, \n"
"\"renew-timer\": 1000, \n"
"\"subnet6\": [ { \n"
" \"subnet\": \"2001:db8::/48\" \n"
" } ],\n"
"\"shared-networks\": [ { \"name\": \"\" } ]\n"
"} \n";
configure(config, CONTROL_RESULT_ERROR,
"Shared-network with subnets is missing mandatory 'name' parameter");
}
// Test verifies that a degenerated shared-network (no subnets) is
// accepted.
TEST_F(Dhcp6ParserTest, sharedNetworksName) {
string config = "{\n"
"\"subnet6\": [ { \n"
" \"subnet\": \"2001:db8::/48\",\n"
" \"pools\": [ { \"pool\": \"2001:db8::1 - 2001:db8::ffff\" } ]\n"
" } ],\n"
"\"shared-networks\": [ { \"name\": \"foo\" } ]\n"
"} \n";
configure(config, CONTROL_RESULT_SUCCESS, "");
// Now verify that the shared network was indeed configured.
CfgSharedNetworks6Ptr cfg_net = CfgMgr::instance().getStagingCfg()
->getCfgSharedNetworks6();
ASSERT_TRUE(cfg_net);
const SharedNetwork6Collection* nets = cfg_net->getAll();
ASSERT_TRUE(nets);
ASSERT_EQ(1, nets->size());
SharedNetwork6Ptr net = *(nets->begin());
ASSERT_TRUE(net);
EXPECT_EQ("foo", net->getName());
// Verify that there are no subnets in this shared-network
const Subnet6Collection * subs = net->getAllSubnets();
ASSERT_TRUE(subs);
EXPECT_EQ(0, subs->size());
}
// Test verifies that a degenerated shared-network (just one subnet) is
// accepted. Also tests that, unless explicitly specified, the subnet
// gets default values.
TEST_F(Dhcp6ParserTest, sharedNetworks1subnet) {
string config = "{\n"
"\"shared-networks\": [ {\n"
" \"name\": \"foo\"\n,"
" \"subnet6\": [ { \n"
" \"subnet\": \"2001:db8::/48\",\n"
" \"pools\": [ { \"pool\": \"2001:db8::1 - 2001:db8::ffff\" } ]\n"
" } ]\n"
" } ]\n"
"} \n";
configure(config, CONTROL_RESULT_SUCCESS, "");
// Now verify that the shared network was indeed configured.
CfgSharedNetworks6Ptr cfg_net = CfgMgr::instance().getStagingCfg()
->getCfgSharedNetworks6();
ASSERT_TRUE(cfg_net);
// There should be exactly one shared subnet.
const SharedNetwork6Collection* nets = cfg_net->getAll();
ASSERT_TRUE(nets);
ASSERT_EQ(1, nets->size());
SharedNetwork6Ptr net = *(nets->begin());
ASSERT_TRUE(net);
EXPECT_EQ("foo", net->getName());
// It should have one subnet. The subnet should have default values.
const Subnet6Collection * subs = net->getAllSubnets();
ASSERT_TRUE(subs);
EXPECT_EQ(1, subs->size());
checkSubnet(*subs, "2001:db8::/48", 900, 1800, 3600, 7200);
// Now make sure the subnet was added to global list of subnets.
CfgSubnets6Ptr subnets6 = CfgMgr::instance().getStagingCfg()->getCfgSubnets6();
ASSERT_TRUE(subnets6);
subs = subnets6->getAll();
ASSERT_TRUE(subs);
checkSubnet(*subs, "2001:db8::/48", 900, 1800, 3600, 7200);
}
// Test verifies that a a proper shared-network (three subnets) is
// accepted. It verifies several things:
// - that more than one subnet can be added to shared subnets
// - that each subnet being part of the shared subnets is also stored in
// global subnets collection
// - that a subnet can inherit global values
// - that subnet can override global parameters
// - that overridden parameters only affect one subnet and not others
TEST_F(Dhcp6ParserTest, sharedNetworks3subnets) {
string config = "{\n"
"\"renew-timer\": 1000, \n"
"\"rebind-timer\": 2000, \n"
"\"preferred-lifetime\": 3000, \n"
"\"valid-lifetime\": 4000, \n"
"\"shared-networks\": [ {\n"
" \"name\": \"foo\"\n,"
" \"subnet6\": [\n"
" { \n"
" \"subnet\": \"2001:db1::/48\",\n"
" \"pools\": [ { \"pool\": \"2001:db1::/64\" } ]\n"
" },\n"
" { \n"
" \"subnet\": \"2001:db2::/48\",\n"
" \"pools\": [ { \"pool\": \"2001:db2::/64\" } ],\n"
" \"renew-timer\": 2,\n"
" \"rebind-timer\": 22,\n"
" \"preferred-lifetime\": 222,\n"
" \"valid-lifetime\": 2222\n"
" },\n"
" { \n"
" \"subnet\": \"2001:db3::/48\",\n"
" \"pools\": [ { \"pool\": \"2001:db3::/64\" } ]\n"
" }\n"
" ]\n"
" } ]\n"