| #!/bin/bash |
| # Copyright 2021 Google LLC |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| [ -n "${ipmi_fru_init-}" ] && return |
| |
| # shellcheck disable=SC2034 |
| IPMI_FRU_COMMON_HEADER_INTERNAL_OFFSET_IDX=1 |
| # shellcheck disable=SC2034 |
| IPMI_FRU_COMMON_HEADER_CHASSIS_OFFSET_IDX=2 |
| # shellcheck disable=SC2034 |
| IPMI_FRU_COMMON_HEADER_BOARD_OFFSET_IDX=3 |
| # shellcheck disable=SC2034 |
| IPMI_FRU_COMMON_HEADER_PRODUCT_OFFSET_IDX=4 |
| # shellcheck disable=SC2034 |
| IPMI_FRU_COMMON_HEADER_MULTI_RECORD_OFFSET_IDX=5 |
| # shellcheck disable=SC2034 |
| IPMI_FRU_AREA_HEADER_SIZE_IDX=1 |
| # shellcheck disable=SC2034 |
| IPMI_FRU_CHECKSUM_IDX=-1 |
| |
| offset_1bw() { |
| local addr="$1" |
| local off="$2" |
| local extra="${3-0}" |
| echo "w$((1+extra))@$addr $(( off & 0xff ))" |
| } |
| |
| offset_2bw() { |
| local addr="$1" |
| local off="$2" |
| local extra="${3-0}" |
| echo "w$((2+extra))@$addr $(( (off >> 8) & 0xff )) $(( off & 0xff ))" |
| } |
| |
| of_name_to_eeproms() { |
| local names |
| if ! names="$(grep -xl "$1" /sys/bus/i2c/devices/*/of_node/name)"; then |
| echo "Failed to find eeproms with of_name '$1'" >&2 |
| return 1 |
| fi |
| echo "${names//of_node\/name/eeprom}" |
| } |
| |
| of_name_to_eeprom() { |
| local eeproms |
| eeproms="$(of_name_to_eeproms "$1")" || return |
| if (( "$(echo "$eeproms" | wc -l)" != 1 )); then |
| echo "Got more than one eeprom for '$1': $eeproms" >&2 |
| return 1 |
| fi |
| echo "$eeproms" |
| } |
| |
| # Each element is a `filename` |
| declare -A IPMI_FRU_EEPROM_FILE=() |
| # Each element is a `bus` + `addr` + `offset_bytes_func` |
| declare -A IPMI_FRU_EEPROM_BUSADDR=() |
| |
| ipmi_fru_device_alloc() { |
| local fdn="$1" |
| local idx="$2" |
| |
| local json |
| json="$(busctl -j call xyz.openbmc_project.FruDevice \ |
| /xyz/openbmc_project/FruDevice/"$fdn" org.freedesktop.DBus.Properties \ |
| GetAll s xyz.openbmc_project.FruDevice)" || return 80 |
| |
| local jqq='.data[0] | (.BUS.data | tostring) + " " + (.ADDRESS.data | tostring)' |
| local busaddr |
| # shellcheck disable=SC2207 |
| busaddr=($(echo "$json" | jq -r "$jqq")) || return |
| |
| # FRU 0 is hardcoded and FruDevice does not report the correct bus for it |
| # Hardcode a workaround for this specifically known bus |
| if (( busaddr[0] == 0 && busaddr[1] == 0 )); then |
| IPMI_FRU_EEPROM_FILE["$idx"]=/etc/fru/baseboard.fru.bin |
| else |
| local offset_bw=offset_2bw |
| local last=0 |
| local rsp=0x100 |
| local start=$SECONDS |
| # Query the FRU multiple times to ensure the return value is stabilized |
| while (( last != rsp )); do |
| # It shouldn't take > 0.1s to stabilize, limit instability |
| if (( SECONDS - start >= 10 )); then |
| echo "Timed out determining offset for ${busaddr[0]}@${busaddr[1]}" >&2 |
| return 1 |
| fi |
| last=$rsp |
| # shellcheck disable=SC2046 |
| rsp=$(i2ctransfer -f -y "${busaddr[0]}" $(offset_1bw "${busaddr[1]}" 0) r1) || return |
| done |
| # FRUs never start with 0xff bytes, so we can figure out addressing mode |
| if (( rsp != 0xff )); then |
| offset_bw=offset_1bw |
| fi |
| IPMI_FRU_EEPROM_BUSADDR["$idx"]="${busaddr[*]} $offset_bw" |
| fi |
| } |
| |
| ipmi_fru_alloc() { |
| local name="$1" |
| local -n ret="$2" |
| |
| # Pick the first free index to return as the allocated entry |
| for (( ret = 0; ret < "${#IPMI_FRU_EEPROM_FILE[@]}"; ++ret )); do |
| [ -n "${IPMI_FRU_EEPROM_FILE[*]+1}" ] || \ |
| [ -n "${IPMI_FRU_EEPROM_BUSADDR[*]+1}" ]|| break |
| done |
| |
| if [[ "$name" =~ ^of-name:(.*)$ || "$name" =~ ^([^:]*)$ ]]; then |
| local ofn="${BASH_REMATCH[1]}" |
| local file |
| file="$(of_name_to_eeprom "$ofn")" || return |
| IPMI_FRU_EEPROM_FILE["$ret"]="$file" |
| elif [[ "$name" =~ ^frudev-name:(.*)$ ]]; then |
| local fdn="${BASH_REMATCH[1]}" |
| local start=$SECONDS |
| local file |
| while (( SECONDS - start < 300 )); do |
| local rc=0 |
| ipmi_fru_device_alloc "$fdn" "$ret" || rc=$? |
| (( rc == 0 )) && break |
| # Immediately return any errors, 80 is special to signify retry |
| (( rc != 80 )) && return $rc |
| sleep 1 |
| done |
| else |
| echo "Invalid IPMI FRU eeprom specification: $name" >&2 |
| return 1 |
| fi |
| } |
| |
| ipmi_fru_free() { |
| unset 'IPMI_FRU_EEPROM_FILE[$1]' |
| unset 'IPMI_FRU_EEPROM_BUSADDR[$1]' |
| } |
| |
| checksum() { |
| local -n checksum_arr="$1" |
| local checksum=0 |
| for byte in "${checksum_arr[@]}"; do |
| checksum=$((checksum + byte)) |
| done |
| echo $((checksum & 0xff)) |
| } |
| |
| fix_checksum() { |
| local -n fix_checksum_arr="$1" |
| old_cksum=${fix_checksum_arr[$IPMI_FRU_CHECKSUM_IDX]} |
| ((fix_checksum_arr[IPMI_FRU_CHECKSUM_IDX]-=$(checksum fix_checksum_arr))) |
| ((fix_checksum_arr[IPMI_FRU_CHECKSUM_IDX]&=0xff)) |
| printf 'Corrected %s checksum from 0x%02X -> 0x%02X\n' \ |
| "$1" "${old_cksum}" "${fix_checksum_arr[$IPMI_FRU_CHECKSUM_IDX]}" >&2 |
| } |
| |
| read_bytes() { |
| # shellcheck disable=SC2206 |
| local busaddr=(${IPMI_FRU_EEPROM_BUSADDR["$1"]-}) |
| local file="${IPMI_FRU_EEPROM_FILE["$1"]-$1}" |
| local offset="$2" |
| local size="$3" |
| |
| if (( "${#busaddr[@]}" > 0)); then |
| echo "Reading ${busaddr[*]} at $offset for $size" >&2 |
| # shellcheck disable=SC2046 |
| i2ctransfer -f -y "${busaddr[0]}" $("${busaddr[2]}" "${busaddr[1]}" "$offset") "r$size" |
| else |
| echo "Reading $file at $offset for $size" >&2 |
| dd if="$file" bs=1 count="$size" skip="$offset" 2>/dev/null | \ |
| hexdump -v -e '1/1 "%d "' |
| fi |
| } |
| |
| write_bytes() { |
| # shellcheck disable=SC2206 |
| local busaddr=(${IPMI_FRU_EEPROM_BUSADDR["$1"]-}) |
| local file="${IPMI_FRU_EEPROM_FILE["$1"]-$1}" |
| local offset="$2" |
| local -n bytes_arr="$3" |
| |
| if (( "${#busaddr[@]}" > 0)); then |
| echo "Writing ${busaddr[*]} at $offset for ${#bytes_arr[@]}" >&2 |
| # shellcheck disable=SC2046 |
| i2ctransfer -f -y "${busaddr[0]}" $("${busaddr[2]}" "${busaddr[1]}" "$offset" "${#bytes_arr[@]}") "${bytes_arr[@]}" |
| else |
| local hexstr |
| hexstr="$(printf '\\x%x' "${bytes_arr[@]}")" || return |
| echo "Writing $file at $offset for ${#bytes_arr[@]}" >&2 |
| # shellcheck disable=SC2059 |
| printf "$hexstr" | dd of="$file" bs=1 seek="$offset" 2>/dev/null |
| fi |
| } |
| |
| read_header() { |
| local eeprom="$1" |
| local -n header_arr="$2" |
| |
| # shellcheck disable=SC2207 |
| header_arr=($(read_bytes "$eeprom" 0 8)) || return |
| echo "Checking $eeprom FRU Header version" >&2 |
| # FRU header is always version 1 |
| (( header_arr[0] == 1 )) || return |
| echo "Checking $eeprom FRU Header checksum" >&2 |
| local sum |
| sum="$(checksum header_arr)" || return |
| # Checksums should be valid |
| (( sum == 0 )) || return 10 |
| } |
| |
| read_area() { |
| local eeprom="$1" |
| local offset="$2" |
| local -n area_arr="$3" |
| local area_size="${4-0}" |
| |
| offset=$((offset*8)) |
| # shellcheck disable=SC2207 |
| area_arr=($(read_bytes "$eeprom" "$offset" 8)) || return |
| echo "Checking $eeprom $offset FRU Area version" >&2 |
| # FRU Area is always version 1 |
| (( area_arr[0] == 1 )) || return |
| if (( area_size == 0 )); then |
| area_size=${area_arr[$IPMI_FRU_AREA_HEADER_SIZE_IDX]} |
| fi |
| # shellcheck disable=SC2207 |
| area_arr=($(read_bytes "$eeprom" "$offset" $((area_size*8)))) || return |
| echo "Checking $eeprom $offset FRU Area checksum" >&2 |
| local sum |
| sum="$(checksum area_arr)" || return |
| # Checksums should be valid |
| (( sum == 0 )) || return 10 |
| } |
| |
| ipmi_fru_init=1 |