This guide applies to bare metal servers where IPv4 addresses need to be added, modified, or removed from the operating system. In such cases, you need to log in to the server and run the IP address change script we provide.
Select the appropriate script based on the server's operating system and save the script content to a file, for example:
change-ip.shYou can create and edit the script file using the following command:
vi change-ip.shCopy the corresponding script content into the file and save it. After saving the file, grant the script execute permission:
chmod +x change-ip.shOpen the script file and locate the NEW_IPS configuration item:
NEW_IPS=()Enter the IPv4 addresses you want to configure into NEW_IPS. The IP addresses can be copied directly from the IP Management page using the one-click copy function.
You can also leave NEW_IPS empty and specify all IP addresses through command-line arguments instead. See Section 4 for details.
Separate multiple IP addresses with spaces. For example:
NEW_IPS=("192.168.1.20" "192.168.1.30")After the script is executed, the server will be configured with the following two IPv4 addresses:
192.168.1.20
192.168.1.30
After configuring the target IP addresses, run the script with sudo:
sudo ./change-ip.shThe script will apply the IP address changes according to the configured target IP addresses.
In addition to configuring NEW_IPS in the script, you can also pass IP addresses as command-line arguments when running the script.
For example:
sudo ./change-ip.sh "192.168.1.10" "192.168.1.11"The IP addresses specified through command-line arguments will be merged with and deduplicated against the IP addresses configured in NEW_IPS, and the resulting list will then be applied.
For example, if the script contains:
NEW_IPS=("192.168.1.20" "192.168.1.30")and you run:
sudo ./change-ip.sh "192.168.1.10" "192.168.1.11"the final set of IP addresses processed by the script will be:
192.168.1.10
192.168.1.11
192.168.1.20
192.168.1.30
If the same IP address is specified both in NEW_IPS and as a command-line argument, it will only be retained once.
The complete workflow is as follows:
Select the script for the corresponding operating system
↓
Create the change-ip.sh file
↓
Copy the script content into the file
↓
Grant execute permission
↓
Configure NEW_IPS
↓
Run the script
↓
Complete the IPv4 address changechange-ip.sh file:
#!/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 "$@"change-ip.sh file:
#!/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"change-ip.sh file:
#!/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."