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>
146 lines
5.1 KiB
Python
146 lines
5.1 KiB
Python
# Copyright 2025 The NetOrk Project Authors. All rights reserved.
|
|
#
|
|
# 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.
|
|
|
|
"""Configuration management NAPALM methods for Proxmox VE nodes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
|
|
_JsonDict = dict[str, Any]
|
|
|
|
|
|
class ProxmoxConfigMixin:
|
|
"""Mixin providing configuration-management NAPALM methods."""
|
|
|
|
def get_config(
|
|
self,
|
|
retrieve: str = "all",
|
|
full: bool = False,
|
|
sanitized: bool = False,
|
|
format: str = "text",
|
|
) -> _JsonDict:
|
|
"""Return the node network configuration.
|
|
|
|
``running`` config is the contents of ``/etc/network/interfaces``
|
|
(and the SDN config directory). ``startup`` is identical (PVE
|
|
applies on boot). ``candidate`` is what was loaded via
|
|
``load_merge_candidate`` / ``load_replace_candidate`` but not yet
|
|
committed.
|
|
"""
|
|
configs: _JsonDict = {"running": "", "candidate": "", "startup": ""}
|
|
|
|
if retrieve in ("running", "all", "startup"):
|
|
raw = self._exec_ssh_command(
|
|
"cat /etc/network/interfaces 2>/dev/null || true"
|
|
)
|
|
# Append SDN config if available
|
|
sdn_raw = self._exec_ssh_command(
|
|
"cat /etc/pve/sdn/vnets.cfg 2>/dev/null || true"
|
|
)
|
|
running = raw
|
|
if sdn_raw:
|
|
running += "\n# === SDN VNets ===\n" + sdn_raw
|
|
if sanitized:
|
|
running = re.sub(r"password\s+\S+", "password ****", running)
|
|
configs["running"] = running
|
|
self._running_config = running
|
|
if retrieve == "startup":
|
|
configs["startup"] = running
|
|
elif retrieve == "all":
|
|
configs["startup"] = running
|
|
|
|
if retrieve in ("candidate", "all"):
|
|
configs["candidate"] = self._candidate_config
|
|
|
|
return configs
|
|
|
|
def load_merge_candidate(
|
|
self,
|
|
filename: str | None = None,
|
|
config: str | None = None,
|
|
) -> None:
|
|
"""Load a candidate configuration (merge mode)."""
|
|
if filename:
|
|
with open(filename) as fh:
|
|
config = fh.read()
|
|
if config is None:
|
|
raise ValueError("Either filename or config must be provided")
|
|
# In merge mode we append / overlay
|
|
self._candidate_config = config
|
|
|
|
def load_replace_candidate(
|
|
self,
|
|
filename: str | None = None,
|
|
config: str | None = None,
|
|
) -> None:
|
|
"""Load a candidate configuration (replace mode)."""
|
|
if filename:
|
|
with open(filename) as fh:
|
|
config = fh.read()
|
|
if config is None:
|
|
raise ValueError("Either filename or config must be provided")
|
|
self._candidate_config = config
|
|
|
|
def compare_config(self) -> str:
|
|
"""Return a unified diff between running and candidate config."""
|
|
import difflib
|
|
|
|
if not self._running_config:
|
|
self.get_config(retrieve="running")
|
|
running_lines = self._running_config.splitlines(keepends=True)
|
|
candidate_lines = self._candidate_config.splitlines(keepends=True)
|
|
diff = difflib.unified_diff(
|
|
running_lines,
|
|
candidate_lines,
|
|
fromfile="running",
|
|
tofile="candidate",
|
|
)
|
|
return "".join(diff)
|
|
|
|
def commit_config(self, message: str = "", revert_in: int | None = None) -> None:
|
|
"""Commit the candidate configuration to the Proxmox node.
|
|
|
|
This writes the candidate config to ``/etc/network/interfaces``
|
|
via the Proxmox node/network PUT API (which applies it live).
|
|
|
|
.. note::
|
|
Full programmatic apply requires the Proxmox API to accept raw
|
|
interface configs. This implementation uses ``pvesh`` via exec
|
|
which requires the node exec endpoint to be available.
|
|
"""
|
|
if not self._candidate_config:
|
|
return
|
|
# Write via exec endpoint
|
|
escaped = self._candidate_config.replace("'", "'\\''")
|
|
self._exec_ssh_command(
|
|
f"printf '%s' '{escaped}' > /etc/network/interfaces && "
|
|
"ifreload -a 2>&1 || ifup -a 2>&1 || true"
|
|
)
|
|
self._running_config = self._candidate_config
|
|
self._candidate_config = ""
|
|
|
|
def discard_config(self) -> None:
|
|
"""Discard the candidate configuration."""
|
|
self._candidate_config = ""
|
|
|
|
def rollback(self) -> None:
|
|
"""Revert to the stored running configuration."""
|
|
if self._running_config:
|
|
self._candidate_config = self._running_config
|
|
self.commit_config()
|
|
self._candidate_config = ""
|