本指南适用于裸金属服务器使用中,需要对操作系统 IPv4 地址进行新增、变更或删除的情形。此时,您需要登录服务器,执行我们提供的 IP 变更脚本。
根据服务器的操作系统选择对应的脚本,并将脚本内容保存为文件,例如:
change-ip.sh可以使用以下命令创建并编辑脚本文件:
vi change-ip.sh将对应的脚本内容复制到文件中并保存。保存完成后,为脚本添加执行权限:
chmod +x change-ip.sh打开脚本文件,找到 NEW_IPS 配置项:
NEW_IPS=()将需要配置的 IPv4 地址(可在IP管理页面一键复制)填写到 NEW_IPS 中(也可不填,完全使用命令行参数指定见4)。多个 IP 地址使用空格分隔,例如:
NEW_IPS=("192.168.1.20" "192.168.1.30")上述配置表示脚本执行后,将为服务器配置以下两个 IPv4 地址:
192.168.1.20
192.168.1.30
完成 IP 地址配置后,使用 sudo 执行脚本:
sudo ./change-ip.sh脚本将按照配置的目标 IP 地址执行变更操作。
除在脚本中配置 NEW_IPS 外,也可以在执行脚本时通过命令行参数传入 IP 地址。
例如:
sudo ./change-ip.sh "192.168.1.10" "192.168.1.11"命令行参数中的 IP 地址会与脚本 NEW_IPS 中配置的 IP 地址进行合并并去重,然后统一应用。
例如,脚本中配置:
NEW_IPS=("192.168.1.20" "192.168.1.30")执行时传入:
sudo ./change-ip.sh "192.168.1.10" "192.168.1.11"最终脚本处理的 IP 地址为:
192.168.1.10
192.168.1.11
192.168.1.20
192.168.1.30
如果同一个 IP 同时存在于 NEW_IPS 和命令行参数中,只会保留一个。
完整操作流程如下:
选择对应操作系统的脚本
↓
创建 change-ip.sh 文件
↓
将脚本内容复制到文件中
↓
添加执行权限
↓
配置 NEW_IPS
↓
执行脚本
↓
完成 IPv4 地址变更#!/usr/bin/env bash
set -o pipefail
# IPs configured here are merged with command-line IPs.
# Example: NEW_IPS=("192.169.21.7" "192.169.21.8")
NEW_IPS=()
CONFIG_DIR="/etc/sysconfig/network-scripts"
INTERFACE=""
CONFIG_FILE=""
BACKUP_FILE=""
PREFIX=""
changes_started=0
success=0
declare -a OLD_CIDRS=()
declare -a NEW_IPS_RESULT=()
log_info() {
printf '[INFO] %s\n' "$*"
}
log_warn() {
printf '[WARN] %s\n' "$*" >&2
}
log_error() {
printf '[ERROR] %s\n' "$*" >&2
}
die() {
log_error "$*"
exit 1
}
usage() {
cat <<EOF
Usage:
$0 <IP1> [IP2] [IP3] ...
Examples:
$0 192.169.21.7
$0 192.169.21.7 192.169.21.8
EOF
}
valid_ipv4() {
local ip="$1"
local octet
local -a octets
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] ||
return 1
IFS=. read -r -a octets <<< "$ip"
for octet in "${octets[@]}"; do
(( 10#$octet <= 255 )) || return 1
done
}
add_unique_ip() {
local ip="$1"
local existing
for existing in "${NEW_IPS_RESULT[@]}"; do
[ "$existing" = "$ip" ] && return 0
done
NEW_IPS_RESULT+=("$ip")
}
detect_interface() {
INTERFACE="$(
ip -4 route get 1.1.1.1 2>/dev/null |
awk '{
for (i = 1; i <= NF; i++) {
if ($i == "dev") {
print $(i + 1)
exit
}
}
}'
)"
[ -n "$INTERFACE" ] ||
INTERFACE="$(
ip -4 route show default 2>/dev/null |
awk 'NR == 1 {print $5}'
)"
[ -n "$INTERFACE" ] ||
die "Unable to determine the default IPv4 interface"
[ "$INTERFACE" != "lo" ] ||
die "The detected interface is loopback"
}
get_config_value() {
local key="$1"
awk -F= -v key="$key" '
$0 !~ /^[[:space:]]*#/ && $1 == key {
value = substr($0, index($0, "=") + 1)
print value
exit
}
' "$CONFIG_FILE" |
sed \
-e 's/^[[:space:]]*//' \
-e 's/[[:space:]]*$//' \
-e 's/^"//' \
-e 's/"$//' \
-e "s/^'//" \
-e "s/'$//"
}
get_current_addresses() {
OLD_CIDRS=()
while IFS= read -r cidr; do
[ -n "$cidr" ] && OLD_CIDRS+=("$cidr")
done < <(
ip -4 -o addr show dev "$INTERFACE" scope global 2>/dev/null |
awk '{print $4}'
)
}
get_prefix() {
local cidr
local config_prefix
for cidr in "${OLD_CIDRS[@]}"; do
PREFIX="${cidr##*/}"
break
done
if [ -z "$PREFIX" ]; then
config_prefix="$(get_config_value "PREFIX0")"
[ -n "$config_prefix" ] ||
config_prefix="$(get_config_value "PREFIX")"
PREFIX="$config_prefix"
fi
[[ "$PREFIX" =~ ^[0-9]+$ ]] &&
(( 10#$PREFIX <= 32 )) ||
die "Unable to determine a valid IPv4 prefix"
PREFIX=$((10#$PREFIX))
}
create_backup() {
local backup_dir
backup_dir="$(mktemp -d "/root/change-ip-backup-${INTERFACE}.XXXXXX")" ||
die "Failed to create backup directory"
chmod 700 "$backup_dir"
BACKUP_FILE="$backup_dir/$(basename "$CONFIG_FILE")"
cp -p "$CONFIG_FILE" "$BACKUP_FILE" ||
die "Failed to create backup: $BACKUP_FILE"
log_info "Backup created: $BACKUP_FILE"
}
update_config() {
local temp_file
local i
temp_file="$(mktemp "${CONFIG_FILE}.tmp.XXXXXX")" ||
die "Failed to create temporary configuration file"
awk '
$0 !~ /^[[:space:]]*IPADDR[0-9]*=/ &&
$0 !~ /^[[:space:]]*PREFIX[0-9]*=/ &&
$0 !~ /^[[:space:]]*NETMASK[0-9]*=/ &&
$0 !~ /^[[:space:]]*DEVTIMEOUT=/ {
print
}
' "$CONFIG_FILE" > "$temp_file" || {
rm -f "$temp_file"
die "Failed to process configuration file"
}
for ((i = 0; i < ${#NEW_IPS_RESULT[@]}; i++)); do
printf 'IPADDR%d=%s\n' "$i" "${NEW_IPS_RESULT[$i]}" >> "$temp_file"
printf 'PREFIX%d=%s\n' "$i" "$PREFIX" >> "$temp_file"
done
# Keep DEVTIMEOUT as the final line of the ifcfg file.
printf 'DEVTIMEOUT=60\n' >> "$temp_file"
chmod --reference="$CONFIG_FILE" "$temp_file" 2>/dev/null || true
chown --reference="$CONFIG_FILE" "$temp_file" 2>/dev/null || true
mv -f "$temp_file" "$CONFIG_FILE" ||
die "Failed to update $CONFIG_FILE"
log_info "Configuration file updated"
}
remove_old_runtime_addresses() {
local old_cidr
local old_ip
local new_ip
local keep
for old_cidr in "${OLD_CIDRS[@]}"; do
old_ip="${old_cidr%%/*}"
keep=false
for new_ip in "${NEW_IPS_RESULT[@]}"; do
if [ "$old_ip" = "$new_ip" ]; then
keep=true
break
fi
done
[ "$keep" = true ] && continue
if ip -4 addr show dev "$INTERFACE" | grep -qw "$old_cidr"; then
log_info "Removing old runtime address: $old_cidr"
ip -4 addr del "$old_cidr" dev "$INTERFACE" ||
die "Failed to remove old runtime address: $old_cidr"
fi
done
}
verify_runtime() {
local actual
local expected
actual="$(
ip -4 -o addr show dev "$INTERFACE" scope global |
awk '{print $4}' |
sort
)"
expected="$(
for ip in "${NEW_IPS_RESULT[@]}"; do
printf '%s/%s\n' "$ip" "$PREFIX"
done | sort
)"
if [ "$actual" != "$expected" ]; then
log_error "Runtime IPv4 addresses do not match the requested list"
log_error "Actual: ${actual//$'\n'/, }"
log_error "Expected: ${expected//$'\n'/, }"
return 1
fi
return 0
}
restore_backup() {
[ -n "$BACKUP_FILE" ] && [ -f "$BACKUP_FILE" ] || return 1
log_warn "Restoring the original configuration..."
cp -p "$BACKUP_FILE" "$CONFIG_FILE" ||
return 1
ifdown "$INTERFACE" >/dev/null 2>&1 || true
if ! ifup "$INTERFACE" >/dev/null 2>&1; then
log_error "Configuration was restored, but interface activation failed"
return 1
fi
log_info "Original configuration restored"
}
cleanup() {
local status=$?
trap - EXIT
if [ "$changes_started" -eq 1 ] && [ "$success" -eq 0 ]; then
restore_backup ||
log_error "Automatic rollback failed; use the console to recover the network"
fi
exit "$status"
}
main() {
local bootproto
local configured_ip
local -a configured_ips
[ "$(id -u)" -eq 0 ] ||
die "Run this script as root or via sudo"
command -v ip >/dev/null 2>&1 ||
die "The ip command is not available"
command -v ifup >/dev/null 2>&1 ||
die "The ifup command is not available"
command -v ifdown >/dev/null 2>&1 ||
die "The ifdown command is not available"
command -v awk >/dev/null 2>&1 ||
die "The awk command is not available"
command -v sed >/dev/null 2>&1 ||
die "The sed command is not available"
command -v grep >/dev/null 2>&1 ||
die "The grep command is not available"
[ -d "$CONFIG_DIR" ] ||
die "$CONFIG_DIR does not exist"
if systemctl is-active --quiet NetworkManager; then
die "NetworkManager is active. Use the nmcli script instead"
fi
configured_ips=("${NEW_IPS[@]}")
NEW_IPS_RESULT=()
for configured_ip in "${configured_ips[@]}" "$@"; do
case "$configured_ip" in
-h|--help)
usage
exit 0
;;
esac
valid_ipv4 "$configured_ip" ||
die "Invalid IPv4 address: $configured_ip"
add_unique_ip "$configured_ip"
done
[ "${#NEW_IPS_RESULT[@]}" -gt 0 ] ||
die "At least one IPv4 address is required"
detect_interface
CONFIG_FILE="$CONFIG_DIR/ifcfg-$INTERFACE"
[ -f "$CONFIG_FILE" ] ||
die "Configuration file not found: $CONFIG_FILE"
bootproto="$(get_config_value "BOOTPROTO")"
case "$bootproto" in
""|none|static)
;;
*)
die "BOOTPROTO=$bootproto is not supported; static IPv4 is required"
;;
esac
get_current_addresses
get_prefix
log_info "Target interface: $INTERFACE"
log_info "Configuration file: $CONFIG_FILE"
log_info "Current IPv4 addresses: ${OLD_CIDRS[*]:-none}"
log_info "New IPv4 addresses:"
for configured_ip in "${NEW_IPS_RESULT[@]}"; do
log_info " $configured_ip/$PREFIX"
done
log_info "DEVTIMEOUT: 60"
log_warn "ifdown/ifup will interrupt the current SSH session"
create_backup
changes_started=1
update_config
log_info "Reactivating interface..."
ifdown "$INTERFACE" >/dev/null 2>&1 || true
ifup "$INTERFACE" ||
die "Failed to activate interface"
remove_old_runtime_addresses
verify_runtime ||
die "Runtime IPv4 verification failed"
success=1
log_info "IPv4 address update completed successfully"
log_info "Active IPv4 addresses:"
ip -4 -o addr show dev "$INTERFACE" scope global |
awk '{print " " $4}'
log_info "Backup retained at: $BACKUP_FILE"
}
trap cleanup EXIT
main "$@"#!/usr/bin/env bash
set -o pipefail
# IP addresses can be specified here or passed through the command line.
# Example: NEW_IPS=("192.168.1.20" "192.168.1.30")
NEW_IPS=()
selected_uuid=""
default_interface=""
old_address_spec=""
modified=0
success=0
die() {
echo "Error: $*" >&2
exit 1
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
valid_ipv4() {
local ip="$1"
local octet
local -a octets
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] ||
return 1
IFS=. read -r -a octets <<< "$ip"
for octet in "${octets[@]}"; do
(( 10#$octet <= 255 )) || return 1
done
}
add_unique_ip() {
local ip="$1"
local existing_ip
for existing_ip in "${NEW_IPS[@]}"; do
[ "$existing_ip" = "$ip" ] && return 0
done
NEW_IPS+=("$ip")
}
detect_default_interface() {
ip -4 route get 1.1.1.1 2>/dev/null |
awk '{
for (i = 1; i <= NF; i++) {
if ($i == "dev") {
print $(i + 1)
exit
}
}
}'
}
normalize_addresses() {
local addresses="$1"
local address
local -a normalized=()
while IFS= read -r address; do
address="$(trim "$address")"
[ -n "$address" ] || continue
[ "$address" = "--" ] && continue
[[ "$address" == */* ]] || continue
normalized+=("$address")
done < <(printf '%s\n' "$addresses" | tr ',' '\n')
(IFS=,; printf '%s' "${normalized[*]}")
}
rollback() {
[ "$modified" -eq 1 ] || return 0
[ -n "$selected_uuid" ] || return 0
[ -n "$old_address_spec" ] || return 0
echo "Attempting to restore the original IPv4 address configuration..." >&2
nmcli connection modify \
uuid "$selected_uuid" \
ipv4.addresses "$old_address_spec" >/dev/null 2>&1 || true
nmcli connection up \
uuid "$selected_uuid" \
ifname "$default_interface" >/dev/null 2>&1 || true
}
cleanup() {
local status=$?
if [ "$modified" -eq 1 ] && [ "$success" -eq 0 ]; then
rollback
fi
exit "$status"
}
trap cleanup EXIT
if [ "$(id -u)" -ne 0 ]; then
die "Please run this script as root or via sudo"
fi
command -v systemctl >/dev/null 2>&1 ||
die "systemctl not found"
command -v nmcli >/dev/null 2>&1 ||
die "nmcli not found. Please make sure NetworkManager is installed"
command -v ip >/dev/null 2>&1 ||
die "ip command not found"
systemctl enable --now NetworkManager ||
die "Failed to start NetworkManager or enable it at boot"
systemctl is-active --quiet NetworkManager ||
die "NetworkManager is not running"
# Merge, validate, and deduplicate IP addresses specified in the script and on the command line.
configured_ips=("${NEW_IPS[@]}")
NEW_IPS=()
for ip in "${configured_ips[@]}" "$@"; do
valid_ipv4 "$ip" ||
die "Invalid IPv4 address: $ip"
add_unique_ip "$ip"
done
[ "${#NEW_IPS[@]}" -gt 0 ] ||
die "No IPv4 address specified. Please fill in NEW_IPS in the script or pass IP addresses through the command line"
default_interface="$(detect_default_interface)"
[ -n "$default_interface" ] ||
die "Unable to determine the network interface used by the current default IPv4 route"
[ "$default_interface" != "lo" ] ||
die "A loopback interface was detected. No usable default IPv4 route is available"
ip link show dev "$default_interface" >/dev/null 2>&1 ||
die "Default route interface does not exist: $default_interface"
selected_uuid="$(
nmcli -t -f UUID,DEVICE connection show --active 2>/dev/null |
awk -F: -v device="$default_interface" '$2 == device { print $1; exit }'
)"
[ -n "$selected_uuid" ] ||
die "Interface $default_interface is not managed by NetworkManager or has no active connection"
connection_type="$(
nmcli -g connection.type connection show uuid "$selected_uuid" 2>/dev/null
)"
[ "$connection_type" = "802-3-ethernet" ] ||
die "Target connection is not a standard Ethernet connection: $connection_type; bond, bridge, and VLAN connections are not supported by this script"
ipv4_method="$(
nmcli -g ipv4.method connection show uuid "$selected_uuid" 2>/dev/null
)"
[ "$ipv4_method" = "manual" ] ||
die "The IPv4 method of the target connection is not manual: $ipv4_method"
old_addresses="$(
nmcli -g ipv4.addresses connection show uuid "$selected_uuid" 2>/dev/null
)" || die "Unable to read the IPv4 addresses of the target connection"
old_address_spec="$(normalize_addresses "$old_addresses")"
[ -n "$old_address_spec" ] ||
die "The target connection has no usable IPv4 CIDR addresses"
first_prefix=""
while IFS= read -r address; do
address="$(trim "$address")"
[[ "$address" == */* ]] || continue
first_prefix="${address##*/}"
break
done < <(printf '%s\n' "$old_address_spec" | tr ',' '\n')
[[ "$first_prefix" =~ ^[0-9]+$ ]] &&
(( 10#$first_prefix <= 32 )) ||
die "Unable to obtain a valid prefix from the original IPv4 address"
new_addresses=()
for ip in "${NEW_IPS[@]}"; do
new_addresses+=("${ip}/${first_prefix}")
done
address_spec="$(IFS=,; printf '%s' "${new_addresses[*]}")"
echo "Target interface: $default_interface"
echo "Target connection UUID: $selected_uuid"
echo "Original IPv4 addresses: $old_address_spec"
echo "New IPv4 addresses: $address_spec"
echo "Warning: The connection will be reactivated, and the current SSH session may be interrupted."
nmcli connection modify \
uuid "$selected_uuid" \
ipv4.addresses "$address_spec" ||
die "Failed to modify IPv4 addresses"
modified=1
nmcli connection down uuid "$selected_uuid" >/dev/null 2>&1 || true
nmcli connection up \
uuid "$selected_uuid" \
ifname "$default_interface" ||
die "Failed to reactivate the NetworkManager connection"
success=1
echo "IPv4 addresses updated and the connection was successfully reactivated"#!/usr/bin/env bash
set -o pipefail
# IPs defined here are merged with command-line IPs.
# Example: NEW_IPS=("192.169.21.45" "192.169.21.46")
NEW_IPS=()
backup_dir=""
netplan_file=""
cloud_init_marker_created=0
modified=0
success=0
die() {
echo "Error: $*" >&2
exit 1
}
valid_ipv4() {
local candidate="$1"
local octet
local -a octets
[[ "$candidate" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] ||
return 1
IFS=. read -r -a octets <<< "$candidate"
for octet in "${octets[@]}"; do
(( 10#$octet <= 255 )) || return 1
done
}
add_unique_ip() {
local candidate="$1"
local existing
for existing in "${new_ips[@]}"; do
[ "$existing" = "$candidate" ] && return 0
done
new_ips+=("$candidate")
}
detect_default_interface() {
ip -4 route get 1.1.1.1 2>/dev/null |
awk '{
for (i = 1; i <= NF; i++) {
if ($i == "dev") {
print $(i + 1)
exit
}
}
}'
}
rollback() {
[ "$modified" -eq 1 ] || return 0
[ -n "$backup_dir" ] || return 0
[ -d "$backup_dir/netplan" ] || return 0
echo "Restoring previous Netplan configuration..." >&2
set +e
find /etc/netplan -maxdepth 1 -type f \
\( -name '*.yaml' -o -name '*.yml' \) -delete
cp -a "$backup_dir/netplan/." /etc/netplan/
if [ "$cloud_init_marker_created" -eq 1 ]; then
rm -f /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
fi
netplan generate
netplan apply
}
cleanup() {
local status=$?
trap - EXIT
if [ "$modified" -eq 1 ] && [ "$success" -eq 0 ]; then
rollback
fi
exit "$status"
}
trap cleanup EXIT
[ "$(id -u)" -eq 0 ] ||
die "Run this script as root or with sudo"
command -v netplan >/dev/null 2>&1 ||
die "netplan is not installed"
command -v ip >/dev/null 2>&1 ||
die "ip command is not installed"
command -v python3 >/dev/null 2>&1 ||
die "python3 is not installed"
python3 -c 'import yaml' >/dev/null 2>&1 ||
die "python3-yaml is required: sudo apt-get install -y python3-yaml"
[ -d /etc/netplan ] ||
die "/etc/netplan does not exist"
interface="$(detect_default_interface)"
[ -n "$interface" ] ||
die "Cannot detect the default IPv4 route interface"
[ "$interface" != "lo" ] ||
die "The detected interface is loopback"
ip link show dev "$interface" >/dev/null 2>&1 ||
die "Interface does not exist: $interface"
configured_ips=("${NEW_IPS[@]}")
new_ips=()
for candidate in "${configured_ips[@]}" "$@"; do
valid_ipv4 "$candidate" ||
die "Invalid IPv4 address: $candidate"
add_unique_ip "$candidate"
done
[ "${#new_ips[@]}" -gt 0 ] ||
die "Specify at least one IPv4 address"
prefix="$(
ip -4 -o addr show dev "$interface" scope global |
awk 'NR == 1 { split($4, fields, "/"); print fields[2] }'
)"
[[ "$prefix" =~ ^([0-9]|[12][0-9]|3[0-2])$ ]] ||
die "Cannot determine a valid IPv4 prefix from $interface"
address_spec=""
for candidate in "${new_ips[@]}"; do
address_spec+="${candidate}/${prefix},"
done
address_spec="${address_spec%,}"
netplan_file="$(
python3 - /etc/netplan "$interface" <<'PY'
import pathlib
import sys
import yaml
root = pathlib.Path(sys.argv[1])
interface = sys.argv[2]
matches = []
for path in sorted(list(root.glob("*.yaml")) + list(root.glob("*.yml"))):
with path.open(encoding="utf-8") as source:
document = yaml.safe_load(source) or {}
ethernets = document.get("network", {}).get("ethernets", {})
if isinstance(ethernets, dict) and interface in ethernets:
matches.append(str(path))
if len(matches) != 1:
print(
f"Expected exactly one Netplan file defining ethernets.{interface}; found {len(matches)}",
file=sys.stderr,
)
sys.exit(1)
print(matches[0])
PY
)"
backup_dir="$(mktemp -d /root/netplan-backup.XXXXXX)"
chmod 700 "$backup_dir"
cp -a /etc/netplan "$backup_dir/netplan"
if [ "$(basename "$netplan_file")" = "50-cloud-init.yaml" ] &&
[ ! -e /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg ]; then
printf '%s\n' 'network: {config: disabled}' \
> /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
chmod 600 /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
cloud_init_marker_created=1
fi
echo "Interface: $interface"
echo "Netplan source: $netplan_file"
echo "IPv4 addresses: $address_spec"
echo "Backup: $backup_dir"
echo "Applying configuration directly; the current SSH session may disconnect."
python3 - "$netplan_file" "$interface" "$address_spec" <<'PY'
import os
import pathlib
import stat
import sys
import tempfile
import yaml
path = pathlib.Path(sys.argv[1])
interface = sys.argv[2]
addresses = [address for address in sys.argv[3].split(",") if address]
with path.open(encoding="utf-8") as source:
document = yaml.safe_load(source) or {}
profile = document["network"]["ethernets"][interface]
profile["addresses"] = addresses
original_mode = stat.S_IMODE(path.stat().st_mode)
fd, temporary_name = tempfile.mkstemp(
dir=path.parent,
prefix=f".{path.name}.",
suffix=".tmp",
)
with os.fdopen(fd, "w", encoding="utf-8") as target:
yaml.safe_dump(
document,
target,
allow_unicode=True,
default_flow_style=False,
sort_keys=False,
)
os.chmod(temporary_name, original_mode)
os.replace(temporary_name, path)
PY
chmod 600 "$netplan_file"
modified=1
netplan generate ||
die "Generated Netplan configuration is invalid"
netplan apply ||
die "Failed to apply Netplan configuration"
success=1
echo "IPv4 configuration applied successfully."