Files
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

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 = ""