ncsid: Import from gBMC

This is the initial code drop from gBMC.

Google-Bug-Id: 179618516
Upstream: 1e71af914bc8c54d8b91d0a1cf377e2696713c2f
Change-Id: Ic653e8271dacd205e04f2bc713071ef2ec5936a4
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/ncsid/src/ncsid_lib.sh b/ncsid/src/ncsid_lib.sh
new file mode 100644
index 0000000..080afed
--- /dev/null
+++ b/ncsid/src/ncsid_lib.sh
@@ -0,0 +1,384 @@
+# Internal handler used for signalling child processes that they should
+# terminate.
+HandleTerm() {
+  GOT_TERM=1
+  if ShouldTerm && (( ${#CHILD_PIDS[@]} > 0 )); then
+    kill "${!CHILD_PIDS[@]}"
+  fi
+}
+
+# Sets up the signal handler and global variables needed to run interruptible
+# services that can be killed gracefully.
+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()
+SuppressTerm() {
+  SUPPRESS_TERM=$((SUPPRESS_TERM + 1))
+}
+
+# Stops suppressing SIGTERM for a single invocation of SuppresssTerm()
+UnsuppressTerm() {
+  SUPPRESS_TERM=$((SUPPRESS_TERM - 1))
+}
+
+# Determines if we got a SIGTERM and should respect it
+ShouldTerm() {
+  (( GOT_TERM == 1 && SUPPRESS_TERM == 0 ))
+}
+
+# Internal, ensures that functions called in a subprocess properly initialize
+# their SIGTERM handling logic
+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
+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.
+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
+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
+}
+
+# Determines if an address could be a valid IPv4 address
+# NOTE: this doesn't sanitize invalid IPv4 addresses
+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.
+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
+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
+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
+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 }.
+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
+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
+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
+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
+JSONToVars() {
+  jq -r 'to_entries[] | @sh "local \(.key)=\(.value)"'
+}
+
+# Returns the DBus object root for the ethernet interface
+EthObjRoot() {
+  local netdev="$1"
+
+  echo "/xyz/openbmc_project/network/$netdev"
+}
+
+# Returns the DBus object root for the static neighbors of an intrerface
+StaticNeighborObjRoot() {
+  local netdev="$1"
+
+  echo "$(EthObjRoot "$netdev")/static_neighbor"
+}
+
+# Returns all of the neighbor { service, object } data for an interface as if
+# a call to GetSubTree() was made
+GetNeighborObjects() {
+  local netdev="$1"
+
+  GetSubTree "$(StaticNeighborObjRoot "$netdev")" 0 \
+    'xyz.openbmc_project.Network.Neighbor'
+}
+
+# Returns the neighbor properties as a JSON object
+GetNeighbor() {
+  local service="$1"
+  local object="$2"
+
+  GetProperties "$service" "$object" 'xyz.openbmc_project.Network.Neighbor'
+}
+
+# Adds a static neighbor to the system network daemon
+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
+GetIPObjects() {
+  local netdev="$1"
+
+  GetSubTree "$(EthObjRoot "$netdev")" 0 \
+    'xyz.openbmc_project.Network.IP'
+}
+
+# Returns the IP properties as a JSON object
+GetIP() {
+  local service="$1"
+  local object="$2"
+
+  GetProperties "$service" "$object" 'xyz.openbmc_project.Network.IP'
+}
+
+# Adds a static IP to the system network daemon
+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
+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.
+UpdateIP() {
+  local service="$1"
+  local netdev="$2"
+  local 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")" = "$(normalize_ip "$ip")" ] && \
+        [ "$PrefixLength" = "$prefix" ]; then
+      should_add=0
+    elif MatchingAF "$ip" "$Address"; 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]}" || return $?
+  done
+
+  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.
+UpdateGateway() {
+  local service="$1"
+  local ip="$2"
+
+  local object='/xyz/openbmc_project/network/config'
+  local interface='xyz.openbmc_project.Network.SystemConfiguration'
+  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.
+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]}" || return $?
+  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
+}