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 <karol.latecki@intel.com>
Reviewed-on: https://review.gerrithub.io/405039
Tested-by: SPDK Automated Test System <sys_sgsw@intel.com>
Reviewed-by: Jim Harris <james.r.harris@intel.com>
Reviewed-by: Ben Walker <benjamin.walker@intel.com>
Reviewed-by: Tomasz Zawadzki <tomasz.zawadzki@intel.com>
This commit is contained in:
Karol Latecki 2018-03-23 11:40:31 +01:00 committed by Daniel Verkamp
parent 4ecb2e1d33
commit 83795a1600
8 changed files with 502 additions and 0 deletions

View File

@ -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 \

View File

@ -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)

61
doc/spdkcli.md Normal file
View File

@ -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.

View File

@ -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

38
scripts/spdkcli.py Executable file
View File

@ -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()

View File

@ -0,0 +1 @@
from .ui_root import UIRoot

297
scripts/spdkcli/ui_node.py Normal file
View File

@ -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

View File

@ -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])