Skip to content

Commit

Permalink
Merge pull request #358 from martin-belanger/nbft-v4
Browse files Browse the repository at this point in the history
nbft: Add NbftConf() object to retrieve and cache NBFT data
  • Loading branch information
martin-belanger authored Apr 21, 2023
2 parents 77d1efe + 9ed56bb commit fc7cece
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 73 deletions.
1 change: 1 addition & 0 deletions coverage.sh.in
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ run_unit_test ${TEST_DIR}/test-gtimer.py
run_unit_test ${TEST_DIR}/test-iputil.py
run_unit_test ${TEST_DIR}/test-log.py
run_unit_test ${TEST_DIR}/test-nbft.py
run_unit_test ${TEST_DIR}/test-nbft_conf.py
run_unit_test sudo ${TEST_DIR}/test-nvme_options.py # Test both with super user...
run_unit_test ${TEST_DIR}/test-nvme_options.py # ... and with regular user
run_unit_test ${TEST_DIR}/test-service.py
Expand Down
84 changes: 83 additions & 1 deletion staslib/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
import logging
import functools
import configparser
from staslib import defs, singleton, timeparse
from urllib.parse import urlparse
from staslib import defs, iputil, nbft, singleton, timeparse

__TOKEN_RE = re.compile(r'\s*;\s*')
__OPTION_RE = re.compile(r'\s*=\s*')
Expand Down Expand Up @@ -702,3 +703,84 @@ def dhchap_hostkey_supp(self):
def dhchap_ctrlkey_supp(self):
'''This option allows specifying the controller DHCHAP key used for authentication.'''
return self._supported_options['dhchap_ctrl_secret']


# ******************************************************************************
class NbftConf(metaclass=singleton.Singleton): # pylint: disable=too-few-public-methods
'''Read and cache configuration file.'''

def __init__(self, root_dir=defs.NBFT_SYSFS_PATH):
self._disc_ctrls = []
self._subs_ctrls = []

for data in nbft.get_nbft_files(root_dir).values():
hfis = data.get('hfi', [])
discovery = data.get('discovery', [])
subsystem = data.get('subsystem', [])

self._disc_ctrls.extend(NbftConf.__nbft_disc_to_cids(discovery, hfis))
self._subs_ctrls.extend(NbftConf.__nbft_subs_to_cids(subsystem, hfis))

dcs = property(lambda self: self._disc_ctrls)
iocs = property(lambda self: self._subs_ctrls)

@staticmethod
def __nbft_disc_to_cids(discovery, hfis):
cids = []

for ctrl in discovery:
cid = NbftConf.__uri2cid(ctrl['uri'])
cid['subsysnqn'] = ctrl['nqn']

host_iface = NbftConf.__get_host_iface(ctrl.get('hfi_index'), hfis)
if host_iface:
cid['host-iface'] = host_iface

cids.append(cid)

return cids

@staticmethod
def __nbft_subs_to_cids(subsystem, hfis):
cids = []

for ctrl in subsystem:
cid = {
'transport': ctrl['trtype'],
'traddr': ctrl['traddr'],
'trsvcid': ctrl['trsvcid'],
'subsysnqn': ctrl['subsys_nqn'],
'hdr-digest': ctrl['pdu_header_digest_required'],
'data-digest': ctrl['data_digest_required'],
}

indexes = ctrl.get('hfi_indexes')
if isinstance(indexes, list) and len(indexes) > 0:
host_iface = NbftConf.__get_host_iface(indexes[0], hfis)
if host_iface:
cid['host-iface'] = host_iface

cids.append(cid)

return cids

@staticmethod
def __get_host_iface(indx, hfis):
if indx is None or indx >= len(hfis):
return None

mac = hfis[indx].get('mac_addr')
if mac is None:
return None

return iputil.mac2iface(mac)

@staticmethod
def __uri2cid(uri: str):
'''Convert a URI of the form "nvme+tcp://100.71.103.50:8009/" to a Controller ID'''
obj = urlparse(uri)
return {
'transport': obj.scheme.split('+')[1],
'traddr': obj.hostname,
'trsvcid': str(obj.port),
}
65 changes: 14 additions & 51 deletions staslib/iputil.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@

import struct
import socket
import logging
import ipaddress
from staslib import conf

RTM_BASE = 16
RTM_GETLINK = 18
Expand All @@ -21,26 +19,29 @@
NLM_F_REQUEST = 0x01
NLM_F_ROOT = 0x100
NLMSG_DONE = 3
IFLA_ADDRESS = 1
NLMSGHDR_SZ = 16
NLMSG_HDRLEN = 16
IFADDRMSG_SZ = 8
IFINFOMSG_SZ = 16
RTATTR_SZ = 4
ARPHRD_ETHER = 1
ARPHRD_LOOPBACK = 772
NLMSG_LENGTH = lambda msg_len: msg_len + NLMSG_HDRLEN # pylint: disable=unnecessary-lambda-assignment

RTATTR_SZ = 4
RTA_ALIGN = lambda length: ((length + 3) & ~3) # pylint: disable=unnecessary-lambda-assignment
IFLA_ADDRESS = 1


def _nlmsghdr(nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid, msg_len: int):
'''Implement this C struct:
struct nlmsghdr {
__u32 nlmsg_len; /* Length of message including header, but excluding length of nlmsg_len itself */
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* Message content */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process port ID */
};
'''
return struct.pack('<LHHLL', 12 + msg_len, nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid)
return struct.pack('<LHHLL', NLMSG_LENGTH(msg_len), nlmsg_type, nlmsg_flags, nlmsg_seq, nlmsg_pid)


def _ifaddrmsg(family=0, prefixlen=0, flags=0, scope=0, index=0):
Expand Down Expand Up @@ -99,14 +100,14 @@ def mac2iface(mac: str): # pylint: disable=too-many-locals
if nlmsg_idx >= len(nlmsg):
nlmsg += sock.recv(8192)

nlmsghdr = nlmsg[nlmsg_idx : nlmsg_idx + NLMSGHDR_SZ]
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_BASE:
msg_indx = nlmsg_idx + NLMSGHDR_SZ
msg_indx = nlmsg_idx + NLMSG_HDRLEN
msg = nlmsg[msg_indx : msg_indx + IFINFOMSG_SZ] # ifinfomsg
_, _, ifi_type, ifi_index, _, _ = struct.unpack('<BBHiII', msg)

Expand All @@ -120,7 +121,7 @@ def mac2iface(mac: str): # pylint: disable=too-many-locals
if _data_matches_mac(data, mac):
return socket.if_indextoname(ifi_index)

rta_len = (rta_len + 3) & ~3 # Round up to multiple of 4
rta_len = RTA_ALIGN(rta_len) # Round up to multiple of 4
rtattr_indx += rta_len # Move to next rtattr

nlmsg_idx += nlmsg_len # Move to next Netlink message
Expand Down Expand Up @@ -163,14 +164,14 @@ def _iface_of(src_addr): # pylint: disable=too-many-locals
if nlmsg_idx >= len(nlmsg):
nlmsg += sock.recv(8192)

nlmsghdr = nlmsg[nlmsg_idx : nlmsg_idx + NLMSGHDR_SZ]
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 + NLMSGHDR_SZ
msg_indx = nlmsg_idx + NLMSG_HDRLEN
msg = nlmsg[msg_indx : msg_indx + IFADDRMSG_SZ] # ifaddrmsg
ifa_family, _, _, _, ifa_index = struct.unpack('<BBBBL', msg)

Expand All @@ -183,7 +184,7 @@ def _iface_of(src_addr): # pylint: disable=too-many-locals
if _data_matches_ip(ifa_family, data, src_addr):
return socket.if_indextoname(ifa_index)

rta_len = (rta_len + 3) & ~3 # Round up to multiple of 4
rta_len = RTA_ALIGN(rta_len) # Round up to multiple of 4
rtattr_indx += rta_len # Move to next rtattr

nlmsg_idx += nlmsg_len # Move to next Netlink message
Expand Down Expand Up @@ -215,41 +216,3 @@ def get_interface(src_addr):
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)


# ******************************************************************************
def remove_invalid_addresses(controllers: list):
'''@brief Remove controllers with invalid addresses from the list of controllers.
@param controllers: List of TIDs
'''
service_conf = conf.SvcConf()
valid_controllers = list()
for controller in controllers:
if controller.transport in ('tcp', 'rdma'):
# Let's make sure that traddr is
# syntactically a valid IPv4 or IPv6 address.
ip = get_ipaddress_obj(controller.traddr)
if ip is None:
logging.warning('%s IP address is not valid', controller)
continue

# Let's make sure the address family is enabled.
if ip.version not in service_conf.ip_family:
logging.debug(
'%s ignored because IPv%s is disabled in %s',
controller,
ip.version,
service_conf.conf_file,
)
continue

valid_controllers.append(controller)

elif controller.transport in ('fc', 'loop'):
# At some point, need to validate FC addresses as well...
valid_controllers.append(controller)

else:
logging.warning('Invalid transport %s', controller.transport)

return valid_controllers
1 change: 1 addition & 0 deletions staslib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ files_to_copy = [
'gutil.py',
'iputil.py',
'log.py',
'nbft.py',
'service.py',
'singleton.py',
'stas.py',
Expand Down
28 changes: 28 additions & 0 deletions staslib/nbft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright (c) 2023, Dell Inc. or its subsidiaries. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# See the LICENSE file for details.
#
# This file is part of NVMe STorage Appliance Services (nvme-stas).
#
# Authors: Martin Belanger <Martin.Belanger@dell.com>
#
'''Code used to access the NVMe Boot Firmware Tables'''

import os
import glob
import logging
from libnvme import nvme
from staslib import defs


def get_nbft_files(root_dir=defs.NBFT_SYSFS_PATH):
"""Return a dictionary containing the NBFT data for all the NBFT binary files located in @root_dir"""
if not defs.HAS_NBFT_SUPPORT:
logging.warning(
"libnvme-%s does not have NBFT support. Please upgrade libnvme.",
defs.LIBNVME_VERSION,
)
return {}

pathname = os.path.join(root_dir, defs.NBFT_SYSFS_FILENAME)
return {fname: nvme.nbft_get(fname) for fname in glob.iglob(pathname=pathname)} # pylint: disable=no-member
6 changes: 3 additions & 3 deletions staslib/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from gi.repository import GLib
from systemd.daemon import notify as sd_notify
from staslib import avahi, conf, ctrl, defs, gutil, iputil, stas, timeparse, trid, udev
from staslib import avahi, conf, ctrl, defs, gutil, stas, timeparse, trid, udev


# ******************************************************************************
Expand Down Expand Up @@ -402,7 +402,7 @@ def _config_ctrls_finish(self, configured_ctrl_list: list): # pylint: disable=t
logging.debug('Stac._config_ctrls_finish() - discovered_ctrl_list = %s', discovered_ctrl_list)

controllers = stas.remove_excluded(configured_ctrl_list + discovered_ctrl_list)
controllers = iputil.remove_invalid_addresses(controllers)
controllers = stas.remove_invalid_addresses(controllers)

new_controller_tids = set(controllers)
cur_controller_tids = set(self._controllers.keys())
Expand Down Expand Up @@ -752,7 +752,7 @@ def _config_ctrls_finish(self, configured_ctrl_list: list):

all_ctrls = configured_ctrl_list + discovered_ctrl_list + referral_ctrl_list
controllers = stas.remove_excluded(all_ctrls)
controllers = iputil.remove_invalid_addresses(controllers)
controllers = stas.remove_invalid_addresses(controllers)

new_controller_tids = set(controllers)
cur_controller_tids = set(self._controllers.keys())
Expand Down
48 changes: 35 additions & 13 deletions staslib/stas.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@
import os
import sys
import abc
import glob
import signal
import pickle
import logging
import dasbus.connection
from libnvme import nvme
from gi.repository import Gio, GLib
from systemd.daemon import notify as sd_notify
from staslib import conf, defs, gutil, log, trid
from staslib import conf, defs, gutil, iputil, log, trid

try:
# Python 3.9 or later
Expand Down Expand Up @@ -89,17 +87,41 @@ def check_if_allowed_to_continue():


# ******************************************************************************
def get_nbft_files(root_dir=defs.NBFT_SYSFS_PATH):
"""Return a dictionary containing the NBFT data for all the NBFT binary files located in @root_dir"""
if not defs.HAS_NBFT_SUPPORT:
logging.warning(
"libnvme-%s does not have NBFT support. Please upgrade libnvme.",
defs.LIBNVME_VERSION,
)
return {}
def remove_invalid_addresses(controllers: list):
'''@brief Remove controllers with invalid addresses from the list of controllers.
@param controllers: List of TIDs
'''
service_conf = conf.SvcConf()
valid_controllers = list()
for controller in controllers:
if controller.transport in ('tcp', 'rdma'):
# Let's make sure that traddr is
# syntactically a valid IPv4 or IPv6 address.
ip = iputil.get_ipaddress_obj(controller.traddr)
if ip is None:
logging.warning('%s IP address is not valid', controller)
continue

# Let's make sure the address family is enabled.
if ip.version not in service_conf.ip_family:
logging.debug(
'%s ignored because IPv%s is disabled in %s',
controller,
ip.version,
service_conf.conf_file,
)
continue

valid_controllers.append(controller)

elif controller.transport in ('fc', 'loop'):
# At some point, need to validate FC addresses as well...
valid_controllers.append(controller)

else:
logging.warning('Invalid transport %s', controller.transport)

pathname = os.path.join(root_dir, defs.NBFT_SYSFS_FILENAME)
return {fname: nvme.nbft_get(fname) for fname in glob.iglob(pathname=pathname)} # pylint: disable=no-member
return valid_controllers


# ******************************************************************************
Expand Down
1 change: 1 addition & 0 deletions test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ else
['Test KernelVersion', [], [srce_dir / 'test-version.py', ]],
['Test log', ['pyfakefs'], [srce_dir / 'test-log.py', ]],
['Test NBFT', [], [srce_dir / 'test-nbft.py', ]],
['Test NbftConf', [], [srce_dir / 'test-nbft_conf.py', ]],
['Test NvmeOptions', ['pyfakefs'], [srce_dir / 'test-nvme_options.py', ]],
['Test Service', ['pyfakefs'], [srce_dir / 'test-service.py', ]],
['Test TID', [], [srce_dir / 'test-transport_id.py', ]],
Expand Down
Loading

0 comments on commit fc7cece

Please sign in to comment.