]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#2322] Adds client classes to Postgresql CB v4
authorThomas Markwalder <tmark@isc.org>
Mon, 21 Feb 2022 19:03:29 +0000 (14:03 -0500)
committerThomas Markwalder <tmark@isc.org>
Thu, 24 Feb 2022 15:57:54 +0000 (10:57 -0500)
configure.ac
    added src/share/database/scripts/pgsql/upgrade_009_to_010.sh

src/bin/admin/tests/pgsql_tests.sh.in
    pgsql_upgrade_8_0_to_9_0()
    pgsql_upgrade_9_0_to_10_0()
    - added/improved upgrade tests

src/hooks/dhcp/pgsql_cb/pgsql_cb_dhcp4.cc
    Implemented client class functions

src/hooks/dhcp/pgsql_cb/pgsql_query_macros_dhcp.h
    Modified client class related queries

src/hooks/dhcp/pgsql_cb/tests/pgsql_cb_dhcp4_unittest.cc
    TEST_F(PgSqlConfigBackendDHCPv4Test, setAndGetAllClientClasses4Test)
    TEST_F(PgSqlConfigBackendDHCPv4Test, getClientClass4Test)
    TEST_F(PgSqlConfigBackendDHCPv4Test, createUpdateClientClass4OptionsTest)
    TEST_F(PgSqlConfigBackendDHCPv4Test, getModifiedClientClasses4Test)
    TEST_F(PgSqlConfigBackendDHCPv4Test, deleteClientClass4Test)
    TEST_F(PgSqlConfigBackendDHCPv4Test, deleteAllClientClasses4Test)
    TEST_F(PgSqlConfigBackendDHCPv4Test, clientClassDependencies4Test)
    TEST_F(PgSqlConfigBackendDHCPv4Test, multipleAuditEntriesTest)
    - new tests

src/lib/pgsql/pgsql_connection.h
    Updated schema version to 10.0

src/share/database/scripts/pgsql/Makefile.am
    Added upgrade_009_to_010.sh

src/share/database/scripts/pgsql/dhcpdb_create.pgsql
    Schema updated to 10.0:
    Replace setClientClass<4/6>Order()
    - wrong local variable type, replace int constants with
    boolean constants

    dhcp<4/6>_client_class_dependency tables -
    - primary key needs to be composite to allow muliptle rows per class id

    func_dhcp<4/6>_client_class_check_dependency_BINS() - trigger functions
    need to return NEW not NULL

configure.ac
src/bin/admin/tests/pgsql_tests.sh.in
src/hooks/dhcp/pgsql_cb/pgsql_cb_dhcp4.cc
src/hooks/dhcp/pgsql_cb/pgsql_query_macros_dhcp.h
src/hooks/dhcp/pgsql_cb/tests/pgsql_cb_dhcp4_unittest.cc
src/lib/pgsql/pgsql_connection.h
src/share/database/scripts/pgsql/Makefile.am
src/share/database/scripts/pgsql/dhcpdb_create.pgsql
src/share/database/scripts/pgsql/upgrade_009_to_010.sh.in [new file with mode: 0644]

index 7abf4a910e1f25363339957b5f6f2277b7fb8f58..2cc5484cf20506d1741de815adecc43b15a92d70 100644 (file)
@@ -1844,6 +1844,8 @@ AC_CONFIG_FILES([src/share/database/scripts/pgsql/upgrade_007_to_008.sh],
                 [chmod +x src/share/database/scripts/pgsql/upgrade_007_to_008.sh])
 AC_CONFIG_FILES([src/share/database/scripts/pgsql/upgrade_008_to_009.sh],
                 [chmod +x src/share/database/scripts/pgsql/upgrade_008_to_009.sh])
+AC_CONFIG_FILES([src/share/database/scripts/pgsql/upgrade_009_to_010.sh],
+                [chmod +x src/share/database/scripts/pgsql/upgrade_009_to_010.sh])
 AC_CONFIG_FILES([src/share/database/scripts/pgsql/wipe_data.sh],
                 [chmod +x src/share/database/scripts/pgsql/wipe_data.sh])
 AC_CONFIG_FILES([src/share/yang/Makefile])
index 54341c26fc485b7c10a442acb46f798abb5f7647..04a896b66797b34a5906bd38419841f7ef03a6ec 100644 (file)
@@ -143,7 +143,7 @@ pgsql_db_version_test() {
     run_command \
         "${kea_admin}" db-version pgsql -u "${db_user}" -p "${db_password}" -n "${db_name}"
     version="${OUTPUT}"
-    assert_str_eq "9.0" "${version}" "Expected kea-admin to return %s, returned value was %s"
+    assert_str_eq "10.0" "${version}" "Expected kea-admin to return %s, returned value was %s"
 
     # Let's wipe the whole database
     pgsql_wipe
@@ -421,14 +421,28 @@ pgsql_upgrade_8_0_to_9_0() {
     run_command \
         pgsql_execute "$session_sql"
 
-    # The changes are not readily testable without querying the information schema,
-    # not sure the effort is worthwhile. For now we'll just check the version.
+    # Most changes are not readily testable without querying the information schema,
+    # not sure the effort is worthwhile.  Verify that function gmt_epoch() was created.
+    run_command \
+        pgsql_execute "select gmt_epoch(now());"
 
-    # Verify that kea-admin db-version returns the correct version
+    assert_eq 0 "${EXIT_CODE}" "function gmt_epoch() broken or missing. (expected status code %d, returned %d)"
+
+}
+
+pgsql_upgrade_9_0_to_10_0() {
     run_command \
-        "${kea_admin}" db-version pgsql -u "${db_user}" -p "${db_password}" -n "${db_name}"
-    version="${OUTPUT}"
-    assert_str_eq "9.0" "${version}" "Expected kea-admin to return %s, returned value was %s"
+        pgsql_execute "$session_sql"
+
+    # Get function source code so we can check that it returns NEW.
+    # Function name must be lower case for WHERE clause.
+    run_command \
+         pgsql_execute "select proname,prosrc from pg_proc where proname='func_dhcp6_client_class_check_dependency_bins'"
+
+    assert_eq 0 "${EXIT_CODE}" '$cmd, expected exit code %d, actual %d'
+
+    count=$(echo "${OUTPUT}" | grep -Eci 'RETURN NEW') || true
+    assert_eq 1 "${count}" "func_dhcp6_client_class_check_dependency_BINS is missing RETURN NEW. (expected count %d, returned %d)"
 }
 
 pgsql_upgrade_test() {
@@ -446,9 +460,9 @@ pgsql_upgrade_test() {
         "${kea_admin}" db-upgrade pgsql -u "${db_user}" -p "${db_password}" -n "${db_name}" -d "${db_scripts_dir}"
     assert_eq 0 "${EXIT_CODE}" "db-upgrade failed, expected exit code: %d, actual: %d"
 
-    # Verify upgraded schema reports version 9.0.
+    # Verify upgraded schema reports version 10.0.
     version=$("${kea_admin}" db-version pgsql -u "${db_user}" -p "${db_password}" -n "${db_name}" -d "${db_scripts_dir}")
-    assert_str_eq "9.0" "${version}" 'Expected kea-admin to return %s, returned value was %s'
+    assert_str_eq "10.0" "${version}" 'Expected kea-admin to return %s, returned value was %s'
 
     # Check 1.0 to 2.0 upgrade
     pgsql_upgrade_1_0_to_2_0
@@ -471,6 +485,9 @@ pgsql_upgrade_test() {
     # Check 8.0 to 9.0 upgrade
     pgsql_upgrade_8_0_to_9_0
 
+    # Check 9.0 to 10.0 upgrade
+    pgsql_upgrade_9_0_to_10_0
+
     # Let's wipe the whole database
     pgsql_wipe
 
index c86c6753620e010688749ae4e164355d2df469d3..70bb30a1eef2f870832c1875a37417adab2ef389 100644 (file)
@@ -1902,10 +1902,54 @@ public:
     /// @param selector Server selector.
     /// @param client_class Pointer to the client_class the option belongs to.
     /// @param option Pointer to the option descriptor encapsulating the option..
-    void createUpdateOption4(const ServerSelector& /* server_selector */,
-                             const ClientClassDefPtr& /* client_class */,
-                             const OptionDescriptorPtr& /* option */) {
-        isc_throw(NotImplemented, NOT_IMPL_STR);
+    void createUpdateOption4(const ServerSelector& server_selector,
+                             const ClientClassDefPtr& client_class,
+                             const OptionDescriptorPtr& option) {
+
+        if (server_selector.amUnassigned()) {
+            isc_throw(NotImplemented, "managing configuration for no particular server"
+                      " (unassigned) is unsupported at the moment");
+        }
+
+        PsqlBindArray in_bindings;
+        std::string class_name = client_class->getName();
+        in_bindings.add(option->option_->getType());
+        addOptionValueBinding(in_bindings, option);
+        in_bindings.addOptional(option->formatted_value_);
+        in_bindings.addOptional(option->space_name_);
+        in_bindings.add(option->persistent_);
+        in_bindings.add(class_name);
+        in_bindings.addNull();
+        in_bindings.add(2);
+        in_bindings.add(option->getContext());
+        in_bindings.addNull();
+        in_bindings.addNull();
+        in_bindings.addTimestamp(option->getModificationTime());
+
+        // Remember the size before we added where clause arguments.
+        size_t pre_where_size = in_bindings.size();
+        in_bindings.add(class_name);
+        in_bindings.add(option->option_->getType());
+        in_bindings.addOptional(option->space_name_);
+
+        // Create scoped audit revision. As long as this instance exists
+        // no new audit revisions are created in any subsequent calls.
+        ScopedAuditRevision
+            audit_revision(this,
+                           PgSqlConfigBackendDHCPv4Impl::CREATE_AUDIT_REVISION,
+                           server_selector, "client class specific option set",
+                           true);
+
+        if (updateDeleteQuery(PgSqlConfigBackendDHCPv4Impl::UPDATE_OPTION4_CLIENT_CLASS,
+                              in_bindings) == 0) {
+            // The option doesn't exist, so we'll try to insert it.
+            // Remove the update where clause bindings.
+            while (in_bindings.size() > pre_where_size) {
+                in_bindings.popBack();
+            }
+
+            insertOption4(server_selector, in_bindings, option->getModificationTime());
+        }
     }
 
     /// @brief Sends query to insert or update option definition.
@@ -1929,11 +1973,17 @@ public:
     /// @param server_selector Server selector.
     /// @param option_def Pointer to the option definition to be inserted or updated.
     /// @param client_class Client class name.
-    void createUpdateOptionDef4(const ServerSelector& /* server_selector */,
-                                const OptionDefinitionPtr& /* option_def */,
-                                const std::string& /* client_class_name */) {
-        isc_throw(NotImplemented, NOT_IMPL_STR);
-    }
+    void createUpdateOptionDef4(const ServerSelector& server_selector,
+                                const OptionDefinitionPtr& option_def,
+                                const std::string& client_class_name) {
+        createUpdateOptionDef(server_selector, option_def, DHCP4_OPTION_SPACE,
+                              PgSqlConfigBackendDHCPv4Impl::GET_OPTION_DEF4_CODE_SPACE,
+                              PgSqlConfigBackendDHCPv4Impl::INSERT_OPTION_DEF4_CLIENT_CLASS,
+                              PgSqlConfigBackendDHCPv4Impl::UPDATE_OPTION_DEF4_CLIENT_CLASS,
+                              PgSqlConfigBackendDHCPv4Impl::CREATE_AUDIT_REVISION,
+                              PgSqlConfigBackendDHCPv4Impl::INSERT_OPTION_DEF4_SERVER,
+                              client_class_name);
+     }
 
     /// @brief Sends query to delete option definition by code and
     /// option space name.
@@ -2126,9 +2176,17 @@ public:
     /// @param client_class Pointer to the client class for which options
     /// should be deleted.
     /// @return Number of deleted options.
-    uint64_t deleteOptions4(const ServerSelector& /* server_selector */,
-                            const ClientClassDefPtr& /* client_class */) {
-        isc_throw(NotImplemented, NOT_IMPL_STR);
+    uint64_t deleteOptions4(const ServerSelector& server_selector,
+                            const ClientClassDefPtr& client_class) {
+        PsqlBindArray in_bindings;
+        in_bindings.addTempString(client_class->getName());
+
+        // Run DELETE.
+        return (deleteTransactional(PgSqlConfigBackendDHCPv4Impl::
+                                    DELETE_OPTIONS4_CLIENT_CLASS, server_selector,
+                                    "deleting options for a client class",
+                                    "client class specific options deleted",
+                                    true, in_bindings));
     }
 
     /// @brief Common function to retrieve client classes.
@@ -2141,11 +2199,124 @@ public:
     /// if the query contains no WHERE clause.
     /// @param [out] client_classes Reference to a container where fetched client
     /// classes will be inserted.
-    void getClientClasses4(const StatementIndex& /* index */,
-                           const ServerSelector& /* server_selector */,
-                           const PsqlBindArray& /* in_bindings */,
-                           ClientClassDictionary& /* client_classes */) {
-        isc_throw(NotImplemented, NOT_IMPL_STR);
+    void getClientClasses4(const StatementIndex& index,
+                           const ServerSelector& server_selector,
+                           const PsqlBindArray& in_bindings,
+                           ClientClassDictionary& client_classes) {
+        std::list<ClientClassDefPtr> class_list;
+        uint64_t last_option_id = 0;
+        uint64_t last_option_def_id = 0;
+        std::string last_tag;
+
+        selectQuery(index, in_bindings,
+                    [this, &class_list, &last_option_id, &last_option_def_id, &last_tag]
+                    (PgSqlResult& r, int row) {
+            // Create a convenience worker for the row.
+            PgSqlResultRowWorker worker(r, row);
+
+            ClientClassDefPtr last_client_class;
+            if (!class_list.empty()) {
+                last_client_class = *class_list.rbegin();
+            }
+
+            // Class ID is column 0.
+            uint64_t id = worker.getBigInt(0) ;
+
+            if (!last_client_class || (last_client_class->getId() != id)) {
+                last_option_id = 0;
+                last_option_def_id = 0;
+                last_tag.clear();
+
+                auto options = boost::make_shared<CfgOption>();
+                auto option_defs = boost::make_shared<CfgOptionDef>();
+                auto expression = boost::make_shared<Expression>();
+
+                last_client_class = boost::make_shared<ClientClassDef>(worker.getString(1), expression, options);
+                last_client_class->setCfgOptionDef(option_defs);
+
+                // id
+                last_client_class->setId(id);
+
+                // name
+                last_client_class->setName(worker.getString(1));
+
+                // test
+                if (!worker.isColumnNull(2)) {
+                    last_client_class->setTest(worker.getString(2));
+                }
+
+                // next server
+                if (!worker.isColumnNull(3)) {
+                    last_client_class->setNextServer(worker.getInet4(3));
+                }
+
+                // sname
+                if (!worker.isColumnNull(4)) {
+                    last_client_class->setSname(worker.getString(4));
+                }
+
+                // filename
+                if (!worker.isColumnNull(5)) {
+                    last_client_class->setFilename(worker.getString(5));
+                }
+
+                // required
+                if (!worker.isColumnNull(6)) {
+                    last_client_class->setRequired(worker.getBool(6));
+                }
+
+                // valid lifetime: default, min, max
+                last_client_class->setValid(worker.getTriplet(7, 8, 9));
+
+                // depend on known directly or indirectly
+                last_client_class->setDependOnKnown(worker.getBool(10) || worker.getBool(11));
+
+                // modification_ts
+                last_client_class->setModificationTime(worker.getTimestamp(12));
+
+                class_list.push_back(last_client_class);
+            }
+
+            // Check for new server tags at 35.
+            if (!worker.isColumnNull(35)) {
+                std::string new_tag = worker.getString(35);
+                if (last_tag != new_tag) {
+                    if (!new_tag.empty() && !last_client_class->hasServerTag(ServerTag(new_tag))) {
+                        last_client_class->setServerTag(new_tag);
+                    }
+
+                    last_tag = new_tag;
+                }
+            }
+
+            // Parse client class specific option definition from 13 to 22.
+            if (!worker.isColumnNull(13) &&
+                (last_option_def_id < worker.getBigInt(13))) {
+                last_option_def_id = worker.getBigInt(13);
+
+                auto def = processOptionDefRow(worker, 13);
+                if (def) {
+                    last_client_class->getCfgOptionDef()->add(def);
+                }
+            }
+
+            // Parse client class specific option from 23 to 34.
+            if (!worker.isColumnNull(23) &&
+                (last_option_id < worker.getBigInt(23))) {
+                last_option_id = worker.getBigInt(23);
+
+                OptionDescriptorPtr desc = processOptionRow(Option::V4, worker, 23);
+                if (desc) {
+                    last_client_class->getCfgOption()->add(*desc, desc->space_name_);
+                }
+            }
+        });
+
+        tossNonMatchingElements(server_selector, class_list);
+
+        for (auto c : class_list) {
+            client_classes.addClass(c);
+        }
     }
 
     /// @brief Sends query to retrieve a client class by name.
@@ -2153,9 +2324,16 @@ public:
     /// @param server_selector Server selector.
     /// @param name Name of the class to be retrieved.
     /// @return Pointer to the client class or null if the class is not found.
-    ClientClassDefPtr getClientClass4(const ServerSelector& /* server_selector */,
-                                      const std::string& /* name */) {
-        isc_throw(NotImplemented, NOT_IMPL_STR);
+    ClientClassDefPtr getClientClass4(const ServerSelector& server_selector,
+                                      const std::string& name) {
+        PsqlBindArray in_bindings;
+        in_bindings.add(name);
+
+        ClientClassDictionary client_classes;
+        getClientClasses4(PgSqlConfigBackendDHCPv4Impl::GET_CLIENT_CLASS4_NAME,
+                          server_selector, in_bindings, client_classes);
+        return (client_classes.getClasses()->empty() ? ClientClassDefPtr() :
+                (*client_classes.getClasses()->begin()));
     }
 
     /// @brief Sends query to retrieve all client classes.
@@ -2163,11 +2341,13 @@ public:
     /// @param server_selector Server selector.
     /// @param [out] client_classes Reference to the client classes collection
     /// where retrieved classes will be stored.
-    void getAllClientClasses4(const ServerSelector& /* server_selector */,
-                              ClientClassDictionary& /* client_classes */) {
-        /// @todo Rather than throw, we do nothing. This allows CB to be used for
-        /// everything except classes.
-        /// isc_throw(NotImplemented, NOT_IMPL_STR);
+    void getAllClientClasses4(const ServerSelector& server_selector,
+                              ClientClassDictionary& client_classes) {
+        PsqlBindArray in_bindings;
+        getClientClasses4(server_selector.amUnassigned() ?
+                          PgSqlConfigBackendDHCPv4Impl::GET_ALL_CLIENT_CLASSES4_UNASSIGNED :
+                          PgSqlConfigBackendDHCPv4Impl::GET_ALL_CLIENT_CLASSES4,
+                          server_selector, in_bindings, client_classes);
     }
 
     /// @brief Sends query to retrieve modified client classes.
@@ -2176,14 +2356,21 @@ public:
     /// @param modification_ts Lower bound modification timestamp.
     /// @param [out] client_classes Reference to the client classes collection
     /// where retrieved classes will be stored.
-    void getModifiedClientClasses4(const ServerSelector& /* server_selector */,
-                                   const boost::posix_time::ptime& /* modification_ts */,
-                                   ClientClassDictionary& /* client_classes */) {
-        /// @todo Rather than throw, we do nothing. This allows CB to be used for
-        /// everything except classes.
-        /// isc_throw(NotImplemented, NOT_IMPL_STR);
-    }
+    void getModifiedClientClasses4(const ServerSelector& server_selector,
+                                   const boost::posix_time::ptime& modification_ts,
+                                   ClientClassDictionary& client_classes) {
+        if (server_selector.amAny()) {
+            isc_throw(InvalidOperation, "fetching modified client classes for ANY "
+                      "server is not supported");
+        }
 
+        PsqlBindArray in_bindings;
+        in_bindings.addTimestamp(modification_ts);
+        getClientClasses4(server_selector.amUnassigned() ?
+                          PgSqlConfigBackendDHCPv4Impl::GET_MODIFIED_CLIENT_CLASSES4_UNASSIGNED :
+                          PgSqlConfigBackendDHCPv4Impl::GET_MODIFIED_CLIENT_CLASSES4,
+                          server_selector, in_bindings, client_classes);
+    }
 
     /// @brief Upserts client class.
     ///
@@ -2193,10 +2380,167 @@ public:
     /// new or updated class should be positioned. An empty value
     /// causes the class to be appended at the end of the class
     /// hierarchy.
-    void createUpdateClientClass4(const ServerSelector& /* server_selector */,
-                                  const ClientClassDefPtr& /* client_class */,
-                                  const std::string& /* follow_class_name */) {
-        isc_throw(NotImplemented, NOT_IMPL_STR);
+    void createUpdateClientClass4(const ServerSelector& server_selector,
+                                  const ClientClassDefPtr& client_class,
+                                  const std::string& follow_class_name) {
+        // We need to evaluate class expression to see if it references any
+        // other classes (dependencies). As part of this evaluation we will
+        // also check if the client class depends on KNOWN/UNKNOWN built-in
+        // classes.
+        std::list<std::string> dependencies;
+        bool depend_on_known = false;
+        if (!client_class->getTest().empty()) {
+            ExpressionPtr expression;
+            ExpressionParser parser;
+            // Parse the test expression. The callback function is normally used to
+            // interrupt config file parsing when one of the classes refers to a
+            // non-existing client class. It returns false in this case. Here,
+            // we use the callback to capture client classes referenced by the
+            // upserted client class and record whether this class depends on
+            // KNOWN/UNKNOWN built-ins. The callback always returns true to avoid
+            // reporting the parsing error. The dependency check is performed later
+            // at the database level.
+            parser.parse(expression, Element::create(client_class->getTest()), AF_INET,
+                         [&dependencies, &depend_on_known](const ClientClass& client_class) -> bool {
+                if (isClientClassBuiltIn(client_class)) {
+                    if ((client_class == "KNOWN") || (client_class == "UNKNOWN")) {
+                        depend_on_known = true;
+                    }
+                } else {
+                    dependencies.push_back(client_class);
+                }
+                return (true);
+            });
+        }
+
+        PsqlBindArray in_bindings;
+        std::string class_name = client_class->getName();
+        in_bindings.add(class_name);
+        in_bindings.addTempString(client_class->getTest());
+        in_bindings.addInet4(client_class->getNextServer());
+        in_bindings.addTempString(client_class->getSname());
+        in_bindings.addTempString(client_class->getFilename());
+        in_bindings.add(client_class->getRequired());
+        in_bindings.add(client_class->getValid());
+        in_bindings.add(client_class->getValid().getMin());
+        in_bindings.add(client_class->getValid().getMax());
+        in_bindings.add(depend_on_known);
+
+        // 11
+        if (follow_class_name.empty()) {
+            in_bindings.addNull();
+        } else {
+            in_bindings.add(follow_class_name);
+        }
+
+        in_bindings.addTimestamp(client_class->getModificationTime());
+
+        PgSqlTransaction transaction(conn_);
+
+        ScopedAuditRevision audit_revision(this, PgSqlConfigBackendDHCPv4Impl::CREATE_AUDIT_REVISION,
+                                           server_selector, "client class set", true);
+
+        // Create a savepoint in case we are called as part of larger
+        // transaction.
+        conn_.createSavepoint("createUpdateClass4");
+
+        // Keeps track of whether the client class is inserted or updated.
+        auto update = false;
+        try {
+            insertQuery(PgSqlConfigBackendDHCPv4Impl::INSERT_CLIENT_CLASS4, in_bindings);
+        } catch (const DuplicateEntry&) {
+            // It already exists, rollback to the savepoint to preserve
+            // any prior work.
+            conn_.rollbackToSavepoint("createUpdateClass4");
+
+            // Delete options and option definitions. They will be re-created from the new class
+            // instance.
+            deleteOptions4(ServerSelector::ANY(), client_class);
+            deleteOptionDefs4(ServerSelector::ANY(), client_class);
+
+            if (follow_class_name.empty()) {
+                // leave follow name on there, SQL ignores it
+                // in_bindings.popBack();
+
+                // Add the class name for the where clause.
+                in_bindings.add(class_name);
+                updateDeleteQuery(PgSqlConfigBackendDHCPv4Impl::UPDATE_CLIENT_CLASS4_SAME_POSITION,
+                                  in_bindings);
+            } else {
+                // Update with follow_class_name specifying the position.
+                // Add the class name for the where clause.
+                in_bindings.add(class_name);
+                updateDeleteQuery(PgSqlConfigBackendDHCPv4Impl::UPDATE_CLIENT_CLASS4,
+                                  in_bindings);
+            }
+
+            // Delete class associations with the servers and dependencies. We will re-create
+            // them according to the new class specification.
+            PsqlBindArray in_assoc_bindings;
+            in_assoc_bindings.add(class_name);
+            updateDeleteQuery(PgSqlConfigBackendDHCPv4Impl::DELETE_CLIENT_CLASS4_DEPENDENCY,
+                              in_assoc_bindings);
+            updateDeleteQuery(PgSqlConfigBackendDHCPv4Impl::DELETE_CLIENT_CLASS4_SERVER,
+                              in_assoc_bindings);
+            update = true;
+        }
+
+        // Associate client class with the servers.
+        PsqlBindArray attach_bindings;
+        attach_bindings.add(class_name);
+        attach_bindings.addTimestamp(client_class->getModificationTime());
+
+        attachElementToServers(PgSqlConfigBackendDHCPv4Impl::INSERT_CLIENT_CLASS4_SERVER,
+                               server_selector, attach_bindings);
+
+        // Iterate over the captured dependencies and try to insert them into the database.
+        for (auto dependency : dependencies) {
+            try {
+                PsqlBindArray in_dependency_bindings;
+                in_dependency_bindings.add(class_name);
+                in_dependency_bindings.add(dependency);
+
+                // We deleted earlier dependencies, so we can simply insert new ones.
+                insertQuery(PgSqlConfigBackendDHCPv4Impl::INSERT_CLIENT_CLASS4_DEPENDENCY,
+                            in_dependency_bindings);
+            } catch (const std::exception& ex) {
+                isc_throw(InvalidOperation, "unmet dependency on client class: " << dependency);
+            }
+        }
+
+        // If we performed client class update we also have to verify that its dependency
+        // on KNOWN/UNKNOWN client classes hasn't changed.
+        if (update) {
+            PsqlBindArray in_check_bindings;
+            insertQuery(PgSqlConfigBackendDHCPv4Impl::CHECK_CLIENT_CLASS_KNOWN_DEPENDENCY_CHANGE,
+                              in_check_bindings);
+        }
+
+        // (Re)create option definitions.
+        if (client_class->getCfgOptionDef()) {
+            auto option_defs = client_class->getCfgOptionDef()->getContainer();
+            auto option_spaces = option_defs.getOptionSpaceNames();
+            for (auto option_space : option_spaces) {
+                OptionDefContainerPtr defs = option_defs.getItems(option_space);
+                for (auto def = defs->begin(); def != defs->end(); ++def) {
+                    createUpdateOptionDef4(server_selector, *def, client_class->getName());
+                }
+            }
+        }
+
+        // (Re)create options.
+        auto option_spaces = client_class->getCfgOption()->getOptionSpaceNames();
+        for (auto option_space : option_spaces) {
+            OptionContainerPtr options = client_class->getCfgOption()->getAll(option_space);
+            for (auto desc = options->begin(); desc != options->end(); ++desc) {
+                OptionDescriptorPtr desc_copy = OptionDescriptor::create(*desc);
+                desc_copy->space_name_ = option_space;
+                createUpdateOption4(server_selector, client_class, desc_copy);
+            }
+        }
+
+        // All ok. Commit the transaction.
+        transaction.commit();
     }
 
     /// @brief Removes client class by name.
@@ -2204,9 +2548,18 @@ public:
     /// @param server_selector Server selector.
     /// @param name Removed client class name.
     /// @return Number of deleted client classes.
-    uint64_t deleteClientClass4(const ServerSelector& /* server_selector */,
-                                const std::string& /* name */) {
-        isc_throw(NotImplemented, NOT_IMPL_STR);
+    uint64_t deleteClientClass4(const ServerSelector& server_selector,
+                                const std::string& name) {
+        int index = server_selector.amAny() ?
+            PgSqlConfigBackendDHCPv4Impl::DELETE_CLIENT_CLASS4_ANY :
+            PgSqlConfigBackendDHCPv4Impl::DELETE_CLIENT_CLASS4;
+
+        uint64_t result = deleteTransactional(index, server_selector,
+                                              "deleting client class",
+                                              "client class deleted",
+                                              true,
+                                              name);
+        return (result);
     }
 
     /// @brief Removes unassigned global parameters, global options and
@@ -3652,18 +4005,18 @@ TaggedStatementArray tagged_statements = { {
             OID_INT8,       //  8 min_valid_lifetime
             OID_INT8,       //  9 max_valid_lifetime
             OID_BOOL,       // 10 depend_on_known_directly
-            OID_TIMESTAMP,  // 11 modification_ts
-            OID_VARCHAR,    // 12 name (of class to update)
-            OID_VARCHAR     // 13 follow_class_name
+            OID_VARCHAR,    // 11 follow_class_name
+            OID_TIMESTAMP,  // 12 modification_ts
+            OID_VARCHAR     // 13 name (of class to update)
         },
         "UPDATE_CLIENT_CLASS4",
-        PGSQL_UPDATE_CLIENT_CLASS4("follow_class_name = $13,")
+        PGSQL_UPDATE_CLIENT_CLASS4("follow_class_name = $11,")
     },
 
     // Update existing client class without specifying its position.
     {
         // PgSqlConfigBackendDHCPv4Impl::UPDATE_CLIENT_CLASS4_SAME_POSITION,
-        12,
+        13,
         {
             OID_VARCHAR,    //  1 name
             OID_TEXT,       //  2 test
@@ -3675,8 +4028,9 @@ TaggedStatementArray tagged_statements = { {
             OID_INT8,       //  8 min_valid_lifetime
             OID_INT8,       //  9 max_valid_lifetime
             OID_BOOL,       // 10 depend_on_known_directly
-            OID_TIMESTAMP,  // 11 modification_ts
-            OID_VARCHAR     // 12 name (of class to update)
+            OID_VARCHAR,    // 11 filler for follow_class_name
+            OID_TIMESTAMP,  // 12 modification_ts
+            OID_VARCHAR     // 13 name (of class to update)
         },
         "UPDATE_CLIENT_CLASS4_SAME_POSITION",
         PGSQL_UPDATE_CLIENT_CLASS4("")
index 5222dcb6b317a9c4ffaff38a48f0bb43d4bec004..71358c19d0c24fe3ac2cd15ad11896e52e12ac17 100644 (file)
@@ -1009,12 +1009,14 @@ namespace {
     "  is_array = $6," \
     "  encapsulate = $7," \
     "  record_types = $8," \
-    "  user_context =  cast($9 as json)" \
+    "  user_context =  cast($9 as json) " \
     "FROM " #table_prefix "_option_def_server as a, " \
     "     " #table_prefix "_server as s " \
     "WHERE d.id = a.option_def_id AND " \
-    "      a.server_id = s.id AND " \
-    "      d.class_id = (SELECT id FROM dhcp4_client_class WHERE name = $10)"
+    "  a.server_id = s.id AND " \
+    "  d.class_id = (SELECT id FROM dhcp4_client_class WHERE name = $10) " \
+    "  AND s.tag = $11 AND d.code = $12 AND d.space = $13"
+
 #endif
 
 
@@ -1089,8 +1091,8 @@ namespace {
     "  max_valid_lifetime = $9," \
     "  depend_on_known_directly = $10," \
     follow_class_name_set \
-    "  modification_ts = $11 " \
-    "WHERE name = $12"
+    "  modification_ts = $12 " \
+    "WHERE name = $13"
 #endif
 
 #ifndef PGSQL_UPDATE_CLIENT_CLASS6
index f2f3209fb75f004140e6ce8a0008aa313d8312e6..4ab87886f9838015a52ed93ff150d8f9ceacf1d2 100644 (file)
@@ -361,6 +361,38 @@ TEST_F(PgSqlConfigBackendDHCPv4Test, sharedNetworkOptionIdOrderTest) {
     sharedNetworkOptionIdOrderTest();
 }
 
+TEST_F(PgSqlConfigBackendDHCPv4Test, setAndGetAllClientClasses4Test) {
+    setAndGetAllClientClasses4Test();
+}
+
+TEST_F(PgSqlConfigBackendDHCPv4Test, getClientClass4Test) {
+    getClientClass4Test();
+}
+
+TEST_F(PgSqlConfigBackendDHCPv4Test, createUpdateClientClass4OptionsTest) {
+    createUpdateClientClass4OptionsTest();
+}
+
+TEST_F(PgSqlConfigBackendDHCPv4Test, getModifiedClientClasses4Test) {
+    getModifiedClientClasses4Test();
+}
+
+TEST_F(PgSqlConfigBackendDHCPv4Test, deleteClientClass4Test) {
+    deleteClientClass4Test();
+}
+
+TEST_F(PgSqlConfigBackendDHCPv4Test, deleteAllClientClasses4Test) {
+    deleteAllClientClasses4Test();
+}
+
+TEST_F(PgSqlConfigBackendDHCPv4Test, clientClassDependencies4Test) {
+    clientClassDependencies4Test();
+}
+
+TEST_F(PgSqlConfigBackendDHCPv4Test, multipleAuditEntriesTest) {
+    multipleAuditEntriesTest();
+}
+
 /// @brief Test fixture for verifying database connection loss-recovery
 /// behavior.
 class PgSqlConfigBackendDHCPv4DbLostCallbackTest : public GenericConfigBackendDbLostCallbackTest {
index 261eaa9ec22e7075adea7298d316eb3c16e697b3..776e8b940da145e35098585bcaf243ddbc93f41a 100644 (file)
@@ -17,8 +17,8 @@
 namespace isc {
 namespace db {
 
-/// @brief Define PostgreSQL backend version: 9.0
-const uint32_t PGSQL_SCHEMA_VERSION_MAJOR = 9;
+/// @brief Define PostgreSQL backend version: 10.0
+const uint32_t PGSQL_SCHEMA_VERSION_MAJOR = 10;
 const uint32_t PGSQL_SCHEMA_VERSION_MINOR = 0;
 
 // Maximum number of parameters that can be used a statement
index 7dd088794c2282dab622c858e9f0f5f3689c39f9..071cba56054dc1bb452a3ee9c63203bbcfb9e67e 100644 (file)
@@ -23,6 +23,7 @@ pgsql_SCRIPTS += upgrade_006.1_to_006.2.sh
 pgsql_SCRIPTS += upgrade_006.2_to_007.0.sh
 pgsql_SCRIPTS += upgrade_007_to_008.sh
 pgsql_SCRIPTS += upgrade_008_to_009.sh
+pgsql_SCRIPTS += upgrade_009_to_010.sh
 pgsql_SCRIPTS += wipe_data.sh
 
 DISTCLEANFILES = ${pgsql_SCRIPTS}
index 40f1d3dc7e226be6cb54680a0b589cc93745c7ea..4ee81e6dddd1b8e6bd0d74430cd98e868c621cb8 100644 (file)
@@ -4510,9 +4510,299 @@ BEGIN
 END;$$
 LANGUAGE plpgsql;
 
+-- Schema 9.0 specification ends here.
+
+-- This starts schema update to 10.0.
+-- It adds corrections for client classes for CB
+
+-- Replace setClientClass4Order():
+-- 1. l_depend_on_known_indirectly needs to be BOOL
+-- 2. follow_class_index needs to be BIGINT
+
+-- -----------------------------------------------------------------------
+-- Stored procedure positioning an inserted or updated client class
+-- within the class hierarchy, depending on the value of the
+-- follow_class_name parameter.
+--
+-- Parameters:
+-- - id id of the positioned class,
+-- - follow_class_name name of the class after which this class should be
+--   positioned within the class hierarchy.
+-- - old_follow_class_name previous name of the class after which this
+--   class was positioned within the class hierarchy.
+-- -----------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION setClientClass4Order(id BIGINT,
+                                                new_follow_class_name VARCHAR(128),
+                                                old_follow_class_name VARCHAR(128))
+RETURNS VOID
+LANGUAGE plpgsql
+AS $$
+DECLARE
+    -- Used to fetch class's current value for depend_on_known_indirectly
+    l_depend_on_known_indirectly BOOL := false;
+
+    -- Optionally set if the follow_class_name column value is specified.
+    follow_class_index BIGINT;
+BEGIN
+    -- Fetch the class's current value of depend_on_known_indirectly.
+    SELECT depend_on_known_indirectly INTO l_depend_on_known_indirectly
+        FROM dhcp4_client_class_order WHERE id = class_id;
+
+    -- Save it to the current session for use elsewhere during this transaction.
+    -- Note this does not work prior to Postgres 9.2 unless the variables are
+    -- defined in postgresql.conf. I think for now we put up with CB not supported
+    -- prior to 9.2 or we tell people how to edit the conf file.
+    PERFORM set_session_value('kea.depend_on_known_indirectly', l_depend_on_known_indirectly);
+
+    -- Bail if the class is updated without re-positioning.
+    IF(
+       l_depend_on_known_indirectly IS NOT NULL AND
+       ((new_follow_class_name IS NULL AND old_follow_class_name IS NULL) OR
+        (new_follow_class_name = old_follow_class_name))
+    ) THEN
+        -- The depend_on_known_indirectly is set to 0 because this procedure is invoked
+        -- whenever the dhcp4_client_class record is updated. Such update may include
+        -- test expression changes impacting the dependency on KNOWN/UNKNOWN classes.
+        -- This value will be later adjusted when dependencies are inserted.
+        -- TKM should we update the session value also or is it moot?
+        UPDATE dhcp4_client_class_order SET depend_on_known_indirectly = false
+            WHERE class_id = id;
+        RETURN;
+    END IF;
+
+    IF new_follow_class_name IS NOT NULL THEN
+        -- Get the position of the class after which the new class should be added.
+        SELECT o.order_index INTO follow_class_index
+            FROM dhcp4_client_class AS c
+            INNER JOIN dhcp4_client_class_order AS o
+                ON c.id = o.class_id
+            WHERE c.name = new_follow_class_name;
+
+        IF follow_class_index IS NULL THEN
+            -- The class with a name specified with new_follow_class_name does
+            -- not exist.
+            RAISE EXCEPTION 'Class %s does not exist.', new_follow_class_name
+                USING ERRCODE = 'sql_routine_exception';
+        END IF;
+
+        -- We need to place the new class at the position of follow_class_index + 1.
+        -- There may be a class at this position already.
+        IF EXISTS(SELECT * FROM dhcp4_client_class_order WHERE order_index = follow_class_index + 1) THEN
+            -- There is a class at this position already. Let's move all classes
+            -- starting from this position by one to create a spot for the new
+            -- class.
+            UPDATE dhcp4_client_class_order
+                SET order_index = order_index + 1
+            WHERE order_index >= follow_class_index + 1;
+            -- TKM postgresql doesn't like order by here, does it matter?
+            -- ORDER BY order_index DESC;
+        END IF;
+
+    ELSE
+        -- A caller did not specify the new_follow_class_name value. Let's append the
+        -- new class at the end of the hierarchy.
+        SELECT MAX(order_index) INTO follow_class_index FROM dhcp4_client_class_order;
+        IF follow_class_index IS NULL THEN
+            -- Apparently, there are no classes. Let's start from 0.
+            follow_class_index = 0;
+        END IF;
+    END IF;
+
+    -- Check if moving the class doesn't break dependent classes.
+    IF EXISTS(
+        SELECT 1 FROM dhcp4_client_class_dependency AS d
+            INNER JOIN dhcp4_client_class_order AS o
+                ON d.class_id = o.class_id
+            WHERE d.dependency_id = id AND o.order_index < follow_class_index + 1
+        LIMIT 1
+    ) THEN
+        RAISE EXCEPTION 'Unable to move class with id %s because it would break its dependencies', id
+            USING ERRCODE = 'sql_routine_exception';
+    END IF;
+
+    -- The depend_on_known_indirectly is set to 0 because this procedure is invoked
+    -- whenever the dhcp4_client_class record is updated. Such update may include
+    -- test expression changes impacting the dependency on KNOWN/UNKNOWN classes.
+    -- This value will be later adjusted when dependencies are inserted.
+    -- ON CONFLICT required 9.5 or later
+    UPDATE dhcp4_client_class_order
+        SET order_index = follow_class_index + 1,
+            depend_on_known_indirectly = l_depend_on_known_indirectly
+        WHERE class_id = id;
+    IF FOUND THEN
+        RETURN;
+    END IF;
+
+    INSERT INTO  dhcp4_client_class_order(class_id, order_index, depend_on_known_indirectly)
+        VALUES (id, follow_class_index + 1, false);
+    RETURN;
+END;$$;
+
+-- Replace setClientClass4Order():
+-- 1. l_depend_on_known_indirectly needs to be BOOL
+
+-- -----------------------------------------------------------------------
+-- Stored procedure positioning an inserted or updated client class
+-- within the class hierarchy, depending on the value of the
+-- new_follow_class_name parameter.
+--
+-- Parameters:
+-- - id id of the positioned class,
+-- - new_follow_class_name name of the class after which this class should be
+--   positioned within the class hierarchy.
+-- - old_follow_class_name previous name of the class after which this
+--   class was positioned within the class hierarchy.
+-- -----------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION setClientClass6Order(id BIGINT,
+                                                new_follow_class_name VARCHAR(128),
+                                                old_follow_class_name VARCHAR(128))
+RETURNS VOID
+LANGUAGE plpgsql
+AS $$
+DECLARE
+    -- Used to fetch class's current value for depend_on_known_indirectly
+    l_depend_on_known_indirectly BOOL := false;
+
+    -- Optionally set if the follow_class_name column value is specified.
+    follow_class_index BIGINT;
+BEGIN
+    -- Fetch the class's current value of depend_on_known_indirectly.
+    SELECT depend_on_known_indirectly INTO l_depend_on_known_indirectly
+        FROM dhcp6_client_class_order WHERE id = class_id;
+
+    -- Save it to the current session for use elsewhere during this transaction.
+    -- Note this does not work prior to Postgres 9.2 unless the variables are
+    -- defined in postgresql.conf. I think for now we put up with CB not supported
+    -- prior to 9.2 or we tell people how to edit the conf file.
+    PERFORM set_session_value('kea.depend_on_known_indirectly', l_depend_on_known_indirectly);
+
+    -- Bail if the class is updated without re-positioning.
+    IF(
+       l_depend_on_known_indirectly IS NOT NULL AND
+       ((new_follow_class_name IS NULL AND old_follow_class_name IS NULL) OR
+        (new_follow_class_name = old_follow_class_name))
+    ) THEN
+        -- The depend_on_known_indirectly is set to 0 because this procedure is invoked
+        -- whenever the dhcp6_client_class record is updated. Such update may include
+        -- test expression changes impacting the dependency on KNOWN/UNKNOWN classes.
+        -- This value will be later adjusted when dependencies are inserted.
+        -- TKM should we update the session value also or is it moot?
+        UPDATE dhcp6_client_class_order SET depend_on_known_indirectly = false
+            WHERE class_id = id;
+        RETURN;
+    END IF;
+
+    IF new_follow_class_name IS NOT NULL THEN
+        -- Get the position of the class after which the new class should be added.
+        SELECT o.order_index INTO follow_class_index
+            FROM dhcp6_client_class AS c
+            INNER JOIN dhcp6_client_class_order AS o
+                ON c.id = o.class_id
+            WHERE c.name = new_follow_class_name;
+
+        IF follow_class_index IS NULL THEN
+            -- The class with a name specified with new_follow_class_name does
+            -- not exist.
+            RAISE EXCEPTION 'Class %s does not exist.', new_follow_class_name
+                USING ERRCODE = 'sql_routine_exception';
+        END IF;
+
+        -- We need to place the new class at the position of follow_class_index + 1.
+        -- There may be a class at this position already.
+        IF EXISTS(SELECT * FROM dhcp6_client_class_order WHERE order_index = follow_class_index + 1) THEN
+            -- There is a class at this position already. Let's move all classes
+            -- starting from this position by one to create a spot for the new
+            -- class.
+            UPDATE dhcp6_client_class_order
+                SET order_index = order_index + 1
+            WHERE order_index >= follow_class_index + 1;
+            -- TKM postgresql doesn't like order by here, does it matter?
+            -- ORDER BY order_index DESC;
+        END IF;
+
+    ELSE
+        -- A caller did not specify the new_follow_class_name value. Let's append the
+        -- new class at the end of the hierarchy.
+        SELECT MAX(order_index) INTO follow_class_index FROM dhcp6_client_class_order;
+        IF follow_class_index IS NULL THEN
+            -- Apparently, there are no classes. Let's start from 0.
+            follow_class_index = 0;
+        END IF;
+    END IF;
+
+    -- Check if moving the class doesn't break dependent classes.
+    IF EXISTS(
+        SELECT 1 FROM dhcp6_client_class_dependency AS d
+            INNER JOIN dhcp6_client_class_order AS o
+                ON d.class_id = o.class_id
+            WHERE d.dependency_id = id AND o.order_index < follow_class_index + 1
+        LIMIT 1
+    ) THEN
+        RAISE EXCEPTION 'Unable to move class with id %s because it would break its dependencies', id
+            USING ERRCODE = 'sql_routine_exception';
+    END IF;
+
+    -- The depend_on_known_indirectly is set to 0 because this procedure is invoked
+    -- whenever the dhcp6_client_class record is updated. Such update may include
+    -- test expression changes impacting the dependency on KNOWN/UNKNOWN classes.
+    -- This value will be later adjusted when dependencies are inserted.
+    -- TKM - note that ON CONFLICT requires PostgreSQL 9.5 or later.
+    UPDATE dhcp6_client_class_order
+        SET order_index = follow_class_index + 1,
+            depend_on_known_indirectly = l_depend_on_known_indirectly
+        WHERE class_id = id;
+    IF FOUND THEN
+        RETURN;
+    END IF;
+
+    INSERT INTO  dhcp6_client_class_order(class_id, order_index, depend_on_known_indirectly)
+        VALUES (id, follow_class_index + 1, false);
+    RETURN;
+END;$$;
+
+-- Change primary key to composite, dependency table can have multiple rows
+-- per class id.
+ALTER TABLE dhcp4_client_class_dependency DROP CONSTRAINT dhcp4_client_class_dependency_pkey;
+ALTER TABLE dhcp4_client_class_dependency ADD PRIMARY KEY(class_id, dependency_id);
+
+ALTER TABLE dhcp6_client_class_dependency DROP CONSTRAINT dhcp6_client_class_dependency_pkey;
+ALTER TABLE dhcp6_client_class_dependency ADD PRIMARY KEY(class_id, dependency_id);
+
+-- Replace triggers that verify class dependency.
+-- Because they are BEFORE INSERT triggers they need to return NEW not NULL.
+-- -----------------------------------------------------------------------
+-- Trigger verifying if class dependency is met. It includes checking
+-- if referenced classes exist, are associated with the same server
+-- or all servers, and are defined before the class specified with
+-- class_id.
+-- -----------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION func_dhcp4_client_class_check_dependency_BINS()
+    RETURNS trigger AS $dhcp4_client_class_check_dependency_BINS$
+BEGIN
+    PERFORM checkDHCPv4ClientClassDependency(NEW.class_id, NEW.dependency_id);
+    RETURN NEW;
+END;
+$dhcp4_client_class_check_dependency_BINS$
+LANGUAGE plpgsql;
+
+-- -----------------------------------------------------------------------
+-- Trigger verifying if class dependency is met. It includes checking
+-- if referenced classes exist, are associated with the same server
+-- or all servers, and are defined before the class specified with
+-- class_id.
+-- -----------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION func_dhcp6_client_class_check_dependency_BINS()
+    RETURNS trigger AS $dhcp6_client_class_check_dependency_BINS$
+BEGIN
+    PERFORM checkDHCPv6ClientClassDependency(NEW.class_id, NEW.dependency_id);
+    RETURN NEW;
+END;
+$dhcp6_client_class_check_dependency_BINS$
+LANGUAGE plpgsql;
+
 -- Update the schema version number.
 UPDATE schema_version
-    SET version = '9', minor = '0';
+    SET version = '10', minor = '0';
 
 -- Commit the script transaction.
 COMMIT;
diff --git a/src/share/database/scripts/pgsql/upgrade_009_to_010.sh.in b/src/share/database/scripts/pgsql/upgrade_009_to_010.sh.in
new file mode 100644 (file)
index 0000000..c84aa89
--- /dev/null
@@ -0,0 +1,334 @@
+#!/bin/sh
+
+# Copyright (C) 2022 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/.
+
+# shellcheck disable=SC1091
+# SC1091: Not following: ... was not specified as input (see shellcheck -x).
+
+# Exit with error if commands exit with non-zero and if undefined variables are
+# used.
+set -eu
+
+# shellcheck disable=SC2034
+# SC2034: ... appears unused. Verify use (or export if used externally).
+prefix="@prefix@"
+
+# Include utilities. Use installed version if available and
+# use build version if it isn't.
+if [ -e @datarootdir@/@PACKAGE_NAME@/scripts/admin-utils.sh ]; then
+    . "@datarootdir@/@PACKAGE_NAME@/scripts/admin-utils.sh"
+else
+    . "@abs_top_builddir@/src/bin/admin/admin-utils.sh"
+fi
+
+VERSION=$(pgsql_version "$@")
+
+if [ "$VERSION" != "9.0" ]; then
+    printf 'This script upgrades 9.0 to 10.0. '
+    printf 'Reported version is %s. Skipping upgrade.\n' "${VERSION}"
+    exit 0
+fi
+
+psql "$@" >/dev/null <<EOF
+START TRANSACTION;
+
+-- This starts schema update to 10.0.
+-- It adds corrections for client classes for CB
+
+-- Replace setClientClass4Order():
+-- 1. l_depend_on_known_indirectly needs to be BOOL
+-- 2. follow_class_index needs to be BIGINT
+
+-- -----------------------------------------------------------------------
+-- Stored procedure positioning an inserted or updated client class
+-- within the class hierarchy, depending on the value of the
+-- follow_class_name parameter.
+--
+-- Parameters:
+-- - id id of the positioned class,
+-- - follow_class_name name of the class after which this class should be
+--   positioned within the class hierarchy.
+-- - old_follow_class_name previous name of the class after which this
+--   class was positioned within the class hierarchy.
+-- -----------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION setClientClass4Order(id BIGINT,
+                                                new_follow_class_name VARCHAR(128),
+                                                old_follow_class_name VARCHAR(128))
+RETURNS VOID
+LANGUAGE plpgsql
+AS \$\$
+DECLARE
+    -- Used to fetch class's current value for depend_on_known_indirectly
+    l_depend_on_known_indirectly BOOL := false;
+
+    -- Optionally set if the follow_class_name column value is specified.
+    follow_class_index BIGINT;
+BEGIN
+    -- Fetch the class's current value of depend_on_known_indirectly.
+    SELECT depend_on_known_indirectly INTO l_depend_on_known_indirectly
+        FROM dhcp4_client_class_order WHERE id = class_id;
+
+    -- Save it to the current session for use elsewhere during this transaction.
+    -- Note this does not work prior to Postgres 9.2 unless the variables are
+    -- defined in postgresql.conf. I think for now we put up with CB not supported
+    -- prior to 9.2 or we tell people how to edit the conf file.
+    PERFORM set_session_value('kea.depend_on_known_indirectly', l_depend_on_known_indirectly);
+
+    -- Bail if the class is updated without re-positioning.
+    IF(
+       l_depend_on_known_indirectly IS NOT NULL AND
+       ((new_follow_class_name IS NULL AND old_follow_class_name IS NULL) OR
+        (new_follow_class_name = old_follow_class_name))
+    ) THEN
+        -- The depend_on_known_indirectly is set to 0 because this procedure is invoked
+        -- whenever the dhcp4_client_class record is updated. Such update may include
+        -- test expression changes impacting the dependency on KNOWN/UNKNOWN classes.
+        -- This value will be later adjusted when dependencies are inserted.
+        -- TKM should we update the session value also or is it moot?
+        UPDATE dhcp4_client_class_order SET depend_on_known_indirectly = false
+            WHERE class_id = id;
+        RETURN;
+    END IF;
+
+    IF new_follow_class_name IS NOT NULL THEN
+        -- Get the position of the class after which the new class should be added.
+        SELECT o.order_index INTO follow_class_index
+            FROM dhcp4_client_class AS c
+            INNER JOIN dhcp4_client_class_order AS o
+                ON c.id = o.class_id
+            WHERE c.name = new_follow_class_name;
+
+        IF follow_class_index IS NULL THEN
+            -- The class with a name specified with new_follow_class_name does
+            -- not exist.
+            RAISE EXCEPTION 'Class %s does not exist.', new_follow_class_name
+                USING ERRCODE = 'sql_routine_exception';
+        END IF;
+
+        -- We need to place the new class at the position of follow_class_index + 1.
+        -- There may be a class at this position already.
+        IF EXISTS(SELECT * FROM dhcp4_client_class_order WHERE order_index = follow_class_index + 1) THEN
+            -- There is a class at this position already. Let's move all classes
+            -- starting from this position by one to create a spot for the new
+            -- class.
+            UPDATE dhcp4_client_class_order
+                SET order_index = order_index + 1
+            WHERE order_index >= follow_class_index + 1;
+            -- TKM postgresql doesn't like order by here, does it matter?
+            -- ORDER BY order_index DESC;
+        END IF;
+
+    ELSE
+        -- A caller did not specify the new_follow_class_name value. Let's append the
+        -- new class at the end of the hierarchy.
+        SELECT MAX(order_index) INTO follow_class_index FROM dhcp4_client_class_order;
+        IF follow_class_index IS NULL THEN
+            -- Apparently, there are no classes. Let's start from 0.
+            follow_class_index = 0;
+        END IF;
+    END IF;
+
+    -- Check if moving the class doesn't break dependent classes.
+    IF EXISTS(
+        SELECT 1 FROM dhcp4_client_class_dependency AS d
+            INNER JOIN dhcp4_client_class_order AS o
+                ON d.class_id = o.class_id
+            WHERE d.dependency_id = id AND o.order_index < follow_class_index + 1
+        LIMIT 1
+    ) THEN
+        RAISE EXCEPTION 'Unable to move class with id %s because it would break its dependencies', id
+            USING ERRCODE = 'sql_routine_exception';
+    END IF;
+
+    -- The depend_on_known_indirectly is set to 0 because this procedure is invoked
+    -- whenever the dhcp4_client_class record is updated. Such update may include
+    -- test expression changes impacting the dependency on KNOWN/UNKNOWN classes.
+    -- This value will be later adjusted when dependencies are inserted.
+    -- ON CONFLICT required 9.5 or later
+    UPDATE dhcp4_client_class_order
+        SET order_index = follow_class_index + 1,
+            depend_on_known_indirectly = l_depend_on_known_indirectly
+        WHERE class_id = id;
+    IF FOUND THEN
+        RETURN;
+    END IF;
+
+    INSERT INTO  dhcp4_client_class_order(class_id, order_index, depend_on_known_indirectly)
+        VALUES (id, follow_class_index + 1, false);
+    RETURN;
+END;\$\$;
+
+-- Replace setClientClass4Order():
+-- 1. l_depend_on_known_indirectly needs to be BOOL
+
+-- -----------------------------------------------------------------------
+-- Stored procedure positioning an inserted or updated client class
+-- within the class hierarchy, depending on the value of the
+-- new_follow_class_name parameter.
+--
+-- Parameters:
+-- - id id of the positioned class,
+-- - new_follow_class_name name of the class after which this class should be
+--   positioned within the class hierarchy.
+-- - old_follow_class_name previous name of the class after which this
+--   class was positioned within the class hierarchy.
+-- -----------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION setClientClass6Order(id BIGINT,
+                                                new_follow_class_name VARCHAR(128),
+                                                old_follow_class_name VARCHAR(128))
+RETURNS VOID
+LANGUAGE plpgsql
+AS \$\$
+DECLARE
+    -- Used to fetch class's current value for depend_on_known_indirectly
+    l_depend_on_known_indirectly BOOL := false;
+
+    -- Optionally set if the follow_class_name column value is specified.
+    follow_class_index BIGINT;
+BEGIN
+    -- Fetch the class's current value of depend_on_known_indirectly.
+    SELECT depend_on_known_indirectly INTO l_depend_on_known_indirectly
+        FROM dhcp6_client_class_order WHERE id = class_id;
+
+    -- Save it to the current session for use elsewhere during this transaction.
+    -- Note this does not work prior to Postgres 9.2 unless the variables are
+    -- defined in postgresql.conf. I think for now we put up with CB not supported
+    -- prior to 9.2 or we tell people how to edit the conf file.
+    PERFORM set_session_value('kea.depend_on_known_indirectly', l_depend_on_known_indirectly);
+
+    -- Bail if the class is updated without re-positioning.
+    IF(
+       l_depend_on_known_indirectly IS NOT NULL AND
+       ((new_follow_class_name IS NULL AND old_follow_class_name IS NULL) OR
+        (new_follow_class_name = old_follow_class_name))
+    ) THEN
+        -- The depend_on_known_indirectly is set to 0 because this procedure is invoked
+        -- whenever the dhcp6_client_class record is updated. Such update may include
+        -- test expression changes impacting the dependency on KNOWN/UNKNOWN classes.
+        -- This value will be later adjusted when dependencies are inserted.
+        -- TKM should we update the session value also or is it moot?
+        UPDATE dhcp6_client_class_order SET depend_on_known_indirectly = false
+            WHERE class_id = id;
+        RETURN;
+    END IF;
+
+    IF new_follow_class_name IS NOT NULL THEN
+        -- Get the position of the class after which the new class should be added.
+        SELECT o.order_index INTO follow_class_index
+            FROM dhcp6_client_class AS c
+            INNER JOIN dhcp6_client_class_order AS o
+                ON c.id = o.class_id
+            WHERE c.name = new_follow_class_name;
+
+        IF follow_class_index IS NULL THEN
+            -- The class with a name specified with new_follow_class_name does
+            -- not exist.
+            RAISE EXCEPTION 'Class %s does not exist.', new_follow_class_name
+                USING ERRCODE = 'sql_routine_exception';
+        END IF;
+
+        -- We need to place the new class at the position of follow_class_index + 1.
+        -- There may be a class at this position already.
+        IF EXISTS(SELECT * FROM dhcp6_client_class_order WHERE order_index = follow_class_index + 1) THEN
+            -- There is a class at this position already. Let's move all classes
+            -- starting from this position by one to create a spot for the new
+            -- class.
+            UPDATE dhcp6_client_class_order
+                SET order_index = order_index + 1
+            WHERE order_index >= follow_class_index + 1;
+            -- TKM postgresql doesn't like order by here, does it matter?
+            -- ORDER BY order_index DESC;
+        END IF;
+
+    ELSE
+        -- A caller did not specify the new_follow_class_name value. Let's append the
+        -- new class at the end of the hierarchy.
+        SELECT MAX(order_index) INTO follow_class_index FROM dhcp6_client_class_order;
+        IF follow_class_index IS NULL THEN
+            -- Apparently, there are no classes. Let's start from 0.
+            follow_class_index = 0;
+        END IF;
+    END IF;
+
+    -- Check if moving the class doesn't break dependent classes.
+    IF EXISTS(
+        SELECT 1 FROM dhcp6_client_class_dependency AS d
+            INNER JOIN dhcp6_client_class_order AS o
+                ON d.class_id = o.class_id
+            WHERE d.dependency_id = id AND o.order_index < follow_class_index + 1
+        LIMIT 1
+    ) THEN
+        RAISE EXCEPTION 'Unable to move class with id %s because it would break its dependencies', id
+            USING ERRCODE = 'sql_routine_exception';
+    END IF;
+
+    -- The depend_on_known_indirectly is set to 0 because this procedure is invoked
+    -- whenever the dhcp6_client_class record is updated. Such update may include
+    -- test expression changes impacting the dependency on KNOWN/UNKNOWN classes.
+    -- This value will be later adjusted when dependencies are inserted.
+    -- TKM - note that ON CONFLICT requires PostgreSQL 9.5 or later.
+    UPDATE dhcp6_client_class_order
+        SET order_index = follow_class_index + 1,
+            depend_on_known_indirectly = l_depend_on_known_indirectly
+        WHERE class_id = id;
+    IF FOUND THEN
+        RETURN;
+    END IF;
+
+    INSERT INTO  dhcp6_client_class_order(class_id, order_index, depend_on_known_indirectly)
+        VALUES (id, follow_class_index + 1, false);
+    RETURN;
+END;\$\$;
+
+-- Change primary key to composite, dependency table can have multiple rows
+-- per class id.
+ALTER TABLE dhcp4_client_class_dependency DROP CONSTRAINT dhcp4_client_class_dependency_pkey;
+ALTER TABLE dhcp4_client_class_dependency ADD PRIMARY KEY(class_id, dependency_id);
+
+ALTER TABLE dhcp6_client_class_dependency DROP CONSTRAINT dhcp6_client_class_dependency_pkey;
+ALTER TABLE dhcp6_client_class_dependency ADD PRIMARY KEY(class_id, dependency_id);
+
+-- Replace triggers that verify class dependency.
+-- Because they are BEFORE INSERT triggers they need to return NEW not NULL.
+-- -----------------------------------------------------------------------
+-- Trigger verifying if class dependency is met. It includes checking
+-- if referenced classes exist, are associated with the same server
+-- or all servers, and are defined before the class specified with
+-- class_id.
+-- -----------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION func_dhcp4_client_class_check_dependency_BINS()
+    RETURNS trigger AS \$dhcp4_client_class_check_dependency_BINS\$
+BEGIN
+    PERFORM checkDHCPv4ClientClassDependency(NEW.class_id, NEW.dependency_id);
+    RETURN NEW;
+END;
+\$dhcp4_client_class_check_dependency_BINS\$
+LANGUAGE plpgsql;
+
+-- -----------------------------------------------------------------------
+-- Trigger verifying if class dependency is met. It includes checking
+-- if referenced classes exist, are associated with the same server
+-- or all servers, and are defined before the class specified with
+-- class_id.
+-- -----------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION func_dhcp6_client_class_check_dependency_BINS()
+    RETURNS trigger AS \$dhcp6_client_class_check_dependency_BINS\$
+BEGIN
+    PERFORM checkDHCPv6ClientClassDependency(NEW.class_id, NEW.dependency_id);
+    RETURN NEW;
+END;
+\$dhcp6_client_class_check_dependency_BINS\$
+LANGUAGE plpgsql;
+
+-- Update the schema version number.
+UPDATE schema_version
+    SET version = '10', minor = '0';
+
+-- Commit the script transaction.
+COMMIT;
+
+EOF