Files
s390-tools/zdev/src/firmware.c
Peter Oberparleiter fe68ec513d zdev: Add support for handling auto-configuration data
Auto-configuration is the name of a new configuration target that is
supported by chzdev and lszdev besides the existing active and
persistent configuration targets. Directives created in this new
configuration are stored as udev rules in the /run/udev/rules.d
directory.

Auto-configuration directives are only in effect if there are no
directives for the same device in the user-provided persistent
configuration. This allows users to override auto-configuration
directives if necessary.

Due to the volatile nature of the /run directory, auto-configuration
directives are cleared on reboot. Therefore mechanisms that generate
auto-configuration directives must recreate them on every boot.

The lszdev tool displays auto-configuration data both in list view
as well as in detail view. Users can specify the new option --auto-conf
to only show data from this configuration target.

Mechanisms that generate automated configuration directives can use
chzdev together with the --auto-conf option to create the corresponding
udev rules.

Note: This change does not include a mechanism that generates
      auto-configuration directives.

Signed-off-by: Peter Oberparleiter <oberpar@linux.vnet.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2018-06-08 15:03:30 +02:00

683 lines
17 KiB
C

/*
* zdev - Modify and display the persistent configuration of devices
*
* Copyright IBM Corp. 2017
*
* s390-tools is free software; you can redistribute it and/or modify
* it under the terms of the MIT license. See LICENSE for details.
*/
#include <err.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "attrib.h"
#include "ccw.h"
#include "ccwgroup.h"
#include "dasd.h"
#include "device.h"
#include "export.h"
#include "firmware.h"
#include "misc.h"
#include "qeth.h"
#include "subtype.h"
#include "zfcp_host.h"
#include "zfcp_lun.h"
/* In-memory firmware file representation. */
struct fw_file {
const char *name;
char *buffer;
size_t size;
char *last_access;
size_t last_size;
};
/* Record access to fields of the buffered file for use in warning messages. */
#define fwacc(f, x) ((f)->last_access = (char *) &(x), \
(f)->last_size = sizeof(x), x)
/*
* Firmware file format definitions.
*/
/* Firmware file header. */
struct fw_filehdr {
uint32_t magic;
uint16_t ver;
uint16_t hdr_len;
uint32_t file_len;
uint32_t seq;
uint32_t zeroes;
uint16_t de_count;
char unused[10];
} __packed;
#define FW_HDR_MAGIC 0x7a646576 /* ASCII "zdev" */
#define FW_HDR_VER_Z14 0x0000
/* I/O device ID. */
struct fw_iodevid {
uint8_t cssid;
uint8_t ssid;
uint16_t devno;
} __packed;
#define FW_IODEVID_FLAG_MCSS 0x01
/* Device setting. */
struct fw_setting {
uint16_t len;
uint8_t key_type;
uint8_t key_len;
uint8_t val_type;
uint8_t val_len;
char data[];
} __packed;
#define FW_SETTING_KEYTYPE_ASCII 0x00
#define FW_SETTING_VALTYPE_ASCII 0x00
#define FW_SETTING_VALTYPE_UINT 0x01
/* Device settings list. */
struct fw_setlist {
uint16_t len;
char data[];
} __packed;
/* Device entry header. */
struct fw_dehdr {
uint16_t type;
uint16_t len;
uint32_t seq;
} __packed;
#define FW_DE_HDR_TYPE_DASD 0x0001
#define FW_DE_HDR_TYPE_ZFCP_HOST 0x0002
#define FW_DE_HDR_TYPE_ZFCP_LUN 0x0003
#define FW_DE_HDR_TYPE_QETH 0x0004
/* DASD device entry. */
struct fw_dasd {
struct fw_dehdr hdr;
uint8_t id_flags;
struct fw_iodevid id;
char settings[];
} __packed;
/* zFCP host device entry. */
struct fw_zfcp_host {
struct fw_dehdr hdr;
uint8_t id_flags;
struct fw_iodevid id;
char settings[];
} __packed;
/* zFCP LUN device entry. */
struct fw_zfcp_lun {
struct fw_dehdr hdr;
uint8_t id_flags;
struct fw_iodevid id;
uint64_t wwpn;
uint64_t fcp_lun;
char settings[];
} __packed;
/* QETH device entry. */
struct fw_qeth {
struct fw_dehdr hdr;
uint8_t id_flags;
struct fw_iodevid read_id;
struct fw_iodevid write_id;
struct fw_iodevid data_id;
char settings[];
} __packed;
/* Emit a warning that refers to a position in a firmware file. */
static void fwwarn(struct fw_file *f, const char *fmt, ...)
{
va_list args;
off_t start = (off_t) (f->last_access - f->buffer),
end = start + f->last_size - 1;
fprintf(stderr, "%s: ", f->name);
if (start == end)
fprintf(stderr, "Byte 0x%zx: ", start);
else
fprintf(stderr, "Bytes 0x%zx-0x%zx: ", start, end);
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fprintf(stderr, "\n");
}
/* Basic file format header sanity check. */
static bool check_header(struct fw_file *f, struct fw_filehdr *hdr)
{
if (fwacc(f, hdr->magic) != FW_HDR_MAGIC)
fwwarn(f, "Invalid file magic (0x%08x)", hdr->magic);
else if (fwacc(f, hdr->ver) != FW_HDR_VER_Z14)
fwwarn(f, "Unsupported file version (0x%04x)", hdr->ver);
else
return true;
return false;
}
#define READ_RETRY 3
/* Read a firmware configuration file. */
static exit_code_t read_fw(struct fw_file *file, FILE *fd, const char *filename)
{
struct fw_file f = { NULL };
struct fw_filehdr *hdr;
char *buffer, *buffer2;
size_t size, size2;
int retry;
exit_code_t rc = EXIT_OK;
for (retry = 0; retry < READ_RETRY; retry++) {
/* Read complete file once */
rc = misc_read_fd(fd, (void **) &buffer, &size);
if (rc) {
warn("%s: Could not read file", filename);
return rc;
}
if (!buffer) {
/* Empty file - skip silently as this is the default
* on machines without firmware support. */
return rc;
}
/* Re-read complete file to detect in-flight modifications. */
if (fseek(fd, 0, SEEK_SET) == -1) {
/* Could be a pipe, socket, or FIFO - accept v1. */
break;
}
rc = misc_read_fd(fd, (void **) &buffer2, &size2);
if (rc || !buffer2) {
/* Could not get second version - accept v1. */
break;
}
if (size == size2 && memcmp(buffer, buffer2, size) == 0) {
/* No change */
free(buffer2);
break;
}
free(buffer);
free(buffer2);
}
if (retry >= READ_RETRY) {
warnx("%s: File changed %d times while reading - aborting",
filename, retry);
return EXIT_RUNTIME_ERROR;
}
/* Perform basic checks */
f.name = filename;
f.buffer = buffer;
f.size = size;
hdr = (void *) buffer;
if (!check_header(&f, hdr)) {
free(buffer);
return EXIT_FORMAT_ERROR;
}
if (fwacc(&f, hdr->file_len) > size) {
fwwarn(&f, "File length mismatch (expect %zu) - adjusting",
size);
hdr->file_len = size;
}
*file = f;
return EXIT_OK;
}
/* Return textual representation of a device entry type. */
static const char *type_to_str(uint16_t type)
{
switch (type) {
case FW_DE_HDR_TYPE_DASD:
return "dasd";
case FW_DE_HDR_TYPE_ZFCP_HOST:
return "zfcp-host";
case FW_DE_HDR_TYPE_ZFCP_LUN:
return "zfcp-lun";
case FW_DE_HDR_TYPE_QETH:
return "qeth";
default:
return "<unknown>";
}
}
/* Convert a binary format device setting of the specified length to integer. */
static unsigned long parse_value(char *data, uint8_t len)
{
switch (len) {
case 1:
return (unsigned long) *((uint8_t *) data);
case 2:
return (unsigned long) *((uint16_t *) data);
case 4:
return (unsigned long) *((uint32_t *) data);
case 8:
return (unsigned long) *((uint64_t *) data);
default:
return 0;
}
}
/* Perform sanity checks on device setting. */
static bool check_setting(struct fw_file *f, struct fw_setting *set)
{
/* Key sanity checks */
if (fwacc(f, set->key_type) != FW_SETTING_KEYTYPE_ASCII) {
fwwarn(f, "Unsupported key type: %d", set->key_type);
return false;
}
if (fwacc(f, set->key_len) < 1) {
fwwarn(f, "Unsupported key length: %d", set->key_len);
return false;
}
if (sizeof(struct fw_setting) + fwacc(f, set->key_len) > set->len) {
fwwarn(f, "Key length exceeds setting");
return false;
}
if (fwacc(f, set->data[set->key_len - 1])) {
fwwarn(f, "Key not null-terminated");
return false;
}
/* Value sanity checks */
if (fwacc(f, set->val_type) != FW_SETTING_VALTYPE_UINT &&
fwacc(f, set->val_type) != FW_SETTING_VALTYPE_ASCII) {
fwwarn(f, "Unsupported value type: %d", set->val_type);
return false;
}
if (fwacc(f, set->val_len) < 1) {
fwwarn(f, "Unsupported value length: %d", set->val_len);
return false;
}
if (sizeof(struct fw_setting) + set->key_len +
fwacc(f, set->val_len) > set->len) {
fwwarn(f, "Value length exceeds setting");
return false;
}
if ((set->val_type == FW_SETTING_VALTYPE_ASCII) &&
fwacc(f, set->data[set->key_len + set->val_len - 1])) {
fwwarn(f, "Value not null-terminated");
return false;
}
if (set->val_type == FW_SETTING_VALTYPE_UINT) {
switch (fwacc(f, set->val_len)) {
case 1:
case 2:
case 4:
case 8:
break;
default:
fwwarn(f, "Unsupported integer value length: %d",
set->val_len);
return false;
}
}
return true;
}
/* Add a setting to the device. Emit a warning if the setting is not known. */
static void _add_setting(const char *filename, struct device *dev,
config_t config, const char *key, const char *value)
{
struct attrib *a;
struct setting_list *list;
list = device_get_setting_list(dev, config);
a = attrib_find(dev->subtype->dev_attribs, key);
if (!a) {
warnx("%s: Applying unknown device setting %s=%s", filename,
key, value);
}
setting_list_apply(list, a, key, value);
}
static void add_setting(const char *filename, struct device *dev,
config_t config, const char *key, const char *value)
{
if (SCOPE_ACTIVE(config))
_add_setting(filename, dev, config_active, key, value);
if (SCOPE_PERSISTENT(config))
_add_setting(filename, dev, config_persistent, key, value);
if (SCOPE_AUTOCONF(config))
_add_setting(filename, dev, config_autoconf, key, value);
}
/* Parse a single device setting in firmware format and apply it to the
* specified device. */
static void parse_setting(struct fw_file *f, struct fw_setting *set,
struct device *dev, config_t config)
{
char *ascii_key, *ascii_val;
unsigned long ulong_val;
if (!check_setting(f, set))
return;
ascii_key = &set->data[0];
if (set->val_type == FW_SETTING_VALTYPE_UINT) {
ulong_val = parse_value(&set->data[set->key_len], set->val_len);
ascii_val = misc_asprintf("%lu", ulong_val);
add_setting(f->name, dev, config, ascii_key, ascii_val);
free(ascii_val);
} else {
ascii_val = &set->data[set->key_len];
add_setting(f->name, dev, config, ascii_key, ascii_val);
}
}
/* Parse a device settings list in firmware format and apply the resulting
* settings to the specified device. */
static void parse_settings(struct fw_file *f, char *data, struct device *dev,
config_t config)
{
struct fw_setlist *list = (struct fw_setlist *) data;
struct fw_setting *set;
uint16_t off;
for (off = sizeof(struct fw_setlist); off < list->len;
off += set->len) {
set = (struct fw_setting *) &data[off];
if (fwacc(f, set->len) < sizeof(struct fw_setting)) {
fwwarn(f, "Setting too short");
break;
}
if (off + fwacc(f, set->len) > list->len) {
fwwarn(f, "Setting too long");
break;
}
parse_setting(f, set, dev, config);
}
}
/* Perform sanity checks on an I/O device ID. */
static bool check_iodevid(struct fw_file *f, uint8_t *flags,
struct fw_iodevid *id)
{
if (fwacc(f, *flags) & FW_IODEVID_FLAG_MCSS) {
fwwarn(f, "Unsupported entry in non-default CSS");
return false;
}
if (fwacc(f, id->cssid) != 0) {
fwwarn(f, "Non-zero CSS-ID");
return false;
}
return true;
}
/* Perform sanity checks on a device entry. */
static bool check_de_size(struct fw_file *f, struct fw_dehdr *de, size_t size)
{
if (fwacc(f, de->len) < size) {
fwwarn(f, "Device entry too short (expect %zu)", size);
return false;
}
return true;
}
/* Convert an I/O device ID to CCW device ID format. */
static void io_to_ccw(struct ccw_devid *c, struct fw_iodevid *i)
{
c->cssid = i->cssid;
c->ssid = i->ssid;
c->devno = i->devno;
}
/* Register a new device configuration. */
static struct device *add_device(struct fw_file *f, struct subtype *st,
const char *id, config_t config,
struct util_list *objects)
{
struct device *dev;
if (!st->devices)
st->devices = device_list_new(st);
dev = device_list_find(st->devices, id, NULL);
if (!dev) {
dev = device_new(st, id);
if (!dev) {
warnx("%s: Skipping invalid %s device ID %s", f->name,
st->name, id);
return NULL;
}
device_list_add(st->devices, dev);
}
ptrlist_add(objects, object_new(export_device, dev));
/* Prepare device for new settings. */
if (SCOPE_ACTIVE(config)) {
setting_list_clear(dev->active.settings);
if (dev->subtype->support_definable)
dev->active.definable = 1;
else
dev->active.exists = 1;
}
if (SCOPE_PERSISTENT(config)) {
setting_list_clear(dev->persistent.settings);
dev->persistent.exists = 1;
}
if (SCOPE_AUTOCONF(config)) {
setting_list_clear(dev->autoconf.settings);
dev->autoconf.exists = 1;
}
return dev;
}
/* Parse a DASD device entry. */
static void parse_dasd(struct fw_file *f, struct fw_dehdr *de, config_t config,
struct util_list *objects)
{
struct fw_dasd *dasd = (struct fw_dasd *) de;
struct ccw_devid devid;
struct device *dev_eckd, *dev_fba;
char *id;
if (!check_de_size(f, de, sizeof(struct fw_dasd)))
return;
if (!check_iodevid(f, &dasd->id_flags, &dasd->id))
return;
/* Could be either dasd_eckd or dasd_fba - add both entries */
io_to_ccw(&devid, &dasd->id);
id = ccw_devid_to_str(&devid);
dev_eckd = add_device(f, &dasd_subtype_eckd, id, config, objects);
dev_fba = add_device(f, &dasd_subtype_fba, id, config, objects);
free(id);
if (dasd->hdr.len > sizeof(struct fw_dasd)) {
if (dev_eckd)
parse_settings(f, dasd->settings, dev_eckd, config);
if (dev_fba)
parse_settings(f, dasd->settings, dev_fba, config);
}
}
/* Parse a zFCP host device entry. */
static void parse_zfcp_host(struct fw_file *f, struct fw_dehdr *de,
config_t config, struct util_list *objects)
{
struct fw_zfcp_host *zfcp_host = (struct fw_zfcp_host *) de;
struct ccw_devid devid;
struct device *dev;
char *id;
if (!check_de_size(f, de, sizeof(struct fw_zfcp_host)))
return;
if (!check_iodevid(f, &zfcp_host->id_flags, &zfcp_host->id))
return;
/* Add zfcp_host entry */
io_to_ccw(&devid, &zfcp_host->id);
id = ccw_devid_to_str(&devid);
dev = add_device(f, &zfcp_host_subtype, id, config, objects);
free(id);
if (dev && zfcp_host->hdr.len > sizeof(struct fw_zfcp_host))
parse_settings(f, zfcp_host->settings, dev, config);
}
/* Parse a zFCP LUN device entry. */
static void parse_zfcp_lun(struct fw_file *f, struct fw_dehdr *de,
config_t config, struct util_list *objects)
{
struct fw_zfcp_lun *zfcp_lun = (struct fw_zfcp_lun *) de;
struct zfcp_lun_devid devid;
struct device *dev;
char *id;
if (!check_de_size(f, de, sizeof(struct fw_zfcp_lun)))
return;
if (!check_iodevid(f, &zfcp_lun->id_flags, &zfcp_lun->id))
return;
/* Add zfcp_lun entry */
io_to_ccw(&devid.fcp_dev, &zfcp_lun->id);
devid.wwpn = zfcp_lun->wwpn;
devid.lun = zfcp_lun->fcp_lun;
id = zfcp_lun_devid_to_str(&devid);
dev = add_device(f, &zfcp_lun_subtype, id, config, objects);
free(id);
if (dev && zfcp_lun->hdr.len > sizeof(struct fw_zfcp_lun))
parse_settings(f, zfcp_lun->settings, dev, config);
}
/* Parse a QETH device entry. */
static void parse_qeth(struct fw_file *f, struct fw_dehdr *de, config_t config,
struct util_list *objects)
{
struct fw_qeth *qeth = (struct fw_qeth *) de;
struct ccwgroup_devid devid;
struct device *dev;
char *id;
if (!check_de_size(f, de, sizeof(struct fw_qeth)))
return;
if (!check_iodevid(f, &qeth->id_flags, &qeth->read_id) ||
!check_iodevid(f, &qeth->id_flags, &qeth->write_id) ||
!check_iodevid(f, &qeth->id_flags, &qeth->data_id))
return;
/* Add qeth entry */
devid.num = 3;
io_to_ccw(&devid.devid[0], &qeth->read_id);
io_to_ccw(&devid.devid[1], &qeth->write_id);
io_to_ccw(&devid.devid[2], &qeth->data_id);
id = ccwgroup_devid_to_str(&devid);
dev = add_device(f, &qeth_subtype_qeth, id, config, objects);
free(id);
if (dev && qeth->hdr.len > sizeof(struct fw_qeth))
parse_settings(f, qeth->settings, dev, config);
}
/* Parse a firmware file. */
static void parse_fw(struct fw_file *f, long skip, config_t config,
struct util_list *objects)
{
char *data = f->buffer;
struct fw_filehdr *hdr = (struct fw_filehdr *) data;
struct fw_dehdr *de;
uint16_t count = 0;
uint32_t off;
for (off = hdr->hdr_len; off < hdr->file_len; off += de->len) {
count++;
de = (struct fw_dehdr *) &data[off];
if (fwacc(f, de->len) == 0) {
fwwarn(f, "Empty device entry");
break;
}
if (off + fwacc(f, de->len) > hdr->file_len) {
fwwarn(f, "Device entry too long");
break;
}
if (skip >= 0 && de->seq <= skip) {
debug("Skipping %s entry due to sequence (%08x)\n",
type_to_str(de->type), de->seq);
continue;
}
switch (fwacc(f, de->type)) {
case FW_DE_HDR_TYPE_DASD:
parse_dasd(f, de, config, objects);
break;
case FW_DE_HDR_TYPE_ZFCP_HOST:
parse_zfcp_host(f, de, config, objects);
break;
case FW_DE_HDR_TYPE_ZFCP_LUN:
parse_zfcp_lun(f, de, config, objects);
break;
case FW_DE_HDR_TYPE_QETH:
parse_qeth(f, de, config, objects);
break;
default:
fwwarn(f, "Unknown entry (type=%04x)", de->type);
break;
}
}
if (count != fwacc(f, hdr->de_count))
fwwarn(f, "Device entry count mismatch");
}
/* Read configuration objects from @fd in firmware file format. Add pointers to
* newly allocated struct export_objects to ptrlist @objects. If @skip is a
* positive number, skip over entries with a sequence number equal to or
* greater than @skip. */
exit_code_t firmware_read(FILE *fd, const char *filename, long skip,
config_t config, struct util_list *objects)
{
struct fw_file file;
exit_code_t rc;
rc = read_fw(&file, fd, filename);
if (rc)
return rc;
parse_fw(&file, skip, config, objects);
free(file.buffer);
return rc;
}
/* Check if @fd refers to a file in binary firmware format. */
bool firmware_detect(FILE *fd)
{
int c;
c = fgetc(fd);
ungetc(c, fd);
/* Note: A full check would require looking at least at the first 4
* bytes, but fd might be non-seekable (e.g. pipe). Since there is no
* way that a textual import file can start with a 'z', looking at
* the first char should be enough. */
return c == 'z';
}