1#!/bin/bash 2# Copyright 2021 Google LLC 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16[ -n "${ipmi_fru_init-}" ] && return 17 18# shellcheck disable=SC2034 19IPMI_FRU_COMMON_HEADER_INTERNAL_OFFSET_IDX=1 20# shellcheck disable=SC2034 21IPMI_FRU_COMMON_HEADER_CHASSIS_OFFSET_IDX=2 22# shellcheck disable=SC2034 23IPMI_FRU_COMMON_HEADER_BOARD_OFFSET_IDX=3 24# shellcheck disable=SC2034 25IPMI_FRU_COMMON_HEADER_PRODUCT_OFFSET_IDX=4 26# shellcheck disable=SC2034 27IPMI_FRU_COMMON_HEADER_MULTI_RECORD_OFFSET_IDX=5 28# shellcheck disable=SC2034 29IPMI_FRU_AREA_HEADER_SIZE_IDX=1 30# shellcheck disable=SC2034 31IPMI_FRU_CHECKSUM_IDX=-1 32 33offset_1bw() { 34 local addr="$1" 35 local off="$2" 36 local extra="${3-0}" 37 echo "w$((1+extra))@$addr $(( off & 0xff ))" 38} 39 40offset_2bw() { 41 local addr="$1" 42 local off="$2" 43 local extra="${3-0}" 44 echo "w$((2+extra))@$addr $(( (off >> 8) & 0xff )) $(( off & 0xff ))" 45} 46 47of_name_to_eeproms() { 48 local names 49 if ! names="$(grep -xl "$1" /sys/bus/i2c/devices/*/of_node/name)"; then 50 echo "Failed to find eeproms with of_name '$1'" >&2 51 return 1 52 fi 53 echo "${names//of_node\/name/eeprom}" 54} 55 56of_name_to_eeprom() { 57 local eeproms 58 eeproms="$(of_name_to_eeproms "$1")" || return 59 if (( "$(echo "$eeproms" | wc -l)" != 1 )); then 60 echo "Got more than one eeprom for '$1': $eeproms" >&2 61 return 1 62 fi 63 echo "$eeproms" 64} 65 66# Each element is a `filename` 67declare -A IPMI_FRU_EEPROM_FILE=() 68# Each element is a `bus` + `addr` + `offset_bytes_func` 69declare -A IPMI_FRU_EEPROM_BUSADDR=() 70 71ipmi_fru_device_alloc() { 72 local fdn="$1" 73 local idx="$2" 74 75 local json 76 json="$(busctl -j call xyz.openbmc_project.FruDevice \ 77 /xyz/openbmc_project/FruDevice/"$fdn" org.freedesktop.DBus.Properties \ 78 GetAll s xyz.openbmc_project.FruDevice)" || return 80 79 80 local jqq='.data[0] | (.BUS.data | tostring) + " " + (.ADDRESS.data | tostring)' 81 local busaddr 82 # shellcheck disable=SC2207 83 busaddr=($(echo "$json" | jq -r "$jqq")) || return 84 85 # FRU 0 is hardcoded and FruDevice does not report the correct bus for it 86 # Hardcode a workaround for this specifically known bus 87 if (( busaddr[0] == 0 && busaddr[1] == 0 )); then 88 IPMI_FRU_EEPROM_FILE["$idx"]=/etc/fru/baseboard.fru.bin 89 else 90 local offset_bw=offset_2bw 91 local last=0 92 local rsp=0x100 93 local start=$SECONDS 94 # Query the FRU multiple times to ensure the return value is stabilized 95 while (( last != rsp )); do 96 # It shouldn't take > 0.1s to stabilize, limit instability 97 if (( SECONDS - start >= 10 )); then 98 echo "Timed out determining offset for ${busaddr[0]}@${busaddr[1]}" >&2 99 return 1 100 fi 101 last=$rsp 102 # shellcheck disable=SC2046 103 rsp=$(i2ctransfer -f -y "${busaddr[0]}" $(offset_1bw "${busaddr[1]}" 0) r1) || return 104 done 105 # FRUs never start with 0xff bytes, so we can figure out addressing mode 106 if (( rsp != 0xff )); then 107 offset_bw=offset_1bw 108 fi 109 IPMI_FRU_EEPROM_BUSADDR["$idx"]="${busaddr[*]} $offset_bw" 110 fi 111} 112 113ipmi_fru_alloc() { 114 local name="$1" 115 local -n ret="$2" 116 117 # Pick the first free index to return as the allocated entry 118 for (( ret = 0; ret < "${#IPMI_FRU_EEPROM_FILE[@]}"; ++ret )); do 119 [ -n "${IPMI_FRU_EEPROM_FILE[*]+1}" ] || \ 120 [ -n "${IPMI_FRU_EEPROM_BUSADDR[*]+1}" ]|| break 121 done 122 123 if [[ "$name" =~ ^of-name:(.*)$ || "$name" =~ ^([^:]*)$ ]]; then 124 local ofn="${BASH_REMATCH[1]}" 125 local file 126 file="$(of_name_to_eeprom "$ofn")" || return 127 IPMI_FRU_EEPROM_FILE["$ret"]="$file" 128 elif [[ "$name" =~ ^frudev-name:(.*)$ ]]; then 129 local fdn="${BASH_REMATCH[1]}" 130 local start=$SECONDS 131 local file 132 while (( SECONDS - start < 300 )); do 133 local rc=0 134 ipmi_fru_device_alloc "$fdn" "$ret" || rc=$? 135 (( rc == 0 )) && break 136 # Immediately return any errors, 80 is special to signify retry 137 (( rc != 80 )) && return $rc 138 sleep 1 139 done 140 else 141 echo "Invalid IPMI FRU eeprom specification: $name" >&2 142 return 1 143 fi 144} 145 146ipmi_fru_free() { 147 unset 'IPMI_FRU_EEPROM_FILE[$1]' 148 unset 'IPMI_FRU_EEPROM_BUSADDR[$1]' 149} 150 151checksum() { 152 local -n checksum_arr="$1" 153 local checksum=0 154 for byte in "${checksum_arr[@]}"; do 155 checksum=$((checksum + byte)) 156 done 157 echo $((checksum & 0xff)) 158} 159 160fix_checksum() { 161 local -n fix_checksum_arr="$1" 162 old_cksum=${fix_checksum_arr[$IPMI_FRU_CHECKSUM_IDX]} 163 ((fix_checksum_arr[IPMI_FRU_CHECKSUM_IDX]-=$(checksum fix_checksum_arr))) 164 ((fix_checksum_arr[IPMI_FRU_CHECKSUM_IDX]&=0xff)) 165 printf 'Corrected %s checksum from 0x%02X -> 0x%02X\n' \ 166 "$1" "${old_cksum}" "${fix_checksum_arr[$IPMI_FRU_CHECKSUM_IDX]}" >&2 167} 168 169read_bytes() { 170 # shellcheck disable=SC2206 171 local busaddr=(${IPMI_FRU_EEPROM_BUSADDR["$1"]-}) 172 local file="${IPMI_FRU_EEPROM_FILE["$1"]-$1}" 173 local offset="$2" 174 local size="$3" 175 176 if (( "${#busaddr[@]}" > 0)); then 177 echo "Reading ${busaddr[*]} at $offset for $size" >&2 178 # shellcheck disable=SC2046 179 i2ctransfer -f -y "${busaddr[0]}" $("${busaddr[2]}" "${busaddr[1]}" "$offset") "r$size" 180 else 181 echo "Reading $file at $offset for $size" >&2 182 dd if="$file" bs=1 count="$size" skip="$offset" 2>/dev/null | \ 183 hexdump -v -e '1/1 "%u "' 184 fi 185} 186 187write_bytes() { 188 # shellcheck disable=SC2206 189 local busaddr=(${IPMI_FRU_EEPROM_BUSADDR["$1"]-}) 190 local file="${IPMI_FRU_EEPROM_FILE["$1"]-$1}" 191 local offset="$2" 192 local -n bytes_arr="$3" 193 194 if (( "${#busaddr[@]}" > 0)); then 195 echo "Writing ${busaddr[*]} at $offset for ${#bytes_arr[@]}" >&2 196 # shellcheck disable=SC2046 197 i2ctransfer -f -y "${busaddr[0]}" $("${busaddr[2]}" "${busaddr[1]}" "$offset" "${#bytes_arr[@]}") "${bytes_arr[@]}" 198 else 199 local hexstr 200 hexstr="$(printf '\\x%x' "${bytes_arr[@]}")" || return 201 echo "Writing $file at $offset for ${#bytes_arr[@]}" >&2 202 # shellcheck disable=SC2059 203 printf "$hexstr" | dd of="$file" bs=1 seek="$offset" 2>/dev/null 204 fi 205} 206 207read_header() { 208 local eeprom="$1" 209 local -n header_arr="$2" 210 211 # shellcheck disable=SC2207 212 header_arr=($(read_bytes "$eeprom" 0 8)) || return 213 echo "Checking $eeprom FRU Header version" >&2 214 # FRU header is always version 1 215 (( header_arr[0] == 1 )) || return 216 echo "Checking $eeprom FRU Header checksum" >&2 217 local sum 218 sum="$(checksum header_arr)" || return 219 # Checksums should be valid 220 (( sum == 0 )) || return 10 221} 222 223read_area() { 224 local eeprom="$1" 225 local offset="$2" 226 local -n area_arr="$3" 227 local area_size="${4-0}" 228 229 offset=$((offset*8)) 230 # shellcheck disable=SC2207 231 area_arr=($(read_bytes "$eeprom" "$offset" 8)) || return 232 echo "Checking $eeprom $offset FRU Area version" >&2 233 # FRU Area is always version 1 234 (( area_arr[0] == 1 )) || return 235 if (( area_size == 0 )); then 236 area_size=${area_arr[$IPMI_FRU_AREA_HEADER_SIZE_IDX]} 237 fi 238 # shellcheck disable=SC2207 239 area_arr=($(read_bytes "$eeprom" "$offset" $((area_size*8)))) || return 240 echo "Checking $eeprom $offset FRU Area checksum" >&2 241 local sum 242 sum="$(checksum area_arr)" || return 243 # Checksums should be valid 244 (( sum == 0 )) || return 10 245} 246 247ipmi_fru_init=1 248