]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#2445] PostgreSQL schema changes in support of lease limiting
authorAndrei Pavel <andrei@isc.org>
Mon, 20 Jun 2022 10:17:24 +0000 (13:17 +0300)
committerAndrei Pavel <andrei@isc.org>
Wed, 29 Jun 2022 12:51:27 +0000 (15:51 +0300)
.gitlab-ci.yml
configure.ac
src/lib/pgsql/pgsql_connection.h
src/share/database/scripts/mysql/dhcpdb_drop.mysql
src/share/database/scripts/pgsql/.gitignore
src/share/database/scripts/pgsql/Makefile.am
src/share/database/scripts/pgsql/dhcpdb_create.pgsql
src/share/database/scripts/pgsql/dhcpdb_drop.pgsql
src/share/database/scripts/pgsql/upgrade_011_to_012.sh.in
src/share/database/scripts/pgsql/upgrade_012_to_013.sh.in [new file with mode: 0644]
src/share/database/scripts/pgsql/wipe_data.sh.in

index 636b11f55a7e09bda1f1e369d241fd9db1fb9cb3..2183dfcca8e66e1f0e06531d1c1098348815921e 100644 (file)
@@ -76,6 +76,9 @@ shellcheck:
     - SCRIPTS+="src/share/database/scripts/mysql/upgrade_009.5_to_009.6.sh.in "
     - SCRIPTS+="src/share/database/scripts/mysql/upgrade_009.6_to_010.0.sh.in "
     - SCRIPTS+="src/share/database/scripts/mysql/upgrade_010_to_011.sh.in "
+    - SCRIPTS+="src/share/database/scripts/mysql/upgrade_011_to_012.sh.in "
+    - SCRIPTS+="src/share/database/scripts/mysql/upgrade_012_to_013.sh.in "
+    - SCRIPTS+="src/share/database/scripts/mysql/upgrade_013_to_014.sh.in "
     - SCRIPTS+="src/share/database/scripts/mysql/wipe_data.sh.in "
     - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_001.0_to_002.0.sh.in "
     - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_002.0_to_003.0.sh.in "
@@ -90,6 +93,11 @@ shellcheck:
     - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_006.1_to_006.2.sh.in "
     - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_006.2_to_007.0.sh.in "
     - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_007_to_008.sh.in "
+    - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_008_to_009.sh.in "
+    - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_009_to_010.sh.in "
+    - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_010_to_011.sh.in "
+    - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_011_to_012.sh.in "
+    - SCRIPTS+="src/share/database/scripts/pgsql/upgrade_012_to_013.sh.in "
     - SCRIPTS+="src/share/database/scripts/pgsql/wipe_data.sh.in "
     - SCRIPTS+="src/share/yang/modules/utils/check-hashes.sh "
     - SCRIPTS+="src/share/yang/modules/utils/check-revisions.sh "
index bc64d9fa32fd28cf7d8ece38f2e99625d1855f42..226d5ed445f3b2f53cdb37bbc56b613a44a9d713 100644 (file)
@@ -1670,6 +1670,8 @@ AC_CONFIG_FILES([src/share/database/scripts/pgsql/upgrade_010_to_011.sh],
                 [chmod +x src/share/database/scripts/pgsql/upgrade_010_to_011.sh])
 AC_CONFIG_FILES([src/share/database/scripts/pgsql/upgrade_011_to_012.sh],
                 [chmod +x src/share/database/scripts/pgsql/upgrade_011_to_012.sh])
+AC_CONFIG_FILES([src/share/database/scripts/pgsql/upgrade_012_to_013.sh],
+                [chmod +x src/share/database/scripts/pgsql/upgrade_012_to_013.sh])
 AC_CONFIG_FILES([src/share/database/scripts/pgsql/wipe_data.sh],
                 [chmod +x src/share/database/scripts/pgsql/wipe_data.sh])
 AC_CONFIG_FILES([src/share/yang/Makefile])
index edd1d8b54dd81821b2d3e260fdf1976754ac2726..65e8e42ada20fc151868adc69fc6ff820e03858f 100644 (file)
@@ -17,8 +17,8 @@
 namespace isc {
 namespace db {
 
-/// @brief Define PostgreSQL backend version: 12.0
-const uint32_t PGSQL_SCHEMA_VERSION_MAJOR = 12;
+/// @brief Define the PostgreSQL backend version.
+const uint32_t PGSQL_SCHEMA_VERSION_MAJOR = 13;
 const uint32_t PGSQL_SCHEMA_VERSION_MINOR = 0;
 
 // Maximum number of parameters that can be used a statement
index 41c90e402e4d58dfd8a496bb90b112287c556da6..0d1789078481262d38a730504c6560ffde360148 100644 (file)
@@ -149,4 +149,5 @@ DROP TRIGGER IF EXISTS lease6_AINS;
 DROP TRIGGER IF EXISTS lease6_AUPD;
 DROP TRIGGER IF EXISTS lease6_ADEL;
 DROP FUNCTION IF EXISTS checkLease4Limits;
-DROP FUNCTION IF EXISTS checkLease6Limits;
\ No newline at end of file
+DROP FUNCTION IF EXISTS checkLease6Limits;
+DROP FUNCTION IF EXISTS isJsonSupported;
index d2e50568ca398c41aab4fa3a04131653c3c718da..96744d8d6aae9937cfd22dba6add22d0ddd28b97 100644 (file)
@@ -15,4 +15,5 @@
 /upgrade_009_to_010.sh
 /upgrade_010_to_011.sh
 /upgrade_011_to_012.sh
+/upgrade_012_to_013.sh
 /wipe_data.sh
index e0b8ea565e87ebff7ab9ded4e3e52645ee2ce99b..f9c20961e453085144ab86b643d2a2f0d03b7cfe 100644 (file)
@@ -26,6 +26,7 @@ pgsql_SCRIPTS += upgrade_008_to_009.sh
 pgsql_SCRIPTS += upgrade_009_to_010.sh
 pgsql_SCRIPTS += upgrade_010_to_011.sh
 pgsql_SCRIPTS += upgrade_011_to_012.sh
+pgsql_SCRIPTS += upgrade_012_to_013.sh
 pgsql_SCRIPTS += wipe_data.sh
 
 DISTCLEANFILES = ${pgsql_SCRIPTS}
index 7d992e70f91faa82565a3cafbb68f406520e61cf..c44a32e41502ab6ffba1c439c094b5bb2505bc66 100644 (file)
@@ -4987,6 +4987,621 @@ UPDATE schema_version
 
 -- This line concludes the schema upgrade to version 12.
 
+-- This line starts the schema upgrade to version 13.
+
+-- JSON functions --
+
+-- Helper function that avoids a casting error when the string
+-- presumed to be in JSON format, is empty.
+CREATE OR REPLACE FUNCTION json_cast(IN json_candidate TEXT)
+RETURNS JSON
+AS $$
+BEGIN
+    IF LENGTH(json_candidate) = 0 THEN
+        RETURN '{}'::json;
+    END IF;
+    RETURN json_candidate::json;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Function that establishes whether JSON functions are supported.
+-- They should be provided with PostgreSQL >= 9.4.
+CREATE OR REPLACE FUNCTION isJsonSupported()
+RETURNS BOOLEAN
+AS $$
+BEGIN
+    IF get_session_value('json_supported') IS NULL THEN
+        IF (SELECT proname FROM pg_proc WHERE proname = 'json_extract_path') = 'json_extract_path' THEN
+            PERFORM set_session_value('kea.json_supported', true);
+        ELSE
+            PERFORM set_session_value('kea.json_supported', false);
+        END IF;
+    END IF;
+    RETURN get_session_value('kea.json_supported');
+END
+$$ LANGUAGE plpgsql;
+
+-- Schema changes related to lease limiting start here. --
+
+-- Recreate the triggers that update the leaseX_stat tables as stored procedures. --
+
+CREATE OR REPLACE FUNCTION lease4_AINS_lease4_stat(IN new_state BIGINT,
+                                                   IN new_subnet_id BIGINT)
+RETURNS VOID
+AS $$
+BEGIN
+    IF new_state = 0 OR new_state = 1 THEN
+        -- Update the state count if it exists.
+        UPDATE lease4_stat SET leases = leases + 1
+            WHERE subnet_id = new_subnet_id AND state = new_state;
+
+        -- Insert the state count record if it does not exist.
+        IF NOT FOUND THEN
+            INSERT INTO lease4_stat VALUES (new_subnet_id, new_state, 1);
+        END IF;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease4_AUPD_lease4_stat(IN old_state BIGINT,
+                                                   IN old_subnet_id BIGINT,
+                                                   IN new_state BIGINT,
+                                                   IN new_subnet_id BIGINT)
+RETURNS VOID
+AS $$
+BEGIN
+    IF old_subnet_id != new_subnet_id OR old_state != new_state THEN
+        IF old_state = 0 OR old_state = 1 THEN
+            -- Decrement the old state count if record exists.
+            UPDATE lease4_stat
+                SET leases = GREATEST(leases - 1, 0)
+                WHERE subnet_id = old_subnet_id AND state = old_state;
+        END IF;
+
+        IF new_state = 0 OR new_state = 1 THEN
+            -- Increment the new state count if record exists.
+            UPDATE lease4_stat SET leases = leases + 1
+                WHERE subnet_id = new_subnet_id AND state = new_state;
+
+            -- Insert new state record if it does not exist.
+            IF NOT FOUND THEN
+                INSERT INTO lease4_stat VALUES (new_subnet_id, new_state, 1);
+            END IF;
+        END IF;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease4_ADEL_lease4_stat(IN old_state BIGINT,
+                                                   IN old_subnet_id BIGINT)
+RETURNS VOID
+AS $$
+BEGIN
+    IF old_state = 0 OR old_state = 1 THEN
+        -- Decrement the state count if record exists.
+        UPDATE lease4_stat
+            SET leases = GREATEST(leases - 1, 0)
+            WHERE subnet_id = old_subnet_id AND old_state = state;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_AINS_lease6_stat(IN new_state BIGINT,
+                                                   IN new_subnet_id BIGINT,
+                                                   IN new_lease_type BIGINT)
+RETURNS VOID
+AS $$
+BEGIN
+    IF new_state = 0 OR new_state = 1 THEN
+        -- Update the state count if it exists.
+        UPDATE lease6_stat SET leases = leases + 1
+            WHERE subnet_id = new_subnet_id AND lease_type = new_lease_type
+            AND state = new_state;
+
+        -- Insert the state count record if it does not exist.
+        IF NOT FOUND THEN
+            INSERT INTO lease6_stat VALUES (new_subnet_id, new_lease_type, new_state, 1);
+        END IF;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_AUPD_lease6_stat(IN old_state BIGINT,
+                                                   IN old_subnet_id BIGINT,
+                                                   IN old_lease_type BIGINT,
+                                                   IN new_state BIGINT,
+                                                   IN new_subnet_id BIGINT,
+                                                   IN new_lease_type BIGINT)
+RETURNS VOID
+AS $$
+BEGIN
+    IF old_subnet_id != new_subnet_id OR
+       old_lease_type != new_lease_type OR
+       old_state != new_state THEN
+        IF old_state = 0 OR old_state = 1 THEN
+            -- Decrement the old state count if record exists.
+            UPDATE lease6_stat
+                SET leases = GREATEST(leases - 1, 0)
+                WHERE subnet_id = old_subnet_id AND lease_type = old_lease_type
+                AND state = old_state;
+        END IF;
+
+        IF new_state = 0 OR new_state = 1 THEN
+            -- Increment the new state count if record exists
+            UPDATE lease6_stat SET leases = leases + 1
+                WHERE subnet_id = new_subnet_id AND lease_type = new_lease_type
+                AND state = new_state;
+
+            -- Insert new state record if it does not exist
+            IF NOT FOUND THEN
+                INSERT INTO lease6_stat
+                VALUES (new_subnet_id, new_lease_type, new_state, 1);
+            END IF;
+        END IF;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_ADEL_lease6_stat(IN old_state BIGINT,
+                                                   IN old_subnet_id BIGINT,
+                                                   IN old_lease_type BIGINT)
+RETURNS VOID
+AS $$
+BEGIN
+    IF old_state = 0 OR old_state = 1 THEN
+        -- Decrement the state count if record exists
+        UPDATE lease6_stat
+            SET leases = GREATEST(leases - 1, 0)
+            WHERE subnet_id = old_subnet_id AND lease_type = old_lease_type
+            AND state = old_state;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Create tables that contain the number of active leases. --
+
+CREATE TABLE lease4_stat_by_client_class (
+    client_class VARCHAR(128) NOT NULL PRIMARY KEY,
+    leases BIGINT NOT NULL
+);
+
+CREATE TABLE lease6_stat_by_client_class (
+    client_class VARCHAR(128) NOT NULL,
+    lease_type BIGINT NOT NULL,
+    leases BIGINT NOT NULL,
+    PRIMARY KEY (client_class, lease_type),
+    CONSTRAINT fk_lease6_stat_by_client_class_lease_type FOREIGN KEY (lease_type)
+        REFERENCES lease6_types (lease_type)
+);
+
+-- Create procedures to be called for each row in after-event triggers for
+-- INSERT, UPDATE and DELETE on lease tables.
+
+CREATE OR REPLACE FUNCTION lease4_AINS_lease4_stat_by_client_class(IN new_state BIGINT,
+                                                                   IN new_user_context TEXT)
+RETURNS VOID
+AS $$
+DECLARE
+    class VARCHAR(128);
+BEGIN
+    -- Only state 0 is needed for lease limiting.
+    IF new_state = 0 THEN
+        -- Dive into client classes.
+        FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(new_user_context)->'ISC'->'client-classes') LOOP
+            SELECT TRIM('"' FROM class) INTO class;
+
+            -- Upsert to increment the lease count.
+            UPDATE lease4_stat_by_client_class SET leases = leases + 1
+                WHERE client_class = class;
+            IF NOT FOUND THEN
+                INSERT INTO lease4_stat_by_client_class VALUES (class, 1);
+            END IF;
+        END LOOP;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease4_AUPD_lease4_stat_by_client_class(IN old_state BIGINT,
+                                                                   IN old_user_context TEXT,
+                                                                   IN new_state BIGINT,
+                                                                   IN new_user_context TEXT)
+RETURNS VOID
+AS $$
+DECLARE
+    old_client_classes TEXT;
+    new_client_classes TEXT;
+    class VARCHAR(128);
+    length INT;
+    i INT;
+BEGIN
+    SELECT json_cast(old_user_context)->'ISC'->'client-classes' INTO old_client_classes;
+    SELECT json_cast(new_user_context)->'ISC'->'client-classes' INTO new_client_classes;
+
+    IF old_state != new_state OR old_client_classes != new_client_classes THEN
+        -- Check if it's moving away from a counted state.
+        IF old_state = 0 THEN
+            -- Dive into client classes.
+            FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(old_client_classes)) LOOP
+                -- Decrement the lease count if the record exists.
+                UPDATE lease4_stat_by_client_class SET leases = GREATEST(leases - 1, 0)
+                    WHERE client_class = class;
+            END LOOP;
+        END IF;
+
+        -- Check if it's moving into a counted state.
+        IF new_state = 0 THEN
+            -- Dive into client classes.
+            FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(new_client_classes)) LOOP
+                -- Upsert to increment the lease count.
+                UPDATE lease4_stat_by_client_class SET leases = leases + 1
+                    WHERE client_class = class;
+                IF NOT FOUND THEN
+                    INSERT INTO lease4_stat_by_client_class VALUES (class, 1);
+                END IF;
+            END LOOP;
+        END IF;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease4_ADEL_lease4_stat_by_client_class(IN old_state BIGINT,
+                                                                   IN old_user_context TEXT)
+RETURNS VOID
+AS $$
+DECLARE
+    class VARCHAR(128);
+BEGIN
+    -- Only state 0 is accounted for in lease limiting.
+    IF old_state = 0 THEN
+        -- Dive into client classes.
+        FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(old_user_context)->'ISC'->'client-classes') LOOP
+            SELECT TRIM('"' FROM class) INTO class;
+
+            -- Decrement the lease count if the record exists.
+            UPDATE lease4_stat_by_client_class SET leases = GREATEST(leases - 1, 0)
+                WHERE client_class = class;
+        END LOOP;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_AINS_lease6_stat_by_client_class(IN new_state BIGINT,
+                                                                   IN new_user_context TEXT,
+                                                                   IN new_lease_type BIGINT)
+RETURNS VOID
+AS $$
+DECLARE
+    client_classes TEXT;
+    class VARCHAR(128);
+    length INT;
+    i INT;
+BEGIN
+    -- Only state 0 is needed for lease limiting.
+    IF new_state = 0 THEN
+        -- Dive into client classes.
+        FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(new_user_context)->'ISC'->'client-classes') LOOP
+            SELECT TRIM('"' FROM class) INTO class;
+
+            -- Upsert to increment the lease count.
+            UPDATE lease6_stat_by_client_class SET leases = leases + 1
+                WHERE client_class = class AND lease_type = new_lease_type;
+            IF NOT FOUND THEN
+                INSERT INTO lease6_stat_by_client_class VALUES (class, new_lease_type, 1);
+            END IF;
+        END LOOP;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_AUPD_lease6_stat_by_client_class(IN old_state BIGINT,
+                                                                   IN old_user_context TEXT,
+                                                                   IN old_lease_type BIGINT,
+                                                                   IN new_state BIGINT,
+                                                                   IN new_user_context TEXT,
+                                                                   IN new_lease_type BIGINT)
+RETURNS VOID
+AS $$
+DECLARE
+    old_client_classes TEXT;
+    new_client_classes TEXT;
+    class VARCHAR(128);
+    length INT;
+    i INT;
+BEGIN
+    SELECT json_cast(old_user_context)->'ISC'->'client-classes' INTO old_client_classes;
+    SELECT json_cast(new_user_context)->'ISC'->'client-classes' INTO new_client_classes;
+
+    IF old_state != new_state OR old_client_classes != new_client_classes OR old_lease_type != new_lease_type THEN
+        -- Check if it's moving away from a counted state.
+        IF old_state = 0 THEN
+            -- Dive into client classes.
+            FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(old_client_classes)) LOOP
+                -- Decrement the lease count if the record exists.
+                UPDATE lease6_stat_by_client_class SET leases = GREATEST(leases - 1, 0)
+                    WHERE client_class = class AND lease_type = old_lease_type;
+            END LOOP;
+        END IF;
+
+        -- Check if it's moving into a counted state.
+        IF new_state = 0 THEN
+            -- Dive into client classes.
+            FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(new_client_classes)) LOOP
+                -- Upsert to increment the lease count.
+                UPDATE lease6_stat_by_client_class SET leases = leases + 1
+                    WHERE client_class = class AND lease_type = new_lease_type;
+                IF NOT FOUND THEN
+                    INSERT INTO lease6_stat_by_client_class VALUES (class, new_lease_type, 1);
+                END IF;
+            END LOOP;
+        END IF;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_ADEL_lease6_stat_by_client_class(IN old_state BIGINT,
+                                                                   IN old_user_context TEXT,
+                                                                   IN old_lease_type BIGINT)
+RETURNS VOID
+AS $$
+DECLARE
+    client_classes VARCHAR(1024);
+    class VARCHAR(128);
+    length INT;
+    i INT;
+BEGIN
+    -- Only state 0 is accounted for in lease limiting. But check both states to be consistent with lease6_stat.
+    IF old_state = 0 THEN
+        -- Dive into client classes.
+        FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(old_user_context)->'ISC'->'client-classes') LOOP
+            SELECT TRIM('"' FROM class) INTO class;
+
+            -- Decrement the lease count if the record exists.
+            UPDATE lease6_stat_by_client_class SET leases = GREATEST(leases - 1, 0)
+                WHERE client_class = class AND lease_type = old_lease_type;
+        END LOOP;
+    END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Recreate the after-event triggers for INSERT, UPDATE and DELETE on lease tables to call the --
+-- stored procedures above in pairs of two: for client classes and for subnets. --
+
+DROP TRIGGER IF EXISTS stat_lease4_insert ON lease4;
+
+CREATE OR REPLACE FUNCTION func_lease4_AINS()
+RETURNS trigger AS $lease4_AINS$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease4_AINS_lease4_stat_by_client_class(NEW.state, NEW.user_context);
+    END IF;
+    PERFORM lease4_AINS_lease4_stat(NEW.state, NEW.subnet_id);
+    RETURN NULL;
+END;
+$lease4_AINS$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease4_AINS AFTER INSERT ON lease4
+    FOR EACH ROW EXECUTE PROCEDURE func_lease4_AINS();
+
+DROP TRIGGER IF EXISTS stat_lease4_update ON lease4;
+
+CREATE OR REPLACE FUNCTION func_lease4_AUPD()
+RETURNS trigger AS $lease4_AUPD$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease4_AUPD_lease4_stat_by_client_class(OLD.state, OLD.user_context, NEW.state, NEW.user_context);
+    END IF;
+    PERFORM lease4_AUPD_lease4_stat(OLD.state, OLD.subnet_id, NEW.state, NEW.subnet_id);
+    RETURN NULL;
+END;
+$lease4_AUPD$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease4_AUPD AFTER UPDATE ON lease4
+    FOR EACH ROW EXECUTE PROCEDURE func_lease4_AUPD();
+
+DROP TRIGGER IF EXISTS stat_lease4_delete ON lease4;
+
+CREATE OR REPLACE FUNCTION func_lease4_ADEL()
+RETURNS trigger AS $lease4_ADEL$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease4_ADEL_lease4_stat_by_client_class(OLD.state, OLD.user_context);
+    END IF;
+    PERFORM lease4_ADEL_lease4_stat(OLD.state, OLD.subnet_id);
+    RETURN NULL;
+END;
+$lease4_ADEL$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease4_ADEL AFTER DELETE ON lease4
+    FOR EACH ROW EXECUTE PROCEDURE func_lease4_ADEL();
+
+DROP TRIGGER IF EXISTS stat_lease6_insert ON lease6;
+
+CREATE OR REPLACE FUNCTION func_lease6_AINS()
+RETURNS trigger AS $lease6_AINS$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease6_AINS_lease6_stat_by_client_class(NEW.state, NEW.user_context, NEW.lease_type);
+    END IF;
+    PERFORM lease6_AINS_lease6_stat(NEW.state, NEW.subnet_id, NEW.lease_type);
+    RETURN NULL;
+END;
+$lease6_AINS$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease6_AINS AFTER INSERT ON lease6
+    FOR EACH ROW EXECUTE PROCEDURE func_lease6_AINS();
+
+DROP TRIGGER IF EXISTS stat_lease6_update ON lease6;
+
+CREATE OR REPLACE FUNCTION func_lease6_AUPD()
+RETURNS trigger AS $lease6_AUPD$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease6_AUPD_lease6_stat_by_client_class(OLD.state, OLD.user_context, OLD.lease_type, NEW.state, NEW.user_context, NEW.lease_type);
+    END IF;
+    PERFORM lease6_AUPD_lease6_stat(OLD.state, OLD.subnet_id, OLD.lease_type, NEW.state, NEW.subnet_id, NEW.lease_type);
+    RETURN NULL;
+END;
+$lease6_AUPD$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease6_AUPD AFTER UPDATE ON lease6
+    FOR EACH ROW EXECUTE PROCEDURE func_lease6_AUPD();
+
+DROP TRIGGER IF EXISTS stat_lease6_delete ON lease6;
+
+CREATE OR REPLACE FUNCTION func_lease6_ADEL()
+RETURNS trigger AS $lease6_ADEL$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease6_ADEL_lease6_stat_by_client_class(OLD.state, OLD.user_context, OLD.lease_type);
+    END IF;
+    PERFORM lease6_ADEL_lease6_stat(OLD.state, OLD.subnet_id, OLD.lease_type);
+    RETURN NULL;
+END;
+$lease6_ADEL$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease6_ADEL AFTER DELETE ON lease6
+    FOR EACH ROW EXECUTE PROCEDURE func_lease6_ADEL();
+
+-- Create functions that return an empty TEXT if all limits allow for more leases, or otherwise a
+-- TEXT in one of the following JSON formats detailing the limit that was reached:
+-- { "limit-type": "client-class", "name": foo, "lease-type": "address", "limit": 2, "count": 2 }
+-- { "limit-type": "subnet", "id": 1, "lease-type": "IA_PD", "limit": 2, "count": 2 }
+-- The following format for user_context is assumed:
+-- { "ISC": { "limits": { "client-classes": [ { "name": "foo", "address-limit": 2, "prefix-limit": 1 } ],
+--                        "subnet": { "id": 1, "address-limit": 2, "prefix-limit": 1 } } } }
+
+CREATE OR REPLACE FUNCTION checkLease4Limits(user_context TEXT)
+RETURNS TEXT
+AS $$
+DECLARE
+    class TEXT;
+    name VARCHAR(255);
+    sid INT;
+    lease_limit INT;
+    lease_count INT;
+BEGIN
+    -- Dive into client class limits.
+    FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(user_context)->'ISC'->'limits'->'client-classes') LOOP
+        SELECT TRIM('"' FROM (json_cast(class)->'name')::text) INTO name;
+        SELECT json_cast(class)->'address-limit' INTO lease_limit;
+
+        IF lease_limit IS NOT NULL THEN
+            -- Get the lease count for this client class.
+            SELECT leases FROM lease4_stat_by_client_class INTO lease_count WHERE client_class = name;
+            IF lease_count IS NULL THEN
+                lease_count := 0;
+            END IF;
+
+            -- Compare. Return immediately if the limit is surpassed.
+            IF lease_limit <= lease_count THEN
+                RETURN CONCAT('address limit ', lease_limit, ' for client class "', name, '", current lease count ', lease_count);
+            END IF;
+        END IF;
+    END LOOP;
+
+    -- Dive into subnet limits.
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'id' INTO sid;
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'address-limit' INTO lease_limit;
+
+    IF lease_limit IS NOT NULL THEN
+        -- Get the lease count for this client class.
+        SELECT leases FROM lease4_stat WHERE subnet_id = sid AND state = 0 INTO lease_count;
+        IF lease_count IS NULL THEN
+            lease_count := 0;
+        END IF;
+
+        -- Compare. Return immediately if the limit is surpassed.
+        IF lease_limit <= lease_count THEN
+            RETURN CONCAT('address limit ', lease_limit, ' for subnet ID ', sid, ', current lease count ', lease_count);
+        END IF;
+    END IF;
+
+    RETURN '';
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION checkLease6Limits(user_context TEXT)
+RETURNS TEXT
+AS $$
+DECLARE
+    class TEXT;
+    name VARCHAR(255);
+    sid INT;
+    lease_limit INT;
+    lease_count INT;
+BEGIN
+    -- Dive into client class limits.
+    FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(user_context)->'ISC'->'limits'->'client-classes') LOOP
+        SELECT TRIM('"' FROM (json_cast(class)->'name')::text) INTO name;
+        SELECT json_cast(class)->'address-limit' INTO lease_limit;
+
+        IF lease_limit IS NOT NULL THEN
+            -- Get the address count for this client class.
+            SELECT leases FROM lease6_stat_by_client_class WHERE client_class = name AND lease_type = 0 INTO lease_count;
+            IF lease_count IS NULL THEN
+                lease_count := 0;
+            END IF;
+
+            -- Compare. Return immediately if the limit is surpassed.
+            IF lease_limit <= lease_count THEN
+                RETURN CONCAT('address limit ', lease_limit, ' for client class "', name, '", current lease count ', lease_count);
+            END IF;
+        END IF;
+
+        SELECT json_cast(class)->'prefix-limit' INTO lease_limit;
+        IF lease_limit IS NOT NULL THEN
+            -- Get the prefix count for this client class.
+            SELECT leases FROM lease6_stat_by_client_class WHERE client_class = name AND lease_type = 2 INTO lease_count;
+            IF lease_count IS NULL THEN
+                lease_count := 0;
+            END IF;
+
+            -- Compare. Return immediately if the limit is surpassed.
+            IF lease_limit <= lease_count THEN
+                RETURN CONCAT('prefix limit ', lease_limit, ' for client class "', name, '", current lease count ', lease_count);
+            END IF;
+        END IF;
+    END LOOP;
+
+    -- Dive into subnet limits.
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'id' INTO sid;
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'address-limit' INTO lease_limit;
+    IF lease_limit IS NOT NULL THEN
+        -- Get the lease count for this subnet.
+        SELECT leases FROM lease6_stat WHERE subnet_id = sid AND lease_type = 0 AND state = 0 INTO lease_count;
+        IF lease_count IS NULL THEN
+            lease_count := 0;
+        END IF;
+
+        -- Compare. Return immediately if the limit is surpassed.
+        IF lease_limit <= lease_count THEN
+            RETURN CONCAT('address limit ', lease_limit, ' for subnet ID ', sid, ', current lease count ', lease_count);
+        END IF;
+    END IF;
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'prefix-limit' INTO lease_limit;
+    IF lease_limit IS NOT NULL THEN
+        -- Get the lease count for this client class.
+        SELECT leases FROM lease6_stat WHERE subnet_id = sid AND lease_type = 2 AND state = 0 INTO lease_count;
+        IF lease_count IS NULL THEN
+            lease_count := 0;
+        END IF;
+
+        -- Compare. Return immediately if the limit is surpassed.
+        IF lease_limit <= lease_count THEN
+            RETURN CONCAT('prefix limit ', lease_limit, ' for subnet ID ', sid, ', current lease count ', lease_count);
+        END IF;
+    END IF;
+
+    RETURN '';
+END;
+$$ LANGUAGE plpgsql;
+
+-- Update the schema version number.
+UPDATE schema_version
+    SET version = '13', minor = '0';
+
+-- This line concludes the schema upgrade to version 13.
+
 -- Commit the script transaction.
 COMMIT;
 
index c4247437c2ed5735fdb8aeab29178b60932ec62d..27bb3114bfadaa571f90a6036b5650a6725982e3 100644 (file)
@@ -177,3 +177,28 @@ DROP FUNCTION IF EXISTS lease4Upload();
 DROP FUNCTION IF EXISTS lease6Upload();
 
 DROP FUNCTION IF EXISTS gmt_epoch(input_time TIMESTAMP WITH TIME ZONE);
+
+-- lease limiting tables and functions
+DROP TABLE IF EXISTS lease4_stat_by_client_class;
+DROP TABLE IF EXISTS lease6_stat_by_client_class;
+DROP FUNCTION IF EXISTS lease4_AINS_lease4_stat();
+DROP FUNCTION IF EXISTS lease4_AUPD_lease4_stat();
+DROP FUNCTION IF EXISTS lease4_ADEL_lease4_stat();
+DROP FUNCTION IF EXISTS lease6_AINS_lease6_stat();
+DROP FUNCTION IF EXISTS lease6_AUPD_lease6_stat();
+DROP FUNCTION IF EXISTS lease6_ADEL_lease6_stat();
+DROP FUNCTION IF EXISTS lease4_AINS_lease4_stat_by_client_class();
+DROP FUNCTION IF EXISTS lease4_AUPD_lease4_stat_by_client_class();
+DROP FUNCTION IF EXISTS lease4_ADEL_lease4_stat_by_client_class();
+DROP FUNCTION IF EXISTS lease6_AINS_lease6_stat_by_client_class();
+DROP FUNCTION IF EXISTS lease6_AUPD_lease6_stat_by_client_class();
+DROP FUNCTION IF EXISTS lease6_ADEL_lease6_stat_by_client_class();
+DROP FUNCTION IF EXISTS func_lease4_AINS();
+DROP FUNCTION IF EXISTS func_lease4_AUPD();
+DROP FUNCTION IF EXISTS func_lease4_ADEL();
+DROP FUNCTION IF EXISTS func_lease6_AINS();
+DROP FUNCTION IF EXISTS func_lease6_AUPD();
+DROP FUNCTION IF EXISTS func_lease6_ADEL();
+DROP FUNCTION IF EXISTS checkLease4Limits();
+DROP FUNCTION IF EXISTS checkLease6Limits();
+DROP FUNCTION IF EXISTS isJsonSupported();
index 6bdeeda61907b93e8d357bfe3f50316cd190e989..00a02dcea2e18dc3f408d63c78cb5124b1f7078e 100644 (file)
@@ -104,4 +104,3 @@ UPDATE schema_version
 COMMIT;
 
 EOF
-
diff --git a/src/share/database/scripts/pgsql/upgrade_012_to_013.sh.in b/src/share/database/scripts/pgsql/upgrade_012_to_013.sh.in
new file mode 100644 (file)
index 0000000..e291974
--- /dev/null
@@ -0,0 +1,657 @@
+#!/bin/sh
+
+# Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC")
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# shellcheck disable=SC1091
+# SC1091: Not following: ... was not specified as input (see shellcheck -x).
+
+# Exit with error if commands exit with non-zero and if undefined variables are
+# used.
+set -eu
+
+# shellcheck disable=SC2034
+# SC2034: ... appears unused. Verify use (or export if used externally).
+prefix="@prefix@"
+
+# Include utilities. Use installed version if available and
+# use build version if it isn't.
+if [ -e @datarootdir@/@PACKAGE_NAME@/scripts/admin-utils.sh ]; then
+    . "@datarootdir@/@PACKAGE_NAME@/scripts/admin-utils.sh"
+else
+    . "@abs_top_builddir@/src/bin/admin/admin-utils.sh"
+fi
+
+VERSION=$(pgsql_version "$@")
+
+if [ "$VERSION" != "12.0" ]; then
+    printf 'This script upgrades 12.0 to 12.0. '
+    printf 'Reported version is %s. Skipping upgrade.\n' "${VERSION}"
+    exit 0
+fi
+
+psql "$@" >/dev/null <<EOF
+START TRANSACTION;
+
+-- This line starts the schema upgrade to version 13.
+
+-- JSON functions --
+
+-- Helper function that avoids a casting error when the string
+-- presumed to be in JSON format, is empty.
+CREATE OR REPLACE FUNCTION json_cast(IN json_candidate TEXT)
+RETURNS JSON
+AS \$\$
+BEGIN
+    IF LENGTH(json_candidate) = 0 THEN
+        RETURN '{}'::json;
+    END IF;
+    RETURN json_candidate::json;
+END;
+\$\$ LANGUAGE plpgsql;
+
+-- Function that establishes whether JSON functions are supported.
+-- They should be provided with PostgreSQL >= 9.4.
+CREATE OR REPLACE FUNCTION isJsonSupported()
+RETURNS BOOLEAN
+AS \$\$
+BEGIN
+    IF get_session_value('json_supported') IS NULL THEN
+        IF (SELECT proname FROM pg_proc WHERE proname = 'json_extract_path') = 'json_extract_path' THEN
+            PERFORM set_session_value('kea.json_supported', true);
+        ELSE
+            PERFORM set_session_value('kea.json_supported', false);
+        END IF;
+    END IF;
+    RETURN get_session_value('kea.json_supported');
+END
+\$\$ LANGUAGE plpgsql;
+
+-- Schema changes related to lease limiting start here. --
+
+-- Recreate the triggers that update the leaseX_stat tables as stored procedures. --
+
+CREATE OR REPLACE FUNCTION lease4_AINS_lease4_stat(IN new_state BIGINT,
+                                                   IN new_subnet_id BIGINT)
+RETURNS VOID
+AS \$\$
+BEGIN
+    IF new_state = 0 OR new_state = 1 THEN
+        -- Update the state count if it exists.
+        UPDATE lease4_stat SET leases = leases + 1
+            WHERE subnet_id = new_subnet_id AND state = new_state;
+
+        -- Insert the state count record if it does not exist.
+        IF NOT FOUND THEN
+            INSERT INTO lease4_stat VALUES (new_subnet_id, new_state, 1);
+        END IF;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease4_AUPD_lease4_stat(IN old_state BIGINT,
+                                                   IN old_subnet_id BIGINT,
+                                                   IN new_state BIGINT,
+                                                   IN new_subnet_id BIGINT)
+RETURNS VOID
+AS \$\$
+BEGIN
+    IF old_subnet_id != new_subnet_id OR old_state != new_state THEN
+        IF old_state = 0 OR old_state = 1 THEN
+            -- Decrement the old state count if record exists.
+            UPDATE lease4_stat
+                SET leases = GREATEST(leases - 1, 0)
+                WHERE subnet_id = old_subnet_id AND state = old_state;
+        END IF;
+
+        IF new_state = 0 OR new_state = 1 THEN
+            -- Increment the new state count if record exists.
+            UPDATE lease4_stat SET leases = leases + 1
+                WHERE subnet_id = new_subnet_id AND state = new_state;
+
+            -- Insert new state record if it does not exist.
+            IF NOT FOUND THEN
+                INSERT INTO lease4_stat VALUES (new_subnet_id, new_state, 1);
+            END IF;
+        END IF;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease4_ADEL_lease4_stat(IN old_state BIGINT,
+                                                   IN old_subnet_id BIGINT)
+RETURNS VOID
+AS \$\$
+BEGIN
+    IF old_state = 0 OR old_state = 1 THEN
+        -- Decrement the state count if record exists.
+        UPDATE lease4_stat
+            SET leases = GREATEST(leases - 1, 0)
+            WHERE subnet_id = old_subnet_id AND old_state = state;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_AINS_lease6_stat(IN new_state BIGINT,
+                                                   IN new_subnet_id BIGINT,
+                                                   IN new_lease_type BIGINT)
+RETURNS VOID
+AS \$\$
+BEGIN
+    IF new_state = 0 OR new_state = 1 THEN
+        -- Update the state count if it exists.
+        UPDATE lease6_stat SET leases = leases + 1
+            WHERE subnet_id = new_subnet_id AND lease_type = new_lease_type
+            AND state = new_state;
+
+        -- Insert the state count record if it does not exist.
+        IF NOT FOUND THEN
+            INSERT INTO lease6_stat VALUES (new_subnet_id, new_lease_type, new_state, 1);
+        END IF;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_AUPD_lease6_stat(IN old_state BIGINT,
+                                                   IN old_subnet_id BIGINT,
+                                                   IN old_lease_type BIGINT,
+                                                   IN new_state BIGINT,
+                                                   IN new_subnet_id BIGINT,
+                                                   IN new_lease_type BIGINT)
+RETURNS VOID
+AS \$\$
+BEGIN
+    IF old_subnet_id != new_subnet_id OR
+       old_lease_type != new_lease_type OR
+       old_state != new_state THEN
+        IF old_state = 0 OR old_state = 1 THEN
+            -- Decrement the old state count if record exists.
+            UPDATE lease6_stat
+                SET leases = GREATEST(leases - 1, 0)
+                WHERE subnet_id = old_subnet_id AND lease_type = old_lease_type
+                AND state = old_state;
+        END IF;
+
+        IF new_state = 0 OR new_state = 1 THEN
+            -- Increment the new state count if record exists
+            UPDATE lease6_stat SET leases = leases + 1
+                WHERE subnet_id = new_subnet_id AND lease_type = new_lease_type
+                AND state = new_state;
+
+            -- Insert new state record if it does not exist
+            IF NOT FOUND THEN
+                INSERT INTO lease6_stat
+                VALUES (new_subnet_id, new_lease_type, new_state, 1);
+            END IF;
+        END IF;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_ADEL_lease6_stat(IN old_state BIGINT,
+                                                   IN old_subnet_id BIGINT,
+                                                   IN old_lease_type BIGINT)
+RETURNS VOID
+AS \$\$
+BEGIN
+    IF old_state = 0 OR old_state = 1 THEN
+        -- Decrement the state count if record exists
+        UPDATE lease6_stat
+            SET leases = GREATEST(leases - 1, 0)
+            WHERE subnet_id = old_subnet_id AND lease_type = old_lease_type
+            AND state = old_state;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+-- Create tables that contain the number of active leases. --
+
+CREATE TABLE lease4_stat_by_client_class (
+    client_class VARCHAR(128) NOT NULL PRIMARY KEY,
+    leases BIGINT NOT NULL
+);
+
+CREATE TABLE lease6_stat_by_client_class (
+    client_class VARCHAR(128) NOT NULL,
+    lease_type BIGINT NOT NULL,
+    leases BIGINT NOT NULL,
+    PRIMARY KEY (client_class, lease_type),
+    CONSTRAINT fk_lease6_stat_by_client_class_lease_type FOREIGN KEY (lease_type)
+        REFERENCES lease6_types (lease_type)
+);
+
+-- Create procedures to be called for each row in after-event triggers for
+-- INSERT, UPDATE and DELETE on lease tables.
+
+CREATE OR REPLACE FUNCTION lease4_AINS_lease4_stat_by_client_class(IN new_state BIGINT,
+                                                                   IN new_user_context TEXT)
+RETURNS VOID
+AS \$\$
+DECLARE
+    class VARCHAR(128);
+BEGIN
+    -- Only state 0 is needed for lease limiting.
+    IF new_state = 0 THEN
+        -- Dive into client classes.
+        FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(new_user_context)->'ISC'->'client-classes') LOOP
+            SELECT TRIM('"' FROM class) INTO class;
+
+            -- Upsert to increment the lease count.
+            UPDATE lease4_stat_by_client_class SET leases = leases + 1
+                WHERE client_class = class;
+            IF NOT FOUND THEN
+                INSERT INTO lease4_stat_by_client_class VALUES (class, 1);
+            END IF;
+        END LOOP;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease4_AUPD_lease4_stat_by_client_class(IN old_state BIGINT,
+                                                                   IN old_user_context TEXT,
+                                                                   IN new_state BIGINT,
+                                                                   IN new_user_context TEXT)
+RETURNS VOID
+AS \$\$
+DECLARE
+    old_client_classes TEXT;
+    new_client_classes TEXT;
+    class VARCHAR(128);
+    length INT;
+    i INT;
+BEGIN
+    SELECT json_cast(old_user_context)->'ISC'->'client-classes' INTO old_client_classes;
+    SELECT json_cast(new_user_context)->'ISC'->'client-classes' INTO new_client_classes;
+
+    IF old_state != new_state OR old_client_classes != new_client_classes THEN
+        -- Check if it's moving away from a counted state.
+        IF old_state = 0 THEN
+            -- Dive into client classes.
+            FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(old_client_classes)) LOOP
+                -- Decrement the lease count if the record exists.
+                UPDATE lease4_stat_by_client_class SET leases = GREATEST(leases - 1, 0)
+                    WHERE client_class = class;
+            END LOOP;
+        END IF;
+
+        -- Check if it's moving into a counted state.
+        IF new_state = 0 THEN
+            -- Dive into client classes.
+            FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(new_client_classes)) LOOP
+                -- Upsert to increment the lease count.
+                UPDATE lease4_stat_by_client_class SET leases = leases + 1
+                    WHERE client_class = class;
+                IF NOT FOUND THEN
+                    INSERT INTO lease4_stat_by_client_class VALUES (class, 1);
+                END IF;
+            END LOOP;
+        END IF;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease4_ADEL_lease4_stat_by_client_class(IN old_state BIGINT,
+                                                                   IN old_user_context TEXT)
+RETURNS VOID
+AS \$\$
+DECLARE
+    class VARCHAR(128);
+BEGIN
+    -- Only state 0 is accounted for in lease limiting.
+    IF old_state = 0 THEN
+        -- Dive into client classes.
+        FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(old_user_context)->'ISC'->'client-classes') LOOP
+            SELECT TRIM('"' FROM class) INTO class;
+
+            -- Decrement the lease count if the record exists.
+            UPDATE lease4_stat_by_client_class SET leases = GREATEST(leases - 1, 0)
+                WHERE client_class = class;
+        END LOOP;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_AINS_lease6_stat_by_client_class(IN new_state BIGINT,
+                                                                   IN new_user_context TEXT,
+                                                                   IN new_lease_type BIGINT)
+RETURNS VOID
+AS \$\$
+DECLARE
+    client_classes TEXT;
+    class VARCHAR(128);
+    length INT;
+    i INT;
+BEGIN
+    -- Only state 0 is needed for lease limiting.
+    IF new_state = 0 THEN
+        -- Dive into client classes.
+        FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(new_user_context)->'ISC'->'client-classes') LOOP
+            SELECT TRIM('"' FROM class) INTO class;
+
+            -- Upsert to increment the lease count.
+            UPDATE lease6_stat_by_client_class SET leases = leases + 1
+                WHERE client_class = class AND lease_type = new_lease_type;
+            IF NOT FOUND THEN
+                INSERT INTO lease6_stat_by_client_class VALUES (class, new_lease_type, 1);
+            END IF;
+        END LOOP;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_AUPD_lease6_stat_by_client_class(IN old_state BIGINT,
+                                                                   IN old_user_context TEXT,
+                                                                   IN old_lease_type BIGINT,
+                                                                   IN new_state BIGINT,
+                                                                   IN new_user_context TEXT,
+                                                                   IN new_lease_type BIGINT)
+RETURNS VOID
+AS \$\$
+DECLARE
+    old_client_classes TEXT;
+    new_client_classes TEXT;
+    class VARCHAR(128);
+    length INT;
+    i INT;
+BEGIN
+    SELECT json_cast(old_user_context)->'ISC'->'client-classes' INTO old_client_classes;
+    SELECT json_cast(new_user_context)->'ISC'->'client-classes' INTO new_client_classes;
+
+    IF old_state != new_state OR old_client_classes != new_client_classes OR old_lease_type != new_lease_type THEN
+        -- Check if it's moving away from a counted state.
+        IF old_state = 0 THEN
+            -- Dive into client classes.
+            FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(old_client_classes)) LOOP
+                -- Decrement the lease count if the record exists.
+                UPDATE lease6_stat_by_client_class SET leases = GREATEST(leases - 1, 0)
+                    WHERE client_class = class AND lease_type = old_lease_type;
+            END LOOP;
+        END IF;
+
+        -- Check if it's moving into a counted state.
+        IF new_state = 0 THEN
+            -- Dive into client classes.
+            FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(new_client_classes)) LOOP
+                -- Upsert to increment the lease count.
+                UPDATE lease6_stat_by_client_class SET leases = leases + 1
+                    WHERE client_class = class AND lease_type = new_lease_type;
+                IF NOT FOUND THEN
+                    INSERT INTO lease6_stat_by_client_class VALUES (class, new_lease_type, 1);
+                END IF;
+            END LOOP;
+        END IF;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION lease6_ADEL_lease6_stat_by_client_class(IN old_state BIGINT,
+                                                                   IN old_user_context TEXT,
+                                                                   IN old_lease_type BIGINT)
+RETURNS VOID
+AS \$\$
+DECLARE
+    client_classes VARCHAR(1024);
+    class VARCHAR(128);
+    length INT;
+    i INT;
+BEGIN
+    -- Only state 0 is accounted for in lease limiting. But check both states to be consistent with lease6_stat.
+    IF old_state = 0 THEN
+        -- Dive into client classes.
+        FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(old_user_context)->'ISC'->'client-classes') LOOP
+            SELECT TRIM('"' FROM class) INTO class;
+
+            -- Decrement the lease count if the record exists.
+            UPDATE lease6_stat_by_client_class SET leases = GREATEST(leases - 1, 0)
+                WHERE client_class = class AND lease_type = old_lease_type;
+        END LOOP;
+    END IF;
+END;
+\$\$ LANGUAGE plpgsql;
+
+-- Recreate the after-event triggers for INSERT, UPDATE and DELETE on lease tables to call the --
+-- stored procedures above in pairs of two: for client classes and for subnets. --
+
+DROP TRIGGER IF EXISTS stat_lease4_insert ON lease4;
+
+CREATE OR REPLACE FUNCTION func_lease4_AINS()
+RETURNS trigger AS \$lease4_AINS\$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease4_AINS_lease4_stat_by_client_class(NEW.state, NEW.user_context);
+    END IF;
+    PERFORM lease4_AINS_lease4_stat(NEW.state, NEW.subnet_id);
+    RETURN NULL;
+END;
+\$lease4_AINS\$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease4_AINS AFTER INSERT ON lease4
+    FOR EACH ROW EXECUTE PROCEDURE func_lease4_AINS();
+
+DROP TRIGGER IF EXISTS stat_lease4_update ON lease4;
+
+CREATE OR REPLACE FUNCTION func_lease4_AUPD()
+RETURNS trigger AS \$lease4_AUPD\$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease4_AUPD_lease4_stat_by_client_class(OLD.state, OLD.user_context, NEW.state, NEW.user_context);
+    END IF;
+    PERFORM lease4_AUPD_lease4_stat(OLD.state, OLD.subnet_id, NEW.state, NEW.subnet_id);
+    RETURN NULL;
+END;
+\$lease4_AUPD\$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease4_AUPD AFTER UPDATE ON lease4
+    FOR EACH ROW EXECUTE PROCEDURE func_lease4_AUPD();
+
+DROP TRIGGER IF EXISTS stat_lease4_delete ON lease4;
+
+CREATE OR REPLACE FUNCTION func_lease4_ADEL()
+RETURNS trigger AS \$lease4_ADEL\$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease4_ADEL_lease4_stat_by_client_class(OLD.state, OLD.user_context);
+    END IF;
+    PERFORM lease4_ADEL_lease4_stat(OLD.state, OLD.subnet_id);
+    RETURN NULL;
+END;
+\$lease4_ADEL\$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease4_ADEL AFTER DELETE ON lease4
+    FOR EACH ROW EXECUTE PROCEDURE func_lease4_ADEL();
+
+DROP TRIGGER IF EXISTS stat_lease6_insert ON lease6;
+
+CREATE OR REPLACE FUNCTION func_lease6_AINS()
+RETURNS trigger AS \$lease6_AINS\$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease6_AINS_lease6_stat_by_client_class(NEW.state, NEW.user_context, NEW.lease_type);
+    END IF;
+    PERFORM lease6_AINS_lease6_stat(NEW.state, NEW.subnet_id, NEW.lease_type);
+    RETURN NULL;
+END;
+\$lease6_AINS\$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease6_AINS AFTER INSERT ON lease6
+    FOR EACH ROW EXECUTE PROCEDURE func_lease6_AINS();
+
+DROP TRIGGER IF EXISTS stat_lease6_update ON lease6;
+
+CREATE OR REPLACE FUNCTION func_lease6_AUPD()
+RETURNS trigger AS \$lease6_AUPD\$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease6_AUPD_lease6_stat_by_client_class(OLD.state, OLD.user_context, OLD.lease_type, NEW.state, NEW.user_context, NEW.lease_type);
+    END IF;
+    PERFORM lease6_AUPD_lease6_stat(OLD.state, OLD.subnet_id, OLD.lease_type, NEW.state, NEW.subnet_id, NEW.lease_type);
+    RETURN NULL;
+END;
+\$lease6_AUPD\$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease6_AUPD AFTER UPDATE ON lease6
+    FOR EACH ROW EXECUTE PROCEDURE func_lease6_AUPD();
+
+DROP TRIGGER IF EXISTS stat_lease6_delete ON lease6;
+
+CREATE OR REPLACE FUNCTION func_lease6_ADEL()
+RETURNS trigger AS \$lease6_ADEL\$
+BEGIN
+    IF isJsonSupported() = true THEN
+        PERFORM lease6_ADEL_lease6_stat_by_client_class(OLD.state, OLD.user_context, OLD.lease_type);
+    END IF;
+    PERFORM lease6_ADEL_lease6_stat(OLD.state, OLD.subnet_id, OLD.lease_type);
+    RETURN NULL;
+END;
+\$lease6_ADEL\$ LANGUAGE plpgsql;
+
+CREATE TRIGGER lease6_ADEL AFTER DELETE ON lease6
+    FOR EACH ROW EXECUTE PROCEDURE func_lease6_ADEL();
+
+-- Create functions that return an empty TEXT if all limits allow for more leases, or otherwise a
+-- TEXT in one of the following JSON formats detailing the limit that was reached:
+-- { "limit-type": "client-class", "name": foo, "lease-type": "address", "limit": 2, "count": 2 }
+-- { "limit-type": "subnet", "id": 1, "lease-type": "IA_PD", "limit": 2, "count": 2 }
+-- The following format for user_context is assumed:
+-- { "ISC": { "limits": { "client-classes": [ { "name": "foo", "address-limit": 2, "prefix-limit": 1 } ],
+--                        "subnet": { "id": 1, "address-limit": 2, "prefix-limit": 1 } } } }
+
+CREATE OR REPLACE FUNCTION checkLease4Limits(user_context TEXT)
+RETURNS TEXT
+AS \$\$
+DECLARE
+    class TEXT;
+    name VARCHAR(255);
+    sid INT;
+    lease_limit INT;
+    lease_count INT;
+BEGIN
+    -- Dive into client class limits.
+    FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(user_context)->'ISC'->'limits'->'client-classes') LOOP
+        SELECT TRIM('"' FROM (json_cast(class)->'name')::text) INTO name;
+        SELECT json_cast(class)->'address-limit' INTO lease_limit;
+
+        IF lease_limit IS NOT NULL THEN
+            -- Get the lease count for this client class.
+            SELECT leases FROM lease4_stat_by_client_class INTO lease_count WHERE client_class = name;
+            IF lease_count IS NULL THEN
+                lease_count := 0;
+            END IF;
+
+            -- Compare. Return immediately if the limit is surpassed.
+            IF lease_limit <= lease_count THEN
+                RETURN CONCAT('address limit ', lease_limit, ' for client class "', name, '", current lease count ', lease_count);
+            END IF;
+        END IF;
+    END LOOP;
+
+    -- Dive into subnet limits.
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'id' INTO sid;
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'address-limit' INTO lease_limit;
+
+    IF lease_limit IS NOT NULL THEN
+        -- Get the lease count for this client class.
+        SELECT leases FROM lease4_stat WHERE subnet_id = sid AND state = 0 INTO lease_count;
+        IF lease_count IS NULL THEN
+            lease_count := 0;
+        END IF;
+
+        -- Compare. Return immediately if the limit is surpassed.
+        IF lease_limit <= lease_count THEN
+            RETURN CONCAT('address limit ', lease_limit, ' for subnet ID ', sid, ', current lease count ', lease_count);
+        END IF;
+    END IF;
+
+    RETURN '';
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION checkLease6Limits(user_context TEXT)
+RETURNS TEXT
+AS \$\$
+DECLARE
+    class TEXT;
+    name VARCHAR(255);
+    sid INT;
+    lease_limit INT;
+    lease_count INT;
+BEGIN
+    -- Dive into client class limits.
+    FOR class IN SELECT * FROM JSON_ARRAY_ELEMENTS(json_cast(user_context)->'ISC'->'limits'->'client-classes') LOOP
+        SELECT TRIM('"' FROM (json_cast(class)->'name')::text) INTO name;
+        SELECT json_cast(class)->'address-limit' INTO lease_limit;
+
+        IF lease_limit IS NOT NULL THEN
+            -- Get the address count for this client class.
+            SELECT leases FROM lease6_stat_by_client_class WHERE client_class = name AND lease_type = 0 INTO lease_count;
+            IF lease_count IS NULL THEN
+                lease_count := 0;
+            END IF;
+
+            -- Compare. Return immediately if the limit is surpassed.
+            IF lease_limit <= lease_count THEN
+                RETURN CONCAT('address limit ', lease_limit, ' for client class "', name, '", current lease count ', lease_count);
+            END IF;
+        END IF;
+
+        SELECT json_cast(class)->'prefix-limit' INTO lease_limit;
+        IF lease_limit IS NOT NULL THEN
+            -- Get the prefix count for this client class.
+            SELECT leases FROM lease6_stat_by_client_class WHERE client_class = name AND lease_type = 2 INTO lease_count;
+            IF lease_count IS NULL THEN
+                lease_count := 0;
+            END IF;
+
+            -- Compare. Return immediately if the limit is surpassed.
+            IF lease_limit <= lease_count THEN
+                RETURN CONCAT('prefix limit ', lease_limit, ' for client class "', name, '", current lease count ', lease_count);
+            END IF;
+        END IF;
+    END LOOP;
+
+    -- Dive into subnet limits.
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'id' INTO sid;
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'address-limit' INTO lease_limit;
+    IF lease_limit IS NOT NULL THEN
+        -- Get the lease count for this subnet.
+        SELECT leases FROM lease6_stat WHERE subnet_id = sid AND lease_type = 0 AND state = 0 INTO lease_count;
+        IF lease_count IS NULL THEN
+            lease_count := 0;
+        END IF;
+
+        -- Compare. Return immediately if the limit is surpassed.
+        IF lease_limit <= lease_count THEN
+            RETURN CONCAT('address limit ', lease_limit, ' for subnet ID ', sid, ', current lease count ', lease_count);
+        END IF;
+    END IF;
+    SELECT json_cast(user_context)->'ISC'->'limits'->'subnet'->'prefix-limit' INTO lease_limit;
+    IF lease_limit IS NOT NULL THEN
+        -- Get the lease count for this client class.
+        SELECT leases FROM lease6_stat WHERE subnet_id = sid AND lease_type = 2 AND state = 0 INTO lease_count;
+        IF lease_count IS NULL THEN
+            lease_count := 0;
+        END IF;
+
+        -- Compare. Return immediately if the limit is surpassed.
+        IF lease_limit <= lease_count THEN
+            RETURN CONCAT('prefix limit ', lease_limit, ' for subnet ID ', sid, ', current lease count ', lease_count);
+        END IF;
+    END IF;
+
+    RETURN '';
+END;
+\$\$ LANGUAGE plpgsql;
+
+-- Update the schema version number.
+UPDATE schema_version
+    SET version = '13', minor = '0';
+
+-- This line concludes the schema upgrade to version 13.
+
+-- Commit the script transaction.
+COMMIT;
+
+EOF
index 154ddb0aa0b6eb107e97b28b7a7e85fa8fff0c6e..088b802e249d55690837b00a62f08bca687e2331 100644 (file)
@@ -72,6 +72,8 @@ DELETE FROM lease4_stat;
 DELETE FROM lease6;
 DELETE FROM lease6_stat;
 DELETE FROM logs;
+DELETE FROM lease4_stat_by_client_class;
+DELETE FROM lease6_stat_by_client_class;
 
 -- Config Backend tables
 DELETE FROM dhcp4_audit;