From 83795a1600b9ccda4564de2fddbe8c80c22ab79d Mon Sep 17 00:00:00 2001 From: Karol Latecki Date: Fri, 23 Mar 2018 11:40:31 +0100 Subject: [PATCH] spdkcli: initial version with bdev management Initial version for SPDKCli Possible basic management of: - Bdevs: malloc, nvme, aio, lvol create / delete operations. - Lvol stores: create / delete operations. Adding dependency to pkgdep.sh. Change-Id: I1a03d7660dad0335e25734b8ffb90592a5b337c2 Signed-off-by: Karol Latecki Reviewed-on: https://review.gerrithub.io/405039 Tested-by: SPDK Automated Test System Reviewed-by: Jim Harris Reviewed-by: Ben Walker Reviewed-by: Tomasz Zawadzki --- doc/Doxyfile | 1 + doc/index.md | 4 + doc/spdkcli.md | 61 ++++++++ scripts/pkgdep.sh | 4 + scripts/spdkcli.py | 38 +++++ scripts/spdkcli/__init__.py | 1 + scripts/spdkcli/ui_node.py | 297 ++++++++++++++++++++++++++++++++++++ scripts/spdkcli/ui_root.py | 96 ++++++++++++ 8 files changed, 502 insertions(+) create mode 100644 doc/spdkcli.md create mode 100755 scripts/spdkcli.py create mode 100644 scripts/spdkcli/__init__.py create mode 100644 scripts/spdkcli/ui_node.py create mode 100644 scripts/spdkcli/ui_root.py diff --git a/doc/Doxyfile b/doc/Doxyfile index 25fc44dbf..609cdc5ab 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -804,6 +804,7 @@ INPUT = ../include/spdk \ nvmf.md \ nvmf_tgt_pg.md \ peer_2_peer.md \ + spdkcli.md \ ssd_internals.md \ userspace.md \ vagrant.md \ diff --git a/doc/index.md b/doc/index.md index ab082d89a..61024f2e3 100644 --- a/doc/index.md +++ b/doc/index.md @@ -53,6 +53,10 @@ - @ref nvme-cli +# Experimental Tools {#experimental_tools} + +- @ref spdkcli + # Performance Reports {#performancereports} - [SPDK 17.07 vhost-scsi Performance Report](https://ci.spdk.io/download/performance-reports/SPDK17_07_vhost_scsi_performance_report.pdf) diff --git a/doc/spdkcli.md b/doc/spdkcli.md new file mode 100644 index 000000000..df72df885 --- /dev/null +++ b/doc/spdkcli.md @@ -0,0 +1,61 @@ +# SPDK CLI {#spdkcli} + +Spdkcli is a command-line management application for SPDK. +Spdkcli has support for a limited number of applications and bdev modules, +and should be considered experimental for the v18.04 release. +This experimental version was added for v18.04 to get early feedback +that can be incorporated as spdkcli becomes more fully-featured +for the next SPDK release. + +### Install needed dependencies + +All dependencies should be handled by scripts/pkgdep.sh script. +Package dependencies at the moment include: + - configshell + +### Run SPDK application instance + +~~~{.sh} +./scripts/setup.sh +./app/vhost/vhost -c vhost.conf +~~~ + +### Run SPDK CLI + +Spdkcli should be run with the same priviliges as SPDK application. +In order to use SPDK CLI in interactive mode please use: +~~~{.sh} +scripts/spdkcli.py +~~~ +Use "help" command to get a list of available commands for each tree node. + +It is also possible to use SPDK CLI to run just a single command, +just use the command as an argument to the application. +For example, to view current configuration and immediately exit: + ~~~{.sh} +scripts/spdkcli.py ls +~~~ + +### Optional - create Python virtual environment + +You can use Python virtual environment if you don't want to litter your +system Python installation. + +First create the virtual environment: +~~~{.sh} +cd spdk +mkdir venv +virtualenv-3 ./venv +source ./venv/bin/activate +~~~ + +Then install the dependencies using pip. That way depedencies will be +installed only inside the virtual environment. +~~~{.sh} +(venv) pip install configshell-fb +~~~ + +Tip: if you are using "sudo" instead of root account, it is suggested to do +"sudo -s" before activating the environment. This is because venv might not work +correctly when calling spdkcli with sudo, like "sudo python spdkcli.py" - +some environment variables might not be passed and you will experience errors. diff --git a/scripts/pkgdep.sh b/scripts/pkgdep.sh index 4b5b2c61d..bb01e119a 100755 --- a/scripts/pkgdep.sh +++ b/scripts/pkgdep.sh @@ -20,6 +20,8 @@ if [ -s /etc/redhat-release ]; then yum install -y doxygen mscgen graphviz # Additional dependencies for building pmem based backends yum install -y libpmemblk-devel || true + # Additional dependencies for SPDK CLI + yum install -y python-configshell elif [ -f /etc/debian_version ]; then # Includes Ubuntu, Debian apt-get install -y gcc g++ make libcunit1-dev libaio-dev libssl-dev \ @@ -30,6 +32,8 @@ elif [ -f /etc/debian_version ]; then apt-get install -y libnuma-dev # Additional dependencies for building docs apt-get install -y doxygen mscgen graphviz + # Additional dependencies for SPDK CLI + apt-get install -y "python-configshell*" elif [ $SYSTEM = "FreeBSD" ] ; then pkg install gmake cunit openssl git devel/astyle bash devel/pep8 \ python misc/e2fsprogs-libuuid sysutils/sg3_utils diff --git a/scripts/spdkcli.py b/scripts/spdkcli.py new file mode 100755 index 000000000..c7c3867b0 --- /dev/null +++ b/scripts/spdkcli.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +import sys +import argparse +from os import getuid +from configshell_fb import ConfigShell +from spdkcli import UIRoot + + +def main(): + """ + Start SPDK CLI + :return: + """ + shell = ConfigShell("~/.scripts") + + parser = argparse.ArgumentParser(description="SPDK command line interface") + parser.add_argument("-s", dest="socket", help="RPC socket path", default="/var/tmp/spdk.sock") + parser.add_argument("commands", metavar="command", type=str, nargs="*", default="", + help="commands to execute by SPDKCli as one-line command") + args = parser.parse_args() + + root_node = UIRoot(args.socket, shell) + try: + root_node.refresh() + except: + pass + + if len(args.commands) > 0: + shell.run_cmdline(" ".join(args.commands)) + sys.exit(0) + + shell.con.display("SPDK CLI v0.1") + shell.con.display("") + shell.run_interactive() + + +if __name__ == "__main__": + main() diff --git a/scripts/spdkcli/__init__.py b/scripts/spdkcli/__init__.py new file mode 100644 index 000000000..571d49a8f --- /dev/null +++ b/scripts/spdkcli/__init__.py @@ -0,0 +1 @@ +from .ui_root import UIRoot diff --git a/scripts/spdkcli/ui_node.py b/scripts/spdkcli/ui_node.py new file mode 100644 index 000000000..cdbc8d59f --- /dev/null +++ b/scripts/spdkcli/ui_node.py @@ -0,0 +1,297 @@ +from configshell_fb import ConfigNode, ExecutionError +from uuid import UUID +import json + + +def convert_bytes_to_human(size): + if not size: + return "" + for x in ["bytes", "K", "M", "G", "T"]: + if size < 1024.0: + return "%3.1f%s" % (size, x) + size /= 1024.0 + + +class UINode(ConfigNode): + def __init__(self, name, parent=None, shell=None): + ConfigNode.__init__(self, name, parent, shell) + + def refresh(self): + for child in self.children: + child.refresh() + + def ui_command_refresh(self): + self.refresh() + + def ui_command_ll(self, path=None, depth=None): + """ + Alias for ls. + """ + self.ui_command_ls(path, depth) + + def execute_command(self, command, pparams=[], kparams={}): + try: + result = ConfigNode.execute_command(self, command, + pparams, kparams) + except Exception as msg: + self.shell.log.error(str(msg)) + pass + else: + self.shell.log.debug("Command %s succeeded." % command) + return result + + +class UIBdevs(UINode): + def __init__(self, parent): + UINode.__init__(self, "bdevs", parent) + self.refresh() + + def refresh(self): + self._children = set([]) + UIMallocBdev(self) + UIAIOBdev(self) + UILvolBdev(self) + UINvmeBdev(self) + + def ui_command_delete(self, name): + """ + Deletes bdev from configuration. + + Arguments: + name - Is a unique identifier of the bdev to be deleted - UUID number or name alias. + """ + self.get_root().delete_bdev(name=name) + self.refresh() + + +class UILvolStores(UINode): + def __init__(self, parent): + UINode.__init__(self, "lvol_stores", parent) + self.refresh() + + def refresh(self): + self._children = set([]) + for lvs in self.get_root().get_lvol_stores(): + UILvsObj(lvs, self) + + def ui_command_create(self, name, bdev_name, cluster_size=None): + """ + Creates logical volume store on target bdev. + + Arguments: + name - Friendly name to use alongside with UUID identifier. + bdev_name - On which bdev to create the lvol store. + cluster_size - Cluster size to use when creating lvol store, in bytes. Default: 4194304. + """ + + cluster_size = self.ui_eval_param(cluster_size, "number", None) + + self.get_root().create_lvol_store(lvs_name=name, bdev_name=bdev_name, cluster_sz=cluster_size) + self.get_root().refresh() + self.refresh() + + def ui_command_delete(self, name=None, uuid=None): + """ + Deletes logical volume store from configuration. + This will also delete all logical volume bdevs created on this lvol store! + + Arguments: + name - Friendly name of the logical volume store to be deleted. + uuid - UUID number of the logical volume store to be deleted. + """ + if name is None and uuid is None: + self.shell.log.error("Please specify one of the identifiers: " + "lvol store name or UUID") + self.get_root().delete_lvol_store(lvs_name=name, uuid=uuid) + self.get_root().refresh() + self.refresh() + + def summary(self): + return "Lvol stores: %s" % len(self.children), None + + +class UIBdev(UINode): + def __init__(self, name, parent): + UINode.__init__(self, name, parent) + self.refresh() + + def refresh(self): + self._children = set([]) + for bdev in self.get_root().get_bdevs(self.name): + UIBdevObj(bdev, self) + + def ui_command_delete(self, name): + """ + Deletes bdev from configuration. + + Arguments: + name - Is a unique identifier of the bdev to be deleted - UUID number or name alias. + """ + self.get_root().delete_bdev(name=name) + self.get_root().refresh() + self.refresh() + + def summary(self): + return "Bdevs: %d" % len(self.children), None + + +class UIMallocBdev(UIBdev): + def __init__(self, parent): + UIBdev.__init__(self, "Malloc", parent) + + def ui_command_create(self, size, block_size, name=None, uuid=None): + """ + Construct a Malloc bdev. + + Arguments: + size - Size in megabytes. + block_size - Integer, block size to use when constructing bdev. + name - Optional argument. Custom name to use for bdev. If not provided + then name will be "MallocX" where X is next available ID. + uuid - Optional parameter. Custom UUID to use. If empty then random + will be generated. + """ + + size = self.ui_eval_param(size, "number", None) + block_size = self.ui_eval_param(block_size, "number", None) + + ret_name = self.get_root().create_malloc_bdev(total_size=size, + block_size=block_size, + name=name, uuid=uuid) + self.shell.log.info(ret_name) + self.get_root().refresh() + self.refresh() + + +class UIAIOBdev(UIBdev): + def __init__(self, parent): + UIBdev.__init__(self, "AIO", parent) + + def ui_command_create(self, name, filename, block_size): + """ + Construct an AIO bdev. + Backend file must exist before trying to create an AIO bdev. + + Arguments: + name - Optional argument. Custom name to use for bdev. If not provided + then name will be "MallocX" where X is next available ID. + filename - Path to AIO backend. + block_size - Integer, block size to use when constructing bdev. + """ + + block_size = self.ui_eval_param(block_size, "number", None) + + ret_name = self.get_root().create_aio_bdev(name=name, + block_size=int(block_size), + filename=filename) + self.shell.log.info(ret_name) + self.get_root().refresh() + self.refresh() + + +class UILvolBdev(UIBdev): + def __init__(self, parent): + UIBdev.__init__(self, "Logical_Volume", parent) + + def ui_command_create(self, name, size, lvs, thin_provision=None): + """ + Construct a Logical Volume bdev. + + Arguments: + name - Friendly name to use for creating logical volume bdev. + size - Size in megabytes. + lvs - Identifier of logical volume store on which the bdev should be + created. Can be either a friendly name or UUID. + thin_provision - Whether the bdev should be thick or thin provisioned. + Default is False, and created bdevs are thick-provisioned. + """ + uuid = None + lvs_name = None + try: + UUID(lvs) + uuid = lvs + except ValueError: + lvs_name = lvs + + size = self.ui_eval_param(size, "number", None) + size *= (1024 * 1024) + thin_provision = self.ui_eval_param(thin_provision, "bool", False) + + ret_uuid = self.get_root().create_lvol_bdev(lvol_name=name, size=size, + lvs_name=lvs_name, uuid=uuid, + thin_provision=thin_provision) + self.shell.log.info(ret_uuid) + self.get_root().refresh() + self.refresh() + + +class UINvmeBdev(UIBdev): + def __init__(self, parent): + UIBdev.__init__(self, "NVMe", parent) + + def ui_command_create(self, name, trtype, traddr, + adrfam=None, trsvcid=None, subnqn=None): + + if "rdma" in trtype and None in [adrfam, trsvcid, subnqn]: + self.shell.log.error("Using RDMA transport type." + "Please provide arguments for adrfam, trsvcid and subnqn.") + + ret_name = self.get_root().create_nvme_bdev(name=name, trtype=trtype, + traddr=traddr, adrfam=adrfam, + trsvcid=trsvcid, subnqn=subnqn) + self.shell.log.info(ret_name) + self.get_root().refresh() + self.refresh() + + +class UIBdevObj(UINode): + def __init__(self, bdev, parent): + self.bdev = bdev + # Using bdev name also for lvol bdevs, which results in displying + # UUID instead of alias. This is because alias naming convention + # (lvol_store_name/lvol_bdev_name) conflicts with configshell paths + # ("/" as separator). + # Solution: show lvol alias in "summary field" for now. + # TODO: Possible next steps: + # - Either change default separator in tree for smth else + # - or add a UI command which would be able to autocomplete + # "cd" command based on objects alias and match is to the + # "main" bdev name. + UINode.__init__(self, self.bdev.name, parent) + + def ui_command_show_details(self): + self.shell.log.info(json.dumps(vars(self.bdev), indent=2)) + + def summary(self): + size = convert_bytes_to_human(self.bdev.block_size * self.bdev.num_blocks) + size = "=".join(["Size", size]) + + in_use = "Not claimed" + if bool(self.bdev.claimed): + in_use = "Claimed" + + alias = None + if self.bdev.aliases: + alias = self.bdev.aliases[0] + + info = ", ".join(filter(None, [alias, size, in_use])) + return info, True + + +class UILvsObj(UINode): + def __init__(self, lvs, parent): + UINode.__init__(self, lvs.name, parent) + self.lvs = lvs + + def ui_command_show_details(self): + self.shell.log.info(json.dumps(vars(self.lvs), indent=2)) + + def summary(self): + size = convert_bytes_to_human(self.lvs.total_data_clusters * self.lvs.cluster_size) + free = convert_bytes_to_human(self.lvs.free_clusters * self.lvs.cluster_size) + if not free: + free = "0" + size = "=".join(["Size", size]) + free = "=".join(["Free", free]) + info = ", ".join([str(size), str(free)]) + return info, True diff --git a/scripts/spdkcli/ui_root.py b/scripts/spdkcli/ui_root.py new file mode 100644 index 000000000..e4c349f46 --- /dev/null +++ b/scripts/spdkcli/ui_root.py @@ -0,0 +1,96 @@ +from .ui_node import UINode, UIBdevs, UILvolStores +import rpc.client +import rpc +from argparse import Namespace as an + + +class UIRoot(UINode): + """ + Root node for CLI menu tree structure. Refreshes running config on startup. + """ + def __init__(self, s, shell): + UINode.__init__(self, "/", shell=shell) + self.current_bdevs = [] + self.current_lvol_stores = [] + self.set_rpc_target(s) + + def refresh(self): + self._children = set([]) + UIBdevs(self) + UILvolStores(self) + + def set_rpc_target(self, s): + self.client = rpc.client.JSONRPCClient(s) + + def print_array(self, a): + return " ".join(a) + + def get_bdevs(self, bdev_type): + self.current_bdevs = rpc.bdev.get_bdevs(self.client, an(name="")) + # Following replace needs to be done in order for some of the bdev + # listings to work. + # For example logical volumes: listing in menu is "Logical_Volume" + # (cannot have space), but the product name in SPDK is "Logical Volume" + bdev_type = bdev_type.replace("_", " ") + for bdev in filter(lambda x: bdev_type in x["product_name"], + self.current_bdevs): + test = Bdev(bdev) + yield test + + def delete_bdev(self, name): + rpc.bdev.delete_bdev(self.client, an(bdev_name=name)) + + def create_malloc_bdev(self, **kwargs): + response = rpc.bdev.construct_malloc_bdev(self.client, an(**kwargs)) + return self.print_array(response) + + def create_aio_bdev(self, **kwargs): + response = rpc.bdev.construct_aio_bdev(self.client, an(**kwargs)) + return self.print_array(response) + + def create_lvol_bdev(self, **kwargs): + response = rpc.lvol.construct_lvol_bdev(self.client, **kwargs) + return self.print_array(response) + + def create_nvme_bdev(self, **kwargs): + response = rpc.bdev.construct_nvme_bdev(self.client, an(**kwargs)) + return self.print_array(response) + + def get_lvol_stores(self): + self.current_lvol_stores = rpc.lvol.get_lvol_stores(self.client) + for lvs in self.current_lvol_stores: + yield LvolStore(lvs) + + def create_lvol_store(self, **kwargs): + response = rpc.lvol.construct_lvol_store(self.client, **kwargs) + new_lvs = rpc.lvol.get_lvol_stores(self.client, + self.print_array(response), + lvs_name=None) + return new_lvs[0]["name"] + + def delete_lvol_store(self, **kwargs): + rpc.lvol.destroy_lvol_store(self.client, **kwargs) + + +class Bdev(object): + def __init__(self, bdev_info): + """ + All class attributes are set based on what information is received + from get_bdevs RPC call. + # TODO: Document in docstring parameters which describe bdevs. + # TODO: Possible improvement: JSON schema might be used here in future + """ + for i in bdev_info.keys(): + setattr(self, i, bdev_info[i]) + + +class LvolStore(object): + def __init__(self, lvs_info): + """ + All class attributes are set based on what information is received + from get_bdevs RPC call. + # TODO: Document in docstring parameters which describe bdevs. + # TODO: Possible improvement: JSON schema might be used here in future + """ + for i in lvs_info.keys(): + setattr(self, i, lvs_info[i])