405 lines
13 KiB
Python
Executable file
405 lines
13 KiB
Python
Executable file
#!/usr/bin/python3
|
|
# Copyright (c) 2021, 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>
|
|
|
|
# PYTHON_ARGCOMPLETE_OK
|
|
|
|
import os
|
|
import sys
|
|
import pprint
|
|
import pathlib
|
|
import subprocess
|
|
from argparse import ArgumentParser
|
|
|
|
VERSION = 1.0
|
|
DEFAULT_CONFIG_FILE = './nvmet.conf'
|
|
|
|
|
|
class Fore:
|
|
RED = '\033[31m'
|
|
GREEN = '\033[32m'
|
|
|
|
|
|
class Style:
|
|
RESET_ALL = '\033[0m'
|
|
|
|
|
|
def _get_loaded_nvmet_modules():
|
|
try:
|
|
cp = subprocess.run('/usr/sbin/lsmod', capture_output=True, text=True)
|
|
except TypeError:
|
|
# For older Python versions that don't support "capture_output" or "text"
|
|
cp = subprocess.run('/usr/sbin/lsmod', stdout=subprocess.PIPE, universal_newlines=True)
|
|
|
|
if cp.returncode != 0 or not cp.stdout:
|
|
return []
|
|
|
|
output = []
|
|
lines = cp.stdout.split('\n')
|
|
for line in lines:
|
|
if 'nvmet_' in line:
|
|
module = line.split()[0]
|
|
for end in ('loop', 'tcp', 'fc', 'rdma'):
|
|
if module.endswith(end):
|
|
output.append(module)
|
|
break
|
|
|
|
return output
|
|
|
|
|
|
def _runcmd(cmd: list, quiet=False):
|
|
if not quiet:
|
|
print(' '.join(cmd))
|
|
if args.dry_run:
|
|
return
|
|
subprocess.run(cmd)
|
|
|
|
|
|
def _modprobe(module: str, args: list = None, quiet=False):
|
|
cmd = ['/usr/sbin/modprobe', module]
|
|
if args:
|
|
cmd.extend(args)
|
|
_runcmd(cmd, quiet)
|
|
|
|
|
|
def _mkdir(dname: str):
|
|
print(f'mkdir -p "{dname}"')
|
|
if args.dry_run:
|
|
return
|
|
pathlib.Path(dname).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def _echo(value, fname: str):
|
|
print(f'echo -n "{value}" > "{fname}"')
|
|
if args.dry_run:
|
|
return
|
|
with open(fname, 'w') as f:
|
|
f.write(str(value))
|
|
|
|
|
|
def _symlink(port: str, subsysnqn: str):
|
|
print(
|
|
f'$( cd "/sys/kernel/config/nvmet/ports/{port}/subsystems" && ln -s "../../../subsystems/{subsysnqn}" "{subsysnqn}" )'
|
|
)
|
|
if args.dry_run:
|
|
return
|
|
target = os.path.join('/sys/kernel/config/nvmet/subsystems', subsysnqn)
|
|
link = pathlib.Path(os.path.join('/sys/kernel/config/nvmet/ports', port, 'subsystems', subsysnqn))
|
|
link.symlink_to(target)
|
|
|
|
|
|
def _create_subsystem(subsysnqn: str) -> 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
|
|
|
|
|
|
def _create_namespace(subsysnqn: str, id: str, node: str) -> str:
|
|
print(f'###{Fore.GREEN} Add namespace: {id}{Style.RESET_ALL}')
|
|
dname = os.path.join('/sys/kernel/config/nvmet/subsystems/', subsysnqn, 'namespaces', id)
|
|
_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):
|
|
if None in (id, trtype):
|
|
return False
|
|
|
|
if trtype != 'loop' and None in (traddr, trsvcid, adrfam):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def _create_port(port: str, traddr: str, trsvcid: str, trtype: str, adrfam: str):
|
|
'''@param port: This is a nvmet port and not a tcp port.'''
|
|
print(f'###{Fore.GREEN} Create port: {port} -> {traddr}:{trsvcid}{Style.RESET_ALL}')
|
|
dname = os.path.join('/sys/kernel/config/nvmet/ports', port)
|
|
_mkdir(dname)
|
|
_echo(trtype, os.path.join(dname, 'addr_trtype'))
|
|
if traddr:
|
|
_echo(traddr, os.path.join(dname, 'addr_traddr'))
|
|
if trsvcid:
|
|
_echo(trsvcid, os.path.join(dname, 'addr_trsvcid'))
|
|
if adrfam:
|
|
_echo(adrfam, os.path.join(dname, 'addr_adrfam'))
|
|
|
|
|
|
def _map_subsystems_to_ports(subsystems: list):
|
|
print(f'###{Fore.GREEN} Map subsystems to ports{Style.RESET_ALL}')
|
|
for subsystem in subsystems:
|
|
subsysnqn, port = subsystem.get('subsysnqn'), str(subsystem.get('port'))
|
|
if None not in (subsysnqn, port):
|
|
_symlink(port, subsysnqn)
|
|
|
|
|
|
def _read_config(fname: str) -> dict:
|
|
try:
|
|
with open(fname) as f:
|
|
return eval(f.read())
|
|
except Exception as e:
|
|
sys.exit(f'Error reading config file. {e}')
|
|
|
|
|
|
def _read_attr_from_file(fname: str) -> str:
|
|
try:
|
|
with open(fname, 'r') as f:
|
|
return f.read().strip('\n')
|
|
except Exception as e:
|
|
sys.exit(f'Error reading attribute. {e}')
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
def create(args):
|
|
# Need to be root to run this script
|
|
if not args.dry_run and os.geteuid() != 0:
|
|
sys.exit(f'Permission denied. You need root privileges to run {os.path.basename(__file__)}.')
|
|
|
|
config = _read_config(args.conf_file)
|
|
|
|
print('')
|
|
|
|
# Create a dummy null block device (if one doesn't already exist)
|
|
dev_node = '/dev/nullb0'
|
|
_modprobe('null_blk', ['nr_devices=1'])
|
|
|
|
ports = config.get('ports')
|
|
if ports is None:
|
|
sys.exit(f'Config file "{args.conf_file}" missing a "ports" section')
|
|
|
|
subsystems = config.get('subsystems')
|
|
if subsystems is None:
|
|
sys.exit(f'Config file "{args.conf_file}" missing a "subsystems" section')
|
|
|
|
# Extract the list of transport types found in the
|
|
# config file and load the corresponding kernel module.
|
|
_modprobe('nvmet')
|
|
trtypes = {port.get('trtype') for port in ports if port.get('trtype') is not None}
|
|
for trtype in trtypes:
|
|
if trtype in ('tcp', 'fc', 'rdma'):
|
|
_modprobe(f'nvmet_{trtype}')
|
|
elif trtype == 'loop':
|
|
_modprobe('nvmet_loop')
|
|
|
|
for port in ports:
|
|
print('')
|
|
id, traddr, trsvcid, trtype, adrfam = (
|
|
str(port.get('id')),
|
|
port.get('traddr'),
|
|
port.get('trsvcid'),
|
|
port.get('trtype'),
|
|
port.get('adrfam'),
|
|
)
|
|
if _args_valid(id, traddr, trsvcid, trtype, adrfam):
|
|
_create_port(id, traddr, trsvcid, trtype, adrfam)
|
|
else:
|
|
print(
|
|
f'{Fore.RED}### Config file "{args.conf_file}" error in "ports" section: id={id}, traddr={traddr}, trsvcid={trsvcid}, trtype={trtype}, adrfam={adrfam}{Style.RESET_ALL}'
|
|
)
|
|
|
|
for subsystem in subsystems:
|
|
print('')
|
|
subsysnqn, port, namespaces = (
|
|
subsystem.get('subsysnqn'),
|
|
str(subsystem.get('port')),
|
|
subsystem.get('namespaces'),
|
|
)
|
|
if None not in (subsysnqn, port, namespaces):
|
|
_create_subsystem(subsysnqn)
|
|
for id in namespaces:
|
|
_create_namespace(subsysnqn, str(id), dev_node)
|
|
else:
|
|
print(
|
|
f'{Fore.RED}### Config file "{args.conf_file}" error in "subsystems" section: subsysnqn={subsysnqn}, port={port}, namespaces={namespaces}{Style.RESET_ALL}'
|
|
)
|
|
|
|
print('')
|
|
_map_subsystems_to_ports(subsystems)
|
|
|
|
print('')
|
|
|
|
|
|
def clean(args):
|
|
# Need to be root to run this script
|
|
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('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('rmdir /sys/kernel/config/nvmet/ports/*')
|
|
for dname in pathlib.Path('/sys/kernel/config/nvmet/ports').glob('*'):
|
|
_runcmd(['rmdir', str(dname)], quiet=True)
|
|
|
|
print('rmdir /sys/kernel/config/nvmet/subsystems/*/namespaces/*')
|
|
for dname in pathlib.Path('/sys/kernel/config/nvmet/subsystems').glob('*/namespaces/*'):
|
|
_runcmd(['rmdir', str(dname)], quiet=True)
|
|
|
|
print('rmdir /sys/kernel/config/nvmet/subsystems/*')
|
|
for dname in pathlib.Path('/sys/kernel/config/nvmet/subsystems').glob('*'):
|
|
_runcmd(['rmdir', str(dname)], quiet=True)
|
|
|
|
for module in _get_loaded_nvmet_modules():
|
|
_modprobe(module, ['--remove'])
|
|
|
|
_modprobe('nvmet', ['--remove'])
|
|
_modprobe('null_blk', ['--remove'])
|
|
|
|
|
|
def link(args):
|
|
port = str(args.port)
|
|
subsysnqn = str(args.subnqn)
|
|
if not args.dry_run:
|
|
if os.geteuid() != 0:
|
|
# Need to be root to run this script
|
|
sys.exit(f'Permission denied. You need root privileges to run {os.path.basename(__file__)}.')
|
|
|
|
symlink = os.path.join('/sys/kernel/config/nvmet/ports', port, 'subsystems', subsysnqn)
|
|
if os.path.exists(symlink):
|
|
sys.exit(f'Symlink already exists: {symlink}')
|
|
|
|
_symlink(port, subsysnqn)
|
|
|
|
|
|
def unlink(args):
|
|
port = str(args.port)
|
|
subsysnqn = str(args.subnqn)
|
|
symlink = os.path.join('/sys/kernel/config/nvmet/ports', port, 'subsystems', subsysnqn)
|
|
if not args.dry_run:
|
|
if os.geteuid() != 0:
|
|
# Need to be root to run this script
|
|
sys.exit(f'Permission denied. You need root privileges to run {os.path.basename(__file__)}.')
|
|
|
|
if not os.path.exists(symlink):
|
|
sys.exit(f'No such symlink: {symlink}')
|
|
|
|
_runcmd(['rm', symlink])
|
|
|
|
|
|
def ls(args):
|
|
ports = list()
|
|
for port_path in pathlib.Path('/sys/kernel/config/nvmet/ports').glob('*'):
|
|
id = port_path.parts[-1]
|
|
port = {
|
|
'id': int(id),
|
|
'traddr': _read_attr_from_file(os.path.join('/sys/kernel/config/nvmet/ports', id, 'addr_traddr')),
|
|
'trsvcid': _read_attr_from_file(os.path.join('/sys/kernel/config/nvmet/ports', id, 'addr_trsvcid')),
|
|
'adrfam': _read_attr_from_file(os.path.join('/sys/kernel/config/nvmet/ports', id, 'addr_adrfam')),
|
|
'trtype': _read_attr_from_file(os.path.join('/sys/kernel/config/nvmet/ports', id, 'addr_trtype')),
|
|
}
|
|
|
|
ports.append(port)
|
|
|
|
subsystems = dict()
|
|
for subsystem_path in pathlib.Path('/sys/kernel/config/nvmet/subsystems').glob('*'):
|
|
subsysnqn = subsystem_path.parts[-1]
|
|
namespaces_path = pathlib.Path(os.path.join('/sys/kernel/config/nvmet/subsystems', subsysnqn, 'namespaces'))
|
|
subsystems[subsysnqn] = {
|
|
'port': None,
|
|
'subsysnqn': subsysnqn,
|
|
'namespaces': sorted([int(namespace_path.parts[-1]) for namespace_path in namespaces_path.glob('*')]),
|
|
}
|
|
|
|
# Find the port that each subsystem is mapped to
|
|
for subsystem_path in pathlib.Path('/sys/kernel/config/nvmet/ports').glob('*/subsystems/*'):
|
|
subsysnqn = subsystem_path.parts[-1]
|
|
if subsysnqn in subsystems:
|
|
subsystems[subsysnqn]['port'] = int(subsystem_path.parts[-3])
|
|
|
|
output = {
|
|
'ports': ports,
|
|
'subsystems': list(subsystems.values()),
|
|
}
|
|
|
|
if sys.version_info < (3, 8):
|
|
print(pprint.pformat(output, width=70))
|
|
else:
|
|
print(pprint.pformat(output, width=70, sort_dicts=False))
|
|
|
|
print('')
|
|
|
|
|
|
################################################################################
|
|
|
|
parser = ArgumentParser(description="Create NVMe-oF Storage Subsystems")
|
|
parser.add_argument('-v', '--version', action='store_true', help='Print version, then exit', default=False)
|
|
|
|
subparser = parser.add_subparsers(title='Commands', description='valid commands')
|
|
|
|
prsr = subparser.add_parser('create', help='Create nvme targets')
|
|
prsr.add_argument(
|
|
'-f',
|
|
'--conf-file',
|
|
action='store',
|
|
help='Configuration file (default: %(default)s)',
|
|
default=DEFAULT_CONFIG_FILE,
|
|
type=str,
|
|
metavar='FILE',
|
|
)
|
|
prsr.add_argument(
|
|
'-d', '--dry-run', action='store_true', help='Just print what would be done. (default: %(default)s)', default=False
|
|
)
|
|
prsr.set_defaults(func=create)
|
|
|
|
prsr = subparser.add_parser('clean', help='Remove all previously created nvme targets')
|
|
prsr.add_argument(
|
|
'-d', '--dry-run', action='store_true', help='Just print what would be done. (default: %(default)s)', default=False
|
|
)
|
|
prsr.set_defaults(func=clean)
|
|
|
|
prsr = subparser.add_parser('ls', help='List ports and subsystems')
|
|
prsr.set_defaults(func=ls)
|
|
|
|
prsr = subparser.add_parser('link', help='Map a subsystem to a port')
|
|
prsr.add_argument(
|
|
'-d', '--dry-run', action='store_true', help='Just print what would be done. (default: %(default)s)', default=False
|
|
)
|
|
prsr.add_argument('-p', '--port', action='store', type=int, help='nvmet port', required=True)
|
|
prsr.add_argument('-s', '--subnqn', action='store', type=str, help='nvmet subsystem NQN', required=True, metavar='NQN')
|
|
prsr.set_defaults(func=link)
|
|
|
|
prsr = subparser.add_parser('unlink', help='Unmap a subsystem from a port')
|
|
prsr.add_argument(
|
|
'-d', '--dry-run', action='store_true', help='Just print what would be done. (default: %(default)s)', default=False
|
|
)
|
|
prsr.add_argument('-p', '--port', action='store', type=int, help='nvmet port', required=True)
|
|
prsr.add_argument('-s', '--subnqn', action='store', type=str, help='nvmet subsystem NQN', required=True, metavar='NQN')
|
|
prsr.set_defaults(func=unlink)
|
|
|
|
|
|
# =============================
|
|
# Tab-completion.
|
|
# MUST BE CALLED BEFORE parser.parse_args() BELOW.
|
|
# Ref: https://kislyuk.github.io/argcomplete/
|
|
#
|
|
# If you do have argcomplete installed, you also need to run
|
|
# "sudo activate-global-python-argcomplete3" to globally activate
|
|
# auto-completion. Ref: https://pypi.python.org/pypi/argcomplete#global-completion
|
|
try:
|
|
import argcomplete
|
|
|
|
argcomplete.autocomplete(parser)
|
|
except ModuleNotFoundError:
|
|
# auto-complete is not necessary for the operation of this script. Just nice to have
|
|
pass
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.version:
|
|
print(f'{os.path.basename(__file__)} {VERSION}')
|
|
sys.exit(0)
|
|
|
|
# Invoke the sub-command
|
|
args.func(args)
|