Browse Source

[master] Merge branch 'trac5307'

Marcin Siodelski 7 years ago
parent
commit
110d0c9e40

+ 68 - 7
src/bin/dhcp6/dhcp6_srv.cc

@@ -879,9 +879,18 @@ Dhcpv6Srv::buildCfgOptionList(const Pkt6Ptr& question,
         }
     };
 
-    // Next, subnet configured options.
-    if (ctx.subnet_ && !ctx.subnet_->getCfgOption()->empty()) {
-        co_list.push_back(ctx.subnet_->getCfgOption());
+    if (ctx.subnet_) {
+        // Next, subnet configured options.
+        if (!ctx.subnet_->getCfgOption()->empty()) {
+            co_list.push_back(ctx.subnet_->getCfgOption());
+        }
+
+        // Then, shared network specific options.
+        SharedNetwork6Ptr network;
+        ctx.subnet_->getSharedNetwork(network);
+        if (network && !network->getCfgOption()->empty()) {
+            co_list.push_back(network->getCfgOption());
+        }
     }
 
     // Each class in the incoming packet
@@ -903,10 +912,12 @@ Dhcpv6Srv::buildCfgOptionList(const Pkt6Ptr& question,
             // Skip it
             continue;
         }
+
         if (ccdef->getCfgOption()->empty()) {
             // Skip classes which don't configure options
             continue;
         }
+
         co_list.push_back(ccdef->getCfgOption());
     }
 
@@ -2432,7 +2443,6 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(solicit, ctx);
-    setReservedClientClasses(solicit, ctx);
 
     Pkt6Ptr response(new Pkt6(DHCPV6_ADVERTISE, solicit->getTransid()));
 
@@ -2454,6 +2464,8 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
     processClientFqdn(solicit, response, ctx);
     assignLeases(solicit, response, ctx);
 
+    setReservedClientClasses(solicit, ctx);
+
     copyClientOptions(solicit, response);
     CfgOptionList co_list;
     buildCfgOptionList(solicit, ctx, co_list);
@@ -2461,6 +2473,8 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
     appendRequestedOptions(solicit, response, co_list);
     appendRequestedVendorOptions(solicit, response, ctx, co_list);
 
+    updateReservedFqdn(ctx, response);
+
     // Only generate name change requests if sending a Reply as a result
     // of receiving Rapid Commit option.
     if (response->getType() == DHCPV6_REPLY) {
@@ -2478,13 +2492,14 @@ Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(request, ctx);
-    setReservedClientClasses(request, ctx);
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, request->getTransid()));
 
     processClientFqdn(request, reply, ctx);
     assignLeases(request, reply, ctx);
 
+    setReservedClientClasses(request, ctx);
+
     copyClientOptions(request, reply);
     CfgOptionList co_list;
     buildCfgOptionList(request, ctx, co_list);
@@ -2492,6 +2507,7 @@ Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
     appendRequestedOptions(request, reply, co_list);
     appendRequestedVendorOptions(request, reply, ctx, co_list);
 
+    updateReservedFqdn(ctx, reply);
     generateFqdn(reply);
     createNameChangeRequests(reply, ctx);
 
@@ -2506,13 +2522,14 @@ Dhcpv6Srv::processRenew(const Pkt6Ptr& renew) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(renew, ctx);
-    setReservedClientClasses(renew, ctx);
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, renew->getTransid()));
 
     processClientFqdn(renew, reply, ctx);
     extendLeases(renew, reply, ctx);
 
+    setReservedClientClasses(renew, ctx);
+
     copyClientOptions(renew, reply);
     CfgOptionList co_list;
     buildCfgOptionList(renew, ctx, co_list);
@@ -2520,6 +2537,7 @@ Dhcpv6Srv::processRenew(const Pkt6Ptr& renew) {
     appendRequestedOptions(renew, reply, co_list);
     appendRequestedVendorOptions(renew, reply, ctx, co_list);
 
+    updateReservedFqdn(ctx, reply);
     generateFqdn(reply);
     createNameChangeRequests(reply, ctx);
 
@@ -2534,13 +2552,14 @@ Dhcpv6Srv::processRebind(const Pkt6Ptr& rebind) {
     // Let's create a simplified client context here.
     AllocEngine::ClientContext6 ctx;
     initContext(rebind, ctx);
-    setReservedClientClasses(rebind, ctx);
 
     Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, rebind->getTransid()));
 
     processClientFqdn(rebind, reply, ctx);
     extendLeases(rebind, reply, ctx);
 
+    setReservedClientClasses(rebind, ctx);
+
     copyClientOptions(rebind, reply);
     CfgOptionList co_list;
     buildCfgOptionList(rebind, ctx, co_list);
@@ -2548,6 +2567,7 @@ Dhcpv6Srv::processRebind(const Pkt6Ptr& rebind) {
     appendRequestedOptions(rebind, reply, co_list);
     appendRequestedVendorOptions(rebind, reply, ctx, co_list);
 
+    updateReservedFqdn(ctx, reply);
     generateFqdn(reply);
     createNameChangeRequests(reply, ctx);
 
@@ -3099,6 +3119,44 @@ Dhcpv6Srv::setReservedClientClasses(const Pkt6Ptr& pkt,
 }
 
 void
+Dhcpv6Srv::updateReservedFqdn(const AllocEngine::ClientContext6& ctx,
+                              const Pkt6Ptr& answer) {
+    if (!answer) {
+        isc_throw(isc::Unexpected, "an instance of the object encapsulating"
+                  " a message must not be NULL when updating reserved FQDN");
+    }
+
+    Option6ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<Option6ClientFqdn>
+        (answer->getOption(D6O_CLIENT_FQDN));
+
+   // If Client FQDN option is not included, there is nothing to do.
+   if (!fqdn) {
+       return;
+   }
+
+   std::string name = fqdn->getDomainName();
+
+   // If there is a host reservation for this client we have to check whether
+   // this reservation has the same hostname as the hostname currently
+   // present in the FQDN option. If not, it indicates that the allocation
+   // engine picked a different subnet (from within a shared network) for
+   // reservations and we have to send this new value to the client.
+   if (ctx.currentHost() &&
+       !ctx.currentHost()->getHostname().empty()) {
+       std::string new_name = CfgMgr::instance().getD2ClientMgr().
+           qualifyName(ctx.currentHost()->getHostname(), true);
+
+       if (new_name != name) {
+           fqdn->setDomainName(new_name, Option6ClientFqdn::FULL);
+
+           // Replace previous instance of Client FQDN option.
+           answer->delOption(D6O_CLIENT_FQDN);
+           answer->addOption(fqdn);
+       }
+   }
+}
+
+void
 Dhcpv6Srv::generateFqdn(const Pkt6Ptr& answer) {
     if (!answer) {
         isc_throw(isc::Unexpected, "an instance of the object encapsulating"
@@ -3162,6 +3220,9 @@ Dhcpv6Srv::generateFqdn(const Pkt6Ptr& answer) {
         // Set the generated FQDN in the Client FQDN option.
         fqdn->setDomainName(generated_name, Option6ClientFqdn::FULL);
 
+       answer->delOption(D6O_CLIENT_FQDN);
+       answer->addOption(fqdn);
+
     } catch (const Exception& ex) {
         LOG_ERROR(ddns6_logger, DHCP6_DDNS_GENERATED_FQDN_UPDATE_FAIL)
             .arg(answer->getLabel())

+ 19 - 0
src/bin/dhcp6/dhcp6_srv.h

@@ -764,6 +764,25 @@ private:
     /// @param classes a reference to added class names for logging
     void classifyByVendor(const Pkt6Ptr& pkt, std::string& classes);
 
+    /// @brief Update FQDN based on the reservations in the current subnet.
+    ///
+    /// When shared networks are in use the allocation engine may switch to
+    /// a different subnet than originally selected. If this new subnet has
+    /// hostname reservations there is a need to update the FQDN option
+    /// value.
+    ///
+    /// This method should be called after lease assignments to perform
+    /// such update when required.
+    ///
+    /// @param ctx Client context.
+    /// @param answer Message being sent to a client, which may hold an FQDN
+    /// option to be updated.
+    ///
+    /// @throw isc::Unexpected if specified message is NULL. This is treated
+    /// as a programmatic error.
+    void updateReservedFqdn(const AllocEngine::ClientContext6& ctx,
+                            const Pkt6Ptr& answer);
+
     /// @private
     /// @brief Generate FQDN to be sent to a client if none exists.
     ///

+ 1 - 0
src/bin/dhcp6/tests/Makefile.am

@@ -97,6 +97,7 @@ dhcp6_unittests_SOURCES += classify_unittests.cc
 dhcp6_unittests_SOURCES += parser_unittest.cc
 dhcp6_unittests_SOURCES += simple_parser6_unittest.cc
 dhcp6_unittests_SOURCES += get_config_unittest.cc get_config_unittest.h
+dhcp6_unittests_SOURCES += shared_network_unittest.cc
 
 nodist_dhcp6_unittests_SOURCES  = marker_file.h test_libraries.h
 

+ 21 - 1
src/bin/dhcp6/tests/dhcp6_client.cc

@@ -474,10 +474,11 @@ Dhcp6Client::doRequest() {
     copyIAs(context_.response_, query);
     appendRequestedIAs(query);
 
+    context_.query_ = query;
+
     // Add Client FQDN if configured.
     appendFQDN();
 
-    context_.query_ = query;
     sendMsg(context_.query_);
     context_.response_ = receiveOneMsg();
 
@@ -591,6 +592,25 @@ Dhcp6Client::doDecline(const bool include_address) {
 }
 
 void
+Dhcp6Client::doRelease() {
+    Pkt6Ptr query = createMsg(DHCPV6_RELEASE);
+    query->addOption(context_.response_->getOption(D6O_SERVERID));
+    copyIAsFromLeases(query);
+
+    context_.query_ = query;
+
+    sendMsg(context_.query_);
+    context_.response_ = receiveOneMsg();
+
+    // Apply configuration only if the server has responded.
+    if (context_.response_) {
+        config_.clear();
+        applyRcvdConfiguration(context_.response_);
+    }
+}
+
+
+void
 Dhcp6Client::generateIAFromLeases(const Pkt6Ptr& query,
                                   const bool include_address) {
     /// @todo: add support for IAPREFIX here.

+ 7 - 1
src/bin/dhcp6/tests/dhcp6_client.h

@@ -305,6 +305,12 @@ public:
     /// way, just stores it.
     void doInfRequest();
 
+    /// @brief Sends Release to the server.
+    ///
+    /// This function simulates sending the Release message to the server
+    /// and receiving server's response.
+    void doRelease();
+
     /// @brief Removes the stateful configuration obtained from the server.
     ///
     /// It removes all leases held by the client.
@@ -503,7 +509,7 @@ public:
     }
 
     /// @brief Returns the server that the client is communicating with.
-    boost::shared_ptr<isc::dhcp::test::NakedDhcpv6Srv> getServer() const {
+    boost::shared_ptr<isc::dhcp::test::NakedDhcpv6Srv>& getServer() {
         return (srv_);
     }
 

File diff suppressed because it is too large
+ 1303 - 0
src/bin/dhcp6/tests/shared_network_unittest.cc


+ 393 - 203
src/lib/dhcpsrv/alloc_engine.cc

@@ -19,6 +19,7 @@
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/ncr_generator.h>
 #include <dhcpsrv/network.h>
+#include <dhcpsrv/shared_network.h>
 #include <hooks/callout_handle.h>
 #include <hooks/hooks_manager.h>
 #include <dhcpsrv/callout_handle_store.h>
@@ -308,7 +309,8 @@ AllocEngine::AllocatorPtr AllocEngine::getAllocator(Lease::Type type) {
 template<typename ContextType>
 void
 AllocEngine::findReservationInternal(ContextType& ctx,
-                                     const AllocEngine::HostGetFunc& host_get) {
+                                     const AllocEngine::HostGetFunc& host_get,
+                                     const bool ipv6_only) {
     ctx.hosts_.clear();
 
     auto subnet = ctx.subnet_;
@@ -316,17 +318,28 @@ AllocEngine::findReservationInternal(ContextType& ctx,
     // We can only search for the reservation if a subnet has been selected.
     while (subnet) {
 
-        // Iterate over configured identifiers in the order of preference
-        // and try to use each of them to search for the reservations.
-        BOOST_FOREACH(const IdentifierPair& id_pair, ctx.host_identifiers_) {
-            // Attempt to find a host using a specified identifier.
-            ConstHostPtr host = host_get(subnet->getID(), id_pair.first,
-                                         &id_pair.second[0], id_pair.second.size());
-            // If we found matching host for this subnet.
-            if (host) {
-                ctx.hosts_[subnet->getID()] = host;
-                break;
+        // Only makes sense to get reservations if the client has access
+        // to the class.
+        if (subnet->clientSupported(ctx.query_->getClasses())) {
+            // Iterate over configured identifiers in the order of preference
+            // and try to use each of them to search for the reservations.
+            BOOST_FOREACH(const IdentifierPair& id_pair, ctx.host_identifiers_) {
+                // Attempt to find a host using a specified identifier.
+                ConstHostPtr host = host_get(subnet->getID(), id_pair.first,
+                                             &id_pair.second[0], id_pair.second.size());
+                // If we found matching host for this subnet.
+                if (host && (!ipv6_only || host->hasIPv6Reservation())) {
+                    ctx.hosts_[subnet->getID()] = host;
+                    break;
+                }
             }
+
+        } else {
+            LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
+                      ALLOC_ENGINE_RESERVATIONS_SKIPPED)
+                .arg(ctx.query_->getLabel())
+                .arg(subnet->toText());
+
         }
 
         // We need to get to the next subnet if this is a shared network. If it
@@ -336,13 +349,53 @@ AllocEngine::findReservationInternal(ContextType& ctx,
     }
 }
 
+} // end of namespace isc::dhcp
+} // end of namespace isc
+
+namespace {
+
+/// @brief Checks if the specified address belongs to one of the subnets
+/// within a shared network.
+///
+/// @param ctx Client context. Current subnet may be modified by this
+/// function when it belongs to a shared network.
+/// @param lease_type Type of the lease.
+/// @param address IPv6 address or prefix to be checked.
+///
+/// @return true if address belongs to a pool in a selected subnet or in
+/// a pool within any of the subnets belonging to the current shared network.
+bool
+inAllowedPool(AllocEngine::ClientContext6& ctx, const Lease::Type& lease_type,
+              const IOAddress& address) {
+    // If the subnet belongs to a shared network we will be iterating
+    // over the subnets that belong to this shared network.
+    Subnet6Ptr current_subnet = ctx.subnet_;
+    while (current_subnet) {
+
+        if (current_subnet->clientSupported(ctx.query_->getClasses())) {
+            if (current_subnet->inPool(lease_type, address)) {
+                return (true);
+            }
+        }
+
+        current_subnet = current_subnet->getNextSubnet(ctx.subnet_);
+    }
+
+    return (false);
+}
+
+}
+
 
 // ##########################################################################
 // #    DHCPv6 lease allocation code starts here.
 // ##########################################################################
 
+namespace isc {
+namespace dhcp {
+
 AllocEngine::ClientContext6::ClientContext6()
-    : query_(), fake_allocation_(false), subnet_(), duid_(),
+    : query_(), fake_allocation_(false), subnet_(), host_subnet_(), duid_(),
       hwaddr_(), host_identifiers_(), hosts_(), fwd_dns_update_(false),
       rev_dns_update_(false), hostname_(), callout_handle_(),
       ias_() {
@@ -397,8 +450,9 @@ isAllocated(const asiolink::IOAddress& prefix, const uint8_t prefix_len) const {
 
 ConstHostPtr
 AllocEngine::ClientContext6::currentHost() const {
-    if (subnet_) {
-        auto host = hosts_.find(subnet_->getID());
+    Subnet6Ptr subnet = host_subnet_ ? host_subnet_ : subnet_;
+    if (subnet) {
+        auto host = hosts_.find(subnet->getID());
         if (host != hosts_.cend()) {
             return (host->second);
         }
@@ -423,13 +477,21 @@ AllocEngine::allocateLeases6(ClientContext6& ctx) {
             isc_throw(InvalidOperation, "DUID is mandatory for IPv6 lease allocation");
         }
 
-        // Check if there are existing leases for that subnet/duid/iaid
-        // combination.
-        Lease6Collection leases =
-            LeaseMgrFactory::instance().getLeases6(ctx.currentIA().type_,
-                                                   *ctx.duid_,
-                                                   ctx.currentIA().iaid_,
-                                                   ctx.subnet_->getID());
+        // Check if there are existing leases for that shared network and
+        // DUID/IAID.
+        Subnet6Ptr subnet = ctx.subnet_;
+        Lease6Collection leases;
+        while (subnet) {
+            Lease6Collection leases_subnet =
+                LeaseMgrFactory::instance().getLeases6(ctx.currentIA().type_,
+                                                       *ctx.duid_,
+                                                       ctx.currentIA().iaid_,
+                                                       subnet->getID());
+            leases.insert(leases.end(), leases_subnet.begin(), leases_subnet.end());
+
+            subnet = subnet->getNextSubnet(ctx.subnet_);
+        }
+
         // Now do the checks:
         // Case 1. if there are no leases, and there are reservations...
         //   1.1. are the reserved addresses are used by someone else?
@@ -447,7 +509,7 @@ AllocEngine::allocateLeases6(ClientContext6& ctx) {
         //       assign new leases
 
         // Case 1: There are no leases and there's a reservation for this host.
-        if (leases.empty() && ctx.currentHost()) {
+        if (leases.empty() && !ctx.hosts_.empty()) {
 
             LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
                       ALLOC_ENGINE_V6_ALLOC_NO_LEASES_HR)
@@ -471,7 +533,7 @@ AllocEngine::allocateLeases6(ClientContext6& ctx) {
         // There is at least one lease for this client and there are no reservations.
         // We will return these leases for the client, but we may need to update
         // FQDN information.
-        } else if (!leases.empty() && !ctx.currentHost()) {
+        } else if (!leases.empty() && ctx.hosts_.empty()) {
 
             LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
                       ALLOC_ENGINE_V6_ALLOC_LEASES_NO_HR)
@@ -490,7 +552,7 @@ AllocEngine::allocateLeases6(ClientContext6& ctx) {
             // assign something new.
 
         // Case 3: There are leases and there are reservations.
-        } else if (!leases.empty() && ctx.currentHost()) {
+        } else if (!leases.empty() && !ctx.hosts_.empty()) {
 
             LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
                       ALLOC_ENGINE_V6_ALLOC_LEASES_HR)
@@ -596,169 +658,201 @@ AllocEngine::allocateUnreservedLeases6(ClientContext6& ctx) {
         hint = ctx.currentIA().hints_[0].first;
     }
 
-    // check if the hint is in pool and is available
-    // This is equivalent of subnet->inPool(hint), but returns the pool
-    Pool6Ptr pool = boost::dynamic_pointer_cast<
-        Pool6>(ctx.subnet_->getPool(ctx.currentIA().type_, hint, false));
-
-    if (pool) {
-
-        /// @todo: We support only one hint for now
-        Lease6Ptr lease =
-            LeaseMgrFactory::instance().getLease6(ctx.currentIA().type_, hint);
-        if (!lease) {
+    Subnet6Ptr original_subnet = ctx.subnet_;
+    Subnet6Ptr subnet = ctx.subnet_;
+    SharedNetwork6Ptr network;
+    subnet->getSharedNetwork(network);
 
-            // In-pool reservations: Check if this address is reserved for someone
-            // else. There is no need to check for whom it is reserved, because if
-            // it has been reserved for us we would have already allocated a lease.
+    Pool6Ptr pool;
 
-            ConstHostPtr host;
-            if (hr_mode != Network::HR_DISABLED) {
-                host = HostMgr::instance().get6(ctx.subnet_->getID(), hint);
-            }
+    while (subnet) {
 
-            if (!host) {
-                // If the in-pool reservations are disabled, or there is no
-                // reservation for a given hint, we're good to go.
+        if (!subnet->clientSupported(ctx.query_->getClasses())) {
+            subnet = subnet->getNextSubnet(original_subnet);
+            continue;
+        }
 
-                // The hint is valid and not currently used, let's create a
-                // lease for it
-                lease = createLease6(ctx, hint, pool->getLength());
+        ctx.subnet_ = subnet;
 
-                // It can happen that the lease allocation failed (we could
-                // have lost the race condition. That means that the hint is
-                // no longer usable and we need to continue the regular
-                // allocation path.
-                if (lease) {
+        // check if the hint is in pool and is available
+        // This is equivalent of subnet->inPool(hint), but returns the pool
+        pool = boost::dynamic_pointer_cast<Pool6>
+            (subnet->getPool(ctx.currentIA().type_, hint, false));
 
-                    /// @todo: We support only one lease per ia for now
-                    Lease6Collection collection;
-                    collection.push_back(lease);
-                    return (collection);
-                }
-            } else {
-                LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
-                          ALLOC_ENGINE_V6_HINT_RESERVED)
-                    .arg(ctx.query_->getLabel())
-                    .arg(hint.toText());
-            }
+        if (pool) {
 
-        } else {
+            /// @todo: We support only one hint for now
+            Lease6Ptr lease =
+                LeaseMgrFactory::instance().getLease6(ctx.currentIA().type_, hint);
+            if (!lease) {
 
-            // If the lease is expired, we may likely reuse it, but...
-            if (lease->expired()) {
+                // In-pool reservations: Check if this address is reserved for someone
+                // else. There is no need to check for whom it is reserved, because if
+                // it has been reserved for us we would have already allocated a lease.
 
                 ConstHostPtr host;
                 if (hr_mode != Network::HR_DISABLED) {
-                    host = HostMgr::instance().get6(ctx.subnet_->getID(), hint);
+                    host = HostMgr::instance().get6(subnet->getID(), hint);
                 }
 
-                // Let's check if there is a reservation for this address.
                 if (!host) {
-
-                    // Copy an existing, expired lease so as it can be returned
-                    // to the caller.
-                    Lease6Ptr old_lease(new Lease6(*lease));
-                    ctx.currentIA().old_leases_.push_back(old_lease);
-
-                    /// We found a lease and it is expired, so we can reuse it
-                    lease = reuseExpiredLease(lease, ctx, pool->getLength());
-
-                    /// @todo: We support only one lease per ia for now
-                    leases.push_back(lease);
-                    return (leases);
-
+                    // If the in-pool reservations are disabled, or there is no
+                    // reservation for a given hint, we're good to go.
+
+                    // The hint is valid and not currently used, let's create a
+                    // lease for it
+                    lease = createLease6(ctx, hint, pool->getLength());
+
+                    // It can happen that the lease allocation failed (we could
+                    // have lost the race condition. That means that the hint is
+                    // no longer usable and we need to continue the regular
+                    // allocation path.
+                    if (lease) {
+
+                        /// @todo: We support only one lease per ia for now
+                        Lease6Collection collection;
+                        collection.push_back(lease);
+                        return (collection);
+                    }
                 } else {
                     LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
-                              ALLOC_ENGINE_V6_EXPIRED_HINT_RESERVED)
+                              ALLOC_ENGINE_V6_HINT_RESERVED)
                         .arg(ctx.query_->getLabel())
                         .arg(hint.toText());
                 }
+
+            } else {
+
+                // If the lease is expired, we may likely reuse it, but...
+                if (lease->expired()) {
+
+                    ConstHostPtr host;
+                    if (hr_mode != Network::HR_DISABLED) {
+                        host = HostMgr::instance().get6(subnet->getID(), hint);
+                    }
+
+                    // Let's check if there is a reservation for this address.
+                    if (!host) {
+
+                        // Copy an existing, expired lease so as it can be returned
+                        // to the caller.
+                        Lease6Ptr old_lease(new Lease6(*lease));
+                        ctx.currentIA().old_leases_.push_back(old_lease);
+
+                        /// We found a lease and it is expired, so we can reuse it
+                        lease = reuseExpiredLease(lease, ctx, pool->getLength());
+
+                        /// @todo: We support only one lease per ia for now
+                        leases.push_back(lease);
+                        return (leases);
+
+                    } else {
+                        LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
+                                  ALLOC_ENGINE_V6_EXPIRED_HINT_RESERVED)
+                            .arg(ctx.query_->getLabel())
+                            .arg(hint.toText());
+                    }
+                }
             }
         }
+
+        subnet = subnet->getNextSubnet(original_subnet);
     }
 
-    // The hint was useless (it was not provided at all, was used by someone else,
-    // was out of pool or reserved for someone else). Search the pool until first
-    // of the following occurs:
-    // - we find a free address
-    // - we find an address for which the lease has expired
-    // - we exhaust number of tries
-    uint64_t max_attempts = (attempts_ > 0 ? attempts_  :
-                             ctx.subnet_->getPoolCapacity(ctx.currentIA().type_));
-    for (uint64_t i = 0; i < max_attempts; ++i)
-    {
-        IOAddress candidate = allocator->pickAddress(ctx.subnet_, ctx.duid_, hint);
+    uint64_t total_attempts = 0;
+    subnet = original_subnet;
 
-        /// In-pool reservations: Check if this address is reserved for someone
-        /// else. There is no need to check for whom it is reserved, because if
-        /// it has been reserved for us we would have already allocated a lease.
-        if (hr_mode == Network::HR_ALL &&
-            HostMgr::instance().get6(ctx.subnet_->getID(), candidate)) {
+    while (subnet) {
 
-            // Don't allocate.
+        if (!subnet->clientSupported(ctx.query_->getClasses())) {
+            subnet = subnet->getNextSubnet(original_subnet);
             continue;
         }
 
-        // The first step is to find out prefix length. It is 128 for
-        // non-PD leases.
-        uint8_t prefix_len = 128;
-        if (ctx.currentIA().type_ == Lease::TYPE_PD) {
-            pool = boost::dynamic_pointer_cast<Pool6>(
-                ctx.subnet_->getPool(ctx.currentIA().type_, candidate, false));
-            if (pool) {
-                prefix_len = pool->getLength();
+        // The hint was useless (it was not provided at all, was used by someone else,
+        // was out of pool or reserved for someone else). Search the pool until first
+        // of the following occurs:
+        // - we find a free address
+        // - we find an address for which the lease has expired
+        // - we exhaust number of tries
+        uint64_t max_attempts = (attempts_ > 0 ? attempts_  :
+                                 subnet->getPoolCapacity(ctx.currentIA().type_));
+
+        for (uint64_t i = 0; i < max_attempts; ++i) {
+
+            ++total_attempts;
+
+            IOAddress candidate = allocator->pickAddress(subnet, ctx.duid_,
+                                                         hint);
+
+            /// In-pool reservations: Check if this address is reserved for someone
+            /// else. There is no need to check for whom it is reserved, because if
+            /// it has been reserved for us we would have already allocated a lease.
+            if (hr_mode == Network::HR_ALL &&
+                HostMgr::instance().get6(subnet->getID(), candidate)) {
+
+                // Don't allocate.
+                continue;
             }
-        }
 
-        Lease6Ptr existing = LeaseMgrFactory::instance().getLease6(ctx.currentIA().type_,
+            // The first step is to find out prefix length. It is 128 for
+            // non-PD leases.
+            uint8_t prefix_len = 128;
+            if (ctx.currentIA().type_ == Lease::TYPE_PD) {
+                pool = boost::dynamic_pointer_cast<Pool6>(
+                    subnet->getPool(ctx.currentIA().type_, candidate, false));
+                if (pool) {
+                    prefix_len = pool->getLength();
+                }
+            }
+
+            Lease6Ptr existing = LeaseMgrFactory::instance().getLease6(ctx.currentIA().type_,
                                                                    candidate);
-        if (!existing) {
+            if (!existing) {
 
-            // there's no existing lease for selected candidate, so it is
-            // free. Let's allocate it.
-
-            Lease6Ptr lease = createLease6(ctx, candidate, prefix_len);
-            if (lease) {
-                // We are allocating a new lease (not renewing). So, the
-                // old lease should be NULL.
-                ctx.currentIA().old_leases_.clear();
-
-                leases.push_back(lease);
-                return (leases);
-            } else if (ctx.callout_handle_ &&
-                       (ctx.callout_handle_->getStatus() !=
-                        CalloutHandle::NEXT_STEP_CONTINUE)) {
-                // Don't retry when the callout status is not continue.
-                break;
-            }
+                // there's no existing lease for selected candidate, so it is
+                // free. Let's allocate it.
 
-            // Although the address was free just microseconds ago, it may have
-            // been taken just now. If the lease insertion fails, we continue
-            // allocation attempts.
-        } else {
-            if (existing->expired()) {
-                // Copy an existing, expired lease so as it can be returned
-                // to the caller.
-                Lease6Ptr old_lease(new Lease6(*existing));
-                ctx.currentIA().old_leases_.push_back(old_lease);
-
-                existing = reuseExpiredLease(existing,
-                                             ctx,
-                                             prefix_len);
-
-                leases.push_back(existing);
-                return (leases);
+                Lease6Ptr lease = createLease6(ctx, candidate, prefix_len);
+                if (lease) {
+                    // We are allocating a new lease (not renewing). So, the
+                    // old lease should be NULL.
+                    ctx.currentIA().old_leases_.clear();
+
+                    leases.push_back(lease);
+                    return (leases);
+                } else if (ctx.callout_handle_ &&
+                           (ctx.callout_handle_->getStatus() !=
+                            CalloutHandle::NEXT_STEP_CONTINUE)) {
+                    // Don't retry when the callout status is not continue.
+                    break;
+                }
+
+                // Although the address was free just microseconds ago, it may have
+                // been taken just now. If the lease insertion fails, we continue
+                // allocation attempts.
+            } else {
+                if (existing->expired()) {
+                    // Copy an existing, expired lease so as it can be returned
+                    // to the caller.
+                    Lease6Ptr old_lease(new Lease6(*existing));
+                    ctx.currentIA().old_leases_.push_back(old_lease);
+
+                    existing = reuseExpiredLease(existing, ctx, prefix_len);
+
+                    leases.push_back(existing);
+                    return (leases);
+                }
             }
         }
+
+        subnet = subnet->getNextSubnet(original_subnet);
     }
 
     // Unable to allocate an address, return an empty lease.
     LOG_WARN(alloc_engine_logger, ALLOC_ENGINE_V6_ALLOC_FAIL)
         .arg(ctx.query_->getLabel())
-        .arg(max_attempts);
-
-
+        .arg(total_attempts);
 
     // We failed to allocate anything. Let's return empty collection.
     return (Lease6Collection());
@@ -768,8 +862,9 @@ void
 AllocEngine::allocateReservedLeases6(ClientContext6& ctx,
                                      Lease6Collection& existing_leases) {
 
+
     // If there are no reservations or the reservation is v4, there's nothing to do.
-    if (!ctx.currentHost() || !ctx.currentHost()->hasIPv6Reservation()) {
+    if (ctx.hosts_.empty()) {
         LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
                   ALLOC_ENGINE_V6_ALLOC_NO_V6_HR)
             .arg(ctx.query_->getLabel());
@@ -785,8 +880,9 @@ AllocEngine::allocateReservedLeases6(ClientContext6& ctx,
     // we already have a lease for a reserved address or prefix.
     BOOST_FOREACH(const Lease6Ptr& lease, existing_leases) {
         if ((lease->valid_lft_ != 0)) {
-            if (ctx.currentHost()->hasReservation(IPv6Resrv(type, lease->addr_,
-                                                            lease->prefixlen_))) {
+            if ((ctx.hosts_.count(lease->subnet_id_) > 0) &&
+                ctx.hosts_[lease->subnet_id_]->hasReservation(IPv6Resrv(type, lease->addr_,
+                                                                        lease->prefixlen_))) {
                 // We found existing lease for a reserved address or prefix.
                 // We'll simply extend the lifetime of the lease.
                 LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
@@ -795,6 +891,37 @@ AllocEngine::allocateReservedLeases6(ClientContext6& ctx,
                     .arg(lease->typeToText(lease->type_))
                     .arg(lease->addr_.toText());
 
+                // Besides IP reservations we're also going to return other reserved
+                // parameters, such as hostname. We want to hand out the hostname value
+                // from the same reservation entry as IP addresses. Thus, let's see if
+                // there is any hostname reservation.
+                if (!ctx.host_subnet_) {
+                    SharedNetwork6Ptr network;
+                    ctx.subnet_->getSharedNetwork(network);
+                    if (network) {
+                        // Remember the subnet that holds this preferred host
+                        // reservation. The server will use it to return appropriate
+                        // FQDN, classes etc.
+                        ctx.host_subnet_ = network->getSubnet(lease->subnet_id_);
+                        ConstHostPtr host = ctx.hosts_[lease->subnet_id_];
+                        // If there is a hostname reservation here we should stick
+                        // to this reservation. By updating the hostname in the
+                        // context we make sure that the database is updated with
+                        // this new value and the server doesn't need to do it and
+                        // its processing performance is not impacted by the hostname
+                        // updates.
+                        if (host && !host->getHostname().empty()) {
+                            // We have to determine whether the hostname is generated
+                            // in response to client's FQDN or not. If yes, we will
+                            // need to qualify the hostname. Otherwise, we just use
+                            // the hostname as it is specified for the reservation.
+                            OptionPtr fqdn = ctx.query_->getOption(D6O_CLIENT_FQDN);
+                            ctx.hostname_ = CfgMgr::instance().getD2ClientMgr().
+                                qualifyName(host->getHostname(), static_cast<bool>(fqdn));
+                        }
+                    }
+                }
+
                 // If this is a real allocation, we may need to extend the lease
                 // lifetime.
                 if (!ctx.fake_allocation_ && conditionalExtendLifetime(*lease)) {
@@ -808,51 +935,97 @@ AllocEngine::allocateReservedLeases6(ClientContext6& ctx,
     // There is no lease for a reservation in this IA. So, let's now iterate
     // over reservations specified and try to allocate one of them for the IA.
 
-    // Get the IPv6 reservations of specified type.
-    const IPv6ResrvRange& reservs = ctx.currentHost()->getIPv6Reservations(type);
-    BOOST_FOREACH(IPv6ResrvTuple type_lease_tuple, reservs) {
-        // We do have a reservation for address or prefix.
-        const IOAddress& addr = type_lease_tuple.second.getPrefix();
-        uint8_t prefix_len = type_lease_tuple.second.getPrefixLen();
+    Subnet6Ptr subnet = ctx.subnet_;
+
+    while (subnet) {
+
+        SubnetID subnet_id = subnet->getID();
 
-        // We have allocated this address/prefix while processing one of the
-        // previous IAs, so let's try another reservation.
-        if (ctx.isAllocated(addr, prefix_len)) {
+        // No hosts for this subnet or the subnet not supported.
+        if (!subnet->clientSupported(ctx.query_->getClasses()) ||
+            ctx.hosts_.count(subnet_id) == 0) {
+            subnet = subnet->getNextSubnet(ctx.subnet_);
             continue;
         }
 
-        // If there's a lease for this address, let's not create it.
-        // It doesn't matter whether it is for this client or for someone else.
-        if (!LeaseMgrFactory::instance().getLease6(ctx.currentIA().type_,
+        ConstHostPtr host = ctx.hosts_[subnet_id];
+
+        // Get the IPv6 reservations of specified type.
+        const IPv6ResrvRange& reservs = host->getIPv6Reservations(type);
+        BOOST_FOREACH(IPv6ResrvTuple type_lease_tuple, reservs) {
+            // We do have a reservation for address or prefix.
+            const IOAddress& addr = type_lease_tuple.second.getPrefix();
+            uint8_t prefix_len = type_lease_tuple.second.getPrefixLen();
+
+            // We have allocated this address/prefix while processing one of the
+            // previous IAs, so let's try another reservation.
+            if (ctx.isAllocated(addr, prefix_len)) {
+                continue;
+            }
+
+            // If there's a lease for this address, let's not create it.
+            // It doesn't matter whether it is for this client or for someone else.
+            if (!LeaseMgrFactory::instance().getLease6(ctx.currentIA().type_,
                                                    addr)) {
-            // Ok, let's create a new lease...
-            Lease6Ptr lease = createLease6(ctx, addr, prefix_len);
 
-            // ... and add it to the existing leases list.
-            existing_leases.push_back(lease);
+                // Let's remember the subnet from which the reserved address has been
+                // allocated. We'll use this subnet for allocating other reserved
+                // resources.
+                ctx.subnet_ = subnet;
 
-            if (ctx.currentIA().type_ == Lease::TYPE_NA) {
-                LOG_INFO(alloc_engine_logger, ALLOC_ENGINE_V6_HR_ADDR_GRANTED)
-                    .arg(addr.toText())
-                    .arg(ctx.query_->getLabel());
-            } else {
-                LOG_INFO(alloc_engine_logger, ALLOC_ENGINE_V6_HR_PREFIX_GRANTED)
-                    .arg(addr.toText())
-                    .arg(static_cast<int>(prefix_len))
-                    .arg(ctx.query_->getLabel());
+                if (!ctx.host_subnet_) {
+                    ctx.host_subnet_ = subnet;
+                    if (!host->getHostname().empty()) {
+                        // If there is a hostname reservation here we should stick
+                        // to this reservation. By updating the hostname in the
+                        // context we make sure that the database is updated with
+                        // this new value and the server doesn't need to do it and
+                        // its processing performance is not impacted by the hostname
+                        // updates.
+
+                        // We have to determine whether the hostname is generated
+                        // in response to client's FQDN or not. If yes, we will
+                        // need to qualify the hostname. Otherwise, we just use
+                        // the hostname as it is specified for the reservation.
+                        OptionPtr fqdn = ctx.query_->getOption(D6O_CLIENT_FQDN);
+                        ctx.hostname_ = CfgMgr::instance().getD2ClientMgr().
+                            qualifyName(host->getHostname(), static_cast<bool>(fqdn));
+                    }
+                }
+
+                // Ok, let's create a new lease...
+                Lease6Ptr lease = createLease6(ctx, addr, prefix_len);
+
+                // ... and add it to the existing leases list.
+                existing_leases.push_back(lease);
+
+
+                if (ctx.currentIA().type_ == Lease::TYPE_NA) {
+                    LOG_INFO(alloc_engine_logger, ALLOC_ENGINE_V6_HR_ADDR_GRANTED)
+                        .arg(addr.toText())
+                        .arg(ctx.query_->getLabel());
+                } else {
+                    LOG_INFO(alloc_engine_logger, ALLOC_ENGINE_V6_HR_PREFIX_GRANTED)
+                        .arg(addr.toText())
+                        .arg(static_cast<int>(prefix_len))
+                        .arg(ctx.query_->getLabel());
+                }
+
+                // We found a lease for this client and this IA. Let's return.
+                // Returning after the first lease was assigned is useful if we
+                // have multiple reservations for the same client. If the client
+                // sends 2 IAs, the first time we call allocateReservedLeases6 will
+                // use the first reservation and return. The second time, we'll
+                // go over the first reservation, but will discover that there's
+                // a lease corresponding to it and will skip it and then pick
+                // the second reservation and turn it into the lease. This approach
+                // would work for any number of reservations.
+                return;
             }
 
-            // We found a lease for this client and this IA. Let's return.
-            // Returning after the first lease was assigned is useful if we
-            // have multiple reservations for the same client. If the client
-            // sends 2 IAs, the first time we call allocateReservedLeases6 will
-            // use the first reservation and return. The second time, we'll
-            // go over the first reservation, but will discover that there's
-            // a lease corresponding to it and will skip it and then pick
-            // the second reservation and turn it into the lease. This approach
-            // would work for any number of reservations.
-            return;
         }
+
+        subnet = subnet->getNextSubnet(ctx.subnet_);
     }
 }
 
@@ -875,16 +1048,16 @@ AllocEngine::removeNonmatchingReservedLeases6(ClientContext6& ctx,
     BOOST_FOREACH(const Lease6Ptr& candidate, copy) {
         // If we have reservation we should check if the reservation is for
         // the candidate lease. If so, we simply accept the lease.
-        if (ctx.currentHost()) {
+        if (ctx.hosts_.count(candidate->subnet_id_) > 0) {
             if (candidate->type_ == Lease6::TYPE_NA) {
-                if (ctx.currentHost()->hasReservation(IPv6Resrv(IPv6Resrv::TYPE_NA,
-                                                                candidate->addr_))) {
+                if (ctx.hosts_[candidate->subnet_id_]->hasReservation(
+                        IPv6Resrv(IPv6Resrv::TYPE_NA, candidate->addr_))) {
                     continue;
                 }
             } else {
-                if (ctx.currentHost()->hasReservation(IPv6Resrv(IPv6Resrv::TYPE_PD,
-                                                                candidate->addr_,
-                                                                candidate->prefixlen_))) {
+                if (ctx.hosts_[candidate->subnet_id_]->hasReservation(
+                        IPv6Resrv(IPv6Resrv::TYPE_PD,candidate->addr_,
+                                  candidate->prefixlen_))) {
                     continue;
                 }
             }
@@ -900,7 +1073,7 @@ AllocEngine::removeNonmatchingReservedLeases6(ClientContext6& ctx,
         // be allocated to us from a dynamic pool, but we must check if
         // this lease belongs to any pool. If it does, we can proceed to
         // checking the next lease.
-        if (!host && ctx.subnet_->inPool(candidate->type_, candidate->addr_)) {
+        if (!host && inAllowedPool(ctx, candidate->type_, candidate->addr_)) {
             continue;
         }
 
@@ -929,7 +1102,7 @@ AllocEngine::removeNonmatchingReservedLeases6(ClientContext6& ctx,
 
         // Need to decrease statistic for assigned addresses.
         StatsMgr::instance().addValue(
-            StatsMgr::generateName("subnet", ctx.subnet_->getID(),
+            StatsMgr::generateName("subnet", candidate->subnet_id_,
                                    ctx.currentIA().type_ == Lease::TYPE_NA ?
                                    "assigned-nas" : "assigned-pds"),
             static_cast<int64_t>(-1));
@@ -971,8 +1144,7 @@ AllocEngine::removeNonreservedLeases6(ClientContext6& ctx,
                                       Lease6Collection& existing_leases) {
     // This method removes leases that are not reserved for this host.
     // It will keep at least one lease, though.
-    if (existing_leases.empty() || !ctx.currentHost() ||
-        !ctx.currentHost()->hasIPv6Reservation()) {
+    if (existing_leases.empty()) {
         return;
     }
 
@@ -983,10 +1155,18 @@ AllocEngine::removeNonreservedLeases6(ClientContext6& ctx,
     // leases for deletion, by setting appropriate pointers to NULL.
     for (Lease6Collection::iterator lease = existing_leases.begin();
          lease != existing_leases.end(); ++lease) {
+
         IPv6Resrv resv(ctx.currentIA().type_ == Lease::TYPE_NA ?
                        IPv6Resrv::TYPE_NA : IPv6Resrv::TYPE_PD,
                        (*lease)->addr_, (*lease)->prefixlen_);
-        if (!ctx.currentHost()->hasReservation(resv)) {
+
+        // If there is no reservation for this subnet.
+        if ((ctx.hosts_.count((*lease)->subnet_id_) > 0) &&
+            (ctx.hosts_[(*lease)->subnet_id_]->hasReservation(resv))) {
+            continue;
+
+        } else {
+
             // We have reservations, but not for this lease. Release it.
 
             // Remove this lease from LeaseMgr
@@ -997,7 +1177,7 @@ AllocEngine::removeNonreservedLeases6(ClientContext6& ctx,
 
             // Need to decrease statistic for assigned addresses.
             StatsMgr::instance().addValue(
-                StatsMgr::generateName("subnet", ctx.subnet_->getID(),
+                StatsMgr::generateName("subnet", (*lease)->subnet_id_,
                                        ctx.currentIA().type_ == Lease::TYPE_NA ?
                                        "assigned-nas" : "assigned-pds"),
                 static_cast<int64_t>(-1));
@@ -1236,9 +1416,19 @@ AllocEngine::renewLeases6(ClientContext6& ctx) {
         }
 
         // Check if there are any leases for this client.
-        Lease6Collection leases = LeaseMgrFactory::instance()
-            .getLeases6(ctx.currentIA().type_, *ctx.duid_,
-                        ctx.currentIA().iaid_, ctx.subnet_->getID());
+        Subnet6Ptr subnet = ctx.subnet_;
+        Lease6Collection leases;
+        while (subnet) {
+            Lease6Collection leases_subnet =
+                LeaseMgrFactory::instance().getLeases6(ctx.currentIA().type_,
+                                                       *ctx.duid_,
+                                                       ctx.currentIA().iaid_,
+                                                       subnet->getID());
+            leases.insert(leases.end(), leases_subnet.begin(), leases_subnet.end());
+
+            subnet = subnet->getNextSubnet(ctx.subnet_);
+        }
+
 
         if (!leases.empty()) {
             LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
@@ -1250,7 +1440,7 @@ AllocEngine::renewLeases6(ClientContext6& ctx) {
             removeNonmatchingReservedLeases6(ctx, leases);
         }
 
-        if (ctx.currentHost()) {
+        if (!ctx.hosts_.empty()) {
 
             LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
                       ALLOC_ENGINE_V6_RENEW_HR)
@@ -1467,9 +1657,9 @@ AllocEngine::updateLeaseData(ClientContext6& ctx, const Lease6Collection& leases
 
                 // If the lease is in the current subnet we need to account
                 // for the re-assignment of The lease.
-                if (ctx.subnet_->inPool(ctx.currentIA().type_, lease->addr_)) {
+                if (inAllowedPool(ctx, ctx.currentIA().type_, lease->addr_)) {
                     StatsMgr::instance().addValue(
-                        StatsMgr::generateName("subnet", ctx.subnet_->getID(),
+                        StatsMgr::generateName("subnet", lease->subnet_id_,
                                                ctx.currentIA().type_ == Lease::TYPE_NA ?
                                                "assigned-nas" : "assigned-pds"),
                         static_cast<int64_t>(1));

+ 10 - 2
src/lib/dhcpsrv/alloc_engine.h

@@ -305,6 +305,11 @@ public:
         /// @brief Subnet selected for the client by the server.
         Subnet6Ptr subnet_;
 
+        /// @brief Subnet from which host reservations should be retrieved.
+        ///
+        /// It can be NULL, in which case @c subnet_ value is used.
+        Subnet6Ptr host_subnet_;
+
         /// @brief Client identifier
         DuidPtr duid_;
 
@@ -443,7 +448,7 @@ public:
             ias_.push_back(IAContext());
         };
 
-        /// @brief Returns host for currently selected subnet.
+        /// @brief Returns host from the most preferred subnet.
         ///
         /// @return Pointer to the host object.
         ConstHostPtr currentHost() const;
@@ -727,10 +732,13 @@ private:
     /// @param ctx Reference to a @ref ClientContext6 or @ref ClientContext4.
     /// @param host_get Pointer to the @ref HostMgr functions to be used
     /// to retrieve reservation by subnet identifier and host identifier.
+    /// @param ipv6_only Boolean value indicating if only IPv6 reservations
+    /// should be retrieved.
     /// @tparam ContextType Either @ref ClientContext6 or @ref ClientContext4.
     template<typename ContextType>
     static void findReservationInternal(ContextType& ctx,
-                                        const HostGetFunc& host_get);
+                                        const HostGetFunc& host_get,
+                                        const bool ipv6_only = false);
 
     /// @brief creates a lease and inserts it in LeaseMgr if necessary
     ///

+ 6 - 0
src/lib/dhcpsrv/alloc_engine_messages.mes

@@ -18,6 +18,12 @@ reclaimed has a corresponding DNS entry it needs to be removed.
 This message indicates that removal of the DNS entry has failed.
 Nevertheless the lease will be reclaimed.
 
+% ALLOC_ENGINE_RESERVATIONS_SKIPPED %1 not using reservations in subnet %2 because client's classes do not match
+This debug message is issued when the allocation engine will not use host
+reservations in a given subnet because this subnet is not allowed for
+the given client due to client classification. The first argument includes
+client identification information. The second argument identifies a subnet.
+
 % ALLOC_ENGINE_V4_ALLOC_ERROR %1: error during attempt to allocate an IPv4 address: %2
 An error occurred during an attempt to allocate an IPv4 address, the
 reason for the failure being contained in the message.  The server will

+ 349 - 0
src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc

@@ -1977,6 +1977,355 @@ TEST_F(AllocEngine6Test, reuseReclaimedExpiredViaRequest) {
     EXPECT_TRUE(testStatistics("reclaimed-leases", 0, subnet_->getID()));
 }
 
+/// @brief This test class is dedicated to testing shared networks
+///
+/// It uses one common configuration:
+/// 1 shared network with 2 subnets:
+///   - 2001:db8:1::/56 subnet with a small pool of single address
+///   - 2001:db8:1::/56 subnet with pool with 64K addresses.
+class SharedNetworkAlloc6Test : public AllocEngine6Test {
+public:
+    SharedNetworkAlloc6Test()
+        :engine_(AllocEngine::ALLOC_ITERATIVE, 0) {
+
+        subnet1_.reset(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+        subnet2_.reset(new Subnet6(IOAddress("2001:db8:2::"), 56, 1, 2, 3, 4));
+        pool1_.reset(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+                               IOAddress("2001:db8:1::1")));
+        pool2_.reset(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:2::"),
+                               IOAddress("2001:db8:2::FF")));
+        subnet1_->addPool(pool1_);
+        subnet2_->addPool(pool2_);
+
+        // Both subnets belong to the same network so they can be used
+        // interchangeably.
+        network_.reset(new SharedNetwork6("test_network"));
+        network_->add(subnet1_);
+        network_->add(subnet2_);
+    }
+
+    Lease6Ptr
+    insertLease(std::string addr, SubnetID subnet_id) {
+        Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress(addr), duid_, iaid_,
+                                   501, 502, 503, 504, subnet_->getID(),
+                                   HWAddrPtr(), 0));
+        lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+        if (!LeaseMgrFactory::instance().addLease(lease)) {
+            ADD_FAILURE() << "Failed to add a lease for address " << addr
+                          << " in subnet with subnet-id " << subnet_id;
+            return (Lease6Ptr());
+        }
+        return (lease);
+    }
+
+    /// Covenience pointers to configuration elements. These are initialized
+    /// in the constructor and are used throughout the tests.
+    AllocEngine engine_;
+    Subnet6Ptr subnet1_;
+    Subnet6Ptr subnet2_;
+    Pool6Ptr pool1_;
+    Pool6Ptr pool2_;
+    SharedNetwork6Ptr network_;
+};
+
+// This test verifies that the server can offer an address from a
+// subnet and the introduction of shared network doesn't break anything here.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkSimple) {
+
+    // Create context which will be used to try to allocate leases from the
+    // shared network. The context points to subnet1, which address space
+    // is exhausted. We expect the allocation engine to find another subnet
+    // within the same shared network and offer an address from there.
+    Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+    AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+                                    query);
+    ctx.currentIA().iaid_ = iaid_;
+
+    Lease6Ptr lease;
+    ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease);
+    ASSERT_TRUE(subnet1_->inRange(lease->addr_));
+}
+
+
+// This test verifies that the server can pick a subnet from shared subnets
+// based on hints.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkHint) {
+
+    // Create context which will be used to try to allocate leases from the
+    // shared network. There's a hint that points to the subnet2. The
+    // shared network mechanism should be able to pick the second subnet
+    // based on it.
+    Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+    AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+                                    query);
+    ctx.currentIA().iaid_ = iaid_;
+    ctx.currentIA().addHint(IOAddress("2001:db8:2::12"));
+
+    Lease6Ptr lease;
+    ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease);
+
+    // The second subnet should be selected.
+    ASSERT_TRUE(subnet2_->inRange(lease->addr_));
+}
+
+// This test verifies that the client is offerred an address from an
+// alternative subnet within shared network when the address pool is
+// exhausted in the first address pool.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkOutOfAddresses) {
+
+    // Create a lease for a single address in the first address pool. The
+    // pool is now exhausted.
+    DuidPtr other_duid(new DUID(vector<uint8_t>(12, 0xff)));
+    const uint32_t other_iaid = 3568;
+    Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+                               other_duid, other_iaid, 501, 502, 503, 504,
+                               subnet1_->getID(),
+                               HWAddrPtr(), 0));
+    lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
+    ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+    // Create context which will be used to try to allocate leases from the
+    // shared network. The context points to subnet1, which address space
+    // is exhausted. We expect the allocation engine to find another subnet
+    // within the same shared network and offer an address from there.
+    Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+    AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+                                    query);
+    ctx.currentIA().iaid_ = iaid_;
+
+    Lease6Ptr lease2;
+    ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease2);
+    ASSERT_TRUE(subnet2_->inRange(lease2->addr_));
+
+    // The client having a lease should be offerred this lease, even if
+    // the client starts allocation from the second subnet. The code should
+    // determine that the client has a lease in subnet1 and use this subnet
+    // instead.
+    AllocEngine::ClientContext6 ctx2(subnet2_, other_duid, false, false, "",
+                                     true, query);
+    ctx2.currentIA().iaid_ = other_iaid;
+    ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx2)));
+    ASSERT_TRUE(lease2);
+    ASSERT_EQ("2001:db8:1::1", lease2->addr_.toText());
+
+    // Delete the lease in the first subnet.
+    ASSERT_TRUE(LeaseMgrFactory::instance().deleteLease(lease->addr_));
+
+    // Now, try requesting this address by providing a hint. The engine
+    // should try to honor the hint even though we start from the subnet2.
+    ctx.subnet_ = subnet2_;
+    ctx.currentIA().hints_.push_back(make_pair(IOAddress("2001:db8:1::1"), 128));
+    ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease2);
+    ASSERT_TRUE(subnet1_->inRange(lease2->addr_));
+}
+
+// This test verifies that the server can offer an address from a
+// different subnet than orginally selected, when the address pool in
+// the first subnet is exhausted.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkClassification) {
+    // Try to offer address from subnet1. There is an address available so
+    // it should be offerred.
+    Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+    AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+                                    query);
+    ctx.currentIA().iaid_ = iaid_;
+
+    Lease6Ptr lease;
+    ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease);
+    ASSERT_TRUE(subnet1_->inRange(lease->addr_));
+
+    // Apply restrictions on the subnet1. This should be only assigned
+    // to clients belonging to cable-modem class.
+    subnet1_->allowClientClass("cable-modem");
+
+    // The allocation engine should determine that the subnet1 is not
+    // available for the client not belonging to the cable-modem class.
+    // Instead, it should offer an address from subnet2 that belongs
+    // to the same shared network.
+    AllocEngine::ClientContext6 ctx2(subnet1_, duid_, false, false, "", true,
+                                    query);
+    ctx2.currentIA().iaid_ = iaid_;
+    ctx2.query_ = query;
+    ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx2)));
+    ASSERT_TRUE(lease);
+    ASSERT_TRUE(subnet2_->inRange(lease->addr_));
+
+    AllocEngine::ClientContext6 ctx3(subnet1_, duid_, false, false, "", true,
+                                    query);
+    ctx3.currentIA().iaid_ = iaid_;
+    ctx3.query_ = query;
+
+    // Create host reservation in the first subnet for this client. The
+    // allocation engine should not assign reserved address to the client
+    // because client classification doesn't allow that.
+    subnet_ = subnet1_;
+    createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1"), 128);
+    AllocEngine::findReservation(ctx3);
+    ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease);
+    ASSERT_TRUE(subnet2_->inRange(lease->addr_));
+
+    AllocEngine::ClientContext6 ctx4(subnet1_, duid_, false, false, "", true,
+                                    query);
+    ctx4.currentIA().iaid_ = iaid_;
+    ctx4.query_ = query;
+
+    // Assign cable-modem class and try again. This time, we should
+    // offer an address from the subnet1_.
+    ctx4.query_->addClass(ClientClass("cable-modem"));
+
+    AllocEngine::findReservation(ctx4);
+    ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx4)));
+    ASSERT_TRUE(lease);
+    EXPECT_EQ("2001:db8:1::1", lease->addr_.toText());
+}
+
+// This test verifies that the client is offerred a reserved address
+// even if this address belongs to another subnet within the same
+// shared network.
+TEST_F(SharedNetworkAlloc6Test, solicitSharedNetworkReservations) {
+    // Create reservation for the client in the second subnet.
+    subnet_ = subnet2_;
+    createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:2::15"), 128);
+
+    // Start allocation from subnet1_. The engine should determine that the
+    // client has reservations in subnet2_ and should rather assign reserved
+    // addresses.
+    Pkt6Ptr query(new Pkt6(DHCPV6_SOLICIT, 1234));
+    AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", true,
+                                    query);
+    ctx.currentIA().iaid_ = iaid_;
+
+    // Find reservations for this subnet/shared network.
+    AllocEngine::findReservation(ctx);
+
+    Lease6Ptr lease;
+    ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease);
+    ASSERT_EQ("2001:db8:2::15", lease->addr_.toText());
+}
+
+// This test verifies that the client is allocated a reserved address
+// even if this address belongs to another subnet within the same
+// shared network.
+TEST_F(SharedNetworkAlloc6Test, requestSharedNetworkReservations) {
+    // Create reservation for the client in the second subnet.
+    subnet_ = subnet2_;
+    createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:2::15"), 128);
+
+    // Start allocation from subnet1_. The engine should determine that the
+    // client has reservations in subnet2_ and should rather assign reserved
+    // addresses.
+    Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+    AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", false,
+                                    query);
+    ctx.currentIA().iaid_ = iaid_;
+
+    // Find reservations for this subnet/shared network.
+    AllocEngine::findReservation(ctx);
+
+    Lease6Ptr lease;
+    ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease);
+    ASSERT_EQ("2001:db8:2::15", lease->addr_.toText());
+}
+
+// This test verifies that client is assigned an existing lease from a
+// shared network, regardless of the default subnet. It also verifies that
+// the client is assigned a reserved address from a shared network which
+// replaces existing lease within this shared network.
+TEST_F(SharedNetworkAlloc6Test, requestSharedNetworkExistingLeases) {
+    // Create a lease in subnet 2 for this client. The lease is in expired
+    // reclaimed state initially to allow for checking whether the lease
+    // gets renewed.
+    Lease6Ptr lease(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:2::1"),
+                               duid_, iaid_, 501, 502, 503, 504,
+                               subnet2_->getID(), HWAddrPtr(), 128));
+    lease->state_ = Lease::STATE_EXPIRED_RECLAIMED;
+    ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+    // Create context which will be used to try to allocate leases from the
+    // shared network. The context points to subnet 1 initially, but the
+    // allocation engine should determine that there are existing leases
+    // in subnet 2 and renew those.
+    Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+    AllocEngine::ClientContext6 ctx(subnet1_, duid_, false, false, "", false,
+                                    query);
+    ctx.currentIA().iaid_ = iaid_;
+
+    // Check that we have been allocated the existing lease.
+    Lease6Ptr lease2;
+    ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease2);
+    EXPECT_EQ("2001:db8:2::1", lease2->addr_.toText());
+
+    // Statistics should be bumped when the lease is re-assigned.
+    EXPECT_TRUE(testStatistics("assigned-nas", 1, subnet2_->getID()));
+
+    // Another interesting case is when there is a reservation in a different
+    // subnet than the one from which the ease has been assigned.
+    subnet_ = subnet1_;
+    createHost6(true, IPv6Resrv::TYPE_NA, IOAddress("2001:db8:1::1"), 128);
+
+    // The reserved lease should take precedence.
+    ctx.subnet_ = subnet1_;
+    ctx.currentIA().iaid_ = iaid_;
+    AllocEngine::findReservation(ctx);
+    ASSERT_NO_THROW(lease2 = expectOneLease(engine_.allocateLeases6(ctx)));
+    ASSERT_TRUE(lease2);
+    EXPECT_EQ("2001:db8:1::1", lease2->addr_.toText());
+
+    // The previous lease should have been removed.
+    ASSERT_EQ(1, ctx.currentIA().old_leases_.size());
+    EXPECT_EQ("2001:db8:2::1", ctx.currentIA().old_leases_[0]->addr_.toText());
+}
+
+// This test verifies that the server can offer an address from a shared
+// subnet if there's at least 1 address left there, but will not offer
+// anything if both subnets are completely full.
+TEST_F(SharedNetworkAlloc6Test, requestRunningOut) {
+
+    // Allocate everything in subnet1
+    insertLease("2001:db8:1::1", subnet1_->getID());
+
+    // Allocate everything, except one address in subnet2.
+    for (int i = 0; i < 255; i++) {
+        stringstream tmp;
+        tmp << "2001:db8:2::" << hex << i;
+        insertLease(tmp.str(), subnet2_->getID());
+    }
+
+    // Create context which will be used to try to allocate leases from the
+    // shared network. The context points to subnet 1 initially, but the
+    // allocation engine should determine that there are existing leases
+    // in subnet 2 and renew those.
+    Pkt6Ptr query(new Pkt6(DHCPV6_REQUEST, 1234));
+    AllocEngine::ClientContext6 ctx1(subnet1_, duid_, false, false, "", false,
+                                    query);
+    ctx1.currentIA().iaid_ = iaid_;
+
+    // Check that we have been allocated the existing lease (there's only
+    // one lease left, so we know exactly which one will be given out.
+    Lease6Ptr lease;
+    ASSERT_NO_THROW(lease = expectOneLease(engine_.allocateLeases6(ctx1)));
+    ASSERT_TRUE(lease);
+    EXPECT_EQ("2001:db8:2::ff", lease->addr_.toText());
+
+    // Ok, now try for anoher client. We should be completely full.
+    DuidPtr other_duid(new DUID(vector<uint8_t>(12, 0xff)));
+    AllocEngine::ClientContext6 ctx2(subnet2_, other_duid, false, false, "", false,
+                                    query);
+    Lease6Collection leases = engine_.allocateLeases6(ctx2);
+
+    // Bugger off, we're full!
+    EXPECT_TRUE(leases.empty());
+}
+
 }; // namespace test
 }; // namespace dhcp
 }; // namespace isc

+ 2 - 1
src/lib/dhcpsrv/tests/alloc_engine_utils.h

@@ -1,6 +1,7 @@
 // Copyright (C) 2015-2017 Internet Systems Consortium, Inc. ("ISC")
 //
-// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 #ifndef LIBDHCPSRV_ALLOC_ENGINE_UTILS_H