CLEANFILES = *.gcno *.gcda db_messages.h db_messages.cc s-messages
lib_LTLIBRARIES = libkea-database.la
-libkea_database_la_SOURCES = backend_selector.cc backend_selector.h
+libkea_database_la_SOURCES = audit_entry.cc audit_entry.h
+libkea_database_la_SOURCES += backend_selector.cc backend_selector.h
libkea_database_la_SOURCES += database_connection.cc database_connection.h
libkea_database_la_SOURCES += dbaccess_parser.h dbaccess_parser.cc
libkea_database_la_SOURCES += db_exceptions.h
# Specify the headers for copying into the installation directory tree.
libkea_database_includedir = $(pkgincludedir)/database
libkea_database_include_HEADERS = \
+ audit_entry.h \
backend_selector.h \
database_connection.h \
dbaccess_parser.h \
--- /dev/null
+// Copyright (C) 2019 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
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <database/audit_entry.h>
+#include <exceptions/exceptions.h>
+
+namespace isc {
+namespace db {
+
+AuditEntry::AuditEntry(const std::string& object_type,
+ const uint64_t object_id,
+ const ModificationType& modification_type,
+ const boost::posix_time::ptime& modification_time,
+ const std::string& log_message)
+ : object_type_(object_type),
+ object_id_(object_id),
+ modification_type_(modification_type),
+ modification_time_(modification_time),
+ log_message_(log_message) {
+ // Check if the provided values are sane.
+ validate();
+}
+
+AuditEntry::AuditEntry(const std::string& object_type,
+ const uint64_t object_id,
+ const ModificationType& modification_type,
+ const std::string& log_message)
+ : object_type_(object_type),
+ object_id_(object_id),
+ modification_type_(modification_type),
+ modification_time_(boost::posix_time::microsec_clock::universal_time()),
+ log_message_(log_message) {
+ // Check if the provided values are sane.
+ validate();
+}
+
+void
+AuditEntry::validate() const {
+ // object type can't be empty
+ if (object_type_.empty()) {
+ isc_throw(BadValue, "object type can't be empty in the database "
+ "audit entry");
+
+ // modification time must be a valid date time value
+ } else if (modification_time_.is_not_a_date_time()) {
+ isc_throw(BadValue, "modification time value is not a valid time "
+ "object in the database audit entry");
+ }
+}
+
+} // end of namespace isc::db
+} // end of namespace isc
--- /dev/null
+// Copyright (C) 2019 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
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef AUDIT_ENTRY_H
+#define AUDIT_ENTRY_H
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/shared_ptr.hpp>
+#include <cstdint>
+#include <string>
+
+namespace isc {
+namespace db {
+
+/// @brief Represents a single entry in the audit table.
+///
+/// The audit tables are used in the databases to track incremental
+/// changes, e.g. configuration changes applied via the configuration
+/// backend. The backend can query for the entries in the audit table
+/// to identify the SQL table in which the records were modified, the
+/// identifiers of the modified objects, type of the modifications,
+/// time of the modifications and optional log messages associated
+/// with the modifications.
+///
+/// The server should remember the most recent modification time out of
+/// all audit entries it has fetched. During the next attempt to fetch
+/// the most recent modifications in the database it will query for all
+/// entries with later modification time than stored. That way the
+/// server queries only for the audit entries it hasn't fetched yet.
+///
+/// When the modification type of the entry is set to
+/// @c AuditEntry::ModificationType::DELETE, the corresponding
+/// configuration entry is already gone from the database. For example:
+/// when a subnet with ID of 123 is deleted from the dhcp4_subnet
+/// table, the audit entry similar to this will be stored in the audit
+/// table:
+///
+/// - object_type: "dhcp4_subnet"
+/// - object_id: 123
+/// - modification_type: 3 (DELETE)
+/// - modification_time: "2019-01-15 15:45:23"
+/// - log_message: "DHCPv4 subnet 123 deleted"
+///
+/// The subnet is instantly removed from the dhcp4_subnet table. When
+/// the server finds such entry in the audit table, it removes the
+/// subnet 123 from its (in-memory) configuration. There is no need
+/// make additional queries to fetch updated data from the dhcp4_subnet
+/// table, unless there are also audit entries indicating that some
+/// new subnets have been added or some subnets have been updated.
+class AuditEntry {
+public:
+
+ /// @brief Types of the modifications.
+ ///
+ /// The numbers representing those modification types correspond
+ /// to the values representing them in the database.
+ enum class ModificationType : uint8_t {
+ CREATE = 0,
+ UPDATE = 1,
+ DELETE = 2
+ };
+
+ /// @brief Constructor using explicit modification time.
+ ///
+ /// @param object_type name of the table where data was modified.
+ /// @param object_id identifier of the modified record in this table.
+ /// @param modification_type type of the modification, e.g. DELETE.
+ /// @param modification_time time of modification for that record.
+ /// @param log_message optional log message associated with the
+ /// modification.
+ AuditEntry(const std::string& object_type,
+ const uint64_t object_id,
+ const ModificationType& modification_type,
+ const boost::posix_time::ptime& modification_time,
+ const std::string& log_message);
+
+ /// @brief Constructor using default modification time.
+ ///
+ /// @param object_type name of the table where data was modified.
+ /// @param object_id identifier of the modified record in this table.
+ /// @param modification_type type of the modification, e.g. DELETE.
+ /// @param log_message optional log message associated with the
+ /// modification.
+ AuditEntry(const std::string& object_type,
+ const uint64_t object_id,
+ const ModificationType& modification_type,
+ const std::string& log_message);
+
+ /// @brief Returns object type.
+ ///
+ /// @return Name of the table in which the modification is present.
+ std::string getObjectType() const {
+ return (object_type_);
+ }
+
+ /// @brief Returns object id.
+ ///
+ /// @return Identifier of the added, updated or deleted object.
+ uint64_t getObjectId() const {
+ return (object_id_);
+ }
+
+ /// @brief Returns modification type.
+ ///
+ /// @return Type of the modification, e.g. DELETE.
+ ModificationType getModificationType() const {
+ return (modification_type_);
+ }
+
+ /// @brief Returns modification time.
+ ///
+ /// @return Modification time of the correponding record.
+ boost::posix_time::ptime getModificationTime() const {
+ return (modification_time_);
+ }
+
+ /// @brief Returns log message.
+ ///
+ /// @return Optional log message corresponding to the changes.
+ std::string getLogMessage() const {
+ return (log_message_);
+ }
+
+private:
+
+ /// @brief Validates the values specified for the audit entry.
+ ///
+ /// @throw BadValue if the object type is empty or if the
+ /// modification time is not a date time value.
+ void validate() const;
+
+ /// @brief Object type.
+ std::string object_type_;
+
+ /// @brief Object id.
+ uint64_t object_id_;
+
+ /// @brief Modification type.
+ ModificationType modification_type_;
+
+ /// @brief Modification time.
+ boost::posix_time::ptime modification_time_;
+
+ /// @brief Log message.
+ std::string log_message_;
+};
+
+/// @brief Pointer to the @c AuditEntry object.
+typedef boost::shared_ptr<AuditEntry> AuditEntryPtr;
+
+} // end of namespace isc::db
+} // end of namespace isc
+
+#endif
if HAVE_GTEST
TESTS += libdatabase_unittests
-libdatabase_unittests_SOURCES = backend_selector_unittest.cc
+libdatabase_unittests_SOURCES = audit_entry_unittest.cc
+libdatabase_unittests_SOURCES += backend_selector_unittest.cc
libdatabase_unittests_SOURCES += database_connection_unittest.cc
libdatabase_unittests_SOURCES += dbaccess_parser_unittest.cc
libdatabase_unittests_SOURCES += run_unittests.cc
--- /dev/null
+// Copyright (C) 2019 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
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+#include <config.h>
+#include <database/audit_entry.h>
+#include <exceptions/exceptions.h>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/make_shared.hpp>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::db;
+
+namespace {
+
+/// @brief Test fixture class for testing @c AuditEntry.
+class AuditEntryTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor.
+ AuditEntryTest()
+ : fixed_time_(now()) {
+ }
+
+ /// @brief Returns current time.
+ static boost::posix_time::ptime now() {
+ return (boost::posix_time::microsec_clock::universal_time());
+ }
+
+ /// @brief Returns always the same time value.
+ ///
+ /// The value is initialized when the test it started.
+ boost::posix_time::ptime fixedTime() const {
+ return (fixed_time_);
+ }
+
+ /// @brief Checks if the given time value is "close" to the
+ /// current time.
+ ///
+ /// This is useful in tests when the @c AuditEntry class sets the
+ /// modification time to a default value (in its constructor).
+ /// Because the test doesn't know the exact value to which the
+ /// modification time is set, it merely checks that this value
+ /// is earlier than current time and within the range of 1s.
+ ///
+ /// @param t time value to be checked.
+ bool almostEqualTime(const boost::posix_time::ptime& t) const {
+ auto current = now();
+
+ // The provided value must be a valid time.
+ if (t.is_not_a_date_time()) {
+ ADD_FAILURE() << "provided value is not a date time";
+ return (false);
+ }
+
+ // It must be earlier than current time.
+ if (t > current) {
+ ADD_FAILURE() << "provided time value is later than current time";
+ return (false);
+ }
+
+ // The difference must be lower than 1 second.
+ boost::posix_time::time_duration dur = current - t;
+ return (dur.total_milliseconds() < 1000);
+ }
+
+ /// @brief Fixed time value initialized in the constructor.
+ ///
+ /// This is used in tests that require the exact time values.
+ boost::posix_time::ptime fixed_time_;
+};
+
+// Checks that the modification time value can be cast to a number.
+TEST_F(AuditEntryTest, modificationType) {
+ EXPECT_EQ(0, static_cast<int>(AuditEntry::ModificationType::CREATE));
+ EXPECT_EQ(1, static_cast<int>(AuditEntry::ModificationType::UPDATE));
+ EXPECT_EQ(2, static_cast<int>(AuditEntry::ModificationType::DELETE));
+}
+
+// Checks that the audit entry can be created.
+TEST_F(AuditEntryTest, create) {
+
+ AuditEntryPtr audit_entry;
+
+ {
+ SCOPED_TRACE("create with modification time");
+
+ ASSERT_NO_THROW(audit_entry = boost::make_shared<AuditEntry>
+ ("dhcp4_subnet", 10, AuditEntry::ModificationType::DELETE,
+ fixedTime(), "deleted subnet 10"));
+ EXPECT_EQ("dhcp4_subnet", audit_entry->getObjectType());
+ EXPECT_EQ(10, audit_entry->getObjectId());
+ EXPECT_EQ(AuditEntry::ModificationType::DELETE, audit_entry->getModificationType());
+ EXPECT_EQ(fixedTime(), audit_entry->getModificationTime());
+ EXPECT_EQ("deleted subnet 10", audit_entry->getLogMessage());
+ }
+
+ {
+ SCOPED_TRACE("create with default modification time");
+
+ ASSERT_NO_THROW(audit_entry = boost::make_shared<AuditEntry>
+ ("dhcp4_option", 123, AuditEntry::ModificationType::CREATE,
+ ""));
+ EXPECT_EQ("dhcp4_option", audit_entry->getObjectType());
+ EXPECT_EQ(123, audit_entry->getObjectId());
+ EXPECT_EQ(AuditEntry::ModificationType::CREATE, audit_entry->getModificationType());
+ EXPECT_TRUE(almostEqualTime(audit_entry->getModificationTime()));
+ EXPECT_TRUE(audit_entry->getLogMessage().empty());
+ }
+}
+
+// Checks that invalid values for the audit entry are rejected.
+TEST_F(AuditEntryTest, createFailures) {
+ {
+ SCOPED_TRACE("empty object type");
+ EXPECT_THROW(AuditEntry("", 10, AuditEntry::ModificationType::DELETE,
+ fixedTime(), "deleted subnet 10"),
+ BadValue);
+ }
+
+ {
+ SCOPED_TRACE("not a date time");
+ EXPECT_THROW(AuditEntry("dhcp4_subnet", 10,
+ AuditEntry::ModificationType::DELETE,
+ boost::posix_time::ptime(), "deleted subnet 10"),
+ BadValue);
+ }
+}
+
+}