Skip to content

Commit 641218b

Browse files
Merge pull request #468 from martin-belanger/code-review-improvements
code-review: fix bugs, modernize, and expand test coverage
2 parents d9e757c + 3de0fdc commit 641218b

21 files changed

Lines changed: 374 additions & 66 deletions

.github/workflows/linters.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ jobs:
121121
122122
- name: "Ruff: lint"
123123
run: |
124-
ruff check .build/stacctl .build/stacd .build/stafctl .build/stafd .build/stasadm .build/staslib
124+
ruff check --diff .build/stacctl .build/stacd .build/stafctl .build/stafd .build/stasadm .build/staslib
125125
126126
- name: "Ruff: format"
127127
if: always()

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,22 @@ Required user-space dependencies include:
9696

9797
This ensures consistency with other NVMe tools like `nvme-cli` and `libnvme`. Alternative values can be set in `/etc/stas/sys.conf`.
9898

99+
## D-Bus Security and Trust
100+
101+
Both *stafd* and *stacd* communicate over the **system D-Bus bus**. D-Bus policy
102+
files (installed under `/usr/share/dbus-1/system.d/`) control which callers are
103+
allowed to own or interact with the service names. Only processes running as
104+
**root** (or with explicit D-Bus policy permission) can invoke the service
105+
interfaces.
106+
107+
*stacd* subscribes to D-Bus signals emitted by *stafd* (`log_pages_changed` and
108+
`dc_removed`). These signals arrive over the trusted system bus — *stacd* does
109+
not accept instructions from untrusted sources.
110+
111+
The host credentials written to `/run/nvme-stas/` by the daemons (e.g. the
112+
last-known-config pickle file) are protected by root-only filesystem
113+
permissions. Do not grant untrusted users write access to that directory.
114+
99115
## Build, Install & Tests
100116

101117
This Python project uses **Meson** as its build system:

stacctl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ def ls(args):
9696
print(f'nvme-stas {defs.VERSION}')
9797
sys.exit(0)
9898

99+
if not hasattr(ARGS, 'func'):
100+
PARSER.print_usage()
101+
sys.exit(1)
102+
99103
try:
100104
ARGS.func(ARGS)
101105
except dasbus.error.DBusError:

stafctl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ def adlp(args):
197197
print(f'nvme-stas {defs.VERSION}')
198198
sys.exit(0)
199199

200+
if not hasattr(ARGS, 'func'):
201+
PARSER.print_usage()
202+
sys.exit(1)
203+
200204
try:
201205
ARGS.func(ARGS)
202206
except dasbus.error.DBusError:

stasadm.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ def get_machine_app_specific(app_id):
6262
if not data:
6363
return None
6464

65+
# app_id is a public domain-separation constant (per systemd's design).
66+
# It intentionally does NOT need to be secret: its only purpose is to
67+
# make the derived ID unique to this application so that two different
68+
# applications deriving from the same machine-id produce different values.
6569
hmac_obj = hmac.new(app_id, uuid.UUID(data).bytes, hashlib.sha256)
6670
id128_bytes = hmac_obj.digest()[0:16]
6771
return str(uuid.UUID(bytes=id128_bytes, version=4))
@@ -196,8 +200,8 @@ def get_parser():
196200
print(f'nvme-stas {defs.VERSION}')
197201
sys.exit(0)
198202

199-
try:
200-
ARGS.cmd(ARGS)
201-
except AttributeError as ex:
202-
print(str(ex))
203+
if not hasattr(ARGS, 'cmd'):
203204
PARSER.print_usage()
205+
sys.exit(1)
206+
207+
ARGS.cmd(ARGS)

staslib/avahi.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import socket
1414
import typing
1515
import logging
16-
import functools
1716
import dasbus.error
1817
import dasbus.connection
1918
import dasbus.client.proxy
@@ -29,7 +28,7 @@ def _txt2dict(txt: list):
2928
the_dict = dict()
3029
for list_of_chars in txt:
3130
try:
32-
string = functools.reduce(lambda accumulator, c: accumulator + chr(c), list_of_chars, '')
31+
string = ''.join(chr(c) for c in list_of_chars)
3332
if string.isprintable():
3433
key, val = string.split('=')
3534
if key: # Make sure the key is not an empty string
@@ -83,6 +82,8 @@ def __init__(self, values: list):
8382

8483
def get_next(self):
8584
'''Get the next value (or last value if ceiling was reached)'''
85+
if not self._values:
86+
return 0
8687
value = self._values[self._index]
8788
if self._index >= 0:
8889
self._index += 1
@@ -146,7 +147,7 @@ def __init__(self, args, identified_cback):
146147

147148
def info(self):
148149
'''Return debug info'''
149-
info = self._data
150+
info = dict(self._data)
150151
info['reachable'] = str(self._reachable)
151152
return info
152153

@@ -417,7 +418,7 @@ def kick_start(self):
417418
self._kick_avahi_tmr.clear()
418419

419420
def _remove_service(self, service_to_rm: typing.Tuple[int, int, str, str, str]):
420-
service = self._services.pop(service_to_rm)
421+
service = self._services.pop(service_to_rm, None)
421422
if service is not None:
422423
service.close()
423424

staslib/conf.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ def _to_int(text):
6666

6767

6868
def _to_bool(text, positive='true'):
69-
return _parse_single_val(text).lower() == positive
69+
val = _parse_single_val(text)
70+
return val is not None and val.lower() == positive
7071

7172

7273
def _to_ncc(text):
@@ -77,7 +78,7 @@ def _to_ncc(text):
7778

7879

7980
def _to_ip_family(text):
80-
return tuple((4 if text == 'ipv4' else 6 for text in _parse_single_val(text).split('+')))
81+
return tuple((4 if token == 'ipv4' else 6 for token in _parse_single_val(text).split('+')))
8182

8283

8384
# ******************************************************************************
@@ -86,6 +87,11 @@ class OrderedMultisetDict(dict):
8687
and allow multiple configuration parameters with the same key. The
8788
result is a list of values, where values are sorted by the order they
8889
appear in the file.
90+
91+
Note: configparser internally joins repeated keys with '\\n' when
92+
strict=False. __getitem__ splits on '\\n' to reconstruct the list, so
93+
callers always receive a list of strings rather than a single newline-
94+
joined string.
8995
'''
9096

9197
def __setitem__(self, key, value):
@@ -214,12 +220,6 @@ class SvcConf(metaclass=singleton.Singleton):
214220
'convert': _parse_list,
215221
'default': [],
216222
},
217-
### BEGIN: LEGACY SECTION TO BE REMOVED ###
218-
'blacklist': {
219-
'convert': _parse_list,
220-
'default': [],
221-
},
222-
### END: LEGACY SECTION TO BE REMOVED ###
223223
},
224224
}
225225

@@ -316,6 +316,10 @@ def persistent_connections(self):
316316
section = 'Discovery controller connection management'
317317
option = 'persistent-connections'
318318

319+
# Use ignore_default=True so we can distinguish "not set in file" from
320+
# "set to false". The per-daemon default (stafd vs stacd) differs and is
321+
# held in self._defaults rather than in OPTION_CHECKER, so we fall back
322+
# to that dict explicitly.
319323
value = self.get_option(section, option, ignore_default=True)
320324
if value is not None:
321325
return value
@@ -379,11 +383,6 @@ def get_excluded(self):
379383
}
380384
'''
381385
controller_list = self.get_option('Controllers', 'exclude')
382-
383-
# 2022-09-20: Look for "blacklist". This is for backwards compatibility
384-
# with releases 1.0 to 1.1.x. This is to be phased out (i.e. remove by 2024)
385-
controller_list += self.get_option('Controllers', 'blacklist')
386-
387386
excluded = [_parse_controller(controller) for controller in controller_list]
388387
for controller in excluded:
389388
controller.pop('host-traddr', None) # remove host-traddr
@@ -544,8 +543,11 @@ def hostnqn(self):
544543
except FileNotFoundError as ex:
545544
sys.exit(f'Error reading mandatory Host NQN (see stasadm --help): {ex}')
546545

547-
if value is not None and not value.startswith('nqn.'):
548-
sys.exit(f'Error Host NQN "{value}" should start with "nqn."')
546+
if value is not None:
547+
if not value.startswith('nqn.'):
548+
sys.exit(f'Error Host NQN "{value}" should start with "nqn."')
549+
if len(value) > 223:
550+
sys.exit(f'Error Host NQN is too long ({len(value)} chars, max 223 per NVMe spec)')
549551

550552
return value
551553

staslib/ctrl.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ def _on_ctrl_removed(self, udev_obj):
170170
self._ctrl = None
171171
self._device = None
172172
self._connect_attempts = 0
173-
self._retry_connect_tmr.start()
173+
# Reset the retry interval to fast since this is effectively a fresh start
174+
self._retry_connect_tmr.start(self.FAST_CONNECT_RETRY_PERIOD_SEC)
174175

175176
def _get_cfg(self):
176177
'''Get configuration parameters. These may either come from the [Global]

staslib/gutil.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,16 @@ def stop(self):
5757
self._source = None
5858

5959
def start(self, new_interval_sec: float = -1.0):
60-
'''@brief Start (or restart) timer'''
60+
'''@brief Start (or restart) timer.
61+
62+
If the timer is not yet running, create and attach a new GLib source
63+
that will fire after @new_interval_sec (or the previously set interval
64+
if @new_interval_sec is negative).
65+
66+
If the timer is already running, reschedule it to fire @new_interval_sec
67+
from NOW (i.e. the deadline is reset relative to the current monotonic
68+
clock, not the original start time).
69+
'''
6170
if new_interval_sec >= 0:
6271
self._interval_sec = float(new_interval_sec)
6372

staslib/iputil.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ def ip_equal(ip1, ip2):
136136
@param ip1: IPv4Address or IPv6Address object
137137
@param ip2: IPv4Address or IPv6Address object
138138
'''
139-
if not isinstance(ip1, ipaddress._BaseAddress):
139+
if not isinstance(ip1, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
140140
return False
141-
if not isinstance(ip2, ipaddress._BaseAddress):
141+
if not isinstance(ip2, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
142142
return False
143143

144144
if ip1.version == 4 and ip2.version == 6:
@@ -256,7 +256,7 @@ def get_interface(ifaces: dict, src_addr):
256256
@param ifaces: Interface info previously returned by @net_if_addrs()
257257
@param src_addr: IPv4Address or IPv6Address object
258258
'''
259-
if not isinstance(src_addr, ipaddress._BaseAddress):
259+
if not isinstance(src_addr, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
260260
return ''
261261

262262
for iface, addr_map in ifaces.items():

0 commit comments

Comments
 (0)