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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
d1e6931ec5
commit
b1ba991e6d
@@ -0,0 +1,145 @@
|
||||
# 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 = ""
|
||||
Reference in New Issue
Block a user