blob: ef4408ea405aacb15670ec16b7bafcdd2e56b027 [file] [log] [blame]
#!/bin/bash
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Internal handler used for signalling child processes that they should
# terminate.
function HandleTerm() {
GOT_TERM=1
if ShouldTerm && (( ${#CHILD_PIDS[@]} > 0 )); then
kill -s TERM "${!CHILD_PIDS[@]}"
fi
}
# Sets up the signal handler and global variables needed to run interruptible
# services that can be killed gracefully.
function InitTerm() {
declare -g -A CHILD_PIDS=()
declare -g GOT_TERM=0
declare -g SUPPRESS_TERM=0
trap HandleTerm TERM
}
# Used to suppress the handling of SIGTERM for critical components that should
# not respect SIGTERM. To finish suppressing, use UnsuppressTerm()
function SuppressTerm() {
SUPPRESS_TERM=$((SUPPRESS_TERM + 1))
}
# Stops suppressing SIGTERM for a single invocation of SuppresssTerm()
function UnsuppressTerm() {
SUPPRESS_TERM=$((SUPPRESS_TERM - 1))
}
# Determines if we got a SIGTERM and should respect it
function ShouldTerm() {
(( GOT_TERM == 1 && SUPPRESS_TERM == 0 ))
}
# Internal, ensures that functions called in a subprocess properly initialize
# their SIGTERM handling logic
function RunInterruptibleFunction() {
CHILD_PIDS=()
trap HandleTerm TERM
"$@"
}
# Runs the provided commandline in the background, and passes any received
# SIGTERMS to the child. Can be waited on using WaitInterruptibleBg
function RunInterruptibleBg() {
if ShouldTerm; then
return 143
fi
if [ "$(type -t "$1")" = "function" ]; then
RunInterruptibleFunction "$@" &
else
"$@" &
fi
CHILD_PIDS["$!"]=1
}
# Runs the provided commandline to completion, and passes any received
# SIGTERMS to the child.
function RunInterruptible() {
RunInterruptibleBg "$@" || return
local child_pid="$!"
wait "$child_pid" || true
unset CHILD_PIDS["$child_pid"]
wait "$child_pid"
}
# Waits until all of the RunInterruptibleBg() jobs have terminated
function WaitInterruptibleBg() {
local wait_on=("${!CHILD_PIDS[@]}")
if (( ${#wait_on[@]} > 0 )); then
wait "${wait_on[@]}" || true
CHILD_PIDS=()
local rc=0
local id
for id in "${wait_on[@]}"; do
wait "$id" || rc=$?
done
return $rc
fi
}
# Runs the provided commandline to completion, capturing stdout
# into a variable
function CaptureInterruptible() {
local var="$1"
shift
if ShouldTerm; then
return 143
fi
coproc "$@" || return
local child_pid="$COPROC_PID"
CHILD_PIDS["$child_pid"]=1
exec {COPROC[1]}>&-
read -d $'\0' -ru "${COPROC[0]}" "$var" || true
wait "$child_pid" || true
unset CHILD_PIDS[$child_pid]
wait "$child_pid"
}
# Determines if an address could be a valid IPv4 address
# NOTE: this doesn't sanitize invalid IPv4 addresses
function IsIPv4() {
local ip="$1"
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
}
# Takes lines of text from an application on stdin and parses out a single
# MAC address per line of input.
function ParseMACFromLine() {
sed -n 's,.*\(\([0-9a-fA-F]\{2\}:\)\{5\}[0-9a-fA-F]\{2\}\).*,\1,p'
}
# Looks up the MAC address of the IPv4 neighbor using ARP
function DetermineNeighbor4() {
local netdev="$1"
local ip="$2"
# Grep intentionally prevented from returning an error to preserve the error
# value of arping
RunInterruptible arping -f -c 5 -w 5 -I "$netdev" "$ip" | \
{ grep 'reply from' || true; } | ParseMACFromLine
}
# Looks up the MAC address of the IPv6 neighbor using ICMPv6 ND
function DetermineNeighbor6() {
local netdev="$1"
local ip="$2"
RunInterruptible ndisc6 -1 -r 5 -w 1000 -q "$ip" "$netdev"
}
# Looks up the MAC address of the neighbor regardless of type
function DetermineNeighbor() {
local netdev="$1"
local ip="$2"
if IsIPv4 "$ip"; then
DetermineNeighbor4 "$netdev" "$ip"
else
DetermineNeighbor6 "$netdev" "$ip"
fi
}
# Performs a mapper call to get the subroot for the object root
# with a maxdepth and list of required interfaces. Returns a streamed list
# of JSON objects that contain an { object, service }.
function GetSubTree() {
local root="$1"
shift
local max_depth="$1"
shift
busctl --json=short call \
'xyz.openbmc_project.ObjectMapper' \
'/xyz/openbmc_project/object_mapper' \
'xyz.openbmc_project.ObjectMapper' \
'GetSubTree' sias "$root" "$max_depth" "$#" "$@" | \
jq -c '.data[0] | to_entries[] | { object: .key, service: (.value | keys[0]) }'
}
# Returns all of the properties for a DBus interface on an object as a JSON
# object where the keys are the property names
function GetProperties() {
local service="$1"
local object="$2"
local interface="$3"
busctl --json=short call \
"$service" \
"$object" \
'org.freedesktop.DBus.Properties' \
'GetAll' s "$interface" | \
jq -c '.data[0] | with_entries({ key, value: .value.data })'
}
# Returns the property for a DBus interface on an object
function GetProperty() {
local service="$1"
local object="$2"
local interface="$3"
local property="$4"
busctl --json=short call \
"$service" \
"$object" \
'org.freedesktop.DBus.Properties' \
'Get' ss "$interface" "$property" | \
jq -r '.data[0].data'
}
# Deletes any OpenBMC DBus object from a service
function DeleteObject() {
local service="$1"
local object="$2"
busctl call \
"$service" \
"$object" \
'xyz.openbmc_project.Object.Delete' \
'Delete'
}
# Transforms the given JSON dictionary into bash local variable
# statements that can be directly evaluated by the interpreter
function JSONToVars() {
jq -r 'to_entries[] | @sh "local \(.key)=\(.value)"'
}
# Returns the DBus object root for the ethernet interface
function EthObjRoot() {
local netdev="$1"
echo "/xyz/openbmc_project/network/$netdev"
}
# Returns all of the neighbor { service, object } data for an interface as if
# a call to GetSubTree() was made
function GetNeighborObjects() {
local netdev="$1"
GetSubTree "$(EthObjRoot "$netdev")" 0 \
'xyz.openbmc_project.Network.Neighbor'
}
# Returns the neighbor properties as a JSON object
function GetNeighbor() {
local service="$1"
local object="$2"
GetProperties "$service" "$object" 'xyz.openbmc_project.Network.Neighbor'
}
# Adds a static neighbor to the system network daemon
function AddNeighbor() {
local service="$1"
local netdev="$2"
local ip="$3"
local mac="$4"
busctl call \
"$service" \
"$(EthObjRoot "$netdev")" \
'xyz.openbmc_project.Network.Neighbor.CreateStatic' \
'Neighbor' ss "$ip" "$mac" >/dev/null
}
# Returns all of the IP { service, object } data for an interface as if
# a call to GetSubTree() was made
function GetIPObjects() {
local netdev="$1"
GetSubTree "$(EthObjRoot "$netdev")" 0 \
'xyz.openbmc_project.Network.IP'
}
# Returns the IP properties as a JSON object
function GetIP() {
local service="$1"
local object="$2"
GetProperties "$service" "$object" 'xyz.openbmc_project.Network.IP'
}
# Returns the Gateway address for the interface and type
function GetGateways() {
local service="$1"
local netdev="$2"
# We fetch both the system properties and the netdev specific properties
# as OpenBMC is in the process of transitioning these to the netdev object
# but the migration is not yet complete.
{
GetProperties "$service" '/xyz/openbmc_project/network/config' \
'xyz.openbmc_project.Network.SystemConfiguration'
GetProperties "$service" "$(EthObjRoot "$netdev")" \
'xyz.openbmc_project.Network.EthernetInterface'
} | jq -s '
. | map(
if .DefaultGateway != "" then
{DefaultGateway: .DefaultGateway}
else
{}
end +
if .DefaultGateway6 != "" then
{DefaultGateway6: .DefaultGateway6}
else
{}
end
) | {DefaultGateway: "", DefaultGateway6: ""} + add'
}
# Adds a static IP to the system network daemon
function AddIP() {
local service="$1"
local netdev="$2"
local ip="$3"
local prefix="$4"
local protocol='xyz.openbmc_project.Network.IP.Protocol.IPv4'
if ! IsIPv4 "$ip"; then
protocol='xyz.openbmc_project.Network.IP.Protocol.IPv6'
fi
busctl call \
"$service" \
"$(EthObjRoot "$netdev")" \
'xyz.openbmc_project.Network.IP.Create' \
'IP' ssys "$protocol" "$ip" "$prefix" '' >/dev/null
}
# Determines if two IP addresses have the same address family
# IE: Both are IPv4 or both are IPv6
function MatchingAF() {
local rc1=0 rc2=0
IsIPv4 "$1" || rc1=$?
IsIPv4 "$2" || rc2=$?
(( rc1 == rc2 ))
}
# Checks to see if the machine has the provided IP address information
# already configured. If not, it deletes all of the information for that
# address family and adds the provided IP address.
function UpdateIP() {
local service="$1"
local netdev="$2"
local ip="$(normalize_ip $3)"
local prefix="$4"
local should_add=1
local delete_services=()
local delete_objects=()
local entry
while read entry; do
eval "$(echo "$entry" | JSONToVars)" || return $?
eval "$(GetIP "$service" "$object" | JSONToVars)" || return $?
if [ "$(normalize_ip "$Address")" = "$ip" ] && \
[ "$PrefixLength" = "$prefix" ]; then
should_add=0
elif MatchingAF "$ip" "$Address" && [[ "$Origin" == *.Static ]]; then
echo "Deleting spurious IP: $Address/$PrefixLength" >&2
delete_services+=("$service")
delete_objects+=("$object")
fi
done < <(GetIPObjects "$netdev")
local i
for (( i=0; i<${#delete_objects[@]}; ++i )); do
DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
done
# The default address is treated as a delete only request
if [ "$ip" = :: -o "$ip" = 0.0.0.0 ]; then
return
fi
if (( should_add == 0 )); then
echo "Not adding IP: $ip/$prefix" >&2
else
echo "Adding IP: $ip/$prefix" >&2
AddIP "$service" "$netdev" "$ip" "$prefix" || return $?
fi
}
# Sets the system gateway property to the provided IP address if not already
# set to the current value.
function UpdateGateway() {
local service="$1"
local netdev="$2"
local ip="$3"
local object="$(EthObjRoot "$netdev")"
local interface='xyz.openbmc_project.Network.EthernetInterface'
local property='DefaultGateway'
if ! IsIPv4 "$ip"; then
property='DefaultGateway6'
fi
local current_ip
current_ip="$(GetProperty "$service" "$object" "$interface" "$property")" || \
return $?
if [ -n "$current_ip" ] && \
[ "$(normalize_ip "$ip")" = "$(normalize_ip "$current_ip")" ]; then
echo "Not reconfiguring gateway: $ip" >&2
return 0
fi
echo "Setting gateway: $ip" >&2
busctl set-property "$service" "$object" "$interface" "$property" s "$ip"
}
# Checks to see if the machine has the provided neighbor information
# already configured. If not, it deletes all of the information for that
# address family and adds the provided neighbor entry.
function UpdateNeighbor() {
local service="$1"
local netdev="$2"
local ip="$3"
local mac="$4"
local should_add=1
local delete_services=()
local delete_objects=()
local entry
while read entry; do
eval "$(echo "$entry" | JSONToVars)" || return $?
eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $?
if [ "$(normalize_ip "$IPAddress")" = "$(normalize_ip "$ip")" ] && \
[ "$(normalize_mac "$MACAddress")" = "$(normalize_mac "$mac")" ]; then
should_add=0
elif MatchingAF "$ip" "$IPAddress"; then
echo "Deleting spurious neighbor: $IPAddress $MACAddress" >&2
delete_services+=("$service")
delete_objects+=("$object")
fi
done < <(GetNeighborObjects "$netdev" 2>/dev/null)
local i
for (( i=0; i<${#delete_objects[@]}; ++i )); do
DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || true
done
if (( should_add == 0 )); then
echo "Not adding neighbor: $ip $mac" >&2
else
echo "Adding neighbor: $ip $mac" >&2
AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $?
fi
}
# Determines the ip and mac of the IPv6 router
function DiscoverRouter6() {
local netdev="$1"
local retries="$2"
local timeout="$3"
local router="${4-}"
local output
local st=0
local args=(-1 -w "$timeout" -n $router "$netdev")
if (( retries < 0 )); then
args+=(-d)
else
args+=(-r "$retries")
fi
CaptureInterruptible output rdisc6 "${args[@]}" || st=$?
if (( st != 0 )); then
echo "rdisc6 failed with: " >&2
echo "$output" >&2
return $st
fi
local ip="$(echo "$output" | grep 'from' | awk '{print $2}')"
local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)"
local staddr="$(echo "$output" | grep 'Stateful address conf.*Yes')"
printf '{"router_ip":"%s","router_mac":"%s","stateful_address":"%s"}\n' \
"$ip" "$mac" "$staddr"
}
# Sets the network configuration of an interface to be static
function SetStatic() {
local service="$1"
local netdev="$2"
echo "Disabling DHCP" >&2
busctl set-property "$service" "$(EthObjRoot "$netdev")" \
xyz.openbmc_project.Network.EthernetInterface DHCPEnabled \
s xyz.openbmc_project.Network.EthernetInterface.DHCPConf.none
}