8 Commits
Author SHA1 Message Date
christianmanivong 39532471b7 Merge pull request 'feat: hand the container engine connection Container Station's docker' (#4) from feat/container-engine-binary into main
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 26s
CI / test (3.12) (push) Successful in 29s
2026-10-07 16:10:44 +00:00
Christian Manivong 5838685690 feat: hand the container engine connection Container Station's docker
CI / test (3.11) (pull_request) Successful in 27s
CI / test (3.12) (pull_request) Successful in 30s
CI / test (3.10) (push) Successful in 29s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 28s
CI / test (3.10) (pull_request) Successful in 29s
napalm-device-types 2.6.0 opens a host's container engine through
`open_container_engine()`, and asks the driver for the binary through
`_container_engine_binary()`. Under QTS docker is not on PATH; the hook
now returns the path `open()` already discovers, so the Engine API stream
(`<path> system dial-stdio`) and the CLI reach Container Station while
callers only pass arguments (NetOrk/netork#765).

Version 0.2.0, requires napalm-device-types >= 2.6.0 and
napalm-linux >= 0.2.0.

Refs NAPALM/napalm-device-types#17
2026-10-07 17:59:55 +02:00
christianmanivong b868f53990 Merge pull request 'ci: run the tests and build the package on every push and pull request' (#3) from ci/workflow into main
CI / test (3.10) (push) Successful in 29s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 29s
2026-10-07 06:37:00 +00:00
Christian Manivong 8ad1c66ace ci: run the tests and build the package on every push and pull request
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 26s
CI / test (3.12) (push) Successful in 28s
CI / test (3.10) (pull_request) Successful in 29s
CI / test (3.11) (pull_request) Successful in 27s
CI / test (3.12) (pull_request) Successful in 30s
The same job as napalm-fritzbox and napalm-opnsense: Python 3.10, 3.11 and
3.12, pytest, then wheel and sdist. napalm-device-types comes from
git.netork.io first, because PyPI has an unrelated package of that name.

napalm-linux, which this driver builds on, is installed from git as well:
it is not on PyPI either.
2026-10-07 07:52:44 +02:00
christianmanivong 0f4b4f56c4 Merge pull request 'fix: do not claim update reading or applying QTS cannot do' (#2) from fix/no-update-reader into main 2026-10-05 22:20:13 +00:00
Christian Manivong 5e371db3af fix: do not claim update reading or applying QTS cannot do
QTS has no apt, dnf or opkg, so the update readers inherited from
LinuxDriver find no package manager, and QPKG updates are not implemented.
get_available_updates, refresh_available_updates and apply_updates are None,
the same opt-out as manage_service: netOrk's patch groups and compliance
report include a driver only when these are callable.
2026-10-06 00:20:12 +02:00
christianmanivong 872e5718a3 Merge pull request 'fix: do not claim service control QTS cannot do' (#1) from fix/no-service-control into main 2026-10-05 11:12:16 +00:00
Christian Manivong 77313ca436 fix: do not claim service control QTS cannot do
QnapQtsDriver inherits LinuxDriver, which now has manage_service() through
napalm-device-types' SystemdServicesMixin. QTS has no systemd, so every
action would fail. manage_service = None makes every capability check --
is manage_service callable -- answer no, so netOrk offers no controls that
cannot work.
2026-10-05 13:12:16 +02:00
5 changed files with 124 additions and 3 deletions
+50
View File
@@ -0,0 +1,50 @@
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"
# napalm-linux, which this driver builds on, is not on PyPI either.
python -m pip install "napalm-linux @ git+https://git.netork.io/NAPALM/napalm-linux.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/*
+13
View File
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.2.0] – 2026-10-07
### Added
- `_container_engine_binary` returns Container Station's docker path, so the
container engine connection from napalm-device-types 2.6.0
(`open_container_engine("docker")`) reaches it although it is not on PATH.
Callers pass arguments only and never see the path (NetOrk/netork#765).
### Changed
- Requires napalm-device-types >= 2.6.0 and napalm-linux >= 0.2.0.
## [0.1.0]
### Added ### Added
- Initial driver scaffold: `QnapQtsDriver(StorageDriver, LinuxDriver)` with - Initial driver scaffold: `QnapQtsDriver(StorageDriver, LinuxDriver)` with
discovery fingerprints (SNMP enterprise OID 1.3.6.1.4.1.24681, HTTP, port discovery fingerprints (SNMP enterprise OID 1.3.6.1.4.1.24681, HTTP, port
+17
View File
@@ -68,6 +68,17 @@ class QnapQtsDriver(StorageDriver, HypervisorDriver, LinuxDriver):
driver_name = "qnap_qts" driver_name = "qnap_qts"
NETMIKO_DEVICE_TYPE = "linux" NETMIKO_DEVICE_TYPE = "linux"
#: QTS has no systemd, so the systemctl actions LinuxDriver inherits from
#: napalm-device-types' SystemdServicesMixin would fail on every unit. None
#: makes every "can it control services?" check -- is ``manage_service``
#: callable -- answer no, rather than offering buttons that cannot work.
manage_service = None # type: ignore[assignment]
#: The same for updates: QTS has no apt, dnf or opkg, and QPKG updates are
#: not implemented. netOrk leaves a driver without them out of patching.
get_available_updates = None # type: ignore[assignment]
refresh_available_updates = None # type: ignore[assignment]
apply_updates = None # type: ignore[assignment]
SNMP_OBJECT_ID_PREFIX = QNAP_ENTERPRISE_OID SNMP_OBJECT_ID_PREFIX = QNAP_ENTERPRISE_OID
SNMP_FINGERPRINT = [ SNMP_FINGERPRINT = [
FingerprintRule("qnap", weight=9.0), FingerprintRule("qnap", weight=9.0),
@@ -143,6 +154,12 @@ class QnapQtsDriver(StorageDriver, HypervisorDriver, LinuxDriver):
"""Override LinuxDriver's hook: docker is not on PATH under QTS.""" """Override LinuxDriver's hook: docker is not on PATH under QTS."""
return getattr(self, "_docker_path", None) or "docker" return getattr(self, "_docker_path", None) or "docker"
def _container_engine_binary(self, engine: str) -> str:
"""Container Station keeps docker off PATH; callers never see the path."""
if engine == "docker":
return self._docker_bin()
return super()._container_engine_binary(engine)
# ── QPKG, not apt ───────────────────────────────────────────────────────── # ── QPKG, not apt ─────────────────────────────────────────────────────────
# #
# QTS is Linux, so LinuxDriver's package methods are in the MRO and would # QTS is Linux, so LinuxDriver's package methods are in the MRO and would
+3 -3
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "napalm-qnap-qts" name = "napalm-qnap-qts"
version = "0.1.0" version = "0.2.0"
description = "NAPALM driver for QNAP NAS systems running QTS via SSH" description = "NAPALM driver for QNAP NAS systems running QTS via SSH"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
@@ -43,8 +43,8 @@ classifiers = [
# processes, Docker) from LinuxDriver rather than reimplementing it. # processes, Docker) from LinuxDriver rather than reimplementing it.
dependencies = [ dependencies = [
"napalm>=4.0", "napalm>=4.0",
"napalm-device-types>=0.5.0", "napalm-device-types>=2.6.0",
"napalm-linux>=0.1.0", "napalm-linux>=0.2.0",
"netmiko>=4.0.0", "netmiko>=4.0.0",
"paramiko>=5.0.0", # CVE-2026-44405 "paramiko>=5.0.0", # CVE-2026-44405
] ]
+41
View File
@@ -182,3 +182,44 @@ class TestDockerBinDiscovery:
def test_docker_bin_hook_returns_the_discovered_path(self, driver): def test_docker_bin_hook_returns_the_discovered_path(self, driver):
driver._docker_path = "/opt/docker" driver._docker_path = "/opt/docker"
assert driver._docker_bin() == "/opt/docker" assert driver._docker_bin() == "/opt/docker"
class TestContainerEngineBinary:
"""The container engine connection (napalm-device-types 2.6.0) reaches
Container Station's docker, not one on PATH: the driver decides where the
binary lives, and netOrk never sees it (NetOrk/netork#765)."""
def test_the_engine_binary_is_the_discovered_path(self, driver):
driver._docker_path = "/share/CACHEDEV1_DATA/.qpkg/container-station/bin/docker"
assert driver._container_engine_binary("docker") == driver._docker_path
def test_the_api_stream_runs_dial_stdio_from_that_path(self, driver):
driver._docker_path = "/share/CACHEDEV1_DATA/.qpkg/container-station/bin/docker"
opened = []
with patch.object(driver, "open_stream", side_effect=lambda cmd, **kw: opened.append(cmd)):
driver.open_container_engine("docker").open_api()
assert opened == [f"{driver._docker_path} system dial-stdio"]
def test_without_a_discovered_path_it_falls_back_to_plain_docker(self, driver):
driver._docker_path = None
assert driver._container_engine_binary("docker") == "docker"
def test_services_cannot_be_controlled():
"""QTS has no systemd: the systemctl actions inherited from LinuxDriver would
fail on every unit, so the driver says it cannot control services at all --
which is what every capability check asks (is ``manage_service`` callable?)."""
from napalm_linux.linux import LinuxDriver
assert callable(getattr(LinuxDriver, "manage_service", None))
assert not callable(getattr(QnapQtsDriver, "manage_service", None))
def test_updates_are_not_read_or_applied_here():
"""QTS has no apt, dnf or opkg: the readers inherited from LinuxDriver find no
package manager, and QPKG updates are not implemented. Saying so keeps QNAP out
of netOrk's patch groups and compliance report, which ask whether these
methods are callable."""
for method in ("get_available_updates", "refresh_available_updates", "apply_updates"):
assert not callable(getattr(QnapQtsDriver, method, None)), method