diff --git a/doc/sphinx/api-files.txt b/doc/sphinx/api-files.txt index 4e363d09b4..ea026dfd67 100644 --- a/doc/sphinx/api-files.txt +++ b/doc/sphinx/api-files.txt @@ -69,6 +69,9 @@ src/share/api/lease6-wipe.json src/share/api/lease6-write.json src/share/api/leases-reclaim.json src/share/api/list-commands.json +src/share/api/localize4.json +src/share/api/localize4o6.json +src/share/api/localize6.json src/share/api/network4-add.json src/share/api/network4-del.json src/share/api/network4-get.json diff --git a/doc/sphinx/arm/ctrl-channel.rst b/doc/sphinx/arm/ctrl-channel.rst index 24bdf59e87..a162cd0b80 100644 --- a/doc/sphinx/arm/ctrl-channel.rst +++ b/doc/sphinx/arm/ctrl-channel.rst @@ -864,6 +864,37 @@ command-line argument. This command does not take any parameters. "command": "version-get" } +Commands Supported by the DHCPv4 Server +======================================= + +.. isccmd:: localize4 +.. _command-localize4: + +The ``localize4`` Command +------------------------- + +The :isccmd:`localize4` command returns the result of DHCPv4 subnet selection. + +.. isccmd:: localize4o6 +.. _command-localize4o6: + +The ``localize4o6`` Command +--------------------------- + +The :isccmd:`localize4o6` command returns the result of DHCPv4-over-DHCPv6 +subnet selection. + +Commands Supported by the DHCPv6 Server +======================================= + +.. isccmd:: localize6 +.. _command-localize6: + +The ``localize6`` Command +------------------------- + +The :isccmd:`localize6` command returns the result of DHCPv6 subnet selection. + Commands Supported by the D2 Server =================================== diff --git a/src/bin/dhcp4/ctrl_dhcp4_srv.cc b/src/bin/dhcp4/ctrl_dhcp4_srv.cc index 8e7d785d87..4dab839f0a 100644 --- a/src/bin/dhcp4/ctrl_dhcp4_srv.cc +++ b/src/bin/dhcp4/ctrl_dhcp4_srv.cc @@ -695,6 +695,337 @@ ControlledDhcpv4Srv::commandLeasesReclaimHandler(const string&, return (answer); } +ConstElementPtr +ControlledDhcpv4Srv::commandLocalize4Handler(const string&, + ConstElementPtr args) { + if (!args) { + return (createAnswer(CONTROL_RESULT_ERROR, "empty arguments")); + } + if (args->getType() != Element::map) { + return (createAnswer(CONTROL_RESULT_ERROR, "arguments must be a map")); + } + bool ignore_link_sel = + CfgMgr::instance().getCurrentCfg()->getIgnoreRAILinkSelection(); + SubnetSelector selector; + for (auto const& entry : args->mapValue()) { + ostringstream errmsg; + if (entry.first == "interface") { + if (entry.second->getType() != Element::string) { + errmsg << "'interface' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.iface_name_ = entry.second->stringValue(); + continue; + } else if (entry.first == "address") { + if (entry.second->getType() != Element::string) { + errmsg << "'address' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'address' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.ciaddr_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'address' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "relay") { + if (entry.second->getType() != Element::string) { + errmsg << "'relay' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'relay' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.giaddr_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'relay' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "local") { + if (entry.second->getType() != Element::string) { + errmsg << "'local' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'local' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.local_address_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'local' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "remote") { + if (entry.second->getType() != Element::string) { + errmsg << "'remote' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'remote' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.remote_address_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'remote' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "link") { + if (entry.second->getType() != Element::string) { + errmsg << "'link' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'link' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + if (!ignore_link_sel) { + selector.option_select_ = addr; + } + continue; + } catch (const exception& ex) { + errmsg << "bad 'link' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "subnet") { + // RAI link-selection has precedence over subnet-selection. + if (args->contains("link") && !ignore_link_sel) { + continue; + } + if (entry.second->getType() != Element::string) { + errmsg << "'subnet' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'subnet' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.option_select_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'subnet' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "classes") { + if (entry.second->getType() != Element::list) { + return (createAnswer(CONTROL_RESULT_ERROR, + "'classes' entry must be a list")); + } + for (auto const& item : entry.second->listValue()) { + if (!item || (item->getType() != Element::string)) { + errmsg << "'classes' entry must be a list of strings"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + // Skip empty client classes. + if (!item->stringValue().empty()) { + selector.client_classes_.insert(item->stringValue()); + } + } + continue; + } else { + errmsg << "unknown entry '" << entry.first << "'"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } + ConstSubnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()-> + getCfgSubnets4()->selectSubnet(selector); + if (!subnet) { + return (createAnswer(CONTROL_RESULT_EMPTY, "no selected subnet")); + } + SharedNetwork4Ptr network; + subnet->getSharedNetwork(subnet); + ostringstream msg; + if (network) { + msg << "selected shared network '" << network->getName() + << "' starting with subnet '" << subnet->toText() + << "' id " << subnet->getID(); + } else { + msg << "selected subnet '" << subnet->toText() + << "' id " << subnet->getID(); + } + return (createAnswer(CONTROL_RESULT_SUCCESS, msg.str())); +} + +ConstElementPtr +ControlledDhcpv4Srv::commandLocalize4o6Handler(const string&, + ConstElementPtr args) { + if (!args) { + return (createAnswer(CONTROL_RESULT_ERROR, "empty arguments")); + } + if (args->getType() != Element::map) { + return (createAnswer(CONTROL_RESULT_ERROR, "arguments must be a map")); + } + SubnetSelector selector; + selector.dhcp4o6_ = true; + for (auto const& entry : args->mapValue()) { + ostringstream errmsg; + if (entry.first == "interface") { + if (entry.second->getType() != Element::string) { + errmsg << "'interface' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.iface_name_ = entry.second->stringValue(); + continue; + } else if (entry.first == "address") { + if (entry.second->getType() != Element::string) { + errmsg << "'address' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'address' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.ciaddr_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'address' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "relay") { + if (entry.second->getType() != Element::string) { + errmsg << "'relay' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'relay' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.giaddr_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'relay' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "local") { + if (entry.second->getType() != Element::string) { + errmsg << "'local' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'local' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.local_address_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'local' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "remote") { + if (entry.second->getType() != Element::string) { + errmsg << "'remote' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'remote' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.remote_address_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'remote' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "link") { + if (entry.second->getType() != Element::string) { + errmsg << "'link' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV6()) { + errmsg << "bad 'link' entry: not IPv6"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.first_relay_linkaddr_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'link' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "subnet") { + if (entry.second->getType() != Element::string) { + errmsg << "'subnet' entry must be a string"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + try { + IOAddress addr(entry.second->stringValue()); + if (!addr.isV4()) { + errmsg << "bad 'subnet' entry: not IPv4"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + selector.option_select_ = addr; + continue; + } catch (const exception& ex) { + errmsg << "bad 'subnet' entry: " << ex.what(); + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } else if (entry.first == "classes") { + if (entry.second->getType() != Element::list) { + return (createAnswer(CONTROL_RESULT_ERROR, + "'classes' entry must be a list")); + } + for (auto const& item : entry.second->listValue()) { + if (!item || (item->getType() != Element::string)) { + errmsg << "'classes' entry must be a list of strings"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + // Skip empty client classes. + if (!item->stringValue().empty()) { + selector.client_classes_.insert(item->stringValue()); + } + } + continue; + } else { + errmsg << "unknown entry '" << entry.first << "'"; + return (createAnswer(CONTROL_RESULT_ERROR, errmsg.str())); + } + } + ConstSubnet4Ptr subnet = CfgMgr::instance().getCurrentCfg()-> + getCfgSubnets4()->selectSubnet4o6(selector); + if (!subnet) { + return (createAnswer(CONTROL_RESULT_EMPTY, "no selected subnet")); + } + SharedNetwork4Ptr network; + subnet->getSharedNetwork(subnet); + ostringstream msg; + if (network) { + msg << "selected shared network '" << network->getName() + << "' starting with subnet '" << subnet->toText() + << "' id " << subnet->getID(); + } else { + msg << "selected subnet '" << subnet->toText() + << "' id " << subnet->getID(); + } + return (createAnswer(CONTROL_RESULT_SUCCESS, msg.str())); +} + ConstElementPtr ControlledDhcpv4Srv::commandServerTagGetHandler(const std::string&, ConstElementPtr) { @@ -1133,6 +1464,12 @@ ControlledDhcpv4Srv::ControlledDhcpv4Srv(uint16_t server_port /*= DHCP4_SERVER_P CommandMgr::instance().registerCommand("leases-reclaim", std::bind(&ControlledDhcpv4Srv::commandLeasesReclaimHandler, this, ph::_1, ph::_2)); + CommandMgr::instance().registerCommand("localize4", + std::bind(&ControlledDhcpv4Srv::commandLocalize4Handler, this, ph::_1, ph::_2)); + + CommandMgr::instance().registerCommand("localize4o6", + std::bind(&ControlledDhcpv4Srv::commandLocalize4o6Handler, this, ph::_1, ph::_2)); + CommandMgr::instance().registerCommand("server-tag-get", std::bind(&ControlledDhcpv4Srv::commandServerTagGetHandler, this, ph::_1, ph::_2)); @@ -1216,6 +1553,8 @@ ControlledDhcpv4Srv::~ControlledDhcpv4Srv() { CommandMgr::instance().deregisterCommand("dhcp-enable"); CommandMgr::instance().deregisterCommand("leases-reclaim"); CommandMgr::instance().deregisterCommand("server-tag-get"); + CommandMgr::instance().deregisterCommand("localize4"); + CommandMgr::instance().deregisterCommand("localize4o6"); CommandMgr::instance().deregisterCommand("shutdown"); CommandMgr::instance().deregisterCommand("statistic-get"); CommandMgr::instance().deregisterCommand("statistic-get-all"); diff --git a/src/bin/dhcp4/ctrl_dhcp4_srv.h b/src/bin/dhcp4/ctrl_dhcp4_srv.h index 1c8e6dc8ad..0b826ad7c6 100644 --- a/src/bin/dhcp4/ctrl_dhcp4_srv.h +++ b/src/bin/dhcp4/ctrl_dhcp4_srv.h @@ -279,6 +279,32 @@ private: commandLeasesReclaimHandler(const std::string& command, isc::data::ConstElementPtr args); + /// @brief Handler for processing 'localize4' command + /// + /// This handler processes localize4 command, which returns + /// the result of DHCPv4 subnet selected. + /// + /// @param command (parameter ignored) + /// @param args arguments map { : } + /// + /// @return status of the command with the selection result + isc::data::ConstElementPtr + commandLocalize4Handler(const std::string& command, + isc::data::ConstElementPtr args); + + /// @brief Handler for processing 'localize4o6' command + /// + /// This handler processes localize4o6 command, which returns + /// the result of DHCP4o6 subnet selected. + /// + /// @param command (parameter ignored) + /// @param args arguments map { : } + /// + /// @return status of the command with the selection result + isc::data::ConstElementPtr + commandLocalize4o6Handler(const std::string& command, + isc::data::ConstElementPtr args); + /// @brief handler for server-tag-get command /// /// This method handles the server-tag-get command, which retrieves diff --git a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc index 632e1df759..3208afd8f3 100644 --- a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc @@ -482,6 +482,8 @@ TEST_F(CtrlChannelDhcpv4SrvTest, commandsRegistration) { EXPECT_TRUE(command_list.find("\"config-set\"") != string::npos); EXPECT_TRUE(command_list.find("\"config-write\"") != string::npos); EXPECT_TRUE(command_list.find("\"leases-reclaim\"") != string::npos); + EXPECT_TRUE(command_list.find("\"localize4\"") != string::npos); + EXPECT_TRUE(command_list.find("\"localize4o6\"") != string::npos); EXPECT_TRUE(command_list.find("\"server-tag-get\"") != string::npos); EXPECT_TRUE(command_list.find("\"shutdown\"") != string::npos); EXPECT_TRUE(command_list.find("\"statistic-get\"") != string::npos); diff --git a/src/share/api/api_files.mk b/src/share/api/api_files.mk index cc3fc5f555..44596f86bf 100644 --- a/src/share/api/api_files.mk +++ b/src/share/api/api_files.mk @@ -69,6 +69,9 @@ api_files += $(top_srcdir)/src/share/api/lease6-wipe.json api_files += $(top_srcdir)/src/share/api/lease6-write.json api_files += $(top_srcdir)/src/share/api/leases-reclaim.json api_files += $(top_srcdir)/src/share/api/list-commands.json +api_files += $(top_srcdir)/src/share/api/localize4.json +api_files += $(top_srcdir)/src/share/api/localize4o6.json +api_files += $(top_srcdir)/src/share/api/localize6.json api_files += $(top_srcdir)/src/share/api/network4-add.json api_files += $(top_srcdir)/src/share/api/network4-del.json api_files += $(top_srcdir)/src/share/api/network4-get.json diff --git a/src/share/api/localize4.json b/src/share/api/localize4.json new file mode 100644 index 0000000000..aa1e790020 --- /dev/null +++ b/src/share/api/localize4.json @@ -0,0 +1,22 @@ +{ + "access": "read", + "avail": "2.7.0", + "brief": [ "This command returns the result of DHCPv4 subnet selection" ], + "cmd-comment": [ "Possible parameters are the incoming interface name, client address, relay address, client classes, etc." ], + "cmd-syntax": [ + "{", + " \"command\": \"localize4\",", + " \"arguments\": {", + " \"interface\": ,", + " \"address\": ,", + " \"relay\": ,", + " \"classes\": [ ]", + " }", + "}" + ], + "description": [ "See " ], + "name": "localize4", + "resp-comment": [ "3 forms for selected shared network, subnet or nothing.." ], + "resp-syntax": [ "{ \"result\": 1, \"text\": }" ], + "support": [ "kea-dhcp4" ] +} diff --git a/src/share/api/localize4o6.json b/src/share/api/localize4o6.json new file mode 100644 index 0000000000..7f182375f3 --- /dev/null +++ b/src/share/api/localize4o6.json @@ -0,0 +1,22 @@ +{ + "access": "read", + "avail": "2.7.0", + "brief": [ "This command returns the result of DHCPv4o6 subnet selection" ], + "cmd-comment": [ "Possible parameters are the incoming interface name, client address, relay address, client classes, etc." ], + "cmd-syntax": [ + "{", + " \"command\": \"localize4o6\",", + " \"arguments\": {", + " \"interface\": ,", + " \"address\": ,", + " \"relay\": ,", + " \"classes\": [ ]", + " }", + "}" + ], + "description": [ "See " ], + "name": "localize4o6", + "resp-comment": [ "3 forms for selected shared network, subnet or nothing.." ], + "resp-syntax": [ "{ \"result\": 1, \"text\": }" ], + "support": [ "kea-dhcp4" ] +} diff --git a/src/share/api/localize6.json b/src/share/api/localize6.json new file mode 100644 index 0000000000..7d082d282e --- /dev/null +++ b/src/share/api/localize6.json @@ -0,0 +1,22 @@ +{ + "access": "read", + "avail": "2.7.0", + "brief": [ "This command returns the result of DHCPv6 subnet selection" ], + "cmd-comment": [ "Possible parameters are the incoming interface name, client address, relay address, client classes, etc." ], + "cmd-syntax": [ + "{", + " \"command\": \"localize6\",", + " \"arguments\": {", + " \"interface\": ,", + " \"address\": ,", + " \"relay\": ,", + " \"classes\": [ ]", + " }", + "}" + ], + "description": [ "See " ], + "name": "localize6", + "resp-comment": [ "3 forms for selected shared network, subnet or nothing.." ], + "resp-syntax": [ "{ \"result\": 1, \"text\": }" ], + "support": [ "kea-dhcp6" ] +}