From c3fbd49fbd4e1293e4827f97b8f70efff079cf57 Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Fri, 16 Jun 2023 10:05:37 -0700 Subject: [PATCH] Issue 5798 - CLI - Add multi-valued support to dsconf config (#5799) Description: Currently, we have two editable multi-valued attributes in cn=config: nsslapd-haproxy-trusted-ip and nsslapd-referral. Our current cn=config implementation doesn't support bunch ADD operations. Make our CLI tools more robust so they can handle multi-valued attributes correctly. Add add_many method to DSLdapObject. Fixes: https://github.com/389ds/389-ds-base/issues/5798 Reviewed by: @mreynolds389 (Thanks!) --- src/lib389/lib389/_mapped_object.py | 37 +++++++++--- src/lib389/lib389/cli_conf/config.py | 90 +++++++++++++++++++++++++--- 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py index 1e36b18399..b7391d8cce 100644 --- a/src/lib389/lib389/_mapped_object.py +++ b/src/lib389/lib389/_mapped_object.py @@ -313,6 +313,29 @@ def add(self, key, value): self.set(key, value, action=ldap.MOD_ADD) + def add_many(self, *args): + """Add many key, value pairs in a single operation. + This is useful for configuration changes that require + atomic operation, and ease of use. + + An example of usage is add_many((key, value), (key, value)) + + No wrapping list is needed for the arguments. + + :param *args: tuples of key,value to replace. + :type *args: (str, str) + """ + + mods = [] + for arg in args: + if isinstance(arg[1], list) or isinstance(arg[1], tuple): + value = ensure_list_bytes(arg[1]) + else: + value = [ensure_bytes(arg[1])] + mods.append((ldap.MOD_ADD, ensure_str(arg[0]), value)) + return _modify_ext_s(self._instance,self._dn, mods, serverctrls=self._server_controls, + clientctrls=self._client_controls, escapehatch='i am sure') + # Basically what it means; def replace(self, key, value): """Replace an attribute with a value @@ -748,7 +771,7 @@ def get_attr_vals_bytes(self, key, use_json=False): :param key: An attribute name :type key: str - :returns: A single bytes value + :returns: A list of bytes values :raises: ValueError - if instance is offline """ @@ -759,7 +782,7 @@ def get_attr_val_utf8(self, key, use_json=False): :param key: An attribute name :type key: str - :returns: A single bytes value + :returns: A single UTF8 value :raises: ValueError - if instance is offline """ @@ -770,7 +793,7 @@ def get_attr_val_utf8_l(self, key, use_json=False): :param key: An attribute name :type key: str - :returns: A single bytes value + :returns: A single lowered UTF8 value :raises: ValueError - if instance is offline """ @@ -785,7 +808,7 @@ def get_attr_vals_utf8(self, key, use_json=False): :param key: An attribute name :type key: str - :returns: A single bytes value + :returns: A list of UTF8 values :raises: ValueError - if instance is offline """ @@ -796,7 +819,7 @@ def get_attr_vals_utf8_l(self, key, use_json=False): :param key: An attribute name :type key: str - :returns: A single bytes value + :returns: A list of lowered UTF8 values :raises: ValueError - if instance is offline """ @@ -807,7 +830,7 @@ def get_attr_val_int(self, key, use_json=False): :param key: An attribute name :type key: str - :returns: A single bytes value + :returns: A single int value :raises: ValueError - if instance is offline """ @@ -818,7 +841,7 @@ def get_attr_vals_int(self, key, use_json=False): :param key: An attribute name :type key: str - :returns: A single bytes value + :returns: A list of int values :raises: ValueError - if instance is offline """ diff --git a/src/lib389/lib389/cli_conf/config.py b/src/lib389/lib389/cli_conf/config.py index ce38bc1ebe..6fbf54ed32 100644 --- a/src/lib389/lib389/cli_conf/config.py +++ b/src/lib389/lib389/cli_conf/config.py @@ -1,20 +1,62 @@ # --- BEGIN COPYRIGHT BLOCK --- -# Copyright (C) 2018 Red Hat, Inc. +# Copyright (C) 2023 Red Hat, Inc. # All rights reserved. # # License: GPL (version 3 or any later version). # See LICENSE for details. # --- END COPYRIGHT BLOCK --- +import ldap +from enum import Enum from lib389.config import Config from lib389.cli_base import ( _generic_get_entry, _generic_get_attr, - _generic_add_attr, _generic_replace_attr, - _generic_del_attr, ) +OpType = Enum("OpType", "add delete") + + +def _config_get_existing_attrs(conf, args, op_type): + """Get the existing attribute from the server and return them in a dict + so we can add them back after the operation is done. + + For op_type == OpType.delete, we delete them from the server so we can add + back only those that are not specified in the command line. + (i.e delete nsslapd-haproxy-trusted-ip="192.168.0.1", but + nsslapd-haproxy-trusted-ip has 192.168.0.1 and 192.168.0.2 values. + So we want only 192.168.0.1 to be deleted in the end) + """ + + existing_attrs = {} + if args and args.attr: + for attr in args.attr: + if "=" in attr: + [attr_name, val] = attr.split("=", 1) + # We should process only multi-valued attributes this way + if attr_name.lower() == "nsslapd-haproxy-trusted-ip" or \ + attr_name.lower() == "nsslapd-referral": + if attr_name not in existing_attrs.keys(): + existing_attrs[attr_name] = conf.get_attr_vals_utf8(attr_name) + existing_attrs[attr_name] = [x for x in existing_attrs[attr_name] if x != val] + + if op_type == OpType.add: + if existing_attrs[attr_name] == []: + del existing_attrs[attr_name] + + if op_type == OpType.delete: + conf.remove_all(attr_name) + else: + if op_type == OpType.delete: + conf.remove_all(attr) + else: + raise ValueError(f"You must specify a value to add for the attribute ({attr_name})") + return existing_attrs + else: + # Missing value + raise ValueError(f"Missing attribute to {op_type.name}") + def config_get(inst, basedn, log, args): if args and args.attrs: @@ -24,10 +66,6 @@ def config_get(inst, basedn, log, args): _generic_get_entry(inst, basedn, log.getChild('config_get'), Config, args) -def config_add_attr(inst, basedn, log, args): - _generic_add_attr(inst, basedn, log.getChild('config_add_attr'), Config, args) - - def config_replace_attr(inst, basedn, log, args): _generic_replace_attr(inst, basedn, log.getChild('config_replace_attr'), Config, args) @@ -39,8 +77,44 @@ def config_replace_attr(inst, basedn, log, args): _generic_replace_attr(inst, basedn, log.getChild('config_get'), Config, args) +def config_add_attr(inst, basedn, log, args): + conf = Config(inst, basedn) + final_mods = [] + + existing_attrs = _config_get_existing_attrs(conf, args, OpType.add) + + if args and args.attr: + for attr in args.attr: + if "=" in attr: + [attr_name, val] = attr.split("=", 1) + if attr_name in existing_attrs: + for v in existing_attrs[attr_name]: + final_mods.append((attr_name, v)) + final_mods.append((attr_name, val)) + try: + conf.add_many(*set(final_mods)) + except ldap.TYPE_OR_VALUE_EXISTS: + pass + else: + raise ValueError(f"You must specify a value to add for the attribute ({attr_name})") + else: + # Missing value + raise ValueError("Missing attribute to add") + + _config_display_ldapimaprootdn_warning(log, args) + + def config_del_attr(inst, basedn, log, args): - _generic_del_attr(inst, basedn, log.getChild('config_del_attr'), Config, args) + conf = Config(inst, basedn) + final_mods = [] + + existing_attrs = _config_get_existing_attrs(conf, args, OpType.delete) + + # Then add the attributes back all except the one we need to remove + for attr_name in existing_attrs.keys(): + for val in existing_attrs[attr_name]: + final_mods.append((attr_name, val)) + conf.add_many(*set(final_mods)) def create_parser(subparsers):