Commit 007bbb7e authored by Marcin Siodelski's avatar Marcin Siodelski
Browse files

[#715] Added support for associating option defs with server tags.

parent 55117255
......@@ -114,6 +114,7 @@ public:
DELETE_ALL_SHARED_NETWORKS4,
DELETE_OPTION_DEF4_CODE_NAME,
DELETE_ALL_OPTION_DEFS4,
DELETE_ALL_OPTION_DEFS4_UNASSIGNED,
DELETE_OPTION4,
DELETE_ALL_GLOBAL_OPTIONS4_UNASSIGNED,
DELETE_OPTION4_SUBNET_ID,
......@@ -1872,6 +1873,18 @@ public:
in_bindings));
}
/// @brief Removes unassigned global parameters, global options and
/// option definitions.
///
/// This function is called when one or more servers are deleted and
/// it is likely that there are some orhpaned configuration elements
/// left in the database. This method removes those elements.
void purgeUnassignedConfig() {
multipleUpdateDeleteQueries(DELETE_ALL_GLOBAL_PARAMETERS4_UNASSIGNED,
DELETE_ALL_GLOBAL_OPTIONS4_UNASSIGNED,
DELETE_ALL_OPTION_DEFS4_UNASSIGNED);
}
/// @brief Attempts to delete a server having a given tag.
///
/// @param server_tag Tag of the server to be deleted.
......@@ -1904,16 +1917,9 @@ public:
in_bindings);
// If we have deleted any servers we have to remove any dangling global
// parameters.
// parameters, options and option definitions.
if (count > 0) {
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::
DELETE_ALL_GLOBAL_PARAMETERS4_UNASSIGNED,
MySqlBindingCollection());
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::
DELETE_ALL_GLOBAL_OPTIONS4_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete option definitions.
purgeUnassignedConfig();
}
transaction.commit();
......@@ -1944,17 +1950,9 @@ public:
in_bindings);
// If we have deleted any servers we have to remove any dangling global
// parameters.
// parameters, options and option definitions.
if (count > 0) {
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::
DELETE_ALL_GLOBAL_PARAMETERS4_UNASSIGNED,
MySqlBindingCollection());
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::
DELETE_ALL_GLOBAL_OPTIONS4_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling option definitions.
purgeUnassignedConfig();
}
transaction.commit();
......@@ -2378,6 +2376,11 @@ TaggedStatementArray tagged_statements = { {
MYSQL_DELETE_OPTION_DEF(dhcp4)
},
// Delete all option definitions which are assigned to no servers.
{ MySqlConfigBackendDHCPv4Impl::DELETE_ALL_OPTION_DEFS4_UNASSIGNED,
MYSQL_DELETE_OPTION_DEF_UNASSIGNED(dhcp4)
},
// Delete single global option.
{ MySqlConfigBackendDHCPv4Impl::DELETE_OPTION4,
MYSQL_DELETE_OPTION(dhcp4, AND o.scope_id = 0 AND o.code = ? AND o.space = ?)
......
......@@ -120,6 +120,7 @@ public:
DELETE_ALL_SHARED_NETWORKS6,
DELETE_OPTION_DEF6_CODE_NAME,
DELETE_ALL_OPTION_DEFS6,
DELETE_ALL_OPTION_DEFS6_UNASSIGNED,
DELETE_OPTION6,
DELETE_ALL_GLOBAL_OPTIONS6_UNASSIGNED,
DELETE_OPTION6_SUBNET_ID,
......@@ -2187,6 +2188,18 @@ public:
in_bindings));
}
/// @brief Removes unassigned global parameters, global options and
/// option definitions.
///
/// This function is called when one or more servers are deleted and
/// it is likely that there are some orhpaned configuration elements
/// left in the database. This method removes those elements.
void purgeUnassignedConfig() {
multipleUpdateDeleteQueries(DELETE_ALL_GLOBAL_PARAMETERS6_UNASSIGNED,
DELETE_ALL_GLOBAL_OPTIONS6_UNASSIGNED,
DELETE_ALL_OPTION_DEFS6_UNASSIGNED);
}
/// @brief Attempts to delete a server having a given tag.
///
/// @param server_tag Tag of the server to be deleted.
......@@ -2219,16 +2232,9 @@ public:
in_bindings);
// If we have deleted any servers we have to remove any dangling global
// parameters.
// parameters, options and option definitions.
if (count > 0) {
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::
DELETE_ALL_GLOBAL_PARAMETERS6_UNASSIGNED,
MySqlBindingCollection());
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::
DELETE_ALL_GLOBAL_OPTIONS6_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling option definitions.
purgeUnassignedConfig();
}
transaction.commit();
......@@ -2259,16 +2265,9 @@ public:
in_bindings);
// If we have deleted any servers we have to remove any dangling global
// parameters.
// parameters, options and option definitions.
if (count > 0) {
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::
DELETE_ALL_GLOBAL_PARAMETERS6_UNASSIGNED,
MySqlBindingCollection());
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::
DELETE_ALL_GLOBAL_OPTIONS6_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling option definitions.
purgeUnassignedConfig();
}
transaction.commit();
......@@ -2735,6 +2734,11 @@ TaggedStatementArray tagged_statements = { {
MYSQL_DELETE_OPTION_DEF(dhcp6)
},
// Delete all option definitions which are assigned to no servers.
{ MySqlConfigBackendDHCPv6Impl::DELETE_ALL_OPTION_DEFS6_UNASSIGNED,
MYSQL_DELETE_OPTION_DEF_UNASSIGNED(dhcp6)
},
// Delete single global option.
{ MySqlConfigBackendDHCPv6Impl::DELETE_OPTION6,
MYSQL_DELETE_OPTION(dhcp6, AND o.scope_id = 0 AND o.code = ? AND o.space = ?)
......
......@@ -387,14 +387,16 @@ MySqlConfigBackendImpl::getOptionDefs(const int index,
uint64_t last_def_id = 0;
OptionDefContainer local_option_defs;
// Run select query.
conn_.selectQuery(index, in_bindings, out_bindings,
[&option_defs, &last_def_id]
[&local_option_defs, &last_def_id]
(MySqlBindingCollection& out_bindings) {
// Get pointer to last fetched option definition.
OptionDefinitionPtr last_def;
if (!option_defs.empty()) {
last_def = *option_defs.rbegin();
if (!local_option_defs.empty()) {
last_def = *local_option_defs.rbegin();
}
// See if the last fetched definition is the one for which we now got
......@@ -453,14 +455,47 @@ MySqlConfigBackendImpl::getOptionDefs(const int index,
last_def->setModificationTime(out_bindings[5]->getTimestamp());
// server_tag
last_def->setServerTag(out_bindings[10]->getString());
ServerTag last_def_server_tag(out_bindings[10]->getString());
last_def->setServerTag(last_def_server_tag.get());
// If we're fetching option definitions for a given server
// (explicit server tag is provided), it takes precedence over
// the same option definition specified for all servers.
// Therefore, we check if the given option already exists and
// belongs to 'all'.
auto& index = local_option_defs.get<1>();
auto existing_it_pair = index.equal_range(last_def->getCode());
auto existing_it = existing_it_pair.first;
bool found = false;
for ( ; existing_it != existing_it_pair.second; ++existing_it) {
if ((*existing_it)->getOptionSpaceName() == last_def->getOptionSpaceName()) {
found = true;
// This option definition was already fetched. Let's check
// if we should replace it or not.
if (!last_def_server_tag.amAll() && (*existing_it)->hasAllServerTag()) {
index.replace(existing_it, last_def);
return;
}
break;
}
}
// Store created option definition.
// (option_defs is a multi-index container with no unique
// indexes so push_back can't fail).
static_cast<void>(option_defs.push_back(last_def));
// If there is no such option definition yet or the existing option
// definition belongs to a different server and the inserted option
// definition is not for all servers.
if (!found ||
(!(*existing_it)->hasServerTag(last_def_server_tag) &&
!last_def_server_tag.amAll())) {
static_cast<void>(local_option_defs.push_back(last_def));
}
}
});
// Append the option definition fetched by this function into the container
// supplied by the caller. The container supplied by the caller may already
// hold some option definitions fetched for other server tags.
option_defs.insert(option_defs.end(), local_option_defs.begin(),
local_option_defs.end());
}
void
......@@ -497,7 +532,10 @@ MySqlConfigBackendImpl::createUpdateOptionDef(const db::ServerSelector& server_s
MySqlBinding::createBool(option_def->getArrayType()),
MySqlBinding::createString(option_def->getEncapsulatedSpace()),
record_types_binding,
createInputContextBinding(option_def)
createInputContextBinding(option_def),
MySqlBinding::createString(tag),
MySqlBinding::createInteger<uint16_t>(option_def->getCode()),
MySqlBinding::createString(option_def->getOptionSpaceName())
};
MySqlTransaction transaction(conn_);
......@@ -525,15 +563,9 @@ MySqlConfigBackendImpl::createUpdateOptionDef(const db::ServerSelector& server_s
"option definition set",
true);
if (existing_definition) {
// Need to add three more bindings for WHERE clause.
in_bindings.push_back(MySqlBinding::createString(tag));
in_bindings.push_back(MySqlBinding::createInteger<uint16_t>(existing_definition->getCode()));
in_bindings.push_back(MySqlBinding::createString(existing_definition->getOptionSpaceName()));
conn_.updateDeleteQuery(update_option_def, in_bindings);
} else {
// If the option definition doesn't exist, let's insert it.
if (conn_.updateDeleteQuery(update_option_def, in_bindings) == 0) {
// Remove the bindings used only during the update.
in_bindings.resize(in_bindings.size() - 3);
conn_.insertQuery(insert_option_def, in_bindings);
// Fetch unique identifier of the inserted option definition and use it
......
......@@ -25,6 +25,7 @@
#include <set>
#include <sstream>
#include <string>
#include <vector>
namespace isc {
namespace dhcp {
......@@ -631,6 +632,28 @@ public:
const int& update_index,
const db::ServerPtr& server);
/// @brief Executes multiple update and/or delete queries with no input
/// bindings.
///
/// This is a convenience function which takes multiple query indexes as
/// arguments and for each index executes an update or delete query.
/// One of the applications of this function is to remove dangling
/// configuration elements after the server associated with these elements
/// have been deleted.
///
/// @tparam T type of the indexes, e.g. @c MySqlConfigBackendDHCPv4Impl::StatementIndex.
/// @tparam R parameter pack holding indexes of type @c T.
/// @param first_index first index.
/// @param other_indexes remaining indexes.
template<typename T, typename... R>
void multipleUpdateDeleteQueries(T first_index, R... other_indexes) {
std::vector<T> indexes({ first_index, other_indexes... });
db::MySqlBindingCollection empty_bindings;
for (auto index : indexes) {
conn_.updateDeleteQuery(index, empty_bindings);
}
}
/// @brief Returns backend type in the textual format.
///
/// @return "mysql".
......
......@@ -657,6 +657,14 @@ namespace {
"WHERE s.tag = ? " #__VA_ARGS__
#endif
#ifndef MYSQL_DELETE_OPTION_DEF_UNASSIGNED
#define MYSQL_DELETE_OPTION_DEF_UNASSIGNED(table_prefix, ...) \
"DELETE d FROM " #table_prefix "_option_def AS d " \
"LEFT JOIN " #table_prefix "_option_def_server AS a " \
" ON d.id = a.option_def_id " \
"WHERE a.option_def_id IS NULL " #__VA_ARGS__
#endif
#ifndef MYSQL_DELETE_OPTION
#define MYSQL_DELETE_OPTION(table_prefix, ...) \
"DELETE o FROM " #table_prefix "_options AS o " \
......
......@@ -263,6 +263,10 @@ public:
option_def.reset(new OptionDefinition("whale", 236, "string"));
option_def->setOptionSpaceName("xyz");
test_option_defs_.push_back(option_def);
option_def.reset(new OptionDefinition("foobar", 234, "uint64", true));
option_def->setOptionSpaceName("dhcp4");
test_option_defs_.push_back(option_def);
}
/// @brief Creates several DHCP options used in tests.
......@@ -1780,26 +1784,217 @@ TEST_F(MySqlConfigBackendDHCPv4Test, getOptionDef4) {
}
}
// This test verifies that it is possible to differentiate between the
// option definitions by server tag and that the option definition
// specified for the particular server overrides the definition for
// all servers.
TEST_F(MySqlConfigBackendDHCPv4Test, optionDefs4WithServerTags) {
OptionDefinitionPtr option1 = test_option_defs_[0];
OptionDefinitionPtr option2 = test_option_defs_[1];
OptionDefinitionPtr option3 = test_option_defs_[4];
// An attempt to create option definition for non-existing server should
// fail.
EXPECT_THROW(cbptr_->createUpdateOptionDef4(ServerSelector::ONE("server1"),
option1),
DbOperationError);
// Create two servers.
EXPECT_NO_THROW(cbptr_->createUpdateServer4(test_servers_[1]));
{
SCOPED_TRACE("server1 is created");
testNewAuditEntry("dhcp4_server",
AuditEntry::ModificationType::CREATE,
"server set");
}
EXPECT_NO_THROW(cbptr_->createUpdateServer4(test_servers_[2]));
{
SCOPED_TRACE("server2 is created");
testNewAuditEntry("dhcp4_server",
AuditEntry::ModificationType::CREATE,
"server set");
}
// This time creation of the option definition for the server1 should pass.
EXPECT_NO_THROW(cbptr_->createUpdateOptionDef4(ServerSelector::ONE("server1"),
option1));
{
SCOPED_TRACE("option definition for server1 is set");
// The value of 3 means there should be 3 audit entries available for the
// server1, two that indicate creation of the servers and one that we
// validate, which sets the option definition.
testNewAuditEntry("dhcp4_option_def",
AuditEntry::ModificationType::CREATE,
"option definition set",
ServerSelector::ONE("server1"),
3, 1);
}
// Creation of the option definition for the server2 should also pass.
EXPECT_NO_THROW(cbptr_->createUpdateOptionDef4(ServerSelector::ONE("server2"),
option2));
{
SCOPED_TRACE("option definition for server2 is set");
// Same as in case of the server1, there should be 3 audit entries and
// we validate one of them.
testNewAuditEntry("dhcp4_option_def",
AuditEntry::ModificationType::CREATE,
"option definition set",
ServerSelector::ONE("server2"),
3, 1);
}
// Finally, creation of the option definition for all servers should
// also pass.
EXPECT_NO_THROW(cbptr_->createUpdateOptionDef4(ServerSelector::ALL(),
option3));
{
SCOPED_TRACE("option definition for server2 is set");
// There should be one new audit entry for all servers. It logs
// the insertion of the option definition.
testNewAuditEntry("dhcp4_option_def",
AuditEntry::ModificationType::CREATE,
"option definition set",
ServerSelector::ALL(),
1, 1);
}
OptionDefinitionPtr returned_option_def;
// Try to fetch the option definition specified for all servers. It should
// return the third one.
EXPECT_NO_THROW(
returned_option_def = cbptr_->getOptionDef4(ServerSelector::ALL(),
option3->getCode(),
option3->getOptionSpaceName())
);
ASSERT_TRUE(returned_option_def);
EXPECT_TRUE(returned_option_def->equals(*option3));
// Try to fetch the option definition specified for server1. It should
// override the definition for all servers.
EXPECT_NO_THROW(
returned_option_def = cbptr_->getOptionDef4(ServerSelector::ONE("server1"),
option1->getCode(),
option1->getOptionSpaceName())
);
ASSERT_TRUE(returned_option_def);
EXPECT_TRUE(returned_option_def->equals(*option1));
// The same in case of the server2.
EXPECT_NO_THROW(
returned_option_def = cbptr_->getOptionDef4(ServerSelector::ONE("server2"),
option2->getCode(),
option2->getOptionSpaceName())
);
ASSERT_TRUE(returned_option_def);
EXPECT_TRUE(returned_option_def->equals(*option2));
OptionDefContainer returned_option_defs;
// Try to fetch the collection of the option definitions for server1, server2
// and server3. The server3 does not have an explicit option definition, so
// for this server we should get the definition associated with "all" servers.
EXPECT_NO_THROW(
returned_option_defs = cbptr_->getAllOptionDefs4(ServerSelector::
MULTIPLE({ "server1", "server2",
"server3" }));
);
ASSERT_EQ(3, returned_option_defs.size());
// Check that expected option definitions have been returned.
auto current_option = returned_option_defs.begin();
EXPECT_TRUE((*current_option)->equals(*option1));
EXPECT_TRUE((*(++current_option))->equals(*option2));
EXPECT_TRUE((*(++current_option))->equals(*option3));
// Try to fetch the collection of options specified for all servers.
// This excludes the options specific to server1 and server2. It returns
// only the common ones.
EXPECT_NO_THROW(
returned_option_defs = cbptr_->getAllOptionDefs4(ServerSelector::ALL());
);
ASSERT_EQ(1, returned_option_defs.size());
EXPECT_TRUE((*returned_option_defs.begin())->equals(*option3));
// Delete the server1. It should remove associations of this server with the
// option definitions and the option definition itself.
EXPECT_NO_THROW(cbptr_->deleteServer4(ServerTag("server1")));
EXPECT_NO_THROW(
returned_option_defs = cbptr_->getAllOptionDefs4(ServerSelector::ONE("server1"));
);
ASSERT_EQ(1, returned_option_defs.size());
EXPECT_TRUE((*returned_option_defs.begin())->equals(*option3));
{
SCOPED_TRACE("DELETE audit entry for the option definition after server deletion");
testNewAuditEntry("dhcp4_option_def",
AuditEntry::ModificationType::DELETE,
"deleting a server", ServerSelector::ONE("server1"),
2, 1);
}
// Attempt to delete option definition for server1.
uint64_t deleted_num = 0;
EXPECT_NO_THROW(deleted_num = cbptr_->deleteOptionDef4(ServerSelector::ONE("server1"),
option1->getCode(),
option1->getOptionSpaceName()));
EXPECT_EQ(0, deleted_num);
// Deleting the existing option definition for server2 should succeed.
EXPECT_NO_THROW(deleted_num = cbptr_->deleteOptionDef4(ServerSelector::ONE("server2"),
option2->getCode(),
option2->getOptionSpaceName()));
EXPECT_EQ(1, deleted_num);
// Create this option definition again to test that deletion of all servers
// removes it too.
EXPECT_NO_THROW(cbptr_->createUpdateOptionDef4(ServerSelector::ONE("server2"),
option2));
// Delete all servers, except 'all'.
EXPECT_NO_THROW(deleted_num = cbptr_->deleteAllServers4());
EXPECT_NO_THROW(
returned_option_defs = cbptr_->getAllOptionDefs4(ServerSelector::ALL());
);
EXPECT_EQ(1, deleted_num);
EXPECT_EQ(1, returned_option_defs.size());
EXPECT_TRUE((*returned_option_defs.begin())->equals(*option3));
{
SCOPED_TRACE("DELETE audit entry for the option definition after deletion of"
" all servers");
testNewAuditEntry("dhcp4_option_def",
AuditEntry::ModificationType::DELETE,
"deleting all servers", ServerSelector::ONE("server2"),
4, 1);
}
}
// Test that all option definitions can be fetched.
TEST_F(MySqlConfigBackendDHCPv4Test, getAllOptionDefs4) {
// Insert test option definitions into the database. Note that the second
// option definition will overwrite the first option definition as they use
// the same code and space.
size_t updates_num = 0;
for (auto option_def : test_option_defs_) {
cbptr_->createUpdateOptionDef4(ServerSelector::ALL(), option_def);
// That option definition overrides the first one so the audit entry should
// indicate an update.
if (option_def->getName() == "bar") {
SCOPED_TRACE("UPDATE audit entry for the option definition " +
option_def->getName());
auto name = option_def->getName();
if (name.find("bar") != std::string::npos) {
SCOPED_TRACE("UPDATE audit entry for the option definition " + name);
testNewAuditEntry("dhcp4_option_def",
AuditEntry::ModificationType::UPDATE,
"option definition set");
++updates_num;
} else {
SCOPED_TRACE("CREATE audit entry for the option defnition " +
option_def->getName());
SCOPED_TRACE("CREATE audit entry for the option defnition " + name);
testNewAuditEntry("dhcp4_option_def",
AuditEntry::ModificationType::CREATE,
"option definition set");
......@@ -1808,12 +2003,12 @@ TEST_F(MySqlConfigBackendDHCPv4Test, getAllOptionDefs4) {
// Fetch all option_definitions.
OptionDefContainer option_defs = cbptr_->getAllOptionDefs4(ServerSelector::ALL());
ASSERT_EQ(test_option_defs_.size() - 1, option_defs.size());
ASSERT_EQ(test_option_defs_.size() - updates_num, option_defs.size());
// All option definitions should also be returned for explicitly specified
// server tag.
option_defs = cbptr_->getAllOptionDefs4(ServerSelector::ONE("server1"));
ASSERT_EQ(test_option_defs_.size() - 1, option_defs.size());
ASSERT_EQ(test_option_defs_.size() - updates_num, option_defs.size());
// See if option definitions are returned ok.
for (auto def = option_defs.begin(); def != option_defs.end(); ++def) {
......@@ -1833,7 +2028,7 @@ TEST_F(MySqlConfigBackendDHCPv4Test, getAllOptionDefs4) {
EXPECT_EQ(0, cbptr_->deleteOptionDef4(ServerSelector::ALL(),
99, "non-exiting-space"));
// All option definitions should be still there.
ASSERT_EQ(test_option_defs_.size() - 1, option_defs.size());
ASSERT_EQ(test_option_defs_.size() - updates_num, option_defs.size());
// Should not delete option definition for explicit server tag
// because our option definition is for all servers.
......
......@@ -302,6 +302,10 @@ public:
option_def.reset(new OptionDefinition("whale", 20236, "string"));
option_def->setOptionSpaceName("xyz");
test_option_defs_.push_back(option_def);
option_def.reset(new OptionDefinition("bar", 1234, "uint64", true));
option_def->setOptionSpaceName("dhcp6");
test_option_defs_.push_back(option_def);
}
/// @brief Creates several DHCP options used in tests.
......@@ -1800,11 +1804,202 @@ TEST_F(MySqlConfigBackendDHCPv6Test, getOptionDef6) {
}
}
// This test verifies that it is possible to differentiate between the
// option definitions by server tag and that the option definition
// specified for the particular server overrides the definition for
// all servers.
TEST_F(MySqlConfigBackendDHCPv6Test, optionDefs6WithServerTags) {
OptionDefinitionPtr option1 = test_option_defs_[0];
OptionDefinitionPtr option2 = test_option_defs_[1];
OptionDefinitionPtr option3 = test_option_defs_[4];
// An attempt to create option definition for non-existing server should
// fail.
EXPECT_THROW(cbptr_->createUpdateOptionDef6(ServerSelector::ONE("server1"),
option1),
DbOperationError);
// Create two servers.
EXPECT_NO_THROW(cbptr_->createUpdateServer6(test_servers_[1]));
{
SCOPED_TRACE("server1 is created");