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
import re
# Фильтруем разрешенные параметры
allowed_params = {
'cores', 'sockets', 'memory', 'balloon', 'name', 'description',
'cpu', 'cpulimit', 'cpuunits', 'onboot', 'boot', 'bootdisk',
'agent', 'ostype', 'tablet', 'hotplug',
'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 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 not filtered_config:
logger.warning(f"Нет разрешенных параметров для обновления VM {vmid}")
return False
try: try:
# Фильтруем разрешенные параметры self.proxmox.nodes(node).qemu(vmid).config.put(**filtered_config)
allowed_params = { logger.info(f"Конфигурация VM {vmid} обновлена: {list(filtered_config.keys())}")
'cores', 'sockets', 'memory', 'balloon', 'name', 'description', return True
'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',
'ciuser', 'cipassword', 'sshkeys', 'ipconfig0', 'ipconfig1',
}
filtered_config = {k: v for k, v in config.items() if k in allowed_params}
if filtered_config:
self.proxmox.nodes(node).qemu(vmid).config.put(**filtered_config)
logger.info(f"Конфигурация VM {vmid} обновлена: {list(filtered_config.keys())}")
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,68 +1344,51 @@ 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 при успехе
""" """
if not self.proxmox: if not self.proxmox:
return False return False
try: import re
params = {}
allowed_params = {
if hostname: 'hostname', 'memory', 'swap', 'cores', 'cpulimit', 'cpuunits',
params['hostname'] = hostname 'onboot', 'description', 'nameserver', 'searchdomain', 'tags',
if memory is not None: }
params['memory'] = memory net_re = re.compile(r'^net\d+$')
if swap is not None: delete_re = re.compile(r'^net\d+(,net\d+)*$')
params['swap'] = swap
if cores is not None: filtered_config = {}
params['cores'] = cores for k, v in config.items():
if cpulimit is not None: if k == 'onboot':
params['cpulimit'] = cpulimit filtered_config['onboot'] = 1 if v in (True, 1, '1') else 0
if onboot is not None: elif k in allowed_params or net_re.match(k):
params['onboot'] = 1 if onboot else 0 filtered_config[k] = v
if description is not None: delete_value = config.get('delete')
params['description'] = description if isinstance(delete_value, str) and delete_re.match(delete_value):
if net0: filtered_config['delete'] = delete_value
params['net0'] = net0
if not filtered_config:
if params: logger.warning(f"Нет разрешенных параметров для обновления LXC {vmid}")
self.proxmox.nodes(node).lxc(vmid).config.put(**params)
logger.info(f"Конфигурация LXC {vmid} обновлена: {list(params.keys())}")
return True
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:
""" """