From 87f0dff43f71ddcafbb4d52343e817a4f642fb2d Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Fri, 23 Oct 2026 08:32:58 +0200 Subject: [PATCH 1/4] style: ruff format and the lint rules of the driver standard Mechanical: the rule set every NAPALM driver uses (E, F, I, UP, B at line length 100, Python 3.10), with E501 off under tests/. The code was already formatted and clean under it; what ruff cannot fix comes in the next commit. --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9495a51..207cdbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,11 @@ 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" From 76f85051cc02a10c07ad0f7a7a70e511d0903f47 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Fri, 23 Oct 2026 08:34:52 +0200 Subject: [PATCH 2/4] fix: what bandit and the remaining lint rules found - WS-Management answers are parsed with defusedxml (new dependency, defusedxml>=0.7) with DTDs forbidden. The check for " 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 diff --git a/napalm_amt/wsman.py b/napalm_amt/wsman.py index 5fb44fa..b9a3d13 100644 --- a/napalm_amt/wsman.py +++ b/napalm_amt/wsman.py @@ -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"=4.16", "requests>=2.31", + "defusedxml>=0.7", ] classifiers = [ "License :: OSI Approved :: Apache Software License", diff --git a/tests/test_wsman.py b/tests/test_wsman.py index 4972b62..52b5cb9 100644 --- a/tests/test_wsman.py +++ b/tests/test_wsman.py @@ -214,6 +214,29 @@ def test_a_non_soap_error_raises(): _transport(retries=0).get("AMT_GeneralSettings") +_WITH_DTD = ( + '' + ']>' + '&a;' +) + + +@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"") + 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")) From c9a7ee4378f3256531707fe70eef6a87268dcc85 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Fri, 23 Oct 2026 08:37:32 +0200 Subject: [PATCH 3/4] chore: adopt the NAPALM driver standard - CI arrives in .gitea/workflows/ci.yml and calls the shared driver workflow of NAPALM/driver-standard; the repo had none before. - setuptools>=77 and the SPDX license string PEP 639 wants, without the License classifier; napalm-device-types>=4.21.1 from PyPI; Issues and Changelog links on git.netork.io. LICENSE, NOTICE and py.typed were already there and stay. - The test extra becomes the dev extra of the standard, plus responses, which the tests mock AMT with. - mypy strict with the untyped imports (napalm, requests, defusedxml) named instead of ignored globally; napalm_amt.driver is exempt for now (5 errors: getters return dicts where the role declares TypedDicts), recorded in tests/quality_baseline.json with get_event_log's complexity of 11. - Coverage floor 95 % (measured 95.87 %). - CHANGELOG.md in Keep a Changelog form; the README's development steps install from PyPI. --- .gitea/workflows/ci.yml | 15 +++++++++++++++ .gitignore | 3 +++ CHANGELOG.md | 23 ++++++++++++++++++++-- README.md | 13 +++++++------ pyproject.toml | 38 ++++++++++++++++++++++++------------- tests/quality_baseline.json | 12 ++++++++++++ 6 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 tests/quality_baseline.json diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..7530c79 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 3b4fdc1..cca36af 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ build/ dist/ .venv/ .pytest_cache/ +.coverage +.mypy_cache/ +.ruff_cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bc7107..11a8641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,31 @@ # 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] + +### 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 diff --git a/README.md b/README.md index 44d46e1..1aba640 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index bfec2e6..ef134e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=64"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] @@ -7,17 +7,15 @@ name = "napalm-amt" version = "0.1.1" 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", @@ -25,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" @@ -41,9 +39,6 @@ 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" @@ -58,6 +53,23 @@ select = ["E", "F", "I", "UP", "B"] [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 diff --git a/tests/quality_baseline.json b/tests/quality_baseline.json new file mode 100644 index 0000000..2f8b93e --- /dev/null +++ b/tests/quality_baseline.json @@ -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": [] +} From 0aec58fe0638600cf19de8bc8241f07308be87c4 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Fri, 23 Oct 2026 10:28:59 +0200 Subject: [PATCH 4/4] chore(release): 0.2.0 --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a8641..71e63f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this driver. The format follows ## [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 diff --git a/pyproject.toml b/pyproject.toml index ef134e6..47d911a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ 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 = "Apache-2.0"