]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1428] Allow non-unique IPs in MySQL and PgSQL
authorMarcin Siodelski <marcin@isc.org>
Fri, 25 Sep 2020 12:12:14 +0000 (14:12 +0200)
committerMarcin Siodelski <marcin@isc.org>
Mon, 5 Oct 2020 13:14:57 +0000 (13:14 +0000)
Introduced new host API function which allows for configuring selected
backends to accept non-unique IP reservations for multiple hosts. Support
for it was added in MySQL, Postgres and Kea config file. It is not
supported in Cassandra. New migrations for MySQL and Postgres have been
created.

34 files changed:
configure.ac
src/bin/admin/tests/mysql_tests.sh.in
src/bin/admin/tests/pgsql_tests.sh.in
src/lib/dhcpsrv/base_host_data_source.h
src/lib/dhcpsrv/cfg_hosts.cc
src/lib/dhcpsrv/cfg_hosts.h
src/lib/dhcpsrv/cql_host_data_source.cc
src/lib/dhcpsrv/cql_host_data_source.h
src/lib/dhcpsrv/host_mgr.cc
src/lib/dhcpsrv/host_mgr.h
src/lib/dhcpsrv/hosts_messages.cc
src/lib/dhcpsrv/hosts_messages.h
src/lib/dhcpsrv/hosts_messages.mes
src/lib/dhcpsrv/mysql_host_data_source.cc
src/lib/dhcpsrv/mysql_host_data_source.h
src/lib/dhcpsrv/pgsql_host_data_source.cc
src/lib/dhcpsrv/pgsql_host_data_source.h
src/lib/dhcpsrv/tests/cfg_hosts_unittest.cc
src/lib/dhcpsrv/tests/cql_host_data_source_unittest.cc
src/lib/dhcpsrv/tests/host_cache_unittest.cc
src/lib/dhcpsrv/tests/host_mgr_unittest.cc
src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc
src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc
src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.cc
src/lib/dhcpsrv/testutils/generic_host_data_source_unittest.h
src/lib/dhcpsrv/testutils/memory_host_data_source.h
src/lib/mysql/mysql_constants.h
src/lib/pgsql/pgsql_connection.h
src/share/database/scripts/mysql/.gitignore
src/share/database/scripts/mysql/dhcpdb_create.mysql
src/share/database/scripts/mysql/upgrade_9.3_to_9.4.sh.in [new file with mode: 0644]
src/share/database/scripts/pgsql/.gitignore
src/share/database/scripts/pgsql/dhcpdb_create.pgsql
src/share/database/scripts/pgsql/upgrade_6.1_to_6.2.sh.in [new file with mode: 0644]

index a559ba0e583b539b05d30698f33a615385286fe0..d10253fe300d77d0b1064456b85946b4a7f8d596 100755 (executable)
@@ -1758,6 +1758,7 @@ AC_CONFIG_FILES([Makefile
                  src/share/database/scripts/mysql/upgrade_9.0_to_9.1.sh
                  src/share/database/scripts/mysql/upgrade_9.1_to_9.2.sh
                  src/share/database/scripts/mysql/upgrade_9.2_to_9.3.sh
+                 src/share/database/scripts/mysql/upgrade_9.3_to_9.4.sh
                  src/share/database/scripts/mysql/wipe_data.sh
                  src/share/database/scripts/pgsql/Makefile
                  src/share/database/scripts/pgsql/upgrade_1.0_to_2.0.sh
@@ -1770,6 +1771,7 @@ AC_CONFIG_FILES([Makefile
                  src/share/database/scripts/pgsql/upgrade_5.0_to_5.1.sh
                  src/share/database/scripts/pgsql/upgrade_5.1_to_6.0.sh
                  src/share/database/scripts/pgsql/upgrade_6.0_to_6.1.sh
+                 src/share/database/scripts/pgsql/upgrade_6.1_to_6.2.sh
                  src/share/database/scripts/pgsql/wipe_data.sh
                  src/share/yang/Makefile
                  src/share/yang/modules/Makefile
index 07e2a4165132b92d51be599b37846a95210af433..eefde5651283975c42586aef6053ffe89771aa7e 100644 (file)
@@ -264,7 +264,7 @@ mysql_upgrade_test() {
 
     assert_str_eq "1.0" ${version} "Expected kea-admin to return %s, returned value was %s"
 
-    # Ok, we have a 1.0 database. Let's upgrade it to 9.3
+    # Ok, we have a 1.0 database. Let's upgrade it to 9.4
     ${keaadmin} db-upgrade mysql -u $db_user -p $db_password -n $db_name -d $db_scripts_dir
     ERRCODE=$?
 
@@ -755,9 +755,20 @@ EOF
     qry="select ddns_send_updates, ddns_override_no_update, ddns_override_client_update, ddns_replace_client_name, ddns_generated_prefix, ddns_qualifying_suffix from dhcp6_subnet"
     run_statement "dhcp6_subnet" "$qry"
 
-    # Verify upgraded schema reports version 9.3
+    # Schema upgrade from 9.3 to 9.4.
+
+    # Non unique indexes on hosts allowing multiple reservation for the same IP.
+
+    insert_sql="\
+insert into hosts(dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address) values (hex('010101010101'), 0, 1, inet_aton('192.0.2.0'));\
+insert into hosts(dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address) values (hex('010101010102'), 0, 1, inet_aton('192.0.2.0'));"
+    mysql_execute "$insert_sql"
+    ERRCODE=$?
+    assert_eq 0 $ERRCODE "insert into hosts failed, expected exit code %d, actual %d"
+
+    # Verify upgraded schema reports version 9.4
     version=$(${keaadmin} db-version mysql -u $db_user -p $db_password -n $db_name -d $db_scripts_dir)
-    assert_str_eq "9.3" ${version} "Expected kea-admin to return %s, returned value was %s"
+    assert_str_eq "9.4" ${version} "Expected kea-admin to return %s, returned value was %s"
 
     # Let's wipe the whole database
     mysql_wipe
index 933678f40f6817f3ada21dc0b034871e108d5ca1..e5ad52c18071c881b5387925635b8dd528894158 100644 (file)
@@ -126,7 +126,7 @@ pgsql_db_version_test() {
 
     # Verify that kea-admin db-version returns the correct version
     version=$(${keaadmin} db-version pgsql -u $db_user -p $db_password -n $db_name)
-    assert_str_eq "6.1" ${version} "Expected kea-admin to return %s, returned value was %s"
+    assert_str_eq "6.2" ${version} "Expected kea-admin to return %s, returned value was %s"
 
     # Let's wipe the whole database
     pgsql_wipe
@@ -237,10 +237,6 @@ pgsql_upgrade_2_0_to_3_0() {
 }
 
 pgsql_upgrade_3_0_to_6_1() {
-    # Verify upgraded schema reports version 6.1.
-    version=$(${keaadmin} db-version pgsql -u $db_user -p $db_password -n $db_name -d $db_scripts_dir)
-    assert_str_eq "6.1" ${version} "Expected kea-admin to return %s, returned value was %s"
-
     # Added user_context to lease4
     output=`pgsql_execute "select user_context from lease4;"`
     ERRCODE=$?
@@ -257,6 +253,19 @@ pgsql_upgrade_3_0_to_6_1() {
     assert_eq 0 $ERRCODE "logs table is missing or broken. (expected status code %d, returned %d)"
 }
 
+pgsql_upgrade_6_1_to_6_2() {
+    # Verify upgraded schema reports version 6.2.
+    version=$(${keaadmin} db-version pgsql -u $db_user -p $db_password -n $db_name -d $db_scripts_dir)
+    assert_str_eq "6.2" ${version} "Expected kea-admin to return %s, returned value was %s"
+
+    insert_sql="\
+insert into hosts(dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address) values (decode('010101010101', 'hex'), 0, 1, x'FFAF0002'::int);\
+insert into hosts(dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_address) values (decode('010101010102', 'hex'), 0, 1, x'FFAF0002'::int);"
+    pgsql_execute "$insert_sql"
+    ERRCODE=$?
+    assert_eq 0 $ERRCODE "insert into hosts failed, expected exit code %d, actual %d"
+}
+
 pgsql_upgrade_test() {
     test_start "pgsql.upgrade-test"
 
@@ -279,6 +288,9 @@ pgsql_upgrade_test() {
     # Check 3.0 to 6.1 upgrade
     pgsql_upgrade_3_0_to_6_1
 
+    # Check 6.1 to 6.2 upgrade
+    pgsql_upgrade_6_1_to_6_2
+
     # Let's wipe the whole database
     pgsql_wipe
 
index 7eb2cf644aedfe98c121efe3d3f87a0787b4b0ce..576b89b02a9e107758c3917db431f034f2adfdfd 100644 (file)
@@ -416,6 +416,27 @@ public:
     /// Rolls back all pending database operations.  On databases that don't
     /// support transactions, this is a no-op.
     virtual void rollback() {};
+
+    /// @brief Controls whether IP reservations are unique or non-unique.
+    ///
+
+    /// In a typical case, the IP reservations are unique and backends verify
+    /// prior to adding a host reservation to the database that the reservation
+    /// for a given IP address/subnet does not exist. In some cases it may be
+    /// required to allow non-unique IP reservations, e.g. in the case when a
+    /// host has several interfaces and independently of which interface is used
+    /// by this host to communicate with the DHCP server the same IP address
+    /// should be assigned. In this case the @c unique value should be set to
+    /// false to disable the checks for uniqueness on the backend side.
+    ///
+    /// All backends are required to support the case when unique setting is
+    /// @c true and they must use this setting by default.
+    ///
+    /// @param unique boolean flag indicating if the IP reservations must be
+    /// unique or can be non-unique.
+    /// @return true if the new setting was accepted by the backend or false
+    /// otherwise.
+    virtual bool setIPReservationUnique(const bool unique) = 0;
 };
 
 /// @brief HostDataSource pointer
index 544cdf74a40c42bac01a5d9ba970c71af2787f81..fe25c071f85737df813f5ea39d58bccb4eb0d8f0 100644 (file)
@@ -1009,7 +1009,7 @@ CfgHosts::add4(const HostPtr& host) {
     }
 
     // Check if the address is already reserved for the specified IPv4 subnet.
-    if (!host->getIPv4Reservation().isV4Zero() &&
+    if (ip_reservations_unique_ && !host->getIPv4Reservation().isV4Zero() &&
         (host->getIPv4SubnetID() != SUBNET_ID_UNUSED) &&
         get4(host->getIPv4SubnetID(), host->getIPv4Reservation())) {
         isc_throw(ReservedAddress, "failed to add new host using the HW"
@@ -1061,15 +1061,17 @@ CfgHosts::add6(const HostPtr& host) {
     for (IPv6ResrvIterator it = reservations.first; it != reservations.second;
          ++it) {
 
-        // If there's an entry for this (subnet-id, address), reject it.
-        if (get6(host->getIPv6SubnetID(), it->second.getPrefix())) {
-            isc_throw(DuplicateHost, "failed to add address reservation for "
-                      << "host using the HW address '"
-                      << (hwaddr ? hwaddr->toText(false) : "(null)")
-                      << " and DUID '" << (duid ? duid->toText() : "(null)")
-                      << "' to the IPv6 subnet id '" << host->getIPv6SubnetID()
-                      << "' for address/prefix " << it->second.getPrefix()
-                      << ": There's already reservation for this address/prefix");
+        if (ip_reservations_unique_) {
+            // If there's an entry for this (subnet-id, address), reject it.
+            if (get6(host->getIPv6SubnetID(), it->second.getPrefix())) {
+                isc_throw(DuplicateHost, "failed to add address reservation for "
+                          << "host using the HW address '"
+                          << (hwaddr ? hwaddr->toText(false) : "(null)")
+                          << " and DUID '" << (duid ? duid->toText() : "(null)")
+                          << "' to the IPv6 subnet id '" << host->getIPv6SubnetID()
+                          << "' for address/prefix " << it->second.getPrefix()
+                          << ": There's already reservation for this address/prefix");
+            }
         }
         hosts6_.insert(HostResrv6Tuple(it->second, host));
     }
@@ -1132,6 +1134,13 @@ CfgHosts::del6(const SubnetID& /*subnet_id*/,
     return (false);
 }
 
+bool
+CfgHosts::setIPReservationUnique(const bool unique) {
+    ip_reservations_unique_ = unique;
+    return (true);
+}
+
+
 ElementPtr
 CfgHosts::toElement() const {
     uint16_t family = CfgMgr::instance().getFamily();
index f5b37d900a2cefcb279c6cdc06ab421620fc37fe..050df7c11f03567ce4f9cd13d5b905a09726fe45 100644 (file)
@@ -544,6 +544,23 @@ public:
         return (std::string("configuration file"));
     }
 
+    /// @brief Controls whether IP reservations are unique or non-unique.
+    ///
+    /// In a typical case, the IP reservations are unique and backends verify
+    /// prior to adding a host reservation to the database that the reservation
+    /// for a given IP address/subnet does not exist. In some cases it may be
+    /// required to allow non-unique IP reservations, e.g. in the case when a
+    /// host has several interfaces and independently of which interface is used
+    /// by this host to communicate with the DHCP server the same IP address
+    /// should be assigned. In this case the @c unique value should be set to
+    /// false to disable the checks for uniqueness on the backend side.
+    ///
+    /// @param unique boolean flag indicating if the IP reservations must be
+    /// unique or can be non-unique.
+    /// @return always true because this data source supports both the case when
+    /// the addresses must be unique and when they may be non-unique.
+    virtual bool setIPReservationUnique(const bool unique);
+
     /// @brief Unparse a configuration object
     ///
     /// host reservation lists are not autonomous so they are
@@ -849,6 +866,10 @@ private:
     /// - IPv6 prefix
     HostContainer6 hosts6_;
 
+    /// @brief Holds the setting whether the IP reservations must be unique or
+    /// may be non-unique.
+    bool ip_reservations_unique_ = true;
+
     /// @brief Unparse a configuration object (DHCPv4 reservations)
     ///
     /// @return a pointer to unparsed configuration
index e3ef27b28affcb854ede58e3f16fecbd7e51afbb..dc83a23bcc353b13fcd127756ded2e6c70f0f048 100644 (file)
@@ -3660,5 +3660,14 @@ CqlHostDataSource::rollback() {
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_CQL_ROLLBACK);
 }
 
+bool
+CqlHostDataSource::setIPReservationUnique(const bool unique) {
+    // This backend does not support the mode in which multiple reservations
+    // for the same IP address are created. If selecting this mode is
+    // attempted this function returns false to indicate that this is
+    // not allowed.
+    return (unique ? true : false);
+}
+
 }  // namespace dhcp
 }  // namespace isc
index f72531230059b787576046322e21811702b2ff06..15fb02de3a880fc3ac9d3c22b0edcb65bdda5fcb 100644 (file)
@@ -411,6 +411,25 @@ public:
     /// Rolls back all pending database operations  (no-op for Cassandra)
     virtual void rollback() override;
 
+    /// @brief Controls whether IP reservations are unique or non-unique.
+    ///
+
+    /// In a typical case, the IP reservations are unique and backends verify
+    /// prior to adding a host reservation to the database that the reservation
+    /// for a given IP address does not exist. In some cases it may be required
+    /// to allow non-unique IP reservations, e.g. in the case when a host has
+    /// several interfaces and independently of which interface is used by this
+    /// host to communicate with the DHCP server the same IP address should be
+    /// assigned. In this case the @c unique value should be set to false to
+    /// disable the checks for uniqueness on the backend side.
+    ///
+    /// @param unique boolean flag indicating if the IP reservations must be
+    /// unique within the subnet or can be non-unique.
+    /// @return true when addresses must be unique, false otherwise because
+    /// this backend does not support specifying the same IP address in multiple
+    /// host reservations.
+    virtual bool setIPReservationUnique(const bool unique) override;
+
 private:
     /// @brief Pointer to the implementation of the @ref CqlHostDataSource.
     CqlHostDataSourceImpl* impl_;
index cc70a5bacc888a85b9cf10146cd89faa47b48dae..ba043d0316a1b49b3a8355aca44b29627af6f8fa 100644 (file)
@@ -604,5 +604,26 @@ HostMgr::cacheNegative(const SubnetID& ipv4_subnet_id,
     }
 }
 
+bool
+HostMgr::setIPReservationUnique(const bool unique) {
+    // Iterate over the alternate sources first, because they may include those
+    // for which the new setting is not supported.
+    for (auto source : alternate_sources_) {
+        if (!source->setIPReservationUnique(unique)) {
+            // One of the sources does not support this new mode of operation.
+            // Let's log a warning and back off the changes to the default
+            // setting which should always be supported.
+            LOG_WARN(hosts_logger, HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED)
+                .arg(source->getType());
+            for (auto source : alternate_sources_) {
+                source->setIPReservationUnique(true);
+            }
+            return (false);
+        }
+    }
+    return (true);
+}
+
+
 } // end of isc::dhcp namespace
 } // end of isc namespace
index bfb977056f84c977626d67c046eef36332d64bc3..6db35817595e0ec04c5d3dc1b4234fc8c99eecf1 100644 (file)
@@ -529,6 +529,26 @@ public:
         disable_single_query_ = disable_single_query;
     }
 
+    /// @brief Controls whether IP reservations are unique or non-unique.
+    ///
+    /// In a typical case, the IP reservations are unique and backends verify
+    /// prior to adding a host reservation to the database that the reservation
+    /// for a given IP address/subnet does not exist. In some cases it may be
+    /// required to allow non-unique IP reservations, e.g. in the case when a
+    /// host has several interfaces and independently of which interface is used
+    /// by this host to communicate with the DHCP server the same IP address
+    /// should be assigned. In this case the @c unique value should be set to
+    /// false to disable the checks for uniqueness on the backend side.
+    ///
+    /// Calling this function on @c HostMgr causes the manager to attempt to
+    /// set this flag on all backends in use.
+    ///
+    /// @param unique boolean flag indicating if the IP reservations must be
+    /// unique or can be non-unique.
+    /// @return true if the new setting was accepted by the backend or false
+    /// otherwise.
+    virtual bool setIPReservationUnique(const bool unique);
+
 protected:
     /// @brief The negative caching flag.
     ///
index c5784a517caa52b961474ff52d252e36aff33382..9be05252399038205ddd87beb38a2c9ba125a3b4 100644 (file)
@@ -66,6 +66,7 @@ extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_ADDRESS6 = "
 extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER = "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER";
 extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_HOST = "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_HOST";
 extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL = "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL";
+extern const isc::log::MessageID HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED = "HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED";
 
 } // namespace dhcp
 } // namespace isc
@@ -132,6 +133,7 @@ const char* values[] = {
     "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER", "get one host with IPv6 reservation for subnet id %1, identified by %2",
     "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_HOST", "using subnet id %1 and identifier %2, found in %3 host: %4",
     "HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL", "host not found using subnet id %1 and identifier %2",
+    "HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED", "host data source %1 does not support the mode in which IP reservations are non-unique",
     NULL
 };
 
index 7d40971d36fd6157e29865b5b1a0409198ef5c03..a1d2b0712db145db5e9f3d5f525c0439adbe162b 100644 (file)
@@ -67,6 +67,7 @@ extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_ADDRESS6;
 extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER;
 extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_HOST;
 extern const isc::log::MessageID HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL;
+extern const isc::log::MessageID HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED;
 
 } // namespace dhcp
 } // namespace isc
index 280c759149ddafbc01d60a2e930e68548d8374e8..8c1e8c0f8ef4f6007cbc4639d956db54c610f459 100644 (file)
@@ -283,3 +283,11 @@ identifier.
 % HOSTS_MGR_ALTERNATE_GET6_SUBNET_ID_IDENTIFIER_NULL host not found using subnet id %1 and identifier %2
 This debug message is issued when no host was found using the specified
 subnet id and host identifier.
+
+% HOSTS_MGR_NON_UNIQUE_IP_UNSUPPORTED host data source %1 does not support the mode in which IP reservations are non-unique
+This warning message is issued when an administrator attempted to configure the
+server to allow multiple host reservations for the same IP address or prefix.
+Some host database backends may not support this mode of operation. In this
+case the administrator should stop using these backends or fall back to the
+default setting which requires that IP addresses are unique within a subnet.
+This setting is guaranteed to work for MySQL and Postgres host backends.
\ No newline at end of file
index bcfcc5980604993b98b78ffefbd4d0b188a09797..e4c97998d70a0d1af134d7261bd66ca8ca910a81 100644 (file)
@@ -235,9 +235,11 @@ public:
     /// @param host Host object to be added to the database.
     ///        None of the fields in the host reservation are modified -
     ///        the host data is only read.
+    /// @param unique_ip boolean value indicating if multiple reservations for the
+    ///        same IP address are allowed (false) or not (true).
     ///
     /// @return Vector of MySQL BIND objects representing the data to be added.
-    std::vector<MYSQL_BIND> createBindForSend(const HostPtr& host) {
+    std::vector<MYSQL_BIND> createBindForSend(const HostPtr& host, const bool unique_ip) {
         // Store host object to ensure it remains valid.
         host_ = host;
 
@@ -381,7 +383,16 @@ public:
         }
 
         // Add the data to the vector.
-        return (std::vector<MYSQL_BIND>(bind_.begin(), bind_.begin() + columns_num_));
+        std::vector<MYSQL_BIND> vec(bind_.begin(), bind_.begin() + HOST_COLUMNS);
+
+        // When checking whether the IP is unique we need to bind the IPv4 address
+        // at the end of the query as it has additional binding for the IPv4
+        // address.
+        if (unique_ip) {
+            vec.push_back(bind_[5]);
+            vec.push_back(bind_[3]);
+        }
+        return (vec);
     };
 
     /// @brief Create BIND array to receive Host data.
@@ -1610,10 +1621,13 @@ public:
     ///        None of the fields in the reservation are modified -
     ///        the reservation data is only read.
     /// @param id ID of a host owning this reservation
+    /// @param unique_ip boolean value indicating if multiple reservations for the
+    ///        same IP address are allowed (false) or not (true).
     ///
     /// @return Vector of MySQL BIND objects representing the data to be added.
     std::vector<MYSQL_BIND> createBindForSend(const IPv6Resrv& resv,
-                                              const HostID& id) {
+                                              const HostID& id,
+                                              const bool unique_ip) {
 
         // Store the values to ensure they remain valid.
         resv_ = resv;
@@ -1672,7 +1686,17 @@ public:
         // Add the data to the vector.  Note the end element is one after the
         // end of the array.
         // RESRV_COLUMNS -1 as we do not set reservation_id.
-        return (std::vector<MYSQL_BIND>(&bind_[0], &bind_[RESRV_COLUMNS-1]));
+        std::vector<MYSQL_BIND> vec(&bind_[0], &bind_[RESRV_COLUMNS-1]);
+
+        // When checking whether the IP is unique we need to bind the IPv6 address
+        // and prefix length at the end of the query as it has additional binding
+        // for the IPv6 address and prefix length.
+        if (unique_ip) {
+            vec.push_back(bind_[0]);
+            vec.push_back(bind_[1]);
+        }
+
+        return (vec);
     }
 
 private:
@@ -1990,30 +2014,32 @@ public:
     /// @note: please add new statements doing read only operations before
     /// the WRITE_STMTS_BEGIN position.
     enum StatementIndex {
-        GET_HOST_DHCPID,        // Gets hosts by host identifier
-        GET_HOST_ADDR,          // Gets hosts by IPv4 address
-        GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID
-        GET_HOST_SUBID6_DHCPID, // Gets host by IPv6 SubnetID, HW address/DUID
-        GET_HOST_SUBID_ADDR,    // Gets host by IPv4 SubnetID and IPv4 address
-        GET_HOST_PREFIX,        // Gets host by IPv6 prefix
-        GET_HOST_SUBID6_ADDR,   // Gets host by IPv6 SubnetID and IPv6 prefix
-        GET_HOST_SUBID4,        // Gets hosts by IPv4 SubnetID
-        GET_HOST_SUBID6,        // Gets hosts by IPv6 SubnetID
-        GET_HOST_HOSTNAME,      // Gets hosts by hostname
-        GET_HOST_HOSTNAME_SUBID4, // Gets hosts by hostname and IPv4 SubnetID
-        GET_HOST_HOSTNAME_SUBID6, // Gets hosts by hostname and IPv6 SubnetID
-        GET_HOST_SUBID4_PAGE,   // Gets hosts by IPv4 SubnetID beginning by HID
-        GET_HOST_SUBID6_PAGE,   // Gets hosts by IPv6 SubnetID beginning by HID
-        GET_HOST_PAGE4,         // Gets v4 hosts beginning by HID
-        GET_HOST_PAGE6,         // Gets v6 hosts beginning by HID
-        INSERT_HOST,            // Insert new host to collection
-        INSERT_V6_RESRV,        // Insert v6 reservation
-        INSERT_V4_HOST_OPTION,  // Insert DHCPv4 option
-        INSERT_V6_HOST_OPTION,  // Insert DHCPv6 option
-        DEL_HOST_ADDR4,         // Delete v4 host (subnet-id, addr4)
-        DEL_HOST_SUBID4_ID,     // Delete v4 host (subnet-id, ident.type, identifier)
-        DEL_HOST_SUBID6_ID,     // Delete v6 host (subnet-id, ident.type, identifier)
-        NUM_STATEMENTS          // Number of statements
+        GET_HOST_DHCPID,           // Gets hosts by host identifier
+        GET_HOST_ADDR,             // Gets hosts by IPv4 address
+        GET_HOST_SUBID4_DHCPID,    // Gets host by IPv4 SubnetID, HW address/DUID
+        GET_HOST_SUBID6_DHCPID,    // Gets host by IPv6 SubnetID, HW address/DUID
+        GET_HOST_SUBID_ADDR,       // Gets host by IPv4 SubnetID and IPv4 address
+        GET_HOST_PREFIX,           // Gets host by IPv6 prefix
+        GET_HOST_SUBID6_ADDR,      // Gets host by IPv6 SubnetID and IPv6 prefix
+        GET_HOST_SUBID4,           // Gets hosts by IPv4 SubnetID
+        GET_HOST_SUBID6,           // Gets hosts by IPv6 SubnetID
+        GET_HOST_HOSTNAME,         // Gets hosts by hostname
+        GET_HOST_HOSTNAME_SUBID4,  // Gets hosts by hostname and IPv4 SubnetID
+        GET_HOST_HOSTNAME_SUBID6,  // Gets hosts by hostname and IPv6 SubnetID
+        GET_HOST_SUBID4_PAGE,      // Gets hosts by IPv4 SubnetID beginning by HID
+        GET_HOST_SUBID6_PAGE,      // Gets hosts by IPv6 SubnetID beginning by HID
+        GET_HOST_PAGE4,            // Gets v4 hosts beginning by HID
+        GET_HOST_PAGE6,            // Gets v6 hosts beginning by HID
+        INSERT_HOST_NON_UNIQUE_IP, // Insert new host to collection with allowing IP duplicates
+        INSERT_HOST_UNIQUE_IP,     // Insert new host to collection with checking for IP duplicates
+        INSERT_V6_RESRV_NON_UNIQUE,// Insert v6 reservation without checking that it is unique
+        INSERT_V6_RESRV_UNIQUE,    // Insert v6 reservation with checking that it is unique
+        INSERT_V4_HOST_OPTION,     // Insert DHCPv4 option
+        INSERT_V6_HOST_OPTION,     // Insert DHCPv6 option
+        DEL_HOST_ADDR4,            // Delete v4 host (subnet-id, addr4)
+        DEL_HOST_SUBID4_ID,        // Delete v4 host (subnet-id, ident.type, identifier)
+        DEL_HOST_SUBID6_ID,        // Delete v6 host (subnet-id, ident.type, identifier)
+        NUM_STATEMENTS             // Number of statements
     };
 
     /// @brief Index of first statement performing write to the database.
@@ -2021,7 +2047,7 @@ public:
     /// This value is used to mark border line between queries and other
     /// statements and statements performing write operation on the database,
     /// such as INSERT, DELETE, UPDATE.
-    static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST;
+    static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST_UNIQUE_IP;
 
     /// @brief Constructor.
     ///
@@ -2197,6 +2223,10 @@ public:
     /// @brief The parameters
     DatabaseConnection::ParameterMap parameters_;
 
+    /// @brief Holds the setting whether the IP reservations must be unique or
+    /// may be non-unique.
+    bool ip_reservations_unique_;
+
     /// @brief The pool of contexts
     MySqlHostContextPoolPtr pool_;
 };
@@ -2565,8 +2595,9 @@ TaggedStatementArray tagged_statements = { {
                 "ON h.host_id = r.host_id "
             "ORDER BY h.host_id, o.option_id, r.reservation_id"},
 
-    // Inserts a host into the 'hosts' table.
-    {MySqlHostDataSourceImpl::INSERT_HOST,
+    // Inserts a host into the 'hosts' table without checking that there is
+    // a reservation for the IP address.
+    {MySqlHostDataSourceImpl::INSERT_HOST_NON_UNIQUE_IP,
             "INSERT INTO hosts(host_id, dhcp_identifier, dhcp_identifier_type, "
                 "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
                 "dhcp4_client_classes, dhcp6_client_classes, "
@@ -2574,12 +2605,46 @@ TaggedStatementArray tagged_statements = { {
                 "dhcp4_server_hostname, dhcp4_boot_file_name, auth_key) "
             "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"},
 
-    // Inserts a single IPv6 reservation into 'reservations' table.
-    {MySqlHostDataSourceImpl::INSERT_V6_RESRV,
+    // Inserts a host into the 'hosts' table with checking that reserved IP
+    // address is unique. The innermost query checks if there is at least
+    // one host for the given IP/subnet combination. If it not exists the
+    // new host is inserted. DUAL is a special MySQL table from which we
+    // can select the values to be inserted. If the host with the given
+    // IP address already exists the new host won't be inserted. The caller
+    // can check the number of affected rows to detect that there was
+    // a duplicate host in the database.
+    {MySqlHostDataSourceImpl::INSERT_HOST_UNIQUE_IP,
+            "INSERT INTO hosts(host_id, dhcp_identifier, dhcp_identifier_type, "
+                "dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
+                "dhcp4_client_classes, dhcp6_client_classes, "
+                "user_context, dhcp4_next_server, "
+                "dhcp4_server_hostname, dhcp4_boot_file_name, auth_key) "
+                "SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM DUAL "
+                    "WHERE NOT EXISTS ("
+                        "SELECT ipv4_address FROM hosts "
+                            "WHERE ipv4_address = ? AND dhcp4_subnet_id = ? "
+                        "LIMIT 1"
+                    ")"},
+
+    // Inserts a single IPv6 reservation into 'reservations' table without
+    // checking that the inserted reservation is unique.
+    {MySqlHostDataSourceImpl::INSERT_V6_RESRV_NON_UNIQUE,
             "INSERT INTO ipv6_reservations(address, prefix_len, type, "
                 "dhcp6_iaid, host_id) "
             "VALUES (?, ?, ?, ?, ?)"},
 
+    // Inserts a single IPv6 reservation into 'reservations' table with
+    // checking that the inserted reservation is unique.
+    {MySqlHostDataSourceImpl::INSERT_V6_RESRV_UNIQUE,
+            "INSERT INTO ipv6_reservations(address, prefix_len, type, "
+                "dhcp6_iaid, host_id) "
+                "SELECT ?, ?, ?, ?, ? FROM DUAL "
+                    "WHERE NOT EXISTS ("
+                        "SELECT 1 FROM ipv6_reservations "
+                            "WHERE address = ? AND prefix_len = ? "
+                        "LIMIT 1"
+                    ")"},
+
     // Inserts a single DHCPv4 option into 'dhcp4_options' table.
     // Using fixed scope_id = 3, which associates an option with host.
     {MySqlHostDataSourceImpl::INSERT_V4_HOST_OPTION,
@@ -2655,7 +2720,7 @@ MySqlHostDataSource::MySqlHostContextAlloc::~MySqlHostContextAlloc() {
 }
 
 MySqlHostDataSourceImpl::MySqlHostDataSourceImpl(const MySqlConnection::ParameterMap& parameters)
-    : parameters_(parameters) {
+    : parameters_(parameters), ip_reservations_unique_(true) {
 
     // Validate the schema version first.
     std::pair<uint32_t, uint32_t> code_version(MYSQL_SCHEMA_VERSION_MAJOR,
@@ -2743,6 +2808,16 @@ MySqlHostDataSourceImpl::addStatement(MySqlHostContextPtr& ctx,
         }
         checkError(ctx, status, stindex, "unable to execute");
     }
+
+    // If the number of rows inserted is 0 it means that the query detected
+    // an attempt to insert duplicated data for which there is no unique
+    // index in the database. Unique indexes are not created in the database
+    // when it may be sometimes allowed to insert duplicated records per
+    // server's configuration.
+    my_ulonglong numrows = mysql_stmt_affected_rows(ctx->conn_.statements_[stindex]);
+    if (numrows == 0) {
+        isc_throw(DuplicateEntry, "Database duplicate entry error");
+    }
 }
 
 bool
@@ -2770,9 +2845,10 @@ void
 MySqlHostDataSourceImpl::addResv(MySqlHostContextPtr& ctx,
                                  const IPv6Resrv& resv,
                                  const HostID& id) {
-    std::vector<MYSQL_BIND> bind = ctx->host_ipv6_reservation_exchange_->createBindForSend(resv, id);
+    std::vector<MYSQL_BIND> bind = ctx->host_ipv6_reservation_exchange_->
+        createBindForSend(resv, id, ip_reservations_unique_);
 
-    addStatement(ctx, INSERT_V6_RESRV, bind);
+    addStatement(ctx, ip_reservations_unique_ ? INSERT_V6_RESRV_UNIQUE : INSERT_V6_RESRV_NON_UNIQUE, bind);
 }
 
 void
@@ -2958,11 +3034,19 @@ MySqlHostDataSource::add(const HostPtr& host) {
     // the MySqlTransaction class.
     MySqlTransaction transaction(ctx->conn_);
 
+    // If we're configured to check that an IP reservation within a given subnet
+    // is unique, the IP reservation exists and the subnet is actually set
+    // we will be using a special query that checks for uniqueness. Otherwise,
+    // we will use a regular insert statement.
+    bool unique_ip = impl_->ip_reservations_unique_ && !host->getIPv4Reservation().isV4Zero()
+        && host->getIPv4SubnetID() != SUBNET_ID_UNUSED;
+
     // Create the MYSQL_BIND array for the host
-    std::vector<MYSQL_BIND> bind = ctx->host_ipv4_exchange_->createBindForSend(host);
+    std::vector<MYSQL_BIND> bind = ctx->host_ipv4_exchange_->createBindForSend(host, unique_ip);
 
     // ... and insert the host.
-    impl_->addStatement(ctx, MySqlHostDataSourceImpl::INSERT_HOST, bind);
+    impl_->addStatement(ctx, unique_ip ? MySqlHostDataSourceImpl::INSERT_HOST_UNIQUE_IP :
+                        MySqlHostDataSourceImpl::INSERT_HOST_NON_UNIQUE_IP, bind);
 
     // Gets the last inserted hosts id
     uint64_t host_id = mysql_insert_id(ctx->conn_.mysql_);
@@ -3646,5 +3730,12 @@ MySqlHostDataSource::rollback() {
     ctx->conn_.rollback();
 }
 
+bool
+MySqlHostDataSource::setIPReservationUnique(const bool unique) {
+    impl_->ip_reservations_unique_ = unique;
+    return (true);
+}
+
+
 }  // namespace dhcp
 }  // namespace isc
index 7790e6525c9128ceb4989508541358285adb693e..f6266d4c6757d1607d6abcbbd9136d2b65a01b5b 100644 (file)
@@ -384,6 +384,23 @@ public:
     /// Rolls back all pending database operations.
     virtual void rollback();
 
+    /// @brief Controls whether IP reservations are unique or non-unique.
+    ///
+    /// In a typical case, the IP reservations are unique and backends verify
+    /// prior to adding a host reservation to the database that the reservation
+    /// for a given IP address/subnet does not exist. In some cases it may be
+    /// required to allow non-unique IP reservations, e.g. in the case when a
+    /// host has several interfaces and independently of which interface is used
+    /// by this host to communicate with the DHCP server the same IP address
+    /// should be assigned. In this case the @c unique value should be set to
+    /// false to disable the checks for uniqueness on the backend side.
+    ///
+    /// @param unique boolean flag indicating if the IP reservations must be
+    /// unique within the subnet or can be non-unique.
+    /// @return always true because this backend supports both the case when
+    /// the addresses must be unique and when they may be non-unique.
+    virtual bool setIPReservationUnique(const bool unique);
+
     /// @brief Context RAII Allocator.
     class MySqlHostContextAlloc {
     public:
index 0eeb52a33e62f61e16b0ea586b7a863fd4cabb1a..84a859b169378452bd2f1597bb0d365b845f1581 100644 (file)
@@ -186,12 +186,14 @@ public:
     /// @param host Host object to be added to the database.
     ///        None of the fields in the host reservation are modified -
     ///        the host data is only read.
+    /// @param unique_ip boolean value indicating if multiple reservations for the
+    ///        same IP address are allowed (false) or not (true).
     ///
     /// @return pointer to newly constructed bind_array containing the
     /// bound values extracted from host
     ///
     /// @throw DbOperationError if bind_array cannot be populated.
-    PsqlBindArrayPtr createBindForSend(const HostPtr& host) {
+    PsqlBindArrayPtr createBindForSend(const HostPtr& host, const bool unique_ip) {
         if (!host) {
             isc_throw(BadValue, "createBindForSend:: host object is NULL");
         }
@@ -265,6 +267,15 @@ public:
                 bind_array->add(key);
             }
 
+            // When checking whether the IP is unique we need to bind the IPv4 address
+            // at the end of the query as it has additional binding for the IPv4
+            // address.
+            if (unique_ip) {
+                bind_array->add(host->getIPv4Reservation());
+                bind_array->add(host->getIPv4SubnetID());
+            }
+
+
         } catch (const std::exception& ex) {
             host_.reset();
             isc_throw(DbOperationError,
@@ -1081,13 +1092,16 @@ public:
     /// @param resv The IPv6 reservation to be added to the database.
     ///        None of the fields in the reservation are modified -
     /// @param host_id ID of the host to which this reservation belongs.
+    /// @param unique_ip boolean value indicating if multiple reservations for the
+    ///        same IP address are allowed (false) or not (true).
     ///
     /// @return pointer to newly constructed bind_array containing the
     /// bound values extracted the IPv6 reservation
     ///
     /// @throw DbOperationError if bind_array cannot be populated.
     PsqlBindArrayPtr createBindForSend(const IPv6Resrv& resv,
-                                       const HostID& host_id) {
+                                       const HostID& host_id,
+                                       const bool unique_ip) {
         // Store the values to ensure they remain valid.
         // Technically we don't need this, as currently all the values
         // are converted to strings and stored by the bind array.
@@ -1113,6 +1127,14 @@ public:
 
             // host_id: BIGINT NOT NULL
             bind_array->add(host_id);
+
+            // When checking whether the IP is unique we need to bind the IPv6 address
+            // and prefix length at the end of the query as it has additional binding
+            // for the IPv6 address and prefix length.
+            if (unique_ip) {
+                bind_array->add(resv.getPrefix());
+                bind_array->add(resv.getPrefixLen());
+            }
         } catch (const std::exception& ex) {
             isc_throw(DbOperationError,
                       "Could not create bind array from IPv6 Reservation: "
@@ -1354,30 +1376,32 @@ public:
     /// @note: please add new statements doing read only operations before
     /// the WRITE_STMTS_BEGIN position.
     enum StatementIndex {
-        GET_HOST_DHCPID,        // Gets hosts by host identifier
-        GET_HOST_ADDR,          // Gets hosts by IPv4 address
-        GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID
-        GET_HOST_SUBID6_DHCPID, // Gets host by IPv6 SubnetID, HW address/DUID
-        GET_HOST_SUBID_ADDR,    // Gets host by IPv4 SubnetID and IPv4 address
-        GET_HOST_PREFIX,        // Gets host by IPv6 prefix
-        GET_HOST_SUBID6_ADDR,   // Gets host by IPv6 SubnetID and IPv6 prefix
-        GET_HOST_SUBID4,        // Gets hosts by IPv4 SubnetID
-        GET_HOST_SUBID6,        // Gets hosts by IPv6 SubnetID
-        GET_HOST_HOSTNAME,      // Gets hosts by hostname
-        GET_HOST_HOSTNAME_SUBID4, // Gets hosts by hostname and IPv4 SubnetID
-        GET_HOST_HOSTNAME_SUBID6, // Gets hosts by hostname and IPv6 SubnetID
-        GET_HOST_SUBID4_PAGE,   // Gets hosts by IPv4 SubnetID beginning by HID
-        GET_HOST_SUBID6_PAGE,   // Gets hosts by IPv6 SubnetID beginning by HID
-        GET_HOST_PAGE4,         // Gets v4 hosts beginning by HID
-        GET_HOST_PAGE6,         // Gets v6 hosts beginning by HID
-        INSERT_HOST,            // Insert new host to collection
-        INSERT_V6_RESRV,        // Insert v6 reservation
-        INSERT_V4_HOST_OPTION,  // Insert DHCPv4 option
-        INSERT_V6_HOST_OPTION,  // Insert DHCPv6 option
-        DEL_HOST_ADDR4,         // Delete v4 host (subnet-id, addr4)
-        DEL_HOST_SUBID4_ID,     // Delete v4 host (subnet-id, ident.type, identifier)
-        DEL_HOST_SUBID6_ID,     // Delete v6 host (subnet-id, ident.type, identifier)
-        NUM_STATEMENTS          // Number of statements
+        GET_HOST_DHCPID,           // Gets hosts by host identifier
+        GET_HOST_ADDR,             // Gets hosts by IPv4 address
+        GET_HOST_SUBID4_DHCPID,    // Gets host by IPv4 SubnetID, HW address/DUID
+        GET_HOST_SUBID6_DHCPID,    // Gets host by IPv6 SubnetID, HW address/DUID
+        GET_HOST_SUBID_ADDR,       // Gets host by IPv4 SubnetID and IPv4 address
+        GET_HOST_PREFIX,           // Gets host by IPv6 prefix
+        GET_HOST_SUBID6_ADDR,      // Gets host by IPv6 SubnetID and IPv6 prefix
+        GET_HOST_SUBID4,           // Gets hosts by IPv4 SubnetID
+        GET_HOST_SUBID6,           // Gets hosts by IPv6 SubnetID
+        GET_HOST_HOSTNAME,         // Gets hosts by hostname
+        GET_HOST_HOSTNAME_SUBID4,  // Gets hosts by hostname and IPv4 SubnetID
+        GET_HOST_HOSTNAME_SUBID6,  // Gets hosts by hostname and IPv6 SubnetID
+        GET_HOST_SUBID4_PAGE,      // Gets hosts by IPv4 SubnetID beginning by HID
+        GET_HOST_SUBID6_PAGE,      // Gets hosts by IPv6 SubnetID beginning by HID
+        GET_HOST_PAGE4,            // Gets v4 hosts beginning by HID
+        GET_HOST_PAGE6,            // Gets v6 hosts beginning by HID
+        INSERT_HOST_NON_UNIQUE_IP, // Insert new host to collection with allowing IP duplicates
+        INSERT_HOST_UNIQUE_IP,     // Insert new host to collection with checking for IP duplicates
+        INSERT_V6_RESRV_NON_UNIQUE,// Insert v6 reservation without checking that it is unique
+        INSERT_V6_RESRV_UNIQUE,    // Insert v6 reservation with checking that it is unique
+        INSERT_V4_HOST_OPTION,     // Insert DHCPv4 option
+        INSERT_V6_HOST_OPTION,     // Insert DHCPv6 option
+        DEL_HOST_ADDR4,            // Delete v4 host (subnet-id, addr4)
+        DEL_HOST_SUBID4_ID,        // Delete v4 host (subnet-id, ident.type, identifier)
+        DEL_HOST_SUBID6_ID,        // Delete v6 host (subnet-id, ident.type, identifier)
+        NUM_STATEMENTS             // Number of statements
     };
 
     /// @brief Index of first statement performing write to the database.
@@ -1385,7 +1409,7 @@ public:
     /// This value is used to mark border line between queries and other
     /// statements and statements performing write operation on the database,
     /// such as INSERT, DELETE, UPDATE.
-    static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST;
+    static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST_UNIQUE_IP;
 
     /// @brief Constructor.
     ///
@@ -1551,6 +1575,10 @@ public:
     /// @brief The parameters
     PgSqlConnection::ParameterMap parameters_;
 
+    /// @brief Holds the setting whether the IP reservations must be unique or
+    /// may be non-unique.
+    bool ip_reservations_unique_;
+
     /// @brief The pool of contexts
     PgSqlHostContextPoolPtr pool_;
 };
@@ -1961,32 +1989,75 @@ TaggedStatementArray tagged_statements = { {
      "ORDER BY h.host_id, o.option_id, r.reservation_id"
     },
 
-    // PgSqlHostDataSourceImpl::INSERT_HOST
-    // Inserts a host into the 'hosts' table. Returns the inserted host id.
+    // Inserts a host into the 'hosts' table without checking that there is
+    // a reservation for the IP address.
     {13,
      { OID_BYTEA, OID_INT2,
        OID_INT8, OID_INT8, OID_INT8, OID_VARCHAR,
        OID_VARCHAR, OID_VARCHAR, OID_TEXT,
-       OID_INT8, OID_VARCHAR, OID_VARCHAR, OID_VARCHAR },
-     "insert_host",
+       OID_INT8, OID_VARCHAR, OID_VARCHAR, OID_VARCHAR},
+     "insert_host_non_unique_ip",
      "INSERT INTO hosts(dhcp_identifier, dhcp_identifier_type, "
      "  dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
      "  dhcp4_client_classes, dhcp6_client_classes, user_context, "
-     "  dhcp4_next_server, dhcp4_server_hostname, dhcp4_boot_file_name, auth_key) "
-     "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) "
+     "  dhcp4_next_server, dhcp4_server_hostname, dhcp4_boot_file_name, auth_key)"
+     "VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 ) "
      "RETURNING host_id"
     },
 
-    // PgSqlHostDataSourceImpl::INSERT_V6_RESRV
-    // Inserts a single IPv6 reservation into 'reservations' table.
+    // PgSqlHostDataSourceImpl::INSERT_HOST_UNIQUE_IP
+    // Inserts a host into the 'hosts' table with checking that reserved IP
+    // address is unique. The innermost query checks if there is at least
+    // one host for the given IP/subnet combination. If it not exists the
+    // new host is inserted. If the host with the given IP address already
+    // exists the new host won't be inserted. The caller can check the
+    // number of affected rows to detect that there was a duplicate host
+    // in the database. Returns the inserted host id.
+    {15,
+     { OID_BYTEA, OID_INT2,
+       OID_INT8, OID_INT8, OID_INT8, OID_VARCHAR,
+       OID_VARCHAR, OID_VARCHAR, OID_TEXT,
+       OID_INT8, OID_VARCHAR, OID_VARCHAR, OID_VARCHAR, OID_INT8,
+       OID_INT8},
+     "insert_host_unique_ip",
+     "INSERT INTO hosts(dhcp_identifier, dhcp_identifier_type, "
+     "  dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
+     "  dhcp4_client_classes, dhcp6_client_classes, user_context, "
+     "  dhcp4_next_server, dhcp4_server_hostname, dhcp4_boot_file_name, auth_key)"
+     "  SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13"
+     "    WHERE NOT EXISTS ("
+     "      SELECT 1 FROM HOSTS WHERE ipv4_address = $14 AND dhcp4_subnet_id = $15"
+     "    ) "
+     "RETURNING host_id"
+    },
+
+    // PgSqlHostDataSourceImpl::INSERT_V6_RESRV_NON_UNIQUE
+    // Inserts a single IPv6 reservation into 'reservations' table without
+    // checking that the inserted reservation is unique.
     {5,
      { OID_VARCHAR, OID_INT2, OID_INT4, OID_INT4, OID_INT4 },
-     "insert_v6_resrv",
+     "insert_v6_resrv_non_unique",
      "INSERT INTO ipv6_reservations(address, prefix_len, type, "
      "  dhcp6_iaid, host_id) "
      "VALUES ($1, $2, $3, $4, $5)"
     },
 
+    // PgSqlHostDataSourceImpl::INSERT_V6_RESRV_UNIQUE
+    // Inserts a single IPv6 reservation into 'reservations' table with
+    // checking that the inserted reservation is unique.
+    {7,
+     { OID_VARCHAR, OID_INT2, OID_INT4, OID_INT4, OID_INT4, OID_VARCHAR, OID_INT2 },
+     "insert_v6_resrv_unique",
+     "INSERT INTO ipv6_reservations(address, prefix_len, type, "
+     "  dhcp6_iaid, host_id) "
+     "SELECT $1, $2, $3, $4, $5 "
+     "  WHERE NOT EXISTS ("
+     "      SELECT 1 FROM ipv6_reservations"
+     "          WHERE address = $6 AND prefix_len = $7"
+     "      LIMIT 1"
+     "  )"
+    },
+
     // PgSqlHostDataSourceImpl::INSERT_V4_HOST_OPTION
     // Inserts a single DHCPv4 option into 'dhcp4_options' table.
     // Using fixed scope_id = 3, which associates an option with host.
@@ -2086,7 +2157,7 @@ PgSqlHostDataSource::PgSqlHostContextAlloc::~PgSqlHostContextAlloc() {
 }
 
 PgSqlHostDataSourceImpl::PgSqlHostDataSourceImpl(const PgSqlConnection::ParameterMap& parameters)
-    : parameters_(parameters) {
+    : parameters_(parameters), ip_reservations_unique_(true) {
 
     // Validate the schema version first.
     std::pair<uint32_t, uint32_t> code_version(PG_SCHEMA_VERSION_MAJOR,
@@ -2168,6 +2239,22 @@ PgSqlHostDataSourceImpl::addStatement(PgSqlHostContextPtr& ctx,
         ctx->conn_.checkStatementError(r, tagged_statements[stindex]);
     }
 
+    // Get the number of affected rows.
+    char* rows_affected = PQcmdTuples(r);
+    if (!rows_affected) {
+        isc_throw(DbOperationError,
+                  "Could not retrieve the number of affected rows.");
+    }
+
+    // If the number of rows inserted is 0 it means that the query detected
+    // an attempt to insert duplicated data for which there is no unique
+    // index in the database. Unique indexes are not created in the database
+    // when it may be sometimes allowed to insert duplicated records per
+    // server's configuration.
+    if (rows_affected[0] == '0') {
+        isc_throw(DuplicateEntry, "Database duplicate entry error");
+    }
+
     if (return_last_id) {
         PgSqlExchange::getColumnValue(r, 0, 0, last_id);
     }
@@ -2207,9 +2294,11 @@ void
 PgSqlHostDataSourceImpl::addResv(PgSqlHostContextPtr& ctx,
                                  const IPv6Resrv& resv,
                                  const HostID& id) {
-    PsqlBindArrayPtr bind_array = ctx->host_ipv6_reservation_exchange_->createBindForSend(resv, id);
+    PsqlBindArrayPtr bind_array = ctx->host_ipv6_reservation_exchange_->
+        createBindForSend(resv, id, ip_reservations_unique_);
 
-    addStatement(ctx, INSERT_V6_RESRV, bind_array);
+    addStatement(ctx, ip_reservations_unique_ ? INSERT_V6_RESRV_UNIQUE : INSERT_V6_RESRV_NON_UNIQUE,
+                 bind_array);
 }
 
 void
@@ -2349,11 +2438,19 @@ PgSqlHostDataSource::add(const HostPtr& host) {
     // the PgSqlTransaction class.
     PgSqlTransaction transaction(ctx->conn_);
 
+    // If we're configured to check that an IP reservation within a given subnet
+    // is unique, the IP reservation exists and the subnet is actually set
+    // we will be using a special query that checks for uniqueness. Otherwise,
+    // we will use a regular insert statement.
+    bool unique_ip = impl_->ip_reservations_unique_ && !host->getIPv4Reservation().isV4Zero()
+        && host->getIPv4SubnetID() != SUBNET_ID_UNUSED;
+
     // Create the PgSQL Bind array for the host
-    PsqlBindArrayPtr bind_array = ctx->host_ipv4_exchange_->createBindForSend(host);
+    PsqlBindArrayPtr bind_array = ctx->host_ipv4_exchange_->createBindForSend(host, unique_ip);
 
     // ... and insert the host.
-    uint32_t host_id = impl_->addStatement(ctx, PgSqlHostDataSourceImpl::INSERT_HOST,
+    uint32_t host_id = impl_->addStatement(ctx, unique_ip ? PgSqlHostDataSourceImpl::INSERT_HOST_UNIQUE_IP :
+                                           PgSqlHostDataSourceImpl::INSERT_HOST_NON_UNIQUE_IP,
                                            bind_array, true);
 
     // Insert DHCPv4 options.
@@ -2902,5 +2999,11 @@ PgSqlHostDataSource::rollback() {
     ctx->conn_.rollback();
 }
 
+bool
+PgSqlHostDataSource::setIPReservationUnique(const bool unique) {
+    impl_->ip_reservations_unique_ = unique;
+    return (true);
+}
+
 }  // namespace dhcp
 }  // namespace isc
index 5f7cb2b6aa0f88f44124d50f4509c858fe740141..152d8b0f722a2304ee659ea87f5e266a1d31d32a 100644 (file)
@@ -435,6 +435,23 @@ public:
     /// Rolls back all pending database operations.
     virtual void rollback();
 
+    /// @brief Controls whether IP reservations are unique or non-unique.
+    ///
+    /// In a typical case, the IP reservations are unique and backends verify
+    /// prior to adding a host reservation to the database that the reservation
+    /// for a given IP address/subnet does not exist. In some cases it may be
+    /// required to allow non-unique IP reservations, e.g. in the case when a
+    /// host has several interfaces and independently of which interface is used
+    /// by this host to communicate with the DHCP server the same IP address
+    /// should be assigned. In this case the @c unique value should be set to
+    /// false to disable the checks for uniqueness on the backend side.
+    ///
+    /// @param unique boolean flag indicating if the IP reservations must be
+    /// unique within the subnet or can be non-unique.
+    /// @return always true because this backend supports both the case when
+    /// the addresses must be unique and when they may be non-unique.
+    virtual bool setIPReservationUnique(const bool unique);
+
     /// @brief Context RAII Allocator.
     class PgSqlHostContextAlloc {
     public:
index 1df25a79cb594d74b2e5e53a662469bdbfd2b7ee..0432587c2197399e900e38d7a18e3efb35296cc3 100644 (file)
@@ -931,6 +931,29 @@ TEST_F(CfgHostsTest, add4AlreadyReserved) {
     EXPECT_THROW(cfg.add(host2), isc::dhcp::ReservedAddress);
 }
 
+// Test that it is possible to allow inserting multiple reservations for
+// the same IP address.
+TEST_F(CfgHostsTest, allow4AlreadyReserved) {
+    CfgHosts cfg;
+    // Allow creating multiple reservations for the same IP address.
+    ASSERT_TRUE(cfg.setIPReservationUnique(false));
+
+    // First host has a reservation for address 192.0.2.1
+    HostPtr host1 = HostPtr(new Host(hwaddrs_[0]->toText(false),
+                                     "hw-address",
+                                     SubnetID(1), SubnetID(SUBNET_ID_UNUSED),
+                                     IOAddress("192.0.2.1")));
+    ASSERT_NO_THROW(cfg.add(host1));
+
+    // The second host has a reservation for the same address.
+    HostPtr host2 = HostPtr(new Host(hwaddrs_[1]->toText(false),
+                                     "hw-address",
+                                     SubnetID(1), SUBNET_ID_UNUSED,
+                                     IOAddress("192.0.2.1")));
+    // Adding this should work because the HW address is different.
+    EXPECT_NO_THROW(cfg.add(host2));
+}
+
 // Checks that it's not possible for two hosts to have the same address
 // reserved at the same time.
 TEST_F(CfgHostsTest, add6Invalid2Hosts) {
@@ -957,6 +980,33 @@ TEST_F(CfgHostsTest, add6Invalid2Hosts) {
     EXPECT_THROW(cfg.add(host2), isc::dhcp::DuplicateHost);
 }
 
+// Test that it is possible to allow inserting multiple reservations for
+// the same IPv6 address.
+TEST_F(CfgHostsTest, allow6AlreadyReserved) {
+    CfgHosts cfg;
+    // Allow creating multiple reservations for the same IP address.
+    ASSERT_TRUE(cfg.setIPReservationUnique(false));
+
+    // First host has a reservation for address 2001:db8::1
+    HostPtr host1 = HostPtr(new Host(duids_[0]->toText(), "duid",
+                                     SUBNET_ID_UNUSED, SubnetID(1),
+                                     IOAddress("0.0.0.0")));
+    host1->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+                                    IOAddress("2001:db8::1")));
+    // Adding this should work.
+    EXPECT_NO_THROW(cfg.add(host1));
+
+    // The second host has a reservation for the same address.
+    HostPtr host2 = HostPtr(new Host(duids_[1]->toText(), "duid",
+                                     SUBNET_ID_UNUSED, SubnetID(1),
+                                     IOAddress("0.0.0.0")));
+    host2->addReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
+                                    IOAddress("2001:db8::1")));
+
+    // Adding this should work because the DUID is different.
+    EXPECT_NO_THROW(cfg.add(host2));
+}
+
 // Check that no error is reported when adding a host with subnet
 // ids equal to global.
 TEST_F(CfgHostsTest, globalSubnetIDs) {
index 1466f6f05d2854c3ef0bd2ad8c61666b84a0c64d..d7d372fbfb4378ffb69a8aae85b91e9d4e701eb4 100644 (file)
@@ -619,6 +619,12 @@ TEST_F(CqlHostDataSourceTest, addDuplicate4) {
     testAddDuplicate4();
 }
 
+/// @brief Test that the CQL backend does not support using non-unique
+/// IP addresses between multiple reservations.
+TEST_F(CqlHostDataSourceTest, disallowDuplicateIP) {
+    testDisallowDuplicateIP();
+}
+
 // This test verifies that DHCPv4 options can be inserted in a binary format
 /// and retrieved from the CQL host database.
 TEST_F(CqlHostDataSourceTest, optionsReservations4) {
index e277b7f41d75261e296582bbeb65fd64a99a3025..1a9e1e6cdf24ccf9286c32f65d720a798975f2e8 100644 (file)
@@ -692,6 +692,10 @@ public:
         return ("one");
     }
 
+    bool setIPReservationUnique(const bool) {
+        return (true);
+    }
+
     /// Specific methods
 
     /// @brief Set the entry
index 174c429e6b4abe88bccbaab67099299b1a684a00..adbbd07ec1073f4efde33577b4a597332d79fe5f 100644 (file)
@@ -1505,10 +1505,18 @@ TEST_F(MySQLHostMgrTest, get6ByPrefix) {
     testGet6ByPrefix(*getCfgHosts(), HostMgr::instance());
 }
 
+// This test verifies that it is possible to control whether the reserved
+// IP addresses are unique or non unique via the HostMgr.
+TEST_F(MySQLHostMgrTest, setIPReservationUnique) {
+    EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(true));
+    EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(false));
+}
+
 // Verifies that loss of connectivity to MySQL is handled correctly.
 TEST_F(MySQLHostMgrDbLostCallbackTest, testDbLostCallback) {
     testDbLostCallback();
 }
+
 #endif
 
 
@@ -1662,6 +1670,13 @@ TEST_F(PostgreSQLHostMgrTest, get6ByPrefix) {
     testGet6ByPrefix(*getCfgHosts(), HostMgr::instance());
 }
 
+// This test verifies that it is possible to control whether the reserved
+// IP addresses are unique or non unique via the HostMgr.
+TEST_F(PostgreSQLHostMgrTest, setIPReservationUnique) {
+    EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(true));
+    EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(false));
+}
+
 // Verifies that loss of connectivity to PostgreSQL is handled correctly.
 TEST_F(PostgreSQLHostMgrDbLostCallbackTest, testDbLostCallback) {
     testDbLostCallback();
@@ -1800,6 +1815,14 @@ TEST_F(CQLHostMgrTest, get6ByPrefix) {
     testGet6ByPrefix(*getCfgHosts(), HostMgr::instance());
 }
 
+// This test verifies that it is possible to control whether the reserved
+// IP addresses are unique or non unique via the HostMgr.
+TEST_F(CQLHostMgrTest, setIPReservationUnique) {
+    EXPECT_TRUE(HostMgr::instance().setIPReservationUnique(true));
+    // This is currently not supported for Cassandra.
+    EXPECT_FALSE(HostMgr::instance().setIPReservationUnique(false));
+}
+
 #endif
 
 }  // namespace
index 40d728bc63e5a66a71489c688452bfe821e6a3f2..d4f990cca6e750a72340db78a6bda56d9b267046 100644 (file)
@@ -1071,6 +1071,32 @@ TEST_F(MySqlHostDataSourceTest, addDuplicate6WithHWAddrMultiThreading) {
     testAddDuplicate6WithSameHWAddr();
 }
 
+/// @brief Test if the same IPv6 reservation can't be inserted multiple times.
+TEST_F(MySqlHostDataSourceTest, addDuplicateIPv6) {
+    testAddDuplicateIPv6();
+}
+
+/// @brief Test if the same IPv6 reservation can't be inserted multiple times.
+TEST_F(MySqlHostDataSourceTest, addDuplicateIPv6MultiThreading) {
+    MultiThreadingTest mt(true);
+    testAddDuplicateIPv6();
+}
+
+/// @brief Test if the host reservation for the same IPv6 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv6) {
+    testAllowDuplicateIPv6();
+}
+
+/// @brief Test if the host reservation for the same IPv6 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv6MultiThreading) {
+    MultiThreadingTest mt(true);
+    testAllowDuplicateIPv6();
+}
+
 /// @brief Test if the duplicate IPv4 host instances can't be inserted. The test logic is as
 /// follows: try to add multiple instances of the same host reservation and
 /// verify that the second and following attempts will throw exceptions.
@@ -1086,6 +1112,21 @@ TEST_F(MySqlHostDataSourceTest, addDuplicate4MultiThreading) {
     testAddDuplicate4();
 }
 
+/// @brief Test if the host reservation for the same IPv4 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv4) {
+    testAllowDuplicateIPv4();
+}
+
+/// @brief Test if the host reservation for the same IPv4 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(MySqlHostDataSourceTest, allowDuplicateIPv4MultiThreading) {
+    MultiThreadingTest mt(true);
+    testAllowDuplicateIPv4();
+}
+
 /// @brief This test verifies that DHCPv4 options can be inserted in a binary format
 /// and retrieved from the MySQL host database.
 TEST_F(MySqlHostDataSourceTest, optionsReservations4) {
index 6d1e0a4e1d42690be3ea04940e6a32bd5f4f9b4a..07551a0cb355431eb3acb13db3b819ba5e84789d 100644 (file)
@@ -1080,6 +1080,17 @@ TEST_F(PgSqlHostDataSourceTest, addDuplicate6WithHWAddrMultiThreading) {
     testAddDuplicate6WithSameHWAddr();
 }
 
+/// @brief Test if the same IPv6 reservation can't be inserted multiple times.
+TEST_F(PgSqlHostDataSourceTest, addDuplicateIPv6) {
+    testAddDuplicateIPv6();
+}
+
+/// @brief Test if the same IPv6 reservation can't be inserted multiple times.
+TEST_F(PgSqlHostDataSourceTest, addDuplicateIPv6MultiThreading) {
+    MultiThreadingTest mt(true);
+    testAddDuplicateIPv6();
+}
+
 /// @brief Test if the duplicate IPv4 host instances can't be inserted. The test logic is as
 /// follows: try to add multiple instances of the same host reservation and
 /// verify that the second and following attempts will throw exceptions.
@@ -1095,6 +1106,21 @@ TEST_F(PgSqlHostDataSourceTest, addDuplicate4MultiThreading) {
     testAddDuplicate4();
 }
 
+/// @brief Test if the host reservation for the same IPv4 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(PgSqlHostDataSourceTest, allowDuplicateIPv4) {
+    testAllowDuplicateIPv4();
+}
+
+/// @brief Test if the host reservation for the same IPv4 address can be inserted
+/// multiple times when allowed by the configuration and when the host identifier
+/// is different.
+TEST_F(PgSqlHostDataSourceTest, allowDuplicateIPv4MultiThreading) {
+    MultiThreadingTest mt(true);
+    testAllowDuplicateIPv4();
+}
+
 /// @brief This test verifies that DHCPv4 options can be inserted in a binary format
 /// and retrieved from the PostgreSQL host database.
 TEST_F(PgSqlHostDataSourceTest, optionsReservations4) {
index 9cd30e8fa36b7114898184baeef0bc2c539d110a..8e34bc15064b160eee057aead2c391f5ed1f0510 100644 (file)
@@ -1808,6 +1808,47 @@ GenericHostDataSourceTest::testAddDuplicate6WithSameHWAddr() {
     ASSERT_THROW(hdsptr_->add(host), DuplicateEntry);
 }
 
+void
+GenericHostDataSourceTest::testAddDuplicateIPv6() {
+    // Make sure we have the pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+
+    // Create a host reservation.
+    HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8::1", Host::IDENT_HWADDR, true);
+
+    // Add this reservation once.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+
+    // Create a host with a different identifier but the same IPv6 address. An attempt
+    // to create the reservation for the same IPv6 address should fail.
+    host = HostDataSourceUtils::initializeHost6("2001:db8::1", Host::IDENT_HWADDR, true);
+    EXPECT_THROW(hdsptr_->add(host), DuplicateEntry);
+}
+
+void
+GenericHostDataSourceTest::testAllowDuplicateIPv6() {
+    // Make sure we have the pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+    ASSERT_TRUE(hdsptr_->setIPReservationUnique(false));
+
+    // Create a host reservations.
+    HostPtr host = HostDataSourceUtils::initializeHost6("2001:db8::1", Host::IDENT_HWADDR, true);
+
+    // Add this reservation once.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+
+    // Then try to add it again, it should throw an exception because the
+    // HWADDR is the same.
+    ASSERT_THROW(hdsptr_->add(host), DuplicateEntry);
+
+    // This time use a different host identifier and try again.
+    // This update should succeed because we permitted to create
+    // multiple IP reservations for the same IP address but different
+    // identifier.
+    ASSERT_NO_THROW(host->setIdentifier("01:02:03:04:05:06", "hw-address"));
+    ASSERT_NO_THROW(hdsptr_->add(host));
+}
+
 void
 GenericHostDataSourceTest::testAddDuplicate4() {
     // Make sure we have the pointer to the host data source.
@@ -1834,6 +1875,42 @@ GenericHostDataSourceTest::testAddDuplicate4() {
     EXPECT_NO_THROW(hdsptr_->add(host));
 }
 
+void
+GenericHostDataSourceTest::testDisallowDuplicateIP() {
+    // Make sure we have the pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+    // The backend does not support switching to the mode in which multiple
+    // reservations for the same address can be created.
+    EXPECT_FALSE(hdsptr_->setIPReservationUnique(false));
+
+    // The default mode still can be used.
+    EXPECT_TRUE(hdsptr_->setIPReservationUnique(true));
+}
+
+void
+GenericHostDataSourceTest::testAllowDuplicateIPv4() {
+    // Make sure we have the pointer to the host data source.
+    ASSERT_TRUE(hdsptr_);
+    ASSERT_TRUE(hdsptr_->setIPReservationUnique(false));
+
+    // Create a host reservations.
+    HostPtr host = HostDataSourceUtils::initializeHost4("192.0.2.1", Host::IDENT_DUID);
+
+    // Add this reservation once.
+    ASSERT_NO_THROW(hdsptr_->add(host));
+
+    // Then try to add it again, it should throw an exception because the
+    // DUID is the same.
+    ASSERT_THROW(hdsptr_->add(host), DuplicateEntry);
+
+    // This time use a different host identifier and try again.
+    // This update should succeed because we permitted to create
+    // multiple IP reservations for the same IP address but different
+    // identifier.
+    ASSERT_NO_THROW(host->setIdentifier("01:02:03:04:05:06", "hw-address"));
+    ASSERT_NO_THROW(hdsptr_->add(host));
+}
+
 void
 GenericHostDataSourceTest::testAddr6AndPrefix() {
     // Make sure we have the pointer to the host data source.
index 57cb9f6cff76b661fd62b0b4e957f47ef179b435..1420f306d2ea105be97be5ff246b640b557de76b 100644 (file)
@@ -382,11 +382,32 @@ public:
     /// Uses gtest macros to report failures.
     void testAddDuplicate6WithSameHWAddr();
 
-    /// @brief Test if the duplicate IPv4 host with can't be inserted.
+    /// @brief Test that duplicate IPv6 reservation can't be inserted.
+    ///
+    /// Uses gtest macros to report failures.
+    void testAddDuplicateIPv6();
+
+    /// @brief Test if the reservation for the same IPv6 address can be
+    /// inserted when allowed by the configuration.
+    ///
+    /// Uses gtest macros to report failures.
+    void testAllowDuplicateIPv6();
+
+    /// @brief Test that duplicate IPv4 reservation can't be inserted.
     ///
     /// Uses gtest macros to report failures.
     void testAddDuplicate4();
 
+    /// @brief Test that the backend does not support a mode in which multiple
+    /// host reservations for the same IP address can be created.
+    void testDisallowDuplicateIP();
+
+    /// @brief Test if the reservation for the same IPv4 address can be
+    /// inserted when allowed by the configuration.
+    ///
+    /// Uses gtest macros to report failures.
+    void testAllowDuplicateIPv4();
+
     /// @brief Test that DHCPv4 options can be inserted and retrieved from
     /// the database.
     ///
index f3f4ae6d8a6024184014be97a2fad8b44eabc6fc..7ea08e0b08955e26e38238150e43a882cc4d814a 100644 (file)
@@ -267,6 +267,29 @@ public:
     /// @return number of hosts in the store.
     virtual size_t size() const;
 
+    /// @brief Controls whether IP reservations are unique or non-unique.
+    ///
+
+    /// In a typical case, the IP reservations are unique and backends verify
+    /// prior to adding a host reservation to the database that the reservation
+    /// for a given IP address does not exist. In some cases it may be required
+    /// to allow non-unique IP reservations, e.g. in the case when a host has
+    /// several interfaces and independently of which interface is used by this
+    /// host to communicate with the DHCP server the same IP address should be
+    /// assigned. In this case the @c unique value should be set to false to
+    /// disable the checks for uniqueness on the backend side.
+    ///
+    /// All backends are required to support the case when unique setting is
+    /// @c true and they must use this setting by default.
+    ///
+    /// @param unique boolean flag indicating if the IP reservations must be
+    /// unique or can be non-unique.
+    /// @return true if the new setting was accepted by the backend or false
+    /// otherwise.
+    virtual bool setIPReservationUnique(const bool) {
+        return (true);
+    }
+
 protected:
     // This is very simple storage.
 
index 21a011725f8e8c77deab3a66eb310125e4cd81c3..534fedf9f82cfb2f9a5cb13e0dfb71b79b4bc3cf 100644 (file)
@@ -53,7 +53,7 @@ const int MLM_MYSQL_FETCH_FAILURE = 0;
 /// @name Current database schema version values.
 //@{
 const uint32_t MYSQL_SCHEMA_VERSION_MAJOR = 9;
-const uint32_t MYSQL_SCHEMA_VERSION_MINOR = 3;
+const uint32_t MYSQL_SCHEMA_VERSION_MINOR = 4;
 
 //@}
 
index fb7764cd8afadc919e776f2c1ebe7400c3e89fac..3920bd81c39d650c54582801c4fb6be49e307d83 100644 (file)
@@ -17,9 +17,9 @@
 namespace isc {
 namespace db {
 
-/// @brief Define PostgreSQL backend version: 6.1
+/// @brief Define PostgreSQL backend version: 6.2
 const uint32_t PG_SCHEMA_VERSION_MAJOR = 6;
-const uint32_t PG_SCHEMA_VERSION_MINOR = 1;
+const uint32_t PG_SCHEMA_VERSION_MINOR = 2;
 
 // Maximum number of parameters that can be used a statement
 // @todo This allows us to use an initializer list (since we can't
index 30c1d7338d5e4ab76fdbb998ab7f522f362a0a90..f34befc7e542cb9b55eb888faae5e2acd00b7463 100644 (file)
@@ -14,4 +14,5 @@
 /upgrade_9.0_to_9.1.sh
 /upgrade_9.1_to_9.2.sh
 /upgrade_9.2_to_9.3.sh
+/upgrade_9.3_to_9.4.sh
 /wipe_data.sh
index f96fdd2545e462e2b941a6e079a25e1bb1d08216..b49c659a492f0632ffe96544414c79cdc81ee408 100644 (file)
@@ -2981,6 +2981,29 @@ SET version = '9', minor = '3';
 
 # This line concludes database upgrade to version 9.3.
 
+# Starting from this version we allow specifying multiple IP reservations
+# for the same address in certain DHCP configurations. The server may check
+# uniqueness of the IP addresses on its own. This is no longer checked at
+# the database level to faciliate the use cases when a single host may
+# get the same reserved IP address via different interfaces.
+
+# Replace the unique index with non-unique index so the queries for
+# hosts by IPv4 address are still efficient.
+DROP INDEX key_dhcp4_ipv4_address_subnet_id ON hosts;
+CREATE INDEX key_dhcp4_ipv4_address_subnet_id_identifier
+    ON hosts (ipv4_address ASC, dhcp4_subnet_id ASC);
+
+# Replace the unique index with non-unique index so the queries for
+# hosts by IPv6 address are still efficient.
+DROP INDEX key_dhcp6_address_prefix_len ON ipv6_reservations;
+CREATE INDEX key_dhcp6_address_prefix_len
+    ON ipv6_reservations (address ASC, prefix_len ASC);
+
+# Update the schema version number
+UPDATE schema_version
+SET version = '9', minor = '4';
+
+# This line concludes database upgrade to version 9.4.
 
 # Notes:
 #
diff --git a/src/share/database/scripts/mysql/upgrade_9.3_to_9.4.sh.in b/src/share/database/scripts/mysql/upgrade_9.3_to_9.4.sh.in
new file mode 100644 (file)
index 0000000..26b133e
--- /dev/null
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+prefix=@prefix@
+# Include utilities. Use installed version if available and
+# use build version if it isn't.
+if [ -e @datarootdir@/@PACKAGE_NAME@/scripts/admin-utils.sh ]; then
+    . @datarootdir@/@PACKAGE_NAME@/scripts/admin-utils.sh
+else
+    . @abs_top_builddir@/src/bin/admin/admin-utils.sh
+fi
+
+VERSION=`mysql_version "$@"`
+
+if [ "$VERSION" != "9.3" ]; then
+    printf "This script upgrades 9.3 to 9.4. Reported version is $VERSION. Skipping upgrade.\n"
+    exit 0
+fi
+
+mysql "$@" <<EOF
+
+# Starting from this version we allow specifying multiple IP reservations
+# for the same address in certain DHCP configurations. The server may check
+# uniqueness of the IP addresses on its own. This is no longer checked at
+# the database level to faciliate the use cases when a single host may
+# get the same reserved IP address via different interfaces.
+
+# Replace the unique index with non-unique index so the queries for
+# hosts by IPv4 address are still efficient.
+DROP INDEX key_dhcp4_ipv4_address_subnet_id ON hosts;
+CREATE INDEX key_dhcp4_ipv4_address_subnet_id_identifier
+    ON hosts (ipv4_address ASC, dhcp4_subnet_id ASC);
+
+# Replace the unique index with non-unique index so the queries for
+# hosts by IPv6 address are still efficient.
+DROP INDEX key_dhcp6_address_prefix_len ON ipv6_reservations;
+CREATE INDEX key_dhcp6_address_prefix_len
+    ON ipv6_reservations (address ASC, prefix_len ASC);
+
+# Update the schema version number
+UPDATE schema_version
+SET version = '9', minor = '4';
+
+# This line concludes database upgrade to version 9.4.
+
+EOF
+
+RESULT=$?
+
+exit $?
index e9fd4a794ae749040465ff2401768a5958387983..5cdd66d63e61ac2e9f3afdc79c5b7b76978ff9a0 100644 (file)
@@ -8,4 +8,5 @@
 /upgrade_5.0_to_5.1.sh
 /upgrade_5.1_to_6.0.sh
 /upgrade_6.0_to_6.1.sh
+/upgrade_6.1_to_6.2.sh
 /wipe_data.sh
index ce472c4fffa89ee8a08e454111eda686ce9abe57..e02b875b7b5f59072fa4ffa66b41ae9905ba5c48 100644 (file)
@@ -1015,6 +1015,33 @@ UPDATE schema_version
 -- Commit the script transaction
 COMMIT;
 
+-- Starting from this version we allow specifying multiple IP reservations
+-- for the same address in certain DHCP configurations. The server may check
+-- uniqueness of the IP addresses on its own. This is no longer checked at
+-- the database level to faciliate the use cases when a single host may
+-- get the same reserved IP address via different interfaces.
+
+-- Replace the unique index with non-unique index so the queries for
+-- hosts by IPv4 address are still efficient.
+DROP INDEX IF EXISTS key_dhcp4_ipv4_address_subnet_id;
+CREATE INDEX key_dhcp4_ipv4_address_subnet_id
+    ON hosts (ipv4_address ASC, dhcp4_subnet_id ASC);
+
+-- Replace the unique index with non-unique index so the queries for
+-- hosts by IPv6 address are still efficient.
+ALTER TABLE ipv6_reservations DROP CONSTRAINT IF EXISTS key_dhcp6_address_prefix_len;
+CREATE INDEX key_dhcp6_address_prefix_len
+    ON ipv6_reservations (address ASC, prefix_len ASC);
+
+-- Update the schema version number
+UPDATE schema_version
+    SET version = '6', minor = '2';
+
+-- Schema 6.2 specification ends here.
+
+-- Commit the script transaction
+COMMIT;
+
 -- Notes:
 
 -- Indexes
diff --git a/src/share/database/scripts/pgsql/upgrade_6.1_to_6.2.sh.in b/src/share/database/scripts/pgsql/upgrade_6.1_to_6.2.sh.in
new file mode 100644 (file)
index 0000000..70f6608
--- /dev/null
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+prefix=@prefix@
+# Include utilities. Use installed version if available and
+# use build version if it isn't.
+if [ -e @datarootdir@/@PACKAGE_NAME@/scripts/admin-utils.sh ]; then
+    . @datarootdir@/@PACKAGE_NAME@/scripts/admin-utils.sh
+else
+    . @abs_top_builddir@/src/bin/admin/admin-utils.sh
+fi
+
+VERSION=`pgsql_version "$@"`
+
+if [ "$VERSION" != "6.1" ]; then
+    printf "This script upgrades 6.1 to 6.2. Reported version is $VERSION. Skipping upgrade.\n"
+    exit 0
+fi
+
+psql "$@" >/dev/null <<EOF
+
+-- Starting from this version we allow specifying multiple IP reservations
+-- for the same address in certain DHCP configurations. The server may check
+-- uniqueness of the IP addresses on its own. This is no longer checked at
+-- the database level to faciliate the use cases when a single host may
+-- get the same reserved IP address via different interfaces.
+
+-- Replace the unique index with non-unique index so the queries for
+-- hosts by IPv4 address are still efficient.
+--
+-- Note we have introduced a bug a while ago which causes the index to have
+-- different names depending on whether the schema was created via the
+-- dhcpdb_create.pgsql script or via updates. Therefore, let's make sure
+-- that we drop the index regarless of its current name.
+DROP INDEX IF EXISTS key_dhcp4_ipv4_address_subnet_id;
+DROP INDEX IF EXISTS hosts_dhcp4_ipv4_address_subnet_id;
+CREATE INDEX key_dhcp4_ipv4_address_subnet_id
+    ON hosts (ipv4_address ASC, dhcp4_subnet_id ASC);
+
+-- Replace the unique index with non-unique index so the queries for
+-- hosts by IPv6 address are still efficient.
+ALTER TABLE ipv6_reservations DROP CONSTRAINT IF EXISTS key_dhcp6_address_prefix_len;
+CREATE INDEX key_dhcp6_address_prefix_len
+    ON ipv6_reservations (address ASC, prefix_len ASC);
+
+-- Update the schema version number
+UPDATE schema_version
+    SET version = '6', minor = '2';
+
+-- Schema 6.2 specification ends here.
+
+-- Commit the script transaction
+COMMIT;
+
+EOF
+
+exit $RESULT
+