diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-09-04 09:15:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-09-04 09:15:27 +0000 |
commit | 425e2c731a79f34454b3b7b35b29040304850197 (patch) | |
tree | 61210e82c8a610c113334f9956ef06f4cb0f3396 | |
parent | Releasing debian version 2.3~rc3-1. (diff) | |
download | nvme-stas-425e2c731a79f34454b3b7b35b29040304850197.tar.xz nvme-stas-425e2c731a79f34454b3b7b35b29040304850197.zip |
Merging upstream version 2.3~rc4.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r-- | .github/workflows/meson-test.yml | 22 | ||||
-rw-r--r-- | NEWS.md | 1 | ||||
-rw-r--r-- | doc/readthedocs/meson.build | 8 | ||||
-rw-r--r-- | etc/dbus-1/system.d/meson.build | 8 | ||||
-rw-r--r-- | meson.build | 3 | ||||
-rw-r--r-- | staslib/defs.py | 19 | ||||
-rw-r--r-- | staslib/gutil.py | 6 | ||||
-rw-r--r-- | staslib/iputil.py | 106 | ||||
-rw-r--r-- | staslib/udev.py | 263 | ||||
-rwxr-xr-x | test/test-iputil.py | 11 | ||||
-rwxr-xr-x | test/test-nbft_conf.py | 11 | ||||
-rwxr-xr-x | test/test-nvme_options.py | 4 | ||||
-rwxr-xr-x | test/test-udev.py | 100 | ||||
-rw-r--r-- | usr/lib/systemd/system/meson.build | 2 | ||||
-rw-r--r-- | usr/lib/systemd/system/stas-config@.service | 6 | ||||
-rw-r--r-- | utils/nvmet/nvmet-large.conf | 420 |
16 files changed, 711 insertions, 279 deletions
diff --git a/.github/workflows/meson-test.yml b/.github/workflows/meson-test.yml index 7ef26fc..24c6e57 100644 --- a/.github/workflows/meson-test.yml +++ b/.github/workflows/meson-test.yml @@ -38,13 +38,15 @@ jobs: python3 -m pip install --upgrade dasbus pylint pyflakes PyGObject python3 -m pip install --upgrade vermin pyfakefs importlib-resources - - name: "INSTALL: libnvme" + - name: "INSTALL: libnvme dependencies" + run: | + sudo apt-get install --yes --quiet swig + sudo apt-get install --yes --quiet libjson-c-dev + + - name: "SETUP: [nvme-stas, libnvme]" run: | - sudo apt-get install --yes --quiet swig libjson-c-dev meson subprojects download - meson setup .build subprojects/libnvme -Dlibdbus=disabled -Dopenssl=disabled -Dbuildtype=release -Dprefix=/usr -Dpython=enabled - ninja -C .build - sudo meson install -C .build + meson setup --buildtype=release --sysconfdir=/etc --prefix=/usr -Dman=true -Dhtml=true -Dlibnvme:python=enabled -Dlibnvme:libdbus=disabled -Dlibnvme:openssl=disabled -Dlibnvme:json-c=disabled -Dlibnvme:keyutils=disabled .build - name: "CONFIG: PYTHONPATH" run: | @@ -55,7 +57,7 @@ jobs: with: action: test directory: .build - setup-options: -Dman=true -Dhtml=true + setup-options: --buildtype=release --sysconfdir=/etc --prefix=/usr -Dman=true -Dhtml=true -Dlibnvme:python=enabled -Dlibnvme:libdbus=disabled -Dlibnvme:openssl=disabled -Dlibnvme:json-c=disabled -Dlibnvme:keyutils=disabled options: --print-errorlogs --suite nvme-stas # Preserve meson's log file on failure @@ -67,9 +69,11 @@ jobs: - name: "Generate coverage report" run: | - python3 -m pip install pytest - python3 -m pip install pytest-cov - PYTHONPATH=.build:.build/subprojects/libnvme:/usr/lib/python3/dist-packages/ pytest --cov=./staslib --cov-report=xml test/test-*.py + python3 -m pip install --upgrade pytest + python3 -m pip install --upgrade pytest-cov + echo $( pwd ) + cp -r .build/staslib/* ./staslib/. + pytest --cov=./staslib --cov-report=xml test/test-*.py - uses: codecov/codecov-action@v3 with: @@ -7,6 +7,7 @@ New features: - Support for nBFT (NVMe-oF Boot Table). - The Avahi driver will now verify reachability of services discovered through mDNS to make sure all discovered IP addresses can be connected to. This avoids invoking the NVMe kernel driver with invalid IP addresses and getting error messages in the syslog. - The Avahi driver will now print an error message if the same IP address is found on multiple interfaces. This indicates a misconfiguration of the network. +- Simplify algorithm that determines if an existing connection (is sysfs) can be reused by stafd/stacd instead of creating a duplicate connection. Bug fixes: diff --git a/doc/readthedocs/meson.build b/doc/readthedocs/meson.build index a8e2305..a81afa1 100644 --- a/doc/readthedocs/meson.build +++ b/doc/readthedocs/meson.build @@ -17,10 +17,10 @@ components = [ 'environment.txt', 'installation.rst', 'nvme-stas.rst', - 'org.nvmexpress.stac.debug.rst', - 'org.nvmexpress.stac.rst', - 'org.nvmexpress.staf.debug.rst', - 'org.nvmexpress.staf.rst', + conf.get('STACD_DBUS_NAME') + '.debug.rst', + conf.get('STACD_DBUS_NAME') + '.rst', + conf.get('STAFD_DBUS_NAME') + '.debug.rst', + conf.get('STAFD_DBUS_NAME') + '.rst', 'stacctl.rst', 'stacd-index.rst', 'stacd.conf.rst', diff --git a/etc/dbus-1/system.d/meson.build b/etc/dbus-1/system.d/meson.build index b9bc858..55f3391 100644 --- a/etc/dbus-1/system.d/meson.build +++ b/etc/dbus-1/system.d/meson.build @@ -9,15 +9,15 @@ dbus_conf_dir = datadir / 'dbus-1' / 'system.d' configure_file( - input: 'org.nvmexpress.staf.in.conf', - output: 'org.nvmexpress.staf.conf', + input: conf.get('STAFD_DBUS_NAME') + '.in.conf', + output: conf.get('STAFD_DBUS_NAME') + '.conf', configuration: conf, install_dir: dbus_conf_dir, ) configure_file( - input: 'org.nvmexpress.stac.in.conf', - output: 'org.nvmexpress.stac.conf', + input: conf.get('STACD_DBUS_NAME') + '.in.conf', + output: conf.get('STACD_DBUS_NAME') + '.conf', configuration: conf, install_dir: dbus_conf_dir, ) diff --git a/meson.build b/meson.build index 2c19b62..509f69c 100644 --- a/meson.build +++ b/meson.build @@ -9,7 +9,7 @@ project( 'nvme-stas', meson_version: '>= 0.53.0', - version: '2.3-rc3', + version: '2.3-rc4', license: 'Apache-2.0', default_options: [ 'buildtype=release', @@ -78,6 +78,7 @@ conf.set('STAFD_DBUS_NAME', 'org.nvmexpress.staf') conf.set('STAFD_DBUS_PATH', '/org/nvmexpress/staf') conf.set('STACD_DBUS_NAME', 'org.nvmexpress.stac') conf.set('STACD_DBUS_PATH', '/org/nvmexpress/stac') +conf.set('ETC', etcdir) #=============================================================================== stafd = configure_file( diff --git a/staslib/defs.py b/staslib/defs.py index 877f6e6..fc1135b 100644 --- a/staslib/defs.py +++ b/staslib/defs.py @@ -36,18 +36,25 @@ KERNEL_IFACE_MIN_VERSION = KernelVersion('5.14') KERNEL_TP8013_MIN_VERSION = KernelVersion('5.16') KERNEL_HOSTKEY_MIN_VERSION = KernelVersion('5.20') KERNEL_CTRLKEY_MIN_VERSION = KernelVersion('5.20') +KERNEL_ALL_MIN_VERSION = max( + # Minimum version required to have support for all + KERNEL_IFACE_MIN_VERSION, + KERNEL_TP8013_MIN_VERSION, + KERNEL_HOSTKEY_MIN_VERSION, + KERNEL_CTRLKEY_MIN_VERSION, +) WELL_KNOWN_DISC_NQN = 'nqn.2014-08.org.nvmexpress.discovery' PROG_NAME = os.path.basename(sys.argv[0]) -NVME_HOSTID = '/etc/nvme/hostid' -NVME_HOSTNQN = '/etc/nvme/hostnqn' -NVME_HOSTKEY = '/etc/nvme/hostkey' +NVME_HOSTID = '@ETC@/nvme/hostid' +NVME_HOSTNQN = '@ETC@/nvme/hostnqn' +NVME_HOSTKEY = '@ETC@/nvme/hostkey' -SYS_CONF_FILE = '/etc/stas/sys.conf' -STAFD_CONF_FILE = '/etc/stas/stafd.conf' -STACD_CONF_FILE = '/etc/stas/stacd.conf' +SYS_CONF_FILE = '@ETC@/stas/sys.conf' +STAFD_CONF_FILE = '@ETC@/stas/stafd.conf' +STACD_CONF_FILE = '@ETC@/stas/stacd.conf' HAS_NBFT_SUPPORT = hasattr(nvme, 'nbft_get') NBFT_SYSFS_PATH = "/sys/firmware/acpi/tables" diff --git a/staslib/gutil.py b/staslib/gutil.py index 1730ac0..9aef347 100644 --- a/staslib/gutil.py +++ b/staslib/gutil.py @@ -6,8 +6,8 @@ # # Authors: Martin Belanger <Martin.Belanger@dell.com> # -'''This module provides utility functions/classes to provide easier to use -access to GLib/Gio/Gobject resources. +'''This module provides utility functions (or classes) that simplify +the use of certain GLib/Gio/Gobject functions/resources. ''' import logging @@ -443,7 +443,7 @@ class TcpChecker: # pylint: disable=too-many-instance-attributes # the GLib context. family = socket.AF_INET if self._traddr.version == 4 else socket.AF_INET6 self._native_sock = socket.socket(family, socket.SOCK_STREAM | socket.SOCK_NONBLOCK, socket.IPPROTO_TCP) - if isinstance(self._host_iface, str): + if self._host_iface and isinstance(self._host_iface, str): self._native_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, self._host_iface.encode('utf-8')) # Convert socket.socket() to a Gio.Socket() object diff --git a/staslib/iputil.py b/staslib/iputil.py index 96c5d56..d5e93dd 100644 --- a/staslib/iputil.py +++ b/staslib/iputil.py @@ -131,66 +131,22 @@ def mac2iface(mac: str): # pylint: disable=too-many-locals # ****************************************************************************** -def _data_matches_ip(data_family, data, ip): - if data_family == socket.AF_INET: - try: - other_ip = ipaddress.IPv4Address(data) - except ValueError: - return False - if ip.version == 6: - ip = ip.ipv4_mapped - elif data_family == socket.AF_INET6: - try: - other_ip = ipaddress.IPv6Address(data) - except ValueError: - return False - if ip.version == 4: - other_ip = other_ip.ipv4_mapped - else: - return False - - return other_ip == ip - - -def _iface_of(src_addr): # pylint: disable=too-many-locals - '''@brief Find the interface that has src_addr as one of its assigned IP addresses. - @param src_addr: The IP address to match - @type src_addr: Instance of ipaddress.IPv4Address or ipaddress.IPv6Address +def ip_equal(ip1, ip2): + '''Check whther two IP addresses are equal. + @param ip1: IPv4Address or IPv6Address object + @param ip2: IPv4Address or IPv6Address object ''' - with socket.socket(socket.AF_NETLINK, socket.SOCK_RAW) as sock: - sock.sendall(GETADDRCMD) - nlmsg = sock.recv(8192) - nlmsg_idx = 0 - while True: - if nlmsg_idx >= len(nlmsg): - nlmsg += sock.recv(8192) - - nlmsghdr = nlmsg[nlmsg_idx : nlmsg_idx + NLMSG_HDRLEN] - nlmsg_len, nlmsg_type, _, _, _ = struct.unpack('<LHHLL', nlmsghdr) - - if nlmsg_type == NLMSG_DONE: - break - - if nlmsg_type == RTM_NEWADDR: - msg_indx = nlmsg_idx + NLMSG_HDRLEN - msg = nlmsg[msg_indx : msg_indx + IFADDRMSG_SZ] # ifaddrmsg - ifa_family, _, _, _, ifa_index = struct.unpack('<BBBBL', msg) - - rtattr_indx = msg_indx + IFADDRMSG_SZ - while rtattr_indx < (nlmsg_idx + nlmsg_len): - rtattr = nlmsg[rtattr_indx : rtattr_indx + RTATTR_SZ] - rta_len, rta_type = struct.unpack('<HH', rtattr) - if rta_type == IFLA_ADDRESS: - data = nlmsg[rtattr_indx + RTATTR_SZ : rtattr_indx + rta_len] - if _data_matches_ip(ifa_family, data, src_addr): - return socket.if_indextoname(ifa_index) + if not isinstance(ip1, ipaddress._BaseAddress): # pylint: disable=protected-access + return False + if not isinstance(ip2, ipaddress._BaseAddress): # pylint: disable=protected-access + return False - rta_len = RTA_ALIGN(rta_len) # Round up to multiple of 4 - rtattr_indx += rta_len # Move to next rtattr + if ip1.version == 4 and ip2.version == 6: + ip2 = ip2.ipv4_mapped + elif ip1.version == 6 and ip2.version == 4: + ip1 = ip1.ipv4_mapped - nlmsg_idx += nlmsg_len # Move to next Netlink message - - return '' + return ip1 == ip2 # ****************************************************************************** @@ -221,21 +177,21 @@ def net_if_addrs(): # pylint: disable=too-many-locals source address. @example: { 'wlp0s20f3': { - 4: ['10.0.0.28'], + 4: [IPv4Address('10.0.0.28')], 6: [ - 'fd5e:9a9e:c5bd:0:5509:890c:1848:3843', - 'fd5e:9a9e:c5bd:0:1fd5:e527:8df7:7912', - '2605:59c8:6128:fb00:c083:1b8:c467:81d2', - '2605:59c8:6128:fb00:e99d:1a02:38e0:ad52', - 'fe80::d71b:e807:d5ee:7614' + IPv6Address('fd5e:9a9e:c5bd:0:5509:890c:1848:3843'), + IPv6Address('fd5e:9a9e:c5bd:0:1fd5:e527:8df7:7912'), + IPv6Address('2605:59c8:6128:fb00:c083:1b8:c467:81d2'), + IPv6Address('2605:59c8:6128:fb00:e99d:1a02:38e0:ad52'), + IPv6Address('fe80::d71b:e807:d5ee:7614'), ], }, 'lo': { - 4: ['127.0.0.1'], - 6: ['::1'], + 4: [IPv4Address('127.0.0.1')], + 6: [IPv6Address('::1')], }, 'docker0': { - 4: ['172.17.0.1'], + 4: [IPv4Address('172.17.0.1')], 6: [] }, } @@ -295,14 +251,18 @@ def net_if_addrs(): # pylint: disable=too-many-locals # ****************************************************************************** -def get_interface(src_addr): +def get_interface(ifaces: dict, src_addr): '''Get interface for given source address - @param src_addr: The source address - @type src_addr: str + @param ifaces: Interface info previously returned by @net_if_addrs() + @param src_addr: IPv4Address or IPv6Address object ''' - if not src_addr: + if not isinstance(src_addr, ipaddress._BaseAddress): # pylint: disable=protected-access return '' - src_addr = src_addr.split('%')[0] # remove scope-id (if any) - src_addr = get_ipaddress_obj(src_addr) - return '' if src_addr is None else _iface_of(src_addr) + for iface, addr_map in ifaces.items(): + for addrs in addr_map.values(): + for addr in addrs: + if ip_equal(src_addr, addr): + return iface + + return '' diff --git a/staslib/udev.py b/staslib/udev.py index b9bd258..48b7d1f 100644 --- a/staslib/udev.py +++ b/staslib/udev.py @@ -11,6 +11,7 @@ import os import time import logging +from functools import partial import pyudev from gi.repository import GLib from staslib import defs, iputil, trid @@ -154,7 +155,7 @@ class Udev: return False @staticmethod - def _cid_matches_tcp_tid_legacy(tid, cid): # pylint: disable=too-many-return-statements,too-many-branches + def _cid_matches_tcp_tid_legacy(tid, cid, ifaces): # pylint: disable=too-many-branches '''On kernels older than 6.1, the src_addr parameter is not available from the sysfs. Therefore, we need to infer a match based on other parameters. And there are a few cases where we're simply not sure @@ -164,76 +165,66 @@ class Udev: cid_host_iface = cid['host-iface'] cid_host_traddr = iputil.get_ipaddress_obj(cid['host-traddr'], ipv4_mapped_convert=True) - if not cid_host_iface: # cid.host_iface is undefined - if not cid_host_traddr: # cid.host_traddr is undefined - # When the existing cid.src_addr, cid.host_traddr, and cid.host_iface - # are all undefined (which can only happen on kernels prior to 6.1), - # we can't know for sure on which interface an existing connection - # was made. In this case, we can only declare a match if both - # tid.host_iface and tid.host_traddr are undefined as well. - logging.debug( - 'Udev._cid_matches_tcp_tid_legacy() - cid=%s, tid=%s - Not enough info. Assume "match" but this could be wrong.', - cid, - tid, - ) - return True + # Only check host_traddr if candidate cares about it + if tid.host_traddr: + tid_host_traddr = iputil.get_ipaddress_obj(tid.host_traddr, ipv4_mapped_convert=True) - # cid.host_traddr is defined. If tid.host_traddr is also - # defined, then it must match the existing cid.host_traddr. - if tid.host_traddr: - tid_host_traddr = iputil.get_ipaddress_obj(tid.host_traddr, ipv4_mapped_convert=True) + if cid_host_traddr: if tid_host_traddr != cid_host_traddr: return False - # If tid.host_iface is defined, then the interface where - # the connection is located must match. If tid.host_iface - # is not defined, then we don't really care on which - # interface the connection was made and we can skip this test. - if tid.host_iface: - # With the existing cid.host_traddr, we can find the - # interface of the exisiting connection. - connection_iface = iputil.get_interface(str(cid_host_traddr)) - if tid.host_iface != connection_iface: + else: + # If c->cfg.host_traddr is unknown, then the controller (c) + # uses the interface's primary address as the source + # address. If c->cfg.host_iface is defined we can + # determine the primary address associated with that + # interface and compare that to the candidate->host_traddr. + if cid_host_iface: + if_addrs = ifaces.get(cid_host_iface, {4: [], 6: []}) + source_addrs = if_addrs[tid_host_traddr.version] + if len(source_addrs): # Make sure it's not empty + primary_addr = iputil.get_ipaddress_obj(source_addrs[0], ipv4_mapped_convert=True) + if primary_addr != tid_host_traddr: + return False + + else: + # If both c->cfg.host_traddr and c->cfg.host_iface are + # unknown, we don't have enough information to make a + # 100% positive match. Regardless, let's be optimistic + # and assume that we have a match. + logging.debug( + 'Udev._cid_matches_tcp_tid_legacy() - [1] cid=%s, tid=%s - Not enough info. Assume "match" but this could be wrong.', + cid, + tid, + ) + + # Only check host_iface if candidate cares about it + if tid.host_iface: + if cid_host_iface: + if tid.host_iface != cid_host_iface: return False - return True - - # cid.host_iface is defined - if not cid_host_traddr: # cid.host_traddr is undefined - if tid.host_iface and tid.host_iface != cid_host_iface: - return False - - if tid.host_traddr: - # It's impossible to tell the existing connection source - # address. So, we can't tell if it matches tid.host_traddr. - # However, if the existing host_iface has only one source - # address assigned to it, we can assume that the source - # address used for the existing connection is that address. - if_addrs = iputil.net_if_addrs().get(cid_host_iface, {4: [], 6: []}) - tid_host_traddr = iputil.get_ipaddress_obj(tid.host_traddr, ipv4_mapped_convert=True) - source_addrs = if_addrs[tid_host_traddr.version] - if len(source_addrs) != 1: - return False - - src_addr0 = iputil.get_ipaddress_obj(source_addrs[0], ipv4_mapped_convert=True) - if src_addr0 != tid_host_traddr: - return False - - return True - - # cid.host_traddr is defined - if tid.host_iface and tid.host_iface != cid_host_iface: - return False + else: + if cid_host_traddr: + connection_iface = iputil.get_interface(ifaces, cid_host_traddr) + if tid.host_iface != connection_iface: + return False - if tid.host_traddr: - tid_host_traddr = iputil.get_ipaddress_obj(tid.host_traddr, ipv4_mapped_convert=True) - if tid_host_traddr != cid_host_traddr: - return False + else: + # If both c->cfg.host_traddr and c->cfg.host_iface are + # unknown, we don't have enough information to make a + # 100% positive match. Regardless, let's be optimistic + # and assume that we have a match. + logging.debug( + 'Udev._cid_matches_tcp_tid_legacy() - [2] cid=%s, tid=%s - Not enough info. Assume "match" but this could be wrong.', + cid, + tid, + ) return True @staticmethod - def _cid_matches_tid(tid, cid): # pylint: disable=too-many-return-statements,too-many-branches + def _cid_matches_tid(tid, cid, ifaces): # pylint: disable=too-many-return-statements,too-many-branches '''Check if existing controller's cid matches candidate controller's tid. @param cid: The Connection ID of an existing controller (from the sysfs). @param tid: The Transport ID of a candidate controller. @@ -242,8 +233,8 @@ class Udev: be re-used for the candidate controller (specified by tid). We do not have a match if the candidate's tid.transport, tid.traddr, - tid.trsvcid, and tid.subsysnqn are not identical to those of the cid. - These 4 parameters are mandatory for a match. + tid.trsvcid, tid.subsysnqn, and tid.host_nqn are not identical to those + of the cid. These 5 parameters are mandatory for a match. The tid.host_traddr and tid.host_iface depend on the transport type. These parameters may not apply or have a different syntax/meaning @@ -274,8 +265,9 @@ class Udev: tid_traddr = iputil.get_ipaddress_obj(tid.traddr, ipv4_mapped_convert=True) cid_traddr = iputil.get_ipaddress_obj(cid['traddr'], ipv4_mapped_convert=True) else: - cid_traddr = cid['traddr'] - tid_traddr = tid.traddr + # For FC and loop we can do a case-insensitive comparison + tid_traddr = tid.traddr.lower() + cid_traddr = cid['traddr'].lower() if cid_traddr != tid_traddr: return False @@ -290,7 +282,7 @@ class Udev: # For legacy kernels (i.e. older than 6.1), the existing cid.src_addr # is always undefined. We need to use advanced logic to determine # whether cid and tid match. - return Udev._cid_matches_tcp_tid_legacy(tid, cid) + return Udev._cid_matches_tcp_tid_legacy(tid, cid, ifaces) # The existing controller's cid.src_addr is always defined for kernel # 6.1 and later. We can use the existing controller's cid.src_addr to @@ -303,12 +295,12 @@ class Udev: return False # host-iface is an optional tcp-only parameter. - if tid.host_iface and tid.host_iface != iputil.get_interface(str(src_addr)): + if tid.host_iface and tid.host_iface != iputil.get_interface(ifaces, src_addr): return False elif tid.transport == 'fc': # host-traddr is mandatory for FC. - if tid.host_traddr != cid['host-traddr']: + if tid.host_traddr.lower() != cid['host-traddr'].lower(): return False elif tid.transport == 'rdma': @@ -326,17 +318,20 @@ class Udev: Discovery Controller. @return The device if a match is found, None otherwise. ''' - for device in self._context.list_devices( + devices = self._context.list_devices( subsystem='nvme', NVME_TRADDR=tid.traddr, NVME_TRSVCID=tid.trsvcid, NVME_TRTYPE=tid.transport - ): - if not self.is_dc_device(device): - continue + ) + if devices: + ifaces = iputil.net_if_addrs() + for device in devices: + if not self.is_dc_device(device): + continue - cid = self.get_cid(device) - if not self._cid_matches_tid(tid, cid): - continue + cid = self.get_cid(device) + if not self._cid_matches_tid(tid, cid, ifaces): + continue - return device + return device return None @@ -345,17 +340,20 @@ class Udev: I/O Controller. @return The device if a match is found, None otherwise. ''' - for device in self._context.list_devices( + devices = self._context.list_devices( subsystem='nvme', NVME_TRADDR=tid.traddr, NVME_TRSVCID=tid.trsvcid, NVME_TRTYPE=tid.transport - ): - if not self.is_ioc_device(device): - continue + ) + if devices: + ifaces = iputil.net_if_addrs() + for device in devices: + if not self.is_ioc_device(device): + continue - cid = self.get_cid(device) - if not self._cid_matches_tid(tid, cid): - continue + cid = self.get_cid(device) + if not self._cid_matches_tid(tid, cid, ifaces): + continue - return device + return device return None @@ -364,68 +362,63 @@ class Udev: @return A list of pyudev.device._device.Device objects ''' tids = [] - for device in self._context.list_devices(subsystem='nvme'): - if device.properties.get('NVME_TRTYPE', '') not in transports: - continue + devices = self._context.list_devices(subsystem='nvme') + if devices: + ifaces = iputil.net_if_addrs() + for device in devices: + if device.properties.get('NVME_TRTYPE', '') not in transports: + continue - if not self.is_ioc_device(device): - continue + if not self.is_ioc_device(device): + continue - tids.append(self.get_tid(device)) + tids.append(self.get_tid(device, ifaces)) return tids def _process_udev_event(self, event_source, condition): # pylint: disable=unused-argument if condition == GLib.IO_IN: - event_count = 0 - while True: - try: - device = self._monitor.poll(timeout=0) - except EnvironmentError as ex: - device = None - # This event seems to happen in bursts. So, let's suppress - # logging for 2 seconds to avoid filling the syslog. - self._log_event_count += 1 - now = time.time() - if now > self._log_event_soak_time: - logging.debug('Udev._process_udev_event() - %s [%s]', ex, self._log_event_count) - self._log_event_soak_time = now + 2 - self._log_event_count = 0 - - if device is None: - break - - event_count += 1 - self._device_event(device, event_count) + try: + self.__handle_events() + + except EnvironmentError as ex: + # Exceptions seem to happen in bursts. So, let's suppress + # logging for 2 seconds to avoid filling the syslog. + self._log_event_count += 1 + now = time.time() + if now > self._log_event_soak_time: + logging.debug('Udev._process_udev_event() - %s [%s]', ex, self._log_event_count) + self._log_event_soak_time = now + 2 + self._log_event_count = 0 return GLib.SOURCE_CONTINUE - @staticmethod - def __cback_names(action_cbacks, device_cback): - names = [] - for cback in action_cbacks: - names.append(cback.__name__ + '()') - if device_cback: - names.append(device_cback.__name__ + '()') - return names - - def _device_event(self, device, event_count): - action_cbacks = self._action_event_registry.get(device.action, set()) - device_cback = self._device_event_registry.get(device.sys_name, None) - - logging.debug( - 'Udev._device_event() - %-8s %-6s %-8s %s', - f'{device.sys_name}:', - device.action, - f'{event_count:2}:{device.sequence_number}', - self.__cback_names(action_cbacks, device_cback), - ) + def __handle_events(self): + event_count = 0 + read_device = partial(self._monitor.poll, timeout=0) + for device in iter(read_device, None): + if device is None: # This should never happen,... + break # ...but better safe than sorry. + + event_count += 1 + + action_cbacks = self._action_event_registry.get(device.action, None) + device_cback = self._device_event_registry.get(device.sys_name, None) + if action_cbacks or device_cback: + logging.debug( + 'Udev.__handle_events() - %-7s %-6s %2s:%s', + device.sys_name, + device.action, + event_count, + device.sequence_number, + ) - for action_cback in action_cbacks: - GLib.idle_add(action_cback, device) + if action_cbacks: + for action_cback in action_cbacks: + GLib.idle_add(action_cback, device) - if device_cback is not None: - GLib.idle_add(device_cback, device) + if device_cback is not None: + GLib.idle_add(device_cback, device) @staticmethod def _get_property(device, prop, default=''): @@ -470,7 +463,7 @@ class Udev: return attr_str[start:end] @staticmethod - def get_tid(device): + def get_tid(device, ifaces): '''@brief return the Transport ID associated with a udev device''' cid = Udev.get_cid(device) if cid['transport'] == 'tcp': @@ -479,7 +472,7 @@ class Udev: # We'll try to find the interface from the source address on # the connection. Only available if kernel exposes the source # address (src_addr) in the "address" attribute. - cid['host-iface'] = iputil.get_interface(src_addr) + cid['host-iface'] = iputil.get_interface(ifaces, iputil.get_ipaddress_obj(src_addr)) return trid.TID(cid) diff --git a/test/test-iputil.py b/test/test-iputil.py index 3af734f..b0a5448 100755 --- a/test/test-iputil.py +++ b/test/test-iputil.py @@ -31,14 +31,17 @@ class Test(unittest.TestCase): def test_get_interface(self): '''Check that get_interface() returns the right info''' + ifaces = iputil.net_if_addrs() for iface in self.ifaces: for addr_entry in iface['addr_info']: addr = ipaddress.ip_address(addr_entry['local']) # Link local addresses may appear on more than one interface and therefore cannot be used. if not addr.is_link_local: - self.assertEqual(iface['ifname'], iputil.get_interface(str(addr))) + self.assertEqual(iface['ifname'], iputil.get_interface(ifaces, addr)) - self.assertEqual('', iputil.get_interface('255.255.255.255')) + self.assertEqual('', iputil.get_interface(ifaces, iputil.get_ipaddress_obj('255.255.255.255'))) + self.assertEqual('', iputil.get_interface(ifaces, '')) + self.assertEqual('', iputil.get_interface(ifaces, None)) def test_mac2iface(self): for iface in self.ifaces: @@ -69,9 +72,7 @@ class Test(unittest.TestCase): self.assertNotIn(bad_trtype, l2) def test__data_matches_ip(self): - self.assertFalse(iputil._data_matches_ip(None, None, None)) - self.assertFalse(iputil._data_matches_ip(socket.AF_INET, None, None)) - self.assertFalse(iputil._data_matches_ip(socket.AF_INET6, None, None)) + self.assertFalse(iputil.ip_equal(None, None)) if __name__ == "__main__": diff --git a/test/test-nbft_conf.py b/test/test-nbft_conf.py index 072c3db..fa499e6 100755 --- a/test/test-nbft_conf.py +++ b/test/test-nbft_conf.py @@ -48,11 +48,12 @@ class Test(unittest.TestCase): self.assertEqual(nbft_conf.iocs, EXPECTED_IOCS) def test_dir_without_nbft_files(self): - conf.NbftConf.destroy() # Make sure singleton does not exist - with self.assertNoLogs(logger=logging.getLogger(), level='DEBUG'): - nbft_conf = conf.NbftConf('/tmp') - self.assertEqual(nbft_conf.dcs, []) - self.assertEqual(nbft_conf.iocs, []) + if hasattr(self, 'assertNoLogs'): # assertNoLogs only in Python 3.10 or later + conf.NbftConf.destroy() # Make sure singleton does not exist + with self.assertNoLogs(logger=logging.getLogger(), level='DEBUG'): + nbft_conf = conf.NbftConf('/tmp') + self.assertEqual(nbft_conf.dcs, []) + self.assertEqual(nbft_conf.iocs, []) if __name__ == "__main__": diff --git a/test/test-nvme_options.py b/test/test-nvme_options.py index adfba5f..91afc2c 100755 --- a/test/test-nvme_options.py +++ b/test/test-nvme_options.py @@ -2,7 +2,7 @@ import os import logging import unittest -from staslib import conf, log +from staslib import defs, conf, log from pyfakefs.fake_filesystem_unittest import TestCase @@ -10,7 +10,7 @@ class TestStandardNvmeFabricsFile(unittest.TestCase): def test_regular_user(self): conf.NvmeOptions.destroy() # Make sure singleton does not exist if os.path.exists('/dev/nvme-fabrics'): - if os.geteuid() != 0: + if os.geteuid() != 0 and defs.KERNEL_VERSION < defs.KERNEL_ALL_MIN_VERSION: with self.assertRaises(PermissionError): nvme_options = conf.NvmeOptions() else: diff --git a/test/test-udev.py b/test/test-udev.py index 9508de4..be257d9 100755 --- a/test/test-udev.py +++ b/test/test-udev.py @@ -3,7 +3,6 @@ import json import shutil import logging import unittest -import ipaddress import subprocess from staslib import defs, iputil, log, trid, udev @@ -294,6 +293,7 @@ class Test(unittest.TestCase): self.assertTrue(udev.UDEV.is_ioc_device(device)) def test__cid_matches_tid(self): + ifaces = iputil.net_if_addrs() for ifname, addrs in self.ifaces.items(): ############################################## # IPV4 @@ -321,10 +321,14 @@ class Test(unittest.TestCase): 'host-nqn': '', } for case_id, tid, match in get_tids_to_test(4, src_ipv4, ifname): - self.assertEqual(match, udev.UDEV._cid_matches_tid(tid, cid), msg=f'Test Case {case_id} failed') + self.assertEqual( + match, udev.UDEV._cid_matches_tid(tid, cid, ifaces), msg=f'Test Case {case_id} failed' + ) if case_id != 8: self.assertEqual( - match, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case {case_id} failed' + match, + udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), + msg=f'Legacy Test Case {case_id} failed', ) cid_legacy = { @@ -347,7 +351,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case A4.1 failed') + self.assertEqual( + True, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case A4.1 failed' + ) cid_legacy = { 'transport': 'tcp', @@ -368,9 +374,13 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case A4.2 failed') self.assertEqual( - True, udev.UDEV._cid_matches_tcp_tid_legacy(tid, cid_legacy), msg=f'Legacy Test Case A4.3 failed' + True, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case A4.2 failed' + ) + self.assertEqual( + True, + udev.UDEV._cid_matches_tcp_tid_legacy(tid, cid_legacy, ifaces), + msg=f'Legacy Test Case A4.3 failed', ) cid_legacy = { @@ -393,7 +403,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case B4 failed') + self.assertEqual( + False, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case B4 failed' + ) tid = trid.TID( { @@ -405,7 +417,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case C4 failed') + self.assertEqual( + False, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case C4 failed' + ) tid = trid.TID( { @@ -417,7 +431,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case D4 failed') + self.assertEqual( + True, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case D4 failed' + ) cid_legacy = { 'transport': 'tcp', @@ -440,7 +456,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case E4 failed') + self.assertEqual( + False, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case E4 failed' + ) tid = trid.TID( { @@ -452,7 +470,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case F4 failed') + self.assertEqual( + False, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case F4 failed' + ) tid = trid.TID( { @@ -467,7 +487,9 @@ class Test(unittest.TestCase): match = len(ipv4_addrs) == 1 and iputil.get_ipaddress_obj( ipv4_addrs[0], ipv4_mapped_convert=True ) == iputil.get_ipaddress_obj(tid.host_traddr, ipv4_mapped_convert=True) - self.assertEqual(match, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case G4 failed') + self.assertEqual( + match, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case G4 failed' + ) ############################################## # IPV6 @@ -495,9 +517,13 @@ class Test(unittest.TestCase): 'host-nqn': '', } for case_id, tid, match in get_tids_to_test(6, src_ipv6, ifname): - self.assertEqual(match, udev.UDEV._cid_matches_tid(tid, cid), msg=f'Test Case {case_id} failed') self.assertEqual( - match, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case {case_id} failed' + match, udev.UDEV._cid_matches_tid(tid, cid, ifaces), msg=f'Test Case {case_id} failed' + ) + self.assertEqual( + match, + udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), + msg=f'Legacy Test Case {case_id} failed', ) cid_legacy = { @@ -520,7 +546,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case A6.1 failed') + self.assertEqual( + True, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case A6.1 failed' + ) cid_legacy = { 'transport': 'tcp', @@ -541,9 +569,13 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case A6.2 failed') self.assertEqual( - True, udev.UDEV._cid_matches_tcp_tid_legacy(tid, cid_legacy), msg=f'Legacy Test Case A6.3 failed' + True, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case A6.2 failed' + ) + self.assertEqual( + True, + udev.UDEV._cid_matches_tcp_tid_legacy(tid, cid_legacy, ifaces), + msg=f'Legacy Test Case A6.3 failed', ) cid_legacy = { @@ -566,7 +598,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case B6 failed') + self.assertEqual( + False, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case B6 failed' + ) tid = trid.TID( { @@ -578,7 +612,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case C6 failed') + self.assertEqual( + False, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case C6 failed' + ) tid = trid.TID( { @@ -590,7 +626,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case D6 failed') + self.assertEqual( + True, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case D6 failed' + ) cid_legacy = { 'transport': 'tcp', @@ -613,7 +651,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case E6 failed') + self.assertEqual( + False, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case E6 failed' + ) tid = trid.TID( { @@ -625,7 +665,9 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case F6 failed') + self.assertEqual( + False, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case F6 failed' + ) tid = trid.TID( { @@ -640,7 +682,9 @@ class Test(unittest.TestCase): match = len(ipv6_addrs) == 1 and iputil.get_ipaddress_obj( ipv6_addrs[0], ipv4_mapped_convert=True ) == iputil.get_ipaddress_obj(tid.host_traddr, ipv4_mapped_convert=True) - self.assertEqual(match, udev.UDEV._cid_matches_tid(tid, cid_legacy), msg=f'Legacy Test Case G6 failed') + self.assertEqual( + match, udev.UDEV._cid_matches_tid(tid, cid_legacy, ifaces), msg=f'Legacy Test Case G6 failed' + ) ############################################## # FC @@ -664,7 +708,7 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid), msg=f'Test Case FC-1 failed') + self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid, ifaces), msg=f'Test Case FC-1 failed') tid = trid.TID( { @@ -676,7 +720,7 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid), msg=f'Test Case FC-2 failed') + self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid, ifaces), msg=f'Test Case FC-2 failed') ############################################## # RDMA @@ -700,7 +744,7 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid), msg=f'Test Case RDMA-1 failed') + self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid, ifaces), msg=f'Test Case RDMA-1 failed') tid = trid.TID( { @@ -712,7 +756,7 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid), msg=f'Test Case RDMA-2 failed') + self.assertEqual(False, udev.UDEV._cid_matches_tid(tid, cid, ifaces), msg=f'Test Case RDMA-2 failed') tid = trid.TID( { @@ -723,7 +767,7 @@ class Test(unittest.TestCase): 'host-nqn': '', } ) - self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid), msg=f'Test Case RDMA-3 failed') + self.assertEqual(True, udev.UDEV._cid_matches_tid(tid, cid, ifaces), msg=f'Test Case RDMA-3 failed') if __name__ == '__main__': diff --git a/usr/lib/systemd/system/meson.build b/usr/lib/systemd/system/meson.build index 0076b01..9fbc877 100644 --- a/usr/lib/systemd/system/meson.build +++ b/usr/lib/systemd/system/meson.build @@ -26,7 +26,7 @@ configure_file( input: 'stas-config@.service', output: 'stas-config@.service', install_dir: sd_unit_dir, - copy: true, + configuration: conf, ) configure_file( diff --git a/usr/lib/systemd/system/stas-config@.service b/usr/lib/systemd/system/stas-config@.service index f070a3c..620825c 100644 --- a/usr/lib/systemd/system/stas-config@.service +++ b/usr/lib/systemd/system/stas-config@.service @@ -5,13 +5,13 @@ # This file is part of NVMe STorage Appliance Services (nvme-stas). # [Unit] -Description=nvme-stas /etc/nvme/%i auto-generation +Description=nvme-stas @ETC@/nvme/%i auto-generation Documentation=man:stas-config@.service(8) -ConditionFileNotEmpty=|!/etc/nvme/%i +ConditionFileNotEmpty=|!@ETC@/nvme/%i [Service] Type=oneshot -ExecStart=/usr/bin/stasadm %i -f /etc/nvme/%i +ExecStart=/usr/bin/stasadm %i -f @ETC@/nvme/%i [Install] WantedBy=stas-config.target diff --git a/utils/nvmet/nvmet-large.conf b/utils/nvmet/nvmet-large.conf new file mode 100644 index 0000000..2e878de --- /dev/null +++ b/utils/nvmet/nvmet-large.conf @@ -0,0 +1,420 @@ +# 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': 8009, + 'trtype': 'tcp', + } + ], + + 'subsystems': [ + { + 'subsysnqn': 'subsystem1', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem2', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem3', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem4', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem5', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem6', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem7', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem8', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem9', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem10', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem11', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem12', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem13', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem14', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem15', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem16', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem17', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem18', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem19', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem20', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem21', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem22', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem23', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem24', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem25', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem26', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem27', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem28', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem29', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem30', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem31', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem32', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem33', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem34', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem35', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem36', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem37', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem38', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem39', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem40', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem41', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem42', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem43', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem44', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem45', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem46', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem47', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem48', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem49', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem50', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem51', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem52', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem53', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem54', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem55', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem56', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem57', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem58', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem59', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem60', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem61', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem62', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem63', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem64', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem65', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem66', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem67', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem68', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem69', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem70', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem71', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem72', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem73', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem74', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem75', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem76', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem77', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem78', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem79', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + { + 'subsysnqn': 'subsystem80', + 'port': 1, + 'namespaces': [1, 2, 3, 4, 5, 6], + }, + ], +} |