]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1920] Database schema for client classes
authorMarcin Siodelski <marcin@isc.org>
Thu, 10 Jun 2021 10:39:30 +0000 (12:39 +0200)
committerMarcin Siodelski <marcin@isc.org>
Mon, 5 Jul 2021 09:24:38 +0000 (09:24 +0000)
Extended database schema with new tables holding information about
configured client classes, ordering them, storing their dependencies and
associating them with the server tags. In addition, extended the tables
holding option definitions with an additional column class_id to optionally
associate an option definition with a class.

configure.ac
src/bin/admin/tests/mysql_tests.sh.in
src/lib/mysql/mysql_constants.h
src/share/database/scripts/mysql/.gitignore
src/share/database/scripts/mysql/Makefile.am
src/share/database/scripts/mysql/dhcpdb_create.mysql
src/share/database/scripts/mysql/dhcpdb_drop.mysql
src/share/database/scripts/mysql/upgrade_9.6_to_10.0.sh.in [new file with mode: 0644]
src/share/database/scripts/mysql/wipe_data.sh.in

index f698402db5a3f041c7bd3ced1033064216e12a90..15e4e6b50f8208c0aee1fb39f693cd1f88095b33 100644 (file)
@@ -1895,6 +1895,8 @@ AC_CONFIG_FILES([src/share/database/scripts/mysql/upgrade_9.4_to_9.5.sh],
                 [chmod +x src/share/database/scripts/mysql/upgrade_9.4_to_9.5.sh])
 AC_CONFIG_FILES([src/share/database/scripts/mysql/upgrade_9.5_to_9.6.sh],
                 [chmod +x src/share/database/scripts/mysql/upgrade_9.5_to_9.6.sh])
+AC_CONFIG_FILES([src/share/database/scripts/mysql/upgrade_9.6_to_10.0.sh],
+                [chmod +x src/share/database/scripts/mysql/upgrade_9.6_to_10.0.sh])
 AC_CONFIG_FILES([src/share/database/scripts/mysql/wipe_data.sh],
                 [chmod +x src/share/database/scripts/mysql/wipe_data.sh])
 AC_CONFIG_FILES([src/share/database/scripts/pgsql/Makefile])
index 5ee14b0e51cc456289f63a4b69653487763c6576..986672e0748681eb8d21e83036a9855228cc8d0c 100644 (file)
@@ -234,6 +234,7 @@ mysql_upgrade_schema_to_version() {
 
     for script in "${db_scripts_dir}"/mysql/upgrade*.sh
     do
+        version=$(mysql_version)
         if [ "${version}" = "${target_version}" ]
         then
             break
@@ -241,7 +242,6 @@ mysql_upgrade_schema_to_version() {
 
         echo "Processing $script file..."
         "${script}" --user="${db_user}" --password="${db_password}" "${db_name}"
-        version=$(mysql_version)
     done
 
     echo "Schema upgraded to $version"
@@ -261,7 +261,7 @@ mysql_upgrade_test() {
     version=$("${kea_admin}" db-version mysql -u "${db_user}" -p "${db_password}" -n "${db_name}" -d "${db_scripts_dir}")
     assert_str_eq "1.0" "${version}" "Expected kea-admin to return %s, returned value was %s"
 
-    # Ok, we have a 1.0 database. Let's upgrade it to 9.6
+    # Ok, we have a 1.0 database. Let's upgrade it to 10.0
     run_command \
         "${kea_admin}" db-upgrade mysql -u "${db_user}" -p "${db_password}" -n "${db_name}" -d "${db_scripts_dir}"
     assert_eq 0 "${EXIT_CODE}" "kea-admin db-upgrade mysql failed, expected %d, returned non-zero status code %d\n"
@@ -832,9 +832,9 @@ insert into hosts(dhcp_identifier, dhcp_identifier_type, dhcp4_subnet_id, ipv4_a
         mysql_execute "${qry}"
     assert_eq 0 "${EXIT_CODE}" "${qry} failed: expected status code %d, returned %d"
 
-    # Verify upgraded schema reports version 9.6
+    # Verify upgraded schema reports version 10.0
     version=$("${kea_admin}" db-version mysql -u "${db_user}" -p "${db_password}" -n "${db_name}" -d "${db_scripts_dir}")
-    assert_str_eq "9.6" "${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
     mysql_wipe
@@ -1565,6 +1565,115 @@ mysql_reservation_mode_upgrade_test() {
     test_finish 0
 }
 
+# Verifies that several tables for holding client classes are created
+# and the triggers and stored procedures positioning the client classes
+# and validating their dependencies behave correctly.
+mysql_client_class_test() {
+    table_prefix="$1"
+
+    test_start "mysql_client_classes_test ${table_prefix}"
+
+    # Let's wipe the whole database
+    mysql_wipe
+
+    # We need to create an older database with lease data so we can
+    # verify the upgrade mechanisms which convert subnet id values
+    #
+    # Initialize database to schema 1.0.
+    mysql -u"${db_user}" -p"${db_password}" "${db_name}" < "@abs_top_srcdir@/src/bin/admin/tests/dhcpdb_create_1.0.mysql"
+
+    # Now upgrade to schema 10.0 that can contain client classes.
+    mysql_upgrade_schema_to_version 10.0
+
+    # Insert a new server.
+    sql=\
+"SET @disable_audit = 1; \
+ INSERT INTO ${table_prefix}_server (tag, modification_ts) VALUES ('server1', NOW()); \
+ SET @disable_audit = 0;"
+
+    run_statement "insert servers" "$sql"
+
+    # Insert client class foo at the top of the hierarchy. It has no dependencies.
+    sql=\
+"START TRANSACTION; \
+ SET @disable_audit = 1; \
+ INSERT INTO ${table_prefix}_client_class (name, modification_ts, follow_class_name, depend_on_known_directly) VALUES ('foo', NOW(), NULL, 1); \
+ SET @last_id = LAST_INSERT_ID(); \
+ INSERT INTO ${table_prefix}_client_class_server (class_id, server_id) \
+     VALUES (@last_id, (SELECT id FROM ${table_prefix}_server WHERE tag = 'all')); \
+ SET @disable_audit = 0; \
+ COMMIT;"
+    run_statement "insert client class foo" "$sql"
+
+    # Insert client class foobar after the foo class.
+    sql=\
+"START TRANSACTION; \
+ SET @disable_audit = 1; \
+ INSERT INTO ${table_prefix}_client_class (name, modification_ts, follow_class_name) VALUES ('foobar', NOW(), NULL); \
+ SET @last_id = LAST_INSERT_ID(); \
+ INSERT INTO ${table_prefix}_client_class_server (class_id, server_id) \
+     VALUES (@last_id, (SELECT id FROM ${table_prefix}_server WHERE tag = 'server1')); \
+ SET @disable_audit = 0; \
+ COMMIT;"
+    run_statement "insert client class foobar" "$sql"
+
+    # Insert the client class bar at the end. This class depends on the client
+    # class foo.
+    sql=\
+"START TRANSACTION; \
+ SET @disable_audit = 1; \
+ INSERT INTO ${table_prefix}_client_class (name, modification_ts, follow_class_name) VALUES ('bar', NOW(), 'foo'); \
+ SET @last_id = LAST_INSERT_ID(); \
+ INSERT INTO ${table_prefix}_client_class_server (class_id, server_id) \
+     VALUES (@last_id, (SELECT id FROM ${table_prefix}_server WHERE tag = 'server1')); \
+ INSERT INTO ${table_prefix}_client_class_dependency (class_id, dependency_id) \
+     VALUES (@last_id, (SELECT id FROM ${table_prefix}_client_class WHERE name = 'foo')); \
+ SET @disable_audit = 0; \
+ COMMIT;"
+    run_statement "insert client class bar" "$sql"
+
+    # Ensure that all thre classes have been added in the expected order.
+    sql="SELECT o.order_index FROM ${table_prefix}_client_class AS c \
+         INNER JOIN ${table_prefix}_client_class_order AS o \
+             ON c.id = o.class_id WHERE c.name = 'foo'";
+    run_statement "#get order index of class foo" "$sql" 1
+
+    sql="SELECT o.order_index FROM ${table_prefix}_client_class AS c \
+         INNER JOIN ${table_prefix}_client_class_order AS o \
+             ON c.id = o.class_id WHERE c.name = 'bar'";
+    run_statement "#get order index of class bar" "$sql" 2
+
+    sql="SELECT o.order_index FROM ${table_prefix}_client_class AS c \
+         INNER JOIN ${table_prefix}_client_class_order AS o \
+             ON c.id = o.class_id WHERE c.name = 'foobar'";
+    run_statement "#get order index of class foobar" "$sql" 3
+
+    # Update the class bar moving behind the foobar class.
+    sql=\
+"START TRANSACTION; \
+ SET @disable_audit = 1; \
+ UPDATE ${table_prefix}_client_class SET follow_class_name = 'foobar' WHERE name = 'bar'; \
+ SET @disable_audit = 0; \
+ COMMIT;"
+    run_statement "update client class bar with re-positioning" "$sql"
+
+    # Check that the order of the last two classes was changed.
+    sql="SELECT o.order_index FROM ${table_prefix}_client_class AS c \
+         INNER JOIN ${table_prefix}_client_class_order AS o \
+             ON c.id = o.class_id WHERE c.name = 'bar'";
+    run_statement "#get order index of class bar" "$sql" 4
+
+    sql="SELECT o.order_index FROM ${table_prefix}_client_class AS c \
+         INNER JOIN ${table_prefix}_client_class_order AS o \
+             ON c.id = o.class_id WHERE c.name = 'foobar'";
+    run_statement "#get order index of class foobar" "$sql" 3
+
+    # Let's wipe the whole database
+    mysql_wipe
+
+    test_finish 0
+}
+
 
 # Run tests.
 mysql_db_init_test
@@ -1579,3 +1688,5 @@ mysql_lease_stat_upgrade_test
 mysql_lease_stat_recount_test
 mysql_unused_subnet_id_test
 mysql_reservation_mode_upgrade_test
+mysql_client_class_test dhcp4
+mysql_client_class_test dhcp6
index aac048d261994665c1d9a2ff0508f43b241dbdfa..7fdb42f9d539c60c009df371cf0c337872dc344d 100644 (file)
@@ -52,8 +52,8 @@ const int MLM_MYSQL_FETCH_FAILURE = 0;
 
 /// @name Current database schema version values.
 //@{
-const uint32_t MYSQL_SCHEMA_VERSION_MAJOR = 9;
-const uint32_t MYSQL_SCHEMA_VERSION_MINOR = 6;
+const uint32_t MYSQL_SCHEMA_VERSION_MAJOR = 10;
+const uint32_t MYSQL_SCHEMA_VERSION_MINOR = 0;
 
 //@}
 
index c37ee1a4c1f6dcc4a23859045c97247deea21b7a..32f032936c5ea70ab931158a433e998ea62a02fe 100644 (file)
@@ -17,4 +17,5 @@
 /upgrade_9.3_to_9.4.sh
 /upgrade_9.4_to_9.5.sh
 /upgrade_9.5_to_9.6.sh
+/upgrade_9.6_to_10.0.sh
 /wipe_data.sh
index 846f468202169ec870b4f93a3f04627d64011926..a88bb9fe2d05e8ac016e68474df5cba80a965983 100644 (file)
@@ -28,6 +28,7 @@ mysql_SCRIPTS += upgrade_9.2_to_9.3.sh
 mysql_SCRIPTS += upgrade_9.3_to_9.4.sh
 mysql_SCRIPTS += upgrade_9.4_to_9.5.sh
 mysql_SCRIPTS += upgrade_9.5_to_9.6.sh
+mysql_SCRIPTS += upgrade_9.6_to_10.0.sh
 mysql_SCRIPTS += wipe_data.sh
 
 DISTCLEANFILES = ${mysql_SCRIPTS}
index b7b9173c32967846e236f1c21781259e6bf66939..4ecb9572094c046618f9ed938c2a9c4f8939b3d3 100644 (file)
@@ -3087,6 +3087,866 @@ UPDATE schema_version
 
 # This line concludes database upgrade to version 9.6.
 
+-- -----------------------------------------------------------------------
+-- Create a table holding the DHCPv4 client classes. Most table
+-- columns map directly to respective client class properties in
+-- Kea configuration. The  depend_on_known_directly column is
+-- explicitly set in an insert or update statement to indicate
+-- if the client class directly depends on KNOWN or UNKNOWN
+-- built-in classes. A caller should determine it by evaluating
+-- a test expression before inserting or updating the client
+-- class in the database. The nullable follow_class_name column
+-- can be used for positioning the inserted or updated client
+-- class within the class hierarchy. Set this column value to
+-- an existing class name, after which this class should be
+-- placed in the class hierarchy. See dhcp4_client_class_order
+-- description for the details of how classes are ordered.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp4_client_class (
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    name VARCHAR(128) NOT NULL,
+    test TEXT,
+    next_server INT UNSIGNED DEFAULT NULL,
+    server_hostname VARCHAR(128) DEFAULT NULL,
+    boot_file_name VARCHAR(512) DEFAULT NULL,
+    only_if_required TINYINT NOT NULL DEFAULT '0',
+    valid_lifetime INT DEFAULT NULL,
+    min_valid_lifetime INT DEFAULT NULL,
+    max_valid_lifetime INT DEFAULT NULL,
+    depend_on_known_directly TINYINT NOT NULL DEFAULT '0',
+    follow_class_name VARCHAR(128) DEFAULT NULL,
+    modification_ts TIMESTAMP NOT NULL,
+    PRIMARY KEY (id),
+    UNIQUE KEY id_UNIQUE (id),
+    UNIQUE KEY name_UNIQUE (name),
+    KEY key_dhcp4_client_class_modification_ts (modification_ts)
+) ENGINE=InnoDB;
+
+-- -----------------------------------------------------------------------
+-- Create a table for ordering client classes and holding information
+-- about indirect dependencies on KNOWN/UKNOWN built-in client classes.
+-- Each class in the dhcp4_client_class table has a corresponding row
+-- in the dhcp4_client_class_order table. A caller should not modify
+-- the contents of this table. Its entries are automatically created
+-- upon inserting or updating client classes in the dhcp4_client_classes
+-- using triggers. The order_index designates the position of the client
+-- class within the class hierarchy. If the follow_class_name value of
+-- the dhcp4_client_class table is set to NULL, the client class is
+-- appended at the end of the hierarchy. The assigned order_index
+-- value for that class is set to a maximum current value + 1.
+-- If the follow_client_class specifies a name of an existing class,
+-- the generated order_index is set to an id of that class + 1, and
+-- the order_index values of the later classes are incremented by 1.
+-- The depend_on_known_directly column holds a boolean value indicating
+-- whether the given class depends on KNOWN/UKNOWN built-in classes
+-- via other classes, i.e. it depends on classes that directly or
+-- indirectly depend on these built-ins. This value is auto-generated
+-- by a trigger on the dhcp4_client_class_dependency table.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp4_client_class_order (
+    class_id BIGINT UNSIGNED NOT NULL,
+    order_index BIGINT UNSIGNED NOT NULL,
+    depend_on_known_indirectly TINYINT NOT NULL DEFAULT '0',
+    PRIMARY KEY (class_id),
+    KEY key_dhcp4_client_class_order_index (order_index),
+    CONSTRAINT fk_dhcp4_client_class_order_class_id FOREIGN KEY (class_id) REFERENCES dhcp4_client_class (id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+DROP TRIGGER IF EXISTS dhcp4_client_class_AINS;
+DROP TRIGGER IF EXISTS dhcp4_client_class_AUPD;
+DROP TRIGGER IF EXISTS dhcp4_client_class_ADEL;
+DROP PROCEDURE IF EXISTS setClientClass4Order;
+
+-- -----------------------------------------------------------------------
+-- 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.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE setClientClass4Order(IN id BIGINT UNSIGNED,
+                                      IN follow_class_name VARCHAR(128))
+BEGIN
+    -- This variable will be optionally set if the follow_class_name
+    -- column value is specified.
+    DECLARE follow_class_index BIGINT UNSIGNED;
+    DECLARE msg TEXT;
+    IF follow_class_name IS NOT NULL THEN
+        -- Get the position of the class after which the new class should be added.
+        SET follow_class_index = (
+            SELECT o.order_index FROM dhcp4_client_class AS c
+                INNER JOIN dhcp4_client_class_order AS o
+                    ON c.id = o.class_id
+            WHERE name = follow_class_name
+        );
+        IF follow_class_index IS NULL THEN
+            -- The class with a name specified with follow_class_name does
+            -- not exist.
+            SET msg = CONCAT('Class ', follow_class_name, ' does not exist.');
+            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
+        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
+            ORDER BY order_index DESC;
+        END IF;
+    ELSE
+        -- A caller did not specify the follow_class_name value. Let's append the
+        -- new class at the end of the hierarchy.
+        SET follow_class_index = (SELECT MAX(order_index) FROM dhcp4_client_class_order);
+        IF follow_class_index IS NULL THEN
+            -- Apparently, there are no classes. Let's start from 0.
+            SET follow_class_index = 0;
+        END IF;
+    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.
+    SET @depend_on_known_indirectly = (
+        SELECT depend_on_known_indirectly FROM dhcp4_client_class_order WHERE id = class_id
+    );
+    REPLACE INTO dhcp4_client_class_order(class_id, order_index, depend_on_known_indirectly)
+        VALUES (id, follow_class_index + 1, 0);
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to position an inserted class within the class hierarchy
+-- and create audit.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_AINS AFTER INSERT ON dhcp4_client_class FOR EACH ROW BEGIN
+    CALL setClientClass4Order(NEW.id, NEW.follow_class_name);
+    CALL createAuditEntryDHCP4('dhcp4_client_class', NEW.id, "create");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to position an updated class within the class hierarchy,
+-- create audit and remember the direct dependency on the
+-- KNOWN/UNKNOWN built-in classes before the class update.
+-- When updating a client class, it is very important to ensure that
+-- its dependency on KNOWN or UNKNOWN built-in client classes is not
+-- changed. It is because there may be other classes that depend on
+-- these built-ins via this class. Changing the dependency would break
+-- the chain of dependencies for other classes. Here, we store the
+-- information about the dependency in the session variables. Their
+-- values will be compared with the new dependencies after an update.
+-- If they change, an error will be signaled.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_AUPD AFTER UPDATE ON dhcp4_client_class FOR EACH ROW BEGIN
+    SET @depend_on_known_directly = OLD.depend_on_known_directly;
+    SET @client_class_id = NEW.id;
+    CALL setClientClass4Order(NEW.id, NEW.follow_class_name);
+    CALL createAuditEntryDHCP4('dhcp4_client_class', NEW.id, "update");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to create dhcp4_client_class audit.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_ADEL AFTER DELETE ON dhcp4_client_class FOR EACH ROW BEGIN
+    CALL createAuditEntryDHCP4('dhcp4_client_class', OLD.id, "delete");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Create a table associating client classes stored in the
+-- dhcp4_client_class table with their dependencies. There is
+-- an M:N relationship between these tables. Each class may have
+-- many dependencies (created using member operator in test expression),
+-- and each class may be a dependency for many other classes. A caller
+-- is responsible for inserting dependencies for a class after inserting
+-- or updating it in the dhcp4_client_class table. A caller should
+-- delete all existing dependencies for an updated client class, evaluate
+-- test expression to discover new dependencies (in case test expression
+-- has changed), and insert new dependencies to this table.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp4_client_class_dependency (
+    class_id BIGINT UNSIGNED NOT NULL,
+    dependency_id BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (class_id,dependency_id),
+    KEY dhcp4_client_class_dependency_id_idx (dependency_id),
+    CONSTRAINT dhcp4_client_class_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp4_client_class (id) ON DELETE CASCADE,
+    CONSTRAINT dhcp4_client_class_dependency_id FOREIGN KEY (dependency_id)
+        REFERENCES dhcp4_client_class (id)
+) ENGINE=InnoDB;
+
+DROP TRIGGER IF EXISTS dhcp4_client_class_dependency_BINS;
+DROP PROCEDURE IF EXISTS checkDHCPv4ClientClassDependency;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure 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.
+--
+-- Parameters:
+-- - class_id id client class,
+-- - dependency_id id of the dependency.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE checkDHCPv4ClientClassDependency(IN class_id BIGINT UNSIGNED,
+                                                  IN dependency_id BIGINT UNSIGNED)
+BEGIN
+    DECLARE class_index BIGINT UNSIGNED;
+    DECLARE dependency_index BIGINT UNSIGNED;
+    DECLARE err_msg TEXT;
+
+    -- We could check the same with a constraint but later in this
+    -- trigger we use this value to verify if the dependencies are
+    -- met.
+    IF class_id IS NULL THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Client class id must not be NULL.';
+    END IF;
+    IF dependency_id IS NULL THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Class dependency id must not be NULL.';
+    END IF;
+    -- Dependencies on self make no sense.
+    IF class_id = dependency_id THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Client class must not have dependency on self.';
+    END IF;
+    -- Check position of our class in the hierarchy.
+    SET class_index = (
+        SELECT o.order_index FROM dhcp4_client_class AS c
+            INNER JOIN dhcp4_client_class_order AS o
+                ON c.id = o.class_id
+        WHERE c.id = class_id);
+    IF class_index IS NULL THEN
+        SET err_msg = CONCAT('Client class with id ', class_id, ' does not exist.');
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+    -- Check position of the dependency.
+    SET dependency_index = (
+        SELECT o.order_index FROM dhcp4_client_class AS c
+            INNER JOIN dhcp4_client_class_order AS o ON c.id = o.class_id
+        WHERE c.id = dependency_id
+    );
+    IF dependency_index IS NULL THEN
+        SET err_msg = CONCAT('Client class with id ', dependency_id, ' does not exist.');
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+    -- The dependency must not be later than our class.
+    IF dependency_index > class_index THEN
+        SET err_msg = CONCAT('Client class with id ', class_id, ' must not depend on class defined later with id ', dependency_id);
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+
+    -- Check if all servers associated with the new class have dependent
+    -- classes configured. This catches the cases that class A belongs to
+    -- server1 and depends on class B which belongs only to server 2.
+    -- It is fine if the class B belongs to all servers in this case.
+    -- Make a SELECT on the dhcp4_client_class_server table to gather
+    -- all servers to which the class belongs. LEFT JOIN it with the
+    -- same table, selecting all records matching the dependency class
+    -- and the servers to which the new class belongs. If there are
+    -- any NULL records joined it implies that some dependencies are
+    -- not met (didn't find a dependency for at least one server).
+    IF EXISTS(
+        SELECT 1 FROM dhcp4_client_class_server AS t1
+            LEFT JOIN dhcp4_client_class_server AS t2
+                ON t2.class_id = dependency_id AND (t2.server_id = 1 OR t2.server_id = t1.server_id)
+        WHERE t1.class_id = class_id AND t2.server_id IS NULL
+        LIMIT 1
+    ) THEN
+        SET err_msg = CONCAT('Unmet dependencies for client class with id ', class_id);
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- 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.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_check_dependency_BINS BEFORE INSERT ON dhcp4_client_class_dependency FOR EACH ROW
+BEGIN
+    CALL checkDHCPv4ClientClassDependency(NEW.class_id, NEW.dependency_id);
+END $$
+DELIMITER ;
+
+DROP TRIGGER IF EXISTS dhcp4_client_class_dependency_AINS;
+DROP PROCEDURE IF EXISTS updateDHCPv4ClientClassKnownDependency;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure setting client class indirect dependency on KNOWN or
+-- UNKNOWN built-in classes by checking this flag for the client classes
+-- on which it depends.
+--
+-- Parameters:
+-- - client_class_id id of the client class which dependency is set,
+-- - dependency_id id of the client class on which the given class depends.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE updateDHCPv4ClientClassKnownDependency(IN client_class_id BIGINT UNSIGNED,
+                                                        IN dependency_id BIGINT UNSIGNED)
+BEGIN
+    DECLARE dependency TINYINT;
+    -- Check if the dependency class references KNOWN/UNKNOWN.
+    SET dependency = (
+        SELECT depend_on_known_directly FROM dhcp4_client_class
+        WHERE id = dependency_id
+    );
+    -- If it doesn't, check if the dependency references KNOWN/UNKNOWN
+    -- indirectly (via other classes).
+    IF dependency = 0 THEN
+        SET dependency = (
+            SELECT depend_on_known_indirectly FROM dhcp4_client_class_order
+            WHERE class_id = dependency_id
+        );
+    END IF;
+    IF dependency <> 0 THEN
+       UPDATE dhcp4_client_class_order
+           SET depend_on_known_indirectly = 1
+       WHERE class_id = client_class_id;
+     END IF;
+ END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger setting client class indirect dependency on KNOWN or UNKNOWN
+-- built-in classes by checking this flag for the client classes on which
+-- it depends.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_dependency_AINS AFTER INSERT ON dhcp4_client_class_dependency FOR EACH ROW
+BEGIN
+    CALL updateDHCPv4ClientClassKnownDependency(NEW.class_id, NEW.dependency_id);
+END $$
+DELIMITER ;
+
+DROP PROCEDURE IF EXISTS checkDHCPv4ClientClassKnownDependencyChange;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure to be executed before committing a transaction
+-- updating a DHCPv4 client class. It verifies if the class dependency on
+-- KNOWN or UNKNOWN built-in classes has changed as a a result of the
+-- update. It signals an error if it has changed and there is at least
+-- one class depending on this class.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE checkDHCPv4ClientClassKnownDependencyChange()
+BEGIN
+    DECLARE depended TINYINT DEFAULT 0;
+    DECLARE depends TINYINT DEFAULT 0;
+
+    -- Session variables are set upon a client class update.
+    IF @client_class_id IS NOT NULL THEN
+        -- Check if any of the classes depend on this class. If not,
+        -- it is ok to change the dependency on KNOWN/UNKNOWN.
+        IF EXISTS(
+            SELECT 1 FROM dhcp4_client_class_dependency
+            WHERE dependency_id = @client_class_id LIMIT 1
+        ) THEN
+            -- Using the session variables, determine whether the client class
+            -- depended on KNOWN/UNKNOWN before the update.
+            IF @depend_on_known_directly <> 0 OR @depend_on_known_indirectly <> 0 THEN
+                SET depended = 1;
+            END IF;
+            -- Check if the client class depends on KNOWN/UNKNOWN after the update.
+            SET depends = (
+                SELECT depend_on_known_directly FROM dhcp4_client_class
+                WHERE id = @client_class_id
+            );
+            -- If it doesn't depend directly, check indirect dependencies.
+            IF depends = 0 THEN
+                SET depends = (
+                    SELECT depend_on_known_indirectly FROM dhcp4_client_class_order
+                    WHERE class_id = @client_class_id
+                );
+            END IF;
+            -- The resulting dependency on KNOWN/UNKNOWN must not change.
+            IF depended <> depends THEN
+                SIGNAL SQLSTATE '45000'
+                    SET MESSAGE_TEXT = 'Class dependency on KNOWN/UNKNOWN built-in classes must not change.';
+            END IF;
+        END IF;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Create table matching DHCPv4 classes with the servers.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp4_client_class_server (
+    class_id bigint unsigned NOT NULL,
+    server_id bigint unsigned NOT NULL,
+    modification_ts timestamp NULL DEFAULT NULL,
+    PRIMARY KEY (class_id,server_id),
+    KEY fk_dhcp4_client_class_server_id (server_id),
+    CONSTRAINT fk_dhcp4_client_class_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp4_client_class (id)
+        ON DELETE CASCADE,
+    CONSTRAINT fk_dhcp4_client_class_server_id FOREIGN KEY (server_id)
+        REFERENCES dhcp4_server (id)
+) ENGINE=InnoDB;
+
+-- -----------------------------------------------------------------------
+-- Extend the table holding DHCPv4 option definitions with a nullable
+-- column matching option defintions with client classes.
+-- -----------------------------------------------------------------------
+ALTER TABLE dhcp4_option_def
+    ADD COLUMN class_id BIGINT UNSIGNED NULL DEFAULT NULL;
+
+ALTER TABLE dhcp4_option_def
+  ADD CONSTRAINT fk_dhcp4_option_def_client_class_id
+    FOREIGN KEY (class_id)
+    REFERENCES dhcp4_client_class (id)
+    ON DELETE CASCADE
+    ON UPDATE CASCADE;
+
+-- -----------------------------------------------------------------------
+-- Create a table holding the DHCPv6 client classes. Most table
+-- columns map directly to respective client class properties in
+-- Kea configuration. The  depend_on_known_directly column is
+-- explicitly set in an insert or update statement to indicate
+-- if the client class directly depends on KNOWN or UNKNOWN
+-- built-in classes. A caller should determine it by evaluating
+-- a test expression before inserting or updating the client
+-- class in the database. The nullable follow_class_name column
+-- can be used for positioning the inserted or updated client
+-- class within the class hierarchy. Set this column value to
+-- an existing class name, after which this class should be
+-- placed in the class hierarchy. See dhcp6_client_class_order
+-- description for the details of how classes are ordered.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp6_client_class (
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    name VARCHAR(128) NOT NULL,
+    test TEXT,
+    only_if_required TINYINT NOT NULL DEFAULT '0',
+    valid_lifetime INT DEFAULT NULL,
+    min_valid_lifetime INT DEFAULT NULL,
+    max_valid_lifetime INT DEFAULT NULL,
+    depend_on_known_directly TINYINT NOT NULL DEFAULT '0',
+    follow_class_name VARCHAR(128) DEFAULT NULL,
+    modification_ts TIMESTAMP NOT NULL,
+    PRIMARY KEY (id),
+    UNIQUE KEY id_UNIQUE (id),
+    UNIQUE KEY name_UNIQUE (name),
+    KEY key_dhcp6_client_class_modification_ts (modification_ts)
+) ENGINE=InnoDB;
+
+-- -----------------------------------------------------------------------
+-- Create a table for ordering client classes and holding information
+-- about indirect dependencies on KNOWN/UKNOWN built-in client classes.
+-- Each class in the dhcp6_client_class table has a corresponding row
+-- in the dhcp6_client_class_order table. A caller should not modify
+-- the contents of this table. Its entries are automatically created
+-- upon inserting or updating client classes in the dhcp6_client_classes
+-- using triggers. The order_index designates the position of the client
+-- class within the class hierarchy. If the follow_class_name value of
+-- the dhcp6_client_class table is set to NULL, the client class is
+-- appended at the end of the hierarchy. The assigned order_index
+-- value for that class is set to a maximum current value + 1.
+-- If the follow_client_class specifies a name of an existing class,
+-- the generated order_index is set to an id of that class + 1, and
+-- the order_index values of the later classes are incremented by 1.
+-- The depend_on_known_directly column holds a boolean value indicating
+-- whether the given class depends on KNOWN/UKNOWN built-in classes
+-- via other classes, i.e. it depends on classes that directly or
+-- indirectly depend on these built-ins. This value is auto-generated
+-- by a trigger on the dhcp6_client_class_dependency table.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp6_client_class_order (
+    class_id BIGINT UNSIGNED NOT NULL,
+    order_index BIGINT UNSIGNED NOT NULL,
+    depend_on_known_indirectly TINYINT NOT NULL DEFAULT '0',
+    PRIMARY KEY (class_id),
+    KEY key_dhcp6_client_class_order_index (order_index),
+    CONSTRAINT fk_dhcp6_client_class_order_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp6_client_class (id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+DROP TRIGGER IF EXISTS dhcp6_client_class_AINS;
+DROP TRIGGER IF EXISTS dhcp6_client_class_AUPD;
+DROP TRIGGER IF EXISTS dhcp6_client_class_ADEL;
+DROP PROCEDURE IF EXISTS setClientClass6Order;
+
+-- -----------------------------------------------------------------------
+-- 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.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE setClientClass6Order(IN id BIGINT UNSIGNED,
+                                      IN follow_class_name VARCHAR(128))
+BEGIN
+    -- This variable will be optionally set if the follow_class_name
+    -- column value is specified.
+    DECLARE follow_class_index BIGINT UNSIGNED;
+    DECLARE msg TEXT;
+    IF follow_class_name IS NOT NULL THEN
+        -- Get the position of the class after which the new class should be added.
+        SET follow_class_index = (
+            SELECT o.order_index FROM dhcp6_client_class AS c
+                INNER JOIN dhcp6_client_class_order AS o
+                    ON c.id = o.class_id
+            WHERE name = follow_class_name
+        );
+        IF follow_class_index IS NULL THEN
+            -- The class with a name specified with follow_class_name does
+            -- not exist.
+            SET msg = CONCAT('Class ', follow_class_name, ' does not exist.');
+            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
+        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
+            ORDER BY order_index DESC;
+        END IF;
+    ELSE
+        -- A caller did not specify the follow_class_name value. Let's append the
+        -- new class at the end of the hierarchy.
+        SET follow_class_index = (SELECT MAX(order_index) FROM dhcp6_client_class_order);
+        IF follow_class_index IS NULL THEN
+            -- Apparently, there are no classes. Let's start from 0.
+            SET follow_class_index = 0;
+        END IF;
+    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.
+    SET @depend_on_known_indirectly = (
+        SELECT depend_on_known_indirectly FROM dhcp6_client_class_order WHERE id = class_id
+    );
+    REPLACE INTO dhcp6_client_class_order(class_id, order_index, depend_on_known_indirectly)
+        VALUES (id, follow_class_index + 1, 0);
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to position an inserted class within the class hierarchy
+-- and create audit.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_AINS AFTER INSERT ON dhcp6_client_class FOR EACH ROW BEGIN
+    CALL setClientClass6Order(NEW.id, NEW.follow_class_name);
+    CALL createAuditEntryDHCP6('dhcp6_client_class', NEW.id, "create");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to position an updated class within the class hierarchy,
+-- create audit and remember the direct dependency on the
+-- KNOWN/UNKNOWN built-in classes before the class update.
+-- When updating a client class, it is very important to ensure that
+-- its dependency on KNOWN or UNKNOWN built-in client classes is not
+-- changed. It is because there may be other classes that depend on
+-- these built-ins via this class. Changing the dependency would break
+-- the chain of dependencies for other classes. Here, we store the
+-- information about the dependency in the session variables. Their
+-- values will be compared with the new dependencies after an update.
+-- If they change, an error will be signaled.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_AUPD AFTER UPDATE ON dhcp6_client_class FOR EACH ROW BEGIN
+    SET @depend_on_known_directly = OLD.depend_on_known_directly;
+    SET @client_class_id = NEW.id;
+    CALL setClientClass6Order(NEW.id, NEW.follow_class_name);
+    CALL createAuditEntryDHCP6('dhcp6_client_class', NEW.id, "update");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to create dhcp6_client_class audit.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_ADEL AFTER DELETE ON dhcp6_client_class FOR EACH ROW BEGIN
+    CALL createAuditEntryDHCP6('dhcp6_client_class', OLD.id, "delete");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Create a table associating client classes stored in the
+-- dhcp6_client_class table with their dependencies. There is
+-- an M:N relationship between these tables. Each class may have
+-- many dependencies (created using member operator in test expression),
+-- and each class may be a dependency for many other classes. A caller
+-- is responsible for inserting dependencies for a class after inserting
+-- or updating it in the dhcp6_client_class table. A caller should
+-- delete all existing dependencies for an updated client class, evaluate
+-- test expression to discover new dependencies (in case test expression
+-- has changed), and insert new dependencies to this table.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp6_client_class_dependency (
+    class_id BIGINT UNSIGNED NOT NULL,
+    dependency_id BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (class_id,dependency_id),
+    KEY dhcp6_client_class_dependency_id_idx (dependency_id),
+    CONSTRAINT dhcp6_client_class_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp6_client_class (id) ON DELETE CASCADE,
+    CONSTRAINT dhcp6_client_class_dependency_id FOREIGN KEY (dependency_id)
+        REFERENCES dhcp6_client_class (id)
+) ENGINE=InnoDB;
+
+DROP TRIGGER IF EXISTS dhcp6_client_class_dependency_BINS;
+DROP PROCEDURE IF EXISTS checkDHCPv6ClientClassDependency;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure 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.
+--
+-- Parameters:
+-- - class_id id client class,
+-- - dependency_id id of the dependency.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE checkDHCPv6ClientClassDependency(IN class_id BIGINT UNSIGNED,
+                                                  IN dependency_id BIGINT UNSIGNED)
+BEGIN
+    DECLARE class_index BIGINT UNSIGNED;
+    DECLARE dependency_index BIGINT UNSIGNED;
+    DECLARE err_msg TEXT;
+
+    -- We could check the same with a constraint but later in this
+    -- trigger we use this value to verify if the dependencies are
+    -- met.
+    IF class_id IS NULL THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Client class id must not be NULL.';
+    END IF;
+    IF dependency_id IS NULL THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Class dependency id must not be NULL.';
+    END IF;
+    -- Dependencies on self make no sense.
+    IF class_id = dependency_id THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Client class must not have dependency on self.';
+    END IF;
+    -- Check position of our class in the hierarchy.
+    SET class_index = (
+        SELECT o.order_index FROM dhcp6_client_class AS c
+            INNER JOIN dhcp6_client_class_order AS o
+                ON c.id = o.class_id
+        WHERE c.id = class_id);
+    IF class_index IS NULL THEN
+        SET err_msg = CONCAT('Client class with id ', class_id, ' does not exist.');
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+    -- Check position of the dependency.
+    SET dependency_index = (
+        SELECT o.order_index FROM dhcp6_client_class AS c
+            INNER JOIN dhcp6_client_class_order AS o ON c.id = o.class_id
+        WHERE c.id = dependency_id
+    );
+    IF dependency_index IS NULL THEN
+        SET err_msg = CONCAT('Client class with id ', dependency_id, ' does not exist.');
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+    -- The dependency must not be later than our class.
+    IF dependency_index > class_index THEN
+        SET err_msg = CONCAT('Client class with id ', class_id, ' must not depend on class defined later with id ', dependency_id);
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+
+    -- Check if all servers associated with the new class have dependent
+    -- classes configured. This catches the cases that class A belongs to
+    -- server1 and depends on class B which belongs only to server 2.
+    -- It is fine if the class B belongs to all servers in this case.
+    -- Make a SELECT on the dhcp6_client_class_server table to gather
+    -- all servers to which the class belongs. LEFT JOIN it with the
+    -- same table, selecting all records matching the dependency class
+    -- and the servers to which the new class belongs. If there are
+    -- any NULL records joined it implies that some dependencies are
+    -- not met (didn't find a dependency for at least one server).
+    IF EXISTS(
+        SELECT 1 FROM dhcp6_client_class_server AS t1
+            LEFT JOIN dhcp6_client_class_server AS t2
+                ON t2.class_id = dependency_id AND (t2.server_id = 1 OR t2.server_id = t1.server_id)
+        WHERE t1.class_id = class_id AND t2.server_id IS NULL
+        LIMIT 1
+    ) THEN
+        SET err_msg = CONCAT('Unmet dependencies for client class with id ', class_id);
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- 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.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_check_dependency_BINS BEFORE INSERT ON dhcp6_client_class_dependency FOR EACH ROW
+BEGIN
+    CALL checkDHCPv6ClientClassDependency(NEW.class_id, NEW.dependency_id);
+END $$
+DELIMITER ;
+
+DROP TRIGGER IF EXISTS dhcp6_client_class_dependency_AINS;
+DROP PROCEDURE IF EXISTS updateDHCPv6ClientClassKnownDependency;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure setting client class indirect dependency on KNOWN or
+-- UNKNOWN built-in classes by checking this flag for the client classes
+-- on which it depends.
+--
+-- Parameters:
+-- - client_class_id id of the client class which dependency is set,
+-- - dependency_id id of the client class on which the given class depends.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE updateDHCPv6ClientClassKnownDependency(IN client_class_id BIGINT UNSIGNED,
+                                                        IN dependency_id BIGINT UNSIGNED)
+BEGIN
+    DECLARE dependency TINYINT;
+    -- Check if the dependency class references KNOWN/UNKNOWN.
+    SET dependency = (
+        SELECT depend_on_known_directly FROM dhcp6_client_class
+        WHERE id = dependency_id
+    );
+    -- If it doesn't, check if the dependency references KNOWN/UNKNOWN
+    -- indirectly (via other classes).
+    IF dependency = 0 THEN
+        SET dependency = (
+            SELECT depend_on_known_indirectly FROM dhcp6_client_class_order
+            WHERE class_id = dependency_id
+        );
+    END IF;
+    IF dependency <> 0 THEN
+       UPDATE dhcp6_client_class_order
+           SET depend_on_known_indirectly = 1
+       WHERE class_id = client_class_id;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger setting client class indirect dependency on KNOWN or UNKNOWN
+-- built-in classes by checking this flag for the client classes on which
+-- it depends.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_dependency_AINS AFTER INSERT ON dhcp6_client_class_dependency FOR EACH ROW
+BEGIN
+    CALL updateDHCPv6ClientClassKnownDependency(NEW.class_id, NEW.dependency_id);
+END $$
+DELIMITER ;
+
+DROP PROCEDURE IF EXISTS checkDHCPv6ClientClassKnownDependencyChange;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure to be executed before committing a transaction
+-- updating a DHCPv6 client class. It verifies if the class dependency on
+-- KNOWN or UNKNOWN built-in classes has changed as a a result of the
+-- update. It signals an error if it has changed and there is at least
+-- one class depending on this class.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE checkDHCPv6ClientClassKnownDependencyChange()
+BEGIN
+    DECLARE depended TINYINT DEFAULT 0;
+    DECLARE depends TINYINT DEFAULT 0;
+
+    -- Session variables are set upon a client class update.
+    IF @client_class_id IS NOT NULL THEN
+        -- Check if any of the classes depend on this class. If not,
+        -- it is ok to change the dependency on KNOWN/UNKNOWN.
+        IF EXISTS(
+            SELECT 1 FROM dhcp6_client_class_dependency
+            WHERE dependency_id = @client_class_id LIMIT 1
+        ) THEN
+            -- Using the session variables, determine whether the client class
+            -- depended on KNOWN/UNKNOWN before the update.
+            IF @depend_on_known_directly <> 0 OR @depend_on_known_indirectly <> 0 THEN
+                SET depended = 1;
+            END IF;
+            -- Check if the client class depends on KNOWN/UNKNOWN after the update.
+            SET depends = (
+                SELECT depend_on_known_directly FROM dhcp6_client_class
+                WHERE id = @client_class_id
+            );
+            -- If it doesn't depend directly, check indirect dependencies.
+            IF depends = 0 THEN
+                SET depends = (
+                    SELECT depend_on_known_indirectly FROM dhcp6_client_class_order
+                    WHERE class_id = @client_class_id
+                );
+            END IF;
+            -- The resulting dependency on KNOWN/UNKNOWN must not change.
+            IF depended <> depends THEN
+                SIGNAL SQLSTATE '45000'
+                    SET MESSAGE_TEXT = 'Class dependency on KNOWN/UNKNOWN built-in classes must not change.';
+            END IF;
+        END IF;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Create table matching DHCPv6 classes with the servers.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp6_client_class_server (
+    class_id bigint unsigned NOT NULL,
+    server_id bigint unsigned NOT NULL,
+    modification_ts timestamp NULL DEFAULT NULL,
+    PRIMARY KEY (class_id,server_id),
+    KEY fk_dhcp6_client_class_server_id (server_id),
+    CONSTRAINT fk_dhcp6_client_class_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp6_client_class (id)
+        ON DELETE CASCADE,
+    CONSTRAINT fk_dhcp6_client_class_server_id FOREIGN KEY (server_id)
+        REFERENCES dhcp6_server (id)
+) ENGINE=InnoDB;
+
+-- -----------------------------------------------------------------------
+-- Extend the table holding DHCPv6 option definitions with a nullable
+-- column matching option defintions with client classes.
+-- -----------------------------------------------------------------------
+ALTER TABLE dhcp6_option_def
+    ADD COLUMN class_id BIGINT UNSIGNED NULL DEFAULT NULL;
+
+ALTER TABLE dhcp6_option_def
+  ADD CONSTRAINT fk_dhcp6_option_def_client_class_id
+    FOREIGN KEY (class_id)
+    REFERENCES dhcp6_client_class (id)
+    ON DELETE CASCADE
+    ON UPDATE CASCADE;
+
+# Update the schema version number
+UPDATE schema_version
+    SET version = '10', minor = '0';
+
+# This line concludes database upgrade to version 10.
+
 # Notes:
 #
 # Indexes
index aaddab13b4ac46c9719a02a0c76a35eeaa1f81fa..35faadb8fab9b6bf886de69f27c45e30aa2b90de 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2016-2020 Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2016-2021 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
@@ -45,6 +45,10 @@ DROP TABLE IF EXISTS dhcp4_shared_network;
 DROP TABLE IF EXISTS dhcp4_shared_network_server;
 DROP TABLE IF EXISTS dhcp4_subnet;
 DROP TABLE IF EXISTS dhcp4_subnet_server;
+DROP TABLE IF EXISTS dhcp4_client_class;
+DROP TABLE IF EXISTS dhcp4_client_class_order;
+DROP TABLE IF EXISTS dhcp4_client_class_dependency;
+DROP TABLE IF EXISTS dhcp4_client_class_server;
 DROP TABLE IF EXISTS dhcp6_audit;
 DROP TABLE IF EXISTS dhcp6_global_parameter;
 DROP TABLE IF EXISTS dhcp6_global_parameter_server;
@@ -58,11 +62,19 @@ DROP TABLE IF EXISTS dhcp6_shared_network;
 DROP TABLE IF EXISTS dhcp6_shared_network_server;
 DROP TABLE IF EXISTS dhcp6_subnet;
 DROP TABLE IF EXISTS dhcp6_subnet_server;
+DROP TABLE IF EXISTS dhcp6_client_class;
+DROP TABLE IF EXISTS dhcp6_client_class_order;
+DROP TABLE IF EXISTS dhcp6_client_class_dependency;
+DROP TABLE IF EXISTS dhcp6_client_class_server;
 DROP TABLE IF EXISTS modification;
 DROP TABLE IF EXISTS parameter_data_type;
 DROP PROCEDURE IF EXISTS createAuditRevisionDHCP4;
 DROP PROCEDURE IF EXISTS createAuditEntryDHCP4;
 DROP PROCEDURE IF EXISTS createOptionAuditDHCP4;
+DROP PROCEDURE IF EXISTS setClientClass4Order;
+DROP PROCEDURE IF EXISTS checkDHCPv4ClientClassDependency;
+DROP PROCEDURE IF EXISTS updateDHCPv4ClientClassKnownDependency;
+DROP PROCEDURE IF EXISTS checkDHCPv4ClientClassKnownDependencyChange;
 DROP TRIGGER IF EXISTS dhcp4_global_parameter_AINS;
 DROP TRIGGER IF EXISTS dhcp4_global_parameter_AUPD;
 DROP TRIGGER IF EXISTS dhcp4_global_parameter_ADEL;
@@ -78,10 +90,19 @@ DROP TRIGGER IF EXISTS dhcp4_option_def_ADEL;
 DROP TRIGGER IF EXISTS dhcp4_options_AINS;
 DROP TRIGGER IF EXISTS dhcp4_options_AUPD;
 DROP TRIGGER IF EXISTS dhcp4_options_ADEL;
+DROP TRIGGER IF EXISTS dhcp4_client_class_AINS;
+DROP TRIGGER IF EXISTS dhcp4_client_class_AUPD;
+DROP TRIGGER IF EXISTS dhcp4_client_class_ADEL;
+DROP TRIGGER IF EXISTS dhcp4_client_class_dependency_BINS;
+DROP TRIGGER IF EXISTS dhcp4_client_class_dependency_AINS;
 DROP TABLE IF EXISTS dhcp6_audit_revision;
 DROP PROCEDURE IF EXISTS createAuditRevisionDHCP6;
 DROP PROCEDURE IF EXISTS createAuditEntryDHCP6;
 DROP PROCEDURE IF EXISTS createOptionAuditDHCP6;
+DROP PROCEDURE IF EXISTS setClientClass6Order;
+DROP PROCEDURE IF EXISTS checkDHCPv6ClientClassDependency;
+DROP PROCEDURE IF EXISTS updateDHCPv6ClientClassKnownDependency;
+DROP PROCEDURE IF EXISTS checkDHCPv6ClientClassKnownDependencyChange;
 DROP TRIGGER IF EXISTS dhcp6_global_parameter_AINS;
 DROP TRIGGER IF EXISTS dhcp6_global_parameter_AUPD;
 DROP TRIGGER IF EXISTS dhcp6_global_parameter_ADEL;
@@ -97,3 +118,8 @@ DROP TRIGGER IF EXISTS dhcp6_option_def_ADEL;
 DROP TRIGGER IF EXISTS dhcp6_options_AINS;
 DROP TRIGGER IF EXISTS dhcp6_options_AUPD;
 DROP TRIGGER IF EXISTS dhcp6_options_ADEL;
+DROP TRIGGER IF EXISTS dhcp6_client_class_AINS;
+DROP TRIGGER IF EXISTS dhcp6_client_class_AUPD;
+DROP TRIGGER IF EXISTS dhcp6_client_class_ADEL;
+DROP TRIGGER IF EXISTS dhcp6_client_class_dependency_BINS;
+DROP TRIGGER IF EXISTS dhcp6_client_class_dependency_AINS;
diff --git a/src/share/database/scripts/mysql/upgrade_9.6_to_10.0.sh.in b/src/share/database/scripts/mysql/upgrade_9.6_to_10.0.sh.in
new file mode 100644 (file)
index 0000000..030ebfc
--- /dev/null
@@ -0,0 +1,897 @@
+#!/bin/sh
+
+# Copyright (C) 2021 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
+
+# Check version.
+version=$(mysql_version "${@}")
+if test "${version}" != "9.6"; then
+    printf 'This script upgrades 9.5 to 9.6. '
+    printf 'Reported version is %s. Skipping upgrade.\n' "${version}"
+    exit 0
+fi
+
+mysql "$@" <<EOF
+
+-- -----------------------------------------------------------------------
+-- Create a table holding the DHCPv4 client classes. Most table
+-- columns map directly to respective client class properties in
+-- Kea configuration. The  depend_on_known_directly column is
+-- explicitly set in an insert or update statement to indicate
+-- if the client class directly depends on KNOWN or UNKNOWN
+-- built-in classes. A caller should determine it by evaluating
+-- a test expression before inserting or updating the client
+-- class in the database. The nullable follow_class_name column
+-- can be used for positioning the inserted or updated client
+-- class within the class hierarchy. Set this column value to
+-- an existing class name, after which this class should be
+-- placed in the class hierarchy. See dhcp4_client_class_order
+-- description for the details of how classes are ordered.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp4_client_class (
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    name VARCHAR(128) NOT NULL,
+    test TEXT,
+    next_server INT UNSIGNED DEFAULT NULL,
+    server_hostname VARCHAR(128) DEFAULT NULL,
+    boot_file_name VARCHAR(512) DEFAULT NULL,
+    only_if_required TINYINT NOT NULL DEFAULT '0',
+    valid_lifetime INT DEFAULT NULL,
+    min_valid_lifetime INT DEFAULT NULL,
+    max_valid_lifetime INT DEFAULT NULL,
+    depend_on_known_directly TINYINT NOT NULL DEFAULT '0',
+    follow_class_name VARCHAR(128) DEFAULT NULL,
+    modification_ts TIMESTAMP NOT NULL,
+    PRIMARY KEY (id),
+    UNIQUE KEY id_UNIQUE (id),
+    UNIQUE KEY name_UNIQUE (name),
+    KEY key_dhcp4_client_class_modification_ts (modification_ts)
+) ENGINE=InnoDB;
+
+-- -----------------------------------------------------------------------
+-- Create a table for ordering client classes and holding information
+-- about indirect dependencies on KNOWN/UKNOWN built-in client classes.
+-- Each class in the dhcp4_client_class table has a corresponding row
+-- in the dhcp4_client_class_order table. A caller should not modify
+-- the contents of this table. Its entries are automatically created
+-- upon inserting or updating client classes in the dhcp4_client_classes
+-- using triggers. The order_index designates the position of the client
+-- class within the class hierarchy. If the follow_class_name value of
+-- the dhcp4_client_class table is set to NULL, the client class is
+-- appended at the end of the hierarchy. The assigned order_index
+-- value for that class is set to a maximum current value + 1.
+-- If the follow_client_class specifies a name of an existing class,
+-- the generated order_index is set to an id of that class + 1, and
+-- the order_index values of the later classes are incremented by 1.
+-- The depend_on_known_directly column holds a boolean value indicating
+-- whether the given class depends on KNOWN/UKNOWN built-in classes
+-- via other classes, i.e. it depends on classes that directly or
+-- indirectly depend on these built-ins. This value is auto-generated
+-- by a trigger on the dhcp4_client_class_dependency table.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp4_client_class_order (
+    class_id BIGINT UNSIGNED NOT NULL,
+    order_index BIGINT UNSIGNED NOT NULL,
+    depend_on_known_indirectly TINYINT NOT NULL DEFAULT '0',
+    PRIMARY KEY (class_id),
+    KEY key_dhcp4_client_class_order_index (order_index),
+    CONSTRAINT fk_dhcp4_client_class_order_class_id FOREIGN KEY (class_id) REFERENCES dhcp4_client_class (id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+DROP TRIGGER IF EXISTS dhcp4_client_class_AINS;
+DROP TRIGGER IF EXISTS dhcp4_client_class_AUPD;
+DROP TRIGGER IF EXISTS dhcp4_client_class_ADEL;
+DROP PROCEDURE IF EXISTS setClientClass4Order;
+
+-- -----------------------------------------------------------------------
+-- 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.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE setClientClass4Order(IN id BIGINT UNSIGNED,
+                                      IN follow_class_name VARCHAR(128))
+BEGIN
+    -- This variable will be optionally set if the follow_class_name
+    -- column value is specified.
+    DECLARE follow_class_index BIGINT UNSIGNED;
+    DECLARE msg TEXT;
+    IF follow_class_name IS NOT NULL THEN
+        -- Get the position of the class after which the new class should be added.
+        SET follow_class_index = (
+            SELECT o.order_index FROM dhcp4_client_class AS c
+                INNER JOIN dhcp4_client_class_order AS o
+                    ON c.id = o.class_id
+            WHERE name = follow_class_name
+        );
+        IF follow_class_index IS NULL THEN
+            -- The class with a name specified with follow_class_name does
+            -- not exist.
+            SET msg = CONCAT('Class ', follow_class_name, ' does not exist.');
+            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
+        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
+            ORDER BY order_index DESC;
+        END IF;
+    ELSE
+        -- A caller did not specify the follow_class_name value. Let's append the
+        -- new class at the end of the hierarchy.
+        SET follow_class_index = (SELECT MAX(order_index) FROM dhcp4_client_class_order);
+        IF follow_class_index IS NULL THEN
+            -- Apparently, there are no classes. Let's start from 0.
+            SET follow_class_index = 0;
+        END IF;
+    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.
+    SET @depend_on_known_indirectly = (
+        SELECT depend_on_known_indirectly FROM dhcp4_client_class_order WHERE id = class_id
+    );
+    REPLACE INTO dhcp4_client_class_order(class_id, order_index, depend_on_known_indirectly)
+        VALUES (id, follow_class_index + 1, 0);
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to position an inserted class within the class hierarchy
+-- and create audit.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_AINS AFTER INSERT ON dhcp4_client_class FOR EACH ROW BEGIN
+    CALL setClientClass4Order(NEW.id, NEW.follow_class_name);
+    CALL createAuditEntryDHCP4('dhcp4_client_class', NEW.id, "create");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to position an updated class within the class hierarchy,
+-- create audit and remember the direct dependency on the
+-- KNOWN/UNKNOWN built-in classes before the class update.
+-- When updating a client class, it is very important to ensure that
+-- its dependency on KNOWN or UNKNOWN built-in client classes is not
+-- changed. It is because there may be other classes that depend on
+-- these built-ins via this class. Changing the dependency would break
+-- the chain of dependencies for other classes. Here, we store the
+-- information about the dependency in the session variables. Their
+-- values will be compared with the new dependencies after an update.
+-- If they change, an error will be signaled.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_AUPD AFTER UPDATE ON dhcp4_client_class FOR EACH ROW BEGIN
+    SET @depend_on_known_directly = OLD.depend_on_known_directly;
+    SET @client_class_id = NEW.id;
+    CALL setClientClass4Order(NEW.id, NEW.follow_class_name);
+    CALL createAuditEntryDHCP4('dhcp4_client_class', NEW.id, "update");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to create dhcp4_client_class audit.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_ADEL AFTER DELETE ON dhcp4_client_class FOR EACH ROW BEGIN
+    CALL createAuditEntryDHCP4('dhcp4_client_class', OLD.id, "delete");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Create a table associating client classes stored in the
+-- dhcp4_client_class table with their dependencies. There is
+-- an M:N relationship between these tables. Each class may have
+-- many dependencies (created using member operator in test expression),
+-- and each class may be a dependency for many other classes. A caller
+-- is responsible for inserting dependencies for a class after inserting
+-- or updating it in the dhcp4_client_class table. A caller should
+-- delete all existing dependencies for an updated client class, evaluate
+-- test expression to discover new dependencies (in case test expression
+-- has changed), and insert new dependencies to this table.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp4_client_class_dependency (
+    class_id BIGINT UNSIGNED NOT NULL,
+    dependency_id BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (class_id,dependency_id),
+    KEY dhcp4_client_class_dependency_id_idx (dependency_id),
+    CONSTRAINT dhcp4_client_class_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp4_client_class (id) ON DELETE CASCADE,
+    CONSTRAINT dhcp4_client_class_dependency_id FOREIGN KEY (dependency_id)
+        REFERENCES dhcp4_client_class (id)
+) ENGINE=InnoDB;
+
+DROP TRIGGER IF EXISTS dhcp4_client_class_dependency_BINS;
+DROP PROCEDURE IF EXISTS checkDHCPv4ClientClassDependency;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure 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.
+--
+-- Parameters:
+-- - class_id id client class,
+-- - dependency_id id of the dependency.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE checkDHCPv4ClientClassDependency(IN class_id BIGINT UNSIGNED,
+                                                  IN dependency_id BIGINT UNSIGNED)
+BEGIN
+    DECLARE class_index BIGINT UNSIGNED;
+    DECLARE dependency_index BIGINT UNSIGNED;
+    DECLARE err_msg TEXT;
+
+    -- We could check the same with a constraint but later in this
+    -- trigger we use this value to verify if the dependencies are
+    -- met.
+    IF class_id IS NULL THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Client class id must not be NULL.';
+    END IF;
+    IF dependency_id IS NULL THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Class dependency id must not be NULL.';
+    END IF;
+    -- Dependencies on self make no sense.
+    IF class_id = dependency_id THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Client class must not have dependency on self.';
+    END IF;
+    -- Check position of our class in the hierarchy.
+    SET class_index = (
+        SELECT o.order_index FROM dhcp4_client_class AS c
+            INNER JOIN dhcp4_client_class_order AS o
+                ON c.id = o.class_id
+        WHERE c.id = class_id);
+    IF class_index IS NULL THEN
+        SET err_msg = CONCAT('Client class with id ', class_id, ' does not exist.');
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+    -- Check position of the dependency.
+    SET dependency_index = (
+        SELECT o.order_index FROM dhcp4_client_class AS c
+            INNER JOIN dhcp4_client_class_order AS o ON c.id = o.class_id
+        WHERE c.id = dependency_id
+    );
+    IF dependency_index IS NULL THEN
+        SET err_msg = CONCAT('Client class with id ', dependency_id, ' does not exist.');
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+    -- The dependency must not be later than our class.
+    IF dependency_index > class_index THEN
+        SET err_msg = CONCAT('Client class with id ', class_id, ' must not depend on class defined later with id ', dependency_id);
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+
+    -- Check if all servers associated with the new class have dependent
+    -- classes configured. This catches the cases that class A belongs to
+    -- server1 and depends on class B which belongs only to server 2.
+    -- It is fine if the class B belongs to all servers in this case.
+    -- Make a SELECT on the dhcp4_client_class_server table to gather
+    -- all servers to which the class belongs. LEFT JOIN it with the
+    -- same table, selecting all records matching the dependency class
+    -- and the servers to which the new class belongs. If there are
+    -- any NULL records joined it implies that some dependencies are
+    -- not met (didn't find a dependency for at least one server).
+    IF EXISTS(
+        SELECT 1 FROM dhcp4_client_class_server AS t1
+            LEFT JOIN dhcp4_client_class_server AS t2
+                ON t2.class_id = dependency_id AND (t2.server_id = 1 OR t2.server_id = t1.server_id)
+        WHERE t1.class_id = class_id AND t2.server_id IS NULL
+        LIMIT 1
+    ) THEN
+        SET err_msg = CONCAT('Unmet dependencies for client class with id ', class_id);
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- 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.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_check_dependency_BINS BEFORE INSERT ON dhcp4_client_class_dependency FOR EACH ROW
+BEGIN
+    CALL checkDHCPv4ClientClassDependency(NEW.class_id, NEW.dependency_id);
+END $$
+DELIMITER ;
+
+DROP TRIGGER IF EXISTS dhcp4_client_class_dependency_AINS;
+DROP PROCEDURE IF EXISTS updateDHCPv4ClientClassKnownDependency;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure setting client class indirect dependency on KNOWN or
+-- UNKNOWN built-in classes by checking this flag for the client classes
+-- on which it depends.
+--
+-- Parameters:
+-- - client_class_id id of the client class which dependency is set,
+-- - dependency_id id of the client class on which the given class depends.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE updateDHCPv4ClientClassKnownDependency(IN client_class_id BIGINT UNSIGNED,
+                                                        IN dependency_id BIGINT UNSIGNED)
+BEGIN
+    DECLARE dependency TINYINT;
+    -- Check if the dependency class references KNOWN/UNKNOWN.
+    SET dependency = (
+        SELECT depend_on_known_directly FROM dhcp4_client_class
+        WHERE id = dependency_id
+    );
+    -- If it doesn't, check if the dependency references KNOWN/UNKNOWN
+    -- indirectly (via other classes).
+    IF dependency = 0 THEN
+        SET dependency = (
+            SELECT depend_on_known_indirectly FROM dhcp4_client_class_order
+            WHERE class_id = dependency_id
+        );
+    END IF;
+    IF dependency <> 0 THEN
+       UPDATE dhcp4_client_class_order
+           SET depend_on_known_indirectly = 1
+       WHERE class_id = client_class_id;
+     END IF;
+ END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger setting client class indirect dependency on KNOWN or UNKNOWN
+-- built-in classes by checking this flag for the client classes on which
+-- it depends.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp4_client_class_dependency_AINS AFTER INSERT ON dhcp4_client_class_dependency FOR EACH ROW
+BEGIN
+    CALL updateDHCPv4ClientClassKnownDependency(NEW.class_id, NEW.dependency_id);
+END $$
+DELIMITER ;
+
+DROP PROCEDURE IF EXISTS checkDHCPv4ClientClassKnownDependencyChange;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure to be executed before committing a transaction
+-- updating a DHCPv4 client class. It verifies if the class dependency on
+-- KNOWN or UNKNOWN built-in classes has changed as a a result of the
+-- update. It signals an error if it has changed and there is at least
+-- one class depending on this class.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE checkDHCPv4ClientClassKnownDependencyChange()
+BEGIN
+    DECLARE depended TINYINT DEFAULT 0;
+    DECLARE depends TINYINT DEFAULT 0;
+
+    -- Session variables are set upon a client class update.
+    IF @client_class_id IS NOT NULL THEN
+        -- Check if any of the classes depend on this class. If not,
+        -- it is ok to change the dependency on KNOWN/UNKNOWN.
+        IF EXISTS(
+            SELECT 1 FROM dhcp4_client_class_dependency
+            WHERE dependency_id = @client_class_id LIMIT 1
+        ) THEN
+            -- Using the session variables, determine whether the client class
+            -- depended on KNOWN/UNKNOWN before the update.
+            IF @depend_on_known_directly <> 0 OR @depend_on_known_indirectly <> 0 THEN
+                SET depended = 1;
+            END IF;
+            -- Check if the client class depends on KNOWN/UNKNOWN after the update.
+            SET depends = (
+                SELECT depend_on_known_directly FROM dhcp4_client_class
+                WHERE id = @client_class_id
+            );
+            -- If it doesn't depend directly, check indirect dependencies.
+            IF depends = 0 THEN
+                SET depends = (
+                    SELECT depend_on_known_indirectly FROM dhcp4_client_class_order
+                    WHERE class_id = @client_class_id
+                );
+            END IF;
+            -- The resulting dependency on KNOWN/UNKNOWN must not change.
+            IF depended <> depends THEN
+                SIGNAL SQLSTATE '45000'
+                    SET MESSAGE_TEXT = 'Class dependency on KNOWN/UNKNOWN built-in classes must not change.';
+            END IF;
+        END IF;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Create table matching DHCPv4 classes with the servers.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp4_client_class_server (
+    class_id bigint unsigned NOT NULL,
+    server_id bigint unsigned NOT NULL,
+    modification_ts timestamp NULL DEFAULT NULL,
+    PRIMARY KEY (class_id,server_id),
+    KEY fk_dhcp4_client_class_server_id (server_id),
+    CONSTRAINT fk_dhcp4_client_class_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp4_client_class (id)
+        ON DELETE CASCADE,
+    CONSTRAINT fk_dhcp4_client_class_server_id FOREIGN KEY (server_id)
+        REFERENCES dhcp4_server (id)
+) ENGINE=InnoDB;
+
+-- -----------------------------------------------------------------------
+-- Extend the table holding DHCPv4 option definitions with a nullable
+-- column matching option defintions with client classes.
+-- -----------------------------------------------------------------------
+ALTER TABLE dhcp4_option_def
+    ADD COLUMN class_id BIGINT UNSIGNED NULL DEFAULT NULL;
+
+ALTER TABLE dhcp4_option_def
+  ADD CONSTRAINT fk_dhcp4_option_def_client_class_id
+    FOREIGN KEY (class_id)
+    REFERENCES dhcp4_client_class (id)
+    ON DELETE CASCADE
+    ON UPDATE CASCADE;
+
+-- -----------------------------------------------------------------------
+-- Create a table holding the DHCPv6 client classes. Most table
+-- columns map directly to respective client class properties in
+-- Kea configuration. The  depend_on_known_directly column is
+-- explicitly set in an insert or update statement to indicate
+-- if the client class directly depends on KNOWN or UNKNOWN
+-- built-in classes. A caller should determine it by evaluating
+-- a test expression before inserting or updating the client
+-- class in the database. The nullable follow_class_name column
+-- can be used for positioning the inserted or updated client
+-- class within the class hierarchy. Set this column value to
+-- an existing class name, after which this class should be
+-- placed in the class hierarchy. See dhcp6_client_class_order
+-- description for the details of how classes are ordered.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp6_client_class (
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    name VARCHAR(128) NOT NULL,
+    test TEXT,
+    only_if_required TINYINT NOT NULL DEFAULT '0',
+    valid_lifetime INT DEFAULT NULL,
+    min_valid_lifetime INT DEFAULT NULL,
+    max_valid_lifetime INT DEFAULT NULL,
+    depend_on_known_directly TINYINT NOT NULL DEFAULT '0',
+    follow_class_name VARCHAR(128) DEFAULT NULL,
+    modification_ts TIMESTAMP NOT NULL,
+    PRIMARY KEY (id),
+    UNIQUE KEY id_UNIQUE (id),
+    UNIQUE KEY name_UNIQUE (name),
+    KEY key_dhcp6_client_class_modification_ts (modification_ts)
+) ENGINE=InnoDB;
+
+-- -----------------------------------------------------------------------
+-- Create a table for ordering client classes and holding information
+-- about indirect dependencies on KNOWN/UKNOWN built-in client classes.
+-- Each class in the dhcp6_client_class table has a corresponding row
+-- in the dhcp6_client_class_order table. A caller should not modify
+-- the contents of this table. Its entries are automatically created
+-- upon inserting or updating client classes in the dhcp6_client_classes
+-- using triggers. The order_index designates the position of the client
+-- class within the class hierarchy. If the follow_class_name value of
+-- the dhcp6_client_class table is set to NULL, the client class is
+-- appended at the end of the hierarchy. The assigned order_index
+-- value for that class is set to a maximum current value + 1.
+-- If the follow_client_class specifies a name of an existing class,
+-- the generated order_index is set to an id of that class + 1, and
+-- the order_index values of the later classes are incremented by 1.
+-- The depend_on_known_directly column holds a boolean value indicating
+-- whether the given class depends on KNOWN/UKNOWN built-in classes
+-- via other classes, i.e. it depends on classes that directly or
+-- indirectly depend on these built-ins. This value is auto-generated
+-- by a trigger on the dhcp6_client_class_dependency table.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp6_client_class_order (
+    class_id BIGINT UNSIGNED NOT NULL,
+    order_index BIGINT UNSIGNED NOT NULL,
+    depend_on_known_indirectly TINYINT NOT NULL DEFAULT '0',
+    PRIMARY KEY (class_id),
+    KEY key_dhcp6_client_class_order_index (order_index),
+    CONSTRAINT fk_dhcp6_client_class_order_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp6_client_class (id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
+DROP TRIGGER IF EXISTS dhcp6_client_class_AINS;
+DROP TRIGGER IF EXISTS dhcp6_client_class_AUPD;
+DROP TRIGGER IF EXISTS dhcp6_client_class_ADEL;
+DROP PROCEDURE IF EXISTS setClientClass6Order;
+
+-- -----------------------------------------------------------------------
+-- 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.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE setClientClass6Order(IN id BIGINT UNSIGNED,
+                                      IN follow_class_name VARCHAR(128))
+BEGIN
+    -- This variable will be optionally set if the follow_class_name
+    -- column value is specified.
+    DECLARE follow_class_index BIGINT UNSIGNED;
+    DECLARE msg TEXT;
+    IF follow_class_name IS NOT NULL THEN
+        -- Get the position of the class after which the new class should be added.
+        SET follow_class_index = (
+            SELECT o.order_index FROM dhcp6_client_class AS c
+                INNER JOIN dhcp6_client_class_order AS o
+                    ON c.id = o.class_id
+            WHERE name = follow_class_name
+        );
+        IF follow_class_index IS NULL THEN
+            -- The class with a name specified with follow_class_name does
+            -- not exist.
+            SET msg = CONCAT('Class ', follow_class_name, ' does not exist.');
+            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
+        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
+            ORDER BY order_index DESC;
+        END IF;
+    ELSE
+        -- A caller did not specify the follow_class_name value. Let's append the
+        -- new class at the end of the hierarchy.
+        SET follow_class_index = (SELECT MAX(order_index) FROM dhcp6_client_class_order);
+        IF follow_class_index IS NULL THEN
+            -- Apparently, there are no classes. Let's start from 0.
+            SET follow_class_index = 0;
+        END IF;
+    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.
+    SET @depend_on_known_indirectly = (
+        SELECT depend_on_known_indirectly FROM dhcp6_client_class_order WHERE id = class_id
+    );
+    REPLACE INTO dhcp6_client_class_order(class_id, order_index, depend_on_known_indirectly)
+        VALUES (id, follow_class_index + 1, 0);
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to position an inserted class within the class hierarchy
+-- and create audit.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_AINS AFTER INSERT ON dhcp6_client_class FOR EACH ROW BEGIN
+    CALL setClientClass6Order(NEW.id, NEW.follow_class_name);
+    CALL createAuditEntryDHCP6('dhcp6_client_class', NEW.id, "create");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to position an updated class within the class hierarchy,
+-- create audit and remember the direct dependency on the
+-- KNOWN/UNKNOWN built-in classes before the class update.
+-- When updating a client class, it is very important to ensure that
+-- its dependency on KNOWN or UNKNOWN built-in client classes is not
+-- changed. It is because there may be other classes that depend on
+-- these built-ins via this class. Changing the dependency would break
+-- the chain of dependencies for other classes. Here, we store the
+-- information about the dependency in the session variables. Their
+-- values will be compared with the new dependencies after an update.
+-- If they change, an error will be signaled.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_AUPD AFTER UPDATE ON dhcp6_client_class FOR EACH ROW BEGIN
+    SET @depend_on_known_directly = OLD.depend_on_known_directly;
+    SET @client_class_id = NEW.id;
+    CALL setClientClass6Order(NEW.id, NEW.follow_class_name);
+    CALL createAuditEntryDHCP6('dhcp6_client_class', NEW.id, "update");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger to create dhcp6_client_class audit.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_ADEL AFTER DELETE ON dhcp6_client_class FOR EACH ROW BEGIN
+    CALL createAuditEntryDHCP6('dhcp6_client_class', OLD.id, "delete");
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Create a table associating client classes stored in the
+-- dhcp6_client_class table with their dependencies. There is
+-- an M:N relationship between these tables. Each class may have
+-- many dependencies (created using member operator in test expression),
+-- and each class may be a dependency for many other classes. A caller
+-- is responsible for inserting dependencies for a class after inserting
+-- or updating it in the dhcp6_client_class table. A caller should
+-- delete all existing dependencies for an updated client class, evaluate
+-- test expression to discover new dependencies (in case test expression
+-- has changed), and insert new dependencies to this table.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp6_client_class_dependency (
+    class_id BIGINT UNSIGNED NOT NULL,
+    dependency_id BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (class_id,dependency_id),
+    KEY dhcp6_client_class_dependency_id_idx (dependency_id),
+    CONSTRAINT dhcp6_client_class_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp6_client_class (id) ON DELETE CASCADE,
+    CONSTRAINT dhcp6_client_class_dependency_id FOREIGN KEY (dependency_id)
+        REFERENCES dhcp6_client_class (id)
+) ENGINE=InnoDB;
+
+DROP TRIGGER IF EXISTS dhcp6_client_class_dependency_BINS;
+DROP PROCEDURE IF EXISTS checkDHCPv6ClientClassDependency;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure 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.
+--
+-- Parameters:
+-- - class_id id client class,
+-- - dependency_id id of the dependency.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE checkDHCPv6ClientClassDependency(IN class_id BIGINT UNSIGNED,
+                                                  IN dependency_id BIGINT UNSIGNED)
+BEGIN
+    DECLARE class_index BIGINT UNSIGNED;
+    DECLARE dependency_index BIGINT UNSIGNED;
+    DECLARE err_msg TEXT;
+
+    -- We could check the same with a constraint but later in this
+    -- trigger we use this value to verify if the dependencies are
+    -- met.
+    IF class_id IS NULL THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Client class id must not be NULL.';
+    END IF;
+    IF dependency_id IS NULL THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Class dependency id must not be NULL.';
+    END IF;
+    -- Dependencies on self make no sense.
+    IF class_id = dependency_id THEN
+        SIGNAL SQLSTATE '45000'
+            SET MESSAGE_TEXT = 'Client class must not have dependency on self.';
+    END IF;
+    -- Check position of our class in the hierarchy.
+    SET class_index = (
+        SELECT o.order_index FROM dhcp6_client_class AS c
+            INNER JOIN dhcp6_client_class_order AS o
+                ON c.id = o.class_id
+        WHERE c.id = class_id);
+    IF class_index IS NULL THEN
+        SET err_msg = CONCAT('Client class with id ', class_id, ' does not exist.');
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+    -- Check position of the dependency.
+    SET dependency_index = (
+        SELECT o.order_index FROM dhcp6_client_class AS c
+            INNER JOIN dhcp6_client_class_order AS o ON c.id = o.class_id
+        WHERE c.id = dependency_id
+    );
+    IF dependency_index IS NULL THEN
+        SET err_msg = CONCAT('Client class with id ', dependency_id, ' does not exist.');
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+    -- The dependency must not be later than our class.
+    IF dependency_index > class_index THEN
+        SET err_msg = CONCAT('Client class with id ', class_id, ' must not depend on class defined later with id ', dependency_id);
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+
+    -- Check if all servers associated with the new class have dependent
+    -- classes configured. This catches the cases that class A belongs to
+    -- server1 and depends on class B which belongs only to server 2.
+    -- It is fine if the class B belongs to all servers in this case.
+    -- Make a SELECT on the dhcp6_client_class_server table to gather
+    -- all servers to which the class belongs. LEFT JOIN it with the
+    -- same table, selecting all records matching the dependency class
+    -- and the servers to which the new class belongs. If there are
+    -- any NULL records joined it implies that some dependencies are
+    -- not met (didn't find a dependency for at least one server).
+    IF EXISTS(
+        SELECT 1 FROM dhcp6_client_class_server AS t1
+            LEFT JOIN dhcp6_client_class_server AS t2
+                ON t2.class_id = dependency_id AND (t2.server_id = 1 OR t2.server_id = t1.server_id)
+        WHERE t1.class_id = class_id AND t2.server_id IS NULL
+        LIMIT 1
+    ) THEN
+        SET err_msg = CONCAT('Unmet dependencies for client class with id ', class_id);
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = err_msg;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- 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.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_check_dependency_BINS BEFORE INSERT ON dhcp6_client_class_dependency FOR EACH ROW
+BEGIN
+    CALL checkDHCPv6ClientClassDependency(NEW.class_id, NEW.dependency_id);
+END $$
+DELIMITER ;
+
+DROP TRIGGER IF EXISTS dhcp6_client_class_dependency_AINS;
+DROP PROCEDURE IF EXISTS updateDHCPv6ClientClassKnownDependency;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure setting client class indirect dependency on KNOWN or
+-- UNKNOWN built-in classes by checking this flag for the client classes
+-- on which it depends.
+--
+-- Parameters:
+-- - client_class_id id of the client class which dependency is set,
+-- - dependency_id id of the client class on which the given class depends.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE updateDHCPv6ClientClassKnownDependency(IN client_class_id BIGINT UNSIGNED,
+                                                        IN dependency_id BIGINT UNSIGNED)
+BEGIN
+    DECLARE dependency TINYINT;
+    -- Check if the dependency class references KNOWN/UNKNOWN.
+    SET dependency = (
+        SELECT depend_on_known_directly FROM dhcp6_client_class
+        WHERE id = dependency_id
+    );
+    -- If it doesn't, check if the dependency references KNOWN/UNKNOWN
+    -- indirectly (via other classes).
+    IF dependency = 0 THEN
+        SET dependency = (
+            SELECT depend_on_known_indirectly FROM dhcp6_client_class_order
+            WHERE class_id = dependency_id
+        );
+    END IF;
+    IF dependency <> 0 THEN
+       UPDATE dhcp6_client_class_order
+           SET depend_on_known_indirectly = 1
+       WHERE class_id = client_class_id;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Trigger setting client class indirect dependency on KNOWN or UNKNOWN
+-- built-in classes by checking this flag for the client classes on which
+-- it depends.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE TRIGGER dhcp6_client_class_dependency_AINS AFTER INSERT ON dhcp6_client_class_dependency FOR EACH ROW
+BEGIN
+    CALL updateDHCPv6ClientClassKnownDependency(NEW.class_id, NEW.dependency_id);
+END $$
+DELIMITER ;
+
+DROP PROCEDURE IF EXISTS checkDHCPv6ClientClassKnownDependencyChange;
+
+-- -----------------------------------------------------------------------
+-- Stored procedure to be executed before committing a transaction
+-- updating a DHCPv6 client class. It verifies if the class dependency on
+-- KNOWN or UNKNOWN built-in classes has changed as a a result of the
+-- update. It signals an error if it has changed and there is at least
+-- one class depending on this class.
+-- -----------------------------------------------------------------------
+DELIMITER $$
+CREATE PROCEDURE checkDHCPv6ClientClassKnownDependencyChange()
+BEGIN
+    DECLARE depended TINYINT DEFAULT 0;
+    DECLARE depends TINYINT DEFAULT 0;
+
+    -- Session variables are set upon a client class update.
+    IF @client_class_id IS NOT NULL THEN
+        -- Check if any of the classes depend on this class. If not,
+        -- it is ok to change the dependency on KNOWN/UNKNOWN.
+        IF EXISTS(
+            SELECT 1 FROM dhcp6_client_class_dependency
+            WHERE dependency_id = @client_class_id LIMIT 1
+        ) THEN
+            -- Using the session variables, determine whether the client class
+            -- depended on KNOWN/UNKNOWN before the update.
+            IF @depend_on_known_directly <> 0 OR @depend_on_known_indirectly <> 0 THEN
+                SET depended = 1;
+            END IF;
+            -- Check if the client class depends on KNOWN/UNKNOWN after the update.
+            SET depends = (
+                SELECT depend_on_known_directly FROM dhcp6_client_class
+                WHERE id = @client_class_id
+            );
+            -- If it doesn't depend directly, check indirect dependencies.
+            IF depends = 0 THEN
+                SET depends = (
+                    SELECT depend_on_known_indirectly FROM dhcp6_client_class_order
+                    WHERE class_id = @client_class_id
+                );
+            END IF;
+            -- The resulting dependency on KNOWN/UNKNOWN must not change.
+            IF depended <> depends THEN
+                SIGNAL SQLSTATE '45000'
+                    SET MESSAGE_TEXT = 'Class dependency on KNOWN/UNKNOWN built-in classes must not change.';
+            END IF;
+        END IF;
+    END IF;
+END $$
+DELIMITER ;
+
+-- -----------------------------------------------------------------------
+-- Create table matching DHCPv6 classes with the servers.
+-- -----------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS dhcp6_client_class_server (
+    class_id bigint unsigned NOT NULL,
+    server_id bigint unsigned NOT NULL,
+    modification_ts timestamp NULL DEFAULT NULL,
+    PRIMARY KEY (class_id,server_id),
+    KEY fk_dhcp6_client_class_server_id (server_id),
+    CONSTRAINT fk_dhcp6_client_class_class_id FOREIGN KEY (class_id)
+        REFERENCES dhcp6_client_class (id)
+        ON DELETE CASCADE,
+    CONSTRAINT fk_dhcp6_client_class_server_id FOREIGN KEY (server_id)
+        REFERENCES dhcp6_server (id)
+) ENGINE=InnoDB;
+
+-- -----------------------------------------------------------------------
+-- Extend the table holding DHCPv6 option definitions with a nullable
+-- column matching option defintions with client classes.
+-- -----------------------------------------------------------------------
+ALTER TABLE dhcp6_option_def
+    ADD COLUMN class_id BIGINT UNSIGNED NULL DEFAULT NULL;
+
+ALTER TABLE dhcp6_option_def
+  ADD CONSTRAINT fk_dhcp6_option_def_client_class_id
+    FOREIGN KEY (class_id)
+    REFERENCES dhcp6_client_class (id)
+    ON DELETE CASCADE
+    ON UPDATE CASCADE;
+
+# Update the schema version number
+UPDATE schema_version
+    SET version = '10', minor = '0';
+
+# This line concludes database upgrade to version 10.
+EOF
index 551201748bdb17c29bcb57429123c2420732cca9..7f8163adabe674c0fdd861f22992d7dd006bedd5 100644 (file)
@@ -76,6 +76,10 @@ DELETE FROM dhcp4_subnet;
 DELETE FROM dhcp4_subnet_server;
 DELETE FROM dhcp4_audit_revision;
 DELETE FROM dhcp4_audit;
+DELETE FROM dhcp4_client_class;
+DELETE FROM dhcp4_client_class_order;
+DELETE FROM dhcp4_client_class_dependency;
+DELETE FROM dhcp4_client_class_server;
 DELETE FROM dhcp6_global_parameter;
 DELETE FROM dhcp6_global_parameter_server;
 DELETE FROM dhcp6_option_def;
@@ -92,6 +96,10 @@ DELETE FROM dhcp6_subnet;
 DELETE FROM dhcp6_subnet_server;
 DELETE FROM dhcp6_audit;
 DELETE FROM dhcp6_audit_revision;
+DELETE FROM dhcp6_client_class;
+DELETE FROM dhcp6_client_class_order;
+DELETE FROM dhcp6_client_class_dependency;
+DELETE FROM dhcp6_client_class_server;
 DELETE FROM hosts;
 DELETE FROM ipv6_reservations;
 DELETE FROM lease4;