diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index a23f7b89..009f6182 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -91,7 +91,7 @@ jobs: strategy: fail-fast: false matrix: - test: ["cli", "cli_change_lb", "state", "multi_gateway", "server", "grpc", "omap_lock", "old_omap", "log_files", "nsid"] + test: ["cli", "cli_change_lb", "state", "multi_gateway", "server", "grpc", "omap_lock", "old_omap", "log_files", "nsid", "psk"] runs-on: ubuntu-latest env: HUGEPAGES: 512 # for multi gateway test, approx 256 per gateway instance @@ -350,6 +350,178 @@ jobs: make down make clean + demo-secure: + needs: [build, build-ceph] + runs-on: ubuntu-latest + env: + HUGEPAGES: 512 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup huge-pages + run: make setup HUGEPAGES=$HUGEPAGES + + - name: Download container images + uses: actions/download-artifact@v4 + with: + pattern: container_images* + merge-multiple: true + + - name: Load container images + run: | + docker load < nvmeof.tar + docker load < nvmeof-cli.tar + docker load < ceph.tar + docker load < bdevperf.tar + + - name: Start containers + timeout-minutes: 3 + run: | + if ! docker-compose --version 2>&1 > /dev/null ; then + sudo apt update + sudo apt install -y docker-compose + fi + docker-compose --version + make up + + - name: Wait for the Gateway to be listening + timeout-minutes: 3 + run: | + . .env + + echo using gateway $NVMEOF_IP_ADDRESS port $NVMEOF_GW_PORT + until nc -z $NVMEOF_IP_ADDRESS $NVMEOF_GW_PORT; do + echo -n . + sleep ${{ env.WAIT_INTERVAL_SECS }} + done + + - name: List containers + if: success() || failure() + run: make ps + + - name: List processes + if: success() || failure() + run: make top + + - name: Test + run: | + . .env + port2=`expr ${NVMEOF_IO_PORT} + 10` + make demosecure OPTS=-T NVMEOF_CONTAINER_NAME="ceph-nvmeof_nvmeof_1" HOSTNQN="${NQN}host" HOSTNQN2="${NQN}host2" NVMEOF_IO_PORT2=${port2} + + - name: List resources + run: | + # https://github.com/actions/toolkit/issues/766 + shopt -s expand_aliases + eval $(make alias) + cephnvmf get_subsystems + cephnvmf subsystem list + subs=$(cephnvmf --output stdio --format json subsystem list | grep nqn | sed 's/"nqn": "//' | sed 's/",$//') + for sub in $subs + do + cephnvmf namespace list --subsystem $sub + cephnvmf listener list --subsystem $sub + cephnvmf host list --subsystem $sub + done + + - name: Run bdevperf + run: | + # see https://spdk.io/doc/nvmf_multipath_howto.html + shopt -s expand_aliases + eval $(make alias) + . .env + set -x + echo -n "ℹ️ Starting bdevperf container" + docker-compose up -d bdevperf + sleep 10 + echo "ℹ️ bdevperf start up logs" + make logs SVC=bdevperf + eval $(make run SVC=bdevperf OPTS="--entrypoint=env" | grep BDEVPERF_SOCKET | tr -d '\n\r' ) + psk_path_prefix="/tmp/psk/" + psk_path="${psk_path_prefix}${NQN}" + mkdir -p ${psk_path} + echo -n "NVMeTLSkey-1:01:YzrPElk4OYy1uUERriPwiiyEJE/+J5ckYpLB+5NHMsR2iBuT:" > ${psk_path}/${NQN}host + chmod 600 ${psk_path}/${NQN}host + docker cp ${psk_path_prefix} ceph-nvmeof_bdevperf_1:${psk_path_prefix} + + rpc="/usr/libexec/spdk/scripts/rpc.py" + port2=`expr ${NVMEOF_IO_PORT} + 10` + echo "ℹ️ bdevperf bdev_nvme_set_options" + make exec SVC=bdevperf OPTS=-T CMD="$rpc -v -s $BDEVPERF_SOCKET bdev_nvme_set_options -r -1" + echo "ℹ️ bdevperf tcp connect ip: $NVMEOF_IP_ADDRESS port: $NVMEOF_IO_PORT nqn: $NQN" + make exec SVC=bdevperf OPTS=-T CMD="$rpc -v -s $BDEVPERF_SOCKET bdev_nvme_attach_controller -b Nvme0 -t tcp -a $NVMEOF_IP_ADDRESS -s $NVMEOF_IO_PORT -f ipv4 -n $NQN -q ${NQN}host -l -1 -o 10 --psk ${psk_path}/${NQN}host" + echo "ℹ️ verify connection list" + conns=$(cephnvmf --output stdio --format json connection list --subsystem $NQN) + echo $conns | grep -q '"status": 0' + echo $conns | grep -q "\"nqn\": \"${NQN}host\"" + echo $conns | grep -q "\"trsvcid\": ${NVMEOF_IO_PORT}" + echo $conns | grep -q "\"traddr\": \"${NVMEOF_IP_ADDRESS}\"" + echo $conns | grep -q "\"adrfam\": \"ipv4\"" + echo $conns | grep -q "\"trtype\": \"TCP\"" + echo $conns | grep -q "\"qpairs_count\": 1" + echo $conns | grep -q "\"connected\": true" + echo $conns | grep -q "\"secure\": true" + echo $conns | grep -q -v "\"secure\": false" + echo $conns | grep -q "\"use_psk\": true" + echo $conns | grep -q "\"use_psk\": false" + con_cnt=$(echo $conns | xargs -n 2 | grep traddr | grep -v "n/a" | wc -l) + if [ $con_cnt -ne 1 ]; then + echo "Number of connections ${con_cnt}, expected 1, list: ${conns}" + exit 1 + fi + echo "ℹ️ bdevperf tcp connect ip: $NVMEOF_IP_ADDRESS port: ${port2} nqn: ${NQN}host2" + make exec SVC=bdevperf OPTS=-T CMD="$rpc -v -s $BDEVPERF_SOCKET bdev_nvme_attach_controller -b Nvme1 -t tcp -a $NVMEOF_IP_ADDRESS -s ${port2} -f ipv4 -n $NQN -q "${NQN}host2" -l -1 -o 10" + echo "ℹ️ verify connection list again" + conns=$(cephnvmf --output stdio --format json connection list --subsystem $NQN) + con_cnt=$(echo $conns | xargs -n 2 | grep traddr | grep -v "n/a" | wc -l) + if [ $con_cnt -ne 2 ]; then + echo "Number of connections ${con_cnt}, expected 2, list: ${conns}" + exit 1 + fi + echo $conns | grep -q "\"nqn\": \"${NQN}host2\"" + echo $conns | grep -q "\"trsvcid\": ${port2}" + echo $conns | grep -q "\"secure\": true" + echo $conns | grep -q "\"secure\": false" + echo $conns | grep -q "\"use_psk\": true" + echo $conns | grep -q "\"use_psk\": false" + echo "ℹ️ bdevperf tcp connect ip: $NVMEOF_IP_ADDRESS port: ${port2} nqn: ${NQN}host2" + echo "ℹ️ bdevperf perform_tests" + eval $(make run SVC=bdevperf OPTS="--entrypoint=env" | grep BDEVPERF_TEST_DURATION | tr -d '\n\r' ) + timeout=$(expr $BDEVPERF_TEST_DURATION \* 2) + bdevperf="/usr/libexec/spdk/scripts/bdevperf.py" + make exec SVC=bdevperf OPTS=-T CMD="$bdevperf -v -t $timeout -s $BDEVPERF_SOCKET perform_tests" + + - name: Check coredump existence + if: success() || failure() + id: check_coredumps + uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2, pinned to SHA for security reasons + with: + files: "/tmp/coredump/core.*" + + - name: Upload demo core dumps + if: steps.check_coredumps.outputs.files_exists == 'true' + uses: actions/upload-artifact@v4 + with: + name: core_demo + path: /tmp/coredump/core.* + + # For debugging purposes (provides an SSH connection to the runner) + # - name: Setup tmate session + # uses: mxschmitt/action-tmate@v3 + # with: + # limit-access-to-actor: true + + - name: Display logs + if: success() || failure() + run: make logs OPTS='' + + - name: Tear down + if: success() || failure() + run: | + make down + make clean + discovery: needs: [build, build-ceph] strategy: diff --git a/Makefile b/Makefile index 1691ae8e..cbf82d42 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ TARGET_ARCH := $(shell uname -m | sed -e 's/aarch64/arm64/') include .env include mk/containerized.mk include mk/demo.mk +include mk/demosecure.mk include mk/misc.mk include mk/autohelp.mk @@ -19,6 +20,7 @@ setup: ## Configure huge-pages (requires sudo/root password) @echo Setup core dump pattern as /tmp/coredump/core.* mkdir -p /tmp/coredump sudo mkdir -p /var/log/ceph + sudo chmod 0755 /var/log/ceph sudo bash -c 'echo "|/usr/bin/env tee /tmp/coredump/core.%e.%p.%h.%t" > /proc/sys/kernel/core_pattern' sudo bash -c 'echo $(HUGEPAGES) > $(HUGEPAGES_DIR)' @echo Actual Hugepages allocation: $$(cat $(HUGEPAGES_DIR)) diff --git a/control/cli.py b/control/cli.py index 5532e52d..6366968f 100644 --- a/control/cli.py +++ b/control/cli.py @@ -855,6 +855,7 @@ def listener_add(self, args): adrfam=adrfam, traddr=traddr, trsvcid=args.trsvcid, + secure=args.secure, ) try: @@ -960,14 +961,15 @@ def listener_list(self, args): for l in listeners_info.listeners: adrfam = GatewayEnumUtils.get_key_from_value(pb2.AddressFamily, l.adrfam) adrfam = self.format_adrfam(adrfam) - listeners_list.append([l.host_name, l.trtype, adrfam, f"{l.traddr}:{l.trsvcid}"]) + secure = "Yes" if l.secure else "No" + listeners_list.append([l.host_name, l.trtype, adrfam, f"{l.traddr}:{l.trsvcid}", secure]) if len(listeners_list) > 0: if args.format == "text": table_format = "fancy_grid" else: table_format = "plain" listeners_out = tabulate(listeners_list, - headers = ["Host", "Transport", "Address Family", "Address"], + headers = ["Host", "Transport", "Address Family", "Address", "Secure"], tablefmt=table_format) out_func(f"Listeners for {args.subsystem}:\n{listeners_out}") else: @@ -1000,6 +1002,7 @@ def listener_list(self, args): argument("--traddr", "-a", help="NVMe host IP", required=True), argument("--trsvcid", "-s", help="Port number", type=int, required=False), argument("--adrfam", "-f", help="Address family", default="", choices=get_enum_keys_list(pb2.AddressFamily)), + argument("--secure", help="Use secure channel", action='store_true', required=False), ] listener_del_args = listener_common_args + [ argument("--host-name", "-t", help="Host name", required=True), @@ -1033,7 +1036,10 @@ def host_add(self, args): out_func, err_func = self.get_output_functions(args) if not args.host: self.cli.parser.error("--host argument is mandatory for add command") - req = pb2.add_host_req(subsystem_nqn=args.subsystem, host_nqn=args.host) + if args.host == "*" and args.psk: + self.cli.parser.error("PSK is only allowed for specific hosts") + + req = pb2.add_host_req(subsystem_nqn=args.subsystem, host_nqn=args.host, psk=args.psk) try: ret = self.stub.add_host(req) except Exception as ex: @@ -1127,16 +1133,17 @@ def host_list(self, args): if hosts_info.status == 0: hosts_list = [] if hosts_info.allow_any_host: - hosts_list.append(["Any host"]) + hosts_list.append(["Any host", "n/a"]) for h in hosts_info.hosts: - hosts_list.append([h.nqn]) + use_psk = "Yes" if h.use_psk else "No" + hosts_list.append([h.nqn, use_psk]) if len(hosts_list) > 0: if args.format == "text": table_format = "fancy_grid" else: table_format = "plain" hosts_out = tabulate(hosts_list, - headers = [f"Host NQN"], + headers = ["Host NQN", "Uses PSK"], tablefmt=table_format, stralign="center") out_func(f"Hosts allowed to access {args.subsystem}:\n{hosts_out}") else: @@ -1166,6 +1173,7 @@ def host_list(self, args): ] host_add_args = host_common_args + [ argument("--host", "-t", help="Host NQN", required=True), + argument("--psk", help="Host's PSK key", required=False), ] host_del_args = host_common_args + [ argument("--host", "-t", help="Host NQN", required=True), @@ -1204,18 +1212,24 @@ def connection_list(self, args): if connections_info.status == 0: connections_list = [] for conn in connections_info.connections: + conn_secure = "" + conn_psk = "Yes" if conn.use_psk else "No" + if conn.connected: + conn_secure = "Yes" if conn.secure else "No" connections_list.append([conn.nqn, f"{conn.traddr}:{conn.trsvcid}" if conn.connected else "", "Yes" if conn.connected else "No", conn.qpairs_count if conn.connected else "", - conn.controller_id if conn.connected else ""]) + conn.controller_id if conn.connected else "", + conn_secure, + conn_psk]) if len(connections_list) > 0: if args.format == "text": table_format = "fancy_grid" else: table_format = "plain" connections_out = tabulate(connections_list, - headers = ["Host NQN", "Address", "Connected", "QPairs Count", "Controller ID"], + headers = ["Host NQN", "Address", "Connected", "QPairs Count", "Controller ID", "Secure", "PSK"], tablefmt=table_format) out_func(f"Connections for {args.subsystem}:\n{connections_out}") else: diff --git a/control/grpc.py b/control/grpc.py index 55ea9303..389b985f 100644 --- a/control/grpc.py +++ b/control/grpc.py @@ -56,6 +56,41 @@ def group_id(self, request: monitor_pb2.group_id_req, context = None) -> Empty: self.set_group_id(request.id) return Empty() +class SubsystemHostAuth: + def __init__(self): + self.subsys_allow_any_hosts = defaultdict(dict) + self.host_has_psk = defaultdict(dict) + + def clean_subsystem(self, subsys): + self.host_has_psk.pop(subsys, None) + self.subsys_allow_any_hosts.pop(subsys, None) + + def add_psk_host(self, subsys, host): + self.host_has_psk[subsys][host] = True + + def remove_psk_host(self, subsys, host): + if subsys in self.host_has_psk: + self.host_has_psk[subsys].pop(host, None) + if len(self.host_has_psk[subsys]) == 0: + self.host_has_psk.pop(subsys, None) # last host was removed from subsystem + + def is_psk_host(self, subsys, host = None) -> bool: + if subsys in self.host_has_psk: + if not host: + return len(self.host_has_psk[subsys]) != 0 + if host in self.host_has_psk[subsys]: + return True + return False + + def allow_any_host(self, subsys): + self.subsys_allow_any_hosts[subsys] = True + + def disallow_any_host(self, subsys): + self.subsys_allow_any_hosts.pop(subsys, None) + + def is_any_host_allowed(self, subsys) -> bool: + return subsys in self.subsys_allow_any_hosts + class NamespaceInfo: def __init__(self, bdev, uuid): self.bdev = bdev @@ -77,6 +112,8 @@ def remove_namespace(self, nqn, nsid=None): if nsid: if nsid in self.namespace_list[nqn]: self.namespace_list[nqn].pop(nsid, None) + if len(self.namespace_list[nqn]) == 0: + self.namespace_list.pop(nqn, None) # last namespace of subsystem was removed else: self.namespace_list.pop(nqn, None) @@ -119,6 +156,7 @@ def __init__(self, config: GatewayConfig, gateway_state: GatewayStateHandler, rp """Constructor""" self.gw_logger_object = GatewayLogger(config) self.logger = self.gw_logger_object.logger + # notice that this was already called from main, the extra call is for the tests environment where we skip main config.display_environment_info(self.logger) self.ceph_utils = ceph_utils self.ceph_utils.fetch_and_display_ceph_version() @@ -188,7 +226,45 @@ def __init__(self, config: GatewayConfig, gateway_state: GatewayStateHandler, rp self.subsystem_listeners = defaultdict(set) self._init_cluster_context() self.subsys_max_ns = {} + self.host_info = SubsystemHostAuth() + def create_host_psk_file(self, subsysnqn : str, hostnqn : str, psk_value : str) -> str: + assert subsysnqn, "Subsystem NQN can't be empty" + assert hostnqn, "Host NQN can't be empty" + assert psk_value, "PSK value can't be empty" + + psk_dir = f"/tmp/psk/{subsysnqn}" + psk_file = f"{psk_dir}/{hostnqn}" + try: + os.makedirs(psk_dir, 0o755, True) + except Exception: + self.logger.exception(f"Error creating directory {psk_dir}") + return None + try: + with open(psk_file, 'wt') as f: + print(psk_value, end="", file=f) + os.chmod(psk_file, 0o600) + except Exception: + self.logger.exception(f"Error creating file {psk_file}") + return None + return psk_file + + def remove_host_psk_file(self, subsysnqn : str, hostnqn : str) -> None: + psk_dir = f"/tmp/psk/{subsysnqn}" + psk_file = f"{psk_dir}/{hostnqn}" + try: + os.remove(psk_file) + except FileNotFoundError: + self.logger.exception(f"Error deleting file {psk_file}") + pass + try: + os.rmdir(psk_dir) + os.rmdir("/tmp/psk") + except Exception: + self.logger.exception(f"Error deleting directory {psk_dir}") + pass + + @staticmethod def is_valid_host_nqn(nqn): if nqn == "*": return pb2.req_status(status=0, error_message=os.strerror(0)) @@ -711,6 +787,7 @@ def delete_subsystem_safe(self, request, context): self.subsys_max_ns.pop(request.subsystem_nqn) if request.subsystem_nqn in self.subsystem_listeners: self.subsystem_listeners.pop(request.subsystem_nqn, None) + self.host_info.clean_subsystem(request.subsystem_nqn) self.subsystem_nsid_bdev_and_uuid.remove_namespace(request.subsystem_nqn) self.logger.debug(f"delete_subsystem {request.subsystem_nqn}: {ret}") except Exception as ex: @@ -904,7 +981,7 @@ def set_ana_state_safe(self, ana_info: pb2.ana_info, context=None): optimized_ana_groups.add(grp_id) self.logger.debug(f"set_ana_state nvmf_subsystem_listener_set_ana_state {nqn=} {listener=} {ana_state=} {grp_id=}") - (adrfam, traddr, trsvcid) = listener + (adrfam, traddr, trsvcid, secure) = listener ret = rpc_nvmf.nvmf_subsystem_listener_set_ana_state( self.spdk_rpc_client, nqn=nqn, @@ -1665,12 +1742,11 @@ def namespace_delete(self, request, context=None): def matching_host_exists(self, context, subsys_nqn, host_nqn) -> bool: if not context: return False - host_key = GatewayState.build_host_key(subsys_nqn, host_nqn) state = self.gateway_state.local.get_state() + host_key = GatewayState.build_host_key(subsys_nqn, host_nqn) if state.get(host_key): return True - else: - return False + return False def add_host_safe(self, request, context): """Adds a host to a subsystem.""" @@ -1709,20 +1785,33 @@ def add_host_safe(self, request, context): self.logger.error(f"{errmsg}") return pb2.req_status(status=errno.EINVAL, error_message=errmsg) + if request.psk and request.host_nqn == "*": + errmsg=f"{host_failure_prefix}: PSK is only allowed for specific hosts" + self.logger.error(f"{errmsg}") + return pb2.req_status(status=errno.EINVAL, error_message=errmsg) + + host_already_exist = self.matching_host_exists(context, request.subsystem_nqn, request.host_nqn) + if host_already_exist: + if request.host_nqn == "*": + errmsg = f"{all_host_failure_prefix}: Open host access is already allowed" + self.logger.error(f"{errmsg}") + return pb2.req_status(status=errno.EEXIST, error_message=errmsg) + else: + errmsg = f"{host_failure_prefix}: Host is already added" + self.logger.error(f"{errmsg}") + return pb2.req_status(status=errno.EEXIST, error_message=errmsg) + + psk_file = None + if request.psk: + psk_file = self.create_host_psk_file(request.subsystem_nqn, request.host_nqn, request.psk) + if not psk_file: + errmsg=f"{host_failure_prefix}: Can't write PSK file" + self.logger.error(f"{errmsg}") + return pb2.req_status(status=errno.ENOENT, error_message=errmsg) + omap_lock = self.omap_lock.get_omap_lock_to_use(context) with omap_lock: try: - host_already_exist = self.matching_host_exists(context, request.subsystem_nqn, request.host_nqn) - if host_already_exist: - if request.host_nqn == "*": - errmsg = f"{all_host_failure_prefix}: Open host access is already allowed" - self.logger.error(f"{errmsg}") - return pb2.req_status(status=errno.EEXIST, error_message=errmsg) - else: - errmsg = f"{host_failure_prefix}: Host is already added" - self.logger.error(f"{errmsg}") - return pb2.req_status(status=errno.EEXIST, error_message=errmsg) - if request.host_nqn == "*": # Allow any host access to subsystem self.logger.info(f"Received request to allow any host access for {request.subsystem_nqn}, context: {context}{peer_msg}") ret = rpc_nvmf.nvmf_subsystem_allow_any_host( @@ -1731,20 +1820,27 @@ def add_host_safe(self, request, context): disable=False, ) self.logger.debug(f"add_host *: {ret}") + self.host_info.allow_any_host(request.subsystem_nqn) else: # Allow single host access to subsystem self.logger.info( - f"Received request to add host {request.host_nqn} to {request.subsystem_nqn}, context: {context}{peer_msg}") + f"Received request to add host {request.host_nqn} to {request.subsystem_nqn}, psk: {request.psk}, context: {context}{peer_msg}") ret = rpc_nvmf.nvmf_subsystem_add_host( self.spdk_rpc_client, nqn=request.subsystem_nqn, host=request.host_nqn, + psk=psk_file, ) self.logger.debug(f"add_host {request.host_nqn}: {ret}") + if psk_file: + self.host_info.add_psk_host(request.subsystem_nqn, request.host_nqn) + self.remove_host_psk_file(request.subsystem_nqn, request.host_nqn) except Exception as ex: if request.host_nqn == "*": self.logger.exception(all_host_failure_prefix) errmsg = f"{all_host_failure_prefix}:\n{ex}" else: + if psk_file: + self.remove_host_psk_file(request.subsystem_nqn, request.host_nqn) self.logger.exception(host_failure_prefix) errmsg = f"{host_failure_prefix}:\n{ex}" resp = self.parse_json_exeption(ex) @@ -1771,8 +1867,7 @@ def add_host_safe(self, request, context): try: json_req = json_format.MessageToJson( request, preserving_proto_field_name=True, including_default_value_fields=True) - self.gateway_state.add_host(request.subsystem_nqn, - request.host_nqn, json_req) + self.gateway_state.add_host(request.subsystem_nqn, request.host_nqn, json_req) except Exception as ex: errmsg = f"Error persisting host {request.host_nqn} access addition" self.logger.exception(errmsg) @@ -1836,6 +1931,7 @@ def remove_host_safe(self, request, context): disable=True, ) self.logger.debug(f"remove_host *: {ret}") + self.host_info.disallow_any_host(request.subsystem_nqn) else: # Remove single host access to subsystem self.logger.info( f"Received request to remove host {request.host_nqn} access from" @@ -1846,6 +1942,7 @@ def remove_host_safe(self, request, context): host=request.host_nqn, ) self.logger.debug(f"remove_host {request.host_nqn}: {ret}") + self.host_info.remove_psk_host(request.subsystem_nqn, request.host_nqn) except Exception as ex: if request.host_nqn == "*": self.logger.exception(all_host_failure_prefix) @@ -1913,7 +2010,9 @@ def list_hosts_safe(self, request, context): host_nqns = [] pass for h in host_nqns: - one_host = pb2.host(nqn = h["nqn"]) + host_nqn = h["nqn"] + psk = self.host_info.is_psk_host(request.subsystem, host_nqn) + one_host = pb2.host(nqn = host_nqn, use_psk = psk) hosts.append(one_host) break except Exception: @@ -2011,6 +2110,8 @@ def list_connections_safe(self, request, context): hostnqn = conn["hostnqn"] connected = False found = False + secure = False + psk = False for qp in qpair_ret: try: @@ -2039,16 +2140,25 @@ def list_connections_safe(self, request, context): except Exception: self.logger.exception(f"Got exception while parsing qpair: {qp}") pass + if not found: self.logger.debug(f"Can't find active qpair for connection {conn}") continue + + psk = self.host_info.is_psk_host(request.subsystem, hostnqn) + + if request.subsystem in self.subsystem_listeners: + if (adrfam, traddr, trsvcid, True) in self.subsystem_listeners[request.subsystem]: + secure = True + if not trtype: trtype = "TCP" if not adrfam: adrfam = "ipv4" one_conn = pb2.connection(nqn=hostnqn, connected=True, traddr=traddr, trsvcid=trsvcid, trtype=trtype, adrfam=adrfam, - qpairs_count=conn["num_io_qpairs"], controller_id=conn["cntlid"]) + qpairs_count=conn["num_io_qpairs"], controller_id=conn["cntlid"], + secure=secure, use_psk=psk) connections.append(one_conn) if hostnqn in host_nqns: host_nqns.remove(hostnqn) @@ -2057,8 +2167,10 @@ def list_connections_safe(self, request, context): pass for nqn in host_nqns: + psk = False + psk = self.host_info.is_psk_host(request.subsystem, nqn) one_conn = pb2.connection(nqn=nqn, connected=False, traddr="", trsvcid=0, - qpairs_count=-1, controller_id=-1) + qpairs_count=-1, controller_id=-1, use_psk=psk) connections.append(one_conn) return pb2.connections_info(status = 0, error_message = os.strerror(0), @@ -2082,7 +2194,7 @@ def create_listener_safe(self, request, context): peer_msg = self.get_peer_message(context) self.logger.info(f"Received request to create {request.host_name}" f" TCP {adrfam} listener for {request.nqn} at" - f" {request.traddr}:{request.trsvcid}, context: {context}{peer_msg}") + f" {request.traddr}:{request.trsvcid}, secure: {request.secure}, context: {context}{peer_msg}") if GatewayUtils.is_discovery_nqn(request.nqn): errmsg=f"{create_listener_error_prefix}: Can't create a listener for a discovery subsystem" @@ -2094,24 +2206,31 @@ def create_listener_safe(self, request, context): self.logger.error(f"{errmsg}") return pb2.req_status(status=errno.EINVAL, error_message=errmsg) + if request.secure and self.host_info.is_any_host_allowed(request.nqn): + errmsg=f"{create_listener_error_prefix}: Secure channel is only allowed for subsystems in which \"allow any host\" is off" + self.logger.error(f"{errmsg}") + return pb2.req_status(status=errno.EINVAL, error_message=errmsg) + + add_listener_args = {} + add_listener_args["nqn"] = request.nqn + add_listener_args["trtype"] = "TCP" + add_listener_args["traddr"] = request.traddr + add_listener_args["trsvcid"] = str(request.trsvcid) + add_listener_args["adrfam"] = adrfam + if request.secure: + add_listener_args["secure_channel"] = True + omap_lock = self.omap_lock.get_omap_lock_to_use(context) with omap_lock: try: if request.host_name == self.host_name: - if (adrfam, request.traddr, request.trsvcid) in self.subsystem_listeners[request.nqn]: + if (adrfam, request.traddr, request.trsvcid, False) in self.subsystem_listeners[request.nqn] or (adrfam, request.traddr, request.trsvcid, True) in self.subsystem_listeners[request.nqn]: self.logger.error(f"{request.nqn} already listens on address {request.traddr}:{request.trsvcid}") return pb2.req_status(status=errno.EEXIST, error_message=f"{create_listener_error_prefix}: Subsystem already listens on this address") - ret = rpc_nvmf.nvmf_subsystem_add_listener( - self.spdk_rpc_client, - nqn=request.nqn, - trtype="TCP", - traddr=request.traddr, - trsvcid=str(request.trsvcid), - adrfam=adrfam, - ) + ret = rpc_nvmf.nvmf_subsystem_add_listener(self.spdk_rpc_client, **add_listener_args) self.logger.debug(f"create_listener: {ret}") - self.subsystem_listeners[request.nqn].add((adrfam, request.traddr, request.trsvcid)) + self.subsystem_listeners[request.nqn].add((adrfam, request.traddr, request.trsvcid, request.secure)) else: if context: errmsg=f"{create_listener_error_prefix}: Gateway's host name must match current host ({self.host_name})" @@ -2210,7 +2329,7 @@ def remove_listener_from_state(self, nqn, host_name, traddr, port, context): listener_hosts = [] if host_name == "*": state = self.gateway_state.local.get_state() - listener_prefix = GatewayState.build_partial_listener_key(nqn) + listener_prefix = GatewayState.build_partial_listener_key(nqn, None) for key, val in state.items(): if not key.startswith(listener_prefix): continue @@ -2309,7 +2428,11 @@ def delete_listener_safe(self, request, context): adrfam=adrfam, ) self.logger.debug(f"delete_listener: {ret}") - self.subsystem_listeners[request.nqn].remove((adrfam, request.traddr, request.trsvcid)) + if request.nqn in self.subsystem_listeners: + if (adrfam, request.traddr, request.trsvcid, False) in self.subsystem_listeners[request.nqn]: + self.subsystem_listeners[request.nqn].remove((adrfam, request.traddr, request.trsvcid, False)) + if (adrfam, request.traddr, request.trsvcid, True) in self.subsystem_listeners[request.nqn]: + self.subsystem_listeners[request.nqn].remove((adrfam, request.traddr, request.trsvcid, True)) else: errmsg=f"{delete_listener_error_prefix}. Gateway's host name must match current host ({self.host_name}). You can continue to delete the listener by adding the `--force` parameter." self.logger.error(f"{errmsg}") @@ -2352,7 +2475,7 @@ def list_listeners_safe(self, request, context): omap_lock = self.omap_lock.get_omap_lock_to_use(context) with omap_lock: state = self.gateway_state.local.get_state() - listener_prefix = GatewayState.build_partial_listener_key(request.subsystem) + listener_prefix = GatewayState.build_partial_listener_key(request.subsystem, None) for key, val in state.items(): if not key.startswith(listener_prefix): continue @@ -2362,11 +2485,15 @@ def list_listeners_safe(self, request, context): if nqn != request.subsystem: self.logger.warning(f"Got subsystem {nqn} instead of {request.subsystem}, ignore") continue + secure = False + if "secure" in listener: + secure = listener["secure"] one_listener = pb2.listener_info(host_name = listener["host_name"], trtype = "TCP", adrfam = listener["adrfam"], traddr = listener["traddr"], - trsvcid = listener["trsvcid"]) + trsvcid = listener["trsvcid"], + secure = secure) listeners.append(one_listener) except Exception: self.logger.exception(f"Got exception while parsing {val}") @@ -2464,7 +2591,47 @@ def get_subsystems_safe(self, request, context): # Parse the JSON dictionary into the protobuf message subsystem = pb2.subsystem() json_format.Parse(json.dumps(s), subsystem, ignore_unknown_fields=True) - subsystems.append(subsystem) + psk_hosts = [] + saw_psk = False + # if now host nqn is passed, just check if there is any psk host in subsystem + if self.host_info.is_psk_host(nqn): + for h in subsystem.hosts: + psk_val = False + if self.host_info.is_psk_host(nqn, h.nqn): + psk_val = True + saw_psk = True + psk_hosts.append(pb2.host(nqn=h.nqn, use_psk = psk_val)) + + secure_listeners = [] + saw_secure = False + if nqn in self.subsystem_listeners: + for lstnr in subsystem.listen_addresses: + secure_val = False + # We get the address family as IPv4 and the port as a string, we need to adjust to internal form + if (lstnr.adrfam.lower(), lstnr.traddr, int(lstnr.trsvcid), True) in self.subsystem_listeners[nqn]: + saw_secure = True + secure_val = True + secure_listeners.append(pb2.listen_address(trtype=lstnr.trtype, + adrfam=lstnr.adrfam.lower(), + traddr=lstnr.traddr, + trsvcid=lstnr.trsvcid, + transport=lstnr.transport, + secure=secure_val)) + + if saw_psk or saw_secure: + secure_subsystem = pb2.subsystem(nqn = subsystem.nqn, + subtype = subsystem.subtype, + listen_addresses = secure_listeners, + hosts = psk_hosts, + allow_any_host = subsystem.allow_any_host, + serial_number = subsystem.serial_number, + max_namespaces = subsystem.max_namespaces, + min_cntlid = subsystem.min_cntlid, + max_cntlid = subsystem.max_cntlid, + namespaces = subsystem.namespaces) + subsystems.append(secure_subsystem) + else: + subsystems.append(subsystem) except Exception: self.logger.exception(f"{s=} parse error") pass diff --git a/control/proto/gateway.proto b/control/proto/gateway.proto index a6e53137..da3f4c0f 100644 --- a/control/proto/gateway.proto +++ b/control/proto/gateway.proto @@ -181,6 +181,7 @@ message list_namespaces_req { message add_host_req { string subsystem_nqn = 1; string host_nqn = 2; + optional string psk = 3; } message remove_host_req { @@ -202,6 +203,7 @@ message create_listener_req { string traddr = 3; optional AddressFamily adrfam = 5; optional uint32 trsvcid = 6; + optional bool secure = 7; } message delete_listener_req { @@ -315,6 +317,7 @@ message listen_address { string traddr = 3; string trsvcid = 4; optional string transport = 5; + optional bool secure = 6; } message namespace { @@ -378,6 +381,7 @@ message listener_info { AddressFamily adrfam = 3; string traddr = 4; uint32 trsvcid = 5; + optional bool secure = 6; } message listeners_info { @@ -387,7 +391,8 @@ message listeners_info { } message host { - string nqn = 1; + string nqn = 1; + optional bool use_psk = 2; } message hosts_info { @@ -407,6 +412,8 @@ message connection { bool connected = 6; int32 qpairs_count = 7; int32 controller_id = 8; + optional bool secure = 9; + optional bool use_psk = 10; } message connections_info { diff --git a/control/server.py b/control/server.py index 98c9384f..106087c6 100644 --- a/control/server.py +++ b/control/server.py @@ -338,20 +338,9 @@ def _get_spdk_rpc_socket_path(self, omap_state) -> str: if spdk_rpc_socket: return spdk_rpc_socket - spdk_rpc_socket_dir = self.config.get_with_default("spdk", "rpc_socket_dir", "") - if not spdk_rpc_socket_dir: - spdk_rpc_socket_dir = GatewayConfig.CEPH_RUN_DIRECTORY - if self.ceph_utils: - fsid = self.ceph_utils.fetch_ceph_fsid() - if fsid: - spdk_rpc_socket_dir += fsid + "/" - if not spdk_rpc_socket_dir.endswith("/"): - spdk_rpc_socket_dir += "/" - try: - os.makedirs(spdk_rpc_socket_dir, 0o777, True) - except Exception: - logger.exception(f"makedirs({spdk_rpc_socket_dir}, 0o777, True) failed") - pass + (spdk_rpc_socket_dir, create_status) = GatewayUtils.get_directory_with_fsid( + self.logger, self.config, self.ceph_utils, "spdk", "rpc_socket_dir", + GatewayConfig.CEPH_RUN_DIRECTORY, "", True) spdk_rpc_socket = spdk_rpc_socket_dir + self.config.get_with_default("spdk", "rpc_socket_name", "spdk.sock") return spdk_rpc_socket diff --git a/control/state.py b/control/state.py index db93e49c..5bd38901 100644 --- a/control/state.py +++ b/control/state.py @@ -63,13 +63,13 @@ def build_namespace_qos_key(subsystem_nqn: str, nsid) -> str: def build_subsystem_key(subsystem_nqn: str) -> str: return GatewayState.SUBSYSTEM_PREFIX + subsystem_nqn - def build_host_key(subsystem_nqn: str, host_nqn) -> str: + def build_host_key(subsystem_nqn: str, host_nqn: str) -> str: key = GatewayState.HOST_PREFIX + subsystem_nqn if host_nqn is not None: key += GatewayState.OMAP_KEY_DELIMITER + host_nqn return key - def build_partial_listener_key(subsystem_nqn: str, host = None) -> str: + def build_partial_listener_key(subsystem_nqn: str, host: str) -> str: key = GatewayState.LISTENER_PREFIX + subsystem_nqn if host: key += GatewayState.OMAP_KEY_DELIMITER + host @@ -135,7 +135,7 @@ def remove_subsystem(self, subsystem_nqn: str): for key in state.keys(): if (key.startswith(GatewayState.build_namespace_key(subsystem_nqn, None)) or key.startswith(GatewayState.build_host_key(subsystem_nqn, None)) or - key.startswith(GatewayState.build_partial_listener_key(subsystem_nqn))): + key.startswith(GatewayState.build_partial_listener_key(subsystem_nqn, None))): self._remove_key(key) def add_host(self, subsystem_nqn: str, host_nqn: str, val: str): @@ -145,20 +145,22 @@ def add_host(self, subsystem_nqn: str, host_nqn: str, val: str): def remove_host(self, subsystem_nqn: str, host_nqn: str): """Removes a host from the state data store.""" + state = self.get_state() key = GatewayState.build_host_key(subsystem_nqn, host_nqn) - self._remove_key(key) + if key in state.keys(): + self._remove_key(key) - def add_listener(self, subsystem_nqn: str, gateway: str, trtype: str, - traddr: str, trsvcid: int, val: str): + def add_listener(self, subsystem_nqn: str, gateway: str, trtype: str, traddr: str, trsvcid: int, val: str): """Adds a listener to the state data store.""" key = GatewayState.build_listener_key(subsystem_nqn, gateway, trtype, traddr, trsvcid) self._add_key(key, val) - def remove_listener(self, subsystem_nqn: str, gateway: str, trtype: str, - traddr: str, trsvcid: int): + def remove_listener(self, subsystem_nqn: str, gateway: str, trtype: str, traddr: str, trsvcid: int): """Removes a listener from the state data store.""" + state = self.get_state() key = GatewayState.build_listener_key(subsystem_nqn, gateway, trtype, traddr, trsvcid) - self._remove_key(key) + if key in state.keys(): + self._remove_key(key) @abstractmethod def delete_state(self): @@ -629,13 +631,10 @@ def remove_host(self, subsystem_nqn: str, host_nqn: str): self.omap.remove_host(subsystem_nqn, host_nqn) self.local.remove_host(subsystem_nqn, host_nqn) - def add_listener(self, subsystem_nqn: str, gateway: str, trtype: str, - traddr: str, trsvcid: str, val: str): + def add_listener(self, subsystem_nqn: str, gateway: str, trtype: str, traddr: str, trsvcid: str, val: str): """Adds a listener to the state data store.""" - self.omap.add_listener(subsystem_nqn, gateway, trtype, traddr, trsvcid, - val) - self.local.add_listener(subsystem_nqn, gateway, trtype, traddr, trsvcid, - val) + self.omap.add_listener(subsystem_nqn, gateway, trtype, traddr, trsvcid, val) + self.local.add_listener(subsystem_nqn, gateway, trtype, traddr, trsvcid, val) def remove_listener(self, subsystem_nqn: str, gateway: str, trtype: str, traddr: str, trsvcid: str): @@ -744,7 +743,7 @@ def update(self) -> bool: GatewayState.SUBSYSTEM_PREFIX, GatewayState.NAMESPACE_PREFIX, GatewayState.HOST_PREFIX, GatewayState.NAMESPACE_QOS_PREFIX, - GatewayState.LISTENER_PREFIX + GatewayState.LISTENER_PREFIX, ] # Get version and state from OMAP diff --git a/control/utils.py b/control/utils.py index b39476fa..83422fb2 100644 --- a/control/utils.py +++ b/control/utils.py @@ -10,6 +10,7 @@ import uuid import errno import os +import os.path import socket import logging import logging.handlers @@ -175,6 +176,43 @@ def is_valid_nqn(nqn): return (0, os.strerror(0)) + def get_directory_with_fsid(logger, config, ceph_utils, conf_section, conf_field, def_value, suffix, should_create): + status = errno.ENOENT + dir_name = config.get_with_default(conf_section, conf_field, "") + if dir_name: + # If there is an explicit dir name in configuration file, do not change it + suffix = "" + else: + dir_name = def_value + if ceph_utils: + fsid = ceph_utils.fetch_ceph_fsid() + if fsid: + dir_name += fsid + "/" + if not dir_name.endswith("/"): + dir_name += "/" + if suffix: + dir_name += suffix + if not dir_name.endswith("/"): + dir_name += "/" + if should_create: + try: + os.makedirs(dir_name, 0o777, True) + status = 0 + except OSError as err: + logger.exception(f"makedirs({dir_name}, 0o777, True) failed") + status = err.errno + except Exception: + logger.exception(f"makedirs({dir_name}, 0o777, True) failed") + status = errno.ENOENT + else: + if os.path.isdir(dir_name): + status = 0 + else: + logger.error(f"Directory {dir_name} does not exist") + status = errno.ENOENT + + return (dir_name, status) + class GatewayLogger: CEPH_LOG_DIRECTORY = "/var/log/ceph/" MAX_LOG_FILE_SIZE_DEFAULT = 10 diff --git a/mk/demosecure.mk b/mk/demosecure.mk new file mode 100644 index 00000000..ab8809c7 --- /dev/null +++ b/mk/demosecure.mk @@ -0,0 +1,15 @@ +## Demo secure: + +HOSTNQN=`cat /etc/nvme/hostnqn` +HOSTNQN2=`cat /etc/nvme/hostnqn | sed 's/......$$/ffffff/'` +NVMEOF_IO_PORT2=`expr $(NVMEOF_IO_PORT) + 1` +# demosecure +demosecure: + $(NVMEOF_CLI) subsystem add --subsystem $(NQN) + $(NVMEOF_CLI) namespace add --subsystem $(NQN) --rbd-pool $(RBD_POOL) --rbd-image $(RBD_IMAGE_NAME) --size $(RBD_IMAGE_SIZE) --rbd-create-image + $(NVMEOF_CLI) listener add --subsystem $(NQN) --host-name `docker ps -q -f name=$(NVMEOF_CONTAINER_NAME)` --traddr $(NVMEOF_IP_ADDRESS) --trsvcid $(NVMEOF_IO_PORT) --secure + $(NVMEOF_CLI) listener add --subsystem $(NQN) --host-name `docker ps -q -f name=$(NVMEOF_CONTAINER_NAME)` --traddr $(NVMEOF_IP_ADDRESS) --trsvcid $(NVMEOF_IO_PORT2) + $(NVMEOF_CLI) host add --subsystem $(NQN) --host "$(HOSTNQN)" --psk "NVMeTLSkey-1:01:YzrPElk4OYy1uUERriPwiiyEJE/+J5ckYpLB+5NHMsR2iBuT:" + $(NVMEOF_CLI) host add --subsystem $(NQN) --host "$(HOSTNQN2)" + +.PHONY: demosecure diff --git a/tests/test_grpc.py b/tests/test_grpc.py index 461d8552..3deef36f 100644 --- a/tests/test_grpc.py +++ b/tests/test_grpc.py @@ -46,7 +46,7 @@ def test_create_get_subsys(caplog, config): for i in range(created_resource_count): create_resource_by_index(i) - assert "failed" not in caplog.text.lower() + assert "failed" not in caplog.text.lower().replace("failed to notify", "") assert "Failure" not in caplog.text assert f"{subsystem_prefix}0 with ANA group id 1" in caplog.text diff --git a/tests/test_psk.py b/tests/test_psk.py new file mode 100644 index 00000000..2af89844 --- /dev/null +++ b/tests/test_psk.py @@ -0,0 +1,181 @@ +import pytest +from control.server import GatewayServer +import socket +from control.cli import main as cli +from control.cli import main_test as cli_test +from control.cephutils import CephUtils +from control.utils import GatewayUtils +from control.config import GatewayConfig +import grpc +from control.proto import gateway_pb2 as pb2 +from control.proto import gateway_pb2_grpc as pb2_grpc +import os +import os.path + +image = "mytestdevimage" +pool = "rbd" +subsystem = "nqn.2016-06.io.spdk:cnode1" +hostnqn = "nqn.2014-08.org.nvmexpress:uuid:22207d09-d8af-4ed2-84ec-a6d80b0cf7eb" +hostnqn2 = "nqn.2014-08.org.nvmexpress:uuid:22207d09-d8af-4ed2-84ec-a6d80b0cf7ec" +hostnqn3 = "nqn.2014-08.org.nvmexpress:uuid:22207d09-d8af-4ed2-84ec-a6d80b0cf7ee" +hostnqn4 = "nqn.2014-08.org.nvmexpress:uuid:6488a49c-dfa3-11d4-ac31-b232c6c68a8a" +hostnqn5 = "nqn.2014-08.org.nvmexpress:uuid:22207d09-d8af-4ed2-84ec-a6d80b0cf7ef" +hostnqn6 = "nqn.2014-08.org.nvmexpress:uuid:22207d09-d8af-4ed2-84ec-a6d80b0cf7f0" +hostnqn7 = "nqn.2014-08.org.nvmexpress:uuid:22207d09-d8af-4ed2-84ec-a6d80b0cf7f1" + +hostpsk = "NVMeTLSkey-1:01:YzrPElk4OYy1uUERriPwiiyEJE/+J5ckYpLB+5NHMsR2iBuT:" +hostpsk2 = "NVMeTLSkey-1:02:FTFds4vH4utVcfrOforxbrWIgv+Qq4GQHgMdWwzDdDxE1bAqK2mOoyXxmbJxGeueEVVa/Q==:" +hostpsk3 = "junk" +hostpsk4 = "NVMeTLSkey-1:01:YzrPElk4OYy1uUERriPwiiyEJE/+J5ckYpLB+5NHMsR2iBuT:" + +host_name = socket.gethostname() +addr = "127.0.0.1" +config = "ceph-nvmeof.conf" + +@pytest.fixture(scope="module") +def gateway(config): + """Sets up and tears down Gateway""" + + addr = config.get("gateway", "addr") + port = config.getint("gateway", "port") + config.config["gateway-logs"]["log_level"] = "debug" + ceph_utils = CephUtils(config) + + with GatewayServer(config) as gateway: + + # Start gateway + gateway.gw_logger_object.set_log_level("debug") + ceph_utils.execute_ceph_monitor_command("{" + f'"prefix":"nvme-gw create", "id": "{gateway.name}", "pool": "{pool}", "group": ""' + "}") + gateway.serve() + + # Bind the client and Gateway + channel = grpc.insecure_channel(f"{addr}:{port}") + yield gateway.gateway_rpc + + # Stop gateway + gateway.server.stop(grace=1) + gateway.gateway_rpc.gateway_state.delete_state() + +def write_file(filepath, content): + with open(filepath, "w") as f: + f.write(content) + os.chmod(filepath, 0o600) + +def test_setup(caplog, gateway): + gw = gateway + caplog.clear() + cli(["subsystem", "add", "--subsystem", subsystem]) + assert f"create_subsystem {subsystem}: True" in caplog.text + caplog.clear() + cli(["namespace", "add", "--subsystem", subsystem, "--rbd-pool", pool, "--rbd-image", image, "--rbd-create-image", "--size", "16MB"]) + assert f"Adding namespace 1 to {subsystem}: Successful" in caplog.text + +def test_allow_any_host(caplog, gateway): + caplog.clear() + cli(["host", "add", "--subsystem", subsystem, "--host", "*"]) + assert f"Allowing open host access to {subsystem}: Successful" in caplog.text + +def test_create_secure_with_any_host(caplog, gateway): + caplog.clear() + cli(["listener", "add", "--subsystem", subsystem, "--host-name", host_name, "-a", addr, "-s", "5001", "--secure"]) + assert f"Secure channel is only allowed for subsystems in which \"allow any host\" is off" in caplog.text + +def test_remove_any_host_access(caplog, gateway): + caplog.clear() + cli(["host", "del", "--subsystem", subsystem, "--host", "*"]) + assert f"Disabling open host access to {subsystem}: Successful" in caplog.text + +def test_create_secure(caplog, gateway): + caplog.clear() + cli(["listener", "add", "--subsystem", subsystem, "--host-name", host_name, "-a", addr, "-s", "5001", "--secure"]) + assert f"Adding {subsystem} listener at {addr}:5001: Successful" in caplog.text + caplog.clear() + cli(["host", "add", "--subsystem", subsystem, "--host", hostnqn, "--psk", hostpsk]) + assert f"Adding host {hostnqn} to {subsystem}: Successful" in caplog.text + caplog.clear() + cli(["host", "add", "--subsystem", subsystem, "--host", hostnqn2, "--psk", hostpsk2]) + assert f"Adding host {hostnqn2} to {subsystem}: Successful" in caplog.text + caplog.clear() + cli(["host", "add", "--subsystem", subsystem, "--host", hostnqn4, "--psk", hostpsk4]) + assert f"Adding host {hostnqn4} to {subsystem}: Successful" in caplog.text + +def test_create_not_secure(caplog, gateway): + caplog.clear() + cli(["listener", "add", "--subsystem", subsystem, "--host-name", host_name, "-a", addr, "-s", "5002"]) + assert f"Adding {subsystem} listener at {addr}:5002: Successful" in caplog.text + caplog.clear() + cli(["host", "add", "--subsystem", subsystem, "--host", hostnqn6]) + assert f"Adding host {hostnqn6} to {subsystem}: Successful" in caplog.text + caplog.clear() + cli(["host", "add", "--subsystem", subsystem, "--host", hostnqn7]) + assert f"Adding host {hostnqn7} to {subsystem}: Successful" in caplog.text + +def test_create_secure_junk_key(caplog, gateway): + caplog.clear() + cli(["host", "add", "--subsystem", subsystem, "--host", hostnqn3, "--psk", hostpsk3]) + assert f"Failure adding host {hostnqn3} to {subsystem}" in caplog.text + +def test_create_secure_no_key(caplog, gateway): + caplog.clear() + rc = 0 + try: + cli(["host", "add", "--subsystem", subsystem, "--host", hostnqn5, "--psk"]) + except SystemExit as sysex: + rc = int(str(sysex)) + pass + assert rc == 2 + assert f"error: argument --psk: expected one argument" in caplog.text + +def test_list_psk_hosts(caplog, gateway): + caplog.clear() + hosts = cli_test(["host", "list", "--subsystem", subsystem]) + found = 0 + assert len(hosts.hosts) == 5 + for h in hosts.hosts: + assert h.nqn != hostnqn3 + assert h.nqn != hostnqn5 + if h.nqn == hostnqn: + found += 1 + assert h.use_psk + elif h.nqn == hostnqn2: + found += 1 + assert h.use_psk + elif h.nqn == hostnqn4: + found += 1 + assert h.use_psk + elif h.nqn == hostnqn6: + found += 1 + assert not h.use_psk + elif h.nqn == hostnqn7: + found += 1 + assert not h.use_psk + else: + assert False + assert found == 5 + +def test_allow_any_host_with_psk(caplog, gateway): + caplog.clear() + rc = 0 + try: + cli(["host", "add", "--subsystem", subsystem, "--host", "*", "--psk", hostpsk]) + except SystemExit as sysex: + rc = int(str(sysex)) + pass + assert rc == 2 + assert f"PSK is only allowed for specific hosts" in caplog.text + +def test_list_listeners(caplog, gateway): + caplog.clear() + listeners = cli_test(["listener", "list", "--subsystem", subsystem]) + assert len(listeners.listeners) == 2 + found = 0 + for l in listeners.listeners: + if l.trsvcid == 5001: + found += 1 + assert l.secure + elif l.trsvcid == 5002: + found += 1 + assert not l.secure + else: + assert False + assert found == 2