2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-09-02 15:36:05 +00:00

Add server-side confluentdbutil showattrib subcommand

Adds `confluentdbutil showattrib <noderange> <attribute>...` to print the
node attribute.

In contrast to nodeattrib it can shows secrets and crypted values with -u flag.
It's server-side only: reads the config store and master key directly, never over
the API.
It's read-only and works without confluentd running.
This commit is contained in:
Markus Hilger
2026-07-08 19:55:18 +02:00
parent 1feec98edf
commit 6f11dffae8
4 changed files with 148 additions and 5 deletions
+31 -3
View File
@@ -4,6 +4,7 @@ confluentdbutil(8) -- Backup or restore confluent database
## SYNOPSIS
`confluentdbutil [options] [dump|restore] <path>`
`confluentdbutil [-u] [-v] showattrib <noderange> <attribute>...`
## DESCRIPTION
@@ -13,6 +14,15 @@ In order to perform restore, the confluent service must not be running. It
is required to indicate how the usernames/passwords are treated in
the json files (password protected, removed from the files, or unprotected).
The `showattrib` subcommand prints the stored value of any attribute for the
given noderange, reading directly from the configuration store. Because it
does not talk to the confluent daemon, it can be used to read attribute values
even while confluent is stopped. It must be run directly on the confluent
server as root. By default, `secret.*` and `crypted.*` values are masked as
`********`, just as nodeattrib(8) shows them. With `-u`, they are revealed:
`secret.*` values decrypted to plaintext and `crypted.*` values as their
stored one-way hashes.
## OPTIONS
* `-p PASSWORD`, `--password=PASSWORD`:
@@ -27,7 +37,10 @@ the json files (password protected, removed from the files, or unprotected).
than including them.
* `-u`, `--unprotected`:
The keys.json file will include the encryption keys without any protection.
With `dump`, the keys.json file will include the encryption keys without
any protection. With `showattrib`, show `secret.*` values decrypted to
plaintext and `crypted.*` values as their stored hashes rather than
masking them as `********`.
* `-s`, `--skipkeys`:
This specifies to dump the encrypted data without
@@ -40,6 +53,21 @@ the json files (password protected, removed from the files, or unprotected).
* `-y`, `--yaml`:
Use YAML instead of JSON as file format
* `-h`, `--help`:
* `-v`, `--value-only`:
With `showattrib`, print only the values, without node and attribute names
(useful for scripting).
* `-h`, `--help`:
Show help message and exit
## EXAMPLES
* Show the decrypted BMC password confluent uses for a node (run on the
server as root):
`# confluentdbutil -u showattrib n1 secret.hardwaremanagementpassword`
`n1: secret.hardwaremanagementpassword: mypassword123`
* Get just the value, for use in a script:
`# confluentdbutil -u -v showattrib n1 secret.hardwaremanagementpassword`
`mypassword123`
@@ -38,6 +38,10 @@ If the word all is specified, then all available attributes are given.
Omitting any attribute name or the word 'all' will display only attributes
that are currently set.
The values of `secret.*` and `crypted.*` attributes are never shown here; they
can only be retrieved on the confluent server with
`confluentdbutil -u showattrib`.
For the `groups` attribute, it is possible to add a group by doing
`groups,=<newgroup>` and to remove by doing `groups^=<oldgroup>`
@@ -23,6 +23,10 @@ node after using the nodeattrib(8) command will not have attributes change autom
It's easiest to see by using the `nodeattrib <noderange> -b` to understand how
the attributes are set on the node versus a group to which a node belongs.
The values of `secret.*` and `crypted.*` attributes are never shown here; they
can only be retrieved on the confluent server with
`confluentdbutil -u showattrib`.
## OPTIONS
* `-b`, `--blame`:
+109 -2
View File
@@ -29,9 +29,11 @@ if path.startswith('/opt'):
import confluent.config.configmanager as cfm
import confluent.config.conf as conf
import confluent.main as main
import confluent.noderange as noderange
argparser = optparse.OptionParser(
usage="Usage: %prog [options] [dump|restore|merge] [path]")
usage="Usage: %prog [options] [dump|restore|merge] [path]\n"
" %prog [-u] [-v] showattrib <noderange> <attribute>...")
argparser.add_option('-p', '--password',
help='Password to use to protect/unlock a protected dump')
argparser.add_option('-i', '--interactivepassword', help='Prompt for password',
@@ -42,7 +44,10 @@ argparser.add_option('-u', '--unprotected', action='store_true',
help='Specify that no password should be used to protect'
' the key information. Fields will be encrypted, '
'but keys.json will contain unencrypted decryption'
' keys that may be used to read the dump')
' keys that may be used to read the dump. With '
'showattrib, show secret.* values decrypted to '
'plaintext and crypted.* values as their stored '
'hashes rather than masking them')
argparser.add_option('-s', '--skipkeys', action='store_true',
help='This specifies to dump the encrypted data without '
'dumping the keys needed to decrypt it. This is '
@@ -53,7 +58,109 @@ argparser.add_option('-s', '--skipkeys', action='store_true',
'they do not require incremental backup')
argparser.add_option('-y', '--yaml', action='store_true',
help='Use YAML instead of JSON as file format')
argparser.add_option('-v', '--value-only', dest='valueonly',
action='store_true',
help='With showattrib, print only the values '
'without node and attribute names')
(options, args) = argparser.parse_args()
def _dispval(value):
# Render a single scalar for display: decode bytes (e.g. decrypted
# secrets) and stringify anything else, including non-string values
if isinstance(value, bytes):
return value.decode('utf-8', errors='replace')
return str(value)
def show_attrib(options, args):
if len(args) < 3:
sys.stderr.write('Usage: confluentdbutil showattrib [-u] [-v] '
'<noderange> <attribute> [<attribute>...]\n')
sys.exit(1)
nr = args[1]
requested = args[2:]
# Load the config store and unlock the master key, mirroring the daemon
# startup sequence in confluent.main. _initsecurity handles the
# externalcfgkey case (key protected by the contents of a key file);
# otherwise the key is stored unprotected and init_masterkey unlocks it
# without any password. autogen=False keeps this read-only (never create a
# key).
cfm.init()
try:
main._initsecurity(conf.get_config())
if not cfm._masterkey:
cfm.init_masterkey(autogen=False)
except Exception:
sys.stderr.write("Error unlocking credential store\n")
sys.exit(1)
if not cfm._masterkey:
sys.stderr.write("Error unlocking credential store\n")
sys.exit(1)
cfg = cfm.ConfigManager(None)
try:
nodes = noderange.NodeRange(nr, config=cfg).nodes
except Exception as e:
sys.stderr.write("Invalid Noderange: " + str(e) + "\n")
sys.exit(1)
attrs = cfg.get_node_attributes(list(nodes), requested,
decrypt=options.unprotected)
# Order nodes and attributes exactly as the server/nodeattrib do:
# natural sort via humanify_nodename (identical to the client's
# naturalize_string), with the same TypeError fallback as core.py.
nodelist = list(attrs)
try:
nodelist.sort(key=noderange.humanify_nodename)
except TypeError:
nodelist.sort()
for node in nodelist:
nodeattrs = attrs[node]
anames = list(nodeattrs)
try:
anames.sort(key=noderange.humanify_nodename)
except TypeError:
anames.sort()
for aname in anames:
adata = nodeattrs[aname]
# Format like nodeattrib: a plain, list, or (decrypted) secret.*
# value lives in 'value'; some attributes come back as a bare list;
# crypted.* is a one-way hash stored as 'hashvalue'. Without -u,
# sensitive values are masked the same way nodeattrib does.
if isinstance(adata, (list, tuple)):
val = ','.join(_dispval(x) for x in adata)
elif 'value' in adata:
aval = adata['value']
if aval is None:
val = ''
elif isinstance(aval, (list, tuple)):
val = ','.join(_dispval(x) for x in aval)
else:
val = _dispval(aval)
elif 'hashvalue' in adata:
if options.unprotected:
val = _dispval(adata['hashvalue'])
else:
val = '******** (hidden: use -u to show)'
elif 'cryptvalue' in adata:
val = '******** (hidden: use -u to show)'
else:
val = ''
if options.valueonly:
print(val)
else:
print('{0}: {1}: {2}'.format(node, aname, val))
# Report explicitly named (non-glob) attributes that are not set.
for attr in requested:
if '*' not in attr and attr not in nodeattrs:
if options.valueonly:
print('')
else:
print('{0}: {1}:'.format(node, attr))
sys.exit(0)
if args and args[0] == 'showattrib':
show_attrib(options, args)
if len(args) != 2 or args[0] not in ('dump', 'restore', 'merge'):
argparser.print_help()
sys.exit(1)