Merge pull request 'chore: adopt the NAPALM driver standard' (#2) from chore/driver-standard into main
driver / standard (push) Successful in 14s
driver / lint (push) Successful in 11s
driver / types (push) Successful in 43s
driver / test (3.10) (push) Successful in 47s
driver / test (3.11) (push) Successful in 46s
driver / test (3.12) (push) Successful in 44s
driver / test (3.13) (push) Successful in 39s
driver / lowest (push) Successful in 22s
driver / security (push) Successful in 48s
driver / build (push) Successful in 22s
driver / publish (push) Successful in 41s
CI / driver (push) Successful in 1h52m46s
driver / standard (push) Successful in 14s
driver / lint (push) Successful in 11s
driver / types (push) Successful in 43s
driver / test (3.10) (push) Successful in 47s
driver / test (3.11) (push) Successful in 46s
driver / test (3.12) (push) Successful in 44s
driver / test (3.13) (push) Successful in 39s
driver / lowest (push) Successful in 22s
driver / security (push) Successful in 48s
driver / build (push) Successful in 22s
driver / publish (push) Successful in 41s
CI / driver (push) Successful in 1h52m46s
This commit was merged in pull request #2.
This commit is contained in:
commit
ffa561289b
9 files changed
+137
-35
No files matched your search
@@ -0,0 +1,15 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
driver:
|
||||
uses: NAPALM/driver-standard/.gitea/workflows/driver.yml@main
|
||||
with:
|
||||
standard-ref: main
|
||||
secrets:
|
||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
@@ -5,3 +5,6 @@ build/
|
||||
dist/
|
||||
.venv/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
+25
-2
@@ -1,12 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.1
|
||||
All notable changes to this driver. The format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/), versions follow
|
||||
[Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.0] - 2026-10-23
|
||||
|
||||
The first release on PyPI.
|
||||
|
||||
### Changed
|
||||
- The driver follows the NAPALM driver standard
|
||||
(https://git.netork.io/NAPALM/driver-standard): CI on Gitea runs ruff, mypy, the
|
||||
tests on Python 3.10–3.13 and at the lowest declared versions, bandit, pip-audit and
|
||||
a build check, and a release tag publishes to PyPI. Python 3.10 stays the floor; the
|
||||
license is declared as the SPDX string `Apache-2.0`, next to the LICENSE file and
|
||||
`py.typed` the package already shipped. napalm-device-types now comes from PyPI
|
||||
(4.21.1 or later).
|
||||
- AMT's answers are parsed with defusedxml (a new dependency). An answer that carries a
|
||||
DTD is refused in any encoding; before, one in UTF-16 got through.
|
||||
|
||||
## [0.1.1] - 2026-10-18
|
||||
|
||||
### Fixed
|
||||
- `SNMP_CAPABLE = False`: AMT runs no SNMP agent, so netOrk does not poll it over SNMP
|
||||
as it does other management controllers.
|
||||
|
||||
## 0.1.0
|
||||
## [0.1.0] - 2026-10-18
|
||||
|
||||
### Added
|
||||
- `amt` driver (`napalm_amt.AmtDriver`), a `ManagementControllerDriver` for Intel AMT over
|
||||
WS-Management: facts, AMT's network port, hardware inventory with the SMBIOS system
|
||||
UUID, firmware (AMT with build, BIOS), power state and power control, the hardware event
|
||||
|
||||
@@ -29,8 +29,8 @@ nothing with napalm-redfish beyond the role, so it lives here.
|
||||
`optional_args`: `port` (16992; 16993 means TLS), `tls` (overrides what the port
|
||||
implies), `verify_ssl` (`True`, a CA file path, or `False`), `retries` (1).
|
||||
|
||||
The WS-Management client in `napalm_amt/wsman.py` is self-contained (requests and
|
||||
ElementTree). Neither `pywsman` (a C binding) nor `python-amt` (unmaintained, power only)
|
||||
The WS-Management client in `napalm_amt/wsman.py` is self-contained (requests, and
|
||||
defusedxml to parse the answers). Neither `pywsman` (a C binding) nor `python-amt` (unmaintained, power only)
|
||||
is used.
|
||||
|
||||
## What AMT knows, and what it does not
|
||||
@@ -54,12 +54,13 @@ of its own in the MEBx ([docs/lenovo-m910x.md](docs/lenovo-m910x.md)).
|
||||
|
||||
## Development
|
||||
|
||||
The rules are in [STANDARDS.md](https://git.netork.io/NAPALM/driver-standard/src/branch/main/STANDARDS.md).
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
.venv/bin/pip install -e ../napalm-device-types # not on PyPI
|
||||
.venv/bin/pip install -e '.[test]' ruff mypy types-requests
|
||||
.venv/bin/python -m pytest
|
||||
.venv/bin/ruff check . && .venv/bin/mypy napalm_amt
|
||||
.venv/bin/pip install -e '.[dev]'
|
||||
.venv/bin/pytest --cov
|
||||
.venv/bin/ruff format --check . && .venv/bin/ruff check . && .venv/bin/mypy napalm_amt
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -107,7 +107,9 @@ def _landing_page(host: str, port: int, tls: bool, out: Path) -> None:
|
||||
"""The page discovery fetches, without credentials, with its status line and headers."""
|
||||
url = f"{'https' if tls else 'http'}://{host}:{port}/"
|
||||
try:
|
||||
response = requests.get(url, timeout=10, verify=False, allow_redirects=True)
|
||||
# Recorded as discovery sees it: no credentials go out, and AMT's own certificate
|
||||
# on 16993 is self-signed unless one was provisioned.
|
||||
response = requests.get(url, timeout=10, verify=False, allow_redirects=True) # nosec B501
|
||||
except requests.RequestException as exc:
|
||||
print(f"landing page: {exc}", file=sys.stderr)
|
||||
return
|
||||
|
||||
+17
-11
@@ -13,14 +13,21 @@ The envelopes follow Intel's own go-wsman-messages, which every AMT since versio
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing import Any
|
||||
from xml.sax.saxutils import escape, quoteattr
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
# escape and quoteattr build the request envelopes; nothing here parses with SAX.
|
||||
from xml.sax.saxutils import escape, quoteattr # nosec B406
|
||||
|
||||
import requests
|
||||
from defusedxml import DefusedXmlException
|
||||
from defusedxml.ElementTree import ParseError, fromstring
|
||||
from requests.auth import HTTPDigestAuth
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# For the annotations only; answers are parsed by defusedxml.
|
||||
import xml.etree.ElementTree as ET # nosec B405
|
||||
|
||||
SOAP_NS = "http://www.w3.org/2003/05/soap-envelope"
|
||||
ADDRESSING_NS = "http://schemas.xmlsoap.org/ws/2004/08/addressing"
|
||||
WSMAN_NS = "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
|
||||
@@ -267,14 +274,13 @@ class WsmanTransport:
|
||||
"""The SOAP body of a response; a fault, a refusal or anything else raises."""
|
||||
if response.status_code == 401:
|
||||
raise WsmanAuthError(f"{self.url}: credentials refused (HTTP 401)")
|
||||
body = None
|
||||
# A DTD has no business in a WS-Management response; refusing one keeps entity
|
||||
# expansion out of the parser.
|
||||
if b"<!DOCTYPE" not in response.content:
|
||||
try:
|
||||
body = ET.fromstring(response.content).find(f"{{{SOAP_NS}}}Body")
|
||||
except ET.ParseError:
|
||||
body = None
|
||||
# A DTD has no business in a WS-Management response; refusing one, in whatever
|
||||
# encoding, keeps entity expansion out of the parser.
|
||||
body: ET.Element | None
|
||||
try:
|
||||
body = fromstring(response.content, forbid_dtd=True).find(f"{{{SOAP_NS}}}Body")
|
||||
except (ParseError, DefusedXmlException):
|
||||
body = None
|
||||
if body is not None:
|
||||
fault = body.find(f"{{{SOAP_NS}}}Fault")
|
||||
if fault is not None:
|
||||
|
||||
+32
-15
@@ -1,22 +1,21 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=64"]
|
||||
requires = ["setuptools>=77"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "napalm-amt"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
description = "NAPALM driver for Intel Active Management Technology (AMT) over WS-Management"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
license = "Apache-2.0"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"napalm>=5.0",
|
||||
# Not on PyPI: install from git before this package (see README).
|
||||
"napalm-device-types>=4.16",
|
||||
"napalm-device-types>=4.21.1",
|
||||
"requests>=2.31",
|
||||
"defusedxml>=0.7",
|
||||
]
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: System :: Hardware",
|
||||
"Topic :: System :: Networking",
|
||||
@@ -24,12 +23,12 @@ classifiers = [
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://git.netork.io/NAPALM/napalm-amt"
|
||||
Issues = "https://git.netork.io/NAPALM/napalm-amt/issues"
|
||||
Changelog = "https://git.netork.io/NAPALM/napalm-amt/src/branch/main/CHANGELOG.md"
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"pytest>=7",
|
||||
"responses>=0.23",
|
||||
]
|
||||
# responses mocks AMT's HTTP answers in the tests.
|
||||
dev = ["pytest>=8", "pytest-cov>=5", "mypy>=1.11", "ruff>=0.6", "responses>=0.23"]
|
||||
|
||||
[project.entry-points."napalm.drivers"]
|
||||
amt = "napalm_amt:AmtDriver"
|
||||
@@ -40,19 +39,37 @@ include = ["napalm_amt*"]
|
||||
[tool.setuptools.package-data]
|
||||
napalm_amt = ["py.typed"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP"]
|
||||
select = ["E", "F", "I", "UP", "B"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
# Recorded device output keeps its lines.
|
||||
"tests/**" = ["E501"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
strict = true
|
||||
ignore_missing_imports = true
|
||||
# napalm itself is untyped, so every driver subclasses Any.
|
||||
disallow_subclassing_any = false
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["napalm.*", "requests.*", "defusedxml.*"]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
# Not yet under strict mypy (quality baseline); may only shrink.
|
||||
module = ["napalm_amt.driver"]
|
||||
ignore_errors = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["napalm_amt"]
|
||||
|
||||
[tool.coverage.report]
|
||||
fail_under = 95
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"coverage_floor": 95.0,
|
||||
"function_complexity": {
|
||||
"napalm_amt/driver.py::AmtDriver.get_event_log": 11
|
||||
},
|
||||
"function_lines": {},
|
||||
"mypy_relaxed": [
|
||||
"napalm_amt.driver"
|
||||
],
|
||||
"python_file_lines": {},
|
||||
"untyped_public": []
|
||||
}
|
||||
@@ -214,6 +214,29 @@ def test_a_non_soap_error_raises():
|
||||
_transport(retries=0).get("AMT_GeneralSettings")
|
||||
|
||||
|
||||
_WITH_DTD = (
|
||||
'<?xml version="1.0" encoding="{encoding}"?>'
|
||||
'<!DOCTYPE Envelope [<!ENTITY a "aaaa">]>'
|
||||
'<Envelope xmlns="http://www.w3.org/2003/05/soap-envelope"><Body>&a;</Body></Envelope>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("encoding", ["utf-8", "utf-16"])
|
||||
def test_an_answer_with_a_dtd_is_refused(encoding):
|
||||
"""A DTD has no business in a WS-Management answer, in whatever encoding it comes."""
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(responses.POST, URL, body=_WITH_DTD.format(encoding=encoding).encode(encoding))
|
||||
with pytest.raises(WsmanError, match="no WS-Management answer"):
|
||||
_transport(retries=0).get("AMT_GeneralSettings")
|
||||
|
||||
|
||||
def test_an_answer_that_is_no_xml_raises():
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(responses.POST, URL, status=200, body=b"<Envelope><Body>")
|
||||
with pytest.raises(WsmanError, match="HTTP 200"):
|
||||
_transport(retries=0).get("AMT_GeneralSettings")
|
||||
|
||||
|
||||
def test_a_connection_error_is_retried_then_raised():
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(responses.POST, URL, body=requests.ConnectionError("refused"))
|
||||
|
||||
Reference in new issue
Block a user