Browse Source

Merge branch 'trac2430'

Mukund Sivaraman 11 years ago
parent
commit
b05064f681

+ 468 - 49
src/lib/dns/master_loader.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014  Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -21,14 +21,16 @@
 #include <dns/rrtype.h>
 #include <dns/rdata.h>
 
-#include <boost/scoped_ptr.hpp>
+#include <boost/format.hpp>
 #include <boost/algorithm/string/predicate.hpp> // for iequals
+#include <boost/scoped_ptr.hpp>
+#include <boost/shared_ptr.hpp>
 
 #include <string>
 #include <memory>
 #include <vector>
-#include <boost/algorithm/string/predicate.hpp> // for iequals
-#include <boost/shared_ptr.hpp>
+
+#include <cstdio> // for sscanf()
 
 using std::string;
 using std::auto_ptr;
@@ -54,9 +56,34 @@ public:
 
 } // end unnamed namespace
 
+/// \brief Private implementation class for the \c MasterLoader
+///
+/// This class is used internally by the \c MasterLoader and is not
+/// publicly visible. It is present to avoid polluting the public API
+/// with internal implementation details of the \c MasterLoader.
 // cppcheck-suppress noConstructor
 class MasterLoader::MasterLoaderImpl {
 public:
+    /// \brief Constructor.
+    ///
+    /// \param master_file Path to the file to load.
+    /// \param zone_origin The origin of zone to be expected inside
+    ///     the master file. Currently unused, but it is expected to
+    ///     be used for some validation.
+    /// \param zone_class The class of zone to be expected inside the
+    ///     master file.
+    /// \param callbacks The callbacks by which it should report problems.
+    ///     Usually, the callback carries a filename and line number of the
+    ///     input where the problem happens. There's a special case of empty
+    ///     filename and zero line in case the opening of the top-level master
+    ///     file fails.
+    /// \param add_callback The callback which would be called with each
+    ///     loaded RR.
+    /// \param options Options for the parsing, which is bitwise-or of
+    ///     the Options values or DEFAULT. If the MANY_ERRORS option is
+    ///     included, the parser tries to continue past errors. If it
+    ///     is not included, it stops at first encountered error.
+    /// \throw std::bad_alloc when there's not enough memory.
     MasterLoaderImpl(const char* master_file,
                      const Name& zone_origin,
                      const RRClass& zone_class,
@@ -81,6 +108,16 @@ public:
         rr_count_(0)
     {}
 
+    /// \brief Wrapper around \c MasterLexer::pushSource() (file version)
+    ///
+    /// This method is used as a wrapper around the lexer's
+    /// \c pushSource() to also save the current origin and the last
+    /// seen name (to be restored upon \c popSource()). It also calls
+    /// \c pushSource(). See \c doInclude() implementation for more
+    /// details.
+    ///
+    /// \param filename Path to the file to push as a new source.
+    /// \param current_origin The current origin name to save.
     void pushSource(const std::string& filename, const Name& current_origin) {
         std::string error;
         if (!lexer_.pushSource(filename.c_str(), &error)) {
@@ -98,17 +135,35 @@ public:
         previous_name_ = false;
     }
 
+    /// \brief Wrapper around \c MasterLexer::pushSource() (stream version)
+    ///
+    /// Similar to \c pushSource(). This method need not save the
+    /// current origin as it is not used with $INCLUDE processing.
+    ///
+    /// \param stream The input stream to use as a new source.
     void pushStreamSource(std::istream& stream) {
         lexer_.pushSource(stream);
         initialized_ = true;
     }
 
+    /// \brief Implementation of \c MasterLoader::loadIncremental()
+    ///
+    /// See \c MasterLoader::loadIncremental() for details.
     bool loadIncremental(size_t count_limit);
 
+    /// \brief Return the total size of the input sources pushed so
+    /// far. See \c MasterLexer::getTotalSourceSize().
     size_t getSize() const { return (lexer_.getTotalSourceSize()); }
+
+    /// \brief Return the line number being parsed in the pushed input
+    /// sources. See \c MasterLexer::getPosition().
     size_t getPosition() const { return (lexer_.getPosition()); }
 
 private:
+    /// \brief Report an error using the callbacks that were supplied
+    /// during \c MasterLoader construction. Note that this method also
+    /// throws \c MasterLoaderError exception if necessary, so the
+    /// caller need not throw it.
     void reportError(const std::string& filename, size_t line,
                      const std::string& reason)
     {
@@ -123,6 +178,12 @@ private:
         }
     }
 
+    /// \brief Wrapper around \c MasterLexer::popSource()
+    ///
+    /// This method is used as a wrapper around the lexer's
+    /// \c popSource() to also restore the current origin and the last
+    /// seen name (at time of push). It also calls \c popSource(). See
+    /// \c doInclude() implementation for more details.
     bool popSource() {
         if (lexer_.getSourceCount() == 1) {
             return (false);
@@ -141,14 +202,43 @@ private:
         return (true);
     }
 
-    // Get a string token. Handle it as error if it is not string.
+    /// \brief Get a string token. Handle it as error if it is not string.
     const string getString() {
         lexer_.getNextToken(MasterToken::STRING).getString(string_token_);
         return (string_token_);
     }
 
+    /// \brief Parse the initial token at the beginning of a line in a
+    /// master file (or stream).
+    ///
+    /// A helper method of \c loadIncremental(), parsing the first token
+    /// of a new line.  If it looks like an RR, detect its owner name
+    /// and return a string token for the next field of the RR.
+    ///
+    /// Otherwise, return either \c END_OF_LINE or \c END_OF_FILE token
+    /// depending on whether the loader continues to the next line or
+    /// completes the load, respectively.  Other corner cases including
+    /// $-directive handling is done here.
+    ///
+    /// For unexpected errors, it throws an exception, which will be
+    /// handled in \c loadIncremental().
     MasterToken handleInitialToken();
 
+    /// \brief Helper method for \c doGenerate().
+    ///
+    /// This is a helper method for \c doGenerate() that processes the
+    /// LHS or RHS for a single iteration in the range that is requested
+    /// by the $GENERATE directive and returns a generated string (that
+    /// is used to build a name (LHS) or RDATA (RHS) for an RR). See the
+    /// commented implementation for details.
+    std::string generateForIter(const std::string& str, const int it);
+
+    /// \brief Process the $GENERATE directive.
+    ///
+    /// See the commented implementation for details.
+    void doGenerate();
+
+    /// \brief Process the $ORIGIN directive.
     void doOrigin(bool is_optional) {
         // Parse and create the new origin. It is relative to the previous
         // one.
@@ -181,6 +271,7 @@ private:
         }
     }
 
+    /// \brief Process the $INCLUDE directive.
     void doInclude() {
         // First, get the filename to include
         const string
@@ -201,11 +292,16 @@ private:
         pushSource(filename, current_origin);
     }
 
-    // A helper method for loadIncremental(). It parses part of an RR
-    // until it finds the RR type field.  If TTL or RR class is
-    // specified before the RR type, it also recognizes and validates
-    // them.  explicit_ttl will be set to true if this method finds a
-    // valid TTL field.
+    /// \brief Parse RR fields (TTL, CLASS and TYPE).
+    ///
+    /// A helper method for \c loadIncremental(). It parses part of an
+    /// RR until it finds the RR type field.  If TTL or RR class is
+    /// specified before the RR type, it also recognizes and validates
+    /// them.
+    ///
+    /// \param explicit_ttl will be set to true if this method finds a
+    /// valid TTL field.
+    /// \param rrparam_token Pass the current (parsed) token here.
     RRType parseRRParams(bool& explicit_ttl, MasterToken rrparam_token) {
         // Find TTL, class and type.  Both TTL and class are
         // optional and may occur in any order if they exist. TTL
@@ -245,20 +341,25 @@ private:
         return (RRType(rrparam_token.getString()));
     }
 
-    // Upper limit check when recognizing a specific TTL value from the
-    // zone file ($TTL, the RR's TTL field, or the SOA minimum).  RFC2181
-    // Section 8 limits the range of TTL values to 2^31-1 (0x7fffffff),
-    // and prohibits transmitting a TTL field exceeding this range.  We
-    // guarantee that by limiting the value at the time of zone
-    // parsing/loading, following what BIND 9 does.  Resetting it to 0
-    // at this point may not be exactly what the RFC states (depending on
-    // the meaning of 'received'), but the end result would be the same (i.e.,
-    // the guarantee on transmission).  Again, we follow the BIND 9's behavior
-    // here.
-    //
-    // post_parsing is true iff this method is called after parsing the entire
-    // RR and the lexer is positioned at the next line.  It's just for
-    // calculating the accurate source line when callback is necessary.
+    /// \brief Check and limit TTL to maximum value.
+    ///
+    /// Upper limit check when recognizing a specific TTL value from the
+    /// zone file ($TTL, the RR's TTL field, or the SOA minimum).  RFC2181
+    /// Section 8 limits the range of TTL values to 2^31-1 (0x7fffffff),
+    /// and prohibits transmitting a TTL field exceeding this range.  We
+    /// guarantee that by limiting the value at the time of zone
+    /// parsing/loading, following what BIND 9 does.  Resetting it to 0
+    /// at this point may not be exactly what the RFC states (depending on
+    /// the meaning of 'received'), but the end result would be the same (i.e.,
+    /// the guarantee on transmission).  Again, we follow the BIND 9's behavior
+    /// here.
+    ///
+    /// \param ttl the TTL to check. If it is larger than the maximum
+    /// allowed, it is set to 0.
+    /// \param post_parsing should be true iff this method is called
+    /// after parsing the entire RR and the lexer is positioned at the
+    /// next line. It's just for calculating the accurate source line
+    /// when callback is necessary.
     void limitTTL(RRTTL& ttl, bool post_parsing) {
         if (ttl > RRTTL::MAX_TTL()) {
             const size_t src_line = lexer_.getSourceLine() -
@@ -270,19 +371,25 @@ private:
         }
     }
 
-    // Set/reset the default TTL.  This should be from either $TTL or SOA
-    // minimum TTL (it's the caller's responsibility; this method doesn't
-    // care about where it comes from).  see LimitTTL() for parameter
-    // post_parsing.
+    /// \brief Set/reset the default TTL.
+    ///
+    /// This should be from either $TTL or SOA minimum TTL (it's the
+    /// caller's responsibility; this method doesn't care about where it
+    /// comes from). See \c limitTTL() for parameter post_parsing.
     void setDefaultTTL(const RRTTL& ttl, bool post_parsing) {
         assignTTL(default_ttl_, ttl);
         limitTTL(*default_ttl_, post_parsing);
     }
 
-    // Try to set/reset the current TTL from candidate TTL text.  It's possible
-    // it does not actually represent a TTL (which is not immediately
-    // considered an error).  Return true iff it's recognized as a valid TTL
-    // (and only in which case the current TTL is set).
+    /// \brief Try to set/reset the current TTL from candidate TTL text.
+    ///
+    /// It's possible it that the text does not actually represent a TTL
+    /// (which is not immediately considered an error). Returns \c true
+    /// iff it's recognized as a valid TTL (and only in which case the
+    /// current TTL is set).
+    ///
+    /// \param ttl_txt The text to parse as a TTL.
+    /// \return true if a TTL was parsed (and set as the current TTL).
     bool setCurrentTTL(const string& ttl_txt) {
         // We use the factory version instead of RRTTL constructor as we
         // need to expect cases where ttl_txt does not actually represent a TTL
@@ -296,14 +403,15 @@ private:
         return (false);
     }
 
-    // Determine the TTL of the current RR based on the given parsing context.
-    //
-    // explicit_ttl is true iff the TTL is explicitly specified for that RR
-    // (in which case current_ttl_ is set to that TTL).
-    // rrtype is the type of the current RR, and rdata is its RDATA.  They
-    // only matter if the type is SOA and no available TTL is known.  In this
-    // case the minimum TTL of the SOA will be used as the TTL of that SOA
-    // and the default TTL for subsequent RRs.
+    /// \brief Determine the TTL of the current RR based on the given
+    /// parsing context.
+    ///
+    /// \c explicit_ttl is true iff the TTL is explicitly specified for that RR
+    /// (in which case current_ttl_ is set to that TTL).
+    /// \c rrtype is the type of the current RR, and \c rdata is its RDATA.  They
+    /// only matter if the type is SOA and no available TTL is known.  In this
+    /// case the minimum TTL of the SOA will be used as the TTL of that SOA
+    /// and the default TTL for subsequent RRs.
     const RRTTL& getCurrentTTL(bool explicit_ttl, const RRType& rrtype,
                                const rdata::ConstRdataPtr& rdata) {
         // We've completed parsing the full of RR, and the lexer is already
@@ -342,12 +450,19 @@ private:
         return (*current_ttl_);
     }
 
+    /// \brief Handle a $DIRECTIVE
+    ///
+    /// This method is called when a $DIRECTIVE is encountered in the
+    /// input stream.
     void handleDirective(const char* directive, size_t length) {
         if (iequals(directive, "INCLUDE")) {
             doInclude();
         } else if (iequals(directive, "ORIGIN")) {
             doOrigin(false);
             eatUntilEOL(true);
+        } else if (iequals(directive, "GENERATE")) {
+            doGenerate();
+            eatUntilEOL(true);
         } else if (iequals(directive, "TTL")) {
             setDefaultTTL(RRTTL(getString()), false);
             eatUntilEOL(true);
@@ -357,6 +472,7 @@ private:
         }
     }
 
+    /// \brief Skip tokens until end-of-line.
     void eatUntilEOL(bool reportExtra) {
         // We want to continue. Try to read until the end of line
         for (;;) {
@@ -437,15 +553,318 @@ public:
     size_t rr_count_;    // number of RRs successfully loaded
 };
 
-// A helper method of loadIncremental, parsing the first token of a new line.
-// If it looks like an RR, detect its owner name and return a string token for
-// the next field of the RR.
-// Otherwise, return either END_OF_LINE or END_OF_FILE token depending on
-// whether the loader continues to the next line or completes the load,
-// respectively.  Other corner cases including $-directive handling is done
-// here.
-// For unexpected errors, it throws an exception, which will be handled in
-// loadIncremental.
+namespace { // begin unnamed namespace
+
+/// \brief Generate a dotted nibble sequence.
+///
+/// This method generates a dotted nibble sequence and returns it as a
+/// string. The nibbles are appended from the least significant digit
+/// (in hex representation of \c num) to the most significant digit with
+/// dots ('.') to separate the digits. If \c width is non-zero and the
+/// dotted nibble sequence has not filled the requested width, the rest
+/// of the width is filled with a dotted nibble sequence of 0 nibbles.
+///
+/// Some sample representations:
+///
+/// num = 0x1234, width = 0
+/// "4.3.2.1"
+///
+/// num = 0x1234, width = 1
+/// "4.3.2.1"
+///
+/// num = 0x1234, width = 8
+/// "4.3.2.1"
+///
+/// num = 0x1234, width = 9
+/// "4.3.2.1."
+///
+/// num = 0x1234, width = 10
+/// "4.3.2.1.0"
+///
+/// num = 0x1234, width = 11
+/// "4.3.2.1.0."
+///
+/// num = 0xabcd, width = 0, uppercase = true
+/// "D.C.B.A"
+///
+/// num = 0, width = 0
+/// "0"
+///
+/// num = 0, width = 1
+/// "0"
+///
+/// num = 0, width = 2
+/// "0."
+///
+/// num = 0, width = 3
+/// "0.0"
+///
+/// \param num The number for which the dotted nibble sequence should be
+/// generated.
+/// \param width The width of the generated string. This is only
+/// meaningful when it is larger than the dotted nibble sequence
+/// representation of \c num.
+/// \param uppercase Whether to use uppercase characters in nibble
+/// sequence.
+/// \return A string containing the dotted nibble sequence.
+std::string
+genNibbles(int num, unsigned int width, bool uppercase) {
+    static const char *hex = "0123456789abcdef0123456789ABCDEF";
+    std::string rstr;
+
+    do {
+        char ch = hex[(num & 0x0f) + (uppercase ? 16 : 0)];
+        num >>= 4;
+        rstr.push_back(ch);
+
+        if (width > 0) {
+            --width;
+        }
+
+        // If width is non zero then we need to add a label separator.
+        // If value is non zero then we need to add another label and
+        // that requires a label separator.
+        if (width > 0 || num != 0) {
+            rstr.push_back('.');
+
+            if (width > 0) {
+                --width;
+            }
+        }
+    } while ((num != 0) || (width > 0));
+
+    return (rstr);
+}
+
+} // end unnamed namespace
+
+std::string
+MasterLoader::MasterLoaderImpl::generateForIter(const std::string& str,
+                                                const int num)
+{
+  std::string rstr;
+
+  for (std::string::const_iterator it = str.begin(); it != str.end();) {
+      switch (*it) {
+      case '$':
+          // This is the case when the '$' character is encountered in
+          // the LHS or RHS. A computed value is added in its place in
+          // the generated string.
+          ++it;
+          if ((it != str.end()) && (*it == '$')) {
+              rstr.push_back('$');
+              ++it;
+              continue;
+          }
+
+          // 'it' can be equal to str.end() here, but it is handled
+          // correctly.
+          if (*it != '{') {
+              // There is no modifier (between {}), so just copy the
+              // passed number into the generated string.
+              rstr += boost::str(boost::format("%d") % num);
+          } else {
+              // There is a modifier (between {}). Parse it and handle
+              // the various cases below.
+              const char* scan_str =
+                  str.c_str() + std::distance(str.begin(), it);
+              int offset = 0;
+              unsigned int width;
+              char base[2] = {'d', 0}; // char plus null byte
+              const int n = sscanf(scan_str, "{%d,%u,%1[doxXnN]}",
+                                   &offset, &width, base);
+              switch (n) {
+              case 1:
+                  // Only 1 item was matched (the offset). Copy (num +
+                  // offset) into the generated string.
+                  rstr += boost::str(boost::format("%d") % (num + offset));
+                  break;
+
+              case 2: {
+                  // 2 items were matched (the offset and width). Copy
+                  // (num + offset) and format it according to the width
+                  // into the generated string.
+                  const std::string fmt =
+                      boost::str(boost::format("%%0%ud") % width);
+                  rstr += boost::str(boost::format(fmt) % (num + offset));
+                  break;
+              }
+
+              case 3:
+                  // 3 items were matched (offset, width and base).
+                  if ((base[0] == 'n') || (base[0] == 'N')) {
+                      // The base is requesting nibbles. Format it
+                      // specially (see genNibbles() documentation).
+                      rstr += genNibbles(num + offset, width, (base[0] == 'N'));
+                  } else {
+                      // The base is not requesting nibbles. Copy (num +
+                      // offset) and format it according to the width
+                      // and base into the generated string.
+                      const std::string fmt =
+                          boost::str(boost::format("%%0%u%c") % width % base[0]);
+                      rstr += boost::str(boost::format(fmt) % (num + offset));
+                  }
+                  break;
+
+              default:
+                  // Any other case in the modifiers is an error.
+                  reportError(lexer_.getSourceName(), lexer_.getSourceLine(),
+                              "Invalid $GENERATE format modifiers");
+                  return ("");
+              }
+
+              // Find the closing brace. Careful that 'it' can be equal
+              // to str.end() here.
+              while ((it != str.end()) && (*it != '}')) {
+                  ++it;
+              }
+              // Skip past the closing brace (if there is one).
+              if (it != str.end()) {
+                  ++it;
+              }
+          }
+          break;
+
+      case '\\':
+          // This is the case when the '\' character is encountered in
+          // the LHS or RHS. The '\' and the following character are
+          // copied as-is into the generated string. This is usually
+          // used for escaping the $ character.
+          rstr.push_back(*it);
+          ++it;
+          if (it == str.end()) {
+              continue;
+          }
+          rstr.push_back(*it);
+          ++it;
+          break;
+
+      default:
+          // This is the default case that handles all other
+          // characters. They are copied as-is into the generated
+          // string.
+          rstr.push_back(*it);
+          ++it;
+          break;
+      }
+  }
+
+  return (rstr);
+}
+
+void
+MasterLoader::MasterLoaderImpl::doGenerate() {
+    // Parse the range token
+    const MasterToken& range_token = lexer_.getNextToken(MasterToken::STRING);
+    if (range_token.getType() != MasterToken::STRING) {
+        reportError(lexer_.getSourceName(), lexer_.getSourceLine(),
+                    "Invalid $GENERATE syntax");
+        return;
+    }
+    const std::string range = range_token.getString();
+
+    // Parse the LHS token
+    const MasterToken& lhs_token = lexer_.getNextToken(MasterToken::STRING);
+    if (lhs_token.getType() != MasterToken::STRING) {
+        reportError(lexer_.getSourceName(), lexer_.getSourceLine(),
+                    "Invalid $GENERATE syntax");
+        return;
+    }
+    const std::string lhs = lhs_token.getString();
+
+    // Parse the TTL, RR class and RR type tokens. Note that TTL and RR
+    // class may come in any order, or may be missing (either or
+    // both). If TTL is missing, we expect that it was either specified
+    // explicitly using $TTL, or is implicitly known from a previous RR,
+    // or that this is the SOA RR from which the MINIMUM field is
+    // used. It's unlikely that $GENERATE will be used with an SOA RR,
+    // but it's possible. The parsing happens within the parseRRParams()
+    // helper method which is called below.
+    const MasterToken& param_token = lexer_.getNextToken(MasterToken::STRING);
+    if (param_token.getType() != MasterToken::STRING) {
+        reportError(lexer_.getSourceName(), lexer_.getSourceLine(),
+                    "Invalid $GENERATE syntax");
+        return;
+    }
+
+    bool explicit_ttl = false;
+    const RRType rrtype = parseRRParams(explicit_ttl, param_token);
+
+    // Parse the RHS token. It can be a quoted string.
+    const MasterToken& rhs_token = lexer_.getNextToken(MasterToken::QSTRING);
+    if ((rhs_token.getType() != MasterToken::QSTRING) &&
+        (rhs_token.getType() != MasterToken::STRING))
+    {
+        reportError(lexer_.getSourceName(), lexer_.getSourceLine(),
+                    "Invalid $GENERATE syntax");
+        return;
+    }
+    const std::string rhs = rhs_token.getString();
+
+    // Range can be one of two forms: start-stop or start-stop/step. If
+    // the first form is used, then step is set to 1. All of start, stop
+    // and step must be positive.
+    unsigned int start;
+    unsigned int stop;
+    unsigned int step;
+    const int n = sscanf(range.c_str(), "%u-%u/%u", &start, &stop, &step);
+    if ((n < 2) || (stop < start)) {
+        reportError(lexer_.getSourceName(), lexer_.getSourceLine(),
+                    "$GENERATE: invalid range: " + range);
+        return;
+    }
+
+    if (n == 2) {
+        step = 1;
+    }
+
+    // Generate and add the records.
+    for (int i = start; i <= stop; i += step) {
+        // Get generated strings for LHS and RHS. LHS goes to form the
+        // name, RHS goes to form the RDATA of the RR.
+        const std::string generated_name = generateForIter(lhs, i);
+        const std::string generated_rdata = generateForIter(rhs, i);
+        if (generated_name.empty() || generated_rdata.empty()) {
+            // The error should have been sent to the callbacks already
+            // by generateForIter().
+            reportError(lexer_.getSourceName(), lexer_.getSourceLine(),
+                        "$GENERATE error");
+            return;
+        }
+
+        // generateForIter() can return a string with a trailing '.' in
+        // case of a nibble representation. So we cannot use the
+        // relative Name constructor. We use concatenate() which is
+        // expensive, but keeps the generated LHS-based Name within the
+        // active origin.
+        last_name_.reset
+            (new Name(Name(generated_name).concatenate(active_origin_)));
+        previous_name_ = true;
+
+        const rdata::RdataPtr rdata =
+            rdata::createRdata(rrtype, zone_class_, generated_rdata);
+        // In case we get NULL, it means there was error creating the
+        // Rdata. The errors should have been reported by callbacks_
+        // already. We need to decide if we want to continue or not.
+        if (rdata) {
+            add_callback_(*last_name_, zone_class_, rrtype,
+                          getCurrentTTL(explicit_ttl, rrtype, rdata),
+                          rdata);
+            // Good, we added another one
+            ++rr_count_;
+        } else {
+            seen_error_ = true;
+            if (!many_errors_) {
+                ok_ = false;
+                complete_ = true;
+                // We don't have the exact error here, but it was
+                // reported by the error callback.
+                isc_throw(MasterLoaderError, "Invalid RR data");
+            }
+        }
+    }
+}
+
 MasterToken
 MasterLoader::MasterLoaderImpl::handleInitialToken() {
     const MasterToken& initial_token =

+ 479 - 2
src/lib/dns/tests/master_loader_unittest.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014  Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -123,7 +123,9 @@ public:
         EXPECT_EQ(rrttl, current->getTTL());
         ASSERT_EQ(1, current->getRdataCount());
         EXPECT_EQ(0, isc::dns::rdata::createRdata(type, RRClass::IN(), data)->
-                  compare(current->getRdataIterator()->getCurrent()));
+                  compare(current->getRdataIterator()->getCurrent()))
+            << data << " vs. "
+            << current->getRdataIterator()->getCurrent().toText();
     }
 
     void checkBasicRRs() {
@@ -307,6 +309,481 @@ TEST_F(MasterLoaderTest, origin) {
     }
 }
 
+TEST_F(MasterLoaderTest, generate) {
+    // Various forms of the directive
+    const char* generates[] = {
+        "$generate",
+        "$GENERATE",
+        "$Generate",
+        "$GeneratE",
+        "\"$GENERATE\"",
+        NULL
+    };
+    for (const char** generate = generates; *generate != NULL; ++generate) {
+        SCOPED_TRACE(*generate);
+
+        clear();
+        const string directive = *generate;
+        const string input =
+          "$ORIGIN example.org.\n"
+          "before.example.org. 3600 IN A 192.0.2.0\n" +
+          directive + " 3-5 host$ A 192.0.2.$\n" +
+          "after.example.org. 3600 IN A 192.0.2.255\n";
+        stringstream ss(input);
+        setLoader(ss, Name("example.org."), RRClass::IN(),
+                  MasterLoader::MANY_ERRORS);
+
+        loader_->load();
+        EXPECT_TRUE(loader_->loadedSucessfully());
+        EXPECT_TRUE(errors_.empty());
+
+        // The "before" and "after" scaffolding below checks that no
+        // extra records are added by $GENERATE outside the requested
+        // range.
+        checkRR("before.example.org", RRType::A(), "192.0.2.0");
+        checkRR("host3.example.org", RRType::A(), "192.0.2.3");
+        checkRR("host4.example.org", RRType::A(), "192.0.2.4");
+        checkRR("host5.example.org", RRType::A(), "192.0.2.5");
+        checkRR("after.example.org", RRType::A(), "192.0.2.255");
+    }
+}
+
+TEST_F(MasterLoaderTest, generateRelativeLHS) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 1-2 @ 3600 NS ns$.example.org.\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("example.org", RRType::NS(), "ns1.example.org.");
+    checkRR("example.org", RRType::NS(), "ns2.example.org.");
+}
+
+TEST_F(MasterLoaderTest, generateInFront) {
+    // $ is in the front
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 9-10 $host 3600 TXT \"$ pomegranate\"\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("9host.example.org", RRType::TXT(), "9 pomegranate");
+    checkRR("10host.example.org", RRType::TXT(), "10 pomegranate");
+}
+
+TEST_F(MasterLoaderTest, generateInMiddle) {
+    // $ is in the middle
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 9-10 num$-host 3600 TXT \"This is $ pomegranate\"\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("num9-host.example.org", RRType::TXT(), "This is 9 pomegranate");
+    checkRR("num10-host.example.org", RRType::TXT(), "This is 10 pomegranate");
+}
+
+TEST_F(MasterLoaderTest, generateAtEnd) {
+    // $ is at the end
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 9-10 num$-host 3600 TXT Pomegranate$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("num9-host.example.org", RRType::TXT(), "Pomegranate9");
+    checkRR("num10-host.example.org", RRType::TXT(), "Pomegranate10");
+}
+
+TEST_F(MasterLoaderTest, generateStripsQuotes) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 1-2 @ 3600 MX \"$ mx$.example.org.\"\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("example.org", RRType::MX(), "1 mx1.example.org.");
+    checkRR("example.org", RRType::MX(), "2 mx2.example.org.");
+}
+
+TEST_F(MasterLoaderTest, generateWithDoublePlaceholder) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 9-10 host$ 3600 TXT \"This is $$ pomegranate\"\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("host9.example.org", RRType::TXT(), "This is $ pomegranate");
+    checkRR("host10.example.org", RRType::TXT(), "This is $ pomegranate");
+}
+
+TEST_F(MasterLoaderTest, generateWithEscape) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 9-10 host$ 3600 TXT \"This is \\$\\pomegranate\"\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("host9.example.org", RRType::TXT(), "This is \\$\\pomegranate");
+    checkRR("host10.example.org", RRType::TXT(), "This is \\$\\pomegranate");
+}
+
+TEST_F(MasterLoaderTest, generateWithParams) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$TTL 3600\n"
+        "$GENERATE 2-3 host$ A 192.0.2.$\n"
+        "$GENERATE 5-6 host$ 3600 A 192.0.2.$\n"
+        "$GENERATE 8-9 host$ IN A 192.0.2.$\n"
+        "$GENERATE 11-12 host$ IN 3600 A 192.0.2.$\n"
+        "$GENERATE 14-15 host$ 3600 IN A 192.0.2.$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("host2.example.org", RRType::A(), "192.0.2.2");
+    checkRR("host3.example.org", RRType::A(), "192.0.2.3");
+
+    checkRR("host5.example.org", RRType::A(), "192.0.2.5");
+    checkRR("host6.example.org", RRType::A(), "192.0.2.6");
+
+    checkRR("host8.example.org", RRType::A(), "192.0.2.8");
+    checkRR("host9.example.org", RRType::A(), "192.0.2.9");
+
+    checkRR("host11.example.org", RRType::A(), "192.0.2.11");
+    checkRR("host12.example.org", RRType::A(), "192.0.2.12");
+
+    checkRR("host14.example.org", RRType::A(), "192.0.2.14");
+    checkRR("host15.example.org", RRType::A(), "192.0.2.15");
+}
+
+TEST_F(MasterLoaderTest, generateWithStep) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 2-9/2 host$ 3600 A 192.0.2.$\n"
+        "$GENERATE 12-21/3 host$ 3600 A 192.0.2.$\n"
+        "$GENERATE 30-31/1 host$ 3600 A 192.0.2.$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("host2.example.org", RRType::A(), "192.0.2.2");
+    checkRR("host4.example.org", RRType::A(), "192.0.2.4");
+    checkRR("host6.example.org", RRType::A(), "192.0.2.6");
+    checkRR("host8.example.org", RRType::A(), "192.0.2.8");
+
+    checkRR("host12.example.org", RRType::A(), "192.0.2.12");
+    checkRR("host15.example.org", RRType::A(), "192.0.2.15");
+    checkRR("host18.example.org", RRType::A(), "192.0.2.18");
+    checkRR("host21.example.org", RRType::A(), "192.0.2.21");
+
+    checkRR("host30.example.org", RRType::A(), "192.0.2.30");
+    checkRR("host31.example.org", RRType::A(), "192.0.2.31");
+}
+
+TEST_F(MasterLoaderTest, generateWithModifiers) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$TTL 3600\n"
+
+        // Use a positive delta of 1 in the LHS and a negative delta of
+        // -1 in the RHS
+        "$GENERATE 2-9/2 host${1} A 192.0.2.${-1}\n"
+
+        "$GENERATE 10-12 host${0,4} A 192.0.2.$\n"
+        "$GENERATE 14-15 host${0,4,d} A 192.0.2.$\n"
+
+        // Names are case-insensitive, so we use TXT's RDATA to check
+        // case with hex representation.
+        "$GENERATE 30-31 host$ TXT \"Value ${0,4,x}\"\n"
+        "$GENERATE 42-43 host$ TXT \"Value ${0,4,X}\"\n"
+
+        // Octal does not use any alphabets
+        "$GENERATE 45-46 host${0,4,o} A 192.0.2.$\n"
+
+        // Here, the LHS has a trailing dot (which would result in an
+        // out-of-zone name), but that should be handled as a relative
+        // name.
+        "$GENERATE 90-92 ${0,8,n} A 192.0.2.$\n"
+
+        // Here, the LHS has no trailing dot, and results in the same
+        // number of labels as width=8 above.
+        "$GENERATE 94-96 ${0,7,n} A 192.0.2.$\n"
+
+        // Names are case-insensitive, so we use TXT's RDATA to check
+        // case with nibble representation.
+        "$GENERATE 106-107 host$ TXT \"Value ${0,9,n}\"\n"
+        "$GENERATE 109-110 host$ TXT \"Value ${0,9,N}\"\n"
+
+        // Junk type will not parse and 'd' is assumed. No error is
+        // generated (this is to match BIND 9 behavior).
+        "$GENERATE 200-201 host${0,4,j} A 192.0.2.$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty());
+
+    checkRR("host3.example.org", RRType::A(), "192.0.2.1");
+    checkRR("host5.example.org", RRType::A(), "192.0.2.3");
+    checkRR("host7.example.org", RRType::A(), "192.0.2.5");
+    checkRR("host9.example.org", RRType::A(), "192.0.2.7");
+
+    checkRR("host0010.example.org", RRType::A(), "192.0.2.10");
+    checkRR("host0011.example.org", RRType::A(), "192.0.2.11");
+    checkRR("host0012.example.org", RRType::A(), "192.0.2.12");
+
+    checkRR("host0014.example.org", RRType::A(), "192.0.2.14");
+    checkRR("host0015.example.org", RRType::A(), "192.0.2.15");
+
+    checkRR("host30.example.org", RRType::TXT(), "Value 001e");
+    checkRR("host31.example.org", RRType::TXT(), "Value 001f");
+
+    checkRR("host42.example.org", RRType::TXT(), "Value 002A");
+    checkRR("host43.example.org", RRType::TXT(), "Value 002B");
+
+    checkRR("host0055.example.org", RRType::A(), "192.0.2.45");
+    checkRR("host0056.example.org", RRType::A(), "192.0.2.46");
+
+    checkRR("a.5.0.0.example.org", RRType::A(), "192.0.2.90");
+    checkRR("b.5.0.0.example.org", RRType::A(), "192.0.2.91");
+    checkRR("c.5.0.0.example.org", RRType::A(), "192.0.2.92");
+
+    checkRR("e.5.0.0.example.org", RRType::A(), "192.0.2.94");
+    checkRR("f.5.0.0.example.org", RRType::A(), "192.0.2.95");
+    checkRR("0.6.0.0.example.org", RRType::A(), "192.0.2.96");
+
+    checkRR("host106.example.org", RRType::TXT(), "Value a.6.0.0.0");
+    checkRR("host107.example.org", RRType::TXT(), "Value b.6.0.0.0");
+    checkRR("host109.example.org", RRType::TXT(), "Value D.6.0.0.0");
+    checkRR("host110.example.org", RRType::TXT(), "Value E.6.0.0.0");
+
+    checkRR("host0200.example.org", RRType::A(), "192.0.2.200");
+    checkRR("host0201.example.org", RRType::A(), "192.0.2.201");
+}
+
+TEST_F(MasterLoaderTest, generateWithNoModifiers) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$TTL 3600\n"
+        "$GENERATE 10-12 host${} A 192.0.2.$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    ASSERT_EQ(2, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "Invalid $GENERATE format modifiers", 3);
+    checkCallbackMessage(errors_.at(1),
+                         "$GENERATE error", 3);
+}
+
+TEST_F(MasterLoaderTest, generateWithBadModifiers) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$TTL 3600\n"
+        "$GENERATE 10-12 host${GARBAGE} A 192.0.2.$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    ASSERT_EQ(2, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "Invalid $GENERATE format modifiers", 3);
+    checkCallbackMessage(errors_.at(1),
+                         "$GENERATE error", 3);
+}
+
+TEST_F(MasterLoaderTest, generateMissingRange) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    EXPECT_EQ(1, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "unexpected end of input", 2);
+}
+
+TEST_F(MasterLoaderTest, generateMissingLHS) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 2-4\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    EXPECT_EQ(1, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "unexpected end of input", 2);
+}
+
+TEST_F(MasterLoaderTest, generateMissingType) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 2-4 host$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    EXPECT_EQ(1, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "unexpected end of input", 2);
+}
+
+TEST_F(MasterLoaderTest, generateMissingRHS) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 2-4 host$ A\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    EXPECT_EQ(1, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "unexpected end of input", 2);
+}
+
+TEST_F(MasterLoaderTest, generateWithBadRangeSyntax) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE ABCD host$ 3600 A 192.0.2.$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    EXPECT_EQ(1, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "$GENERATE: invalid range: ABCD", 2);
+}
+
+TEST_F(MasterLoaderTest, generateWithInvalidRange) {
+    // start > stop
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 2-1 host$ 3600 A 192.0.2.$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    EXPECT_EQ(1, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "$GENERATE: invalid range: 2-1", 2);
+}
+
+TEST_F(MasterLoaderTest, generateWithInvalidClass) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 1-2 host$ 3600 CH A 192.0.2.$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    EXPECT_EQ(1, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "Class mismatch: CH vs. IN", 2);
+}
+
+TEST_F(MasterLoaderTest, generateWithNoAvailableTTL) {
+    const string input =
+        "$ORIGIN example.org.\n"
+        "$GENERATE 1-2 host$ A 192.0.2.$\n";
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    EXPECT_EQ(1, errors_.size()); // For the broken GENERATE
+    EXPECT_TRUE(warnings_.empty());
+
+    checkCallbackMessage(errors_.at(0),
+                         "no TTL specified; load rejected", 2);
+}
+
 // Test the source is correctly popped even after error
 TEST_F(MasterLoaderTest, popAfterError) {
     const string include_str = "$include " TEST_DATA_SRCDIR

+ 1 - 0
tests/lettuce/configurations/.gitignore

@@ -1,4 +1,5 @@
 /bindctl_commands.config
 /example.org.config
+/generate.config
 /root.config
 /static.config

+ 35 - 0
tests/lettuce/configurations/generate.config.orig

@@ -0,0 +1,35 @@
+{
+    "version": 3,
+    "Logging": {
+        "loggers": [{
+            "severity": "DEBUG",
+            "name": "*",
+            "debuglevel": 99
+        }]
+    },
+    "Auth": {
+        "listen_on": [{
+            "port": 56176,
+            "address": "127.0.0.1"
+        }]
+    },
+    "data_sources": {
+        "classes": {
+            "IN": [
+                {
+                    "type": "MasterFiles",
+                    "cache-enable": true,
+                    "params": {
+                        "example.org": "data/generate.zone"
+                    }
+                }
+            ]
+        }
+    },
+    "Init": {
+        "components": {
+            "b10-auth": { "kind": "needed", "special": "auth" },
+            "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
+        }
+    }
+}

+ 4 - 0
tests/lettuce/data/generate.zone

@@ -0,0 +1,4 @@
+$ORIGIN example.org.
+example.org.    3600    IN      SOA     ns1.example.org. admin.example.org. 12341 3600 1800 2419200 7200
+$GENERATE 1-2   @               NS      ns$.example.org.
+$GENERATE 1-4 host$ A 192.0.2.$

+ 50 - 0
tests/lettuce/features/master_loader.feature

@@ -0,0 +1,50 @@
+Feature: Master loader feature
+    This feature is a collection of tests for the zone file loader in
+    BIND 10.
+
+    Scenario: $GENERATE support
+        Given I have bind10 running with configuration generate.config
+        And wait for bind10 stderr message BIND10_STARTED_CC
+        And wait for bind10 stderr message CMDCTL_STARTED
+        And wait for bind10 stderr message AUTH_SERVER_STARTED
+
+        bind10 module Auth should be running
+        And bind10 module Resolver should not be running
+        And bind10 module Xfrout should not be running
+        And bind10 module Zonemgr should not be running
+        And bind10 module Xfrin should not be running
+        And bind10 module Stats should not be running
+        And bind10 module StatsHttpd should not be running
+
+        A query for www.example.org should have rcode NXDOMAIN
+        The SOA serial for example.org should be 12341
+
+        A query for host0.example.org should have rcode NXDOMAIN
+        A query for host1.example.org should have rcode NOERROR
+        The answer section of the last query response should be
+        """
+        host1.example.org.        3600    IN      A       192.0.2.1
+        """
+        A query for host2.example.org should have rcode NOERROR
+        The answer section of the last query response should be
+        """
+        host2.example.org.        3600    IN      A       192.0.2.2
+        """
+        A query for host3.example.org should have rcode NOERROR
+        The answer section of the last query response should be
+        """
+        host3.example.org.        3600    IN      A       192.0.2.3
+        """
+        A query for host4.example.org should have rcode NOERROR
+        The answer section of the last query response should be
+        """
+        host4.example.org.        3600    IN      A       192.0.2.4
+        """
+        A query for host5.example.org should have rcode NXDOMAIN
+
+        A query for example.org type NS should have rcode NOERROR
+        The answer section of the last query response should be
+        """
+        example.org.              3600    IN      NS      ns1.example.org.
+        example.org.              3600    IN      NS      ns2.example.org.
+        """

+ 3 - 2
tests/lettuce/features/terrain/terrain.py

@@ -1,5 +1,4 @@
-
-# Copyright (C) 2011  Internet Systems Consortium.
+# Copyright (C) 2011-2014  Internet Systems Consortium.
 #
 # Permission to use, copy, modify, and distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -52,6 +51,8 @@ copylist = [
      "configurations/bindctl_commands.config"],
     ["configurations/example.org.config.orig",
      "configurations/example.org.config"],
+    ["configurations/generate.config.orig",
+     "configurations/generate.config"],
     ["configurations/bindctl/bindctl.config.orig",
      "configurations/bindctl/bindctl.config"],
     ["configurations/auth/auth_basic.config.orig",