Files
napalm-proxmox/napalm_proxmox/lldp_mixin.py
T
Christian ManivongandClaude Sonnet 4.6 b1ba991e6d feat: set_hostname — update /etc/hostname, /etc/hosts, cert, Postfix
Implements set_hostname on ProxmoxSystemMixin:
1. Writes /etc/hostname (short name, base64-safe transfer)
2. Replaces old hostname in /etc/hosts via Python regex + base64
3. Updates /etc/mailname if present
4. Updates Postfix myhostname via postconf -e if installed
5. Applies hostname immediately at runtime via hostname(1)
6. Regenerates Proxmox node TLS certificate via pvecm updatecerts -f
   (falls back to pvenode cert create → pveproxy restart)

Accepts bare hostname or FQDN. A reboot is required for the Proxmox
node name to update in the web UI / cluster — the driver logs this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 16:30:45 +02:00

108 lines
4.2 KiB
Python

# Copyright 2025 The NetOrk Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""LLDP-related NAPALM getters for Proxmox VE nodes."""
from __future__ import annotations
import re
from typing import Any
_JsonDict = dict[str, Any]
class ProxmoxLLDPMixin:
"""Mixin providing LLDP NAPALM methods."""
def get_lldp_neighbors(self) -> dict[str, list[_JsonDict]]:
"""Return LLDP neighbours (requires lldpd on the Proxmox node)."""
result: dict[str, list[_JsonDict]] = {}
raw = self._exec_ssh_command(
"lldpcli show neighbors summary 2>/dev/null || true"
)
current_iface = ""
for line in raw.splitlines():
m_iface = re.match(r"^\s*Interface:\s+(\S+?),?\s", line)
if m_iface:
current_iface = m_iface.group(1)
result.setdefault(current_iface, [])
continue
m_sys = re.match(r"^\s*SysName:\s+(.+)", line)
m_port = re.match(r"^\s*PortID:\s+\S+\s+(.+)", line)
if m_sys and current_iface:
hostname = m_sys.group(1).strip()
if result[current_iface]:
result[current_iface][-1]["hostname"] = hostname
else:
result[current_iface].append({"hostname": hostname, "port": ""})
if m_port and current_iface and result[current_iface]:
result[current_iface][-1]["port"] = m_port.group(1).strip()
return result
def get_lldp_neighbors_detail(self, interface: str = "") -> dict[str, list[_JsonDict]]:
"""Return detailed LLDP neighbour information."""
result: dict[str, list[_JsonDict]] = {}
raw = self._exec_ssh_command(
"lldpcli show neighbors details 2>/dev/null || true"
)
current_iface = ""
current_entry: _JsonDict = {}
def _flush():
if current_iface and current_entry:
result.setdefault(current_iface, []).append(current_entry.copy())
for line in raw.splitlines():
m_iface = re.match(r"^\s*Interface:\s+(\S+?),?\s", line)
if m_iface:
_flush()
current_iface = m_iface.group(1)
if interface and current_iface != interface:
current_iface = ""
current_entry = {
"parent_interface": "",
"remote_chassis_id": "",
"remote_system_name": "",
"remote_port": "",
"remote_port_description": "",
"remote_system_description": "",
"remote_system_capab": [],
"remote_system_enable_capab": [],
}
continue
if not current_iface:
continue
for key, pattern in (
("remote_chassis_id", r"ChassisID:\s+\S+\s+(.+)"),
("remote_system_name", r"SysName:\s+(.+)"),
("remote_port", r"PortID:\s+\S+\s+(.+)"),
("remote_port_description", r"PortDescr:\s+(.+)"),
("remote_system_description", r"SysDescr:\s+(.+)"),
):
m = re.match(rf"^\s*{pattern}", line)
if m:
current_entry[key] = m.group(1).strip()
m_cap = re.match(r"^\s*Capability:\s+(\S+),\s+(\w+)", line)
if m_cap:
cap = m_cap.group(1).lower()
enabled = m_cap.group(2).lower() == "on"
current_entry["remote_system_capab"].append(cap)
if enabled:
current_entry["remote_system_enable_capab"].append(cap)
_flush()
return result