Commit ed7befb6 authored by Marcin Siodelski's avatar Marcin Siodelski
Browse files

[#714,!409] Associate global options with the server tags.

parent 711c1dca
......@@ -115,6 +115,7 @@ public:
DELETE_OPTION_DEF4_CODE_NAME,
DELETE_ALL_OPTION_DEFS4,
DELETE_OPTION4,
DELETE_ALL_OPTIONS4_UNASSIGNED,
DELETE_OPTION4_SUBNET_ID,
DELETE_OPTION4_POOL_RANGE,
DELETE_OPTION4_SHARED_NETWORK,
......@@ -1441,7 +1442,10 @@ public:
createInputContextBinding(option),
MySqlBinding::createNull(),
MySqlBinding::createNull(),
MySqlBinding::createTimestamp(option->getModificationTime())
MySqlBinding::createTimestamp(option->getModificationTime()),
MySqlBinding::createString(tag),
MySqlBinding::createInteger<uint8_t>(option->option_->getType()),
MySqlBinding::condCreateString(option->space_name_)
};
MySqlTransaction transaction(conn_);
......@@ -1456,16 +1460,11 @@ public:
MySqlConfigBackendDHCPv4Impl::CREATE_AUDIT_REVISION,
server_selector, "global option set", false);
if (existing_option) {
in_bindings.push_back(MySqlBinding::createString(tag));
in_bindings.push_back(MySqlBinding::createInteger<uint8_t>(option->option_->getType()));
in_bindings.push_back(MySqlBinding::condCreateString(option->space_name_));
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::UPDATE_OPTION4,
in_bindings);
} else {
if (conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::UPDATE_OPTION4,
in_bindings) == 0) {
// Remove the 3 bindings used only in case of update.
in_bindings.resize(in_bindings.size() - 3);
insertOption4(server_selector, in_bindings);
}
transaction.commit();
......@@ -1931,7 +1930,11 @@ public:
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::
DELETE_ALL_GLOBAL_PARAMETERS4_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling options and option definitions.
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::
DELETE_ALL_OPTIONS4_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete option definitions.
}
transaction.commit();
......@@ -1967,7 +1970,12 @@ public:
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::
DELETE_ALL_GLOBAL_PARAMETERS4_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling options and option definitions.
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv4Impl::
DELETE_ALL_OPTIONS4_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling option definitions.
}
transaction.commit();
......@@ -2396,6 +2404,11 @@ TaggedStatementArray tagged_statements = { {
MYSQL_DELETE_OPTION(dhcp4, AND o.scope_id = 0 AND o.code = ? AND o.space = ?)
},
// Delete all options which are unassigned to any servers.
{ MySqlConfigBackendDHCPv4Impl::DELETE_ALL_OPTIONS4_UNASSIGNED,
MYSQL_DELETE_OPTION_UNASSIGNED(dhcp4)
},
// Delete single option from a subnet.
{ MySqlConfigBackendDHCPv4Impl::DELETE_OPTION4_SUBNET_ID,
MYSQL_DELETE_OPTION(dhcp4,
......
......@@ -121,6 +121,7 @@ public:
DELETE_OPTION_DEF6_CODE_NAME,
DELETE_ALL_OPTION_DEFS6,
DELETE_OPTION6,
DELETE_ALL_OPTIONS6_UNASSIGNED,
DELETE_OPTION6_SUBNET_ID,
DELETE_OPTION6_POOL_RANGE,
DELETE_OPTION6_PD_POOL,
......@@ -1653,7 +1654,10 @@ public:
MySqlBinding::createNull(),
MySqlBinding::createNull(),
MySqlBinding::createTimestamp(option->getModificationTime()),
MySqlBinding::createNull()
MySqlBinding::createNull(),
MySqlBinding::createString(tag),
MySqlBinding::createInteger<uint8_t>(option->option_->getType()),
MySqlBinding::condCreateString(option->space_name_)
};
MySqlTransaction transaction(conn_);
......@@ -1668,16 +1672,11 @@ public:
MySqlConfigBackendDHCPv6Impl::CREATE_AUDIT_REVISION,
server_selector, "global option set", false);
if (existing_option) {
in_bindings.push_back(MySqlBinding::createString(tag));
in_bindings.push_back(MySqlBinding::createInteger<uint16_t>(option->option_->getType()));
in_bindings.push_back(MySqlBinding::condCreateString(option->space_name_));
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::UPDATE_OPTION6,
in_bindings);
} else {
if (conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::UPDATE_OPTION6,
in_bindings) == 0) {
// Remove the 3 bindings used only in case of update.
in_bindings.resize(in_bindings.size() - 3);
insertOption6(server_selector, in_bindings);
}
transaction.commit();
......@@ -2245,7 +2244,11 @@ public:
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::
DELETE_ALL_GLOBAL_PARAMETERS6_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling options and option definitions.
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::
DELETE_ALL_OPTIONS6_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling option definitions.
}
transaction.commit();
......@@ -2281,7 +2284,11 @@ public:
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::
DELETE_ALL_GLOBAL_PARAMETERS6_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling options and option definitions.
conn_.updateDeleteQuery(MySqlConfigBackendDHCPv6Impl::
DELETE_ALL_OPTIONS6_UNASSIGNED,
MySqlBindingCollection());
/// @todo delete dangling option definitions.
}
transaction.commit();
......@@ -2753,6 +2760,11 @@ TaggedStatementArray tagged_statements = { {
MYSQL_DELETE_OPTION(dhcp6, AND o.scope_id = 0 AND o.code = ? AND o.space = ?)
},
// Delete all options which are unassigned to any servers.
{ MySqlConfigBackendDHCPv6Impl::DELETE_ALL_OPTIONS6_UNASSIGNED,
MYSQL_DELETE_OPTION_UNASSIGNED(dhcp6)
},
// Delete single option from a subnet.
{ MySqlConfigBackendDHCPv6Impl::DELETE_OPTION6_SUBNET_ID,
MYSQL_DELETE_OPTION(dhcp6,
......
......@@ -762,8 +762,10 @@ MySqlConfigBackendImpl::getOptions(const int index,
uint64_t last_option_id = 0;
OptionContainer local_options;
conn_.selectQuery(index, in_bindings, out_bindings,
[this, universe, &options, &last_option_id]
[this, universe, &local_options, &last_option_id]
(MySqlBindingCollection& out_bindings) {
// Parse option.
if (!out_bindings[0]->amNull() &&
......@@ -774,11 +776,46 @@ MySqlConfigBackendImpl::getOptions(const int index,
OptionDescriptorPtr desc = processOptionRow(universe, out_bindings.begin());
if (desc) {
// server_tag for the global option
desc->setServerTag(out_bindings[12]->getString());
static_cast<void>(options.push_back(*desc));
ServerTag last_option_server_tag(out_bindings[12]->getString());
desc->setServerTag(last_option_server_tag.get());
// If we're fetching options for a given server (explicit server
// tag is provided), it takes precedence over the same option
// specified for all servers. Therefore, we check if the given
// option already exists and belongs to 'all'.
auto& index = local_options.get<1>();
auto existing_it_pair = index.equal_range(desc->option_->getType());
auto existing_it = existing_it_pair.first;
bool found = false;
for ( ; existing_it != existing_it_pair.second; ++existing_it) {
if (existing_it->space_name_ == desc->space_name_) {
found = true;
// This option was already fetched. Let's check if we should
// replace it or not.
if (!last_option_server_tag.amAll() && existing_it->hasAllServerTag()) {
index.replace(existing_it, *desc);
return;
}
break;
}
}
// If there is no such global option yet or the existing option
// belongs to a different server and the inserted option is not
// for all servers.
if (!found ||
(!existing_it->hasServerTag(last_option_server_tag) &&
!last_option_server_tag.amAll())) {
static_cast<void>(local_options.push_back(*desc));
}
}
}
});
// Append the options fetched by this function into the container supplied
// by the caller. The container supplied by the caller may already hold
// some options fetched for other server tags.
options.insert(options.end(), local_options.begin(), local_options.end());
}
OptionDescriptorPtr
......
......@@ -353,7 +353,7 @@ namespace {
"INNER JOIN " #table_prefix "_server AS s" \
" ON a.server_id = s.id " \
"WHERE (s.tag = ? OR s.id = 1) " #__VA_ARGS__ \
" ORDER BY o.option_id"
" ORDER BY o.option_id, s.id"
#define MYSQL_GET_OPTION4(...) \
MYSQL_GET_OPTION_COMMON(dhcp4, "", __VA_ARGS__)
......@@ -667,6 +667,14 @@ namespace {
"WHERE s.tag = ? " #__VA_ARGS__
#endif
#ifndef MYSQL_DELETE_OPTION_UNASSIGNED
#define MYSQL_DELETE_OPTION_UNASSIGNED(table_prefix, ...) \
"DELETE o FROM " #table_prefix "_options AS o " \
"LEFT JOIN " #table_prefix "_options_server AS a " \
" ON o.option_id = a.option_id " \
"WHERE a.option_id IS NULL " #__VA_ARGS__
#endif
#ifndef MYSQL_DELETE_OPTION_POOL_RANGE
#define MYSQL_DELETE_OPTION_POOL_RANGE(table_prefix, ...) \
"DELETE o FROM " #table_prefix "_options AS o " \
......
......@@ -302,6 +302,18 @@ public:
desc.space_name_ = "isc";
test_options_.push_back(OptionDescriptorPtr(new OptionDescriptor(desc)));
desc = createOption<OptionString>(Option::V4, DHO_BOOT_FILE_NAME,
true, false, "my-boot-file-2");
desc.space_name_ = DHCP4_OPTION_SPACE;
desc.setContext(user_context);
test_options_.push_back(OptionDescriptorPtr(new OptionDescriptor(desc)));
desc = createOption<OptionString>(Option::V4, DHO_BOOT_FILE_NAME,
true, false, "my-boot-file-3");
desc.space_name_ = DHCP4_OPTION_SPACE;
desc.setContext(user_context);
test_options_.push_back(OptionDescriptorPtr(new OptionDescriptor(desc)));
// Add definitions for DHCPv4 non-standard options in case we need to
// compare subnets, networks and pools in JSON format. In that case,
// the @c toElement functions require option definitions to generate the
......@@ -1931,8 +1943,9 @@ TEST_F(MySqlConfigBackendDHCPv4Test, createUpdateDeleteOption4) {
opt_boot_file_name);
// Retrieve the option again and make sure that updates were
// properly propagated to the database.
returned_opt_boot_file_name = cbptr_->getOption4(ServerSelector::ALL(),
// properly propagated to the database. Use explicit server selector
// which should also return this option.
returned_opt_boot_file_name = cbptr_->getOption4(ServerSelector::ONE("server1"),
opt_boot_file_name->option_->getType(),
opt_boot_file_name->space_name_);
ASSERT_TRUE(returned_opt_boot_file_name);
......@@ -1971,6 +1984,189 @@ TEST_F(MySqlConfigBackendDHCPv4Test, createUpdateDeleteOption4) {
}
}
// This test verifies that it is possible to differentiate between the
// global options by server tag and that the option specified for the
// particular server overrides the value specified for all servers.
TEST_F(MySqlConfigBackendDHCPv4Test, globalOptions4WithServerTags) {
OptionDescriptorPtr opt_boot_file_name1 = test_options_[0];
OptionDescriptorPtr opt_boot_file_name2 = test_options_[6];
OptionDescriptorPtr opt_boot_file_name3 = test_options_[7];
EXPECT_THROW(cbptr_->createUpdateOption4(ServerSelector::ONE("server1"),
opt_boot_file_name1),
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");
}
EXPECT_NO_THROW(cbptr_->createUpdateOption4(ServerSelector::ONE("server1"),
opt_boot_file_name1));
{
SCOPED_TRACE("global option 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 global option.
testNewAuditEntry("dhcp4_options",
AuditEntry::ModificationType::CREATE,
"global option set",
ServerSelector::ONE("server1"),
3, 1);
}
EXPECT_NO_THROW(cbptr_->createUpdateOption4(ServerSelector::ONE("server2"),
opt_boot_file_name2));
{
SCOPED_TRACE("global option 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_options",
AuditEntry::ModificationType::CREATE,
"global option set",
ServerSelector::ONE("server2"),
3, 1);
}
EXPECT_NO_THROW(cbptr_->createUpdateOption4(ServerSelector::ALL(),
opt_boot_file_name3));
{
SCOPED_TRACE("global option for all servers is set");
// There should be one new audit entry for all servers. It logs
// the insertion of the global option.
testNewAuditEntry("dhcp4_options",
AuditEntry::ModificationType::CREATE,
"global option set",
ServerSelector::ALL(),
1, 1);
}
OptionDescriptorPtr returned_option;
// Try to fetch the option specified for all servers. It should return
// the third option.
EXPECT_NO_THROW(
returned_option = cbptr_->getOption4(ServerSelector::ALL(),
opt_boot_file_name3->option_->getType(),
opt_boot_file_name3->space_name_);
);
ASSERT_TRUE(returned_option);
testOptionsEquivalent(*opt_boot_file_name3, *returned_option);
// Try to fetch the option specified for the server1. It should override the
// option specified for all servers.
EXPECT_NO_THROW(
returned_option = cbptr_->getOption4(ServerSelector::ONE("server1"),
opt_boot_file_name1->option_->getType(),
opt_boot_file_name1->space_name_);
);
ASSERT_TRUE(returned_option);
testOptionsEquivalent(*opt_boot_file_name1, *returned_option);
// The same in case of the server2.
EXPECT_NO_THROW(
returned_option = cbptr_->getOption4(ServerSelector::ONE("server2"),
opt_boot_file_name2->option_->getType(),
opt_boot_file_name2->space_name_);
);
ASSERT_TRUE(returned_option);
testOptionsEquivalent(*opt_boot_file_name2, *returned_option);
OptionContainer returned_options;
// Try to fetch the collection of global options for the server1, server2
// and server3. The server3 does not have an explicit value so for this server
// we should get the option associated with "all" servers.
EXPECT_NO_THROW(
returned_options = cbptr_->getAllOptions4(ServerSelector::
MULTIPLE({ "server1", "server2",
"server3" }));
);
ASSERT_EQ(3, returned_options.size());
// Check that expected options have been returned.
auto current_option = returned_options.begin();
testOptionsEquivalent(*opt_boot_file_name1, *current_option);
testOptionsEquivalent(*opt_boot_file_name2, *(++current_option));
testOptionsEquivalent(*opt_boot_file_name3, *(++current_option));
// 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_options = cbptr_->getAllOptions4(ServerSelector::ALL());
);
ASSERT_EQ(1, returned_options.size());
testOptionsEquivalent(*opt_boot_file_name3, *returned_options.begin());
// Delete the server1. It should remove associations of this server with the
// option and the option itself.
EXPECT_NO_THROW(cbptr_->deleteServer4(ServerTag("server1")));
EXPECT_NO_THROW(
returned_options = cbptr_->getAllOptions4(ServerSelector::ONE("server1"));
);
ASSERT_EQ(1, returned_options.size());
testOptionsEquivalent(*opt_boot_file_name3, *returned_options.begin());
{
SCOPED_TRACE("DELETE audit entry for the global option after server deletion");
testNewAuditEntry("dhcp4_options",
AuditEntry::ModificationType::DELETE,
"deleting a server", ServerSelector::ONE("server1"),
2, 1);
}
// Attempt to delete global option for server1.
uint64_t deleted_num = 0;
EXPECT_NO_THROW(deleted_num = cbptr_->deleteOption4(ServerSelector::ONE("server1"),
opt_boot_file_name1->option_->getType(),
opt_boot_file_name1->space_name_));
EXPECT_EQ(0, deleted_num);
// Deleting the existing option for server2 should succeed.
EXPECT_NO_THROW(deleted_num = cbptr_->deleteOption4(ServerSelector::ONE("server2"),
opt_boot_file_name2->option_->getType(),
opt_boot_file_name2->space_name_));
EXPECT_EQ(1, deleted_num);
// Create this option again to test that deletion of all servers removes it too.
EXPECT_NO_THROW(cbptr_->createUpdateOption4(ServerSelector::ONE("server2"),
opt_boot_file_name2));
// Delete all servers, except 'all'.
EXPECT_NO_THROW(deleted_num = cbptr_->deleteAllServers4());
EXPECT_NO_THROW(
returned_options = cbptr_->getAllOptions4(ServerSelector::ALL());
);
EXPECT_EQ(1, deleted_num);
ASSERT_EQ(1, returned_options.size());
testOptionsEquivalent(*opt_boot_file_name3, *returned_options.begin());
{
SCOPED_TRACE("DELETE audit entry for the global option after deletion of"
" all servers");
testNewAuditEntry("dhcp4_options",
AuditEntry::ModificationType::DELETE,
"deleting all servers", ServerSelector::ONE("server2"),
4, 1);
}
}
// This test verifies that all global options can be retrieved.
TEST_F(MySqlConfigBackendDHCPv4Test, getAllOptions4) {
// Add three global options to the database.
......
......@@ -343,6 +343,18 @@ public:
desc.space_name_ = "isc";
test_options_.push_back(OptionDescriptorPtr(new OptionDescriptor(desc)));
desc = createOption<OptionString>(Option::V6, D6O_NEW_POSIX_TIMEZONE,
true, false, "my-timezone-2");
desc.space_name_ = DHCP6_OPTION_SPACE;
desc.setContext(user_context);
test_options_.push_back(OptionDescriptorPtr(new OptionDescriptor(desc)));
desc = createOption<OptionString>(Option::V6, D6O_NEW_POSIX_TIMEZONE,
true, false, "my-timezone-3");
desc.space_name_ = DHCP6_OPTION_SPACE;
desc.setContext(user_context);
test_options_.push_back(OptionDescriptorPtr(new OptionDescriptor(desc)));
// Add definitions for DHCPv6 non-standard options in case we need to
// compare subnets, networks and pools in JSON format. In that case,
// the @c toElement functions require option definitions to generate the
......@@ -1953,8 +1965,9 @@ TEST_F(MySqlConfigBackendDHCPv6Test, createUpdateDeleteOption6) {
opt_posix_timezone);
// Retrieve the option again and make sure that updates were
// properly propagated to the database.
returned_opt_posix_timezone = cbptr_->getOption6(ServerSelector::ALL(),
// properly propagated to the database. Use explicit server selector
// which should also return this option.
returned_opt_posix_timezone = cbptr_->getOption6(ServerSelector::ONE("server1"),
opt_posix_timezone->option_->getType(),
opt_posix_timezone->space_name_);
ASSERT_TRUE(returned_opt_posix_timezone);
......@@ -1994,6 +2007,189 @@ TEST_F(MySqlConfigBackendDHCPv6Test, createUpdateDeleteOption6) {
}
}
// This test verifies that it is possible to differentiate between the
// global options by server tag and that the option specified for the
// particular server overrides the value specified for all servers.
TEST_F(MySqlConfigBackendDHCPv6Test, globalOptions6WithServerTags) {
OptionDescriptorPtr opt_timezone1 = test_options_[0];
OptionDescriptorPtr opt_timezone2 = test_options_[6];
OptionDescriptorPtr opt_timezone3 = test_options_[7];
EXPECT_THROW(cbptr_->createUpdateOption6(ServerSelector::ONE("server1"),
opt_timezone1),
DbOperationError);
// Create two servers.
EXPECT_NO_THROW(cbptr_->createUpdateServer6(test_servers_[1]));
{
SCOPED_TRACE("server1 is created");
testNewAuditEntry("dhcp6_server",
AuditEntry::ModificationType::CREATE,
"server set");
}
EXPECT_NO_THROW(cbptr_->createUpdateServer6(test_servers_[2]));
{
SCOPED_TRACE("server2 is created");
testNewAuditEntry("dhcp6_server",
AuditEntry::ModificationType::CREATE,
"server set");
}
EXPECT_NO_THROW(cbptr_->createUpdateOption6(ServerSelector::ONE("server1"),
opt_timezone1));
{
SCOPED_TRACE("global option 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 global option.
testNewAuditEntry("dhcp6_options",
AuditEntry::ModificationType::CREATE,
"global option set",
ServerSelector::ONE("server1"),
3, 1);
}
EXPECT_NO_THROW(cbptr_->createUpdateOption6(ServerSelector::ONE("server2"),
opt_timezone2));
{
SCOPED_TRACE("global option for server2 is set");
// Same as in case of the server1, there should be 3 audit entries and
// we validate one of them.
testNewAuditEntry("dhcp6_options",
AuditEntry::ModificationType::CREATE,
"global option set",
ServerSelector::ONE("server2"),
3, 1);
}
EXPECT_NO_THROW(cbptr_->createUpdateOption6(ServerSelector::ALL(),
opt_timezone3));
{
SCOPED_TRACE("global option for all servers is set");
// There should be one new audit entry for all servers. It logs
// the insertion of the global option.
testNewAuditEntry("dhcp6_options",
AuditEntry::ModificationType::CREATE,
"global option set",
ServerSelector::ALL(),
1, 1);
}
OptionDescriptorPtr returned_option;
// Try to fetch the option specified for all servers. It should return
// the third option.
EXPECT_NO_THROW(
returned_option = cbptr_->getOption6(ServerSelector::ALL(),
opt_timezone3->option_->getType(),
opt_timezone3->space_name_);
);
ASSERT_TRUE(returned_option);
testOptionsEquivalent(*opt_timezone3, *returned_option);
// Try to fetch the option specified for the server1. It should override the
// option specified for all servers.
EXPECT_NO_THROW(
returned_option = cbptr_->getOption6(ServerSelector::ONE("server1"),
opt_timezone1->option_->getType(),
opt_timezone1->space_name_);
);
ASSERT_TRUE(returned_option);
testOptionsEquivalent(*opt_timezone1, *returned_option);
// The same in case of the server2.
EXPECT_NO_THROW(
returned_option = cbptr_->getOption6(ServerSelector::ONE("server2"),
opt_timezone2->option_->getType(),
opt_timezone2->space_name_);
);