703 lines
26 KiB
Python
703 lines
26 KiB
Python
# Copyright (c) 2022, 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>
|
|
#
|
|
'''nvme-stas configuration module'''
|
|
|
|
import re
|
|
import os
|
|
import sys
|
|
import logging
|
|
import functools
|
|
import configparser
|
|
from staslib import defs, singleton, timeparse
|
|
|
|
__TOKEN_RE = re.compile(r'\s*;\s*')
|
|
__OPTION_RE = re.compile(r'\s*=\s*')
|
|
|
|
|
|
class InvalidOption(Exception):
|
|
'''Exception raised when an invalid option value is detected'''
|
|
|
|
|
|
def _parse_controller(controller):
|
|
'''@brief Parse a "controller" entry. Controller entries are strings
|
|
composed of several configuration parameters delimited by
|
|
semi-colons. Each configuration parameter is specified as a
|
|
"key=value" pair.
|
|
@return A dictionary of key-value pairs.
|
|
'''
|
|
options = dict()
|
|
tokens = __TOKEN_RE.split(controller)
|
|
for token in tokens:
|
|
if token:
|
|
try:
|
|
option, val = __OPTION_RE.split(token)
|
|
options[option.strip()] = val.strip()
|
|
except ValueError:
|
|
pass
|
|
|
|
return options
|
|
|
|
|
|
def _parse_single_val(text):
|
|
if isinstance(text, str):
|
|
return text
|
|
if not isinstance(text, list) or len(text) == 0:
|
|
return None
|
|
|
|
return text[-1]
|
|
|
|
|
|
def _parse_list(text):
|
|
return text if isinstance(text, list) else [text]
|
|
|
|
|
|
def _to_int(text):
|
|
try:
|
|
return int(_parse_single_val(text))
|
|
except (ValueError, TypeError):
|
|
raise InvalidOption # pylint: disable=raise-missing-from
|
|
|
|
|
|
def _to_bool(text, positive='true'):
|
|
return _parse_single_val(text).lower() == positive
|
|
|
|
|
|
def _to_ncc(text):
|
|
value = _to_int(text)
|
|
if value == 1: # 1 is invalid. A minimum of 2 is required (with the exception of 0, which is valid).
|
|
value = 2
|
|
return value
|
|
|
|
|
|
def _to_ip_family(text):
|
|
return tuple((4 if text == 'ipv4' else 6 for text in _parse_single_val(text).split('+')))
|
|
|
|
|
|
# ******************************************************************************
|
|
class OrderedMultisetDict(dict):
|
|
'''This class is used to change the behavior of configparser.ConfigParser
|
|
and allow multiple configuration parameters with the same key. The
|
|
result is a list of values.
|
|
'''
|
|
|
|
def __setitem__(self, key, value):
|
|
if key in self and isinstance(value, list):
|
|
self[key].extend(value)
|
|
else:
|
|
super().__setitem__(key, value)
|
|
|
|
def __getitem__(self, key):
|
|
value = super().__getitem__(key)
|
|
|
|
if isinstance(value, str):
|
|
return value.split('\n')
|
|
|
|
return value
|
|
|
|
|
|
class SvcConf(metaclass=singleton.Singleton): # pylint: disable=too-many-public-methods
|
|
'''Read and cache configuration file.'''
|
|
|
|
OPTION_CHECKER = {
|
|
'Global': {
|
|
'tron': {
|
|
'convert': _to_bool,
|
|
'default': False,
|
|
'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'),
|
|
},
|
|
'kato': {
|
|
'convert': _to_int,
|
|
},
|
|
'pleo': {
|
|
'convert': functools.partial(_to_bool, positive='enabled'),
|
|
'default': True,
|
|
'txt-chk': lambda text: _parse_single_val(text).lower() in ('disabled', 'enabled'),
|
|
},
|
|
'ip-family': {
|
|
'convert': _to_ip_family,
|
|
'default': (4, 6),
|
|
'txt-chk': lambda text: _parse_single_val(text) in ('ipv4', 'ipv6', 'ipv4+ipv6', 'ipv6+ipv4'),
|
|
},
|
|
'queue-size': {
|
|
'convert': _to_int,
|
|
'rng-chk': lambda value: None if value in range(16, 1025) else range(16, 1025),
|
|
},
|
|
'hdr-digest': {
|
|
'convert': _to_bool,
|
|
'default': False,
|
|
'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'),
|
|
},
|
|
'data-digest': {
|
|
'convert': _to_bool,
|
|
'default': False,
|
|
'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'),
|
|
},
|
|
'ignore-iface': {
|
|
'convert': _to_bool,
|
|
'default': False,
|
|
'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'),
|
|
},
|
|
'nr-io-queues': {
|
|
'convert': _to_int,
|
|
},
|
|
'ctrl-loss-tmo': {
|
|
'convert': _to_int,
|
|
},
|
|
'disable-sqflow': {
|
|
'convert': _to_bool,
|
|
'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'),
|
|
},
|
|
'nr-poll-queues': {
|
|
'convert': _to_int,
|
|
},
|
|
'nr-write-queues': {
|
|
'convert': _to_int,
|
|
},
|
|
'reconnect-delay': {
|
|
'convert': _to_int,
|
|
},
|
|
### BEGIN: LEGACY SECTION TO BE REMOVED ###
|
|
'persistent-connections': {
|
|
'convert': _to_bool,
|
|
'default': False,
|
|
'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'),
|
|
},
|
|
### END: LEGACY SECTION TO BE REMOVED ###
|
|
},
|
|
'Service Discovery': {
|
|
'zeroconf': {
|
|
'convert': functools.partial(_to_bool, positive='enabled'),
|
|
'default': True,
|
|
'txt-chk': lambda text: _parse_single_val(text).lower() in ('disabled', 'enabled'),
|
|
},
|
|
},
|
|
'Discovery controller connection management': {
|
|
'persistent-connections': {
|
|
'convert': _to_bool,
|
|
'default': True,
|
|
'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'),
|
|
},
|
|
'zeroconf-connections-persistence': {
|
|
'convert': lambda text: timeparse.timeparse(_parse_single_val(text)),
|
|
'default': timeparse.timeparse('72hours'),
|
|
},
|
|
},
|
|
'I/O controller connection management': {
|
|
'disconnect-scope': {
|
|
'convert': _parse_single_val,
|
|
'default': 'only-stas-connections',
|
|
'txt-chk': lambda text: _parse_single_val(text)
|
|
in ('only-stas-connections', 'all-connections-matching-disconnect-trtypes', 'no-disconnect'),
|
|
},
|
|
'disconnect-trtypes': {
|
|
# Use set() to eliminate potential duplicates
|
|
'convert': lambda text: set(_parse_single_val(text).split('+')),
|
|
'default': [
|
|
'tcp',
|
|
],
|
|
'lst-chk': ('tcp', 'rdma', 'fc'),
|
|
},
|
|
'connect-attempts-on-ncc': {
|
|
'convert': _to_ncc,
|
|
'default': 0,
|
|
},
|
|
},
|
|
'Controllers': {
|
|
'controller': {
|
|
'convert': _parse_list,
|
|
'default': [],
|
|
},
|
|
'exclude': {
|
|
'convert': _parse_list,
|
|
'default': [],
|
|
},
|
|
### BEGIN: LEGACY SECTION TO BE REMOVED ###
|
|
'blacklist': {
|
|
'convert': _parse_list,
|
|
'default': [],
|
|
},
|
|
### END: LEGACY SECTION TO BE REMOVED ###
|
|
},
|
|
}
|
|
|
|
def __init__(self, default_conf=None, conf_file='/dev/null'):
|
|
self._config = None
|
|
self._defaults = default_conf if default_conf else {}
|
|
|
|
if self._defaults is not None and len(self._defaults) != 0:
|
|
self._valid_conf = {}
|
|
for section, option in self._defaults:
|
|
self._valid_conf.setdefault(section, set()).add(option)
|
|
else:
|
|
self._valid_conf = None
|
|
|
|
self._conf_file = conf_file
|
|
self.reload()
|
|
|
|
def reload(self):
|
|
'''@brief Reload the configuration file.'''
|
|
self._config = self._read_conf_file()
|
|
|
|
@property
|
|
def conf_file(self):
|
|
'''Return the configuration file name'''
|
|
return self._conf_file
|
|
|
|
def set_conf_file(self, fname):
|
|
'''Set the configuration file name and reload config'''
|
|
self._conf_file = fname
|
|
self.reload()
|
|
|
|
def get_option(self, section, option, ignore_default=False): # pylint: disable=too-many-locals
|
|
'''Retrieve @option from @section, convert raw text to
|
|
appropriate object type, and validate.'''
|
|
try:
|
|
checker = self.OPTION_CHECKER[section][option]
|
|
except KeyError:
|
|
logging.error('Requesting invalid section=%s and/or option=%s', section, option)
|
|
raise
|
|
|
|
default = checker.get('default', None)
|
|
|
|
try:
|
|
text = self._config.get(section=section, option=option)
|
|
except (configparser.NoSectionError, configparser.NoOptionError, KeyError):
|
|
return None if ignore_default else self._defaults.get((section, option), default)
|
|
|
|
return self._check(text, section, option, default)
|
|
|
|
tron = property(functools.partial(get_option, section='Global', option='tron'))
|
|
kato = property(functools.partial(get_option, section='Global', option='kato'))
|
|
ip_family = property(functools.partial(get_option, section='Global', option='ip-family'))
|
|
queue_size = property(functools.partial(get_option, section='Global', option='queue-size'))
|
|
hdr_digest = property(functools.partial(get_option, section='Global', option='hdr-digest'))
|
|
data_digest = property(functools.partial(get_option, section='Global', option='data-digest'))
|
|
ignore_iface = property(functools.partial(get_option, section='Global', option='ignore-iface'))
|
|
pleo_enabled = property(functools.partial(get_option, section='Global', option='pleo'))
|
|
nr_io_queues = property(functools.partial(get_option, section='Global', option='nr-io-queues'))
|
|
ctrl_loss_tmo = property(functools.partial(get_option, section='Global', option='ctrl-loss-tmo'))
|
|
disable_sqflow = property(functools.partial(get_option, section='Global', option='disable-sqflow'))
|
|
nr_poll_queues = property(functools.partial(get_option, section='Global', option='nr-poll-queues'))
|
|
nr_write_queues = property(functools.partial(get_option, section='Global', option='nr-write-queues'))
|
|
reconnect_delay = property(functools.partial(get_option, section='Global', option='reconnect-delay'))
|
|
|
|
zeroconf_enabled = property(functools.partial(get_option, section='Service Discovery', option='zeroconf'))
|
|
|
|
zeroconf_persistence_sec = property(
|
|
functools.partial(
|
|
get_option, section='Discovery controller connection management', option='zeroconf-connections-persistence'
|
|
)
|
|
)
|
|
|
|
disconnect_scope = property(
|
|
functools.partial(get_option, section='I/O controller connection management', option='disconnect-scope')
|
|
)
|
|
disconnect_trtypes = property(
|
|
functools.partial(get_option, section='I/O controller connection management', option='disconnect-trtypes')
|
|
)
|
|
connect_attempts_on_ncc = property(
|
|
functools.partial(get_option, section='I/O controller connection management', option='connect-attempts-on-ncc')
|
|
)
|
|
|
|
@property
|
|
def stypes(self):
|
|
'''@brief Get the DNS-SD/mDNS service types.'''
|
|
return ['_nvme-disc._tcp', '_nvme-disc._udp'] if self.zeroconf_enabled else list()
|
|
|
|
@property
|
|
def persistent_connections(self):
|
|
'''@brief return the "persistent-connections" config parameter'''
|
|
section = 'Discovery controller connection management'
|
|
option = 'persistent-connections'
|
|
|
|
value = self.get_option(section, option, ignore_default=True)
|
|
legacy = self.get_option('Global', 'persistent-connections', ignore_default=True)
|
|
|
|
if value is None and legacy is None:
|
|
return self._defaults.get((section, option), True)
|
|
|
|
return value or legacy
|
|
|
|
def get_controllers(self):
|
|
'''@brief Return the list of controllers in the config file.
|
|
Each controller is in the form of a dictionary as follows.
|
|
Note that some of the keys are optional.
|
|
{
|
|
'transport': [TRANSPORT],
|
|
'traddr': [TRADDR],
|
|
'trsvcid': [TRSVCID],
|
|
'host-traddr': [TRADDR],
|
|
'host-iface': [IFACE],
|
|
'subsysnqn': [NQN],
|
|
'dhchap-ctrl-secret': [KEY],
|
|
'hdr-digest': [BOOL]
|
|
'data-digest': [BOOL]
|
|
'nr-io-queues': [NUMBER]
|
|
'nr-write-queues': [NUMBER]
|
|
'nr-poll-queues': [NUMBER]
|
|
'queue-size': [SIZE]
|
|
'kato': [KATO]
|
|
'reconnect-delay': [SECONDS]
|
|
'ctrl-loss-tmo': [SECONDS]
|
|
'disable-sqflow': [BOOL]
|
|
}
|
|
'''
|
|
controller_list = self.get_option('Controllers', 'controller')
|
|
cids = [_parse_controller(controller) for controller in controller_list]
|
|
for cid in cids:
|
|
try:
|
|
# replace 'nqn' key by 'subsysnqn', if present.
|
|
cid['subsysnqn'] = cid.pop('nqn')
|
|
except KeyError:
|
|
pass
|
|
|
|
# Verify values of the options used to overload the matching [Global] options
|
|
for option in cid:
|
|
if option in self.OPTION_CHECKER['Global']:
|
|
value = self._check(cid[option], 'Global', option, None)
|
|
if value is not None:
|
|
cid[option] = value
|
|
|
|
return cids
|
|
|
|
def get_excluded(self):
|
|
'''@brief Return the list of excluded controllers in the config file.
|
|
Each excluded controller is in the form of a dictionary
|
|
as follows. All the keys are optional.
|
|
{
|
|
'transport': [TRANSPORT],
|
|
'traddr': [TRADDR],
|
|
'trsvcid': [TRSVCID],
|
|
'host-iface': [IFACE],
|
|
'subsysnqn': [NQN],
|
|
}
|
|
'''
|
|
controller_list = self.get_option('Controllers', 'exclude')
|
|
|
|
# 2022-09-20: Look for "blacklist". This is for backwards compatibility
|
|
# with releases 1.0 to 1.1.6. This is to be phased out (i.e. remove by 2024)
|
|
controller_list += self.get_option('Controllers', 'blacklist')
|
|
|
|
excluded = [_parse_controller(controller) for controller in controller_list]
|
|
for controller in excluded:
|
|
controller.pop('host-traddr', None) # remove host-traddr
|
|
try:
|
|
# replace 'nqn' key by 'subsysnqn', if present.
|
|
controller['subsysnqn'] = controller.pop('nqn')
|
|
except KeyError:
|
|
pass
|
|
return excluded
|
|
|
|
def _check(self, text, section, option, default):
|
|
checker = self.OPTION_CHECKER[section][option]
|
|
text_checker = checker.get('txt-chk', None)
|
|
if text_checker is not None and not text_checker(text):
|
|
logging.warning(
|
|
'File:%s [%s]: %s - Text check found invalid value "%s". Default will be used',
|
|
self.conf_file,
|
|
section,
|
|
option,
|
|
text,
|
|
)
|
|
return self._defaults.get((section, option), default)
|
|
|
|
converter = checker.get('convert', None)
|
|
try:
|
|
value = converter(text)
|
|
except InvalidOption:
|
|
logging.warning(
|
|
'File:%s [%s]: %s - Data converter found invalid value "%s". Default will be used',
|
|
self.conf_file,
|
|
section,
|
|
option,
|
|
text,
|
|
)
|
|
return self._defaults.get((section, option), default)
|
|
|
|
value_in_range = checker.get('rng-chk', None)
|
|
if value_in_range is not None:
|
|
expected_range = value_in_range(value)
|
|
if expected_range is not None:
|
|
logging.warning(
|
|
'File:%s [%s]: %s - "%s" is not within range %s..%s. Default will be used',
|
|
self.conf_file,
|
|
section,
|
|
option,
|
|
value,
|
|
min(expected_range),
|
|
max(expected_range),
|
|
)
|
|
return self._defaults.get((section, option), default)
|
|
|
|
list_checker = checker.get('lst-chk', None)
|
|
if list_checker:
|
|
values = set()
|
|
for item in value:
|
|
if item not in list_checker:
|
|
logging.warning(
|
|
'File:%s [%s]: %s - List checker found invalid item "%s" will be ignored.',
|
|
self.conf_file,
|
|
section,
|
|
option,
|
|
item,
|
|
)
|
|
else:
|
|
values.add(item)
|
|
|
|
if len(values) == 0:
|
|
return self._defaults.get((section, option), default)
|
|
|
|
value = list(values)
|
|
|
|
return value
|
|
|
|
def _read_conf_file(self):
|
|
'''@brief Read the configuration file if the file exists.'''
|
|
config = configparser.ConfigParser(
|
|
default_section=None,
|
|
allow_no_value=True,
|
|
delimiters=('='),
|
|
interpolation=None,
|
|
strict=False,
|
|
dict_type=OrderedMultisetDict,
|
|
)
|
|
if self._conf_file and os.path.isfile(self._conf_file):
|
|
config.read(self._conf_file)
|
|
|
|
# Parse Configuration and validate.
|
|
if self._valid_conf is not None:
|
|
invalid_sections = set()
|
|
for section in config.sections():
|
|
if section not in self._valid_conf:
|
|
invalid_sections.add(section)
|
|
else:
|
|
invalid_options = set()
|
|
for option in config.options(section):
|
|
if option not in self._valid_conf.get(section, []):
|
|
invalid_options.add(option)
|
|
|
|
if len(invalid_options) != 0:
|
|
logging.error(
|
|
'File:%s [%s] contains invalid options: %s',
|
|
self.conf_file,
|
|
section,
|
|
invalid_options,
|
|
)
|
|
|
|
if len(invalid_sections) != 0:
|
|
logging.error(
|
|
'File:%s contains invalid sections: %s',
|
|
self.conf_file,
|
|
invalid_sections,
|
|
)
|
|
|
|
return config
|
|
|
|
|
|
# ******************************************************************************
|
|
class SysConf(metaclass=singleton.Singleton):
|
|
'''Read and cache the host configuration file.'''
|
|
|
|
def __init__(self, conf_file=defs.SYS_CONF_FILE):
|
|
self._config = None
|
|
self._conf_file = conf_file
|
|
self.reload()
|
|
|
|
def reload(self):
|
|
'''@brief Reload the configuration file.'''
|
|
self._config = self._read_conf_file()
|
|
|
|
@property
|
|
def conf_file(self):
|
|
'''Return the configuration file name'''
|
|
return self._conf_file
|
|
|
|
def set_conf_file(self, fname):
|
|
'''Set the configuration file name and reload config'''
|
|
self._conf_file = fname
|
|
self.reload()
|
|
|
|
def as_dict(self):
|
|
'''Return configuration as a dictionary'''
|
|
return {
|
|
'hostnqn': self.hostnqn,
|
|
'hostid': self.hostid,
|
|
'hostkey': self.hostkey,
|
|
'symname': self.hostsymname,
|
|
}
|
|
|
|
@property
|
|
def hostnqn(self):
|
|
'''@brief return the host NQN
|
|
@return: Host NQN
|
|
@raise: Host NQN is mandatory. The program will terminate if a
|
|
Host NQN cannot be determined.
|
|
'''
|
|
try:
|
|
value = self.__get_value('Host', 'nqn', defs.NVME_HOSTNQN)
|
|
except FileNotFoundError as ex:
|
|
sys.exit(f'Error reading mandatory Host NQN (see stasadm --help): {ex}')
|
|
|
|
if value is not None and not value.startswith('nqn.'):
|
|
sys.exit(f'Error Host NQN "{value}" should start with "nqn."')
|
|
|
|
return value
|
|
|
|
@property
|
|
def hostid(self):
|
|
'''@brief return the host ID
|
|
@return: Host ID
|
|
@raise: Host ID is mandatory. The program will terminate if a
|
|
Host ID cannot be determined.
|
|
'''
|
|
try:
|
|
value = self.__get_value('Host', 'id', defs.NVME_HOSTID)
|
|
except FileNotFoundError as ex:
|
|
sys.exit(f'Error reading mandatory Host ID (see stasadm --help): {ex}')
|
|
|
|
return value
|
|
|
|
@property
|
|
def hostkey(self):
|
|
'''@brief return the host key
|
|
@return: Host key
|
|
@raise: Host key is optional, but mandatory if authorization will be performed.
|
|
'''
|
|
try:
|
|
value = self.__get_value('Host', 'key', defs.NVME_HOSTKEY)
|
|
except FileNotFoundError as ex:
|
|
logging.info('Host key undefined: %s', ex)
|
|
value = None
|
|
|
|
return value
|
|
|
|
@property
|
|
def hostsymname(self):
|
|
'''@brief return the host symbolic name (or None)
|
|
@return: symbolic name or None
|
|
'''
|
|
try:
|
|
value = self.__get_value('Host', 'symname')
|
|
except FileNotFoundError as ex:
|
|
logging.warning('Error reading host symbolic name (will remain undefined): %s', ex)
|
|
value = None
|
|
|
|
return value
|
|
|
|
def _read_conf_file(self):
|
|
'''@brief Read the configuration file if the file exists.'''
|
|
config = configparser.ConfigParser(
|
|
default_section=None, allow_no_value=True, delimiters=('='), interpolation=None, strict=False
|
|
)
|
|
if os.path.isfile(self._conf_file):
|
|
config.read(self._conf_file)
|
|
return config
|
|
|
|
def __get_value(self, section, option, default_file=None):
|
|
'''@brief A configuration file consists of sections, each led by a
|
|
[section] header, followed by key/value entries separated
|
|
by a equal sign (=). This method retrieves the value
|
|
associated with the key @option from the section @section.
|
|
If the value starts with the string "file://", then the value
|
|
will be retrieved from that file.
|
|
|
|
@param section: Configuration section
|
|
@param option: The key to look for
|
|
@param default_file: A file that contains the default value
|
|
|
|
@return: On success, the value associated with the key. On failure,
|
|
this method will return None is a default_file is not
|
|
specified, or will raise an exception if a file is not
|
|
found.
|
|
|
|
@raise: This method will raise the FileNotFoundError exception if
|
|
the value retrieved is a file that does not exist.
|
|
'''
|
|
try:
|
|
value = self._config.get(section=section, option=option)
|
|
if not value.startswith('file://'):
|
|
return value
|
|
file = value[7:]
|
|
except (configparser.NoSectionError, configparser.NoOptionError, KeyError):
|
|
if default_file is None:
|
|
return None
|
|
file = default_file
|
|
|
|
try:
|
|
with open(file) as f: # pylint: disable=unspecified-encoding
|
|
return f.readline().split()[0]
|
|
except IndexError:
|
|
return None
|
|
|
|
|
|
# ******************************************************************************
|
|
class NvmeOptions(metaclass=singleton.Singleton):
|
|
'''Object used to read and cache contents of file /dev/nvme-fabrics.
|
|
Note that this file was not readable prior to Linux 5.16.
|
|
'''
|
|
|
|
def __init__(self):
|
|
# Supported options can be determined by looking at the kernel version
|
|
# or by reading '/dev/nvme-fabrics'. The ability to read the options
|
|
# from '/dev/nvme-fabrics' was only introduced in kernel 5.17, but may
|
|
# have been backported to older kernels. In any case, if the kernel
|
|
# version meets the minimum version for that option, then we don't
|
|
# even need to read '/dev/nvme-fabrics'.
|
|
self._supported_options = {
|
|
'discovery': defs.KERNEL_VERSION >= defs.KERNEL_TP8013_MIN_VERSION,
|
|
'host_iface': defs.KERNEL_VERSION >= defs.KERNEL_IFACE_MIN_VERSION,
|
|
'dhchap_secret': defs.KERNEL_VERSION >= defs.KERNEL_HOSTKEY_MIN_VERSION,
|
|
'dhchap_ctrl_secret': defs.KERNEL_VERSION >= defs.KERNEL_CTRLKEY_MIN_VERSION,
|
|
}
|
|
|
|
# If some of the options are False, we need to check wether they can be
|
|
# read from '/dev/nvme-fabrics'. This method allows us to determine that
|
|
# an older kernel actually supports a specific option because it was
|
|
# backported to that kernel.
|
|
if not all(self._supported_options.values()): # At least one option is False.
|
|
try:
|
|
with open('/dev/nvme-fabrics') as f: # pylint: disable=unspecified-encoding
|
|
options = [option.split('=')[0].strip() for option in f.readline().rstrip('\n').split(',')]
|
|
except PermissionError: # Must be root to read this file
|
|
raise
|
|
except (OSError, FileNotFoundError):
|
|
logging.warning('Cannot determine which NVMe options the kernel supports')
|
|
else:
|
|
for option, supported in self._supported_options.items():
|
|
if not supported:
|
|
self._supported_options[option] = option in options
|
|
|
|
def __str__(self):
|
|
return f'supported options: {self._supported_options}'
|
|
|
|
def get(self):
|
|
'''get the supported options as a dict'''
|
|
return self._supported_options
|
|
|
|
@property
|
|
def discovery_supp(self):
|
|
'''This option adds support for TP8013'''
|
|
return self._supported_options['discovery']
|
|
|
|
@property
|
|
def host_iface_supp(self):
|
|
'''This option allows forcing connections to go over
|
|
a specific interface regardless of the routing tables.
|
|
'''
|
|
return self._supported_options['host_iface']
|
|
|
|
@property
|
|
def dhchap_hostkey_supp(self):
|
|
'''This option allows specifying the host DHCHAP key used for authentication.'''
|
|
return self._supported_options['dhchap_secret']
|
|
|
|
@property
|
|
def dhchap_ctrlkey_supp(self):
|
|
'''This option allows specifying the controller DHCHAP key used for authentication.'''
|
|
return self._supported_options['dhchap_ctrl_secret']
|