root/usr.sbin/zonectl/zonectl.c
/*-
 * Copyright (c) 2015, 2016 Spectra Logic Corporation
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions, and the following disclaimer,
 *    without modification.
 * 2. Redistributions in binary form must reproduce at minimum a disclaimer
 *    substantially similar to the "NO WARRANTY" disclaimer below
 *    ("Disclaimer") and any redistribution must be conditioned upon
 *    including a substantially similar Disclaimer requirement for further
 *    binary redistribution.
 *
 * NO WARRANTY
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDERS OR CONTRIBUTORS BE LIABLE FOR SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES.
 *
 * Authors: Ken Merry           (Spectra Logic Corporation)
 */

#include <sys/cdefs.h>
#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/stdint.h>
#include <sys/endian.h>
#include <sys/sbuf.h>
#include <sys/queue.h>
#include <sys/disk.h>
#include <sys/disk_zone.h>
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>
#include <err.h>
#include <locale.h>

#include <cam/cam.h>
#include <cam/cam_debug.h>
#include <cam/cam_ccb.h>
#include <cam/scsi/scsi_all.h>

static struct scsi_nv zone_cmd_map[] = {
        { "rz", DISK_ZONE_REPORT_ZONES },
        { "reportzones", DISK_ZONE_REPORT_ZONES },
        { "close", DISK_ZONE_CLOSE },
        { "finish", DISK_ZONE_FINISH },
        { "open", DISK_ZONE_OPEN },
        { "rwp", DISK_ZONE_RWP },
        { "params", DISK_ZONE_GET_PARAMS }
};

static struct scsi_nv zone_rep_opts[] = {
        { "all", DISK_ZONE_REP_ALL },
        { "empty", DISK_ZONE_REP_EMPTY },
        { "imp_open", DISK_ZONE_REP_IMP_OPEN },
        { "exp_open", DISK_ZONE_REP_EXP_OPEN },
        { "closed", DISK_ZONE_REP_CLOSED },
        { "full", DISK_ZONE_REP_FULL },
        { "readonly", DISK_ZONE_REP_READONLY },
        { "ro", DISK_ZONE_REP_READONLY },
        { "offline", DISK_ZONE_REP_OFFLINE },
        { "reset", DISK_ZONE_REP_RWP },
        { "rwp", DISK_ZONE_REP_RWP },
        { "nonseq", DISK_ZONE_REP_NON_SEQ },
        { "nonwp", DISK_ZONE_REP_NON_WP }
};


typedef enum {
        ZONE_OF_NORMAL  = 0x00,
        ZONE_OF_SUMMARY = 0x01,
        ZONE_OF_SCRIPT  = 0x02
} zone_output_flags;

static struct scsi_nv zone_print_opts[] = {
        { "normal", ZONE_OF_NORMAL },
        { "summary", ZONE_OF_SUMMARY },
        { "script", ZONE_OF_SCRIPT }
};

static struct scsi_nv zone_cmd_desc_table[] = {
        {"Report Zones", DISK_ZONE_RZ_SUP },
        {"Open", DISK_ZONE_OPEN_SUP },
        {"Close", DISK_ZONE_CLOSE_SUP },
        {"Finish", DISK_ZONE_FINISH_SUP },
        {"Reset Write Pointer", DISK_ZONE_RWP_SUP }
};

typedef enum {
        ZONE_PRINT_OK,
        ZONE_PRINT_MORE_DATA,
        ZONE_PRINT_ERROR
} zone_print_status;

typedef enum {
        ZONE_FW_START,
        ZONE_FW_LEN,
        ZONE_FW_WP,
        ZONE_FW_TYPE,
        ZONE_FW_COND,
        ZONE_FW_SEQ,
        ZONE_FW_RESET,
        ZONE_NUM_FIELDS
} zone_field_widths;


static void usage(int error);
static void zonectl_print_params(struct disk_zone_disk_params *params);
zone_print_status zonectl_print_rz(struct disk_zone_report *report,
                                   zone_output_flags out_flags, int first_pass);

static void
usage(int error)
{
        fprintf(error ? stderr : stdout,
"usage: zonectl <-d dev> <-c cmd> [-a][-o rep_opts] [-l lba][-P print_opts]\n"
        );
}

static void
zonectl_print_params(struct disk_zone_disk_params *params)
{
        unsigned int i;
        int first;

        printf("Zone Mode: ");
        switch (params->zone_mode) {
        case DISK_ZONE_MODE_NONE:
                printf("None");
                break;
        case DISK_ZONE_MODE_HOST_AWARE:
                printf("Host Aware");
                break;
        case DISK_ZONE_MODE_DRIVE_MANAGED:
                printf("Drive Managed");
                break;
        case DISK_ZONE_MODE_HOST_MANAGED:
                printf("Host Managed");
                break;
        default:
                printf("Unknown mode %#x", params->zone_mode);
                break;
        }
        printf("\n");

        first = 1;
        printf("Command support: ");
        for (i = 0; i < sizeof(zone_cmd_desc_table) /
             sizeof(zone_cmd_desc_table[0]); i++) {
                if (params->flags & zone_cmd_desc_table[i].value) {
                        if (first == 0)
                                printf(", ");
                        else
                                first = 0;
                        printf("%s", zone_cmd_desc_table[i].name);
                }
        }
        if (first == 1)
                printf("None");
        printf("\n");

        printf("Unrestricted Read in Sequential Write Required Zone "
            "(URSWRZ): %s\n", (params->flags & DISK_ZONE_DISK_URSWRZ) ?
            "Yes" : "No");

        printf("Optimal Number of Open Sequential Write Preferred Zones: ");
        if (params->flags & DISK_ZONE_OPT_SEQ_SET)
                if (params->optimal_seq_zones == SVPD_ZBDC_OPT_SEQ_NR)
                        printf("Not Reported");
                else
                        printf("%ju", (uintmax_t)params->optimal_seq_zones);
        else
                printf("Not Set");
        printf("\n");


        printf("Optimal Number of Non-Sequentially Written Sequential Write "
           "Preferred Zones: ");
        if (params->flags & DISK_ZONE_OPT_NONSEQ_SET)
                if (params->optimal_nonseq_zones == SVPD_ZBDC_OPT_NONSEQ_NR)
                        printf("Not Reported");
                else
                        printf("%ju",(uintmax_t)params->optimal_nonseq_zones);
        else
                printf("Not Set");
        printf("\n");

        printf("Maximum Number of Open Sequential Write Required Zones: ");
        if (params->flags & DISK_ZONE_MAX_SEQ_SET)
                if (params->max_seq_zones == SVPD_ZBDC_MAX_SEQ_UNLIMITED)
                        printf("Unlimited");
                else
                        printf("%ju", (uintmax_t)params->max_seq_zones);
        else
                printf("Not Set");
        printf("\n");
}

zone_print_status
zonectl_print_rz(struct disk_zone_report *report, zone_output_flags out_flags,
                 int first_pass)
{
        zone_print_status status = ZONE_PRINT_OK;
        struct disk_zone_rep_header *header = &report->header;
        int field_widths[ZONE_NUM_FIELDS];
        struct disk_zone_rep_entry *entry;
        uint64_t next_lba = 0;
        char tmpstr[80];
        char word_sep;
        uint32_t i;

        field_widths[ZONE_FW_START] = 11;
        field_widths[ZONE_FW_LEN] = 6;
        field_widths[ZONE_FW_WP] = 11;
        field_widths[ZONE_FW_TYPE] = 13;
        field_widths[ZONE_FW_COND] = 13;
        field_widths[ZONE_FW_SEQ] = 14;
        field_widths[ZONE_FW_RESET] = 16;

        if ((report->entries_available - report->entries_filled) > 0)
                status = ZONE_PRINT_MORE_DATA;

        if (out_flags == ZONE_OF_SCRIPT)
                word_sep = '_';
        else
                word_sep = ' ';

        if ((out_flags != ZONE_OF_SCRIPT)
         && (first_pass != 0)) {
                printf("%u zones, Maximum LBA %#jx (%ju)\n",
                    report->entries_available,
                    (uintmax_t)header->maximum_lba,
                    (uintmax_t)header->maximum_lba);

                switch (header->same) {
                case DISK_ZONE_SAME_ALL_DIFFERENT:
                        printf("Zone lengths and types may vary\n");
                        break;
                case DISK_ZONE_SAME_ALL_SAME:
                        printf("Zone lengths and types are all the same\n");
                        break;
                case DISK_ZONE_SAME_LAST_DIFFERENT:
                        printf("Zone types are the same, last zone length "
                            "differs\n");
                        break;
                case DISK_ZONE_SAME_TYPES_DIFFERENT:
                        printf("Zone lengths are the same, types vary\n");
                        break;
                default:
                        printf("Unknown SAME field value %#x\n",header->same);
                        break;
                }
        }
        if (out_flags == ZONE_OF_SUMMARY) {
                status = ZONE_PRINT_OK;
                goto bailout;
        }

        if ((out_flags == ZONE_OF_NORMAL)
         && (first_pass != 0)) {
                printf("%*s  %*s  %*s  %*s  %*s  %*s  %*s\n",
                    field_widths[ZONE_FW_START], "Start LBA",
                    field_widths[ZONE_FW_LEN], "Length",
                    field_widths[ZONE_FW_WP], "WP LBA",
                    field_widths[ZONE_FW_TYPE], "Zone Type",
                    field_widths[ZONE_FW_COND], "Condition",
                    field_widths[ZONE_FW_SEQ], "Sequential",
                    field_widths[ZONE_FW_RESET], "Reset");
        }

        for (i = 0; i < report->entries_filled; i++) {
                entry = &report->entries[i];

                printf("%#*jx, %*ju, %#*jx, ", field_widths[ZONE_FW_START],
                    (uintmax_t)entry->zone_start_lba,
                    field_widths[ZONE_FW_LEN],
                    (uintmax_t)entry->zone_length, field_widths[ZONE_FW_WP],
                    (uintmax_t)entry->write_pointer_lba);

                switch (entry->zone_type) {
                case DISK_ZONE_TYPE_CONVENTIONAL:
                        snprintf(tmpstr, sizeof(tmpstr), "Conventional");
                        break;
                case DISK_ZONE_TYPE_SEQ_PREFERRED:
                case DISK_ZONE_TYPE_SEQ_REQUIRED:
                        snprintf(tmpstr, sizeof(tmpstr), "Seq%c%s",
                            word_sep, (entry->zone_type ==
                            DISK_ZONE_TYPE_SEQ_PREFERRED) ? "Preferred" :
                            "Required");
                        break;
                default:
                        snprintf(tmpstr, sizeof(tmpstr), "Zone%ctype%c%#x",
                            word_sep, word_sep, entry->zone_type);
                        break;
                }
                printf("%*s, ", field_widths[ZONE_FW_TYPE], tmpstr);

                switch (entry->zone_condition) {
                case DISK_ZONE_COND_NOT_WP:
                        snprintf(tmpstr, sizeof(tmpstr), "NWP");
                        break;
                case DISK_ZONE_COND_EMPTY:
                        snprintf(tmpstr, sizeof(tmpstr), "Empty");
                        break;
                case DISK_ZONE_COND_IMPLICIT_OPEN:
                        snprintf(tmpstr, sizeof(tmpstr), "Implicit%cOpen",
                            word_sep);
                        break;
                case DISK_ZONE_COND_EXPLICIT_OPEN:
                        snprintf(tmpstr, sizeof(tmpstr), "Explicit%cOpen",
                            word_sep);
                        break;
                case DISK_ZONE_COND_CLOSED:
                        snprintf(tmpstr, sizeof(tmpstr), "Closed");
                        break;
                case DISK_ZONE_COND_READONLY:
                        snprintf(tmpstr, sizeof(tmpstr), "Readonly");
                        break;
                case DISK_ZONE_COND_FULL:
                        snprintf(tmpstr, sizeof(tmpstr), "Full");
                        break;
                case DISK_ZONE_COND_OFFLINE:
                        snprintf(tmpstr, sizeof(tmpstr), "Offline");
                        break;
                default:
                        snprintf(tmpstr, sizeof(tmpstr), "%#x",
                            entry->zone_condition);
                        break;
                }

                printf("%*s, ", field_widths[ZONE_FW_COND], tmpstr);

                if (entry->zone_flags & DISK_ZONE_FLAG_NON_SEQ)
                        snprintf(tmpstr, sizeof(tmpstr), "Non%cSequential",
                            word_sep);
                else
                        snprintf(tmpstr, sizeof(tmpstr), "Sequential");

                printf("%*s, ", field_widths[ZONE_FW_SEQ], tmpstr);

                if (entry->zone_flags & DISK_ZONE_FLAG_RESET)
                        snprintf(tmpstr, sizeof(tmpstr), "Reset%cNeeded",
                            word_sep);
                else
                        snprintf(tmpstr, sizeof(tmpstr), "No%cReset%cNeeded",
                            word_sep, word_sep);

                printf("%*s\n", field_widths[ZONE_FW_RESET], tmpstr);

                next_lba = entry->zone_start_lba + entry->zone_length;
        }
bailout:
        report->starting_id = next_lba;

        return (status);
}

int
main(int argc, char **argv)
{
        int c;
        int all_zones = 0;
        int error = 0;
        int action = -1, rep_option = -1;
        int fd = -1;
        uint64_t lba = 0;
        zone_output_flags out_flags = ZONE_OF_NORMAL;
        char *filename = NULL;
        struct disk_zone_args zone_args;
        struct disk_zone_rep_entry *entries = NULL;
        uint32_t num_entries = 16384;
        zone_print_status zp_status;
        int first_pass = 1;
        size_t entry_alloc_size;
        int open_flags = O_RDONLY;

        while ((c = getopt(argc, argv, "ac:d:hl:o:P:?")) != -1) {
                switch (c) {
                case 'a':
                        all_zones = 1;
                        break;
                case 'c': {
                        scsi_nv_status status;
                        int entry_num;

                        status = scsi_get_nv(zone_cmd_map,
                            nitems(zone_cmd_map),
                            optarg, &entry_num, SCSI_NV_FLAG_IG_CASE);
                        if (status == SCSI_NV_FOUND)
                                action = zone_cmd_map[entry_num].value;
                        else {
                                warnx("%s: %s: %s option %s", __func__,
                                    (status == SCSI_NV_AMBIGUOUS) ?
                                    "ambiguous" : "invalid", "zone command",
                                    optarg);
                                error = 1;
                                goto bailout;
                        }
                        break;
                }
                case 'd':
                        filename = strdup(optarg);
                        if (filename == NULL)
                                err(1, "Unable to allocate memory for "
                                    "filename");
                        break;
                case 'l': {
                        char *endptr;

                        lba = strtoull(optarg, &endptr, 0);
                        if (*endptr != '\0') {
                                warnx("%s: invalid lba argument %s", __func__,
                                    optarg);
                                error = 1;
                                goto bailout;
                        }
                        break;
                }
                case 'o': {
                        scsi_nv_status status;
                        int entry_num;

                        status = scsi_get_nv(zone_rep_opts,
                            (sizeof(zone_rep_opts) /
                            sizeof(zone_rep_opts[0])),
                            optarg, &entry_num, SCSI_NV_FLAG_IG_CASE);
                        if (status == SCSI_NV_FOUND)
                                rep_option = zone_rep_opts[entry_num].value;
                        else {
                                warnx("%s: %s: %s option %s", __func__,
                                    (status == SCSI_NV_AMBIGUOUS) ?
                                    "ambiguous" : "invalid", "report zones",
                                    optarg);
                                error = 1;
                                goto bailout;
                        }
                        break;
                }
                case 'P': {
                        scsi_nv_status status;
                        int entry_num;

                        status = scsi_get_nv(zone_print_opts,
                            (sizeof(zone_print_opts) /
                            sizeof(zone_print_opts[0])), optarg, &entry_num,
                            SCSI_NV_FLAG_IG_CASE);
                        if (status == SCSI_NV_FOUND)
                                out_flags = zone_print_opts[entry_num].value;
                        else {
                                warnx("%s: %s: %s option %s", __func__,
                                    (status == SCSI_NV_AMBIGUOUS) ?
                                    "ambiguous" : "invalid", "print",
                                    optarg);
                                error = 1;
                                goto bailout;
                        }
                        break;
                }
                default:
                        error = 1;
                case 'h': /*FALLTHROUGH*/
                        usage(error);
                        goto bailout;
                        break; /*NOTREACHED*/
                }
        }

        if (filename == NULL) {
                warnx("You must specify a device with -d");
                error = 1;
        }
        if (action == -1) {
                warnx("You must specify an action with -c");
                error = 1;
        }

        if (error != 0) {
                usage(error);
                goto bailout;
        }

        bzero(&zone_args, sizeof(zone_args));

        zone_args.zone_cmd = action;

        switch (action) {
        case DISK_ZONE_OPEN:
        case DISK_ZONE_CLOSE:
        case DISK_ZONE_FINISH:
        case DISK_ZONE_RWP:
                open_flags = O_RDWR;
                zone_args.zone_params.rwp.id = lba;
                if (all_zones != 0)
                        zone_args.zone_params.rwp.flags |=
                            DISK_ZONE_RWP_FLAG_ALL;
                break;
        case DISK_ZONE_REPORT_ZONES: {
                entry_alloc_size = num_entries *
                    sizeof(struct disk_zone_rep_entry);
                entries = malloc(entry_alloc_size);
                if (entries == NULL) {
                        warn("Could not allocate %zu bytes",
                            entry_alloc_size);
                        error = 1;
                        goto bailout;
                }
                zone_args.zone_params.report.entries_allocated = num_entries;
                zone_args.zone_params.report.entries = entries;
                zone_args.zone_params.report.starting_id = lba;
                if (rep_option != -1)
                        zone_args.zone_params.report.rep_options = rep_option;
                break;
        }
        case DISK_ZONE_GET_PARAMS:
                break;
        default:
                warnx("Unknown action %d", action);
                error = 1;
                goto bailout;
                break; /*NOTREACHED*/
        }

        fd = open(filename, open_flags);
        if (fd == -1) {
                warn("Unable to open device %s", filename);
                error = 1;
                goto bailout;
        }
next_chunk:
        error = ioctl(fd, DIOCZONECMD, &zone_args);
        if (error == -1) {
                warn("DIOCZONECMD ioctl failed");
                error = 1;
                goto bailout;
        }

        switch (action) {
        case DISK_ZONE_OPEN:
        case DISK_ZONE_CLOSE:
        case DISK_ZONE_FINISH:
        case DISK_ZONE_RWP:
                break;
        case DISK_ZONE_REPORT_ZONES:
                zp_status = zonectl_print_rz(&zone_args.zone_params.report,
                    out_flags, first_pass);
                if (zp_status == ZONE_PRINT_MORE_DATA) {
                        first_pass = 0;
                        bzero(entries, entry_alloc_size);
                        zone_args.zone_params.report.entries_filled = 0;
                        goto next_chunk;
                } else if (zp_status == ZONE_PRINT_ERROR)
                        error = 1;
                break;
        case DISK_ZONE_GET_PARAMS:
                zonectl_print_params(&zone_args.zone_params.disk_params);
                break;
        default:
                warnx("Unknown action %d", action);
                error = 1;
                goto bailout;
                break; /*NOTREACHED*/
        }
bailout:
        free(entries);

        if (fd != -1)
                close(fd);
        exit (error);
}