Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d71b98e6a | ||
|
|
0dc6fcb43a | ||
|
|
80e9fc3c81 | ||
|
|
171ab8888f | ||
|
|
c644519af6 | ||
|
|
6c9a6e7017 |
@@ -0,0 +1,48 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: pip
|
||||||
|
|
||||||
|
- name: Install package with dev extras
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
# napalm-device-types lives in git.netork.io/NAPALM, not on PyPI: without this
|
||||||
|
# pip looks there, finds an unrelated 0.1.0 and the job dies before any test.
|
||||||
|
python -m pip install "napalm-device-types @ git+https://git.netork.io/NAPALM/napalm-device-types.git"
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: |
|
||||||
|
python -m pytest -q --tb=short
|
||||||
|
|
||||||
|
- name: Build wheel and sdist
|
||||||
|
run: |
|
||||||
|
python -m pip install build
|
||||||
|
python -m build
|
||||||
|
|
||||||
|
- name: Upload dist artifacts
|
||||||
|
# v4 refuses to run on Gitea ("not currently supported on GHES").
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dist-${{ matrix.python-version }}
|
||||||
|
path: dist/*
|
||||||
@@ -39,6 +39,7 @@ from napalm_device_types import (
|
|||||||
HostStatusMixin,
|
HostStatusMixin,
|
||||||
HypervisorDriver,
|
HypervisorDriver,
|
||||||
KernelFactsMixin,
|
KernelFactsMixin,
|
||||||
|
ListeningSocketsMixin,
|
||||||
PortSpec,
|
PortSpec,
|
||||||
SystemdServicesMixin,
|
SystemdServicesMixin,
|
||||||
)
|
)
|
||||||
@@ -84,6 +85,7 @@ class ProxmoxDriver(
|
|||||||
ProxmoxRoutingMixin,
|
ProxmoxRoutingMixin,
|
||||||
ProxmoxSystemMixin,
|
ProxmoxSystemMixin,
|
||||||
KernelFactsMixin,
|
KernelFactsMixin,
|
||||||
|
ListeningSocketsMixin,
|
||||||
SystemdServicesMixin,
|
SystemdServicesMixin,
|
||||||
HostStatusMixin,
|
HostStatusMixin,
|
||||||
HypervisorDriver,
|
HypervisorDriver,
|
||||||
|
|||||||
@@ -231,6 +231,15 @@ class ProxmoxSystemMixin:
|
|||||||
"""The transport for ``KernelFactsMixin.get_kernel_facts``: the exec path."""
|
"""The transport for ``KernelFactsMixin.get_kernel_facts``: the exec path."""
|
||||||
return self._exec_ssh_command(command)
|
return self._exec_ssh_command(command)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Listening sockets (ListeningSocketsMixin supplies get_listening_sockets)
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def _run_listening_sockets_command(self, command: str, *, privileged: bool) -> str:
|
||||||
|
"""The transport for ``ListeningSocketsMixin.get_listening_sockets``:
|
||||||
|
the exec path, which runs as root either way."""
|
||||||
|
return str(self._exec_ssh_command(command))
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Packages (Debian APT)
|
# Packages (Debian APT)
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ classifiers = [
|
|||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"napalm>=5.0.0",
|
"napalm>=5.0.0",
|
||||||
"napalm_device_types>=2.3.0",
|
"napalm_device_types>=2.4.0",
|
||||||
"paramiko>=5.0.0", # CVE-2026-44405; imported directly for SSH fallback (driver.py)
|
"paramiko>=5.0.0", # CVE-2026-44405; imported directly for SSH fallback (driver.py)
|
||||||
"proxmoxer>=2.0.0",
|
"proxmoxer>=2.0.0",
|
||||||
"netaddr>=0.9.0",
|
"netaddr>=0.9.0",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""`get_listening_sockets`: what listens on the node, and which service it is.
|
||||||
|
|
||||||
|
Whether pveproxy, a Ceph manager or a guest-facing service is reachable from
|
||||||
|
outside the node is decided by the address it listens on. The command and its
|
||||||
|
parse are napalm-device-types'; the driver only carries the command over its
|
||||||
|
exec path, which already runs as root.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from napalm_device_types import ListeningSocketsMixin
|
||||||
|
from napalm_proxmox.driver import ProxmoxDriver
|
||||||
|
|
||||||
|
WIRE = (
|
||||||
|
"SOCK_BEGIN\n[ss]\n"
|
||||||
|
'tcp LISTEN 0 4096 *:8006 *:* users:(("pveproxy worker",pid=2101,fd=6))\n'
|
||||||
|
"__SS_RC=0\n[cgroups]\n"
|
||||||
|
"2101 0::/system.slice/pveproxy.service\n"
|
||||||
|
"SOCK_END\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_driver_declares_the_contract():
|
||||||
|
assert issubclass(ProxmoxDriver, ListeningSocketsMixin)
|
||||||
|
|
||||||
|
|
||||||
|
def test_it_reads_as_root_over_the_exec_path(driver):
|
||||||
|
with patch.object(driver, "_exec_ssh_command", return_value=WIRE) as exec_:
|
||||||
|
reading = driver.get_listening_sockets()
|
||||||
|
|
||||||
|
[command] = [c.args[0] for c in exec_.call_args_list]
|
||||||
|
assert command.startswith("sh -c '")
|
||||||
|
assert reading["attributed"] is True
|
||||||
|
[socket] = reading["sockets"]
|
||||||
|
assert (socket["address"], socket["port"], socket["unit"]) == ("*", 8006, "pveproxy")
|
||||||
@@ -459,7 +459,13 @@ def test_destroy_vm_success():
|
|||||||
|
|
||||||
|
|
||||||
def test_destroy_vm_already_stopped():
|
def test_destroy_vm_already_stopped():
|
||||||
"""destroy_vm succeeds even if VM already stopped."""
|
"""destroy_vm succeeds even if VM already stopped.
|
||||||
|
|
||||||
|
Proxmox refuses to stop a VM that is not running, so the stop call raises.
|
||||||
|
(A bare MagicMock as the stop task never reports "stopped": _wait_for_task
|
||||||
|
then spun for the full timeout with sleep patched out, and every recorded
|
||||||
|
mock call made the test take 60 s and several GB, enough for CI to kill it.)
|
||||||
|
"""
|
||||||
mixin = ProxmoxVMProvisionMixin()
|
mixin = ProxmoxVMProvisionMixin()
|
||||||
|
|
||||||
mock_node = MagicMock()
|
mock_node = MagicMock()
|
||||||
@@ -468,6 +474,7 @@ def test_destroy_vm_already_stopped():
|
|||||||
# Mock VM operations
|
# Mock VM operations
|
||||||
mock_vm = MagicMock()
|
mock_vm = MagicMock()
|
||||||
mock_node.qemu.return_value = mock_vm
|
mock_node.qemu.return_value = mock_vm
|
||||||
|
mock_vm.status.stop.post.side_effect = Exception("VM 101 not running")
|
||||||
mock_vm.config.get.return_value = {}
|
mock_vm.config.get.return_value = {}
|
||||||
mock_vm.delete.return_value = None
|
mock_vm.delete.return_value = None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user