"""Shared helpers: an Intel AMT WS-Management service served from the recorded responses."""
import re
import xml.etree.ElementTree as ET
from pathlib import Path
import pytest
import responses
FIXTURES = Path(__file__).parent / "fixtures"
DMT = FIXTURES / "dmt-wsman-messages"
HOST = "amt.example.net"
URL = f"http://{HOST}:16992/wsman"
_SOAP = "http://www.w3.org/2003/05/soap-envelope"
_ADDRESSING = "http://schemas.xmlsoap.org/ws/2004/08/addressing"
_WSMAN = "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
_ENUMERATION = "http://schemas.xmlsoap.org/ws/2004/09/enumeration"
def load(name: str, fixtures: Path = DMT) -> str:
return (fixtures / name).read_text()
def _end_of_sequence(resource_uri: str) -> str:
"""The last, empty page of an enumeration."""
return (
f''
f''
f"{_ENUMERATION}/PullResponse"
f"{resource_uri}"
f""
f""
)
def _enumerate_response(resource_uri: str, context: str) -> str:
return (
f''
f''
f"{_ENUMERATION}/EnumerateResponse"
f"{resource_uri}"
f"{context}"
f""
)
class Request:
"""One WS-Management request as the mock received it."""
def __init__(self, body: bytes | str):
self.xml = body.decode() if isinstance(body, bytes) else body
root = ET.fromstring(self.xml)
self.action = root.findtext(f"{{{_SOAP}}}Header/{{{_ADDRESSING}}}Action") or ""
self.resource_uri = root.findtext(f"{{{_SOAP}}}Header/{{{_WSMAN}}}ResourceURI") or ""
self.resource = self.resource_uri.rsplit("/", 1)[-1]
self.operation = self.action.rsplit("/", 1)[-1]
self.context = root.findtext(f".//{{{_ENUMERATION}}}EnumerationContext")
self.body = root.find(f"{{{_SOAP}}}Body")
header = root.find(f"{{{_SOAP}}}Header")
self.selectors = {s.get("Name"): s.text for s in header.iter(f"{{{_WSMAN}}}Selector")}
class AmtServer:
"""Answer WS-Management requests from ``..xml`` files.
``Get`` -> ``.get.xml``, ``Pull`` -> ``.pull.xml`` (a page without
``EndOfSequence`` is followed by an empty last page), a method -> ``..xml``.
A resource without a file is answered with the fault for an unknown resource URI, as
AMT does. ``override(resource, operation, status, body)`` replaces one answer.
"""
def __init__(self, rsps, fixtures: Path = DMT, url: str = URL):
self.fixtures = fixtures
self.requests: list[Request] = []
self.overrides: dict[tuple[str, str], tuple[int, str]] = {}
self._finished: set[str] = set()
rsps.add_callback(responses.POST, re.compile(re.escape(url)), callback=self._handle)
def override(self, resource: str, operation: str, status: int, body: str) -> None:
self.overrides[(resource, operation)] = (status, body)
def calls(self, operation: str | None = None) -> list[Request]:
return [r for r in self.requests if operation is None or r.operation == operation]
def _file(self, name: str) -> tuple[int, str]:
path = self.fixtures / name
if not path.exists():
return 400, load("fault_destination_unreachable.xml")
return 200, path.read_text()
def _handle(self, request):
req = Request(request.body)
self.requests.append(req)
status, text = self._answer(req)
return status, {"Content-Type": "application/soap+xml;charset=UTF-8"}, text
def _answer(self, req: Request) -> tuple[int, str]:
if (req.resource, req.operation) in self.overrides:
return self.overrides[(req.resource, req.operation)]
if req.operation == "Get":
return self._file(f"{req.resource}.get.xml")
if req.operation == "Enumerate":
if not (self.fixtures / f"{req.resource}.pull.xml").exists():
return self._file(f"{req.resource}.pull.xml")
return 200, _enumerate_response(req.resource_uri, f"ctx-{req.resource}")
if req.operation == "Pull":
if req.resource in self._finished:
return 200, _end_of_sequence(req.resource_uri)
status, text = self._file(f"{req.resource}.pull.xml")
if "EndOfSequence" not in text:
self._finished.add(req.resource)
return status, text
return self._file(f"{req.resource}.{req.operation}.xml")
@pytest.fixture
def amt():
"""A mocked AMT on http://amt.example.net:16992 answering from the DMT recordings."""
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
yield AmtServer(rsps)
def instances(name: str, fixtures: Path = DMT) -> list[dict]:
"""The instances in a recorded Get or Pull response, as the transport returns them."""
from napalm_amt.wsman import to_instance
body = ET.fromstring(load(name, fixtures)).find(f"{{{_SOAP}}}Body")
page = body.find(f"{{{_ENUMERATION}}}PullResponse")
if page is not None:
return [to_instance(item) for item in page.findall(f"{{{_ENUMERATION}}}Items/*")]
return [to_instance(body[0])]
def patched(name: str, old: str, new: str, count: int = -1) -> str:
"""A recorded response with ``old`` replaced; fails when ``old`` is not in it."""
text = load(name)
assert old in text, f"{old!r} not in {name}"
return text.replace(old, new, count)