]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#715] Added support for associating option defs with server tags.
authorMarcin Siodelski <marcin@isc.org>
Thu, 4 Jul 2019 19:18:16 +0000 (21:18 +0200)
committerMarcin Siodelski <marcin@isc.org>
Thu, 4 Jul 2019 20:25:31 +0000 (22:25 +0200)
src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc
src/hooks/dhcp/mysql_cb/mysql_cb_dhcp6.cc
src/hooks/dhcp/mysql_cb/mysql_cb_impl.cc
src/hooks/dhcp/mysql_cb/mysql_cb_impl.h
src/hooks/dhcp/mysql_cb/mysql_query_macros_dhcp.h
src/hooks/dhcp/mysql_cb/tests/mysql_cb_dhcp4_unittest.cc
src/hooks/dhcp/mysql_cb/tests/mysql_cb_dhcp6_unittest.cc

index 3a57105e5c5641d557315e220328048292b0207b..201ab51fc0662a8712b61518deb7de928afce4a3 100644 (file)
@@ -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 = ?)
index 94a4537d4f7597bcaf35798a27e71d5a48e1a5fd..0f1ce65424032b0d64561e7692fcf2913ffef83f 100644 (file)
@@ -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 = ?)
index dfa24124b898d537f182b115cd18d60362221a2b..00c18aef04be5bd09b7973ce2d274b851e2abbbf 100644 (file)
@@ -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
index f37d9de10486d278e1f2ba33a76e531d7163ef04..e858ce7078a21d51345c1e783622b2aab691d56e 100644 (file)
@@ -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".
index d257f0821211b5e849d7709336f2c3d2d067592f..96f24cd74b09803152c0563a570614628ab0db04 100644 (file)
@@ -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 " \
index b8060499b1e497b60fa973f9d2695215e0ebae24..513279f77c0bcb3ce27b8b583680a34456eb31e3 100644 (file)
@@ -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.
index f92f3d9dddbfde5db473eedac8879b57fdef8926..95789f2145d89cc93b01a89e82cd3324e0be2c1d 100644 (file)
@@ -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");
+        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");
+    }
+
+    // This time creation of the option definition for the server1 should pass.
+    EXPECT_NO_THROW(cbptr_->createUpdateOptionDef6(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("dhcp6_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_->createUpdateOptionDef6(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("dhcp6_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_->createUpdateOptionDef6(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("dhcp6_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_->getOptionDef6(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_->getOptionDef6(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_->getOptionDef6(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_->getAllOptionDefs6(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_->getAllOptionDefs6(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_->deleteServer6(ServerTag("server1")));
+    EXPECT_NO_THROW(
+        returned_option_defs = cbptr_->getAllOptionDefs6(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("dhcp6_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_->deleteOptionDef6(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_->deleteOptionDef6(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_->createUpdateOptionDef6(ServerSelector::ONE("server2"),
+                                                   option2));
+
+    // Delete all servers, except 'all'.
+    EXPECT_NO_THROW(deleted_num = cbptr_->deleteAllServers6());
+    EXPECT_NO_THROW(
+        returned_option_defs = cbptr_->getAllOptionDefs6(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("dhcp6_option_def",
+                          AuditEntry::ModificationType::DELETE,
+                          "deleting all servers", ServerSelector::ONE("server2"),
+                          4, 1);
+    }
+}
+
 // Test that all option definitions can be fetched.
 TEST_F(MySqlConfigBackendDHCPv6Test, getAllOptionDefs6) {
     // 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_->createUpdateOptionDef6(ServerSelector::ALL(), option_def);
 
@@ -1816,6 +2011,7 @@ TEST_F(MySqlConfigBackendDHCPv6Test, getAllOptionDefs6) {
             testNewAuditEntry("dhcp6_option_def",
                               AuditEntry::ModificationType::UPDATE,
                               "option definition set");
+            ++updates_num;
 
         } else {
             SCOPED_TRACE("CREATE audit entry for the option definition " +
@@ -1828,12 +2024,12 @@ TEST_F(MySqlConfigBackendDHCPv6Test, getAllOptionDefs6) {
 
     // Fetch all option_definitions.
     OptionDefContainer option_defs = cbptr_->getAllOptionDefs6(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_->getAllOptionDefs6(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) {
@@ -1853,7 +2049,7 @@ TEST_F(MySqlConfigBackendDHCPv6Test, getAllOptionDefs6) {
     EXPECT_EQ(0, cbptr_->deleteOptionDef6(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.