diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index ff2cee1..985a929 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -59,8 +59,8 @@ jobs:
- name: "INSTALL: pip packages"
run: |
- pip install pylint --upgrade
- pip install PyGObject --upgrade
+ pip install pylint
+ # pip install PyGObject
- name: "BUILD: [libnvme, nvme-stas]"
uses: BSFishy/meson-build@v1.0.3
diff --git a/coverage.sh.in b/coverage.sh.in
index 51d1106..0d2478e 100755
--- a/coverage.sh.in
+++ b/coverage.sh.in
@@ -377,7 +377,7 @@ persistent-connections = false
zeroconf-connections-persistence = 1:01
[Controllers]
-controller = transport = tcp ; traddr = localhost ; ; ; kato=31; dhchap-ctrl-secret=not-so-secret
+controller = transport = tcp ; traddr = localhost ; ; ; kato=31; dhchap-ctrl-secret=DHHC-1:00:not-so-secret/not-so-secret/not-so-secret/not-so: ; dhchap-secret=DHHC-1:00:very-secret/very-secret/very-secret/very-secret/:
controller=transport=tcp;traddr=1.1.1.1
controller=transport=tcp;traddr=100.100.100.100
controller=transport=tcp;traddr=2607:f8b0:4002:c2c::71
diff --git a/doc/standard-conf.xml b/doc/standard-conf.xml
index 50d4fe5..36cb19f 100644
--- a/doc/standard-conf.xml
+++ b/doc/standard-conf.xml
@@ -376,6 +376,21 @@
+
+ dhchap-secret=
+
+
+ NVMe In-band authentication host secret (i.e. key);
+ needs to be in ASCII format as specified in NVMe 2.0
+ section 8.13.5.8 Secret representation. If this
+ option is not specified, the default is read
+ from /etc/stas/sys.conf (see the 'key' parameter
+ under the [Host] section). In-band authentication
+ is attempted when this is present.
+
+
+
+
dhchap-ctrl-secret=
diff --git a/etc/stas/stacd.conf b/etc/stas/stacd.conf
index 9fbc1c3..c1bcbed 100644
--- a/etc/stas/stacd.conf
+++ b/etc/stas/stacd.conf
@@ -235,6 +235,14 @@
# This forces the connection to be made on a specific interface
# instead of letting the system decide.
#
+# dhchap-secret [OPTIONAL]
+# NVMe In-band authentication host secret (i.e. key); needs to be
+# in ASCII format as specified in NVMe 2.0 section 8.13.5.8 Secret
+# representation. If this option is not specified, the default is
+# read from /etc/stas/sys.conf (see the 'key' parameter under the
+# [Host] section). In-band authentication is attempted when this
+# is present.
+#
# dhchap-ctrl-secret [OPTIONAL]
# NVMe In-band authentication controller secret (i.e. key) for
# bi-directional authentication; needs to be in ASCII format as
diff --git a/staslib/conf.py b/staslib/conf.py
index 053c5e9..3be6f50 100644
--- a/staslib/conf.py
+++ b/staslib/conf.py
@@ -341,6 +341,7 @@ def get_controllers(self):
'host-traddr': [TRADDR],
'host-iface': [IFACE],
'host-nqn': [NQN],
+ 'dhchap-secret': [KEY],
'dhchap-ctrl-secret': [KEY],
'hdr-digest': [BOOL]
'data-digest': [BOOL]
diff --git a/staslib/ctrl.py b/staslib/ctrl.py
index e4cda6b..e18414e 100644
--- a/staslib/ctrl.py
+++ b/staslib/ctrl.py
@@ -221,23 +221,39 @@ def _do_connect(self):
host_traddr=self.tid.host_traddr if self.tid.host_traddr else None,
host_iface=host_iface,
)
- self._ctrl.discovery_ctrl_set(self._discovery_ctrl)
+ self._ctrl.discovery_ctrl = self._discovery_ctrl
- # Set the DHCHAP key on the controller
- # NOTE that this will eventually have to
+ # Set the DHCHAP host key on the controller
+ # NOTE that this may eventually have to
# change once we have support for AVE (TP8019)
- ctrl_dhchap_key = self.tid.cfg.get('dhchap-ctrl-secret')
- if ctrl_dhchap_key and self._nvme_options.dhchap_ctrlkey_supp:
- has_dhchap_key = hasattr(self._ctrl, 'dhchap_key')
- if not has_dhchap_key:
+ # This is used for in-band authentication
+ dhchap_host_key = self.tid.cfg.get('dhchap-secret')
+ if dhchap_host_key and self._nvme_options.dhchap_hostkey_supp:
+ try:
+ self._ctrl.dhchap_host_key = dhchap_host_key
+ except AttributeError:
logging.warning(
- '%s | %s - libnvme-%s does not allow setting the controller DHCHAP key. Please upgrade libnvme.',
+ '%s | %s - libnvme-%s does not allow setting the host DHCHAP key on the controller. Please upgrade libnvme.',
+ self.id,
+ self.device,
+ defs.LIBNVME_VERSION,
+ )
+
+ # Set the DHCHAP controller key on the controller
+ # NOTE that this may eventually have to
+ # change once we have support for AVE (TP8019)
+ # This is used for bidirectional authentication
+ dhchap_ctrl_key = self.tid.cfg.get('dhchap-ctrl-secret')
+ if dhchap_ctrl_key and self._nvme_options.dhchap_ctrlkey_supp:
+ try:
+ self._ctrl.dhchap_key = dhchap_ctrl_key
+ except AttributeError:
+ logging.warning(
+ '%s | %s - libnvme-%s does not allow setting the controller DHCHAP key on the controller. Please upgrade libnvme.',
self.id,
self.device,
defs.LIBNVME_VERSION,
)
- else:
- self._ctrl.dhchap_key = ctrl_dhchap_key
# Audit existing nvme devices. If we find a match, then
# we'll just borrow that device instead of creating a new one.
diff --git a/staslib/gutil.py b/staslib/gutil.py
index 4cdc087..0922fce 100644
--- a/staslib/gutil.py
+++ b/staslib/gutil.py
@@ -123,7 +123,7 @@ def resolve_ctrl_async(self, cancellable, controllers_in: list, callback):
The callback @callback will be called once all hostnames have
been resolved.
- @param controllers: List of trid.TID
+ @param controllers_in: List of trid.TID
'''
pending_resolution_count = 0
controllers_out = []
diff --git a/staslib/trid.py b/staslib/trid.py
index e814f4e..cb4a5de 100644
--- a/staslib/trid.py
+++ b/staslib/trid.py
@@ -33,6 +33,7 @@ def __init__(self, cid: dict):
'host-nqn': str, # [optional]
# Connection parameters
+ 'dhchap-secret': str, # [optional]
'dhchap-ctrl-secret': str, # [optional]
'hdr-digest': str, # [optional]
'data-digest': str, # [optional]
diff --git a/utils/nvmet/auth.conf b/utils/nvmet/auth.conf
new file mode 100644
index 0000000..b4c23e6
--- /dev/null
+++ b/utils/nvmet/auth.conf
@@ -0,0 +1,34 @@
+# Config file format: Python, i.e. dict(), list(), int, str, etc...
+# port ids (id) are integers 0...N
+# namespaces are integers 0..N
+# subsysnqn can be integers or strings
+{
+ 'ports': [
+ {
+ 'id': 1,
+ #'adrfam': 'ipv6',
+ #'traddr': '::',
+ 'adrfam': 'ipv4',
+ 'traddr': '0.0.0.0',
+ 'trsvcid': 4420,
+ 'trtype': 'tcp',
+ }
+ ],
+
+ 'subsystems': [
+ {
+ 'subsysnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34ae8e28',
+ 'port': 1,
+ 'namespaces': [1],
+ 'allowed_hosts': [
+ {
+ # Must match with the NQN and key configured on the host
+ # Key was generated with:
+ # nvme gen-dhchap-key ...
+ 'nqn': 'nqn.2014-08.org.nvmexpress:uuid:46ba5037-7ce5-41fa-9452-48477bf00080',
+ 'key': 'DHHC-1:00:2kx1hDTUPdvwtxHYUXFRl8pzn5hYZH7K3Z77IYM4hNN6/fQT:',
+ },
+ ],
+ },
+ ]
+}
diff --git a/utils/nvmet/nvmet.py b/utils/nvmet/nvmet.py
index baf6560..9049d63 100755
--- a/utils/nvmet/nvmet.py
+++ b/utils/nvmet/nvmet.py
@@ -52,19 +52,26 @@ def _get_loaded_nvmet_modules():
return output
-def _runcmd(cmd: list, quiet=False):
+def _runcmd(cmd: list, quiet=False, capture_output=False):
if not quiet:
print(' '.join(cmd))
if args.dry_run:
return
- subprocess.run(cmd)
+
+ try:
+ cp = subprocess.run(cmd, capture_output=capture_output, text=True)
+ except TypeError:
+ # For older Python versions that don't support "capture_output" or "text"
+ cp = subprocess.run(cmd, stdout=subprocess.PIPE, universal_newlines=True)
+
+ return cp.stdout if capture_output else None
def _modprobe(module: str, args: list = None, quiet=False):
cmd = ['/usr/sbin/modprobe', module]
if args:
cmd.extend(args)
- _runcmd(cmd, quiet)
+ _runcmd(cmd, quiet=quiet)
def _mkdir(dname: str):
@@ -93,12 +100,32 @@ def _symlink(port: str, subsysnqn: str):
link.symlink_to(target)
-def _create_subsystem(subsysnqn: str) -> str:
+def _symlink_allowed_hosts(hostnqn: str, subsysnqn: str):
+ print(
+ f'$( cd "/sys/kernel/config/nvmet/subsystems/{subsysnqn}/allowed_hosts" && ln -s "../../../hosts/{hostnqn}" "{hostnqn}" )'
+ )
+ if args.dry_run:
+ return
+ target = os.path.join('/sys/kernel/config/nvmet/hosts', hostnqn)
+ link = pathlib.Path(os.path.join('/sys/kernel/config/nvmet/subsystems', subsysnqn, 'allowed_hosts', hostnqn))
+ link.symlink_to(target)
+
+
+def _create_subsystem(subsysnqn: str, allowed_hosts: list) -> str:
print(f'###{Fore.GREEN} Create subsystem: {subsysnqn}{Style.RESET_ALL}')
dname = os.path.join('/sys/kernel/config/nvmet/subsystems/', subsysnqn)
_mkdir(dname)
- _echo(1, os.path.join(dname, 'attr_allow_any_host'))
- return dname
+ _echo(0 if allowed_hosts else 1, os.path.join(dname, 'attr_allow_any_host'))
+
+ # Configure all the hosts that are allowed to access this subsystem
+ for host in allowed_hosts:
+ hostnqn = host.get('nqn')
+ hostkey = host.get('key')
+ if all([hostnqn, hostkey]):
+ dname = os.path.join('/sys/kernel/config/nvmet/hosts/', hostnqn)
+ _mkdir(dname)
+ _echo(hostkey, os.path.join(dname, 'dhchap_key'))
+ _symlink_allowed_hosts(hostnqn, subsysnqn)
def _create_namespace(subsysnqn: str, id: str, node: str) -> str:
@@ -107,7 +134,6 @@ def _create_namespace(subsysnqn: str, id: str, node: str) -> str:
_mkdir(dname)
_echo(node, os.path.join(dname, 'device_path'))
_echo(1, os.path.join(dname, 'enable'))
- return dname
def _args_valid(id, traddr, trsvcid, trtype, adrfam):
@@ -215,8 +241,9 @@ def create(args):
str(subsystem.get('port')),
subsystem.get('namespaces'),
)
+
if None not in (subsysnqn, port, namespaces):
- _create_subsystem(subsysnqn)
+ _create_subsystem(subsysnqn, subsystem.get('allowed_hosts', []))
for id in namespaces:
_create_namespace(subsysnqn, str(id), dev_node)
else:
@@ -235,10 +262,16 @@ def clean(args):
if not args.dry_run and os.geteuid() != 0:
sys.exit(f'Permission denied. You need root privileges to run {os.path.basename(__file__)}.')
+ print(f'###{Fore.GREEN} 1st) Remove the symlinks{Style.RESET_ALL}')
print('rm -f /sys/kernel/config/nvmet/ports/*/subsystems/*')
for dname in pathlib.Path('/sys/kernel/config/nvmet/ports').glob('*/subsystems/*'):
_runcmd(['rm', '-f', str(dname)], quiet=True)
+ print('rm -f /sys/kernel/config/nvmet/subsystems/*/allowed_hosts/*')
+ for dname in pathlib.Path('/sys/kernel/config/nvmet/subsystems').glob('*/allowed_hosts/*'):
+ _runcmd(['rm', '-f', str(dname)], quiet=True)
+
+ print(f'###{Fore.GREEN} 2nd) Remove directories{Style.RESET_ALL}')
print('rmdir /sys/kernel/config/nvmet/ports/*')
for dname in pathlib.Path('/sys/kernel/config/nvmet/ports').glob('*'):
_runcmd(['rmdir', str(dname)], quiet=True)
@@ -251,6 +284,11 @@ def clean(args):
for dname in pathlib.Path('/sys/kernel/config/nvmet/subsystems').glob('*'):
_runcmd(['rmdir', str(dname)], quiet=True)
+ print('rmdir /sys/kernel/config/nvmet/hosts/*')
+ for dname in pathlib.Path('/sys/kernel/config/nvmet/hosts').glob('*'):
+ _runcmd(['rmdir', str(dname)], quiet=True)
+
+ print(f'###{Fore.GREEN} 3rd) Unload kernel modules{Style.RESET_ALL}')
for module in _get_loaded_nvmet_modules():
_modprobe(module, ['--remove'])