/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2017 Joyent, Inc.
 * Copyright 2026 Oxide Computer Company
 * Copyright 2022 Tintri by DDN, Inc. All rights reserved.
 */

/*
 * nvmeadm -- NVMe administration utility
 *
 * nvmeadm [-v] [-d] [-h] <command> [<ctl>[/<ns>][,...]] [args]
 * commands:	list
 *		identify
 *		list-logpages [logpage name],...
 *		get-logpage <logpage name>
 *		get-features <feature>[,...]
 *		format ...
 *		secure-erase ...
 *		detach ...
 *		attach ...
 *		list-firmware ...
 *		load-firmware ...
 *		commit-firmware ...
 *		activate-firmware ...
 */

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <unistd.h>
#include <fcntl.h>
#include <strings.h>
#include <ctype.h>
#include <err.h>
#include <sys/sunddi.h>
#include <libdevinfo.h>
#include <sys/sysmacros.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/fm/protocol.h>
#include <fm/topo_hc.h>

#include <sys/nvme.h>

#include "nvmeadm.h"

/*
 * Assertions to make sure that we've properly captured various aspects of the
 * packed structures and haven't broken them during updates.
 */
CTASSERT(sizeof (nvme_identify_ctrl_t) == NVME_IDENTIFY_BUFSIZE);
CTASSERT(offsetof(nvme_identify_ctrl_t, id_oacs) == 256);
CTASSERT(offsetof(nvme_identify_ctrl_t, id_sqes) == 512);
CTASSERT(offsetof(nvme_identify_ctrl_t, id_oncs) == 520);
CTASSERT(offsetof(nvme_identify_ctrl_t, id_subnqn) == 768);
CTASSERT(offsetof(nvme_identify_ctrl_t, id_nvmof) == 1792);
CTASSERT(offsetof(nvme_identify_ctrl_t, id_psd) == 2048);
CTASSERT(offsetof(nvme_identify_ctrl_t, id_vs) == 3072);

CTASSERT(sizeof (nvme_identify_nsid_t) == NVME_IDENTIFY_BUFSIZE);
CTASSERT(offsetof(nvme_identify_nsid_t, id_fpi) == 32);
CTASSERT(offsetof(nvme_identify_nsid_t, id_anagrpid) == 92);
CTASSERT(offsetof(nvme_identify_nsid_t, id_nguid) == 104);
CTASSERT(offsetof(nvme_identify_nsid_t, id_lbaf) == 128);
CTASSERT(offsetof(nvme_identify_nsid_t, id_vs) == 384);

CTASSERT(sizeof (nvme_identify_nsid_list_t) == NVME_IDENTIFY_BUFSIZE);
CTASSERT(sizeof (nvme_identify_ctrl_list_t) == NVME_IDENTIFY_BUFSIZE);

CTASSERT(sizeof (nvme_identify_primary_caps_t) == NVME_IDENTIFY_BUFSIZE);
CTASSERT(offsetof(nvme_identify_primary_caps_t, nipc_vqfrt) == 32);
CTASSERT(offsetof(nvme_identify_primary_caps_t, nipc_vifrt) == 64);

CTASSERT(sizeof (nvme_nschange_list_t) == 4096);

static void usage(const nvmeadm_cmd_t *);
static bool nvmeadm_ctrl_disc_cb(nvme_t *, const nvme_ctrl_disc_t *, void *);

static int do_list(const nvme_process_arg_t *);
static int do_identify(const nvme_process_arg_t *);
static int do_identify_ctrl(const nvme_process_arg_t *);
static int do_identify_ns(const nvme_process_arg_t *);
static int do_list_logs(const nvme_process_arg_t *);
static int do_get_logpage_fwslot(const nvme_process_arg_t *);
static int do_get_logpage(const nvme_process_arg_t *);
static int do_print_logpage(const nvme_process_arg_t *);
static int do_list_features(const nvme_process_arg_t *);
static boolean_t do_get_feat_intr_vect(const nvme_process_arg_t *,
    const nvme_feat_disc_t *, const nvmeadm_feature_t *);
static boolean_t do_get_feat_temp_thresh(const nvme_process_arg_t *,
    const nvme_feat_disc_t *, const nvmeadm_feature_t *);
static int do_get_features(const nvme_process_arg_t *);
static int do_format(const nvme_process_arg_t *);
static int do_secure_erase(const nvme_process_arg_t *);
static int do_attach_bd(const nvme_process_arg_t *);
static int do_detach_bd(const nvme_process_arg_t *);
static int do_firmware_load(const nvme_process_arg_t *);
static int do_firmware_commit(const nvme_process_arg_t *);
static int do_firmware_activate(const nvme_process_arg_t *);

static void optparse_list(nvme_process_arg_t *);
static void optparse_identify(nvme_process_arg_t *);
static void optparse_identify_ctrl(nvme_process_arg_t *);
static void optparse_identify_ns(nvme_process_arg_t *);
static void optparse_list_logs(nvme_process_arg_t *);
static void optparse_get_logpage(nvme_process_arg_t *);
static void optparse_print_logpage(nvme_process_arg_t *);
static void optparse_list_features(nvme_process_arg_t *);
static void optparse_secure_erase(nvme_process_arg_t *);

static void usage_list(const char *);
static void usage_identify(const char *);
static void usage_identify_ctrl(const char *);
static void usage_identify_ns(const char *);
static void usage_list_logs(const char *);
static void usage_get_logpage(const char *);
static void usage_print_logpage(const char *);
static void usage_list_features(const char *);
static void usage_get_features(const char *);
static void usage_format(const char *);
static void usage_secure_erase(const char *);
static void usage_attach_detach_bd(const char *);
static void usage_firmware_list(const char *);
static void usage_firmware_load(const char *);
static void usage_firmware_commit(const char *);
static void usage_firmware_activate(const char *);

int verbose;
int debug;

/*
 * nvmeadm Secure-erase specific options
 */
#define	NVMEADM_O_SE_CRYPTO	0x00000004

/*
 * nvmeadm identify specific options
 */
#define	NVMEADM_O_ID_NSID_LIST	0x00000008
#define	NVMEADM_O_ID_COMMON_NS	0x00000010
#define	NVMEADM_O_ID_CTRL_LIST	0x00000020
#define	NVMEADM_O_ID_DESC_LIST	0x00000040
#define	NVMEADM_O_ID_ALLOC_NS	0x00000080

static int exitcode;

/*
 * Nvmeadm subcommand definitons.
 *
 * When adding a new subcommand, please check that the commands still
 * line up in the usage() message, and adjust the format string in
 * usage() below if necessary.
 */
static const nvmeadm_cmd_t nvmeadm_cmds[] = {
	{
		"list",
		"list controllers and namespaces",
		"  -c\t\tlist only controllers\n"
		"  -L\t\tlist controller location\n"
		"  -p\t\tprint parsable output\n"
		"  -o field\tselect a field for parsable output\n",
		"  model\t\tthe controller's model name\n"
		"  serial\tthe controller's serial number\n"
		"  fwrev\t\tthe controller's current firmware revision\n"
		"  version\tthe controller's NVMe specification version\n"
		"  capacity\tthe controller or namespace's capacity in bytes\n"
		"  instance\tthe device driver instance (e.g. nvme3)\n"
		"  ctrlpath\tthe controller's /devices path\n"
		"  unallocated\tthe amount of unallocated NVM in bytes\n"
		"  size\t\tthe namespace's logical size in bytes\n"
		"  used\t\tthe namespace's bytes used\n"
		"  disk\t\tthe name of the namespace's disk device\n"
		"  namespace\tthe namespace's numerical value\n"
		"  ns-state\tthe namespace's current state\n"
		"  format\ta description of the namespace's format\n"
		"  fmtid\t\tthe numerical id of the namespace's format\n"
		"  fmtds\t\tthe data size of the namespace's format\n"
		"  fmtms\t\tthe metadata size of the namespace's format\n"
		"  location\tcontroller location (-L)\n"
		"  ctlap\t\tcontrolling attachment point (-L)\n",
		do_list, usage_list, optparse_list,
		NVMEADM_C_MULTI
	},
	{
		"identify",
		"identify controllers and/or namespaces",
		"  -C\t\tget Common Namespace Identification\n"
		"  -a\t\tget only allocated namespace information\n"
		"  -c\t\tget controller identifier list\n"
		"  -d\t\tget namespace identification descriptors list\n"
		"  -n\t\tget namespaces identifier list",
		NULL,
		do_identify, usage_identify, optparse_identify,
		NVMEADM_C_MULTI
	},
	{
		"identify-controller",
		"identify controllers",
		"  -C\t\tget Common Namespace Identification\n"
		"  -a\t\tget only allocated namespace information\n"
		"  -c\t\tget controller identifier list\n"
		"  -n\t\tget namespaces identifier list",
		NULL,
		do_identify_ctrl, usage_identify_ctrl, optparse_identify_ctrl,
		NVMEADM_C_MULTI
	},
	{
		"identify-namespace",
		"identify namespaces",
		"  -c\t\tget attached controller identifier list\n"
		"  -d\t\tget namespace identification descriptors list",
		NULL,
		do_identify_ns, usage_identify_ns, optparse_identify_ns,
		NVMEADM_C_MULTI
	},
	{
		"list-logpages",
		"list a device's supported log pages",
		"  -a\t\tprint all log pages, including unimplemented ones\n"
		"  -H\t\tomit column headers\n"
		"  -o field\tselect a field for parsable output\n"
		"  -p\t\tprint parsable output\n"
		"  -s scope\tprint logs that match the specified scopes "
		"(default is based on\n\t\tdevice)\n",
		"  device\tthe name of the controller or namespace\n"
		"  name\t\tthe name of the log page\n"
		"  desc\t\ta description of the loage page\n"
		"  scope\t\tthe valid device scopes for the log page\n"
		"  fields\tthe list of fields in the get log request that may "
		"be set or required\n\t\t(e.g. lsi, lsp, rae, etc.)\n"
		"  csi\t\tthe command set interface the log page belongs to\n"
		"  lid\t\tthe log page's numeric ID\n"
		"  impl\t\tindicates whether the device implements the log "
		"page\n"
		"  size\t\tthe size of the log page for fixed size logs\n"
		"  minsize\tthe minimum size required to determine the full "
		"log page size\n\t\tfor variable-length pages\n"
		"  sources\twhere information for this log page came from\n"
		"  kind\t\tindicates the kind of log page e.g. standard, "
		"vendor-specific,\n\t\tetc.",
		do_list_logs, usage_list_logs, optparse_list_logs,
		NVMEADM_C_MULTI
	},
	{
		.c_name = "get-logpage",
		.c_desc = "get a log page from controllers and/or namespaces",
		.c_flagdesc = "  -H\t\tomit column headers\n"
		    "  -o field\tselect a field for parsable output\n"
		    "  -O file\toutput log raw binary data to a file\n"
		    "  -p\t\tprint parsable output\n"
		    "  -x\t\tforce hex dump output\n",
		.c_fielddesc = "  short\t\tfield's short name\n"
		    "  desc\t\tdescription of the field\n"
		    "  value\t\tthe raw field value\n"
		    "  human\t\tthe human printable form of the value\n"
		    "  length\tthe length of the field in bytes\n"
		    "  bitlen\tthe number of additional bits long the field "
		    "is\n"
		    "  offset\tthe offset in bytes to the start of the field\n"
		    "  bitoff\tthe additional number of bits to the start of "
		    "the field\n",
		.c_func = do_get_logpage,
		.c_usage = usage_get_logpage,
		.c_optparse = optparse_get_logpage,
		.c_flags = NVMEADM_C_MULTI
	},
	{
		.c_name = "print-logpage",
		.c_desc = "print and filter a log page from disk",
		.c_flagdesc = "  -f file\tfile that contains log data\n"
		    "  -H\t\tomit column headers\n"
		    "  -o field\tselect a field for parsable output\n"
		    "  -p\t\tprint parsable output\n"
		    "  -x\t\tforce hex dump output\n",
		.c_fielddesc = "  short\t\tfield's short name\n"
		    "  desc\t\tdescription of the field\n"
		    "  value\t\tthe raw field value\n"
		    "  human\t\tthe human printable form of the value\n"
		    "  offset\tthe length of the field in bytes\n"
		    "  bitlen\tthe number of additional bits long the field "
		    "is\n"
		    "  offset\tthe offset in bytes to the start of the field\n"
		    "  bitoff\tthe additional number of bits to the start of "
		    "the field\n",
		.c_func = do_print_logpage,
		.c_usage = usage_print_logpage,
		.c_optparse = optparse_print_logpage,
		.c_flags = NVMEADM_C_NOCTRL
	},
	{
		"list-features",
		"list a device's supported features",
		"  -a\t\tprint all features, including unsupported\n"
		"  -H\t\tomit column headers\n"
		"  -o field\tselect a field for parsable output\n"
		"  -p\t\tprint parsable output",
		"  device\tthe name of the controller or namespace\n"
		"  short\t\tthe short name of the feature\n"
		"  spec\t\tthe longer feature description from the NVMe spec\n"
		"  fid\t\tthe numeric feature ID\n"
		"  scope\t\tthe valid device scopes for the feature\n"
		"  kind\t\tindicates the kind of feature e.g. standard, "
		"vendor-specific,\n\t\tetc.\n"
		"  csi\t\tindicates the features command set interface\n"
		"  flags\t\tindicates additional properties of the feature\n"
		"  get-in\tindicates the fields that are required to get the "
		"feature\n"
		"  set-in\tindicates the fields that are required to set the "
		"feature\n"
		"  get-out\tindicates the fields the feature outputs\n"
		"  set-out\tindicates the fields the feature outputs when "
		"setting the feature\n"
		"  datalen\tindicates the length of the feature's data "
		"payload\n"
		"  impl\t\tindicates whether the device implements the "
		"feature",
		do_list_features, usage_list_features, optparse_list_features,
		NVMEADM_C_MULTI
	},
	{
		"get-features",
		"get features from controllers and/or namespaces",
		NULL,
		NULL,
		do_get_features, usage_get_features, NULL,
		NVMEADM_C_MULTI
	},
	{
		"format",
		"format namespace(s) of a controller",
		NULL,
		NULL,
		do_format, usage_format, NULL,
		NVMEADM_C_EXCL
	},
	{
		"secure-erase",
		"secure erase namespace(s) of a controller",
		"  -c  Do a cryptographic erase.",
		NULL,
		do_secure_erase, usage_secure_erase, optparse_secure_erase,
		NVMEADM_C_EXCL
	},
	{
		"create-namespace",
		"create a new namespace",
		"  -b block-size\tNamespace format chosen to match the "
		"requested block-size\n"
		"  -c cap\tSpecifies the namespace capacity in bytes, defaults "
		"to the\n\t\tnamespace's size. When the size is greater than "
		"the\n\t\tcapacity, the namespace is thin provisioned.\n"
		"  -f flbas\tformatted LBA block size index\n"
		"  -n nmic\tmulti-path I/O and namespace sharing capabilities, "
		"valid values:\n"
		"\t\tnone\tno namespace sharing\n"
		"\t\tshared\tthe namespace may be attached by two or more "
		"controllers\n"
		"  -t csi\tspecifies the namespace's command set interface, "
		"defaults to\n\t\tnvm\n",
		NULL,
		do_create_ns, usage_create_ns, optparse_create_ns,
		NVMEADM_C_EXCL
	},
	{
		"delete-namespace",
		"delete a namespace",
		NULL, NULL,
		do_delete_ns, usage_delete_ns, NULL,
		NVMEADM_C_EXCL
	},
	{
		"attach-namespace",
		"attach a namespace to a controller",
		NULL, NULL,
		do_attach_ns, usage_attach_ns, NULL,
		NVMEADM_C_EXCL
	},
	{
		"detach-namespace",
		"detach a namespace from a controller",
		NULL, NULL,
		do_detach_ns, usage_detach_ns, NULL,
		NVMEADM_C_EXCL
	},

	{
		"detach",
		"detach blkdev(4D) from namespace(s) of a controller",
		NULL,
		NULL,
		do_detach_bd, usage_attach_detach_bd, NULL,
		NVMEADM_C_EXCL
	},
	{
		"attach",
		"attach blkdev(4D) to namespace(s) of a controller",
		NULL,
		NULL,
		do_attach_bd, usage_attach_detach_bd, NULL,
		NVMEADM_C_EXCL
	},
	{
		"list-firmware",
		"list firmware on a controller",
		NULL,
		NULL,
		do_get_logpage_fwslot, usage_firmware_list, NULL,
		0
	},
	{
		"load-firmware",
		"load firmware to a controller",
		NULL,
		NULL,
		do_firmware_load, usage_firmware_load, NULL,
		NVMEADM_C_EXCL
	},
	{
		"commit-firmware",
		"commit downloaded firmware to a slot of a controller",
		NULL,
		NULL,
		do_firmware_commit, usage_firmware_commit, NULL,
		NVMEADM_C_EXCL
	},
	{
		"activate-firmware",
		"activate a firmware slot of a controller",
		NULL,
		NULL,
		do_firmware_activate, usage_firmware_activate, NULL,
		NVMEADM_C_EXCL
	},
	{
		.c_name = "measure-phyeye",
		.c_desc = "measure the physical interface eye",
		.c_flagdesc = "  -o output\tthe data output file\n"
		    "  -Q quality\tthe quality of the eye measurement (good, "
		    "better, or best)\n",
		.c_func = do_measure_phyeye_cmd,
		.c_usage = usage_measure_phyeye_cmd,
		.c_optparse = optparse_measure_phyeye_cmd,
		.c_flags = NVMEADM_C_EXCL
	},
	{
		.c_name = "report-phyeye",
		.c_desc = "report information about the measured eye",
		.c_flagdesc = "  -e eye\toptional eye index to report\n"
		    "  -l lane\tprint information only about the specific "
		    "lane\n"
		    "  -m mode\tdata report mode, either 'print-eye' or "
		    "'eye-data'\n",
		.c_func = do_report_phyeye_cmd,
		.c_usage = usage_report_phyeye_cmd,
		.c_optparse = optparse_report_phyeye_cmd,
		.c_flags = NVMEADM_C_NOCTRL
	},
	{
		.c_name = "vendor-cmd",
		.c_desc = "run an arbitrary vendor-specific command",
		.c_flagdesc = "  -O opcode\tcommand opcode\n"
		    "  -n nsid\tcommand namespace identifier\n"
		    "  --cdw12 cdw12\tcdw12 32-bit value\n"
		    "  --cdw13 cdw13\tcdw13 32-bit value\n"
		    "  --cdw14 cdw14\tcdw14 32-bit value\n"
		    "  --cdw15 cdw15\tcdw15 32-bit value\n"
		    "  -l length\tthe command data size in bytes\n"
		    "  -i input\tthe command input data file\n"
		    "  -o output\tthe command output data file\n"
		    "  -L lock\trequest a controller or namespace lock\n"
		    "  -I impact\trequest impact on the system\n"
		    "  -t timeout\trequest timeout in seconds\n",
		.c_func = do_vendor_cmd,
		.c_usage = usage_vendor_cmd,
		.c_optparse = optparse_vendor_cmd
	},
	{
		.c_name = "sandisk/hwrev",
		.c_desc = "obtain device hardware revision",
		.c_func = do_sandisk_hwrev,
		.c_usage = usage_sandisk_hwrev,
		.c_flags = 0
	},
	{
		.c_name = "sandisk/pci-eye",
		.c_desc = "get PCIe receiver eye diagram",
		.c_flagdesc = "  -l lane\t\tspecify the PCIe lane (0-3)\n"
		    "  -o output\tspecify output file destination\n",
		.c_func = do_sandisk_pcieye,
		.c_usage = usage_sandisk_pcieye,
		.c_optparse = optparse_sandisk_pcieye
	},
	{
		"wdc/e6dump",
		"dump WDC e6 diagnostic log",
		"  -o output\tspecify output file destination\n",
		NULL,
		do_wdc_e6dump, usage_wdc_e6dump, optparse_wdc_e6dump,
		0
	},
	{
		"wdc/resize",
		"change a WDC device's capacity",
		"  -g\t\tquery the device's current resized capacity\n"
		"  -s size\tset the size of a device to the specified in gb",
		NULL,
		do_wdc_resize, usage_wdc_resize, optparse_wdc_resize,
		/*
		 * We do not set NVMEADM_C_EXCL here as that is handled by the
		 * vendor unique command logic and operates based on the
		 * information we get from vuc discovery.
		 */
		0
	},
	{
		"wdc/clear-assert",
		"clear internal device assertion",
		NULL,
		NULL,
		do_wdc_clear_assert, usage_wdc_clear_assert, NULL
	},
	{
		"wdc/inject-assert",
		"inject internal device assertion",
		NULL,
		NULL,
		do_wdc_inject_assert, usage_wdc_inject_assert, NULL
	},
	{
		NULL, NULL, NULL,
		NULL, NULL, NULL, 0
	}
};

static const nvmeadm_feature_t features[] = {
	{
		.f_feature = NVME_FEAT_ARBITRATION,
		.f_print = nvme_print_feat_arbitration
	}, {
		.f_feature = NVME_FEAT_POWER_MGMT,
		.f_print = nvme_print_feat_power_mgmt
	}, {
		.f_feature = NVME_FEAT_LBA_RANGE,
		.f_print = nvme_print_feat_lba_range
	}, {
		.f_feature = NVME_FEAT_TEMPERATURE,
		.f_get = do_get_feat_temp_thresh,
		.f_print = nvme_print_feat_temperature
	}, {
		.f_feature = NVME_FEAT_ERROR,
		.f_print = nvme_print_feat_error
	}, {
		.f_feature = NVME_FEAT_WRITE_CACHE,
		.f_print = nvme_print_feat_write_cache
	}, {
		.f_feature = NVME_FEAT_NQUEUES,
		.f_print = nvme_print_feat_nqueues
	}, {
		.f_feature = NVME_FEAT_INTR_COAL,
		.f_print = nvme_print_feat_intr_coal
	}, {
		.f_feature = NVME_FEAT_INTR_VECT,
		.f_get = do_get_feat_intr_vect,
		.f_print = nvme_print_feat_intr_vect
	}, {
		.f_feature = NVME_FEAT_WRITE_ATOM,
		.f_print = nvme_print_feat_write_atom
	}, {
		.f_feature = NVME_FEAT_ASYNC_EVENT,
		.f_print = nvme_print_feat_async_event
	}, {
		.f_feature = NVME_FEAT_AUTO_PST,
		.f_print = nvme_print_feat_auto_pst
	}, {
		.f_feature = NVME_FEAT_PROGRESS,
		.f_print = nvme_print_feat_progress
	}, {
		.f_feature = NVME_FEAT_HOST_BEHAVE,
		.f_print = nvme_print_feat_host_behavior
	}
};

static void
nvmeadm_ctrl_vwarn(const nvme_process_arg_t *npa, const char *fmt, va_list ap)
{
	nvme_ctrl_t *ctrl = npa->npa_ctrl;

	(void) fprintf(stderr, "nvmeadm: ");
	(void) vfprintf(stderr, fmt, ap);
	(void) fprintf(stderr, ": %s: %s (libnvme: 0x%x, sys: %d)\n",
	    nvme_ctrl_errmsg(ctrl), nvme_ctrl_errtostr(npa->npa_ctrl,
	    nvme_ctrl_err(ctrl)), nvme_ctrl_err(ctrl), nvme_ctrl_syserr(ctrl));
}

static void
nvmeadm_hdl_vwarn(const nvme_process_arg_t *npa, const char *fmt, va_list ap)
{
	nvme_t *nvme = npa->npa_nvme;

	(void) fprintf(stderr, "nvmeadm: ");
	(void) vfprintf(stderr, fmt, ap);
	(void) fprintf(stderr, ": %s: %s (libnvme: 0x%x, sys: %d)\n",
	    nvme_errmsg(nvme), nvme_errtostr(nvme, nvme_err(nvme)),
	    nvme_err(nvme), nvme_syserr(nvme));
}

static void
nvmeadm_ctrl_info_vwarn(const nvme_process_arg_t *npa, const char *fmt,
    va_list ap)
{
	nvme_ctrl_info_t *info = npa->npa_ctrl_info;

	(void) fprintf(stderr, "nvmeadm: ");
	(void) vfprintf(stderr, fmt, ap);
	(void) fprintf(stderr, ": %s: %s (libnvme info: 0x%x, sys: %d)\n",
	    nvme_ctrl_info_errmsg(info), nvme_ctrl_info_errtostr(info,
	    nvme_ctrl_info_err(info)), nvme_ctrl_info_err(info),
	    nvme_ctrl_info_syserr(info));
}

void
nvmeadm_warn(const nvme_process_arg_t *npa, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	nvmeadm_ctrl_vwarn(npa, fmt, ap);
	va_end(ap);
}

void __NORETURN
nvmeadm_fatal(const nvme_process_arg_t *npa, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	nvmeadm_ctrl_vwarn(npa, fmt, ap);
	va_end(ap);

	exit(-1);
}

void
nvmeadm_hdl_warn(const nvme_process_arg_t *npa, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	nvmeadm_hdl_vwarn(npa, fmt, ap);
	va_end(ap);
}

void __NORETURN
nvmeadm_hdl_fatal(const nvme_process_arg_t *npa, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	nvmeadm_hdl_vwarn(npa, fmt, ap);
	va_end(ap);

	exit(-1);
}

static void
nvmeadm_ctrl_info_warn(const nvme_process_arg_t *npa, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	nvmeadm_ctrl_info_vwarn(npa, fmt, ap);
	va_end(ap);
}

static void
nvmeadm_ctrl_info_fatal(const nvme_process_arg_t *npa, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	nvmeadm_ctrl_info_vwarn(npa, fmt, ap);
	va_end(ap);

	exit(-1);
}

boolean_t
nvme_version_check(const nvme_process_arg_t *npa, const nvme_version_t *vers)
{
	return (nvme_vers_atleast(npa->npa_version, vers) ? B_TRUE : B_FALSE);
}

/*
 * Because nvmeadm operates on a series of NVMe devices for several commands,
 * here we need to clean up everything that we allocated for this device so we
 * can prepare for the next.
 */
static void
nvmeadm_cleanup_npa(nvme_process_arg_t *npa)
{
	npa->npa_idctl = NULL;
	npa->npa_version = NULL;

	if (npa->npa_excl) {
		if (npa->npa_ns != NULL) {
			nvme_ns_unlock(npa->npa_ns);
		} else if (npa->npa_ctrl != NULL) {
			nvme_ctrl_unlock(npa->npa_ctrl);
		}
	}

	if (npa->npa_ns_info != NULL) {
		nvme_ns_info_free(npa->npa_ns_info);
		npa->npa_ns_info = NULL;
	}

	if (npa->npa_ctrl_info != NULL) {
		nvme_ctrl_info_free(npa->npa_ctrl_info);
		npa->npa_ctrl_info = NULL;
	}

	if (npa->npa_ns != NULL) {
		nvme_ns_fini(npa->npa_ns);
		npa->npa_ns = NULL;
	}

	if (npa->npa_ctrl != NULL) {
		nvme_ctrl_fini(npa->npa_ctrl);
		npa->npa_ctrl = NULL;
	}
}

/*
 * Determine if a command requires a controller or namespace write lock. If so
 * we first attempt to grab it non-blocking and then if that fails, we'll warn
 * that we may be blocking for the lock so that way the user has a chance to do
 * something and can cancel it.
 */
void
nvmeadm_excl(const nvme_process_arg_t *npa, nvme_lock_level_t level)
{
	bool ret;
	nvme_lock_flags_t flags = NVME_LOCK_F_DONT_BLOCK;

	if (npa->npa_ns != NULL) {
		ret = nvme_ns_lock(npa->npa_ns, level, flags);
	} else {
		ret = nvme_ctrl_lock(npa->npa_ctrl, level, flags);
	}

	if (ret) {
		return;
	}

	if (nvme_ctrl_err(npa->npa_ctrl) != NVME_ERR_LOCK_WOULD_BLOCK) {
		nvmeadm_fatal(npa, "failed to acquire lock on %s",
		    npa->npa_name);
	}

	(void) fprintf(stderr, "Waiting on contended %s lock on %s...",
	    npa->npa_ns != NULL ? "namespace": "controller", npa->npa_name);
	(void) fflush(stderr);

	flags &= ~NVME_LOCK_F_DONT_BLOCK;
	if (npa->npa_ns != NULL) {
		ret = nvme_ns_lock(npa->npa_ns, level, flags);
	} else {
		ret = nvme_ctrl_lock(npa->npa_ctrl, level, flags);
	}

	if (!ret) {
		nvmeadm_fatal(npa, "failed to acquire lock on %s",
		    npa->npa_name);
	}

	(void) fprintf(stderr, " acquired\n");
}

/*
 * Most of nvmeadm was written before the existence of libnvme and always had
 * things like the identify controller or namespace information sitting around.
 * As such we try to grab all this in one place for it. Note, regardless if this
 * succeeds or fails, our callers will still call nvmeadm_cleanup_npa() so we
 * don't need to clean up the various libnvme objects.
 */
static boolean_t
nvmeadm_open_dev(nvme_process_arg_t *npa)
{
	if (!nvme_ctrl_ns_init(npa->npa_nvme, npa->npa_name, &npa->npa_ctrl,
	    &npa->npa_ns)) {
		nvmeadm_hdl_warn(npa, "failed to open '%s'", npa->npa_name);
		exitcode = -1;
		return (B_FALSE);
	}

	/*
	 * Several commands expect to be able to access the controller's
	 * information snapshot. Grab that now for it and the namespace if it
	 * exists.
	 */
	if (!nvme_ctrl_info_snap(npa->npa_ctrl, &npa->npa_ctrl_info)) {
		nvmeadm_warn(npa, "failed to get controller info for %s",
		    npa->npa_ctrl_name);
		exitcode = -1;
		return (B_FALSE);
	}

	if (npa->npa_ns != NULL && !nvme_ns_info_snap(npa->npa_ns,
	    &npa->npa_ns_info)) {
		nvmeadm_warn(npa, "failed to get namespace info for %s",
		    npa->npa_name);
		exitcode = -1;
		return (B_FALSE);
	}

	/*
	 * Snapshot data the rest of the command has fairly ingrained.
	 */
	npa->npa_version = nvme_ctrl_info_version(npa->npa_ctrl_info);
	npa->npa_idctl = nvme_ctrl_info_identify(npa->npa_ctrl_info);

	/*
	 * If this command has requested exclusive access, proceed to grab that
	 * before we continue.
	 */
	if (npa->npa_excl) {
		nvmeadm_excl(npa, NVME_LOCK_L_WRITE);
	}

	return (B_TRUE);
}

static bool
nvmeadm_ctrl_disc_cb(nvme_t *nvme, const nvme_ctrl_disc_t *disc, void *arg)
{
	nvme_process_arg_t *npa = arg;
	di_node_t di = nvme_ctrl_disc_devi(disc);
	char name[128];

	(void) snprintf(name, sizeof (name), "%s%d", di_driver_name(di),
	    di_instance(di));
	npa->npa_name = name;
	npa->npa_ctrl_name = name;

	if (nvmeadm_open_dev(npa)) {
		if (npa->npa_cmd->c_func(npa) != 0) {
			exitcode = -1;
		}
	}

	nvmeadm_cleanup_npa(npa);
	return (true);
}

int
main(int argc, char **argv)
{
	int c;
	const nvmeadm_cmd_t *cmd;
	nvme_process_arg_t npa = { 0 };
	int help = 0;
	char *ctrl = NULL;

	while ((c = getopt(argc, argv, "dhv")) != -1) {
		switch (c) {
		case 'd':
			debug++;
			break;

		case 'v':
			verbose++;
			break;

		case 'h':
			help++;
			break;

		case '?':
			usage(NULL);
			exit(-1);
		}
	}

	if (optind == argc) {
		usage(NULL);
		if (help)
			exit(0);
		else
			exit(-1);
	}

	/* Look up the specified command in the command table. */
	for (cmd = &nvmeadm_cmds[0]; cmd->c_name != NULL; cmd++)
		if (strcmp(cmd->c_name, argv[optind]) == 0)
			break;

	if (cmd->c_name == NULL) {
		usage(NULL);
		exit(-1);
	}

	if (help) {
		usage(cmd);
		exit(0);
	}

	npa.npa_nvme = nvme_init();
	if (npa.npa_nvme == NULL) {
		err(-1, "failed to initialize libnvme");
	}
	npa.npa_cmd = cmd;
	npa.npa_excl = ((npa.npa_cmd->c_flags & NVMEADM_C_EXCL) != 0);

	optind++;

	/*
	 * Store the remaining arguments for use by the command. Give the
	 * command a chance to process the options across the board before going
	 * into each controller.
	 */
	npa.npa_argc = argc - optind;
	npa.npa_argv = &argv[optind];

	if (npa.npa_cmd->c_optparse != NULL) {
		optind = 0;
		npa.npa_cmd->c_optparse(&npa);
		npa.npa_argc -= optind;
		npa.npa_argv += optind;
	}

	/*
	 * If this command indicates that it does not require a controller, then
	 * don't bother with the rest and just call it immediately.
	 */
	if ((npa.npa_cmd->c_flags & NVMEADM_C_NOCTRL) != 0) {
		if (npa.npa_cmd->c_func(&npa) != 0) {
			exitcode = -1;
		}
		exit(exitcode);
	}

	/*
	 * All commands but "list" require a ctl/ns argument. However, this
	 * should not be passed through to the command in its subsequent
	 * arguments.
	 */
	if (npa.npa_argc == 0 && cmd->c_func != do_list) {
		warnx("missing controller/namespace name");
		usage(cmd);
		exit(-1);
	}

	if (npa.npa_argc > 0) {
		ctrl = npa.npa_argv[0];
		npa.npa_argv++;
		npa.npa_argc--;
	} else {
		if (!nvme_ctrl_discover(npa.npa_nvme, nvmeadm_ctrl_disc_cb,
		    &npa)) {
			nvmeadm_hdl_fatal(&npa, "failed to walk controllers");
		}
		exit(exitcode);
	}

	/*
	 * Make sure we're not running commands on multiple controllers that
	 * aren't allowed to do that.
	 */
	if (ctrl != NULL && strchr(ctrl, ',') != NULL &&
	    (cmd->c_flags & NVMEADM_C_MULTI) == 0) {
		warnx("%s not allowed on multiple controllers",
		    cmd->c_name);
		usage(cmd);
		exit(-1);
	}

	/*
	 * Get controller/namespace arguments and run command.
	 */
	while ((npa.npa_name = strsep(&ctrl, ",")) != NULL) {
		char *ctrl_name, *slash;

		/*
		 * We may be given just a controller as an argument or a
		 * controller and a namespace as an argument. Parts of the
		 * commands want to know what controller they're referring to
		 * even if the overall argument was for a namespace. So we
		 * always dup the argument and try to make the controller out of
		 * it.
		 */
		ctrl_name = strdup(npa.npa_name);
		if (ctrl_name == NULL) {
			err(-1, "failed to duplicate NVMe controller/namespace "
			    "name");
		}
		if ((slash = strchr(ctrl_name, '/')) != NULL)
			*slash = '\0';
		npa.npa_ctrl_name = ctrl_name;

		if (nvmeadm_open_dev(&npa)) {
			if (npa.npa_cmd->c_func(&npa) != 0) {
				exitcode = -1;
			}
		}

		nvmeadm_cleanup_npa(&npa);
		free(ctrl_name);
	}

	exit(exitcode);
}

static void
nvme_oferr(const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	verrx(-1, fmt, ap);
}

static void
usage(const nvmeadm_cmd_t *cmd)
{
	const char *progname = getprogname();

	(void) fprintf(stderr, "usage:\n");
	(void) fprintf(stderr, "  %s -h %s\n", progname,
	    cmd != NULL ? cmd->c_name : "[<command>]");
	(void) fprintf(stderr, "  %s [-dv] ", progname);

	if (cmd != NULL) {
		cmd->c_usage(cmd->c_name);
	} else {
		(void) fprintf(stderr,
		    "<command> <ctl>[/<ns>][,...] [<args>]\n");
		(void) fprintf(stderr,
		    "\n  Manage NVMe controllers and namespaces.\n");
		(void) fprintf(stderr, "\ncommands:\n");

		for (cmd = &nvmeadm_cmds[0]; cmd->c_name != NULL; cmd++) {
			/*
			 * The longest nvmeadm subcommand is 19 characters long.
			 * The format string needs to be updated every time a
			 * longer subcommand is added.
			 */
			(void) fprintf(stderr, "  %-19s - %s\n",
			    cmd->c_name, cmd->c_desc);
		}
	}
	(void) fprintf(stderr, "\n%s flags:\n"
	    "  -h\t\tprint usage information\n"
	    "  -d\t\tprint information useful for debugging %s\n"
	    "  -v\t\tprint verbose information\n",
	    progname, progname);

	if (cmd != NULL && cmd->c_flagdesc != NULL) {
		(void) fprintf(stderr, "\n%s %s flags:\n",
		    progname, cmd->c_name);
		(void) fprintf(stderr, "%s\n", cmd->c_flagdesc);
	}

	if (cmd != NULL && cmd->c_fielddesc != NULL) {
		(void) fprintf(stderr, "\n%s %s valid fields:\n",
		    progname, cmd->c_name);
		(void) fprintf(stderr, "%s\n", cmd->c_fielddesc);
	}
}

char *
nvme_dskname(di_node_t ctrl, const char *bd_addr)
{
	di_dim_t dim;
	char *diskname = NULL;

	dim = di_dim_init();
	if (dim == NULL) {
		err(-1, "failed to initialize devinfo minor translation");
	}

	for (di_node_t child = di_child_node(ctrl); child != DI_NODE_NIL;
	    child = di_sibling_node(child)) {
		char *disk_ctd, *path = NULL;
		const char *addr = di_bus_addr(child);
		if (addr == NULL)
			continue;

		if (strcmp(addr, bd_addr) != 0)
			continue;

		path = di_dim_path_dev(dim, di_driver_name(child),
		    di_instance(child), "c");

		/*
		 * Error out if we didn't get a path, or if it's too short for
		 * the following operations to be safe.
		 */
		if (path == NULL || strlen(path) < 2) {
			errx(-1, "failed to get a valid minor path");
		}

		/* Chop off 's0' and get everything past the last '/' */
		path[strlen(path) - 2] = '\0';
		disk_ctd = strrchr(path, '/');
		if (disk_ctd == NULL) {
			errx(-1, "encountered malformed minor path: %s", path);
		}

		diskname = strdup(++disk_ctd);
		if (diskname == NULL) {
			err(-1, "failed to duplicate disk path");
		}

		free(path);
		break;
	}

	di_dim_fini(dim);
	return (diskname);
}

static void
usage_list(const char *c_name)
{
	(void) fprintf(stderr, "%s "
	    "[-c | -L] [-p -o field[,...]] [<ctl>[/<ns>][,...]\n\n"
	    "  List NVMe controllers and their namespaces. If no "
	    "controllers and/or name-\n  spaces are specified, all "
	    "controllers and namespaces in the system will be\n  "
	    "listed.\n", c_name);
}

static void
optparse_list(nvme_process_arg_t *npa)
{
	int c;
	uint_t oflags = 0;
	boolean_t parse = B_FALSE, cflag = B_FALSE, Lflag = B_FALSE;
	const char *fields = NULL;
	const ofmt_field_t *ofmt = nvmeadm_list_nsid_ofmt;
	nvmeadm_list_t *list = NULL;

	if ((list = calloc(1, sizeof (nvmeadm_list_t))) == NULL) {
		err(-1, "failed to allocate memory for argument tracking");
	}
	list->list_mode = NVMEADM_LIST_DEFAULT;

	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":cLo:p")) != -1) {
		switch (c) {
		case 'c':
			cflag = B_TRUE;
			list->list_mode = NVMEADM_LIST_CTRL;
			ofmt = nvmeadm_list_ctrl_ofmt;
			break;
		case 'L':
			Lflag = B_TRUE;
			list->list_mode = NVMEADM_LIST_LOC;
			ofmt = nvmeadm_list_loc_ofmt;
			break;
		case 'o':
			fields = optarg;
			break;

		case 'p':
			parse = B_TRUE;
			oflags |= OFMT_PARSABLE;
			break;

		case '?':
			errx(-1, "unknown option: -%c", optopt);

		case ':':
			errx(-1, "option -%c requires an argument", optopt);
		}
	}

	if (cflag && Lflag) {
		errx(-1, "only one of -c and -L may be selected");
	}

	if (fields != NULL && !parse) {
		errx(-1, "-o can only be used when in parsable mode (-p)");
	}

	/*
	 * We use ofmt by default with -L.
	 */
	if (fields == NULL && Lflag) {
		fields = "instance,model,location,ctlap";
	}

	if (parse && fields == NULL) {
		errx(-1, "parsable mode (-p) requires one to specify output "
		    "fields with -o");
	}

	if (parse || Lflag) {
		ofmt_status_t oferr;

		oferr = ofmt_open(fields, ofmt, oflags, 0,
		    &npa->npa_ofmt);
		ofmt_check(oferr, B_TRUE, npa->npa_ofmt, nvme_oferr, warnx);
	}

	if (Lflag) {
		int e;

		list->list_topo = topo_open(TOPO_VERSION, NULL, &e);
		if (list->list_topo == NULL) {
			errx(-1, "location information unavailable: unable to "
			    "obtain topo handle: %s", topo_strerror(e));
		}

		(void) topo_snap_hold(list->list_topo, NULL, &e);
		if (e != 0) {
			errx(-1, "location information unavailable: unable to "
			    "obtain topo snapshot: %s", topo_strerror(e));
		}
	}

	npa->npa_cmd_arg = list;
}

static void
do_list_nsid(const nvme_process_arg_t *npa, nvme_ctrl_info_t *ctrl,
    nvme_ns_info_t *ns)
{
	const char *bd_addr, *disk = NULL, *state = NULL;
	char *disk_path = NULL;
	di_node_t ctrl_devi;

	switch (nvme_ns_info_level(ns)) {
	case NVME_NS_DISC_F_ALL:
		disk = "unallocated";
		state = "unallocated";
		break;
	case NVME_NS_DISC_F_ALLOCATED:
		disk = "inactive";
		state = "allocated";
		break;
	case NVME_NS_DISC_F_ACTIVE:
		disk = "ignored";
		state = "active";
		break;
	case NVME_NS_DISC_F_NOT_IGNORED:
		disk = "unattached";
		state = "active-usable";
		break;
	case NVME_NS_DISC_F_BLKDEV:
		disk = "unknown";
		state = "blkdev";
		if (nvme_ns_info_bd_addr(ns, &bd_addr) &&
		    nvme_ctrl_devi(npa->npa_ctrl, &ctrl_devi)) {
			disk_path = nvme_dskname(ctrl_devi, bd_addr);
			disk = disk_path;
		}
		break;
	}

	if (npa->npa_ofmt != NULL) {
		nvmeadm_list_ofmt_arg_t oarg = { 0 };

		oarg.nloa_name = npa->npa_ctrl_name;
		oarg.nloa_ctrl = ctrl;
		oarg.nloa_ns = ns;
		oarg.nloa_disk = disk_path;
		oarg.nloa_state = state;
		if (!nvme_ctrl_devi(npa->npa_ctrl, &oarg.nloa_dip))
			oarg.nloa_dip = DI_NODE_NIL;

		ofmt_print(npa->npa_ofmt, &oarg);
	} else {
		(void) printf("  %s/%u (%s)", npa->npa_ctrl_name,
		    nvme_ns_info_nsid(ns), disk);
		if (nvme_ns_info_level(ns) >= NVME_NS_DISC_F_ACTIVE) {
			(void) printf(": ");
			nvme_print_nsid_summary(ns);
		} else {
			(void) printf("\n");
		}
	}

	free(disk_path);
}

/*
 * Determine a device's controlling AP. As most NVMe devices are PCIe devices
 * (except when virtualized) we look at the parent pcieb instance and look for
 * its ap-names and slot-names properties.
 */
static void
do_list_phys_map_ctlap(nvmeadm_list_ofmt_arg_t *oarg)
{
	di_node_t pdip;
	int *ap_names, *slot_names;

	pdip = di_parent_node(oarg->nloa_dip);
	if (pdip == DI_NODE_NIL) {
		return;
	}

	/*
	 * This probably will need to change a bit when we understand the exact
	 * shape AP's take when ACPI-based PCI hotplug is supported for virtual
	 * machines. In that world, it's not clear where the AP will live.
	 */
	const char *drv = di_driver_name(pdip);
	if (drv == NULL || strcmp(drv, "pcieb") != 0) {
		return;
	}

	/*
	 * The AP names property is a bitfield that indicates which entry in the
	 * slot-names. A given device can in theory support multiple slots, but
	 * PCIe is always going to be a value of 1.
	 */
	if (di_prop_lookup_ints(DDI_DEV_T_ANY, pdip, "ap-names", &ap_names) !=
	    1 || *ap_names != 1) {
		return;
	}

	/*
	 * The slot-names property is structured with the first word as a
	 * bitfield mask and then a series of names which are guaranteed to be
	 * NULL terminated.
	 */
	int nu32 = di_prop_lookup_ints(DDI_DEV_T_ANY, pdip, "slot-names",
	    &slot_names);
	if (nu32 <= 1 || slot_names[0] != 1) {
		return;
	}

	oarg->nloa_ap = calloc(nu32 - 1, sizeof (uint32_t));
	if (oarg->nloa_ap == NULL) {
		err(-1, "failed to allocate memory for attachment point name");
	}

	(void) memcpy(oarg->nloa_ap, &slot_names[1], sizeof (uint32_t) *
	    (nu32 - 1));
}

/*
 * NVMe devices are always enumerated with an 'nvme' node. See if we can find
 * that.
 */
static int
do_list_phys_map_loc_cb(topo_hdl_t *hp, tnode_t *tn, void *arg)
{
	nvmeadm_list_ofmt_arg_t *oarg = arg;
	char *label = NULL;
	uint32_t tinst;
	int err;

	if (strcmp(topo_node_name(tn), NVME) != 0) {
		return (TOPO_WALK_NEXT);
	}

	if (topo_prop_get_uint32(tn, TOPO_PGROUP_IO, TOPO_IO_INSTANCE, &tinst,
	    &err) != 0) {
		return (TOPO_WALK_NEXT);
	}

	if (tinst != (uint32_t)di_instance(oarg->nloa_dip)) {
		return (TOPO_WALK_NEXT);
	}

	/*
	 * Walk the node hierarchy looking for something that has a label. We
	 * basically only consider ourselves and our immediate parent if that
	 * parent is a bay or slot. If we go too far then we may end up finding
	 * a label for a device that has no relationship to us.
	 */
	if (topo_prop_get_string(tn, TOPO_PGROUP_PROTOCOL, TOPO_PROP_LABEL,
	    &label, &err) != 0) {
		tnode_t *pn = topo_node_parent(tn);
		const char *pname = topo_node_name(pn);
		if (strcmp(pname, BAY) == 0 || strcmp(pname, SLOT) == 0) {
			if (topo_prop_get_string(pn, TOPO_PGROUP_PROTOCOL,
			    TOPO_PROP_LABEL, &label, &err) != 0) {
				label = NULL;
			}
		}
	}

	oarg->nloa_loc = label;
	return (TOPO_WALK_TERMINATE);
}

static void
do_list_phys_map_loc(nvmeadm_list_t *list, nvmeadm_list_ofmt_arg_t *oarg)
{
	topo_walk_t *wp;
	int err;

	wp = topo_walk_init(list->list_topo, FM_FMRI_SCHEME_HC,
	    do_list_phys_map_loc_cb, oarg, &err);
	if (wp == NULL) {
		warnx("location information not available: failed to "
		    "initialize topo walk: %s", topo_strerror(err));
		exitcode = -1;
		return;
	}

	err = topo_walk_step(wp, TOPO_WALK_SIBLING);
	if (err == TOPO_WALK_ERR) {
		warnx("location information not available: topo walk failed");
		exitcode = -1;
	}

	topo_walk_fini(wp);
}

static int
do_list(const nvme_process_arg_t *npa)
{
	nvme_ctrl_info_t *info = NULL;
	nvme_ns_iter_t *iter = NULL;
	nvme_iter_t ret;
	const nvme_ns_disc_t *disc;
	nvme_ns_disc_level_t level;
	int rv = -1;
	nvmeadm_list_t *list = npa->npa_cmd_arg;

	if (npa->npa_argc > 0) {
		errx(-1, "%s passed extraneous arguments starting with %s",
		    npa->npa_cmd->c_name, npa->npa_argv[0]);
	}

	if (!nvme_ctrl_info_snap(npa->npa_ctrl, &info)) {
		nvmeadm_warn(npa, "failed to get controller information for %s",
		    npa->npa_ctrl_name);
		return (-1);
	}

	if (npa->npa_ofmt == NULL) {
		(void) printf("%s: ", npa->npa_ctrl_name);
		nvme_print_ctrl_summary(info);
	} else if (list->list_mode != NVMEADM_LIST_DEFAULT) {
		nvmeadm_list_ofmt_arg_t oarg = { 0 };
		oarg.nloa_name = npa->npa_ctrl_name;
		oarg.nloa_ctrl = info;
		if (!nvme_ctrl_devi(npa->npa_ctrl, &oarg.nloa_dip))
			oarg.nloa_dip = DI_NODE_NIL;

		if (list->list_mode == NVMEADM_LIST_LOC) {
			do_list_phys_map_ctlap(&oarg);
			do_list_phys_map_loc(list, &oarg);
		}

		ofmt_print(npa->npa_ofmt, &oarg);
		free(oarg.nloa_ap);
		if (list->list_topo != NULL) {
			topo_hdl_strfree(list->list_topo, oarg.nloa_loc);
		}
	}

	if (list->list_mode != NVMEADM_LIST_DEFAULT) {
		rv = 0;
		goto out;
	}

	/*
	 * Check if we were given an explicit namespace as an argument. If so,
	 * we always list it and don't need to do discovery.
	 */
	if (npa->npa_ns != NULL) {
		nvme_ns_info_t *ns_info;

		if (!nvme_ns_info_snap(npa->npa_ns, &ns_info)) {
			nvmeadm_warn(npa, "failed to get namespace "
			    "information for %s", npa->npa_name);
			goto out;
		}

		do_list_nsid(npa, info, ns_info);
		nvme_ns_info_free(ns_info);
		rv = 0;
		goto out;
	}

	if (verbose) {
		level = NVME_NS_DISC_F_ALL;
	} else {
		level = NVME_NS_DISC_F_NOT_IGNORED;
	}

	if (!nvme_ns_discover_init(npa->npa_ctrl, level, &iter)) {
		nvmeadm_warn(npa, "failed to iterate namespaces on %s",
		    npa->npa_ctrl_name);
		goto out;
	}

	while ((ret = nvme_ns_discover_step(iter, &disc)) == NVME_ITER_VALID) {
		nvme_ns_info_t *ns_info;
		uint32_t nsid = nvme_ns_disc_nsid(disc);

		if (!nvme_ctrl_ns_info_snap(npa->npa_ctrl, nsid, &ns_info)) {
			nvmeadm_warn(npa, "failed to get namespace "
			    "information for %s/%u", npa->npa_ctrl_name, nsid);
			exitcode = -1;
			continue;
		}

		do_list_nsid(npa, info, ns_info);
		nvme_ns_info_free(ns_info);
	}

	nvme_ns_discover_fini(iter);
	if (ret == NVME_ITER_ERROR) {
		nvmeadm_warn(npa, "failed to iterate all namespaces on %s",
		    npa->npa_ctrl_name);
	} else {
		rv = 0;
	}

out:
	nvme_ctrl_info_free(info);
	return (rv);
}

static void
optparse_identify_ctrl(nvme_process_arg_t *npa)
{
	int c;

	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":Cacn")) != -1) {
		switch (c) {
		case 'C':
			npa->npa_cmdflags |= NVMEADM_O_ID_COMMON_NS;
			break;

		case 'a':
			npa->npa_cmdflags |= NVMEADM_O_ID_ALLOC_NS;
			break;

		case 'c':
			npa->npa_cmdflags |= NVMEADM_O_ID_CTRL_LIST;
			break;

		case 'n':
			npa->npa_cmdflags |= NVMEADM_O_ID_NSID_LIST;
			break;

		case '?':
			errx(-1, "unknown option: -%c", optopt);

		case ':':
			errx(-1, "option -%c requires an argument", optopt);
		}
	}
}

static void
usage_identify_ctrl(const char *c_name)
{
	(void) fprintf(stderr, "%s [-C | -c | [-a] -n] <ctl>[,...]\n\n"
	    "  Print detailed information about the specified NVMe "
	    "controllers.\n", c_name);
}

static int
do_identify_ctrl(const nvme_process_arg_t *npa)
{
	boolean_t alloc = B_FALSE;

	if (npa->npa_ns != NULL)
		errx(-1, "identify-controller cannot be used on namespaces");

	if (npa->npa_argc > 0) {
		errx(-1, "%s passed extraneous arguments starting with %s",
		    npa->npa_cmd->c_name, npa->npa_argv[0]);
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_COMMON_NS) != 0 &&
	    npa->npa_cmdflags != NVMEADM_O_ID_COMMON_NS) {
		errx(-1, "-C cannot be combined with other flags");
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_CTRL_LIST) != 0 &&
	    npa->npa_cmdflags != NVMEADM_O_ID_CTRL_LIST) {
		errx(-1, "-c cannot be combined with other flags");
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_ALLOC_NS) != 0 &&
	    npa->npa_cmdflags !=
	    (NVMEADM_O_ID_ALLOC_NS | NVMEADM_O_ID_NSID_LIST)) {
		errx(-1, "-a can only be used together with -n");
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_ALLOC_NS) != 0) {
		alloc = B_TRUE;
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_COMMON_NS) != 0) {
		const nvme_identify_nsid_t *idns;

		if (!nvme_ctrl_info_common_ns(npa->npa_ctrl_info, &idns)) {
			nvmeadm_ctrl_info_warn(npa, "failed to get common "
			    "namespace information for %s", npa->npa_name);
			return (-1);
		}

		(void) printf("%s: ", npa->npa_name);
		nvme_print_identify_nsid(idns, npa->npa_version);
	} else if ((npa->npa_cmdflags & NVMEADM_O_ID_NSID_LIST) != 0) {
		const char *caption;
		uint32_t cns;
		nvme_identify_nsid_list_t *idnslist;
		nvme_id_req_t *req;

		if (alloc) {
			caption = "Identify Allocated Namespace List";
			cns = NVME_IDENTIFY_NSID_ALLOC_LIST;
		} else {
			caption = "Identify Active Namespace List";
			cns = NVME_IDENTIFY_NSID_LIST;
		}

		if ((idnslist = malloc(NVME_IDENTIFY_BUFSIZE)) == NULL) {
			err(-1, "failed to allocate identify buffer size");
		}

		if (!nvme_id_req_init_by_cns(npa->npa_ctrl, NVME_CSI_NVM, cns,
		    &req)) {
			nvmeadm_fatal(npa, "failed to initialize %s request",
			    caption);
		}

		/*
		 * Always set the NSID for these requests to NSID 0 so that way
		 * we can start the list at the beginning. When we encounter
		 * devices with more than 1024 NSIDs then we'll need to issue
		 * additional requests.
		 */
		if (!nvme_id_req_set_nsid(req, 0) ||
		    !nvme_id_req_set_output(req, idnslist,
		    NVME_IDENTIFY_BUFSIZE)) {
			nvmeadm_fatal(npa, "failed to set required fields for "
			    "identify request");
		}

		if (!nvme_id_req_exec(req)) {
			nvmeadm_fatal(npa, "failed to execute identify "
			    "request");
		}
		nvme_id_req_fini(req);

		(void) printf("%s: ", npa->npa_name);

		nvme_print_identify_nsid_list(caption, idnslist);
		free(idnslist);
	} else if ((npa->npa_cmdflags & NVMEADM_O_ID_CTRL_LIST) != 0) {
		nvme_identify_ctrl_list_t *ctlist;
		nvme_id_req_t *req;

		if ((ctlist = malloc(NVME_IDENTIFY_BUFSIZE)) == NULL) {
			err(-1, "failed to allocate identify buffer size");
		}

		if (!nvme_id_req_init_by_cns(npa->npa_ctrl, NVME_CSI_NVM,
		    NVME_IDENTIFY_CTRL_LIST, &req)) {
			nvmeadm_fatal(npa, "failed to initialize identify "
			    "request");
		}

		if (!nvme_id_req_set_ctrlid(req, 0) ||
		    !nvme_id_req_set_output(req, ctlist,
		    NVME_IDENTIFY_BUFSIZE)) {
			nvmeadm_fatal(npa, "failed to set required fields for "
			    "identify request");
		}
		if (!nvme_id_req_exec(req)) {
			nvmeadm_fatal(npa, "failed to execute identify "
			    "request");
		}
		nvme_id_req_fini(req);

		(void) printf("%s: ", npa->npa_name);
		nvme_print_identify_ctrl_list("Identify Controller List",
		    ctlist);
		free(ctlist);
	} else {
		uint32_t mpsmin;

		if (!nvme_ctrl_info_pci_mps_min(npa->npa_ctrl_info,
		    &mpsmin)) {
			nvmeadm_ctrl_info_fatal(npa, "failed to get minimum "
			    "memory page size");
		}

		(void) printf("%s: ", npa->npa_name);
		nvme_print_identify_ctrl(npa->npa_idctl, mpsmin,
		    npa->npa_version);
	}

	return (0);
}

static void
optparse_identify_ns(nvme_process_arg_t *npa)
{
	int c;

	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":cd")) != -1) {
		switch (c) {
		case 'c':
			npa->npa_cmdflags |= NVMEADM_O_ID_CTRL_LIST;
			break;

		case 'd':
			npa->npa_cmdflags |= NVMEADM_O_ID_DESC_LIST;
			break;

		case '?':
			errx(-1, "unknown option: -%c", optopt);

		case ':':
			errx(-1, "option -%c requires an argument", optopt);
		}
	}
}

static void
usage_identify_ns(const char *c_name)
{
	(void) fprintf(stderr, "%s [-c | -d ] <ctl>/<ns>[,...]\n\n"
	    "  Print detailed information about the specified NVMe "
	    "namespaces.\n", c_name);
}

static int
do_identify_ns(const nvme_process_arg_t *npa)
{
	uint32_t nsid;

	if (npa->npa_ns == NULL)
		errx(-1, "identify-namespace cannot be used on controllers");

	if (npa->npa_argc > 0) {
		errx(-1, "%s passed extraneous arguments starting with %s",
		    npa->npa_cmd->c_name, npa->npa_argv[0]);
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_CTRL_LIST) != 0 &&
	    npa->npa_cmdflags != NVMEADM_O_ID_CTRL_LIST) {
		errx(-1, "-c cannot be combined with other flags");
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_DESC_LIST) != 0 &&
	    npa->npa_cmdflags != NVMEADM_O_ID_DESC_LIST) {
		errx(-1, "-d cannot be combined with other flags");
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_ALLOC_NS) != 0) {
		errx(-1, "-a cannot be used on namespaces");
	}

	nsid = nvme_ns_info_nsid(npa->npa_ns_info);

	if ((npa->npa_cmdflags & NVMEADM_O_ID_CTRL_LIST) != 0) {
		nvme_identify_ctrl_list_t *ctlist;
		nvme_id_req_t *req;

		if ((ctlist = malloc(NVME_IDENTIFY_BUFSIZE)) == NULL) {
			err(-1, "failed to allocate identify buffer size");
		}

		if (!nvme_id_req_init_by_cns(npa->npa_ctrl, NVME_CSI_NVM,
		    NVME_IDENTIFY_NSID_CTRL_LIST, &req)) {
			nvmeadm_fatal(npa, "failed to initialize identify "
			    "request");
		}

		if (!nvme_id_req_set_nsid(req, nsid) ||
		    !nvme_id_req_set_ctrlid(req, 0) ||
		    !nvme_id_req_set_output(req, ctlist,
		    NVME_IDENTIFY_BUFSIZE)) {
			nvmeadm_fatal(npa, "failed to set required fields for "
			    "identify request");
		}

		if (!nvme_id_req_exec(req)) {
			nvmeadm_fatal(npa, "failed to execute identify "
			    "request");
		}
		nvme_id_req_fini(req);

		(void) printf("%s: ", npa->npa_name);
		nvme_print_identify_ctrl_list(
		    "Identify Attached Controller List", ctlist);
		free(ctlist);
	} else if ((npa->npa_cmdflags & NVMEADM_O_ID_DESC_LIST) != 0) {
		nvme_identify_nsid_desc_t *nsdesc;
		nvme_id_req_t *req;

		if ((nsdesc = malloc(NVME_IDENTIFY_BUFSIZE)) == NULL) {
			err(-1, "failed to allocate identify buffer size");
		}

		if (!nvme_id_req_init_by_cns(npa->npa_ctrl, NVME_CSI_NVM,
		    NVME_IDENTIFY_NSID_DESC, &req)) {
			nvmeadm_fatal(npa, "failed to initialize identify "
			    "request");
		}

		if (!nvme_id_req_set_nsid(req, nsid) ||
		    !nvme_id_req_set_output(req, nsdesc,
		    NVME_IDENTIFY_BUFSIZE)) {
			nvmeadm_fatal(npa, "failed to set required fields for "
			    "identify request");
		}

		if (!nvme_id_req_exec(req)) {
			nvmeadm_fatal(npa, "failed to execute identify "
			    "request");
		}
		nvme_id_req_fini(req);

		(void) printf("%s: ", npa->npa_name);
		nvme_print_identify_nsid_desc(nsdesc);
		free(nsdesc);
	} else {
		const nvme_identify_nsid_t *idns;

		(void) printf("%s: ", npa->npa_name);
		idns = nvme_ns_info_identify(npa->npa_ns_info);
		nvme_print_identify_nsid(idns, npa->npa_version);
	}

	return (0);
}

static void
optparse_identify(nvme_process_arg_t *npa)
{
	int c;

	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":Cacdn")) != -1) {
		switch (c) {
		case 'C':
			npa->npa_cmdflags |= NVMEADM_O_ID_COMMON_NS;
			break;

		case 'a':
			npa->npa_cmdflags |= NVMEADM_O_ID_ALLOC_NS;
			break;

		case 'c':
			npa->npa_cmdflags |= NVMEADM_O_ID_CTRL_LIST;
			break;

		case 'd':
			npa->npa_cmdflags |= NVMEADM_O_ID_DESC_LIST;
			break;

		case 'n':
			npa->npa_cmdflags |= NVMEADM_O_ID_NSID_LIST;
			break;

		case '?':
			errx(-1, "unknown option: -%c", optopt);

		case ':':
			errx(-1, "option -%c requires an argument", optopt);

		}
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_ALLOC_NS) != 0 &&
	    (npa->npa_cmdflags &
	    ~(NVMEADM_O_ID_ALLOC_NS | NVMEADM_O_ID_NSID_LIST)) != 0) {
		errx(-1, "-a can only be used alone or together with -n");
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_COMMON_NS) != 0 &&
	    npa->npa_cmdflags != NVMEADM_O_ID_COMMON_NS) {
		errx(-1, "-C cannot be combined with other flags");

	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_CTRL_LIST) != 0 &&
	    npa->npa_cmdflags != NVMEADM_O_ID_CTRL_LIST) {
		errx(-1, "-c cannot be combined with other flags");
	}

	if ((npa->npa_cmdflags & NVMEADM_O_ID_DESC_LIST) != 0 &&
	    npa->npa_cmdflags != NVMEADM_O_ID_DESC_LIST) {
		errx(-1, "-d cannot be combined with other flags");
	}
}

static void
usage_identify(const char *c_name)
{
	(void) fprintf(stderr,
	    "%s [ -C | -c | -d | [-a] -n ] <ctl>[/<ns>][,...]\n\n"
	    "  Print detailed information about the specified NVMe "
	    "controllers and/or name-\n  spaces.\n", c_name);
}

static int
do_identify(const nvme_process_arg_t *npa)
{
	if (npa->npa_argc > 0) {
		errx(-1, "%s passed extraneous arguments starting with %s",
		    npa->npa_cmd->c_name, npa->npa_argv[0]);
	}

	if (npa->npa_ns != NULL) {
		if ((npa->npa_cmdflags & NVMEADM_O_ID_COMMON_NS) != 0)
			errx(-1, "-C cannot be used on namespaces");

		if ((npa->npa_cmdflags & NVMEADM_O_ID_ALLOC_NS) != 0)
			errx(-1, "-a cannot be used on namespaces");

		if ((npa->npa_cmdflags & NVMEADM_O_ID_NSID_LIST) != 0)
			errx(-1, "-n cannot be used on namespaces");

		return (do_identify_ns(npa));
	} else {
		if ((npa->npa_cmdflags & NVMEADM_O_ID_DESC_LIST) != 0)
			errx(-1, "-d cannot be used on controllers");

		return (do_identify_ctrl(npa));
	}
}

static void
optparse_list_logs(nvme_process_arg_t *npa)
{
	int c;
	uint_t oflags = 0;
	boolean_t parse = B_FALSE;
	const char *fields = NULL;
	char *scope = NULL;
	ofmt_status_t oferr;
	nvmeadm_list_logs_t *nll;

	if ((nll = calloc(1, sizeof (nvmeadm_list_logs_t))) == NULL) {
		err(-1, "failed to allocate memory to track log information");
	}

	npa->npa_cmd_arg = nll;

	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":aHo:ps:")) != -1) {
		switch (c) {
		case 'a':
			nll->nll_unimpl = B_TRUE;
			break;
		case 'H':
			oflags |= OFMT_NOHEADER;
			break;
		case 'o':
			fields = optarg;
			break;
		case 'p':
			parse = B_TRUE;
			oflags |= OFMT_PARSABLE;
			break;
		case 's':
			scope = optarg;
			break;
		case '?':
			errx(-1, "unknown option: -%c", optopt);
		case ':':
			errx(-1, "option -%c requires an argument", optopt);
		}
	}

	if (!parse) {
		oflags |= OFMT_WRAP;
	}

	if (parse && fields == NULL) {
		errx(-1, "parsable mode (-p) requires fields specified with "
		    "-o");
	}

	if (fields == NULL) {
		if (nll->nll_unimpl) {
			fields = nvmeadm_list_logs_fields_impl;
		} else {
			fields = nvmeadm_list_logs_fields;
		}
	}

	if (scope != NULL) {
		const char *str;

		while ((str = strsep(&scope, ",")) != NULL) {
			if (strcasecmp(str, "nvm") == 0) {
				nll->nll_scope |= NVME_LOG_SCOPE_NVM;
			} else if (strcasecmp(str, "ns") == 0 ||
			    strcasecmp(str, "namespace") == 0) {
				nll->nll_scope |= NVME_LOG_SCOPE_NS;
			} else if (strcasecmp(str, "ctrl") == 0 ||
			    strcasecmp(str, "controller") == 0) {
				nll->nll_scope |= NVME_LOG_SCOPE_CTRL;
			} else {
				errx(-1, "unknown scope string: '%s'; valid "
				    "values are 'nvm', 'namespace', and "
				    "'controller'", str);
			}
		}
	}

	oferr = ofmt_open(fields, nvmeadm_list_logs_ofmt, oflags, 0,
	    &npa->npa_ofmt);
	ofmt_check(oferr, B_TRUE, npa->npa_ofmt, nvme_oferr, warnx);

	if (npa->npa_argc - optind > 1) {
		nll->nll_nfilts = npa->npa_argc - optind - 1;
		nll->nll_filts = npa->npa_argv + optind + 1;
		nll->nll_used = calloc(nll->nll_nfilts, sizeof (boolean_t));
		if (nll->nll_used == NULL) {
			err(-1, "failed to allocate memory for tracking log "
			    "page filters");
		}
	}
}

static void
usage_list_logs(const char *c_name)
{
	(void) fprintf(stderr, "%s [-H] [-o field,[...] [-p]] [-s scope,[...]] "
	    "[-a]\n\t  <ctl>[/<ns>][,...] [logpage...]\n\n"
	    "  List log pages supported by controllers or namespaces.\n",
	    c_name);
}

static boolean_t
do_list_logs_match(const nvme_log_disc_t *disc, nvmeadm_list_logs_t *nll)
{
	if (!nll->nll_unimpl && !nvme_log_disc_impl(disc)) {
		return (B_FALSE);
	}

	if (nll->nll_nfilts <= 0) {
		return (B_TRUE);
	}

	/*
	 * Check all filters. If we have multiple specified that would match we
	 * want that to count.
	 */
	boolean_t match = B_FALSE;
	for (int i = 0; i < nll->nll_nfilts; i++) {
		if (strcmp(nvme_log_disc_name(disc), nll->nll_filts[i]) == 0) {
			nll->nll_used[i] = B_TRUE;
			match = B_TRUE;
			continue;
		}

		size_t naliases = nvme_log_disc_naliases(disc);
		const char *const *aliases = nvme_log_disc_aliases(disc);
		for (size_t a = 0; a < naliases; a++) {
			if (strcmp(aliases[a], nll->nll_filts[i]) == 0) {
				nll->nll_used[i] = B_TRUE;
				match = B_TRUE;
				break;
			}
		}
	}

	return (match);
}

static int
do_list_logs(const nvme_process_arg_t *npa)
{
	nvme_log_disc_scope_t scope;
	nvme_log_iter_t *iter;
	nvme_iter_t ret;
	const nvme_log_disc_t *disc;
	nvmeadm_list_logs_t *nll = npa->npa_cmd_arg;

	if (nll->nll_scope != 0) {
		scope = nll->nll_scope;
	} else if (npa->npa_ns != NULL) {
		scope = NVME_LOG_SCOPE_NS;
	} else {
		scope = NVME_LOG_SCOPE_CTRL | NVME_LOG_SCOPE_NVM;
	}

	if (!nvme_log_discover_init(npa->npa_ctrl, scope, 0, &iter)) {
		nvmeadm_warn(npa, "failed to iterate logs on %s",
		    npa->npa_ctrl_name);
		return (-1);
	}

	while ((ret = nvme_log_discover_step(iter, &disc)) == NVME_ITER_VALID) {
		if (do_list_logs_match(disc, nll)) {
			nvmeadm_list_logs_ofmt_arg_t print;

			print.nlloa_name = npa->npa_name;
			print.nlloa_disc = disc;
			ofmt_print(npa->npa_ofmt, &print);
			nll->nll_nprint++;
		}
	}

	nvme_log_discover_fini(iter);
	if (ret == NVME_ITER_ERROR) {
		nvmeadm_warn(npa, "failed to iterate logs on %s",
		    npa->npa_ctrl_name);
		return (-1);
	}

	for (int i = 0; i < nll->nll_nfilts; i++) {
		if (!nll->nll_used[i]) {
			warnx("log page filter '%s' did match any log pages",
			    nll->nll_filts[i]);
			exitcode = -1;
		}
	}

	if (nll->nll_nprint == 0) {
		if (nll->nll_nfilts == 0) {
			warnx("no log pages found for %s", npa->npa_name);
		}
		exitcode = -1;
	}

	return (exitcode);
}

static void
usage_get_logpage(const char *c_name)
{
	(void) fprintf(stderr, "%s [-O file | -x | -p -o field,[...] [-H]]\n"
	    "\t  <ctl>[/<ns>][,...] <logpage> [filter...]\n\n"
	    "  Print the specified log page of the specified NVMe "
	    "controllers and/or name-\n  spaces. Run \"nvmeadm list-logpages\" "
	    "for supported log pages. All devices\n  support error, health, "
	    "and firmware.\n", c_name);
}

static void
usage_firmware_list(const char *c_name)
{
	(void) fprintf(stderr, "%s <ctl>\n\n"
	    "  Print the log page that contains the list of firmware "
	    "images installed on the specified NVMe controller.\n", c_name);
}

static uint64_t
do_get_logpage_size(const nvme_process_arg_t *npa, nvme_log_disc_t *disc,
    nvme_log_req_t *req)
{
	uint64_t len, ret;
	void *buf;
	nvme_log_size_kind_t kind;

	kind = nvme_log_disc_size(disc, &len);
	if (kind != NVME_LOG_SIZE_K_VAR) {
		return (len);
	}

	/*
	 * We have a log with a variable length size. To determine the actual
	 * size we must actually determine the full length of this.
	 */
	if ((buf = malloc(len)) == NULL) {
		errx(-1, "failed to allocate %zu byte buffer to get log "
		    "page size", len);
	}

	if (!nvme_log_req_set_output(req, buf, len)) {
		nvmeadm_fatal(npa, "failed to set output parameters to "
		    "determine log length");
	}

	if (!nvme_log_req_exec(req)) {
		nvmeadm_fatal(npa, "failed to execute log request %s to "
		    "determine log length", npa->npa_argv[0]);
	}

	if (!nvme_log_disc_calc_size(disc, &ret, buf, len)) {
		errx(-1, "failed to determine full %s log length",
		    npa->npa_argv[0]);
	}

	free(buf);
	return (ret);
}

static void
do_get_logpage_dump(const void *buf, size_t len, const char *file)
{
	size_t off = 0;
	int fd = open(file, O_WRONLY | O_TRUNC | O_CREAT, 0644);

	if (fd < 0) {
		err(-1, "failed to create output file %s", file);
	}

	while (len > 0) {
		ssize_t ret = write(fd, buf + off, len - off);
		if (ret < 0) {
			err(EXIT_FAILURE, "failed to write log data to file %s "
			    "at offset %zu", file, off);
		}

		off += (size_t)ret;
		len -= (size_t)ret;
	}

	(void) close(fd);
}

/*
 * Here we need to explicitly attempt to release any context that has previously
 * existed for the persistent event log. It is fine if none exists as the
 * controller is required not to error. However, if we don't do this and attempt
 * to establish a new context, then it will generate an error.
 *
 * We'll use our existing request, which doesn't ask for data yet and issue the
 * get log page request with the LSP in question. After it is completed, we'll
 * reset the LSP to establish a context.
 */
static void
do_get_logpage_pev_relctx(const nvme_process_arg_t *npa, nvme_log_req_t *req)
{
	uint32_t buf;

	if (!nvme_log_req_set_lsp(req, NVME_PEV_LSP_REL_CTX)) {
		nvmeadm_fatal(npa, "failed to set lsp to release the "
		    "persistent event log context");
	}

	/*
	 * In NVMe 2.0 the spec made it explicit that the controller was
	 * supposed to ignore the length and offset for a request to release the
	 * context; however, that wasn't present in NVMe 1.4. The number of
	 * dwords part of the get log page command is a zeros based value,
	 * meaning there is no explicit way to request zero bytes. Rather than
	 * trust that all controllers get this right (especially when it wasn't
	 * exactly specified in NVMe 1.4), we just toss a throwaway buffer here.
	 */
	if (!nvme_log_req_set_output(req, &buf, sizeof (buf))) {
		nvmeadm_fatal(npa, "failed to set zero log length for "
		    "persistent event log release context");
	}

	if (!nvme_log_req_exec(req)) {
		nvmeadm_fatal(npa, "failed to execute log request %s to "
		    "release the event log context", npa->npa_argv[0]);
	}

	if (!nvme_log_req_set_lsp(req, NVME_PEV_LSP_EST_CTX_READ)) {
		nvmeadm_fatal(npa, "failed to set lsp to establish the "
		    "persistent event log context");
	}

	/*
	 * Make sure that our stack buffer is no longer part of the log request.
	 */
	if (!nvme_log_req_clear_output(req)) {
		nvmeadm_fatal(npa, "failed to clear output from persistent "
		    "event log release context");
	}
}

/*
 * Fill in the baseline context for a phyeye request. In this case we always
 * perform a normal read command and set the quality to "good" (the
 * lowest quality). Configurable use of this is driven through the phyeye
 * commands in nvmeadm_phyeye.c. In addition, we must always set the controller
 * ID in this request to our own.
 */
static void
do_get_logpage_phyeye_ctx(const nvme_process_arg_t *npa, nvme_log_req_t *req)
{
	nvme_eom_lsp_t lsp;

	(void) memset(&lsp, 0, sizeof (lsp));
	lsp.nel_mqual = NVME_EOM_LSP_MQUAL_GOOD;
	lsp.nel_act = NVME_EOM_LSP_READ;

	if (!nvme_log_req_set_lsp(req, lsp.r)) {
		nvmeadm_fatal(npa, "failed to set lsp for host phyeye");
	}

	if (!nvme_log_req_set_lsi(req, npa->npa_idctl->id_cntlid)) {
		nvmeadm_fatal(npa, "failed to set lsi for host phyeye");
	}
}

static int
do_get_logpage_common(const nvme_process_arg_t *npa, const char *page,
    nvmeadm_field_filt_t *filts, size_t nfilts)
{
	int ret = 0;
	nvme_log_disc_t *disc;
	nvme_log_req_t *req;
	nvme_log_disc_scope_t scope;
	void *buf;
	size_t toalloc;
	nvmeadm_get_logpage_t *log = npa->npa_cmd_arg;

	/*
	 * If we have enough information to identify a log-page via libnvme (or
	 * in the future take enough options to allow us to actually do this
	 * manually), then we will fetch it. If we don't know how to print it,
	 * then we'll just hex dump it for now.
	 */
	if (!nvme_log_req_init_by_name(npa->npa_ctrl, page, 0, &disc, &req)) {
		nvmeadm_fatal(npa, "could not initialize log request for %s",
		    page);
	}

	if (npa->npa_ns != NULL) {
		scope = NVME_LOG_SCOPE_NS;
	} else {
		scope = NVME_LOG_SCOPE_CTRL | NVME_LOG_SCOPE_NVM;
	}

	if ((scope & nvme_log_disc_scopes(disc)) == 0) {
		errx(-1, "log page %s does not support operating on %s", page,
		    npa->npa_ns != NULL ? "namespaces" : "controllers");
	}

	/*
	 * In the future we should add options to allow one to specify and set
	 * the fields for the lsp, lsi, etc. and set them here. Some log pages
	 * need a specific lsp set and special handling related to contexts. Do
	 * that now.
	 */
	switch (nvme_log_disc_lid(disc)) {
	case NVME_LOGPAGE_PEV:
		do_get_logpage_pev_relctx(npa, req);
		break;
	case NVME_LOGPAGE_TELMHOST:
		return (do_get_logpage_telemetry(npa, disc, req));
	case NVME_LOGPAGE_PHYEYE:
		do_get_logpage_phyeye_ctx(npa, req);
		break;
	default:
		break;
	}

	if (npa->npa_ns != NULL) {
		uint32_t nsid = nvme_ns_info_nsid(npa->npa_ns_info);

		if (!nvme_log_req_set_nsid(req, nsid)) {
			nvmeadm_fatal(npa, "failed to set log request "
			    "namespace ID to 0x%x", nsid);
		}
	}

	/*
	 * The output size should be the last thing that we determine as we may
	 * need to issue a log request to figure out how much data we should
	 * actually be reading.
	 */
	toalloc = do_get_logpage_size(npa, disc, req);
	buf = malloc(toalloc);
	if (buf == NULL) {
		err(-1, "failed to allocate %zu bytes for log "
		    "request %s", toalloc, page);
	}

	if (!nvme_log_req_set_output(req, buf, toalloc)) {
		nvmeadm_fatal(npa, "failed to set output parameters");
	}

	/*
	 * Again, we need to potentially adjust specific LSP values here for the
	 * various contexts that exist. Note that we are reusing the existing
	 * request so if the prior values for the initial request are fine, then
	 * we can just leave it there.
	 */
	switch (nvme_log_disc_lid(disc)) {
	case NVME_LOGPAGE_PEV:
		if (!nvme_log_req_set_lsp(req, NVME_PEV_LSP_READ)) {
			nvmeadm_fatal(npa, "failed to set lsp to read the "
			    "persistent event log");
		}
		break;
	default:
		break;
	}

	if (!nvme_log_req_exec(req)) {
		nvmeadm_fatal(npa, "failed to execute log request %s",
		    npa->npa_argv[0]);
	}

	if (log != NULL && log->ngl_output != NULL) {
		do_get_logpage_dump(buf, toalloc, log->ngl_output);
		goto done;
	} else if (log != NULL && log->ngl_hex) {
		nvmeadm_dump_hex(buf, toalloc);
		goto done;
	}

	if (npa->npa_ofmt == NULL) {
		(void) printf("%s: ", npa->npa_name);
	}
	if (strcmp(page, "error") == 0) {
		size_t nlog = toalloc / sizeof (nvme_error_log_entry_t);
		if (nfilts > 0) {
			warnx("log filters are not currently supported with "
			    "the %s log page", page);
			ret = -1;
		}

		nvme_print_error_log(nlog, buf, npa->npa_version);
	} else if (strcmp(page, "health") == 0) {
		if (nfilts > 0) {
			warnx("log filters are not currently supported with "
			    "the %s log page", page);
			ret = -1;
		}

		nvme_print_health_log(buf, npa->npa_idctl, npa->npa_version);
	} else if (strcmp(page, "firmware") == 0) {
		if (nfilts > 0) {
			warnx("log filters are not currently supported with "
			    "the %s log page", page);
			ret = -1;
		}

		nvme_print_fwslot_log(buf, npa->npa_idctl);
	} else {
		if (npa->npa_ofmt == NULL) {
			(void) printf("%s (%s)\n", nvme_log_disc_desc(disc),
			    page);
		}
		if (!nvmeadm_log_page_fields(npa, page, buf, toalloc, filts,
		    nfilts, 0)) {
			ret = -1;
		}
	}

done:
	free(buf);
	nvme_log_disc_free(disc);
	nvme_log_req_fini(req);

	return (ret);
}

static int
do_get_logpage_fwslot(const nvme_process_arg_t *npa)
{
	if (npa->npa_argc >= 1) {
		warnx("no additional arguments may be specified to %s",
		    npa->npa_cmd->c_name);
		usage(npa->npa_cmd);
		exit(-1);
	}

	return (do_get_logpage_common(npa, "firmware", NULL, 0));
}

static void
optparse_get_logpage(nvme_process_arg_t *npa)
{
	int c;
	const char *fields = NULL;
	bool parse = false;
	uint_t oflags = 0;
	nvmeadm_get_logpage_t *log;
	uint_t count = 0;

	if ((log = calloc(1, sizeof (nvmeadm_get_logpage_t))) == NULL) {
		err(-1, "failed to allocate memory to track log page "
		    "information");
	}

	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":Ho:O:px")) != -1) {
		switch (c) {
		case 'H':
			oflags |= OFMT_NOHEADER;
			break;
		case 'o':
			fields = optarg;
			break;
		case 'O':
			log->ngl_output = optarg;
			count++;
			break;
		case 'p':
			parse = true;
			oflags |= OFMT_PARSABLE;
			count++;
			break;
		case 'x':
			log->ngl_hex = true;
			count++;
			break;
		case '?':
			errx(-1, "unknown option: -%c", optopt);
		case ':':
			errx(-1, "option -%c requires an argument", optopt);
		}
	}

	if (parse && fields == NULL) {
		errx(-1, "parsable mode (-p) requires fields specified with "
		    "-o");
	}

	if (!parse && fields != NULL) {
		errx(-1, "output field selection (-o) only usable in "
		    "parsable mode (-p)");
	}

	if (!parse && (oflags & OFMT_NOHEADER) != 0) {
		errx(-1, "omitting headers (-H) only usable in parsable mode "
		    "(-p)");
	}

	if (count > 1) {
		errx(-1, "only one of parsable output (-p), outputing to a "
		    "file (-O), and hexadecimal output (-x) may be requested");
	}

	if (parse) {
		ofmt_status_t oferr = ofmt_open(fields, nvmeadm_field_ofmt,
		    oflags, 0, &npa->npa_ofmt);
		ofmt_check(oferr, B_TRUE, npa->npa_ofmt, nvme_oferr, warnx);
	}

	npa->npa_cmd_arg = log;
}

static void
do_logpage_filts(const nvme_process_arg_t *npa, nvmeadm_field_filt_t **filtsp,
    size_t *nfiltsp)
{
	size_t nfilts;
	nvmeadm_field_filt_t *filts;

	*filtsp = NULL;
	*nfiltsp = 0;

	if (npa->npa_argc <= 1)
		return;

	nfilts = (size_t)npa->npa_argc - 1;
	filts = calloc(nfilts, sizeof (nvmeadm_field_filt_t));
	if (filts == NULL) {
		err(-1, "failed to allocate memory for filter "
		    "tracking");
	}

	for (size_t i = 0; i < nfilts; i++) {
		filts[i].nff_len = strlen(npa->npa_argv[i + 1]);
		filts[i].nff_str = npa->npa_argv[i + 1];
	}

	*filtsp = filts;
	*nfiltsp = nfilts;
}

static int
do_get_logpage(const nvme_process_arg_t *npa)
{
	size_t nfilts = 0;
	nvmeadm_field_filt_t *filts = NULL;
	nvmeadm_get_logpage_t *log = npa->npa_cmd_arg;

	if (npa->npa_argc < 1) {
		warnx("missing log page name");
		usage(npa->npa_cmd);
		exit(-1);
	}

	do_logpage_filts(npa, &filts, &nfilts);

	if (log != NULL && log->ngl_hex && nfilts > 0) {
		errx(-1, "hexadecimal output (-x) cannot be used with "
		    "log filter operands");
	}

	return (do_get_logpage_common(npa, npa->npa_argv[0], filts, nfilts));
}

static void
usage_print_logpage(const char *c_name)
{
	(void) fprintf(stderr, "%s -f file [-x | -p -o field,[...] [-H]] "
	    "<logpage>\n\t  [filter...]\n\n"
	    "  Print the specified log page from a file. Optional filters "
	    "may be used to\n  restrict the log fields printed. See nvmeadm(8) "
	    "for a list of all log\n  pages. Run \"nvmeadm list-logpages\" to "
	    "see log pages a specific controller\n  supports.\n", c_name);
}

typedef struct {
	const char *pl_input;
	bool pl_hex;
} print_logpages_t;

static void
optparse_print_logpage(nvme_process_arg_t *npa)
{
	int c;
	const char *fields = NULL;
	bool parse = false;
	uint_t oflags = 0;
	print_logpages_t *pl;

	if ((pl = calloc(1, sizeof (print_logpages_t))) == NULL) {
		err(-1, "failed to allocate memory for option tracking");
	}

	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":f:Ho:px")) != -1) {
		switch (c) {
		case 'f':
			pl->pl_input = optarg;
			break;
		case 'H':
			oflags |= OFMT_NOHEADER;
			break;
		case 'o':
			fields = optarg;
			break;
		case 'p':
			parse = true;
			oflags |= OFMT_PARSABLE;
			break;
		case 'x':
			pl->pl_hex = true;
			break;
		case '?':
			errx(-1, "unknown option: -%c", optopt);
		case ':':
			errx(-1, "option -%c requires an argument", optopt);
		}
	}

	if (pl->pl_input == NULL) {
		errx(-1, "missing required input file to process (-f)");
	}

	if (parse && fields == NULL) {
		errx(-1, "parsable mode (-p) requires fields specified with "
		    "-o");
	}

	if (!parse && fields != NULL) {
		errx(-1, "output field selection (-o) only usable in "
		    "parsable mode (-p)");
	}

	if (!parse && (oflags & OFMT_NOHEADER) != 0) {
		errx(-1, "omitting headers (-H) only usable in parsable mode "
		    "(-p)");
	}

	if (pl->pl_hex && parse) {
		errx(-1, "only one of parsable output (-p) and heaxdecimal "
		    "output (-x) may be requested");
	}

	if (parse) {
		ofmt_status_t oferr = ofmt_open(fields, nvmeadm_field_ofmt,
		    oflags, 0, &npa->npa_ofmt);
		ofmt_check(oferr, B_TRUE, npa->npa_ofmt, nvme_oferr, warnx);
	}

	npa->npa_cmd_arg = pl;
}

static int
do_print_logpage(const nvme_process_arg_t *npa)
{
	int fd = -1, ret = 0;
	struct stat st;
	void *data;
	const char *logpage = NULL;
	size_t nfilts = 0;
	nvmeadm_field_filt_t *filts = NULL;
	print_logpages_t *pl = npa->npa_cmd_arg;
	nvmeadm_log_field_flag_t flag = 0;

	if (pl->pl_hex) {
		if (npa->npa_argc != 0) {
			errx(-1, "log page and filters not supported with -x");
		}
	} else {
		if (npa->npa_argc == 0) {
			errx(-1, "missing required log page");
		}

		logpage = npa->npa_argv[0];
		do_logpage_filts(npa, &filts, &nfilts);
		flag |= NVMEADM_LFF_CHECK_NAME;
	}

	if ((fd = open(pl->pl_input, O_RDONLY)) < 0) {
		err(-1, "failed to open input file %s", pl->pl_input);
	}

	if (fstat(fd, &st) != 0) {
		err(-1, "failed to stat %s", pl->pl_input);
	}

	if (st.st_size > NVMEADM_MAX_MMAP) {
		errx(-1, "%s file size of 0x%lx exceeds maximum allowed size "
		    "of 0x%llx", pl->pl_input, st.st_size, NVMEADM_MAX_MMAP);
	}

	data = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
	if (data == MAP_FAILED) {
		errx(-1, "failed to mmap %s", pl->pl_input);
	}

	if (!nvmeadm_log_page_fields(npa, logpage, data, st.st_size,
	    filts, nfilts, flag)) {
		ret = -1;
	}

	VERIFY0(munmap(data, st.st_size));
	VERIFY0(close(fd));
	free(filts);

	return (ret);
}

static void
optparse_list_features(nvme_process_arg_t *npa)
{
	int c;
	uint_t oflags = 0;
	boolean_t parse = B_FALSE;
	const char *fields = NULL;
	nvmeadm_features_t *feat;
	ofmt_status_t oferr;

	if ((feat = calloc(1, sizeof (nvmeadm_features_t))) == NULL) {
		err(-1, "failed to allocate memory to track feature "
		    "information");
	}

	npa->npa_cmd_arg = feat;

	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":aHo:p")) != -1) {
		switch (c) {
		case 'a':
			feat->nf_unimpl = B_TRUE;
			break;
		case 'H':
			oflags |= OFMT_NOHEADER;
			break;
		case 'o':
			fields = optarg;
			break;
		case 'p':
			parse = B_TRUE;
			oflags |= OFMT_PARSABLE;
			break;
		case '?':
			errx(-1, "unknown option: -%c", optopt);
		case ':':
			errx(-1, "option -%c requires an argument", optopt);
		}
	}

	if (!parse) {
		oflags |= OFMT_WRAP;
	}

	if (parse && fields == NULL) {
		errx(-1, "parsable mode (-p) requires fields specified with "
		    "-o");
	}

	if (fields == NULL) {
		fields = nvmeadm_list_features_fields;
	}

	oferr = ofmt_open(fields, nvmeadm_list_features_ofmt, oflags, 0,
	    &npa->npa_ofmt);
	ofmt_check(oferr, B_TRUE, npa->npa_ofmt, nvme_oferr, warnx);

	if (npa->npa_argc - optind > 1) {
		feat->nf_nfilts = (uint32_t)(npa->npa_argc - optind - 1);
		feat->nf_filts = npa->npa_argv + optind + 1;
		feat->nf_used = calloc(feat->nf_nfilts, sizeof (boolean_t));
		if (feat->nf_used == NULL) {
			err(-1, "failed to allocate memory for tracking "
			    "feature filters");
		}
	}
}

static void
usage_list_features(const char *c_name)
{
	(void) fprintf(stderr, "%s [-a] [-H] [-o field,[...] [-p]] "
	    "<ctl>[/<ns>][,...]\n\t  [feature...]\n\n"
	    "  List features supported by controllers or namespaces.\n",
	    c_name);
}

static boolean_t
do_features_match(const nvme_feat_disc_t *disc, nvmeadm_features_t *nf)
{
	if (nf->nf_nfilts == 0) {
		return (B_TRUE);
	}

	for (uint32_t i = 0; i < nf->nf_nfilts; i++) {
		const char *match = nf->nf_filts[i];
		long long fid;
		const char *err;

		if (strcmp(nvme_feat_disc_short(disc), match) == 0 ||
		    strcasecmp(nvme_feat_disc_spec(disc), match) == 0) {
			nf->nf_used[i] = B_TRUE;
			return (B_TRUE);
		}

		fid = strtonumx(match, 0, UINT32_MAX, &err, 0);
		if (err == NULL && fid == nvme_feat_disc_fid(disc)) {
			nf->nf_used[i] = B_TRUE;
			return (B_TRUE);
		}
	}

	return (B_FALSE);
}


/*
 * This is a common entry point for both list-features and get-features, which
 * iterate over all features and take action for each one.
 */
typedef void (*do_features_cb_f)(const nvme_process_arg_t *,
    const nvme_feat_disc_t *);
static int
do_features(const nvme_process_arg_t *npa, nvmeadm_features_t *nf,
    do_features_cb_f func)
{
	nvme_feat_scope_t scope;
	nvme_feat_iter_t *iter;
	nvme_iter_t ret;
	const nvme_feat_disc_t *disc;

	if (npa->npa_ns != NULL) {
		scope = NVME_FEAT_SCOPE_NS;
	} else {
		scope = NVME_FEAT_SCOPE_CTRL;
	}

	if (!nvme_feat_discover_init(npa->npa_ctrl, scope, 0, &iter)) {
		nvmeadm_warn(npa, "failed to iterate features on %s",
		    npa->npa_ctrl_name);
		return (-1);
	}

	while ((ret = nvme_feat_discover_step(iter, &disc)) ==
	    NVME_ITER_VALID) {
		if (do_features_match(disc, nf)) {
			if (!nf->nf_unimpl && nvme_feat_disc_impl(disc) ==
			    NVME_FEAT_IMPL_UNSUPPORTED) {
				continue;
			}

			func(npa, disc);
			nf->nf_nprint++;
		}
	}

	nvme_feat_discover_fini(iter);
	if (ret == NVME_ITER_ERROR) {
		nvmeadm_warn(npa, "failed to iterate features on %s",
		    npa->npa_ctrl_name);
		return (-1);
	}

	for (uint32_t i = 0; i < nf->nf_nfilts; i++) {
		if (!nf->nf_used[i]) {
			warnx("feature filter '%s' did match any features",
			    nf->nf_filts[i]);
			exitcode = -1;
		}
	}

	if (nf->nf_nprint == 0) {
		if (nf->nf_nfilts == 0) {
			warnx("no features found for %s", npa->npa_name);
		}
		exitcode = -1;
	}

	return (exitcode);
}

static void
do_list_features_cb(const nvme_process_arg_t *npa, const nvme_feat_disc_t *disc)
{
	nvmeadm_list_features_ofmt_arg_t print;

	print.nlfoa_name = npa->npa_name;
	print.nlfoa_feat = disc;
	ofmt_print(npa->npa_ofmt, &print);
}

static int
do_list_features(const nvme_process_arg_t *npa)
{
	nvmeadm_features_t *nf = npa->npa_cmd_arg;

	return (do_features(npa, nf, do_list_features_cb));
}

static void
usage_get_features(const char *c_name)
{
	(void) fprintf(stderr, "%s <ctl>[/<ns>][,...] [<feature>[,...]]\n\n"
	    "  Print the specified features of the specified NVMe controllers "
	    "and/or\n  namespaces. Feature support varies on the controller.\n"
	    "Run \"nvmeadm list-features <ctl>\" to see supported features.\n",
	    c_name);
}

/*
 * The nvmeadm(8) get-features output has traditionally swallowed certain errors
 * for features that it considers unimplemented in tandem with the kernel. With
 * the introduction of libnvme and ioctl interface changes, the kernel no longer
 * caches information about features that are unimplemented.
 *
 * There are two cases that we currently swallow errors on and the following
 * must all be true:
 *
 * 1) We have a controller error.
 * 2) The system doesn't know whether the feature is implemented or not.
 * 3) The controller error indicates that we have an invalid field.
 *
 * There is one additional wrinkle that we are currently papering over due to
 * the history of nvmeadm swallowing errors. The error recovery feature was made
 * explicitly namespace-specific in NVMe 1.4. However, various NVMe 1.3 devices
 * will error if we ask for it without specifying a namespace. Conversely, older
 * devices will be upset if you do ask for a namespace. This case can be removed
 * once we better survey devices and come up with a heuristic for how to handle
 * this across older generations.
 *
 * If we add a single feature endpoint that gives flexibility over how the
 * feature are listed, then we should not swallow errors.
 */
static boolean_t
swallow_get_feat_err(const nvme_process_arg_t *npa,
    const nvme_feat_disc_t *disc)
{
	uint32_t sct, sc;

	if (nvme_ctrl_err(npa->npa_ctrl) != NVME_ERR_CONTROLLER) {
		return (B_FALSE);
	}

	nvme_ctrl_deverr(npa->npa_ctrl, &sct, &sc);
	if (nvme_feat_disc_impl(disc) == NVME_FEAT_IMPL_UNKNOWN &&
	    sct == NVME_CQE_SCT_GENERIC && sc == NVME_CQE_SC_GEN_INV_FLD) {
		return (B_TRUE);
	}

	if (nvme_feat_disc_fid(disc) == NVME_FEAT_ERROR &&
	    sct == NVME_CQE_SCT_GENERIC && (sc == NVME_CQE_SC_GEN_INV_FLD ||
	    sc == NVME_CQE_SC_GEN_INV_NS)) {
		return (B_TRUE);
	}

	return (B_FALSE);
}

static boolean_t
do_get_feat_common(const nvme_process_arg_t *npa, const nvme_feat_disc_t *disc,
    uint32_t cdw11, uint32_t *cdw0, void **datap, size_t *lenp)
{
	nvme_get_feat_req_t *req = NULL;
	void *data = NULL;
	uint64_t datalen = 0;
	nvme_get_feat_fields_t fields = nvme_feat_disc_fields_get(disc);

	if (!nvme_get_feat_req_init_by_disc(npa->npa_ctrl, disc, &req)) {
		nvmeadm_warn(npa, "failed to initialize get feature request "
		    "for feature %s", nvme_feat_disc_short(disc));
		exitcode = -1;
		goto err;
	}

	if ((fields & NVME_GET_FEAT_F_CDW11) != 0 &&
	    !nvme_get_feat_req_set_cdw11(req, cdw11)) {
		nvmeadm_warn(npa, "failed to set cdw11 to 0x%x for feature %s",
		    cdw11, nvme_feat_disc_short(disc));
		exitcode = -1;
		goto err;
	}

	if ((fields & NVME_GET_FEAT_F_DATA) != 0) {
		datalen = nvme_feat_disc_data_size(disc);
		VERIFY3U(datalen, !=, 0);
		data = malloc(datalen);
		if (data == NULL) {
			err(-1, "failed to allocate %zu bytes for feature %s "
			    "data buffer", datalen, nvme_feat_disc_short(disc));
		}

		if (!nvme_get_feat_req_set_output(req, data, datalen)) {
			nvmeadm_warn(npa, "failed to set output data for "
			    "feature %s", nvme_feat_disc_short(disc));
			exitcode = -1;
			goto err;
		}
	}

	if ((fields & NVME_GET_FEAT_F_NSID) != 0) {
		uint32_t nsid = nvme_ns_info_nsid(npa->npa_ns_info);

		if (!nvme_get_feat_req_set_nsid(req, nsid)) {
			nvmeadm_warn(npa, "failed to set nsid to 0x%x for "
			    "feature %s", nsid, nvme_feat_disc_spec(disc));
			exitcode = -1;
			goto err;
		}
	}

	if (!nvme_get_feat_req_exec(req)) {
		if (!swallow_get_feat_err(npa, disc)) {
			nvmeadm_warn(npa, "failed to get feature %s",
			    nvme_feat_disc_spec(disc));
			exitcode = -1;
		}

		goto err;
	}

	if (!nvme_get_feat_req_get_cdw0(req, cdw0)) {
		nvmeadm_warn(npa, "failed to get cdw0 result data for %s",
		    nvme_feat_disc_spec(disc));
		goto err;
	}

	*datap = data;
	*lenp = datalen;
	nvme_get_feat_req_fini(req);
	return (B_TRUE);

err:
	free(data);
	nvme_get_feat_req_fini(req);
	return (B_FALSE);
}

static void
do_get_feat_temp_thresh_one(const nvme_process_arg_t *npa,
    const nvme_feat_disc_t *disc, const nvmeadm_feature_t *feat,
    const char *label, uint16_t tmpsel, uint16_t thsel)
{
	uint32_t cdw0;
	void *buf = NULL;
	size_t buflen;
	nvme_temp_threshold_t tt;

	tt.r = 0;
	tt.b.tt_tmpsel = tmpsel;
	tt.b.tt_thsel = thsel;

	/*
	 * The printing function treats the buffer argument as the label to
	 * print for this threshold.
	 */
	if (!do_get_feat_common(npa, disc, tt.r, &cdw0, &buf, &buflen)) {
		return;
	}

	feat->f_print(cdw0, (void *)label, 0, npa->npa_idctl,
	    npa->npa_version);
	free(buf);
}

/*
 * In NVMe 1.2, the specification allowed for up to 8 sensors to be on the
 * device and changed the main device to have a composite temperature sensor. As
 * a result, there is a set of thresholds for each sensor. In addition, they
 * added both an over-temperature and under-temperature threshold. Since most
 * devices don't actually implement all the sensors, we get the health page and
 * see which sensors have a non-zero value to determine how to proceed.
 */
static boolean_t
do_get_feat_temp_thresh(const nvme_process_arg_t *npa,
    const nvme_feat_disc_t *disc, const nvmeadm_feature_t *feat)
{
	nvme_log_req_t *req = NULL;
	nvme_log_disc_t *log_disc = NULL;
	size_t toalloc;
	void *buf = NULL;
	boolean_t ret = B_FALSE;
	const nvme_health_log_t *hlog;

	nvme_print(2, nvme_feat_disc_spec(disc), -1, NULL);
	do_get_feat_temp_thresh_one(npa, disc, feat,
	    "Composite Over Temp. Threshold", 0, NVME_TEMP_THRESH_OVER);

	if (!nvme_version_check(npa, &nvme_vers_1v2)) {
		return (B_TRUE);
	}

	if (!nvme_log_req_init_by_name(npa->npa_ctrl, "health", 0, &log_disc,
	    &req)) {
		nvmeadm_warn(npa, "failed to initialize health log page "
		    "request");
		return (B_FALSE);
	}

	toalloc = do_get_logpage_size(npa, log_disc, req);
	buf = malloc(toalloc);
	if (buf == NULL) {
		err(-1, "failed to allocate %zu bytes for health log page",
		    toalloc);
	}

	if (!nvme_log_req_set_output(req, buf, toalloc)) {
		nvmeadm_warn(npa, "failed to set output parameters for health "
		    "log page");
		goto out;
	}

	if (!nvme_log_req_exec(req)) {
		nvmeadm_warn(npa, "failed to retrieve the health log page");
		goto out;
	}

	/* cast required to prove our intentionality to smatch */
	hlog = (const nvme_health_log_t *)buf;

	do_get_feat_temp_thresh_one(npa, disc, feat,
	    "Composite Under Temp. Threshold", 0, NVME_TEMP_THRESH_UNDER);
	if (hlog->hl_temp_sensor_1 != 0) {
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 1 Over Temp. Threshold", 1,
		    NVME_TEMP_THRESH_OVER);
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 1 Under Temp. Threshold", 1,
		    NVME_TEMP_THRESH_UNDER);
	}

	if (hlog->hl_temp_sensor_2 != 0) {
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 2 Over Temp. Threshold", 2,
		    NVME_TEMP_THRESH_OVER);
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 2 Under Temp. Threshold", 2,
		    NVME_TEMP_THRESH_UNDER);
	}

	if (hlog->hl_temp_sensor_3 != 0) {
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 3 Over Temp. Threshold", 3,
		    NVME_TEMP_THRESH_OVER);
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 3 Under Temp. Threshold", 3,
		    NVME_TEMP_THRESH_UNDER);
	}

	if (hlog->hl_temp_sensor_4 != 0) {
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 4 Over Temp. Threshold", 4,
		    NVME_TEMP_THRESH_OVER);
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 4 Under Temp. Threshold", 4,
		    NVME_TEMP_THRESH_UNDER);
	}

	if (hlog->hl_temp_sensor_5 != 0) {
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 5 Over Temp. Threshold", 5,
		    NVME_TEMP_THRESH_OVER);
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 5 Under Temp. Threshold", 5,
		    NVME_TEMP_THRESH_UNDER);
	}

	if (hlog->hl_temp_sensor_6 != 0) {
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 6 Over Temp. Threshold", 6,
		    NVME_TEMP_THRESH_OVER);
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 6 Under Temp. Threshold", 6,
		    NVME_TEMP_THRESH_UNDER);
	}

	if (hlog->hl_temp_sensor_7 != 0) {
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 7 Over Temp. Threshold", 7,
		    NVME_TEMP_THRESH_OVER);
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 7 Under Temp. Threshold", 7,
		    NVME_TEMP_THRESH_UNDER);
	}

	if (hlog->hl_temp_sensor_8 != 0) {
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 8 Over Temp. Threshold", 8,
		    NVME_TEMP_THRESH_OVER);
		do_get_feat_temp_thresh_one(npa, disc, feat,
		    "Temp. Sensor 8 Under Temp. Threshold", 8,
		    NVME_TEMP_THRESH_UNDER);
	}

	ret = B_TRUE;
out:
	nvme_log_req_fini(req);
	free(buf);
	return (ret);
}

static boolean_t
do_get_feat_intr_vect(const nvme_process_arg_t *npa,
    const nvme_feat_disc_t *disc, const nvmeadm_feature_t *feat)
{
	uint32_t nintrs;
	boolean_t ret = B_TRUE;

	if (!nvme_ctrl_info_pci_nintrs(npa->npa_ctrl_info, &nintrs)) {
		nvmeadm_ctrl_info_warn(npa, "failed to get interrupt count "
		    "from controller %s information snapshot", npa->npa_name);
		return (B_FALSE);
	}

	nvme_print(2, nvme_feat_disc_spec(disc), -1, NULL);
	for (uint32_t i = 0; i < nintrs; i++) {
		uint32_t cdw0;
		void *buf;
		size_t buflen;
		nvme_intr_vect_t vect;

		vect.r = 0;
		vect.b.iv_iv = i;

		if (!do_get_feat_common(npa, disc, vect.r, &cdw0, &buf,
		    &buflen)) {
			ret = B_FALSE;
			continue;
		}

		feat->f_print(cdw0, buf, buflen, npa->npa_idctl,
		    npa->npa_version);
		free(buf);
	}

	return (ret);
}

/*
 * We've been asked to print the following feature that the controller probably
 * supports. Find our internal feature information for this to see if we know
 * how to deal with it.
 */
static void
do_get_features_cb(const nvme_process_arg_t *npa, const nvme_feat_disc_t *disc)
{
	const nvmeadm_feature_t *feat = NULL;
	uint32_t fid = nvme_feat_disc_fid(disc);
	nvme_get_feat_fields_t fields;
	void *data = NULL;
	size_t datalen = 0;
	uint32_t cdw0;

	for (size_t i = 0; i < ARRAY_SIZE(features); i++) {
		if (features[i].f_feature == fid) {
			feat = &features[i];
			break;
		}
	}

	/*
	 * Determine if we have enough logic in here to get and print the
	 * feature. The vast majority of NVMe features only output a single
	 * uint32_t in cdw0 and potentially a data buffer. As long as no input
	 * arguments are required, then we can go ahead and get this and print
	 * the data. If there is, then we will refuse unless we have a
	 * particular function. If we have a specific get function, we expect it
	 * to do all the printing.
	 */
	if (feat != NULL && feat->f_get != NULL) {
		if (!feat->f_get(npa, disc, feat)) {
			exitcode = -1;
		}
		return;
	}

	fields = nvme_feat_disc_fields_get(disc);
	if ((fields & NVME_GET_FEAT_F_CDW11) != 0) {
		warnx("unable to get feature %s due to missing nvmeadm(8) "
		    "implementation logic", nvme_feat_disc_spec(disc));
		exitcode = -1;
		return;
	}

	/*
	 * We do not set exitcode on failure here so that way we can swallow
	 * errors from unimplemented features.
	 */
	if (!do_get_feat_common(npa, disc, 0, &cdw0, &data, &datalen)) {
		return;
	}

	nvme_print(2, nvme_feat_disc_spec(disc), -1, NULL);
	if (feat != NULL && feat->f_print != NULL) {
		feat->f_print(cdw0, data, datalen, npa->npa_idctl,
		    npa->npa_version);
	} else {
		nvme_feat_output_t output = nvme_feat_disc_output_get(disc);
		nvme_print_feat_unknown(output, cdw0, data, datalen);
	}

	free(data);
}

/*
 * This is an entry point which prints every feature that we know about. We
 * often go to lengths to discover all the variable inputs that can be used for
 * a given feature that requires an argument in cdw11. Due to the semantics of
 * filtering being used for features and the need to print each feature, this is
 * not the place to add general field filtering or a means to request a specific
 * cdw11 argument or similar. Instead, a new get-feature which requires someone
 * to specify the short name for a feature and then allows particular fields to
 * be grabbed and arguments should be created instead.
 *
 * This uses the same general feature logic that underpins do_list_features()
 * and therefore we transform filter arguments into the same style used there.
 */
static int
do_get_features(const nvme_process_arg_t *npa)
{
	char *fstr = NULL;
	char **filts = NULL;
	boolean_t *used = NULL;
	nvmeadm_features_t nf;
	int ret;

	if (npa->npa_argc > 1)
		errx(-1, "unexpected arguments");

	if (npa->npa_ns != NULL && nvme_ns_info_level(npa->npa_ns_info) <
	    NVME_NS_DISC_F_ACTIVE) {
		errx(-1, "cannot get feature: namespace is inactive");
	}

	/*
	 * We always leave nf_unimpl set to false as we don't want to bother
	 * trying to print a feature that we know the device doesn't support.
	 */
	(void) memset(&nf, 0, sizeof (nvmeadm_features_t));

	/*
	 * If we've been given a series of features to print, treat those as
	 * filters on the features as we're walking them to determine which to
	 * print or not.
	 */
	if (npa->npa_argc == 1) {
		char *f;
		uint32_t i;

		nf.nf_nfilts = 1;
		fstr = strdup(npa->npa_argv[0]);

		if (fstr == NULL) {
			err(-1, "failed to allocate memory to duplicate "
			    "feature string");
		}

		for (const char *c = strchr(fstr, ','); c != NULL;
		    c = strchr(c + 1, ',')) {
			nf.nf_nfilts++;
		}

		filts = calloc(nf.nf_nfilts, sizeof (char *));
		if (filts == NULL) {
			err(-1, "failed to allocate memory for filter list");
		}

		i = 0;
		while ((f = strsep(&fstr, ",")) != NULL) {
			filts[i] = f;
			i++;
		}
		VERIFY3U(i, ==, nf.nf_nfilts);
		nf.nf_filts = filts;

		used = calloc(nf.nf_nfilts, sizeof (boolean_t));
		if (used == NULL) {
			err(-1, "failed to allocate memory for filter use "
			    "tracking");
		}
		nf.nf_used = used;
	}

	(void) printf("%s: Get Features\n", npa->npa_name);
	ret = do_features(npa, &nf, do_get_features_cb);

	free(fstr);
	free(filts);
	free(used);
	return (ret);
}

static int
do_format_common(const nvme_process_arg_t *npa, uint32_t lbaf,
    uint32_t ses)
{
	int ret = 0;
	nvme_format_req_t *req;

	if (npa->npa_ns != NULL && nvme_ns_info_level(npa->npa_ns_info) <
	    NVME_NS_DISC_F_ACTIVE) {
		errx(-1, "cannot %s: namespace is inactive",
		    npa->npa_cmd->c_name);
	}

	if (!nvme_format_req_init(npa->npa_ctrl, &req)) {
		nvmeadm_fatal(npa, "failed to initialize format request for "
		    "%s", npa->npa_name);
	}

	if (npa->npa_ns != NULL) {
		uint32_t nsid = nvme_ns_info_nsid(npa->npa_ns_info);

		if (!nvme_format_req_set_nsid(req, nsid)) {
			nvmeadm_fatal(npa, "failed to set format request "
			    "namespace ID to 0x%x", nsid);
		}
	}

	if (!nvme_format_req_set_lbaf(req, lbaf) ||
	    !nvme_format_req_set_ses(req, ses)) {
		nvmeadm_fatal(npa, "failed to set format request fields for %s",
		    npa->npa_name);
	}

	if (do_detach_bd(npa) != 0) {
		errx(-1, "cannot %s %s due to namespace detach failure",
		    npa->npa_cmd->c_name, npa->npa_name);
	}

	if (!nvme_format_req_exec(req)) {
		nvmeadm_warn(npa, "failed to %s %s", npa->npa_cmd->c_name,
		    npa->npa_name);
		ret = -1;
	}

	if (do_attach_bd(npa) != 0)
		ret = -1;

	return (ret);
}

static void
usage_format(const char *c_name)
{
	(void) fprintf(stderr, "%s <ctl>[/<ns>] [<lba-format>]\n\n"
	    "  Format one or all namespaces of the specified NVMe "
	    "controller. Supported LBA\n  formats can be queried with "
	    "the \"%s identify\" command on the namespace\n  to be "
	    "formatted.\n", c_name, getprogname());
}

static uint32_t
do_format_determine_lbaf(const nvme_process_arg_t *npa)
{
	const nvme_nvm_lba_fmt_t *fmt;
	nvme_ns_info_t *ns_info = NULL;
	uint32_t lbaf;

	if (npa->npa_argc > 0) {
		unsigned long lba;
		uint32_t nlbaf = nvme_ctrl_info_nformats(npa->npa_ctrl_info);

		errno = 0;
		lba = strtoul(npa->npa_argv[0], NULL, 10);
		if (errno != 0 || lba >= nlbaf)
			errx(-1, "invalid LBA format %s", npa->npa_argv[0]);

		if (!nvme_ctrl_info_format(npa->npa_ctrl_info, (uint32_t)lba,
		    &fmt)) {
			nvmeadm_fatal(npa, "failed to get LBA format %lu "
			    "information", lba);
		}
	} else {
		/*
		 * If we have a namespace then we use the current namespace's
		 * LBA format. If we don't have a namespace, then we promised
		 * we'd look at namespace 1 in the manual page.
		 */
		if (npa->npa_ns_info == NULL) {
			if (!nvme_ctrl_ns_info_snap(npa->npa_ctrl, 1,
			    &ns_info)) {
				nvmeadm_fatal(npa, "failed to get namespace 1 "
				    "information, please explicitly specify an "
				    "LBA format");
			}

			if (!nvme_ns_info_curformat(ns_info, &fmt)) {
				nvmeadm_fatal(npa, "failed to retrieve current "
				    "namespace format from namespace 1");
			}
		} else {
			if (!nvme_ns_info_curformat(npa->npa_ns_info, &fmt)) {
				nvmeadm_fatal(npa, "failed to get the current "
				    "format information from %s",
				    npa->npa_name);
			}
		}
	}

	if (nvme_nvm_lba_fmt_meta_size(fmt) != 0) {
		errx(-1, "LBA formats with metadata are not supported");
	}

	lbaf = nvme_nvm_lba_fmt_id(fmt);
	nvme_ns_info_free(ns_info);
	return (lbaf);
}

static int
do_format(const nvme_process_arg_t *npa)
{
	uint32_t lbaf;

	if (npa->npa_argc > 1) {
		errx(-1, "%s passed extraneous arguments starting with %s",
		    npa->npa_cmd->c_name, npa->npa_argv[1]);
	}

	lbaf = do_format_determine_lbaf(npa);
	return (do_format_common(npa, lbaf, 0));
}

static void
usage_secure_erase(const char *c_name)
{
	(void) fprintf(stderr, "%s [-c] <ctl>[/<ns>]\n\n"
	    "  Secure-Erase one or all namespaces of the specified "
	    "NVMe controller.\n", c_name);
}

static void
optparse_secure_erase(nvme_process_arg_t *npa)
{
	int c;

	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":c")) != -1) {
		switch (c) {
		case 'c':
			npa->npa_cmdflags |= NVMEADM_O_SE_CRYPTO;
			break;

		case '?':
			errx(-1, "unknown option: -%c", optopt);

		case ':':
			errx(-1, "option -%c requires an argument", optopt);

		}
	}
}

static int
do_secure_erase(const nvme_process_arg_t *npa)
{
	unsigned long lbaf;
	uint8_t ses = NVME_FRMT_SES_USER;

	if (npa->npa_argc > 0) {
		errx(-1, "%s passed extraneous arguments starting with %s",
		    npa->npa_cmd->c_name, npa->npa_argv[0]);
	}

	if ((npa->npa_cmdflags & NVMEADM_O_SE_CRYPTO) != 0)
		ses = NVME_FRMT_SES_CRYPTO;

	lbaf = do_format_determine_lbaf(npa);
	return (do_format_common(npa, lbaf, ses));
}

static void
usage_attach_detach_bd(const char *c_name)
{
	(void) fprintf(stderr, "%s <ctl>[/<ns>]\n\n"
	    "  %c%s blkdev(4D) %s one or all namespaces of the "
	    "specified NVMe controller.\n",
	    c_name, toupper(c_name[0]), &c_name[1],
	    c_name[0] == 'd' ? "from" : "to");
}

/*
 * nvmeadm does not generate an error when trying to attach blkdev to something
 * that already has it attached. Swallow that here.
 */
static boolean_t
swallow_attach_bd_err(const nvme_process_arg_t *npa)
{
	return (nvme_ctrl_err(npa->npa_ctrl) == NVME_ERR_NS_BLKDEV_ATTACH);
}

static int
do_attach_bd(const nvme_process_arg_t *npa)
{
	int rv;
	nvme_ns_iter_t *iter = NULL;
	nvme_iter_t ret;
	const nvme_ns_disc_t *disc;

	if (npa->npa_ns != NULL) {
		if (!nvme_ns_bd_attach(npa->npa_ns) &&
		    !swallow_attach_bd_err(npa)) {
			nvmeadm_warn(npa, "faild to attach %s", npa->npa_name);
			return (-1);
		}
		return (0);
	}

	if (!nvme_ns_discover_init(npa->npa_ctrl, NVME_NS_DISC_F_NOT_IGNORED,
	    &iter))  {
		nvmeadm_fatal(npa, "failed to initialize namespace discovery "
		    "on %s", npa->npa_name);
	}

	rv = 0;
	while ((ret = nvme_ns_discover_step(iter, &disc)) == NVME_ITER_VALID) {
		nvme_ns_t *ns;
		uint32_t nsid;

		if (nvme_ns_disc_level(disc) == NVME_NS_DISC_F_BLKDEV)
			continue;

		nsid = nvme_ns_disc_nsid(disc);
		if (!nvme_ns_init(npa->npa_ctrl, nsid, &ns)) {
			nvmeadm_warn(npa, "failed to open namespace %s/%u "
			    "handle", npa->npa_name, nsid);
			rv = -1;
			continue;
		}

		/*
		 * nvmeadm has historically swallowed the case where you ask to
		 * attach an already attached namespace.
		 */
		if (!nvme_ns_bd_attach(ns) && !swallow_attach_bd_err(npa)) {
			nvmeadm_warn(npa, "failed to attach namespace "
			    "%s/%u", npa->npa_name, nsid);
			rv = -1;
		}
		nvme_ns_fini(ns);
	}

	nvme_ns_discover_fini(iter);
	if (ret == NVME_ITER_ERROR) {
		nvmeadm_warn(npa, "failed to iterate namespaces on %s",
		    npa->npa_name);
		rv = -1;
	}

	return (rv);
}

/*
 * nvmeadm does not generate an error when trying to attach blkdev to something
 * that already has it attached. Swallow that here.
 */
static boolean_t
swallow_detach_bd_err(const nvme_process_arg_t *npa)
{
	switch (nvme_ctrl_err(npa->npa_ctrl)) {
	case NVME_ERR_NS_UNALLOC:
	case NVME_ERR_NS_CTRL_NOT_ATTACHED:
	case NVME_ERR_NS_CTRL_ATTACHED:
		return (B_TRUE);
	default:
		return (B_FALSE);
	}
}

static int
do_detach_bd(const nvme_process_arg_t *npa)
{
	int rv;
	nvme_ns_iter_t *iter = NULL;
	nvme_iter_t ret;
	const nvme_ns_disc_t *disc;

	if (npa->npa_ns != NULL) {
		if (!nvme_ns_bd_detach(npa->npa_ns) &&
		    !swallow_detach_bd_err(npa)) {
			nvmeadm_warn(npa, "failed to detach %s", npa->npa_name);
			return (-1);
		}
		return (0);
	}

	if (!nvme_ns_discover_init(npa->npa_ctrl, NVME_NS_DISC_F_BLKDEV,
	    &iter))  {
		nvmeadm_fatal(npa, "failed to initialize namespace discovery "
		    "on %s", npa->npa_name);
	}

	rv = 0;
	while ((ret = nvme_ns_discover_step(iter, &disc)) == NVME_ITER_VALID) {
		nvme_ns_t *ns;
		uint32_t nsid = nvme_ns_disc_nsid(disc);

		if (!nvme_ns_init(npa->npa_ctrl, nsid, &ns)) {
			nvmeadm_warn(npa, "failed to open namespace %s/%u "
			    "handle", npa->npa_name, nsid);
			rv = -1;
			continue;
		}

		if (!nvme_ns_bd_detach(ns) && !swallow_detach_bd_err(npa)) {
			nvmeadm_warn(npa, "failed to detach namespace %s/%u",
			    npa->npa_name, nsid);
			rv = -1;
		}
		nvme_ns_fini(ns);
	}

	nvme_ns_discover_fini(iter);
	if (ret == NVME_ITER_ERROR) {
		nvmeadm_warn(npa, "failed to iterate namespaces on %s",
		    npa->npa_name);
		rv = -1;
	}

	return (rv);
}

static void
usage_firmware_load(const char *c_name)
{
	(void) fprintf(stderr, "%s <ctl> <image file> [<offset>]\n\n"
	    "  Load firmware <image file> to offset <offset>.\n"
	    "  The firmware needs to be committed to a slot using "
	    "\"nvmeadm commit-firmware\"\n  command.\n", c_name);
}

/*
 * Read exactly len bytes, or until eof.
 */
static size_t
read_block(const nvme_process_arg_t *npa, int fd, char *buf, size_t len)
{
	size_t remain;

	remain = len;
	while (remain > 0) {
		ssize_t bytes = read(fd, buf, remain);
		if (bytes == 0)
			break;

		if (bytes < 0) {
			if (errno == EINTR)
				continue;

			err(-1, "Error reading \"%s\"", npa->npa_argv[0]);
		}

		buf += (size_t)bytes;
		remain -= (size_t)bytes;
	}

	return (len - remain);
}

/*
 * Convert a string to a valid firmware upload offset (in bytes).
 */
static uint64_t
get_fw_offsetb(char *str)
{
	longlong_t offsetb;
	char *valend;

	errno = 0;
	offsetb = strtoll(str, &valend, 0);
	if (errno != 0 || *valend != '\0' || offsetb < 0 ||
	    offsetb > NVME_FW_OFFSETB_MAX)
		errx(-1, "Offset must be numeric and in the range of 0 to %llu",
		    NVME_FW_OFFSETB_MAX);

	if ((offsetb & NVME_DWORD_MASK) != 0)
		errx(-1, "Offset must be multiple of %d", NVME_DWORD_SIZE);

	return ((uint64_t)offsetb);
}

#define	FIRMWARE_READ_BLKSIZE	(64 * 1024)		/* 64K */

static int
do_firmware_load(const nvme_process_arg_t *npa)
{
	int fw_fd;
	uint64_t offset = 0;
	size_t size, len;
	char buf[FIRMWARE_READ_BLKSIZE];

	if (npa->npa_argc > 2)
		errx(-1, "%s passed extraneous arguments starting with %s",
		    npa->npa_cmd->c_name, npa->npa_argv[2]);

	if (npa->npa_argc == 0)
		errx(-1, "Requires firmware file name, and an "
		    "optional offset");

	if (npa->npa_ns != NULL)
		errx(-1, "Firmware loading not available on a per-namespace "
		    "basis");

	if (npa->npa_argc == 2)
		offset = get_fw_offsetb(npa->npa_argv[1]);

	fw_fd = open(npa->npa_argv[0], O_RDONLY);
	if (fw_fd < 0)
		errx(-1, "Failed to open \"%s\": %s", npa->npa_argv[0],
		    strerror(errno));

	size = 0;
	do {
		len = read_block(npa, fw_fd, buf, sizeof (buf));

		if (len == 0)
			break;

		if (!nvme_fw_load(npa->npa_ctrl, buf, len, offset)) {
			nvmeadm_fatal(npa, "failed to load firmware image "
			    "\"%s\" at offset %" PRIu64, npa->npa_argv[0],
			    offset);
		}

		offset += len;
		size += len;
	} while (len == sizeof (buf));

	(void) close(fw_fd);

	if (verbose)
		(void) printf("%zu bytes downloaded.\n", size);

	return (0);
}

/*
 * Common firmware commit for nvmeadm commit-firmware and activate-firmware.
 */
static void
nvmeadm_firmware_commit(const nvme_process_arg_t *npa, uint32_t slot,
    uint32_t act)
{
	nvme_fw_commit_req_t *req;

	if (!nvme_fw_commit_req_init(npa->npa_ctrl, &req)) {
		nvmeadm_fatal(npa, "failed to initialize firmware commit "
		    "request for %s", npa->npa_name);
	}

	if (!nvme_fw_commit_req_set_slot(req, slot) ||
	    !nvme_fw_commit_req_set_action(req, act)) {
		nvmeadm_fatal(npa, "failed to set firmware commit fields for "
		    "%s", npa->npa_name);
	}

	if (!nvme_fw_commit_req_exec(req)) {
		/*
		 * A number of command specific status values are informational
		 * and indicate that the operation was successful but that
		 * something else, such as a device reset, is still required
		 * before the new firmware is active.
		 * We distinguish those here and report them as a note rather
		 * than a fatal error.
		 */
		if (nvme_ctrl_err(npa->npa_ctrl) == NVME_ERR_CONTROLLER) {
			uint32_t sct, sc;

			nvme_ctrl_deverr(npa->npa_ctrl, &sct, &sc);
			if (sct == NVME_CQE_SCT_SPECIFIC && (
			    sc == NVME_CQE_SC_SPC_FW_RESET ||
			    sc == NVME_CQE_SC_SPC_FW_NSSR ||
			    sc == NVME_CQE_SC_SPC_FW_NEXT_RESET)) {
				fprintf(stderr,
				    "nvmeadm: commit successful but %s\n",
				    nvme_sctostr(npa->npa_ctrl, NVME_CSI_NVM,
				    sct, sc));
			} else {
				nvmeadm_fatal(npa, "failed to %s on %s",
				    npa->npa_cmd->c_name, npa->npa_name);
			}
		} else {
			nvmeadm_fatal(npa, "failed to %s on %s",
			    npa->npa_cmd->c_name, npa->npa_name);
		}
	}

	nvme_fw_commit_req_fini(req);
}

/*
 * Convert str to a valid firmware slot number.
 */
static uint32_t
get_slot_number(char *str)
{
	longlong_t slot;
	char *valend;

	errno = 0;
	slot = strtoll(str, &valend, 0);
	if (errno != 0 || *valend != '\0' ||
	    slot < NVME_FW_SLOT_MIN || slot > NVME_FW_SLOT_MAX)
		errx(-1, "Slot must be numeric and in the range of %u to %u",
		    NVME_FW_SLOT_MIN, NVME_FW_SLOT_MAX);

	return ((uint32_t)slot);
}

static void
usage_firmware_commit(const char *c_name)
{
	(void) fprintf(stderr, "%s <ctl> <slot>\n\n"
	    "  Commit previously downloaded firmware to slot <slot>.\n"
	    "  The firmware is only activated after a "
	    "\"nvmeadm activate-firmware\" command.\n", c_name);
}

static int
do_firmware_commit(const nvme_process_arg_t *npa)
{
	uint32_t slot;

	if (npa->npa_argc > 1)
		errx(-1, "%s passed extraneous arguments starting with %s",
		    npa->npa_cmd->c_name, npa->npa_argv[1]);

	if (npa->npa_argc == 0)
		errx(-1, "Firmware slot number is required");

	if (npa->npa_ns != NULL)
		errx(-1, "Firmware committing not available on a per-namespace "
		    "basis");

	slot = get_slot_number(npa->npa_argv[0]);

	if (slot == 1 && npa->npa_idctl->id_frmw.fw_readonly)
		errx(-1, "Cannot commit firmware to slot 1: slot is read-only");

	nvmeadm_firmware_commit(npa, slot, NVME_FWC_SAVE);

	if (verbose)
		(void) printf("Firmware committed to slot %u.\n", slot);

	return (0);
}

static void
usage_firmware_activate(const char *c_name)
{
	(void) fprintf(stderr, "%s <ctl> <slot>\n\n"
	    "  Activate firmware in slot <slot>.\n"
	    "  The firmware will be in use after the next system reset.\n",
	    c_name);
}

static int
do_firmware_activate(const nvme_process_arg_t *npa)
{
	uint32_t slot;

	if (npa->npa_argc > 1)
		errx(-1, "%s passed extraneous arguments starting with %s",
		    npa->npa_cmd->c_name, npa->npa_argv[1]);

	if (npa->npa_argc == 0)
		errx(-1, "Firmware slot number is required");

	if (npa->npa_ns != NULL)
		errx(-1, "Firmware activation not available on a per-namespace "
		    "basis");

	slot = get_slot_number(npa->npa_argv[0]);

	nvmeadm_firmware_commit(npa, slot, NVME_FWC_ACTIVATE);

	if (verbose)
		(void) printf("Slot %u successfully activated.\n", slot);

	return (0);
}
