init
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
napalm-openmediavault
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
NAPALM driver for OpenMediaVault NAS systems.
|
||||
|
||||
Extends LinuxDriver with OMV-specific storage capabilities:
|
||||
physical disk inventory, md RAID pools, mounted filesystems,
|
||||
and SMB/NFS/FTP share enumeration via the OMV config database.
|
||||
|
||||
Usage::
|
||||
|
||||
from napalm_openmediavault import OpenMediaVaultDriver
|
||||
|
||||
driver = OpenMediaVaultDriver("192.168.1.10", "admin", "secret")
|
||||
driver.open()
|
||||
print(driver.get_facts())
|
||||
print(driver.get_disks())
|
||||
print(driver.get_shares())
|
||||
driver.close()
|
||||
"""
|
||||
|
||||
from napalm_openmediavault.openmediavault import OpenMediaVaultDriver
|
||||
|
||||
__all__ = ["OpenMediaVaultDriver"]
|
||||
@@ -0,0 +1,612 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0
|
||||
|
||||
"""NAPALM driver for OpenMediaVault NAS systems.
|
||||
|
||||
Extends LinuxDriver with OMV-specific storage management:
|
||||
|
||||
* Physical disk inventory via lsblk and smartctl
|
||||
* md RAID array status via /proc/mdstat and mdadm
|
||||
* Mounted filesystem info via df
|
||||
* SMB, NFS, and FTP share enumeration via omv-confdbadm
|
||||
|
||||
Connects via SSH. Sudo access is required for smartctl and mdadm.
|
||||
All optional_args from LinuxDriver are accepted (pkg_manager, sudo_password, …).
|
||||
|
||||
Tested against OpenMediaVault 6.x and 7.x (Debian Bullseye / Bookworm base).
|
||||
"""
|
||||
|
||||
import json as _json
|
||||
import re
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from napalm_device_types.models import (
|
||||
DiskPoolDict,
|
||||
LogicalVolumeDict,
|
||||
NASShareDict,
|
||||
PhysicalDiskDict,
|
||||
ReplicationJobDict,
|
||||
StorageQuotaDict,
|
||||
StorageServiceDict,
|
||||
VolumeSnapshotDict,
|
||||
)
|
||||
from napalm_linux.linux import LinuxDriver
|
||||
|
||||
|
||||
class OpenMediaVaultDriver(LinuxDriver):
|
||||
"""NAPALM driver for OpenMediaVault NAS systems.
|
||||
|
||||
Inherits all Linux OS functionality from LinuxDriver (packages,
|
||||
services, users, processes, cron, Docker) and adds OMV-specific
|
||||
storage management capabilities.
|
||||
|
||||
NAS services are exposed via ``get_storage_services()`` to avoid a
|
||||
naming collision with LinuxDriver's ``get_services()`` (which returns
|
||||
the full systemd service list).
|
||||
"""
|
||||
|
||||
driver_name = "openmediavault"
|
||||
storage_device = True # signals netOrk to classify this driver as "storage"
|
||||
|
||||
# ── Facts override ─────────────────────────────────────────────────────────
|
||||
|
||||
def get_facts(self) -> Dict[str, Any]:
|
||||
facts = super().get_facts()
|
||||
try:
|
||||
ver = self._send(
|
||||
"dpkg-query -W -f='${Version}' openmediavault 2>/dev/null"
|
||||
).strip()
|
||||
if ver:
|
||||
facts["os_version"] = f"OMV {ver}"
|
||||
except Exception:
|
||||
pass
|
||||
return facts
|
||||
|
||||
# ── Warnings ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_device_warnings(self) -> List[Dict[str, Any]]:
|
||||
"""Return warnings, ignoring packages held back by apt-get upgrade.
|
||||
|
||||
On OMV, kernel and OMV-specific packages are routinely held back
|
||||
(they require dist-upgrade or omv-upgrade). Reporting them as
|
||||
"updates available" after a successful apt-get upgrade is misleading.
|
||||
We simulate the upgrade first and only warn if there are packages
|
||||
that a standard apt-get upgrade would actually install.
|
||||
"""
|
||||
warnings: List[Dict[str, Any]] = []
|
||||
try:
|
||||
sim = self._sudo(
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get upgrade --simulate 2>&1",
|
||||
read_timeout=60,
|
||||
)
|
||||
m = re.search(r'(\d+) upgraded', sim)
|
||||
upgradable_count = int(m.group(1)) if m else 0
|
||||
except Exception:
|
||||
upgradable_count = -1 # unknown — fall back to parent behaviour
|
||||
|
||||
if upgradable_count == 0:
|
||||
return [] # nothing to do with apt-get upgrade; held-back only
|
||||
if upgradable_count < 0:
|
||||
return super().get_device_warnings()
|
||||
|
||||
updates = self.get_available_updates()
|
||||
if updates:
|
||||
warnings.append({
|
||||
"code": "updates_available",
|
||||
"severity": "warning",
|
||||
"action": None,
|
||||
"meta": {
|
||||
"count": upgradable_count,
|
||||
"packages": [u.get("name", "") for u in updates[:10]],
|
||||
},
|
||||
})
|
||||
return warnings
|
||||
|
||||
# ── Physical disks ─────────────────────────────────────────────────────────
|
||||
|
||||
def get_disks(self) -> List[PhysicalDiskDict]:
|
||||
"""Return physical disk inventory.
|
||||
|
||||
Uses ``lsblk -J`` for device enumeration and ``smartctl -iA``
|
||||
(via sudo) for health, temperature, and RPM data.
|
||||
|
||||
Each entry contains:
|
||||
|
||||
* slot (string) - device path, e.g. ``"/dev/sda"``
|
||||
* model (string) - drive model string
|
||||
* serial (string) - drive serial number
|
||||
* vendor (string) - drive manufacturer
|
||||
* type (string) - ``"hdd"``, ``"ssd"``, or ``"nvme"``
|
||||
* size (int) - raw capacity in bytes
|
||||
* rpm (int) - rotational speed; ``0`` for SSD/NVMe
|
||||
* temperature (int) - temperature in Celsius; ``-1`` if unavailable
|
||||
* health (string) - ``"healthy"``, ``"failed"``, or ``"unknown"``
|
||||
* pool (string) - md array name if member; empty string otherwise
|
||||
|
||||
Example::
|
||||
|
||||
[
|
||||
{
|
||||
"slot": "/dev/sda",
|
||||
"model": "HGST HUS726T6TALE6L4",
|
||||
"serial": "K3GXXXXX",
|
||||
"vendor": "HGST",
|
||||
"type": "hdd",
|
||||
"size": 6001175126016,
|
||||
"rpm": 7200,
|
||||
"temperature": 34,
|
||||
"health": "healthy",
|
||||
"pool": "md1",
|
||||
},
|
||||
]
|
||||
"""
|
||||
result: List[PhysicalDiskDict] = []
|
||||
|
||||
try:
|
||||
raw = self._send(
|
||||
"lsblk -J -b -o NAME,SIZE,TYPE,ROTA,VENDOR,MODEL,SERIAL,TRAN 2>/dev/null"
|
||||
)
|
||||
lsblk = _json.loads(raw)
|
||||
except Exception:
|
||||
return result
|
||||
|
||||
for dev in lsblk.get("blockdevices", []):
|
||||
if dev.get("type") != "disk":
|
||||
continue
|
||||
name = dev.get("name", "")
|
||||
if not name:
|
||||
continue
|
||||
|
||||
size = int(dev.get("size") or 0)
|
||||
rota = str(dev.get("rota", "1"))
|
||||
vendor = (dev.get("vendor") or "").strip()
|
||||
model = (dev.get("model") or "").strip()
|
||||
serial = (dev.get("serial") or "").strip()
|
||||
tran = (dev.get("tran") or "").strip().lower()
|
||||
|
||||
if tran == "nvme":
|
||||
disk_type = "nvme"
|
||||
elif rota == "0":
|
||||
disk_type = "ssd"
|
||||
else:
|
||||
disk_type = "hdd"
|
||||
|
||||
temperature = -1
|
||||
health = "unknown"
|
||||
rpm = 0
|
||||
|
||||
try:
|
||||
smart = self._sudo(
|
||||
f"smartctl -iA /dev/{name} 2>/dev/null", read_timeout=15
|
||||
)
|
||||
if "PASSED" in smart:
|
||||
health = "healthy"
|
||||
elif "FAILED" in smart:
|
||||
health = "failed"
|
||||
temp_m = re.search(
|
||||
r"(?:Temperature_Celsius|Current Temperature|Temperature:)"
|
||||
r"[^\n]*?(\d+)\s*(?:Celsius|\(|$)",
|
||||
smart,
|
||||
)
|
||||
if temp_m:
|
||||
temperature = int(temp_m.group(1))
|
||||
if disk_type == "hdd":
|
||||
rpm_m = re.search(r"Rotation Rate:\s+(\d+)\s+rpm", smart)
|
||||
if rpm_m:
|
||||
rpm = int(rpm_m.group(1))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result.append(PhysicalDiskDict(
|
||||
slot=f"/dev/{name}",
|
||||
model=model,
|
||||
serial=serial,
|
||||
vendor=vendor,
|
||||
type=disk_type,
|
||||
size=size,
|
||||
rpm=rpm,
|
||||
temperature=temperature,
|
||||
health=health,
|
||||
pool=self._disk_to_pool(name),
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
def _disk_to_pool(self, disk_name: str) -> str:
|
||||
"""Return the md array a disk belongs to, or empty string."""
|
||||
try:
|
||||
mdstat = self._send("cat /proc/mdstat 2>/dev/null")
|
||||
for line in mdstat.splitlines():
|
||||
if disk_name in line and re.match(r'^md\d+', line):
|
||||
return line.split(":")[0].strip()
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
# ── md RAID pools ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_disk_pools(self) -> Dict[str, DiskPoolDict]:
|
||||
"""Return md RAID array status from ``/proc/mdstat`` and ``mdadm --detail``.
|
||||
|
||||
Keys are array names (e.g. ``"md1"``). Each value contains:
|
||||
|
||||
* name (string) - array name
|
||||
* type (string) - always ``"md"``
|
||||
* level (string) - RAID level, e.g. ``"raid1"``, ``"raid5"``
|
||||
* status (string) - ``"online"``, ``"degraded"``, ``"faulted"``, or ``"offline"``
|
||||
* total (int) - usable array size in bytes (``0`` if mdadm unavailable)
|
||||
* used (int) - always ``0`` (use ``get_volumes()`` for per-filesystem usage)
|
||||
* available (int) - always ``0``
|
||||
* disks (list of strings) - member device names (e.g. ``["sda1", "sdb1"]``)
|
||||
* auto_expand (bool) - always ``False`` (not applicable for md)
|
||||
* dedup (bool) - always ``False``
|
||||
* compression (string) - always ``"off"``
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
"md1": {
|
||||
"name": "md1",
|
||||
"type": "md",
|
||||
"level": "raid1",
|
||||
"status": "online",
|
||||
"total": 2000398925824,
|
||||
"used": 0,
|
||||
"available": 0,
|
||||
"disks": ["sda1", "sdb1"],
|
||||
"auto_expand": False,
|
||||
"dedup": False,
|
||||
"compression": "off",
|
||||
},
|
||||
}
|
||||
"""
|
||||
result: Dict[str, DiskPoolDict] = {}
|
||||
|
||||
try:
|
||||
mdstat = self._send("cat /proc/mdstat 2>/dev/null")
|
||||
except Exception:
|
||||
return result
|
||||
|
||||
for line in mdstat.splitlines():
|
||||
m = re.match(r'^(md\d+)\s*:\s+(\S+)\s+(\S+)\s+(.*)', line)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
md_name, state_word, level_raw, members_raw = m.groups()
|
||||
member_disks = re.findall(r'(\w+)\[\d+\]', members_raw)
|
||||
status = "online" if state_word == "active" else "offline"
|
||||
total = 0
|
||||
|
||||
try:
|
||||
detail = self._sudo(
|
||||
f"mdadm --detail /dev/{md_name} 2>/dev/null", read_timeout=10
|
||||
)
|
||||
size_m = re.search(r"Array Size\s*:\s*(\d+)\s*\(", detail)
|
||||
if size_m:
|
||||
total = int(size_m.group(1)) * 1024 # kB → bytes
|
||||
if "degraded" in detail.lower():
|
||||
status = "degraded"
|
||||
elif re.search(r"State\s*:.*(?:faulty|failed)", detail, re.IGNORECASE):
|
||||
status = "faulted"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result[md_name] = DiskPoolDict(
|
||||
name=md_name,
|
||||
type="md",
|
||||
level=level_raw.lower(),
|
||||
status=status,
|
||||
total=total,
|
||||
used=0,
|
||||
available=0,
|
||||
disks=member_disks,
|
||||
auto_expand=False,
|
||||
dedup=False,
|
||||
compression="off",
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
# ── Logical volumes / filesystems ──────────────────────────────────────────
|
||||
|
||||
def get_volumes(self) -> Dict[str, LogicalVolumeDict]:
|
||||
"""Return mounted filesystem info from ``df``.
|
||||
|
||||
Only real block devices (``/dev/…``) are included; tmpfs, devtmpfs,
|
||||
overlays, and similar pseudo-filesystems are skipped.
|
||||
|
||||
Keys are derived from the mountpoint (e.g. ``"srv-dev-disk-by-label-data"``
|
||||
for ``/srv/dev/disk/by-label/data``; ``"root"`` for ``/``).
|
||||
|
||||
Each value contains:
|
||||
|
||||
* name (string) - key repeated for convenience
|
||||
* pool (string) - device name without ``/dev/`` prefix
|
||||
* type (string) - always ``"filesystem"``
|
||||
* total (int) - total size in bytes
|
||||
* used (int) - used space in bytes
|
||||
* available (int) - available space in bytes
|
||||
* mountpoint (string) - absolute mount path
|
||||
* compression (string) - always ``"off"``
|
||||
* dedup (bool) - always ``False``
|
||||
* readonly (bool) - ``True`` if mounted ``ro``
|
||||
* snapshots (int) - always ``0``
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
"srv-data": {
|
||||
"name": "srv-data",
|
||||
"pool": "md1",
|
||||
"type": "filesystem",
|
||||
"total": 2000398925824,
|
||||
"used": 945000000000,
|
||||
"available": 1055398925824,
|
||||
"mountpoint": "/srv/data",
|
||||
"compression": "off",
|
||||
"dedup": False,
|
||||
"readonly": False,
|
||||
"snapshots": 0,
|
||||
},
|
||||
}
|
||||
"""
|
||||
result: Dict[str, LogicalVolumeDict] = {}
|
||||
|
||||
try:
|
||||
df_out = self._send("df -Pk 2>/dev/null")
|
||||
except Exception:
|
||||
return result
|
||||
|
||||
try:
|
||||
mounts = self._send("cat /proc/mounts 2>/dev/null")
|
||||
except Exception:
|
||||
mounts = ""
|
||||
|
||||
for line in df_out.splitlines()[1:]:
|
||||
parts = line.split()
|
||||
if len(parts) < 6:
|
||||
continue
|
||||
device = parts[0]
|
||||
total_k, used_k, avail_k = int(parts[1]), int(parts[2]), int(parts[3])
|
||||
mountpoint = parts[5]
|
||||
|
||||
if not device.startswith("/dev/"):
|
||||
continue
|
||||
|
||||
key = mountpoint.lstrip("/").replace("/", "-") or "root"
|
||||
pool = device.removeprefix("/dev/")
|
||||
|
||||
readonly = bool(re.search(
|
||||
rf'^{re.escape(device)}\s+\S+\s+\S+\s+(?:[^,\s]*,)*ro(?:,|\s|$)',
|
||||
mounts, re.MULTILINE,
|
||||
))
|
||||
|
||||
result[key] = LogicalVolumeDict(
|
||||
name=key,
|
||||
pool=pool,
|
||||
type="filesystem",
|
||||
total=total_k * 1024,
|
||||
used=used_k * 1024,
|
||||
available=avail_k * 1024,
|
||||
mountpoint=mountpoint,
|
||||
compression="off",
|
||||
dedup=False,
|
||||
readonly=readonly,
|
||||
snapshots=0,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
# ── NAS shares ────────────────────────────────────────────────────────────
|
||||
|
||||
def get_shares(self) -> Dict[str, NASShareDict]:
|
||||
"""Return all configured NAS shares from the OMV config database.
|
||||
|
||||
Covers SMB/CIFS, NFS, and FTP shares. Keys are prefixed with the
|
||||
protocol (e.g. ``"smb:media"``, ``"nfs:backups"``, ``"ftp:upload"``).
|
||||
|
||||
Each value contains:
|
||||
|
||||
* name (string) - share name without protocol prefix
|
||||
* protocol (string) - ``"smb"``, ``"nfs"``, or ``"ftp"``
|
||||
* path (string) - absolute filesystem path
|
||||
* volume (string) - last path component (leaf directory)
|
||||
* enabled (bool) - whether the share is currently exported
|
||||
* readonly (bool) - whether the share is exported read-only
|
||||
* description (string) - human-readable comment (SMB only)
|
||||
* clients (list of strings) - allowed client addresses (NFS only)
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
"smb:media": {
|
||||
"name": "media",
|
||||
"protocol": "smb",
|
||||
"path": "/srv/dev/disk/by-label/data/media",
|
||||
"volume": "media",
|
||||
"enabled": True,
|
||||
"readonly": False,
|
||||
"description": "Media library",
|
||||
"clients": [],
|
||||
},
|
||||
"nfs:backups": {
|
||||
"name": "backups",
|
||||
"protocol": "nfs",
|
||||
"path": "/srv/dev/disk/by-label/data/backups",
|
||||
"volume": "backups",
|
||||
"enabled": True,
|
||||
"readonly": False,
|
||||
"description": "",
|
||||
"clients": ["192.168.1.0/24"],
|
||||
},
|
||||
}
|
||||
"""
|
||||
shares: Dict[str, NASShareDict] = {}
|
||||
shares.update(self._smb_shares())
|
||||
shares.update(self._nfs_shares())
|
||||
shares.update(self._ftp_shares())
|
||||
return shares
|
||||
|
||||
# ── Storage services ──────────────────────────────────────────────────────
|
||||
|
||||
def get_storage_services(self) -> Dict[str, StorageServiceDict]:
|
||||
"""Return status of key NAS services (SMB, NFS, FTP, SSH, rsync).
|
||||
|
||||
This method is separate from ``get_services()`` (inherited from
|
||||
LinuxDriver) which returns the full systemd service list.
|
||||
|
||||
Keys are service names. Each value contains:
|
||||
|
||||
* name (string) - service name
|
||||
* enabled (bool) - configured to start on boot
|
||||
* running (bool) - currently active
|
||||
* port (int) - primary listening port
|
||||
* version (string) - protocol version; empty if unknown
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
"smb": {"name": "smb", "enabled": True, "running": True, "port": 445, "version": ""},
|
||||
"nfs": {"name": "nfs", "enabled": True, "running": True, "port": 2049, "version": ""},
|
||||
"ftp": {"name": "ftp", "enabled": False,"running": False, "port": 21, "version": ""},
|
||||
"ssh": {"name": "ssh", "enabled": True, "running": True, "port": 22, "version": ""},
|
||||
"rsync": {"name": "rsync", "enabled": False,"running": False, "port": 873, "version": ""},
|
||||
}
|
||||
"""
|
||||
_SVC_MAP = [
|
||||
# (key, systemd unit, default port)
|
||||
("smb", "smbd", 445),
|
||||
("nfs", "nfs-server", 2049),
|
||||
("ftp", "proftpd", 21),
|
||||
("ssh", "ssh", 22),
|
||||
("rsync", "rsync", 873),
|
||||
]
|
||||
result: Dict[str, StorageServiceDict] = {}
|
||||
for key, unit, port in _SVC_MAP:
|
||||
try:
|
||||
running = (
|
||||
self._send(f"systemctl is-active {unit} 2>/dev/null || true").strip()
|
||||
== "active"
|
||||
)
|
||||
enabled = (
|
||||
self._send(f"systemctl is-enabled {unit} 2>/dev/null || true").strip()
|
||||
== "enabled"
|
||||
)
|
||||
except Exception:
|
||||
running = enabled = False
|
||||
result[key] = StorageServiceDict(
|
||||
name=key, enabled=enabled, running=running, port=port, version="",
|
||||
)
|
||||
return result
|
||||
|
||||
# ── Not implemented ────────────────────────────────────────────────────────
|
||||
|
||||
def get_volume_snapshots(self, volume: str = "") -> List[VolumeSnapshotDict]:
|
||||
raise NotImplementedError(
|
||||
"OpenMediaVault does not expose volume snapshots via this driver"
|
||||
)
|
||||
|
||||
def get_replication_jobs(self) -> List[ReplicationJobDict]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_quotas(self) -> List[StorageQuotaDict]:
|
||||
raise NotImplementedError
|
||||
|
||||
# ── Internal helpers ───────────────────────────────────────────────────────
|
||||
|
||||
def _omv_read(self, key: str) -> list:
|
||||
"""Query the OMV config database and return a list of objects.
|
||||
|
||||
Handles both OMV 6+ (returns a JSON array directly) and older
|
||||
versions that wrap the array in a dict under a common key.
|
||||
"""
|
||||
try:
|
||||
raw = self._send(f"omv-confdbadm read {key!r} 2>/dev/null").strip()
|
||||
if not raw:
|
||||
return []
|
||||
data = _json.loads(raw)
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
# Older OMV wraps the list: {"share": [...]}
|
||||
for wrapper in ("share", "rule", "entry", "mntent", "sharedfolder"):
|
||||
if wrapper in data and isinstance(data[wrapper], list):
|
||||
return data[wrapper]
|
||||
return []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _resolve_shared_folder(self, uuid: str) -> str:
|
||||
"""Resolve an OMV shared folder UUID to its absolute filesystem path."""
|
||||
if not uuid:
|
||||
return ""
|
||||
try:
|
||||
for folder in self._omv_read("conf.system.sharedfolder"):
|
||||
if folder.get("uuid") != uuid:
|
||||
continue
|
||||
rel = folder.get("reldirpath", "").strip("/")
|
||||
mnt_uuid = folder.get("mntentref", "")
|
||||
mnt_dir = ""
|
||||
for mntent in self._omv_read("conf.system.filesystem.mountpoint"):
|
||||
if mntent.get("uuid") == mnt_uuid:
|
||||
mnt_dir = mntent.get("dir", "").rstrip("/")
|
||||
break
|
||||
return f"{mnt_dir}/{rel}".rstrip("/") if rel else mnt_dir
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def _smb_shares(self) -> Dict[str, NASShareDict]:
|
||||
out: Dict[str, NASShareDict] = {}
|
||||
for s in self._omv_read("conf.service.smb.shares"):
|
||||
name = s.get("name", "")
|
||||
if not name:
|
||||
continue
|
||||
path = self._resolve_shared_folder(s.get("sharedfolderref", ""))
|
||||
out[f"smb:{name}"] = NASShareDict(
|
||||
name=name,
|
||||
protocol="smb",
|
||||
path=path,
|
||||
volume=path.split("/")[-1] if path else "",
|
||||
enabled=bool(s.get("enable", True)),
|
||||
readonly=bool(s.get("readonly", False)),
|
||||
description=s.get("comment", ""),
|
||||
clients=[],
|
||||
)
|
||||
return out
|
||||
|
||||
def _nfs_shares(self) -> Dict[str, NASShareDict]:
|
||||
out: Dict[str, NASShareDict] = {}
|
||||
for s in self._omv_read("conf.service.nfs.shares"):
|
||||
path = self._resolve_shared_folder(s.get("sharedfolderref", ""))
|
||||
name = path.split("/")[-1] or s.get("sharedfolderref", "")
|
||||
clients_raw = s.get("client", "") or ""
|
||||
clients = [c.strip() for c in clients_raw.split(",") if c.strip()]
|
||||
opts = s.get("options", "") or ""
|
||||
out[f"nfs:{name}"] = NASShareDict(
|
||||
name=name,
|
||||
protocol="nfs",
|
||||
path=path,
|
||||
volume=path.split("/")[-1] if path else "",
|
||||
enabled=bool(s.get("enable", True)),
|
||||
readonly="ro" in [o.strip() for o in opts.split(",")],
|
||||
description="",
|
||||
clients=clients,
|
||||
)
|
||||
return out
|
||||
|
||||
def _ftp_shares(self) -> Dict[str, NASShareDict]:
|
||||
out: Dict[str, NASShareDict] = {}
|
||||
for s in self._omv_read("conf.service.ftp.shares"):
|
||||
path = self._resolve_shared_folder(s.get("sharedfolderref", ""))
|
||||
name = path.split("/")[-1] or s.get("sharedfolderref", "")
|
||||
out[f"ftp:{name}"] = NASShareDict(
|
||||
name=name,
|
||||
protocol="ftp",
|
||||
path=path,
|
||||
volume=path.split("/")[-1] if path else "",
|
||||
enabled=bool(s.get("enable", True)),
|
||||
readonly=bool(s.get("readonly", False)),
|
||||
description="",
|
||||
clients=[],
|
||||
)
|
||||
return out
|
||||
@@ -0,0 +1,44 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "napalm-openmediavault"
|
||||
version = "0.1.0"
|
||||
description = "NAPALM driver for OpenMediaVault NAS (SSH)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
license = { text = "Apache-2.0" }
|
||||
authors = [
|
||||
{ name = "Christian Manivong", email = "christian@manivong.de" },
|
||||
]
|
||||
keywords = ["napalm", "network", "automation", "nas", "openmediavault", "omv", "storage"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: System :: Networking",
|
||||
"Topic :: System :: Systems Administration",
|
||||
]
|
||||
dependencies = [
|
||||
"napalm>=4.0",
|
||||
"napalm-device-types>=0.3.0",
|
||||
"napalm-linux>=0.1.0",
|
||||
]
|
||||
|
||||
[project.entry-points."napalm.drivers"]
|
||||
openmediavault = "napalm_openmediavault:OpenMediaVaultDriver"
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/chrismanivong/napalm-openmediavault"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["napalm_openmediavault*"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.9"
|
||||
strict = true
|
||||
ignore_missing_imports = true
|
||||
Reference in New Issue
Block a user