Add some CLI tools
pyeclib-backend version Print the versions of pyeclib, liberasurecode, and Python pyeclib-backend list List the status of backends pyeclib-backend check Check the status of a particular backend pyeclib-backend verify Verify the ability to decode given some unavailable fragments pyeclib-backend bench Benchmarks available backends Co-Authored-By: Matthew Oliver <matt@oliver.net.au> Signed-off-by: Tim Burke <tim.burke@gmail.com> Change-Id: Ibe47b4665bfe763c07e68d8be0f92983bc15dff0
This commit is contained in:
56
doc/source/cli.rst
Normal file
56
doc/source/cli.rst
Normal file
@@ -0,0 +1,56 @@
|
||||
PyECLib CLI
|
||||
===========
|
||||
|
||||
PyECLib provides a ``pyeclib-backend`` tool to provide various information about backends.
|
||||
|
||||
``version`` subcommand
|
||||
----------------------
|
||||
.. code:: text
|
||||
|
||||
pyeclib-backend [-V | version]
|
||||
|
||||
Displays the versions of pyeclib, liberasurecode, and python.
|
||||
|
||||
``list`` subcommand
|
||||
-------------------
|
||||
.. code:: text
|
||||
|
||||
pyeclib-backend list [-a | --available] [<ec_type>]
|
||||
|
||||
Displays the status (available, missing, or unknwon) of requested backends.
|
||||
By default, all backends are displayed; if ``--available`` is provided, only
|
||||
available backends are displayed, and status is not displayed.
|
||||
|
||||
``check`` subcommand
|
||||
--------------------
|
||||
.. code:: text
|
||||
|
||||
pyeclib-backend check [-q | --quiet] <ec_type>
|
||||
|
||||
Check whether a backend is available. Exits
|
||||
|
||||
- 0 if ``ec_type`` is available,
|
||||
- 1 if ``ec_type`` is missing, or
|
||||
- 2 if ``ec_type`` is not recognized
|
||||
|
||||
If ``--quiet`` is provided, output nothing; rely only on exit codes.
|
||||
|
||||
``verify`` subcommand
|
||||
---------------------
|
||||
.. code:: text
|
||||
|
||||
pyeclib-backend verify [-q | --quiet] [--ec-type=all]
|
||||
[--n-data=10] [--n-parity=5] [--unavailable=2] [--segment-size=1024]
|
||||
|
||||
Verify the ability to decode all combinations of fragments given some number
|
||||
of unavailable fragments.
|
||||
|
||||
``bench`` subcommand
|
||||
--------------------
|
||||
.. code:: text
|
||||
|
||||
pyeclib-backend bench [-e | --encode] [-d | --decode] [--ec-type=all]
|
||||
[--n-data=10] [--n-parity=5] [--unavailable=2] [--segment-size=1048576]
|
||||
[--iterations=200]
|
||||
|
||||
Benchmark one or more backends.
|
@@ -6,6 +6,8 @@ Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
cli
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
92
pyeclib/cli/__init__.py
Normal file
92
pyeclib/cli/__init__.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# Copyright (c) 2025, NVIDIA
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution. THIS SOFTWARE IS
|
||||
# PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
|
||||
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
||||
# NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from pyeclib import ec_iface
|
||||
|
||||
|
||||
_type_abbreviations = {
|
||||
# name -> prefix
|
||||
"all": "",
|
||||
"isa-l": "isa_l_",
|
||||
"isa_l": "isa_l_",
|
||||
"isal": "isa_l_",
|
||||
"jerasure": "jerasure_",
|
||||
"flat-xor": "flat_xor_",
|
||||
"flat_xor": "flat_xor_",
|
||||
"flatxor": "flat_xor_",
|
||||
"xor": "flat_xor_",
|
||||
}
|
||||
|
||||
|
||||
def expand_ec_types(user_types):
|
||||
result = set(user_types or ["all"])
|
||||
for abbrev, prefix in _type_abbreviations.items():
|
||||
if abbrev in result:
|
||||
result.remove(abbrev)
|
||||
result.update(
|
||||
ec_type
|
||||
for ec_type in ec_iface.ALL_EC_TYPES
|
||||
if ec_type.startswith(prefix)
|
||||
)
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def add_instance_args(parser, default_segment_size=1024):
|
||||
"""
|
||||
Add arguments to ``parser`` for instance instantiation.
|
||||
"""
|
||||
parser.add_argument(
|
||||
"--ec-type",
|
||||
action="append",
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-data",
|
||||
"--ndata",
|
||||
"-k",
|
||||
metavar="K",
|
||||
type=int,
|
||||
default=10,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-parity",
|
||||
"--nparity",
|
||||
"-m",
|
||||
metavar="M",
|
||||
type=int,
|
||||
default=5,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--unavailable",
|
||||
"-u",
|
||||
metavar="N",
|
||||
type=int,
|
||||
default=2,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--segment-size",
|
||||
"-s",
|
||||
metavar="BYTES",
|
||||
type=int,
|
||||
default=default_segment_size,
|
||||
)
|
83
pyeclib/cli/__main__.py
Normal file
83
pyeclib/cli/__main__.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Copyright (c) 2025, NVIDIA
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution. THIS SOFTWARE IS
|
||||
# PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
|
||||
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
||||
# NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from pyeclib.cli import bench
|
||||
from pyeclib.cli import check
|
||||
from pyeclib.cli import list as list_cli
|
||||
from pyeclib.cli import verify
|
||||
from pyeclib.cli import version
|
||||
|
||||
|
||||
def main(args=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="pyeclib-backend",
|
||||
description="tool to get various erasure coding information",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-V",
|
||||
"--version",
|
||||
action="store_const",
|
||||
dest="func",
|
||||
const=version.version_command,
|
||||
help=version.version_description,
|
||||
)
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
version_parser = subparsers.add_parser(
|
||||
"version", help=version.version_description)
|
||||
version_parser.set_defaults(func=version.version_command)
|
||||
|
||||
list_parser = subparsers.add_parser(
|
||||
"list", help=list_cli.list_description)
|
||||
list_parser.set_defaults(func=list_cli.list_command)
|
||||
list_cli.add_list_args(list_parser)
|
||||
|
||||
check_parser = subparsers.add_parser(
|
||||
"check", help=check.check_description)
|
||||
check_parser.set_defaults(func=check.check_command)
|
||||
check.add_check_args(check_parser)
|
||||
|
||||
verify_parser = subparsers.add_parser(
|
||||
"verify", help=verify.verify_description)
|
||||
verify_parser.set_defaults(func=verify.verify_command)
|
||||
verify.add_verify_args(verify_parser)
|
||||
|
||||
bench_parser = subparsers.add_parser(
|
||||
"bench", help=bench.bench_description)
|
||||
bench_parser.set_defaults(func=bench.bench_command)
|
||||
bench.add_bench_args(bench_parser)
|
||||
|
||||
parsed_args = parser.parse_args(args)
|
||||
if parsed_args.func is None:
|
||||
parser.error(
|
||||
f"the following arguments are required: "
|
||||
f"{{{','.join(subparsers.choices)}}}"
|
||||
)
|
||||
sys.exit(parsed_args.func(parsed_args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
101
pyeclib/cli/bench.py
Normal file
101
pyeclib/cli/bench.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# Copyright (c) 2025, NVIDIA
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution. THIS SOFTWARE IS
|
||||
# PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
|
||||
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
||||
# NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
|
||||
from pyeclib import cli
|
||||
from pyeclib import ec_iface
|
||||
|
||||
|
||||
def add_bench_args(parser):
|
||||
parser.add_argument("-e", "--encode", action="store_true")
|
||||
parser.add_argument("-d", "--decode", action="store_true")
|
||||
cli.add_instance_args(parser, default_segment_size=2**20)
|
||||
parser.add_argument("--iterations", "-i", type=int, default=200)
|
||||
|
||||
|
||||
def bench_command(args):
|
||||
args.ec_type = cli.expand_ec_types(args.ec_type)
|
||||
data = os.urandom(args.segment_size + args.iterations)
|
||||
width = max(len(ec_type) for ec_type in args.ec_type)
|
||||
print(f"Using {args.n_data} data + {args.n_parity} parity with "
|
||||
f"{args.unavailable} unavailable frags")
|
||||
|
||||
for ec_type in args.ec_type:
|
||||
if ec_type not in ec_iface.ALL_EC_TYPES:
|
||||
print(f"{ec_type:<{width}} unknown")
|
||||
continue
|
||||
if ec_type not in ec_iface.VALID_EC_TYPES:
|
||||
print(f"{ec_type:<{width}} not available")
|
||||
continue
|
||||
try:
|
||||
instance = ec_iface.ECDriver(
|
||||
ec_type=ec_type,
|
||||
k=args.n_data,
|
||||
m=args.n_parity,
|
||||
)
|
||||
except ec_iface.ECDriverError:
|
||||
print(f"{ec_type:<{width}} could not be instantiated")
|
||||
continue
|
||||
frags = instance.encode(data[:args.segment_size])
|
||||
|
||||
if args.encode or not args.decode:
|
||||
start = time.time()
|
||||
for i in range(args.iterations):
|
||||
_ = instance.encode(data[i:i + args.segment_size])
|
||||
dt = time.time() - start
|
||||
mb_encoded = args.iterations * args.segment_size / (2 ** 20)
|
||||
print(f'{ec_type} (encode): {mb_encoded / dt:.1f}MB/s')
|
||||
|
||||
if args.decode or not args.encode:
|
||||
start = time.time()
|
||||
for i in range(args.iterations):
|
||||
data_frags = random.sample(
|
||||
frags[:args.n_data],
|
||||
args.n_data - args.unavailable,
|
||||
)
|
||||
if ec_type.startswith('flat_xor'):
|
||||
# The math is actually more complicated than this, but ...
|
||||
parity_frags = frags[args.n_data:]
|
||||
else:
|
||||
parity_frags = random.sample(
|
||||
frags[args.n_data:],
|
||||
args.unavailable,
|
||||
)
|
||||
_ = instance.decode(data_frags + parity_frags)
|
||||
dt = time.time() - start
|
||||
mb_encoded = args.iterations * args.segment_size / (2 ** 20)
|
||||
print(f'{ec_type} (decode): {mb_encoded / dt:.1f}MB/s')
|
||||
|
||||
|
||||
bench_description = "benchmark EC schemas"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=bench_description)
|
||||
add_bench_args(parser)
|
||||
args = parser.parse_args()
|
||||
bench_command(args)
|
58
pyeclib/cli/check.py
Normal file
58
pyeclib/cli/check.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Copyright (c) 2025, NVIDIA
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution. THIS SOFTWARE IS
|
||||
# PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
|
||||
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
||||
# NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from pyeclib import ec_iface
|
||||
|
||||
|
||||
def add_check_args(parser):
|
||||
parser.add_argument("-q", "--quiet", action="store_true")
|
||||
parser.add_argument("ec_type")
|
||||
|
||||
|
||||
def check_command(args):
|
||||
if args.ec_type in ec_iface.VALID_EC_TYPES:
|
||||
if not args.quiet:
|
||||
print(args.ec_type, 'is available')
|
||||
return 0
|
||||
|
||||
if args.ec_type in ec_iface.ALL_EC_TYPES:
|
||||
if not args.quiet:
|
||||
print(args.ec_type, 'is missing')
|
||||
return 1
|
||||
|
||||
if not args.quiet:
|
||||
print(args.ec_type, 'is unknown')
|
||||
return 2
|
||||
|
||||
|
||||
check_description = "check for backend availability"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=check_description)
|
||||
add_check_args(parser)
|
||||
args = parser.parse_args()
|
||||
sys.exit(check_command(args))
|
66
pyeclib/cli/list.py
Normal file
66
pyeclib/cli/list.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Copyright (c) 2025, NVIDIA
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution. THIS SOFTWARE IS
|
||||
# PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
|
||||
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
||||
# NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from pyeclib import cli
|
||||
from pyeclib import ec_iface
|
||||
|
||||
|
||||
def add_list_args(parser):
|
||||
parser.add_argument("-a", "--available", action="store_true",
|
||||
help="display only available backends")
|
||||
parser.add_argument("ec_type", nargs="*", type=str,
|
||||
help="display these backends (default: all)")
|
||||
|
||||
|
||||
def list_command(args):
|
||||
args.ec_type = cli.expand_ec_types(args.ec_type)
|
||||
|
||||
width = max(len(backend) for backend in args.ec_type)
|
||||
found = 0
|
||||
for backend in args.ec_type:
|
||||
if args.available:
|
||||
if backend in ec_iface.VALID_EC_TYPES:
|
||||
print(backend)
|
||||
found += 1
|
||||
else:
|
||||
if backend not in ec_iface.ALL_EC_TYPES:
|
||||
print(f"{backend:<{width}} unknown")
|
||||
elif backend in ec_iface.VALID_EC_TYPES:
|
||||
print(f"{backend:<{width}} available")
|
||||
found += 1
|
||||
else:
|
||||
print(f"{backend:<{width}} missing")
|
||||
return 0 if found else 1
|
||||
|
||||
|
||||
list_description = "list availability of EC backends"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=list_description)
|
||||
add_list_args(parser)
|
||||
args = parser.parse_args()
|
||||
sys.exit(list_command(args))
|
105
pyeclib/cli/verify.py
Normal file
105
pyeclib/cli/verify.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) 2025, NVIDIA
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution. THIS SOFTWARE IS
|
||||
# PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
|
||||
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
||||
# NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pyeclib import cli
|
||||
from pyeclib import ec_iface
|
||||
|
||||
|
||||
def add_verify_args(parser):
|
||||
parser.add_argument("-q", "--quiet", action="store_true")
|
||||
cli.add_instance_args(parser)
|
||||
|
||||
|
||||
def verify_command(args):
|
||||
args.ec_type = cli.expand_ec_types(args.ec_type)
|
||||
total_failures = 0
|
||||
total_corrupt = 0
|
||||
data = os.urandom(args.segment_size)
|
||||
width = max(len(ec_type) for ec_type in args.ec_type)
|
||||
print(f"Using {args.n_data} data + {args.n_parity} parity with "
|
||||
f"{args.unavailable} unavailable frags")
|
||||
|
||||
for ec_type in args.ec_type:
|
||||
if ec_type not in ec_iface.ALL_EC_TYPES:
|
||||
print(f"{ec_type:<{width}} unknown")
|
||||
continue
|
||||
if ec_type not in ec_iface.VALID_EC_TYPES:
|
||||
print(f"{ec_type:<{width}} not available")
|
||||
continue
|
||||
try:
|
||||
instance = ec_iface.ECDriver(
|
||||
ec_type=ec_type,
|
||||
k=args.n_data,
|
||||
m=args.n_parity,
|
||||
)
|
||||
except ec_iface.ECDriverError:
|
||||
print(f"{ec_type:<{width}} could not be instantiated")
|
||||
continue
|
||||
frags = instance.encode(data)
|
||||
combinations, failures, corrupt = check_instance(
|
||||
instance, frags, args.unavailable, data)
|
||||
total_failures += failures
|
||||
total_corrupt += corrupt
|
||||
if corrupt:
|
||||
print(f"\x1b[91;40m{ec_type:<{width}} {combinations=}, "
|
||||
f"{failures=}, {corrupt=}\x1b[0m")
|
||||
elif failures:
|
||||
print(f"\x1b[1;91m{ec_type:<{width}} {combinations=}, "
|
||||
f"{failures=}, {corrupt=}\x1b[0m")
|
||||
else:
|
||||
print(f"{ec_type:<{width}} {combinations=}")
|
||||
|
||||
if total_corrupt:
|
||||
return 3
|
||||
if total_failures:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def check_instance(instance, frags, unavailable, data):
|
||||
combinations = corrupt = failures = 0
|
||||
for to_decode in itertools.combinations(frags, len(frags) - unavailable):
|
||||
combinations += 1
|
||||
try:
|
||||
rebuilt = instance.decode(to_decode)
|
||||
if rebuilt != data:
|
||||
corrupt += 1
|
||||
except ec_iface.ECDriverError:
|
||||
failures += 1
|
||||
# Might want to log?
|
||||
return combinations, failures, corrupt
|
||||
|
||||
|
||||
verify_description = "validate reconstructability of EC schemas"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=verify_description)
|
||||
add_verify_args(parser)
|
||||
args = parser.parse_args()
|
||||
sys.exit(verify_command(args))
|
38
pyeclib/cli/version.py
Normal file
38
pyeclib/cli/version.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Copyright (c) 2025, NVIDIA
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution. THIS SOFTWARE IS
|
||||
# PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
|
||||
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
||||
# NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import platform
|
||||
|
||||
from pyeclib import ec_iface
|
||||
|
||||
version_description = "print pyeclib and liberasurecode versions"
|
||||
|
||||
|
||||
def version_command(args=None):
|
||||
print(f"pyeclib {ec_iface.__version__}")
|
||||
print(f"liberasurecode {ec_iface.LIBERASURECODE_VERSION}")
|
||||
print(f"{platform.python_implementation()} {platform.python_version()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
version_command()
|
@@ -52,6 +52,7 @@ PYECLIB_MINOR = 6
|
||||
PYECLIB_REV = 4
|
||||
PYECLIB_VERSION = PyECLibVersion(PYECLIB_MAJOR, PYECLIB_MINOR,
|
||||
PYECLIB_REV)
|
||||
__version__ = '%d.%d.%d' % (PYECLIB_MAJOR, PYECLIB_MINOR, PYECLIB_REV)
|
||||
|
||||
|
||||
PYECLIB_MAX_DATA = 32
|
||||
|
@@ -28,13 +28,16 @@ classifiers = [
|
||||
"License :: OSI Approved :: BSD License",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
pyeclib-backend = "pyeclib.cli.__main__:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://opendev.org/openstack/pyeclib"
|
||||
"Bug Tracker" = "https://bugs.launchpad.net/pyeclib"
|
||||
"Release Notes" = "https://opendev.org/openstack/pyeclib/src/branch/master/ChangeLog"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["pyeclib"]
|
||||
packages = ["pyeclib", "pyeclib.cli"]
|
||||
platforms = ["Linux"]
|
||||
|
||||
[[tool.setuptools.ext-modules]]
|
||||
|
229
test/test_pyeclib_cli.py
Normal file
229
test/test_pyeclib_cli.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# Copyright (c) 2025, NVIDIA
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution. THIS SOFTWARE IS
|
||||
# PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
|
||||
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
||||
# NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import io
|
||||
import platform
|
||||
import re
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from pyeclib.cli.__main__ import main
|
||||
from pyeclib import ec_iface
|
||||
|
||||
|
||||
class TestVersion(unittest.TestCase):
|
||||
def _test_version(self, args):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(args)
|
||||
self.assertTrue(stdout.getvalue().endswith("\n"))
|
||||
line_parts = [
|
||||
line.split(" ")
|
||||
for line in stdout.getvalue()[:-1].split("\n")
|
||||
]
|
||||
self.assertEqual([prog for prog, vers in line_parts], [
|
||||
'pyeclib',
|
||||
'liberasurecode',
|
||||
platform.python_implementation(),
|
||||
])
|
||||
re_version = re.compile(r"\d+\.\d+\.\d+")
|
||||
self.assertEqual(
|
||||
[bool(re_version.match(vers)) for prog, vers in line_parts],
|
||||
[True] * 3)
|
||||
self.assertEqual(caught.exception.code, None)
|
||||
|
||||
def test_subcommand(self):
|
||||
self._test_version(["version"])
|
||||
|
||||
def test_opt(self):
|
||||
self._test_version(["-V"])
|
||||
|
||||
|
||||
class TestList(unittest.TestCase):
|
||||
def test_list(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["list"])
|
||||
self.assertTrue(stdout.getvalue().endswith("\n"))
|
||||
line_parts = [
|
||||
line.split()
|
||||
for line in stdout.getvalue()[:-1].split("\n")
|
||||
]
|
||||
self.assertEqual(
|
||||
[backend for backend, status in line_parts],
|
||||
sorted(ec_iface.ALL_EC_TYPES))
|
||||
self.assertEqual(
|
||||
{status for backend, status in line_parts},
|
||||
{"available", "missing"})
|
||||
self.assertEqual(caught.exception.code, 0)
|
||||
|
||||
def test_list_one(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["list", "liberasurecode_rs_vand"])
|
||||
self.assertTrue(stdout.getvalue().endswith("\n"))
|
||||
line_parts = [
|
||||
line.split()
|
||||
for line in stdout.getvalue()[:-1].split("\n")
|
||||
]
|
||||
self.assertEqual(line_parts, [["liberasurecode_rs_vand", "available"]])
|
||||
self.assertEqual(caught.exception.code, 0)
|
||||
|
||||
def test_list_one_unknown(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["list", "missing-backend"])
|
||||
self.assertTrue(stdout.getvalue().endswith("\n"))
|
||||
line_parts = [
|
||||
line.split()
|
||||
for line in stdout.getvalue()[:-1].split("\n")
|
||||
]
|
||||
self.assertEqual(line_parts, [["missing-backend", "unknown"]])
|
||||
self.assertEqual(caught.exception.code, 1)
|
||||
|
||||
def test_list_multiple_mixed_status(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["list", "missing-backend", "liberasurecode_rs_vand"])
|
||||
self.assertTrue(stdout.getvalue().endswith("\n"))
|
||||
line_parts = [
|
||||
line.split()
|
||||
for line in stdout.getvalue()[:-1].split("\n")
|
||||
]
|
||||
self.assertEqual(line_parts, [
|
||||
["liberasurecode_rs_vand", "available"],
|
||||
["missing-backend", "unknown"],
|
||||
])
|
||||
self.assertEqual(caught.exception.code, 0)
|
||||
|
||||
def test_list_abbreviation(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["list", "isal"])
|
||||
self.assertTrue(stdout.getvalue().endswith("\n"))
|
||||
line_parts = [
|
||||
line.split()
|
||||
for line in stdout.getvalue()[:-1].split("\n")
|
||||
]
|
||||
self.assertEqual(
|
||||
[backend for backend, status in line_parts],
|
||||
sorted(backend for backend in ec_iface.ALL_EC_TYPES
|
||||
if backend.startswith("isa_l_")))
|
||||
found_status = {status for backend, status in line_parts}
|
||||
self.assertEqual(len(found_status), 1, found_status)
|
||||
found_status = list(found_status)[0]
|
||||
self.assertIn(found_status, {"available", "missing"})
|
||||
self.assertEqual(caught.exception.code,
|
||||
0 if found_status == "available" else 1)
|
||||
|
||||
def test_list_available(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["list", "--available"])
|
||||
self.assertTrue(stdout.getvalue().endswith("\n"))
|
||||
self.assertEqual(
|
||||
stdout.getvalue()[:-1].split("\n"),
|
||||
sorted(ec_iface.VALID_EC_TYPES))
|
||||
self.assertEqual(caught.exception.code, 0)
|
||||
|
||||
def test_list_available_abbreviation(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["list", "--available", "flatxor"])
|
||||
self.assertTrue(stdout.getvalue().endswith("\n"))
|
||||
self.assertEqual(stdout.getvalue()[:-1].split("\n"), [
|
||||
"flat_xor_hd_3",
|
||||
"flat_xor_hd_4",
|
||||
])
|
||||
self.assertEqual(caught.exception.code, 0)
|
||||
|
||||
|
||||
class TestCheck(unittest.TestCase):
|
||||
def test_check_backend_required(self):
|
||||
with mock.patch("sys.stderr", new=io.StringIO()) as stderr, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["check"])
|
||||
self.assertIn("the following arguments are required: ec_type",
|
||||
stderr.getvalue())
|
||||
self.assertEqual(caught.exception.code, 2)
|
||||
|
||||
def test_check_available(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
mock.patch("sys.stderr", new=io.StringIO()) as stderr, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["check", "liberasurecode_rs_vand"])
|
||||
self.assertEqual("liberasurecode_rs_vand is available\n",
|
||||
stdout.getvalue())
|
||||
self.assertEqual("", stderr.getvalue())
|
||||
self.assertEqual(caught.exception.code, 0)
|
||||
|
||||
def test_check_missing(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
mock.patch("sys.stderr", new=io.StringIO()) as stderr, \
|
||||
mock.patch("pyeclib.ec_iface.VALID_EC_TYPES", []), \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["check", "liberasurecode_rs_vand"])
|
||||
self.assertEqual("liberasurecode_rs_vand is missing\n",
|
||||
stdout.getvalue())
|
||||
self.assertEqual("", stderr.getvalue())
|
||||
self.assertEqual(caught.exception.code, 1)
|
||||
|
||||
def test_check_unknown(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
mock.patch("sys.stderr", new=io.StringIO()) as stderr, \
|
||||
mock.patch("pyeclib.ec_iface.VALID_EC_TYPES", []), \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["check", "unknown-backend"])
|
||||
self.assertEqual("unknown-backend is unknown\n",
|
||||
stdout.getvalue())
|
||||
self.assertEqual("", stderr.getvalue())
|
||||
self.assertEqual(caught.exception.code, 2)
|
||||
|
||||
def test_check_quiet_available(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
mock.patch("sys.stderr", new=io.StringIO()) as stderr, \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["check", "-q", "liberasurecode_rs_vand"])
|
||||
self.assertEqual("", stdout.getvalue())
|
||||
self.assertEqual("", stderr.getvalue())
|
||||
self.assertEqual(caught.exception.code, 0)
|
||||
|
||||
def test_check_quiet_missing(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
mock.patch("sys.stderr", new=io.StringIO()) as stderr, \
|
||||
mock.patch("pyeclib.ec_iface.VALID_EC_TYPES", []), \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["check", "--quiet", "liberasurecode_rs_vand"])
|
||||
self.assertEqual("", stdout.getvalue())
|
||||
self.assertEqual("", stderr.getvalue())
|
||||
self.assertEqual(caught.exception.code, 1)
|
||||
|
||||
def test_check_quiet_unknown(self):
|
||||
with mock.patch("sys.stdout", new=io.StringIO()) as stdout, \
|
||||
mock.patch("sys.stderr", new=io.StringIO()) as stderr, \
|
||||
mock.patch("pyeclib.ec_iface.VALID_EC_TYPES", []), \
|
||||
self.assertRaises(SystemExit) as caught:
|
||||
main(["check", "-q", "unknown-backend"])
|
||||
self.assertEqual("", stdout.getvalue())
|
||||
self.assertEqual("", stderr.getvalue())
|
||||
self.assertEqual(caught.exception.code, 2)
|
Reference in New Issue
Block a user