Blob Blame History Raw
#!/usr/bin/bash

persist_base=/etc/mdevctl.d
mdev_base=/sys/bus/mdev/devices
parent_base=/sys/class/mdev_bus

# Alias 'lsmdev' to 'mdevctl list'
if [ $(basename $0) == "lsmdev" ]; then
    set -- "list" "${@}"
fi

# See https://stackoverflow.com/a/29754866/4775714 for getopt usage
getopt --test > /dev/null
if [ $? -ne 4 ]; then
    echo "Sorry, $0 requires enhanced getopt support"
    exit 1
fi

config={}
attrs=[]

jsonify() {
    echo "\"$1\""
}

validate_attr() {
    # first arg: expected root, second arg: attr to check
    if [ ! -w "$1/$2" ]; then
       echo "Attribute $2 cannot be set" >&2
       return 1
    fi
    if [ $(realpath --relative-base "$1" "$1/$2") == "$2" ]; then
        return 0
    else
        echo "Invalid attribute $2" >&2
        return 1
    fi
}

del_config_key() {
    key="$1"

    config=$(echo "$config" | jq -c -M --arg key "$key" 'del(."$key")')
}

set_config_key() {
    key=$(jsonify "$1")
    value=$(jsonify "$2")

    del_config_key "$key"

    config=$(echo "$config" | jq -c -M --argjson obj "{$key:$value}" '. + $obj')
}

has_config_key() {
    key="$1"

    if [ $(echo "$config" | jq -M --arg key "$key" 'has($key)') == "true" ]; then
        return 0
    else
        return 1
    fi
}

get_config_key() {
    key="$1"

    echo "$config" | jq -r -M --arg key "$key" '.[$key]'
}

config_file() {
    uuid="$1"
    parent="$2"

    if [ -n "$parent" ]; then
        if [ ! -e "$persist_base/$parent/$uuid" ]; then
            echo "Config for $uuid on $parent does not exist, define it first?" >&2
            return 1
        fi
        echo "$persist_base/$parent/$uuid"
    else
        count=$(find "$persist_base" -name "$uuid" -type f | wc -l)
        if [ "$count" -eq 0 ]; then
            echo "Config for $uuid does not exist, define it first?" >&2
            return 1
        elif [ "$count" -gt 1 ]; then
            echo "Multiple configs found for $uuid, specify a parent" >&2
            return 1
        fi
        echo $(find "$persist_base" -name "$uuid" -type f)
    fi

    return 0
}

get_attr_length() {
    echo "$attrs" | jq -M '. | length'
}

get_attrs_raw() {
    echo "$attrs"
}

add_attr_index() {
    key=$(jsonify "$1")
    value=$(jsonify "$2")

    if [ -z "$3" ]; then
        index=$(get_attr_length)
    else
        index="$3"
    fi

    attrs=$(echo "$attrs" | jq -c -M --argjson obj "{$key:$value}" \
            --argjson i $index '.[0:$i] + [$obj] + .[$i:]')
}

del_attr_index() {
    if [ -z "$1" ]; then
        index=$(( $(get_attr_length) - 1 ))
    else
        index="$1"
    fi

    attrs=$(echo "$attrs" | jq -c -M --argjson i "$index" '.[0:$i] + .[$i+1:]')
}

get_attr_index_key() {
    if [ -z "$1" ]; then
        index=0
    else
        index="$1"
    fi

    echo "$attrs" | jq -r -M --argjson i "$index" '.[$i] | keys | .[]'
}

get_attr_index_value() {
    if [ -z "$1" ]; then
        index=0
    else
        index="$1"
    fi

    echo "$attrs" | jq -r -M --argjson i "$index" '.[$i] | .[]'
}

get_attr_index_raw() {
    if [ -z "$1" ]; then
        index=0
    else
        index="$1"
    fi

    echo "$attrs" | jq -c -M --argjson i "$index" '.[$i]'
}

read_config() {
    file="$1"

    config=$(jq -c -M '.' "$file")
    if [ $? -eq 0 ] && has_config_key mdev_type && has_config_key start; then
        attrs=$(echo "$config" | jq -c -M '.attrs')
        if [ "$attrs" == null ]; then
            attrs=[]
        fi
        config=$(echo "$config" | jq -c -M 'del(.attrs)')
        return 0
    else
        config={}
        attrs=[]
        return 1
    fi
}

dump_config() {
    echo "$config" | jq -M --argjson attrs "{\"attrs\":$attrs}" '. + $attrs'
}

write_config() {
    file="$1"

    dump_config > "$file"
}

valid_uuid () {
    uuid="$1"

    if [[ "$uuid" =~ ^\{?[A-F0-9a-f]{8}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{12}\}?$ ]]; then
        echo "$uuid"
    fi
}

create_mdev() {
    uuid="$1"
    parent="$2"
    type="$3"

    if [ -z "$(valid_uuid $uuid)" ]; then
        echo "Invalid UUID $uuid" >&2
        return 1
    fi

    if [ -L "$mdev_base/$uuid" ]; then
        cur_parent=$(basename $(realpath "$mdev_base/$uuid" | sed -s "s/\/$uuid//"))
        if [ $? -ne 0 ] || [ "$cur_parent" != "$parent" ]; then
            echo "Device exists under different parent" >&2
            return 1
        fi

        cur_type=$(basename $(realpath "$mdev_base/$uuid/mdev_type"))
        if [ $? -ne 0 ] || [ "$cur_type" != "$type" ]; then
            echo "Device exists with different type" >&2
            return 1
        fi

        return 1
    fi

    if [ ! -d "$parent_base/$parent/mdev_supported_types" ]; then
        echo "Parent $parent is not currently registered for mdev support" >&2
        return 1
    fi

    if [ ! -d "$parent_base/$parent/mdev_supported_types/$type" ]; then
        echo "Parent $parent does not support mdev type $type" >&2
        return 1
    fi

    avail=$(cat "$parent_base/$parent/mdev_supported_types/$type/available_instances")
    if [ $? -ne 0 ] || [ "$avail" -eq 0 ]; then
        echo "No available instances of $type on $parent" >&2
        return 1
    fi

    echo "$uuid" > "$parent_base/$parent/mdev_supported_types/$type/create"
    if [ $? -ne 0 ]; then
        echo "Error creating mdev type $type on $parent" >&2
        return 1
    fi

    return 0
}

start_mdev() {
    uuid="$1"
    parent="$2"
    type="$3"
    if [ $# -eq 4 ]; then
        print_uuid="$4"
    fi

    create_mdev "$uuid" "$parent" "$type"
    if [ $? -eq 0 ]; then
        count=$(( $(get_attr_length) - 1 ))
        if [ "$count" -ge 0 ]; then
            for i in $(seq 0 "$count"); do
                attr=$(get_attr_index_key $i)
                validate_attr "$mdev_base/$uuid" "$attr"
                if [ $? -ne 0 ]; then
                    remove_mdev "$uuid"
                    return 1
                fi
                val=$(get_attr_index_value $i)
                echo -e "$val" > "$mdev_base/$uuid/$attr"
                if [ $? -ne 0 ]; then
                    echo "Failed to write $val to attribute $attr" >&2
                    remove_mdev "$uuid"
                    return 1
                fi
            done
        fi
        $print_uuid
        return 0
    fi
    return 1
}

remove_mdev() {
    uuid="$1"

    if [ -z "$(valid_uuid $uuid)" ]; then
        echo "Invalid UUID $uuid" >&2
        return 1
    fi

    if [ ! -L "$mdev_base/$uuid" ]; then
        return 1
    fi

    echo 1 > "$mdev_base/$uuid/remove"
    if [ $? -ne 0 ]; then
        echo "Error removing device $uuid" >&2
        return 1
    fi

    return 0
}

# Get a UUID that's not locally defined or running
unique_uuid() {
    count=1
    while [ $count -ne 0 ]; do
        uuid=$(uuidgen)
        count=$(find "$persist_base" -name "$uuid" -type f | wc -l)
        if [ "$count" -eq 0 ] && [ -L "$mdev_base/$uuid" ]; then
            count=1
        fi
    done

    echo "$uuid"
}

usage() {
    cat >&2 <<EOF
Usage: $(basename $0) {COMMAND} [options...]

Available commands:
define		Define a config for an mdev device.  Options:
	<-u|--uuid=UUID> [-a|--auto]
	[-u|--uuid=UUID] <-p|--parent=PARENT> <-t|--type=TYPE> [-a|--auto]
	[-u|--uuid=UUID] <-p|--parent=PARENT> <--jsonfile=FILE>
		If the device specified by the UUID currently exists, parent
		and type may be omitted to use the existing values. The auto
		option marks the device to start on parent availability.
		If defined via FILE then type, startup, and any attributes
		are provided via the file.  Running devices are unaffected
		by this command.
undefine	Undefine, or remove a config for an mdev device.  Options:
	<-u|--uuid=UUID> [-p|--parent=PARENT]
		If a UUID exists for multiple parents, all will be removed
		unless a parent is specified.  Running devices are unaffected
		by this command.
modify		Modify the config for a defined mdev device.  Options:
	<-u|--uuid=UUID> [-p|--parent=PARENT] [-t|--type=TYPE] \\
	[--addattr=ATTRIBUTE] [--delattr] [-i|--index=INDEX] [--value=VALUE] \\
	[-a|--auto|-m|--manual]
		The parent option further identifies a UUID if it is not
		unique, the parent for a device cannot be modified via this
		command, undefine and re-define should be used instead.  An
		ATTRIBUTE can be added or removed, which correlates to a
		sysfs attribute under the created device.  Unless an INDEX
		value is provided, operations are performed at the end of
		the attribute list.  VALUE is to be specified in the format
		that is accepted by the attribute.  Upon device start, mdevctl
		will go through each attribute in order, writing the value into
		the corresponding sysfs attribute for the device.  The startup
		mode of the device can also be selected, auto or manual.
		Running devices are unaffected by this command.
start		Start an mdev device.  Options:
	<-u|--uuid=UUID> [-p|--parent=PARENT]
	[-u|--uuid=UUID] <-p|--parent=PARENT> <-t|--type=TYPE>
	[-u|--uuid=UUID] <-p|--parent=PARENT> <--jsonfile=FILE>
		If the UUID is previously defined and unique, the UUID is
		sufficient to start the device (UUIDs may not collide between
		running devices).  If a UUID is used in multiple defined
		configs, the parent device is necessary to identify the config.
		When specified with PARENT and TYPE, the device is fully
		specified and will be started based only on these parameters.
		The UUID is optional in this case, if not provided a UUID is
		generated and returned as output.  A FILE may replace the TYPE
		specification and also include additional attributes to be
		applied to the started device.
stop		Stop an mdev device.  Options:
	<-u|--uuid=UUID>
list		List mdev devices.  Options:
	[-d|--defined] [-u|--uuid=UUID] [-p|--parent=PARENT] \\
	[--dumpjson] [-v|--verbose]
		With no options, information about the currently running mdev
		devices is provided.  Specifying DEFINED lists the
		configuration of defined devices, regardless of their running
		state.  This may be further reduced by specifying specific
		UUID or PARENT devices to list.  The dumpjson option provides
		output listing in machine readable JSON format.  When a UUID
		option is provided and the result is a single device, the
		output contains only the JSON fields necessary to recreate a
		config file for the device (minus attributes for listings of
		running devices).  When the verbose option is provided, the
		human readable listing will include attributes for the
		device(s).
types		List mdev types.  Options:
	[-p|--parent=PARENT] [--dumpjson]
		Specifying a PARENT lists only the types provided by the given
		parent device.  The dumpjson option provides output in machine
		readable JSON format.
EOF
    exit 1
}

if [ $# -lt 1 ]; then
    usage
fi

case ${1} in
    #
    # Internal commands, these are expected to be called from other scripts
    # and therefore do not offer the convenience of getopt processing
    # (they typically take one arg after the command) and are not listed in
    # the usage text
    #
    start-parent-mdevs)
        if [ $# -ne 2 ]; then
            echo "Usage: $0 $1 <parent device>" >&2
            exit 1
        fi

        parent="$2"
        if [ ! -d "$persist_base/$parent" ]; then
            # Nothing to do
            exit 0
        fi

        for file in $(find "$persist_base/$parent/" -maxdepth 1 -mindepth 1 -type f); do
            uuid=$(basename "$file")
            if [ -n "$(valid_uuid $uuid)" ]; then
                read_config "$file"
                if [ $? -ne 0 ]; then
                    continue
                fi

                if [ "$(get_config_key start)" == "auto" ]; then
                    create_mdev "$uuid" "$parent" "$(get_config_key mdev_type)"
                    if [ $? -ne 0 ]; then
                        echo "Failed to create mdev $uuid, type $(get_config_key mdev_type) on $parent" >&2
                        # continue...
                    fi
                fi
            fi
        done
        exit 0
        ;;
    #
    # User commands
    #
    --help|-h|-?)
        usage
        ;;
    define)
        cmd="$1"
        OPTIONS="u:p:t:a"
        LONGOPTS="uuid:,parent:,type:,auto,jsonfile:"
        shift
        ;;
    undefine)
        cmd="$1"
        OPTIONS="u:p:"
        LONGOPTS="uuid:,parent:"
        shift
        ;;
    modify)
        cmd="$1"
        OPTIONS="u:p:t:ami:"
        LONGOPTS="uuid:,parent:,type:,auto,manual,addattr:,delattr,index:,value:"
        shift
        ;;
    start)
        cmd="$1"
        OPTIONS="u:p:t:"
        LONGOPTS="uuid:,parent:,type:,jsonfile:"
        shift
        ;;
    stop)
        cmd="$1"
        OPTIONS="u:"
        LONGOPTS="uuid:"
        shift
        ;;
    list)
        cmd="$1"
        OPTIONS="du:p:v"
        LONGOPTS="defined,uuid:,dumpjson,parent:,verbose"
        shift
        ;;
    types)
        cmd="$1"
        OPTIONS="p:"
        LONGOPTS="parent:,dumpjson"
        shift
        ;;
    *)
        echo "Unknown command $1" >&2
        usage
        ;;
esac

PARSED=$(getopt --options="$OPTIONS" --longoptions="$LONGOPTS" --name "$(basename $0)" -- "$@")
if [ $? -ne 0 ]; then
    exit 1
fi

eval set -- "$PARSED"

while true; do
    case "$1" in
        -u|--uuid)
            uuid="$2"
            shift 2
            ;;
        -p|--parent)
            parent="$2"
            shift 2
            ;;
        -t|--type)
            type="$2"
            shift 2
            ;;
        --jsonfile)
            jsonfile="$2"
            shift 2
            ;;
        --addattr)
            addattr="$2"
            shift 2
            ;;
        -i|--index)
            index="$2"
            shift 2
            ;;
        --value)
            value="$2"
            shift 2
            ;;
        --delattr)
            delattr=y
            shift 1
            ;;
        --dumpjson)
            dumpjson=y
            shift
            ;;
        -a|--auto)
            auto=y
            shift 1
            ;;
        -m|--manual)
            manual=y
            shift 1
            ;;
        -d|--defined)
            defined=y
            shift 1
            ;;
        -a|--available)
            available=y
            shift 1
            ;;
        -v|--verbose)
            verbose=y
            shift 1
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Programming error"
            exit 1
            ;;
    esac
done

if [ $# -ne 0 ]; then
    echo "$(basename $0): Unknown options: $@"
    exit 1
fi

case "$cmd" in
    define)
        if [ -n "$jsonfile" ]; then
            if [ ! -r "$jsonfile" ]; then
                echo "Unable to read file $jsonfile" >&2
                exit 1
            fi

            if [ -n "$type" ]; then
                echo "Device type cannot be specified separately from $jsonfile" >&2
                exit 1
            fi

            if [ -z "$parent" ]; then
                echo "Parent device required to define device via $jsonfile" >&2
                exit 1
            fi

            if [ -z "$uuid" ]; then
                uuid=$(unique_uuid)
                print_uuid="echo $uuid"
            fi

            if [ -e "$persist_base/$parent/$uuid" ]; then
                echo "Cowardly refusing to overwrite existing config for $parent/$uuid" >&2
                exit 1
            fi

            set -o errexit

            read_config "$jsonfile"
            if [ $? -ne 0 ]; then
                echo "Error reading $jsonfile" >&2
                exit 1
            fi

            write_config "$persist_base/$parent/$uuid"
            if [ $? -ne 0 ]; then
                exit 1
            fi

            $print_uuid
            exit 0
        fi

        if [ -n "$uuid" ]; then
            if [ -z "$parent" ]; then
                if [ ! -L "$mdev_base/$uuid" ] || [ -n "$type" ]; then
                    usage
                fi

                parent=$(basename $(realpath "$mdev_base/$uuid" | sed -s "s/\/$uuid//"))
                type=$(basename $(realpath "$mdev_base/$uuid/mdev_type"))
            fi

            if [ -e "$persist_base/$parent/$uuid" ]; then
                echo "Device $uuid on $parent already defined, try modify?" >&2
                exit 1
            fi
        else
            uuid=$(unique_uuid)
            print_uuid="echo $uuid"
        fi

        if [ -z "$parent" ]; then
            usage
        fi

        if [ -z "$type" ]; then
            usage
        fi

        if [ -n "$auto" ]; then
            start="auto"
        else
            start="manual"
        fi

        set -o errexit

        mkdir -p "$persist_base/$parent"
        set_config_key mdev_type "$type"
        set_config_key start "$start"
        write_config "$persist_base/$parent/$uuid"
        if [ $? -eq 0 ]; then
            $print_uuid
        fi
        ;;
    undefine)
        if [ -z "$uuid" ]; then
            usage
        fi

        set -o errexit

        if [ -n "$parent" ]; then
            rm -f "$persist_base/$parent/$uuid"
        else
            find "$persist_base" -name "$uuid" -type f | xargs rm -f
        fi
        ;;
    modify)
        if [ -z "$uuid" ]; then
            usage
        fi

        if [ -n "$auto" ] && [ -n "$manual" ]; then
            echo "Options --auto and --manual are mutually exclusive" >&2
            exit 1
        fi

        set -o errexit

        file=$(config_file "$uuid" "$parent")
        if [ $? -ne 0 ]; then
            exit 1
        fi

        read_config "$file"
        if [ $? -ne 0 ]; then
            echo "Config file $file invalid" >&2
            exit 1
        fi

        if [ -n "$type" ]; then
            set_config_key mdev_type "$type"
        fi

        if [ -n "$auto" ]; then
            set_config_key start auto
        fi

        if [ -n "$manual" ]; then
            set_config_key start manual
        fi

        if [ -n "$addattr" ] && [ -n "$delattr" ]; then
            usage
        fi

        if [ -n "$addattr" ]; then
            if [ -n "$index" ]; then
                if [ "$index" -eq "$index" ] 2>/dev/null; then
                    :
                else
                    echo "Provided index is not a number" >&2
                    usage
                fi
            fi

            if [ -z "$value" ]; then
                echo "No attribute value provided" >&2
                usage
            fi

            add_attr_index "$addattr" "$value" "$index"
        fi

        if [ -n "$delattr" ]; then
            if [ -n "$index" ]; then
                if [ "$index" -eq "$index" ] 2>/dev/null; then
                    :
                else
                    echo "Provided index is not a number" >&2
                    usage
                fi
            fi

            del_attr_index "$index"
        fi

        write_config "$file"
        ;;
    start)
        set -o errexit

        if [ -n "$jsonfile" ]; then
            if [ ! -r "$jsonfile" ]; then
                echo "Unable to read file $jsonfile" >&2
                exit 1
            fi

            if [ -n "$type" ]; then
                echo "Device type cannot be specified separately from $jsonfile" >&2
                exit 1
            fi

            if [ -z "$parent" ]; then
                echo "Parent device required to start device via $jsonfile" >&2
                exit 1
            fi

            if [ -z "$uuid" ]; then
                uuid=$(unique_uuid)
                print_uuid="echo $uuid"
            fi

            read_config "$jsonfile"
            if [ $? -ne 0 ]; then
                echo "Error reading $jsonfile" >&2
                exit 1
            fi

            type="$(get_config_key mdev_type)"

            start_mdev "$uuid" "$parent" "$type" "$print_uuid"
            exit $?
        fi

        # We don't implement a placement policy
        if [ -n "$type" ] && [ -z "$parent" ]; then
            usage
        fi

        # The device is not fully specified without TYPE, we must find
        # a config file, with optional PARENT for disambiguation
        if [ -z "$type" ] && [ -n "$uuid" ]; then
            count=$(find "$persist_base" -name "$uuid" -type f | wc -l)
            if [ "$count" -eq 0 ]; then
                echo "Config for $uuid does not exist, define it first?" >&2
                exit 1
            elif [ "$count" -gt 1 ]; then
                if [ -z "$parent" ] || [ ! -e "$persist_base/$parent/$uuid" ]; then
                    echo "Multiple configs found for $uuid, specify a parent" >&2
                    exit 1
                fi
                file="$persist_base/$parent/$uuid"
            else
                file=$(find "$persist_base" -name "$uuid" -type f)
                if [ -n "$parent" ]; then
                    cur_parent=$(basename $(echo "$file" | sed -s "s/\/$uuid//"))
                    if [ "$cur_parent" != "$parent" ]; then
                        echo "Config for $parent/$uuid does not exist, define it first?" >&2
                        exit 1
                    fi
                fi
            fi

            read_config "$file"
            if [ $? -ne 0 ]; then
                echo "Config file $file invalid" >&2
                exit 1
            fi

            if [ -z "$parent" ]; then
                parent=$(basename $(echo "$file" | sed -s "s/\/$uuid//"))
            fi

            type="$(get_config_key mdev_type)"
        fi

        if [ -z "$uuid" ]; then
            # Device must be full specified otherwise for generated UUID
            if [ -z "$parent" ] || [ -z "$type" ]; then
                echo "Device is insufficiently specified" >&2
                usage
            fi
            uuid=$(unique_uuid)
            print_uuid="echo $uuid"
        fi

        start_mdev "$uuid" "$parent" "$type" "$print_uuid"
        exit $?
        ;;
    stop)
        if [ -z "$uuid" ]; then
            usage
        fi

        set -o errexit

        remove_mdev "$uuid"
        ;;
    list)
        json="[]"
        txt=""

        if [ -n "$defined" ]; then
            for dir in $(find "$persist_base/" -maxdepth 1 -mindepth 1 -type d | sort); do
                p=$(basename "$dir")
                if [ -n "$parent" ] && [ "$parent" != "$p" ]; then
                    continue
                fi

                for mdev in $(find "$dir/" -maxdepth 1 -mindepth 1 -type f); do
                    u=$(basename "$mdev")
                    if [ -n "$uuid" ] && [ "$uuid" != "$u" ]; then
                        continue
                    fi

                    read_config "$mdev"
                    if [ $? -ne 0 ]; then
                        continue
                    fi

                    type="$(get_config_key mdev_type)"
                    start="$(get_config_key start)"

                    txt+="$u $p $type $start"

                    if [ -L "$mdev_base/$u" ]; then
                        cur_parent=$(basename $(realpath "$mdev_base/$u" | sed -s "s/\/$u//"))
                        if [ "$cur_parent" == "$p" ]; then
                            cur_type=$(basename $(realpath "$mdev_base/$u/mdev_type"))
                            if [ "$cur_type" == "$type" ]; then
                                txt+=" (active)"
                            fi
                        fi
                    fi

                    json_tmp="{\"$p\":[{\"$u\":{"\"mdev_type\":\"$type\"",\"start\":\"$start\""
                    txt+="\n"

                    if [ -n "$verbose" ] || [ -n "$dumpjson" ]; then
                        count=$(( $(get_attr_length) - 1 ))
                        if [ $count -ge 0 ]; then
                            json_tmp+=",\"attrs\":$(get_attrs_raw)"
                            txt+="  Attrs:\n"
                            for i in $(seq 0 "$count"); do
                                txt+="    @{$i}: $(get_attr_index_raw $i)\n"
                            done
                        fi
                    fi
                    json_tmp+="}}]}"
                    json=$(echo "$json" | jq -c -M --argjson obj "$json_tmp" '. + [$obj]')
                done
            done
        else
            if [ ! -d "$mdev_base" ]; then
                exit 0
            fi

            for mdev in $(find "$mdev_base/" -maxdepth 1 -mindepth 1 -type l); do
                u=$(basename "$mdev")
                if [ -n "$uuid" ] && [ "$uuid" != "$u" ]; then
                    continue
                fi

                p=$(basename $(realpath "$mdev_base/$u" | sed -s "s/\/$u//"))
                if [ -n "$parent" ] && [ "$parent" != "$p" ]; then
                    continue
                fi

                type=$(basename $(realpath "$mdev/mdev_type"))

                json_tmp="{\"$p\":[{\"$u\":{\"mdev_type\":\"$type\"}}]}"
                txt+="$u $p $type"

                if [ -f "$persist_base/$p/$u" ]; then
                    read_config "$persist_base/$p/$u"
                    if [ $? -eq 0 ] && [ "$(get_config_key mdev_type)" == "$type" ]; then
                        txt+=" (defined)"
                    fi
                fi

                txt+="\n"
                json=$(echo "$json" | jq -c -M --argjson obj "$json_tmp" '. + [$obj]')

            done
        fi

        if [ -n "$dumpjson" ]; then
            if [ $(echo "$json" | jq 'length') -gt 0 ]; then
                # https://stackoverflow.com/a/43337323/4775714
                json=$(echo "$json" | jq -c -M '[reduce .[] as $o ({}; reduce ($o|keys)[] as $key (.; .[$key] += $o[$key] ))]')
            fi

            # If specified to a single device, output such that it can be
            # piped into a config file, else print entire hierarchy
            if [ -n "$uuid" ] && [ $(echo "$json" | jq -M 'length') -eq 1 ] &&
               [ $(echo "$json" | jq -M '.[] | length') -eq 1 ] &&
               [ $(echo "$json" | jq -M '.[] | .[] | length') -eq 1 ]; then
                key=$(echo "$json" | jq -r -M '.[] | .[] | .[] | keys | .[]')
                key=$(jsonify $key)
                echo "$json" | jq -M --argjson key "$key" '.[] | .[] | .[] | .[$key]'
            else
                echo "$json" | jq -M '.'
            fi
        else
            echo -en "$txt"
        fi
        ;;
    types)
        if [ ! -d "$parent_base" ]; then
            if [ -n "$dumpjson" ]; then
                echo "[]" | jq -M '.'
            fi
            exit 0
        fi

        json="[]"
        txt=""

        for dir in $(find "$parent_base/" -maxdepth 1 -mindepth 1 -type l | sort); do
            p=$(basename "$dir")
            if [ -n "$parent" ] && [ "$parent" != "$p" ]; then
                continue
            fi

            txt+="$p\n"

            for parent_type in $(find "$dir/mdev_supported_types/" -maxdepth 1 -mindepth 1 -type d | sort); do
                type=$(basename "$parent_type")
                txt+="  $type\n"

                avail=$(cat "$parent_type/available_instances")
                txt+="    Available instances: $avail\n"

                api=$(cat "$parent_type/device_api")
                txt+="    Device API: $api\n"

                json_tmp="{\"$p\":[{\"$type\":{\"available_instances\":$avail,\"device_api\":\"$api\""

                if [ -e "$parent_type/name" ]; then
                    name=$(cat "$parent_type/name")
                    json_tmp+=",\"name\":\"$name\""
                    txt+="    Name: $name\n"
                fi

                if [ -e "$parent_type/description" ]; then
                    descr=$(cat "$parent_type/description" | sed -e ':a;N;$!ba;s/\n/, /g')
                    json_tmp+=",\"description\":\"$descr\""
                    txt+="    Description: $descr\n"
                fi

                json_tmp+="}}]}"
                json=$(echo "$json" | jq -c -M --argjson obj "$json_tmp" '. + [$obj]')
            done
        done

        if [ -n "$dumpjson" ]; then
            if [ $(echo "$json" | jq 'length') -gt 0 ]; then
                # https://stackoverflow.com/a/43337323/4775714
                json=$(echo "$json" | jq -c -M '[reduce .[] as $o ({}; reduce ($o|keys)[] as $key (.; .[$key] += $o[$key] ))]')
            fi

            echo "$json" | jq -M '.'
        else
            echo -en "$txt"
        fi
        ;;
esac