Skip to content

Commit

Permalink
Issue 5798 - CLI - Add multi-valued support to dsconf config (#5799)
Browse files Browse the repository at this point in the history
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: #5798

Reviewed by: @mreynolds389 (Thanks!)
  • Loading branch information
droideck committed Jan 11, 2024
1 parent b82b8f3 commit c3fbd49
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 15 deletions.
37 changes: 30 additions & 7 deletions src/lib389/lib389/_mapped_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
"""

Expand All @@ -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
"""

Expand All @@ -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
"""

Expand All @@ -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
"""

Expand All @@ -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
"""

Expand All @@ -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
"""

Expand All @@ -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
"""

Expand Down
90 changes: 82 additions & 8 deletions src/lib389/lib389/cli_conf/config.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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)

Expand All @@ -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):
Expand Down

0 comments on commit c3fbd49

Please sign in to comment.