device/rockchip/rk3588/rk3588_docker/container/aic.sh

574 lines
18 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
### 默认配置,可根据需求进行修改
# Android容器配置文件地址
CONTAINER_CONFIG_DIR=/userdata/container/android_config
COMMON_CONFIG=$CONTAINER_CONFIG_DIR/container_common.conf
# docker daemon.json文件地址
DOCKER_DAEMON_FILE=/userdata/container/daemon.json
# 默认容器数量
CONTAINER_NUM=1
# 默认网络类型: macvlan_dhcp/macvlan_static/docker0/host
# 如需修改请修改android_config/container_common.conf或container_x.conf对应的network.type
NETWORK_TYPE=docker0
# macvlan网络名称
MACVLAN_NAME=macvlan
# 容器data地址
ANDROID_DATA_DIR=/userdata/container/android_data
# 容器data是否使用project quota 默认没有配置需要配置设置为true即可
CONTAINER_DATA_PRJQUOTA=
# 启用CONTAINER_DATA_PRJQUOTA后配置
CONTAINER_DATA_PRJQUOTA_LIMIT=10G
# 全局变量
DOCKER_IMAGE="test"
ANDROID_IP="192.168.12.2"
MAC_ADDR=""
CONTAINER_DATA_DIR="/userdata/android_data/data_0"
run_cmd() {
cmd=$1
echo "$(date +"%Y-%m-%d %H:%M:%S") $cmd"
eval "$cmd"
if [ $? -ne 0 ]; then
echo "$(date +"%Y-%m-%d %H:%M:%S") $cmd error."
exit 1
fi
}
install_package() {
package_name=$1
if [ "$(dpkg -l | grep $package_name | awk '{print $2}')"x != "$package_name"x ]; then
run_cmd "apt-get install $package_name -y"
else
if [ "$(dpkg -l | grep $package_name | awk '{print $1}')"x != "ii"x ]; then
run_cmd "apt-get install $package_name -y"
fi
fi
}
# 解析配置文件并将其中的键值对export为环境变量
# 输入参数:配置文件路径
parse_config() {
config_file=$1
# 检查配置文件是否存在
if [ ! -f "$config_file" ]; then
echo "配置文件 $config_file 不存在."
exit 1
fi
# 逐行读取配置文件并设置变量
while IFS= read -r line; do
# 忽略注释和空行
if [[ $line =~ ^\s*# || -z $line ]]; then
continue
fi
# 使用等号(=)分隔键值对
key=$(echo "$line" | cut -d= -f1)
value=$(echo "$line" | cut -d= -f2)
# 移除键和值中的空白字符,并将key的转换为大写
key=$(echo "$key" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')
value=$(echo "$value" | tr -d '[:space:]')
# 因为shell脚本环境变量不能包含“.”, 将key中的“.”转换为“_”
key=$(echo "$key" | sed 's/\./_/g')
# 将键值对设置为环境变量
export "$key=$value"
done < "$config_file"
# export
}
# 判断输入的IMAGE_TAG是否存在
is_docker_image_exist(){
IMAGE_TAG=$1
ANDROID_IMAGE_TAG_LIST=$(docker images | grep -v REPOSITORY | awk -F" " '{print $2}')
for ANDROID_IMAGE in $ANDROID_IMAGE_TAG_LIST;do
if [ "$ANDROID_IMAGE"x == "$IMAGE_TAG"x ];then
echo "Android image is exist"
return 1
fi
done
return 0
}
# 创建DOCKER IMAGE
# 需要输入一个参数:容器固件地址
creat_docker_image() {
if [ $# -eq 1 ]; then
SUPER_IMG=$1
else
echo "error: you must specify the path to android firmware"
exit 1
fi
# 检查输入的容器固件地址是否存在
if test -e "$SUPER_IMG"; then
echo "Found container firmware in: $SUPER_IMG"
else
echo "ERROR: Container firmware is not exist"
return
fi
# 删除所有没有使用的docke images / network / container
run_cmd "docker system prune -af"
# 解压容器固件主要就是super.img
run_cmd "rm -rf super_img"
run_cmd "mkdir super_img"
run_cmd "tar -xvf $SUPER_IMG"
# 挂载分区
# mount system as root
run_cmd "mkdir super_img/root"
run_cmd "sudo mount super_img/system.img super_img/root -o rw"
MOUNT_LIST=$(ls super_img| grep img)
for i in $MOUNT_LIST; do
mount_point="${i%*${i:(-4)}}"
if [ "$mount_point"x == "system"x ]; then
echo "system has mounted as root already"
else
echo "mount_point: $mount_point"
run_cmd "sudo mount super_img/$i super_img/root/$mount_point -o rw"
fi
done
# Android 容器无需再挂载分区所以删掉fstab.rk30board
# if [ -e "super_img/root/vendor/etc/fstab.rk30board" ]; then
# sudo rm super_img/root/vendor/etc/fstab.rk30board
# fi
# 准备版本信息
SOC_NAME=$(tr -d '\0' </sys/firmware/devicetree/base/compatible | awk -F, '{print $3}')
VERSION=$(cat super_img/root/vendor/build.prop | grep ro.rksdk.version |awk -F= '{print $2}')
BUILD_TIME=$(cat super_img/root/vendor/build.prop | grep ro.vendor.build.fingerprint | awk -F= '{print $2}'|awk -F/ '{print $5}' | awk -F: '{print $1}')
# 判断Android image是否已经存在
IMAGE_TAG="$VERSION-$BUILD_TIME"
is_docker_image_exist $IMAGE_TAG
IS_IMAGE_EXIST=$?
if [ "$IS_IMAGE_EXIST"x == "0"x ]; then
sudo tar --xattrs -c -C super_img/root . | sudo docker import -c 'ENTRYPOINT ["/init", "androidboot.hardware=rk30board"]' - $SOC_NAME:$VERSION-$BUILD_TIME
#sudo docker save -o $SOC_NAME-$VERSION.img $SOC_NAME:$VERSION
#sudo docker rmi $TARGET_PRODUCT:$VERSION
else
echo "Android image exit, do not load again!"
fi
# umount分区
DOCKER_IMAGE=$SOC_NAME:$VERSION-$BUILD_TIME
for i in $MOUNT_LIST; do
mount_point="${i%*${i:(-4)}}"
if [ "$mount_point"x == "system"x ]; then
echo "systen mount as root, umount late"
else
echo "unmount: $mount_point"
sudo umount super_img/root/$mount_point
fi
done
sudo umount super_img/root
sudo rm -rf super_img
}
# 准备Android容器的data目录
# 根据容器编号区分
# 输入参数: 容器编号
prepare_android_data() {
CONTAINER_NO=$1
CONTAINER_DATA_DIR="$ANDROID_DATA_DIR/data_$CONTAINER_NO"
run_cmd "rm -rf $CONTAINER_DATA_DIR"
run_cmd "mkdir -p $CONTAINER_DATA_DIR"
# project quota 配额
if [ "$CONTAINER_DATA_PRJQUOTA"x == "true"x ]; then
echo "container data use project quota"
run_cmd "apt install quota"
PROJECT_ID="110$CONTAINER_NO"
run_cmd "chattr +P -p $PROJECT_ID $CONTAINER_DATA_DIR"
run_cmd "setquota -P $PROJECT_ID $CONTAINER_DATA_PRJQUOTA_LIMIT $CONTAINER_DATA_PRJQUOTA_LIMIT 0 0 /userdata"
fi
}
# 获取网段中一个空闲ip
# 注意本机虚拟机的ip需要过滤掉
# 需要安装fpingapt install fping
get_ip(){
install_package "fping"
IP_NETWORK_SEGMENT=$(ifconfig -a|grep inet|grep -v 127.0.0.1 | grep -v 172.17.0|grep -v inet6|awk '{print $2}' | awk -F"." '{print $1"."$2"."$3".0/24"}')
IDLE_IP_LIST=$(fping -ugq $IP_NETWORK_SEGMENT)
CONTAINER_ID_LIST=$(docker ps -a |grep -v "CONTAINER ID" | awk '{print $1}')
for IP in $IDLE_IP_LIST; do
for CONTAINER_ID in $CONTAINER_ID_LIST; do
CONTAIN_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $CONTAINER_ID)
if [ "$CONTAIN_IP"x == "$IP"x ]; then
IP=""
break
fi
done
if [ "$IP"x != ""x ]; then
break
fi
done
ANDROID_IP=$IP
echo "get a valid IP: $IP"
}
# 创建macvlan网络
creat_macvlan_network(){
echo " creat macvlan network ..."
# 获取所有网络接口的名称,wifi不支持macvlan
all_interfaces=$(ip link show | awk -F': ' '{print $2}' | grep -v "lo" | grep -v veth | grep -v docker | grep -v wlan)
# 遍历每个网络接口并尝试连接到互联网, 默认使用第一个能联网的网络接口
for interface in $all_interfaces; do
if ping -c 1 -W 1 www.baidu.com -I $interface &> /dev/null; then
PARENT=$interface
SUB_NET=$(ip addr show dev $interface | awk '/inet /{print $2}' | awk -F"." '{print $1"."$2"."$3".0/24"}')
break
fi
done
# 物理机网卡设置为混杂模式
run_cmd "ip link set $PARENT promisc on"
run_cmd "docker network create -d macvlan --subnet=$SUB_NET -o macvlan_mode=bridge -o parent=$PARENT $MACVLAN_NAME"
}
# 随机Mac地址
random_mac_addr() {
# 生成一个随机的mac地址
# od -An -N6 -tx1 /dev/urandom使用 /dev/urandom 从操作系统中生成6个随机字节
# sed -e 's/^ *//':去掉输出中每行前面可能的空格
# sed -e 's/ */:/g'将每个字节之间的空格替换为冒号以匹配标准MAC地址格式
# sed -e 's/:$//'删除MAC地址末尾的冒号如果有的话
# sed -e 's/^\(.\)[13579bdf]/\10/'确保生成的MAC地址的最低有效位LSB的第二位是0
# 以表示这是一个本地MAC地址而不是全局唯一的MAC地址
MAC_ADDR=$(od -An -N6 -tx1 /dev/urandom | sed -e 's/^ *//' -e 's/ */:/g' -e 's/:$//' -e 's/^\(.\)[13579bdf]/\10/')
}
# 准备网络是否使用macvlan 使用静态or动态
# 静态网络是否指定了ip没有则先获取有效ip
prepare_network() {
IS_MACVLAN=$(echo $NETWORK_TYPE | awk -F"_" '{print $1}')
echo "NETWORK_TYPE=$NETWORK_TYPE, IS_MACVLAN=$IS_MACVLAN"
if [ "$IS_MACVLAN"x == "macvlan"x ]; then
MACVLAN=$(docker network ls |awk '{print $3}' | grep macvlan)
if [ "$MACVLAN"x != "macvlan"x ]; then
creat_macvlan_network
fi
fi
# macvlan 使用静态ip
if [ "$NETWORK_TYPE"x == "macvlan_static"x ]; then
# 配置文件中没有指定ip需要获取一个空闲ip
get_ip
fi
random_mac_addr
}
# 判断是否输入容器名是否已存在
is_container_name_exist() {
CONTAINER_NAME=$1
CONTAINER_ID=$(docker ps -a|grep $CONTAINER_NAME | awk '{print $1}')
if [ "$CONTAINER_ID"x != ""x ]; then
echo "container name $CONTAINER_NAME is exist: $CONTAINER_ID!"
return 1
fi
return 0
}
# 执行docker run 命令运行容器
run_docker_image() {
NAME=$1
DATA_DIR=$2
ANDROID_IMAGE=$3
CONTAINER_CONFIG=$4
CONTAINER_NO=$5
# 准备docker run 参数
CONTAINER_NAME="--name=$NAME"
HOST_NAME="--hostname=$NAME"
DATA_BIND="-v $DATA_DIR:/data"
CONFIG_COMMON="-v $CONTAINER_CONFIG_DIR/container_common.conf:/vendor/etc/container/container_common.conf"
CONFIG_PRIVATE="-v $CONTAINER_CONFIG_DIR/$CONTAINER_CONFIG:/vendor/etc/container/container.conf"
PORT_BIND="-p 110$CONTAINER_NO:5555"
CONTAINER_MAC="--mac-address=$MAC_ADDR"
if [ "$NETWORK_TYPE"x == "macvlan_static"x ]; then
CONTAINER_NETWORK="--network=$MACVLAN_NAME"
CONTAINER_IP="--ip=$ANDROID_IP"
elif [ "$NETWORK_TYPE"x == "macvlan_dhcp"x ]; then
CONTAINER_NETWORK="--network=$MACVLAN_NAME"
CONTAINER_IP=""
PORT_BIND=""
elif [ "$NETWORK_TYPE"x == "docker0"x ]; then
# docker0 网络
CONTAINER_NETWORK=""
CONTAINER_IP=""
elif [ "$NETWORK_TYPE"x == "host"x ]; then
CONTAINER_NETWORK="--network=host"
CONTAINER_IP=""
# 使用host网络不支持指定mac地址
CONTAINER_MAC=""
fi
run_cmd "docker run -itd --restart=always --privileged $CONTAINER_NAME $HOST_NAME $DATA_BIND \
$CONFIG_COMMON $CONFIG_PRIVATE \
$CONTAINER_NETWORK $CONTAINER_MAC $CONTAINER_IP $PORT_BIND \
$ANDROID_IMAGE"
}
# 更新容器固件仅更新固件不改变网络、data数据等
# 不支持Android跨版本升级
# 输入参数:
# 1、容器固件地址必选
# 2、容器名称或者容器ID, 可选。未指定时,升级全部。
update_container() {
if [ $# -eq 2 ]; then
NEW_IMAGE_PATH=$1
CONTAINER_NAME=$2
elif [ $# -eq 1 ]; then
NEW_IMAGE_PATH=$1
CONTAINER_NAME=""
fi
# 安装json解析器
install_package "jq"
# 1. 准备新的容器固件
if test -e "$NEW_IMAGE_PATH"; then
creat_docker_image $NEW_IMAGE_PATH
fi
# 2. 确定要升级的容器, 并停止容器
UPDATE_CONTAINER_LIST=()
if [ -n "$CONTAINER_NAME" ]; then
is_container_name_exist $CONTAINER_NAME
IS_CONTAINER_EXIST=$?
if [ "$IS_CONTAINER_EXIST" == "1" ]; then
run_cmd "docker stop $CONTAINER_NAME"
# 如果输入是container_id需要进行转换成container_name,便于后面查找容器编号android_x 中的x就是容器编号
UPDATE_CONTAINER_NAME=$(docker ps -a | grep $CONTAINER_NAME | awk '{print $NF}')
UPDATE_CONTAINER_LIST+=($UPDATE_CONTAINER_NAME)
else
echo "FATAL: Not found $CONTAINER_NAME"
exit 1
fi
else
CONTAINER_LIST=$(docker ps -a | awk '{print $NF}' | sed 1d)
for CONTAINER in $CONTAINER_LIST; do
run_cmd "docker stop $CONTAINER"
UPDATE_CONTAINER_LIST+=($CONTAINER)
done
fi
# 3. 删除原来的容器并升级容器固件
CONTAINER_NUM=${#UPDATE_CONTAINER_LIST[@]}
echo "CONTAINER_NUM=$CONTAINER_NUM"
for UPDATE_CONTAINER_NAME in ${UPDATE_CONTAINER_LIST[@]}; do
ANDROID_IP=$(docker inspect $UPDATE_CONTAINER_NAME | jq -r '.[0].NetworkSettings.IPAddress')
MAC_ADDR=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.MacAddress}}{{end}}' $UPDATE_CONTAINER_NAME)
CONTAINER_NO=$(echo "$UPDATE_CONTAINER_NAME" | awk -F'_' '{print $2}')
CONTAINER_DATA_DIR=$(docker inspect $UPDATE_CONTAINER_NAME | jq -r '.[0].HostConfig.Binds' | grep "/data" |awk -F':' '{print $1}' | tr -d '"')
COTAINER_CONFIG="container_$CONTAINER_NO.conf"
run_cmd "docker rm -f $UPDATE_CONTAINER_NAME"
run_docker_image $UPDATE_CONTAINER_NAME $CONTAINER_DATA_DIR $DOCKER_IMAGE $COTAINER_CONFIG $CONTAINER_NO
done
}
# 运行容器
# 输入两个参数:
# 1、固件地址必选或者DOCKER IMAGE: REPOSITORY:TAG
# 2、容器数量可选没有配置默认为1
run_container() {
if [ $# -eq 2 ]; then
CONTAINER_NUM=$2
IMAGE_OR_PATH=$1
elif [ $# -eq 1 ]; then
CONTAINER_NUM=1
IMAGE_OR_PATH=$1
fi
# 判断IMAGE_OR_PATH是否为一个文件
# IMAGE_OR_PATH 为文件时creat DOCKER IMAGE
# IMAGE_OR_PATH 为非文件时判断IMAGE是否存在
if test -e "$IMAGE_OR_PATH"; then
creat_docker_image $IMAGE_OR_PATH
else
# 判断输入的 IMAGE 是否存在
IMAGE_TAG="$(echo "$IMAGE_OR_PATH" | cut -d: -f2)"
is_docker_image_exist $IMAGE_TAG
IS_IMAGE_EXIST=$?
if [ "$IS_IMAGE_EXIST"x == "1"x ]; then
echo "$IMAGE_OR_PATH IMAGE is exist"
DOCKER_IMAGE="$IMAGE_OR_PATH"
else
echo "FATAL: Not found $IMAGE_OR_PATH, please check it"
exit 1
fi
fi
if [ "$CONTAINER_NUM" -le 0 ]; then
echo "CONTAINER_NUM=$CONTAINER_NUM <= 0, set CONTAINER_NUM=1"
CONTAINER_NUM=1
fi
for ((i = 0; i < ${CONTAINER_NUM}; i++)); do
NEW_CONTAINER_NO=$i
# 获取已经存在的Android容器序号并按大小排列
EXIST_CONTAINER_NO_LIST=$(docker ps | grep -v "CONTAINER ID" | awk -F" " '{print $NF}'|awk -F"_" '{print $2}'|sort -n)
for EXIST_CONTAINER_NO in $EXIST_CONTAINER_NO_LIST; do
echo "EXIST_CONTAINER_NO=$EXIST_CONTAINER_NO, NEW_CONTAINER_NO=$NEW_CONTAINER_NO"
if [ "$EXIST_CONTAINER_NO" -lt "$NEW_CONTAINER_NO" ];then
continue
fi
if [ "$EXIST_CONTAINER_NO" == "$NEW_CONTAINER_NO" ];then
NEW_CONTAINER_NO=$(($NEW_CONTAINER_NO + 1))
continue
fi
break
done
echo "NEW_CONTAINER_NO=$NEW_CONTAINER_NO"
CONTAINER_NAME="android"_$NEW_CONTAINER_NO
COTAINER_CONFIG="container_$NEW_CONTAINER_NO.conf"
parse_config $COMMON_CONFIG
parse_config $CONTAINER_CONFIG_DIR/$COTAINER_CONFIG
prepare_android_data $NEW_CONTAINER_NO
prepare_network
run_docker_image $CONTAINER_NAME $CONTAINER_DATA_DIR $DOCKER_IMAGE $COTAINER_CONFIG $NEW_CONTAINER_NO
done
}
# 初始化设备环境
# 安装docker及必要的应用配置docker环境等
init_device() {
run_cmd "update-alternatives --set iptables /usr/sbin/iptables-legacy"
# 安装必要软件包
run_cmd "sudo apt update"
install_package "fping"
# 安装docker包
install_package "docker.io"
# 修改docker的默认存储至/data目录
if [ ! -e "/etc/docker/daemon.json" ]; then
mkdir -p /data/docker
cp $DOCKER_DAEMON_FILE /etc/docker/daemon.json
systemctl daemon-reload
systemctl restart docker
fi
# 如果未配置wifibt 需关掉rkwifibt服务避免因为ko加载问题导致卡住
# SDK 版本不同wifibt服务有变化
run_cmd "systemctl disable rkwifibt"
run_cmd "systemctl disable wifibt-init.service"
}
# 打印帮助函数
print_help() {
echo "用法: $0 [-h] [-i][-c firmware_path] [-r image_or_path [container_num] ] [-u update_image_or_path [container_name] ]"
echo " 该脚本主要是用来在 Debian 系统上通过 docker 部署和运行Android容器。"
echo " 首次部署环境,请先运行 ./aic.sh -i 命令进行设备的初始化。"
echo "选项:"
echo " -h 显示帮助信息"
echo " -i 初始化设备环境:./aic.sh -i"
echo " 安装docker及必须的应用包初始化docker环境和容器运行环境等"
echo " -c 创建docker image: ./aic.sh -c firmware_path"
echo " 参数1firmware_path: 必选参数,表示容器的固件地址"
echo " 例如: ./aic.sh -c /path/to/android_firmware.tgz"
echo " -r 运行容器: ./aic.sh -r image_or_path [container_num]"
echo " 参数1image_or_path: 必选参数表示容器的固件地址或者docker imageREPOSITORY:TAG"
echo " 参数2container_num: 可选参数表示容器数量未配置默认为1个"
echo " 例如: ./aic.sh -r /path/to/android_firmware.tgz 3"
echo " -u 升级容器: ./aic.sh -u update_image_or_path [container_name/container_id]"
echo " 参数1update_image_or_path: 必选参数表示要升级的容器的固件地址或者imageREPOSITORY:TAG"
echo " 参数2container_name/container_id: 可选参数要升级的容器名称或容器ID未配置默认全部升级"
echo " 例如: ./aic.sh -c /path/to/android_firmware.tgz android_0"
}
# 解析命令行选项和参数
parse_opts() {
HAS_OPTION=false
OPTION=()
while getopts ":hvic:r:u:" opt; do
case $opt in
h)
print_help
HAS_OPTION=true
exit 0
;;
i)
echo "init device for run container"
init_device
HAS_OPTION=true
;;
v)
echo "aic.sh version is v1.0.2"
HAS_OPTION=true
exit 0
;;
c)
echo "creat docker images, OPTARG is: $OPTARG"
ANDROID_FIRMWARE_PATH=$OPTARG
creat_docker_image $ANDROID_FIRMWARE_PATH
HAS_OPTION=true
;;
r)
OPTION+=("$OPTARG")
shift $((OPTIND-1))
OPTION+=("$@")
echo "run docker images, OPTARG is: ${OPTION[*]}"
IMAGE_OR_PATH=${OPTION[0]}
CONTAINER_NUM=${OPTION[1]}
echo "IMAGE_OR_PATH=$IMAGE_OR_PATH, CONTAINER_NUM=$CONTAINER_NUM"
run_container $IMAGE_OR_PATH $CONTAINER_NUM
HAS_OPTION=true
;;
u)
OPTION+=("$OPTARG")
shift $((OPTIND-1))
OPTION+=("$@")
echo "update docker container images, OPTARG is: ${OPTION[*]}"
IMAGE_OR_PATH=${OPTION[0]}
CONTAINER_NO=${OPTION[1]}
echo "IMAGE_OR_PATH=$IMAGE_OR_PATH, CONTAINER_NO=$CONTAINER_NO"
update_container $IMAGE_OR_PATH $CONTAINER_NO
HAS_OPTION=true
;;
?)
echo "无效选项: -$OPTARG" >&2
print_help
exit 1
;;
:)
echo "选项 -$OPTARG 需要参数." >&2
print_help
exit 1
;;
esac
done
if [ "$HAS_OPTION" == false ]; then
echo "无效选项,请检查输入参数:"
echo "运行脚本,需要指明要进行的操作(-c/-r/-i..."
print_help
fi
}
main() {
# 如果没有提供任何参数,则打印帮助信息
if [ $# -eq 0 ]; then
print_help
exit 0
fi
# parse_config $COMMON_CONFIG
parse_opts "$@"
}
main "$@"