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