diff options
Diffstat (limited to '')
-rw-r--r-- | nvmet/nvme.py | 928 |
1 files changed, 928 insertions, 0 deletions
diff --git a/nvmet/nvme.py b/nvmet/nvme.py new file mode 100644 index 0000000..59efdb5 --- /dev/null +++ b/nvmet/nvme.py @@ -0,0 +1,928 @@ +''' +Implements access to the NVMe target configfs hierarchy + +Copyright (c) 2011-2013 by Datera, Inc. +Copyright (c) 2011-2014 by Red Hat, Inc. +Copyright (c) 2016 by HGST, a Western Digital Company. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +''' + +import os +import stat +import uuid +import json +from glob import iglob as glob +from six import iteritems, moves + +DEFAULT_SAVE_FILE = '/etc/nvmet/config.json' + + +class CFSError(Exception): + ''' + Generic slib error. + ''' + pass + + +class CFSNotFound(CFSError): + ''' + The underlying configfs object does not exist. Happens when + calling methods of an object that is instantiated but have + been deleted from configfs, or when trying to lookup an + object that does not exist. + ''' + pass + + +class CFSNode(object): + + configfs_dir = '/sys/kernel/config/nvmet' + + def __init__(self): + self._path = self.configfs_dir + self._enable = None + self.attr_groups = [] + + def __eq__(self, other): + return self._path == other._path + + def __ne__(self, other): + return self._path != other._path + + def _get_path(self): + return self._path + + def _create_in_cfs(self, mode): + ''' + Creates the configFS node if it does not already exist, depending on + the mode. + any -> makes sure it exists, also works if the node already does exist + lookup -> make sure it does NOT exist + create -> create the node which must not exist beforehand + ''' + if mode not in ['any', 'lookup', 'create']: + raise CFSError("Invalid mode: %s" % mode) + if self.exists and mode == 'create': + raise CFSError("This %s already exists in configFS" % + self.__class__.__name__) + elif not self.exists and mode == 'lookup': + raise CFSNotFound("No such %s in configfs: %s" % + (self.__class__.__name__, self.path)) + + if not self.exists: + try: + os.mkdir(self.path) + except: + raise CFSError("Could not create %s in configFS" % + self.__class__.__name__) + self.get_enable() + + def _exists(self): + return os.path.isdir(self.path) + + def _check_self(self): + if not self.exists: + raise CFSNotFound("This %s does not exist in configFS" % + self.__class__.__name__) + + def list_attrs(self, group, writable=None): + ''' + @param group: The attribute group + @param writable: If None (default), returns all attributes, if True, + returns read-write attributes, if False, returns just the read-only + attributes. + @type writable: bool or None + @return: A list of existing attribute names as strings. + ''' + self._check_self() + + names = [os.path.basename(name).split('_', 1)[1] + for name in glob("%s/%s_*" % (self._path, group)) + if os.path.isfile(name)] + + if writable is True: + names = [name for name in names + if self._attr_is_writable(group, name)] + elif writable is False: + names = [name for name in names + if not self._attr_is_writable(group, name)] + + names.sort() + return names + + def _attr_is_writable(self, group, name): + s = os.stat("%s/%s_%s" % (self._path, group, name)) + return s[stat.ST_MODE] & stat.S_IWUSR + + def set_attr(self, group, attribute, value): + ''' + Sets the value of a named attribute. + The attribute must exist in configFS. + @param group: The attribute group + @param attribute: The attribute's name. + @param value: The attribute's value. + @type value: string + ''' + self._check_self() + path = "%s/%s_%s" % (self.path, str(group), str(attribute)) + + if not os.path.isfile(path): + raise CFSError("Cannot find attribute: %s" % path) + + if self._enable: + raise CFSError("Cannot set attribute while %s is enabled" % + self.__class__.__name__) + + try: + with open(path, 'w') as file_fd: + file_fd.write(str(value)) + except Exception as e: + raise CFSError("Cannot set attribute %s: %s" % (path, e)) + + def get_attr(self, group, attribute): + ''' + Gets the value of a named attribute. + @param group: The attribute group + @param attribute: The attribute's name. + @return: The named attribute's value, as a string. + ''' + self._check_self() + path = "%s/%s_%s" % (self.path, str(group), str(attribute)) + if not os.path.isfile(path): + raise CFSError("Cannot find attribute: %s" % path) + + with open(path, 'r') as file_fd: + return file_fd.read().strip() + + def get_enable(self): + self._check_self() + path = "%s/enable" % self.path + if not os.path.isfile(path): + return None + + with open(path, 'r') as file_fd: + self._enable = int(file_fd.read().strip()) + return self._enable + + def set_enable(self, value): + self._check_self() + path = "%s/enable" % self.path + + if not os.path.isfile(path) or self._enable is None: + raise CFSError("Cannot enable %s" % self.path) + + try: + with open(path, 'w') as file_fd: + file_fd.write(str(value)) + except Exception as e: + raise CFSError("Cannot enable %s: %s (%s)" % + (self.path, e, value)) + self._enable = value + + def delete(self): + ''' + If the underlying configFS object does not exist, this method does + nothing. If the underlying configFS object exists, this method attempts + to delete it. + ''' + if self.exists: + os.rmdir(self.path) + + path = property(_get_path, + doc="Get the configFS object path.") + exists = property(_exists, + doc="Is True as long as the underlying configFS object exists. " + + "If the underlying configFS objects gets deleted " + + "either by calling the delete() method, or by any " + + "other means, it will be False.") + + def dump(self): + d = {} + for group in self.attr_groups: + a = {} + for i in self.list_attrs(group, writable=True): + a[str(i)] = self.get_attr(group, i) + d[str(group)] = a + if self._enable is not None: + d['enable'] = self._enable + return d + + def _setup_attrs(self, attr_dict, err_func): + for group in self.attr_groups: + for name, value in iteritems(attr_dict.get(group, {})): + try: + self.set_attr(group, name, value) + except CFSError as e: + err_func(str(e)) + enable = attr_dict.get('enable') + if enable is not None: + self.set_enable(enable) + + +class Root(CFSNode): + def __init__(self): + super(Root, self).__init__() + + if not os.path.isdir(self.configfs_dir): + self._modprobe('nvmet') + + if not os.path.isdir(self.configfs_dir): + raise CFSError("%s does not exist. Giving up." % + self.configfs_dir) + + self._path = self.configfs_dir + self._create_in_cfs('lookup') + + def _modprobe(self, modname): + try: + from kmodpy import kmod + + try: + kmod.Kmod().modprobe(modname, quiet=True) + except kmod.KmodError: + pass + except ImportError: + # Try the ctypes library included with the libkmod itself. + try: + import kmod + kmod.Kmod().modprobe(modname) + except Exception as e: + pass + + def _list_subsystems(self): + self._check_self() + + for d in os.listdir("%s/subsystems/" % self._path): + yield Subsystem(d, 'lookup') + + subsystems = property(_list_subsystems, + doc="Get the list of Subsystems.") + + def _list_ports(self): + self._check_self() + + for d in os.listdir("%s/ports/" % self._path): + yield Port(d, 'lookup') + + ports = property(_list_ports, + doc="Get the list of Ports.") + + def _list_hosts(self): + self._check_self() + + for h in os.listdir("%s/hosts/" % self._path): + yield Host(h, 'lookup') + + hosts = property(_list_hosts, + doc="Get the list of Hosts.") + + def save_to_file(self, savefile=None): + ''' + Write the configuration in json format to a file. + ''' + if savefile: + savefile = os.path.expanduser(savefile) + else: + savefile = DEFAULT_SAVE_FILE + + savefile_abspath = os.path.abspath(savefile) + savefile_dir = os.path.dirname(savefile_abspath) + if not os.path.exists(savefile_dir): + os.makedirs(savefile_dir) + + with open(savefile + ".temp", "w+") as f: + os.fchmod(f.fileno(), stat.S_IRUSR | stat.S_IWUSR) + f.write(json.dumps(self.dump(), sort_keys=True, indent=2)) + f.write("\n") + f.flush() + os.fsync(f.fileno()) + + os.rename(savefile + ".temp", savefile) + + # Sync the containing directory too + dir_fd = None + try: + dir_fd = os.open(savefile_dir, os.O_RDONLY) + os.fsync(dir_fd) + finally: + if dir_fd: + os.close(dir_fd) + + def clear_existing(self): + ''' + Remove entire current configuration. + ''' + + for p in self.ports: + p.delete() + for s in self.subsystems: + s.delete() + for h in self.hosts: + h.delete() + + def restore(self, config, clear_existing=False, abort_on_error=False): + ''' + Takes a dict generated by dump() and reconfigures the target to match. + Returns list of non-fatal errors that were encountered. + Will refuse to restore over an existing configuration unless + clear_existing is True. + ''' + if clear_existing: + self.clear_existing() + else: + if any(self.subsystems): + raise CFSError("subsystems present, not restoring") + + errors = [] + + if abort_on_error: + def err_func(err_str): + raise CFSError(err_str) + else: + def err_func(err_str): + errors.append(err_str + ", skipped") + + # Create the hosts first because the subsystems reference them + for index, t in enumerate(config.get('hosts', [])): + if 'nqn' not in t: + err_func("'nqn' not defined in host %d" % index) + continue + + Host.setup(t, err_func) + + for index, t in enumerate(config.get('subsystems', [])): + if 'nqn' not in t: + err_func("'nqn' not defined in subsystem %d" % index) + continue + + Subsystem.setup(t, err_func) + + for index, t in enumerate(config.get('ports', [])): + if 'portid' not in t: + err_func("'portid' not defined in port %d" % index) + continue + + Port.setup(self, t, err_func) + + return errors + + def restore_from_file(self, savefile=None, clear_existing=True, + abort_on_error=False): + ''' + Restore the configuration from a file in json format. + Returns a list of non-fatal errors. If abort_on_error is set, + it will raise the exception instead of continuing. + ''' + if savefile: + savefile = os.path.expanduser(savefile) + else: + savefile = DEFAULT_SAVE_FILE + + with open(savefile, "r") as f: + config = json.loads(f.read()) + return self.restore(config, clear_existing=clear_existing, + abort_on_error=abort_on_error) + + def dump(self): + d = super(Root, self).dump() + d['subsystems'] = [s.dump() for s in self.subsystems] + d['ports'] = [p.dump() for p in self.ports] + d['hosts'] = [h.dump() for h in self.hosts] + return d + + +class Subsystem(CFSNode): + ''' + This is an interface to a NVMe Subsystem in configFS. + A Subsystem is identified by its NQN. + ''' + + def __repr__(self): + return "<Subsystem %s>" % self.nqn + + def __init__(self, nqn=None, mode='any'): + ''' + @param nqn: The Subsystems' NQN. + If no NQN is specified, one will be generated. + @type nqn: string + @param mode:An optional string containing the object creation mode: + - I{'any'} means the configFS object will be either looked up + or created. + - I{'lookup'} means the object MUST already exist configFS. + - I{'create'} means the object must NOT already exist in configFS. + @type mode:string + @return: A Subsystem object. + ''' + super(Subsystem, self).__init__() + + if nqn is None: + if mode == 'lookup': + raise CFSError("Need NQN for lookup") + nqn = self._generate_nqn() + + self.nqn = nqn + self.attr_groups = ['attr'] + self._path = "%s/subsystems/%s" % (self.configfs_dir, nqn) + self._create_in_cfs(mode) + + def _generate_nqn(self): + prefix = "nqn.2014-08.org.nvmexpress:NVMf:uuid" + name = str(uuid.uuid4()) + return "%s:%s" % (prefix, name) + + def delete(self): + ''' + Recursively deletes a Subsystem object. + This will delete all attached Namespace objects and then the + Subsystem itself. + ''' + self._check_self() + for ns in self.namespaces: + ns.delete() + for h in self.allowed_hosts: + self.remove_allowed_host(h) + super(Subsystem, self).delete() + + def _list_namespaces(self): + self._check_self() + for d in os.listdir("%s/namespaces/" % self._path): + yield Namespace(self, int(d), 'lookup') + + namespaces = property(_list_namespaces, + doc="Get the list of Namespaces for the Subsystem.") + + def _list_allowed_hosts(self): + return [os.path.basename(name) + for name in os.listdir("%s/allowed_hosts/" % self._path)] + + allowed_hosts = property(_list_allowed_hosts, + doc="Get the list of Allowed Hosts for the Subsystem.") + + def add_allowed_host(self, nqn): + ''' + Enable access for the host identified by I{nqn} to the Subsystem + ''' + try: + os.symlink("%s/hosts/%s" % (self.configfs_dir, nqn), + "%s/allowed_hosts/%s" % (self._path, nqn)) + except Exception as e: + raise CFSError("Could not symlink %s in configFS: %s" % (nqn, e)) + + def remove_allowed_host(self, nqn): + ''' + Disable access for the host identified by I{nqn} to the Subsystem + ''' + try: + os.unlink("%s/allowed_hosts/%s" % (self._path, nqn)) + except Exception as e: + raise CFSError("Could not unlink %s in configFS: %s" % (nqn, e)) + + @classmethod + def setup(cls, t, err_func): + ''' + Set up Subsystem objects based upon t dict, from saved config. + Guard against missing or bad dict items, but keep going. + Call 'err_func' for each error. + ''' + + if 'nqn' not in t: + err_func("'nqn' not defined for Subsystem") + return + + try: + s = Subsystem(t['nqn']) + except CFSError as e: + err_func("Could not create Subsystem object: %s" % e) + return + + for ns in t.get('namespaces', []): + Namespace.setup(s, ns, err_func) + for h in t.get('allowed_hosts', []): + s.add_allowed_host(h) + + s._setup_attrs(t, err_func) + + def dump(self): + d = super(Subsystem, self).dump() + d['nqn'] = self.nqn + d['namespaces'] = [ns.dump() for ns in self.namespaces] + d['allowed_hosts'] = self.allowed_hosts + return d + + +class Namespace(CFSNode): + ''' + This is an interface to a NVMe Namespace in configFS. + A Namespace is identified by its parent Subsystem and Namespace ID. + ''' + + MAX_NSID = 8192 + + def __repr__(self): + return "<Namespace %d>" % self.nsid + + def __init__(self, subsystem, nsid=None, mode='any'): + ''' + @param subsystem: The parent Subsystem object + @param nsid: The Namespace identifier + If no nsid is specified, the next free one will be used. + @type nsid: int + @param mode:An optional string containing the object creation mode: + - I{'any'} means the configFS object will be either looked up + or created. + - I{'lookup'} means the object MUST already exist configFS. + - I{'create'} means the object must NOT already exist in configFS. + @type mode:string + @return: A Namespace object. + ''' + super(Namespace, self).__init__() + + if not isinstance(subsystem, Subsystem): + raise CFSError("Invalid parent class") + + if nsid is None: + if mode == 'lookup': + raise CFSError("Need NSID for lookup") + + nsids = [n.nsid for n in subsystem.namespaces] + for index in moves.xrange(1, self.MAX_NSID + 1): + if index not in nsids: + nsid = index + break + if nsid is None: + raise CFSError("All NSIDs 1-%d in use" % self.MAX_NSID) + else: + nsid = int(nsid) + if nsid < 1 or nsid > self.MAX_NSID: + raise CFSError("NSID must be 1 to %d" % self.MAX_NSID) + + self.attr_groups = ['device', 'ana'] + self._subsystem = subsystem + self._nsid = nsid + self._path = "%s/namespaces/%d" % (self.subsystem.path, self.nsid) + self._create_in_cfs(mode) + + def _get_subsystem(self): + return self._subsystem + + def _get_nsid(self): + return self._nsid + + def _get_grpid(self): + self._check_self() + _grpid = 0 + path = "%s/ana_grpid" % self.path + if os.path.isfile(path): + with open(path, 'r') as file_fd: + _grpid = int(file_fd.read().strip()) + return _grpid + + def set_grpid(self, grpid): + self._check_self() + path = "%s/ana_grpid" % self.path + if os.path.isfile(path): + with open(path, 'w') as file_fd: + file_fd.write(str(grpid)) + + grpid = property(_get_grpid, doc="Get the ANA Group ID.") + + subsystem = property(_get_subsystem, + doc="Get the parent Subsystem object.") + nsid = property(_get_nsid, doc="Get the NSID as an int.") + + @classmethod + def setup(cls, subsys, n, err_func): + ''' + Set up a Namespace object based upon n dict, from saved config. + Guard against missing or bad dict items, but keep going. + Call 'err_func' for each error. + ''' + + if 'nsid' not in n: + err_func("'nsid' not defined for Namespace") + return + + try: + ns = Namespace(subsys, n['nsid']) + except CFSError as e: + err_func("Could not create Namespace object: %s" % e) + return + + ns._setup_attrs(n, err_func) + if 'ana_grpid' in n: + ns.set_grpid(int(n['ana_grpid'])) + + def dump(self): + d = super(Namespace, self).dump() + d['nsid'] = self.nsid + d['ana_grpid'] = self.grpid + return d + + +class Port(CFSNode): + ''' + This is an interface to a NVMe Port in configFS. + ''' + + MAX_PORTID = 8192 + + def __repr__(self): + return "<Port %d>" % self.portid + + def __init__(self, portid, mode='any'): + super(Port, self).__init__() + + self.attr_groups = ['addr', 'param'] + self._portid = int(portid) + self._path = "%s/ports/%d" % (self.configfs_dir, self._portid) + self._create_in_cfs(mode) + + def _get_portid(self): + return self._portid + + portid = property(_get_portid, doc="Get the Port ID as an int.") + + def _list_subsystems(self): + return [os.path.basename(name) + for name in os.listdir("%s/subsystems/" % self._path)] + + subsystems = property(_list_subsystems, + doc="Get the list of Subsystem for this Port.") + + def add_subsystem(self, nqn): + ''' + Enable access to the Subsystem identified by I{nqn} through this Port. + ''' + try: + os.symlink("%s/subsystems/%s" % (self.configfs_dir, nqn), + "%s/subsystems/%s" % (self._path, nqn)) + except Exception as e: + raise CFSError("Could not symlink %s in configFS: %s" % (nqn, e)) + + def remove_subsystem(self, nqn): + ''' + Disable access to the Subsystem identified by I{nqn} through this Port. + ''' + try: + os.unlink("%s/subsystems/%s" % (self._path, nqn)) + except Exception as e: + raise CFSError("Could not unlink %s in configFS: %s" % (nqn, e)) + + def delete(self): + ''' + Recursively deletes a Port object. + ''' + self._check_self() + for s in self.subsystems: + self.remove_subsystem(s) + for a in self.ana_groups: + a.delete() + for r in self.referrals: + r.delete() + super(Port, self).delete() + + def _list_referrals(self): + self._check_self() + for d in os.listdir("%s/referrals/" % self._path): + yield Referral(self, d, 'lookup') + + referrals = property(_list_referrals, + doc="Get the list of Referrals for this Port.") + + def _list_ana_groups(self): + self._check_self() + if os.path.isdir("%s/ana_groups/" % self._path): + for d in os.listdir("%s/ana_groups/" % self._path): + yield ANAGroup(self, int(d), 'lookup') + + ana_groups = property(_list_ana_groups, + doc="Get the list of ANA Groups for this Port.") + + @classmethod + def setup(cls, root, n, err_func): + ''' + Set up a Port object based upon n dict, from saved config. + Guard against missing or bad dict items, but keep going. + Call 'err_func' for each error. + ''' + + if 'portid' not in n: + err_func("'portid' not defined for Port") + return + + try: + port = Port(n['portid']) + except CFSError as e: + err_func("Could not create Port object: %s" % e) + return + + port._setup_attrs(n, err_func) + for s in n.get('subsystems', []): + port.add_subsystem(s) + for a in n.get('ana_groups', []): + ANAGroup.setup(port, a, err_func) + for r in n.get('referrals', []): + Referral.setup(port, r, err_func) + + def dump(self): + d = super(Port, self).dump() + d['portid'] = self.portid + d['subsystems'] = self.subsystems + d['ana_groups'] = [a.dump() for a in self.ana_groups] + d['referrals'] = [r.dump() for r in self.referrals] + return d + + +class Referral(CFSNode): + ''' + This is an interface to a NVMe Referral in configFS. + ''' + + def __repr__(self): + return "<Referral %d>" % self.name + + def __init__(self, port, name, mode='any'): + super(Referral, self).__init__() + + if not isinstance(port, Port): + raise CFSError("Invalid parent class") + + self.attr_groups = ['addr'] + self.port = port + self._name = name + self._path = "%s/referrals/%s" % (self.port.path, self._name) + self._create_in_cfs(mode) + + def _get_name(self): + return self._name + + name = property(_get_name, doc="Get the Referral name.") + + @classmethod + def setup(cls, port, n, err_func): + ''' + Set up a Referral based upon n dict, from saved config. + Guard against missing or bad dict items, but keep going. + Call 'err_func' for each error. + ''' + + if 'name' not in n: + err_func("'name' not defined for Referral") + return + + try: + r = Referral(port, n['name']) + except CFSError as e: + err_func("Could not create Referral object: %s" % e) + return + + r._setup_attrs(n, err_func) + + def dump(self): + d = super(Referral, self).dump() + d['name'] = self.name + return d + + +class ANAGroup(CFSNode): + ''' + This is an interface to a NVMe ANA Group in configFS. + ''' + + MAX_GRPID = 1024 + + def __repr__(self): + return "<ANA Group %d>" % self.grpid + + def __init__(self, port, grpid, mode='any'): + super(ANAGroup, self).__init__() + + if not os.path.isdir("%s/ana_groups" % port.path): + raise CFSError("ANA not supported") + + if grpid is None: + if mode == 'lookup': + raise CFSError("Need grpid for lookup") + + grpids = [n.grpid for n in port.ana_groups] + for index in moves.xrange(2, self.MAX_GRPID + 1): + if index not in grpids: + grpid = index + break + if grpid is None: + raise CFSError("All ANA Group IDs 1-%d in use" % self.MAX_GRPID) + else: + grpid = int(grpid) + if grpid < 1 or grpid > self.MAX_GRPID: + raise CFSError("GRPID %d must be 1 to %d" % (grpid, self.MAX_GRPID)) + + self.attr_groups = ['ana'] + self._port = port + self._grpid = grpid + self._path = "%s/ana_groups/%d" % (self._port.path, self.grpid) + self._create_in_cfs(mode) + + def _get_grpid(self): + return self._grpid + + grpid = property(_get_grpid, doc="Get the ANA Group ID.") + + @classmethod + def setup(cls, port, n, err_func): + ''' + Set up an ANA Group object based upon n dict, from saved config. + Guard against missing or bad dict items, but keep going. + Call 'err_func' for each error. + ''' + + if 'grpid' not in n: + err_func("'grpid' not defined for ANA Group") + return + + try: + a = ANAGroup(port, n['grpid']) + except CFSError as e: + err_func("Could not create ANA Group object: %s" % e) + return + + a._setup_attrs(n, err_func) + + def delete(self): + # ANA Group 1 is automatically created/deleted + if self.grpid != 1: + super(ANAGroup, self).delete() + + def dump(self): + d = super(ANAGroup, self).dump() + d['grpid'] = self.grpid + return d + + +class Host(CFSNode): + ''' + This is an interface to a NVMe Host in configFS. + A Host is identified by its NQN. + ''' + + def __repr__(self): + return "<Host %s>" % self.nqn + + def __init__(self, nqn, mode='any'): + ''' + @param nqn: The Hosts's NQN. + @type nqn: string + @param mode:An optional string containing the object creation mode: + - I{'any'} means the configFS object will be either looked up + or created. + - I{'lookup'} means the object MUST already exist configFS. + - I{'create'} means the object must NOT already exist in configFS. + @type mode:string + @return: A Host object. + ''' + super(Host, self).__init__() + + self.nqn = nqn + self._path = "%s/hosts/%s" % (self.configfs_dir, nqn) + self._create_in_cfs(mode) + + @classmethod + def setup(cls, t, err_func): + ''' + Set up Host objects based upon t dict, from saved config. + Guard against missing or bad dict items, but keep going. + Call 'err_func' for each error. + ''' + + if 'nqn' not in t: + err_func("'nqn' not defined for Host") + return + + try: + h = Host(t['nqn']) + except CFSError as e: + err_func("Could not create Host object: %s" % e) + return + + def dump(self): + d = super(Host, self).dump() + d['nqn'] = self.nqn + return d + + +def _test(): + from doctest import testmod + testmod() + +if __name__ == "__main__": + _test() |