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