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