diff --git a/io-engine/src/bin/io-engine-client/main.rs b/io-engine/src/bin/io-engine-client/main.rs index 10eae3ef70..251fad384b 100644 --- a/io-engine/src/bin/io-engine-client/main.rs +++ b/io-engine/src/bin/io-engine-client/main.rs @@ -1,5 +1,6 @@ use byte_unit::Byte; use snafu::{Backtrace, Snafu}; +use strum::ParseError; use tonic::transport::Channel; use io_engine_api::v0::{ @@ -23,6 +24,11 @@ pub enum ClientError { source: tonic::Status, backtrace: Backtrace, }, + #[snafu(display("gRPC status: {}", source))] + GrpcParseStatus { + source: ParseError, + backtrace: Backtrace, + }, #[snafu(display("Context building error: {}", source))] ContextCreate { source: context::Error, diff --git a/io-engine/src/bin/io-engine-client/v1/replica_cli.rs b/io-engine/src/bin/io-engine-client/v1/replica_cli.rs index 815d24fe77..bcdd4dd0df 100644 --- a/io-engine/src/bin/io-engine-client/v1/replica_cli.rs +++ b/io-engine/src/bin/io-engine-client/v1/replica_cli.rs @@ -3,6 +3,7 @@ use crate::{ context::{Context, OutputFormat}, parse_size, ClientError, + GrpcParseStatus, GrpcStatus, }; use byte_unit::Byte; @@ -345,11 +346,11 @@ async fn replica_list( matches: &ArgMatches, ) -> crate::Result<()> { let pooltype = matches - .get_one::("type") + .get_many::("type") + .unwrap_or_default() .map(|s| pool_cli::PoolType::from_str(s.as_str())) - .transpose() - .map_err(|e| Status::invalid_argument(e.to_string())) - .context(GrpcStatus)?; + .collect::, _>>() + .context(GrpcParseStatus)?; let pooltypes = pooltype .into_iter() .map(|t| v1_rpc::pool::PoolType::from(t) as i32) diff --git a/io-engine/src/grpc/v1/replica.rs b/io-engine/src/grpc/v1/replica.rs index 58bc35044b..97ac93298d 100644 --- a/io-engine/src/grpc/v1/replica.rs +++ b/io-engine/src/grpc/v1/replica.rs @@ -254,7 +254,9 @@ impl ReplicaRpc for ReplicaService { let mut replicas = vec![]; - if args.pooltypes.iter().any(|t| *t == PoolType::Lvs as i32) { + if args.pooltypes.is_empty() + || args.pooltypes.iter().any(|t| *t == PoolType::Lvs as i32) + { replicas.extend(Self::list_lvs_replicas().await?); } if args.pooltypes.iter().any(|t| *t == PoolType::Lvm as i32) { diff --git a/scripts/pytest-tests.sh b/scripts/pytest-tests.sh index f07a5b8b5b..00fdd48394 100755 --- a/scripts/pytest-tests.sh +++ b/scripts/pytest-tests.sh @@ -47,7 +47,7 @@ function run_tests() ( set -x report=$(echo "${name}-xunit-report.xml" | tr '/' '-') - python -m pytest --tc-file='test_config.ini' --docker-compose="$name" "$name" --junit-xml="$ROOTDIR/$report" + python -m pytest --tc-file='test_config.ini' --docker-compose="$name" "$name" --junit-xml="$ROOTDIR/$report" $TEST_ARGS ) elif [ -f "$name" ] || [ -f "${name%::*}" ] then @@ -56,7 +56,7 @@ function run_tests() base=$(dirname "$name") ( cd "$base"; docker-compose down 2>/dev/null || true ) report=$(echo "$base/${name%.py}-xunit-report.xml" | tr '/' '-') - python -m pytest --tc-file='test_config.ini' --docker-compose="$base" "$name" --junit-xml="$ROOTDIR/$report" + python -m pytest --tc-file='test_config.ini' --docker-compose="$base" "$name" --junit-xml="$ROOTDIR/$report" $TEST_ARGS ) fi done @@ -71,6 +71,7 @@ fi pushd "$SRCDIR/test/python" >/dev/null && source ./venv/bin/activate && popd >/dev/null TEST_LIST= +TEST_ARGS= while [ "$#" -gt 0 ]; do case "$1" in --clean-all) @@ -87,6 +88,8 @@ while [ "$#" -gt 0 ]; do param="$1" if [ -d "$real_1" ] || [ -f "$real_1" ] || [ -f "${real_1%::*}" ]; then param="$real_1" + else + TEST_ARGS="${TEST_ARGS:-}$1" fi TEST_LIST="$TEST_LIST \n$param" ;; diff --git a/test/python/v1/pool/docker-compose.yml b/test/python/v1/pool/docker-compose.yml index 97c5692180..f5397508d4 100644 --- a/test/python/v1/pool/docker-compose.yml +++ b/test/python/v1/pool/docker-compose.yml @@ -13,7 +13,7 @@ services: - NEXUS_NVMF_RESV_ENABLE=1 - PATH=${LLVM_SYMBOLIZER_DIR:-}:${LVM_BINS:-} - ASAN_OPTIONS=detect_leaks=0 - - LVM_ENABLE=1 + - LVM=true command: ${SRCDIR}/${IO_ENGINE_DIR}/io-engine -g 0.0.0.0 -l 1,2 -r /tmp/ms0.sock networks: mayastor_net: diff --git a/test/python/v1/pool/test_bdd_lvm.py b/test/python/v1/pool/test_bdd_lvm.py index 5b86f2d3cb..3a7c8e0fdc 100644 --- a/test/python/v1/pool/test_bdd_lvm.py +++ b/test/python/v1/pool/test_bdd_lvm.py @@ -104,7 +104,7 @@ def create(name, disks, pooltype): def find_pool(get_mayastor_instance): def find(name): for pool in get_mayastor_instance.pool_rpc.ListPools( - pb.ListPoolOptions() + pb.ListPoolOptions() ).pools: if pool.name == name: return pool @@ -120,14 +120,14 @@ def get_lvm_feature(get_mayastor_instance, get_mayastor_info): @then("the instance shall report if it supports the LVM feature") def the_instance_shall_report_if_it_supports_the_lvm_feature( - get_mayastor_instance, get_mayastor_info, get_lvm_feature + get_mayastor_instance, get_mayastor_info, get_lvm_feature ): assert get_lvm_feature @when("the user creates a pool specifying a URI representing an loop disk") def the_user_creates_a_pool_specifying_a_uri_representing_an_loop_disk( - get_mayastor_instance, volgrp_with_losetup_disk, create_pool + get_mayastor_instance, volgrp_with_losetup_disk, create_pool ): create_pool(f"{volgrp_with_losetup_disk}", [], pb.Lvm) @@ -139,7 +139,9 @@ def the_lvm_pool_should_be_created(find_pool): @when("the user destroys a pool specifying type as lvm") def the_user_destroys_a_pool_specifying_type_as_lvm(get_mayastor_instance): - get_mayastor_instance.pool_rpc.DestroyPool(pb.DestroyPoolRequest(name="lvmpool", pooltype=pb.Lvm)) + get_mayastor_instance.pool_rpc.DestroyPool( + pb.DestroyPoolRequest(name="lvmpool", pooltype=pb.Lvm) + ) @then("the lvm pool should be removed") diff --git a/test/python/v1/pool/test_bdd_pool.py b/test/python/v1/pool/test_bdd_pool.py index 16c62764cf..2fb9b773b7 100644 --- a/test/python/v1/pool/test_bdd_pool.py +++ b/test/python/v1/pool/test_bdd_pool.py @@ -298,7 +298,9 @@ def pool_creation_should_fail(find_pool): @then("the pool create command should fail") def the_pool_create_command_should_fail(create_pool_that_already_exists): - assert create_pool_that_already_exists.value.code() == grpc.StatusCode.ALREADY_EXISTS + assert ( + create_pool_that_already_exists.value.code() == grpc.StatusCode.ALREADY_EXISTS + ) @then("the pool destroy command should fail") diff --git a/test/python/v1/replica/docker-compose.yml b/test/python/v1/replica/docker-compose.yml index 7fe863759d..99cad247b1 100644 --- a/test/python/v1/replica/docker-compose.yml +++ b/test/python/v1/replica/docker-compose.yml @@ -11,9 +11,11 @@ services: - MY_POD_IP=10.0.0.2 - NEXUS_NVMF_ANA_ENABLE=1 - NEXUS_NVMF_RESV_ENABLE=1 - - PATH=${LLVM_SYMBOLIZER_DIR:-} + - PATH=${LLVM_SYMBOLIZER_DIR:-}:${LVM_BINS:-} - ASAN_OPTIONS=detect_leaks=0 - command: ${SRCDIR}/${IO_ENGINE_DIR}/io-engine -g 0.0.0.0 -l 1 -r /tmp/ms0.sock + - LVM=true + - RUST_LOG=debug + command: ${SRCDIR}/${IO_ENGINE_DIR}/io-engine -g 0.0.0.0 -l 1 -r /tmp/ms0.sock --reactor-freeze-detection networks: mayastor_net: ipv4_address: 10.0.0.2 @@ -32,6 +34,14 @@ services: - /dev/hugepages:/dev/hugepages - /tmp:/tmp - /var/tmp:/var/tmp + - /dev:/dev + - /run/udev:/run/udev + privileged: true + devices: + - /dev/loop0 + - /dev/loop1 + - /dev/loop2 + ipc: "host" networks: mayastor_net: name: mayastor_net diff --git a/test/python/v1/replica/features/lvm_replica.feature b/test/python/v1/replica/features/lvm_replica.feature new file mode 100644 index 0000000000..d8e91d87fa --- /dev/null +++ b/test/python/v1/replica/features/lvm_replica.feature @@ -0,0 +1,20 @@ +Feature: LVM replica support + + Background: + Given a mayastor instance "ms0" + And an LVM VG backed pool called "lvmpool" + + Scenario: Creating an lvm volume on an imported lvm volume group + When a user calls the createreplica on pool "lvmpool" + Then an lv should be created on the lvmpool + + Scenario: Destroying a replica backed by lvm pool + Given an LVM backed replica + When a user calls destroy replica + Then the replica gets destroyed + + Scenario: Listing replicas from either an LVS or LVM pool + Given an LVS pool with a replica + And an LVM backed replica + When a user calls list replicas + Then all replicas should be listed diff --git a/test/python/v1/replica/test_bdd_lvm_replica.py b/test/python/v1/replica/test_bdd_lvm_replica.py new file mode 100644 index 0000000000..bc519079cf --- /dev/null +++ b/test/python/v1/replica/test_bdd_lvm_replica.py @@ -0,0 +1,211 @@ +"""LVM replica support feature tests.""" + +import pytest +from pytest_bdd import ( + given, + scenario, + then, + when, + parsers, +) +from common.command import run_cmd +from v1.mayastor import mayastor_mod, container_mod +import grpc +import pool_pb2 as pool_pb +import replica_pb2 as pb +import common_pb2 as common_pb +import subprocess + +LVS_LV_UUID = "5b3d904f-d695-4a28-b3d6-b9fc1cbb39a3" +LVM_LV_UUID = "22ca10d3-4f2b-4b95-9814-9181c025cc1a" +REPLICA_SIZE = 32 * 1024 * 1024 + + +@scenario( + "features/lvm_replica.feature", + "Creating an lvm volume on an imported lvm volume group", +) +def test_creating_an_lvm_volume_on_an_imported_lvm_volume_group(): + """Creating an lvm volume on an imported lvm volume group.""" + + +@scenario("features/lvm_replica.feature", "Destroying a replica backed by lvm pool") +def test_destroying_a_replica_backed_by_lvm_pool(): + """Destroying a replica backed by lvm pool""" + + +@scenario( + "features/lvm_replica.feature", "Listing replicas from either an LVS or LVM pool" +) +def test_listing_replicas_from_either_an_lvs_or_lvm_pool(): + """Listing replicas from either an LVS or LVM pool""" + + +@pytest.fixture +def create_replica(get_mayastor_instance): + def create(uuid, pool, size, share, pooltype): + get_mayastor_instance.replica_rpc.CreateReplica( + pb.CreateReplicaRequest( + name=uuid, + uuid=uuid, + pooluuid=pool, + size=size, + share=share, + pooltype=pooltype, + ) + ) + + yield create + + +@pytest.fixture +def find_replica(get_mayastor_instance): + def find(uuid, pooltype): + for replica in get_mayastor_instance.replica_rpc.ListReplicas( + pb.ListReplicaOptions(pooltypes=[pooltype]) + ).replicas: + if replica.uuid == uuid: + return replica + return None + + yield find + + +@pytest.fixture +def create_pool(get_mayastor_instance): + def create(name, disks, pooltype): + get_mayastor_instance.pool_rpc.CreatePool( + pool_pb.CreatePoolRequest(name=name, disks=disks, pooltype=pooltype) + ) + + yield create + + +@pytest.fixture +def volgrp_with_losetup_disk(container_mod): + pool_name = "lvmpool" + p = subprocess.run(f"sudo vgs {pool_name}", shell=True, check=False) + file = "/tmp/ms0-disk0.img" + # if volume group already exists then don't create it again + if p.returncode != 0: + run_cmd(f"rm -f '{file}'", True) + run_cmd(f"truncate -s 128M '{file}'", True) + out = subprocess.run( + f"sudo -E losetup -f '{file}' --show", + shell=True, + check=True, + capture_output=True, + ) + disk = out.stdout.decode("ascii").strip("\n") + run_cmd(f"sudo -E pvcreate '{disk}'", True) + run_cmd(f"sudo -E vgcreate '{pool_name}' '{disk}'", True) + out = subprocess.run( + f"sudo -E pvs -opv_name --select=vg_name={pool_name} --noheadings", + shell=True, + check=True, + capture_output=True, + ) + disk = out.stdout.decode("ascii").strip("\n").lstrip() + pytest.disk = disk + out = subprocess.run( + f"sudo -E vgs lvmpool -ovg_uuid --noheadings", + shell=True, + check=True, + capture_output=True, + ) + pytest.vg_uuid = out.stdout.decode("ascii").strip("\n").lstrip() + yield pool_name + run_cmd(f"sudo -E vgremove -y {pool_name}", True) + if p.returncode != 0: + run_cmd(f"sudo -E losetup -d {disk}", True) + run_cmd(f"rm -f '{file}'", True) + + +@given( + parsers.parse('a mayastor instance "{name}"'), + target_fixture="get_mayastor_instance", +) +def get_mayastor_instance(mayastor_mod, name): + return mayastor_mod[f"{name}"] + + +@given( + parsers.parse('an LVM VG backed pool called "{pool_name}"'), + target_fixture="create_pool_on_vol_group", +) +def create_pool_on_vol_group(volgrp_with_losetup_disk, create_pool): + create_pool(f"{volgrp_with_losetup_disk}", [pytest.disk], pool_pb.Lvm) + + +@given("an LVS pool with a replica") +def an_lvs_pool_with_a_replica(create_pool, create_replica): + create_pool("lvspool", ["malloc:///disk0?size_mb=64"], pool_pb.Lvs) + create_replica( + LVS_LV_UUID, + "lvspool", + REPLICA_SIZE, + share_protocol("none"), + pool_pb.Lvs, + ) + + +@when( + parsers.parse('a user calls the createreplica on pool "{pool_name}"'), + target_fixture="a_user_calls_the_create_replica", +) +@given("an LVM backed replica") +def a_user_calls_the_create_replica(get_mayastor_instance, create_replica): + create_replica( + LVM_LV_UUID, pytest.vg_uuid, REPLICA_SIZE, share_protocol("none"), pool_pb.Lvm + ) + yield + try: + get_mayastor_instance.replica_rpc.DestroyReplica( + pb.DestroyReplicaRequest(uuid=LVM_LV_UUID, pooltype=pool_pb.Lvm) + ) + except grpc.RpcError as rpc_error: + if rpc_error.code() == grpc.StatusCode.NOT_FOUND: + pass + + +@then("an lv should be created on the lvmpool") +def an_lv_should_be_created_on_the_lvmpool(find_replica): + assert find_replica(LVM_LV_UUID, pool_pb.Lvm) is not None + + +@when("a user calls destroy replica") +def a_user_calls_destroy_replica(get_mayastor_instance): + get_mayastor_instance.replica_rpc.DestroyReplica( + pb.DestroyReplicaRequest(uuid=LVM_LV_UUID, pooltype=pool_pb.Lvm) + ) + + +@then("the replica gets destroyed") +def the_replica_gets_destroyed(find_replica): + assert find_replica(LVM_LV_UUID, pool_pb.Lvm) is None + + +@when("a user calls list replicas", target_fixture="list_replicas") +def list_replicas(get_mayastor_instance): + return get_mayastor_instance.replica_rpc.ListReplicas( + pb.ListReplicaOptions(pooltypes=[pool_pb.Lvm, pool_pb.Lvs]) + ).replicas + + +@then("all replicas should be listed") +def all_replicas_should_be_listed(list_replicas): + for replica in list_replicas: + assert replica.size == REPLICA_SIZE + if replica.uuid == LVM_LV_UUID: + assert replica.pooltype == pool_pb.Lvm + if replica.uuid == LVS_LV_UUID: + assert replica.pooltype == pool_pb.Lvs + + +def share_protocol(name): + PROTOCOLS = { + "none": common_pb.NONE, + "nvmf": common_pb.NVMF, + "iscsi": common_pb.ISCSI, + } + return PROTOCOLS[name]