Skip to content

Commit 6650e0b

Browse files
authored
PVE-Privilege-Converter (#4906)
1 parent f2bf6c9 commit 6650e0b

File tree

2 files changed

+244
-0
lines changed

2 files changed

+244
-0
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "PVE Privilege Converter",
3+
"slug": "pve-privilege-converter",
4+
"categories": [
5+
1
6+
],
7+
"date_created": "2025-05-22",
8+
"type": "pve",
9+
"updateable": false,
10+
"privileged": false,
11+
"interface_port": null,
12+
"documentation": "https://github.com/onethree7/proxmox-lxc-privilege-converter",
13+
"website": null,
14+
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/proxmox.webp",
15+
"config_path": "",
16+
"description": "This script allows converting Proxmox LXC containers between privileged and unprivileged modes using vzdump backup and restore. It guides you through container selection, backup storage, ID assignment, and privilege flipping via automated restore. Useful for applying changes that require different LXC modes.",
17+
"install_methods": [
18+
{
19+
"type": "default",
20+
"script": "tools/pve/pve-privilege-converter.sh",
21+
"resources": {
22+
"cpu": null,
23+
"ram": null,
24+
"hdd": null,
25+
"os": null,
26+
"version": null
27+
}
28+
}
29+
],
30+
"default_credentials": {
31+
"username": null,
32+
"password": null
33+
},
34+
"notes": [
35+
{
36+
"text": "Execute this script inside the Proxmox shell as root.",
37+
"type": "info"
38+
},
39+
{
40+
"text": "Ensure that the backup and target storage have enough space.",
41+
"type": "warning"
42+
},
43+
{
44+
"text": "The container will be recreated with a new ID and desired privilege setting.",
45+
"type": "info"
46+
}
47+
]
48+
}

tools/pve/pve-privilege-converter.sh

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/env bash
2+
3+
# Copyright (c) 2021-2025 community-scripts ORG
4+
# Author: MickLesk
5+
# Adapted from onethree7 (https://github.com/onethree7/proxmox-lxc-privilege-converter)
6+
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
7+
8+
if ! command -v curl >/dev/null 2>&1; then
9+
printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2
10+
apt-get update >/dev/null 2>&1
11+
apt-get install -y curl >/dev/null 2>&1
12+
fi
13+
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
14+
load_functions
15+
16+
set -euo pipefail
17+
shopt -s inherit_errexit nullglob
18+
19+
APP="PVE-Privilege-Converter"
20+
APP_TYPE="tools"
21+
header_info "$APP"
22+
23+
check_root() {
24+
if [[ $EUID -ne 0 ]]; then
25+
msg_error "Script must be run as root"
26+
exit 1
27+
fi
28+
}
29+
30+
select_target_storage_and_container_id() {
31+
echo -e "\nSelect target storage for restored container:\n"
32+
mapfile -t target_storages < <(pvesm status --content images | awk 'NR > 1 {print $1}')
33+
for i in "${!target_storages[@]}"; do
34+
printf "%s) %s\n" "$((i + 1))" "${target_storages[$i]}"
35+
done
36+
37+
while true; do
38+
read -rp "Enter number of target storage: " choice
39+
if [[ "$choice" =~ ^[0-9]+$ ]] && ((choice >= 1 && choice <= ${#target_storages[@]})); then
40+
TARGET_STORAGE="${target_storages[$((choice - 1))]}"
41+
break
42+
else
43+
echo "Invalid selection. Try again."
44+
fi
45+
done
46+
47+
next_free_id=$(pvesh get /cluster/nextid 2>/dev/null || echo 999)
48+
[[ "$next_free_id" =~ ^[0-9]+$ ]] || next_free_id=999
49+
50+
echo ""
51+
read -rp "Suggested next free container ID: $next_free_id. Enter new container ID [default: $next_free_id]: " NEW_CONTAINER_ID
52+
NEW_CONTAINER_ID="${NEW_CONTAINER_ID:-$next_free_id}"
53+
}
54+
55+
select_container() {
56+
mapfile -t lxc_list_raw < <(pct list | awk 'NR > 1 {print $1, $3}')
57+
lxc_list=()
58+
for entry in "${lxc_list_raw[@]}"; do
59+
[[ -n "$entry" ]] && lxc_list+=("$entry")
60+
done
61+
62+
if [[ ${#lxc_list[@]} -eq 0 ]]; then
63+
msg_error "No containers found"
64+
exit 1
65+
fi
66+
67+
PS3="Enter number of container to convert: "
68+
select opt in "${lxc_list[@]}"; do
69+
if [[ -n "$opt" ]]; then
70+
read -r CONTAINER_ID CONTAINER_NAME <<<"$opt"
71+
CONTAINER_NAME="${CONTAINER_NAME:-}"
72+
break
73+
else
74+
echo "Invalid selection. Try again."
75+
fi
76+
done
77+
}
78+
79+
select_backup_storage() {
80+
echo -e "Select backup storage (temporary vzdump location):"
81+
mapfile -t backup_storages < <(pvesm status --content backup | awk 'NR > 1 {print $1}')
82+
local PS3="Enter number of backup storage: "
83+
84+
select opt in "${backup_storages[@]}"; do
85+
if [[ -n "$opt" ]]; then
86+
BACKUP_STORAGE="$opt"
87+
break
88+
else
89+
echo "Invalid selection. Try again."
90+
fi
91+
done
92+
}
93+
94+
backup_container() {
95+
msg_custom "📦" "\e[36m" "Backing up container $CONTAINER_ID"
96+
vzdump_output=$(mktemp)
97+
vzdump "$CONTAINER_ID" --compress zstd --storage "$BACKUP_STORAGE" --mode snapshot | tee "$vzdump_output"
98+
BACKUP_PATH=$(awk '/tar.zst/ {print $NF}' "$vzdump_output" | tr -d "'")
99+
if [ -z "$BACKUP_PATH" ] || ! grep -q "Backup job finished successfully" "$vzdump_output"; then
100+
rm "$vzdump_output"
101+
msg_error "Backup failed"
102+
exit 1
103+
fi
104+
rm "$vzdump_output"
105+
msg_ok "Backup complete: $BACKUP_PATH"
106+
}
107+
108+
perform_conversion() {
109+
if pct config "$CONTAINER_ID" | grep -q 'unprivileged: 1'; then
110+
UNPRIVILEGED=true
111+
else
112+
UNPRIVILEGED=false
113+
fi
114+
115+
msg_custom "🛠️" "\e[36m" "Restoring as $(if $UNPRIVILEGED; then echo privileged; else echo unprivileged; fi) container"
116+
restore_opts=("$NEW_CONTAINER_ID" "$BACKUP_PATH" --storage "$TARGET_STORAGE")
117+
if $UNPRIVILEGED; then
118+
restore_opts+=(--unprivileged false)
119+
else
120+
restore_opts+=(--unprivileged)
121+
fi
122+
123+
if pct restore "${restore_opts[@]}" -ignore-unpack-errors 1; then
124+
msg_ok "Conversion successful"
125+
else
126+
msg_error "Conversion failed"
127+
exit 1
128+
fi
129+
}
130+
131+
manage_states() {
132+
read -rp "Shutdown source and start new container? [Y/n]: " answer
133+
answer=${answer:-Y}
134+
if [[ $answer =~ ^[Yy] ]]; then
135+
pct shutdown "$CONTAINER_ID"
136+
for i in {1..36}; do
137+
sleep 5
138+
! pct status "$CONTAINER_ID" | grep -q running && break
139+
done
140+
if pct status "$CONTAINER_ID" | grep -q running; then
141+
read -rp "Timeout reached. Force shutdown? [Y/n]: " force
142+
if [[ ${force:-Y} =~ ^[Yy] ]]; then
143+
pkill -9 -f "lxc-start -F -n $CONTAINER_ID"
144+
fi
145+
fi
146+
pct start "$NEW_CONTAINER_ID"
147+
msg_ok "New container started"
148+
else
149+
msg_custom "ℹ️" "\e[36m" "Skipped container state change"
150+
fi
151+
}
152+
153+
cleanup_files() {
154+
read -rp "Delete backup archive? [$BACKUP_PATH] [Y/n]: " cleanup
155+
if [[ ${cleanup:-Y} =~ ^[Yy] ]]; then
156+
rm -f "$BACKUP_PATH" && msg_ok "Removed backup archive"
157+
else
158+
msg_custom "💾" "\e[36m" "Retained backup archive"
159+
fi
160+
}
161+
162+
summary() {
163+
local conversion="Unknown"
164+
if [[ -n "${UNPRIVILEGED:-}" ]]; then
165+
if $UNPRIVILEGED; then
166+
conversion="Unprivileged → Privileged"
167+
else
168+
conversion="Privileged → Unprivileged"
169+
fi
170+
fi
171+
172+
echo
173+
msg_custom "📄" "\e[36m" "Summary:"
174+
msg_custom " " "\e[36m" "$(printf "%-22s %s" "Original Container:" "$CONTAINER_ID ($CONTAINER_NAME)")"
175+
msg_custom " " "\e[36m" "$(printf "%-22s %s" "Backup Storage:" "$BACKUP_STORAGE")"
176+
msg_custom " " "\e[36m" "$(printf "%-22s %s" "Target Storage:" "$TARGET_STORAGE")"
177+
msg_custom " " "\e[36m" "$(printf "%-22s %s" "Backup Path:" "$BACKUP_PATH")"
178+
msg_custom " " "\e[36m" "$(printf "%-22s %s" "New Container ID:" "$NEW_CONTAINER_ID")"
179+
msg_custom " " "\e[36m" "$(printf "%-22s %s" "Privilege Conversion:" "$conversion")"
180+
echo
181+
}
182+
183+
main() {
184+
header_info
185+
check_root
186+
select_container
187+
select_backup_storage
188+
backup_container
189+
select_target_storage_and_container_id
190+
perform_conversion
191+
manage_states
192+
cleanup_files
193+
summary
194+
}
195+
196+
main

0 commit comments

Comments
 (0)