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