A QNAP is a NAS, a hypervisor and a Linux host. It can now say so, because role
bases in napalm-device-types v1.0 declare their methods without implementing
them:
class QnapQtsDriver(StorageDriver, HypervisorDriver, LinuxDriver):
device_class comes from the first base, so DEVICE_CLASS is gone. The get_services
forwarder is gone with it — nothing shadows LinuxDriver's version any more — and
so is the TestMroForwarding class that guarded the collisions.
Removing the shadowing exposed something the shadowing had been hiding by
accident. This driver never implemented package management; StorageDriver's stub
for install_package covered LinuxDriver's working one, so calling it raised and
looked correct. It was a coincidence, not a decision: without the stub the call
falls through to LinuxDriver, which would run apt or dnf against a NAS that has
neither. QTS uses QPKG.
get_packages, install_package and uninstall_package are therefore overridden
here to refuse with a reason, until the device harvest supplies real QPKG
parsing. See netork#114.
165 lines
6.8 KiB
Python
165 lines
6.8 KiB
Python
# Licensed under the Apache License, Version 2.0
|
|
|
|
"""NAPALM driver for QNAP NAS systems running QTS.
|
|
|
|
QTS is a Linux distribution, so this driver inherits the whole OS surface from
|
|
:class:`napalm_linux.linux.LinuxDriver` — packages, services, users, processes,
|
|
Docker — and adds QNAP's own storage, QPKG and virtualisation layers on top:
|
|
|
|
* Physical disk inventory and SMART via ``qcli_storage`` and ``get_hd_smartinfo``
|
|
* Storage pools and volumes via ``qcli_storage`` and ``/proc/mdstat``
|
|
* SMB/NFS/AFP/FTP shares from ``/etc/config/smb.conf`` and ``/etc/exports``
|
|
* QPKG packages instead of a distribution package manager
|
|
* Virtualization Station guests via ``virsh``
|
|
* Container Station via LinuxDriver's Docker support, redirected to the
|
|
QPKG-local ``docker`` binary
|
|
|
|
Connects via SSH. SSH has to be enabled on the NAS first
|
|
(Control Panel → Telnet/SSH). Targets QTS 4.x and QTS 5.x; the parsers are
|
|
written against command output captured from both (see tools/harvest.sh).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
from napalm_device_types import (
|
|
FingerprintRule,
|
|
HypervisorDriver,
|
|
PortSpec,
|
|
StorageDriver,
|
|
)
|
|
from napalm_linux.linux import LinuxDriver
|
|
|
|
#: QNAP Systems' IANA enterprise number. Used by discovery to recognise a NAS
|
|
#: from its SNMP sysObjectID before anyone has supplied credentials.
|
|
QNAP_ENTERPRISE_OID = "1.3.6.1.4.1.24681"
|
|
|
|
#: Where Container Station puts the docker binary. It is not on PATH, and the
|
|
#: volume name varies with which pool the app was installed on, so this is a
|
|
#: glob evaluated on the device rather than a fixed path.
|
|
_DOCKER_GLOBS = (
|
|
"/share/*/.qpkg/container-station/bin/docker",
|
|
"/share/*/.qpkg/container-station/usr/bin/docker",
|
|
)
|
|
|
|
_VERSION_RE = re.compile(r"(\d+)\.")
|
|
|
|
|
|
class QnapQtsDriver(StorageDriver, HypervisorDriver, LinuxDriver):
|
|
"""NAPALM driver for QNAP NAS systems running QTS.
|
|
|
|
Declares all three roles it fills. The role bases carry no implementations,
|
|
so listing them alongside ``LinuxDriver`` costs nothing and shadows nothing;
|
|
what the driver can actually do is whatever it implements below.
|
|
|
|
NAS services are ``get_storage_services()``; the OS service list inherited
|
|
from ``LinuxDriver`` stays ``get_services()``. Two names because they are two
|
|
different things with different return shapes, not because they collided.
|
|
"""
|
|
|
|
# A QNAP is three things at once, and the order says which one leads:
|
|
# storage first, so netOrk shows it as a NAS. Nothing has to restate that
|
|
# -- device_class is read from this line.
|
|
TYPE_LABEL = "Storage"
|
|
VENDOR = "QNAP"
|
|
DRIVER_NAME = "qnap_qts"
|
|
driver_name = "qnap_qts"
|
|
NETMIKO_DEVICE_TYPE = "linux"
|
|
|
|
SNMP_OBJECT_ID_PREFIX = QNAP_ENTERPRISE_OID
|
|
SNMP_FINGERPRINT = [
|
|
FingerprintRule("qnap", weight=9.0),
|
|
FingerprintRule("nas", weight=1.0),
|
|
]
|
|
# QTS answers with a stock OpenSSH banner, so SSH alone cannot identify a
|
|
# QNAP — it only confirms the transport this driver needs.
|
|
SSH_FINGERPRINT = [
|
|
FingerprintRule("openssh", weight=1.0),
|
|
]
|
|
# Mandatory: without it every unidentified NAS web UI would score as a QNAP.
|
|
HTTP_FINGERPRINT = [
|
|
FingerprintRule("qnap", weight=9.0, mandatory=True),
|
|
FingerprintRule("qts", weight=4.0),
|
|
]
|
|
PORT_SPECS = [
|
|
PortSpec("https", 443),
|
|
PortSpec("http", 8080),
|
|
]
|
|
|
|
# ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
|
|
def open(self) -> None:
|
|
"""Connect, then resolve the facts that decide which code paths run.
|
|
|
|
QTS 4 and QTS 5 differ in the output of several tools, and the QPKG apps
|
|
that provide docker and virsh live on whichever storage pool they were
|
|
installed on. Resolving all of that once per session keeps every getter
|
|
below free of discovery round trips.
|
|
"""
|
|
super().open()
|
|
self._qts_major = self._detect_qts_major()
|
|
self._docker_path = self._discover_docker_path()
|
|
self._virsh_path = self._discover_virsh_path()
|
|
|
|
def _detect_qts_major(self) -> int | None:
|
|
"""Return the QTS major version, or None when it cannot be read.
|
|
|
|
Deliberately not fatal: a NAS that answers nothing useful here is still
|
|
worth polling, and the newer code path is the better default.
|
|
"""
|
|
try:
|
|
raw = self._send("getcfg System Version").strip()
|
|
except Exception:
|
|
return None
|
|
match = _VERSION_RE.match(raw)
|
|
return int(match.group(1)) if match else None
|
|
|
|
def _discover_docker_path(self) -> str:
|
|
"""Locate Container Station's docker binary, falling back to PATH."""
|
|
try:
|
|
found = self._send(f"ls {' '.join(_DOCKER_GLOBS)} 2>/dev/null | head -1").strip()
|
|
except Exception:
|
|
return "docker"
|
|
return found.splitlines()[0].strip() if found else "docker"
|
|
|
|
def _discover_virsh_path(self) -> str | None:
|
|
"""Locate Virtualization Station's virsh, or None when it is not installed.
|
|
|
|
None is a normal outcome — plenty of QNAP models never run VMs — and it
|
|
is what makes ``get_vms`` return an empty list instead of raising.
|
|
"""
|
|
try:
|
|
found = self._send(
|
|
"ls /share/*/.qpkg/QKVM/usr/bin/virsh /share/*/.qpkg/*/bin/virsh "
|
|
"2>/dev/null | head -1"
|
|
).strip()
|
|
except Exception:
|
|
return None
|
|
return found.splitlines()[0].strip() or None if found else None
|
|
|
|
def _docker_bin(self) -> str:
|
|
"""Override LinuxDriver's hook: docker is not on PATH under QTS."""
|
|
return getattr(self, "_docker_path", None) or "docker"
|
|
|
|
# ── QPKG, not apt ─────────────────────────────────────────────────────────
|
|
#
|
|
# QTS is Linux, so LinuxDriver's package methods are in the MRO and would
|
|
# run happily -- against a package manager QTS does not have. They used to
|
|
# be shadowed by StorageDriver's stubs, which made a QNAP refuse them by
|
|
# accident. Now that role bases implement nothing, the refusal has to be
|
|
# deliberate. QPKG parsing lands with the device harvest; until then,
|
|
# refusing is the only honest answer.
|
|
|
|
def get_packages(self) -> Any:
|
|
raise NotImplementedError(
|
|
"QTS uses QPKG, not a Linux package manager; QPKG parsing is not implemented yet"
|
|
)
|
|
|
|
def install_package(self, name: str, version: str = "") -> None:
|
|
raise NotImplementedError("QPKG install is not implemented yet")
|
|
|
|
def uninstall_package(self, name: str) -> None:
|
|
raise NotImplementedError("QPKG removal is not implemented yet")
|