blob: a4899c73a7da05bba4f357583d13e23881c3633b [file] [log] [blame]
Brandon Kimdab96f12021-02-18 11:21:37 -08001# Copyright 2021 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
William A. Kennington III7d6fa422021-02-08 17:04:02 -080015# Internal handler used for signalling child processes that they should
16# terminate.
17HandleTerm() {
18 GOT_TERM=1
19 if ShouldTerm && (( ${#CHILD_PIDS[@]} > 0 )); then
20 kill "${!CHILD_PIDS[@]}"
21 fi
22}
23
24# Sets up the signal handler and global variables needed to run interruptible
25# services that can be killed gracefully.
26InitTerm() {
27 declare -g -A CHILD_PIDS=()
28 declare -g GOT_TERM=0
29 declare -g SUPPRESS_TERM=0
30 trap HandleTerm TERM
31}
32
33# Used to suppress the handling of SIGTERM for critical components that should
34# not respect SIGTERM. To finish suppressing, use UnsuppressTerm()
35SuppressTerm() {
36 SUPPRESS_TERM=$((SUPPRESS_TERM + 1))
37}
38
39# Stops suppressing SIGTERM for a single invocation of SuppresssTerm()
40UnsuppressTerm() {
41 SUPPRESS_TERM=$((SUPPRESS_TERM - 1))
42}
43
44# Determines if we got a SIGTERM and should respect it
45ShouldTerm() {
46 (( GOT_TERM == 1 && SUPPRESS_TERM == 0 ))
47}
48
49# Internal, ensures that functions called in a subprocess properly initialize
50# their SIGTERM handling logic
51RunInterruptibleFunction() {
52 CHILD_PIDS=()
53 trap HandleTerm TERM
54 "$@"
55}
56
57# Runs the provided commandline in the background, and passes any received
58# SIGTERMS to the child. Can be waited on using WaitInterruptibleBg
59RunInterruptibleBg() {
60 if ShouldTerm; then
61 return 143
62 fi
63 if [ "$(type -t "$1")" = "function" ]; then
64 RunInterruptibleFunction "$@" &
65 else
66 "$@" &
67 fi
68 CHILD_PIDS["$!"]=1
69}
70
71# Runs the provided commandline to completion, and passes any received
72# SIGTERMS to the child.
73RunInterruptible() {
74 RunInterruptibleBg "$@" || return
75 local child_pid="$!"
76 wait "$child_pid" || true
77 unset CHILD_PIDS["$child_pid"]
78 wait "$child_pid"
79}
80
81# Waits until all of the RunInterruptibleBg() jobs have terminated
82WaitInterruptibleBg() {
83 local wait_on=("${!CHILD_PIDS[@]}")
84 if (( ${#wait_on[@]} > 0 )); then
85 wait "${wait_on[@]}" || true
86 CHILD_PIDS=()
87 local rc=0
88 local id
89 for id in "${wait_on[@]}"; do
90 wait "$id" || rc=$?
91 done
92 return $rc
93 fi
94}
95
96# Determines if an address could be a valid IPv4 address
97# NOTE: this doesn't sanitize invalid IPv4 addresses
98IsIPv4() {
99 local ip="$1"
100
101 [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
102}
103
104# Takes lines of text from an application on stdin and parses out a single
105# MAC address per line of input.
106ParseMACFromLine() {
107 sed -n 's,.*\(\([0-9a-fA-F]\{2\}:\)\{5\}[0-9a-fA-F]\{2\}\).*,\1,p'
108}
109
110# Looks up the MAC address of the IPv4 neighbor using ARP
111DetermineNeighbor4() {
112 local netdev="$1"
113 local ip="$2"
114
115 # Grep intentionally prevented from returning an error to preserve the error
116 # value of arping
117 RunInterruptible arping -f -c 5 -w 5 -I "$netdev" "$ip" | \
118 { grep 'reply from' || true; } | ParseMACFromLine
119}
120
121# Looks up the MAC address of the IPv6 neighbor using ICMPv6 ND
122DetermineNeighbor6() {
123 local netdev="$1"
124 local ip="$2"
125
126 RunInterruptible ndisc6 -1 -r 5 -w 1000 -q "$ip" "$netdev"
127}
128
129# Looks up the MAC address of the neighbor regardless of type
130DetermineNeighbor() {
131 local netdev="$1"
132 local ip="$2"
133
134 if IsIPv4 "$ip"; then
135 DetermineNeighbor4 "$netdev" "$ip"
136 else
137 DetermineNeighbor6 "$netdev" "$ip"
138 fi
139}
140
141# Performs a mapper call to get the subroot for the object root
142# with a maxdepth and list of required interfaces. Returns a streamed list
143# of JSON objects that contain an { object, service }.
144GetSubTree() {
145 local root="$1"
146 shift
147 local max_depth="$1"
148 shift
149
150 busctl --json=short call \
151 'xyz.openbmc_project.ObjectMapper' \
152 '/xyz/openbmc_project/object_mapper' \
153 'xyz.openbmc_project.ObjectMapper' \
154 'GetSubTree' sias "$root" "$max_depth" "$#" "$@" | \
155 jq -c '.data[0] | to_entries[] | { object: .key, service: (.value | keys[0]) }'
156}
157
158# Returns all of the properties for a DBus interface on an object as a JSON
159# object where the keys are the property names
160GetProperties() {
161 local service="$1"
162 local object="$2"
163 local interface="$3"
164
165 busctl --json=short call \
166 "$service" \
167 "$object" \
168 'org.freedesktop.DBus.Properties' \
169 'GetAll' s "$interface" | \
170 jq -c '.data[0] | with_entries({ key, value: .value.data })'
171}
172
173# Returns the property for a DBus interface on an object
174GetProperty() {
175 local service="$1"
176 local object="$2"
177 local interface="$3"
178 local property="$4"
179
180 busctl --json=short call \
181 "$service" \
182 "$object" \
183 'org.freedesktop.DBus.Properties' \
184 'Get' ss "$interface" "$property" | \
185 jq -r '.data[0].data'
186}
187
188# Deletes any OpenBMC DBus object from a service
189DeleteObject() {
190 local service="$1"
191 local object="$2"
192
193 busctl call \
194 "$service" \
195 "$object" \
196 'xyz.openbmc_project.Object.Delete' \
197 'Delete'
198}
199
200# Transforms the given JSON dictionary into bash local variable
201# statements that can be directly evaluated by the interpreter
202JSONToVars() {
203 jq -r 'to_entries[] | @sh "local \(.key)=\(.value)"'
204}
205
206# Returns the DBus object root for the ethernet interface
207EthObjRoot() {
208 local netdev="$1"
209
210 echo "/xyz/openbmc_project/network/$netdev"
211}
212
213# Returns the DBus object root for the static neighbors of an intrerface
214StaticNeighborObjRoot() {
215 local netdev="$1"
216
217 echo "$(EthObjRoot "$netdev")/static_neighbor"
218}
219
220# Returns all of the neighbor { service, object } data for an interface as if
221# a call to GetSubTree() was made
222GetNeighborObjects() {
223 local netdev="$1"
224
225 GetSubTree "$(StaticNeighborObjRoot "$netdev")" 0 \
226 'xyz.openbmc_project.Network.Neighbor'
227}
228
229# Returns the neighbor properties as a JSON object
230GetNeighbor() {
231 local service="$1"
232 local object="$2"
233
234 GetProperties "$service" "$object" 'xyz.openbmc_project.Network.Neighbor'
235}
236
237# Adds a static neighbor to the system network daemon
238AddNeighbor() {
239 local service="$1"
240 local netdev="$2"
241 local ip="$3"
242 local mac="$4"
243
244 busctl call \
245 "$service" \
246 "$(EthObjRoot "$netdev")" \
247 'xyz.openbmc_project.Network.Neighbor.CreateStatic' \
248 'Neighbor' ss "$ip" "$mac" >/dev/null
249}
250
251# Returns all of the IP { service, object } data for an interface as if
252# a call to GetSubTree() was made
253GetIPObjects() {
254 local netdev="$1"
255
256 GetSubTree "$(EthObjRoot "$netdev")" 0 \
257 'xyz.openbmc_project.Network.IP'
258}
259
260# Returns the IP properties as a JSON object
261GetIP() {
262 local service="$1"
263 local object="$2"
264
265 GetProperties "$service" "$object" 'xyz.openbmc_project.Network.IP'
266}
267
William A. Kennington IIIb163a2c2021-05-20 17:33:01 -0700268# Returns the Gateway address for the interface and type
269GetGateways() {
270 local service="$1"
271 local netdev="$2"
272
273 # We fetch both the system properties and the netdev specific properties
274 # as OpenBMC is in the process of transitioning these to the netdev object
275 # but the migration is not yet complete.
276 {
277 GetProperties "$service" '/xyz/openbmc_project/network/config' \
278 'xyz.openbmc_project.Network.SystemConfiguration'
279 GetProperties "$service" "$(EthObjRoot "$netdev")" \
280 'xyz.openbmc_project.Network.EthernetInterface'
281 } | jq -s '
282 . | map(
283 if .DefaultGateway != "" then
284 {DefaultGateway: .DefaultGateway}
285 else
286 {}
287 end +
288 if .DefaultGateway6 != "" then
289 {DefaultGateway6: .DefaultGateway6}
290 else
291 {}
292 end
293 ) | {DefaultGateway: "", DefaultGateway6: ""} + add'
294}
295
William A. Kennington III7d6fa422021-02-08 17:04:02 -0800296# Adds a static IP to the system network daemon
297AddIP() {
298 local service="$1"
299 local netdev="$2"
300 local ip="$3"
301 local prefix="$4"
302
303 local protocol='xyz.openbmc_project.Network.IP.Protocol.IPv4'
304 if ! IsIPv4 "$ip"; then
305 protocol='xyz.openbmc_project.Network.IP.Protocol.IPv6'
306 fi
307
308 busctl call \
309 "$service" \
310 "$(EthObjRoot "$netdev")" \
311 'xyz.openbmc_project.Network.IP.Create' \
312 'IP' ssys "$protocol" "$ip" "$prefix" '' >/dev/null
313}
314
315# Determines if two IP addresses have the same address family
316# IE: Both are IPv4 or both are IPv6
317MatchingAF() {
318 local rc1=0 rc2=0
319 IsIPv4 "$1" || rc1=$?
320 IsIPv4 "$2" || rc2=$?
321 (( rc1 == rc2 ))
322}
323
324# Checks to see if the machine has the provided IP address information
325# already configured. If not, it deletes all of the information for that
326# address family and adds the provided IP address.
327UpdateIP() {
328 local service="$1"
329 local netdev="$2"
330 local ip="$3"
331 local prefix="$4"
332
333 local should_add=1
334 local delete_services=()
335 local delete_objects=()
336 local entry
337 while read entry; do
338 eval "$(echo "$entry" | JSONToVars)" || return $?
339 eval "$(GetIP "$service" "$object" | JSONToVars)" || return $?
340 if [ "$(normalize_ip "$Address")" = "$(normalize_ip "$ip")" ] && \
341 [ "$PrefixLength" = "$prefix" ]; then
342 should_add=0
343 elif MatchingAF "$ip" "$Address"; then
344 echo "Deleting spurious IP: $Address/$PrefixLength" >&2
345 delete_services+=("$service")
346 delete_objects+=("$object")
347 fi
348 done < <(GetIPObjects "$netdev")
349
350 local i
351 for (( i=0; i<${#delete_objects[@]}; ++i )); do
William A. Kennington III2be05362021-11-05 02:58:59 -0700352 DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
William A. Kennington III7d6fa422021-02-08 17:04:02 -0800353 done
354
355 if (( should_add == 0 )); then
356 echo "Not adding IP: $ip/$prefix" >&2
357 else
358 echo "Adding IP: $ip/$prefix" >&2
359 AddIP "$service" "$netdev" "$ip" "$prefix" || return $?
360 fi
361}
362
363# Sets the system gateway property to the provided IP address if not already
364# set to the current value.
365UpdateGateway() {
366 local service="$1"
367 local ip="$2"
368
369 local object='/xyz/openbmc_project/network/config'
370 local interface='xyz.openbmc_project.Network.SystemConfiguration'
371 local property='DefaultGateway'
372 if ! IsIPv4 "$ip"; then
373 property='DefaultGateway6'
374 fi
375
376 local current_ip
377 current_ip="$(GetProperty "$service" "$object" "$interface" "$property")" || \
378 return $?
379 if [ -n "$current_ip" ] && \
380 [ "$(normalize_ip "$ip")" = "$(normalize_ip "$current_ip")" ]; then
381 echo "Not reconfiguring gateway: $ip" >&2
382 return 0
383 fi
384
385 echo "Setting gateway: $ip" >&2
386 busctl set-property "$service" "$object" "$interface" "$property" s "$ip"
387}
388
389# Checks to see if the machine has the provided neighbor information
390# already configured. If not, it deletes all of the information for that
391# address family and adds the provided neighbor entry.
392UpdateNeighbor() {
393 local service="$1"
394 local netdev="$2"
395 local ip="$3"
396 local mac="$4"
397
398 local should_add=1
399 local delete_services=()
400 local delete_objects=()
401 local entry
402 while read entry; do
403 eval "$(echo "$entry" | JSONToVars)" || return $?
404 eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $?
405 if [ "$(normalize_ip "$IPAddress")" = "$(normalize_ip "$ip")" ] && \
406 [ "$(normalize_mac "$MACAddress")" = "$(normalize_mac "$mac")" ]; then
407 should_add=0
408 elif MatchingAF "$ip" "$IPAddress"; then
409 echo "Deleting spurious neighbor: $IPAddress $MACAddress" >&2
410 delete_services+=("$service")
411 delete_objects+=("$object")
412 fi
413 done < <(GetNeighborObjects "$netdev" 2>/dev/null)
414
415 local i
416 for (( i=0; i<${#delete_objects[@]}; ++i )); do
William A. Kennington III2be05362021-11-05 02:58:59 -0700417 DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
William A. Kennington III7d6fa422021-02-08 17:04:02 -0800418 done
419
420 if (( should_add == 0 )); then
421 echo "Not adding neighbor: $ip $mac" >&2
422 else
423 echo "Adding neighbor: $ip $mac" >&2
424 AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $?
425 fi
426}
William A. Kennington IIIb163a2c2021-05-20 17:33:01 -0700427
428# Determines the ip and mac of the IPv6 router
429DiscoverRouter6() {
430 local netdev="$1"
431 local retries="$2"
432 local timeout="$3"
433 local router="${4-}"
434
435 local output
436 local st=0
437 local args=(-1 -w "$timeout" -n $router "$netdev")
438 if (( retries < 0 )); then
439 args+=(-d)
440 else
441 args+=(-r "$retries")
442 fi
443 output="$(RunInterruptible rdisc6 "${args[@]}")" || st=$?
444 if (( st != 0 )); then
445 echo "rdisc6 failed with: " >&2
446 echo "$output" >&2
447 return $st
448 fi
449
450 local ip="$(echo "$output" | grep 'from' | awk '{print $2}')"
451 local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)"
William A. Kennington III379b0612021-11-04 02:42:30 -0700452 local staddr="$(echo "$output" | grep 'Stateful address conf.*Yes')"
453 printf '{"router_ip":"%s","router_mac":"%s","stateful_address":"%s"}\n' \
454 "$ip" "$mac" "$staddr"
William A. Kennington IIIb163a2c2021-05-20 17:33:01 -0700455}