Files
s390-tools/zdev/src/udev_ccwgroup.c
Vineeth Vijayan d7b3196286 zdev: fix warning about failed ATTR writes by udev
The qeth-udev-rule execution logs a warning as below in the journalfs.

Failed to write ATTR{/sys/bus/ccwgroup/drivers/qeth/group}, \
ignoring: Invalid argument

This occurs because the udev rule for qeth devices is triggered by
multiple ccw devices and can be invoked in parallel. This can lead to a
situation where each of the three parallel-running udev rules tries to
group the devices simultaneously. As a result, only the first rule
succeeds, while the others fail with an -EINVAL error because the CCW
devices have already been grouped.

To prevent this, schedule a new change uevent for the base device, which
will verify the sysfs before attempting the grouping. The change event is
triggered by all three devices, ensuring that the actual write to the
attribute occurs only once and only when the sysfs attribute is ready.

Hence the add events on different devices are serialized to change event
on the base device, which then prevent duplicate concurrent grouping
attempts.

Suggested-by: Peter Oberparleiter <oberpar@linux.ibm.com>
Signed-off-by: Vineeth Vijayan <vneethv@linux.ibm.com>
Reviewed-by: Peter Oberparleiter <oberpar@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2024-07-08 09:25:12 +02:00

432 lines
10 KiB
C

/*
* zdev - Modify and display the persistent configuration of devices
*
* Copyright IBM Corp. 2016, 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 <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lib/util_path.h"
#include "lib/util_udev.h"
#include "attrib.h"
#include "ccwgroup.h"
#include "device.h"
#include "internal.h"
#include "misc.h"
#include "path.h"
#include "setting.h"
#include "udev.h"
#include "udev_ccwgroup.h"
static char *get_rule_path_by_devid(const char *type,
struct ccwgroup_devid *devid, bool autoconf)
{
char *ccw_id, *path;
ccw_id = ccw_devid_to_str(&devid->devid[0]);
path = path_get_udev_rule(type, ccw_id, autoconf);
free(ccw_id);
return path;
}
static char *get_rule_path(const char *type, const char *id, bool autoconf)
{
struct ccwgroup_devid devid;
if (ccwgroup_parse_devid(&devid, id, err_ignore) != EXIT_OK)
return NULL;
return get_rule_path_by_devid(type, &devid, autoconf);
}
/* Check if a udev rule for the specified CCWGROUP device exists. */
bool udev_ccwgroup_exists(const char *type, const char *id, bool autoconf)
{
char *path;
bool rc;
path = get_rule_path(type, id, autoconf);
if (!path)
return false;
rc = util_path_is_reg_file(path);
free(path);
return rc;
}
static void add_setting_from_entry(struct setting_list *list,
struct util_udev_entry_node *entry,
struct attrib **attribs)
{
char *copy, *name, *end;
struct attrib *a;
/* ENV{zdev_var}="1" */
if (starts_with(entry->key, "ENV{zdev_") &&
strcmp(entry->op, "=") == 0) {
udev_add_internal_from_entry(list, entry, attribs);
return;
}
/* ATTR{[ccwgroup/0.0.f5f0]online}=1 */
if (strncmp(entry->key, "ATTR{[ccwgroup/", 10) != 0 ||
strcmp(entry->op, "=") != 0)
return;
copy = misc_strdup(entry->key);
name = copy;
/* Find attribute name start. */
name = strchr(entry->key, ']');
end = strrchr(entry->key, '}');
if (!name || !end)
goto out;
*end = 0;
name++;
a = attrib_find(attribs, name);
setting_list_apply_actual(list, a, name, entry->value);
out:
free(copy);
}
/* Extract CCWGROUP device settings from a CCWGROUP device udev rule file. */
static void udev_file_get_settings(struct util_udev_file *file,
struct attrib **attribs,
struct setting_list *list)
{
struct util_udev_line_node *line;
struct util_udev_entry_node *entry;
util_list_iterate(&file->lines, line) {
entry = util_list_start(&line->entries);
if (!entry)
continue;
add_setting_from_entry(list, entry, attribs);
}
}
/* Determine full CCWGROUP from data in udev rule file. */
static void expand_id(struct device *dev, struct util_udev_file *file)
{
struct ccwgroup_devid devid;
struct ccwgroup_devid *devid_ptr = dev->devid;
struct util_udev_line_node *line;
struct util_udev_entry_node *entry;
char *id;
int i;
if (devid_ptr->num != 1)
return;
util_list_iterate(&file->lines, line) {
entry = util_list_start(&line->entries);
if (!entry)
continue;
if (!starts_with(entry->key, "ATTR{[drivers/ccwgroup:") ||
!ends_with(entry->key, "]group}"))
continue;
/* Extract CCWGROUP ID from comma-separated list of CCW
* device IDs. */
id = misc_strdup(entry->value);
for (i = 0; id[i]; i++) {
if (id[i] == ',')
id[i] = ':';
}
if (ccwgroup_parse_devid(&devid, id, err_ignore) == EXIT_OK)
*devid_ptr = devid;
free(dev->id);
dev->id = ccwgroup_devid_to_str(&devid);
free(id);
break;
}
}
/* Read the persistent configuration of a CCWGROUP device from a udev rule. */
exit_code_t udev_ccwgroup_read_device(struct device *dev, bool autoconf)
{
struct subtype *st = dev->subtype;
struct device_state *state = autoconf ? &dev->autoconf :
&dev->persistent;
struct util_udev_file *file = NULL;
exit_code_t rc;
char *path;
path = get_rule_path_by_devid(st->name, dev->devid, autoconf);
rc = (exit_code_t) util_udev_read_file(path, &file);
if (rc)
goto out;
if (udev_file_is_empty(file)) {
warn_once("Warning: Invalid udev rule: %s\n", path);
state->exists = 0;
} else {
udev_file_get_settings(file, st->dev_attribs, state->settings);
expand_id(dev, file);
state->exists = 1;
}
util_udev_free_file(file);
out:
free(path);
return rc;
}
/* Return an ID suitable for use as udev label. */
static char *get_label_id(const char *prefix, const char *type,
const char *dev_id)
{
char *id;
int i;
id = misc_asprintf("%s_%s_%s", prefix, type, dev_id);
for (i = 0; id[i]; i++) {
if (isalnum(id[i]) || id[i] == '_' || id[i] == '.')
continue;
id[i] = '_';
}
return id;
}
/* Write the persistent configuration of a CCWGROUP device to a udev rule. */
exit_code_t udev_ccwgroup_write_device(struct device *dev, bool autoconf)
{
struct subtype *st = dev->subtype;
struct device_state *state = autoconf ? &dev->autoconf :
&dev->persistent;
struct ccwgroup_subtype_data *data = st->data;
const char *type = st->name, *drv = data->ccwgroupdrv, *id = dev->id;
struct ccwgroup_devid devid;
char *path, *cfg_label = NULL, *group_label = NULL, *end_label = NULL,
*ccw_id, *chan_id;
struct util_list *list;
struct ptrlist_node *p;
struct setting *s;
struct strlist_node *str;
exit_code_t rc = EXIT_OK;
FILE *fd;
unsigned int i;
if (!state->exists)
return udev_ccwgroup_remove_rule(type, id, autoconf);
if (ccwgroup_parse_devid(&devid, id, err_ignore) != EXIT_OK)
return EXIT_INVALID_ID;
ccw_id = ccw_devid_to_str(&devid.devid[0]);
path = get_rule_path(type, ccw_id, autoconf);
group_label = get_label_id("group", type, ccw_id);
cfg_label = get_label_id("cfg", type, ccw_id);
end_label = get_label_id("end", type, ccw_id);
/* Apply attributes in correct order. */
list = setting_list_get_sorted(state->settings);
debug("Writing %s udev rule file %s\n", type, path);
if (!util_path_exists(path)) {
rc = path_create(path);
if (rc)
goto out;
}
fd = misc_fopen(path, "w");
if (!fd) {
error("Could not write to file %s: %s\n", path,
strerror(errno));
rc = EXIT_RUNTIME_ERROR;
goto out;
}
/* Write udev rule prolog. */
fprintf(fd, "# Generated by chzdev\n");
/* Triggers. */
fprintf(fd, "ACTION!=\"add|change\", GOTO=\"%s\"\n", end_label);
fprintf(fd, "SUBSYSTEM==\"drivers\", "
"KERNEL==\"%s\", GOTO=\"%s\"\n", drv, group_label);
for (i = 0; i < devid.num; i++) {
chan_id = ccw_devid_to_str(&devid.devid[i]);
fprintf(fd, "SUBSYSTEM==\"ccw\", "
"KERNEL==\"%s\", DRIVER==\"%s\", "
"GOTO=\"%s\"\n", chan_id, drv, group_label);
free(chan_id);
}
fprintf(fd, "ACTION==\"add\", SUBSYSTEM==\"ccwgroup\", "
"KERNEL==\"%s\", DRIVER==\"%s\", GOTO=\"%s\"\n", ccw_id,
drv, cfg_label);
fprintf(fd, "GOTO=\"%s\"\n\n", end_label);
/* Group. */
fprintf(fd, "LABEL=\"%s\"\n", group_label);
fprintf(fd, "TEST==\"[ccwgroup/%s]\", GOTO=\"%s\"\n", ccw_id,
end_label);
for (i = 0; i < devid.num; i++) {
chan_id = ccw_devid_to_str(&devid.devid[i]);
fprintf(fd, "TEST!=\"[ccw/%s]\", GOTO=\"%s\"\n", chan_id,
end_label);
free(chan_id);
}
/* Parallel execution of the ccwgroup devices udev rule can lead to
* udevadm warnings. To prevent this, serialize the execution.
*/
fprintf(fd, "\n# Note: Serialize add events on different devices to"
" change event on single\n# base device to prevent duplicate"
" concurrent grouping attempts\n");
/* The new change uevent should be generated to the base-device only */
chan_id = ccw_devid_to_str(&devid.devid[0]);
fprintf(fd, "ACTION==\"add\", ATTR{[ccw/%s]uevent}=\"change\"\n", chan_id);
fprintf(fd, "ACTION==\"change\", ATTR{[drivers/ccwgroup:%s]group}=\"", drv);
free(chan_id);
for (i = 0; i < devid.num; i++) {
chan_id = ccw_devid_to_str(&devid.devid[i]);
fprintf(fd, "%s%s", i > 0 ? "," : "", chan_id);
free(chan_id);
}
fprintf(fd, "\"\n");
fprintf(fd, "GOTO=\"%s\"\n\n", end_label);
/* Configure. */
fprintf(fd, "LABEL=\"%s\"\n", cfg_label);
util_list_iterate(list, p) {
s = p->ptr;
if (s->removed)
continue;
if (s->values) {
util_list_iterate(s->values, str) {
fprintf(fd, "ATTR{[ccwgroup/%s]%s}=\"%s\"\n",
ccw_id, s->name, str->str);
}
} else if ((s->attrib && s->attrib->internal) ||
internal_by_name(s->name)) {
fprintf(fd, "ENV{zdev_%s}=\"%s\"\n",
internal_get_name(s->name), s->value);
} else {
fprintf(fd, "ATTR{[ccwgroup/%s]%s}=\"%s\"\n", ccw_id,
s->name, s->value);
}
}
/* Write udev rule epilog. */
fprintf(fd, "\n");
fprintf(fd, "LABEL=\"%s\"\n", end_label);
if (misc_fclose(fd))
warn("Could not close file %s: %s\n", path, strerror(errno));
out:
ptrlist_free(list, 0);
free(end_label);
free(cfg_label);
free(group_label);
free(path);
free(ccw_id);
return rc;
}
static char *read_full_id(const char *path)
{
char *text, *start, *end, *id = NULL;
text = misc_read_text_file(path, 0, err_ignore);
if (!text)
return NULL;
start = strstr(text, "ATTR{[drivers/ccwgroup:");
if (!start)
goto out;
start = strchr(start, '"');
if (!start)
goto out;
start++;
end = strchr(start, '"');
if (!end)
goto out;
*end = 0;
id = misc_strdup(start);
start = id;
while ((start = strchr(start, ',')))
*start = ':';
out:
free(text);
if (!id)
warn_once("Warning: Invalid udev rule: %s\n", path);
return id;
}
struct get_ids_cb_data {
char *prefix;
struct util_list *ids;
};
static exit_code_t get_ids_cb(const char *path, const char *filename,
void *data)
{
struct get_ids_cb_data *cb_data = data;
char *id;
if (!starts_with(filename, cb_data->prefix))
return EXIT_OK;
id = read_full_id(path);
if (!id)
return EXIT_OK;
strlist_add(cb_data->ids, id);
free(id);
return EXIT_OK;
}
/* Add the IDs for all devices of the specified subtype name for which a
* udev rule exists to strlist LIST. */
void udev_ccwgroup_add_device_ids(const char *type, struct util_list *list,
bool autoconf)
{
struct get_ids_cb_data cb_data;
char *path;
path = path_get_udev_rules(autoconf);
cb_data.prefix = misc_asprintf("%s-%s-", UDEV_PREFIX, type);
cb_data.ids = list;
if (util_path_is_dir(path))
path_for_each(path, get_ids_cb, &cb_data);
free(cb_data.prefix);
free(path);
}
/* Remove UDEV rule for CCWGROUP device. */
exit_code_t udev_ccwgroup_remove_rule(const char *type, const char *id,
bool autoconf)
{
char *partial_id, *path;
exit_code_t rc = EXIT_OK;
partial_id = ccwgroup_get_partial_id(id);
if (!partial_id)
return EXIT_INVALID_ID;
path = path_get_udev_rule(type, partial_id, autoconf);
if (util_path_is_reg_file(path))
rc = remove_file(path);
free(path);
free(partial_id);
return rc;
}