--- name: manage-proxmox-ct version: 1.0.0 description: Full LXC container lifecycle — create, list, start, stop, shutdown, destroy, update config. Based on therootcompany/proxmox-sh. tags: [proxmox, lxc, containers, api, shell] --- # Manage Proxmox CT Assumes `my_auth`, `my_base_url`, `my_curl` are set (see `auth-proxmox-api` skill). Endpoints: see `docs/api-endpoints.md`. OS templates: see `docs/ct-os-templates.md`. ## VMID Scheme VMIDs are `{PREFIX}{3-digit-index}`. PREFIX is a 4-digit VLAN/network ID. ``` PROXMOX_ID_PREFIX=1100, index=51 => vmid=1100051 ``` Find the next available index within a prefix: ```sh fn_resources_next_index() { ( my_prefix="${1}" my_resource_ids="$( ${my_curl} -H "${my_auth}" "${my_base_url}/cluster/resources" | jq -r '.data[].vmid' | grep "^${my_prefix}" || true )" if test -z "${my_resource_ids}"; then echo "${PROXMOX_ID_START:-51}" return fi my_last_id="$(printf '%s' "${my_resource_ids}" | sort -u | tail -n 1)" echo "$((my_last_id % 1000 + 1))" ); } ``` ## Deterministic MAC + IP from Prefix + Index ```sh # prefix=1100, unit=51 => MAC 00:52:11:00:00:51, IP 10.11.0.51/24, GW 10.11.0.1 fn_id_to_net() { ( my_id_prefix="${1}" my_next_unit="${2}" my_p12="$(echo "${my_id_prefix}" | cut -c1-2)" my_p34="$(echo "${my_id_prefix}" | cut -c3-4)" my_p12_int=$((my_p12 + 0)) my_p34_int=$((my_p34 + 0)) my_3digit="$((my_next_unit + 1000))" my_3digit="$(echo "${my_3digit}" | cut -c2-4)" my_4digit="0${my_3digit}" my_u12="$(echo "${my_4digit}" | cut -c1-2)" my_u34="$(echo "${my_4digit}" | cut -c3-4)" printf "hwaddr=00:52:%s:%s:%s:%s," "${my_p12}" "${my_p34}" "${my_u12}" "${my_u34}" printf "ip=10.%s.%s.%s/24," "${my_p12_int}" "${my_p34_int}" "${my_next_unit}" printf "gw=10.%s.%s.1" "${my_p12_int}" "${my_p34_int}" ); } ``` ## Create ```sh fn_create_lxc() { ( my_target_node="${1}" my_vlan="${2:-}" # optional VLAN tag my_next_unit="${3}" # index within prefix my_hostname="${4}" my_pubkeys="${5}" my_os_tmpl="${6}" # vztmpl/... path my_memory="${7}" # MB my_mp0_size="${8}" # GB for /mnt/storage my_cores="${9}" my_cpulimit="${10}" # float <= cores my_vmid="${11}" my_vlan_param="" if test -n "${my_vlan}"; then my_vlan_param=",tag=${my_vlan}" fi my_password="$(xxd -l12 -ps /dev/urandom | xxd -r -ps | base64 | tr -d = | tr + - | tr / _)" my_bridge="${PROXMOX_VNET:-$PROXMOX_BRIDGE}" my_net="$(fn_id_to_net "${PROXMOX_ID_PREFIX}" "${my_next_unit}")" my_task_result="$( ${my_curl} -H "${my_auth}" \ -X POST "${my_base_url}/nodes/${my_target_node}/lxc" \ --data-urlencode "vmid=${my_vmid}" \ --data-urlencode "hostname=${my_hostname}" \ --data-urlencode "unprivileged=1" \ --data-urlencode "start=1" \ --data-urlencode "onboot=1" \ --data-urlencode "pool=${PROXMOX_RESOURCE_POOL}" \ --data-urlencode "password=${my_password}" \ --data-urlencode "ssh-public-keys=${my_pubkeys}" \ --data-urlencode "ostemplate=${PROXMOX_TEMPLATE_STORAGE}:${my_os_tmpl}" \ --data-urlencode "rootfs=${PROXMOX_FS_POOL}:8,mountoptions=discard;lazytime;noatime" \ --data-urlencode "mp0=${PROXMOX_DATA_POOL}:${my_mp0_size},mp=/mnt/storage,mountoptions=discard;lazytime;noatime;nodev;nosuid,backup=1" \ --data-urlencode "cores=${my_cores}" \ --data-urlencode "cpulimit=${my_cpulimit}" \ --data-urlencode "memory=${my_memory}" \ --data-urlencode "swap=0" \ --data-urlencode "net0=name=eth0,bridge=${my_bridge}${my_vlan_param},${my_net},rate=91" \ --data-urlencode "searchdomain=${PROXMOX_ID_PREFIX}.${PROXMOX_SEARCH_DOMAIN}" \ --data-urlencode "nameserver=${PROXMOX_NAMESERVER}" )" my_task_id="$(printf '%s' "${my_task_result}" | jq -r '.data' | grep '^UPID:' || true)" if test -z "${my_task_id}"; then printf '%s' "${my_task_result}" | jq '.' >&2 echo "Error: CT create failed" >&2 return 1 fi echo "${my_task_id}" ); } ``` Full create flow: ```sh main() { ( my_hostname="${1}" my_pubkeys="${2:-${PROXMOX_AUTHORIZED_KEYS:-}}" my_os_tmpl="${PROXMOX_TEMPLATE_DEFAULT}" my_cores="${3:-2}" my_cpulimit="${4:-${my_cores}}" my_ram="${5:-512}" my_storage="${6:-1}" my_next_unit="$(fn_resources_next_index "${PROXMOX_ID_PREFIX}")" my_3digit="$((my_next_unit + 1000))" my_3digit="$(echo "${my_3digit}" | cut -c2-4)" my_vmid="${PROXMOX_ID_PREFIX}${my_3digit}" my_task_id="$( fn_create_lxc "${PROXMOX_TARGET_NODE}" "${PROXMOX_VLAN:-}" \ "${my_next_unit}" "${my_hostname}" "${my_pubkeys}" \ "${my_os_tmpl}" "${my_ram}" "${my_storage}" \ "${my_cores}" "${my_cpulimit}" "${my_vmid}" )" fn_wait_status "${PROXMOX_TARGET_NODE}" "${my_task_id}" ); } ``` ## List (TSV) ```sh fn_lxc_list() { ( my_node="${1:-${PROXMOX_TARGET_NODE}}" ${my_curl} -H "${my_auth}" "${my_base_url}/nodes/${my_node}/lxc" | jq -r '.data[] | [.vmid, .name, .status, .maxcpu, .maxmem, .maxdisk] | @tsv' ); } ``` ## Status ```sh fn_lxc_status() { ( my_node="${1}"; my_vmid="${2}" ${my_curl} -H "${my_auth}" \ "${my_base_url}/nodes/${my_node}/lxc/${my_vmid}/status/current" | jq -r '.data | [.vmid, .name, .status, .uptime, .cpu, .mem] | @tsv' ); } ``` ## Start / Shutdown / Stop ```sh fn_lxc_start() { ( my_node="${1}"; my_vmid="${2}" ${my_curl} -H "${my_auth}" -X POST \ "${my_base_url}/nodes/${my_node}/lxc/${my_vmid}/status/start" | jq -r '.data' ); } fn_lxc_shutdown() { ( my_node="${1}"; my_vmid="${2}" ${my_curl} -H "${my_auth}" -X POST \ "${my_base_url}/nodes/${my_node}/lxc/${my_vmid}/status/shutdown" | jq -r '.data' ); } fn_lxc_stop() { ( my_node="${1}"; my_vmid="${2}" ${my_curl} -H "${my_auth}" -X POST \ "${my_base_url}/nodes/${my_node}/lxc/${my_vmid}/status/stop" | jq -r '.data' ); } ``` ## Destroy ```sh fn_lxc_destroy() { ( my_node="${1}"; my_vmid="${2}" ${my_curl} -H "${my_auth}" \ -X DELETE "${my_base_url}/nodes/${my_node}/lxc/${my_vmid}?purge=1&destroy-unreferenced-disks=1" | jq -r '.data' ); } ``` ## Update Config ```sh fn_lxc_config_set() { ( my_node="${1}"; my_vmid="${2}" shift 2 ${my_curl} -H "${my_auth}" \ -X PUT "${my_base_url}/nodes/${my_node}/lxc/${my_vmid}/config" \ "$@" ); } ``` ## Required ENV Vars ``` PROXMOX_TARGET_NODE PROXMOX_ID_PREFIX # 4-digit e.g. 1100 PROXMOX_ID_START # first index e.g. 51 PROXMOX_RESOURCE_POOL PROXMOX_TEMPLATE_STORAGE PROXMOX_TEMPLATE_DEFAULT PROXMOX_FS_POOL PROXMOX_DATA_POOL PROXMOX_VNET # or PROXMOX_BRIDGE + PROXMOX_VLAN (mutually exclusive) PROXMOX_NAMESERVER PROXMOX_SEARCH_DOMAIN PROXMOX_AUTHORIZED_KEYS ``` ## Systemd Containers (nesting=1 required) Any distro using systemd (Ubuntu, Debian, Fedora, etc.) requires `nesting=1` in the `features` param since approximately 2023. Systemd's cgroup v2 management needs Linux namespace support that is off by default in unprivileged containers. Add to `fn_create_lxc` POST call: ```sh --data-urlencode "features=nesting=1" ``` Distros using OpenRC (Alpine) do **not** need this. When in doubt, set it — it has no meaningful overhead. ## Notes - `unprivileged=1` is the default; use privileged only when explicitly needed. - `cpulimit` is a float: `1.5` = up to 1.5 physical cores of CPU time. - `swap=0` by convention — CTs manage their own memory. - Run quota pre-flight before creating (see `manage-proxmox-pool` skill). - Poll UPIDs with `fn_wait_status` (see `auth-proxmox-api` skill).