# -*- shell-script -*-

# 30device-tree - Hardware database scanning routines and variables for
#                 systems with device-tree, with or without ibm,vpd properties.

# This file is part of the Linux lsvpd package.

# (C) Copyright IBM Corp. 2002, 2003, 2004

# Maintained by Martin Schwenke <martins@au.ibm.com>

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
    
# $Id: 30device-tree,v 1.35 2004/06/17 05:34:21 martins Exp $

source_device_tree="/proc/device-tree"
[ -f "${source_device_tree}/system-id" ] || source_device_tree=""

[ -n "$source_device_tree" ] || return 0

######################################################################

if [ -f "${source_device_tree}/device_type" ] ; then
    device_tree_device_type=$(cat "${source_device_tree}/device_type")
fi

db_subdir_vars_hooks="${db_subdir_vars_hooks} db_set_subdir_vars_dt"
db_set_subdir_vars_dt

db_initialise_bus ()
{
    # FIXME: Try testing with this disabled to test "hotplug" code.
    ensure_directory "$db_bus_dir"
    cp -a "$source_device_tree" "$db_bus_dt_dir"
}

######################################################################

# Adapters.

dt_adapter_init_hooks=""

adapter_init_node ()
{
    local type="$1"
    local bus_info="$2"

    local d=$(get_adapter_bus_dir "$type" "$bus_info")

    if [ -z "$d" ] ; then
	debug "adapter_init_node: bus directory not found for \"${bus_info}\""
	return 1
    fi

    # Sanity check.
    if [ "$d" == "${d#${db_bus_dt_dir}}" ] ; then
	debug "adapter_init_node: bus node not under \"${db_bus_dir}\""
	return 1
    fi

    # Find how much of the device tree is missing?  Above tests
    # guarantee loop terminatation.
    while [ ! -d "$d" -a "$d" != "$db_bus_dt_dir" ] ; do
	d="${d%/*}" # dirname
    done

    if [ "$d" != "$db_bus_dt_dir" ] ; then
	# Copy over the missing part of the device tree.
	s="${source_device_tree}${d#${db_bus_dt_dir}}"
	if [ -d "$s" ] ; then
	    ensure_directory "$d"
	    cp -a "${s}/." "${d}/"
	else
	    debug "adapter_init_node: missing device-tree node \"$s\""
	fi
    fi

    local i
    for i in $dt_adapter_init_hooks ; do
	$i "$d"
    done

    case "$bus_info" in
	(pci/*) pci_do_pci22_vpd "${bus_addr%/*}" "$d" ;;
    esac
}

#---------------------------------------------------------------------

do_adapter_crosslink ()
{
    local type="$1"
    local bus_info="$2"
    local name="$3"

    local adapter_bus_dir adapter_os_dir

    do_adapter_crosslink_basic "$type" "$bus_info" "$name"

    # channels: fun for all the family...  :-)
    local i ch
    for i in "${adapter_bus_dir}/${type}@"* ; do
	local ch=$((0x${i##*@}))
	local d="${adapter_os_dir}:${ch}"
	ensure_directory "$d"
	cross_link "$i" "$d"
    done
}

#---------------------------------------------------------------------

list_adapters_DEFAULT ()
{
    local type="$1"

    # Could do a single find for all adapter types (in
    # db_initialise_bus) and create a map, but BusyBox's find doesn't
    # support "-o", so we might as well do a find per type in this
    # function.
    local n bus_info subdir
    for n in $(find "${source_device_tree}" -name "${type}@*" -print) ; do
	bus_info=$(dt_get_bus_info "$n")
	if [ -n "$bus_info" ] ; then
	    local relnode="${db_bus_dt_subdir}/${n#${source_device_tree}/}"
	    bus_alias_set "$bus_info" "$relnode"
            # Has channel?  Also alias without channel.
	    case "$bus_info" in
		*/*/*)
		    bus_alias_set "${bus_info%/*}" "${relnode%/*}" # dirname
		    ;;
	    esac
	    echo "$bus_info"
	else
	    debug "list_adapters: failed to get bus_info for \"${n}\""
	fi
    done
}

list_adapters_scsi ()
{
    list_adapters_DEFAULT "scsi"
    list_adapters_DEFAULT "fibre-channel"
}

#---------------------------------------------------------------------

get_adapter_bus_dir ()
{
    local type="$1"
    local bus_info="$2"

    local node=$(bus_alias_get "$bus_info")

    if [ -z "$node" ] ; then
	case "$bus_info" in
	    pci/*)
		local bus_addr="${bus_info#*/}"
		local pci_addr="${bus_addr%/*}"
		local channel
		case "$bus_addr" in
		    (*/*) channel=""${bus_addr#*/}"" ;;
		esac
		node=$(dt_get_pci_adapter_relnode "$pci_addr")
		if [ -n "$node" -a -n "$channel" ] ; then
		    local subdir="${type}@${channel}"
		    if [ -d "${source_device_tree}/${node}/${subdir}" ] ; then
			node="${node}/${subdir}"
		    fi
		fi
		;;
	    *)
		debug "get_adapter_bus_dir: unknown bus type in \"$bus_info\""
	esac

	if [ -n "$node" ] ; then
	    local relnode="${db_bus_dt_subdir}/${node}"
	    bus_alias_set "$bus_info" "$relnode"
            # Has channel?  Also alias without channel.
	    case "$bus_info" in
		*/*/*)
		    bus_alias_set "${bus_info%/*}" "${relnode%/*}" # dirname
		    ;;
	    esac
	    node="${db_bus_dt_dir}/${node}"
	fi
    fi

    [ -n "$node" ] && echo "$node"
}

#---------------------------------------------------------------------

do_adapter_DEFAULT ()
{
    local type="$1"
    local bus_info="$2" # bus_type/dom:bus:dev.fun[/channel]

    local node=$(get_adapter_bus_dir "$type" "$bus_info")

    if [ ! -n "$node" ] ; then
	debug "do_adapter_DEFAULT: failed to get node for \"$bus_info\""
	return
    fi

    [ ! -d "$node" ] && \
	adapter_init_node "$type" "$bus_info"

    local ds yl
    do_adapter_hook "$type"

    local vpd_dir

    # Assumption: if $node is for a channel in a multi-channel
    # adapter, it will not contain a "class-code" property.
    local have_parent_vpd=false
    if [ ! -f "${node}/class-code" ] ; then 
	vpd_dir_set_hook "${node%/*}" # dirname
	if [ -e "$vpd_dir" ] ; then
	    have_parent_vpd=true
	    # The only things we want to do to the parent VPD are
	    # those in the extra hook.
	    adapter_extra_vpd_hook \
		"$type" "${bus_info%/*}" "${node%/*}" # dirname x 2
	fi
    fi

    # If there's already VPD, ensure YL exists, run extra hooks, and
    # we're done.
    vpd_dir_set_hook "$node"
    if [ -e "$vpd_dir" ] ; then
	vpd_field_ensure "$vpd_dir" "YL" "$yl"
	adapter_extra_vpd_hook "$type" "$bus_info" "$node"
	return 0
    fi

    # Process PCI vendor/device only if parent has no useful VPD.
    local mf tm cd
    $have_parent_vpd || \
	dt_do_pci_adapter_hook "$node"

    # Render.
    local rl rm sn

    vpd_create_hook "$node"

    $have_parent_vpd || \
	adapter_extra_vpd_hook "$type" "$bus_info" "$node"
}

do_adapter_display ()
{
    local bus_info="$1"

    dt_do_adapter_fake "display" "$bus_info"
}

#---------------------------------------------------------------------

# do_adapter_hook:
#
# Uses: type, node
# Sets: ds, yl

unset -f do_adapter_hook_ide

do_adapter_hook_DEFAULT ()
{
    ds=$(get_adapter_ds "$type")

    dt_manufacture_yl_hook "$node"
}

do_adapter_hook_scsi ()
{
    do_adapter_hook_DEFAULT

    # Horrible hack.
    case "$node" in
	(*fibre-channel@*) ds="Fibre Channel Adapter" ;;
    esac

    if [ -f "${node}/ibm,fw-adapter-name" ] ; then
	ds="$(cat ${node}/ibm,fw-adapter-name) ${ds}"
	if [ -f "${node}/wide" ] ; then
	    ds="Wide/${ds}"
	fi
    fi
}

do_adapter_hook_ethernet ()
{
    do_adapter_hook_DEFAULT

    if [ -f "${node}/ibm,fw-adapter-name" ] ; then
	ds="$(cat ${node}/ibm,fw-adapter-name)"
    fi
}

######################################################################

# Devices.

do_device_hook_scsi ()
{
    # Uses: bus_info, adapter
    # Sets: adapter_os_dir, device_bus_dir, yl

    local adapter_bus_dir

    if ! do_device_hook_common ; then
	debug "do_device_hook_scsi: no device-tree node for $adapter"
	return 1
    fi

    local host bus target lun
    scsi_parse_addr_hook "${bus_info#*/}"
    
    # Check for multi-channel card.
    local bus_hex=$(dec2hex "$bus")
    local maybe="${adapter_bus_dir}/scsi@${bus_hex}"
    if [ -d "$maybe" ] ; then
	adapter_bus_dir="$maybe"
    fi
    
    dt_do_device_hook_post "$device_type" "$target" "$lun"
}

do_device_hook_ide ()
{
    # Uses: bus_info, adapter
    # Sets:  adapter_os_dir, device_bus_dir, yl

    local adapter_bus_dir

    if ! do_device_hook_common ; then
	debug "do_device_hook_ide: no device-tree node for $adapter"
	return 1
    fi

    local dt_type
    case "$device_type" in
	cdrom) dt_type="disk"         ;;
	*)     dt_type="$device_type" ;;
    esac
    
    # FIXME: Untested on a complex IDE system.
    local ide_addr="${bus_info#*/}"
    dt_do_device_hook_post "$dt_type" "${ide_addr%.*}" "${ide_addr#*.}"
}

dt_do_device_hook_post ()
{
    # Sets: device_bus_dir, yl

    local device_type="$1"
    local id="$2"
    local subid="$3"

    local id_hex=$(dec2hex "$id")
    local subid_hex=$(dec2hex "$subid")

    device_bus_dir="${adapter_bus_dir}/${device_type}@${id_hex}"
    if [ $(hex2dec "$subid_hex") -ne 0 ] ; then
	device_bus_dir="${device_bus_dir},${subid_hex}"
    fi

    local t=$(pseudo_realpath "$device_bus_dir")
    bus_alias_set "$bus_info" "${t#${db_bus_dir}/}"

    if [ -f "${device_bus_dir}/ibm,loc-code" ] ; then
	dt_manufacture_yl_hook "$device_bus_dir"
    else
	dt_manufacture_yl_hook "$adapter_bus_dir"
	dt_device_yl_hook
    fi
}

dt_device_yl_hook ()
{
    # Uses: yl, id, subid, id_hex, subid_hex
    # Sets: yl
    if [ -n "$yl" ] ; then
	yl="${yl}-A${id_hex}"
	if [ "$subid_hex" != "0" ] ; then
	    yl="${yl},${subid_hex}"
	fi
    fi
}

######################################################################

dt_check_bus_range ()
{
    local bus_range_file="$1"
    local bus_num=$(hex2dec "$2")

    # bus-range contains 2 (network order) longs, but only the bottom
    # byte can be non-zero.
    set -- $(tdump "$bus_range_file")
    local min=$(hex2dec "$4")
    local max=$(hex2dec "$8")
    if [ "$min" -le "$bus_num" -a "$bus_num" -le "$max" ] ; then
	return 0
    else
	return 1
    fi
}

dt_check_bus_num ()
{
    local bus_range_file="$1"
    local bus_num=$(hex2dec "$2")

    # bus-range contains 2 (network order) longs, but only the bottom
    # byte can be non-zero.
    set -- $(tdump "$bus_range_file")
    local min=$(hex2dec "$4")

    # Only the minimum bus number in the range on a PCI-PCI bridge is
    # for the things hanging off that bridge.  The other (higher)
    # numbers in the range are for buses behind PCI-PCI bridges that
    # are behind this one.
    if [ "$min" -eq "$bus_num" ] ; then
	return 0
    else
	return 1
    fi
}

dt_check_pci_domain ()
{
    local pci_domain_file="$1"
    local dom=$(hex2dec "$2")

    # $pci_domain contains an unsigned long in network order.
    set -- $(tdump "$pci_domain_file")
    local t=$((0x$1 * 4096 + 0x$2 * 256 + 0x$3 * 16 + 0x$4))
    if [ $t -eq $dom ] ; then
	return 0
    else
	return 1
    fi
}

dt_get_pci_adapter_relnode ()
{
    local pci_addr="$1"

    local dom bus dev fun
    pci_parse_addr_hook "$pci_addr"

    local phb=""

    local pci_domain_prop i
    pci_domain_prop=$(dt_pci_domain_prop)

    # Find the top-level PCI host bus directory.
    if [ -n "$pci_domain_prop" ] ; then
	# This is a bignum machine.
	for i in ${source_device_tree}/pci@*/${pci_domain_prop} ; do
	    if dt_check_pci_domain "$i" "$dom" ; then
		phb="${i%/*}" # dirname
		# There will only be one match.
		break
	    fi
	done
    else
	# Normal machine.
	for i in ${source_device_tree}/pci@*/bus-range ; do
	    if dt_check_bus_range "$i" "$bus" ; then
		phb="${i%/*}" # dirname
		# There will only be one match.
		break
	    fi
	done
    fi

    if [ -z "$phb" ] ; then
	debug "dt_find_pci_adapter: failed to find PHB for ${dom}:${bus}:${dev}.${fun}"
	return 1
    fi

    # Search for bus number.
    local dir="$phb"
    while : ; do
	local br="${dir}/bus-range"
	if dt_check_bus_num "$br" "$bus" ; then
	    # Got it!  Removed any leading "0x".
	    local devfun=$(printf "%x" "$dev")
	    # ",$func" gets appended if $func is non-zero.
	    if [ $(hex2dec "$fun") -ne 0 ] ; then
		devfun="${devfun},"$(printf "%x" "$fun")
	    fi
	    local out=$(echo "${dir}/"?*"@${devfun}")
	    echo "${out#${source_device_tree}/}"
	    break
	else
	    # Find the next candidate.
	    for i in "${dir}/pci@"* ; do
		br="${i}/bus-range"
		if [ -f "$br" ] && dt_check_bus_range "$br" "$bus" ; then
		    dir="$i"
		    continue 2
		fi
	    done

	    # No next candidate?  Something's gone horribly wrong!
	    debug "dt_find_pci_adapter: Internal error - could not find PCI bus $bus in $phb"
	    return 1
	fi
    done

    return 0
}

######################################################################

dt_pci_domain_prop ()
{
    local d="${source_device_tree}/pci"
    local big_bus_num_flag="${source_device_tree}/rtas/ibm,read-pci-config"

    # Under 2.6, linux,pci-domain is always used.
    if [ -f "${d}/linux,pci-domain" ] ; then
	echo "linux,pci-domain"
	return
    fi

    # Under 2.4, only take notice of these on big-bus-num machines.
    if [ -f "$big_bus_num_flag" ] ; then
	local i
	for i in "linux,phbnum" "linux,global-number" ; do
	    if [ -f "${d}/${i}" ] ; then
		echo "$i"
		return
	    fi
	done
    fi
    
    # Sanity check.
    [ -f "$big_bus_num_flag" ] && \
	: "dt_pci_domain_prop: unable to determine PCI domain property name"
}

######################################################################

dt_do_adapter_fake ()
{
    local type="$1"
    local bus_info="$2"

    local node=$(get_adapter_bus_dir "$type" "$bus_info")

    if [ ! -n "$node" ] ; then
	debug "do_adapter_DEFAULT: failed to get node for \"$bus_info\""
	return
    fi

    if [ ! -d "$node" ] ; then
	adapter_init_node "$type" "$bus_info"
    fi
}

######################################################################

dt_manufacture_yl_hook ()
{
    # Sets: yl
    yl=""

    local d="$1"
    while [ ! -r "$d/ibm,loc-code" -a "$d" != "$db_bus_dt_dir" ] ; do
	d="${d%/*}" # dirname
    done

    [ -r "$d/ibm,loc-code" ] && { read yl <"${d}/ibm,loc-code" ; true ; }
}

######################################################################

dt_do_pci_adapter_hook ()
{
    # Sets: ds, mf, tm, cd

    local node="$1"

    local subvendor subdevice
    if [ -f "${node}/subsystem-vendor-id" -a -f "${node}/subsystem-id" ] ; then
	set -- $(tdump "${node}/subsystem-vendor-id")
	local b1="$3"
	local b2="$4"
	set -- $(tdump "${node}/subsystem-id")
	local b3="$3"
	local b4="$4"

	subvendor="${b1}${b2}"
	subdevice="${b3}${b4}"
	cd="${subvendor}${subdevice}"

	local dscd="${b2}${b1}${b4}${b3}"
	[ -n "$dscd" ] && ds="${ds} (${dscd})"
    fi

    local vendor device
    if [ -f "${node}/vendor-id" -a -f "${node}/device-id" ] ; then
	set -- $(tdump "${node}/vendor-id")
	local b1="$3"
	local b2="$4"
	set -- $(tdump "${node}/device-id")
	local b3="$3"
	local b4="$4"

	vendor="${b1}${b2}"
	device="${b3}${b4}"
    fi

    pci_set_mf_tm_hook
}

######################################################################

dt_get_bus_info ()
{
    local node="$1"

    local bus_type dom bus dev fun channel

    # Sanity check to ensure the loop below will terminate.
    [ "${node#${source_device_tree}}" = "$node" ] && return

    # Get property name for PCI domain on IBM pSeries big-bus-num machines.
    local pci_domain_prop=$(dt_pci_domain_prop)

    local d="$node"

    while [ "$d" != "$source_device_tree" ] ; do
	local device_type=""
	[ -f "${d}/device_type" ] && read device_type <"${d}/device_type"

	case "$device_type" in
	    pci)
	        # The range bottom of the 1st PCI bus-range we hit is the bus#.
		if [ -z "$bus" ] ; then
		    set -- $(tdump "${d}/bus-range")
		    bus_type="pci"
		    bus="$4"
		fi
		
	        # Found a PCI domain property: remember it & we're done!
		if [ -n "$pci_domain_prop" -a \
		    -f "${d}/${pci_domain_prop}" ] ; then
		    dom=$(tdump "${d}/${pci_domain_prop}")
		    dom="${dom// /}"
		    break
		fi
		;;
	    *)
	        # Channel possibly only in original basename.
		[ -z "$channel" ] && channel="$dev"
		
	        # Adapter node?  Put dev[,fun] into $dev. 
		local bn="${d##*/}" # basename
		dev="${bn#*@}"
		;;	    
	esac

	d="${d%/*}" # dirname
    done

    if [ -n "$bus" -a -n "$dev" ] ; then
	[ -n "$dom" ] || dom=0

	fun=0
	case "$dev" in
	    # Note: order is important here.  :-)
	    (*,*) fun="${dev#*,}" ; dev="${dev%,*}" ;;
	esac

	local result=$(printf "%04x:%02x:%02x.%x" \
	    "0x${dom}" "0x${bus}" "0x${dev}" "0x${fun}")

	[ -n "$channel" ] && result="${result}/${channel}"

	echo "${bus_type}/${result}"
    fi
}
