#!/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 # ''' STorage Appliance Services Admin Tool ''' import os import sys import uuid import configparser from argparse import ArgumentParser from staslib import defs try: import hmac import hashlib except (ImportError, ModuleNotFoundError): hmac = None hashlib = None def read_from_file(fname, size): # pylint: disable=missing-function-docstring try: with open(fname) as f: # pylint: disable=unspecified-encoding data = f.read(size) if len(data) == size: return data except FileNotFoundError: pass return None def get_machine_app_specific(app_id): '''@brief Get a machine ID specific to an application. We use the value retrieved from /etc/machine-id. The documentation states that /etc/machine-id: "should be considered "confidential", and must not be exposed in untrusted environments, in particular on the network. If a stable unique identifier that is tied to the machine is needed for some application, the machine ID or any part of it must not be used directly. Instead the machine ID should be hashed with a crypto- graphic, keyed hash function, using a fixed, application-specific key. That way the ID will be properly unique, and derived in a constant way from the machine ID but there will be no way to retrieve the original machine ID from the application-specific one" @note systemd's C function sd_id128_get_machine_app_specific() was the inspiration for this code. @ref https://www.freedesktop.org/software/systemd/man/machine-id.html ''' if not hmac: return None data = read_from_file('/etc/machine-id', 32) if not data: return None hmac_obj = hmac.new(app_id, uuid.UUID(data).bytes, hashlib.sha256) id128_bytes = hmac_obj.digest()[0:16] return str(uuid.UUID(bytes=id128_bytes, version=4)) def get_uuid_from_system(): '''@brief Try to find system UUID in the following order: 1) /etc/machine-id 2) /sys/class/dmi/id/product_uuid 3) /proc/device-tree/ibm,partition-uuid ''' uuid_str = get_machine_app_specific(b'$nvmexpress.org$') if uuid_str: return uuid_str # The following files are only readable by root if os.geteuid() != 0: sys.exit('Permission denied. Root privileges required.') id128 = read_from_file('/sys/class/dmi/id/product_uuid', 36) if id128: # Swap little-endian to network order per # DMTF SMBIOS 3.0 Section 7.2.1 System UUID. swapped = ''.join([id128[x] for x in (6, 7, 4, 5, 2, 3, 0, 1, 8, 11, 12, 9, 10, 13, 16, 17, 14, 15)]) return swapped + id128[18:] return read_from_file('/proc/device-tree/ibm,partition-uuid', 36) def save(section, option, string, conf_file, fname): '''@brief Save configuration @param section: section in @conf_file where @option will be added @param option: option to be added under @section in @conf_file @param string: Text to be saved to @fname @param conf_file: Configuration file name @param fname: Optional file where @string will be saved ''' if fname and string is not None: with open(fname, 'w') as f: # pylint: disable=unspecified-encoding print(string, file=f) if conf_file: config = configparser.ConfigParser( default_section=None, allow_no_value=True, delimiters=('='), interpolation=None, strict=False ) if os.path.isfile(conf_file): config.read(conf_file) try: config.add_section(section) except configparser.DuplicateSectionError: pass if fname: string = 'file://' + fname if string is not None: config.set(section, option, string) else: config.remove_option(section, option) with open(conf_file, 'w') as f: # pylint: disable=unspecified-encoding config.write(f) def hostnqn(args): '''@brief Configure the host NQN''' uuid_str = get_uuid_from_system() or str(uuid.uuid4()) uuid_str = f'nqn.2014-08.org.nvmexpress:uuid:{uuid_str}' save('Host', 'nqn', uuid_str, args.conf_file, args.file) def hostid(args): '''@brief Configure the host ID''' save('Host', 'id', str(uuid.uuid4()), args.conf_file, args.file) def set_symname(args): '''@brief Define the host Symbolic Name''' save('Host', 'symname', args.symname, args.conf_file, args.file) def clr_symname(args): '''@brief Undefine the host NQN''' save('Host', 'symname', None, args.conf_file, None) def get_parser(): # pylint: disable=missing-function-docstring parser = ArgumentParser(description='Configuration utility for STAS.') parser.add_argument('-v', '--version', action='store_true', help='Print version, then exit', default=False) parser.add_argument( '-c', '--conf-file', action='store', help='Configuration file. Default %(default)s.', default=defs.SYS_CONF_FILE, type=str, metavar='FILE', ) subparser = parser.add_subparsers(title='Commands') prsr = subparser.add_parser('hostnqn', help='Configure the host NQN. The NQN is auto-generated.') prsr.add_argument( '-f', '--file', action='store', help='Optional file where to save the NQN.', type=str, metavar='FILE' ) prsr.set_defaults(cmd=hostnqn) prsr = subparser.add_parser('hostid', help='Configure the host ID. The ID is auto-generated.') prsr.add_argument( '-f', '--file', action='store', help='Optional file where to save the ID.', type=str, metavar='FILE' ) prsr.set_defaults(cmd=hostid) prsr = subparser.add_parser('set-symname', help='Set the host symbolic') prsr.add_argument( '-f', '--file', action='store', help='Optional file where to save the symbolic name.', type=str, metavar='FILE' ) prsr.add_argument('symname', action='store', help='Symbolic name', default=None, metavar='SYMNAME') prsr.set_defaults(cmd=set_symname) prsr = subparser.add_parser('clear-symname', help='Clear the host symbolic') prsr.set_defaults(cmd=clr_symname) return parser PARSER = get_parser() ARGS = PARSER.parse_args() if ARGS.version: print(f'nvme-stas {defs.VERSION}') sys.exit(0) try: ARGS.cmd(ARGS) except AttributeError as ex: print(str(ex)) PARSER.print_usage()