diff options
Diffstat (limited to 'lib/device/device_id.c')
-rw-r--r-- | lib/device/device_id.c | 830 |
1 files changed, 830 insertions, 0 deletions
diff --git a/lib/device/device_id.c b/lib/device/device_id.c new file mode 100644 index 000000000..989d80221 --- /dev/null +++ b/lib/device/device_id.c @@ -0,0 +1,830 @@ +/* + * Copyright (C) 2020 Red Hat, Inc. All rights reserved. + * + * This file is part of LVM2. + * + * This copyrighted material is made available to anyone wishing to use, + * modify, copy, or redistribute it subject to the terms and conditions + * of the GNU Lesser General Public License v.2.1. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "base/memory/zalloc.h" +#include "lib/misc/lib.h" +#include "lib/commands/toolcontext.h" +#include "lib/device/device.h" +#include "lib/device/device_id.h" +#include "lib/device/dev-type.h" +#include "lib/device/device-types.h" + +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <time.h> +#include <sys/types.h> +#include <sys/file.h> +#include <sys/sysmacros.h> + +void free_uid(struct use_id *uid) +{ + if (uid->idname) + free(uid->idname); + if (uid->devname) + free(uid->devname); + if (uid->pvid) + free(uid->pvid); + free(uid); +} + +void free_uids(struct dm_list *uids) +{ + struct use_id *uid, *safe; + + dm_list_iterate_items_safe(uid, safe, uids) { + dm_list_del(&uid->list); + free_uid(uid); + } +} + +void free_did(struct dev_id *did) +{ + if (did->idname) + free(did->idname); + free(did); +} + +void free_dids(struct dm_list *dids) +{ + struct dev_id *did, *safe; + + dm_list_iterate_items_safe(did, safe, dids) { + dm_list_del(&did->list); + free_did(did); + } +} + +static int _read_sys_wwid(struct cmd_context *cmd, struct device *dev, char **idname) +{ + char path[PATH_MAX]; + char buf[PATH_MAX] = { 0 }; + + if (dm_snprintf(path, sizeof(path), "%sdev/block/%d:%d/device/wwid", + dm_sysfs_dir(), (int)MAJOR(dev->dev), (int)MINOR(dev->dev)) < 0) { + return 0; + } + + get_sysfs_value(path, buf, sizeof(buf), 0); + + if (buf[0]) { + if (!(*idname = strdup(buf))) + return 0; + } else { + *idname = NULL; + } + return 1; +} + +static int _read_sys_serial(struct cmd_context *cmd, struct device *dev, char **idname) +{ + char path[PATH_MAX]; + char buf[PATH_MAX] = { 0 }; + + if (dm_snprintf(path, sizeof(path), "%sdev/block/%d:%d/device/serial", + dm_sysfs_dir(), (int)MAJOR(dev->dev), (int)MINOR(dev->dev)) < 0) { + return 0; + } + + get_sysfs_value(path, buf, sizeof(buf), 0); + + if (buf[0]) { + if (!(*idname = strdup(buf))) + return 0; + } else { + *idname = NULL; + } + return 1; +} + +/* the dm uuid uses the wwid of the underlying dev */ + +static int _read_mpath_uuid(struct cmd_context *cmd, struct device *dev, char **idname) +{ + char path[PATH_MAX]; + char buf[PATH_MAX] = { 0 }; + + if (dm_snprintf(path, sizeof(path), "%sdev/block/%d:%d/dm/uuid", + dm_sysfs_dir(), (int)MAJOR(dev->dev), (int)MINOR(dev->dev)) < 0) { + return 0; + } + + get_sysfs_value(path, buf, sizeof(buf), 0); + + if (buf[0]) { + if (!(*idname = strdup(buf))) + return 0; + } else { + *idname = NULL; + } + return 1; +} + +static int _dev_has_mpath_uuid(struct cmd_context *cmd, struct device *dev, char **idname) +{ + if (MAJOR(dev->dev) != cmd->dev_types->device_mapper_major) + return 0; + + _read_mpath_uuid(cmd, dev, idname); + + if (*idname) + return 1; + return 0; +} + +static int _read_loop_file(struct cmd_context *cmd, struct device *dev, char **idname) +{ + char path[PATH_MAX]; + char buf[PATH_MAX] = { 0 }; + + if (dm_snprintf(path, sizeof(path), "%sdev/block/%d:%d/loop/backing_file", + dm_sysfs_dir(), (int)MAJOR(dev->dev), (int)MINOR(dev->dev)) < 0) { + return 0; + } + + get_sysfs_value(path, buf, sizeof(buf), 0); + + if (buf[0]) { + if (!(*idname = strdup(buf))) + return 0; + } else { + *idname = NULL; + } + return 1; +} + +/* + * TODO: should there be a list like lvm.conf + * device_id_types = [ "sys_wwid", "sys_serial" ] + * that controls which idtype's will be used? + * + * TODO: add a type for md devices, probably have it + * use the uuid from the md dev superblock. This would + * help in case of inconsistent md dev names, but would + * not help in case md components were all cloned. + */ +static char *_device_id_system_read(struct cmd_context *cmd, struct device *dev, uint16_t idtype) +{ + char *idname = NULL; + + if (idtype == DEV_ID_TYPE_SYS_WWID) + _read_sys_wwid(cmd, dev, &idname); + + else if (idtype == DEV_ID_TYPE_SYS_SERIAL) + _read_sys_serial(cmd, dev, &idname); + + else if (idtype == DEV_ID_TYPE_DEVNAME) + idname = strdup(dev_name(dev)); + + else if (idtype == DEV_ID_TYPE_MPATH_UUID) + _read_mpath_uuid(cmd, dev, &idname); + + else if (idtype == DEV_ID_TYPE_LOOP_FILE) + _read_loop_file(cmd, dev, &idname); + + return idname; +} + +const char *idtype_to_str(uint16_t idtype) +{ + if (idtype == DEV_ID_TYPE_SYS_WWID) + return "sys_wwid"; + + if (idtype == DEV_ID_TYPE_SYS_SERIAL) + return "sys_serial"; + + if (idtype == DEV_ID_TYPE_DEVNAME) + return "devname"; + + if (idtype == DEV_ID_TYPE_MPATH_UUID) + return "mpath_uuid"; + + if (idtype == DEV_ID_TYPE_LOOP_FILE) + return "loop_file"; + + return "unknown"; +} + +uint16_t idtype_from_str(const char *str) +{ + if (!strcmp(str, "sys_wwid")) + return DEV_ID_TYPE_SYS_WWID; + if (!strcmp(str, "sys_serial")) + return DEV_ID_TYPE_SYS_SERIAL; + if (!strcmp(str, "devname")) + return DEV_ID_TYPE_DEVNAME; + if (!strcmp(str, "mpath_uuid")) + return DEV_ID_TYPE_MPATH_UUID; + if (!strcmp(str, "loop_file")) + return DEV_ID_TYPE_LOOP_FILE; + return 0; +} + +const char *dev_idtype(struct device *dev) +{ + if (!dev->id) + return NULL; + + return idtype_to_str(dev->id->idtype); +} + +const char *dev_id(struct device *dev) +{ + if (dev->id) + return dev->id->idname; + return NULL; +} + +static void _copy_idline_str(char *src, char *dst, int len) +{ + char *s, *d = dst; + + memset(dst, 0, len); + + if (!(s = strchr(src, '='))) + return; + s++; + while ((*s == ' ') && (s < src + len)) + s++; + while ((*s != ' ') && (*s != '\0') && (*s != '\n') && (s < src + len)) { + *d = *s; + s++; + d++; + } +} + +int device_ids_read(struct cmd_context *cmd) +{ + char line[PATH_MAX]; + char buf[PATH_MAX]; + char *idtype, *idname, *devname, *pvid; + struct use_id *uid; + FILE *fp; + int fl_fd, fl_err = -1; + int ret = 1; + + /* + * TODO: allow the use_device_ids list to come from a + * command line option instead of devices_file? + * If so, add use_id structs to use_device_ids based + * on the reading the command line args here. + */ + + if (!cmd->enable_device_ids) + return 1; + + free_uids(&cmd->use_device_ids); + + if (cmd->nolocking) + goto use_file; + if ((fl_fd = open(cmd->devices_file, O_RDWR)) < 0) { + log_warn("Cannot open devices_file to flock."); + goto use_file; + } + if ((fl_err = flock(fl_fd, LOCK_SH))) { + log_warn("Cannot lock devices_file to read."); + close(fl_fd); + } + +use_file: + if (!(fp = fopen(cmd->devices_file, "r"))) { + log_warn("Cannot open devices_file to read."); + ret = 0; + goto out; + } + + while (fgets(line, sizeof(line), fp)) { + if (line[0] == '#') + continue; + + idtype = strstr(line, "IDTYPE"); + idname = strstr(line, "IDNAME"); + devname = strstr(line, "DEVNAME"); + pvid = strstr(line, "PVID"); + + /* These two are the minimum required. */ + if (!idtype || !idname) + continue; + + if (!(uid = zalloc(sizeof(struct use_id)))) + return 0; + + _copy_idline_str(idtype, buf, PATH_MAX); + if (buf[0]) + uid->idtype = idtype_from_str(buf); + + _copy_idline_str(idname, buf, PATH_MAX); + if (buf[0]) + uid->idname = strdup(buf); + + if (!uid->idtype || !uid->idname) { + log_print("Ignoring device: %s", line); + free_uid(uid); + continue; + } + + if (devname) { + _copy_idline_str(devname, buf, PATH_MAX); + if (buf[0] && (buf[0] != '.')) + uid->devname = strdup(buf); + } + + if (pvid) { + _copy_idline_str(pvid, buf, PATH_MAX); + if (buf[0] && (buf[0] != '.')) + uid->pvid = strdup(buf); + } + + dm_list_add(&cmd->use_device_ids, &uid->list); + } + + if (fclose(fp)) + stack; +out: + if (!cmd->nolocking && !fl_err) { + if (flock(fl_fd, LOCK_UN)) + stack; + if (close(fl_fd)) + stack; + } + return ret; +} + +int device_ids_write(struct cmd_context *cmd) +{ + FILE *fp; + time_t t; + struct use_id *uid; + const char *devname; + const char *pvid; + int fl_fd, fl_err = -1; + int ret = 1; + + if (!cmd->enable_device_ids) + return 1; + + if (cmd->nolocking) + goto use_file; + if ((fl_fd = open(cmd->devices_file, O_RDWR)) < 0) { + log_warn("Cannot open devices_file to flock."); + goto use_file; + } + if ((fl_err = flock(fl_fd, LOCK_EX))) { + log_warn("Cannot lock devices_file to write."); + close(fl_fd); + } + +use_file: + if (!(fp = fopen(cmd->devices_file, "w"))) { + log_warn("Cannot open devices_file to write."); + ret = 0; + goto out; + } + + t = time(NULL); + + log_print("updating devices_file"); + + fprintf(fp, "# LVM will use devices listed in this file.\n"); + fprintf(fp, "# IDTYPE and IDNAME fields are required, the DEVNAME path may change.\n"); + fprintf(fp, "# Created by LVM command %s pid %d at %s\n", cmd->name, getpid(), ctime(&t)); + + dm_list_iterate_items(uid, &cmd->use_device_ids) { + devname = uid->dev ? dev_name(uid->dev) : "."; + if (devname[0] != '/') + devname = "."; + + if (!uid->pvid || !uid->pvid[0] || (uid->pvid[0] == '.')) + pvid = "."; + else + pvid = uid->pvid; + + fprintf(fp, "IDTYPE=%s IDNAME=%s DEVNAME=%s PVID=%s\n", + idtype_to_str(uid->idtype) ?: ".", + uid->idname ?: ".", devname, pvid); + } + + if (fflush(fp)) + stack; + if (fclose(fp)) + stack; + +out: + if (!cmd->nolocking && !fl_err) { + if (flock(fl_fd, LOCK_UN)) + stack; + if (close(fl_fd)) + stack; + } + return ret; +} + +static struct use_id *_get_uid_for_dev(struct cmd_context *cmd, struct device *dev) +{ + struct use_id *uid; + + dm_list_iterate_items(uid, &cmd->use_device_ids) { + if (uid->dev == dev) + return uid; + } + return NULL; +} + +/* + * Add or update entry for this dev. + * IDTYPE=sys_wwid IDNAME=01234566 DEVNAME=/dev/sdb PVID=99393939 [OPTS=xx,yy,zz] + * + * add an entry to dev->ids and point dev->id to it. + * add or update entry in cmd->use_device_ids + */ +int device_id_pvcreate(struct cmd_context *cmd, struct device *dev, const char *pvid, + const char *idtype_arg, const char *id_arg) +{ + uint16_t idtype = 0; + char *idname; + struct use_id *uid; + struct dev_id *did; + int found_did = 0; + + if (!cmd->enable_device_ids) + return 1; + + /* TODO: handle dev with existing match */ + if (dev->id || (dev->flags & DEV_MATCHED_USE_ID)) + return 0; + + if (!(uid = _get_uid_for_dev(cmd, dev))) { + if (!(uid = zalloc(sizeof(struct use_id)))) + return_0; + } + + /* TODO: should deviceidtype command line option work for mpath/loop? */ + /* TODO: add more idtypes for special devs (e.g. MD) that don't have wwid */ + + if (_dev_has_mpath_uuid(cmd, dev, &idname)) { + idtype = DEV_ID_TYPE_MPATH_UUID; + goto id_done; + } + + if (MAJOR(dev->dev) == cmd->dev_types->loop_major) { + idtype = DEV_ID_TYPE_LOOP_FILE; + goto id_name; + } + + /* + * First use type specified by user option, then use a previous + * type, then use the default type. + * TODO: allow lvm.conf device_id_types to control idtypes used here? + */ + + if (idtype_arg) { + if (!(idtype = idtype_from_str(idtype_arg))) + log_warn("WARNING: ignoring unknown device_id type %s.", idtype_arg); + else { + if (id_arg) { + idname = id_arg; + goto id_done; + } + goto id_name; + } + } + + if (!idtype && uid->idtype) { + idtype = uid->idtype; + log_print("Create device_id for %s using previous idtype %u", dev_name(dev), idtype); + goto id_name; + } + + idtype = DEV_ID_TYPE_SYS_WWID; + +id_name: + if (!(idname = _device_id_system_read(cmd, dev, idtype))) { + if (idtype == DEV_ID_TYPE_SYS_WWID) { + idtype = DEV_ID_TYPE_SYS_SERIAL; + goto id_name; + } + idtype = DEV_ID_TYPE_DEVNAME; + goto id_name; + } + +id_done: + dm_list_iterate_items(did, &dev->ids) { + if (did->idtype == idtype) { + /* TODO: verify did->idname matches idname */ + found_did = 1; + break; + } + } + + if (!found_did) { + if (!(did = zalloc(sizeof(struct dev_id)))) + return_0; + did->idtype = idtype; + did->idname = idname; + did->dev = dev; + dm_list_add(&dev->ids, &did->list); + } + dev->id = did; + dev->flags |= DEV_MATCHED_USE_ID; + + if (uid->idtype && (uid->idtype != idtype)) { + log_print("Changing device_id_type from %s to %s for %s", + idtype_to_str(uid->idtype), idtype_to_str(idtype), dev_name(dev)); + } + if (uid->idtype && (uid->idtype == idtype) && + strcmp(uid->idname, idname)) { + log_print("Changing device_id from %s to %s for %s", + uid->idname, idname, dev_name(dev)); + } + + uid->idtype = did->idtype; + uid->idname = strdup(did->idname); + uid->devname = strdup(dev_name(dev)); + uid->dev = dev; + uid->pvid = strdup(pvid); + + if (!uid->idname || !uid->idname || !uid->pvid) { + free_uid(uid); + return 0; + } + + dm_list_add(&cmd->use_device_ids, &uid->list); + + return 1; +} + +/* + * Update entry for this dev. + * Set PVID=. + * update entry in cmd->use_device_ids + */ +void device_id_pvremove(struct cmd_context *cmd, struct device *dev) +{ + struct use_id *uid; + + if (!cmd->enable_device_ids) + return; + + if (!(uid = _get_uid_for_dev(cmd, dev))) { + log_warn("WARNING: use_device_ids does not include %s", dev_name(dev)); + return; + } + + if (uid->pvid) { + free(uid->pvid); + uid->pvid = NULL; + } +} + +/* + * check for dev->ids entry with uid->idtype, if found compare it, + * if not, system_read of this type and add entry to dev->ids, compare it. + * When a match is found, set up links among uid/did/dev. + */ + +int device_id_match(struct cmd_context *cmd, struct use_id *uid, struct device *dev) +{ + struct dev_id *did; + char *idname; + + dm_list_iterate_items(did, &dev->ids) { + if (did->idtype == uid->idtype) { + if (did->idname && !strcmp(did->idname, uid->idname)) { + uid->dev = dev; + dev->id = did; + dev->flags |= DEV_MATCHED_USE_ID; + return 1; + } else { + return_0; + } + } + } + + if (!(did = zalloc(sizeof(struct dev_id)))) + return_0; + + if (!(idname = _device_id_system_read(cmd, dev, uid->idtype))) { + /* Save a new did in dev->ids for this type to indicate no match + to avoid repeated system_read, since this called many times. + Setting idtype and NULL idname means no id of this type. */ + did->idtype = uid->idtype; + did->dev = dev; + dm_list_add(&dev->ids, &did->list); + return 0; + } + + /* Save this id for the device (so it can be quickly checked again), even + if it's not the idtype used to identify the dev in device_id_file. */ + did->idtype = uid->idtype; + did->idname = idname; + did->dev = dev; + dm_list_add(&dev->ids, &did->list); + + if (!strcmp(idname, uid->idname)) { + uid->dev = dev; + dev->id = did; + dev->flags |= DEV_MATCHED_USE_ID; + return 1; + } + + return 0; +} + +/* + +pvid is needed in the device_id_file, and device_id is needed in metadata +in order to handle cases where a device has no wwid/id or the wwid/id +changes. In these cases the correct set of devices can be found and the +devices file can be recreated. (A wwid/id in the metadata will also allow +remove the problem of duplicate pvs.) + +Three identifiers: wwid, dev, pvid +- dev can change, cannot be duplicated, cannot be unknown +- wwid can change, can be duplicated, can be unknown +- pvid can change, can be duplicated, cannot be unknown + +if dev changes +. if wwid exists, lvm corrects dev +. no wwid, new dev out of filter, missing PV, run cmd to find pvid +. no wwid, new dev in filter, lvm corrects filter + +if wwid changes +. lvm sees wrong pvid on wwid +. if the same device is in filter by another means, lvm finds the pvid and corrects wwid +. if the device is not in filter, missing PV, run cmd to find pvid + +if pvid changes +. lvm sees wrong pvid for wwid and corrects filter + +if wwid is duplicated +. lvm can still use the wwid for filtering, + but it won't help if pvid is also duplicated + +if pvid is duplicated +. lvm will use the wwid to choose the right one, + but if the wwid is also duplicated, lvm has to + fall back to picking a prefered devname + +if wwid is unknown +. if dev changes and new dev in filter, lvm fixes devname +. if dev changes and new dev out of filter, pv missing, run cmd to find pvid +. if pvid is duplicated and dev changes, lvm may pick the wrong device + +*/ + +/* + * For each entry on cmd->use_device_ids, find a struct device from dev-cache. + * This must not open or read devices. filters are applied after this, + * and they may open devs in the first filter stage. The second filtering + * stage, done as a part of label_scan, is finally allowed to read devices. + * + * When a device id of a particular type is read for a dev, a did for that + * type is saved in dev->ids in case it needs to be checked again. + * + * When a particular dev_id for a dev (in dev-cache) is matched to a use_dev + * (from use_device_ids), then: + * . uid->dev = dev; + * . dev->id = did; + * . dev->flags |= DEV_MATCHED_USE_ID; + */ + +void device_ids_match(struct cmd_context *cmd) +{ + struct dev_iter *iter; + struct use_id *uid; + struct device *dev; + int update_file = 0; + + if (!cmd->enable_device_ids) + return; + + dm_list_iterate_items(uid, &cmd->use_device_ids) { + /* already matched */ + if (uid->dev && (uid->dev->flags & DEV_MATCHED_USE_ID)) + continue; + + /* + * uid->devname may be incorrect, but it's often correct, so it's the + * most efficient place to begin. + */ + if (uid->devname && + (dev = dev_cache_get(cmd, uid->devname, NULL))) { + /* On success, device_id_match() links the uid, dev, and did. */ + if (device_id_match(cmd, uid, dev)) + continue; + else { + /* uid->devname now belongs to a different device */ + log_print("Device with name %s has changed.", uid->devname); + } + } + + /* At a minimum some devname needs to be added or updated. + device_ids_validate may find other reasons to update the + file. Are there commands where device_ids_validate would + not be run, so we should update the file here? */ + + /* + * Iterate through all devs and try to match uid. + * + * A filter is not used when iterating since this is not selecting + * devs to use. The next dev iteration in label scan does use filters. + * The device_id matches created here are used by filter-deviceid later. + * (Might we apply a couple simple filters here, though, to avoid + * doing some pointless match attempts?) + * + * If a match is made here it means the uid->devname is wrong so the + * device_id file should be udpated with a new devname. + */ + if (!(iter = dev_iter_create(NULL, 0))) + continue; + while ((dev = dev_iter_get(cmd, iter))) { + if (dev->flags & DEV_MATCHED_USE_ID) + continue; + if (device_id_match(cmd, uid, dev)) + break; + } + dev_iter_destroy(iter); + } + + /* + * Look for entries in devices_file for which we found no device. + */ + dm_list_iterate_items(uid, &cmd->use_device_ids) { + if (uid->dev && (uid->dev->flags & DEV_MATCHED_USE_ID)) + continue; + + if (uid->dev && !(uid->dev->flags & DEV_MATCHED_USE_ID)) { + /* is this possible? */ + } + + log_print("Device with previous name %s not found with %s %s PVID %s.", + uid->devname, idtype_to_str(uid->idtype), uid->idname, uid->pvid); + } +} + +/* + * This is called after label_scan() to compare what was found on disks + * vs what's in the devices_file. The devices_file could be outdated + * and need correcting; the authoritative data is what's on disk. + * Now that we have read the device labels in label_scan and have the PVID's + * we can check the pvid's of use_device_ids entries from the device_id_file. + */ +void device_ids_validate(struct cmd_context *cmd) +{ + struct use_id *uid; + int update_file = 0; + + if (!cmd->enable_device_ids) + return; + + dm_list_iterate_items(uid, &cmd->use_device_ids) { + if (!uid->dev) + continue; + + if (uid->dev->pvid[0] && (strcmp(uid->dev->pvid, uid->pvid))) { + log_print("Device %s has updated PVID %s from devices_file (%s)", + dev_name(uid->dev), uid->dev->pvid, uid->pvid); + if (uid->pvid) + free(uid->pvid); + uid->pvid = strdup(uid->dev->pvid); + update_file = 1; + } + + if (!uid->devname || strcmp(dev_name(uid->dev), uid->devname)) { + log_print("Device %s has updated devname from devices_file (%s).", + dev_name(uid->dev), uid->devname ?: "."); + if (uid->devname) + free(uid->devname); + uid->devname = strdup(dev_name(uid->dev)); + update_file = 1; + } + } + + if (update_file) + device_ids_write(cmd); +} + +int devices_file_valid(struct cmd_context *cmd) +{ + struct stat buf; + + if (!cmd->devices_file || !strlen(cmd->devices_file)) + return 0; + + if (stat(cmd->devices_file, &buf)) + return 0; + + return 1; +} + |