diff --git a/usr.sbin/ctld/Makefile b/usr.sbin/ctld/Makefile index ad3a2661794a..61efe8a05cfb 100644 --- a/usr.sbin/ctld/Makefile +++ b/usr.sbin/ctld/Makefile @@ -1,30 +1,32 @@ .include CFLAGS+=-I${SRCTOP}/contrib/libucl/include .PATH: ${SRCTOP}/contrib/libucl/include PACKAGE= ctl PROG_CXX= ctld SRCS= ctld.cc conf.cc discovery.cc iscsi.cc isns.cc kernel.cc -SRCS+= login.cc parse.y token.l y.tab.h uclparse.cc +SRCS+= login.cc nvmf.cc nvmf_discovery.cc +SRCS+= parse.y token.l y.tab.h uclparse.cc CFLAGS+= -I${.CURDIR} CFLAGS+= -I${SRCTOP}/sys CFLAGS+= -I${SRCTOP}/sys/cam/ctl CFLAGS+= -I${SRCTOP}/sys/dev/iscsi CFLAGS+= -I${SRCTOP}/lib/libiscsiutil CFLAGS+= -I${SRCTOP}/lib/libutil++ +CFLAGS+= -I${SRCTOP}/lib/libnvmf #CFLAGS+= -DICL_KERNEL_PROXY NO_WCAST_ALIGN= CXXWARNFLAGS.gcc= -Wno-shadow MAN= ctld.8 ctl.conf.5 -LIBADD= bsdxml iscsiutil md sbuf util ucl m nv util++ +LIBADD= bsdxml iscsiutil nvmf md sbuf util ucl m nv util++ YFLAGS+= -v CLEANFILES= y.tab.c y.tab.h y.output NO_WMISSING_VARIABLE_DECLARATIONS= .include CXXWARNFLAGS.uclparse.cc= -Wno-shadow -Wno-cast-qual diff --git a/usr.sbin/ctld/conf.cc b/usr.sbin/ctld/conf.cc index d8f941e0bc52..ab76f8e2ed0b 100644 --- a/usr.sbin/ctld/conf.cc +++ b/usr.sbin/ctld/conf.cc @@ -1,411 +1,478 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2012 The FreeBSD Foundation * * This software was developed by Edward Tomasz Napierala under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 AUTHOR 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 AUTHOR 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. * */ #include #include #include #include #include #include #include #include #include #include #include "conf.h" #include "ctld.hh" static struct conf *conf = NULL; static struct auth_group *auth_group = NULL; static struct portal_group *portal_group = NULL; static struct target *target = NULL; static struct lun *lun = NULL; void conf_start(struct conf *new_conf) { assert(conf == NULL); conf = new_conf; } void conf_finish(void) { auth_group = NULL; portal_group = NULL; target = NULL; lun = NULL; conf = NULL; } bool isns_add_server(const char *addr) { return (conf->add_isns(addr)); } void conf_set_debug(int debug) { conf->set_debug(debug); } void conf_set_isns_period(int period) { conf->set_isns_period(period); } void conf_set_isns_timeout(int timeout) { conf->set_isns_timeout(timeout); } void conf_set_maxproc(int maxproc) { conf->set_maxproc(maxproc); } bool conf_set_pidfile_path(const char *path) { return (conf->set_pidfile_path(path)); } void conf_set_timeout(int timeout) { conf->set_timeout(timeout); } bool auth_group_add_chap(const char *user, const char *secret) { return (auth_group->add_chap(user, secret)); } bool auth_group_add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2) { return (auth_group->add_chap_mutual(user, secret, user2, secret2)); } +bool +auth_group_add_host_address(const char *portal) +{ + return (auth_group->add_host_address(portal)); +} + +bool +auth_group_add_host_nqn(const char *name) +{ + return (auth_group->add_host_nqn(name)); +} + bool auth_group_add_initiator_name(const char *name) { return (auth_group->add_initiator_name(name)); } bool auth_group_add_initiator_portal(const char *portal) { return (auth_group->add_initiator_portal(portal)); } bool auth_group_set_type(const char *type) { return (auth_group->set_type(type)); } bool auth_group_start(const char *name) { if (strcmp(name, "default") == 0) auth_group = conf->define_default_auth_group(); else auth_group = conf->add_auth_group(name); return (auth_group != nullptr); } void auth_group_finish(void) { auth_group = NULL; } bool portal_group_start(const char *name) { if (strcmp(name, "default") == 0) portal_group = conf->define_default_portal_group(); else portal_group = conf->add_portal_group(name); return (portal_group != NULL); } void portal_group_finish(void) { portal_group = NULL; } bool portal_group_add_listen(const char *listen, bool iser) { return (portal_group->add_portal(listen, iser ? portal_protocol::ISER : portal_protocol::ISCSI)); } bool portal_group_add_option(const char *name, const char *value) { return (portal_group->add_option(name, value)); } bool portal_group_set_discovery_auth_group(const char *name) { return (portal_group->set_discovery_auth_group(name)); } bool portal_group_set_dscp(u_int dscp) { return (portal_group->set_dscp(dscp)); } bool portal_group_set_filter(const char *str) { return (portal_group->set_filter(str)); } void portal_group_set_foreign(void) { portal_group->set_foreign(); } bool portal_group_set_offload(const char *offload) { return (portal_group->set_offload(offload)); } bool portal_group_set_pcp(u_int pcp) { return (portal_group->set_pcp(pcp)); } bool portal_group_set_redirection(const char *addr) { return (portal_group->set_redirection(addr)); } void portal_group_set_tag(uint16_t tag) { portal_group->set_tag(tag); } +bool +transport_group_start(const char *name) +{ + if (strcmp(name, "default") == 0) + portal_group = conf->define_default_transport_group(); + else + portal_group = conf->add_transport_group(name); + return (portal_group != NULL); +} + +bool +transport_group_add_listen_discovery_tcp(const char *listen) +{ + return portal_group->add_portal(listen, + portal_protocol::NVME_DISCOVERY_TCP); +} + +bool +transport_group_add_listen_tcp(const char *listen) +{ + return portal_group->add_portal(listen, portal_protocol::NVME_TCP); +} + bool lun_start(const char *name) { lun = conf->add_lun(name); return (lun != NULL); } void lun_finish(void) { lun = NULL; } bool lun_add_option(const char *name, const char *value) { return (lun->add_option(name, value)); } bool lun_set_backend(const char *value) { return (lun->set_backend(value)); } bool lun_set_blocksize(size_t value) { return (lun->set_blocksize(value)); } bool lun_set_device_type(const char *value) { return (lun->set_device_type(value)); } bool lun_set_device_id(const char *value) { return (lun->set_device_id(value)); } bool lun_set_path(const char *value) { return (lun->set_path(value)); } bool lun_set_serial(const char *value) { return (lun->set_serial(value)); } bool lun_set_size(uint64_t value) { return (lun->set_size(value)); } bool lun_set_ctl_lun(uint32_t value) { return (lun->set_ctl_lun(value)); } bool target_start(const char *name) { target = conf->add_target(name); return (target != NULL); } void target_finish(void) { target = NULL; } bool target_add_chap(const char *user, const char *secret) { return (target->add_chap(user, secret)); } bool target_add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2) { return (target->add_chap_mutual(user, secret, user2, secret2)); } bool target_add_initiator_name(const char *name) { return (target->add_initiator_name(name)); } bool target_add_initiator_portal(const char *addr) { return (target->add_initiator_portal(addr)); } bool target_add_lun(u_int id, const char *name) { return (target->add_lun(id, name)); } bool target_add_portal_group(const char *pg_name, const char *ag_name) { return (target->add_portal_group(pg_name, ag_name)); } bool target_set_alias(const char *alias) { return (target->set_alias(alias)); } bool target_set_auth_group(const char *name) { return (target->set_auth_group(name)); } bool target_set_auth_type(const char *type) { return (target->set_auth_type(type)); } bool target_set_physical_port(const char *pport) { return (target->set_physical_port(pport)); } bool target_set_redirection(const char *addr) { return (target->set_redirection(addr)); } bool target_start_lun(u_int id) { lun = target->start_lun(id); return (lun != nullptr); } +bool +controller_start(const char *name) +{ + target = conf->add_controller(name); + return (target != nullptr); +} + +bool +controller_add_host_address(const char *addr) +{ + return (target->add_host_address(addr)); +} + +bool +controller_add_host_nqn(const char *name) +{ + return (target->add_host_nqn(name)); +} + +bool +controller_add_namespace(u_int id, const char *name) +{ + return (target->add_namespace(id, name)); +} + +bool +controller_start_namespace(u_int id) +{ + lun = target->start_namespace(id); + return (lun != nullptr); +} + bool parse_conf(const char *path) { freebsd::FILE_up fp(fopen(path, "r")); if (fp == nullptr) { log_warn("unable to open configuration file %s", path); return (false); } bool parsed; try { parsed = yyparse_conf(fp.get()); } catch (std::bad_alloc) { log_warnx("failed to allocate memory parsing %s", path); return (false); } catch (...) { log_warnx("unknown exception parsing %s", path); return (false); } return (parsed); } diff --git a/usr.sbin/ctld/conf.h b/usr.sbin/ctld/conf.h index b13fd80e9fe5..642c8f234d30 100644 --- a/usr.sbin/ctld/conf.h +++ b/usr.sbin/ctld/conf.h @@ -1,104 +1,116 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2012 The FreeBSD Foundation * * This software was developed by Edward Tomasz Napierala under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 AUTHOR 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 AUTHOR 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. */ #ifndef __CONF_H__ #define __CONF_H__ /* * This file defines the interface between parse.y and the rest of * ctld. */ __BEGIN_DECLS bool auth_group_start(const char *name); void auth_group_finish(void); bool auth_group_add_chap(const char *user, const char *secret); bool auth_group_add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2); +bool auth_group_add_host_address(const char *portal); +bool auth_group_add_host_nqn(const char *name); bool auth_group_add_initiator_name(const char *name); bool auth_group_add_initiator_portal(const char *portal); bool auth_group_set_type(const char *type); void conf_set_debug(int debug); void conf_set_isns_period(int period); void conf_set_isns_timeout(int timeout); void conf_set_maxproc(int maxproc); bool conf_set_pidfile_path(const char *path); void conf_set_timeout(int timeout); bool isns_add_server(const char *addr); bool portal_group_start(const char *name); void portal_group_finish(void); bool portal_group_add_listen(const char *listen, bool iser); bool portal_group_add_option(const char *name, const char *value); bool portal_group_set_discovery_auth_group(const char *name); bool portal_group_set_dscp(u_int dscp); bool portal_group_set_filter(const char *filter); void portal_group_set_foreign(void); bool portal_group_set_offload(const char *offload); bool portal_group_set_pcp(u_int pcp); bool portal_group_set_redirection(const char *addr); void portal_group_set_tag(uint16_t tag); +bool transport_group_start(const char *name); +bool transport_group_add_listen_discovery_tcp(const char *listen); +bool transport_group_add_listen_tcp(const char *listen); + bool target_start(const char *name); void target_finish(void); bool target_add_chap(const char *user, const char *secret); bool target_add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2); bool target_add_initiator_name(const char *name); bool target_add_initiator_portal(const char *addr); bool target_add_lun(u_int id, const char *name); bool target_add_portal_group(const char *pg_name, const char *ag_name); bool target_set_alias(const char *alias); bool target_set_auth_group(const char *name); bool target_set_auth_type(const char *type); bool target_set_physical_port(const char *pport); bool target_set_redirection(const char *addr); bool target_start_lun(u_int id); +bool controller_start(const char *name); +bool controller_add_host_address(const char *addr); +bool controller_add_host_nqn(const char *name); +bool controller_add_namespace(u_int id, const char *name); +bool controller_start_namespace(u_int id); + bool lun_start(const char *name); void lun_finish(void); bool lun_add_option(const char *name, const char *value); bool lun_set_backend(const char *value); bool lun_set_blocksize(size_t value); bool lun_set_ctl_lun(uint32_t value); bool lun_set_device_id(const char *value); bool lun_set_device_type(const char *value); bool lun_set_path(const char *value); bool lun_set_serial(const char *value); bool lun_set_size(uint64_t value); bool yyparse_conf(FILE *fp); __END_DECLS #endif /* !__CONF_H__ */ diff --git a/usr.sbin/ctld/ctl.conf.5 b/usr.sbin/ctld/ctl.conf.5 index e42dd8067006..12f4186a6844 100644 --- a/usr.sbin/ctld/ctl.conf.5 +++ b/usr.sbin/ctld/ctl.conf.5 @@ -1,601 +1,831 @@ .\" Copyright (c) 2012 The FreeBSD Foundation .\" Copyright (c) 2015 Alexander Motin .\" All rights reserved. .\" .\" This software was developed by Edward Tomasz Napierala under sponsorship .\" from the FreeBSD Foundation. .\" .\" Redistribution and use in source and binary forms, with or without .\" modification, are permitted provided that the following conditions .\" are met: .\" 1. Redistributions of source code must retain the above copyright .\" notice, this list of conditions and the following disclaimer. .\" 2. 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 AUTHORS 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 AUTHORS 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. .\" -.Dd February 26, 2025 +.Dd August 6, 2025 .Dt CTL.CONF 5 .Os .Sh NAME .Nm ctl.conf -.Nd CAM Target Layer / iSCSI target daemon configuration file +.Nd CAM Target Layer / iSCSI target / NVMeoF controller daemon configuration file .Sh DESCRIPTION The .Nm configuration file is used by the .Xr ctld 8 daemon. Lines starting with .Ql # are interpreted as comments. The general syntax of the .Nm file is: .Bd -literal -offset indent .No pidfile Ar path .No auth-group Ar name No { .Dl chap Ar user Ar secret .Dl ... } .No portal-group Ar name No { .Dl listen Ar address .\".Dl listen-iser Ar address .Dl discovery-auth-group Ar name .Dl ... } +.No transport-group Ar name No { +.Dl listen Ar transport Ar address +.Dl ... +} + .No target Ar name { .Dl auth-group Ar name .Dl portal-group Ar name .Dl lun Ar number No { .Dl path Ar path .Dl } .Dl ... } + +.No controller Ar name { +.Dl auth-group Ar name +.Dl transport-group Ar name +.Dl namespace Ar number No { +.Dl path Ar path +.Dl } +.Dl ... +} .Ed .Ss Global Context .Bl -tag -width indent .It Ic auth-group Ar name Create an .Sy auth-group configuration context, defining a new auth-group, which can then be assigned to any number of targets. .It Ic debug Ar level The debug verbosity level. The default is 0. .It Ic maxproc Ar number The limit for concurrently running child processes handling incoming connections. The default is 30. A setting of 0 disables the limit. .It Ic pidfile Ar path The path to the pidfile. The default is .Pa /var/run/ctld.pid . .It Ic portal-group Ar name Create a .Sy portal-group configuration context, defining a new portal-group, which can then be assigned to any number of targets. +.It Ic transport-group Ar name +Create a +.Sy transport-group +configuration context, +defining a new transport-group, +which can then be assigned to any number of NVMeoF controllers. .It Ic lun Ar name Create a .Sy lun -configuration context, defining a LUN to be exported by any number of targets. +configuration context, defining a LUN to be exported by any number of targets +or controllers. .It Ic target Ar name Create a .Sy target configuration context, which can optionally contain one or more .Sy lun contexts. +.It Ic controller Ar name +Create a +.Sy controller +configuration context, which can optionally contain one or more +.Sy namespace +contexts. .It Ic timeout Ar seconds The timeout for login sessions, after which the connection will be forcibly terminated. The default is 60. A setting of 0 disables the timeout. .It Ic isns-server Ar address An IPv4 or IPv6 address and optionally port of iSNS server to register on. .It Ic isns-period Ar seconds iSNS registration period. Registered Network Entity not updated during this period will be unregistered. The default is 900. .It Ic isns-timeout Ar seconds Timeout for iSNS requests. The default is 5. .El .Ss auth-group Context .Bl -tag -width indent .It Ic auth-type Ar type Sets the authentication type. Type can be either .Qq Ar none , .Qq Ar deny , .Qq Ar chap , or .Qq Ar chap-mutual . In most cases it is not necessary to set the type using this clause; it is usually used to disable authentication for a given .Sy auth-group . .It Ic chap Ar user Ar secret A set of CHAP authentication credentials. Note that for any .Sy auth-group , the configuration may only contain either .Sy chap or .Sy chap-mutual entries; it is an error to mix them. .It Ic chap-mutual Ar user Ar secret Ar mutualuser Ar mutualsecret A set of mutual CHAP authentication credentials. Note that for any .Sy auth-group , the configuration may only contain either .Sy chap or .Sy chap-mutual entries; it is an error to mix them. +.It Ic host-address Ar address Ns Op / Ns Ar prefixlen +An NVMeoF host address: an IPv4 or IPv6 address, optionally +followed by a literal slash and a prefix length. +Only NVMeoF hosts with an address matching one of the defined +addresses will be allowed to connect. +If not defined, there will be no restrictions based on host +address. +.It Ic host-nqn Ar name +An NVMeoF host name. +Only NVMeoF hosts with a name matching one of the defined +names will be allowed to connect. +If not defined, there will be no restrictions based on NVMe host +name. .It Ic initiator-name Ar initiator-name An iSCSI initiator name. Only initiators with a name matching one of the defined names will be allowed to connect. If not defined, there will be no restrictions based on initiator name. .It Ic initiator-portal Ar address Ns Op / Ns Ar prefixlen An iSCSI initiator portal: an IPv4 or IPv6 address, optionally followed by a literal slash and a prefix length. Only initiators with an address matching one of the defined addresses will be allowed to connect. If not defined, there will be no restrictions based on initiator address. .El .Ss portal-group Context .Bl -tag -width indent .It Ic discovery-auth-group Ar name Assign a previously defined authentication group to the portal group, to be used for target discovery. By default, portal groups are assigned predefined .Sy auth-group .Qq Ar default , which denies discovery. Another predefined .Sy auth-group , .Qq Ar no-authentication , may be used to permit discovery without authentication. .It Ic discovery-filter Ar filter Determines which targets are returned during discovery. Filter can be either .Qq Ar none , .Qq Ar portal , .Qq Ar portal-name , or .Qq Ar portal-name-auth . When set to .Qq Ar none , discovery will return all targets assigned to that portal group. When set to .Qq Ar portal , discovery will not return targets that cannot be accessed by the initiator because of their .Sy initiator-portal . When set to .Qq Ar portal-name , the check will include both .Sy initiator-portal and .Sy initiator-name . When set to .Qq Ar portal-name-auth , the check will include .Sy initiator-portal , .Sy initiator-name , and authentication credentials. The target is returned if it does not require CHAP authentication, or if the CHAP user and secret used during discovery match those used by the target. Note that when using .Qq Ar portal-name-auth , targets that require CHAP authentication will only be returned if .Sy discovery-auth-group requires CHAP. The default is .Qq Ar none . .It Ic listen Ar address An IPv4 or IPv6 address and port to listen on for incoming connections. .\".It Ic listen-iser Ar address .\"An IPv4 or IPv6 address and port to listen on for incoming connections .\"using iSER (iSCSI over RDMA) protocol. .It Ic offload Ar driver Define iSCSI hardware offload driver to use for this .Sy portal-group . The default is .Qq Ar none . .It Ic option Ar name Ar value The CTL-specific port options passed to the kernel. .It Ic redirect Ar address IPv4 or IPv6 address to redirect initiators to. When configured, all initiators attempting to connect to portal belonging to this .Sy portal-group will get redirected using "Target moved temporarily" login response. Redirection happens before authentication and any .Sy initiator-name or .Sy initiator-portal checks are skipped. .It Ic tag Ar value Unique 16-bit tag value of this .Sy portal-group . If not specified, the value is generated automatically. .It Ic foreign Specifies that this .Sy portal-group is listened by some other host. This host will announce it on discovery stage, but won't listen. .It Ic dscp Ar value The DiffServ Codepoint used for sending data. The DSCP can be set to numeric, or hexadecimal values directly, as well as the well-defined .Qq Ar CSx and .Qq Ar AFxx codepoints. .It Ic pcp Ar value The 802.1Q Priority CodePoint used for sending packets. The PCP can be set to a value in the range between .Qq Ar 0 to .Qq Ar 7 . When omitted, the default for the outgoing interface is used. .El +.Ss transport-group Context +.Bl -tag -width indent +.It Ic discovery-auth-group Ar name +See the description for this option for +.Sy portal-group +contexts. +.It Ic discovery-filter Ar filter +Filter can be either +.Qq Ar none , +.Qq Ar address , +or +.Qq Ar address-name . +When set to +.Qq Ar none , +discovery will return all controllers assigned to that transport group. +When set to +.Qq Ar address , +discovery will not return controllers that cannot be accessed by the +host because of their +.Sy host-address . +When set to +.Qq Ar address-name , +the check will include both +.Sy host-address +and +.Sy host-nqn . +The default is +.Qq Ar none . +.It Ic listen Ar transport Ar address +An IPv4 or IPv6 address and port to listen on for incoming connections +using the specified NVMeoF transport. +Supported transports are +.Qq Ar tcp +.Pq for NVMe/TCP I/O controllers +and +.Qq Ar discovery-tcp +.Pq for NVMe/TCP discovery controllers . +.It Ic option Ar name Ar value +One of the following options: +.Bl -column "max_admin_qsize" "Default" "Transports" +.It Sy Name Ta Sy Default Ta Sy Transports Ta Sy Description +.It MAXH2CDATA Ta 256KiB Ta TCP Ta +Size in bytes of the maximum data payload size for data PDUs accepted from +remote hosts. +The value must be at least 4KiB and must be a multiple of 4. +.It SQFC Ta false Ta any Ta +Always enable SQ flow control. +.It HDGST Ta false Ta TCP Ta +Enable PDU header digests if requested by a remote host. +.It DDGST Ta false Ta TCP Ta +Enable PDU data digests if requested by a remote host. +.It max_admin_qsize Ta 4096 Ta any Ta +The maximum number of entries a remote host can request for an admin queue pair. +.It max_io_qsize Ta 65536 Ta any Ta +The maximum number of entries a remote host can request for an I/O queue pair. +.El +.It Ic tag Ar value +Unique 16-bit port ID for this +.Sy transport-group . +If not specified, the value is generated automatically. +.It Ic dscp Ar value +See the description for this option for +.Sy portal-group +contexts. +.It Ic pcp Ar value +See the description for this option for +.Sy portal-group +contexts. +.El .Ss target Context .Bl -tag -width indent .It Ic alias Ar text Assign a human-readable description to the target. There is no default. .It Ic auth-group Ar name Assign a previously defined authentication group to the target. By default, targets that do not specify their own auth settings, using clauses such as .Sy chap or .Sy initiator-name , are assigned predefined .Sy auth-group .Qq Ar default , which denies all access. Another predefined .Sy auth-group , .Qq Ar no-authentication , may be used to permit access without authentication. Note that this clause can be overridden using the second argument to a .Sy portal-group clause. .It Ic auth-type Ar type Sets the authentication type. Type can be either .Qq Ar none , .Qq Ar deny , .Qq Ar chap , or .Qq Ar chap-mutual . In most cases it is not necessary to set the type using this clause; it is usually used to disable authentication for a given .Sy target . This clause is mutually exclusive with .Sy auth-group ; one cannot use both in a single target. .It Ic chap Ar user Ar secret A set of CHAP authentication credentials. Note that targets must only use one of .Sy auth-group , chap , No or Sy chap-mutual ; it is a configuration error to mix multiple types in one target. .It Ic chap-mutual Ar user Ar secret Ar mutualuser Ar mutualsecret A set of mutual CHAP authentication credentials. Note that targets must only use one of .Sy auth-group , chap , No or Sy chap-mutual ; it is a configuration error to mix multiple types in one target. .It Ic initiator-name Ar initiator-name An iSCSI initiator name. Only initiators with a name matching one of the defined names will be allowed to connect. If not defined, there will be no restrictions based on initiator name. This clause is mutually exclusive with .Sy auth-group ; one cannot use both in a single target. .It Ic initiator-portal Ar address Ns Op / Ns Ar prefixlen An iSCSI initiator portal: an IPv4 or IPv6 address, optionally followed by a literal slash and a prefix length. Only initiators with an address matching one of the defined addresses will be allowed to connect. If not defined, there will be no restrictions based on initiator address. This clause is mutually exclusive with .Sy auth-group ; one cannot use both in a single target. .Pp The .Sy auth-type , .Sy chap , .Sy chap-mutual , .Sy initiator-name , and .Sy initiator-portal clauses in the target context provide an alternative to assigning an .Sy auth-group defined separately, useful in the common case of authentication settings specific to a single target. .It Ic portal-group Ar name Op Ar ag-name Assign a previously defined portal group to the target. The default portal group is .Qq Ar default , which makes the target available on TCP port 3260 on all configured IPv4 and IPv6 addresses. Optional second argument specifies .Sy auth-group for connections to this specific portal group. If second argument is not specified, target .Sy auth-group is used. .It Ic port Ar name .It Ic port Ar name/pp .It Ic port Ar name/pp/vp Assign specified CTL port (such as "isp0" or "isp2/1") to the target. This is used to export the target through a specific physical - eg Fibre Channel - port, in addition to portal-groups configured for the target. Use .Cm "ctladm portlist" command to retrieve the list of available ports. On startup .Xr ctld 8 configures LUN mapping and enables all assigned ports. Each port can be assigned to only one target. .It Ic redirect Ar address IPv4 or IPv6 address to redirect initiators to. When configured, all initiators attempting to connect to this target will get redirected using "Target moved temporarily" login response. Redirection happens after successful authentication. .It Ic lun Ar number Ar name Export previously defined .Sy lun by the parent target. .It Ic lun Ar number Create a .Sy lun configuration context, defining a LUN exported by the parent target. .Pp This is an alternative to defining the LUN separately, useful in the common case of a LUN being exported by a single target. .El +.Ss controller Context +.Bl -tag -width indent +.It Ic auth-group Ar name +Assign a previously defined authentication group to the controller. +By default, controllers that do not specify their own auth settings, +using clauses such as +.Sy host-address +or +.Sy host-nqn , +are assigned to the +predefined +.Sy auth-group +.Qq Ar default , +which denies all access. +Another predefined +.Sy auth-group , +.Qq Ar no-authentication , +may be used to permit access +without authentication. +Note that this clause can be overridden using the second argument +to a +.Sy transport-group +clause. +.It Ic auth-type Ar type +Sets the authentication type. +Type can be either +.Qq Ar none +or +.Qq Ar deny . +In most cases it is not necessary to set the type using this clause; +it is usually used to disable authentication for a given +.Sy controller . +This clause is mutually exclusive with +.Sy auth-group ; +one cannot use +both in a single controller. +.It Ic host-address Ar address Ns Op / Ns Ar prefixlen +An NVMeoF host address: an IPv4 or IPv6 address, optionally +followed by a literal slash and a prefix length. +Only NVMeoF hosts with an address matching one of the defined +addresses will be allowed to connect. +If not defined, there will be no restrictions based on host +address. +This clause is mutually exclusive with +.Sy auth-group ; +one cannot use +both in a single controller. +.It Ic host-nqn Ar name +An NVMeoF host name. +Only NVMeoF hosts with a name matching one of the defined +names will be allowed to connect. +If not defined, there will be no restrictions based on NVMe host +name. +This clause is mutually exclusive with +.Sy auth-group ; +one cannot use +both in a single target. +.Pp +The +.Sy auth-type , +.Sy host-address , +and +.Sy host-nqn +clauses in the controller context provide an alternative to assigning an +.Sy auth-group +defined separately, useful in the common case of authentication settings +specific to a single controller. +.It Ic transport-group Ar name Op Ar ag-name +Assign a previously defined transport group to the controller. +The default transport group is +.Qq Ar default , +which makes the controller available +on TCP port 4420 on all configured IPv4 and IPv6 addresses. +The optional second argument specifies the +.Sy auth-group +for connections to this specific transport group group. +If the second argument is not specified, the controller +.Sy auth-group +is used. +.It Ic namespace Ar number Ar name +Export previously defined +.Sy lun +as an NVMe namespace from the parent controller. +.It Ic namespace Ar number +Create a +.Sy namespace +configuration context, defining an NVMe namespace exported by the parent target. +.Pp +This is an alternative to defining the namespace separately, +useful in the common case of a namespace being exported by a single controller. +.Sy namespace +configuration contexts accept the the same properties as +.Sy lun +contexts. +.El .Ss lun Context .Bl -tag -width indent .It Ic backend Ar block No | Ar ramdisk The CTL backend to use for a given LUN. Valid choices are .Qq Ar block and .Qq Ar ramdisk ; block is used for LUNs backed by files or disk device nodes; ramdisk is a bitsink device, used mostly for testing. The default backend is block. .It Ic blocksize Ar size The blocksize visible to the initiator. The default blocksize is 512 for disks, and 2048 for CD/DVDs. .It Ic ctl-lun Ar lun_id Global numeric identifier to use for a given LUN inside CTL. By default CTL allocates those IDs dynamically, but explicit specification may be needed for consistency in HA configurations. .It Ic device-id Ar string -The SCSI Device Identification string presented to the initiator. +The SCSI Device Identification string presented to iSCSI initiators. .It Ic device-type Ar type Specify the SCSI device type to use when creating the LUN. Currently CTL supports Direct Access (type 0), Processor (type 3) and CD/DVD (type 5) LUNs. .It Ic option Ar name Ar value The CTL-specific options passed to the kernel. All CTL-specific options are documented in the .Sx OPTIONS section of .Xr ctladm 8 . .It Ic path Ar path The path to the file, device node, or .Xr zfs 8 volume used to back the LUN. -For optimal performance, create the volume with the +For optimal performance, create ZFS volumes with the .Qq Ar volmode=dev property set. .It Ic serial Ar string -The SCSI serial number presented to the initiator. +The SCSI serial number presented to iSCSI initiators. .It Ic size Ar size The LUN size, in bytes or by number with a suffix of .Sy K , M , G , T (for kilobytes, megabytes, gigabytes, or terabytes). When the configuration is in UCL format, use the suffix format .Sy kKmMgG Ns | Ns Sy bB , (i.e., 4GB, 4gb, and 4Gb are all equivalent). .El .Sh FILES .Bl -tag -width ".Pa /etc/ctl.conf" -compact .It Pa /etc/ctl.conf The default location of the .Xr ctld 8 configuration file. .El .Sh EXAMPLES .Bd -literal auth-group ag0 { chap-mutual "user" "secret" "mutualuser" "mutualsecret" chap-mutual "user2" "secret2" "mutualuser" "mutualsecret" initiator-portal 192.168.1.1/16 } auth-group ag1 { auth-type none initiator-name "iqn.2012-06.com.example:initiatorhost1" initiator-name "iqn.2012-06.com.example:initiatorhost2" initiator-portal 192.168.1.1/24 initiator-portal [2001:db8::de:ef] } portal-group pg0 { discovery-auth-group no-authentication listen 0.0.0.0:3260 listen [::]:3260 listen [fe80::be:ef]:3261 } target iqn.2012-06.com.example:target0 { alias "Example target" auth-group no-authentication lun 0 { path /dev/zvol/tank/example_0 blocksize 4096 size 4G } } lun example_1 { path /dev/zvol/tank/example_1 option naa 0x50015178f369f093 } target iqn.2012-06.com.example:target1 { auth-group ag0 portal-group pg0 lun 0 example_1 lun 1 { path /dev/zvol/tank/example_2 option vendor "FreeBSD" } } target naa.50015178f369f092 { port isp0 port isp1 lun 0 example_1 } + +controller nqn.2012-06.com.example:controller1 { + auth-group no-authentication; + namespace 1 example_1 + namespace 2 { + backend ramdisk + size 1G + option capacity 1G + } +} .Ed .Pp An equivalent configuration in UCL format, for use with .Fl u : .Bd -literal auth-group { ag0 { chap-mutual = [ { user = "user" secret = "secretsecret" mutual-user = "mutualuser" mutual-secret = "mutualsecret" }, { user = "user2" secret = "secret2secret2" mutual-user = "mutualuser" mutual-secret = "mutualsecret" } ] } ag1 { auth-type = none initiator-name = [ "iqn.2012-06.com.example:initiatorhost1", "iqn.2012-06.com.example:initiatorhost2" ] initiator-portal = [192.168.1.1/24, "[2001:db8::de:ef]"] } } portal-group { pg0 { discovery-auth-group = no-authentication listen = [ 0.0.0.0:3260, "[::]:3260", "[fe80::be:ef]:3261" ] } } lun { example_1 { path = /dev/zvol/tank/example_1 options { naa = "0x50015178f369f093" } } } target { "iqn.2012-06.com.example:target0" { alias = "Example target" auth-group = no-authentication lun = { 0 { path = /dev/zvol/tank/example_0 blocksize = 4096 size = 4GB } } } "iqn.2012-06.com.example:target1" { auth-group = ag0 portal-group = pg0 lun { 0 = example_1 1 { path = /dev/zvol/tank/example_2 options { vendor = "FreeBSD" } } } } naa.50015178f369f092 { port = isp0 lun { 0 = example_1 } } } + +controller { + "nqn.2012-06.com.example:controller1" { + auth-group = no-authentication + namespace = { + 1 = example_1, + 2 { + backend = ramdisk + size = 1G + options { + capacity = 1G + } + } + } + } +} .Ed .Sh SEE ALSO .Xr ctl 4 , .Xr ctladm 8 , .Xr ctld 8 , .Xr zfs 8 .Sh AUTHORS The .Nm configuration file functionality for .Xr ctld 8 was developed by .An Edward Tomasz Napierala Aq Mt trasz@FreeBSD.org under sponsorship from the FreeBSD Foundation. diff --git a/usr.sbin/ctld/ctld.cc b/usr.sbin/ctld/ctld.cc index 451245b8d5fa..10c12f25068e 100644 --- a/usr.sbin/ctld/ctld.cc +++ b/usr.sbin/ctld/ctld.cc @@ -1,2629 +1,2795 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2012 The FreeBSD Foundation * * This software was developed by Edward Tomasz Napierala under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 AUTHOR 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 AUTHOR 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. * */ #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include "conf.h" #include "ctld.hh" #include "isns.hh" bool proxy_mode = false; static volatile bool sighup_received = false; static volatile bool sigterm_received = false; static volatile bool sigalrm_received = false; static int kqfd; static int nchildren = 0; +uint32_t conf::global_genctr; + static void usage(void) { fprintf(stderr, "usage: ctld [-d][-u][-f config-file]\n"); fprintf(stderr, " ctld -t [-u][-f config-file]\n"); exit(1); } +conf::conf() +{ + conf_genctr = global_genctr++; +} + void conf::set_debug(int debug) { conf_debug = debug; } void conf::set_isns_period(int period) { conf_isns_period = period; } void conf::set_isns_timeout(int timeout) { conf_isns_timeout = timeout; } void conf::set_maxproc(int maxproc) { conf_maxproc = maxproc; } void conf::set_timeout(int timeout) { conf_timeout = timeout; } bool conf::set_pidfile_path(std::string_view path) { if (!conf_pidfile_path.empty()) { log_warnx("pidfile specified more than once"); return (false); } conf_pidfile_path = path; return (true); } void conf::open_pidfile() { const char *path; pid_t otherpid; assert(!conf_pidfile_path.empty()); path = conf_pidfile_path.c_str(); log_debugx("opening pidfile %s", path); conf_pidfile = pidfile_open(path, 0600, &otherpid); if (!conf_pidfile) { if (errno == EEXIST) log_errx(1, "daemon already running, pid: %jd.", (intmax_t)otherpid); log_err(1, "cannot open or create pidfile \"%s\"", path); } } void conf::write_pidfile() { conf_pidfile.write(); } void conf::close_pidfile() { conf_pidfile.close(); } #ifdef ICL_KERNEL_PROXY int conf::add_proxy_portal(portal *portal) { conf_proxy_portals.push_back(portal); return (conf_proxy_portals.size() - 1); } portal * conf::proxy_portal(int id) { if (id >= conf_proxy_portals.size()) return (nullptr); return (conf_proxy_portals[id]); } #endif bool auth_group::set_type(const char *str) { auth_type type; if (strcmp(str, "none") == 0) { type = auth_type::NO_AUTHENTICATION; } else if (strcmp(str, "deny") == 0) { type = auth_type::DENY; } else if (strcmp(str, "chap") == 0) { type = auth_type::CHAP; } else if (strcmp(str, "chap-mutual") == 0) { type = auth_type::CHAP_MUTUAL; } else { log_warnx("invalid auth-type \"%s\" for %s", str, label()); return (false); } if (ag_type != auth_type::UNKNOWN && ag_type != type) { log_warnx("cannot set auth-type to \"%s\" for %s; " "already has a different type", str, label()); return (false); } ag_type = type; return (true); } void auth_group::set_type(auth_type type) { assert(ag_type == auth_type::UNKNOWN); ag_type = type; } const struct auth * auth_group::find_auth(std::string_view user) const { auto it = ag_auths.find(std::string(user)); if (it == ag_auths.end()) return (nullptr); return (&it->second); } void auth_group::check_secret_length(const char *user, const char *secret, const char *secret_type) { size_t len; len = strlen(secret); assert(len != 0); if (len > 16) { log_warnx("%s for user \"%s\", %s, is too long; it should be " "at most 16 characters long", secret_type, user, label()); } if (len < 12) { log_warnx("%s for user \"%s\", %s, is too short; it should be " "at least 12 characters long", secret_type, user, label()); } } bool auth_group::add_chap(const char *user, const char *secret) { if (ag_type == auth_type::UNKNOWN) ag_type = auth_type::CHAP; if (ag_type != auth_type::CHAP) { log_warnx("cannot mix \"chap\" authentication with " "other types for %s", label()); return (false); } check_secret_length(user, secret, "secret"); const auto &pair = ag_auths.try_emplace(user, secret); if (!pair.second) { log_warnx("duplicate credentials for user \"%s\" for %s", user, label()); return (false); } return (true); } bool auth_group::add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2) { if (ag_type == auth_type::UNKNOWN) ag_type = auth_type::CHAP_MUTUAL; if (ag_type != auth_type::CHAP_MUTUAL) { log_warnx("cannot mix \"chap-mutual\" authentication " "with other types for %s", label()); return (false); } check_secret_length(user, secret, "secret"); check_secret_length(user, secret2, "mutual secret"); const auto &pair = ag_auths.try_emplace(user, secret, user2, secret2); if (!pair.second) { log_warnx("duplicate credentials for user \"%s\" for %s", user, label()); return (false); } return (true); } +bool +auth_group::add_host_nqn(std::string_view nqn) +{ + /* Silently ignore duplicates. */ + ag_host_names.emplace(nqn); + return (true); +} + +bool +auth_group::host_permitted(std::string_view nqn) const +{ + if (ag_host_names.empty()) + return (true); + + return (ag_host_names.count(std::string(nqn)) != 0); +} + bool auth_group::add_initiator_name(std::string_view name) { /* Silently ignore duplicates. */ ag_initiator_names.emplace(name); return (true); } bool auth_group::initiator_permitted(std::string_view initiator_name) const { if (ag_initiator_names.empty()) return (true); return (ag_initiator_names.count(std::string(initiator_name)) != 0); } bool auth_portal::parse(const char *portal) { std::string net(portal); std::string mask; /* Split into 'net' (address) and 'mask'. */ size_t pos = net.find('/'); if (pos != net.npos) { mask = net.substr(pos + 1); if (mask.empty()) return false; net.resize(pos); } if (net.empty()) return false; /* * If 'net' starts with a '[', ensure it ends with a ']' and * force interpreting the address as IPv6. */ bool brackets = net[0] == '['; if (brackets) { net.erase(0, 1); size_t len = net.length(); if (len < 2) return false; if (net[len - 1] != ']') return false; net.resize(len - 1); } /* Parse address from 'net' and set default mask. */ if (brackets || net.find(':') != net.npos) { struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&ap_sa; sin6->sin6_len = sizeof(*sin6); sin6->sin6_family = AF_INET6; if (inet_pton(AF_INET6, net.c_str(), &sin6->sin6_addr) <= 0) return false; ap_mask = sizeof(sin6->sin6_addr) * 8; } else { struct sockaddr_in *sin = (struct sockaddr_in *)&ap_sa; sin->sin_len = sizeof(*sin); sin->sin_family = AF_INET; if (inet_pton(AF_INET, net.c_str(), &sin->sin_addr) <= 0) return false; ap_mask = sizeof(sin->sin_addr) * 8; } /* Parse explicit mask if present. */ if (!mask.empty()) { char *tmp; long m = strtol(mask.c_str(), &tmp, 0); if (m < 0 || m > ap_mask || tmp[0] != 0) return false; ap_mask = m; } return true; } +bool +auth_group::add_host_address(const char *address) +{ + auth_portal ap; + if (!ap.parse(address)) { + log_warnx("invalid controller address \"%s\" for %s", address, + label()); + return (false); + } + + ag_host_addresses.emplace_back(ap); + return (true); +} + bool auth_group::add_initiator_portal(const char *portal) { auth_portal ap; if (!ap.parse(portal)) { log_warnx("invalid initiator portal \"%s\" for %s", portal, label()); return (false); } ag_initiator_portals.emplace_back(ap); return (true); } bool auth_portal::matches(const struct sockaddr *sa) const { const uint8_t *a, *b; int i; if (ap_sa.ss_family != sa->sa_family) return (false); if (sa->sa_family == AF_INET) { a = (const uint8_t *) &((const struct sockaddr_in *)sa)->sin_addr; b = (const uint8_t *) &((const struct sockaddr_in *)&ap_sa)->sin_addr; } else { a = (const uint8_t *) &((const struct sockaddr_in6 *)sa)->sin6_addr; b = (const uint8_t *) &((const struct sockaddr_in6 *)&ap_sa)->sin6_addr; } for (i = 0; i < ap_mask / 8; i++) { if (a[i] != b[i]) return (false); } if ((ap_mask % 8) != 0) { uint8_t bmask = 0xff << (8 - (ap_mask % 8)); if ((a[i] & bmask) != (b[i] & bmask)) return (false); } return (true); } +bool +auth_group::host_permitted(const struct sockaddr *sa) const +{ + if (ag_host_addresses.empty()) + return (true); + + for (const auth_portal &ap : ag_host_addresses) + if (ap.matches(sa)) + return (true); + return (false); +} + bool auth_group::initiator_permitted(const struct sockaddr *sa) const { if (ag_initiator_portals.empty()) return (true); for (const auth_portal &ap : ag_initiator_portals) if (ap.matches(sa)) return (true); return (false); } struct auth_group * conf::add_auth_group(const char *name) { const auto &pair = conf_auth_groups.try_emplace(name, std::make_shared(freebsd::stringf("auth-group \"%s\"", name))); if (!pair.second) { log_warnx("duplicated auth-group \"%s\"", name); return (NULL); } return (pair.first->second.get()); } /* * Make it possible to redefine the default auth-group, but only once. */ struct auth_group * conf::define_default_auth_group() { if (conf_default_ag_defined) { log_warnx("duplicated auth-group \"default\""); return (nullptr); } conf_default_ag_defined = true; return (find_auth_group("default").get()); } auth_group_sp conf::find_auth_group(std::string_view name) { auto it = conf_auth_groups.find(std::string(name)); if (it == conf_auth_groups.end()) return {}; return (it->second); } portal_group::portal_group(struct conf *conf, std::string_view name) : pg_conf(conf), pg_options(nvlist_create(0)), pg_name(name) { } struct portal_group * conf::add_portal_group(const char *name) { auto pair = conf_portal_groups.try_emplace(name, iscsi_make_portal_group(this, name)); if (!pair.second) { log_warnx("duplicated portal-group \"%s\"", name); return (nullptr); } return (pair.first->second.get()); } /* * Make it possible to redefine the default portal-group, but only * once. */ struct portal_group * conf::define_default_portal_group() { if (conf_default_pg_defined) { log_warnx("duplicated portal-group \"default\""); return (nullptr); } conf_default_pg_defined = true; return (find_portal_group("default")); } struct portal_group * conf::find_portal_group(std::string_view name) { auto it = conf_portal_groups.find(std::string(name)); if (it == conf_portal_groups.end()) return (nullptr); return (it->second.get()); } +struct portal_group * +conf::add_transport_group(const char *name) +{ + auto pair = conf_transport_groups.try_emplace(name, + nvmf_make_transport_group(this, name)); + if (!pair.second) { + log_warnx("duplicated transport-group \"%s\"", name); + return (nullptr); + } + + return (pair.first->second.get()); +} + +/* + * Make it possible to redefine the default transport-group, but only + * once. + */ +struct portal_group * +conf::define_default_transport_group() +{ + if (conf_default_tg_defined) { + log_warnx("duplicated transport-group \"default\""); + return (nullptr); + } + + conf_default_tg_defined = true; + return (find_transport_group("default")); +} + +struct portal_group * +conf::find_transport_group(std::string_view name) +{ + auto it = conf_transport_groups.find(std::string(name)); + if (it == conf_transport_groups.end()) + return (nullptr); + + return (it->second.get()); +} + bool portal_group::is_dummy() const { if (pg_foreign) return (true); if (pg_portals.empty()) return (true); return (false); } freebsd::addrinfo_up parse_addr_port(const char *address, const char *def_port) { struct addrinfo hints, *ai; int error; std::string addr(address); std::string port(def_port); if (addr[0] == '[') { /* * IPv6 address in square brackets, perhaps with port. */ addr.erase(0, 1); size_t pos = addr.find(']'); if (pos == 0 || pos == addr.npos) return {}; if (pos < addr.length() - 1) { port = addr.substr(pos + 1); if (port[0] != ':' || port.length() < 2) return {}; port.erase(0, 1); } addr.resize(pos); } else { /* * Either IPv6 address without brackets - and without * a port - or IPv4 address. Just count the colons. */ size_t pos = addr.find(':'); if (pos != addr.npos && addr.find(':', pos + 1) == addr.npos) { /* Only a single colon at `pos`. */ if (pos == addr.length() - 1) return {}; port = addr.substr(pos + 1); addr.resize(pos); } } memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; error = getaddrinfo(addr.c_str(), port.c_str(), &hints, &ai); if (error != 0) return {}; return freebsd::addrinfo_up(ai); } void portal_group::add_port(struct portal_group_port *port) { pg_ports.emplace(port->target()->name(), port); } void portal_group::remove_port(struct portal_group_port *port) { auto it = pg_ports.find(port->target()->name()); pg_ports.erase(it); } freebsd::nvlist_up portal_group::options() const { return (freebsd::nvlist_up(nvlist_clone(pg_options.get()))); } bool portal_group::add_option(const char *name, const char *value) { return (option_new(pg_options.get(), name, value)); } bool portal_group::set_discovery_auth_group(const char *ag_name) { if (pg_discovery_auth_group != nullptr) { log_warnx("discovery-auth-group for %s " "\"%s\" specified more than once", keyword(), name()); return (false); } pg_discovery_auth_group = pg_conf->find_auth_group(ag_name); if (pg_discovery_auth_group == nullptr) { log_warnx("unknown discovery-auth-group \"%s\" " "for %s \"%s\"", ag_name, keyword(), name()); return (false); } return (true); } bool portal_group::set_dscp(u_int dscp) { if (dscp >= 0x40) { log_warnx("invalid DSCP value %u for %s \"%s\"", dscp, keyword(), name()); return (false); } pg_dscp = dscp; return (true); } void portal_group::set_foreign() { pg_foreign = true; } bool portal_group::set_offload(const char *offload) { if (!pg_offload.empty()) { log_warnx("cannot set offload to \"%s\" for " "%s \"%s\"; already defined", offload, keyword(), name()); return (false); } pg_offload = offload; return (true); } bool portal_group::set_pcp(u_int pcp) { if (pcp > 7) { log_warnx("invalid PCP value %u for %s \"%s\"", pcp, keyword(), name()); return (false); } pg_pcp = pcp; return (true); } bool portal_group::set_redirection(const char *addr) { if (!pg_redirection.empty()) { log_warnx("cannot set redirection to \"%s\" for " "%s \"%s\"; already defined", addr, keyword(), name()); return (false); } pg_redirection = addr; return (true); } void portal_group::set_tag(uint16_t tag) { pg_tag = tag; } void portal_group::verify(struct conf *conf) { if (pg_discovery_auth_group == nullptr) { pg_discovery_auth_group = conf->find_auth_group("default"); assert(pg_discovery_auth_group != nullptr); } if (pg_discovery_filter == discovery_filter::UNKNOWN) pg_discovery_filter = discovery_filter::NONE; if (!pg_redirection.empty()) { if (!pg_ports.empty()) { log_debugx("%s \"%s\" assigned to target, " "but configured for redirection", keyword(), name()); } pg_assigned = true; } else if (!pg_ports.empty()) { pg_assigned = true; } else { if (pg_name != "default") log_warnx("%s \"%s\" not assigned " "to any target", keyword(), name()); pg_assigned = false; } } /* * Try to reuse a socket for 'newp' from an existing socket in one of * our portals. */ bool portal_group::reuse_socket(struct portal &newp) { for (portal_up &portal : pg_portals) { if (newp.reuse_socket(*portal)) return (true); } return (false); } int portal_group::open_sockets(struct conf &oldconf) { int cumulated_error = 0; if (pg_foreign) return (0); if (!pg_assigned) { log_debugx("not listening on %s \"%s\", " "not assigned to any target", keyword(), name()); return (0); } for (portal_up &portal : pg_portals) { /* * Try to find already open portal and reuse the * listening socket. We don't care about what portal * or portal group that was, what matters is the * listening address. */ if (oldconf.reuse_portal_group_socket(*portal)) continue; if (!portal->init_socket()) { cumulated_error++; continue; } } return (cumulated_error); } void portal_group::close_sockets() { for (portal_up &portal : pg_portals) { if (portal->socket() < 0) continue; log_debugx("closing socket for %s, %s \"%s\"", portal->listen(), keyword(), name()); portal->close(); } } bool conf::add_isns(const char *addr) { if (conf_isns.count(addr) > 0) { log_warnx("duplicate iSNS address %s", addr); return (false); } freebsd::addrinfo_up ai = parse_addr_port(addr, "3205"); if (!ai) { log_warnx("invalid iSNS address %s", addr); return (false); } /* * XXX: getaddrinfo(3) may return multiple addresses; we should turn * those into multiple servers. */ conf_isns.emplace(addr, isns(addr, std::move(ai))); return (true); } freebsd::fd_up isns::connect() { freebsd::fd_up s; s = socket(i_ai->ai_family, i_ai->ai_socktype, i_ai->ai_protocol); if (!s) { log_warn("socket(2) failed for %s", addr()); return (s); } if (::connect(s, i_ai->ai_addr, i_ai->ai_addrlen)) { log_warn("connect(2) failed for %s", addr()); s.reset(); } return (s); } bool isns::send_request(int s, struct isns_req req) { if (!req.send(s)) { log_warn("send(2) failed for %s", addr()); return (false); } if (!req.receive(s)) { log_warn("receive(2) failed for %s", addr()); return (false); } uint32_t error = req.get_status(); if (error != 0) { log_warnx("iSNS %s error %u for %s", req.descr(), error, addr()); return (false); } return (true); } struct isns_req conf::isns_register_request(const char *hostname) { const struct portal_group *pg; isns_req req(ISNS_FUNC_DEVATTRREG, ISNS_FLAG_CLIENT, "register"); req.add_str(32, conf_first_target->name()); req.add_delim(); req.add_str(1, hostname); req.add_32(2, 2); /* 2 -- iSCSI */ req.add_32(6, conf_isns_period); for (const auto &kv : conf_portal_groups) { pg = kv.second.get(); if (!pg->assigned()) continue; for (const portal_up &portal : pg->portals()) { req.add_addr(16, portal->ai()); req.add_port(17, portal->ai()); } } for (const auto &kv : conf_targets) { const struct target *target = kv.second.get(); req.add_str(32, target->name()); req.add_32(33, 1); /* 1 -- Target*/ if (target->has_alias()) req.add_str(34, target->alias()); for (const port *port : target->ports()) { pg = port->portal_group(); if (pg == nullptr) continue; req.add_32(51, pg->tag()); for (const portal_up &portal : pg->portals()) { req.add_addr(49, portal->ai()); req.add_port(50, portal->ai()); } } } return (req); } struct isns_req conf::isns_check_request(const char *hostname) { isns_req req(ISNS_FUNC_DEVATTRQRY, ISNS_FLAG_CLIENT, "check"); req.add_str(32, conf_first_target->name()); req.add_str(1, hostname); req.add_delim(); req.add(2, 0, NULL); return (req); } struct isns_req conf::isns_deregister_request(const char *hostname) { isns_req req(ISNS_FUNC_DEVDEREG, ISNS_FLAG_CLIENT, "deregister"); req.add_str(32, conf_first_target->name()); req.add_delim(); req.add_str(1, hostname); return (req); } void conf::isns_register_targets(struct isns *isns, struct conf *oldconf) { int error; char hostname[256]; if (conf_targets.empty() || conf_portal_groups.empty()) return; start_timer(conf_isns_timeout); freebsd::fd_up s = isns->connect(); if (!s) { stop_timer(); return; } error = gethostname(hostname, sizeof(hostname)); if (error != 0) log_err(1, "gethostname"); if (oldconf == nullptr || oldconf->conf_first_target == nullptr) oldconf = this; isns->send_request(s, oldconf->isns_deregister_request(hostname)); isns->send_request(s, isns_register_request(hostname)); s.reset(); stop_timer(); } void conf::isns_check(struct isns *isns) { int error; char hostname[256]; if (conf_targets.empty() || conf_portal_groups.empty()) return; start_timer(conf_isns_timeout); freebsd::fd_up s = isns->connect(); if (!s) { stop_timer(); return; } error = gethostname(hostname, sizeof(hostname)); if (error != 0) log_err(1, "gethostname"); if (!isns->send_request(s, isns_check_request(hostname))) { isns->send_request(s, isns_deregister_request(hostname)); isns->send_request(s, isns_register_request(hostname)); } s.reset(); stop_timer(); } void conf::isns_deregister_targets(struct isns *isns) { int error; char hostname[256]; if (conf_targets.empty() || conf_portal_groups.empty()) return; start_timer(conf_isns_timeout); freebsd::fd_up s = isns->connect(); if (!s) return; error = gethostname(hostname, sizeof(hostname)); if (error != 0) log_err(1, "gethostname"); isns->send_request(s, isns_deregister_request(hostname)); s.reset(); stop_timer(); } void conf::isns_schedule_update() { if (!conf_isns.empty()) start_timer((conf_isns_period + 2) / 3); } void conf::isns_update() { stop_timer(); for (auto &kv : conf_isns) isns_check(&kv.second); isns_schedule_update(); } bool kports::add_port(std::string &name, uint32_t ctl_port) { const auto &pair = pports.try_emplace(name, name, ctl_port); if (!pair.second) { log_warnx("duplicate kernel port \"%s\" (%u)", name.c_str(), ctl_port); return (false); } return (true); } bool kports::has_port(std::string_view name) { return (pports.count(std::string(name)) > 0); } struct pport * kports::find_port(std::string_view name) { auto it = pports.find(std::string(name)); if (it == pports.end()) return (nullptr); return (&it->second); } port::port(struct target *target) : p_target(target) { target->add_port(this); } void port::clear_references() { p_target->remove_port(this); } portal_group_port::portal_group_port(struct target *target, struct portal_group *pg, auth_group_sp ag) : port(target), p_auth_group(ag), p_portal_group(pg) { p_portal_group->add_port(this); } portal_group_port::portal_group_port(struct target *target, struct portal_group *pg, uint32_t ctl_port) : port(target), p_portal_group(pg) { p_ctl_port = ctl_port; p_portal_group->add_port(this); } bool portal_group_port::is_dummy() const { return (p_portal_group->is_dummy()); } void portal_group_port::clear_references() { p_portal_group->remove_port(this); port::clear_references(); } bool conf::add_port(struct target *target, struct portal_group *pg, auth_group_sp ag) { std::string name = freebsd::stringf("%s-%s", pg->name(), target->name()); const auto &pair = conf_ports.try_emplace(name, pg->create_port(target, ag)); if (!pair.second) { log_warnx("duplicate port \"%s\"", name.c_str()); return (false); } return (true); } bool conf::add_port(struct target *target, struct portal_group *pg, uint32_t ctl_port) { std::string name = freebsd::stringf("%s-%s", pg->name(), target->name()); const auto &pair = conf_ports.try_emplace(name, pg->create_port(target, ctl_port)); if (!pair.second) { log_warnx("duplicate port \"%s\"", name.c_str()); return (false); } return (true); } bool conf::add_port(struct target *target, struct pport *pp) { std::string name = freebsd::stringf("%s-%s", pp->name(), target->name()); const auto &pair = conf_ports.try_emplace(name, std::make_unique(target, pp)); if (!pair.second) { log_warnx("duplicate port \"%s\"", name.c_str()); return (false); } pp->link(); return (true); } bool conf::add_port(struct kports &kports, struct target *target, int pp, int vp) { struct pport *pport; std::string pname = freebsd::stringf("ioctl/%d/%d", pp, vp); pport = kports.find_port(pname); if (pport != NULL) return (add_port(target, pport)); std::string name = pname + "-" + target->name(); const auto &pair = conf_ports.try_emplace(name, std::make_unique(target, pp, vp)); if (!pair.second) { log_warnx("duplicate port \"%s\"", name.c_str()); return (false); } return (true); } const struct port * portal_group::find_port(std::string_view target) const { auto it = pg_ports.find(std::string(target)); if (it == pg_ports.end()) return (nullptr); return (it->second); } +struct target * +conf::add_controller(const char *name) +{ + if (!nvmf_nqn_valid_strict(name)) { + log_warnx("controller name \"%s\" is invalid for NVMe", name); + return nullptr; + } + + /* + * Normalize the name to lowercase to match iSCSI. + */ + std::string t_name(name); + for (char &c : t_name) + c = tolower(c); + + auto const &pair = conf_controllers.try_emplace(t_name, + nvmf_make_controller(this, t_name)); + if (!pair.second) { + log_warnx("duplicated controller \"%s\"", name); + return nullptr; + } + + return pair.first->second.get(); +} + +struct target * +conf::find_controller(std::string_view name) +{ + auto it = conf_controllers.find(std::string(name)); + if (it == conf_controllers.end()) + return nullptr; + return it->second.get(); +} + target::target(struct conf *conf, const char *keyword, std::string_view name) : t_conf(conf), t_name(name) { t_label = freebsd::stringf("%s \"%s\"", keyword, t_name.c_str()); } struct target * conf::add_target(const char *name) { if (!valid_iscsi_name(name, log_warnx)) return (nullptr); /* * RFC 3722 requires us to normalize the name to lowercase. */ std::string t_name(name); for (char &c : t_name) c = tolower(c); auto const &pair = conf_targets.try_emplace(t_name, iscsi_make_target(this, t_name)); if (!pair.second) { log_warnx("duplicated target \"%s\"", name); return (NULL); } if (conf_first_target == nullptr) conf_first_target = pair.first->second.get(); return (pair.first->second.get()); } struct target * conf::find_target(std::string_view name) { auto it = conf_targets.find(std::string(name)); if (it == conf_targets.end()) return (nullptr); return (it->second.get()); } bool target::use_private_auth(const char *keyword) { if (t_private_auth) return (true); if (t_auth_group != nullptr) { log_warnx("cannot use both auth-group and %s for %s", keyword, label()); return (false); } t_auth_group = std::make_shared(t_label); t_private_auth = true; return (true); } bool target::add_chap(const char *user, const char *secret) { if (!use_private_auth("chap")) return (false); return (t_auth_group->add_chap(user, secret)); } bool target::add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2) { if (!use_private_auth("chap-mutual")) return (false); return (t_auth_group->add_chap_mutual(user, secret, user2, secret2)); } bool target::add_lun(u_int id, const char *lun_label, const char *lun_name) { struct lun *t_lun; if (id >= MAX_LUNS) { log_warnx("%s too big for %s", lun_label, label()); return (false); } if (t_luns[id] != NULL) { log_warnx("duplicate %s for %s", lun_label, label()); return (false); } t_lun = t_conf->find_lun(lun_name); if (t_lun == NULL) { log_warnx("unknown LUN named %s used for %s", lun_name, label()); return (false); } t_luns[id] = t_lun; return (true); } bool target::set_alias(std::string_view alias) { if (has_alias()) { log_warnx("alias for %s specified more than once", label()); return (false); } t_alias = alias; return (true); } bool target::set_auth_group(const char *ag_name) { if (t_auth_group != nullptr) { if (t_private_auth) log_warnx("cannot use both auth-group and explicit " "authorisations for %s", label()); else log_warnx("auth-group for %s " "specified more than once", label()); return (false); } t_auth_group = t_conf->find_auth_group(ag_name); if (t_auth_group == nullptr) { log_warnx("unknown auth-group \"%s\" for %s", ag_name, label()); return (false); } return (true); } bool target::set_auth_type(const char *type) { if (!use_private_auth("auth-type")) return (false); return (t_auth_group->set_type(type)); } bool target::set_physical_port(std::string_view pport) { if (!t_pport.empty()) { log_warnx("cannot set multiple physical ports for target " "\"%s\"", name()); return (false); } t_pport = pport; return (true); } bool target::set_redirection(const char *addr) { if (!t_redirection.empty()) { log_warnx("cannot set redirection to \"%s\" for " "%s; already defined", addr, label()); return (false); } t_redirection = addr; return (true); } struct lun * target::start_lun(u_int id, const char *lun_label, const char *lun_name) { if (id >= MAX_LUNS) { log_warnx("%s too big for %s", lun_label, label()); return (nullptr); } if (t_luns[id] != NULL) { log_warnx("duplicate %s for %s", lun_label, label()); return (nullptr); } struct lun *new_lun = t_conf->add_lun(lun_name); if (new_lun == nullptr) return (nullptr); new_lun->set_scsiname(lun_name); t_luns[id] = new_lun; return (new_lun); } void target::add_port(struct port *port) { t_ports.push_back(port); } void target::remove_port(struct port *port) { t_ports.remove(port); } void target::remove_lun(struct lun *lun) { /* XXX: clang is not able to deduce the type without the cast. */ std::replace(t_luns.begin(), t_luns.end(), lun, static_cast(nullptr)); } void target::verify() { if (t_auth_group == nullptr) { t_auth_group = t_conf->find_auth_group("default"); assert(t_auth_group != nullptr); } if (t_ports.empty()) { struct portal_group *pg = default_portal_group(); assert(pg != NULL); t_conf->add_port(this, pg, nullptr); } bool found = std::any_of(t_luns.begin(), t_luns.end(), [](struct lun *lun) { return (lun != nullptr); }); if (!found && t_redirection.empty()) log_warnx("no LUNs defined for %s", label()); if (found && !t_redirection.empty()) log_debugx("%s contains LUNs, but configured " "for redirection", label()); } lun::lun(struct conf *conf, std::string_view name) : l_conf(conf), l_options(nvlist_create(0)), l_name(name) { } struct lun * conf::add_lun(const char *name) { const auto &pair = conf_luns.try_emplace(name, std::make_unique(this, name)); if (!pair.second) { log_warnx("duplicated lun \"%s\"", name); return (NULL); } return (pair.first->second.get()); } void conf::delete_target_luns(struct lun *lun) { for (const auto &kv : conf_targets) kv.second->remove_lun(lun); + for (const auto &kv : conf_controllers) + kv.second->remove_lun(lun); } struct lun * conf::find_lun(std::string_view name) { auto it = conf_luns.find(std::string(name)); if (it == conf_luns.end()) return (nullptr); return (it->second.get()); } static void nvlist_replace_string(nvlist_t *nvl, const char *name, const char *value) { if (nvlist_exists_string(nvl, name)) nvlist_free_string(nvl, name); nvlist_add_string(nvl, name, value); } freebsd::nvlist_up lun::options() const { freebsd::nvlist_up nvl(nvlist_clone(l_options.get())); if (!l_path.empty()) nvlist_replace_string(nvl.get(), "file", l_path.c_str()); nvlist_replace_string(nvl.get(), "ctld_name", l_name.c_str()); if (!nvlist_exists_string(nvl.get(), "scsiname") && !l_scsiname.empty()) nvlist_add_string(nvl.get(), "scsiname", l_scsiname.c_str()); return (nvl); } bool lun::add_option(const char *name, const char *value) { return (option_new(l_options.get(), name, value)); } bool lun::set_backend(std::string_view value) { if (!l_backend.empty()) { log_warnx("backend for lun \"%s\" specified more than once", name()); return (false); } l_backend = value; return (true); } bool lun::set_blocksize(size_t value) { if (l_blocksize != 0) { log_warnx("blocksize for lun \"%s\" specified more than once", name()); return (false); } l_blocksize = value; return (true); } bool lun::set_ctl_lun(uint32_t value) { if (l_ctl_lun >= 0) { log_warnx("ctl_lun for lun \"%s\" specified more than once", name()); return (false); } l_ctl_lun = value; return (true); } bool lun::set_device_type(uint8_t device_type) { if (device_type > 15) { log_warnx("invalid device-type \"%u\" for lun \"%s\"", device_type, name()); return (false); } l_device_type = device_type; return (true); } bool lun::set_device_type(const char *value) { const char *errstr; int device_type; if (strcasecmp(value, "disk") == 0 || strcasecmp(value, "direct") == 0) device_type = T_DIRECT; else if (strcasecmp(value, "processor") == 0) device_type = T_PROCESSOR; else if (strcasecmp(value, "cd") == 0 || strcasecmp(value, "cdrom") == 0 || strcasecmp(value, "dvd") == 0 || strcasecmp(value, "dvdrom") == 0) device_type = T_CDROM; else { device_type = strtonum(value, 0, 15, &errstr); if (errstr != NULL) { log_warnx("invalid device-type \"%s\" for lun \"%s\"", value, name()); return (false); } } l_device_type = device_type; return (true); } bool lun::set_device_id(std::string_view value) { if (!l_device_id.empty()) { log_warnx("device_id for lun \"%s\" specified more than once", name()); return (false); } l_device_id = value; return (true); } bool lun::set_path(std::string_view value) { if (!l_path.empty()) { log_warnx("path for lun \"%s\" specified more than once", name()); return (false); } l_path = value; return (true); } void lun::set_scsiname(std::string_view value) { l_scsiname = value; } bool lun::set_serial(std::string_view value) { if (!l_serial.empty()) { log_warnx("serial for lun \"%s\" specified more than once", name()); return (false); } l_serial = value; return (true); } bool lun::set_size(uint64_t value) { if (l_size != 0) { log_warnx("size for lun \"%s\" specified more than once", name()); return (false); } l_size = value; return (true); } bool lun::changed(const struct lun &newlun) const { if (l_backend != newlun.l_backend) { log_debugx("backend for lun \"%s\", CTL lun %d changed; " "removing", name(), l_ctl_lun); return (true); } if (l_blocksize != newlun.l_blocksize) { log_debugx("blocksize for lun \"%s\", CTL lun %d changed; " "removing", name(), l_ctl_lun); return (true); } if (l_device_id != newlun.l_device_id) { log_debugx("device-id for lun \"%s\", CTL lun %d changed; " "removing", name(), l_ctl_lun); return (true); } if (l_path != newlun.l_path) { log_debugx("path for lun \"%s\", CTL lun %d, changed; " "removing", name(), l_ctl_lun); return (true); } if (l_serial != newlun.l_serial) { log_debugx("serial for lun \"%s\", CTL lun %d changed; " "removing", name(), l_ctl_lun); return (true); } return (false); } bool option_new(nvlist_t *nvl, const char *name, const char *value) { int error; if (nvlist_exists_string(nvl, name)) { log_warnx("duplicated option \"%s\"", name); return (false); } nvlist_add_string(nvl, name, value); error = nvlist_error(nvl); if (error != 0) { log_warnc(error, "failed to add option \"%s\"", name); return (false); } return (true); } bool lun::verify() { if (l_backend.empty()) l_backend = "block"; if (l_backend == "block") { if (l_path.empty()) { log_warnx("missing path for lun \"%s\"", name()); return (false); } } else if (l_backend == "ramdisk") { if (l_size == 0) { log_warnx("missing size for ramdisk-backed lun \"%s\"", name()); return (false); } if (!l_path.empty()) { log_warnx("path must not be specified " "for ramdisk-backed lun \"%s\"", name()); return (false); } } if (l_blocksize == 0) { if (l_device_type == T_CDROM) l_blocksize = DEFAULT_CD_BLOCKSIZE; else l_blocksize = DEFAULT_BLOCKSIZE; } else if (l_blocksize < 0) { log_warnx("invalid blocksize %d for lun \"%s\"; " "must be larger than 0", l_blocksize, name()); return (false); } if (l_size != 0 && (l_size % l_blocksize) != 0) { log_warnx("invalid size for lun \"%s\"; " "must be multiple of blocksize", name()); return (false); } return (true); } bool conf::verify() { if (conf_pidfile_path.empty()) conf_pidfile_path = DEFAULT_PIDFILE; std::unordered_map path_map; for (const auto &kv : conf_luns) { struct lun *lun = kv.second.get(); if (!lun->verify()) return (false); const std::string &path = lun->path(); if (path.empty()) continue; const auto &pair = path_map.try_emplace(path, lun); if (!pair.second) { struct lun *lun2 = pair.first->second; log_debugx("WARNING: path \"%s\" duplicated " "between lun \"%s\", and " "lun \"%s\"", path.c_str(), lun->name(), lun2->name()); } } for (auto &kv : conf_targets) { kv.second->verify(); } + for (auto &kv : conf_controllers) { + kv.second->verify(); + } for (auto &kv : conf_portal_groups) { kv.second->verify(this); } + for (auto &kv : conf_transport_groups) { + kv.second->verify(this); + } for (const auto &kv : conf_auth_groups) { const std::string &ag_name = kv.first; if (ag_name == "default" || ag_name == "no-authentication" || ag_name == "no-access") continue; if (kv.second.use_count() == 1) { log_warnx("auth-group \"%s\" not assigned " "to any target", ag_name.c_str()); } } return (true); } bool portal::reuse_socket(struct portal &oldp) { struct kevent kev; if (p_listen != oldp.p_listen) return (false); if (!oldp.p_socket) return (false); EV_SET(&kev, oldp.p_socket, EVFILT_READ, EV_ADD, 0, 0, this); if (kevent(kqfd, &kev, 1, NULL, 0, NULL) == -1) return (false); p_socket = std::move(oldp.p_socket); return (true); } bool portal::init_socket() { struct portal_group *pg = portal_group(); struct kevent kev; freebsd::fd_up s; int error; int one = 1; #ifdef ICL_KERNEL_PROXY if (proxy_mode) { int id = pg->conf()->add_proxy_portal(this); log_debugx("listening on %s, %s \"%s\", " "portal id %d, using ICL proxy", listen(), pg->keyword(), pg->name(), id); kernel_listen(ai(), protocol() == ISER, id); return (true); } #endif assert(proxy_mode == false); assert(protocol() != portal_protocol::ISER); log_debugx("listening on %s, %s \"%s\"", listen(), pg->keyword(), pg->name()); s = ::socket(p_ai->ai_family, p_ai->ai_socktype, p_ai->ai_protocol); if (!s) { log_warn("socket(2) failed for %s", listen()); return (false); } if (setsockopt(s, SOL_SOCKET, SO_NO_DDP, &one, sizeof(one)) == -1) log_warn("setsockopt(SO_NO_DDP) failed for %s", listen()); error = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); if (error != 0) { log_warn("setsockopt(SO_REUSEADDR) failed for %s", listen()); return (false); } if (pg->dscp() != -1) { /* Only allow the 6-bit DSCP field to be modified */ int tos = pg->dscp() << 2; switch (p_ai->ai_family) { case AF_INET: if (setsockopt(s, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)) == -1) log_warn("setsockopt(IP_TOS) failed for %s", listen()); break; case AF_INET6: if (setsockopt(s, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos)) == -1) log_warn("setsockopt(IPV6_TCLASS) failed for %s", listen()); break; } } if (pg->pcp() != -1) { int pcp = pg->pcp(); switch (p_ai->ai_family) { case AF_INET: if (setsockopt(s, IPPROTO_IP, IP_VLAN_PCP, &pcp, sizeof(pcp)) == -1) log_warn("setsockopt(IP_VLAN_PCP) failed for %s", listen()); break; case AF_INET6: if (setsockopt(s, IPPROTO_IPV6, IPV6_VLAN_PCP, &pcp, sizeof(pcp)) == -1) log_warn("setsockopt(IPV6_VLAN_PCP) failed for %s", listen()); break; } } if (!init_socket_options(s)) return (false); error = bind(s, p_ai->ai_addr, p_ai->ai_addrlen); if (error != 0) { log_warn("bind(2) failed for %s", listen()); return (false); } error = ::listen(s, -1); if (error != 0) { log_warn("listen(2) failed for %s", listen()); return (false); } EV_SET(&kev, s, EVFILT_READ, EV_ADD, 0, 0, this); error = kevent(kqfd, &kev, 1, NULL, 0, NULL); if (error == -1) { log_warn("kevent(2) failed to register for %s", listen()); return (false); } p_socket = std::move(s); return (true); } bool conf::reuse_portal_group_socket(struct portal &newp) { for (auto &kv : conf_portal_groups) { struct portal_group &pg = *kv.second; if (pg.reuse_socket(newp)) return (true); } + for (auto &kv : conf_transport_groups) { + struct portal_group &pg = *kv.second; + + if (pg.reuse_socket(newp)) + return (true); + } return (false); } int conf::apply(struct conf *oldconf) { int cumulated_error = 0; if (oldconf->conf_debug != conf_debug) { log_debugx("changing debug level to %d", conf_debug); log_init(conf_debug); } /* * On startup, oldconf created via conf_new_from_kernel will * not contain a valid pidfile_path, and the current * conf_pidfile will already own the pidfile. On shutdown, * the temporary newconf will not contain a valid * pidfile_path, and the pidfile will be cleaned up when the * oldconf is deleted. */ if (!oldconf->conf_pidfile_path.empty() && !conf_pidfile_path.empty()) { if (oldconf->conf_pidfile_path != conf_pidfile_path) { /* pidfile has changed. rename it */ log_debugx("moving pidfile to %s", conf_pidfile_path.c_str()); if (rename(oldconf->conf_pidfile_path.c_str(), conf_pidfile_path.c_str()) != 0) { log_err(1, "renaming pidfile %s -> %s", oldconf->conf_pidfile_path.c_str(), conf_pidfile_path.c_str()); } } conf_pidfile = std::move(oldconf->conf_pidfile); } /* * Go through the new portal groups, assigning tags or preserving old. */ for (auto &kv : conf_portal_groups) { struct portal_group &newpg = *kv.second; if (newpg.tag() != 0) continue; auto it = oldconf->conf_portal_groups.find(kv.first); if (it != oldconf->conf_portal_groups.end()) newpg.set_tag(it->second->tag()); else newpg.allocate_tag(); } + for (auto &kv : conf_transport_groups) { + struct portal_group &newpg = *kv.second; + + if (newpg.tag() != 0) + continue; + auto it = oldconf->conf_transport_groups.find(kv.first); + if (it != oldconf->conf_transport_groups.end()) + newpg.set_tag(it->second->tag()); + else + newpg.allocate_tag(); + } /* Deregister on removed iSNS servers. */ for (auto &kv : oldconf->conf_isns) { if (conf_isns.count(kv.first) == 0) oldconf->isns_deregister_targets(&kv.second); } /* * XXX: If target or lun removal fails, we should somehow "move" * the old lun or target into this, so that subsequent * conf::apply() would try to remove them again. That would * be somewhat hairy, though, and lun deletion failures don't * really happen, so leave it as it is for now. */ /* * First, remove any ports present in the old configuration * and missing in the new one. */ for (const auto &kv : oldconf->conf_ports) { const std::string &name = kv.first; port *oldport = kv.second.get(); if (oldport->is_dummy()) continue; const auto it = conf_ports.find(name); if (it != conf_ports.end() && !it->second->is_dummy()) continue; log_debugx("removing port \"%s\"", name.c_str()); if (!oldport->kernel_remove()) { log_warnx("failed to remove port %s", name.c_str()); /* * XXX: Uncomment after fixing the root cause. * * cumulated_error++; */ } } /* * Second, remove any LUNs present in the old configuration * and missing in the new one. */ for (auto it = oldconf->conf_luns.begin(); it != oldconf->conf_luns.end(); ) { struct lun *oldlun = it->second.get(); auto newit = conf_luns.find(it->first); if (newit == conf_luns.end()) { log_debugx("lun \"%s\", CTL lun %d " "not found in new configuration; " "removing", oldlun->name(), oldlun->ctl_lun()); if (!oldlun->kernel_remove()) { log_warnx("failed to remove lun \"%s\", " "CTL lun %d", oldlun->name(), oldlun->ctl_lun()); cumulated_error++; } it++; continue; } /* * Also remove the LUNs changed by more than size. */ struct lun *newlun = newit->second.get(); if (oldlun->changed(*newlun)) { if (!oldlun->kernel_remove()) { log_warnx("failed to remove lun \"%s\", " "CTL lun %d", oldlun->name(), oldlun->ctl_lun()); cumulated_error++; } /* * Delete the lun from the old configuration * so it is added as a new LUN below. */ it = oldconf->conf_luns.erase(it); continue; } newlun->set_ctl_lun(oldlun->ctl_lun()); it++; } for (auto it = conf_luns.begin(); it != conf_luns.end(); ) { struct lun *newlun = it->second.get(); auto oldit = oldconf->conf_luns.find(it->first); if (oldit != oldconf->conf_luns.end()) { log_debugx("modifying lun \"%s\", CTL lun %d", newlun->name(), newlun->ctl_lun()); if (!newlun->kernel_modify()) { log_warnx("failed to " "modify lun \"%s\", CTL lun %d", newlun->name(), newlun->ctl_lun()); cumulated_error++; } it++; continue; } log_debugx("adding lun \"%s\"", newlun->name()); if (!newlun->kernel_add()) { log_warnx("failed to add lun \"%s\"", newlun->name()); delete_target_luns(newlun); it = conf_luns.erase(it); cumulated_error++; } else it++; } /* * Now add new ports or modify existing ones. */ for (auto it = conf_ports.begin(); it != conf_ports.end(); ) { const std::string &name = it->first; port *newport = it->second.get(); if (newport->is_dummy()) { it++; continue; } const auto oldit = oldconf->conf_ports.find(name); if (oldit == oldconf->conf_ports.end() || oldit->second->is_dummy()) { log_debugx("adding port \"%s\"", name.c_str()); if (!newport->kernel_add()) { log_warnx("failed to add port %s", name.c_str()); /* * XXX: Uncomment after fixing the * root cause. * * cumulated_error++; */ /* * conf "owns" the port, but other * objects contain pointers to this * port that must be removed before * deleting the port. */ newport->clear_references(); it = conf_ports.erase(it); } else it++; } else { log_debugx("updating port \"%s\"", name.c_str()); if (!newport->kernel_update(oldit->second.get())) log_warnx("failed to update port %s", name.c_str()); it++; } } /* * Go through the new portals, opening the sockets as necessary. */ for (auto &kv : conf_portal_groups) { cumulated_error += kv.second->open_sockets(*oldconf); } + for (auto &kv : conf_transport_groups) { + cumulated_error += kv.second->open_sockets(*oldconf); + } /* * Go through the no longer used sockets, closing them. */ for (auto &kv : oldconf->conf_portal_groups) { kv.second->close_sockets(); } + for (auto &kv : oldconf->conf_transport_groups) { + kv.second->close_sockets(); + } /* (Re-)Register on remaining/new iSNS servers. */ for (auto &kv : conf_isns) { auto it = oldconf->conf_isns.find(kv.first); if (it == oldconf->conf_isns.end()) isns_register_targets(&kv.second, nullptr); else isns_register_targets(&kv.second, oldconf); } isns_schedule_update(); return (cumulated_error); } bool timed_out(void) { return (sigalrm_received); } static void sigalrm_handler_fatal(int dummy __unused) { /* * It would be easiest to just log an error and exit. We can't * do this, though, because log_errx() is not signal safe, since * it calls syslog(3). Instead, set a flag checked by pdu_send() * and pdu_receive(), to call log_errx() there. Should they fail * to notice, we'll exit here one second later. */ if (sigalrm_received) { /* * Oh well. Just give up and quit. */ _exit(2); } sigalrm_received = true; } static void sigalrm_handler(int dummy __unused) { sigalrm_received = true; } void stop_timer() { struct itimerval itv; int error; log_debugx("session timeout disabled"); bzero(&itv, sizeof(itv)); error = setitimer(ITIMER_REAL, &itv, NULL); if (error != 0) log_err(1, "setitimer"); sigalrm_received = false; } void start_timer(int timeout, bool fatal) { struct sigaction sa; struct itimerval itv; int error; if (timeout <= 0) { stop_timer(); return; } sigalrm_received = false; bzero(&sa, sizeof(sa)); if (fatal) sa.sa_handler = sigalrm_handler_fatal; else sa.sa_handler = sigalrm_handler; sigfillset(&sa.sa_mask); error = sigaction(SIGALRM, &sa, NULL); if (error != 0) log_err(1, "sigaction"); /* * First SIGALRM will arive after timeout seconds. * If we do nothing, another one will arrive a second later. */ log_debugx("setting session timeout to %d seconds", timeout); bzero(&itv, sizeof(itv)); itv.it_interval.tv_sec = 1; itv.it_value.tv_sec = timeout; error = setitimer(ITIMER_REAL, &itv, NULL); if (error != 0) log_err(1, "setitimer"); } static int wait_for_children(bool block) { pid_t pid; int status; int num = 0; for (;;) { /* * If "block" is true, wait for at least one process. */ if (block && num == 0) pid = wait4(-1, &status, 0, NULL); else pid = wait4(-1, &status, WNOHANG, NULL); if (pid <= 0) break; if (WIFSIGNALED(status)) { log_warnx("child process %d terminated with signal %d", pid, WTERMSIG(status)); } else if (WEXITSTATUS(status) != 0) { log_warnx("child process %d terminated with exit status %d", pid, WEXITSTATUS(status)); } else { log_debugx("child process %d terminated gracefully", pid); } num++; } return (num); } static void handle_connection(struct portal *portal, freebsd::fd_up fd, const struct sockaddr *client_sa, bool dont_fork) { struct portal_group *pg; int error; pid_t pid; char host[NI_MAXHOST + 1]; struct conf *conf; pg = portal->portal_group(); conf = pg->conf(); if (dont_fork) { log_debugx("incoming connection; not forking due to -d flag"); } else { nchildren -= wait_for_children(false); assert(nchildren >= 0); while (conf->maxproc() > 0 && nchildren >= conf->maxproc()) { log_debugx("maxproc limit of %d child processes hit; " "waiting for child process to exit", conf->maxproc()); nchildren -= wait_for_children(true); assert(nchildren >= 0); } log_debugx("incoming connection; forking child process #%d", nchildren); nchildren++; pid = fork(); if (pid < 0) log_err(1, "fork"); if (pid > 0) return; conf->close_pidfile(); } error = getnameinfo(client_sa, client_sa->sa_len, host, sizeof(host), NULL, 0, NI_NUMERICHOST); if (error != 0) log_errx(1, "getnameinfo: %s", gai_strerror(error)); log_debugx("accepted connection from %s; portal group \"%s\"", host, pg->name()); log_set_peer_addr(host); setproctitle("%s", host); portal->handle_connection(std::move(fd), host, client_sa); log_debugx("nothing more to do; exiting"); exit(0); } static void main_loop(bool dont_fork) { struct kevent kev; struct portal *portal; struct sockaddr_storage client_sa; socklen_t client_salen; #ifdef ICL_KERNEL_PROXY int connection_id; int portal_id; #endif int error, client_fd; for (;;) { if (sighup_received || sigterm_received || timed_out()) return; #ifdef ICL_KERNEL_PROXY if (proxy_mode) { client_salen = sizeof(client_sa); kernel_accept(&connection_id, &portal_id, (struct sockaddr *)&client_sa, &client_salen); assert(client_salen >= client_sa.ss_len); log_debugx("incoming connection, id %d, portal id %d", connection_id, portal_id); portal = conf->proxy_portal(portal_id); if (portal == nullptr) log_errx(1, "kernel returned invalid portal_id %d", portal_id); handle_connection(portal, connection_id, (struct sockaddr *)&client_sa, dont_fork); } else { #endif assert(proxy_mode == false); error = kevent(kqfd, NULL, 0, &kev, 1, NULL); if (error == -1) { if (errno == EINTR) continue; log_err(1, "kevent"); } switch (kev.filter) { case EVFILT_READ: portal = reinterpret_cast(kev.udata); assert(portal->socket() == (int)kev.ident); client_salen = sizeof(client_sa); client_fd = accept(portal->socket(), (struct sockaddr *)&client_sa, &client_salen); if (client_fd < 0) { if (errno == ECONNABORTED) continue; log_err(1, "accept"); } assert(client_salen >= client_sa.ss_len); handle_connection(portal, client_fd, (struct sockaddr *)&client_sa, dont_fork); break; default: __assert_unreachable(); } #ifdef ICL_KERNEL_PROXY } #endif } } static void sighup_handler(int dummy __unused) { sighup_received = true; } static void sigterm_handler(int dummy __unused) { sigterm_received = true; } static void sigchld_handler(int dummy __unused) { /* * The only purpose of this handler is to make SIGCHLD * interrupt the ISCSIDWAIT ioctl(2), so we can call * wait_for_children(). */ } static void register_signals(void) { struct sigaction sa; int error; bzero(&sa, sizeof(sa)); sa.sa_handler = sighup_handler; sigfillset(&sa.sa_mask); error = sigaction(SIGHUP, &sa, NULL); if (error != 0) log_err(1, "sigaction"); sa.sa_handler = sigterm_handler; error = sigaction(SIGTERM, &sa, NULL); if (error != 0) log_err(1, "sigaction"); sa.sa_handler = sigterm_handler; error = sigaction(SIGINT, &sa, NULL); if (error != 0) log_err(1, "sigaction"); sa.sa_handler = sigchld_handler; error = sigaction(SIGCHLD, &sa, NULL); if (error != 0) log_err(1, "sigaction"); } static void check_perms(const char *path) { struct stat sb; int error; error = stat(path, &sb); if (error != 0) { log_warn("stat"); return; } if (sb.st_mode & S_IWOTH) { log_warnx("%s is world-writable", path); } else if (sb.st_mode & S_IROTH) { log_warnx("%s is world-readable", path); } else if (sb.st_mode & S_IXOTH) { /* * Ok, this one doesn't matter, but still do it, * just for consistency. */ log_warnx("%s is world-executable", path); } /* * XXX: Should we also check for owner != 0? */ } static conf_up conf_new_from_file(const char *path, bool ucl) { struct auth_group *ag; struct portal_group *pg; bool valid; log_debugx("obtaining configuration from %s", path); conf_up conf = std::make_unique(); ag = conf->add_auth_group("default"); assert(ag != NULL); ag = conf->add_auth_group("no-authentication"); assert(ag != NULL); ag->set_type(auth_type::NO_AUTHENTICATION); ag = conf->add_auth_group("no-access"); assert(ag != NULL); ag->set_type(auth_type::DENY); pg = conf->add_portal_group("default"); assert(pg != NULL); + pg = conf->add_transport_group("default"); + assert(pg != NULL); + conf_start(conf.get()); if (ucl) valid = uclparse_conf(path); else valid = parse_conf(path); conf_finish(); if (!valid) { conf.reset(); return {}; } check_perms(path); if (!conf->default_auth_group_defined()) { log_debugx("auth-group \"default\" not defined; " "going with defaults"); ag = conf->find_auth_group("default").get(); assert(ag != NULL); ag->set_type(auth_type::DENY); } if (!conf->default_portal_group_defined()) { log_debugx("portal-group \"default\" not defined; " "going with defaults"); pg = conf->find_portal_group("default"); assert(pg != NULL); pg->add_default_portals(); } + if (!conf->default_portal_group_defined()) { + log_debugx("transport-group \"default\" not defined; " + "going with defaults"); + pg = conf->find_transport_group("default"); + assert(pg != NULL); + pg->add_default_portals(); + } + if (!conf->verify()) { conf.reset(); return {}; } return (conf); } /* * If the config file specifies physical ports for any target, associate them * with the config file. If necessary, create them. */ bool conf::add_pports(struct kports &kports) { struct pport *pp; int ret, i_pp, i_vp; for (auto &kv : conf_targets) { struct target *targ = kv.second.get(); if (!targ->has_pport()) continue; ret = sscanf(targ->pport(), "ioctl/%d/%d", &i_pp, &i_vp); if (ret > 0) { if (!add_port(kports, targ, i_pp, i_vp)) { log_warnx("can't create new ioctl port " "for %s", targ->label()); return (false); } continue; } pp = kports.find_port(targ->pport()); if (pp == NULL) { log_warnx("unknown port \"%s\" for %s", targ->pport(), targ->label()); return (false); } if (pp->linked()) { log_warnx("can't link port \"%s\" to %s, " "port already linked to some target", targ->pport(), targ->label()); return (false); } if (!add_port(targ, pp)) { log_warnx("can't link port \"%s\" to %s", targ->pport(), targ->label()); return (false); } } return (true); } int main(int argc, char **argv) { struct kports kports; const char *config_path = DEFAULT_CONFIG_PATH; int debug = 0, ch, error; bool daemonize = true; bool test_config = false; bool use_ucl = false; while ((ch = getopt(argc, argv, "dtuf:R")) != -1) { switch (ch) { case 'd': daemonize = false; debug++; break; case 't': test_config = true; break; case 'u': use_ucl = true; break; case 'f': config_path = optarg; break; case 'R': #ifndef ICL_KERNEL_PROXY log_errx(1, "ctld(8) compiled without ICL_KERNEL_PROXY " "does not support iSER protocol"); #endif proxy_mode = true; break; case '?': default: usage(); } } argc -= optind; if (argc != 0) usage(); log_init(debug); kernel_init(); conf_up newconf = conf_new_from_file(config_path, use_ucl); if (newconf == NULL) log_errx(1, "configuration error; exiting"); if (test_config) return (0); newconf->open_pidfile(); register_signals(); conf_up oldconf = conf_new_from_kernel(kports); if (debug > 0) { oldconf->set_debug(debug); newconf->set_debug(debug); } if (!newconf->add_pports(kports)) log_errx(1, "Error associating physical ports; exiting"); if (daemonize) { log_debugx("daemonizing"); if (daemon(0, 0) == -1) { log_warn("cannot daemonize"); return (1); } } kqfd = kqueue(); if (kqfd == -1) { log_warn("Cannot create kqueue"); return (1); } error = newconf->apply(oldconf.get()); if (error != 0) log_errx(1, "failed to apply configuration; exiting"); oldconf.reset(); newconf->write_pidfile(); newconf->isns_schedule_update(); for (;;) { main_loop(!daemonize); if (sighup_received) { sighup_received = false; log_debugx("received SIGHUP, reloading configuration"); conf_up tmpconf = conf_new_from_file(config_path, use_ucl); if (tmpconf == NULL) { log_warnx("configuration error, " "continuing with old configuration"); } else if (!tmpconf->add_pports(kports)) { log_warnx("Error associating physical ports, " "continuing with old configuration"); } else { if (debug > 0) tmpconf->set_debug(debug); oldconf = std::move(newconf); newconf = std::move(tmpconf); error = newconf->apply(oldconf.get()); if (error != 0) log_warnx("failed to reload " "configuration"); oldconf.reset(); } } else if (sigterm_received) { log_debugx("exiting on signal; " "reloading empty configuration"); log_debugx("removing CTL iSCSI ports " "and terminating all connections"); oldconf = std::move(newconf); newconf = std::make_unique(); if (debug > 0) newconf->set_debug(debug); error = newconf->apply(oldconf.get()); if (error != 0) log_warnx("failed to apply configuration"); oldconf.reset(); log_warnx("exiting on signal"); return (0); } else { nchildren -= wait_for_children(false); assert(nchildren >= 0); if (timed_out()) { newconf->isns_update(); } } } /* NOTREACHED */ } diff --git a/usr.sbin/ctld/ctld.hh b/usr.sbin/ctld/ctld.hh index 6ecee3b73c4f..a5aab65e339b 100644 --- a/usr.sbin/ctld/ctld.hh +++ b/usr.sbin/ctld/ctld.hh @@ -1,600 +1,637 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2012 The FreeBSD Foundation * * This software was developed by Edward Tomasz Napierala under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 AUTHOR 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 AUTHOR 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. */ #ifndef __CTLD_HH__ #define __CTLD_HH__ #include #include #ifdef ICL_KERNEL_PROXY #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #define DEFAULT_CONFIG_PATH "/etc/ctl.conf" #define DEFAULT_PIDFILE "/var/run/ctld.pid" #define DEFAULT_BLOCKSIZE 512 #define DEFAULT_CD_BLOCKSIZE 2048 #define MAX_LUNS 1024 struct isns_req; struct port; struct auth { auth(std::string_view secret) : a_secret(secret) {} auth(std::string_view secret, std::string_view mutual_user, std::string_view mutual_secret) : a_secret(secret), a_mutual_user(mutual_user), a_mutual_secret(mutual_secret) {} bool mutual() const { return !a_mutual_user.empty(); } const char *secret() const { return a_secret.c_str(); } const char *mutual_user() const { return a_mutual_user.c_str(); } const char *mutual_secret() const { return a_mutual_secret.c_str(); } private: std::string a_secret; std::string a_mutual_user; std::string a_mutual_secret; }; struct auth_portal { bool matches(const struct sockaddr *sa) const; bool parse(const char *portal); private: struct sockaddr_storage ap_sa; int ap_mask = 0; }; enum class auth_type { UNKNOWN, DENY, NO_AUTHENTICATION, CHAP, CHAP_MUTUAL }; struct auth_group { auth_group(std::string label) : ag_label(label) {} auth_type type() const { return ag_type; } bool set_type(const char *str); void set_type(auth_type type); const char *label() const { return ag_label.c_str(); } bool add_chap(const char *user, const char *secret); bool add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2); const struct auth *find_auth(std::string_view user) const; + bool add_host_nqn(std::string_view nqn); + bool host_permitted(std::string_view nqn) const; + + bool add_host_address(const char *address); + bool host_permitted(const struct sockaddr *sa) const; + bool add_initiator_name(std::string_view initiator_name); bool initiator_permitted(std::string_view initiator_name) const; bool add_initiator_portal(const char *initiator_portal); bool initiator_permitted(const struct sockaddr *sa) const; private: void check_secret_length(const char *user, const char *secret, const char *secret_type); std::string ag_label; auth_type ag_type = auth_type::UNKNOWN; std::unordered_map ag_auths; + std::unordered_set ag_host_names; + std::list ag_host_addresses; std::unordered_set ag_initiator_names; std::list ag_initiator_portals; }; using auth_group_sp = std::shared_ptr; enum class portal_protocol { ISCSI, - ISER + ISER, + NVME_TCP, + NVME_DISCOVERY_TCP, }; struct portal { portal(struct portal_group *pg, std::string_view listen, portal_protocol protocol, freebsd::addrinfo_up ai) : p_portal_group(pg), p_listen(listen), p_ai(std::move(ai)), p_protocol(protocol) {} virtual ~portal() = default; bool reuse_socket(portal &oldp); bool init_socket(); virtual bool init_socket_options(int s __unused) { return true; } virtual void handle_connection(freebsd::fd_up fd, const char *host, const struct sockaddr *client_sa) = 0; - portal_group *portal_group() { return p_portal_group; } + portal_group *portal_group() const { return p_portal_group; } const char *listen() const { return p_listen.c_str(); } const addrinfo *ai() const { return p_ai.get(); } portal_protocol protocol() const { return p_protocol; } int socket() const { return p_socket; } void close() { p_socket.reset(); } private: struct portal_group *p_portal_group; std::string p_listen; freebsd::addrinfo_up p_ai; portal_protocol p_protocol; freebsd::fd_up p_socket; }; using portal_up = std::unique_ptr; using port_up = std::unique_ptr; enum class discovery_filter { UNKNOWN, NONE, PORTAL, PORTAL_NAME, PORTAL_NAME_AUTH }; struct portal_group { portal_group(struct conf *conf, std::string_view name); virtual ~portal_group() = default; struct conf *conf() const { return pg_conf; } virtual const char *keyword() const = 0; const char *name() const { return pg_name.c_str(); } bool assigned() const { return pg_assigned; } bool is_dummy() const; bool is_redirecting() const { return !pg_redirection.empty(); } struct auth_group *discovery_auth_group() const { return pg_discovery_auth_group.get(); } discovery_filter discovery_filter() const { return pg_discovery_filter; } int dscp() const { return pg_dscp; } const char *offload() const { return pg_offload.c_str(); } const char *redirection() const { return pg_redirection.c_str(); } int pcp() const { return pg_pcp; } uint16_t tag() const { return pg_tag; } freebsd::nvlist_up options() const; const std::list &portals() const { return pg_portals; } const std::unordered_map &ports() const { return pg_ports; } virtual void allocate_tag() = 0; virtual bool add_portal(const char *value, portal_protocol protocol) = 0; virtual void add_default_portals() = 0; bool add_option(const char *name, const char *value); bool set_discovery_auth_group(const char *name); bool set_dscp(u_int dscp); virtual bool set_filter(const char *str) = 0; void set_foreign(); bool set_offload(const char *offload); bool set_pcp(u_int pcp); bool set_redirection(const char *addr); void set_tag(uint16_t tag); virtual port_up create_port(struct target *target, auth_group_sp ag) = 0; virtual port_up create_port(struct target *target, uint32_t ctl_port) = 0; void add_port(struct portal_group_port *port); const struct port *find_port(std::string_view target) const; void remove_port(struct portal_group_port *port); void verify(struct conf *conf); bool reuse_socket(struct portal &newp); int open_sockets(struct conf &oldconf); void close_sockets(); protected: struct conf *pg_conf; freebsd::nvlist_up pg_options; const char *pg_keyword; std::string pg_name; auth_group_sp pg_discovery_auth_group; enum discovery_filter pg_discovery_filter = discovery_filter::UNKNOWN; bool pg_foreign = false; bool pg_assigned = false; std::list pg_portals; std::unordered_map pg_ports; std::string pg_offload; std::string pg_redirection; int pg_dscp = -1; int pg_pcp = -1; uint16_t pg_tag = 0; }; using portal_group_up = std::unique_ptr; struct port { port(struct target *target); virtual ~port() = default; struct target *target() const { return p_target; } virtual struct auth_group *auth_group() const { return nullptr; } virtual struct portal_group *portal_group() const { return nullptr; } virtual bool is_dummy() const { return true; } virtual void clear_references(); bool kernel_add(); bool kernel_update(const port *oport); bool kernel_remove(); virtual bool kernel_create_port() = 0; virtual bool kernel_remove_port() = 0; protected: struct target *p_target; uint32_t p_ctl_port = 0; }; struct portal_group_port : public port { portal_group_port(struct target *target, struct portal_group *pg, auth_group_sp ag); portal_group_port(struct target *target, struct portal_group *pg, uint32_t ctl_port); ~portal_group_port() override = default; struct auth_group *auth_group() const override { return p_auth_group.get(); } struct portal_group *portal_group() const override { return p_portal_group; } bool is_dummy() const override; void clear_references() override; protected: auth_group_sp p_auth_group; struct portal_group *p_portal_group; }; struct ioctl_port final : public port { ioctl_port(struct target *target, int pp, int vp) : port(target), p_ioctl_pp(pp), p_ioctl_vp(vp) {} ~ioctl_port() override = default; bool kernel_create_port() override; bool kernel_remove_port() override; private: int p_ioctl_pp; int p_ioctl_vp; }; struct kernel_port final : public port { kernel_port(struct target *target, struct pport *pp) : port(target), p_pport(pp) {} ~kernel_port() override = default; bool kernel_create_port() override; bool kernel_remove_port() override; private: struct pport *p_pport; }; struct lun { lun(struct conf *conf, std::string_view name); const char *name() const { return l_name.c_str(); } const std::string &path() const { return l_path; } int ctl_lun() const { return l_ctl_lun; } freebsd::nvlist_up options() const; bool add_option(const char *name, const char *value); bool set_backend(std::string_view value); bool set_blocksize(size_t value); bool set_ctl_lun(uint32_t value); bool set_device_type(uint8_t device_type); bool set_device_type(const char *value); bool set_device_id(std::string_view value); bool set_path(std::string_view value); void set_scsiname(std::string_view value); bool set_serial(std::string_view value); bool set_size(uint64_t value); bool changed(const struct lun &old) const; bool verify(); bool kernel_add(); bool kernel_modify() const; bool kernel_remove() const; private: struct conf *l_conf; freebsd::nvlist_up l_options; std::string l_name; std::string l_backend; uint8_t l_device_type = 0; int l_blocksize = 0; std::string l_device_id; std::string l_path; std::string l_scsiname; std::string l_serial; uint64_t l_size = 0; int l_ctl_lun = -1; }; struct target { target(struct conf *conf, const char *keyword, std::string_view name); virtual ~target() = default; bool has_alias() const { return !t_alias.empty(); } bool has_pport() const { return !t_pport.empty(); } bool has_redirection() const { return !t_redirection.empty(); } const char *alias() const { return t_alias.c_str(); } const char *name() const { return t_name.c_str(); } const char *label() const { return t_label.c_str(); } const char *pport() const { return t_pport.c_str(); } bool private_auth() const { return t_private_auth; } const char *redirection() const { return t_redirection.c_str(); } struct auth_group *auth_group() const { return t_auth_group.get(); } const std::list &ports() const { return t_ports; } const struct lun *lun(int idx) const { return t_luns[idx]; } bool add_chap(const char *user, const char *secret); bool add_chap_mutual(const char *user, const char *secret, const char *user2, const char *secret2); + virtual bool add_host_address(const char *) { return false; } + virtual bool add_host_nqn(std::string_view) { return false; } virtual bool add_initiator_name(std::string_view) { return false; } virtual bool add_initiator_portal(const char *) { return false; } virtual bool add_lun(u_int, const char *) { return false; } + virtual bool add_namespace(u_int, const char *) { return false; } virtual bool add_portal_group(const char *pg_name, const char *ag_name) = 0; bool set_alias(std::string_view alias); bool set_auth_group(const char *ag_name); bool set_auth_type(const char *type); bool set_physical_port(std::string_view pport); bool set_redirection(const char *addr); virtual struct lun *start_lun(u_int) { return nullptr; } + virtual struct lun *start_namespace(u_int) { return nullptr; } void add_port(struct port *port); void remove_lun(struct lun *lun); void remove_port(struct port *port); void verify(); protected: bool use_private_auth(const char *keyword); bool add_lun(u_int id, const char *lun_label, const char *lun_name); struct lun *start_lun(u_int id, const char *lun_label, const char *lun_name); virtual struct portal_group *default_portal_group() = 0; struct conf *t_conf; std::array t_luns; auth_group_sp t_auth_group; std::list t_ports; std::string t_name; std::string t_label; std::string t_alias; std::string t_redirection; /* Name of this target's physical port, if any, i.e. "isp0" */ std::string t_pport; bool t_private_auth; }; using target_up = std::unique_ptr; struct isns { isns(std::string_view addr, freebsd::addrinfo_up ai) : i_addr(addr), i_ai(std::move(ai)) {} const char *addr() const { return i_addr.c_str(); } freebsd::fd_up connect(); bool send_request(int s, struct isns_req req); private: std::string i_addr; freebsd::addrinfo_up i_ai; }; struct conf { + conf(); + int maxproc() const { return conf_maxproc; } int timeout() const { return conf_timeout; } + uint32_t genctr() const { return conf_genctr; } bool default_auth_group_defined() const { return conf_default_ag_defined; } bool default_portal_group_defined() const { return conf_default_pg_defined; } + bool default_transport_group_defined() const + { return conf_default_tg_defined; } struct auth_group *add_auth_group(const char *ag_name); struct auth_group *define_default_auth_group(); auth_group_sp find_auth_group(std::string_view ag_name); struct portal_group *add_portal_group(const char *name); struct portal_group *define_default_portal_group(); struct portal_group *find_portal_group(std::string_view name); + struct portal_group *add_transport_group(const char *name); + struct portal_group *define_default_transport_group(); + struct portal_group *find_transport_group(std::string_view name); + bool add_port(struct target *target, struct portal_group *pg, auth_group_sp ag); bool add_port(struct target *target, struct portal_group *pg, uint32_t ctl_port); bool add_port(struct target *target, struct pport *pp); bool add_port(struct kports &kports, struct target *target, int pp, int vp); bool add_pports(struct kports &kports); + struct target *add_controller(const char *name); + struct target *find_controller(std::string_view name); + struct target *add_target(const char *name); struct target *find_target(std::string_view name); struct lun *add_lun(const char *name); struct lun *find_lun(std::string_view name); void set_debug(int debug); void set_isns_period(int period); void set_isns_timeout(int timeout); void set_maxproc(int maxproc); bool set_pidfile_path(std::string_view path); void set_timeout(int timeout); void open_pidfile(); void write_pidfile(); void close_pidfile(); bool add_isns(const char *addr); void isns_register_targets(struct isns *isns, struct conf *oldconf); void isns_deregister_targets(struct isns *isns); void isns_schedule_update(); void isns_update(); int apply(struct conf *oldconf); void delete_target_luns(struct lun *lun); bool reuse_portal_group_socket(struct portal &newp); bool verify(); private: struct isns_req isns_register_request(const char *hostname); struct isns_req isns_check_request(const char *hostname); struct isns_req isns_deregister_request(const char *hostname); void isns_check(struct isns *isns); std::string conf_pidfile_path; std::unordered_map> conf_luns; - std::unordered_map> conf_targets; + std::unordered_map conf_targets; + std::unordered_map conf_controllers; std::unordered_map conf_auth_groups; std::unordered_map> conf_ports; std::unordered_map conf_portal_groups; + std::unordered_map conf_transport_groups; std::unordered_map conf_isns; struct target *conf_first_target = nullptr; int conf_isns_period = 900; int conf_isns_timeout = 5; int conf_debug = 0; int conf_timeout = 60; int conf_maxproc = 30; + uint32_t conf_genctr = 0; freebsd::pidfile conf_pidfile; bool conf_default_pg_defined = false; + bool conf_default_tg_defined = false; bool conf_default_ag_defined = false; + static uint32_t global_genctr; + #ifdef ICL_KERNEL_PROXY public: int add_proxy_portal(portal *); portal *proxy_portal(int); private: std::vector conf_proxy_portals; #endif }; using conf_up = std::unique_ptr; /* Physical ports exposed by the kernel */ struct pport { pport(std::string_view name, uint32_t ctl_port) : pp_name(name), pp_ctl_port(ctl_port) {} const char *name() const { return pp_name.c_str(); } uint32_t ctl_port() const { return pp_ctl_port; } bool linked() const { return pp_linked; } void link() { pp_linked = true; } private: std::string pp_name; uint32_t pp_ctl_port; bool pp_linked; }; struct kports { bool add_port(std::string &name, uint32_t ctl_port); bool has_port(std::string_view name); struct pport *find_port(std::string_view name); private: std::unordered_map pports; }; extern bool proxy_mode; extern int ctl_fd; bool parse_conf(const char *path); bool uclparse_conf(const char *path); conf_up conf_new_from_kernel(struct kports &kports); void conf_finish(void); void conf_start(struct conf *new_conf); bool option_new(nvlist_t *nvl, const char *name, const char *value); freebsd::addrinfo_up parse_addr_port(const char *address, const char *def_port); void kernel_init(void); void kernel_capsicate(void); #ifdef ICL_KERNEL_PROXY void kernel_listen(struct addrinfo *ai, bool iser, int portal_id); void kernel_accept(int *connection_id, int *portal_id, struct sockaddr *client_sa, socklen_t *client_salen); void kernel_send(struct pdu *pdu); void kernel_receive(struct pdu *pdu); #endif bool ctl_create_port(const char *driver, const nvlist_t *nvl, uint32_t *ctl_port); bool ctl_remove_port(const char *driver, nvlist_t *nvl); portal_group_up iscsi_make_portal_group(struct conf *conf, std::string_view name); target_up iscsi_make_target(struct conf *conf, std::string_view name); +portal_group_up nvmf_make_transport_group(struct conf *conf, + std::string_view name); +target_up nvmf_make_controller(struct conf *conf, + std::string_view name); + void start_timer(int timeout, bool fatal = false); void stop_timer(); bool timed_out(); #endif /* !__CTLD_HH__ */ diff --git a/usr.sbin/ctld/discovery.cc b/usr.sbin/ctld/discovery.cc index 9fca8502c81c..8f6d371b696d 100644 --- a/usr.sbin/ctld/discovery.cc +++ b/usr.sbin/ctld/discovery.cc @@ -1,269 +1,272 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2012 The FreeBSD Foundation * * This software was developed by Edward Tomasz Napierala under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 AUTHOR 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 AUTHOR 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. * */ #include #include #include #include #include #include #include #include #include "ctld.hh" #include "iscsi.hh" #include "iscsi_proto.h" static struct pdu * logout_receive(struct connection *conn) { struct pdu *request; struct iscsi_bhs_logout_request *bhslr; request = pdu_new(conn); pdu_receive(request); if ((request->pdu_bhs->bhs_opcode & ~ISCSI_BHS_OPCODE_IMMEDIATE) != ISCSI_BHS_OPCODE_LOGOUT_REQUEST) log_errx(1, "protocol error: received invalid opcode 0x%x", request->pdu_bhs->bhs_opcode); bhslr = (struct iscsi_bhs_logout_request *)request->pdu_bhs; if ((bhslr->bhslr_reason & 0x7f) != BHSLR_REASON_CLOSE_SESSION) log_debugx("received Logout PDU with invalid reason 0x%x; " "continuing anyway", bhslr->bhslr_reason & 0x7f); if (ISCSI_SNLT(ntohl(bhslr->bhslr_cmdsn), conn->conn_cmdsn)) { log_errx(1, "received Logout PDU with decreasing CmdSN: " "was %u, is %u", conn->conn_cmdsn, ntohl(bhslr->bhslr_cmdsn)); } if (ntohl(bhslr->bhslr_expstatsn) != conn->conn_statsn) { log_errx(1, "received Logout PDU with wrong ExpStatSN: " "is %u, should be %u", ntohl(bhslr->bhslr_expstatsn), conn->conn_statsn); } conn->conn_cmdsn = ntohl(bhslr->bhslr_cmdsn); if ((bhslr->bhslr_opcode & ISCSI_BHS_OPCODE_IMMEDIATE) == 0) conn->conn_cmdsn++; return (request); } static struct pdu * logout_new_response(struct pdu *request) { struct pdu *response; struct connection *conn; struct iscsi_bhs_logout_request *bhslr; struct iscsi_bhs_logout_response *bhslr2; bhslr = (struct iscsi_bhs_logout_request *)request->pdu_bhs; conn = request->pdu_connection; response = pdu_new_response(request); bhslr2 = (struct iscsi_bhs_logout_response *)response->pdu_bhs; bhslr2->bhslr_opcode = ISCSI_BHS_OPCODE_LOGOUT_RESPONSE; bhslr2->bhslr_flags = 0x80; bhslr2->bhslr_response = BHSLR_RESPONSE_CLOSED_SUCCESSFULLY; bhslr2->bhslr_initiator_task_tag = bhslr->bhslr_initiator_task_tag; bhslr2->bhslr_statsn = htonl(conn->conn_statsn++); bhslr2->bhslr_expcmdsn = htonl(conn->conn_cmdsn); bhslr2->bhslr_maxcmdsn = htonl(conn->conn_cmdsn); return (response); } static void discovery_add_target(struct keys *response_keys, const struct target *targ) { char *buf; char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV]; const struct addrinfo *ai; int ret; keys_add(response_keys, "TargetName", targ->name()); for (const port *port : targ->ports()) { const struct portal_group *pg = port->portal_group(); if (pg == nullptr) continue; for (const portal_up &portal : pg->portals()) { + if (portal->protocol() != portal_protocol::ISCSI && + portal->protocol() != portal_protocol::ISER) + continue; ai = portal->ai(); ret = getnameinfo(ai->ai_addr, ai->ai_addrlen, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV); if (ret != 0) { log_warnx("getnameinfo: %s", gai_strerror(ret)); continue; } switch (ai->ai_addr->sa_family) { case AF_INET: if (strcmp(hbuf, "0.0.0.0") == 0) continue; ret = asprintf(&buf, "%s:%s,%d", hbuf, sbuf, pg->tag()); break; case AF_INET6: if (strcmp(hbuf, "::") == 0) continue; ret = asprintf(&buf, "[%s]:%s,%d", hbuf, sbuf, pg->tag()); break; default: continue; } if (ret <= 0) log_err(1, "asprintf"); keys_add(response_keys, "TargetAddress", buf); free(buf); } } } bool iscsi_connection::discovery_target_filtered_out(const struct port *port) const { const struct auth_group *ag; const struct portal_group *pg; const struct target *targ; const struct auth *auth; int error; targ = port->target(); ag = port->auth_group(); if (ag == nullptr) ag = targ->auth_group(); pg = conn_portal->portal_group(); assert(pg->discovery_filter() != discovery_filter::UNKNOWN); if (pg->discovery_filter() >= discovery_filter::PORTAL && !ag->initiator_permitted(conn_initiator_sa)) { log_debugx("initiator does not match initiator portals " "allowed for target \"%s\"; skipping", targ->name()); return (true); } if (pg->discovery_filter() >= discovery_filter::PORTAL_NAME && !ag->initiator_permitted(conn_initiator_name)) { log_debugx("initiator does not match initiator names " "allowed for target \"%s\"; skipping", targ->name()); return (true); } if (pg->discovery_filter() >= discovery_filter::PORTAL_NAME_AUTH && ag->type() != auth_type::NO_AUTHENTICATION) { if (conn_chap == nullptr) { assert(pg->discovery_auth_group()->type() == auth_type::NO_AUTHENTICATION); log_debugx("initiator didn't authenticate, but target " "\"%s\" requires CHAP; skipping", targ->name()); return (true); } assert(!conn_user.empty()); auth = ag->find_auth(conn_user); if (auth == NULL) { log_debugx("CHAP user \"%s\" doesn't match target " "\"%s\"; skipping", conn_user.c_str(), targ->name()); return (true); } error = chap_authenticate(conn_chap, auth->secret()); if (error != 0) { log_debugx("password for CHAP user \"%s\" doesn't " "match target \"%s\"; skipping", conn_user.c_str(), targ->name()); return (true); } } return (false); } void iscsi_connection::discovery() { struct pdu *request, *response; struct keys *request_keys, *response_keys; const struct port *port; const struct portal_group *pg; const char *send_targets; pg = conn_portal->portal_group(); log_debugx("beginning discovery session; waiting for TextRequest PDU"); request_keys = text_read_request(&conn, &request); send_targets = keys_find(request_keys, "SendTargets"); if (send_targets == NULL) log_errx(1, "received TextRequest PDU without SendTargets"); response_keys = keys_new(); if (strcmp(send_targets, "All") == 0) { for (const auto &kv : pg->ports()) { port = kv.second; if (discovery_target_filtered_out(port)) { /* Ignore this target. */ continue; } discovery_add_target(response_keys, port->target()); } } else { port = pg->find_port(send_targets); if (port == NULL) { log_debugx("initiator requested information on unknown " "target \"%s\"; returning nothing", send_targets); } else { if (discovery_target_filtered_out(port)) { /* Ignore this target. */ } else { discovery_add_target(response_keys, port->target()); } } } text_send_response(request, response_keys); keys_delete(response_keys); pdu_delete(request); keys_delete(request_keys); log_debugx("done sending targets; waiting for Logout PDU"); request = logout_receive(&conn); response = logout_new_response(request); pdu_send(response); pdu_delete(response); pdu_delete(request); log_debugx("discovery session done"); } diff --git a/usr.sbin/ctld/kernel.cc b/usr.sbin/ctld/kernel.cc index 00330555a118..b214cd4e8c29 100644 --- a/usr.sbin/ctld/kernel.cc +++ b/usr.sbin/ctld/kernel.cc @@ -1,1080 +1,1130 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2003, 2004 Silicon Graphics International Corp. * Copyright (c) 1997-2007 Kenneth D. Merry * Copyright (c) 2012 The FreeBSD Foundation * Copyright (c) 2017 Jakub Wojciech Klama * All rights reserved. * * Portions of this software were developed by Edward Tomasz Napierala * under sponsorship from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions, and the following disclaimer, * without modification. * 2. Redistributions in binary form must reproduce at minimum a disclaimer * substantially similar to the "NO WARRANTY" disclaimer below * ("Disclaimer") and any redistribution must be conditioned upon * including a substantially similar Disclaimer requirement for further * binary redistribution. * * NO WARRANTY * 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 MERCHANTIBILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR 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 DAMAGES. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ctld.hh" #ifdef ICL_KERNEL_PROXY #include #endif #define NVLIST_BUFSIZE 1024 int ctl_fd = 0; void kernel_init(void) { int retval, saved_errno; ctl_fd = open(CTL_DEFAULT_DEV, O_RDWR); if (ctl_fd < 0 && errno == ENOENT) { saved_errno = errno; retval = kldload("ctl"); if (retval != -1) ctl_fd = open(CTL_DEFAULT_DEV, O_RDWR); else errno = saved_errno; } if (ctl_fd < 0) log_err(1, "failed to open %s", CTL_DEFAULT_DEV); #ifdef WANT_ISCSI else { saved_errno = errno; if (modfind("cfiscsi") == -1 && kldload("cfiscsi") == -1) log_warn("couldn't load cfiscsi"); errno = saved_errno; } #endif } /* * Backend LUN information. */ using attr_list = std::list>; struct cctl_lun { uint64_t lun_id; std::string backend_type; uint8_t device_type; uint64_t size_blocks; uint32_t blocksize; std::string serial_number; std::string device_id; std::string ctld_name; attr_list attr_list; }; struct cctl_port { uint32_t port_id; std::string port_frontend; std::string port_name; int pp; int vp; + uint16_t portid; int cfiscsi_state; std::string cfiscsi_target; + std::string nqn; uint16_t cfiscsi_portal_group_tag; std::string ctld_portal_group_name; + std::string ctld_transport_group_name; attr_list attr_list; }; struct cctl_devlist_data { std::list lun_list; struct cctl_lun *cur_lun = nullptr; std::list port_list; struct cctl_port *cur_port = nullptr; u_int level = 0; struct sbuf *cur_sb[32] = {}; }; static void cctl_start_element(void *user_data, const char *name, const char **attr) { int i; struct cctl_devlist_data *devlist; struct cctl_lun *cur_lun; devlist = (struct cctl_devlist_data *)user_data; cur_lun = devlist->cur_lun; devlist->level++; if (devlist->level >= nitems(devlist->cur_sb)) log_errx(1, "%s: too many nesting levels, %zu max", __func__, nitems(devlist->cur_sb)); devlist->cur_sb[devlist->level] = sbuf_new_auto(); if (devlist->cur_sb[devlist->level] == NULL) log_err(1, "%s: unable to allocate sbuf", __func__); if (strcmp(name, "lun") == 0) { if (cur_lun != NULL) log_errx(1, "%s: improper lun element nesting", __func__); devlist->lun_list.emplace_back(); cur_lun = &devlist->lun_list.back(); devlist->cur_lun = cur_lun; for (i = 0; attr[i] != NULL; i += 2) { if (strcmp(attr[i], "id") == 0) { cur_lun->lun_id = strtoull(attr[i+1], NULL, 0); } else { log_errx(1, "%s: invalid LUN attribute %s = %s", __func__, attr[i], attr[i+1]); } } } } static void cctl_end_element(void *user_data, const char *name) { struct cctl_devlist_data *devlist; struct cctl_lun *cur_lun; std::string str; devlist = (struct cctl_devlist_data *)user_data; cur_lun = devlist->cur_lun; if ((cur_lun == NULL) && (strcmp(name, "ctllunlist") != 0)) log_errx(1, "%s: cur_lun == NULL! (name = %s)", __func__, name); if (devlist->cur_sb[devlist->level] == NULL) log_errx(1, "%s: no valid sbuf at level %d (name %s)", __func__, devlist->level, name); sbuf_finish(devlist->cur_sb[devlist->level]); str = sbuf_data(devlist->cur_sb[devlist->level]); sbuf_delete(devlist->cur_sb[devlist->level]); devlist->cur_sb[devlist->level] = NULL; devlist->level--; if (strcmp(name, "backend_type") == 0) { cur_lun->backend_type = std::move(str); } else if (strcmp(name, "lun_type") == 0) { if (str.empty()) log_errx(1, "%s: %s missing its argument", __func__, name); cur_lun->device_type = strtoull(str.c_str(), NULL, 0); } else if (strcmp(name, "size") == 0) { if (str.empty()) log_errx(1, "%s: %s missing its argument", __func__, name); cur_lun->size_blocks = strtoull(str.c_str(), NULL, 0); } else if (strcmp(name, "blocksize") == 0) { if (str.empty()) log_errx(1, "%s: %s missing its argument", __func__, name); cur_lun->blocksize = strtoul(str.c_str(), NULL, 0); } else if (strcmp(name, "serial_number") == 0) { cur_lun->serial_number = std::move(str); } else if (strcmp(name, "device_id") == 0) { cur_lun->device_id = std::move(str); } else if (strcmp(name, "ctld_name") == 0) { cur_lun->ctld_name = std::move(str); } else if (strcmp(name, "lun") == 0) { devlist->cur_lun = NULL; } else if (strcmp(name, "ctllunlist") == 0) { /* Nothing. */ } else { cur_lun->attr_list.emplace_back(name, std::move(str)); } } static void cctl_start_pelement(void *user_data, const char *name, const char **attr) { int i; struct cctl_devlist_data *devlist; struct cctl_port *cur_port; devlist = (struct cctl_devlist_data *)user_data; cur_port = devlist->cur_port; devlist->level++; if (devlist->level >= nitems(devlist->cur_sb)) log_errx(1, "%s: too many nesting levels, %zu max", __func__, nitems(devlist->cur_sb)); devlist->cur_sb[devlist->level] = sbuf_new_auto(); if (devlist->cur_sb[devlist->level] == NULL) log_err(1, "%s: unable to allocate sbuf", __func__); if (strcmp(name, "targ_port") == 0) { if (cur_port != NULL) log_errx(1, "%s: improper port element nesting (%s)", __func__, name); devlist->port_list.emplace_back(); cur_port = &devlist->port_list.back(); devlist->cur_port = cur_port; for (i = 0; attr[i] != NULL; i += 2) { if (strcmp(attr[i], "id") == 0) { cur_port->port_id = strtoul(attr[i+1], NULL, 0); } else { log_errx(1, "%s: invalid LUN attribute %s = %s", __func__, attr[i], attr[i+1]); } } } } static void cctl_end_pelement(void *user_data, const char *name) { struct cctl_devlist_data *devlist; struct cctl_port *cur_port; std::string str; devlist = (struct cctl_devlist_data *)user_data; cur_port = devlist->cur_port; if ((cur_port == NULL) && (strcmp(name, "ctlportlist") != 0)) log_errx(1, "%s: cur_port == NULL! (name = %s)", __func__, name); if (devlist->cur_sb[devlist->level] == NULL) log_errx(1, "%s: no valid sbuf at level %d (name %s)", __func__, devlist->level, name); sbuf_finish(devlist->cur_sb[devlist->level]); str = sbuf_data(devlist->cur_sb[devlist->level]); sbuf_delete(devlist->cur_sb[devlist->level]); devlist->cur_sb[devlist->level] = NULL; devlist->level--; if (strcmp(name, "frontend_type") == 0) { cur_port->port_frontend = std::move(str); } else if (strcmp(name, "port_name") == 0) { cur_port->port_name = std::move(str); } else if (strcmp(name, "physical_port") == 0) { if (str.empty()) log_errx(1, "%s: %s missing its argument", __func__, name); cur_port->pp = strtoul(str.c_str(), NULL, 0); } else if (strcmp(name, "virtual_port") == 0) { if (str.empty()) log_errx(1, "%s: %s missing its argument", __func__, name); cur_port->vp = strtoul(str.c_str(), NULL, 0); } else if (strcmp(name, "cfiscsi_target") == 0) { cur_port->cfiscsi_target = std::move(str); } else if (strcmp(name, "cfiscsi_state") == 0) { if (str.empty()) log_errx(1, "%s: %s missing its argument", __func__, name); cur_port->cfiscsi_state = strtoul(str.c_str(), NULL, 0); } else if (strcmp(name, "cfiscsi_portal_group_tag") == 0) { if (str.empty()) log_errx(1, "%s: %s missing its argument", __func__, name); cur_port->cfiscsi_portal_group_tag = strtoul(str.c_str(), NULL, 0); } else if (strcmp(name, "ctld_portal_group_name") == 0) { cur_port->ctld_portal_group_name = std::move(str); + } else if (strcmp(name, "ctld_transport_group_name") == 0) { + cur_port->ctld_transport_group_name = std::move(str); + } else if (strcmp(name, "nqn") == 0) { + cur_port->nqn = std::move(str); + } else if (strcmp(name, "portid") == 0) { + if (str.empty()) + log_errx(1, "%s: %s missing its argument", __func__, name); + cur_port->portid = strtoul(str.c_str(), NULL, 0); } else if (strcmp(name, "targ_port") == 0) { devlist->cur_port = NULL; } else if (strcmp(name, "ctlportlist") == 0) { /* Nothing. */ } else { cur_port->attr_list.emplace_back(name, std::move(str)); } } static void cctl_char_handler(void *user_data, const XML_Char *str, int len) { struct cctl_devlist_data *devlist; devlist = (struct cctl_devlist_data *)user_data; sbuf_bcat(devlist->cur_sb[devlist->level], str, len); } static bool parse_kernel_config(struct cctl_devlist_data &devlist) { struct ctl_lun_list list; XML_Parser parser; int retval; std::vector buf(4096); retry: bzero(&list, sizeof(list)); list.alloc_len = buf.size(); list.status = CTL_LUN_LIST_NONE; list.lun_xml = buf.data(); if (ioctl(ctl_fd, CTL_LUN_LIST, &list) == -1) { log_warn("error issuing CTL_LUN_LIST ioctl"); return (false); } if (list.status == CTL_LUN_LIST_ERROR) { log_warnx("error returned from CTL_LUN_LIST ioctl: %s", list.error_str); return (false); } if (list.status == CTL_LUN_LIST_NEED_MORE_SPACE) { buf.resize(buf.size() << 1); goto retry; } parser = XML_ParserCreate(NULL); if (parser == NULL) { log_warnx("unable to create XML parser"); return (false); } XML_SetUserData(parser, &devlist); XML_SetElementHandler(parser, cctl_start_element, cctl_end_element); XML_SetCharacterDataHandler(parser, cctl_char_handler); retval = XML_Parse(parser, buf.data(), strlen(buf.data()), 1); XML_ParserFree(parser); if (retval != 1) { log_warnx("XML_Parse failed"); return (false); } retry_port: bzero(&list, sizeof(list)); list.alloc_len = buf.size(); list.status = CTL_LUN_LIST_NONE; list.lun_xml = buf.data(); if (ioctl(ctl_fd, CTL_PORT_LIST, &list) == -1) { log_warn("error issuing CTL_PORT_LIST ioctl"); return (false); } if (list.status == CTL_LUN_LIST_ERROR) { log_warnx("error returned from CTL_PORT_LIST ioctl: %s", list.error_str); return (false); } if (list.status == CTL_LUN_LIST_NEED_MORE_SPACE) { buf.resize(buf.size() << 1); goto retry_port; } parser = XML_ParserCreate(NULL); if (parser == NULL) { log_warnx("unable to create XML parser"); return (false); } XML_SetUserData(parser, &devlist); XML_SetElementHandler(parser, cctl_start_pelement, cctl_end_pelement); XML_SetCharacterDataHandler(parser, cctl_char_handler); retval = XML_Parse(parser, buf.data(), strlen(buf.data()), 1); XML_ParserFree(parser); if (retval != 1) { log_warnx("XML_Parse failed"); return (false); } return (true); } void add_iscsi_port(struct kports &kports, struct conf *conf, const struct cctl_port &port, std::string &name) { if (port.cfiscsi_target.empty()) { log_debugx("CTL port %u \"%s\" wasn't managed by ctld; ", port.port_id, name.c_str()); if (!kports.has_port(name)) { if (!kports.add_port(name, port.port_id)) { log_warnx("kports::add_port failed"); return; } } return; } if (port.cfiscsi_state != 1) { log_debugx("CTL port %ju is not active (%d); ignoring", (uintmax_t)port.port_id, port.cfiscsi_state); return; } const char *t_name = port.cfiscsi_target.c_str(); struct target *targ = conf->find_target(t_name); if (targ == nullptr) { targ = conf->add_target(t_name); if (targ == nullptr) { log_warnx("Failed to add target \"%s\"", t_name); return; } } if (port.ctld_portal_group_name.empty()) return; const char *pg_name = port.ctld_portal_group_name.c_str(); struct portal_group *pg = conf->find_portal_group(pg_name); if (pg == nullptr) { pg = conf->add_portal_group(pg_name); if (pg == nullptr) { - log_warnx("Failed to add portal_group \"%s\"", pg_name); + log_warnx("Failed to add portal-group \"%s\"", pg_name); return; } } pg->set_tag(port.cfiscsi_portal_group_tag); if (!conf->add_port(targ, pg, port.port_id)) { log_warnx("Failed to add port for target \"%s\" and portal-group \"%s\"", t_name, pg_name); } } +void +add_nvmf_port(struct conf *conf, const struct cctl_port &port, + std::string &name) +{ + if (port.nqn.empty() || port.ctld_transport_group_name.empty()) { + log_debugx("CTL port %u \"%s\" wasn't managed by ctld; ", + port.port_id, name.c_str()); + return; + } + + const char *nqn = port.nqn.c_str(); + struct target *targ = conf->find_controller(nqn); + if (targ == nullptr) { + targ = conf->add_controller(nqn); + if (targ == nullptr) { + log_warnx("Failed to add controller \"%s\"", nqn); + return; + } + } + + const char *tg_name = port.ctld_transport_group_name.c_str(); + struct portal_group *pg = conf->find_transport_group(tg_name); + if (pg == nullptr) { + pg = conf->add_transport_group(tg_name); + if (pg == nullptr) { + log_warnx("Failed to add transport-group \"%s\"", + tg_name); + return; + } + } + pg->set_tag(port.portid); + if (!conf->add_port(targ, pg, port.port_id)) { + log_warnx("Failed to add port for controller \"%s\" and transport-group \"%s\"", + nqn, tg_name); + } +} + conf_up conf_new_from_kernel(struct kports &kports) { struct cctl_devlist_data devlist; log_debugx("obtaining previously configured CTL luns from the kernel"); if (!parse_kernel_config(devlist)) return {}; conf_up conf = std::make_unique(); for (const auto &port : devlist.port_list) { if (port.port_frontend == "ha") continue; std::string name = port.port_name; if (port.pp != 0) { name += "/" + std::to_string(port.pp); if (port.vp != 0) name += "/" + std::to_string(port.vp); } if (port.port_frontend == "iscsi") { add_iscsi_port(kports, conf.get(), port, name); + } else if (port.port_frontend == "nvmf") { + add_nvmf_port(conf.get(), port, name); } else { /* XXX: Treat all unknown ports as iSCSI? */ add_iscsi_port(kports, conf.get(), port, name); } } for (const auto &lun : devlist.lun_list) { if (lun.ctld_name.empty()) { log_debugx("CTL lun %ju wasn't managed by ctld; " "ignoring", (uintmax_t)lun.lun_id); continue; } const char *l_name = lun.ctld_name.c_str(); struct lun *cl = conf->find_lun(l_name); if (cl != NULL) { log_warnx("found CTL lun %ju \"%s\", " "also backed by CTL lun %d; ignoring", (uintmax_t)lun.lun_id, l_name, cl->ctl_lun()); continue; } log_debugx("found CTL lun %ju \"%s\"", (uintmax_t)lun.lun_id, l_name); cl = conf->add_lun(l_name); if (cl == NULL) { log_warnx("lun_new failed"); continue; } cl->set_backend(lun.backend_type.c_str()); cl->set_device_type(lun.device_type); cl->set_blocksize(lun.blocksize); cl->set_device_id(lun.device_id.c_str()); cl->set_serial(lun.serial_number.c_str()); cl->set_size(lun.size_blocks * lun.blocksize); cl->set_ctl_lun(lun.lun_id); for (const auto &pair : lun.attr_list) { const char *key = pair.first.c_str(); const char *value = pair.second.c_str(); if (pair.first == "file" || pair.first == "dev") { cl->set_path(value); continue; } if (!cl->add_option(key, value)) log_warnx("unable to add CTL lun option " "%s for CTL lun %ju \"%s\"", key, (uintmax_t)lun.lun_id, cl->name()); } } return (conf); } bool lun::kernel_add() { struct ctl_lun_req req; int error; bzero(&req, sizeof(req)); strlcpy(req.backend, l_backend.c_str(), sizeof(req.backend)); req.reqtype = CTL_LUNREQ_CREATE; req.reqdata.create.blocksize_bytes = l_blocksize; if (l_size != 0) req.reqdata.create.lun_size_bytes = l_size; if (l_ctl_lun >= 0) { req.reqdata.create.req_lun_id = l_ctl_lun; req.reqdata.create.flags |= CTL_LUN_FLAG_ID_REQ; } req.reqdata.create.flags |= CTL_LUN_FLAG_DEV_TYPE; req.reqdata.create.device_type = l_device_type; if (!l_serial.empty()) { strncpy((char *)req.reqdata.create.serial_num, l_serial.c_str(), sizeof(req.reqdata.create.serial_num)); req.reqdata.create.flags |= CTL_LUN_FLAG_SERIAL_NUM; } if (!l_device_id.empty()) { strncpy((char *)req.reqdata.create.device_id, l_device_id.c_str(), sizeof(req.reqdata.create.device_id)); req.reqdata.create.flags |= CTL_LUN_FLAG_DEVID; } freebsd::nvlist_up nvl = options(); req.args = nvlist_pack(nvl.get(), &req.args_len); if (req.args == NULL) { log_warn("error packing nvlist"); return (false); } error = ioctl(ctl_fd, CTL_LUN_REQ, &req); free(req.args); if (error != 0) { log_warn("error issuing CTL_LUN_REQ ioctl"); return (false); } switch (req.status) { case CTL_LUN_ERROR: log_warnx("LUN creation error: %s", req.error_str); return (false); case CTL_LUN_WARNING: log_warnx("LUN creation warning: %s", req.error_str); break; case CTL_LUN_OK: break; default: log_warnx("unknown LUN creation status: %d", req.status); return (false); } l_ctl_lun = req.reqdata.create.req_lun_id; return (true); } bool lun::kernel_modify() const { struct ctl_lun_req req; int error; bzero(&req, sizeof(req)); strlcpy(req.backend, l_backend.c_str(), sizeof(req.backend)); req.reqtype = CTL_LUNREQ_MODIFY; req.reqdata.modify.lun_id = l_ctl_lun; req.reqdata.modify.lun_size_bytes = l_size; freebsd::nvlist_up nvl = options(); req.args = nvlist_pack(nvl.get(), &req.args_len); if (req.args == NULL) { log_warn("error packing nvlist"); return (false); } error = ioctl(ctl_fd, CTL_LUN_REQ, &req); free(req.args); if (error != 0) { log_warn("error issuing CTL_LUN_REQ ioctl"); return (false); } switch (req.status) { case CTL_LUN_ERROR: log_warnx("LUN modification error: %s", req.error_str); return (false); case CTL_LUN_WARNING: log_warnx("LUN modification warning: %s", req.error_str); break; case CTL_LUN_OK: break; default: log_warnx("unknown LUN modification status: %d", req.status); return (false); } return (true); } bool lun::kernel_remove() const { struct ctl_lun_req req; bzero(&req, sizeof(req)); strlcpy(req.backend, l_backend.c_str(), sizeof(req.backend)); req.reqtype = CTL_LUNREQ_RM; req.reqdata.rm.lun_id = l_ctl_lun; if (ioctl(ctl_fd, CTL_LUN_REQ, &req) == -1) { log_warn("error issuing CTL_LUN_REQ ioctl"); return (false); } switch (req.status) { case CTL_LUN_ERROR: log_warnx("LUN removal error: %s", req.error_str); return (false); case CTL_LUN_WARNING: log_warnx("LUN removal warning: %s", req.error_str); break; case CTL_LUN_OK: break; default: log_warnx("unknown LUN removal status: %d", req.status); return (false); } return (true); } bool ctl_create_port(const char *driver, const nvlist_t *nvl, uint32_t *ctl_port) { struct ctl_req req; char result_buf[NVLIST_BUFSIZE]; int error; bzero(&req, sizeof(req)); req.reqtype = CTL_REQ_CREATE; strlcpy(req.driver, driver, sizeof(req.driver)); req.args = nvlist_pack(nvl, &req.args_len); if (req.args == NULL) { log_warn("error packing nvlist"); return (false); } req.result = result_buf; req.result_len = sizeof(result_buf); error = ioctl(ctl_fd, CTL_PORT_REQ, &req); free(req.args); if (error != 0) { log_warn("error issuing CTL_PORT_REQ ioctl"); return (false); } if (req.status == CTL_LUN_ERROR) { log_warnx("error returned from port creation request: %s", req.error_str); return (false); } if (req.status != CTL_LUN_OK) { log_warnx("unknown port creation request status %d", req.status); return (false); } freebsd::nvlist_up result_nvl(nvlist_unpack(result_buf, req.result_len, 0)); if (result_nvl == NULL) { log_warnx("error unpacking result nvlist"); return (false); } *ctl_port = nvlist_get_number(result_nvl.get(), "port_id"); return (true); } bool ioctl_port::kernel_create_port() { freebsd::nvlist_up nvl(nvlist_create(0)); nvlist_add_stringf(nvl.get(), "pp", "%d", p_ioctl_pp); nvlist_add_stringf(nvl.get(), "vp", "%d", p_ioctl_vp); return (ctl_create_port("ioctl", nvl.get(), &p_ctl_port)); } bool kernel_port::kernel_create_port() { struct ctl_port_entry entry; struct target *targ = p_target; p_ctl_port = p_pport->ctl_port(); if (strncmp(targ->name(), "naa.", 4) == 0 && strlen(targ->name()) == 20) { bzero(&entry, sizeof(entry)); entry.port_type = CTL_PORT_NONE; entry.targ_port = p_ctl_port; entry.flags |= CTL_PORT_WWNN_VALID; entry.wwnn = strtoull(targ->name() + 4, NULL, 16); if (ioctl(ctl_fd, CTL_SET_PORT_WWNS, &entry) == -1) log_warn("CTL_SET_PORT_WWNS ioctl failed"); } return (true); } bool port::kernel_add() { struct ctl_port_entry entry; struct ctl_lun_map lm; struct target *targ = p_target; int error, i; if (!kernel_create_port()) return (false); /* Explicitly enable mapping to block any access except allowed. */ lm.port = p_ctl_port; lm.plun = UINT32_MAX; lm.lun = 0; error = ioctl(ctl_fd, CTL_LUN_MAP, &lm); if (error != 0) log_warn("CTL_LUN_MAP ioctl failed"); /* Map configured LUNs */ for (i = 0; i < MAX_LUNS; i++) { if (targ->lun(i) == nullptr) continue; lm.port = p_ctl_port; lm.plun = i; lm.lun = targ->lun(i)->ctl_lun(); error = ioctl(ctl_fd, CTL_LUN_MAP, &lm); if (error != 0) log_warn("CTL_LUN_MAP ioctl failed"); } /* Enable port */ bzero(&entry, sizeof(entry)); entry.targ_port = p_ctl_port; error = ioctl(ctl_fd, CTL_ENABLE_PORT, &entry); if (error != 0) { log_warn("CTL_ENABLE_PORT ioctl failed"); return (false); } return (true); } bool port::kernel_update(const struct port *oport) { struct ctl_lun_map lm; struct target *targ = p_target; struct target *otarg = oport->p_target; int error, i; uint32_t olun; p_ctl_port = oport->p_ctl_port; /* Map configured LUNs and unmap others */ for (i = 0; i < MAX_LUNS; i++) { lm.port = p_ctl_port; lm.plun = i; if (targ->lun(i) == nullptr) lm.lun = UINT32_MAX; else lm.lun = targ->lun(i)->ctl_lun(); if (otarg->lun(i) == nullptr) olun = UINT32_MAX; else olun = otarg->lun(i)->ctl_lun(); if (lm.lun == olun) continue; error = ioctl(ctl_fd, CTL_LUN_MAP, &lm); if (error != 0) log_warn("CTL_LUN_MAP ioctl failed"); } return (true); } bool ctl_remove_port(const char *driver, nvlist_t *nvl) { struct ctl_req req; int error; strlcpy(req.driver, driver, sizeof(req.driver)); req.reqtype = CTL_REQ_REMOVE; req.args = nvlist_pack(nvl, &req.args_len); if (req.args == NULL) { log_warn("error packing nvlist"); return (false); } error = ioctl(ctl_fd, CTL_PORT_REQ, &req); free(req.args); if (error != 0) { log_warn("error issuing CTL_PORT_REQ ioctl"); return (false); } if (req.status == CTL_LUN_ERROR) { log_warnx("error returned from port removal request: %s", req.error_str); return (false); } if (req.status != CTL_LUN_OK) { log_warnx("unknown port removal request status %d", req.status); return (false); } return (true); } bool ioctl_port::kernel_remove_port() { freebsd::nvlist_up nvl(nvlist_create(0)); nvlist_add_stringf(nvl.get(), "port_id", "%d", p_ctl_port); return (ctl_remove_port("ioctl", nvl.get())); } bool kernel_port::kernel_remove_port() { struct ctl_lun_map lm; int error; /* Disable LUN mapping. */ lm.port = p_ctl_port; lm.plun = UINT32_MAX; lm.lun = UINT32_MAX; error = ioctl(ctl_fd, CTL_LUN_MAP, &lm); if (error != 0) log_warn("CTL_LUN_MAP ioctl failed"); return (true); } bool port::kernel_remove() { struct ctl_port_entry entry; int error; /* Disable port */ bzero(&entry, sizeof(entry)); entry.targ_port = p_ctl_port; error = ioctl(ctl_fd, CTL_DISABLE_PORT, &entry); if (error != 0) { log_warn("CTL_DISABLE_PORT ioctl failed"); return (false); } return (kernel_remove_port()); } #ifdef ICL_KERNEL_PROXY void kernel_listen(struct addrinfo *ai, bool iser, int portal_id) { struct ctl_iscsi req; bzero(&req, sizeof(req)); req.type = CTL_ISCSI_LISTEN; req.data.listen.iser = iser; req.data.listen.domain = ai->ai_family; req.data.listen.socktype = ai->ai_socktype; req.data.listen.protocol = ai->ai_protocol; req.data.listen.addr = ai->ai_addr; req.data.listen.addrlen = ai->ai_addrlen; req.data.listen.portal_id = portal_id; if (ioctl(ctl_fd, CTL_ISCSI, &req) == -1) log_err(1, "error issuing CTL_ISCSI ioctl"); if (req.status != CTL_ISCSI_OK) { log_errx(1, "error returned from CTL iSCSI listen: %s", req.error_str); } } void kernel_accept(int *connection_id, int *portal_id, struct sockaddr *client_sa, socklen_t *client_salen) { struct ctl_iscsi req; struct sockaddr_storage ss; bzero(&req, sizeof(req)); req.type = CTL_ISCSI_ACCEPT; req.data.accept.initiator_addr = (struct sockaddr *)&ss; if (ioctl(ctl_fd, CTL_ISCSI, &req) == -1) log_err(1, "error issuing CTL_ISCSI ioctl"); if (req.status != CTL_ISCSI_OK) { log_errx(1, "error returned from CTL iSCSI accept: %s", req.error_str); } *connection_id = req.data.accept.connection_id; *portal_id = req.data.accept.portal_id; *client_salen = req.data.accept.initiator_addrlen; memcpy(client_sa, &ss, *client_salen); } void kernel_send(struct pdu *pdu) { struct ctl_iscsi req; bzero(&req, sizeof(req)); req.type = CTL_ISCSI_SEND; req.data.send.connection_id = pdu->pdu_connection->conn_socket; req.data.send.bhs = pdu->pdu_bhs; req.data.send.data_segment_len = pdu->pdu_data_len; req.data.send.data_segment = pdu->pdu_data; if (ioctl(ctl_fd, CTL_ISCSI, &req) == -1) { log_err(1, "error issuing CTL_ISCSI ioctl; " "dropping connection"); } if (req.status != CTL_ISCSI_OK) { log_errx(1, "error returned from CTL iSCSI send: " "%s; dropping connection", req.error_str); } } void kernel_receive(struct pdu *pdu) { struct connection *conn; struct ctl_iscsi req; conn = pdu->pdu_connection; pdu->pdu_data = malloc(conn->conn_max_recv_data_segment_length); if (pdu->pdu_data == NULL) log_err(1, "malloc"); bzero(&req, sizeof(req)); req.type = CTL_ISCSI_RECEIVE; req.data.receive.connection_id = conn->conn_socket; req.data.receive.bhs = pdu->pdu_bhs; req.data.receive.data_segment_len = conn->conn_max_recv_data_segment_length; req.data.receive.data_segment = pdu->pdu_data; if (ioctl(ctl_fd, CTL_ISCSI, &req) == -1) { log_err(1, "error issuing CTL_ISCSI ioctl; " "dropping connection"); } if (req.status != CTL_ISCSI_OK) { log_errx(1, "error returned from CTL iSCSI receive: " "%s; dropping connection", req.error_str); } } #endif /* ICL_KERNEL_PROXY */ /* * XXX: I CANT INTO LATIN */ void kernel_capsicate(void) { cap_rights_t rights; - const unsigned long cmds[] = { CTL_ISCSI }; + const unsigned long cmds[] = { CTL_ISCSI, CTL_NVMF }; cap_rights_init(&rights, CAP_IOCTL); if (caph_rights_limit(ctl_fd, &rights) < 0) log_err(1, "cap_rights_limit"); if (caph_ioctls_limit(ctl_fd, cmds, nitems(cmds)) < 0) log_err(1, "cap_ioctls_limit"); if (caph_enter() < 0) log_err(1, "cap_enter"); if (cap_sandboxed()) log_debugx("Capsicum capability mode enabled"); else log_warnx("Capsicum capability mode not supported"); } diff --git a/usr.sbin/ctld/nvmf.cc b/usr.sbin/ctld/nvmf.cc new file mode 100644 index 000000000000..d1240bfa4f6c --- /dev/null +++ b/usr.sbin/ctld/nvmf.cc @@ -0,0 +1,478 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Chelsio Communications, Inc. + * Written by: John Baldwin + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ctld.hh" +#include "nvmf.hh" + +#define DEFAULT_MAXH2CDATA (256 * 1024) + +struct nvmf_io_portal final : public nvmf_portal { + nvmf_io_portal(struct portal_group *pg, const char *listen, + portal_protocol protocol, freebsd::addrinfo_up ai, + const struct nvmf_association_params &aparams, + nvmf_association_up na) : + nvmf_portal(pg, listen, protocol, std::move(ai), aparams, + std::move(na)) {} + + void handle_connection(freebsd::fd_up fd, const char *host, + const struct sockaddr *client_sa) override; +}; + +struct nvmf_transport_group final : public portal_group { + nvmf_transport_group(struct conf *conf, std::string_view name) : + portal_group(conf, name) {} + + const char *keyword() const override + { return "transport-group"; } + + void allocate_tag() override; + bool add_portal(const char *value, portal_protocol protocol) + override; + void add_default_portals() override; + bool set_filter(const char *str) override; + + virtual port_up create_port(struct target *target, auth_group_sp ag) + override; + virtual port_up create_port(struct target *target, uint32_t ctl_port) + override; + +private: + struct nvmf_association_params init_aparams(portal_protocol protocol); + + static uint16_t last_port_id; +}; + +struct nvmf_port final : public portal_group_port { + nvmf_port(struct target *target, struct portal_group *pg, + auth_group_sp ag) : + portal_group_port(target, pg, ag) {} + nvmf_port(struct target *target, struct portal_group *pg, + uint32_t ctl_port) : + portal_group_port(target, pg, ctl_port) {} + + bool kernel_create_port() override; + bool kernel_remove_port() override; + +private: + static bool modules_loaded; + static void load_kernel_modules(); +}; + +struct nvmf_controller final : public target { + nvmf_controller(struct conf *conf, std::string_view name) : + target(conf, "controller", name) {} + + bool add_host_nqn(std::string_view name) override; + bool add_host_address(const char *addr) override; + bool add_namespace(u_int id, const char *lun_name) override; + bool add_portal_group(const char *pg_name, const char *ag_name) + override; + struct lun *start_namespace(u_int id) override; + +protected: + struct portal_group *default_portal_group() override; +}; + +uint16_t nvmf_transport_group::last_port_id = 0; +bool nvmf_port::modules_loaded = false; + +static bool need_tcp_transport = false; + +static bool +parse_bool(const nvlist_t *nvl, const char *key, bool def) +{ + const char *value; + + if (!nvlist_exists_string(nvl, key)) + return def; + + value = nvlist_get_string(nvl, key); + if (strcasecmp(value, "true") == 0 || + strcasecmp(value, "1") == 0) + return true; + if (strcasecmp(value, "false") == 0 || + strcasecmp(value, "0") == 0) + return false; + + log_warnx("Invalid value \"%s\" for boolean option %s", value, key); + return def; +} + +static uint64_t +parse_number(const nvlist_t *nvl, const char *key, uint64_t def, uint64_t minv, + uint64_t maxv) +{ + const char *value; + int64_t val; + + if (!nvlist_exists_string(nvl, key)) + return def; + + value = nvlist_get_string(nvl, key); + if (expand_number(value, &val) == 0 && val >= 0 && + (uint64_t)val >= minv && (uint64_t)val <= maxv) + return (uint64_t)val; + + log_warnx("Invalid value \"%s\" for numeric option %s", value, key); + return def; +} + +struct nvmf_association_params +nvmf_transport_group::init_aparams(portal_protocol protocol) +{ + struct nvmf_association_params params; + memset(¶ms, 0, sizeof(params)); + + /* Options shared between discovery and I/O associations. */ + const nvlist_t *nvl = pg_options.get(); + params.tcp.header_digests = parse_bool(nvl, "HDGST", false); + params.tcp.data_digests = parse_bool(nvl, "DDGST", false); + uint64_t value = parse_number(nvl, "MAXH2CDATA", DEFAULT_MAXH2CDATA, + 4096, UINT32_MAX); + if (value % 4 != 0) { + log_warnx("Invalid value \"%ju\" for option MAXH2CDATA", + (uintmax_t)value); + value = DEFAULT_MAXH2CDATA; + } + params.tcp.maxh2cdata = value; + + switch (protocol) { + case portal_protocol::NVME_TCP: + params.sq_flow_control = parse_bool(nvl, "SQFC", false); + params.dynamic_controller_model = true; + params.max_admin_qsize = parse_number(nvl, "max_admin_qsize", + NVME_MAX_ADMIN_ENTRIES, NVME_MIN_ADMIN_ENTRIES, + NVME_MAX_ADMIN_ENTRIES); + params.max_io_qsize = parse_number(nvl, "max_io_qsize", + NVME_MAX_IO_ENTRIES, NVME_MIN_IO_ENTRIES, + NVME_MAX_IO_ENTRIES); + params.tcp.pda = 0; + break; + case portal_protocol::NVME_DISCOVERY_TCP: + params.sq_flow_control = false; + params.dynamic_controller_model = true; + params.max_admin_qsize = NVME_MAX_ADMIN_ENTRIES; + params.tcp.pda = 0; + break; + default: + __assert_unreachable(); + } + + return params; +} + +portal_group_up +nvmf_make_transport_group(struct conf *conf, std::string_view name) +{ + return std::make_unique(conf, name); +} + +target_up +nvmf_make_controller(struct conf *conf, std::string_view name) +{ + return std::make_unique(conf, name); +} + +void +nvmf_transport_group::allocate_tag() +{ + set_tag(++last_port_id); +} + +bool +nvmf_transport_group::add_portal(const char *value, portal_protocol protocol) +{ + freebsd::addrinfo_up ai; + enum nvmf_trtype trtype; + + switch (protocol) { + case portal_protocol::NVME_TCP: + trtype = NVMF_TRTYPE_TCP; + ai = parse_addr_port(value, "4420"); + break; + case portal_protocol::NVME_DISCOVERY_TCP: + trtype = NVMF_TRTYPE_TCP; + ai = parse_addr_port(value, "8009"); + break; + default: + log_warnx("unsupported transport protocol for %s", value); + return false; + } + + if (!ai) { + log_warnx("invalid listen address %s", value); + return false; + } + + struct nvmf_association_params aparams = init_aparams(protocol); + nvmf_association_up association(nvmf_allocate_association(trtype, true, + &aparams)); + if (!association) { + log_warn("Failed to create NVMe controller association"); + return false; + } + + /* + * XXX: getaddrinfo(3) may return multiple addresses; we should turn + * those into multiple portals. + */ + + portal_up portal; + if (protocol == portal_protocol::NVME_DISCOVERY_TCP) { + portal = std::make_unique(this, value, + protocol, std::move(ai), aparams, std::move(association)); + } else { + portal = std::make_unique(this, value, + protocol, std::move(ai), aparams, std::move(association)); + need_tcp_transport = true; + } + + pg_portals.emplace_back(std::move(portal)); + return true; +} + +void +nvmf_transport_group::add_default_portals() +{ + add_portal("0.0.0.0", portal_protocol::NVME_DISCOVERY_TCP); + add_portal("[::]", portal_protocol::NVME_DISCOVERY_TCP); + add_portal("0.0.0.0", portal_protocol::NVME_TCP); + add_portal("[::]", portal_protocol::NVME_TCP); +} + +bool +nvmf_transport_group::set_filter(const char *str) +{ + enum discovery_filter filter; + + if (strcmp(str, "none") == 0) { + filter = discovery_filter::NONE; + } else if (strcmp(str, "address") == 0) { + filter = discovery_filter::PORTAL; + } else if (strcmp(str, "address-name") == 0) { + filter = discovery_filter::PORTAL_NAME; + } else { + log_warnx("invalid discovery-filter \"%s\" for transport-group " + "\"%s\"; valid values are \"none\", \"address\", " + "and \"address-name\"", + str, name()); + return false; + } + + if (pg_discovery_filter != discovery_filter::UNKNOWN && + pg_discovery_filter != filter) { + log_warnx("cannot set discovery-filter to \"%s\" for " + "transport-group \"%s\"; already has a different " + "value", str, name()); + return false; + } + + pg_discovery_filter = filter; + return true; +} + +port_up +nvmf_transport_group::create_port(struct target *target, auth_group_sp ag) +{ + return std::make_unique(target, this, ag); +} + +port_up +nvmf_transport_group::create_port(struct target *target, uint32_t ctl_port) +{ + return std::make_unique(target, this, ctl_port); +} + +void +nvmf_port::load_kernel_modules() +{ + int saved_errno; + + if (modules_loaded) + return; + + saved_errno = errno; + if (modfind("nvmft") == -1 && kldload("nvmft") == -1) + log_warn("couldn't load nvmft"); + + if (need_tcp_transport) { + if (modfind("nvmf/tcp") == -1 && kldload("nvmf_tcp") == -1) + log_warn("couldn't load nvmf_tcp"); + } + + errno = saved_errno; + modules_loaded = true; +} + +bool +nvmf_port::kernel_create_port() +{ + struct portal_group *pg = p_portal_group; + struct target *targ = p_target; + + load_kernel_modules(); + + freebsd::nvlist_up nvl = pg->options(); + nvlist_add_string(nvl.get(), "subnqn", targ->name()); + nvlist_add_string(nvl.get(), "ctld_transport_group_name", + pg->name()); + nvlist_add_stringf(nvl.get(), "portid", "%u", pg->tag()); + if (!nvlist_exists_string(nvl.get(), "max_io_qsize")) + nvlist_add_stringf(nvl.get(), "max_io_qsize", "%u", + NVME_MAX_IO_ENTRIES); + + return ctl_create_port("nvmf", nvl.get(), &p_ctl_port); +} + +bool +nvmf_port::kernel_remove_port() +{ + freebsd::nvlist_up nvl(nvlist_create(0)); + nvlist_add_string(nvl.get(), "subnqn", p_target->name()); + + return ctl_remove_port("nvmf", nvl.get()); +} + +bool +nvmf_controller::add_host_nqn(std::string_view name) +{ + if (!use_private_auth("host-nqn")) + return false; + return t_auth_group->add_host_nqn(name); +} + +bool +nvmf_controller::add_host_address(const char *addr) +{ + if (!use_private_auth("host-address")) + return false; + return t_auth_group->add_host_address(addr); +} + +bool +nvmf_controller::add_namespace(u_int id, const char *lun_name) +{ + if (id == 0) { + log_warnx("namespace ID cannot be 0 for %s", label()); + return false; + } + + std::string lun_label = "namespace ID " + std::to_string(id - 1); + return target::add_lun(id, lun_label.c_str(), lun_name); +} + +bool +nvmf_controller::add_portal_group(const char *pg_name, const char *ag_name) +{ + struct portal_group *pg; + auth_group_sp ag; + + pg = t_conf->find_transport_group(pg_name); + if (pg == NULL) { + log_warnx("unknown transport-group \"%s\" for %s", pg_name, + label()); + return false; + } + + if (ag_name != NULL) { + ag = t_conf->find_auth_group(ag_name); + if (ag == NULL) { + log_warnx("unknown auth-group \"%s\" for %s", ag_name, + label()); + return false; + } + } + + if (!t_conf->add_port(this, pg, std::move(ag))) { + log_warnx("can't link transport-group \"%s\" to %s", pg_name, + label()); + return false; + } + return true; +} + +struct lun * +nvmf_controller::start_namespace(u_int id) +{ + if (id == 0) { + log_warnx("namespace ID cannot be 0 for %s", label()); + return nullptr; + } + + std::string lun_label = "namespace ID " + std::to_string(id - 1); + std::string lun_name = freebsd::stringf("%s,nsid,%u", name(), id); + return target::start_lun(id, lun_label.c_str(), lun_name.c_str()); +} + +struct portal_group * +nvmf_controller::default_portal_group() +{ + return t_conf->find_transport_group("default"); +} + +void +nvmf_io_portal::handle_connection(freebsd::fd_up fd, const char *host __unused, + const struct sockaddr *client_sa __unused) +{ + struct nvmf_qpair_params qparams; + memset(&qparams, 0, sizeof(qparams)); + qparams.tcp.fd = fd; + + struct nvmf_capsule *nc = NULL; + struct nvmf_fabric_connect_data data; + nvmf_qpair_up qp(nvmf_accept(association(), &qparams, &nc, &data)); + if (!qp) { + log_warnx("Failed to create NVMe I/O qpair: %s", + nvmf_association_error(association())); + return; + } + nvmf_capsule_up nc_guard(nc); + const struct nvmf_fabric_connect_cmd *cmd = + (const struct nvmf_fabric_connect_cmd *)nvmf_capsule_sqe(nc); + + struct ctl_nvmf req; + memset(&req, 0, sizeof(req)); + req.type = CTL_NVMF_HANDOFF; + int error = nvmf_handoff_controller_qpair(qp.get(), cmd, &data, + &req.data.handoff); + if (error != 0) { + log_warnc(error, + "Failed to prepare NVMe I/O qpair for handoff"); + return; + } + + if (ioctl(ctl_fd, CTL_NVMF, &req) != 0) + log_warn("ioctl(CTL_NVMF/CTL_NVMF_HANDOFF)"); + if (req.status == CTL_NVMF_ERROR) + log_warnx("Failed to handoff NVMF connection: %s", + req.error_str); + else if (req.status != CTL_NVMF_OK) + log_warnx("Failed to handoff NVMF connection with status %d", + req.status); +} diff --git a/usr.sbin/ctld/nvmf.hh b/usr.sbin/ctld/nvmf.hh new file mode 100644 index 000000000000..0b4f8d45adfd --- /dev/null +++ b/usr.sbin/ctld/nvmf.hh @@ -0,0 +1,71 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Chelsio Communications, Inc. + * Written by: John Baldwin + */ + +#ifndef __NVMF_HH__ +#define __NVMF_HH__ + +struct nvmf_association_deleter { + void operator()(struct nvmf_association *na) const + { + nvmf_free_association(na); + } +}; + +using nvmf_association_up = std::unique_ptr; + +struct nvmf_capsule_deleter { + void operator()(struct nvmf_capsule *nc) const + { + nvmf_free_capsule(nc); + } +}; + +using nvmf_capsule_up = std::unique_ptr; + +struct nvmf_qpair_deleter { + void operator()(struct nvmf_qpair *qp) const + { + nvmf_free_qpair(qp); + } +}; + +using nvmf_qpair_up = std::unique_ptr; + +struct nvmf_portal : public portal { + nvmf_portal(struct portal_group *pg, const char *listen, + portal_protocol protocol, freebsd::addrinfo_up ai, + const struct nvmf_association_params &aparams, + nvmf_association_up na) : + portal(pg, listen, protocol, std::move(ai)), + p_aparams(aparams), p_association(std::move(na)) {} + virtual ~nvmf_portal() override = default; + + const struct nvmf_association_params *aparams() const + { return &p_aparams; } + +protected: + struct nvmf_association *association() { return p_association.get(); } + +private: + struct nvmf_association_params p_aparams; + nvmf_association_up p_association; +}; + +struct nvmf_discovery_portal final : public nvmf_portal { + nvmf_discovery_portal(struct portal_group *pg, const char *listen, + portal_protocol protocol, freebsd::addrinfo_up ai, + const struct nvmf_association_params &aparams, + nvmf_association_up na) : + nvmf_portal(pg, listen, protocol, std::move(ai), aparams, + std::move(na)) {} + + void handle_connection(freebsd::fd_up fd, const char *host, + const struct sockaddr *client_sa) override; +}; + +#endif /* !__NVMF_HH__ */ diff --git a/usr.sbin/ctld/nvmf_discovery.cc b/usr.sbin/ctld/nvmf_discovery.cc new file mode 100644 index 000000000000..a32094ddafa1 --- /dev/null +++ b/usr.sbin/ctld/nvmf_discovery.cc @@ -0,0 +1,518 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2023-2025 Chelsio Communications, Inc. + * Written by: John Baldwin + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ctld.hh" +#include "nvmf.hh" + +struct discovery_log { + discovery_log(const struct portal_group *pg); + + const char *data() const { return buf.data(); } + size_t length() const { return buf.size(); } + + void append(const struct nvme_discovery_log_entry *entry); + +private: + struct nvme_discovery_log *header() + { return reinterpret_cast(buf.data()); } + + std::vector buf; +}; + +struct discovery_controller { + discovery_controller(freebsd::fd_up s, struct nvmf_qpair *qp, + const discovery_log &discovery_log); + + void handle_admin_commands(); +private: + bool update_cc(uint32_t new_cc); + void handle_property_get(const struct nvmf_capsule *nc, + const struct nvmf_fabric_prop_get_cmd *pget); + void handle_property_set(const struct nvmf_capsule *nc, + const struct nvmf_fabric_prop_set_cmd *pset); + void handle_fabrics_command(const struct nvmf_capsule *nc, + const struct nvmf_fabric_cmd *cmd); + void handle_identify_command(const struct nvmf_capsule *nc, + const struct nvme_command *cmd); + void handle_get_log_page_command(const struct nvmf_capsule *nc, + const struct nvme_command *cmd); + + struct nvmf_qpair *qp; + + uint64_t cap = 0; + uint32_t vs = 0; + uint32_t cc = 0; + uint32_t csts = 0; + + bool shutdown = false; + + struct nvme_controller_data cdata; + + const discovery_log &discovery_log; + freebsd::fd_up s; +}; + +discovery_log::discovery_log(const struct portal_group *pg) : + buf(sizeof(nvme_discovery_log)) +{ + struct nvme_discovery_log *log = header(); + + log->genctr = htole32(pg->conf()->genctr()); + log->recfmt = 0; +} + +void +discovery_log::append(const struct nvme_discovery_log_entry *entry) +{ + const char *cp = reinterpret_cast(entry); + buf.insert(buf.end(), cp, cp + sizeof(*entry)); + + struct nvme_discovery_log *log = header(); + log->numrec = htole32(le32toh(log->numrec) + 1); +} + +static bool +discovery_controller_filtered(const struct portal_group *pg, + const struct sockaddr *client_sa, std::string_view hostnqn, + const struct port *port) +{ + const struct target *targ = port->target(); + const struct auth_group *ag = port->auth_group(); + if (ag == nullptr) + ag = targ->auth_group(); + + assert(pg->discovery_filter() != discovery_filter::UNKNOWN); + + if (pg->discovery_filter() >= discovery_filter::PORTAL && + !ag->host_permitted(client_sa)) { + log_debugx("host address does not match addresses " + "allowed for controller \"%s\"; skipping", targ->name()); + return true; + } + + if (pg->discovery_filter() >= discovery_filter::PORTAL_NAME && + !ag->host_permitted(hostnqn) != 0) { + log_debugx("HostNQN does not match NQNs " + "allowed for controller \"%s\"; skipping", targ->name()); + return true; + } + + /* XXX: auth not yet implemented for NVMe */ + + return false; +} + +static bool +portal_uses_wildcard_address(const struct portal *p) +{ + const struct addrinfo *ai = p->ai(); + + switch (ai->ai_family) { + case AF_INET: + { + const struct sockaddr_in *sin; + + sin = (const struct sockaddr_in *)ai->ai_addr; + return sin->sin_addr.s_addr == htonl(INADDR_ANY); + } + case AF_INET6: + { + const struct sockaddr_in6 *sin6; + + sin6 = (const struct sockaddr_in6 *)ai->ai_addr; + return memcmp(&sin6->sin6_addr, &in6addr_any, + sizeof(in6addr_any)) == 0; + } + default: + __assert_unreachable(); + } +} + +static bool +init_discovery_log_entry(struct nvme_discovery_log_entry *entry, + const struct target *target, const struct portal *portal, + const char *wildcard_host) +{ + /* + * The TCP port for I/O controllers might not be fixed, so + * fetch the sockaddr of the socket to determine which port + * the kernel chose. + */ + struct sockaddr_storage ss; + socklen_t len = sizeof(ss); + if (getsockname(portal->socket(), (struct sockaddr *)&ss, &len) == -1) { + log_warn("Failed getsockname building discovery log entry"); + return false; + } + + const struct nvmf_association_params *aparams = + static_cast(portal)->aparams(); + + memset(entry, 0, sizeof(*entry)); + entry->trtype = NVMF_TRTYPE_TCP; + int error = getnameinfo((struct sockaddr *)&ss, len, + (char *)entry->traddr, sizeof(entry->traddr), + (char *)entry->trsvcid, sizeof(entry->trsvcid), + NI_NUMERICHOST | NI_NUMERICSERV); + if (error != 0) { + log_warnx("Failed getnameinfo building discovery log entry: %s", + gai_strerror(error)); + return false; + } + + if (portal_uses_wildcard_address(portal)) + strncpy((char *)entry->traddr, wildcard_host, + sizeof(entry->traddr)); + switch (portal->ai()->ai_family) { + case AF_INET: + entry->adrfam = NVMF_ADRFAM_IPV4; + break; + case AF_INET6: + entry->adrfam = NVMF_ADRFAM_IPV6; + break; + default: + __assert_unreachable(); + } + entry->subtype = NVMF_SUBTYPE_NVME; + if (!aparams->sq_flow_control) + entry->treq |= (1 << 2); + entry->portid = htole16(portal->portal_group()->tag()); + entry->cntlid = htole16(NVMF_CNTLID_DYNAMIC); + entry->aqsz = aparams->max_admin_qsize; + strncpy((char *)entry->subnqn, target->name(), sizeof(entry->subnqn)); + return true; +} + +static discovery_log +build_discovery_log_page(const struct portal_group *pg, int fd, + const struct sockaddr *client_sa, + const struct nvmf_fabric_connect_data &data) +{ + discovery_log discovery_log(pg); + + struct sockaddr_storage ss; + socklen_t len = sizeof(ss); + if (getsockname(fd, (struct sockaddr *)&ss, &len) == -1) { + log_warn("build_discovery_log_page: getsockname"); + return discovery_log; + } + + char wildcard_host[NI_MAXHOST]; + int error = getnameinfo((struct sockaddr *)&ss, len, wildcard_host, + sizeof(wildcard_host), NULL, 0, NI_NUMERICHOST); + if (error != 0) { + log_warnx("build_discovery_log_page: getnameinfo: %s", + gai_strerror(error)); + return discovery_log; + } + + const char *nqn = (const char *)data.hostnqn; + std::string hostnqn(nqn, strnlen(nqn, sizeof(data.hostnqn))); + for (const auto &kv : pg->ports()) { + const struct port *port = kv.second; + if (discovery_controller_filtered(pg, client_sa, hostnqn, port)) + continue; + + for (const portal_up &portal : pg->portals()) { + if (portal->protocol() != portal_protocol::NVME_TCP) + continue; + + if (portal_uses_wildcard_address(portal.get()) && + portal->ai()->ai_family != client_sa->sa_family) + continue; + + struct nvme_discovery_log_entry entry; + if (init_discovery_log_entry(&entry, port->target(), + portal.get(), wildcard_host)) + discovery_log.append(&entry); + } + } + + return discovery_log; +} + +bool +discovery_controller::update_cc(uint32_t new_cc) +{ + uint32_t changes; + + if (shutdown) + return false; + if (!nvmf_validate_cc(qp, cap, cc, new_cc)) + return false; + + changes = cc ^ new_cc; + cc = new_cc; + + /* Handle shutdown requests. */ + if (NVMEV(NVME_CC_REG_SHN, changes) != 0 && + NVMEV(NVME_CC_REG_SHN, new_cc) != 0) { + csts &= ~NVMEM(NVME_CSTS_REG_SHST); + csts |= NVMEF(NVME_CSTS_REG_SHST, NVME_SHST_COMPLETE); + shutdown = true; + } + + if (NVMEV(NVME_CC_REG_EN, changes) != 0) { + if (NVMEV(NVME_CC_REG_EN, new_cc) == 0) { + /* Controller reset. */ + csts = 0; + shutdown = true; + } else + csts |= NVMEF(NVME_CSTS_REG_RDY, 1); + } + return true; +} + +void +discovery_controller::handle_property_get(const struct nvmf_capsule *nc, + const struct nvmf_fabric_prop_get_cmd *pget) +{ + struct nvmf_fabric_prop_get_rsp rsp; + + nvmf_init_cqe(&rsp, nc, 0); + + switch (le32toh(pget->ofst)) { + case NVMF_PROP_CAP: + if (pget->attrib.size != NVMF_PROP_SIZE_8) + goto error; + rsp.value.u64 = htole64(cap); + break; + case NVMF_PROP_VS: + if (pget->attrib.size != NVMF_PROP_SIZE_4) + goto error; + rsp.value.u32.low = htole32(vs); + break; + case NVMF_PROP_CC: + if (pget->attrib.size != NVMF_PROP_SIZE_4) + goto error; + rsp.value.u32.low = htole32(cc); + break; + case NVMF_PROP_CSTS: + if (pget->attrib.size != NVMF_PROP_SIZE_4) + goto error; + rsp.value.u32.low = htole32(csts); + break; + default: + goto error; + } + + nvmf_send_response(nc, &rsp); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +void +discovery_controller::handle_property_set(const struct nvmf_capsule *nc, + const struct nvmf_fabric_prop_set_cmd *pset) +{ + switch (le32toh(pset->ofst)) { + case NVMF_PROP_CC: + if (pset->attrib.size != NVMF_PROP_SIZE_4) + goto error; + if (!update_cc(le32toh(pset->value.u32.low))) + goto error; + break; + default: + goto error; + } + + nvmf_send_success(nc); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +void +discovery_controller::handle_fabrics_command(const struct nvmf_capsule *nc, + const struct nvmf_fabric_cmd *fc) +{ + switch (fc->fctype) { + case NVMF_FABRIC_COMMAND_PROPERTY_GET: + handle_property_get(nc, + (const struct nvmf_fabric_prop_get_cmd *)fc); + break; + case NVMF_FABRIC_COMMAND_PROPERTY_SET: + handle_property_set(nc, + (const struct nvmf_fabric_prop_set_cmd *)fc); + break; + case NVMF_FABRIC_COMMAND_CONNECT: + log_warnx("CONNECT command on connected queue"); + nvmf_send_generic_error(nc, NVME_SC_COMMAND_SEQUENCE_ERROR); + break; + case NVMF_FABRIC_COMMAND_DISCONNECT: + log_warnx("DISCONNECT command on admin queue"); + nvmf_send_error(nc, NVME_SCT_COMMAND_SPECIFIC, + NVMF_FABRIC_SC_INVALID_QUEUE_TYPE); + break; + default: + log_warnx("Unsupported fabrics command %#x", fc->fctype); + nvmf_send_generic_error(nc, NVME_SC_INVALID_OPCODE); + break; + } +} + +void +discovery_controller::handle_identify_command(const struct nvmf_capsule *nc, + const struct nvme_command *cmd) +{ + uint8_t cns; + + cns = le32toh(cmd->cdw10) & 0xFF; + switch (cns) { + case 1: + break; + default: + log_warnx("Unsupported CNS %#x for IDENTIFY", cns); + goto error; + } + + nvmf_send_controller_data(nc, &cdata, sizeof(cdata)); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +void +discovery_controller::handle_get_log_page_command(const struct nvmf_capsule *nc, + const struct nvme_command *cmd) +{ + uint64_t offset; + uint32_t length; + + switch (nvmf_get_log_page_id(cmd)) { + case NVME_LOG_DISCOVERY: + break; + default: + log_warnx("Unsupported log page %u for discovery controller", + nvmf_get_log_page_id(cmd)); + goto error; + } + + offset = nvmf_get_log_page_offset(cmd); + if (offset >= discovery_log.length()) + goto error; + + length = nvmf_get_log_page_length(cmd); + if (length > discovery_log.length() - offset) + length = discovery_log.length() - offset; + + nvmf_send_controller_data(nc, discovery_log.data() + offset, length); + return; +error: + nvmf_send_generic_error(nc, NVME_SC_INVALID_FIELD); +} + +void +discovery_controller::handle_admin_commands() +{ + for (;;) { + struct nvmf_capsule *nc; + int error = nvmf_controller_receive_capsule(qp, &nc); + if (error != 0) { + if (error != ECONNRESET) + log_warnc(error, + "Failed to read command capsule"); + break; + } + nvmf_capsule_up nc_guard(nc); + + const struct nvme_command *cmd = + (const struct nvme_command *)nvmf_capsule_sqe(nc); + + /* + * Only permit Fabrics commands while a controller is + * disabled. + */ + if (NVMEV(NVME_CC_REG_EN, cc) == 0 && + cmd->opc != NVME_OPC_FABRICS_COMMANDS) { + log_warnx("Unsupported admin opcode %#x while disabled\n", + cmd->opc); + nvmf_send_generic_error(nc, + NVME_SC_COMMAND_SEQUENCE_ERROR); + continue; + } + + switch (cmd->opc) { + case NVME_OPC_FABRICS_COMMANDS: + handle_fabrics_command(nc, + (const struct nvmf_fabric_cmd *)cmd); + break; + case NVME_OPC_IDENTIFY: + handle_identify_command(nc, cmd); + break; + case NVME_OPC_GET_LOG_PAGE: + handle_get_log_page_command(nc, cmd); + break; + default: + log_warnx("Unsupported admin opcode %#x", cmd->opc); + nvmf_send_generic_error(nc, NVME_SC_INVALID_OPCODE); + break; + } + } +} + +discovery_controller::discovery_controller(freebsd::fd_up fd, + struct nvmf_qpair *qp, const struct discovery_log &discovery_log) : + qp(qp), discovery_log(discovery_log), s(std::move(fd)) +{ + nvmf_init_discovery_controller_data(qp, &cdata); + cap = nvmf_controller_cap(qp); + vs = cdata.ver; +} + +void +nvmf_discovery_portal::handle_connection(freebsd::fd_up fd, + const char *host __unused, const struct sockaddr *client_sa) +{ + struct nvmf_qpair_params qparams; + memset(&qparams, 0, sizeof(qparams)); + qparams.tcp.fd = fd; + + struct nvmf_capsule *nc = NULL; + struct nvmf_fabric_connect_data data; + nvmf_qpair_up qp(nvmf_accept(association(), &qparams, &nc, &data)); + if (!qp) { + log_warnx("Failed to create NVMe discovery qpair: %s", + nvmf_association_error(association())); + return; + } + nvmf_capsule_up nc_guard(nc); + + if (strncmp((char *)data.subnqn, NVMF_DISCOVERY_NQN, + sizeof(data.subnqn)) != 0) { + log_warnx("Discovery NVMe qpair with invalid SubNQN: %.*s", + (int)sizeof(data.subnqn), data.subnqn); + nvmf_connect_invalid_parameters(nc, true, + offsetof(struct nvmf_fabric_connect_data, subnqn)); + return; + } + + /* Just use a controller ID of 1 for all discovery controllers. */ + int error = nvmf_finish_accept(nc, 1); + if (error != 0) { + log_warnc(error, "Failed to send NVMe CONNECT reponse"); + return; + } + nc_guard.reset(); + + discovery_log discovery_log = build_discovery_log_page(portal_group(), + fd, client_sa, data); + + discovery_controller controller(std::move(fd), qp.get(), discovery_log); + controller.handle_admin_commands(); +} diff --git a/usr.sbin/ctld/parse.y b/usr.sbin/ctld/parse.y index 432183ed794c..5725c16b459a 100644 --- a/usr.sbin/ctld/parse.y +++ b/usr.sbin/ctld/parse.y @@ -1,903 +1,1127 @@ %{ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2012 The FreeBSD Foundation * * This software was developed by Edward Tomasz Napierala under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 AUTHOR 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 AUTHOR 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. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "conf.h" extern FILE *yyin; extern char *yytext; extern int lineno; extern void yyerror(const char *); extern void yyrestart(FILE *); %} %token ALIAS AUTH_GROUP AUTH_TYPE BACKEND BLOCKSIZE CHAP CHAP_MUTUAL -%token CLOSING_BRACKET CTL_LUN DEBUG DEVICE_ID DEVICE_TYPE -%token DISCOVERY_AUTH_GROUP DISCOVERY_FILTER DSCP FOREIGN +%token CLOSING_BRACKET CONTROLLER CTL_LUN DEBUG DEVICE_ID DEVICE_TYPE +%token DISCOVERY_AUTH_GROUP DISCOVERY_FILTER DISCOVERY_TCP DSCP FOREIGN +%token HOST_ADDRESS HOST_NQN %token INITIATOR_NAME INITIATOR_PORTAL ISNS_SERVER ISNS_PERIOD ISNS_TIMEOUT -%token LISTEN LISTEN_ISER LUN MAXPROC OFFLOAD OPENING_BRACKET OPTION +%token LISTEN LISTEN_ISER LUN MAXPROC NAMESPACE +%token OFFLOAD OPENING_BRACKET OPTION %token PATH PCP PIDFILE PORT PORTAL_GROUP REDIRECT SEMICOLON SERIAL -%token SIZE STR TAG TARGET TIMEOUT +%token SIZE STR TAG TARGET TCP TIMEOUT TRANSPORT_GROUP %token AF11 AF12 AF13 AF21 AF22 AF23 AF31 AF32 AF33 AF41 AF42 AF43 %token BE EF CS0 CS1 CS2 CS3 CS4 CS5 CS6 CS7 %union { char *str; } %token STR %% statements: | statements statement | statements statement SEMICOLON ; statement: debug | timeout | maxproc | pidfile | isns_server | isns_period | isns_timeout | auth_group | portal_group | + transport_group + | lun | target + | + controller ; debug: DEBUG STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); conf_set_debug(tmp); } ; timeout: TIMEOUT STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); conf_set_timeout(tmp); } ; maxproc: MAXPROC STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); conf_set_maxproc(tmp); } ; pidfile: PIDFILE STR { bool ok; ok = conf_set_pidfile_path($2); free($2); if (!ok) return (1); } ; isns_server: ISNS_SERVER STR { bool ok; ok = isns_add_server($2); free($2); if (!ok) return (1); } ; isns_period: ISNS_PERIOD STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); conf_set_isns_period(tmp); } ; isns_timeout: ISNS_TIMEOUT STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); conf_set_isns_timeout(tmp); } ; auth_group: AUTH_GROUP auth_group_name OPENING_BRACKET auth_group_entries CLOSING_BRACKET { auth_group_finish(); } ; auth_group_name: STR { bool ok; ok = auth_group_start($1); free($1); if (!ok) return (1); } ; auth_group_entries: | auth_group_entries auth_group_entry | auth_group_entries auth_group_entry SEMICOLON ; auth_group_entry: auth_group_auth_type | auth_group_chap | auth_group_chap_mutual | + auth_group_host_address + | + auth_group_host_nqn + | auth_group_initiator_name | auth_group_initiator_portal ; auth_group_auth_type: AUTH_TYPE STR { bool ok; ok = auth_group_set_type($2); free($2); if (!ok) return (1); } ; auth_group_chap: CHAP STR STR { bool ok; ok = auth_group_add_chap($2, $3); free($2); free($3); if (!ok) return (1); } ; auth_group_chap_mutual: CHAP_MUTUAL STR STR STR STR { bool ok; ok = auth_group_add_chap_mutual($2, $3, $4, $5); free($2); free($3); free($4); free($5); if (!ok) return (1); } ; +auth_group_host_address: HOST_ADDRESS STR + { + bool ok; + + ok = auth_group_add_host_address($2); + free($2); + if (!ok) + return (1); + } + ; + +auth_group_host_nqn: HOST_NQN STR + { + bool ok; + + ok = auth_group_add_host_nqn($2); + free($2); + if (!ok) + return (1); + } + ; + auth_group_initiator_name: INITIATOR_NAME STR { bool ok; ok = auth_group_add_initiator_name($2); free($2); if (!ok) return (1); } ; auth_group_initiator_portal: INITIATOR_PORTAL STR { bool ok; ok = auth_group_add_initiator_portal($2); free($2); if (!ok) return (1); } ; portal_group: PORTAL_GROUP portal_group_name OPENING_BRACKET portal_group_entries CLOSING_BRACKET { portal_group_finish(); } ; portal_group_name: STR { bool ok; ok = portal_group_start($1); free($1); if (!ok) return (1); } ; portal_group_entries: | portal_group_entries portal_group_entry | portal_group_entries portal_group_entry SEMICOLON ; portal_group_entry: portal_group_discovery_auth_group | portal_group_discovery_filter | portal_group_foreign | portal_group_listen | portal_group_listen_iser | portal_group_offload | portal_group_option | portal_group_redirect | portal_group_tag | portal_group_dscp | portal_group_pcp ; portal_group_discovery_auth_group: DISCOVERY_AUTH_GROUP STR { bool ok; ok = portal_group_set_discovery_auth_group($2); free($2); if (!ok) return (1); } ; portal_group_discovery_filter: DISCOVERY_FILTER STR { bool ok; ok = portal_group_set_filter($2); free($2); if (!ok) return (1); } ; portal_group_foreign: FOREIGN { portal_group_set_foreign(); } ; portal_group_listen: LISTEN STR { bool ok; ok = portal_group_add_listen($2, false); free($2); if (!ok) return (1); } ; portal_group_listen_iser: LISTEN_ISER STR { bool ok; ok = portal_group_add_listen($2, true); free($2); if (!ok) return (1); } ; portal_group_offload: OFFLOAD STR { bool ok; ok = portal_group_set_offload($2); free($2); if (!ok) return (1); } ; portal_group_option: OPTION STR STR { bool ok; ok = portal_group_add_option($2, $3); free($2); free($3); if (!ok) return (1); } ; portal_group_redirect: REDIRECT STR { bool ok; ok = portal_group_set_redirection($2); free($2); if (!ok) return (1); } ; portal_group_tag: TAG STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); portal_group_set_tag(tmp); } ; portal_group_dscp : DSCP STR { int64_t tmp; if (strcmp($2, "0x") == 0) { tmp = strtol($2 + 2, NULL, 16); } else if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); if (!portal_group_set_dscp(tmp)) return (1); } | DSCP BE { portal_group_set_dscp(IPTOS_DSCP_CS0 >> 2); } | DSCP EF { portal_group_set_dscp(IPTOS_DSCP_EF >> 2); } | DSCP CS0 { portal_group_set_dscp(IPTOS_DSCP_CS0 >> 2); } | DSCP CS1 { portal_group_set_dscp(IPTOS_DSCP_CS1 >> 2); } | DSCP CS2 { portal_group_set_dscp(IPTOS_DSCP_CS2 >> 2); } | DSCP CS3 { portal_group_set_dscp(IPTOS_DSCP_CS3 >> 2); } | DSCP CS4 { portal_group_set_dscp(IPTOS_DSCP_CS4 >> 2); } | DSCP CS5 { portal_group_set_dscp(IPTOS_DSCP_CS5 >> 2); } | DSCP CS6 { portal_group_set_dscp(IPTOS_DSCP_CS6 >> 2); } | DSCP CS7 { portal_group_set_dscp(IPTOS_DSCP_CS7 >> 2); } | DSCP AF11 { portal_group_set_dscp(IPTOS_DSCP_AF11 >> 2); } | DSCP AF12 { portal_group_set_dscp(IPTOS_DSCP_AF12 >> 2); } | DSCP AF13 { portal_group_set_dscp(IPTOS_DSCP_AF13 >> 2); } | DSCP AF21 { portal_group_set_dscp(IPTOS_DSCP_AF21 >> 2); } | DSCP AF22 { portal_group_set_dscp(IPTOS_DSCP_AF22 >> 2); } | DSCP AF23 { portal_group_set_dscp(IPTOS_DSCP_AF23 >> 2); } | DSCP AF31 { portal_group_set_dscp(IPTOS_DSCP_AF31 >> 2); } | DSCP AF32 { portal_group_set_dscp(IPTOS_DSCP_AF32 >> 2); } | DSCP AF33 { portal_group_set_dscp(IPTOS_DSCP_AF33 >> 2); } | DSCP AF41 { portal_group_set_dscp(IPTOS_DSCP_AF41 >> 2); } | DSCP AF42 { portal_group_set_dscp(IPTOS_DSCP_AF42 >> 2); } | DSCP AF43 { portal_group_set_dscp(IPTOS_DSCP_AF43 >> 2); } ; portal_group_pcp: PCP STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); if (!portal_group_set_pcp(tmp)) return (1); } ; +transport_group: TRANSPORT_GROUP transport_group_name + OPENING_BRACKET transport_group_entries CLOSING_BRACKET + { + portal_group_finish(); + } + ; + +transport_group_name: STR + { + bool ok; + + ok = transport_group_start($1); + free($1); + if (!ok) + return (1); + } + ; + +transport_group_entries: + | + transport_group_entries transport_group_entry + | + transport_group_entries transport_group_entry SEMICOLON + ; + +transport_group_entry: + portal_group_discovery_auth_group + | + portal_group_discovery_filter + | + transport_group_listen_discovery_tcp + | + transport_group_listen_tcp + | + portal_group_option + | + portal_group_tag + | + portal_group_dscp + | + portal_group_pcp + ; + +transport_group_listen_discovery_tcp: LISTEN DISCOVERY_TCP STR + { + bool ok; + + ok = transport_group_add_listen_discovery_tcp($3); + free($3); + if (!ok) + return (1); + } + ; + +transport_group_listen_tcp: LISTEN TCP STR + { + bool ok; + + ok = transport_group_add_listen_tcp($3); + free($3); + if (!ok) + return (1); + } + ; + lun: LUN lun_name OPENING_BRACKET lun_entries CLOSING_BRACKET { lun_finish(); } ; lun_name: STR { bool ok; ok = lun_start($1); free($1); if (!ok) return (1); } ; target: TARGET target_name OPENING_BRACKET target_entries CLOSING_BRACKET { target_finish(); } ; target_name: STR { bool ok; ok = target_start($1); free($1); if (!ok) return (1); } ; target_entries: | target_entries target_entry | target_entries target_entry SEMICOLON ; target_entry: target_alias | target_auth_group | target_auth_type | target_chap | target_chap_mutual | target_initiator_name | target_initiator_portal | target_portal_group | target_port | target_redirect | target_lun | target_lun_ref ; target_alias: ALIAS STR { bool ok; ok = target_set_alias($2); free($2); if (!ok) return (1); } ; target_auth_group: AUTH_GROUP STR { bool ok; ok = target_set_auth_group($2); free($2); if (!ok) return (1); } ; target_auth_type: AUTH_TYPE STR { bool ok; ok = target_set_auth_type($2); free($2); if (!ok) return (1); } ; target_chap: CHAP STR STR { bool ok; ok = target_add_chap($2, $3); free($2); free($3); if (!ok) return (1); } ; target_chap_mutual: CHAP_MUTUAL STR STR STR STR { bool ok; ok = target_add_chap_mutual($2, $3, $4, $5); free($2); free($3); free($4); free($5); if (!ok) return (1); } ; target_initiator_name: INITIATOR_NAME STR { bool ok; ok = target_add_initiator_name($2); free($2); if (!ok) return (1); } ; target_initiator_portal: INITIATOR_PORTAL STR { bool ok; ok = target_add_initiator_portal($2); free($2); if (!ok) return (1); } ; target_portal_group: PORTAL_GROUP STR STR { bool ok; ok = target_add_portal_group($2, $3); free($2); free($3); if (!ok) return (1); } | PORTAL_GROUP STR { bool ok; ok = target_add_portal_group($2, NULL); free($2); if (!ok) return (1); } ; target_port: PORT STR { bool ok; ok = target_set_physical_port($2); free($2); if (!ok) return (1); } ; target_redirect: REDIRECT STR { bool ok; ok = target_set_redirection($2); free($2); if (!ok) return (1); } ; target_lun: LUN lun_number OPENING_BRACKET lun_entries CLOSING_BRACKET { lun_finish(); } ; lun_number: STR { int64_t tmp; if (expand_number($1, &tmp) != 0) { yyerror("invalid numeric value"); free($1); return (1); } free($1); if (!target_start_lun(tmp)) return (1); } ; target_lun_ref: LUN STR STR { int64_t tmp; bool ok; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); free($3); return (1); } free($2); ok = target_add_lun(tmp, $3); free($3); if (!ok) return (1); } ; +controller: CONTROLLER controller_name + OPENING_BRACKET controller_entries CLOSING_BRACKET + { + target_finish(); + } + ; + +controller_name: STR + { + bool ok; + + ok = controller_start($1); + free($1); + if (!ok) + return (1); + } + ; + +controller_entries: + | + controller_entries controller_entry + | + controller_entries controller_entry SEMICOLON + ; + +controller_entry: + target_auth_group + | + target_auth_type + | + controller_host_address + | + controller_host_nqn + | + controller_transport_group + | + controller_namespace + | + controller_namespace_ref + ; + +controller_host_address: HOST_ADDRESS STR + { + bool ok; + + ok = controller_add_host_address($2); + free($2); + if (!ok) + return (1); + } + ; + +controller_host_nqn: HOST_NQN STR + { + bool ok; + + ok = controller_add_host_nqn($2); + free($2); + if (!ok) + return (1); + } + ; + +controller_transport_group: TRANSPORT_GROUP STR STR + { + bool ok; + + ok = target_add_portal_group($2, $3); + free($2); + free($3); + if (!ok) + return (1); + } + | TRANSPORT_GROUP STR + { + bool ok; + + ok = target_add_portal_group($2, NULL); + free($2); + if (!ok) + return (1); + } + ; + +controller_namespace: NAMESPACE ns_number + OPENING_BRACKET lun_entries CLOSING_BRACKET + { + lun_finish(); + } + ; + +ns_number: STR + { + uint64_t tmp; + + if (expand_number($1, &tmp) != 0) { + yyerror("invalid numeric value"); + free($1); + return (1); + } + free($1); + + if (!controller_start_namespace(tmp)) + return (1); + } + ; + +controller_namespace_ref: NAMESPACE STR STR + { + uint64_t tmp; + bool ok; + + if (expand_number($2, &tmp) != 0) { + yyerror("invalid numeric value"); + free($2); + free($3); + return (1); + } + free($2); + + ok = controller_add_namespace(tmp, $3); + free($3); + if (!ok) + return (1); + } + ; + lun_entries: | lun_entries lun_entry | lun_entries lun_entry SEMICOLON ; lun_entry: lun_backend | lun_blocksize | lun_device_id | lun_device_type | lun_ctl_lun | lun_option | lun_path | lun_serial | lun_size ; lun_backend: BACKEND STR { bool ok; ok = lun_set_backend($2); free($2); if (!ok) return (1); } ; lun_blocksize: BLOCKSIZE STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); if (!lun_set_blocksize(tmp)) return (1); } ; lun_device_id: DEVICE_ID STR { bool ok; ok = lun_set_device_id($2); free($2); if (!ok) return (1); } ; lun_device_type: DEVICE_TYPE STR { bool ok; ok = lun_set_device_type($2); free($2); if (!ok) return (1); } ; lun_ctl_lun: CTL_LUN STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); if (!lun_set_ctl_lun(tmp)) return (1); } ; lun_option: OPTION STR STR { bool ok; ok = lun_add_option($2, $3); free($2); free($3); if (!ok) return (1); } ; lun_path: PATH STR { bool ok; ok = lun_set_path($2); free($2); if (!ok) return (1); } ; lun_serial: SERIAL STR { bool ok; ok = lun_set_serial($2); free($2); if (!ok) return (1); } ; lun_size: SIZE STR { int64_t tmp; if (expand_number($2, &tmp) != 0) { yyerror("invalid numeric value"); free($2); return (1); } free($2); if (!lun_set_size(tmp)) return (1); } ; %% void yyerror(const char *str) { log_warnx("error in configuration file at line %d near '%s': %s", lineno, yytext, str); } bool yyparse_conf(FILE *fp) { int error; yyin = fp; lineno = 1; yyrestart(yyin); error = yyparse(); return (error == 0); } diff --git a/usr.sbin/ctld/token.l b/usr.sbin/ctld/token.l index c8f54103db55..5f959f648969 100644 --- a/usr.sbin/ctld/token.l +++ b/usr.sbin/ctld/token.l @@ -1,120 +1,127 @@ %{ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2012 The FreeBSD Foundation * * This software was developed by Edward Tomasz Napierala under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 AUTHOR 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 AUTHOR 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. */ #include #include #include #include "y.tab.h" int lineno; #define YY_DECL int yylex(void) extern int yylex(void); %} %option noyywrap %option noinput %option nounput %% alias { return ALIAS; } auth-group { return AUTH_GROUP; } auth-type { return AUTH_TYPE; } backend { return BACKEND; } blocksize { return BLOCKSIZE; } chap { return CHAP; } chap-mutual { return CHAP_MUTUAL; } +controller { return CONTROLLER; } ctl-lun { return CTL_LUN; } debug { return DEBUG; } device-id { return DEVICE_ID; } device-type { return DEVICE_TYPE; } discovery-auth-group { return DISCOVERY_AUTH_GROUP; } discovery-filter { return DISCOVERY_FILTER; } +discovery-tcp { return DISCOVERY_TCP; } dscp { return DSCP; } pcp { return PCP; } foreign { return FOREIGN; } +host-address { return HOST_ADDRESS; } +host-nqn { return HOST_NQN; } initiator-name { return INITIATOR_NAME; } initiator-portal { return INITIATOR_PORTAL; } listen { return LISTEN; } listen-iser { return LISTEN_ISER; } lun { return LUN; } maxproc { return MAXPROC; } +namespace { return NAMESPACE; } offload { return OFFLOAD; } option { return OPTION; } path { return PATH; } pidfile { return PIDFILE; } isns-server { return ISNS_SERVER; } isns-period { return ISNS_PERIOD; } isns-timeout { return ISNS_TIMEOUT; } port { return PORT; } portal-group { return PORTAL_GROUP; } redirect { return REDIRECT; } serial { return SERIAL; } size { return SIZE; } tag { return TAG; } target { return TARGET; } +tcp { return TCP; } timeout { return TIMEOUT; } +transport-group { return TRANSPORT_GROUP; } af11 { return AF11; } af12 { return AF12; } af13 { return AF13; } af21 { return AF21; } af22 { return AF22; } af23 { return AF23; } af31 { return AF31; } af32 { return AF32; } af33 { return AF33; } af41 { return AF41; } af42 { return AF42; } af43 { return AF43; } be { return CS0; } ef { return EF; } cs0 { return CS0; } cs1 { return CS1; } cs2 { return CS2; } cs3 { return CS3; } cs4 { return CS4; } cs5 { return CS5; } cs6 { return CS6; } cs7 { return CS7; } \"[^"]+\" { yylval.str = strndup(yytext + 1, strlen(yytext) - 2); return STR; } [a-zA-Z0-9\.\-@_/\:\[\]]+ { yylval.str = strdup(yytext); return STR; } \{ { return OPENING_BRACKET; } \} { return CLOSING_BRACKET; } #.*$ /* ignore comments */; \r\n { lineno++; } \n { lineno++; } ; { return SEMICOLON; } [ \t]+ /* ignore whitespace */; . { yylval.str = strdup(yytext); return STR; } %% diff --git a/usr.sbin/ctld/uclparse.cc b/usr.sbin/ctld/uclparse.cc index 843e727a2e52..cb3b0a17cd74 100644 --- a/usr.sbin/ctld/uclparse.cc +++ b/usr.sbin/ctld/uclparse.cc @@ -1,1049 +1,1430 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2015 iXsystems Inc. * All rights reserved. * * This software was developed by Jakub Klama * under sponsorship from iXsystems Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 AUTHOR 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 AUTHOR 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. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "conf.h" #include "ctld.hh" struct scope_exit { using callback = void(); scope_exit(callback *fn) : fn(fn) {} ~scope_exit() { fn(); } private: callback *fn; }; static bool uclparse_toplevel(const ucl::Ucl &); static bool uclparse_chap(const char *, const ucl::Ucl &); static bool uclparse_chap_mutual(const char *, const ucl::Ucl &); static bool uclparse_lun(const char *, const ucl::Ucl &); static bool uclparse_lun_entries(const char *, const ucl::Ucl &); static bool uclparse_auth_group(const char *, const ucl::Ucl &); static bool uclparse_portal_group(const char *, const ucl::Ucl &); +static bool uclparse_transport_group(const char *, const ucl::Ucl &); +static bool uclparse_controller(const char *, const ucl::Ucl &); +static bool uclparse_controller_transport_group(const char *, const ucl::Ucl &); +static bool uclparse_controller_namespace(const char *, const ucl::Ucl &); static bool uclparse_target(const char *, const ucl::Ucl &); static bool uclparse_target_portal_group(const char *, const ucl::Ucl &); static bool uclparse_target_lun(const char *, const ucl::Ucl &); static bool uclparse_chap(const char *ag_name, const ucl::Ucl &obj) { auto user = obj["user"]; if (!user || user.type() != UCL_STRING) { log_warnx("chap section in auth-group \"%s\" is missing " "\"user\" string key", ag_name); return (false); } auto secret = obj["secret"]; if (!secret || secret.type() != UCL_STRING) { log_warnx("chap section in auth-group \"%s\" is missing " "\"secret\" string key", ag_name); return (false); } return (auth_group_add_chap( user.string_value().c_str(), secret.string_value().c_str())); } static bool uclparse_chap_mutual(const char *ag_name, const ucl::Ucl &obj) { auto user = obj["user"]; if (!user || user.type() != UCL_STRING) { log_warnx("chap-mutual section in auth-group \"%s\" is missing " "\"user\" string key", ag_name); return (false); } auto secret = obj["secret"]; if (!secret || secret.type() != UCL_STRING) { log_warnx("chap-mutual section in auth-group \"%s\" is missing " "\"secret\" string key", ag_name); return (false); } auto mutual_user = obj["mutual-user"]; if (!mutual_user || mutual_user.type() != UCL_STRING) { log_warnx("chap-mutual section in auth-group \"%s\" is missing " "\"mutual-user\" string key", ag_name); return (false); } auto mutual_secret = obj["mutual-secret"]; if (!mutual_secret || mutual_secret.type() != UCL_STRING) { log_warnx("chap-mutual section in auth-group \"%s\" is missing " "\"mutual-secret\" string key", ag_name); return (false); } return (auth_group_add_chap_mutual( user.string_value().c_str(), secret.string_value().c_str(), mutual_user.string_value().c_str(), mutual_secret.string_value().c_str())); } static bool uclparse_target_chap(const char *t_name, const ucl::Ucl &obj) { auto user = obj["user"]; if (!user || user.type() != UCL_STRING) { log_warnx("chap section in target \"%s\" is missing " "\"user\" string key", t_name); return (false); } auto secret = obj["secret"]; if (!secret || secret.type() != UCL_STRING) { log_warnx("chap section in target \"%s\" is missing " "\"secret\" string key", t_name); return (false); } return (target_add_chap( user.string_value().c_str(), secret.string_value().c_str())); } static bool uclparse_target_chap_mutual(const char *t_name, const ucl::Ucl &obj) { auto user = obj["user"]; if (!user || user.type() != UCL_STRING) { log_warnx("chap-mutual section in target \"%s\" is missing " "\"user\" string key", t_name); return (false); } auto secret = obj["secret"]; if (!secret || secret.type() != UCL_STRING) { log_warnx("chap-mutual section in target \"%s\" is missing " "\"secret\" string key", t_name); return (false); } auto mutual_user = obj["mutual-user"]; if (!mutual_user || mutual_user.type() != UCL_STRING) { log_warnx("chap-mutual section in target \"%s\" is missing " "\"mutual-user\" string key", t_name); return (false); } auto mutual_secret = obj["mutual-secret"]; if (!mutual_secret || mutual_secret.type() != UCL_STRING) { log_warnx("chap-mutual section in target \"%s\" is missing " "\"mutual-secret\" string key", t_name); return (false); } return (target_add_chap_mutual( user.string_value().c_str(), secret.string_value().c_str(), mutual_user.string_value().c_str(), mutual_secret.string_value().c_str())); } static bool uclparse_target_portal_group(const char *t_name, const ucl::Ucl &obj) { /* * If the value is a single string, assume it is a * portal-group name. */ if (obj.type() == UCL_STRING) return (target_add_portal_group(obj.string_value().c_str(), NULL)); if (obj.type() != UCL_OBJECT) { log_warnx("portal-group section in target \"%s\" must be " "an object or string", t_name); return (false); } auto portal_group = obj["name"]; if (!portal_group || portal_group.type() != UCL_STRING) { log_warnx("portal-group section in target \"%s\" is missing " "\"name\" string key", t_name); return (false); } auto auth_group = obj["auth-group-name"]; if (auth_group) { if (auth_group.type() != UCL_STRING) { log_warnx("\"auth-group-name\" property in " "portal-group section for target \"%s\" is not " "a string", t_name); return (false); } return (target_add_portal_group( portal_group.string_value().c_str(), auth_group.string_value().c_str())); } return (target_add_portal_group(portal_group.string_value().c_str(), NULL)); } +static bool +uclparse_controller_transport_group(const char *t_name, const ucl::Ucl &obj) +{ + /* + * If the value is a single string, assume it is a + * transport-group name. + */ + if (obj.type() == UCL_STRING) + return target_add_portal_group(obj.string_value().c_str(), + nullptr); + + if (obj.type() != UCL_OBJECT) { + log_warnx("transport-group section in controller \"%s\" must " + "be an object or string", t_name); + return false; + } + + auto portal_group = obj["name"]; + if (!portal_group || portal_group.type() != UCL_STRING) { + log_warnx("transport-group section in controller \"%s\" is " + "missing \"name\" string key", t_name); + return false; + } + + auto auth_group = obj["auth-group-name"]; + if (auth_group) { + if (auth_group.type() != UCL_STRING) { + log_warnx("\"auth-group-name\" property in " + "transport-group section for controller \"%s\" is " + "not a string", t_name); + return false; + } + return target_add_portal_group( + portal_group.string_value().c_str(), + auth_group.string_value().c_str()); + } + + return target_add_portal_group(portal_group.string_value().c_str(), + nullptr); +} + static bool uclparse_target_lun(const char *t_name, const ucl::Ucl &obj) { char *end; u_int id; std::string key = obj.key(); if (!key.empty()) { id = strtoul(key.c_str(), &end, 0); if (*end != '\0') { log_warnx("lun key \"%s\" in target \"%s\" is invalid", key.c_str(), t_name); return (false); } if (obj.type() == UCL_STRING) return (target_add_lun(id, obj.string_value().c_str())); } if (obj.type() != UCL_OBJECT) { log_warnx("lun section entries in target \"%s\" must be objects", t_name); return (false); } if (key.empty()) { auto num = obj["number"]; if (!num || num.type() != UCL_INT) { log_warnx("lun section in target \"%s\" is missing " "\"number\" integer property", t_name); return (false); } id = num.int_value(); } auto name = obj["name"]; if (!name) { if (!target_start_lun(id)) return (false); scope_exit finisher(lun_finish); std::string lun_name = freebsd::stringf("lun %u for target \"%s\"", id, t_name); return (uclparse_lun_entries(lun_name.c_str(), obj)); } if (name.type() != UCL_STRING) { log_warnx("\"name\" property for lun %u for target " "\"%s\" is not a string", id, t_name); return (false); } return (target_add_lun(id, name.string_value().c_str())); } +static bool +uclparse_controller_namespace(const char *t_name, const ucl::Ucl &obj) +{ + char *end; + u_int id; + + std::string key = obj.key(); + if (!key.empty()) { + id = strtoul(key.c_str(), &end, 0); + if (*end != '\0') { + log_warnx("namespace key \"%s\" in controller \"%s\"" + " is invalid", key.c_str(), t_name); + return false; + } + + if (obj.type() == UCL_STRING) + return controller_add_namespace(id, + obj.string_value().c_str()); + } + + if (obj.type() != UCL_OBJECT) { + log_warnx("namespace section entries in controller \"%s\"" + " must be objects", t_name); + return false; + } + + if (key.empty()) { + auto num = obj["number"]; + if (!num || num.type() != UCL_INT) { + log_warnx("namespace section in controller \"%s\" is " + "missing \"id\" integer property", t_name); + return (false); + } + id = num.int_value(); + } + + auto name = obj["name"]; + if (!name) { + if (!controller_start_namespace(id)) + return false; + + std::string lun_name = + freebsd::stringf("namespace %u for controller \"%s\"", id, + t_name); + return uclparse_lun_entries(lun_name.c_str(), obj); + } + + if (name.type() != UCL_STRING) { + log_warnx("\"name\" property for namespace %u for " + "controller \"%s\" is not a string", id, t_name); + return (false); + } + + return controller_add_namespace(id, name.string_value().c_str()); +} + static bool uclparse_toplevel(const ucl::Ucl &top) { /* Pass 1 - everything except targets */ for (const auto &obj : top) { std::string key = obj.key(); if (key == "debug") { if (obj.type() == UCL_INT) conf_set_debug(obj.int_value()); else { log_warnx("\"debug\" property value is not integer"); return (false); } } if (key == "timeout") { if (obj.type() == UCL_INT) conf_set_timeout(obj.int_value()); else { log_warnx("\"timeout\" property value is not integer"); return (false); } } if (key == "maxproc") { if (obj.type() == UCL_INT) conf_set_maxproc(obj.int_value()); else { log_warnx("\"maxproc\" property value is not integer"); return (false); } } if (key == "pidfile") { if (obj.type() == UCL_STRING) { if (!conf_set_pidfile_path( obj.string_value().c_str())) return (false); } else { log_warnx("\"pidfile\" property value is not string"); return (false); } } if (key == "isns-server") { if (obj.type() == UCL_ARRAY) { for (const auto &child : obj) { if (child.type() != UCL_STRING) return (false); if (!isns_add_server( child.string_value().c_str())) return (false); } } else { log_warnx("\"isns-server\" property value is " "not an array"); return (false); } } if (key == "isns-period") { if (obj.type() == UCL_INT) conf_set_isns_period(obj.int_value()); else { log_warnx("\"isns-period\" property value is not integer"); return (false); } } if (key == "isns-timeout") { if (obj.type() == UCL_INT) conf_set_isns_timeout(obj.int_value()); else { log_warnx("\"isns-timeout\" property value is not integer"); return (false); } } if (key == "auth-group") { if (obj.type() == UCL_OBJECT) { for (const auto &child : obj) { if (!uclparse_auth_group( child.key().c_str(), child)) return (false); } } else { log_warnx("\"auth-group\" section is not an object"); return (false); } } if (key == "portal-group") { if (obj.type() == UCL_OBJECT) { for (const auto &child : obj) { if (!uclparse_portal_group( child.key().c_str(), child)) return (false); } } else { log_warnx("\"portal-group\" section is not an object"); return (false); } } + if (key == "transport-group") { + if (obj.type() == UCL_OBJECT) { + for (const auto &child : obj) { + if (!uclparse_transport_group( + child.key().c_str(), child)) + return false; + } + } else { + log_warnx("\"transport-group\" section is not an object"); + return false; + } + } + if (key == "lun") { if (obj.type() == UCL_OBJECT) { for (const auto &child : obj) { if (!uclparse_lun(child.key().c_str(), child)) return (false); } } else { log_warnx("\"lun\" section is not an object"); return (false); } } } /* Pass 2 - targets */ for (const auto &obj : top) { std::string key = obj.key(); + if (key == "controller") { + if (obj.type() == UCL_OBJECT) { + for (const auto &child : obj) { + if (!uclparse_controller( + child.key().c_str(), child)) + return false; + } + } else { + log_warnx("\"controller\" section is not an object"); + return false; + } + } + if (key == "target") { if (obj.type() == UCL_OBJECT) { for (const auto &child : obj) { if (!uclparse_target( child.key().c_str(), child)) return (false); } } else { log_warnx("\"target\" section is not an object"); return (false); } } } return (true); } static bool uclparse_auth_group(const char *name, const ucl::Ucl &top) { if (!auth_group_start(name)) return (false); scope_exit finisher(auth_group_finish); for (const auto &obj : top) { std::string key = obj.key(); if (key == "auth-type") { if (!auth_group_set_type(obj.string_value().c_str())) return false; } if (key == "chap") { if (obj.type() == UCL_OBJECT) { if (!uclparse_chap(name, obj)) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!uclparse_chap(name, tmp)) return false; } } else { log_warnx("\"chap\" property of auth-group " "\"%s\" is not an array or object", name); return false; } } if (key == "chap-mutual") { if (obj.type() == UCL_OBJECT) { if (!uclparse_chap_mutual(name, obj)) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!uclparse_chap_mutual(name, tmp)) return false; } } else { log_warnx("\"chap-mutual\" property of " "auth-group \"%s\" is not an array or object", name); return false; } } + if (key == "host-address") { + if (obj.type() == UCL_STRING) { + if (!auth_group_add_host_address( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!auth_group_add_host_address( + tmp.string_value().c_str())) + return false; + } + } else { + log_warnx("\"host-address\" property of " + "auth-group \"%s\" is not an array or string", + name); + return false; + } + } + + if (key == "host-nqn") { + if (obj.type() == UCL_STRING) { + if (!auth_group_add_host_nqn( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!auth_group_add_host_nqn( + tmp.string_value().c_str())) + return false; + } + } else { + log_warnx("\"host-nqn\" property of " + "auth-group \"%s\" is not an array or string", + name); + return false; + } + } + if (key == "initiator-name") { if (obj.type() == UCL_STRING) { if (!auth_group_add_initiator_name( obj.string_value().c_str())) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!auth_group_add_initiator_name( tmp.string_value().c_str())) return false; } } else { log_warnx("\"initiator-name\" property of " "auth-group \"%s\" is not an array or string", name); return false; } } if (key == "initiator-portal") { if (obj.type() == UCL_STRING) { if (!auth_group_add_initiator_portal( obj.string_value().c_str())) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!auth_group_add_initiator_portal( tmp.string_value().c_str())) return false; } } else { log_warnx("\"initiator-portal\" property of " "auth-group \"%s\" is not an array or string", name); return false; } } } return (true); } static bool uclparse_dscp(const char *group_type, const char *pg_name, const ucl::Ucl &obj) { if ((obj.type() != UCL_STRING) && (obj.type() != UCL_INT)) { log_warnx("\"dscp\" property of %s group \"%s\" is not a " "string or integer", group_type, pg_name); return (false); } if (obj.type() == UCL_INT) return (portal_group_set_dscp(obj.int_value())); std::string key = obj.key(); if (key == "be" || key == "cs0") portal_group_set_dscp(IPTOS_DSCP_CS0 >> 2); else if (key == "ef") portal_group_set_dscp(IPTOS_DSCP_EF >> 2); else if (key == "cs0") portal_group_set_dscp(IPTOS_DSCP_CS0 >> 2); else if (key == "cs1") portal_group_set_dscp(IPTOS_DSCP_CS1 >> 2); else if (key == "cs2") portal_group_set_dscp(IPTOS_DSCP_CS2 >> 2); else if (key == "cs3") portal_group_set_dscp(IPTOS_DSCP_CS3 >> 2); else if (key == "cs4") portal_group_set_dscp(IPTOS_DSCP_CS4 >> 2); else if (key == "cs5") portal_group_set_dscp(IPTOS_DSCP_CS5 >> 2); else if (key == "cs6") portal_group_set_dscp(IPTOS_DSCP_CS6 >> 2); else if (key == "cs7") portal_group_set_dscp(IPTOS_DSCP_CS7 >> 2); else if (key == "af11") portal_group_set_dscp(IPTOS_DSCP_AF11 >> 2); else if (key == "af12") portal_group_set_dscp(IPTOS_DSCP_AF12 >> 2); else if (key == "af13") portal_group_set_dscp(IPTOS_DSCP_AF13 >> 2); else if (key == "af21") portal_group_set_dscp(IPTOS_DSCP_AF21 >> 2); else if (key == "af22") portal_group_set_dscp(IPTOS_DSCP_AF22 >> 2); else if (key == "af23") portal_group_set_dscp(IPTOS_DSCP_AF23 >> 2); else if (key == "af31") portal_group_set_dscp(IPTOS_DSCP_AF31 >> 2); else if (key == "af32") portal_group_set_dscp(IPTOS_DSCP_AF32 >> 2); else if (key == "af33") portal_group_set_dscp(IPTOS_DSCP_AF33 >> 2); else if (key == "af41") portal_group_set_dscp(IPTOS_DSCP_AF41 >> 2); else if (key == "af42") portal_group_set_dscp(IPTOS_DSCP_AF42 >> 2); else if (key == "af43") portal_group_set_dscp(IPTOS_DSCP_AF43 >> 2); else { log_warnx("\"dscp\" property value is not a supported textual value"); return (false); } return (true); } static bool uclparse_pcp(const char *group_type, const char *pg_name, const ucl::Ucl &obj) { if (obj.type() != UCL_INT) { log_warnx("\"pcp\" property of %s group \"%s\" is not an " "integer", group_type, pg_name); return (false); } return (portal_group_set_pcp(obj.int_value())); } static bool uclparse_portal_group(const char *name, const ucl::Ucl &top) { if (!portal_group_start(name)) return (false); scope_exit finisher(portal_group_finish); for (const auto &obj : top) { std::string key = obj.key(); if (key == "discovery-auth-group") { if (obj.type() != UCL_STRING) { log_warnx("\"discovery-auth-group\" property " "of portal-group \"%s\" is not a string", name); return false; } if (!portal_group_set_discovery_auth_group( obj.string_value().c_str())) return false; } if (key == "discovery-filter") { if (obj.type() != UCL_STRING) { log_warnx("\"discovery-filter\" property of " "portal-group \"%s\" is not a string", name); return false; } if (!portal_group_set_filter( obj.string_value().c_str())) return false; } if (key == "foreign") { portal_group_set_foreign(); } if (key == "listen") { if (obj.type() == UCL_STRING) { if (!portal_group_add_listen( obj.string_value().c_str(), false)) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!portal_group_add_listen( tmp.string_value().c_str(), false)) return false; } } else { log_warnx("\"listen\" property of " "portal-group \"%s\" is not a string", name); return false; } } if (key == "listen-iser") { if (obj.type() == UCL_STRING) { if (!portal_group_add_listen( obj.string_value().c_str(), true)) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!portal_group_add_listen( tmp.string_value().c_str(), true)) return false; } } else { log_warnx("\"listen\" property of " "portal-group \"%s\" is not a string", name); return false; } } if (key == "offload") { if (obj.type() != UCL_STRING) { log_warnx("\"offload\" property of " "portal-group \"%s\" is not a string", name); return false; } if (!portal_group_set_offload( obj.string_value().c_str())) return false; } if (key == "redirect") { if (obj.type() != UCL_STRING) { log_warnx("\"listen\" property of " "portal-group \"%s\" is not a string", name); return false; } if (!portal_group_set_redirection( obj.string_value().c_str())) return false; } if (key == "options") { if (obj.type() != UCL_OBJECT) { log_warnx("\"options\" property of portal group " "\"%s\" is not an object", name); return false; } for (const auto &tmp : obj) { if (!portal_group_add_option( tmp.key().c_str(), tmp.forced_string_value().c_str())) return false; } } if (key == "tag") { if (obj.type() != UCL_INT) { log_warnx("\"tag\" property of portal group " "\"%s\" is not an integer", name); return false; } portal_group_set_tag(obj.int_value()); } if (key == "dscp") { if (!uclparse_dscp("portal", name, obj)) return false; } if (key == "pcp") { if (!uclparse_pcp("portal", name, obj)) return false; } } return (true); } +static bool +uclparse_transport_listen_obj(const char *pg_name, const ucl::Ucl &top) +{ + for (const auto &obj : top) { + std::string key = obj.key(); + + if (key.empty()) { + log_warnx("missing protocol for \"listen\" " + "property of transport-group \"%s\"", pg_name); + return false; + } + + if (key == "tcp") { + if (obj.type() == UCL_STRING) { + if (!transport_group_add_listen_tcp( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!transport_group_add_listen_tcp( + tmp.string_value().c_str())) + return false; + } + } + } else if (key == "discovery-tcp") { + if (obj.type() == UCL_STRING) { + if (!transport_group_add_listen_discovery_tcp( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!transport_group_add_listen_discovery_tcp( + tmp.string_value().c_str())) + return false; + } + } + } else { + log_warnx("invalid listen protocol \"%s\" for " + "transport-group \"%s\"", key.c_str(), pg_name); + return false; + } + } + return true; +} + +static bool +uclparse_transport_group(const char *name, const ucl::Ucl &top) +{ + if (!transport_group_start(name)) + return false; + + scope_exit finisher(portal_group_finish); + for (const auto &obj : top) { + std::string key = obj.key(); + + if (key == "discovery-auth-group") { + if (obj.type() != UCL_STRING) { + log_warnx("\"discovery-auth-group\" property " + "of transport-group \"%s\" is not a string", + name); + return false; + } + + if (!portal_group_set_discovery_auth_group( + obj.string_value().c_str())) + return false; + } + + if (key == "discovery-filter") { + if (obj.type() != UCL_STRING) { + log_warnx("\"discovery-filter\" property of " + "transport-group \"%s\" is not a string", + name); + return false; + } + + if (!portal_group_set_filter( + obj.string_value().c_str())) + return false; + } + + if (key == "listen") { + if (obj.type() != UCL_OBJECT) { + log_warnx("\"listen\" property of " + "transport-group \"%s\" is not an object", + name); + return false; + } + if (!uclparse_transport_listen_obj(name, obj)) + return false; + } + + if (key == "options") { + if (obj.type() != UCL_OBJECT) { + log_warnx("\"options\" property of transport group " + "\"%s\" is not an object", name); + return false; + } + + for (const auto &tmp : obj) { + if (!portal_group_add_option( + tmp.key().c_str(), + tmp.forced_string_value().c_str())) + return false; + } + } + + if (key == "dscp") { + if (!uclparse_dscp("transport", name, obj)) + return false; + } + + if (key == "pcp") { + if (!uclparse_pcp("transport", name, obj)) + return false; + } + } + + return true; +} + +static bool +uclparse_controller(const char *name, const ucl::Ucl &top) +{ + if (!controller_start(name)) + return false; + + scope_exit finisher(target_finish); + for (const auto &obj : top) { + std::string key = obj.key(); + + if (key == "auth-group") { + if (obj.type() != UCL_STRING) { + log_warnx("\"auth-group\" property of " + "controller \"%s\" is not a string", name); + return false; + } + + if (!target_set_auth_group(obj.string_value().c_str())) + return false; + } + + if (key == "auth-type") { + if (obj.type() != UCL_STRING) { + log_warnx("\"auth-type\" property of " + "controller \"%s\" is not a string", name); + return false; + } + + if (!target_set_auth_type(obj.string_value().c_str())) + return false; + } + + if (key == "host-address") { + if (obj.type() == UCL_STRING) { + if (!controller_add_host_address( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!controller_add_host_address( + tmp.string_value().c_str())) + return false; + } + } else { + log_warnx("\"host-address\" property of " + "controller \"%s\" is not an array or " + "string", name); + return false; + } + } + + if (key == "host-nqn") { + if (obj.type() == UCL_STRING) { + if (!controller_add_host_nqn( + obj.string_value().c_str())) + return false; + } else if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!controller_add_host_nqn( + tmp.string_value().c_str())) + return false; + } + } else { + log_warnx("\"host-nqn\" property of " + "controller \"%s\" is not an array or " + "string", name); + return false; + } + } + + if (key == "transport-group") { + if (obj.type() == UCL_ARRAY) { + for (const auto &tmp : obj) { + if (!uclparse_controller_transport_group(name, + tmp)) + return false; + } + } else { + if (!uclparse_controller_transport_group(name, + obj)) + return false; + } + } + + if (key == "namespace") { + for (const auto &tmp : obj) { + if (!uclparse_controller_namespace(name, tmp)) + return false; + } + } + } + + return true; +} + static bool uclparse_target(const char *name, const ucl::Ucl &top) { if (!target_start(name)) return (false); scope_exit finisher(target_finish); for (const auto &obj : top) { std::string key = obj.key(); if (key == "alias") { if (obj.type() != UCL_STRING) { log_warnx("\"alias\" property of target " "\"%s\" is not a string", name); return false; } if (!target_set_alias(obj.string_value().c_str())) return false; } if (key == "auth-group") { if (obj.type() != UCL_STRING) { log_warnx("\"auth-group\" property of target " "\"%s\" is not a string", name); return false; } if (!target_set_auth_group(obj.string_value().c_str())) return false; } if (key == "auth-type") { if (obj.type() != UCL_STRING) { log_warnx("\"auth-type\" property of target " "\"%s\" is not a string", name); return false; } if (!target_set_auth_type(obj.string_value().c_str())) return false; } if (key == "chap") { if (obj.type() == UCL_OBJECT) { if (!uclparse_target_chap(name, obj)) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!uclparse_target_chap(name, tmp)) return false; } } else { log_warnx("\"chap\" property of target " "\"%s\" is not an array or object", name); return false; } } if (key == "chap-mutual") { if (obj.type() == UCL_OBJECT) { if (!uclparse_target_chap_mutual(name, obj)) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!uclparse_target_chap_mutual(name, tmp)) return false; } } else { log_warnx("\"chap-mutual\" property of target " "\"%s\" is not an array or object", name); return false; } } if (key == "initiator-name") { if (obj.type() == UCL_STRING) { if (!target_add_initiator_name( obj.string_value().c_str())) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!target_add_initiator_name( tmp.string_value().c_str())) return false; } } else { log_warnx("\"initiator-name\" property of " "target \"%s\" is not an array or string", name); return false; } } if (key == "initiator-portal") { if (obj.type() == UCL_STRING) { if (!target_add_initiator_portal( obj.string_value().c_str())) return false; } else if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!target_add_initiator_portal( tmp.string_value().c_str())) return false; } } else { log_warnx("\"initiator-portal\" property of " "target \"%s\" is not an array or string", name); return false; } } if (key == "portal-group") { if (obj.type() == UCL_ARRAY) { for (const auto &tmp : obj) { if (!uclparse_target_portal_group(name, tmp)) return false; } } else { if (!uclparse_target_portal_group(name, obj)) return false; } } if (key == "port") { if (obj.type() != UCL_STRING) { log_warnx("\"port\" property of target " "\"%s\" is not a string", name); return false; } if (!target_set_physical_port(obj.string_value().c_str())) return false; } if (key == "redirect") { if (obj.type() != UCL_STRING) { log_warnx("\"redirect\" property of target " "\"%s\" is not a string", name); return false; } if (!target_set_redirection(obj.string_value().c_str())) return false; } if (key == "lun") { for (const auto &tmp : obj) { if (!uclparse_target_lun(name, tmp)) return false; } } } return (true); } static bool uclparse_lun(const char *name, const ucl::Ucl &top) { if (!lun_start(name)) return (false); scope_exit finisher(lun_finish); std::string lun_name = freebsd::stringf("lun \"%s\"", name); return (uclparse_lun_entries(lun_name.c_str(), top)); } static bool uclparse_lun_entries(const char *name, const ucl::Ucl &top) { for (const auto &obj : top) { std::string key = obj.key(); if (key == "backend") { if (obj.type() != UCL_STRING) { log_warnx("\"backend\" property of %s " "is not a string", name); return false; } if (!lun_set_backend(obj.string_value().c_str())) return false; } if (key == "blocksize") { if (obj.type() != UCL_INT) { log_warnx("\"blocksize\" property of %s " "is not an integer", name); return false; } if (!lun_set_blocksize(obj.int_value())) return false; } if (key == "device-id") { if (obj.type() != UCL_STRING) { log_warnx("\"device-id\" property of %s " "is not an integer", name); return false; } if (!lun_set_device_id(obj.string_value().c_str())) return false; } if (key == "device-type") { if (obj.type() != UCL_STRING) { log_warnx("\"device-type\" property of %s " "is not an integer", name); return false; } if (!lun_set_device_type(obj.string_value().c_str())) return false; } if (key == "ctl-lun") { if (obj.type() != UCL_INT) { log_warnx("\"ctl-lun\" property of %s " "is not an integer", name); return false; } if (!lun_set_ctl_lun(obj.int_value())) return false; } if (key == "options") { if (obj.type() != UCL_OBJECT) { log_warnx("\"options\" property of %s " "is not an object", name); return false; } for (const auto &child : obj) { if (!lun_add_option(child.key().c_str(), child.forced_string_value().c_str())) return false; } } if (key == "path") { if (obj.type() != UCL_STRING) { log_warnx("\"path\" property of %s " "is not a string", name); return false; } if (!lun_set_path(obj.string_value().c_str())) return false; } if (key == "serial") { if (obj.type() != UCL_STRING) { log_warnx("\"serial\" property of %s " "is not a string", name); return false; } if (!lun_set_serial(obj.string_value().c_str())) return false; } if (key == "size") { if (obj.type() != UCL_INT) { log_warnx("\"size\" property of %s " "is not an integer", name); return false; } if (!lun_set_size(obj.int_value())) return false; } } return (true); } bool uclparse_conf(const char *path) { std::string err; ucl::Ucl top = ucl::Ucl::parse_from_file(path, err); if (!top) { log_warnx("unable to parse configuration file %s: %s", path, err.c_str()); return (false); } bool parsed; try { parsed = uclparse_toplevel(top); } catch (std::bad_alloc) { log_warnx("failed to allocate memory parsing %s", path); parsed = false; } catch (...) { log_warnx("unknown exception parsing %s", path); parsed = false; } return (parsed); }