Repository: irods/irods
Pull request #8263: [#6766] ModAVUMetadata / imeta: Remove adda, lsw, addw, and rmw (main)
Author: login: alanking
Created: 2025-03-07T20:53:38Z | Merged: 2025-03-08T00:10:44Z

In service of #6766 

Companion PR: https://github.com/irods/irods_client_icommands/pull/541

Unit tests pass. Core tests running now, but they're almost done and are passing so far. Will take out of draft once complete.



Diff:
diff --git a/lib/api/include/irods/modAVUMetadata.h b/lib/api/include/irods/modAVUMetadata.h
index 5de2e871b2..f3bb558cda 100644
--- a/lib/api/include/irods/modAVUMetadata.h
+++ b/lib/api/include/irods/modAVUMetadata.h
@@ -8,7 +8,7 @@
 struct RcComm;
 
 typedef struct ModifyAVUMetadataInput {
-    char *arg0; // option add, adda, rm, rmw, rmi, cp, mod, or set
+    char *arg0; // option add, rm, rmi, cp, mod, or set
     char *arg1; // item type -d,-D,-c,-C,-r,-R,-u,-U
     char *arg2; // item name
     char *arg3; // attr name
diff --git a/packaging/core.re.template b/packaging/core.re.template
index 52a0d18edc..0474f6ea81 100644
--- a/packaging/core.re.template
+++ b/packaging/core.re.template
@@ -364,7 +364,7 @@ acSetVaultPathPolicy {msiSetGraftPathScheme("no","1"); }
 # mod
 # new values have the prefix n:, v:, u:, and "" means that that value remain unchanged
 # acPreProcForModifyAVUMetadata(*Option,*ItemType,*ItemName,*AName,*AValue,*AUnit, *NAName, *NAValue, *NAUnit) { }
-# add, adda, addw, set, rm, rmw, rmi
+# add, set, rm, rmi
 # acPreProcForModifyAVUMetadata(*Option,*ItemType,*ItemName,*AName,*AValue,*AUnit) { }
 # cp
 # acPreProcForModifyAVUMetadata(*Option,*SourceItemType,*TargetItemType,*SourceItemName,*TargetItemName) { }
diff --git a/plugins/database/src/db_plugin.cpp b/plugins/database/src/db_plugin.cpp
index 5cd277130a..bc07572333 100644
--- a/plugins/database/src/db_plugin.cpp
+++ b/plugins/database/src/db_plugin.cpp
@@ -433,157 +433,6 @@ int get_object_count_of_resource_by_name(
 
 } // get_object_count_of_resource_by_name
 
-// =-=-=-=-=-=-=-
-// @brief determine if user had write permission to data object
-irods::error determine_user_has_modify_metadata_access(
-    const std::string& _data_name,
-    const std::string& _collection,
-    const std::string& _user_name,
-    const std::string& _zone)
-{
-    int status = 0;
-
-    log_db::debug(
-        "{} :: [{}] [{}] [{}] [{}]",
-        __FUNCTION__,
-        _data_name.c_str(),
-        _collection.c_str(),
-        _user_name.c_str(),
-        _zone.c_str());
-
-    // get the number of data object to which this will apply
-    rodsLong_t num_data_objects = -1;
-    {
-        std::vector<std::string> bind_vars;
-        bind_vars.reserve(2);
-        bind_vars.push_back( _data_name );
-        bind_vars.push_back( _collection );
-        status = cmlGetIntegerValueFromSql(
-             "select count(DISTINCT DM.data_id) "
-             "from R_DATA_MAIN DM, R_COLL_MAIN CM "
-             "where DM.data_name like ? and DM.coll_id=CM.coll_id and CM.coll_name like ?",
-              &num_data_objects,
-              bind_vars,
-              &icss );
-        if( 0 != status ) {
-            _rollback( "chlAddAVUMetadataWild" );
-            return ERROR( status, "failed to get object count" );
-        }
-
-        if( 0 == num_data_objects ) {
-            std::string msg = "no data objects found for collection ";
-            msg += _collection;
-            msg += " and object name ";
-            msg += _data_name;
-            _rollback( "chlAddAVUMetadataWild" );
-            return ERROR( CAT_NO_ROWS_FOUND, msg );
-        }
-    }
-
-    // This is the minimum permission level required of the user.
-    namespace ic = irods::experimental::catalog;
-    using integral_access_type = std::underlying_type_t<ic::access_type>;
-    const rodsLong_t access_needed = static_cast<integral_access_type>(ic::access_type::modify_metadata);
-
-    // Get the minimum permission level across all data objects.
-    // The user is allowed to modify the metadata of all matched data objects if and only if
-    // the minimum permission level found among the list of data objects is greater than or
-    // equal to required permission level (i.e. "modify_metadata").
-    rodsLong_t access_permission = -1;
-    {
-        const char* const query =
-            "select min(max_access_type_id) from ( "
-                "select max(access_type_id) max_access_type_id from ( "
-                    "select access_type_id, DM.data_id "
-                    "from R_DATA_MAIN DM, R_OBJT_ACCESS OA, R_USER_GROUP UG, R_USER_MAIN UM, R_COLL_MAIN CM "
-                    "where DM.data_name like ? and "
-                          "DM.coll_id = CM.coll_id and "
-                          "CM.coll_name like ? and "
-                          "UM.user_name = ? and "
-                          "UM.zone_name = ? and "
-                          "UM.user_type_name != 'rodsgroup' and "
-                          "UM.user_id = UG.user_id and "
-                          "OA.object_id = DM.data_id and "
-                          "UG.group_user_id = OA.user_id "
-#if ORA_ICAT
-                ") "
-                "group by data_id "
-            ")";
-#else
-                ") as foo "
-                "group by data_id "
-            ") as bar";
-#endif
-        std::vector<std::string> bind_vars;
-        bind_vars.reserve(4);
-        bind_vars.push_back( _data_name.c_str() );
-        bind_vars.push_back( _collection.c_str() );
-        bind_vars.push_back( _user_name.c_str() );
-        bind_vars.push_back( _zone.c_str() );
-        status = cmlGetIntegerValueFromSql( query, &access_permission, bind_vars, &icss );
-        if ( status == CAT_NO_ROWS_FOUND ) {
-            _rollback( "chlAddAVUMetadataWild" );
-            return ERROR( CAT_NO_ACCESS_PERMISSION, "access denied" );
-        }
-
-        if ( access_permission < access_needed ) {
-            _rollback( "chlAddAVUMetadataWild" );
-            return ERROR( CAT_NO_ACCESS_PERMISSION, "access denied" );
-        }
-    }
-
-    // reproduce the count of access permission entries in "ACCESS_VIEW_TWO"
-    rodsLong_t access_permission_count = -1;
-    {
-        const char* const query =
-            "select count(max) from ( "
-                "select max(access_type_id) max from ( "
-                    "select access_type_id, DM.data_id "
-                    "from R_DATA_MAIN DM, R_OBJT_ACCESS OA, R_USER_GROUP UG, R_USER_MAIN UM, R_COLL_MAIN CM "
-                    "where DM.data_name like ? and "
-                          "DM.coll_id = CM.coll_id and "
-                          "CM.coll_name like ? and "
-                          "UM.user_name = ? and "
-                          "UM.zone_name = ? and "
-                          "UM.user_type_name != 'rodsgroup' and "
-                          "UM.user_id = UG.user_id and "
-                          "OA.object_id = DM.data_id and "
-                          "UG.group_user_id = OA.user_id "
-#if ORA_ICAT
-                ") "
-                "group by data_id "
-            ")";
-#else
-                ") as foo "
-                "group by data_id "
-            ") as baz";
-#endif
-        std::vector<std::string> bind_vars;
-        bind_vars.reserve(4);
-        bind_vars.push_back( _data_name.c_str() );
-        bind_vars.push_back( _collection.c_str() );
-        bind_vars.push_back( _user_name.c_str() );
-        bind_vars.push_back( _zone.c_str() );
-        status = cmlGetIntegerValueFromSql( query, &access_permission_count, bind_vars, &icss );
-        if( 0 != status ) {
-            _rollback( "chlAddAVUMetadataWild" );
-            return ERROR( status, "query for access permission count failed" );
-        }
-
-        if( num_data_objects > access_permission_count ) {
-            const auto msg = fmt::format("access denied - num_data_objects [{}] > access_permission_count [{}]",
-                                         num_data_objects, access_permission_count);
-            _rollback("chlAddAVUMetadataWild");
-            return ERROR(CAT_NO_ACCESS_PERMISSION, msg);
-        }
-    }
-
-    // return number of data objects, keeping the semantics of the
-    // original imeta addw operation.
-    return CODE( num_data_objects );
-} // determine_user_has_modify_metadata_access
-
-
 // remove AVU (user defined metadata) for an object, the metadata mapping information, if any.
 int removeMetaMapAndAVU(const char* _id)
 {
@@ -8711,136 +8560,6 @@ irods::error db_set_avu_metadata_op(
     return CODE( status );
 } // db_set_avu_metadata_op
 
-#define ACCESS_MAX 999999  /* A large access value (larger than the
-maximum used (i.e. for fail safe)) and
-    also indicates not initialized*/
-// =-=-=-=-=-=-=-
-// Add an Attribute-Value [Units] pair/triple metadata item to one or
-// more data objects.  This is the Wildcard version, where the
-// collection/data-object name can match multiple objects).
-
-// The return value is error code (negative) or the number of objects
-// to which the AVU was associated.
-irods::error db_add_avu_metadata_wild_op(
-    irods::plugin_context& _ctx,
-    const char*            _type,
-    const char*            _name,
-    const char*            _attribute,
-    const char*            _value,
-    const char*            _units,
-    const KeyValPair*      _cond_input)
-{
-    // =-=-=-=-=-=-=-
-    // check the context
-    if (const auto ret = _ctx.valid(); !ret.ok()) {
-        return PASS(ret);
-    }
-
-    const bool admin_mode = _cond_input && getValByKey(_cond_input, ADMIN_KW);
-
-    if (admin_mode && !irods::is_privileged_client(*_ctx.comm())) {
-        return ERROR(CAT_INSUFFICIENT_PRIVILEGE_LEVEL, "Insufficient privileges");
-    }
-
-    // =-=-=-=-=-=-=-
-    // check the params
-    if (!_type || !_name || !_attribute) {
-        return ERROR( CAT_INVALID_ARGUMENT, "null parameter" );
-    }
-
-    rodsLong_t status;//, status2;
-    rodsLong_t seqNum;
-    rodsLong_t object_count = -1;
-    char collection[MAX_NAME_LEN];
-    char objectName[MAX_NAME_LEN];
-    char myTime[50];
-    char seqNumStr[MAX_NAME_LEN];
-
-    if (const auto ec = splitPathByKey(_name, collection, MAX_NAME_LEN, objectName, MAX_NAME_LEN, '/'); ec < 0) {
-        return ERROR(ec, fmt::format(
-                     "[{}:{}] - failed in splitPathByKey [path=[{}], ec=[{}]]",
-                     __func__, __LINE__, _name, ec));
-    }
-
-    if ( strlen( collection ) == 0 ) {
-        snprintf( collection, sizeof( collection ), "%s", PATH_SEPARATOR );
-        snprintf( objectName, sizeof( objectName ), "%s", _name );
-    }
-
-    if (admin_mode) {
-        // Skip the permission checks and get the number of matched data objects
-        // if operating in administrator mode.
-
-        std::vector<std::string> bind_vars{objectName, collection};
-
-        status = cmlGetIntegerValueFromSql(
-             "select count(distinct data_id) from R_DATA_MAIN d "
-             "inner join R_COLL_MAIN c on d.coll_id = c.coll_id "
-             "where d.data_name like ? and c.coll_name like ?",
-              &object_count, bind_vars, &icss);
-
-        if (0 != status) {
-            _rollback("chlAddAVUMetadataWild");
-            return ERROR(status, "failed to get object count");
-        }
-    }
-    else {
-        const auto ret = determine_user_has_modify_metadata_access(
-            objectName,
-            collection,
-            _ctx.comm()->clientUser.userName,
-            _ctx.comm()->clientUser.rodsZone);
-
-        if (!ret.ok()) {
-            return PASS(ret);
-        }
-
-        object_count = ret.code();
-    }
-
-    // user has write access, set up the AVU and associate it with the data-objects
-    status = findOrInsertAVU( _attribute, _value, _units );
-    if ( status < 0 ) {
-        log_db::info("chlAddAVUMetadataWild findOrInsertAVU failure {}", status);
-        _rollback( "chlAddAVUMetadata" );
-        return ERROR( status, "findOrInsertAVU failure" );
-    }
-    seqNum = status;
-
-    getNowStr( myTime );
-    snprintf( seqNumStr, sizeof seqNumStr, "%lld", seqNum );
-    cllBindVars[cllBindVarCount++] = seqNumStr;
-    cllBindVars[cllBindVarCount++] = myTime;
-    cllBindVars[cllBindVarCount++] = myTime;
-    cllBindVars[cllBindVarCount++] = objectName;
-    cllBindVars[cllBindVarCount++] = collection;
-    if ( logSQL != 0 ) {
-        log_sql::debug("chlAddAVUMetadataWild SQL 8");
-    }
-    status =  cmlExecuteNoAnswerSql(
-                  "insert into R_OBJT_METAMAP \
-                   (object_id, meta_id, create_ts, modify_ts) \
-                   select DM.data_id, ?, ?, ? from R_DATA_MAIN DM, \
-                   R_COLL_MAIN CM where DM.data_name like ? \
-                   and DM.coll_id=CM.coll_id and CM.coll_name like ? \
-                   group by DM.data_id",
-                  &icss );
-    if ( status != 0 ) {
-        log_db::info("chlAddAVUMetadataWild cmlExecuteNoAnswerSql insert failure {}", status);
-        _rollback( "chlAddAVUMetadataWild" );
-        return ERROR( status, "insert failure" );
-    }
-
-    /* Commit */
-    status =  cmlExecuteNoAnswerSql( "commit", &icss );
-    if ( status != 0 ) {
-        log_db::info("chlAddAVUMetadataWild cmlExecuteNoAnswerSql commit failure {}", status);
-        return ERROR( status, "commit failure" );
-    }
-
-    return CODE(object_count);
-} // db_add_avu_metadata_wild_op
-
 irods::error db_add_avu_metadata_op(
     irods::plugin_context& _ctx,
     const char*            _type,
@@ -15883,10 +15602,6 @@ irods::database* plugin_factory(
         DATABASE_OP_SET_AVU_METADATA,
         function<error(plugin_context&,const char*,const char*,const char*,const char*,const char*,const KeyValPair*)>(
             db_set_avu_metadata_op ) );
-    pg->add_operation(
-        DATABASE_OP_ADD_AVU_METADATA_WILD,
-        function<error(plugin_context&,const char*,const char*,const char*,const char*,const char*,const KeyValPair*)>(
-            db_add_avu_metadata_wild_op ) );
     pg->add_operation(
         DATABASE_OP_ADD_AVU_METADATA,
         function<error(plugin_context&,const char*,const char*,const char*,const char*,const char*,const KeyValPair*)>(
diff --git a/scripts/core_tests_list.json b/scripts/core_tests_list.json
index 2b89d54c75..3d6b5881a6 100644
--- a/scripts/core_tests_list.json
+++ b/scripts/core_tests_list.json
@@ -47,7 +47,6 @@
     "test_imeta_error_handling",
     "test_imeta_help",
     "test_imeta_interactive",
-    "test_imeta_set.Test_ImetaAdda",
     "test_imeta_set.Test_ImetaCp",
     "test_imeta_set.Test_ImetaLsLongmode",
     "test_imeta_set.Test_ImetaQu",
diff --git a/scripts/irods/test/test_iadmin.py b/scripts/irods/test/test_iadmin.py
index 5b760d8f56..2e6bc7b574 100644
--- a/scripts/irods/test/test_iadmin.py
+++ b/scripts/irods/test/test_iadmin.py
@@ -966,16 +966,18 @@ def test_rebalance_for_repl_in_repl_node(self):
     def test_rebalance_visibility_in_resource_avu__3683(self):
         hostname = lib.get_hostname()
         # populate demoResc with an AVU, to confirm we're matching the right resource below
-        self.admin.assert_icommand(["imeta","add","-R","demoResc","rebalance_operation",hostname+":1234","20181004T123456Z"])
+        value = f"{hostname}:1234"
+        unit = "20181004T123456Z"
+        self.admin.assert_icommand(["imeta","add","-R","demoResc","rebalance_operation", value, unit])
         # rebalance, and check for avu removal
         self.admin.assert_icommand(["iadmin","modresc",self.testresc,"rebalance"])
         self.admin.assert_icommand(["imeta","ls","-R",self.testresc], 'STDOUT_SINGLELINE', "None")
         # rebalance already running
-        self.admin.assert_icommand(["imeta","add","-R",self.testresc,"rebalance_operation",hostname+":1234","20181004T123456Z"])
+        self.admin.assert_icommand(["imeta","add","-R",self.testresc,"rebalance_operation", value, unit])
         self.admin.assert_icommand(["iadmin","modresc",self.testresc,"rebalance"], 'STDERR_SINGLELINE', "REBALANCE_ALREADY_ACTIVE_ON_RESOURCE")
         # clean up
-        self.admin.assert_icommand(["imeta","rmw","-R",self.testresc,"rebalance_operation","%","%"])
-        self.admin.assert_icommand(["imeta","rmw","-R","demoResc","rebalance_operation","%","%"])
+        self.admin.assert_icommand(["imeta","rm","-R",self.testresc,"rebalance_operation", value, unit])
+        self.admin.assert_icommand(["imeta","rm","-R","demoResc","rebalance_operation", value, unit])
 
     @unittest.skip( "configuration requires sudo to create the environment")
     def test_rebalance_for_repl_node_with_different_users_with_write_failure__issue_3674(self):
@@ -2138,15 +2140,20 @@ def tearDown(self):
 
     # Issue 3862:  test that CAT_STATEMENT_TABLE_FULL is not encountered during parallel rebalance
     def test_rebalance__ticket_3862(self):
-
+        resource_name = "repl"
         def check_and_remove_rebalance_visibility_metadata(attempts):
             print("checking rebalance visibility metadata [{0} attempts remaining]".format(attempts))
-            self.assertTrue(attempts != 0, msg="rebalance visibility metadata not in catalog")
-            out,_,_ = self.admin.run_icommand(['imeta','ls','-R','repl'])
-            if "rebalance_operation" not in out:
-                check_and_remove_rebalance_visibility_metadata(attempts - 1)
-            else:
-                self.admin.assert_icommand("imeta rmw -R repl rebalance_operation % %") # b/c #3683
+            self.assertGreater(attempts, 0, msg="rebalance visibility metadata not in catalog")
+            resource_metadata_query = "select META_RESC_ATTR_NAME, META_RESC_ATTR_VALUE, META_RESC_ATTR_UNITS " \
+                                      f"where RESC_NAME = '{resource_name}'"
+            resource_avus = json.loads(self.admin.run_icommand(["iquery", resource_metadata_query])[0].strip())
+            # There should only ever be, at most, one AVU on this resource for this test.
+            self.assertLess(len(resource_avus), 2)
+            for a, v, u in resource_avus:
+                if "rebalance_operation" == a:
+                    self.admin.assert_icommand(["imeta", "rm", "-R", resource_name, a, v, u])
+                    return
+            check_and_remove_rebalance_visibility_metadata(attempts - 1)
 
         with session.make_session_for_existing_admin() as admin_session:
             admin_session.assert_icommand(['ils', '-l', '-r', self.test_dir], 'STDOUT_SINGLELINE', self.test_dir)
@@ -2154,11 +2161,11 @@ def check_and_remove_rebalance_visibility_metadata(attempts):
         # =-=-=-=-=-=-=-
         # call two separate rebalances, the first in background
         # prior to the fix, the second would generate a CAT_STATEMENT_TABLE_FULL error
-        subprocess.Popen(["iadmin", "modresc",  "repl", "rebalance"])
+        subprocess.Popen(["iadmin", "modresc",  resource_name, "rebalance"])
         # make sure rebalance visibility metadata has been populated before removing it
         check_and_remove_rebalance_visibility_metadata(10); # try 10 times
         # fire parallel rebalance - this will encounter some errors due to self-inflicted race condition
-        out,err,_ = self.admin.run_icommand(['iadmin', 'modresc', 'repl', 'rebalance'])
+        out,err,_ = self.admin.run_icommand(['iadmin', 'modresc', resource_name, 'rebalance'])
         self.assertNotIn('CAT_STATEMENT_TABLE_FULL', out)
         self.assertNotIn('CAT_STATEMENT_TABLE_FULL', err)
 
diff --git a/scripts/irods/test/test_imeta_admin_mode.py b/scripts/irods/test/test_imeta_admin_mode.py
index 4623d617c7..75816078c8 100644
--- a/scripts/irods/test/test_imeta_admin_mode.py
+++ b/scripts/irods/test/test_imeta_admin_mode.py
@@ -26,7 +26,7 @@ def setUp(self):
     def tearDown(self):
         super(Test_Imeta_Admin_Mode, self).tearDown()
 
-    def test_subcommands_add_adda_set_mod_and_rm__issue_6124(self):
+    def test_subcommands_add_set_mod_and_rm__issue_6124(self):
         attr_name  = 'issue_6124_attr'
         attr_value = 'issue_6124_value'
 
@@ -45,29 +45,19 @@ def test_subcommands_add_adda_set_mod_and_rm__issue_6124(self):
             object_name = tc['name']
             object_type_option = tc['type']
 
-            for op in ['add', 'adda', 'set']:
-                # rError messages do not return to the client after redirect to provider
-                if test.settings.TOPOLOGY_FROM_RESOURCE_SERVER and op == 'adda':
-                    continue
-
+            for op in ['add', 'set']:
                 try:
                     # Create a data object or collection based on the object type.
                     self.user.assert_icommand(['itouch' if object_type_option == '-d' else 'imkdir', object_name])
 
-                    if op == 'adda':
-                        # "adda" automatically enables admin mode, so the administrator is always allowed
-                        # to add metadata.
-                        self.admin.assert_icommand(['imeta', op, object_type_option, object_name, attr_name, attr_value],
-                                                   'STDERR', ['"adda" is deprecated. Please use "add" with admin mode enabled instead.'])
-                    else:
-                        # Show that without the admin option, the administrator cannot add/set metadata on
-                        # another user's collection or data object.
-                        self.admin.assert_icommand(['imeta', op, object_type_option, object_name, attr_name, attr_value],
-                                                   'STDERR', ['CAT_NO_ACCESS_PERMISSION'])
-
-                        # Show that using the admin option allows the administrator to add metadata to
-                        # collections and data objects they do not have permissions on.
-                        self.admin.assert_icommand(['imeta', '-M', op, object_type_option, object_name, attr_name, attr_value])
+                    # Show that without the admin option, the administrator cannot add/set metadata on
+                    # another user's collection or data object.
+                    self.admin.assert_icommand(['imeta', op, object_type_option, object_name, attr_name, attr_value],
+                                               'STDERR', ['CAT_NO_ACCESS_PERMISSION'])
+
+                    # Show that using the admin option allows the administrator to add metadata to
+                    # collections and data objects they do not have permissions on.
+                    self.admin.assert_icommand(['imeta', '-M', op, object_type_option, object_name, attr_name, attr_value])
 
                     # As the owner of the collection or data object, verify that the metadata is attached
                     # to the target object.
@@ -94,41 +84,6 @@ def test_subcommands_add_adda_set_mod_and_rm__issue_6124(self):
                 finally:
                     self.user.run_icommand(['irm', '-rf', object_name])
 
-    def test_subcommands_addw_and_rmw__issue_6124(self):
-        # Create data objects at the root of the current collection and
-        # inside of the child collection.
-        data_objects = [
-            os.path.join(self.user.session_collection, 'foo1'),
-            os.path.join(self.user.session_collection, 'foo2')
-        ]
-        for data_object in data_objects:
-            self.user.assert_icommand(['itouch', data_object])
-
-        attr_name  = 'issue_6124_attr'
-        attr_value = 'issue_6124_value'
-
-        # Show that without the admin option, the administrator cannot attach metadata to
-        # another user's data objects they do not have permissions on.
-        self.admin.assert_icommand(['imeta', 'addw', '-d', self.user.session_collection + '/%', attr_name, attr_value],
-                                   'STDERR', ['CAT_NO_ACCESS_PERMISSION'])
-
-        # Show that using the admin option allows the administrator to attach metadata to
-        # data objects they do not have permissions on.
-        self.admin.assert_icommand(['imeta', '-M', 'addw', '-d', self.user.session_collection + '/%', attr_name, attr_value],
-                                   'STDOUT', ['AVU added to 2 data-objects'])
-
-        for data_object in data_objects:
-            # Show that without the admin option, the administrator cannot detach metadata from
-            # another user's data objects they do not have permissions on.
-            self.admin.assert_icommand(['imeta', 'rmw', '-d', data_object, '%', '%'], 'STDERR', ['CAT_NO_ACCESS_PERMISSION'])
-
-            # Show that using the admin option allows the administrator to detach metadata from
-            # another user's data objects they do not have permissions on.
-            self.admin.assert_icommand(['imeta', '-M', 'rmw', '-d', data_object, '%', '%'])
-
-            # As the owner of the data object, verify that the metadata is no longer attached to it.
-            self.user.assert_icommand(['imeta', 'ls', '-d', data_object], 'STDOUT', ['None\n'])
-
     def test_subcommand_rmi__issue_6124(self):
         attr_name  = 'issue_6124_attr'
         attr_value = 'issue_6124_value'
diff --git a/scripts/irods/test/test_imeta_error_handling.py b/scripts/irods/test/test_imeta_error_handling.py
index 25b152e008..972c8fa5d5 100644
--- a/scripts/irods/test/test_imeta_error_handling.py
+++ b/scripts/irods/test/test_imeta_error_handling.py
@@ -60,19 +60,6 @@ def test_imeta_too_many_args(self):
     def test_imeta_badcmd(self):
         self.admin.assert_icommand(['imeta', 'badcmd'], 'STDERR_SINGLELINE', 'Unrecognized subcommand', desired_rc=4)
 
-    def test_imeta_addw_insuff_args(self):
-        self.admin.assert_icommand(['imeta', 'addw', '-d'], 'STDERR_SINGLELINE', 'Not enough arguments provided', desired_rc=4)
-    def test_imeta_addw_no_args(self):
-        self.admin.assert_icommand(['imeta', 'addw'], 'STDERR_SINGLELINE', 'No object type descriptor', desired_rc=4)
-
-    def test_imeta_addw_return_code(self):
-        attr = 'test_imeta_addw_return_code_attr'
-        val = 'test_imeta_addw_return_code_val'
-        self.admin.assert_icommand(['imeta', 'addw', '-d', self.test_data_paths_wildcard, attr, val], 'STDOUT', desired_rc=0)
-        _, err, ec = self.admin.run_icommand(['imeta', 'addw', '-d', self.test_data_paths_wildcard, attr, val])
-        self.assertEqual(4, ec)
-        self.assertIn('CATALOG_ALREADY_HAS_ITEM_BY_THAT_NAME', err)
-
     def test_imeta_add_return_code(self):
         data_path = f'{self.test_data_path_base}0'
         attr = 'test_imeta_add_return_code_attr'
@@ -82,15 +69,6 @@ def test_imeta_add_return_code(self):
         self.assertEqual(4, ec)
         self.assertIn('CATALOG_ALREADY_HAS_ITEM_BY_THAT_NAME', err)
 
-    def test_imeta_addw_stderr_5184(self):
-        self.admin.assert_icommand(['imeta', 'addw', '-d', self.test_data_paths_wildcard, '5184_attr', '5184_attr'], 'STDOUT')
-        self.admin.assert_icommand(['imeta', 'addw', '-d', self.test_data_paths_wildcard, '5184_attr', '5184_attr'], 'EMPTY_STDOUT')
-
-    def test_imeta_rmw_insuff_args(self):
-        self.admin.assert_icommand(['imeta', 'rmw', '-d'], 'STDERR_SINGLELINE', 'Not enough arguments provided', desired_rc=4)
-    def test_imeta_rmw_no_args(self):
-        self.admin.assert_icommand(['imeta', 'rmw'], 'STDERR_SINGLELINE', 'No object type descriptor', desired_rc=4)
-
     def test_imeta_rmi_insuff_args(self):
         self.admin.assert_icommand(['imeta', 'rmi', '-d'], 'STDERR_SINGLELINE', 'Not enough arguments provided', desired_rc=4)
     def test_imeta_rmi_no_args(self):
@@ -112,46 +90,6 @@ def test_imeta_set_insuff_args(self):
     def test_imeta_set_no_args(self):
         self.admin.assert_icommand(['imeta', 'set'], 'STDERR_SINGLELINE', 'No object type descriptor', desired_rc=4)
 
-    def test_imeta_lsw_insuff_args(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-d'], 'STDERR_SINGLELINE', 'Not enough arguments provided', desired_rc=4)
-    def test_imeta_lsw_no_args(self):
-        self.admin.assert_icommand(['imeta', 'lsw'], 'STDERR_SINGLELINE', 'No object type descriptor', desired_rc=4)
-
-    def test_imeta_lsw_d_parse_toolong(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-d', self.long_name, '%'], 'STDERR_SINGLELINE', 'path too long', desired_rc=4)
-    def test_imeta_lsw_ld_parse_toolong(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-ld', self.long_name, '%'], 'STDERR_SINGLELINE', 'path too long', desired_rc=4)
-    def test_imeta_lsw_C_parse_toolong(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-C', self.long_name, '%'], 'STDERR_SINGLELINE', 'path too long', desired_rc=4)
-    def test_imeta_lsw_lC_parse_toolong(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-lC', self.long_name, '%'], 'STDERR_SINGLELINE', 'path too long', desired_rc=4)
-    def test_imeta_lsw_d_parse_waytoolong(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-d', self.longer_name, '%'], 'STDERR_SINGLELINE', 'path too long', desired_rc=4)
-    def test_imeta_lsw_ld_parse_waytoolong(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-ld', self.longer_name, '%'], 'STDERR_SINGLELINE', 'path too long', desired_rc=4)
-    def test_imeta_lsw_C_parse_waytoolong(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-C', self.longer_name, '%'], 'STDERR_SINGLELINE', 'path too long', desired_rc=4)
-    def test_imeta_lsw_lC_parse_waytoolong(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-lC', self.longer_name, '%'], 'STDERR_SINGLELINE', 'path too long', desired_rc=4)
-    def test_imeta_lsw_d_parse_badtrav(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-d', '../../../../../../../../../../../'], 'STDERR_SINGLELINE', 'parsing error', desired_rc=4)
-    def test_imeta_lsw_ld_parse_badtrav(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-ld', '../../../../../../../../../../../'], 'STDERR_SINGLELINE', 'parsing error', desired_rc=4)
-    def test_imeta_lsw_C_parse_badtrav(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-C', '../../../../../../../../../../../'], 'STDERR_SINGLELINE', 'parsing error', desired_rc=4)
-    def test_imeta_lsw_lC_parse_badtrav(self):
-        self.admin.assert_icommand(['imeta', 'lsw', '-lC', '../../../../../../../../../../../'], 'STDERR_SINGLELINE', 'parsing error', desired_rc=4)
-    def test_imeta_lsw_d_parse_rstrcpy(self):
-        self.admin.assert_icommand_fail(['imeta', 'lsw', '-d', self.long_name, '%'], 'STDERR_SINGLELINE', 'not enough space in dest')
-    def test_imeta_lsw_ld_parse_rstrcpy(self):
-        self.admin.assert_icommand_fail(['imeta', 'lsw', '-ld', self.longer_name, '%'], 'STDERR_SINGLELINE', 'not enough space in dest')
-    def test_imeta_lsw_C_parse_rstrcpy(self):
-        self.admin.assert_icommand_fail(['imeta', 'lsw', '-C', self.longer_name, '%'], 'STDERR_SINGLELINE', 'not enough space in dest')
-    def test_imeta_lsw_lC_parse_rstrcpy(self):
-        self.admin.assert_icommand_fail(['imeta', 'lsw', '-lC', self.longer_name, '%'], 'STDERR_SINGLELINE', 'not enough space in dest')
-    def test_imeta_lsw_lC_parse_skip(self):
-        self.admin.assert_icommand_fail(['imeta', 'lsw', '-lC', self.longer_name, '%'], 'STDERR_SINGLELINE', 'fullName could not be explicitly null terminated')
-
     def test_imeta_ls_d_nonexist(self):
         self.admin.assert_icommand(['imeta', 'ls', '-d', 'nonexist'], 'STDERR', 'does not exist', desired_rc=4)
     def test_imeta_ls_ld_nonexist(self):
diff --git a/scripts/irods/test/test_imeta_help.py b/scripts/irods/test/test_imeta_help.py
index 33df7fb343..22f5a89d9e 100644
--- a/scripts/irods/test/test_imeta_help.py
+++ b/scripts/irods/test/test_imeta_help.py
@@ -23,15 +23,11 @@ def test_imeta_help(self):
         env_without_valid_environment_file['IRODS_AUTHENTICATION_FILE'] = '/invalid'
         assert_command('imeta help', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
         assert_command('imeta help add', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
-        assert_command('imeta help adda', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
-        assert_command('imeta help addw', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
         assert_command('imeta help rm', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
-        assert_command('imeta help rmw', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
         assert_command('imeta help rmi', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
         assert_command('imeta help mod', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
         assert_command('imeta help set', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
         assert_command('imeta help ls', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
-        assert_command('imeta help lsw', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
         assert_command('imeta help qu', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
         assert_command('imeta help cp', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
         assert_command('imeta help upper', 'STDOUT_SINGLELINE', env=env_without_valid_environment_file)
diff --git a/scripts/irods/test/test_imeta_interactive.py b/scripts/irods/test/test_imeta_interactive.py
index 08d5d82a72..7adaa82989 100644
--- a/scripts/irods/test/test_imeta_interactive.py
+++ b/scripts/irods/test/test_imeta_interactive.py
@@ -188,7 +188,7 @@ def test_imeta_quoted_arguments__5518(self):
         A = 'test 5518 quoted arguments with spaces'
         V = 'c\td'
         U = 'e \tf'
-        self.imeta_p.stdin.write('adda -d {data_name} "{A}" "{V}" "{U}"\n'.format(**locals()).encode())
+        self.imeta_p.stdin.write('add -d {data_name} "{A}" "{V}" "{U}"\n'.format(**locals()).encode())
         self.imeta_p.stdin.flush()
         (out, _) = self.get_output()
         self.assertEqual(out, 'imeta>')
diff --git a/scripts/irods/test/test_imeta_set.py b/scripts/irods/test/test_imeta_set.py
index f3db8468da..0fc4cded02 100644
--- a/scripts/irods/test/test_imeta_set.py
+++ b/scripts/irods/test/test_imeta_set.py
@@ -29,12 +29,11 @@ def setUp(self):
             self.admin.assert_icommand('imeta ls -u ' + u, 'STDOUT_SINGLELINE', 'None')
 
     def tearDown(self):
-        usernames = [s.username for s in itertools.chain(self.admin_sessions, self.user_sessions)]
-
-        for u in usernames:
-            self.admin.run_icommand(['imeta', 'rmw', '-u', u, '%', '%', '%'])
-
+        # Parent class tearDown method will remove the users which had metadata annotated to them.
         super(Test_ImetaSet, self).tearDown()
+        # Clean up the now-unused metadata annotated to the users which have just been removed.
+        with session.make_session_for_existing_admin() as admin_session:
+            admin_session.run_icommand(["iadmin", "rum"])
 
     def mod_avu_value(self, user_name, a, v, u, newv):
         self.admin.assert_icommand('imeta mod -u %s %s %s %s v:%s' % (user_name, a, v, u, newv))
@@ -239,50 +238,6 @@ def test_imeta_with_many_bind_vars(self):
         command_str = '''imeta qu -d a in "1'{0}'v"'''.format("'1'" * num_extra_bind_vars)
         self.admin.assert_icommand(command_str, 'STDOUT_SINGLELINE', self.testfile)
 
-    def test_imeta_addw(self):
-        base_name = "file_";
-        for i in range(5):
-            object_name = base_name + str(i)
-            self.admin.assert_icommand(['iput', self.testfile, object_name])
-
-        self.admin.assert_icommand('ils', 'STDOUT_SINGLELINE', 'file_')
-
-        attribute = 'test_imeta_addw_attribute'
-        value = 'test_imeta_addw_value'
-        wild = base_name+"%"
-        self.admin.assert_icommand(['imeta', 'addw', '-d', wild, attribute, value], 'STDOUT_SINGLELINE', 'AVU added to 5 data-objects')
-
-        for i in range(5):
-            object_name = base_name + str(i)
-            self.admin.assert_icommand('imeta ls -d %s' % (object_name), 'STDOUT_SINGLELINE', ['attribute: ' + attribute])
-            self.admin.assert_icommand('imeta ls -d %s' % (object_name), 'STDOUT_SINGLELINE', ['value: ' + value])
-
-    def test_imeta_addw_for_other_user(self):
-        base_name = "file_";
-        for i in range(5):
-            object_name = base_name + str(i)
-            self.admin.assert_icommand(['iput', self.testfile, object_name])
-
-        self.admin.assert_icommand('ils', 'STDOUT_SINGLELINE', 'file_')
-
-        attribute = 'test_imeta_addw_attribute'
-        value = 'test_imeta_addw_value'
-        wild = self.admin.session_collection+'/'+base_name+"%"
-        self.user0.assert_icommand(['imeta', 'addw', '-d', wild, attribute, value], 'STDERR_SINGLELINE', 'CAT_NO_ACCESS_PERMISSION')
-
-    def test_imeta_addw_for_other_user_by_admin(self):
-        base_name = "file_";
-        for i in range(5):
-            object_name = base_name + str(i)
-            self.user0.assert_icommand(['iput', self.testfile, object_name])
-
-        self.user0.assert_icommand('ils', 'STDOUT_SINGLELINE', 'file_')
-
-        attribute = 'test_imeta_addw_attribute'
-        value = 'test_imeta_addw_value'
-        wild = self.user0.session_collection+'/'+base_name+"%"
-        self.admin.assert_icommand(['imeta', 'addw', '-d', wild, attribute, value], 'STDERR_SINGLELINE', 'CAT_NO_ACCESS_PERMISSION')
-
     def test_imeta_duplicate_attr_3787(self):
         self.admin.assert_icommand(['imeta', 'add', '-d', self.testfile, 'a1', 'v1', 'u1'])
         self.admin.assert_icommand(['imeta', 'mod', '-d', self.testfile, 'a1', 'v1', 'u1', 'n:a2', 'v:v2', 'u:'])
@@ -303,8 +258,12 @@ def test_mod_avu_msvc_4521(self):
         self.admin.assert_icommand(["iquest", "%s %s",
                                     "select META_DATA_ATTR_UNITS,count(DATA_ID) where DATA_NAME = '{0}'".format(self.testfile)],
                                     'STDOUT_MULTILINE', ['^u1 1$','^u2 1$'],use_regex=True)
-        self.admin.assert_icommand(['irule', '-v', '''msiModAVUMetadata("-d","{0}","rmw","a1","v1","%")'''.format(sescoln + '/' + self.testfile),'null','null'],
-          'STDOUT_SINGLELINE', "completed successfully")
+        self.admin.assert_icommand(
+            ['irule', '-v', '''msiModAVUMetadata("-d","{0}","rm","a1","v1","u1")'''.format(sescoln + '/' + self.testfile),'null','null'],
+            'STDOUT', "completed successfully")
+        self.admin.assert_icommand(
+            ['irule', '-v', '''msiModAVUMetadata("-d","{0}","rm","a1","v1","u2")'''.format(sescoln + '/' + self.testfile),'null','null'],
+            'STDOUT', "completed successfully")
         self.admin.assert_icommand(['iquest', "%s", "select count(META_DATA_ATTR_ID) where DATA_NAME = '{0}'".format(self.testfile)],
           'STDOUT_SINGLELINE',"^0$",use_regex=True)
 
@@ -321,9 +280,10 @@ def test_mod_avu_msvc_with_rodsuser_invoking_rule_4521(self):
                     printmeta("1",*b)
                     msiModAVUMetadata("-C", "*homeColln","set","a2","v2","u")
                     printmeta("2",*b)
-                    msiModAVUMetadata("-C", "*homeColln","rmw","%","%","")
+                    msiModAVUMetadata("-C", "*homeColln","rm","a1","v1","")
                     printmeta("3",*b)
-                    msiModAVUMetadata("-C", "*homeColln","rmw","%","%","%")
+                    msiModAVUMetadata("-C", "*homeColln","rm","a1","v1","x")
+                    msiModAVUMetadata("-C", "*homeColln","rm","a2","v2","u")
                     printmeta("4",*b)
                 }
                 printmeta(*id,*z) {
@@ -591,42 +551,3 @@ def create_object(_object_name, _is_data_object):
 
             finally:
                 self.admin.run_icommand(['irm', '-rf', src_object, dst_object])
-
-class Test_ImetaAdda(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
-
-    def setUp(self):
-        super(Test_ImetaAdda, self).setUp()
-        self.admin = self.admin_sessions[0]
-
-    def tearDown(self):
-        super(Test_ImetaAdda, self).tearDown()
-
-    @unittest.skipIf(test.settings.TOPOLOGY_FROM_RESOURCE_SERVER, 'rError messages do not return to the client after redirect to provider')
-    def test_use_of_adda_subcommand_produces_deprecation_message__issue_6187(self):
-        attr_name  = 'issue_6187_attr'
-        attr_value = 'issue_6187_value'
-        expected_avu_output = ['attribute: ' + attr_name, 'value: ' + attr_value]
-        expected_output = ['"adda" is deprecated. Please use "add" with admin mode enabled instead.']
-
-        # Show that the deprecation message appears for data objects.
-        self.admin.assert_icommand(['itouch', 'foo'])
-        self.admin.assert_icommand(['imeta', 'adda', '-d', 'foo', attr_name, attr_value], 'STDERR', expected_output)
-        self.admin.assert_icommand(['imeta', 'ls', '-d', 'foo'], 'STDOUT', expected_avu_output)
-
-        # Show that the deprecation message appears for collections.
-        self.admin.assert_icommand(['imeta', 'adda', '-C', '.', attr_name, attr_value], 'STDERR', expected_output)
-        self.admin.assert_icommand(['imeta', 'ls', '-C', '.'], 'STDOUT', expected_avu_output)
-
-        # Show that the deprecation message appears for users.
-        self.admin.assert_icommand(['imeta', 'adda', '-u', self.admin.username, attr_name, attr_value], 'STDERR', expected_output)
-        self.admin.assert_icommand(['imeta', 'ls', '-u', self.admin.username], 'STDOUT', expected_avu_output)
-
-        try:
-            # Show that the deprecation message appears for resources.
-            self.admin.assert_icommand(['imeta', 'adda', '-R', self.admin.default_resource, attr_name, attr_value], 'STDERR', expected_output)
-            self.admin.assert_icommand(['imeta', 'ls', '-R', self.admin.default_resource], 'STDOUT', expected_avu_output)
-
-        finally:
-            # Make sure the AVU is removed from the resource even if an assertion fails.
-            self.admin.run_icommand(['imeta', 'rm', '-R', self.admin.default_resource, attr_name, attr_value])
-
diff --git a/scripts/irods/test/test_prep_genquery_iterator.py b/scripts/irods/test/test_prep_genquery_iterator.py
index bdc2f868dc..2501674b67 100644
--- a/scripts/irods/test/test_prep_genquery_iterator.py
+++ b/scripts/irods/test/test_prep_genquery_iterator.py
@@ -259,7 +259,8 @@ def main(rule_args,callback,rei):
                 )
                 d0 = [ i for i in q ]
                 d1 = [ i for i in q.copy(options=Option.NO_DISTINCT) ]
-                callback.msiModAVUMetadata("-C",TestCollection,"rmw", "aa", "bb", "%")
+                callback.msiModAVUMetadata("-C",TestCollection,"rm", "aa", "bb", "cc")
+                callback.msiModAVUMetadata("-C",TestCollection,"rm", "aa", "bb", "dd")
                 b = repr([len(d0),len(d1)])
                 callback.writeLine('stdout',b)
                 callback.msiDeleteUnusedAVUs()
diff --git a/server/api/src/rsGeneralAdmin.cpp b/server/api/src/rsGeneralAdmin.cpp
index 7d2075436c..378a4b0ffa 100644
--- a/server/api/src/rsGeneralAdmin.cpp
+++ b/server/api/src/rsGeneralAdmin.cpp
@@ -178,6 +178,25 @@ namespace
             THROW(err, msg);
         }
     } // throw_if_password_is_being_set_on_a_group
+
+    auto get_value_for_rebalance_avu() -> std::string
+    {
+        char hostname[MAX_NAME_LEN];
+        gethostname(hostname, MAX_NAME_LEN);
+        return fmt::format("{}:{}", hostname, getpid());
+    } // get_value_for_rebalance_avu
+
+    auto get_unit_for_rebalance_avu() -> std::string
+    {
+        // get timestamp in defined format
+        const boost::posix_time::ptime timeUTC = boost::posix_time::second_clock::universal_time();
+        std::stringstream stream;
+        boost::posix_time::time_facet* facet = new boost::posix_time::time_facet();
+        facet->format("%Y%m%dT%H%M%SZ");
+        stream.imbue(std::locale(std::locale::classic(), facet));
+        stream << timeUTC;
+        return stream.str();
+    } // get_unit_for_rebalance_avu
 } // anonymous namespace
 
 int _check_rebalance_timestamp_avu_on_resource(
@@ -226,10 +245,11 @@ int _check_rebalance_timestamp_avu_on_resource(
     return 0;
 }
 
-int _set_rebalance_timestamp_avu_on_resource(
-    rsComm_t* _rsComm,
-    const std::string& _resource_name) {
-
+int _set_rebalance_timestamp_avu_on_resource(rsComm_t* _rsComm,
+                                             const std::string& _resource_name,
+                                             const std::string& _value,
+                                             const std::string& _unit)
+{
     modAVUMetadataInp_t modAVUMetadataInp{};
     irods::at_scope_exit<std::function<void()>> at_scope_exit{[&] {
         free( modAVUMetadataInp.arg0 );
@@ -239,49 +259,38 @@ int _set_rebalance_timestamp_avu_on_resource(
         free( modAVUMetadataInp.arg4 );
         free( modAVUMetadataInp.arg5 );
     }};
-    // get hostname
-    char hostname[MAX_NAME_LEN];
-    gethostname(hostname, MAX_NAME_LEN);
-    // get PID
-    int pid = getpid();
-    // get timestamp in defined format
-    const boost::posix_time::ptime timeUTC = boost::posix_time::second_clock::universal_time();
-    std::stringstream stream;
-    boost::posix_time::time_facet* facet = new boost::posix_time::time_facet();
-    facet->format("%Y%m%dT%H%M%SZ");
-    stream.imbue(std::locale(std::locale::classic(), facet));
-    stream << timeUTC;
     // add AVU to resource
     modAVUMetadataInp.arg0 = strdup( "set" );
     modAVUMetadataInp.arg1 = strdup( "-R" );
     modAVUMetadataInp.arg2 = strdup( _resource_name.c_str() );
     modAVUMetadataInp.arg3 = strdup( "rebalance_operation" );
-    std::string value = std::string(hostname) + std::string(":") + std::to_string(pid);
-    modAVUMetadataInp.arg4 = strdup( value.c_str() );
-    std::string unit = stream.str();
-    modAVUMetadataInp.arg5 = strdup( unit.c_str() );
+    modAVUMetadataInp.arg4 = strdup(_value.c_str());
+    modAVUMetadataInp.arg5 = strdup(_unit.c_str());
     // do it
     return rsModAVUMetadata( _rsComm, &modAVUMetadataInp );
 }
 
-int _check_and_set_rebalance_timestamp_avu_on_resource(
-    rsComm_t* _rsComm,
-    const std::string& _resource_name) {
-
+int _check_and_set_rebalance_timestamp_avu_on_resource(rsComm_t* _rsComm,
+                                                       const std::string& _resource_name,
+                                                       const std::string& _value,
+                                                       const std::string& _unit)
+{
     int status = _check_rebalance_timestamp_avu_on_resource(_rsComm, _resource_name);
     if (status < 0 ){
         return status;
     }
-    status = _set_rebalance_timestamp_avu_on_resource(_rsComm, _resource_name);
+    status = _set_rebalance_timestamp_avu_on_resource(_rsComm, _resource_name, _value, _unit);
     if (status < 0 ){
         return status;
     }
     return 0;
 }
 
-int _remove_rebalance_timestamp_avu_from_resource(
-    rsComm_t* _rsComm,
-    const std::string& _resource_name) {
+int _remove_rebalance_timestamp_avu_from_resource(rsComm_t* _rsComm,
+                                                  const std::string& _resource_name,
+                                                  const std::string& _value,
+                                                  const std::string& _unit)
+{
     // build genquery to find active or stale "rebalance operation" entries for this resource
 
     genQueryInp_t gen_inp{};
@@ -316,12 +325,12 @@ int _remove_rebalance_timestamp_avu_from_resource(
             free( modAVUMetadataInp.arg5 );
         }};
         // remove AVU from resource
-        modAVUMetadataInp.arg0 = strdup( "rmw" );
+        modAVUMetadataInp.arg0 = strdup("rm");
         modAVUMetadataInp.arg1 = strdup( "-R" );
         modAVUMetadataInp.arg2 = strdup( _resource_name.c_str() );
         modAVUMetadataInp.arg3 = strdup( "rebalance_operation" );
-        modAVUMetadataInp.arg4 = strdup( "%" );
-        modAVUMetadataInp.arg5 = strdup( "%" );
+        modAVUMetadataInp.arg4 = strdup(_value.c_str());
+        modAVUMetadataInp.arg5 = strdup(_unit.c_str());
         // do it
         return rsModAVUMetadata( _rsComm, &modAVUMetadataInp );
     }
@@ -330,7 +339,6 @@ int _remove_rebalance_timestamp_avu_from_resource(
     }
 }
 
-
 int
 rsGeneralAdmin( rsComm_t *rsComm, generalAdminInp_t *generalAdminInp ) {
     rodsServerHost_t *rodsServerHost;
@@ -1125,7 +1133,10 @@ int _rsGeneralAdmin(rsComm_t* rsComm, generalAdminInp_t* generalAdminInp)
                     status = ret.code();
                 }
                 else {
-                    int visibility_status = _check_and_set_rebalance_timestamp_avu_on_resource(rsComm, args[0]);
+                    const auto value = get_value_for_rebalance_avu();
+                    const auto unit = get_unit_for_rebalance_avu();
+                    int visibility_status =
+                        _check_and_set_rebalance_timestamp_avu_on_resource(rsComm, args[0], value, unit);
                     if (visibility_status < 0){
                         return visibility_status;
                     }
@@ -1138,7 +1149,7 @@ int _rsGeneralAdmin(rsComm_t* rsComm, generalAdminInp_t* generalAdminInp)
                         status = ret.code();
 
                     }
-                    visibility_status = _remove_rebalance_timestamp_avu_from_resource(rsComm, args[0]);
+                    visibility_status = _remove_rebalance_timestamp_avu_from_resource(rsComm, args[0], value, unit);
                     if (visibility_status < 0){
                         return visibility_status;
                     }
diff --git a/server/api/src/rsModAVUMetadata.cpp b/server/api/src/rsModAVUMetadata.cpp
index 79f6f8c9b7..cac3a11263 100644
--- a/server/api/src/rsModAVUMetadata.cpp
+++ b/server/api/src/rsModAVUMetadata.cpp
@@ -21,15 +21,6 @@ rsModAVUMetadata( rsComm_t *rsComm, modAVUMetadataInp_t *modAVUMetadataInp ) {
     if ( strcmp( modAVUMetadataInp->arg0, "add" ) == 0 ) {
         myHint = modAVUMetadataInp->arg2;
     }
-    else if ( strcmp( modAVUMetadataInp->arg0, "adda" ) == 0 ) {
-        myHint = modAVUMetadataInp->arg2;
-    }
-    else if ( strcmp( modAVUMetadataInp->arg0, "addw" ) == 0 ) {
-        myHint = modAVUMetadataInp->arg2;
-    }
-    else if ( strcmp( modAVUMetadataInp->arg0, "rmw" ) == 0 ) {
-        myHint = modAVUMetadataInp->arg2;
-    }
     else if ( strcmp( modAVUMetadataInp->arg0, "rmi" ) == 0 ) {
         myHint = modAVUMetadataInp->arg2;
     }
@@ -103,7 +94,7 @@ _rsModAVUMetadata( rsComm_t *rsComm, modAVUMetadataInp_t *modAVUMetadataInp ) {
         rei2.uoip = &rsComm->proxyUser;
     }
 
-    args[0] = modAVUMetadataInp->arg0; // option add, adda, rm, rmw, rmi, cp, mod, or set
+    args[0] = modAVUMetadataInp->arg0; // option add, rm, rmi, cp, mod, or set
     args[1] = modAVUMetadataInp->arg1; // item type -d,-D,-c,-C,-r,-R,-u,-U
     args[2] = modAVUMetadataInp->arg2; // item name
     args[3] = modAVUMetadataInp->arg3; // attr name
@@ -157,54 +148,6 @@ _rsModAVUMetadata( rsComm_t *rsComm, modAVUMetadataInp_t *modAVUMetadataInp ) {
                                    modAVUMetadataInp->arg5,
                                    &modAVUMetadataInp->condInput);
     }
-    else if ( strcmp( modAVUMetadataInp->arg0, "adda" ) == 0 ) {
-        const char* const msg = R"_("adda" is deprecated. Please use "add" with admin mode enabled instead.)_";
-        logger::api::debug("{} - {}", __func__, msg);
-        addRErrorMsg(&rsComm->rError, DEPRECATED_PARAMETER, msg);
-
-        // Determine if the client set the ADMIN_KW.
-        // If they haven't, set it and remember to remove it once the function returns.
-        // This keeps the key value pair's state clean.
-        //
-        // Setting the ADMIN_KW is required for proper operation of the "adda" subcommand.
-        const bool remove_admin_keyword = !getValByKey(&modAVUMetadataInp->condInput, ADMIN_KW);
-
-        if (remove_admin_keyword) {
-            addKeyVal(&modAVUMetadataInp->condInput, ADMIN_KW, "");
-        }
-
-        status = chlAddAVUMetadata(rsComm,
-                                   modAVUMetadataInp->arg1,
-                                   modAVUMetadataInp->arg2,
-                                   modAVUMetadataInp->arg3,
-                                   modAVUMetadataInp->arg4,
-                                   modAVUMetadataInp->arg5,
-                                   &modAVUMetadataInp->condInput);
-
-        if (remove_admin_keyword) {
-            // Restore the key value pair to its original state.
-            rmKeyVal(&modAVUMetadataInp->condInput, ADMIN_KW);
-        }
-    }
-    else if ( strcmp( modAVUMetadataInp->arg0, "addw" ) == 0 ) {
-        status = chlAddAVUMetadataWild(rsComm,
-                                       modAVUMetadataInp->arg1,
-                                       modAVUMetadataInp->arg2,
-                                       modAVUMetadataInp->arg3,
-                                       modAVUMetadataInp->arg4,
-                                       modAVUMetadataInp->arg5,
-                                       &modAVUMetadataInp->condInput);
-    }
-    else if ( strcmp( modAVUMetadataInp->arg0, "rmw" ) == 0 ) {
-        status = chlDeleteAVUMetadata(rsComm, 1,
-                                      modAVUMetadataInp->arg1,
-                                      modAVUMetadataInp->arg2,
-                                      modAVUMetadataInp->arg3,
-                                      modAVUMetadataInp->arg4,
-                                      modAVUMetadataInp->arg5,
-                                      0,
-                                      &modAVUMetadataInp->condInput);
-    }
     else if ( strcmp( modAVUMetadataInp->arg0, "rmi" ) == 0 ) {
         status = chlDeleteAVUMetadata(rsComm, 2,
                                       modAVUMetadataInp->arg1,
diff --git a/server/core/include/irods/irods_database_constants.hpp b/server/core/include/irods/irods_database_constants.hpp
index e4415002ab..743c8ee2aa 100644
--- a/server/core/include/irods/irods_database_constants.hpp
+++ b/server/core/include/irods/irods_database_constants.hpp
@@ -49,7 +49,6 @@ namespace irods
     const std::string DATABASE_OP_MOD_RESC_FREESPACE( "database_mod_resc_freespace" );
     const std::string DATABASE_OP_REG_USER_RE( "database_reg_user_re" );
     const std::string DATABASE_OP_ADD_AVU_METADATA( "database_add_avu_metadata" );
-    const std::string DATABASE_OP_ADD_AVU_METADATA_WILD( "database_add_avu_metadata_wild" );
     const std::string DATABASE_OP_DEL_AVU_METADATA( "database_del_avu_metadata" );
     const std::string DATABASE_OP_SET_AVU_METADATA( "database_set_avu_metadata" );
     const std::string DATABASE_OP_COPY_AVU_METADATA( "database_copy_avu_metadata" );
diff --git a/server/icat/include/irods/icatHighLevelRoutines.hpp b/server/icat/include/irods/icatHighLevelRoutines.hpp
index c133176a55..79489c4cc9 100644
--- a/server/icat/include/irods/icatHighLevelRoutines.hpp
+++ b/server/icat/include/irods/icatHighLevelRoutines.hpp
@@ -80,9 +80,6 @@ int chlRegUserRE( rsComm_t *rsComm, userInfo_t *userInfo );
 int chlAddAVUMetadata( rsComm_t *rsComm, const char *type,
                        const char *name, const char *attribute, const char *value, const char *units,
                        const KeyValPair* condInput);
-int chlAddAVUMetadataWild( rsComm_t *rsComm, const char *type,
-                           const char *name, const char *attribute, const char *value, const char *units,
-                           const KeyValPair* condInput);
 int chlDeleteAVUMetadata( rsComm_t *rsComm, int option, const char *type,
                           const char *name, const char *attribute, const char *value, const char *units, int noCommit,
                           const KeyValPair* condInput);
diff --git a/server/icat/src/icatHighLevelRoutines.cpp b/server/icat/src/icatHighLevelRoutines.cpp
index 400bd97635..db535743c7 100644
--- a/server/icat/src/icatHighLevelRoutines.cpp
+++ b/server/icat/src/icatHighLevelRoutines.cpp
@@ -2389,67 +2389,6 @@ int chlSetAVUMetadata(
 
 } // chlSetAVUMetadata
 
-// =-=-=-=-=-=-=-
-// Add an Attribute-Value [Units] pair/triple metadata item to one or
-// more data objects.  This is the Wildcard version, where the
-// collection/data-object name can match multiple objects).
-
-// The return value is error code (negative) or the number of objects
-// to which the AVU was associated.
-int chlAddAVUMetadataWild(
-    rsComm_t*         _comm,
-    const char*       _type,
-    const char*       _name,
-    const char*       _attribute,
-    const char*       _value,
-    const char*       _units,
-    const KeyValPair* _cond_input)
-{
-    // =-=-=-=-=-=-=-
-    // call factory for database object
-    irods::database_object_ptr db_obj_ptr;
-    irods::error ret = irods::database_factory( database_plugin_type, db_obj_ptr );
-    if ( !ret.ok() ) {
-        irods::log( PASS( ret ) );
-        return ret.code();
-    }
-
-    // =-=-=-=-=-=-=-
-    // resolve a plugin for that object
-    irods::plugin_ptr db_plug_ptr;
-    ret = db_obj_ptr->resolve( irods::DATABASE_INTERFACE, db_plug_ptr );
-    if ( !ret.ok() ) {
-        irods::log( PASSMSG( "failed to resolve database interface", ret ) );
-        return ret.code();
-    }
-
-    // =-=-=-=-=-=-=-
-    // cast plugin and object to db and fco for call
-    irods::first_class_object_ptr ptr = boost::dynamic_pointer_cast < irods::first_class_object > ( db_obj_ptr );
-    irods::database_ptr db = boost::dynamic_pointer_cast < irods::database > ( db_plug_ptr );
-
-    // =-=-=-=-=-=-=-
-    // call the operation on the plugin
-    ret = db->call<
-          const char*,
-          const char*,
-          const char*,
-          const char*,
-          const char*,
-          const KeyValPair*> (
-              _comm,
-              irods::DATABASE_OP_ADD_AVU_METADATA_WILD,
-              ptr,
-              _type,
-              _name,
-              _attribute,
-              _value,
-              _units,
-              _cond_input);
-
-    return ret.code();
-} // chlAddAVUMetadataWild
-
 // =-=-=-=-=-=-=-
 // Add an Attribute-Value [Units] pair/triple metadata item to an object
 int chlAddAVUMetadata(
diff --git a/server/re/src/extractAvuMS.cpp b/server/re/src/extractAvuMS.cpp
index 391f89ee69..71cd3270fb 100644
--- a/server/re/src/extractAvuMS.cpp
+++ b/server/re/src/extractAvuMS.cpp
@@ -684,7 +684,6 @@ msiRemoveKeyValuePairsFromObj( msParam_t *metadataParam, msParam_t* objParam,
  *  \li "add"
  *  \li "set"
  *  \li "rm"
- *  \li "rmw"
  *
  * \usage See clients/icommands/test/rules/
  *
@@ -701,7 +700,7 @@ msiRemoveKeyValuePairsFromObj( msParam_t *metadataParam, msParam_t* objParam,
  * \DolVarDependence none
  * \DolVarModified none
  * \iCatAttrDependence none
- * \iCatAttrModified AVU triples add / set / rm / rmw (rm-with-wildcard)
+ * \iCatAttrModified AVU triples add / set / rm
  * \sideeffect none
  *
  * \return integer
@@ -711,7 +710,7 @@ msiRemoveKeyValuePairsFromObj( msParam_t *metadataParam, msParam_t* objParam,
  * \pre none
  * \post none
  * \sa rsModAVUMetadata
-**/
+ **/
 int
 msiModAVUMetadata(
     msParam_t* _item_type,
@@ -732,7 +731,7 @@ msiModAVUMetadata(
         return SYS_INVALID_INPUT_PARAM;
     }
 
-    static const std::vector<std::string> v { "add", "set", "rm", "rmw" };
+    static const std::vector<std::string> v{"add", "set", "rm"};
     char *op_str = parseMspForStr( _avu_op );
     if( !op_str || std::find(v.begin(), v.end(), op_str) == v.end()) {
         return SYS_INVALID_INPUT_PARAM;


Reviews:
user: login: korydraughn
body: Looks good.

Waiting for test results before approval.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T21:16:21Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T21:23:38Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T21:27:56Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T21:31:31Z
user: login: trel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T21:34:11Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T21:57:41Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T21:58:55Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T22:01:44Z
user: login: korydraughn
body: Squash to taste and pound it.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T22:10:18Z

Review comments:
diff_hunk: @@ -2138,27 +2140,30 @@ def tearDown(self):
 
     # Issue 3862:  test that CAT_STATEMENT_TABLE_FULL is not encountered during parallel rebalance
     def test_rebalance__ticket_3862(self):
-
+        resource_name = "repl"
         def check_and_remove_rebalance_visibility_metadata(attempts):
             print("checking rebalance visibility metadata [{0} attempts remaining]".format(attempts))
-            self.assertTrue(attempts != 0, msg="rebalance visibility metadata not in catalog")
-            out,_,_ = self.admin.run_icommand(['imeta','ls','-R','repl'])
-            if "rebalance_operation" not in out:
-                check_and_remove_rebalance_visibility_metadata(attempts - 1)
-            else:
-                self.admin.assert_icommand("imeta rmw -R repl rebalance_operation % %") # b/c #3683
+            self.assertGreater(attempts, 0, msg="rebalance visibility metadata not in catalog")
+            resource_metadata_query = "select META_RESC_ATTR_NAME, META_RESC_ATTR_VALUE, META_RESC_ATTR_UNITS " \
+                                      f"where RESC_NAME = '{resource_name}'"
+            resource_avus = json.loads(self.admin.run_icommand(["iquery", resource_metadata_query])[0].strip())
path: scripts/irods/test/test_iadmin.py
user: login: korydraughn
body: Just want to put on your radar that GenQuery2 limits the resultset to 256 rows when the query doesn't define a limit.
created_at: 2025-03-07T21:03:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,12 +325,12 @@ int _remove_rebalance_timestamp_avu_from_resource(
             free( modAVUMetadataInp.arg5 );
         }};
         // remove AVU from resource
-        modAVUMetadataInp.arg0 = strdup( "rmw" );
+        modAVUMetadataInp.arg0 = strdup("rm");
         modAVUMetadataInp.arg1 = strdup( "-R" );
         modAVUMetadataInp.arg2 = strdup( _resource_name.c_str() );
         modAVUMetadataInp.arg3 = strdup( "rebalance_operation" );
-        modAVUMetadataInp.arg4 = strdup( "%" );
-        modAVUMetadataInp.arg5 = strdup( "%" );
path: server/api/src/rsGeneralAdmin.cpp
user: login: korydraughn
body: The original removed many AVUs. Does the new code still cover that?

Another question is - should it remove multiple AVUs?
created_at: 2025-03-07T21:14:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2138,27 +2140,30 @@ def tearDown(self):
 
     # Issue 3862:  test that CAT_STATEMENT_TABLE_FULL is not encountered during parallel rebalance
     def test_rebalance__ticket_3862(self):
-
+        resource_name = "repl"
         def check_and_remove_rebalance_visibility_metadata(attempts):
             print("checking rebalance visibility metadata [{0} attempts remaining]".format(attempts))
-            self.assertTrue(attempts != 0, msg="rebalance visibility metadata not in catalog")
-            out,_,_ = self.admin.run_icommand(['imeta','ls','-R','repl'])
-            if "rebalance_operation" not in out:
-                check_and_remove_rebalance_visibility_metadata(attempts - 1)
-            else:
-                self.admin.assert_icommand("imeta rmw -R repl rebalance_operation % %") # b/c #3683
+            self.assertGreater(attempts, 0, msg="rebalance visibility metadata not in catalog")
+            resource_metadata_query = "select META_RESC_ATTR_NAME, META_RESC_ATTR_VALUE, META_RESC_ATTR_UNITS " \
+                                      f"where RESC_NAME = '{resource_name}'"
+            resource_avus = json.loads(self.admin.run_icommand(["iquery", resource_metadata_query])[0].strip())
path: scripts/irods/test/test_iadmin.py
user: login: alanking
body: Gotcha. If this ever has more than one AVU on it, then that's actually a problem. Perhaps I can insert an assertion to make sure of that here and we'll be good.
created_at: 2025-03-07T21:23:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,12 +325,12 @@ int _remove_rebalance_timestamp_avu_from_resource(
             free( modAVUMetadataInp.arg5 );
         }};
         // remove AVU from resource
-        modAVUMetadataInp.arg0 = strdup( "rmw" );
+        modAVUMetadataInp.arg0 = strdup("rm");
         modAVUMetadataInp.arg1 = strdup( "-R" );
         modAVUMetadataInp.arg2 = strdup( _resource_name.c_str() );
         modAVUMetadataInp.arg3 = strdup( "rebalance_operation" );
-        modAVUMetadataInp.arg4 = strdup( "%" );
-        modAVUMetadataInp.arg5 = strdup( "%" );
path: server/api/src/rsGeneralAdmin.cpp
user: login: alanking
body: I actually think this is more correct than removing all of the rebalance_operation AVUs. If multiple rebalance_operation AVUs appear on a resource, that means that multiple rebalance operations were started and each annotated its own metadata, which is a bug. The whole point of the AVU is to prevent multiple rebalances from occurring, so if that ever happens, this at least makes the bug more visible. Thoughts?
created_at: 2025-03-07T21:27:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,12 +325,12 @@ int _remove_rebalance_timestamp_avu_from_resource(
             free( modAVUMetadataInp.arg5 );
         }};
         // remove AVU from resource
-        modAVUMetadataInp.arg0 = strdup( "rmw" );
+        modAVUMetadataInp.arg0 = strdup("rm");
         modAVUMetadataInp.arg1 = strdup( "-R" );
         modAVUMetadataInp.arg2 = strdup( _resource_name.c_str() );
         modAVUMetadataInp.arg3 = strdup( "rebalance_operation" );
-        modAVUMetadataInp.arg4 = strdup( "%" );
-        modAVUMetadataInp.arg5 = strdup( "%" );
path: server/api/src/rsGeneralAdmin.cpp
user: login: alanking
body: @trel may have more insight into this. Here's the original issue from which this feature came: https://github.com/irods/irods/issues/3683

I don't see any specific requirements around removing all of the rebalance_operation AVUs once a rebalance completes. Again, the fact that multiple rebalances can run simultaneously is itself a bug.
created_at: 2025-03-07T21:31:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,12 +325,12 @@ int _remove_rebalance_timestamp_avu_from_resource(
             free( modAVUMetadataInp.arg5 );
         }};
         // remove AVU from resource
-        modAVUMetadataInp.arg0 = strdup( "rmw" );
+        modAVUMetadataInp.arg0 = strdup("rm");
         modAVUMetadataInp.arg1 = strdup( "-R" );
         modAVUMetadataInp.arg2 = strdup( _resource_name.c_str() );
         modAVUMetadataInp.arg3 = strdup( "rebalance_operation" );
-        modAVUMetadataInp.arg4 = strdup( "%" );
-        modAVUMetadataInp.arg5 = strdup( "%" );
path: server/api/src/rsGeneralAdmin.cpp
user: login: trel
body: Correct - the AVU itself is the 'lock' for a rebalance running.  So there should / cannot be more than one.
created_at: 2025-03-07T21:34:11Z
author_association: MEMBER
diff_hunk: @@ -2138,27 +2140,30 @@ def tearDown(self):
 
     # Issue 3862:  test that CAT_STATEMENT_TABLE_FULL is not encountered during parallel rebalance
     def test_rebalance__ticket_3862(self):
-
+        resource_name = "repl"
         def check_and_remove_rebalance_visibility_metadata(attempts):
             print("checking rebalance visibility metadata [{0} attempts remaining]".format(attempts))
-            self.assertTrue(attempts != 0, msg="rebalance visibility metadata not in catalog")
-            out,_,_ = self.admin.run_icommand(['imeta','ls','-R','repl'])
-            if "rebalance_operation" not in out:
-                check_and_remove_rebalance_visibility_metadata(attempts - 1)
-            else:
-                self.admin.assert_icommand("imeta rmw -R repl rebalance_operation % %") # b/c #3683
+            self.assertGreater(attempts, 0, msg="rebalance visibility metadata not in catalog")
+            resource_metadata_query = "select META_RESC_ATTR_NAME, META_RESC_ATTR_VALUE, META_RESC_ATTR_UNITS " \
+                                      f"where RESC_NAME = '{resource_name}'"
+            resource_avus = json.loads(self.admin.run_icommand(["iquery", resource_metadata_query])[0].strip())
path: scripts/irods/test/test_iadmin.py
user: login: korydraughn
body: Sure. Doesn't hurt.
created_at: 2025-03-07T21:57:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,12 +325,12 @@ int _remove_rebalance_timestamp_avu_from_resource(
             free( modAVUMetadataInp.arg5 );
         }};
         // remove AVU from resource
-        modAVUMetadataInp.arg0 = strdup( "rmw" );
+        modAVUMetadataInp.arg0 = strdup("rm");
         modAVUMetadataInp.arg1 = strdup( "-R" );
         modAVUMetadataInp.arg2 = strdup( _resource_name.c_str() );
         modAVUMetadataInp.arg3 = strdup( "rebalance_operation" );
-        modAVUMetadataInp.arg4 = strdup( "%" );
-        modAVUMetadataInp.arg5 = strdup( "%" );
path: server/api/src/rsGeneralAdmin.cpp
user: login: korydraughn
body: All of that sounds great.

Resolving.
created_at: 2025-03-07T21:58:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2138,27 +2140,30 @@ def tearDown(self):
 
     # Issue 3862:  test that CAT_STATEMENT_TABLE_FULL is not encountered during parallel rebalance
     def test_rebalance__ticket_3862(self):
-
+        resource_name = "repl"
         def check_and_remove_rebalance_visibility_metadata(attempts):
             print("checking rebalance visibility metadata [{0} attempts remaining]".format(attempts))
-            self.assertTrue(attempts != 0, msg="rebalance visibility metadata not in catalog")
-            out,_,_ = self.admin.run_icommand(['imeta','ls','-R','repl'])
-            if "rebalance_operation" not in out:
-                check_and_remove_rebalance_visibility_metadata(attempts - 1)
-            else:
-                self.admin.assert_icommand("imeta rmw -R repl rebalance_operation % %") # b/c #3683
+            self.assertGreater(attempts, 0, msg="rebalance visibility metadata not in catalog")
+            resource_metadata_query = "select META_RESC_ATTR_NAME, META_RESC_ATTR_VALUE, META_RESC_ATTR_UNITS " \
+                                      f"where RESC_NAME = '{resource_name}'"
+            resource_avus = json.loads(self.admin.run_icommand(["iquery", resource_metadata_query])[0].strip())
path: scripts/irods/test/test_iadmin.py
user: login: alanking
body: Done. Test passed
created_at: 2025-03-07T22:01:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: alanking
created_at: 2025-03-07T21:18:07Z
body: Core tests passed. Taking this out of draft.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-03-07T21:59:37Z
body: Will circle back to this once that final review comment is resolved.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-03-07T22:12:19Z
body: Consider mentioning that admins should not rely on the db PEP for policy as it is not guaranteed to exist in later versions.

For 4f17d56.
author_association: CONTRIBUTOR
user: login: alanking
created_at: 2025-03-07T22:19:04Z
body: > Consider mentioning that admins should not rely on the db PEP for policy as it is not guaranteed to exist in later versions.
> 
> For [4f17d56](https://github.com/irods/irods/commit/4f17d56284952b46e727c7cb9dcce194566ab0eb).

Well, the (worst?) thing about that is... `rmw` doesn't have a dedicated database PEP, but `addw` does (or... did, I guess). :( It is hidden in the `option` parameter for `db_del_avu_metadata_op`:
https://github.com/irods/irods/blob/a73a90cf0ba46c86f7cb6e9c5eae7e132faeb147/server/icat/src/icatHighLevelRoutines.cpp#L2573-L2576

So... we won't be removing a database PEP but rather the `1` value for `option` in that PEP.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-03-07T22:21:55Z
body: Gotcha. Forgot that's how that worked.

Is that part of this PR or is it planned for a later one?
author_association: CONTRIBUTOR
user: login: alanking
created_at: 2025-03-07T22:24:04Z
body: I was not planning to remove that as part of this PR because it's being used internally by the `set` operation to clear all the AVUs with a particular attribute before setting a new one with a particular value/units. I don't think re-implementing `set` is in scope for the removal of `rmw` from the ModAVUMetadata API. Thoughts?
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-03-07T22:34:38Z
body: Agreed.

Let's capture that in an issue for later. We should avoid triggering policy related to removed APIs if possible. As you said, that's out of scope for this PR though.
author_association: CONTRIBUTOR
user: login: alanking
created_at: 2025-03-07T23:26:54Z
body: Created https://github.com/irods/irods/issues/8266 for that.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-03-07T23:32:35Z
body: Pound and merge it.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-03-07T23:33:05Z
body: Oops, noticed a leftover commit that needs to be squashed.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-03-07T23:33:35Z
body: See commit 4891b5a
author_association: CONTRIBUTOR
user: login: alanking
created_at: 2025-03-08T00:10:40Z
body: Squashed, #'d, mergin
author_association: CONTRIBUTOR