#!/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 # 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)