Ubuntu 服务器运维:实现自动配置网络

Ubuntu 系统中,默认通过 Netplan 工具管理网络接口。通常我们只需要手动写好 /etc/netplan/ 目录下的 YAML 文件,一劳永逸地让系统在每次启动时应用相同配置。然而,有些场景下可能需要一个脚本能:

  • 自动检测当前服务器是否已经是一个可用的静态 IP;如果网络畅通,就无需改动
  • 如果网络不可用,则尝试对服务器所有网卡进行DHCP 获取 IP,并检测能否访问外网(例如 Ping 目标 IP 或域名);
  • 一旦找到能连通外网的网卡,就固定(静态)它当前的 IP/Gateway/DNS;
  • 如果所有网卡都无法获取可用网络,退出并提示失败。

类似逻辑多用于初次部署或自动化安装场景。有以下风险需了解:

  • 如果只有单块网卡且依赖 SSH 远程,IP 变更后 SSH 会话极有可能断开;
  • 脚本会移除所有原先的 Netplan 文件后再写新的配置,过程里没有“netplan try”的回滚;
  • 如果脚本未能找出可用网卡并成功配置,系统最终会没有 Netplan 配置文件,需要人工从备份里恢复。

二、脚本逻辑概述

最终版本脚本(详见后文)包含以下步骤:

  1. STEP A:判断是否已有可用静态网络

    • 如果系统内没有任何网卡显示为“dynamic”,一般表示所有网卡要么是静态,要么未配置 IP。
    • 脚本尝试 ping -c 3 baidu.com(或其他可达地址);若能联通,则说明已有可用的静态 IP 配置,脚本直接退出,不做改动。
    • 如果 Ping 不通,则继续执行后续步骤。
  2. STEP 0:备份并清空 Netplan 文件

    • 将 /etc/netplan/*.yaml 文件备份到 /var/backups/netplan-时间戳
    • 然后 rm -f /etc/netplan/*.yaml,保证后续生成的配置不与旧文件冲突。
  3. STEP 1 ~ STEP 2:枚举所有网卡,检测/强制设置 DHCP

    • 脚本列出除 lo 以外的所有网卡(ip link show),依次检查:
    • 若成功,可马上把它转成静态配置;若失败(未分配到 IP,或无法通外网),就删掉临时文件,继续下一个网卡。
    1. 如果网卡已经是 “dynamic”(DHCP 模式),那么直接获取其当前 IP / 网关 / DNS,然后转为静态
    2. 否则,脚本临时写入一个只含 dhcp4: yes 的 netplan 文件,netplan apply 后等待几秒,看能否成功分配 IP 并Ping 通目标。
  4. STEP 3:将获取到的 IP 配置写回静态

    • 用 ip -4 addr show dev NIC 拿到 IP(带 CIDR),ip route 拿到网关,/etc/resolv.conf 里拿到 DNS;
    • 写进 /etc/netplan/99-auto-network.yaml,其中将 gateway4 替换为用 routes: 设置默认路由,并将 dhcp4 改为 no
    • nameservers 部分默认会追加 8.8.8.8 和 114.114.114.114,如果只想用这两个,可把脚本里追加的逻辑改为覆盖;
    • netplan apply 使之生效,然后做一次 Ping 测试,观察连通性。
  5. 若所有网卡都尝试完仍失败

    • 脚本会退出,并不会留下任何新的 netplan 文件;此时需要从前面备份的目录手动复制恢复。

三、常见注意事项

  1. SSH 连接风险
    如果脚本在改动当前使用的网卡 IP 段,SSH 会立刻断开,需要物理/带外多网卡支持才能继续操作。

  2. netplan apply 无回滚
    脚本使用 netplan apply 而非 netplan try,一旦出现配置不通,将无法自动回退。
    所以要么有冗余的访问方式,要么在测试环境充分验证。

  3. DNS 逻辑

    • 脚本默认将 DHCP 下发的 DNS 与 8.8.8.8114.114.114.114 一起写入静态配置;
    • 如果你只想保留那两个公共 DNS,请把相关代码改成直接覆盖。
  4. DNS 或外网

    • 脚本主要通过 ping baidu.com 判断外网连通。如果你所在网络访问百度不通,请改成 ping 114.114.114.114 或其他更适合的目标。
  5. 一次性或开机自启

    • 大多数情况下,这种自动脚本用于一次性的网络初始化,不必在每次开机都重复执行;
    • 如果确实要开机时自动跑,可做一个 systemd service 或放进 cron @reboot(但要注意网络依赖顺序)。

四、脚本示例

下文为最终整合的脚本示例:auto_dhcp_then_static.sh。其中包含:

  • 前置检查:如果没有网卡处于 DHCP(dynamic),且 ping 测试外网成功,说明已有可用静态网络,脚本立即退出。
  • 否则,清空 netplan 文件并循环尝试各网卡:
    • 若网卡已是 DHCP,则转为静态;
    • 否则临时改为 DHCP,看能否上网;
    • 一旦成功,就“锁定”成静态 IP 并停止脚本。
    • 如果所有网卡都失败,则提示错误并退出。

请将其以 UTF-8 编码保存为 auto_dhcp_then_static.sh 并给予可执行权限:

#!/usr/bin/env bash
# -*- coding: utf-8 -*-
#
# auto_dhcp_then_static.sh
#
# 功能:
#   1) 如果当前已有可用静态网络(无dynamic网卡,且能ping外网),则直接退出
#   2) 否则备份并删除 /etc/netplan/*.yaml
#   3) 枚举所有网卡(excl. lo),若网卡是dynamic则直接转静态,否则强制DHCP测试
#   4) 找到能ping通外网的网卡后,将其固定IP
#   5) 若所有网卡都失败,退出不做最终保存
#
# 注意:
#   - netplan apply 无回滚,可能导致SSH断开
#   - 默认会追加 8.8.8.8、114.114.114.114 到 DNS
#   - ping目标默认为 baidu.com,请根据需要修改

set -e  # 一旦出错立即退出

CONFIG_FILE="/etc/netplan/99-auto-network.yaml"
PING_TARGET="baidu.com"  # 或换成 114.114.114.114

###############################################################################
# STEP A:判断是否已有可用静态网络
###############################################################################
echo ">>> 检查是否已有可用的静态IP和网络连通..."

HAS_DYNAMIC=false
ALL_NICS=$(ip -o link show | awk -F': ' '{print $2}' | grep -v '^lo$')
for nic in $ALL_NICS; do
  if ip -4 addr show dev "$nic" | grep -q "dynamic"; then
    HAS_DYNAMIC=true
    break
  fi
done

# 无dynamic网卡 -> 可能全是静态或没IP -> 测试能否ping通
if [ "$HAS_DYNAMIC" = false ]; then
  echo "系统内未检测到dynamic网卡,可能已有静态IP或无IP。测试ping: $PING_TARGET"
  if ping -c 3 "$PING_TARGET" &> /dev/null; then
    echo ">>> 当前网络已可达,视为静态网络可用,无需修改。退出。"
    exit 0
  else
    echo ">>> 无法ping通,继续脚本流程..."
  fi
else
  echo ">>> 存在dynamic网卡,可能需要转换,继续执行..."
fi

###############################################################################
# STEP 0:备份并删除旧的 netplan 配置
###############################################################################
timestamp=$(date +%Y%m%d-%H%M%S)
backup_path="/var/backups/netplan-${timestamp}"

echo ">>> 备份 /etc/netplan 到: $backup_path"
mkdir -p "$backup_path"
cp -r /etc/netplan/* "$backup_path"/ 2>/dev/null || true
echo ">>> 备份完成。"

echo ">>> 删除现有 /etc/netplan/*.yaml"
rm -f /etc/netplan/*.yaml
echo ">>> 已删除旧的 netplan 文件。"
echo

###############################################################################
# 列出非 lo 的网卡
###############################################################################
NICS=$(ip -o link show | awk -F': ' '{print $2}' | grep -v '^lo$')
if [[ -z "$NICS" ]]; then
  echo "没有可用网卡(除lo外)!退出。"
  exit 0
fi

###############################################################################
# 函数:尝试给网卡启用 DHCP 并测试连通
###############################################################################
try_dhcp_and_test() {
  local nic="$1"

  echo ">>> 对网卡 $nic 写入临时 DHCP 配置..."
  cat <<EOF > "$CONFIG_FILE"
network:
  version: 2
  renderer: networkd
  ethernets:
    $nic:
      dhcp4: yes
EOF

  chmod 600 "$CONFIG_FILE"

  echo ">>> netplan apply (DHCP) on $nic..."
  netplan apply

  echo ">>> 等待5秒,让DHCP有机会拿到IP..."
  sleep 5

  local ip_cidr
  ip_cidr=$(ip -4 addr show dev "$nic" | grep -oP '(?<=inet\s)\S+')
  if [[ -z "$ip_cidr" ]]; then
    echo ">>> DHCP模式下,$nic 未获取到IP。"
    return 1
  fi

  echo ">>> 测试ping: $PING_TARGET"
  if ping -c 3 "$PING_TARGET" &> /dev/null; then
    echo ">>> 网卡 $nic 已可连通 $PING_TARGET。"
    return 0
  else
    echo ">>> 虽拿到IP,但无法ping通 $PING_TARGET。"
    return 2
  fi
}

###############################################################################
# 函数:将当前 DHCP 下发的IP/Gateway/DNS 转为静态
###############################################################################
make_static_config() {
  local nic="$1"

  echo ">>> 读取 $nic 的IP/Gateway/DNS,准备写入静态配置..."
  local ip_cidr
  ip_cidr=$(ip -4 addr show dev "$nic" | grep -oP '(?<=inet\s)\S+')

  local gateway
  gateway=$(ip route | grep -E "^default .* $nic" | awk '{print $3}')

  local dns_list
  dns_list=$(grep -E '^nameserver' /etc/resolv.conf | awk '{print $2}' | xargs echo)
  dns_list="$dns_list 8.8.8.8 114.114.114.114"

  # 拼成 YAML 列表
  local dns_yaml="["
  local first=1
  for dns in $dns_list; do
    if [[ $first -eq 1 ]]; then
      dns_yaml="$dns_yaml$dns"
      first=0
    else
      dns_yaml="$dns_yaml, $dns"
    fi
  done
  dns_yaml="$dns_yaml]"

  echo ">>> 生成最终静态配置 $CONFIG_FILE"
  cat <<EOF > "$CONFIG_FILE"
network:
  version: 2
  renderer: networkd
  ethernets:
    $nic:
      addresses:
        - $ip_cidr
EOF

  if [[ -n "$gateway" ]]; then
    cat <<EOF >> "$CONFIG_FILE"
      routes:
        - to: default
          via: $gateway
EOF
  fi

  cat <<EOF >> "$CONFIG_FILE"
      nameservers:
        addresses: $dns_yaml
      dhcp4: no
EOF

  chmod 600 "$CONFIG_FILE"

  echo ">>> netplan apply (static) on $nic..."
  netplan apply

  echo ">>> 再次测试ping: $PING_TARGET"
  if ping -c 3 "$PING_TARGET" &> /dev/null; then
    echo ">>> $nic (静态) 可成功访问 $PING_TARGET。"
  else
    echo ">>> 警告:$nic (静态) 依然无法ping通 $PING_TARGET。"
  fi
}

###############################################################################
# 主循环:对每个网卡检查
###############################################################################
found_working_nic=false

for nic in $NICS; do
  echo "=== 检查网卡: $nic ==="

  # 如果网卡是 DHCP (dynamic),直接转静态
  if ip -4 addr show dev "$nic" | grep -q "dynamic"; then
    echo ">>> 网卡 $nic 已是 DHCP 模式,直接转换为静态..."
    make_static_config "$nic"
    found_working_nic=true
    break
  else
    # 否则尝试强制 DHCP
    if try_dhcp_and_test "$nic"; then
      echo ">>> 成功拿到IP并连通外网,现在转为静态..."
      make_static_config "$nic"
      found_working_nic=true
      break
    else
      echo ">>> $nic DHCP 尝试失败或无法连接外网,继续下一个网卡..."
      rm -f "$CONFIG_FILE"
    fi
  fi
done

if [ "$found_working_nic" = false ]; then
  echo
  echo "没有任何网卡成功配置DHCP并连通外网,退出,不做最终保存。"
fi

echo
echo "===== [脚本结束] ====="

五、总结

该脚本是一个“先判断是否已有可用静态网络,若否再进行DHCP→静态”的综合示例。主要功能与注意事项包括:

  1. 如果脚本检测到已有静态网络并可访问外网,则不修改任何配置
  2. 备份后清空 /etc/netplan/避免旧文件干扰;
  3. 逐一尝试网卡
    • 若已是 DHCP,直接将其当前分配信息转静态;
    • 否则强制启用 DHCP,能连通就转静态,不能就尝试下一个网卡;
  4. 最终只固定找到的首个可用网卡。若无可用网卡,全局退出、不写任何新的 netplan 文件;
  5. DNS 默认追加 8.8.8.8 与 114.114.114.114,可按需改成只使用这两个;
  6. 警惕 SSH 断开、无 netplan try 回滚等风险,确保有可回退或带外访问方式。

在实际生产环境,通常只需在安装或首配时运行此脚本一次,一旦拿到稳定 IP 并静态化,后续就可无需重复执行。

来源:DevOpsAI

THE END