linux-nvme.lists.infradead.org archive mirror
 help / color / mirror / Atom feed
* [RFC PATCH 0/3] nvmetcli: remote configuation
@ 2021-02-12 15:52 Hannes Reinecke
  2021-02-12 15:52 ` [PATCH 1/3] nvmetcli: add 'merge' parameter to set_config() Hannes Reinecke
                   ` (3 more replies)
  0 siblings, 4 replies; 6+ messages in thread
From: Hannes Reinecke @ 2021-02-12 15:52 UTC (permalink / raw)
  To: Christoph Hellwig
  Cc: Daniel Wagner, Keith Busch, Chaitanya Kulkarni, Hannes Reinecke,
	linux-nvme, Enzo Matsumiya, Sagi Grimberg

Hi all,

I've discovered that SPDK has an JSON RPC interface which allows for
remote configuration. And it turns out that the SPDK configuration
itself is pretty similar to the in-kernel NVMe-oF target (surprise,
surprise).
And to not let SPDK have an upper edge here's a simple JSON-RPC proxy
for nvmet, together with a JSON-RPC client 'nvmetadm'.
This allows a full remote configuration of an NVMe-oF target:

# nvmetadm nvmf_create_subsystem --nqn=nqn.nvmf-test1
# nvmetadm nvmf_subsystem_add_port --nqn=nqn.nvmf-test1 \
  --traddr=<target-ip> --trtrype=tcp --adrfam=ipv6
# nvmetadm bdev_create_file --file-name=Testfile1 --size=1G
# nvmetadm nvmf_subsystem_add_ns --nqn=nqn.nvmf-test1 \
  --bdev=Testfile1
# nvmetadm nvmf_subsystem_add_host --nqn=nqn.nvmf-test1 \
  --host=<hostnqn>
# nvme discover --transport=tcp --traddr=<target-ip> \
  --trsvcid=4420

With this it should be possible to convert blktests nvme suite to use
this interface, which will allow blktest to run across arbitrary
transports instead of just nvme-loop.

As usual, comments and reviews are welcome.

Hannes Reinecke (3):
  nvmetcli: add 'merge' parameter to set_config()
  nvmetproxy: add a JSON-RPC proxy daemon
  nvmetadm: add JSON-RPC client for remote configuration

 Documentation/Makefile       |  24 +-
 Documentation/nvmetproxy.txt | 111 ++++++++
 nvmet/__init__.py            |   4 +-
 nvmet/bdev.py                |  72 +++++
 nvmet/nvme.py                |   8 +-
 nvmet/rpc.py                 | 495 +++++++++++++++++++++++++++++++++++
 nvmetadm                     | 259 ++++++++++++++++++
 nvmetcli                     |   4 +-
 nvmetproxy                   | 197 ++++++++++++++
 setup.py                     |   2 +-
 10 files changed, 1157 insertions(+), 19 deletions(-)
 create mode 100644 Documentation/nvmetproxy.txt
 create mode 100644 nvmet/bdev.py
 create mode 100644 nvmet/rpc.py
 create mode 100755 nvmetadm
 create mode 100755 nvmetproxy

-- 
2.29.2


_______________________________________________
Linux-nvme mailing list
Linux-nvme@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-nvme

^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/3] nvmetcli: add 'merge' parameter to set_config()
  2021-02-12 15:52 [RFC PATCH 0/3] nvmetcli: remote configuation Hannes Reinecke
@ 2021-02-12 15:52 ` Hannes Reinecke
  2021-02-12 15:52 ` [PATCH 2/3] nvmetproxy: add a JSON-RPC proxy daemon Hannes Reinecke
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 6+ messages in thread
From: Hannes Reinecke @ 2021-02-12 15:52 UTC (permalink / raw)
  To: Christoph Hellwig
  Cc: Daniel Wagner, Keith Busch, Chaitanya Kulkarni, Hannes Reinecke,
	linux-nvme, Enzo Matsumiya, Sagi Grimberg

Add a parameter 'merge' to set_config() to allow for merge a given
configuration with the existing one.

Signed-off-by: Hannes Reinecke <hare@suse.de>
---
 nvmet/nvme.py | 8 +++++---
 nvmetcli      | 4 ++--
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/nvmet/nvme.py b/nvmet/nvme.py
index 83fd75b..22aef5a 100644
--- a/nvmet/nvme.py
+++ b/nvmet/nvme.py
@@ -331,17 +331,19 @@ class Root(CFSNode):
         for h in self.hosts:
             h.delete()
 
-    def restore(self, config, clear_existing=False, abort_on_error=False):
+    def restore(self, config, clear_existing=False, abort_on_error=False, merge=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.
+        clear_existing or merge is True. If clear_existing is True the
+        existing configuration will be cleared. If merge is True the
+        given configuration will be attempted to be merged.
         '''
         if clear_existing:
             self.clear_existing()
         else:
-            if any(self.subsystems):
+            if any(self.subsystems) and not merge:
                 raise CFSError("subsystems present, not restoring")
 
         errors = []
diff --git a/nvmetcli b/nvmetcli
index 8ee8590..f2b59bb 100755
--- a/nvmetcli
+++ b/nvmetcli
@@ -104,11 +104,11 @@ class UIRootNode(UINode):
         UIPortsNode(self)
         UIHostsNode(self)
 
-    def ui_command_restoreconfig(self, savefile=None, clear_existing=False):
+    def ui_command_restoreconfig(self, savefile=None, clear_existing=False, merge=False):
         '''
         Restores configuration from a file.
         '''
-        errors = self.cfnode.restore_from_file(savefile, clear_existing)
+        errors = self.cfnode.restore_from_file(savefile, clear_existing, merge)
         self.refresh()
 
         if errors:
-- 
2.29.2


_______________________________________________
Linux-nvme mailing list
Linux-nvme@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-nvme

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/3] nvmetproxy: add a JSON-RPC proxy daemon
  2021-02-12 15:52 [RFC PATCH 0/3] nvmetcli: remote configuation Hannes Reinecke
  2021-02-12 15:52 ` [PATCH 1/3] nvmetcli: add 'merge' parameter to set_config() Hannes Reinecke
@ 2021-02-12 15:52 ` Hannes Reinecke
  2021-02-12 15:52 ` [PATCH 3/3] nvmetadm: add JSON-RPC client for remote configuration Hannes Reinecke
  2021-07-13  9:21 ` [RFC PATCH 0/3] nvmetcli: remote configuation Christoph Hellwig
  3 siblings, 0 replies; 6+ messages in thread
From: Hannes Reinecke @ 2021-02-12 15:52 UTC (permalink / raw)
  To: Christoph Hellwig
  Cc: Daniel Wagner, Keith Busch, Chaitanya Kulkarni, Hannes Reinecke,
	linux-nvme, Enzo Matsumiya, Sagi Grimberg

Add a JSON-RPC proxy daemon to allow for remote configuration
of the NVMe-over-Fabrics target.

Signed-off-by: Hannes Reinecke <hare@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@suse.de[Hannes Reinecke]
+
+REPORTING BUGS & DEVELOPMENT
+-----------------------------
+Please send patches and bug reports to linux-nvme@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@lst.de',
     test_suite='nose2.collector.collector',
     packages = ['nvmet'],
-    scripts=['nvmetcli']
+    scripts=['nvmetcli', 'nvmetproxy']
     )
-- 
2.29.2


_______________________________________________
Linux-nvme mailing list
Linux-nvme@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-nvme

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 3/3] nvmetadm: add JSON-RPC client for remote configuration
  2021-02-12 15:52 [RFC PATCH 0/3] nvmetcli: remote configuation Hannes Reinecke
  2021-02-12 15:52 ` [PATCH 1/3] nvmetcli: add 'merge' parameter to set_config() Hannes Reinecke
  2021-02-12 15:52 ` [PATCH 2/3] nvmetproxy: add a JSON-RPC proxy daemon Hannes Reinecke
@ 2021-02-12 15:52 ` Hannes Reinecke
  2021-07-13  9:21 ` [RFC PATCH 0/3] nvmetcli: remote configuation Christoph Hellwig
  3 siblings, 0 replies; 6+ messages in thread
From: Hannes Reinecke @ 2021-02-12 15:52 UTC (permalink / raw)
  To: Christoph Hellwig
  Cc: Daniel Wagner, Keith Busch, Chaitanya Kulkarni, Hannes Reinecke,
	linux-nvme, Enzo Matsumiya, Sagi Grimberg

Add a JSON-RPC client for remote configuration of an NVMe-over-Fabrics
target.

Signed-off-by: Hannes Reinecke <hare@suse.de>
---
 nvmetadm | 259 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 setup.py |   2 +-
 2 files changed, 260 insertions(+), 1 deletion(-)
 create mode 100755 nvmetadm

diff --git a/nvmetadm b/nvmetadm
new file mode 100755
index 0000000..74479ff
--- /dev/null
+++ b/nvmetadm
@@ -0,0 +1,259 @@
+#!/usr/bin/python3
+
+import sys
+import argparse
+import requests
+import json
+
+class rpc_client():
+    def bdev_file_create(self, args):
+        self.parser.add_argument('--filename', required=True)
+        self.parser.add_argument('--size', required=True)
+        self.parser.add_argument('--pool')
+        a = self.parser.parse_args(args)
+        s = a.size.lower().rfind('g')
+        if s != -1:
+            size = int(a.size.lower()[:-s]) * 1024 * 1024 * 1024
+        else:
+            s = a.size.lower().rfind('m')
+            if s != -1:
+                size = int(a.size.lower()[:-s]) * 1024 * 1024
+            else:
+                s = a.size.lower().rfind('k')
+                if s != -1:
+                    size = int(a.size.lower()[:-s]) * 1024
+                else:
+                    size = int(a.size)
+        params = {'file_name': a.filename, 'size': size}
+        if a.pool:
+            params['pool'] = a.pool
+        return params
+
+    def bdev_file_delete(self, args):
+        self.parser.add_argument('--filename', required=True)
+        self.parser.add_argument('--pool')
+        a = self.parser.parse_args(args)
+        params = {'filename': a.filename }
+        if a.pool:
+            params['pool'] = a.pool
+        return params
+
+    def nvmf_set_config(self, args):
+        self.parser.add_argument('--cfg-file', required=True)
+        a = self.parser.parse_args(args)
+        try:
+            cfg = open(a.cfg_file, "r")
+        except OSError:
+            sys.exit(1)
+        params = json.load(cfg)
+        cfg.close()
+        return params
+
+    def nvmf_create_transport(self, args):
+        self.parser.add_argument('--trtype', required=True)
+        a = self.parser.parse_args(args)
+        if not a.trtype:
+            return None
+        return {"trtype": a.trtype}
+
+    def nvmf_create_subsystem(self, args):
+        self.parser.add_argument('--nqn')
+        a = self.parser.parse_args(args)
+        params = None
+        if not a.nqn:
+            return None
+        return {"nqn": a.nqn}
+
+    def nvmf_delete_subsystem(self, args):
+        self.parser.add_argument('--nqn', required=True)
+        a = self.parser.parse_args(args)
+        return {"nqn": a.nqn}
+
+    def nvmf_subsystem_add_ns(self, args):
+        self.parser.add_argument('--nqn', required=True)
+        self.parser.add_argument('--bdev-name', required=True)
+        self.parser.add_argument('--nsid')
+        self.parser.add_argument('--nguid')
+        self.parser.add_argument('--eui64')
+        self.parser.add_argument('--uuid')
+        a = self.parser.parse_args(args)
+        ns = { 'bdev_name': a.bdev_name }
+        if a.nsid:
+            ns['nsid'] = a.nsid
+        if a.nguid:
+            ns['nguid'] = a.nguid
+        if a.eui64:
+            ns['eui64'] = a.eui64
+        if a.uuid:
+            ns['uuid'] = a.uuid
+        return {"nqn": a.nqn, 'namespace': ns}
+
+    def nvmf_subsystem_remove_ns(self, args):
+        self.parser.add_argument('--nqn', required=True)
+        self.parser.add_argument('--nsid', required=True, type=int)
+        a = self.parser.parse_args(args)
+        return {'nqn': a.nqn, 'nsid': a.nsid }
+
+    def nvmf_subsystem_port(self, args):
+        self.parser.add_argument('--nqn', required=True)
+        self.parser.add_argument('--portid', type=int)
+        self.parser.add_argument('--trtype', required=True)
+        self.parser.add_argument('--traddr', required=True)
+        self.parser.add_argument('--adrfam')
+        self.parser.add_argument('--trscvid')
+        self.parser.add_argument('--host-traddr')
+        a = self.parser.parse_args(args)
+        port = { 'trtype': a.trtype, 'traddr': a.traddr }
+        if a.portid:
+            port['portid'] = a.portid
+        if a.adrfam:
+            port['adrfam'] = a.adrfam
+        if a.trscvid:
+            port['trsvcid'] = a.trscvid
+        if a.host_traddr:
+            port['host_traddr'] = a.host_traddr
+        return {"nqn": a.nqn, 'port': port}
+
+    def nvmf_subsystem_host(self, args):
+        self.parser.add_argument('--nqn', required=True)
+        self.parser.add_argument('--host', required=True)
+        a = self.parser.parse_args(args)
+        return {'nqn': a.nqn, 'host': a.host }
+
+    def nvmf_port_ana(self, args):
+        self.parser.add_argument('--portid', required=True)
+        self.parser.add_argument('--grpid')
+        self.parser.add_argument('--ana_state')
+        a = self.parser.parse_args(args)
+        params = { 'portid': a.portid }
+        if a.grpid:
+            params['grpid'] = a.grpid
+        if a.ana_state:
+            params['ana_state'] = a.ana_state
+        return params
+
+    def method_no_param(self, args = None):
+        a = self.parser.parse_args(args)
+        return
+
+    _rpc_methods = dict(bdev_file_list_pools=method_no_param,
+                        bdev_file_create=bdev_file_create,
+                        bdev_file_delete=bdev_file_delete,
+                        nvmf_get_config=method_no_param,
+                        nvmf_set_config=nvmf_set_config,
+                        nvmf_get_interfaces=method_no_param,
+                        nvmf_create_transport=nvmf_create_transport,
+                        nvmf_get_transports=method_no_param,
+                        nvmf_create_subsystem=nvmf_create_subsystem,
+                        nvmf_delete_subsystem=nvmf_delete_subsystem,
+                        nvmf_subsystem_add_ns= nvmf_subsystem_add_ns,
+                        nvmf_subsystem_remove_ns=nvmf_subsystem_remove_ns,
+                        nvmf_subsystem_add_port=nvmf_subsystem_port,
+                        nvmf_subsystem_remove_port=nvmf_subsystem_port,
+                        nvmf_subsystem_add_host=nvmf_subsystem_host,
+                        nvmf_subsystem_remove_host=nvmf_subsystem_host,
+                        nvmf_port_add_ana=nvmf_port_ana,
+                        nvmf_port_set_ana=nvmf_port_ana,
+                        nvmf_port_remove_ana=nvmf_port_ana,
+                        nvmf_get_subsystems=method_no_param)
+
+    def __init__(self, method):
+        if method not in self._rpc_methods:
+            print("Invalid rpc method '%s'; must be one of" % method)
+            print(", ".join([x for x in list(self._rpc_methods)]))
+            sys.exit(1)
+                
+        self.parser = argparse.ArgumentParser(prog="nvmetadm %s" % method)
+        self._method = self._rpc_methods[method]
+
+    def _error(self, err):
+        data = None
+        if 'code' not in err:
+            code = -32000
+            message = "Server error"
+            data = []
+            data.append("Invalid JSON RPC response")
+        else:
+            code = err['code']
+        if 'message' in err:
+            message = err['message']
+        else:
+            if code == -32700:
+                message = "Parse error"
+            elif code == -32600:
+                message = "Invalid Request"
+            elif code == -32601:
+                message = "Method not found"
+            elif code == -32602:
+                message = "Invalid params"
+            elif code == -32603:
+                message = "Internal error"
+            elif code <= -32000 and code > -32100:
+                message = "Server error"
+            else:
+                data = []
+                data.append("Invalid JSON RPC error code %d" & code)
+                code = -32001
+                message = "Server error"
+        if 'data' in err:
+            data = err['data']
+        return (code, message, data)
+
+    def call(self, args):
+        return self._method(self, args)
+
+    def response(self, response):
+        if 'error' in response:
+            (code, message, data) = self._error(response['error'])
+            if data:
+                sys.exit("JSON RPC error %d: %s\n%s" % (code, message, json.dumps(data)))
+            else:
+                sys.exit("JSON RPC error %d: %s" % (code, message))
+        elif 'result' not in response:
+            sys.exit("JSON RPC error: Not a valid response\n%s", response)
+        elif response['result']:
+            return response['result']
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-H', '--host', help='Hostname to connect to',
+                        required=True)
+    parser.add_argument('-p', '--port', type=int, default=4260,
+                        help='Port number for the connection')
+    parser.add_argument('-U', '--username')
+    parser.add_argument('-P', '--password')
+    parser.add_argument('-u', '--uri', default='nvmet',
+                        help='URI of the JSON-RPC namespace')
+    (a, args) = parser.parse_known_args()
+    if a.username:
+        if not a.password:
+            sys.exit("Need to specify both username and password")
+        auth = (a.username, a.password)
+    elif not a.password:
+        auth = None
+    else:
+        sys.exit("Need to specify both username and password")
+    url = "http://%s:%d/%s" % (a.host, a.port, a.uri)
+
+    if not args or not len(args):
+        sys.exit("No method given")
+
+    method = args.pop(0)
+    payload = {
+        "method": method,
+        "jsonrpc": "2.0",
+        "id": 0,
+    }
+    rpc = rpc_client(method)
+    params = rpc.call(args)
+    if params:
+        payload['params'] = params
+    response = requests.post(url, auth=auth,json=payload)
+    response.raise_for_status()
+    msg = rpc.response(response.json())
+    if msg:
+        print("%s" % json.dumps(msg, indent=2))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/setup.py b/setup.py
index f15e5b0..998ff78 100755
--- a/setup.py
+++ b/setup.py
@@ -27,5 +27,5 @@ setup(
     maintainer_email = 'hch@lst.de',
     test_suite='nose2.collector.collector',
     packages = ['nvmet'],
-    scripts=['nvmetcli', 'nvmetproxy']
+    scripts=['nvmetcli', 'nvmetproxy', 'nvmetadm']
     )
-- 
2.29.2


_______________________________________________
Linux-nvme mailing list
Linux-nvme@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-nvme

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [RFC PATCH 0/3] nvmetcli: remote configuation
  2021-02-12 15:52 [RFC PATCH 0/3] nvmetcli: remote configuation Hannes Reinecke
                   ` (2 preceding siblings ...)
  2021-02-12 15:52 ` [PATCH 3/3] nvmetadm: add JSON-RPC client for remote configuration Hannes Reinecke
@ 2021-07-13  9:21 ` Christoph Hellwig
  2021-07-13  9:41   ` Hannes Reinecke
  3 siblings, 1 reply; 6+ messages in thread
From: Christoph Hellwig @ 2021-07-13  9:21 UTC (permalink / raw)
  To: Hannes Reinecke
  Cc: Christoph Hellwig, Chaitanya Kulkarni, Sagi Grimberg,
	Keith Busch, Daniel Wagner, Enzo Matsumiya, linux-nvme

So this doesn't look horrible to me, but do you plan to actually
have users for this?  Test cases would also be really helpful.

_______________________________________________
Linux-nvme mailing list
Linux-nvme@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-nvme

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [RFC PATCH 0/3] nvmetcli: remote configuation
  2021-07-13  9:21 ` [RFC PATCH 0/3] nvmetcli: remote configuation Christoph Hellwig
@ 2021-07-13  9:41   ` Hannes Reinecke
  0 siblings, 0 replies; 6+ messages in thread
From: Hannes Reinecke @ 2021-07-13  9:41 UTC (permalink / raw)
  To: Christoph Hellwig
  Cc: Chaitanya Kulkarni, Sagi Grimberg, Keith Busch, Daniel Wagner,
	Enzo Matsumiya, linux-nvme

On 7/13/21 11:21 AM, Christoph Hellwig wrote:
> So this doesn't look horrible to me, but do you plan to actually
> have users for this?  Test cases would also be really helpful.
> 
Well, idea was to enable things like blktest to configure remote systems
such that we can expand the test matrix beyond nvme-loop to 'real'
hardware like nvme-fc or nvme-rdma.

But then I found that jsonrpc is actually defunct as google switched to
gRPC (and go in general), making the use of jsonrpc ever so slightly
awkward.

So not sure; I'd be happy to leave it for now, and I'll see to tie in
blktests with this.

Cheers,

Hannes
-- 
Dr. Hannes Reinecke		           Kernel Storage Architect
hare@suse.de			                  +49 911 74053 688
SUSE Software Solutions Germany GmbH, Maxfeldstr. 5, 90409 Nürnberg
HRB 36809 (AG Nürnberg), GF: Felix Imendörffer

_______________________________________________
Linux-nvme mailing list
Linux-nvme@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-nvme

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2021-07-13  9:41 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-02-12 15:52 [RFC PATCH 0/3] nvmetcli: remote configuation Hannes Reinecke
2021-02-12 15:52 ` [PATCH 1/3] nvmetcli: add 'merge' parameter to set_config() Hannes Reinecke
2021-02-12 15:52 ` [PATCH 2/3] nvmetproxy: add a JSON-RPC proxy daemon Hannes Reinecke
2021-02-12 15:52 ` [PATCH 3/3] nvmetadm: add JSON-RPC client for remote configuration Hannes Reinecke
2021-07-13  9:21 ` [RFC PATCH 0/3] nvmetcli: remote configuation Christoph Hellwig
2021-07-13  9:41   ` Hannes Reinecke

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).