meta-google: Add google-usb-network recipe

Add recipe for bringing up USB ECM network devices.

The recipe generates
* network configuration file for the interface;
* systemd service that starts(stops) the USB Gadget

The ECM gadget, as well as the network interface are expected
to be fully configured in the bbappend file.

Signed-off-by: Maksym Sloyko <maxims@google.com>
Change-Id: Ibb57584ce2bfd838e650eae7a4d3935c9580e502
diff --git a/meta-google/recipes-google/networking/google-usb-network/00-bmc-usb0.network.m4 b/meta-google/recipes-google/networking/google-usb-network/00-bmc-usb0.network.m4
new file mode 100644
index 0000000..699ee85
--- /dev/null
+++ b/meta-google/recipes-google/networking/google-usb-network/00-bmc-usb0.network.m4
@@ -0,0 +1,7 @@
+[Match]
+Name=usb0
+[Address]
+Address=M_BMC_IP_ADDR
+[Network]
+LinkLocalAddressing=ipv6
+IPv6AcceptRA=no
diff --git a/meta-google/recipes-google/networking/google-usb-network/usb_network.service.m4 b/meta-google/recipes-google/networking/google-usb-network/usb_network.service.m4
new file mode 100644
index 0000000..581c29b
--- /dev/null
+++ b/meta-google/recipes-google/networking/google-usb-network/usb_network.service.m4
@@ -0,0 +1,28 @@
+divert(-1)
+define(`HOST_MAC_ARG', `ifelse($1, `invalid', `',
+            ifelse($1, `', `',
+                ` --host-mac "$1"'))')
+
+define(`DEV_MAC_ARG', `ifelse($1, `invalid', `',
+            ifelse($1, `', `',
+                ` --dev-mac "$1"'))')
+
+divert(0)dnl
+dnl
+[Unit]
+Description=USB ECM Gadget
+After=phosphor-ipmi-host.service
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=M_SCRIPT_INSTALL_DIR/usb_network.sh \
+    --product-id "M_BMC_USB_ECM_PRODUCT_ID" \
+    --product-name "M_BMC_USB_ECM_PRODUCT_NAME" \
+    HOST_MAC_ARG(M_BMC_USB_ECM_HOST_MAC) \
+    DEV_MAC_ARG(M_BMC_USB_ECM_DEV_MAC) \
+    --bind-device "M_BMC_USB_ECM_BIND_DEV"
+ExecStop=M_SCRIPT_INSTALL_DIR/usb_network.sh stop
+
+[Install]
+WantedBy=multi-user.target
diff --git a/meta-google/recipes-google/networking/google-usb-network/usb_network.sh b/meta-google/recipes-google/networking/google-usb-network/usb_network.sh
new file mode 100755
index 0000000..08f9ad2
--- /dev/null
+++ b/meta-google/recipes-google/networking/google-usb-network/usb_network.sh
@@ -0,0 +1,165 @@
+#!/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.
+
+
+# List of options the script accepts. Trailing column means that the option
+# requires an argument.
+ARGUMENT_LIST=(
+    "help"
+    "product-id:"
+    "product-name:"
+    "host-mac:"
+    "bind-device:"
+    "dev-mac:"
+    "gadget-dir-name:"
+    "iface-name:"
+)
+
+print_usage() {
+    cat <<HELP
+$0 [OPTIONS] [stop|start]
+    Create USB Gadget Configuration
+        --product-id USB Product Id for the gadget.
+        --product-name Product name string (en) for the gadget.
+        --host-mac MAC address of the host part of the connection. Optional.
+        --dev-mac MAC address of the device (gadget) part of the connection. Optional.
+        --bind-device Name of the device to bind, as listed in /sys/class/udc/
+        --gadget-dir-name Optional base name for gadget directory. Default: "g1"
+        --iface-name Optional name of the network interface. Default: "usb0"
+        --help  Print this help and exit.
+HELP
+}
+
+gadget_start() {
+    local gadget_dir="${CONFIGFS_HOME}/usb_gadget/${GADGET_DIR_NAME}"
+    mkdir -p "${gadget_dir}"
+    echo ${ID_VENDOR} > "${gadget_dir}/idVendor"
+    echo ${ID_PRODUCT} > "${gadget_dir}/idProduct"
+
+    local str_en_dir="${gadget_dir}/strings/0x409"
+    mkdir -p "${str_en_dir}"
+    echo ${STR_EN_VENDOR} > "${str_en_dir}/manufacturer"
+    echo ${STR_EN_PRODUCT} > "${str_en_dir}/product"
+
+    local config_dir="${gadget_dir}/configs/c.1"
+    mkdir -p "${config_dir}"
+    echo 100 > "${config_dir}/MaxPower"
+    mkdir -p "${config_dir}/strings/0x409"
+    echo "ECM" > "${config_dir}/strings/0x409/configuration"
+
+    local func_dir="${gadget_dir}/functions/ecm.${IFACE_NAME}"
+    mkdir -p "${func_dir}"
+
+    if [[ -n $HOST_MAC_ADDR ]]; then
+        echo ${HOST_MAC_ADDR} > ${func_dir}/host_addr
+    fi
+
+    if [[ -n $DEV_MAC_ADDR ]]; then
+        echo ${DEV_MAC_ADDR} > ${func_dir}/dev_addr
+    fi
+
+    ln -s "${func_dir}" "${config_dir}"
+
+    echo "${BIND_DEVICE}" > ${gadget_dir}/UDC
+}
+
+gadget_stop() {
+    local gadget_dir="${CONFIGFS_HOME}/usb_gadget/${GADGET_DIR_NAME}"
+    rm -f ${gadget_dir}/configs/c.1/ecm.${IFACE_NAME}
+    rm -rf ${gadget_dir}/configs/c.1/strings/0x409
+    rm -rf ${gadget_dir}/configs/c.1
+    rm -rf ${gadget_dir}/strings/0x409
+    rm -rf ${gadget_dir}/functions/ecm.${IFACE_NAME}
+    rm -rf ${gadget_dir}
+}
+
+opts=$(getopt \
+    --longoptions "$(printf "%s," "${ARGUMENT_LIST[@]}")" \
+    --name "$(basename "$0")" \
+    --options "" \
+    -- "$@"
+)
+
+eval set --$opts
+
+CONFIGFS_HOME=${CONFIGFS_HOME:-/sys/kernel/config}
+ID_VENDOR="0x18d1" # Google
+ID_PRODUCT=""
+STR_EN_VENDOR="Google"
+STR_EN_PRODUCT=""
+DEV_MAC_ADDR=""
+HOST_MAC_ADDR=""
+BIND_DEVICE=""
+ACTION="start"
+GADGET_DIR_NAME="g1"
+IFACE_NAME="usb0"
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+        --product-id)
+            ID_PRODUCT=$2
+            shift 2
+            ;;
+        --product-name)
+            STR_EN_PRODUCT=$2
+            shift 2
+            ;;
+        --host-mac)
+            HOST_MAC_ADDR=$2
+            shift 2
+            ;;
+        --dev-mac)
+            DEV_MAC_ADDR=$2
+            shift 2
+            ;;
+        --bind-device)
+            BIND_DEVICE=$2
+            shift 2
+            ;;
+        --gadget-dir-name)
+            GADGET_DIR_NAME=$2
+            shift 2
+            ;;
+        --iface-name)
+            IFACE_NAME=$2
+            shift 2
+            ;;
+        --help)
+            print_usage
+            exit 0
+            ;;
+        start)
+            ACTION="start"
+            shift 1
+            break
+            ;;
+        stop)
+            ACTION="stop"
+            shift 1
+            break
+            ;;
+        --)
+            shift 1
+            ;;
+        *)
+            break
+            ;;
+    esac
+done
+
+if [[ $ACTION == "stop" ]]; then
+    gadget_stop
+else
+    gadget_start
+fi
diff --git a/meta-google/recipes-google/networking/google-usb-network/usb_network_test.sh b/meta-google/recipes-google/networking/google-usb-network/usb_network_test.sh
new file mode 100755
index 0000000..e589433
--- /dev/null
+++ b/meta-google/recipes-google/networking/google-usb-network/usb_network_test.sh
@@ -0,0 +1,295 @@
+#!/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.
+
+
+TEMPDIRS=""
+TEMPFILES=""
+# Script under test
+SUT=$PWD/usb_network.sh
+
+TEST_STATUS="OK"
+
+test_setup() {
+    echo -n "Testing $1 ..."
+    FAKE_CONFIGFS=$(mktemp -d)
+    TEMPDIRS="${TEMPDIRS} ${FAKE_CONFIGFS}"
+    FAKE_GADGETFS=$FAKE_CONFIGFS/usb_gadget
+    mkdir -p "$FAKE_GADGETFS"
+}
+
+test_teardown() {
+    echo ${TEST_STATUS}
+    if test -n "$TEMPDIRS"; then
+        rm -rf ${TEMPDIRS}
+    fi
+    if test -n "$TEMPFILES"; then
+        rm -f $TEMPFILES
+    fi
+}
+
+test_fail() {
+    echo -n " $@ " >&2
+    TEST_STATUS="FAIL"
+
+    test_teardown
+    exit 1
+}
+
+check_file_content() {
+    local filename="$1"
+    local expected_content="$2"
+
+    if ! test -f "${filename}"; then
+        test_fail "File ${filename} does not exist!"
+    fi
+
+    local actual_content=$(cat ${filename})
+    if [[ $expected_content != $actual_content ]]; then
+        test_fail "Expected ${expected_content}, got ${actual_content}"
+    fi
+}
+
+test_gadget_creation_with_defaults() {
+    local extra_args=()
+    local gadget_dir="$1"
+    if [[ $gadget_dir == "" ]]; then
+        gadget_dir="g1";
+    else
+        extra_args=(${extra_args} --gadget-dir-name "${gadget_dir}")
+    fi
+    local product_name="Souvenier BMC"
+    local product_id="0xcafe"
+    local host_mac="ab:cd:ef:10:11:12"
+    local dev_mac="12:11:10:ef:cd:ab"
+    local bind_device="f80002000.udc"
+    CONFIGFS_HOME=${FAKE_CONFIGFS} ${SUT} --product-id "${product_id}" \
+        --product-name "${product_name}" \
+        --host-mac "${host_mac}" \
+        --dev-mac "${dev_mac}" \
+        --bind-device "${bind_device}" \
+        ${extra_args[@]}
+
+    if test $? -ne 0; then
+        test_fail "${SUT} failed"
+    fi
+
+    if ! test -d "${FAKE_GADGETFS}/${gadget_dir}"; then
+        test_fail "Gadget was not created!"
+    fi
+
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/idVendor "0x18d1"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/idProduct "${product_id}"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/strings/0x409/manufacturer "Google"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/strings/0x409/product "${product_name}"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/configs/c.1/MaxPower "100"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/configs/c.1/strings/0x409/configuration "ECM"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.usb0/dev_addr "${dev_mac}"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.usb0/host_addr "${host_mac}"
+
+    if ! test -d ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.usb0; then
+        test_fail "Function directory was not created"
+    fi
+
+    local func_link="${FAKE_GADGETFS}/${gadget_dir}/configs/c.1/ecm.usb0"
+    if ! test -L "${func_link}"; then
+        test_fail "Symlink to the function was not created in the config"
+    fi
+
+    local link_dest=$(realpath ${func_link})
+    if [[ $link_dest != ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.usb0 ]]; then
+        test_fail "Symlink points to the wrong file/dir"
+    fi
+
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/UDC "${bind_device}"
+}
+
+test_gadget_creation_with_override() {
+    mkdir -p ${FAKE_GADGETFS}/g1/{strings,configs,functions}
+    touch ${FAKE_GADGETFS}/g1/{idVendor,idProduct}
+
+    test_gadget_creation_with_defaults
+}
+
+test_gadget_stopping() {
+    local extra_args=()
+    local gadget_dir="$1"
+    local iface_name="$2"
+    if [[ $gadget_dir == "" ]]; then
+        gadget_dir="g1";
+    else
+        extra_args=(${extra_args} --gadget-dir-name "${gadget_dir}")
+    fi
+
+    if [[ $iface_name == "" ]]; then
+        iface_name="usb0";
+    else
+        extra_args=(${extra_args} --iface-name "${iface_name}")
+    fi
+
+    CONFIGFS_HOME=${FAKE_CONFIGFS} ${SUT} ${extra_args[@]} stop
+
+    if test -d "${FAKE_GADGETFS}/${gadget_dir}"; then
+        test_fail "Gadget was not removed!"
+    fi
+}
+
+test_gadget_creation_no_macs() {
+    local gadget_dir="g1";
+    local product_name="Souvenier BMC"
+    local product_id="0xcafe"
+    local bind_device="f80002000.udc"
+    CONFIGFS_HOME=${FAKE_CONFIGFS} ${SUT} --product-id "${product_id}" \
+        --product-name "${product_name}" \
+        --bind-device "${bind_device}"
+
+    if test $? -ne 0; then
+        test_fail "${SUT} failed"
+    fi
+
+    if ! test -d "${FAKE_GADGETFS}/${gadget_dir}"; then
+        test_fail "Gadget was not created!"
+    fi
+
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/idVendor "0x18d1"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/idProduct "${product_id}"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/strings/0x409/manufacturer "Google"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/strings/0x409/product "${product_name}"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/configs/c.1/MaxPower "100"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/configs/c.1/strings/0x409/configuration "ECM"
+
+    if test -e ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.usb0/dev_addr; then
+        test_fail "dev_addr should not be set"
+    fi
+
+    if test -e ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.usb0/host_addr; then
+        test_fail "host_addr should not be set"
+    fi
+
+    local func_link="${FAKE_GADGETFS}/${gadget_dir}/configs/c.1/ecm.usb0"
+    if ! test -L "${func_link}"; then
+        test_fail "Symlink to the function was not created in the config"
+    fi
+
+    local link_dest=$(realpath ${func_link})
+    if [[ $link_dest != ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.usb0 ]]; then
+        test_fail "Symlink points to the wrong file/dir"
+    fi
+
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/UDC "${bind_device}"
+}
+
+test_gadget_creation_alt_iface() {
+    local gadget_dir="g1";
+    local product_name="Souvenier BMC"
+    local product_id="0xcafe"
+    local bind_device="f80002000.udc"
+    local iface_name="iface0"
+    CONFIGFS_HOME=${FAKE_CONFIGFS} ${SUT} --product-id "${product_id}" \
+        --product-name "${product_name}" \
+        --bind-device "${bind_device}" \
+        --iface-name "${iface_name}"
+
+    if test $? -ne 0; then
+        test_fail "${SUT} failed"
+    fi
+
+    if ! test -d "${FAKE_GADGETFS}/${gadget_dir}"; then
+        test_fail "Gadget was not created!"
+    fi
+
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/idVendor "0x18d1"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/idProduct "${product_id}"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/strings/0x409/manufacturer "Google"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/strings/0x409/product "${product_name}"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/configs/c.1/MaxPower "100"
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/configs/c.1/strings/0x409/configuration "ECM"
+
+    if ! test -d ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.${iface_name}; then
+        test_fail "Function directory was not created"
+    fi
+
+    if test -e ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.${iface_name}/dev_addr; then
+        test_fail "dev_addr should not be set"
+    fi
+
+    if test -e ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.${iface_name}/host_addr; then
+        test_fail "host_addr should not be set"
+    fi
+
+    local func_link="${FAKE_GADGETFS}/${gadget_dir}/configs/c.1/ecm.${iface_name}"
+    if ! test -L "${func_link}"; then
+        test_fail "Symlink to the function was not created in the config"
+    fi
+
+    local link_dest=$(realpath ${func_link})
+    if [[ $link_dest != ${FAKE_GADGETFS}/${gadget_dir}/functions/ecm.${iface_name} ]]; then
+        test_fail "Symlink points to the wrong file/dir"
+    fi
+
+    check_file_content ${FAKE_GADGETFS}/${gadget_dir}/UDC "${bind_device}"
+}
+
+
+# -------------------------------------------------------------------
+test_setup "Device Creation"
+
+test_gadget_creation_with_defaults
+
+test_teardown
+# -------------------------------------------------------------------
+
+# -------------------------------------------------------------------
+test_setup "Device Creation With Override"
+
+test_gadget_creation_with_override
+
+test_teardown
+# -------------------------------------------------------------------
+
+# -------------------------------------------------------------------
+test_setup "Test Device Stop"
+
+test_gadget_creation_with_defaults
+test_gadget_stopping
+
+test_teardown
+# -------------------------------------------------------------------
+
+# -------------------------------------------------------------------
+test_setup "Device Creation/Stopping, Alternative Name"
+
+test_gadget_creation_with_defaults "gAlt"
+test_gadget_stopping "gAlt"
+
+test_teardown
+# -------------------------------------------------------------------
+
+# -------------------------------------------------------------------
+test_setup "Device Creation without MAC Addrs"
+
+test_gadget_creation_no_macs
+
+test_teardown
+# -------------------------------------------------------------------
+
+# -------------------------------------------------------------------
+test_setup "Device Creation/Stopping, Alternative Interface"
+
+test_gadget_creation_alt_iface
+
+test_teardown
+# -------------------------------------------------------------------
+
+echo "SUCCESS!"
diff --git a/meta-google/recipes-google/networking/google-usb-network_git.bb b/meta-google/recipes-google/networking/google-usb-network_git.bb
new file mode 100644
index 0000000..ec51eaa
--- /dev/null
+++ b/meta-google/recipes-google/networking/google-usb-network_git.bb
@@ -0,0 +1,64 @@
+SUMMARY = "Google USB ECM Gadget Configuration Script"
+DESCRIPTION = "Google USB ECM Gadget Configuration Script"
+PR = "r1"
+PV = "0.2"
+
+LICENSE = "Apache-2.0"
+LIC_FILES_CHKSUM = "file://${COREBASE}/meta/files/common-licenses/Apache-2.0;md5=89aea4e17d99a7cacdbeed46a0096b10"
+
+FILESEXTRAPATHS_prepend = "${THISDIR}/${PN}:"
+
+inherit systemd
+
+DEPENDS += "m4-native"
+DEPENDS += "systemd"
+RDEPENDS_${PN} += "bash"
+
+SYSTEMD_PACKAGES = "${PN}"
+SYSTEMD_SERVICE_${PN} = "usb_network.service"
+
+BMC_IP_ADDR ??= "169.254.95.118/16"
+BMC_USB_ECM_PRODUCT_ID ??= ""
+BMC_USB_ECM_PRODUCT_NAME ??= "${MACHINE} BMC"
+BMC_USB_ECM_HOST_MAC ??= "invalid"
+BMC_USB_ECM_DEV_MAC ??= "invalid"
+BMC_USB_ECM_BIND_DEV ??= ""
+BMC_USB_CONFIG_PRIORITY ??= ""
+BMC_USB_CONFIG_FILENAME ??= "${BMC_USB_CONFIG_PRIORITY}-bmc-usb0.network"
+
+SRC_URI += "file://00-bmc-usb0.network.m4"
+SRC_URI += "file://usb_network.service.m4"
+SRC_URI += "file://usb_network.sh"
+
+FILES_${PN} = "${bindir}/usb_network.sh"
+FILES_${PN}_append = " ${systemd_unitdir}/network/${BMC_USB_CONFIG_FILENAME}"
+
+do_compile() {
+    test "X${BMC_IP_ADDR}" != "X" || bberror "Please define BMC_IP_ADDR"
+    m4 -DM_BMC_IP_ADDR=${BMC_IP_ADDR} ${WORKDIR}/00-bmc-usb0.network.m4 > ${S}/00-bmc-usb0.network
+
+    test "X${BMC_USB_ECM_PRODUCT_ID}" != "X" || bberror "Please define BMC_USB_ECM_PRODUCT_ID"
+    test "X${BMC_USB_ECM_PRODUCT_NAME}" != "X" || bberror "Please define BMC_USB_ECM_PRODUCT_NAME"
+    test "X${BMC_USB_ECM_BIND_DEV}" != "X" || bberror "Please define BMC_USB_ECM_BIND_DEV"
+
+    m4 \
+        -DM_BMC_USB_ECM_PRODUCT_ID="${BMC_USB_ECM_PRODUCT_ID}" \
+        -DM_BMC_USB_ECM_PRODUCT_NAME="${BMC_USB_ECM_PRODUCT_NAME}" \
+        -DM_BMC_USB_ECM_HOST_MAC="${BMC_USB_ECM_HOST_MAC}" \
+        -DM_BMC_USB_ECM_DEV_MAC="${BMC_USB_ECM_DEV_MAC}" \
+        -DM_BMC_USB_ECM_BIND_DEV="${BMC_USB_ECM_BIND_DEV}" \
+        -DM_SCRIPT_INSTALL_DIR="${bindir}" \
+        ${WORKDIR}/usb_network.service.m4 > ${S}/usb_network.service
+}
+
+do_install() {
+  install -d ${D}/${bindir}
+  install -m 0755 ${WORKDIR}/usb_network.sh ${D}/${bindir}
+
+  install -d ${D}${systemd_system_unitdir}
+  install -m 0644 ${S}/usb_network.service ${D}${systemd_system_unitdir}
+
+  install -d ${D}${systemd_unitdir}/network
+  install -m 0644 ${S}/00-bmc-usb0.network \
+      ${D}${systemd_unitdir}/network/${BMC_USB_CONFIG_FILENAME}
+}