feat(api): support netN add/remove in VM and LXC config updates

- update_vm_config: whitelist netN/scsiN/virtioN/ideN/sataN/ipconfigN by
  regex instead of hardcoded 0-3; accept validated delete='netN' key;
  raise with real Proxmox error text instead of returning False
- update_container_config: refactor to dict signature with whitelist +
  netN + delete (fixes LXC config PUT endpoint that passed the whole
  dict as hostname)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 02:32:21 +05:00
parent 7d52297f76
commit 161182b392

View File

@@ -1290,32 +1290,38 @@ class ProxmoxClient(VmMixin, LxcMixin, ClusterMixin, StorageMixin, NetworkMixin,
if not self.proxmox: if not self.proxmox:
return False return False
try: import re
# Фильтруем разрешенные параметры # Фильтруем разрешенные параметры
allowed_params = { allowed_params = {
'cores', 'sockets', 'memory', 'balloon', 'name', 'description', 'cores', 'sockets', 'memory', 'balloon', 'name', 'description',
'cpu', 'cpulimit', 'cpuunits', 'onboot', 'boot', 'bootdisk', 'cpu', 'cpulimit', 'cpuunits', 'onboot', 'boot', 'bootdisk',
'net0', 'net1', 'net2', 'net3',
'scsi0', 'scsi1', 'scsi2', 'scsi3',
'virtio0', 'virtio1', 'virtio2', 'virtio3',
'ide0', 'ide1', 'ide2', 'ide3',
'sata0', 'sata1', 'sata2', 'sata3',
'agent', 'ostype', 'tablet', 'hotplug', 'agent', 'ostype', 'tablet', 'hotplug',
'ciuser', 'cipassword', 'sshkeys', 'ipconfig0', 'ipconfig1', 'ciuser', 'cipassword', 'sshkeys',
} }
indexed_re = re.compile(r'^(net|scsi|virtio|ide|sata|ipconfig)\d+$')
delete_re = re.compile(r'^net\d+(,net\d+)*$')
filtered_config = {k: v for k, v in config.items() if k in allowed_params} filtered_config = {
k: v for k, v in config.items()
if k in allowed_params or indexed_re.match(k)
}
# Удаление интерфейсов: delete='net1' (или 'net1,net2')
delete_value = config.get('delete')
if isinstance(delete_value, str) and delete_re.match(delete_value):
filtered_config['delete'] = delete_value
if filtered_config: if not filtered_config:
logger.warning(f"Нет разрешенных параметров для обновления VM {vmid}")
return False
try:
self.proxmox.nodes(node).qemu(vmid).config.put(**filtered_config) self.proxmox.nodes(node).qemu(vmid).config.put(**filtered_config)
logger.info(f"Конфигурация VM {vmid} обновлена: {list(filtered_config.keys())}") logger.info(f"Конфигурация VM {vmid} обновлена: {list(filtered_config.keys())}")
return True return True
else:
logger.warning(f"Нет разрешенных параметров для обновления VM {vmid}")
return False
except Exception as e: except Exception as e:
logger.error(f"Ошибка обновления конфигурации VM {vmid}: {e}") logger.error(f"Ошибка обновления конфигурации VM {vmid}: {e}")
return False raise RuntimeError(f"Proxmox: {e}") from e
def get_container_config(self, node: str, vmid: int) -> Optional[Dict]: def get_container_config(self, node: str, vmid: int) -> Optional[Dict]:
""" """
@@ -1338,33 +1344,14 @@ class ProxmoxClient(VmMixin, LxcMixin, ClusterMixin, StorageMixin, NetworkMixin,
logger.error(f"Ошибка получения конфигурации LXC {vmid}: {e}") logger.error(f"Ошибка получения конфигурации LXC {vmid}: {e}")
return None return None
def update_container_config( def update_container_config(self, node: str, vmid: int, config: Dict) -> bool:
self,
node: str,
vmid: int,
hostname: str = None,
memory: int = None,
swap: int = None,
cores: int = None,
cpulimit: float = None,
onboot: bool = None,
description: str = None,
net0: str = None
) -> bool:
""" """
Обновить конфигурацию LXC контейнера Обновить конфигурацию LXC контейнера
Args: Args:
node: Имя ноды node: Имя ноды
vmid: ID контейнера vmid: ID контейнера
hostname: Новое имя хоста config: Словарь с параметрами для обновления
memory: Память в MB
swap: Swap в MB
cores: Количество ядер
cpulimit: Лимит CPU (0.0-128.0)
onboot: Автозапуск
description: Описание
net0: Конфигурация сети
Returns: Returns:
True при успехе True при успехе
@@ -1372,34 +1359,36 @@ class ProxmoxClient(VmMixin, LxcMixin, ClusterMixin, StorageMixin, NetworkMixin,
if not self.proxmox: if not self.proxmox:
return False return False
try: import re
params = {}
if hostname: allowed_params = {
params['hostname'] = hostname 'hostname', 'memory', 'swap', 'cores', 'cpulimit', 'cpuunits',
if memory is not None: 'onboot', 'description', 'nameserver', 'searchdomain', 'tags',
params['memory'] = memory }
if swap is not None: net_re = re.compile(r'^net\d+$')
params['swap'] = swap delete_re = re.compile(r'^net\d+(,net\d+)*$')
if cores is not None:
params['cores'] = cores
if cpulimit is not None:
params['cpulimit'] = cpulimit
if onboot is not None:
params['onboot'] = 1 if onboot else 0
if description is not None:
params['description'] = description
if net0:
params['net0'] = net0
if params: filtered_config = {}
self.proxmox.nodes(node).lxc(vmid).config.put(**params) for k, v in config.items():
logger.info(f"Конфигурация LXC {vmid} обновлена: {list(params.keys())}") if k == 'onboot':
return True filtered_config['onboot'] = 1 if v in (True, 1, '1') else 0
elif k in allowed_params or net_re.match(k):
filtered_config[k] = v
delete_value = config.get('delete')
if isinstance(delete_value, str) and delete_re.match(delete_value):
filtered_config['delete'] = delete_value
if not filtered_config:
logger.warning(f"Нет разрешенных параметров для обновления LXC {vmid}")
return False return False
try:
self.proxmox.nodes(node).lxc(vmid).config.put(**filtered_config)
logger.info(f"Конфигурация LXC {vmid} обновлена: {list(filtered_config.keys())}")
return True
except Exception as e: except Exception as e:
logger.error(f"Ошибка обновления конфигурации LXC {vmid}: {e}") logger.error(f"Ошибка обновления конфигурации LXC {vmid}: {e}")
return False raise RuntimeError(f"Proxmox: {e}") from e
def resize_container_disk(self, node: str, vmid: int, disk: str, size: str) -> bool: def resize_container_disk(self, node: str, vmid: int, disk: str, size: str) -> bool:
""" """