/*
 * 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 <stdlib.h>
#include <string.h>

#include "lib/util_path.h"

#include "blkinfo.h"
#include "ccw.h"
#include "device.h"
#include "devnode.h"
#include "findmnt.h"
#include "iscsi.h"
#include "misc.h"
#include "namespace.h"
#include "net.h"
#include "select.h"
#include "udev.h"

/* Create and initialize selection options. */
struct select_opts *select_opts_new(void)
{
	struct select_opts *select;

	select = misc_malloc(sizeof(struct select_opts));
	util_list_init(&select->devids, struct strlist_node, node);
	util_list_init(&select->by_path, struct strlist_node, node);
	util_list_init(&select->by_node, struct strlist_node, node);
	util_list_init(&select->by_if, struct strlist_node, node);
	util_list_init(&select->by_attr, struct strlist_node, node);

	return select;
}

/* Free selection options. */
void select_opts_free(struct select_opts *select)
{
	void *next, *this;

	if (!select)
		return;
	util_list_iterate_safe(&select->devids, this, next)
		free(this);
	util_list_iterate_safe(&select->by_path, this, next)
		free(this);
	util_list_iterate_safe(&select->by_node, this, next)
		free(this);
	util_list_iterate_safe(&select->by_if, this, next)
		free(this);
	util_list_iterate_safe(&select->by_attr, this, next)
		free(this);

	free(select);
}

/* Check if devices are specified in the options structure. */
bool select_opts_dev_specified(struct select_opts *select)
{
	if (!util_list_is_empty(&select->devids) ||
	    !util_list_is_empty(&select->by_path) ||
	    !util_list_is_empty(&select->by_node) ||
	    !util_list_is_empty(&select->by_if) ||
	    !util_list_is_empty(&select->by_attr) ||
	    select->all || select->configured || select->existing ||
	    select->online || select->offline || select->failed ||
	    select->ipldev)
		return true;

	return false;
}

/* Allocated and initialize a new list of selected_dev objects. */
struct util_list *selected_dev_list_new(void)
{
	struct util_list *list;

	list = misc_malloc(sizeof(struct util_list));
	util_list_init(list, struct selected_dev_node, node);

	return list;
}

/* Free resources associated with specified selected_dev_node. */
void selected_dev_free(struct selected_dev_node *dev)
{
	free(dev->id);
	free(dev->param);
	free(dev);
}

/* Release all resources associated with the specified list. */
void selected_dev_list_free(struct util_list *list)
{
	struct selected_dev_node *p, *n;

	if (!list)
		return;
	util_list_iterate_safe(list, p, n) {
		util_list_remove(list, p);
		selected_dev_free(p);
	}
	free(list);
}

/* Used for debugging purpose. */
void selected_dev_list_print(struct util_list *list, int i)
{
	struct selected_dev_node *sel;

	indent(i, "selected_dev_list at %p:\n", (void *) list);
	if (!list)
		return;
	util_list_iterate(list, sel)
		selected_dev_print(sel, i + 2);
}

static struct selected_dev_node *selected_dev_node_new(struct devtype *dt,
						       struct subtype *st,
						       const char *id,
						       const char *param,
						       exit_code_t rc)
{
	struct selected_dev_node *sel;

	sel = misc_malloc(sizeof(struct selected_dev_node));
	sel->dt = dt;
	sel->st = st;
	if (st)
		sel->id = st->namespace->normalize_id(id);
	if (!sel->id)
		sel->id = misc_strdup(id);
	sel->param = param ? misc_strdup(param) : NULL;
	sel->rc = rc;

	return sel;
}

/* Add a new selected_dev object to the list. */
struct selected_dev_node *selected_dev_list_add(struct util_list *list,
						struct devtype *dt,
						struct subtype *st,
						const char *id,
						const char *param,
						exit_code_t rc)
{
	struct selected_dev_node *sel;

	sel = selected_dev_node_new(dt, st, id, param, rc);
	util_list_add_tail(list, sel);

	return sel;
}

/* Used for debugging. */
void selected_dev_print(struct selected_dev_node *sel, int indent)
{
	printf("%*sselected_dev_node at %p\n", indent, "", (void *) sel);
	indent += 2;
	printf("%*sdt=%p (%s)\n", indent, "", (void *) sel->dt,
	       sel->dt ? sel->dt->name : "<none>");
	printf("%*sst=%p (%s)\n", indent, "", (void *) sel->st,
	       sel->st ? sel->st->name : "<none>");
	printf("%*sid=%s\n", indent, "", sel->id);
	printf("%*sparam=%s\n", indent, "", sel->param);
	printf("%*src=%d\n", indent, "", sel->rc);
}

#define is_range(x)	(strchr((x), '-'))

/**
 * devid_spec - Device ID specification
 * @spec: Device ID or range as specified on the command line
 * @match: The resulting selected_dev_node
 * @exists: A hint that this spec matched an existing device
 */
struct devid_spec {
	const char *spec;
	struct selected_dev_node *match;
	int exists;
};

/* Make sure that all devices specified by ID are available. */
static void unblacklist_devices(struct select_opts *select)
{
	struct devtype *dt, *only_dt = select->devtype;
	struct subtype *st, *only_st = select->subtype;
	struct namespace *ns;
	struct strlist_node *s;
	int ns_done[NUM_NAMESPACES];
	int i, j, nsi, need_wait;

	/* Reset done array. */
	for (i = 0; i < NUM_NAMESPACES; i++)
		ns_done[i] = 0;

	/* Process all selected namespaces. */
	need_wait = 0;
	for (i = 0; (dt = devtypes[i]); i++) {
		if (only_dt && dt != only_dt)
			continue;
		for (j = 0; (st = dt->subtypes[j]); j++) {
			if (only_st && st != only_st)
				continue;
			ns = st->namespace;

			/* Ensure that each namespace is handled only once. */
			nsi = namespaces_index(ns);
			if (nsi < 0)
				continue;
			if (ns_done[nsi])
				continue;
			ns_done[nsi] = 1;

			/* Check if blacklist is supported. */
			if (!ns->unblacklist_id && !ns->unblacklist_id_range)
				continue;

			/* Unblacklist all specified IDs. */
			util_list_iterate(&select->devids, s) {
				if (ns_is_id_valid(ns, s->str) &&
				    ns->is_id_blacklisted(s->str) &&
				    ns->unblacklist_id) {
					ns->unblacklist_id(s->str);
					need_wait = 1;
				} else if (ns_is_id_range_valid(ns, s->str) &&
					ns->is_id_range_blacklisted(s->str) &&
					ns->unblacklist_id_range) {
					ns->unblacklist_id_range(s->str);
					need_wait = 1;
				}
			}
		}
	}

	/* Devices may have become available which trigger udev rules.
	 * Wait for udev to settle before reading device state. */
	if (need_wait)
		udev_settle();
}

/* Check if @dev matches state selection parameters in @select. */
bool select_match_state(struct device *dev, struct select_opts *select)
{
	struct subtype *st = dev->subtype;
	int exists, configured, autoconf, online;
	struct util_list *errors;

	exists = dev->active.exists || dev->active.definable;
	configured = dev->persistent.exists;
	autoconf = dev->autoconf.exists;
	if (select->all && !(exists || configured || autoconf))
		return false;
	if (select->existing && !exists)
		return false;
	if (select->configured && !configured)
		return false;

	online = subtype_online_get(st, dev, config_active);
	if (select->online && online != 1)
		return false;
	if (select->offline && online == 1)
		return false;

	errors = subtype_get_errors(st, dev->id);
	if (select->failed && !errors)
		return false;
	strlist_free(errors);

	return true;
}

/* If device @ID of subtype @st matches state selection specified in @select,
 * add it to @selected. */
static exit_code_t match_state_and_add(struct select_opts *select,
				       struct util_list *selected,
				       config_t config,
				       read_scope_t scope,
				       struct subtype *st,
				       const char *id, const char *param,
				       struct selected_dev_node **match_ptr)
{
	struct device *dev;
	struct selected_dev_node *sel;
	struct devtype *dt = st->devtype;
	exit_code_t rc;
	config_t c;

	/* Online information must be obtained from active config. */
	c = SCOPE_ACTIVE(config) ? config : config_all;

	/* Heavy-weight, but we may need device online information. */
	rc = subtype_read_device(st, id, c, scope, &dev);
	if (rc)
		return rc;

	if (!select_match_state(dev, select))
		return EXIT_OK;

	sel = selected_dev_list_add(selected, dt, st, dev->id, param, 0);

	if (match_ptr && !*match_ptr)
		*match_ptr = sel;

	return EXIT_OK;
}

/* Ask for confirmation when the number of devices addressed by range exceeds
 * RANGE_LIMIT. */
static bool check_range_size(struct namespace *ns, const char *range)
{
	static int warned;
	unsigned long num;

	if (force || warned)
		return true;

	num = ns->num_ids_in_range(range);
	if (num <= RANGE_LIMIT)
		return true;

	/* Warn only once. */
	warned = 1;

	return confirm("Note: This will attempt to select more than %d "
		       "devices (at least %u)\nContinue?", RANGE_LIMIT, num);
}

/* Check if device is matched by list of device IDs and ranges in @specs. */
static struct devid_spec *match_id(struct namespace *ns, const char *id,
				   struct util_list *specs)
{
	struct ptrlist_node *p;
	struct devid_spec *spec;

	util_list_iterate(specs, p) {
		spec = p->ptr;
		if (ns_is_id_range_valid(ns, spec->spec)) {
			if (ns->is_id_in_range(id, spec->spec))
				return spec;
		} else if (ns->cmp_ids(id, spec->spec) == 0)
			return spec;
	}

	return NULL;
}

/* Set spec->match of all matching spec entries in @specs to @match. */
static void set_match(struct namespace *ns, const char *id,
		      struct util_list *specs, struct selected_dev_node *match)
{
	struct ptrlist_node *p;
	struct devid_spec *spec;

	util_list_iterate(specs, p) {
		spec = p->ptr;
		if (spec->match)
			continue;
		if (ns_is_id_range_valid(ns, spec->spec)) {
			if (ns->is_id_in_range(id, spec->spec))
				spec->match = match;
		} else if (ns->cmp_ids(id, spec->spec) == 0)
			spec->match = match;
	}
}

struct select_cb_data {
	struct select_opts *select;
	struct util_list *selected;
	config_t config;
	read_scope_t scope;
	struct util_list *specs;
};

/* Check if this existing device matches any of the devices specified.
 * If so, add a selected_dev_node to data->selected. */
static exit_code_t select_cb(struct subtype *st, const char *id,
			     config_t config, void *data)
{
	struct select_cb_data *cb_data = data;
	struct devid_spec *spec;
	const char *param;
	struct selected_dev_node **match, *old_match = NULL;
	exit_code_t rc;

	longrun_current++;
	if (!cb_data->specs) {
		/* No devids provided on command line. */
		param = NULL;
		match = NULL;
		goto add;
	}
	spec = match_id(st->namespace, id, cb_data->specs);
	if (!spec)
		return EXIT_OK;
	param = spec->spec;
	match = &spec->match;
	old_match = spec->match;

	/* Note that ID matched existing device to generate a more fitting
	 * exit code. */
	spec->exists = 1;

add:
	rc = match_state_and_add(cb_data->select, cb_data->selected,
				 cb_data->config, cb_data->scope,
				 st, id, param, match);

	if (rc || !match || !*match || old_match)
		return rc;

	/* At this point the following is true:
	 * - device IDs were specified
	 * - the current device ID spec matched
	 * - this is the first match.
	 * To prevent "Device not found" error messages on multiple
	 * specification of the same IDs, set spec->match for all subsequent
	 * matching device ID specs. */
	set_match(st->namespace, id, cb_data->specs, *match);

	return rc;
}

/* Select devices according to @select in subtype @st. Select only devices
 * which exist/are configured. */
static exit_code_t select_by_st_nocreate(struct select_opts *select,
					 struct util_list *selected,
					 config_t config,
					 read_scope_t scope,
					 struct subtype *st,
					 struct util_list *specs)
{
	struct select_cb_data data;

	data.select = select;
	data.selected = selected;
	data.config = config;
	data.scope = scope;
	if (util_list_is_empty(specs))
		data.specs = NULL;
	else
		data.specs = specs;

	return subtype_for_each_id(st, config, select_cb, &data);
}

/* Add a node for a devid specification that did not match any device. */
static struct selected_dev_node *add_failed_devid_spec(
					struct select_opts *select,
					struct util_list *selected,
					struct devid_spec *spec,
					struct selected_dev_node *before)
{
	struct selected_dev_node *sel;
	exit_code_t rc;

	if (select->subtype || spec->exists)
		rc = EXIT_DEVICE_NOT_FOUND;
	else
		rc = EXIT_INCOMPLETE_TYPE;

	sel = selected_dev_node_new(select->devtype, select->subtype,
				    spec->spec, spec->spec, rc);

	if (before)
		util_list_add_prev(selected, sel, before);
	else
		util_list_add_tail(selected, sel);

	return sel;
}

/* Add a devnode for any device that is a parent device of @devnode to
 * @result. */
static bool add_parent_devnodes(struct util_list *result,
				struct devnode *devnode)
{
	struct util_list *devnodes;
	struct ptrlist_node *p;
	struct devnode *d;
	bool rc = false;

	switch (devnode->type) {
	case BLOCKDEV:
		devnodes = blkinfo_get_ancestor_devnodes(devnode);
		if (devnodes) {
			rc = true;

			util_list_iterate(devnodes, p)
				ptrlist_add(result, p->ptr);

			ptrlist_free(devnodes, 0);
			break;
		}
		d = iscsi_get_net_devnode(devnode);
		if (d) {
			ptrlist_add(result, d);
			rc = true;
			break;
		}
		break;
	case NETDEV:
		if (net_add_linked_devnodes(result, devnode)) {
			rc = true;
			break;
		}
		if (net_add_vlan_base(result, devnode)) {
			rc = true;
			break;
		}
		if (net_add_bonding_base(result, devnode)) {
			rc = true;
			break;
		}
		break;
	default:
		break;
	}

	return rc;
}

/* Select device that provides device node found at specified path. */
exit_code_t select_by_devnode(struct select_opts *select,
			      struct util_list *selected,
			      config_t config, read_scope_t scope,
			      struct devtype *only_dt, struct subtype *only_st,
			      struct devnode *devnode, const char *path,
			      err_t err)
{
	struct select_opts *dummy_opts = NULL;
	struct subtype *st;
	char *id = NULL, *str;
	const char *name = path ? path : devnode->name;
	struct util_list *todos, *unresolved;
	exit_code_t rc = EXIT_OK;
	struct ptrlist_node *p;
	int num_resolved = 0;

	/* Allocate dummy selection options in case none were supplied. */
	if (!select) {
		dummy_opts = select_opts_new();
		select = dummy_opts;
	}

	todos = ptrlist_new();
	ptrlist_add(todos, devnode_copy(devnode));
	unresolved = strlist_new();

	util_list_iterate(todos, p) {
		devnode = p->ptr;

		if (subtypes_find_by_devnode(devnode, &st, &id)) {
			/* In case devtype or subtype was specified on command
			 * line, filter out devices that do not match type. */
			if ((only_st && st != only_st) ||
			    (only_dt && st->devtype != only_dt))
				goto next;

			match_state_and_add(select, selected, config, scope, st,
					    id, NULL, NULL);
			num_resolved++;
next:
			free(id);
		} else if (!add_parent_devnodes(todos, devnode))
			strlist_add(unresolved, devnode->name);
	}

	str = strlist_flatten(unresolved, " ");
	if (num_resolved == 0) {
		/* Not a single device could be resolved. */
		err_t_print(err, "Could not determine device that provides "
			    "%s (%s)\n", name, str);
		rc = EXIT_DEVICE_NOT_FOUND;
	} else if (!util_list_is_empty(unresolved)) {
		/* Some devices in a compound device could not be selected. */
		err_t_print(err, "Could not determine all devices that provide "
			    "%s (%s)\n", name, str);
	}
	free(str);

	ptrlist_free(todos, 1);
	strlist_free(unresolved);
	select_opts_free(dummy_opts);

	return rc;
}

/* Select device that provides device node found at specified path. */
exit_code_t select_by_node(struct select_opts *select,
			   struct util_list *selected, config_t config,
			   read_scope_t scope, struct devtype *only_dt,
			   struct subtype *only_st, const char *path,
			   err_t err)
{
	struct devnode *devnode;
	exit_code_t rc;

	devnode = devnode_from_node(path, err);
	if (!devnode)
		return EXIT_DEVICE_NOT_FOUND;

	rc = select_by_devnode(select, selected, config, scope, only_dt,
			       only_st, devnode, path, err);

	free(devnode);

	return rc;
}

/* Select devices that provide the mountpoint in which path lies. */
exit_code_t select_by_path(struct select_opts *select,
			   struct util_list *selected, config_t config,
			   read_scope_t scope, struct devtype *only_dt,
			   struct subtype *only_st, const char *path,
			   err_t err)
{
	struct devnode *devnode;
	struct util_list *devnodes;
	struct ptrlist_node *p;
	exit_code_t rc;

	if (!util_path_exists(path)) {
		err_t_print(err, "Path not found: %s\n", path);
		return EXIT_DEVICE_NOT_FOUND;
	}

	/* Try to get devnode by using stat. */
	devnode = devnode_from_path(path);
	if (devnode)
		goto found;

	/* Try to get devnode via blkinfo. */
	devnode = blkinfo_get_devnode_by_path(path);
	if (devnode)
		goto found;

	/* Try to get devnodes via findmnt (e.g. for btrfs subvolume mounts) */
	devnodes = findmnt_get_devnodes_by_path(path);
	if (devnodes)
		goto found_multi;

	goto notfound;

found:
	/* Select "main" devnode. */
	rc = select_by_devnode(select, selected, config, scope, only_dt,
			       only_st, devnode, NULL, err);
	if (rc)
		goto out;

	/* Check if additional devnodes are involved (same file system UUID,
	 * e.g. for a btrfs file system with multiple devices). */
	devnodes = blkinfo_get_same_uuid_devnodes(devnode);
	if (!devnodes)
		goto out;

found_multi:
	util_list_iterate(devnodes, p) {
		rc = select_by_devnode(select, selected, config, scope, only_dt,
				       only_st, p->ptr, NULL, err);
		if (rc)
			break;
	}

	ptrlist_free(devnodes, 1);

out:
	free(devnode);

	return rc;

notfound:
	err_t_print(err, "Could not determine device that provides %s%s\n",
		    path, file_is_devnode(path) ? " (did you mean --by-node?)" :
						  "");

	return EXIT_DEVICE_NOT_FOUND;
}

static exit_code_t select_create_one(struct select_opts *select,
				     struct util_list *selected,
				     struct devtype *only_dt,
				     struct subtype *only_st,
				     config_t config, read_scope_t scope,
				     struct namespace *ns, const char *id,
				     const char *spec,
				     struct selected_dev_node **match_ptr)
{
	int i, j;
	struct devtype *dt;
	struct subtype *st;
	exit_code_t rc = EXIT_OK;

	for (i = 0; (dt = devtypes[i]); i++) {
		if (only_dt && dt != only_dt)
			continue;
		for (j = 0; (st = dt->subtypes[j]); j++) {
			if (only_st && st != only_st)
				continue;
			if (ns && st->namespace != ns)
				continue;

			rc = match_state_and_add(select, selected, config,
						 scope, st, id,
						 spec, match_ptr);
			if (rc)
				return rc;
		}
	}

	return rc;
}

static exit_code_t select_create_range(struct select_opts *select,
				       struct util_list *selected,
				       struct devtype *only_dt,
				       struct subtype *only_st,
				       config_t config, read_scope_t scope,
				       const char *range,
				       struct selected_dev_node **match_ptr)
{
	int i;
	struct namespace *ns;
	struct ns_range_iterator *it;
	exit_code_t rc = EXIT_OK;

	for (i = 0; (ns = namespaces[i]); i++) {
		if (!ns_is_id_range_valid(ns, range))
			continue;

		if (!check_range_size(ns, range))
			return EXIT_ABORTED;

		/* Select device for each ID in range. */
		it = ns_range_iterator_new();
		ns_range_for_each(ns, range, it) {
			rc = select_create_one(select, selected, only_dt,
					       only_st, config, scope,
					       ns, it->id, range, match_ptr);
			if (rc)
				break;
		}
		ns_range_iterator_free(it);
		if (rc)
			return rc;
	}

	return EXIT_OK;
}

static exit_code_t select_create_by_spec(struct select_opts *select,
					 struct util_list *selected,
					 config_t config, read_scope_t scope,
					 struct devid_spec *spec)
{
	struct util_list *cand;
	exit_code_t rc;
	struct selected_dev_node *match, *sel, *n;
	struct subtype *st;

	/* Special handling in case type is not fully specified: add
	 * candidates to special list that is checked afterwards. */
	if (select->subtype)
		cand = selected;
	else
		cand = selected_dev_list_new();

	match = NULL;
	if (is_range(spec->spec)) {
		/* Select range of devices. */
		rc = select_create_range(select, cand, select->devtype,
					 select->subtype, config, scope,
					 spec->spec, &match);
	} else {
		/* Select device by ID. */
		rc = select_create_one(select, cand, select->devtype,
				       select->subtype, config, scope, NULL,
				       spec->spec, spec->spec, &match);
	}
	if (rc)
		goto out;

	if (select->subtype) {
		/* Type was fully specified, we're done. */
		goto out;
	}

	/* Check for type ambiguities. */
	st = NULL;
	util_list_iterate(cand, sel) {
		if (!st) {
			st = sel->st;
			continue;
		}
		if (sel->st == st)
			continue;

		/* There are too many candidates. Create error entry. */
		match = selected_dev_list_add(selected, select->devtype,
					      select->subtype,
					      spec->spec, spec->spec,
					      EXIT_INCOMPLETE_TYPE);
		goto out;
	}

	/* No ambiguity found - copy candidates to selected list. */
	util_list_iterate_safe(cand, sel, n) {
		util_list_remove(cand, sel);
		util_list_add_tail(selected, sel);
		if (!match)
			match = sel;
	}

out:
	if (!spec->match)
		spec->match = match;
	if (cand != selected)
		selected_dev_list_free(cand);

	return rc;
}

static exit_code_t select_create(struct select_opts *select,
				 struct util_list *selected,
				 config_t config, read_scope_t scope,
				 struct util_list *specs)
{
	struct ptrlist_node *p;
	exit_code_t rc = EXIT_OK;

	/* Iterate over device IDs because there's a chance we don't know
	 * the subtype. */
	util_list_iterate(specs, p) {
		rc = select_create_by_spec(select, selected, config, scope,
					   p->ptr);
		if (rc)
			break;
	}

	return rc;
}

static exit_code_t select_nocreate(struct select_opts *select,
				   struct util_list *selected, int pairs,
				   config_t config, read_scope_t scope,
				   struct util_list *specs)
{
	int i, j;
	struct devtype *dt;
	struct subtype *st;
	exit_code_t rc = EXIT_OK;
	config_t id_config;
	unsigned long count;

	/* Determine from which configuration sets to check for devices. */
	id_config = get_config(select->existing || select->all ||
			       select->offline || select->online ||
			       SCOPE_ACTIVE(config),
			       select->configured || select->all ||
			       SCOPE_PERSISTENT(config),
			       select->all || SCOPE_AUTOCONF(config));

	longrun_total = 0;
	if (quiet)
		goto skip;

	/* Count devices. */
	verb("Scanning for devices in %s configuration%s:\n",
	     config_to_str(id_config), !SCOPE_SINGLE(id_config) ? "s" : "");
	for (i = 0; (dt = devtypes[i]); i++) {
		if (select->devtype && dt != select->devtype)
			continue;
		for (j = 0; (st = dt->subtypes[j]); j++) {
			if (select->subtype && st != select->subtype)
				continue;
			count = subtype_count_ids(st, id_config);
			longrun_total += count;
			verb("  %-19s: %lu\n", st->devname, count);
		}
	}

	if (longrun_total == 0) {
		verb("  No device found\n");
		return EXIT_OK;
	}
	verb("  %-19s: %lu\n", "Total", longrun_total);

skip:
	/* Select devices. */
	longrun_start("Reading device information", pairs);
	for (i = 0; (dt = devtypes[i]); i++) {
		if (select->devtype && dt != select->devtype)
			continue;
		for (j = 0; (st = dt->subtypes[j]); j++) {
			if (select->subtype && st != select->subtype)
				continue;
			rc = select_by_st_nocreate(select, selected,
						   id_config, scope, st, specs);
			if (rc)
				goto out;
		}
	}

out:
	longrun_stop();

	return rc;
}

static struct subtype *derive_st(const char *spec)
{
	int i, j;
	struct devtype *dt;
	struct subtype *st, *result = NULL;

	for (i = 0; (dt = devtypes[i]); i++) {
		for (j = 0; (st = dt->subtypes[j]); j++) {
			if (!ns_is_id_valid(st->namespace, spec) &&
			    !ns_is_id_range_valid(st->namespace, spec))
				continue;
			/* Abort on ambiguous match. */
			if (result)
				return NULL;
			result = st;
		}
	}

	return result;
}

static exit_code_t select_remaining(struct select_opts *select,
				    struct util_list *selected, int define,
				    config_t config, read_scope_t scope,
				    struct util_list *specs)
{
	struct subtype *st;
	struct ptrlist_node *p;
	struct devid_spec *spec;
	exit_code_t rc = EXIT_OK;

	util_list_iterate(specs, p) {
		spec = p->ptr;
		if (spec->match)
			continue;
		if (select->subtype)
			st = select->subtype;
		else
			st = derive_st(spec->spec);
		if (!st)
			continue;
		if (define && !st->support_definable)
			continue;
		if (is_range(spec->spec)) {
			/* Select range of devices. */
			rc = select_create_range(select, selected, st->devtype,
						 st, config,
						 scope, spec->spec,
						 &spec->match);
		} else {
			/* Select device by ID. */
			rc = select_create_one(select, selected, st->devtype,
					       st, config, scope,
					       NULL, spec->spec, spec->spec,
					       &spec->match);
		}
		if (rc)
			break;
	}

	return rc;
}

static exit_code_t select_by_devid(struct select_opts *select,
				   struct util_list *selected, int existing,
				   int pairs, config_t config,
				   read_scope_t scope, struct util_list *specs)
{
	static exit_code_t rc;

	if (existing) {
		rc = select_nocreate(select, selected, pairs, config, scope,
				     specs);
		if (rc)
			return rc;
		return select_remaining(select, selected, 1, config, scope,
					specs);
	}

	if (select->subtype) {
		/* Subtype specified - no guesswork involved. */
		if (!util_list_is_empty(specs)) {
			return select_create(select, selected, config, scope,
					     specs);
		}
		return select_nocreate(select, selected, pairs, config,
				       scope, specs);
	}

	/* Subtype not specified - check for existing devices first. Use
	 * config_all to enable using active devices as template for persistent
	 * configurations. */
	rc = select_nocreate(select, selected, pairs, config_all, scope, specs);
	if (rc)
		return rc;

	/* No information about the device type available. There are two
	 * possibilities:
	 * 1. Subtype can be inferred from namespace
	 * 2. A devid specification cannot be resolved */
	return select_remaining(select, selected, 0, config, scope, specs);
}

/* Add selected_dev_node entries for all device ID specifications that did not
 *  match anything. */
static void handle_unmatched_specs(struct select_opts *select,
				   struct util_list *selected,
				   struct util_list *specs)
{
	struct ptrlist_node *p;
	struct selected_dev_node *before;
	struct devid_spec *spec;

	/* Since unmatched entries should be added before the first match of
	 * the following devid specification, we need to traverse the
	 * devid_spec list in reverse. */
	before = NULL;
	for (p = util_list_end(specs); p; p = util_list_prev(specs, p)) {
		spec = p->ptr;
		if (spec->match) {
			/* This devid spec matched something. */
			before = spec->match;
			continue;
		}

		/* Add "not found" result for this devid spec. */
		before = add_failed_devid_spec(select, selected, spec, before);
	}
}

/* Remove duplicate entries in selected list. */
static void remove_duplicates(struct util_list *selected,
			      struct selected_dev_node *first)
{
	struct selected_dev_node *sel, *s, *n;

	sel = first ? first : util_list_start(selected);

	for (; sel; sel = util_list_next(selected, sel)) {
		for (s = util_list_next(selected, sel); s; s = n) {
			n = util_list_next(selected, s);
			if (sel->dt != s->dt || sel->st != s->st)
				continue;
			if (sel->id && s->id) {
				if (strcmp(sel->id, s->id) == 0)
					goto remove;
				continue;
			}
			if (sel->param && s->param &&
			    strcmp(sel->param, s->param) == 0)
				goto remove;
			continue;

remove:
			util_list_remove(selected, s);
			selected_dev_free(s);
		}
	}
}

/* Select devices that provide networking interface @name. */
exit_code_t select_by_interface(struct select_opts *select,
				struct util_list *selected, config_t config,
				read_scope_t scope, struct devtype *only_dt,
				struct subtype *only_st, const char *name,
				err_t err)
{
	struct devnode *d;
	exit_code_t rc;

	d = devnode_new(NETDEV, 0, 0, name);
	rc = select_by_devnode(select, selected, config, scope, only_dt,
			       only_st, d, NULL, err);
	free(d);

	return rc;
}

struct by_attr_cb_data {
	struct select_opts *select;
	struct util_list *selected;
	read_scope_t scope;
	const char *key;
	const char *value;
	bool invert;
};

static exit_code_t by_attr_cb(struct subtype *st, const char *id,
			      config_t config, void *data)
{
	struct by_attr_cb_data *cb_data = data;
	struct device *dev;
	static exit_code_t rc;
	struct setting *s;
	char *value;
	bool match = false;

	rc = subtype_read_device(st, id, config_all, cb_data->scope, &dev);
	if (rc)
		return rc;

	/* Check for attribute value in active configuration. */
	s = setting_list_find(dev->active.settings, cb_data->key);
	if (s)
		match = setting_match_value(s, cb_data->value);
	else {
		/* Try reading setting directly. */
		value = device_read_active_attrib(dev, cb_data->key);
		if (value && strcmp(value, cb_data->value) == 0)
			match = true;
		free(value);
	}
	if (match)
		goto out;

	/* Check for attribute value in persistent configuration. */
	s = setting_list_find(dev->persistent.settings, cb_data->key);
	if (s) {
		match = setting_match_value(s, cb_data->value);
		if (match)
			goto out;
	}
	/* Check for attribute value in autoconf configuration. */
	s = setting_list_find(dev->autoconf.settings, cb_data->key);
	if (s)
		match = setting_match_value(s, cb_data->value);

out:
	if ((match && !cb_data->invert) || (!match && cb_data->invert)) {
		/* Apply state selection and add. */
		rc = match_state_and_add(cb_data->select, cb_data->selected,
					 config, cb_data->scope, st, id, NULL,
					 NULL);
	}

	return rc;
}

/* Select devices with specified attribute settings. */
static exit_code_t select_by_attr(struct select_opts *select,
				  struct util_list *selected, config_t config,
				  read_scope_t scope, struct devtype *only_dt,
				  struct subtype *only_st, const char *attr,
				  err_t err)
{
	char *key, *value;
	exit_code_t rc = EXIT_OK;
	int i, j;
	struct devtype *dt;
	struct subtype *st;
	struct by_attr_cb_data cb_data;
	bool invert = false;

	key = misc_strdup(attr);
	value = strchr(key, '=');
	if (!value) {
		rc = EXIT_USAGE_ERROR;
		goto out;
	}
	/* Check for KEY!=VALUE. */
	if (value > key && value[-1] == '!') {
		value[-1] = 0;
		invert = true;
	}
	*value = 0;
	value++;

	/* Iterate over all devices. */
	cb_data.select = select;
	cb_data.selected = selected;
	cb_data.scope = scope;
	cb_data.key = key;
	cb_data.value = value;
	cb_data.invert = invert;
	for (i = 0; (dt = devtypes[i]); i++) {
		if (only_dt && dt != only_dt)
			continue;
		for (j = 0; (st = dt->subtypes[j]); j++) {
			if (only_st && st != only_st)
				continue;
			rc = subtype_for_each_id(st, config_all, by_attr_cb,
						 &cb_data);
			if (rc)
				goto out;
		}
	}


out:
	free(key);

	return rc;
}

/* Select device that was used for IPL. */
static void select_ipldev(struct util_list *selected)
{
	struct subtype *st;
	char *id;

	if (!subtypes_find_ipldev(&st, &id))
		return;

	selected_dev_list_add(selected, st->devtype, st, id, NULL, 0);
	free(id);
}

/* Determine list of IDs of selected devices based on selection options.
 * Results are stored as list of struct selected_dev_node in SELECTED.
 * If @existing is set, only return nodes for existing devices.
 * If @unblacklist is set, remove all devices specified by ID or ID range from
 * blacklists. If @pairs is set, output progress output in "pairs" format.
 * @config specifies the configuration from which to read devices. */
exit_code_t select_devices(struct select_opts *select,
			   struct util_list *selected, int existing,
			   int unblacklist, int pairs, config_t config,
			   read_scope_t scope, err_t err)
{
	struct util_list *specs = NULL;
	struct strlist_node *s;
	struct devid_spec *spec;
	exit_code_t rc = EXIT_OK;
	struct selected_dev_node *first;

	/* Note for later. */
	first = util_list_start(selected);

	if (unblacklist)
		unblacklist_devices(select);

	/* Skip non-function selection if only by- function was specified. */
	if (util_list_is_empty(&select->devids) &&
	    !(util_list_is_empty(&select->by_path) &&
	      util_list_is_empty(&select->by_node) &&
	      util_list_is_empty(&select->by_if) &&
	      util_list_is_empty(&select->by_attr) && !select->ipldev))
		goto by_function;

	/* Convert device ID specifications to enable tracking of specifications
	 * that did not match any device. */
	specs = ptrlist_new();
	util_list_iterate(&select->devids, s) {
		spec = misc_malloc(sizeof(struct devid_spec));
		spec->spec = s->str;
		ptrlist_add(specs, spec);
	}

	/* Select in all specified devtypes and subtypes. */
	rc = select_by_devid(select, selected, existing, pairs, config, scope,
			     specs);
	if (rc)
		goto out;

	handle_unmatched_specs(select, selected, specs);

by_function:
	/* Select devices specified via --by-node */
	util_list_iterate(&select->by_node, s) {
		rc = select_by_node(select, selected, config, scope,
				    select->devtype, select->subtype, s->str,
				    err);
		if (rc)
			goto out;
	}
	/* Select device specified via --by-path */
	util_list_iterate(&select->by_path, s) {
		rc = select_by_path(select, selected, config, scope,
				    select->devtype, select->subtype, s->str,
				    err);
		if (rc)
			goto out;
	}

	/* Select devices specified via --by-interface */
	util_list_iterate(&select->by_if, s) {
		rc = select_by_interface(select, selected, config, scope,
					 select->devtype, select->subtype,
					 s->str, err);
		if (rc)
			goto out;
	}

	/* Select devices specified via --by-attr */
	util_list_iterate(&select->by_attr, s) {
		rc = select_by_attr(select, selected, config, scope,
				    select->devtype, select->subtype,
				    s->str, err);
		if (rc)
			goto out;
	}

	if (select->ipldev)
		select_ipldev(selected);

out:
	/* Release device ID specifications tracking list. */
	ptrlist_free(specs, 1);

	if (!rc)
		remove_duplicates(selected, first);

	return rc;
}
