[PATCH 2/3] nvmetproxy: add a JSON-RPC proxy daemon
Hannes Reinecke
hare at suse.de
Fri Feb 12 10:52:28 EST 2021
Add a JSON-RPC proxy daemon to allow for remote configuration
of the NVMe-over-Fabrics target.
Signed-off-by: Hannes Reinecke <hare at suse.de>
---
Documentation/Makefile | 24 +-
Documentation/nvmetproxy.txt | 111 ++++++++
nvmet/__init__.py | 4 +-
nvmet/bdev.py | 72 +++++
nvmet/rpc.py | 495 +++++++++++++++++++++++++++++++++++
nvmetproxy | 197 ++++++++++++++
setup.py | 2 +-
7 files changed, 891 insertions(+), 14 deletions(-)
create mode 100644 Documentation/nvmetproxy.txt
create mode 100644 nvmet/bdev.py
create mode 100644 nvmet/rpc.py
create mode 100755 nvmetproxy
diff --git a/Documentation/Makefile b/Documentation/Makefile
index 8e0281c..2778429 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -1,31 +1,31 @@
PKGNAME = nvmetcli
-MANPAGE = ${PKGNAME}.8
-HTMLFILE = ${PKGNAME}.html
-XMLFILE = ${PKGNAME}.xml
INSTALL ?= install
PREFIX := /usr
ASCIIDOC = asciidoc
XMLTO = xmlto --skip-validation
-DOCDATA = ${XMLFILE} ${HTMLFILE}
-
-${MANPAGE}: ${DOCDATA}
+DOCFILES = ${PKGNAME}.txt nvmetproxy.txt
+MANPAGES = ${DOCFILES:.txt=.8}
+HTMLFILES = ${DOCFILES:.txt=.html}
+XMLFILES = ${DOCFILES:.txt=.xml}
+
+%.8: %.xml
${XMLTO} man $<
-
+
%.xml: %.txt
${ASCIIDOC} -b docbook -d manpage -o $@ $<
-
+
%.html: %.txt
${ASCIIDOC} -a toc -o $@ $<
-
+
installdoc: man8
-man8:
- ${INSTALL} -m 644 ${MANPAGE} ${PREFIX}/share/man/man8
+man8: ${MANPAGES}
+ ${INSTALL} -m 644 $< ${PREFIX}/share/man/man8
uninstalldoc:
-rm -f ${PREFIX}/share/man/man8/${MANPAGE}
clean:
- -rm -f ${MANPAGE} ${HTMLFILE} ${XMLFILE}
+ -rm -f ${MANPAGES} ${HTMLFILES} ${XMLFILES}
diff --git a/Documentation/nvmetproxy.txt b/Documentation/nvmetproxy.txt
new file mode 100644
index 0000000..b273f5b
--- /dev/null
+++ b/Documentation/nvmetproxy.txt
@@ -0,0 +1,111 @@
+nvmetproxy(8)
+===========
+
+NAME
+----
+nvmetproxy - JSON-RPC proxy server to configure NVMe-over-Fabrics Target.
+
+USAGE
+------
+[verse]
+'nvmetproxy'
+ [--socket=<socket> | -s <socket>]
+ [--host=<hostname> | -H <hostname>]
+ [--port=<port> | -p <port>]
+ [--user=<username> | -U <username> ]
+ [--password=<password> | -P <password> ]
+ [--cert=<certfile> | -c <certfile> ]
+ [--url=<url> | -u <url> ]
+
+DESCRIPTION
+-----------
+*nvmetproxy* is JSON-RPC proxy for viewing, editing, saving,
+and starting a Linux kernel NVMe Target, used for an NVMe-over-Fabrics
+network configuration. It allows an administrator to export
+a storage resource (such as block devices, files, and volumes)
+to a local NVMe block device or expose them to remote systems
+based on the NVMe-over-Fabrics specification from http://www.nvmexpress.org.
+
+The JSON-RPC commands are compatible with the JSON-RPC commands
+the from SPDK.
+
+Connection to the daemon can be via a socket (as specified --socket)
+or via HTTP if the --host option is present.
+
+OPTIONS
+-------
+-s <socket>::
+--socket=<socket>::
+ Socket to listen on, default is /var/tmp/nvmet.sock
+
+-H <hostname>::
+--host=<hostname>::
+ Hostname to listen for HTTP requests. If specified the HTTP
+ server will started and the --socket option is ignored.
+
+-p <port>::
+--port=<port>::
+ Port number to listen for HTTP requests. Default is 4260
+
+-U <username>::
+--user=<username>::
+ Username to use for HTTP Basic Authentication. If not present
+ the HTTP server will not require authentication.
+
+-P <password>::
+--password=<password>::
+ Password to use for HTTP Basic Authentication. Must be specified
+ together with --user
+
+-c <cert>::
+--cert=<cert>::
+ SSL Server certificate to use for HTTP server. If specified the
+ HTTP server will only accept encrypted connections (https).
+
+-u <url>::
+--url=<url>::
+ URL directory to use for the configuration. Defaults to '/nvmet'
+
+
+JSON-RPC commands
+-----------------
+The proxy is compatible with the command set from the JSON-RPC calls from SPDK.
+Currently the following methods are implemented:
+
+ bdev_file_list_pools
+ bdev_file_create
+ bdev_file_delete
+ bdev_file_snapshot
+ bdev_file_clone
+ nvmf_get_interfaces
+ nvmf_create_transport
+ nvmf_get_transports
+ nvmf_create_subsystem
+ nvmf_delete_subsystem
+ nvmf_subsystem_add_ns
+ nvmf_subsystem_remove_ns
+ nvmf_subsystem_add_port
+ nvmf_subsystem_remove_port
+ nvmf_subsystem_add_host
+ nvmf_subsystem_remove_host
+ nvmf_get_subsystems
+ nvmf_port_add_ana
+ nvmf_port_set_ana
+ nvmf_port_remove_ana
+ nvmf_get_config
+ nvmf_set_config
+
+AUTHORS
+-------
+nvmetproxy was written by mailto:hare at suse.de[Hannes Reinecke]
+
+REPORTING BUGS & DEVELOPMENT
+-----------------------------
+Please send patches and bug reports to linux-nvme at lists.infradead.org
+for review and acceptance.
+
+LICENSE
+-------
+nvmetproxy is licensed under the *Apache License, Version 2.0*. Software
+distributed under this license is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied.
diff --git a/nvmet/__init__.py b/nvmet/__init__.py
index cf172bd..730393b 100644
--- a/nvmet/__init__.py
+++ b/nvmet/__init__.py
@@ -1,2 +1,4 @@
-from .nvme import Root, Subsystem, Namespace, Port, Host, Referral, ANAGroup,\
+from .bdev import BackingFile
+from .rpc import JsonRPC
+from .nvme import CFSError, Root, Subsystem, Namespace, Port, Host, Referral, ANAGroup,\
DEFAULT_SAVE_FILE
diff --git a/nvmet/bdev.py b/nvmet/bdev.py
new file mode 100644
index 0000000..41735f5
--- /dev/null
+++ b/nvmet/bdev.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python
+
+'''
+BackingFile JSON-RPC handling functions
+
+Copyright (c) 2021 by Hannes Reinecke, SUSE Linux LLC
+
+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.
+'''
+
+from __future__ import print_function
+
+import os
+
+class BackingFile:
+ def __init__(self, pools, name, pool = None, mode = 'lookup'):
+ self.name = name
+ if mode != 'lookup' and not pool:
+ for pool in pools:
+ break
+ if pool and pool not in pools:
+ raise NameError("Invalid pool '%s'" % pool)
+ if mode == 'lookup':
+ if not pool:
+ for pool in pools:
+ self.prefix = pools[pool]
+ path = self.prefix + '/' + name
+ if os.path.exists(path):
+ self.file_path = path
+ break
+ if not self.file_path:
+ raise NameError("backing file '%s' not found" % name)
+ else:
+ self.prefix = pools[pool]
+ path = self.prefix + '/' + name
+ if not os.path.exists(path):
+ raise NameError("%s: backing file '%s' not found" % (pool, name))
+ self.file_path = path
+ elif mode == 'create':
+ self.prefix = pools[pool]
+ path = self.prefix + '/' + name
+ if os.path.exists(path):
+ raise NameError("%s: backing file '%s' already exists" % (pool, path))
+ try:
+ fd = open(path, 'x')
+ except:
+ raise NameError("%s: Cannot create backing file '%s'" % (pool, name))
+ fd.close()
+ self.file_path = path
+
+ def delete(self):
+ try:
+ os.remove(self.file_path)
+ except FileNotFoundError:
+ return RuntimeError("%s: cannot delete backing file '%s'" % (pool, name))
+
+ def set_size(self, file_size):
+ self.file_size = file_size
+ try:
+ os.truncate(self.file_path, self.file_size)
+ except:
+ raise RuntimeError("Failed to truncate %s" % self.file_path)
diff --git a/nvmet/rpc.py b/nvmet/rpc.py
new file mode 100644
index 0000000..4ec7d75
--- /dev/null
+++ b/nvmet/rpc.py
@@ -0,0 +1,495 @@
+'''
+Implements JSON-RPC handlers for the NVMe target configfs hierarchy
+
+Copyright (c) 2021 Hannes Reinecke, SUSE LLC
+
+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 json
+import nvmet as nvme
+import netifaces
+from pathlib import Path
+
+class JsonRPC:
+ def _get_subsystems(self, params = None):
+ return [s.dump() for s in self.cfg.subsystems]
+
+ def _get_transports(self, params = None):
+ km = kmod.Kmod()
+ ml = []
+ for m in km.loaded():
+ if m.name.startswith("nvmet_"):
+ ml.append(m.name[len("nvmet_"):])
+ elif m.name == "nvme_loop":
+ ml.append(m.name[len("nvme_"):])
+ return ml
+
+ def _create_transport(self, params):
+ if not params or 'trtype' not in params:
+ raise NameError("Parameter 'trtype' missing")
+ trtype = params['trtype']
+ if trtype.lower() == 'loop':
+ prefix = "nvme_"
+ else:
+ prefix = "nvmet_"
+ try:
+ self.cfg._modprobe(prefix + trtype.lower())
+ except:
+ raise NameError("Module %s%s not found" % (prefix, trtype.lower()))
+
+ def _create_subsystem(self, params):
+ if not params or 'nqn' not in params:
+ nqn = None
+ else:
+ nqn = params['nqn']
+ try:
+ subsys = nvme.Subsystem(nqn, mode='create')
+ except:
+ raise RuntimeError("Failed to create subsystem %s" % nqn)
+ if not params:
+ return subsys.nqn
+ if 'model_number' in params:
+ model = params['model_number']
+ if (len(model) > 40):
+ subsys.delete()
+ raise NameError("Model number longer than 40 characters")
+ try:
+ subsys.set_attr("attr", "model", model)
+ except:
+ subsys.delete()
+ raise RuntimeError("Failed to set model %s" % model)
+ if 'serial_number' in params:
+ serial = params['serial_number']
+ if len(serial) > 20:
+ subsys.delete()
+ raise NameError("Serial number longer than 20 characters")
+ try:
+ subsys.set_attr("attr", "serial", serial)
+ except:
+ subsys.delete()
+ raise RuntimeError("Failed to set serial %s" % serial)
+ if 'allow_any_host' in params:
+ subsys.set_attr("attr", "allow_any_host", "1")
+ return subsys.nqn
+
+ def _delete_subsystem(self, params):
+ if not params and 'nqn' not in params:
+ raise NameError("Parameter 'nqn' missing")
+ nqn = params['nqn']
+ try:
+ subsys = nvme.Subsystem(nqn, mode='lookup');
+ except nvme.CFSError:
+ raise NameError("Subsystem %s not found" % nqn)
+ try:
+ subsys.delete()
+ except:
+ raise RuntimeError("Failed to delete subsystem %s" % nqn)
+
+ def _add_ns(self, params):
+ if not params or 'nqn' not in params:
+ raise NameError("Parameter 'nqn' missing")
+ nqn = params['nqn']
+ if 'namespace' not in params:
+ raise NameError("Parameter 'namespace' missing")
+ ns_params = params['namespace']
+ if 'bdev_name' not in ns_params:
+ raise NameError("Parameter 'namespace:bdev_name' missing")
+ bdev_name = ns_params['bdev_name']
+ try:
+ bdev = nvme.BackingFile(self.pools, bdev_name)
+ except:
+ raise NameError("bdev %s not found" % bdev_name)
+ try:
+ subsys = nvme.Subsystem(nqn, mode='lookup')
+ except nvme.CFSError:
+ raise NameError("Subsystem %s not found" % nqn)
+
+ if 'nsid' in ns_params:
+ nsid = ns_params['nsid']
+ else:
+ nsid = None
+
+ try:
+ ns = nvme.Namespace(subsys, nsid, mode='create')
+ except:
+ raise NameError("Namespace %s already exists" % nsid)
+ nsid = ns.nsid
+ if 'uuid' in ns_params:
+ try:
+ ns.set_attr("device", "uuid", ns_params['uuid'])
+ except:
+ ns.delete()
+ raise RuntimeError("Failed to set uuid %s on ns %s" % (ns_params['uuid'], nsid))
+ if 'nguid' in ns_params:
+ try:
+ ns.set_attr("device", "nguid", ns_params['nguid'])
+ except:
+ ns.delete()
+ raise RuntimeError("Failed to set nguid %s on ns %s" % (ns_params['nguid'], nsid))
+ try:
+ ns.set_attr("device", "path", bdev.file_path)
+ except:
+ ns.delete()
+ raise RuntimeError("Failed to set path on ns %s" % nsid)
+ try:
+ ns.set_enable(1)
+ except:
+ raise RuntimeError("Failed to enable ns %s" % nsid)
+ return nsid
+
+ def _remove_ns(self, params):
+ if not params and 'nqn' not in params:
+ raise NameError("Parameter 'nqn' missing")
+ nqn = params['nqn']
+ if 'nsid' not in params:
+ raise NameError("Parameter 'nsid' missing")
+ nsid = params['nsid']
+ try:
+ subsys = nvme.Subsystem(nqn, mode='lookup')
+ except nvme.CFSError:
+ raise NameError("Subsystem %s not found" % nqn)
+ try:
+ ns = nvme.Namespace(subsys, nsid, mode='lookup')
+ except nvme.CFSError:
+ raise NameError("Namespace %d not found" % nsid)
+ ns.delete()
+
+ def _add_port(self, params):
+ if not params or 'nqn' not in params:
+ raise NameError("Parameter 'nqn' missing")
+ if 'listen_address' in params:
+ port_params = params['listen_address']
+ elif 'port' in params:
+ port_params = params['port']
+ else:
+ raise NameError("Parameter 'listen_address' missing")
+ if 'portid' not in port_params:
+ ids = [port.portid for port in self.cfg.ports]
+ if len(ids):
+ portid = max(ids)
+ else:
+ portid = 0
+ portid = portid + 1
+ else:
+ portid = int(port_params['portid'])
+ try:
+ port = nvme.Port(portid, mode='create')
+ except:
+ raise RuntimeError("Port %d already exists" % portid)
+ for p in ('trtype', 'adrfam', 'traddr', 'trsvcid'):
+ if p not in port_params:
+ if p != 'trsvcid':
+ port.delete()
+ raise NameError("Invalid listen_address parameter %s" % p)
+ else:
+ v = 4420
+ else:
+ v = port_params[p]
+ if p == 'adrfam':
+ v = port_params[p].lower()
+ try:
+ port.set_attr("addr", p, v)
+ except:
+ port.delete()
+ raise RuntimeError("Failed to set %s to %s" % (p, v))
+ nqn = params['nqn']
+ try:
+ port.add_subsystem(nqn)
+ except:
+ port.delete()
+ raise NameError("subsystem %s not found" % nqn)
+ return port.portid
+
+ def _remove_port(self, params):
+ if not params or 'nqn' not in params:
+ raise NameError("Parameter 'nqn' missing")
+ if 'listen_address' in params:
+ port_params = params['listen_address']
+ elif 'port' in params:
+ port_params = params['port']
+ else:
+ raise NameError("Parameter 'listen_address' missing")
+ nqn = params['nqn']
+ for port in self.cfg.ports:
+ for p in ('trtype', 'adrfam', 'traddr', 'trsvcid'):
+ if p not in port_params:
+ continue
+ if port.get_attr("addr", p) != port_params[p]:
+ continue
+ for s in port.subsystems:
+ if s != nqn:
+ continue
+ port.remove_subsystem(nqn)
+ if not len(port.subsystems):
+ port.delete()
+
+ def _add_host(self, params):
+ if not params or 'nqn' not in params:
+ raise NameError("Parameter 'nqn' missing")
+ if 'host' not in params:
+ raise NameError("Parameter 'host' missing")
+ nqn = params['nqn']
+ try:
+ subsys = nvme.Subsystem(nqn, mode='lookup')
+ except nvme.CFSError:
+ raise NameError("Subsystem %s not found" % nqn)
+ host = nvme.Host(params['host'])
+ try:
+ subsys.add_allowed_host(host.nqn)
+ except nvme.CFSError:
+ raise RuntimeError("Could not add host %s to subsys %s" % (host, nqn))
+
+ def _remove_host(self, params):
+ if not params or 'nqn' not in params:
+ raise NameError("Parameter 'nqn' missing")
+ if 'host' not in params:
+ raise NameError("Parameter 'host' missing")
+ nqn = params['nqn']
+ try:
+ subsys = nvme.Subsystem(nqn, mode='lookup')
+ except nvme.CFSError:
+ raise NameError("Subsystem %s not found" % nqn)
+ try:
+ host = nvme.Host(params['host'], mode='lookup')
+ except nvme.CFSError:
+ raise NameError("Host %s not found" % params['host'])
+ try:
+ subsys.remove_allowed_host(host.nqn)
+ except nvme.CFSError:
+ raise RuntimeError("Failed to remove host %s from subsys %s" %
+ (nqn, host.nqn))
+ found = 0
+ for subsys in self.cfg.subsystems:
+ for h in subsys.allowed_hosts:
+ if h.nqn == host.nqn:
+ found = found + 1
+ if found == 0:
+ host.delete()
+
+ def _add_ana(self, params):
+ if not params or 'portid' not in params:
+ raise NameError("Parameter 'portid' missing")
+ portid = params['portid']
+ if 'grpid' in params:
+ grpid = params['grpid']
+ else:
+ grpid = None
+ try:
+ port = nvme.Port(portid, mode='lookup')
+ except:
+ raise RuntimeError("Port %s not present" % portid)
+ try:
+ a = nvme.ANAGroup(port, grpid)
+ except nvme.CFSError:
+ raise RuntimeError("Port %s ANA Group %s already present" %
+ (portid, grpid))
+ if 'ana_state' in params:
+ try:
+ a.set_attr("ana", "state", params['ana_state'])
+ except nvme.CFSError:
+ raise RuntimeError("Failed to set ANA state on group %s to %s"
+ % (grpid, params['ana_state']))
+ return a.get_attr("ana", "state")
+
+ def _set_ana(self, params):
+ if not params or 'portid' not in params:
+ raise NameError("Parameter 'portid' missing")
+ portid = params['portid']
+ if 'grpid' not in params:
+ raise NameError("Parameter 'grpid' missing")
+ grpid = params['grpid']
+ if 'ana_state' not in params:
+ raise NameError("Parameter 'ana_state' missing")
+ try:
+ port = nvme.Port(portid, mode='lookup')
+ except:
+ raise RuntimeError("Port %s not found" % portid)
+ for ana in port.ana_groups:
+ if ana.grpid == int(grpid):
+ try:
+ ana.set_attr("ana", "state", params['ana_state'])
+ return
+ except nvme.CFSError:
+ raise RuntimeError("Failed to set ANA state to %s" % params['ana_state'])
+ raise RuntimeError("ANA group %s not found" % grpid)
+
+ def _remove_ana(self, params):
+ if not params or 'portid' not in params:
+ raise NameError("Parameter 'portid' missing")
+ if 'grpid' not in params:
+ raise NameError("Parameter 'grpid' missing")
+ grpid = params['grpid']
+ try:
+ port = nvme.Port(params['portid'], mode='lookup')
+ except:
+ raise RuntimeError("Port %s not found" % params['portid'])
+ grpids = [n.grpid for n in port.ana_groups]
+ if int(grpid) not in grpids:
+ raise RuntimeError("ANA group %s not found" % grpid)
+ for ana in port.ana_groups:
+ if ana.grpid == grpid:
+ ana.delete()
+
+ def _get_config(self, params):
+ return self.cfg.dump()
+
+ def _set_config(self, params):
+ if not params:
+ raise RuntimeError("Invalid configuration")
+ try:
+ self.cfg.restore(params, merge=True)
+ except nvme.CFSError:
+ raise RuntimeError("Failed to apply configuration")
+
+ def _get_interfaces(self, params):
+ ifnames = {}
+ for i in netifaces.interfaces():
+ if i == 'lo':
+ continue;
+ iflist = {}
+ ifaddrs = netifaces.ifaddresses(i)
+ try:
+ a = ifaddrs[netifaces.AF_INET]
+ except:
+ pass
+ else:
+ addrlist = []
+ for n in a:
+ addrlist.append(n['addr'])
+ iflist['ipv4'] = addrlist
+ try:
+ a = ifaddrs[netifaces.AF_INET6]
+ except:
+ pass
+ else:
+ addrlist = []
+ for n in a:
+ addrlist.append(n['addr'])
+ iflist['ipv6'] = addrlist
+ ifnames[i] = iflist
+ return ifnames
+
+ def _create_file(self, params):
+ if not params or 'file_name' not in params:
+ raise NameError("parameter 'file_name' missing")
+ file_name = params['file_name']
+ if 'size' not in params:
+ raise NameError("parameter 'size' missing")
+ file_size = params['size']
+ if 'pool' in params:
+ file_pool = params['pool']
+ else:
+ file_pool = None
+ bfile = nvme.BackingFile(self.pools, file_name, file_pool,
+ mode='create')
+ bfile.set_size(int(file_size))
+ return bfile.name
+
+ def _delete_file(self, params):
+ if not params or 'file_name' not in params:
+ raise NameError("parameter 'file_name' missing")
+ file_name = params['file_name']
+ bfile = nvme.BackingFile(self.pools, file_name)
+ bfile.delete()
+
+ def _snapshot_file(self, params):
+ if not params or 'file_name' not in params:
+ raise NameError("parameter 'file_name' missing")
+ file_name = params['file_name']
+ snap = params['snapshot_name']
+ bfile = nvme.BackingFile(self.pools, file_name)
+ return bfile.snapshot(snap)
+
+ def _clone_file(self, params):
+ if not params or 'snapshot_name' not in params:
+ raise NameError("parameter 'snapshot_name' missing")
+ snap = params['snapshot_name']
+ clone = params['clone_name']
+ bfile = nvme.BackingFile(self.pools, snap)
+ return bfile.snapshot(clone)
+
+ def _get_pools(self, params):
+ r = []
+ for p in self.pools:
+ path = Path(self.pools[p])
+ filelist = []
+ for f in path.iterdir():
+ if f.is_file():
+ filelist.append(f.name)
+ st = os.statvfs(self.pools[p])
+ a = dict(name=p, cluster_size=st.f_frsize,
+ free_clusters=st.f_bavail,
+ total_data_clusters=st.f_blocks,
+ files=filelist)
+ r.append(a)
+ return r
+
+ _rpc_methods = dict(bdev_file_list_pools=_get_pools,
+ bdev_file_create=_create_file,
+ bdev_file_delete=_delete_file,
+ bdev_file_snapshot=_snapshot_file,
+ bdev_file_clone=_clone_file,
+ nvmf_get_interfaces=_get_interfaces,
+ nvmf_create_transport=_create_transport,
+ nvmf_get_transports=_get_transports,
+ nvmf_create_subsystem=_create_subsystem,
+ nvmf_delete_subsystem=_delete_subsystem,
+ nvmf_subsystem_add_ns=_add_ns,
+ nvmf_subsystem_remove_ns=_remove_ns,
+ nvmf_subsystem_add_port=_add_port,
+ nvmf_subsystem_remove_port=_remove_port,
+ nvmf_subsystem_add_host=_add_host,
+ nvmf_subsystem_remove_host=_remove_host,
+ nvmf_get_subsystems=_get_subsystems,
+ nvmf_port_add_ana=_add_ana,
+ nvmf_port_set_ana=_set_ana,
+ nvmf_port_remove_ana=_remove_ana,
+ nvmf_get_config=_get_config,
+ nvmf_set_config=_set_config)
+
+ def __init__(self, pools = None):
+ self.cfg = nvme.Root()
+ self.pools = pools
+
+ def rpc_call(self, req):
+ print ("%s" % req)
+ if 'method' in req:
+ method = req['method']
+ else:
+ method = None
+ if 'params' in req:
+ params = req['params']
+ else:
+ params = None
+ resp = dict(jsonrpc='2.0')
+ if 'id' in req:
+ resp['id'] = req['id']
+ if method not in self._rpc_methods:
+ error = dict(code=-32601,message='Method not found')
+ resp['error'] = error;
+ else:
+ try:
+ result = self._rpc_methods[method](self, params)
+ resp['result'] = result
+ except NameError as n:
+ error = dict(code=-32602, message='Invalid params', data=n.args)
+ resp['error'] = error
+ except RuntimeError as err:
+ error = dict(code=-32000, message=err.args)
+ resp['error'] = error
+ print ("%s" % json.dumps(resp))
+ return json.dumps(resp)
+
diff --git a/nvmetproxy b/nvmetproxy
new file mode 100755
index 0000000..9c644d5
--- /dev/null
+++ b/nvmetproxy
@@ -0,0 +1,197 @@
+#!/usr/bin/python
+
+'''
+Simple json-rpc proxy daemon, based on the SPDK JSON RPC methods
+
+Copyright (c) 2021 by Hannes Reinecke, SUSE Linux LLC
+
+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.
+'''
+
+from __future__ import print_function
+
+import os
+import sys
+import json
+import socket
+import argparse
+import base64
+import uuid as UUID
+import nvmet as nvme
+try:
+ from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+except ImportError:
+ from http.server import HTTPServer
+ from http.server import BaseHTTPRequestHandler
+
+nvmet_sock = '/var/tmp/nvmet.sock'
+nvmet_url = '/nvmet'
+
+class SocketHandler:
+ def __init__(self, sockname):
+ self.sockname = sockname
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ self.sock.bind(self.sockname)
+ except OSError:
+ print("cannot bind to %s" % self.sockname)
+ return None
+ self.sock.listen(10)
+
+ def __enter__(self):
+ return self
+
+ def rpc_server(self):
+ while True:
+ buf = ''
+ closed = False
+ response = None
+ (client, address) = self.sock.accept()
+ while not closed:
+ newdata = client.recv(1024)
+ if (newdata == b''):
+ closed = True
+ buf += newdata.decode('ascii')
+ try:
+ req = json.loads(buf)
+ except ValueError:
+ continue
+ rpc = nvme.JsonRPC(self.pool)
+ response = rpc.rpc_call(req)
+ client.sendall(response.encode('ascii'))
+ client.close()
+ closed = True
+
+ def rpc_shutdown(self):
+ self.sock.close()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ os.remove(self.sockname)
+
+class ServerHandler(BaseHTTPRequestHandler):
+ key = ""
+ pool = None
+
+ def do_HEAD(self):
+ self.send_response(200)
+ self.send_header('Content-type', 'application/json')
+ self.end_headers()
+
+ def do_AUTHHEAD(self):
+ self.send_response(401)
+ self.send_header('WWW-Authenticate', 'text/html')
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+
+ def do_INTERNALERROR(self):
+ self.send_error(500, message='Internal Server Error',
+ explain='Failed to parse JSON RPC request')
+
+ def do_NOTFOUND(self):
+ self.send_error(404)
+
+ def do_GET(self):
+ self.send_error(501, message="Unsupported method ('%s')" % self.command)
+
+ def do_PUT(self):
+ self.send_error(501, message="Unsupported method ('%s')" % self.command)
+
+ def do_POST(self):
+ if self.path != self.directory:
+ self.do_NOTFOUND()
+ return
+ if self.key and self.headers['Authorization'] != 'Basic ' + self.key:
+ self.do_AUTHHEAD()
+ else:
+ data_string = self.rfile.read(int(self.headers['Content-Length']))
+ try:
+ req = json.loads(data_string)
+ except ValueError:
+ self.do_INTERNALERROR()
+ return
+ rpc = nvme.JsonRPC(self.pool)
+ response = rpc.rpc_call(req)
+ self.do_HEAD()
+ self.wfile.write(bytes(response.encode(encoding='ascii')))
+
+def main():
+ pool = []
+
+ parser = argparse.ArgumentParser(description='JSON RPC proxy for nvmet')
+ parser.add_argument('-s', '--socket', dest='sock', default=nvmet_sock,
+ help="Socket to listen on, default is " + nvmet_sock)
+ parser.add_argument('-H', '--host', dest='host', help='Host address')
+ parser.add_argument('-p', '--port', dest='port', type=int, default=4260,
+ help='Port number')
+ parser.add_argument('-U', '--user', dest='user',
+ help='user name for authentication')
+ parser.add_argument('-P', '--password', dest='password',
+ help='password for authentication')
+ parser.add_argument('-c', '--cert', dest='cert',
+ help='SSL certificate')
+ parser.add_argument('-u', '--url', dest='url', default=nvmet_url,
+ help="URL path to serve, default is " + nvmet_url)
+ parser.add_argument('-d', '--filepool', dest='filepool', action='append',
+ help="Directory for backing files")
+ args = parser.parse_args()
+ if args.user and not args.password:
+ sys.exit("No password specified for username %s" % args.user)
+ if args.password and not args.user:
+ sys.exit("No username specified")
+ if args.user:
+ if not args.host:
+ sys.exit("Username and password are only valid for HTTP server")
+ key = base64.b64encode(bytes('%s:%s' % (args.user, args.password), 'utf-8')).decode('ascii')
+ ServerHandler.key = key
+
+ if args.url:
+ ServerHandler.directory = args.url
+
+ if args.filepool:
+ for i in args.filepool:
+ if not os.path.isdir(args.filepool[i]):
+ sys.exit("'%s' is not a directory" % args.filepool[i])
+ pool["pool%d" % i] = args.filepool[i]
+
+ ServerHandler.pool = pool
+ SocketHandler.pool = pool
+
+ if os.geteuid() != 0:
+ print("%s: must run as root." % sys.argv[0], file=sys.stderr)
+ sys.exit(-1)
+
+
+ if args.host:
+ with HTTPServer((args.host, args.port), ServerHandler) as httpd:
+ try:
+ if args.cert is not None:
+ http.socket = ssl.wrap_socket(httpd.socket,
+ certfile=args.cert,
+ server_side=True)
+ print("Started JSON RPC http proxy server on %s:%d" % (args.host, args.port))
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ print("Shutting down server")
+ httpd.socket.close()
+ else:
+ with SocketHandler(args.sock) as s:
+ try:
+ print("Started JSON RPC proxy server on %s" % args.sock)
+ s.rpc_server()
+ except KeyboardInterrupt:
+ print('Shutting down server')
+ s.rpc_shutdown()
+
+if __name__ == '__main__':
+ main()
diff --git a/setup.py b/setup.py
index 1956d95..f15e5b0 100755
--- a/setup.py
+++ b/setup.py
@@ -27,5 +27,5 @@ setup(
maintainer_email = 'hch at lst.de',
test_suite='nose2.collector.collector',
packages = ['nvmet'],
- scripts=['nvmetcli']
+ scripts=['nvmetcli', 'nvmetproxy']
)
--
2.29.2
More information about the Linux-nvme
mailing list