| /* SPDX-License-Identifier: GPL-2.0-or-later */ |
| /* |
| * Copyright © IBM Corp. 2003 |
| * Copyright © SUSE Linux Products GmbH, 2006 |
| */ |
| |
| #include <ctype.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <getopt.h> |
| #include <signal.h> |
| #include <stdarg.h> |
| #include <stdbool.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include "alloc-util.h" |
| #include "build.h" |
| #include "fd-util.h" |
| #include "libudev-util.h" |
| #include "scsi_id.h" |
| #include "string-util.h" |
| #include "strxcpyx.h" |
| #include "udev-util.h" |
| |
| static const struct option options[] = { |
| { "device", required_argument, NULL, 'd' }, |
| { "config", required_argument, NULL, 'f' }, |
| { "page", required_argument, NULL, 'p' }, |
| { "blacklisted", no_argument, NULL, 'b' }, |
| { "whitelisted", no_argument, NULL, 'g' }, |
| { "replace-whitespace", no_argument, NULL, 'u' }, |
| { "sg-version", required_argument, NULL, 's' }, |
| { "verbose", no_argument, NULL, 'v' }, |
| { "version", no_argument, NULL, 'V' }, /* don't advertise -V */ |
| { "export", no_argument, NULL, 'x' }, |
| { "help", no_argument, NULL, 'h' }, |
| {} |
| }; |
| |
| static bool all_good = false; |
| static bool dev_specified = false; |
| static char config_file[MAX_PATH_LEN] = "/etc/scsi_id.config"; |
| static enum page_code default_page_code = PAGE_UNSPECIFIED; |
| static int sg_version = 4; |
| static bool reformat_serial = false; |
| static bool export = false; |
| static char vendor_str[64]; |
| static char model_str[64]; |
| static char vendor_enc_str[256]; |
| static char model_enc_str[256]; |
| static char revision_str[16]; |
| static char type_str[16]; |
| |
| static void set_type(const char *from, char *to, size_t len) { |
| int type_num; |
| char *eptr; |
| const char *type = "generic"; |
| |
| type_num = strtoul(from, &eptr, 0); |
| if (eptr != from) { |
| switch (type_num) { |
| case 0: |
| type = "disk"; |
| break; |
| case 1: |
| type = "tape"; |
| break; |
| case 4: |
| type = "optical"; |
| break; |
| case 5: |
| type = "cd"; |
| break; |
| case 7: |
| type = "optical"; |
| break; |
| case 0xe: |
| type = "disk"; |
| break; |
| case 0xf: |
| type = "optical"; |
| break; |
| default: |
| break; |
| } |
| } |
| strscpy(to, len, type); |
| } |
| |
| /* |
| * get_value: |
| * |
| * buf points to an '=' followed by a quoted string ("foo") or a string ending |
| * with a space or ','. |
| * |
| * Return a pointer to the NUL terminated string, returns NULL if no |
| * matches. |
| */ |
| static char *get_value(char **buffer) { |
| static const char *quote_string = "\"\n"; |
| static const char *comma_string = ",\n"; |
| char *val; |
| const char *end; |
| |
| if (**buffer == '"') { |
| /* |
| * skip leading quote, terminate when quote seen |
| */ |
| (*buffer)++; |
| end = quote_string; |
| } else |
| end = comma_string; |
| val = strsep(buffer, end); |
| if (val && end == quote_string) |
| /* |
| * skip trailing quote |
| */ |
| (*buffer)++; |
| |
| while (isspace(**buffer)) |
| (*buffer)++; |
| |
| return val; |
| } |
| |
| static int argc_count(char *opts) { |
| int i = 0; |
| while (*opts != '\0') |
| if (*opts++ == ' ') |
| i++; |
| return i; |
| } |
| |
| /* |
| * get_file_options: |
| * |
| * If vendor == NULL, find a line in the config file with only "OPTIONS="; |
| * if vendor and model are set find the first OPTIONS line in the config |
| * file that matches. Set argc and argv to match the OPTIONS string. |
| * |
| * vendor and model can end in '\n'. |
| */ |
| static int get_file_options(const char *vendor, const char *model, |
| int *argc, char ***newargv) { |
| _cleanup_free_ char *buffer = NULL; |
| _cleanup_fclose_ FILE *f; |
| char *buf; |
| char *str1; |
| char *vendor_in, *model_in, *options_in; /* read in from file */ |
| int lineno; |
| int c; |
| int retval = 0; |
| |
| f = fopen(config_file, "re"); |
| if (!f) { |
| if (errno == ENOENT) |
| return 1; |
| else { |
| log_error_errno(errno, "can't open %s: %m", config_file); |
| return -1; |
| } |
| } |
| |
| /* |
| * Allocate a buffer rather than put it on the stack so we can |
| * keep it around to parse any options (any allocated newargv |
| * points into this buffer for its strings). |
| */ |
| buffer = malloc(MAX_BUFFER_LEN); |
| if (!buffer) |
| return log_oom(); |
| |
| *newargv = NULL; |
| lineno = 0; |
| for (;;) { |
| vendor_in = model_in = options_in = NULL; |
| |
| buf = fgets(buffer, MAX_BUFFER_LEN, f); |
| if (!buf) |
| break; |
| lineno++; |
| if (buf[strlen(buffer) - 1] != '\n') { |
| log_error("Config file line %d too long", lineno); |
| break; |
| } |
| |
| while (isspace(*buf)) |
| buf++; |
| |
| /* blank or all whitespace line */ |
| if (*buf == '\0') |
| continue; |
| |
| /* comment line */ |
| if (*buf == '#') |
| continue; |
| |
| str1 = strsep(&buf, "="); |
| if (str1 && strcaseeq(str1, "VENDOR")) { |
| str1 = get_value(&buf); |
| if (!str1) { |
| retval = log_oom(); |
| break; |
| } |
| vendor_in = str1; |
| |
| str1 = strsep(&buf, "="); |
| if (str1 && strcaseeq(str1, "MODEL")) { |
| str1 = get_value(&buf); |
| if (!str1) { |
| retval = log_oom(); |
| break; |
| } |
| model_in = str1; |
| str1 = strsep(&buf, "="); |
| } |
| } |
| |
| if (str1 && strcaseeq(str1, "OPTIONS")) { |
| str1 = get_value(&buf); |
| if (!str1) { |
| retval = log_oom(); |
| break; |
| } |
| options_in = str1; |
| } |
| |
| /* |
| * Only allow: [vendor=foo[,model=bar]]options=stuff |
| */ |
| if (!options_in || (!vendor_in && model_in)) { |
| log_error("Error parsing config file line %d '%s'", lineno, buffer); |
| retval = -1; |
| break; |
| } |
| if (!vendor) { |
| if (!vendor_in) |
| break; |
| } else if (vendor_in && |
| startswith(vendor, vendor_in) && |
| (!model_in || startswith(model, model_in))) { |
| /* |
| * Matched vendor and optionally model. |
| * |
| * Note: a short vendor_in or model_in can |
| * give a partial match (that is FOO |
| * matches FOOBAR). |
| */ |
| break; |
| } |
| } |
| |
| if (retval == 0) { |
| if (vendor_in != NULL || model_in != NULL || |
| options_in != NULL) { |
| /* |
| * Something matched. Allocate newargv, and store |
| * values found in options_in. |
| */ |
| strcpy(buffer, options_in); |
| c = argc_count(buffer) + 2; |
| *newargv = calloc(c, sizeof(**newargv)); |
| if (!*newargv) |
| retval = log_oom(); |
| else { |
| *argc = c; |
| c = 0; |
| /* |
| * argv[0] at 0 is skipped by getopt, but |
| * store the buffer address there for |
| * later freeing |
| */ |
| (*newargv)[c] = buffer; |
| for (c = 1; c < *argc; c++) |
| (*newargv)[c] = strsep(&buffer, " \t"); |
| buffer = NULL; |
| } |
| } else { |
| /* No matches */ |
| retval = 1; |
| } |
| } |
| return retval; |
| } |
| |
| static void help(void) { |
| printf("Usage: %s [OPTION...] DEVICE\n\n" |
| "SCSI device identification.\n\n" |
| " -h --help Print this message\n" |
| " --version Print version of the program\n\n" |
| " -d --device= Device node for SG_IO commands\n" |
| " -f --config= Location of config file\n" |
| " -p --page=0x80|0x83|pre-spc3-83 SCSI page (0x80, 0x83, pre-spc3-83)\n" |
| " -s --sg-version=3|4 Use SGv3 or SGv4\n" |
| " -b --blacklisted Treat device as blacklisted\n" |
| " -g --whitelisted Treat device as whitelisted\n" |
| " -u --replace-whitespace Replace all whitespace by underscores\n" |
| " -v --verbose Verbose logging\n" |
| " -x --export Print values as environment keys\n" |
| , program_invocation_short_name); |
| |
| } |
| |
| static int set_options(int argc, char **argv, |
| char *maj_min_dev) { |
| int option; |
| |
| /* |
| * optind is a global extern used by getopt. Since we can call |
| * set_options twice (once for command line, and once for config |
| * file) we have to reset this back to 1. |
| */ |
| optind = 1; |
| while ((option = getopt_long(argc, argv, "d:f:gp:uvVxhbs:", options, NULL)) >= 0) |
| switch (option) { |
| case 'b': |
| all_good = false; |
| break; |
| |
| case 'd': |
| dev_specified = true; |
| strscpy(maj_min_dev, MAX_PATH_LEN, optarg); |
| break; |
| |
| case 'f': |
| strscpy(config_file, MAX_PATH_LEN, optarg); |
| break; |
| |
| case 'g': |
| all_good = true; |
| break; |
| |
| case 'h': |
| help(); |
| exit(EXIT_SUCCESS); |
| |
| case 'p': |
| if (streq(optarg, "0x80")) |
| default_page_code = PAGE_80; |
| else if (streq(optarg, "0x83")) |
| default_page_code = PAGE_83; |
| else if (streq(optarg, "pre-spc3-83")) |
| default_page_code = PAGE_83_PRE_SPC3; |
| else |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), |
| "Unknown page code '%s'", |
| optarg); |
| break; |
| |
| case 's': |
| sg_version = atoi(optarg); |
| if (sg_version < 3 || sg_version > 4) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), |
| "Unknown SG version '%s'", |
| optarg); |
| break; |
| |
| case 'u': |
| reformat_serial = true; |
| break; |
| |
| case 'v': |
| log_set_target(LOG_TARGET_CONSOLE); |
| log_set_max_level(LOG_DEBUG); |
| log_open(); |
| break; |
| |
| case 'V': |
| printf("%s\n", GIT_VERSION); |
| exit(EXIT_SUCCESS); |
| |
| case 'x': |
| export = true; |
| break; |
| |
| case '?': |
| return -1; |
| |
| default: |
| assert_not_reached("Unknown option"); |
| } |
| |
| if (optind < argc && !dev_specified) { |
| dev_specified = true; |
| strscpy(maj_min_dev, MAX_PATH_LEN, argv[optind]); |
| } |
| |
| return 0; |
| } |
| |
| static int per_dev_options(struct scsi_id_device *dev_scsi, int *good_bad, int *page_code) { |
| int retval; |
| int newargc; |
| char **newargv = NULL; |
| int option; |
| |
| *good_bad = all_good; |
| *page_code = default_page_code; |
| |
| retval = get_file_options(vendor_str, model_str, &newargc, &newargv); |
| |
| optind = 1; /* reset this global extern */ |
| while (retval == 0) { |
| option = getopt_long(newargc, newargv, "bgp:", options, NULL); |
| if (option == -1) |
| break; |
| |
| switch (option) { |
| case 'b': |
| *good_bad = 0; |
| break; |
| |
| case 'g': |
| *good_bad = 1; |
| break; |
| |
| case 'p': |
| if (streq(optarg, "0x80")) { |
| *page_code = PAGE_80; |
| } else if (streq(optarg, "0x83")) { |
| *page_code = PAGE_83; |
| } else if (streq(optarg, "pre-spc3-83")) { |
| *page_code = PAGE_83_PRE_SPC3; |
| } else { |
| log_error("Unknown page code '%s'", optarg); |
| retval = -1; |
| } |
| break; |
| |
| default: |
| log_error("Unknown or bad option '%c' (0x%x)", option, option); |
| retval = -1; |
| break; |
| } |
| } |
| |
| if (newargv) { |
| free(newargv[0]); |
| free(newargv); |
| } |
| return retval; |
| } |
| |
| static int set_inq_values(struct scsi_id_device *dev_scsi, const char *path) { |
| int retval; |
| |
| dev_scsi->use_sg = sg_version; |
| |
| retval = scsi_std_inquiry(dev_scsi, path); |
| if (retval) |
| return retval; |
| |
| udev_util_encode_string(dev_scsi->vendor, vendor_enc_str, sizeof(vendor_enc_str)); |
| udev_util_encode_string(dev_scsi->model, model_enc_str, sizeof(model_enc_str)); |
| |
| util_replace_whitespace(dev_scsi->vendor, vendor_str, sizeof(vendor_str)-1); |
| util_replace_chars(vendor_str, NULL); |
| util_replace_whitespace(dev_scsi->model, model_str, sizeof(model_str)-1); |
| util_replace_chars(model_str, NULL); |
| set_type(dev_scsi->type, type_str, sizeof(type_str)); |
| util_replace_whitespace(dev_scsi->revision, revision_str, sizeof(revision_str)-1); |
| util_replace_chars(revision_str, NULL); |
| return 0; |
| } |
| |
| /* |
| * scsi_id: try to get an id, if one is found, printf it to stdout. |
| * returns a value passed to exit() - 0 if printed an id, else 1. |
| */ |
| static int scsi_id(char *maj_min_dev) { |
| struct scsi_id_device dev_scsi = {}; |
| int good_dev; |
| int page_code; |
| int retval = 0; |
| |
| if (set_inq_values(&dev_scsi, maj_min_dev) < 0) { |
| retval = 1; |
| goto out; |
| } |
| |
| /* get per device (vendor + model) options from the config file */ |
| per_dev_options(&dev_scsi, &good_dev, &page_code); |
| if (!good_dev) { |
| retval = 1; |
| goto out; |
| } |
| |
| /* read serial number from mode pages (no values for optical drives) */ |
| scsi_get_serial(&dev_scsi, maj_min_dev, page_code, MAX_SERIAL_LEN); |
| |
| if (export) { |
| char serial_str[MAX_SERIAL_LEN]; |
| |
| printf("ID_SCSI=1\n"); |
| printf("ID_VENDOR=%s\n", vendor_str); |
| printf("ID_VENDOR_ENC=%s\n", vendor_enc_str); |
| printf("ID_MODEL=%s\n", model_str); |
| printf("ID_MODEL_ENC=%s\n", model_enc_str); |
| printf("ID_REVISION=%s\n", revision_str); |
| printf("ID_TYPE=%s\n", type_str); |
| if (dev_scsi.serial[0] != '\0') { |
| util_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1); |
| util_replace_chars(serial_str, NULL); |
| printf("ID_SERIAL=%s\n", serial_str); |
| util_replace_whitespace(dev_scsi.serial_short, serial_str, sizeof(serial_str)-1); |
| util_replace_chars(serial_str, NULL); |
| printf("ID_SERIAL_SHORT=%s\n", serial_str); |
| } |
| if (dev_scsi.wwn[0] != '\0') { |
| printf("ID_WWN=0x%s\n", dev_scsi.wwn); |
| if (dev_scsi.wwn_vendor_extension[0] != '\0') { |
| printf("ID_WWN_VENDOR_EXTENSION=0x%s\n", dev_scsi.wwn_vendor_extension); |
| printf("ID_WWN_WITH_EXTENSION=0x%s%s\n", dev_scsi.wwn, dev_scsi.wwn_vendor_extension); |
| } else |
| printf("ID_WWN_WITH_EXTENSION=0x%s\n", dev_scsi.wwn); |
| } |
| if (dev_scsi.tgpt_group[0] != '\0') |
| printf("ID_TARGET_PORT=%s\n", dev_scsi.tgpt_group); |
| if (dev_scsi.unit_serial_number[0] != '\0') |
| printf("ID_SCSI_SERIAL=%s\n", dev_scsi.unit_serial_number); |
| goto out; |
| } |
| |
| if (dev_scsi.serial[0] == '\0') { |
| retval = 1; |
| goto out; |
| } |
| |
| if (reformat_serial) { |
| char serial_str[MAX_SERIAL_LEN]; |
| |
| util_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1); |
| util_replace_chars(serial_str, NULL); |
| printf("%s\n", serial_str); |
| goto out; |
| } |
| |
| printf("%s\n", dev_scsi.serial); |
| out: |
| return retval; |
| } |
| |
| int main(int argc, char **argv) { |
| int retval = 0; |
| char maj_min_dev[MAX_PATH_LEN]; |
| int newargc; |
| char **newargv = NULL; |
| |
| log_set_target(LOG_TARGET_AUTO); |
| udev_parse_config(); |
| log_parse_environment(); |
| log_open(); |
| |
| /* |
| * Get config file options. |
| */ |
| retval = get_file_options(NULL, NULL, &newargc, &newargv); |
| if (retval < 0) { |
| retval = 1; |
| goto exit; |
| } |
| if (retval == 0) { |
| assert(newargv); |
| |
| if (set_options(newargc, newargv, maj_min_dev) < 0) { |
| retval = 2; |
| goto exit; |
| } |
| } |
| |
| /* |
| * Get command line options (overriding any config file settings). |
| */ |
| if (set_options(argc, argv, maj_min_dev) < 0) |
| exit(EXIT_FAILURE); |
| |
| if (!dev_specified) { |
| log_error("No device specified."); |
| retval = 1; |
| goto exit; |
| } |
| |
| retval = scsi_id(maj_min_dev); |
| |
| exit: |
| if (newargv) { |
| free(newargv[0]); |
| free(newargv); |
| } |
| log_close(); |
| return retval; |
| } |