feat: Intel AMT driver over WS-Management (0.1.0)

An `amt` driver (napalm_amt.AmtDriver), a ManagementControllerDriver for the
management controller of Intel vPro machines, with its own lean WS-Management
client (requests + ElementTree, HTTP Digest, ports 16992/16993).

It reads the machine's identity, hardware with the SMBIOS system UUID, firmware
(AMT with build, BIOS), power state and the hardware event log, reports what AMT
listens on (plain HTTP or TLS, KVM incl. port 5900, IDE-R), and switches the
machine's power. AMT cannot be reset over WS-Management, so there is no
reboot_host. Discovery recognises AMT by its ports and its Server header.

Tested against Intel's published AMT 12 recordings (go-wsman-messages,
Apache-2.0, see NOTICE). `python -m napalm_amt.record` records a real AMT
read-only; the first test devices are two Lenovo ThinkCentre M910x.
This commit is contained in:
christianmanivong committed 2026-10-18 14:55:32 +02:00
commit 0052ba7884
61 files changed
+4303

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
*.egg-info/
build/
dist/
.venv/
.pytest_cache/
+10
View File
@@ -0,0 +1,10 @@
# Changelog
## 0.1.0
- `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
log, and what AMT listens on (plain HTTP or TLS, KVM, virtual media).
- Discovery: ports 16992/16993 and AMT's name in the `Server` header and web UI.
- `python -m napalm_amt.record`: records a real AMT's answers as fixtures, read-only.
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship made available under
the License, as indicated by a copyright notice that is included in
or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other transformations
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean, as submitted to the Licensor for inclusion
in the Work by the copyright owner or by an individual or Legal Entity
authorized to submit on behalf of the copyright owner. For the purposes
of this definition, "submitted" means any form of electronic, verbal,
or written communication sent to the Licensor or its representatives,
including but not limited to communication on electronic mailing lists,
source code control systems, and issue tracking systems that are managed
by, or on behalf of, the Licensor for the purpose of discussing and
improving the Work, but excluding communication that is conspicuously
marked or designated in writing by the copyright owner as "Not a
Contribution."
"Contributor" shall mean Licensor and any Legal Entity on behalf of
whom a Contribution has been received by the Licensor and incorporated
within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by the combined Contribution(s) and the Work
to which such Contribution(s) was submitted. If You institute patent
litigation against any entity (including a cross-claim or counterclaim
in a lawsuit) alleging that the Work or any Contribution embodied
within the Work constitutes direct or contributory patent infringement,
then any patent licenses granted to You under this License for that
Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or Derivative
Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, You must include a readable copy of the
attribution notices contained within such NOTICE file, in
at least one of the following places: within a NOTICE text file
distributed as part of the Derivative Works; within the Source
form or documentation, if provided along with the Derivative
Works; or, within a display generated by the Derivative Works,
if and wherever such third-party notices normally appear. The
contents of the NOTICE file are for informational purposes only
and do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own license statement for Your modifications and
may provide additional grant of rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Work,
and to permit persons to whom the Work is furnished to do so,
subject to the following conditions:
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any conditions of TITLE,
MERCHANTIBILITY, or FITNESS FOR A PARTICULAR PURPOSE.
See the License for the specific language governing permissions and
limitations under the License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or exemplary damages of any character arising as a
result of this License or out of the use or inability to use the
Work (even if such Contributor has been advised of the possibility
of such damages).
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may offer only
conditions consistent with this License, subject to the following
additional conditions:
(a) You may offer additional warranty, indemnity, or other liability
terms and conditions; and
(b) You may offer support for the Work in return for a fee.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format in question. It may also be
possible to make it available with a creative commons license.
Copyright 2026 Christian Manivong
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+8
View File
@@ -0,0 +1,8 @@
napalm-amt
Copyright 2026 Christian Manivong
This product includes material from go-wsman-messages
(https://github.com/device-management-toolkit/go-wsman-messages),
Copyright Intel Corporation, licensed under the Apache License, Version 2.0:
- the recorded WS-Management responses in tests/fixtures/dmt-wsman-messages/
- the event record texts in napalm_amt/parsers/logs.py
+73
View File
@@ -0,0 +1,73 @@
# napalm-amt
NAPALM driver for **Intel Active Management Technology (AMT)**, the management controller
built into Intel vPro business desktops, mini PCs and notebooks, over DMTF
WS-Management. Built on
[napalm-device-types](https://git.netork.io/NAPALM/napalm-device-types). Written for netOrk.
**Status: early development.** One driver, `amt` (`napalm_amt.AmtDriver`), a
`ManagementControllerDriver` from napalm-device-types like the server BMC drivers in
[napalm-redfish](https://git.netork.io/NAPALM/napalm-redfish). It reads the machine's
identity, hardware, firmware, power state and event log, reports what AMT listens on, and
switches the machine's power. It is tested against Intel's published recordings of an
AMT 12 (NUC9). Recordings of the first real test devices, two Lenovo ThinkCentre M910x
(AMT 11.x), are still to come.
## Why a driver of its own
AMT does the job of a server BMC (power control and inventory while the operating system
is down or the machine is off), but speaks a different protocol: no Redfish and no IPMI,
only WS-Management (SOAP, CIM classes) on port 16992, or 16993 with TLS. It shares
nothing with napalm-redfish beyond the role, so it lives here.
## Layout
| Entry point | Transport | Scope |
|---|---|---|
| `amt` | WS-Management over HTTP (16992) or HTTPS (16993), HTTP Digest auth | Read, plus power control; no reset of AMT itself |
`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)
is used.
## What AMT knows, and what it does not
AMT reads the machine's SMBIOS: manufacturer, model, serial number, system UUID, CPUs,
DIMMs, drives (sizes only), BIOS version. It has no temperature, fan or power-supply
readings on client platforms, no core counts, and no drive models. Where it knows nothing,
the driver says so (`None`, empty) and does not guess. See [docs/amt-map.md](docs/amt-map.md).
AMT shares the machine's network controller. In DHCP mode it also uses the operating
system's IP address. netOrk keeps one device per address, so give AMT a static IP address
of its own in the MEBx ([docs/lenovo-m910x.md](docs/lenovo-m910x.md)).
## Documents
- [docs/amt-map.md](docs/amt-map.md): getters mapped to WS-Management classes
- [docs/security.md](docs/security.md): plain HTTP vs TLS, accounts, KVM, known CVEs
- [docs/lenovo-m910x.md](docs/lenovo-m910x.md): first test devices, MEBx setup, what to verify
- [docs/testing.md](docs/testing.md): fixtures, recording a real AMT
- [docs/references.md](docs/references.md)
## Development
```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
```
## Conventions
These follow the other drivers in the `NAPALM` organisation:
- Role bases declare, they never implement. Generic logic goes into napalm-device-types,
vendor mechanics stay here.
- Parsing lives in pure functions (`napalm_amt/parsers/`), tested on recorded responses
under `tests/fixtures/<source>/`, with a `COMMANDS.txt` listing the request behind each
file.
+44
View File
@@ -0,0 +1,44 @@
# Getters and the WS-Management classes behind them
Classes are named by their prefix: `AMT_` and `IPS_` are Intel's
(`http://intel.com/wbem/wscim/1/{amt,ips}-schema/1/`), `CIM_` the DMTF's
(`http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/`). "Enum" is `Enumerate` + `Pull`.
## Connection
| Step | Request |
|---|---|
| `open()` | Get `AMT_GeneralSettings`. Exists on every AMT and needs a valid login, so it proves both |
Every read is cached for the connection, except the power state.
## NAPALM getters (describe AMT)
| Getter | Source | Notes |
|---|---|---|
| `get_facts` | `AMT_GeneralSettings` (HostName, DomainName), `CIM_Chassis`/`CIM_Card`, `CIM_SoftwareIdentity`, `AMT_EthernetPortSettings` | `os_version` = AMT version with build (`12.0.67.1579`); vendor/model/serial are the machine's (chassis, baseboard where the chassis holds SMBIOS placeholders); `uptime` -1 |
| `get_interfaces` | `AMT_EthernetPortSettings` | `wired`/`wireless`; a port without a MAC (no WLAN card) is left out; description says DHCP/static and whether AMT shares the host's IP |
| `get_interfaces_ip` | `AMT_EthernetPortSettings` (IPAddress, SubnetMask) | Only where AMT reports an address of its own (static configuration) |
| `get_environment` | none | Empty: client AMT has no sensor readings. `CIM_Fan`/`CIM_Sensor` exist but report placeholders (EnabledState 5, no speed) |
## Role methods (describe and control the machine)
| Method | Source | Notes |
|---|---|---|
| `get_power_state` | Enum `CIM_AssociatedPowerManagementService` (PowerState, AvailableRequestedPowerStates) | 2 = On; 3/4 (sleep), 7 (hibernate), 6/8/12/13 (off) = Off; actions = role actions whose CIM state AMT lists |
| `set_power_state` | Invoke `CIM_PowerManagementService.RequestPowerStateChange` | On → 2, ForceOff → 8, ForceRestart → 10, PowerCycle → 5, GracefulShutdown → 12, GracefulRestart → 14. ReturnValue 0 or 4096 (job started) = accepted |
| `get_hardware_inventory` | `CIM_Chassis`, `CIM_Card`, `CIM_ComputerSystemPackage` (PlatformGUID), `CIM_Processor` + `CIM_Chip`, `CIM_PhysicalMemory`, `CIM_MediaAccessDevice`, `AMT_EthernetPortSettings` | `system_uuid`: PlatformGUID with the first three groups byte-swapped (SMBIOS order, as dmidecode and Intel's console decode it). Drive size: `MaxMediaSize` × 1000. No cores/threads, no drive models, no PSUs |
| `get_event_log` | Invoke `AMT_MessageLog.PositionToFirstRecord`, then `GetRecords` (390 per call) until `NoMoreRecords` | 21-byte PET records, decoded after Intel's go-wsman-messages; `id` = position in the log |
| `get_firmware_inventory` | `CIM_SoftwareIdentity` (AMT, Build Number), `CIM_BIOSElement` | `updateable` unknown: AMT updates neither itself nor the BIOS |
| `get_management_protocols` | `AMT_TLSSettingData`, `AMT_RedirectionService`, `IPS_KVMRedirectionSettingData`, `CIM_KVMRedirectionSAP` | http/https from the "802.3" TLS setting; kvm/virtual_media on 16994 (16995 with TLS), kvm on 5900 when that port is on; ipmi/ssh/snmp always off |
| `reboot_host` | none | Not offered: WS-Management cannot restart AMT itself |
A class this AMT does not know (older versions know fewer) answers with a
`DestinationUnreachable` fault; its part of the inventory stays empty.
## Not read yet
- `AMT_AuthorizationService` (accounts and realms) for `get_users`.
- `AMT_AuditLog` (who logged in, who switched power). The event log covers hardware only.
- `AMT_SetupAndConfigurationService` (provisioning state and mode, ACM/CCM). Recorded by
`napalm_amt.record` to decide later whether it is worth a warning.
+54
View File
@@ -0,0 +1,54 @@
# First test devices: Lenovo ThinkCentre M910x
Two ThinkCentre M910x (Tiny, Intel Q270, vPro) with Intel ME/AMT 11.x, plus possibly a
third to experiment on. Nothing has been recorded from them yet; the driver is tested
against Intel's AMT 12 recordings (see [testing.md](testing.md)).
## Setting up AMT
Menu names vary between BIOS and ME firmware versions.
1. **BIOS** (F1): under *Advanced → Intel Manageability*, enable Intel Manageability
Control and the Ctrl+P prompt for MEBx.
2. **MEBx** (Ctrl+P at boot): log in with the default password `admin` and set a new one
(8 to 32 characters with upper and lower case, a digit and a special character).
3. *Intel AMT Configuration → Manageability Feature Selection*: enabled.
4. *Network Setup → TCP/IP Settings → Wired LAN IPv4*: **turn DHCP off** and give AMT a
**static address of its own**, with mask, gateway and DNS. In DHCP mode AMT uses the
operating system's address, and netOrk keeps one device per address.
5. *Network Access State*: activate network access.
6. *User Consent*: for KVM without a code from the person at the machine, set opt-in to
none. Admin Control Mode, which manual MEBx setup gives, allows that.
7. *Redirection Features*: SOL, storage redirection (IDE-R) and KVM as wanted.
Afterwards `http://<amt-ip>:16992/` answers with AMT's web UI, and the `Server` header
names the version.
## Recording
From a machine that reaches AMT, with the password in a file:
```bash
python -m napalm_amt.record <amt-ip> --user admin --password-file ~/.amt-m910x \
--out tests/fixtures/lenovo-m910x-<amt-version>
```
The recorder only reads (see [testing.md](testing.md)). It redacts the provisioning OTP
and the RFB password and keeps serial numbers.
## What the recordings have to settle
These are assumptions the driver makes from Intel's AMT 12 recordings and documentation:
- [ ] **System UUID byte order.** `get_hardware_inventory()["system_uuid"]` should equal
`dmidecode -s system-uuid` on the machine's OS. netOrk links AMT to its host by it.
- [ ] **Chassis versus board.** Whether `CIM_Chassis.Model` holds Lenovo's machine type
(`10MU…`) and `CIM_Card` the board.
- [ ] **Event log timestamps.** Taken as UTC. Some AMT versions keep local time.
- [ ] **Drives.** Whether AMT 11 lists `CIM_MediaAccessDevice` at all.
- [ ] **Graceful power states.** Whether AMT lists 12 and 14 with the OS running, and
whether that needs the Intel ME driver (LMS) on the host.
- [ ] **Sleep.** A sleeping machine is reported as Off. Check that On wakes it.
- [ ] **Static address.** `AMT_EthernetPortSettings.IPAddress` present with a static
configuration.
- [ ] **AMT version and CVE state.** Which 11.8 build Lenovo's latest ME firmware gives.
+12
View File
@@ -0,0 +1,12 @@
# References
- DMTF DSP0226, *Web Services for Management (WS-Management)*: operations, faults.
- DMTF DSP0227, *WS-Management CIM Binding*: resource URIs, enumeration.
- Intel AMT SDK / *Intel AMT Implementation and Reference Guide*: the AMT_ and IPS_
classes, power state codes, the event log record format.
- Intel Device Management Toolkit, `go-wsman-messages`
(github.com/device-management-toolkit/go-wsman-messages, Apache-2.0): request envelopes,
the event record decoder, and the recorded responses used as fixtures.
- Intel security advisories, e.g. INTEL-SA-00075 (CVE-2017-5689).
- napalm-redfish (git.netork.io/NAPALM/napalm-redfish): the server BMC drivers with the
same role.
+57
View File
@@ -0,0 +1,57 @@
# Security
## Plain HTTP (16992) versus TLS (16993)
Out of the box, AMT talks WS-Management on **16992 over plain HTTP**. HTTP Digest (MD5)
keeps the password off the wire. Everything else travels in the clear, including the
machine's inventory and every power command, and nothing stops a man in the middle from
replaying or changing them. TLS on 16993 needs a certificate and key provisioned into AMT
(`AMT_PublicKeyManagementService`, `AMT_TLSSettingData`); MEBx alone cannot set it up.
`get_management_protocols` reports `http` enabled while AMT accepts plain connections from
the network. Like IPMI over LAN for a server BMC, that is worth a finding in netOrk.
The driver verifies AMT's certificate on 16993 by default (`verify_ssl=True`). A
self-signed certificate needs `verify_ssl` set to its CA file, or `False`.
## Accounts
MEBx setup leaves one account, `admin`, with every right. AMT can hold more Digest
accounts, each limited to *realms* (`AMT_AuthorizationService`). Reading needs the
hardware asset, general info and event log realms; power control needs remote control.
A separate account for netOrk keeps `admin` out of it, but is not required.
AMT locks an account out for a while after repeated failed logins and writes the failures
to its event log ("Authentication failed N times"). The driver reports that entry as
Critical.
## KVM, SOL and IDE-R
- The **KVM console** goes over the redirection port (16994, or 16995 with TLS), behind
the AMT login. With **port 5900** enabled, AMT additionally speaks plain VNC, protected
only by an eight-character RFB password. Worth a finding when on.
- **User consent** (opt-in) asks the person at the machine for a code before KVM starts.
It is mandatory in Client Control Mode and optional in Admin Control Mode, which MEBx
setup gives.
- **IDE-R/USB-R** (virtual media) lets whoever holds the login boot the machine from a
remote image.
## Known vulnerabilities
AMT's firmware has a long CVE history. Most important is **CVE-2017-5689**
(INTEL-SA-00075): AMT 6 to 11.6 accepted an empty Digest response, so anyone on the
network got in without a password. Fixed firmware came through the PC vendors (Lenovo:
ME firmware updates). Many later advisories cover ME/AMT 11.x (SA-00086, SA-00112,
SA-00213, SA-00241, SA-00295 and others).
`get_firmware_inventory` reports AMT's version with its build (`11.8.50.3399`), and AMT
sends the same version without a login in its `Server` header. That version is what netOrk
needs to match advisories, and what a finding for outdated AMT firmware would rest on.
## Power control
`set_power_state` switches the machine itself, like a power button. It checks the action
against the power state and AMT's list of accepted states first, and refuses `On` for a
running machine. `ForceOff`, `ForceRestart` and `PowerCycle` do not ask the operating
system. The graceful actions need the operating system's cooperation, and AMT lists them
only where the host supports them.
+35
View File
@@ -0,0 +1,35 @@
# Testing
## Without hardware
`tests/conftest.py` serves recorded WS-Management answers over mocked HTTP
(`responses`): `Get` → `<Class>.get.xml`, `Pull` → `<Class>.pull.xml`, a method →
`<Class>.<Method>.xml`. A class without a file answers with the fault for an unknown
resource URI, as AMT does. A test changes one answer with `AmtServer.override` and a
recorded file with `patched(name, old, new)`, which fails when `old` is not in it.
| Fixtures | Source |
|---|---|
| `tests/fixtures/dmt-wsman-messages/` | Intel's go-wsman-messages (Apache-2.0), `pkg/wsman/wsmantesting/responses` at commit 00402b9: AMT 12.0.67 on an Intel NUC9, a few values replaced by placeholders. `fault_destination_unreachable.xml` is written by hand after DSP0226 |
There is no AMT simulator worth the name. Intel's Device Management Toolkit talks to
real AMT or to its own recordings, the ones used here.
## Recording a real AMT
`python -m napalm_amt.record HOST --password-file FILE --out DIR` makes every read the
driver makes and writes each raw answer under the name the mock serves it by, plus
`COMMANDS.txt` and `landing_page.txt` (the unauthenticated page with its headers, which
is what discovery sees). It never switches power and never writes to AMT. Pages after the
first become `<Class>.pull.2.xml`. A class AMT does not know is kept as
`<Class>.enumerate.xml` with its fault.
Name the directory `<vendor>-<model>-<amt version>`, for example
`lenovo-m910x-11.8.50.3399`.
## Running
```bash
.venv/bin/python -m pytest
.venv/bin/ruff check . && .venv/bin/mypy napalm_amt
```
+5
View File
@@ -0,0 +1,5 @@
"""NAPALM driver for Intel Active Management Technology (AMT) over WS-Management."""
from napalm_amt.driver import AmtDriver
__all__ = ["AmtDriver"]
+348
View File
@@ -0,0 +1,348 @@
"""NAPALM driver for Intel Active Management Technology (AMT) over WS-Management.
AMT is the management controller built into Intel vPro client platforms (business desktops,
mini PCs, notebooks). Like a server BMC it works while the operating system is down or the
machine is off; unlike one it shares the machine's network controller, has no sensors to
speak of, and cannot be restarted over the network.
The device is AMT. NAPALM's getters describe AMT (its network port, its version); the
``ManagementControllerDriver`` methods describe and control the machine it manages. There
is no ``reboot_host``: WS-Management offers no way to restart AMT itself.
Optional args
-------------
port : int
16992 (default, plain HTTP) or 16993 (TLS, once TLS is set up in AMT).
tls : bool
Overrides the scheme the port implies.
verify_ssl : bool or str
With TLS: ``True`` (default) verifies AMT's certificate against the CA bundle of
``requests``, a path verifies against that CA file, ``False`` turns verification off.
retries : int
Extra attempts after a connection error (default 1).
"""
from __future__ import annotations
import logging
from collections.abc import Callable
from typing import Any, TypeVar
from napalm.base.exceptions import ConnectionException
from napalm_device_types import ManagementControllerDriver
from napalm_device_types.base import FingerprintRule, PortSpec
from napalm_device_types.models import POWER_ACTIONS
from napalm_amt.parsers import (
POWER_REQUESTS,
accepted_actions,
parse_cpus,
parse_drives,
parse_ethernet_ports,
parse_event_record,
parse_firmware,
parse_management_protocols,
parse_memory,
parse_platform_guid,
parse_power_state,
parse_software_identity,
parse_system_identity,
)
from napalm_amt.parsers.common import is_true, text
from napalm_amt.wsman import PORT, WsmanAuthError, WsmanError, WsmanFault, WsmanTransport
LOG = logging.getLogger(__name__)
_JsonDict = dict[str, Any]
_T = TypeVar("_T")
#: Records per ``AMT_MessageLog.GetRecords`` call; 390 is what Intel's tools ask for.
_RECORDS_PER_CALL = 390
#: GetRecords calls per log; AMT keeps a few hundred records, this stops a runaway loop.
_MAX_RECORD_CALLS = 50
#: ``PositionToFirstRecord`` ReturnValue for an empty log.
_NO_RECORD = "2"
#: ``RequestPowerStateChange`` ReturnValues that mean accepted: done, or job started.
_POWER_ACCEPTED = {"0", "4096"}
#: The machine AMT manages, as ``RequestPowerStateChange`` names it (from Intel's tools).
_MANAGED_SYSTEM = (
"<h:ManagedElement>"
'<Address xmlns="http://schemas.xmlsoap.org/ws/2004/08/addressing">'
"http://schemas.xmlsoap.org/ws/2004/08/addressing</Address>"
'<ReferenceParameters xmlns="http://schemas.xmlsoap.org/ws/2004/08/addressing">'
'<ResourceURI xmlns="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">'
"http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem</ResourceURI>"
'<SelectorSet xmlns="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">'
'<Selector Name="CreationClassName">CIM_ComputerSystem</Selector>'
'<Selector Name="Name">ManagedSystem</Selector>'
"</SelectorSet></ReferenceParameters></h:ManagedElement>"
)
class AmtDriver(ManagementControllerDriver):
"""NAPALM driver for Intel AMT: reads the machine, and switches its power."""
platform = "amt"
DRIVER_NAME = "amt"
default_port = PORT
#: AMT listens on 16992 without TLS and on 16993 with it; one of them answers.
PORT_SPECS = [
PortSpec("http", 16992, ("/",), weight=5.0),
PortSpec("https", 16993, ("/",), weight=5.0),
]
#: AMT names itself in the ``Server`` header of every answer, and its web UI in the
#: title ("Intel(R) Active Management Technology 11.8.50.3399").
HTTP_FINGERPRINT = [FingerprintRule("active management technology", 10.0)]
def __init__(
self,
hostname: str,
username: str,
password: str,
timeout: int = 60,
optional_args: _JsonDict | None = None,
) -> None:
self.hostname = hostname
self.username = username
self.password = password
self.timeout = timeout
self.optional_args: _JsonDict = optional_args or {}
self.transport = WsmanTransport(
hostname,
username,
password,
port=self.optional_args.get("port"),
tls=self.optional_args.get("tls"),
verify=self.optional_args.get("verify_ssl", True),
timeout=timeout,
retries=self.optional_args.get("retries", 1),
)
self._cache: _JsonDict = {}
self._open = False
# --- connection ----------------------------------------------------------------------
def open(self) -> None:
"""Connect: read AMT's general settings, which every AMT has."""
self._cache = {}
try:
self._cache["general"] = self.transport.get("AMT_GeneralSettings")
except WsmanAuthError as exc:
raise ConnectionException(f"{self.hostname}: AMT refused the credentials") from exc
except WsmanError as exc:
raise ConnectionException(f"Cannot connect to AMT at {self.hostname}: {exc}") from exc
self._open = True
def close(self) -> None:
self.transport.close()
self._cache = {}
self._open = False
def is_alive(self) -> _JsonDict:
if not self._open:
return {"is_alive": False}
try:
self.transport.get("AMT_GeneralSettings")
except WsmanError:
return {"is_alive": False}
return {"is_alive": True}
# --- reads (cached per connection) ---------------------------------------------------
def _connected(self) -> WsmanTransport:
if not self._open:
raise ConnectionException(f"{self.hostname}: not connected, call open() first")
return self.transport
def _cached(self, key: str, load: Callable[[], _T]) -> _T:
self._connected()
if key not in self._cache:
self._cache[key] = load()
return self._cache[key] # type: ignore[no-any-return]
def _get(self, resource: str) -> _JsonDict:
return self._cached(resource, lambda: self.transport.get(resource))
def _all(self, resource: str) -> list[_JsonDict]:
"""Every instance of ``resource``; none where this AMT does not know the class
(older versions know fewer)."""
def load() -> list[_JsonDict]:
try:
return self.transport.enumerate(resource)
except WsmanFault as exc:
LOG.info("%s: no %s (%s)", self.hostname, resource, exc)
return []
return self._cached(resource, load)
def _general(self) -> _JsonDict:
return self._cached("general", lambda: self.transport.get("AMT_GeneralSettings"))
def _identity(self) -> dict[str, str]:
return parse_system_identity(self._all("CIM_Chassis"), self._all("CIM_Card"))
def _software(self) -> dict[str, str]:
return parse_software_identity(self._all("CIM_SoftwareIdentity"))
def _ports(self) -> list[_JsonDict]:
return parse_ethernet_ports(self._all("AMT_EthernetPortSettings"))
def _power(self) -> _JsonDict:
"""The power state, read fresh: it is what changes between two calls."""
services = self._connected().enumerate("CIM_AssociatedPowerManagementService")
return parse_power_state(services[0] if services else {})
# --- NAPALM getters ------------------------------------------------------------------
def get_facts(self) -> _JsonDict:
general = self._general()
identity = self._identity()
name = text(general.get("HostName"))
domain = text(general.get("DomainName"))
return {
"uptime": -1.0, # AMT keeps no uptime of the machine or of itself
"vendor": identity["manufacturer"],
"model": identity["model"],
"hostname": name or self.hostname,
"fqdn": f"{name}.{domain}" if name and domain else name or self.hostname,
"os_version": self._software()["amt"],
"serial_number": identity["serial_number"],
"interface_list": [p["name"] for p in self._ports()],
}
def get_interfaces(self) -> _JsonDict:
enabled = is_true(self._general().get("NetworkInterfaceEnabled", "true"))
interfaces = {}
for port in self._ports():
address = "DHCP" if port["dhcp"] else "static"
shared = ", shares the host's IP address" if port["shared_ip"] else ""
interfaces[port["name"]] = {
"is_up": port["link_up"],
"is_enabled": enabled,
"description": f"Intel AMT, {port['name']}, {address}{shared}",
"last_flapped": -1.0,
"speed": -1.0,
"mtu": -1,
"mac_address": port["mac_address"],
}
return interfaces
def get_interfaces_ip(self) -> _JsonDict:
return {
p["name"]: {"ipv4": {p["ip"]: {"prefix_length": p["prefix_length"]}}}
for p in self._ports()
if p["ip"] and p["prefix_length"] is not None
}
def get_environment(self) -> _JsonDict:
"""Empty: AMT on a client platform reports no temperatures, fans or power supplies,
and like any management controller it does not know the OS's load."""
self._connected()
return {"fans": {}, "temperature": {}, "power": {}, "cpu": {}, "memory": {}}
# --- role methods --------------------------------------------------------------------
def get_power_state(self) -> _JsonDict:
power = self._power()
return {
"state": power["state"],
"actions": accepted_actions(power["state"], power["available"]),
}
def set_power_state(self, action: str) -> None:
if action not in POWER_ACTIONS:
raise ValueError(f"{action!r} is not one of {', '.join(POWER_ACTIONS)}")
power = self._power()
accepted = accepted_actions(power["state"], power["available"])
if action not in accepted:
raise RuntimeError(
f"{self.hostname}: {action} not accepted while the machine is "
f"{power['state'] or 'in an unknown state'} "
f"(accepted: {', '.join(accepted) or 'none'})"
)
try:
output = self.transport.invoke(
"CIM_PowerManagementService",
"RequestPowerStateChange",
[("PowerState", POWER_REQUESTS[action])],
raw=_MANAGED_SYSTEM,
check=False,
)
except WsmanError as exc:
raise RuntimeError(f"{self.hostname}: {action} refused: {exc}") from exc
code = text(output.get("ReturnValue"))
if code not in _POWER_ACCEPTED:
raise RuntimeError(f"{self.hostname}: {action} refused by AMT (ReturnValue {code})")
def get_hardware_inventory(self) -> _JsonDict:
identity = self._identity()
packages = self._all("CIM_ComputerSystemPackage")
return {
"manufacturer": identity["manufacturer"],
"model": identity["model"],
"serial_number": identity["serial_number"],
"system_uuid": parse_platform_guid(
packages[0].get("PlatformGUID", "") if packages else ""
),
"cpus": parse_cpus(self._all("CIM_Processor"), self._all("CIM_Chip")),
"memory": parse_memory(self._all("CIM_PhysicalMemory")),
"drives": parse_drives(self._all("CIM_MediaAccessDevice")),
"power_supplies": [],
"nics": [
{
"name": p["name"],
"mac_address": p["mac_address"],
"speed_mbps": None,
"link_up": p["link_up"],
}
for p in self._ports()
],
}
def get_event_log(self, limit: int = 500) -> list[_JsonDict]:
"""AMT's hardware event log, newest first. ``id`` is the record's position in the
log, oldest first: AMT numbers its records no other way."""
start = self._connected().invoke("AMT_MessageLog", "PositionToFirstRecord", check=False)
if text(start.get("ReturnValue")) == _NO_RECORD:
return []
if text(start.get("ReturnValue")) != "0":
raise WsmanError(
f"AMT_MessageLog.PositionToFirstRecord failed: ReturnValue "
f"{text(start.get('ReturnValue'))}"
)
identifier = text(start.get("IterationIdentifier"))
records: list[str] = []
for _ in range(_MAX_RECORD_CALLS):
page = self.transport.invoke(
"AMT_MessageLog",
"GetRecords",
[("IterationIdentifier", identifier), ("MaxReadRecords", _RECORDS_PER_CALL)],
)
batch = page.get("RecordArray") or []
records.extend(batch if isinstance(batch, list) else [batch])
identifier = text(page.get("IterationIdentifier"))
if is_true(page.get("NoMoreRecords")) or not batch:
break
entries = [
e for e in (parse_event_record(r, str(n)) for n, r in enumerate(records, start=1)) if e
]
entries.sort(key=lambda e: e["timestamp"] or 0.0, reverse=True)
return entries[:limit]
def get_firmware_inventory(self) -> list[_JsonDict]:
return parse_firmware(self._software(), self._all("CIM_BIOSElement"))
def get_management_protocols(self) -> _JsonDict:
def optional_get(resource: str) -> _JsonDict:
try:
return self._get(resource)
except WsmanFault:
return {}
return parse_management_protocols(
tls_settings=self._all("AMT_TLSSettingData"),
redirection=optional_get("AMT_RedirectionService"),
kvm_settings=optional_get("IPS_KVMRedirectionSettingData"),
kvm_saps=self._all("CIM_KVMRedirectionSAP"),
)
+31
View File
@@ -0,0 +1,31 @@
"""Pure functions from WS-Management instances (dicts) to the shapes the driver returns."""
from napalm_amt.parsers.inventory import parse_cpus, parse_drives, parse_memory
from napalm_amt.parsers.logs import parse_event_record
from napalm_amt.parsers.network import parse_ethernet_ports
from napalm_amt.parsers.power import POWER_REQUESTS, accepted_actions, parse_power_state
from napalm_amt.parsers.protocols import parse_management_protocols
from napalm_amt.parsers.system import (
parse_firmware,
parse_platform_guid,
parse_software_identity,
parse_system_identity,
uuid_from_base64,
)
__all__ = [
"POWER_REQUESTS",
"accepted_actions",
"parse_cpus",
"parse_drives",
"parse_ethernet_ports",
"parse_event_record",
"parse_firmware",
"parse_management_protocols",
"parse_memory",
"parse_platform_guid",
"parse_power_state",
"parse_software_identity",
"parse_system_identity",
"uuid_from_base64",
]
+56
View File
@@ -0,0 +1,56 @@
"""Helpers shared by the parsers: WS-Management values are strings, lists or missing."""
from __future__ import annotations
from typing import Any
#: Values firmware puts where it has nothing to say (SMBIOS defaults), compared lowercased.
PLACEHOLDERS = frozenset(
{
"",
"0",
"none",
"default string",
"not specified",
"to be filled by o.e.m.",
"system product name",
"system serial number",
"123456789",
}
)
def as_list(value: Any) -> list[Any]:
"""A repeated element as a list; a single one as a list of one; none as empty."""
if value is None or value == "":
return []
return value if isinstance(value, list) else [value]
def first(value: Any) -> Any:
"""The first of a value that may be repeated."""
values = as_list(value)
return values[0] if values else None
def to_int(value: Any) -> int | None:
try:
return int(str(first(value)).strip())
except (TypeError, ValueError):
return None
def is_true(value: Any) -> bool:
return str(first(value)).strip().lower() in ("true", "1")
def text(value: Any) -> str:
"""A string value, stripped; a missing or nested one as empty."""
value = first(value)
return value.strip() if isinstance(value, str) else ""
def meaningful(value: Any) -> str:
"""``text(value)``, or empty for a firmware placeholder."""
result = text(value)
return "" if result.lower() in PLACEHOLDERS else result
+90
View File
@@ -0,0 +1,90 @@
"""CPUs, DIMMs and drives of the managed machine, as AMT reads them from SMBIOS.
AMT knows less than a server BMC: no core counts, no drive models, no health per DIMM.
What it does not know is ``None`` or empty, never guessed.
"""
from __future__ import annotations
from typing import Any
from napalm_amt.parsers.common import text, to_int
_Instance = dict[str, Any]
#: CIM_Processor.CPUStatus -> role status.
_CPU_STATUS = {1: "OK", 4: "OK", 2: "Warning", 3: "Critical"}
#: CIM_PhysicalMemory.MemoryType, which AMT fills with the SMBIOS memory type.
_MEMORY_TYPES = {
18: "DDR",
19: "DDR2",
24: "DDR3",
26: "DDR4",
27: "LPDDR",
28: "LPDDR2",
29: "LPDDR3",
30: "LPDDR4",
34: "DDR5",
35: "LPDDR5",
}
#: CIM OperationalStatus -> role status.
_OPERATIONAL = {2: "OK", 3: "Warning", 4: "Warning", 5: "Warning", 6: "Critical", 7: "Critical"}
def parse_cpus(processors: list[_Instance], chips: list[_Instance]) -> list[dict[str, Any]]:
"""One entry per ``CIM_Processor``; the model is the ``CIM_Chip`` with its tag."""
models = {text(c.get("Tag")): text(c.get("Version")) for c in chips}
return [
{
"socket": text(p.get("DeviceID")),
"model": models.get(text(p.get("DeviceID")), ""),
"cores": None,
"threads": None,
"status": _CPU_STATUS.get(to_int(p.get("CPUStatus")) or 0, ""),
}
for p in processors
]
def _positive(value: Any) -> int | None:
number = to_int(value)
return number if number and number > 0 else None
def parse_memory(modules: list[_Instance]) -> list[dict[str, Any]]:
"""One entry per ``CIM_PhysicalMemory``. AMT reports no health per module."""
return [
{
"slot": text(m.get("BankLabel")) or text(m.get("Tag")),
"size_bytes": _positive(m.get("Capacity")),
"type": _MEMORY_TYPES.get(to_int(m.get("MemoryType")) or 0, ""),
"speed_mts": (
_positive(m.get("ConfiguredMemoryClockSpeed"))
or _positive(m.get("MaxMemorySpeed"))
or _positive(m.get("Speed"))
),
"part_number": text(m.get("PartNumber")),
"serial_number": text(m.get("SerialNumber")),
"status": "",
}
for m in modules
]
def parse_drives(devices: list[_Instance]) -> list[dict[str, Any]]:
"""One entry per ``CIM_MediaAccessDevice``; ``MaxMediaSize`` is in kilobytes of 1000."""
drives = []
for d in devices:
size = _positive(d.get("MaxMediaSize"))
drives.append(
{
"slot": text(d.get("DeviceID")),
"model": "",
"serial_number": "",
"size_bytes": size * 1000 if size else None,
"media": "",
"protocol": "",
"status": _OPERATIONAL.get(to_int(d.get("OperationalStatus")) or 0, ""),
}
)
return drives
+173
View File
@@ -0,0 +1,173 @@
"""AMT's hardware event log: the base64 records of ``AMT_MessageLog.GetRecords``.
Each record is a 21-byte platform event (ASF/PET): a little-endian Unix timestamp, then
device address, sensor type, event type, offset, source type, severity, sensor number,
entity, entity instance and eight bytes of event data. The texts follow Intel's decoder in
go-wsman-messages, which knows the events AMT itself writes: failed logins, BIOS progress
and errors, watchdog and boot failures.
"""
from __future__ import annotations
import base64
import binascii
from typing import Any
_RECORD_LENGTH = 21
#: Entity codes (IPMI entity IDs) -> name.
_ENTITIES = {
0: "Unspecified",
1: "Other",
2: "Unknown",
3: "Processor",
4: "Disk",
5: "Peripheral",
6: "System management module",
7: "System board",
8: "Memory module",
9: "Processor module",
10: "Power supply",
11: "Add in card",
12: "Front panel board",
13: "Back panel board",
14: "Power system board",
15: "Drive backplane",
16: "System internal expansion board",
17: "Other system board",
18: "Processor board",
19: "Power unit",
20: "Power module",
21: "Power management board",
22: "Chassis back panel board",
23: "System chassis",
24: "Sub chassis",
25: "Other chassis board",
26: "Disk drive bay",
27: "Peripheral bay",
28: "Device bay",
29: "Fan cooling",
30: "Cooling unit",
31: "Cable interconnect",
32: "Memory device",
33: "System management software",
34: "BIOS",
35: "Intel(r) ME",
36: "System bus",
37: "Group",
38: "Intel(r) ME",
39: "External environment",
40: "Battery",
41: "Processing blade",
42: "Connectivity switch",
43: "Processor/memory module",
44: "I/O module",
45: "Processor I/O module",
46: "Management controller firmware",
47: "IPMI channel",
48: "PCI bus",
49: "PCI express bus",
50: "SCSI bus",
51: "SATA/SAS bus",
52: "Processor front side bus",
}
_FIRMWARE_ERRORS = {
0: "Unspecified.",
1: "No system memory is physically installed in the system.",
2: "No usable system memory, all installed memory has experienced an unrecoverable failure.",
3: "Unrecoverable hard-disk/ATAPI/IDE device failure.",
4: "Unrecoverable system-board failure.",
5: "Unrecoverable diskette subsystem failure.",
6: "Unrecoverable hard-disk controller failure.",
7: "Unrecoverable PS/2 or USB keyboard failure.",
8: "Removable boot media not found.",
9: "Unrecoverable video controller failure.",
10: "No video device detected.",
11: "Firmware (BIOS) ROM corruption detected.",
12: "CPU voltage mismatch (processors that share same supply have mismatched voltage "
"requirements)",
13: "CPU speed matching failure",
}
_FIRMWARE_PROGRESS = {
0: "Unspecified.",
1: "Memory initialization.",
2: "Starting hard-disk initialization and test",
3: "Secondary processor(s) initialization",
4: "User authentication",
5: "User-initiated system setup",
6: "USB resource configuration",
7: "PCI resource configuration",
8: "Option ROM initialization",
9: "Video initialization",
10: "Cache initialization",
11: "SM Bus initialization",
12: "Keyboard controller initialization",
13: "Embedded controller/management controller initialization",
14: "Docking station attachment",
15: "Enabling docking station",
16: "Docking station ejection",
17: "Disabling docking station",
18: "Calling operating system wake-up vector",
19: "Starting operating system boot process",
20: "Baseboard or motherboard initialization",
21: "reserved",
22: "Floppy initialization",
23: "Keyboard test",
24: "Pointing device test",
25: "Primary processor initialization",
}
_WATCHDOG_STATES = {1: "Not Started", 2: "Stopped", 4: "Running", 8: "Expired", 16: "Suspended"}
_FIXED = {
30: "No bootable media",
32: "Operating system lockup or power interrupt",
35: "System boot failure",
37: "System firmware started (at least one CPU is properly executing).",
}
def _severity(code: int) -> str:
"""PET severity: 16 critical, 32 non-recoverable; 8 non-critical; the rest is fine."""
if code >= 16:
return "Critical"
if code >= 8:
return "Warning"
return "OK"
def _description(sensor_type: int, offset: int, data: bytes) -> str:
if sensor_type == 6:
return (
f"Authentication failed {data[1] + (data[2] << 8)} times. "
"The system may be under attack."
)
if sensor_type == 15:
if data[0] == 235:
return "Invalid Data"
table = _FIRMWARE_ERRORS if offset == 0 else _FIRMWARE_PROGRESS
return table.get(data[1], f"code {data[1]}")
if sensor_type == 18:
if data[0] == 170:
agent = f"{data[4]:x}{data[3]:x}{data[2]:x}{data[1]:x}-{data[6]:x}{data[5]:x}"
state = _WATCHDOG_STATES.get(data[7], str(data[7]))
return f"Agent watchdog {agent}-... changed to {state}"
return "Unknown event data field"
return _FIXED.get(sensor_type, f"sensor type {sensor_type}")
def parse_event_record(record: str, entry_id: str) -> dict[str, Any] | None:
"""One base64 record as a role event log entry; None for a record that is not one."""
try:
raw = base64.b64decode(record, validate=True)
except (binascii.Error, ValueError):
return None
if len(raw) < _RECORD_LENGTH:
return None
timestamp = int.from_bytes(raw[0:4], "little")
sensor_type, offset, severity, entity = raw[5], raw[7], raw[9], raw[11]
entity_name = _ENTITIES.get(entity, f"Entity {entity}")
return {
"id": entry_id,
"timestamp": float(timestamp) if timestamp else None,
"severity": _severity(severity),
"message": f"{entity_name}: {_description(sensor_type, offset, raw[13:21])}",
}
+57
View File
@@ -0,0 +1,57 @@
"""AMT's own network ports (``AMT_EthernetPortSettings``).
AMT shares the machine's network controller: the wired port has the host's MAC address
and, in DHCP mode, the host's IP address too. A port without a MAC address is a wireless
port on a machine without a WLAN card.
"""
from __future__ import annotations
import ipaddress
from typing import Any
from napalm_amt.parsers.common import is_true, text
_Instance = dict[str, Any]
_WIRELESS = "3" # PhysicalConnectionType: 0 integrated LAN, 1 discrete LAN, 2 dock, 3 WLAN
def _mac(value: str) -> str:
mac = value.replace("-", ":").lower()
return "" if not mac.replace(":", "").strip("0") else mac
def _prefix(mask: str) -> int | None:
if not mask:
return None
try:
return ipaddress.IPv4Network(f"0.0.0.0/{mask}").prefixlen
except ValueError:
return None
def parse_ethernet_ports(ports: list[_Instance]) -> list[dict[str, Any]]:
"""The ports in use: name (``wired``/``wireless``), MAC, link, DHCP, whether AMT shares
the host's IP address, and its own address where AMT reports one."""
parsed: list[dict[str, Any]] = []
for port in ports:
mac = _mac(text(port.get("MACAddress")))
if not mac:
continue
name = "wireless" if text(port.get("PhysicalConnectionType")) == _WIRELESS else "wired"
if any(p["name"] == name for p in parsed):
name = f"{name}{len(parsed)}"
dhcp = is_true(port.get("DHCPEnabled"))
ip = text(port.get("IPAddress"))
parsed.append(
{
"name": name,
"mac_address": mac,
"link_up": is_true(port.get("LinkIsUp")),
"dhcp": dhcp,
"shared_ip": is_true(port.get("SharedDynamicIP" if dhcp else "SharedStaticIp")),
"ip": ip,
"prefix_length": _prefix(text(port.get("SubnetMask"))) if ip else None,
}
)
return parsed
+55
View File
@@ -0,0 +1,55 @@
"""The machine's power state and the power actions AMT accepts.
``CIM_AssociatedPowerManagementService.PowerState`` uses the CIM power state codes, and
``AvailableRequestedPowerStates`` lists the codes AMT accepts now. AMT lists the graceful
ones (12, 14) only where the host can honour them.
"""
from __future__ import annotations
from typing import Any
from napalm_device_types.models import POWER_ACTIONS
from napalm_amt.parsers.common import as_list, to_int
_Instance = dict[str, Any]
#: Role power action -> the CIM power state AMT is asked for. 8 is "Power Off - Hard" and
#: 10 "Master Bus Reset" (a hard reset); Intel verified both, and 2 and 5, on AMT.
POWER_REQUESTS: dict[str, int] = {
"On": 2,
"GracefulShutdown": 12,
"ForceOff": 8,
"GracefulRestart": 14,
"ForceRestart": 10,
"PowerCycle": 5,
}
_ON = {2}
#: Off, but also asleep (3, 4) and hibernating (7): the OS is not running, and On wakes it.
_OFF = {3, 4, 6, 7, 8, 12, 13}
def parse_power_state(service: _Instance) -> dict[str, Any]:
"""``{"state": "On" | "Off" | "", "available": [CIM power state codes]}``."""
code = to_int(service.get("PowerState"))
state = "On" if code in _ON else "Off" if code in _OFF else ""
available = [
n
for n in (to_int(v) for v in as_list(service.get("AvailableRequestedPowerStates")))
if n is not None
]
return {"state": state, "available": available}
def accepted_actions(state: str, available: list[int]) -> list[str]:
"""The role actions that make sense in ``state`` and that AMT offers.
A machine that is off accepts On (AMT may leave 2 out of its list then). A machine that
is on accepts every other action whose state AMT lists.
"""
if state == "Off":
return ["On"] if not available or POWER_REQUESTS["On"] in available else []
if state == "On":
return [a for a in POWER_ACTIONS if a != "On" and POWER_REQUESTS[a] in available]
return []
+61
View File
@@ -0,0 +1,61 @@
"""What AMT listens on, in the role's ``get_management_protocols`` shape.
AMT has no IPMI, SSH or SNMP. It answers WS-Management and its web UI on 16992 (plain
HTTP, Digest auth) until TLS is set up, then on 16993; with TLS, plain connections from the
network are refused unless ``AcceptNonSecureConnections`` says otherwise. The KVM console,
serial-over-LAN and IDE/USB redirection (virtual media) go over the redirection port 16994,
or 16995 with TLS; KVM can also listen on the standard VNC port 5900.
"""
from __future__ import annotations
from typing import Any
from napalm_amt.parsers.common import is_true, text, to_int
from napalm_amt.wsman import PORT, TLS_PORT
_Instance = dict[str, Any]
_REDIRECTION_PORT = 16994
_REDIRECTION_TLS_PORT = 16995
_VNC_PORT = 5900
#: CIM_KVMRedirectionSAP.EnabledState: 2 enabled, 6 enabled but offline (no session yet).
_KVM_ENABLED = {2, 6}
#: AMT_RedirectionService.EnabledState: 32768 none, 32769 IDE-R, 32770 SOL, 32771 both.
_IDER_ENABLED = {32769, 32771}
def _network_tls(tls_settings: list[_Instance]) -> _Instance:
"""The TLS setting for the network ("802.3"); the LMS one only covers local access."""
return next((t for t in tls_settings if "802.3" in text(t.get("InstanceID"))), {})
def parse_management_protocols(
tls_settings: list[_Instance],
redirection: _Instance,
kvm_settings: _Instance,
kvm_saps: list[_Instance],
) -> dict[str, dict[str, Any]]:
network_tls = _network_tls(tls_settings)
tls = is_true(network_tls.get("Enabled"))
plain = not tls or is_true(network_tls.get("AcceptNonSecureConnections"))
redirection_port = _REDIRECTION_TLS_PORT if tls else _REDIRECTION_PORT
kvm_enabled = text(kvm_settings.get("EnabledByMEBx")).lower() != "false" and any(
to_int(sap.get("EnabledState")) in _KVM_ENABLED for sap in kvm_saps
)
vnc = is_true(kvm_settings.get("Is5900PortEnabled"))
ider = (
to_int(redirection.get("EnabledState")) in _IDER_ENABLED
and text(redirection.get("ListenerEnabled")).lower() != "false"
)
return {
"ipmi": {"enabled": False, "port": 623},
"ssh": {"enabled": False, "port": 22},
"http": {"enabled": plain, "port": PORT},
"https": {"enabled": tls, "port": TLS_PORT},
"snmp": {"enabled": False, "port": 161},
"kvm": {"enabled": kvm_enabled, "port": _VNC_PORT if vnc else redirection_port},
"virtual_media": {"enabled": ider, "port": redirection_port},
}
+92
View File
@@ -0,0 +1,92 @@
"""The managed machine's identity and AMT's firmware.
``CIM_Chassis`` carries the system's SMBIOS identity (manufacturer, product, serial), and
``CIM_Card`` the baseboard's. ``CIM_ComputerSystemPackage.PlatformGUID`` is the SMBIOS
system UUID, the same that the operating system on the machine reads.
"""
from __future__ import annotations
import base64
import binascii
import re
from typing import Any
from napalm_amt.parsers.common import meaningful, text
_Instance = dict[str, Any]
_HEX32 = re.compile(r"^[0-9a-fA-F]{32}$")
def _uuid_from_hex(raw: str) -> str:
"""The SMBIOS UUID from its 16 bytes as stored: the first three groups little-endian,
as dmidecode and Intel's console decode them. Empty for all-zero or all-FF bytes, which
SMBIOS defines as "not present" and "not set"."""
if not _HEX32.match(raw or ""):
return ""
raw = raw.lower()
if raw in ("0" * 32, "f" * 32):
return ""
b = bytes.fromhex(raw)
swapped = b[3::-1] + b[5:3:-1] + b[7:5:-1] + b[8:]
h = swapped.hex()
return f"{h[:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:]}"
def parse_platform_guid(guid: str) -> str:
"""``CIM_ComputerSystemPackage.PlatformGUID`` (32 hex digits) as the SMBIOS UUID."""
return _uuid_from_hex(text(guid))
def uuid_from_base64(value: str) -> str:
"""``AMT_SetupAndConfigurationService.GetUuid``'s base64 UUID as the SMBIOS UUID."""
try:
raw = base64.b64decode(text(value), validate=True)
except (binascii.Error, ValueError):
return ""
return _uuid_from_hex(raw.hex())
def parse_software_identity(identities: list[_Instance]) -> dict[str, str]:
"""AMT's version with its build number (``12.0.67.1579``, as AMT's ``Server`` header
and Intel's advisories write it) and the ME firmware (``Flash``) version."""
versions = {text(i.get("InstanceID")): text(i.get("VersionString")) for i in identities}
amt = versions.get("AMT", "")
build = versions.get("Build Number", "")
return {
"amt": f"{amt}.{build}" if amt and build else amt,
"flash": versions.get("Flash", ""),
}
def parse_system_identity(chassis: list[_Instance], cards: list[_Instance]) -> dict[str, str]:
"""Manufacturer, model and serial of the machine, from the chassis and, where it holds
only placeholders, the baseboard; plus the baseboard's own model and serial."""
system = chassis[0] if chassis else {}
board = cards[0] if cards else {}
def pick(key: str) -> str:
return meaningful(system.get(key)) or meaningful(board.get(key))
return {
"manufacturer": pick("Manufacturer"),
"model": pick("Model"),
"serial_number": pick("SerialNumber"),
"board_model": meaningful(board.get("Model")),
"board_serial": meaningful(board.get("SerialNumber")),
}
def parse_firmware(identity: dict[str, str], bios: list[_Instance]) -> list[dict[str, Any]]:
"""AMT and the BIOS. Neither is updated through AMT, so ``updateable`` is unknown."""
primary = next(
(b for b in bios if text(b.get("PrimaryBIOS")).lower() == "true"), bios[0] if bios else {}
)
firmware = []
if identity.get("amt"):
firmware.append({"name": "Intel AMT", "version": identity["amt"], "updateable": None})
if text(primary.get("Version")):
firmware.append(
{"name": "BIOS", "version": text(primary.get("Version")), "updateable": None}
)
return firmware
View File
Whitespace-only changes.
+158
View File
@@ -0,0 +1,158 @@
"""Record a real AMT's answers as test fixtures: ``python -m napalm_amt.record HOST``.
Makes every read the driver makes and writes each raw answer under the name the tests'
mock serves it by (``<Class>.get.xml``, ``<Class>.pull.xml``, ``<Class>.<Method>.xml``),
plus a ``COMMANDS.txt`` and the unauthenticated landing page with its headers (what
discovery sees). It never switches power and never writes to AMT.
The password comes from ``--password-file`` (a file on the host, the way netOrk keeps
secrets) or the ``AMT_PASSWORD`` environment variable, never from the command line.
"""
from __future__ import annotations
import argparse
import os
import re
import sys
from collections.abc import Sequence
from pathlib import Path
import requests
from napalm_amt.wsman import PORT, WsmanError, WsmanTransport
#: Everything the driver reads: (class, operation). Methods with input are handled below.
READS: list[tuple[str, str]] = [
("AMT_GeneralSettings", "Get"),
("AMT_SetupAndConfigurationService", "Get"),
("AMT_RedirectionService", "Get"),
("IPS_KVMRedirectionSettingData", "Get"),
("AMT_EthernetPortSettings", "Enumerate"),
("AMT_TLSSettingData", "Enumerate"),
("CIM_KVMRedirectionSAP", "Enumerate"),
("CIM_SoftwareIdentity", "Enumerate"),
("CIM_BIOSElement", "Enumerate"),
("CIM_Chassis", "Enumerate"),
("CIM_Card", "Enumerate"),
("CIM_ComputerSystemPackage", "Enumerate"),
("CIM_Processor", "Enumerate"),
("CIM_Chip", "Enumerate"),
("CIM_PhysicalMemory", "Enumerate"),
("CIM_MediaAccessDevice", "Enumerate"),
("CIM_AssociatedPowerManagementService", "Enumerate"),
# Not read by the driver yet; recorded to see what this AMT knows.
("CIM_Fan", "Enumerate"),
("CIM_Sensor", "Enumerate"),
("AMT_SetupAndConfigurationService", "GetUuid"),
]
#: Values that are secrets, or close to one, replaced before anything is written.
_REDACT = re.compile(r"(<(\w+:)?(ProvisioningServerOTP|RFBPassword)>)[^<]+(</)")
class _Recorder:
def __init__(self, out: Path) -> None:
self.out = out
self.pages: dict[str, int] = {}
self.commands: list[str] = []
def __call__(self, resource: str, operation: str, content: bytes) -> None:
if operation == "Enumerate" and b"Fault" not in content:
return # the context only; the instances come with Pull
suffix = operation.lower() if operation in ("Get", "Pull", "Enumerate") else operation
key = f"{resource}.{suffix}"
self.pages[key] = self.pages.get(key, 0) + 1
name = f"{key}.xml" if self.pages[key] == 1 else f"{key}.{self.pages[key]}.xml"
text = _REDACT.sub(r"\1REDACTED\4", content.decode("utf-8", errors="replace"))
(self.out / name).write_text(text)
self.commands.append(f"{name:<55} {operation} {resource}")
def record(transport: WsmanTransport, out: Path) -> list[str]:
"""Make every read in ``READS`` plus the event log; return the files written."""
out.mkdir(parents=True, exist_ok=True)
recorder = _Recorder(out)
transport.recorder = recorder
try:
for resource, operation in READS:
try:
if operation == "Get":
transport.get(resource)
elif operation == "Enumerate":
transport.enumerate(resource)
else:
transport.invoke(resource, operation, check=False)
except WsmanError as exc:
print(f"{resource} {operation}: {exc}", file=sys.stderr)
start = transport.invoke("AMT_MessageLog", "PositionToFirstRecord", check=False)
if start.get("ReturnValue") == "0":
transport.invoke(
"AMT_MessageLog",
"GetRecords",
[
("IterationIdentifier", start.get("IterationIdentifier", "1")),
("MaxReadRecords", 390),
],
check=False,
)
finally:
transport.recorder = None
(out / "COMMANDS.txt").write_text(
"# Recorded with python -m napalm_amt.record\n" + "\n".join(recorder.commands) + "\n"
)
return [c.split()[0] for c in recorder.commands]
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)
except requests.RequestException as exc:
print(f"landing page: {exc}", file=sys.stderr)
return
headers = "\n".join(f"{k}: {v}" for k, v in response.headers.items())
(out / "landing_page.txt").write_text(
f"GET {url} -> {response.status_code}\n{headers}\n\n{response.text}"
)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="python -m napalm_amt.record",
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("host")
parser.add_argument("--user", default="admin")
parser.add_argument("--password-file", type=Path)
parser.add_argument("--port", type=int, default=PORT)
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--no-landing-page", action="store_true")
parser.add_argument(
"--insecure", action="store_true", help="do not verify AMT's TLS certificate (port 16993)"
)
args = parser.parse_args(argv)
if args.password_file:
password = args.password_file.read_text().strip()
elif os.environ.get("AMT_PASSWORD"):
password = os.environ["AMT_PASSWORD"]
else:
parser.error("give --password-file or set AMT_PASSWORD")
transport = WsmanTransport(
args.host, args.user, password, port=args.port, verify=not args.insecure
)
try:
written = record(transport, args.out)
finally:
transport.close()
if not args.no_landing_page:
_landing_page(args.host, args.port, transport.tls, args.out)
print(f"{len(written)} files in {args.out}")
return 0
if __name__ == "__main__":
sys.exit(main())
+284
View File
@@ -0,0 +1,284 @@
"""A lean WS-Management client for Intel AMT.
AMT speaks WS-Management (DMTF DSP0226) with the CIM binding: SOAP 1.2 envelopes POSTed to
``/wsman``, on port 16992 over plain HTTP or 16993 over TLS, authenticated with HTTP Digest.
This client covers the four operations a driver needs: ``Get`` one instance, ``Enumerate``
plus ``Pull`` all instances of a class, and invoke a method. Instances come back as plain
dicts keyed by local element names; a repeated element becomes a list, an element with
children a nested dict, and every value a string, as on the wire.
The envelopes follow Intel's own go-wsman-messages, which every AMT since version 6 accepts.
"""
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
import requests
from requests.auth import HTTPDigestAuth
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"
ENUMERATION_NS = "http://schemas.xmlsoap.org/ws/2004/09/enumeration"
TRANSFER_NS = "http://schemas.xmlsoap.org/ws/2004/09/transfer"
ANONYMOUS = ADDRESSING_NS + "/role/anonymous"
PORT = 16992
TLS_PORT = 16993
#: Class name prefix -> the schema its resource URI lives under.
_SCHEMAS = {
"AMT_": "http://intel.com/wbem/wscim/1/amt-schema/1/",
"IPS_": "http://intel.com/wbem/wscim/1/ips-schema/1/",
"CIM_": "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/",
}
#: Pages an enumeration may take. AMT's longest lists are a few dozen instances, and it
#: returns all of them in one page when asked for 999; this only stops a runaway loop.
_MAX_PAGES = 100
_HEADERS = {"Content-Type": "application/soap+xml;charset=UTF-8"}
Instance = dict[str, Any]
class WsmanError(Exception):
"""AMT did not answer, or answered with something other than the expected response."""
class WsmanAuthError(WsmanError):
"""AMT refused the credentials (HTTP 401 after the Digest exchange)."""
class WsmanFault(WsmanError):
"""AMT answered with a SOAP fault."""
def __init__(self, code: str, subcode: str, reason: str, detail: str) -> None:
self.code = code
self.subcode = subcode
self.reason = reason
self.detail = detail
super().__init__(f"{subcode or code}: {reason}" + (f" ({detail})" if detail else ""))
def resource_uri(resource: str) -> str:
"""The resource URI of a class, from its prefix (``AMT_``, ``IPS_``, ``CIM_``)."""
for prefix, base in _SCHEMAS.items():
if resource.startswith(prefix):
return base + resource
raise ValueError(f"{resource!r} is not an AMT, IPS or CIM class")
def _local(tag: str) -> str:
return tag.rsplit("}", 1)[-1]
def _value(element: ET.Element) -> str | Instance:
"""An element as a string (no children) or a dict of its children."""
children = list(element)
if not children:
return (element.text or "").strip()
result: Instance = {}
for child in children:
key, value = _local(child.tag), _value(child)
if key not in result:
result[key] = value
elif isinstance(result[key], list):
result[key].append(value)
else:
result[key] = [result[key], value]
return result
def to_instance(element: ET.Element | None) -> Instance:
"""An instance element as a dict; an empty or missing one as an empty dict."""
if element is None:
return {}
value = _value(element)
return value if isinstance(value, dict) else {}
def _fault(element: ET.Element) -> WsmanFault:
def text(path: str) -> str:
found = element.find(path)
return "".join(found.itertext()).strip() if found is not None else ""
def unprefixed(qname: str) -> str:
return qname.split(":", 1)[-1]
s = f"{{{SOAP_NS}}}"
return WsmanFault(
code=unprefixed(text(f"{s}Code/{s}Value")),
subcode=unprefixed(text(f"{s}Code/{s}Subcode/{s}Value")),
reason=text(f"{s}Reason/{s}Text"),
detail=text(f"{s}Detail"),
)
class WsmanTransport:
"""WS-Management over HTTP(S) with Digest auth, to one AMT.
``tls`` defaults to on for port 16993 and off otherwise. ``verify`` is passed to
``requests``: ``True`` checks the certificate against its CA bundle, a path against
that CA file, ``False`` not at all. ``retries`` counts extra attempts after a
connection error; a response, even an error, is never retried. ``recorder``, when set,
is called with the class, the operation and the raw body of every answer (to record
fixtures).
"""
def __init__(
self,
host: str,
username: str,
password: str,
port: int | None = None,
tls: bool | None = None,
verify: bool | str = True,
timeout: float = 60,
retries: int = 1,
) -> None:
self.port = int(port or PORT)
self.tls = self.port == TLS_PORT if tls is None else bool(tls)
self.url = f"{'https' if self.tls else 'http'}://{host}:{self.port}/wsman"
self.timeout = timeout
self.retries = max(0, int(retries))
self.session = requests.Session()
self.session.auth = HTTPDigestAuth(username, password)
self.session.verify = verify
self._message_ids = itertools.count(1)
self.recorder: Callable[[str, str, bytes], None] | None = None
def close(self) -> None:
self.session.close()
# --- operations ----------------------------------------------------------------------
def get(self, resource: str, selectors: Mapping[str, str] | None = None) -> Instance:
"""The one instance of ``resource`` (or the one ``selectors`` pick)."""
body = self._post(resource, f"{TRANSFER_NS}/Get", "", selectors)
return to_instance(next(iter(body), None))
def enumerate(self, resource: str) -> list[Instance]:
"""Every instance of ``resource``: ``Enumerate``, then ``Pull`` to the end."""
e = f"{{{ENUMERATION_NS}}}"
body = self._post(
resource, f"{ENUMERATION_NS}/Enumerate", f'<Enumerate xmlns="{ENUMERATION_NS}"/>'
)
context = body.findtext(f"{e}EnumerateResponse/{e}EnumerationContext")
instances: list[Instance] = []
for _ in range(_MAX_PAGES):
if not context:
return instances
body = self._post(
resource,
f"{ENUMERATION_NS}/Pull",
f'<Pull xmlns="{ENUMERATION_NS}"><EnumerationContext>{escape(context)}'
f"</EnumerationContext><MaxElements>999</MaxElements>"
f"<MaxCharacters>99999</MaxCharacters></Pull>",
)
page = body.find(f"{e}PullResponse")
if page is None:
raise WsmanError(f"{resource}: Pull answered without a PullResponse")
instances.extend(to_instance(item) for item in page.findall(f"{e}Items/*"))
if page.find(f"{e}EndOfSequence") is not None:
return instances
context = page.findtext(f"{e}EnumerationContext")
raise WsmanError(f"{resource}: no end of the enumeration after {_MAX_PAGES} pages")
def invoke(
self,
resource: str,
method: str,
params: Iterable[tuple[str, Any]] = (),
raw: str = "",
selectors: Mapping[str, str] | None = None,
check: bool = True,
) -> Instance:
"""Call ``method`` of ``resource`` and return its output parameters.
``params`` are sent in order as ``<h:Name>value</h:Name>``; ``raw`` is appended
unescaped after them, for parameters that are references (``h:`` is the class's
namespace). With ``check``, a ``ReturnValue`` other than 0 raises.
"""
uri = resource_uri(resource)
inner = "".join(f"<h:{name}>{escape(str(value))}</h:{name}>" for name, value in params)
body = self._post(
resource,
f"{uri}/{method}",
f'<h:{method}_INPUT xmlns:h="{uri}">{inner}{raw}</h:{method}_INPUT>',
selectors,
)
output = to_instance(body.find(f"{{{uri}}}{method}_OUTPUT"))
code = output.get("ReturnValue")
if check and code not in (None, "0"):
raise WsmanError(f"{resource}.{method} failed: ReturnValue {code}")
return output
# --- plumbing ------------------------------------------------------------------------
def _envelope(
self, uri: str, action: str, body: str, selectors: Mapping[str, str] | None
) -> str:
selector_set = ""
if selectors:
selector_set = (
"<w:SelectorSet>"
+ "".join(
f"<w:Selector Name={quoteattr(name)}>{escape(value)}</w:Selector>"
for name, value in selectors.items()
)
+ "</w:SelectorSet>"
)
return (
f'<?xml version="1.0" encoding="utf-8"?>'
f'<Envelope xmlns="{SOAP_NS}" xmlns:a="{ADDRESSING_NS}" xmlns:w="{WSMAN_NS}">'
f"<Header><a:Action>{action}</a:Action><a:To>/wsman</a:To>"
f"<w:ResourceURI>{uri}</w:ResourceURI>"
f"<a:MessageID>{next(self._message_ids)}</a:MessageID>"
f"<a:ReplyTo><a:Address>{ANONYMOUS}</a:Address></a:ReplyTo>"
f"<w:OperationTimeout>PT60S</w:OperationTimeout>{selector_set}</Header>"
f"<Body>{body}</Body></Envelope>"
)
def _post(
self, resource: str, action: str, body: str, selectors: Mapping[str, str] | None = None
) -> ET.Element:
envelope = self._envelope(resource_uri(resource), action, body, selectors)
response = self._send(envelope.encode())
if self.recorder is not None:
self.recorder(resource, action.rsplit("/", 1)[-1], response.content)
return self._body(response)
def _send(self, data: bytes) -> requests.Response:
error: requests.RequestException | None = None
for _ in range(self.retries + 1):
try:
return self.session.post(
self.url, data=data, headers=_HEADERS, timeout=self.timeout
)
except requests.RequestException as exc:
error = exc
raise WsmanError(f"{self.url}: {error}") from error
def _body(self, response: requests.Response) -> ET.Element:
"""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
if body is not None:
fault = body.find(f"{{{SOAP_NS}}}Fault")
if fault is not None:
raise _fault(fault)
if body is None or response.status_code >= 400:
raise WsmanError(f"{self.url}: HTTP {response.status_code}, no WS-Management answer")
return body
+58
View File
@@ -0,0 +1,58 @@
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"
[project]
name = "napalm-amt"
version = "0.1.0"
description = "NAPALM driver for Intel Active Management Technology (AMT) over WS-Management"
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.10"
dependencies = [
"napalm>=5.0",
# Not on PyPI: install from git before this package (see README).
"napalm-device-types>=4.16",
"requests>=2.31",
]
classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Topic :: System :: Hardware",
"Topic :: System :: Networking",
]
[project.urls]
Repository = "https://git.netork.io/NAPALM/napalm-amt"
[project.optional-dependencies]
test = [
"pytest>=7",
"responses>=0.23",
]
[project.entry-points."napalm.drivers"]
amt = "napalm_amt:AmtDriver"
[tool.setuptools.packages.find]
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"]
[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
View File
Whitespace-only changes.
+142
View File
@@ -0,0 +1,142 @@
"""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'<?xml version="1.0" encoding="UTF-8"?>'
f'<a:Envelope xmlns:a="{_SOAP}" xmlns:b="{_ADDRESSING}" xmlns:c="{_WSMAN}" '
f'xmlns:g="{_ENUMERATION}"><a:Header>'
f"<b:Action>{_ENUMERATION}/PullResponse</b:Action>"
f"<c:ResourceURI>{resource_uri}</c:ResourceURI></a:Header>"
f"<a:Body><g:PullResponse><g:Items/><g:EndOfSequence/></g:PullResponse></a:Body>"
f"</a:Envelope>"
)
def _enumerate_response(resource_uri: str, context: str) -> str:
return (
f'<?xml version="1.0" encoding="UTF-8"?>'
f'<a:Envelope xmlns:a="{_SOAP}" xmlns:b="{_ADDRESSING}" xmlns:c="{_WSMAN}" '
f'xmlns:g="{_ENUMERATION}"><a:Header>'
f"<b:Action>{_ENUMERATION}/EnumerateResponse</b:Action>"
f"<c:ResourceURI>{resource_uri}</c:ResourceURI></a:Header>"
f"<a:Body><g:EnumerateResponse><g:EnumerationContext>{context}</g:EnumerationContext>"
f"</g:EnumerateResponse></a:Body></a:Envelope>"
)
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 ``<Resource>.<operation>.xml`` files.
``Get`` -> ``<Resource>.get.xml``, ``Pull`` -> ``<Resource>.pull.xml`` (a page without
``EndOfSequence`` is followed by an empty last page), a method -> ``<Resource>.<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)
@@ -0,0 +1,48 @@
<?xml version= "1.0" encoding= "UTF-8"?>
<a:Envelope xmlns:a= "http://www.w3.org/2003/05/soap-envelope" xmlns:b= "http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:c= "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:d= "http://schemas.xmlsoap.org/ws/2005/02/trust" xmlns:e= "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:f= "http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:g= "http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:h= "http://intel.com/wbem/wscim/1/amt-schema/1/AMT_EthernetPortSettings"
xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>4</b:RelatesTo>
<b:Action a:mustUnderstand= "true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-0000000002E8</b:MessageID>
<c:ResourceURI>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_EthernetPortSettings</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:AMT_EthernetPortSettings>
<h:DHCPEnabled>true</h:DHCPEnabled>
<h:DefaultGateway>192.168.6.1</h:DefaultGateway>
<h:ElementName>Intel(r) AMT Ethernet Port Settings</h:ElementName>
<h:InstanceID>Intel(r) AMT Ethernet Port Settings 0</h:InstanceID>
<h:IpSyncEnabled>true</h:IpSyncEnabled>
<h:LinkIsUp>true</h:LinkIsUp>
<h:LinkPolicy>1</h:LinkPolicy>
<h:LinkPolicy>14</h:LinkPolicy>
<h:MACAddress>00-be-43-d8-22-a4</h:MACAddress>
<h:PhysicalConnectionType>0</h:PhysicalConnectionType>
<h:PrimaryDNS>192.168.6.1</h:PrimaryDNS>
<h:SharedDynamicIP>true</h:SharedDynamicIP>
<h:SharedMAC>true</h:SharedMAC>
<h:SharedStaticIp>false</h:SharedStaticIp>
<h:SubnetMask>255.255.255.0</h:SubnetMask>
</h:AMT_EthernetPortSettings>
<h:AMT_EthernetPortSettings>
<h:ConsoleTcpMaxRetransmissions>5</h:ConsoleTcpMaxRetransmissions>
<h:DHCPEnabled>true</h:DHCPEnabled>
<h:ElementName>Intel(r) AMT Ethernet Port Settings</h:ElementName>
<h:InstanceID>Intel(r) AMT Ethernet Port Settings 1</h:InstanceID>
<h:LinkControl>2</h:LinkControl>
<h:LinkIsUp>false</h:LinkIsUp>
<h:LinkPreference>2</h:LinkPreference>
<h:MACAddress>00-00-00-00-00-00</h:MACAddress>
<h:PhysicalConnectionType>3</h:PhysicalConnectionType>
<h:SharedMAC>true</h:SharedMAC>
<h:WLANLinkProtectionLevel>1</h:WLANLinkProtectionLevel>
</h:AMT_EthernetPortSettings>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/EnumerateResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-00000000007B</b:MessageID>
<c:ResourceURI>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_GeneralSettings</c:ResourceURI>
</a:Header>
<a:Body>
<g:EnumerateResponse>
<g:EnumerationContext>14000000-0000-0000-0000-000000000000</g:EnumerationContext>
</g:EnumerateResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,39 @@
<?xml version= "1.0" encoding= "UTF-8"?>
<a:Envelope xmlns:a= "http://www.w3.org/2003/05/soap-envelope" xmlns:b= "http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:c= "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:d= "http://schemas.xmlsoap.org/ws/2005/02/trust" xmlns:e= "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:f= "http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:g= "http://intel.com/wbem/wscim/1/amt-schema/1/AMT_GeneralSettings"
xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand= "true">http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-0000000002E4</b:MessageID>
<c:ResourceURI>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_GeneralSettings</c:ResourceURI>
</a:Header>
<a:Body>
<g:AMT_GeneralSettings>
<g:AMTNetworkEnabled>1</g:AMTNetworkEnabled>
<g:DDNSPeriodicUpdateInterval>1440</g:DDNSPeriodicUpdateInterval>
<g:DDNSTTL>900</g:DDNSTTL>
<g:DDNSUpdateByDHCPServerEnabled>true</g:DDNSUpdateByDHCPServerEnabled>
<g:DDNSUpdateEnabled>false</g:DDNSUpdateEnabled>
<g:DHCPSyncRequiresHostname>1</g:DHCPSyncRequiresHostname>
<g:DHCPv6ConfigurationTimeout>0</g:DHCPv6ConfigurationTimeout>
<g:DigestRealm>Digest:F3EB554784E729164447A89F60B641C5</g:DigestRealm>
<g:DomainName>Test Domain Name</g:DomainName>
<g:ElementName>Intel(r) AMT: General Settings</g:ElementName>
<g:HostName>Test Host Name</g:HostName>
<g:HostOSFQDN>Test Host OS FQDN</g:HostOSFQDN>
<g:IdleWakeTimeout>1</g:IdleWakeTimeout>
<g:InstanceID>Intel(r) AMT: General Settings</g:InstanceID>
<g:NetworkInterfaceEnabled>true</g:NetworkInterfaceEnabled>
<g:PingResponseEnabled>true</g:PingResponseEnabled>
<g:PowerSource>0</g:PowerSource>
<g:PreferredAddressFamily>0</g:PreferredAddressFamily>
<g:PresenceNotificationInterval>0</g:PresenceNotificationInterval>
<g:PrivacyLevel>0</g:PrivacyLevel>
<g:RmcpPingResponseEnabled>true</g:RmcpPingResponseEnabled>
<g:SharedFQDN>true</g:SharedFQDN>
<g:ThunderboltDockEnabled>0</g:ThunderboltDockEnabled>
<g:WsmanOnlyMode>false</g:WsmanOnlyMode>
</g:AMT_GeneralSettings>
</a:Body>
</a:Envelope>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://intel.com/wbem/wscim/1/amt-schema/1/AMT_MessageLog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>9</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://intel.com/wbem/wscim/1/amt-schema/1/AMT_MessageLog/GetRecordsResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000002722</b:MessageID>
<c:ResourceURI>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_MessageLog</c:ResourceURI>
</a:Header>
<a:Body>
<g:GetRecords_OUTPUT>
<g:IterationIdentifier>3</g:IterationIdentifier>
<g:NoMoreRecords>true</g:NoMoreRecords>
<g:RecordArray>Y8iYZf8GbwVoEP8mYaoKAAAAAAAA</g:RecordArray>
<g:RecordArray>IgYBZf8PbwJoAf8iAEAHAAAAAAAA</g:RecordArray>
<g:RecordArray>IgYBZf8PbwJoAf8iAEAHAAAAAAAA</g:RecordArray>
<g:ReturnValue>0</g:ReturnValue>
</g:GetRecords_OUTPUT>
</a:Body>
</a:Envelope>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://intel.com/wbem/wscim/1/amt-schema/1/AMT_MessageLog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>10</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://intel.com/wbem/wscim/1/amt-schema/1/AMT_MessageLog/PositionToFirstRecordResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000002723</b:MessageID>
<c:ResourceURI>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_MessageLog</c:ResourceURI>
</a:Header>
<a:Body>
<g:PositionToFirstRecord_OUTPUT>
<g:IterationIdentifier>1</g:IterationIdentifier>
<g:ReturnValue>0</g:ReturnValue>
</g:PositionToFirstRecord_OUTPUT>
</a:Body>
</a:Envelope>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://intel.com/wbem/wscim/1/amt-schema/1/AMT_RedirectionService"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>5</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-0000000028B5</b:MessageID>
<c:ResourceURI>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_RedirectionService</c:ResourceURI>
</a:Header>
<a:Body>
<g:AMT_RedirectionService>
<g:CreationClassName>AMT_RedirectionService</g:CreationClassName>
<g:ElementName>Intel(r) AMT Redirection Service</g:ElementName>
<g:EnabledState>32771</g:EnabledState>
<g:ListenerEnabled>true</g:ListenerEnabled>
<g:Name>Intel(r) AMT Redirection Service</g:Name>
<g:SystemCreationClassName>CIM_ComputerSystem</g:SystemCreationClassName>
<g:SystemName>Intel(r) AMT</g:SystemName>
</g:AMT_RedirectionService>
</a:Body>
</a:Envelope>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns="http://www.w3.org/2003/05/soap-envelope">
<Header>
<a:Action>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_SetupAndConfigurationService/GetUuid</a:Action>
<a:To>/wsman</a:To>
<w:ResourceURI>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_SetupAndConfigurationService</w:ResourceURI>
<a:MessageID>1</a:MessageID>
<a:ReplyTo>
<a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<w:OperationTimeout>PT60S</w:OperationTimeout>
</Header>
<Body>
<h:GetUuid_OUTPUT xmlns:h="http://intel.com/wbem/wscim/1/amt-schema/1/AMT_SetupAndConfigurationService"><UUID>E67jVdK/u2EXoIiu3XA36g==</UUID></h:GetUuid_OUTPUT>
</Body>
</Envelope>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://intel.com/wbem/wscim/1/amt-schema/1/AMT_SetupAndConfigurationService"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>0</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000332</b:MessageID>
<c:ResourceURI>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_SetupAndConfigurationService</c:ResourceURI>
</a:Header>
<a:Body>
<g:AMT_SetupAndConfigurationService>
<g:CreationClassName>AMT_SetupAndConfigurationService</g:CreationClassName>
<g:ElementName>Intel(r) AMT Setup and Configuration Service</g:ElementName>
<g:EnabledState>5</g:EnabledState>
<g:Name>Intel(r) AMT Setup and Configuration Service</g:Name>
<g:PasswordModel>1</g:PasswordModel>
<g:ProvisioningMode>1</g:ProvisioningMode>
<g:ProvisioningServerOTP>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</g:ProvisioningServerOTP>
<g:ProvisioningState>2</g:ProvisioningState>
<g:RequestedState>12</g:RequestedState>
<g:SystemCreationClassName>CIM_ComputerSystem</g:SystemCreationClassName>
<g:SystemName>Intel(r) AMT</g:SystemName>
<g:ZeroTouchConfigurationEnabled>true</g:ZeroTouchConfigurationEnabled>
</g:AMT_SetupAndConfigurationService>
</a:Body>
</a:Envelope>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TLSSettingData"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>8</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000002915</b:MessageID>
<c:ResourceURI>http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TLSSettingData</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:AMT_TLSSettingData>
<h:AcceptNonSecureConnections>false</h:AcceptNonSecureConnections>
<h:ElementName>Intel(r) AMT 802.3 TLS Settings</h:ElementName>
<h:Enabled>false</h:Enabled>
<h:InstanceID>Intel(r) AMT 802.3 TLS Settings</h:InstanceID>
<h:MutualAuthentication>false</h:MutualAuthentication>
</h:AMT_TLSSettingData>
<h:AMT_TLSSettingData>
<h:AcceptNonSecureConnections>true</h:AcceptNonSecureConnections>
<h:ElementName>Intel(r) AMT LMS TLS Settings</h:ElementName>
<h:Enabled>false</h:Enabled>
<h:InstanceID>Intel(r) AMT LMS TLS Settings</h:InstanceID>
<h:MutualAuthentication>false</h:MutualAuthentication>
</h:AMT_TLSSettingData>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope" xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust" xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_AssociatedPowerManagementService" xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000007A95</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_AssociatedPowerManagementService</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_AssociatedPowerManagementService>
<h:AvailableRequestedPowerStates>10</h:AvailableRequestedPowerStates>
<h:AvailableRequestedPowerStates>8</h:AvailableRequestedPowerStates>
<h:AvailableRequestedPowerStates>5</h:AvailableRequestedPowerStates>
<h:AvailableRequestedPowerStates>11</h:AvailableRequestedPowerStates>
<h:AvailableRequestedPowerStates>4</h:AvailableRequestedPowerStates>
<h:AvailableRequestedPowerStates>7</h:AvailableRequestedPowerStates>
<h:AvailableRequestedPowerStates>14</h:AvailableRequestedPowerStates>
<h:AvailableRequestedPowerStates>12</h:AvailableRequestedPowerStates>
<h:PowerState>2</h:PowerState>
<h:ServiceProvided>
<b:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:Address>
<b:ReferenceParameters>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService</c:ResourceURI>
<c:SelectorSet>
<c:Selector Name="CreationClassName">CIM_PowerManagementService</c:Selector>
<c:Selector Name="Name">Intel(r) AMT Power Management Service</c:Selector>
<c:Selector Name="SystemCreationClassName">CIM_ComputerSystem</c:Selector>
<c:Selector Name="SystemName">Intel(r) AMT</c:Selector>
</c:SelectorSet>
</b:ReferenceParameters>
</h:ServiceProvided>
<h:UserOfService>
<b:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:Address>
<b:ReferenceParameters>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem</c:ResourceURI>
<c:SelectorSet>
<c:Selector Name="CreationClassName">CIM_ComputerSystem</c:Selector>
<c:Selector Name="Name">ManagedSystem</c:Selector>
</c:SelectorSet>
</b:ReferenceParameters>
</h:UserOfService>
</h:CIM_AssociatedPowerManagementService>
</g:Items>
<g:EndOfSequence/>
</g:PullResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BIOSElement"
xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000E39</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BIOSElement</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_BIOSElement>
<h:ElementName>Primary BIOS</h:ElementName>
<h:Manufacturer>Intel Corp.</h:Manufacturer>
<h:Name>Primary BIOS</h:Name>
<h:OperationalStatus>0</h:OperationalStatus>
<h:PrimaryBIOS>true</h:PrimaryBIOS>
<h:ReleaseDate>
<i:Datetime>2020-08-10T00:00:00Z</i:Datetime>
</h:ReleaseDate>
<h:SoftwareElementID>QNCFLX70.0054.2020.0810.2227</h:SoftwareElementID>
<h:SoftwareElementState>2</h:SoftwareElementState>
<h:TargetOperatingSystem>66</h:TargetOperatingSystem>
<h:Version>QNCFLX70.0054.2020.0810.2227</h:Version>
</h:CIM_BIOSElement>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Card"
xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000E77</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Card</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_Card>
<h:CanBeFRUed>true</h:CanBeFRUed>
<h:CreationClassName>CIM_Card</h:CreationClassName>
<h:ElementName>Managed System Base Board</h:ElementName>
<h:Manufacturer>Intel Corporation</h:Manufacturer>
<h:Model>NUC9V7QNB</h:Model>
<h:OperationalStatus>0</h:OperationalStatus>
<h:PackageType>9</h:PackageType>
<h:SerialNumber>KNQN0221020W</h:SerialNumber>
<h:Tag>CIM_Card</h:Tag>
<h:Version>K47180-402</h:Version>
</h:CIM_Card>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Chassis"
xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000E7B</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Chassis</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_Chassis>
<h:ChassisPackageType>0</h:ChassisPackageType>
<h:CreationClassName>CIM_Chassis</h:CreationClassName>
<h:ElementName>Managed System Chassis</h:ElementName>
<h:Manufacturer>Intel(R) Client Systems</h:Manufacturer>
<h:Model>NUC9V7QNX</h:Model>
<h:OperationalStatus>0</h:OperationalStatus>
<h:PackageType>3</h:PackageType>
<h:SerialNumber>JRQN0243007J</h:SerialNumber>
<h:Tag>CIM_Chassis</h:Tag>
<h:Version>K47174-402</h:Version>
</h:CIM_Chassis>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Chip"
xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000E7F</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Chip</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:EnumerationContext>D4020000-0000-0000-0000-000000000000</g:EnumerationContext>
<g:Items>
<h:CIM_Chip>
<h:CanBeFRUed>true</h:CanBeFRUed>
<h:CreationClassName>CIM_Chip</h:CreationClassName>
<h:ElementName>Managed System Processor Chip</h:ElementName>
<h:Manufacturer>Intel(R) Corporation</h:Manufacturer>
<h:OperationalStatus>0</h:OperationalStatus>
<h:Tag>CPU 0</h:Tag>
<h:Version>Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz</h:Version>
</h:CIM_Chip>
</g:Items>
</g:PullResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystemPackage"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-0000000000BF</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystemPackage</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_ComputerSystemPackage>
<h:Antecedent>
<b:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:Address>
<b:ReferenceParameters>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Chassis</c:ResourceURI>
<c:SelectorSet>
<c:Selector Name="CreationClassName">CIM_Chassis</c:Selector>
<c:Selector Name="Tag">CIM_Chassis</c:Selector>
</c:SelectorSet>
</b:ReferenceParameters>
</h:Antecedent>
<h:Dependent>
<b:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:Address>
<b:ReferenceParameters>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem</c:ResourceURI>
<c:SelectorSet>
<c:Selector Name="CreationClassName">CIM_ComputerSystem</c:Selector>
<c:Selector Name="Name">ManagedSystem</c:Selector>
</c:SelectorSet>
</b:ReferenceParameters>
</h:Dependent>
<h:PlatformGUID>13AEE355D2BFBB6117A088AEDD7037EA</h:PlatformGUID>
</h:CIM_ComputerSystemPackage>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_KVMRedirectionSAP"
xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000E6F</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_KVMRedirectionSAP</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_KVMRedirectionSAP>
<h:CreationClassName>CIM_KVMRedirectionSAP</h:CreationClassName>
<h:ElementName>KVM Redirection Service Access Point</h:ElementName>
<h:EnabledState>6</h:EnabledState>
<h:KVMProtocol>4</h:KVMProtocol>
<h:Name>KVM Redirection Service Access Point</h:Name>
<h:RequestedState>5</h:RequestedState>
<h:SystemCreationClassName>CIM_ComputerSystem</h:SystemCreationClassName>
<h:SystemName>ManagedSystem</h:SystemName>
</h:CIM_KVMRedirectionSAP>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_MediaAccessDevice"
xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000E72</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_MediaAccessDevice</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_MediaAccessDevice>
<h:Capabilities>4</h:Capabilities>
<h:CreationClassName>CIM_MediaAccessDevice</h:CreationClassName>
<h:DeviceID>MEDIA DEV 0</h:DeviceID>
<h:ElementName>Managed System Media Access Device</h:ElementName>
<h:EnabledDefault>2</h:EnabledDefault>
<h:EnabledState>0</h:EnabledState>
<h:MaxMediaSize>960197124</h:MaxMediaSize>
<h:OperationalStatus>0</h:OperationalStatus>
<h:RequestedState>12</h:RequestedState>
<h:Security>2</h:Security>
<h:SystemCreationClassName>CIM_ComputerSystem</h:SystemCreationClassName>
<h:SystemName>ManagedSystem</h:SystemName>
</h:CIM_MediaAccessDevice>
<h:CIM_MediaAccessDevice>
<h:Capabilities>4</h:Capabilities>
<h:CreationClassName>CIM_MediaAccessDevice</h:CreationClassName>
<h:DeviceID>MEDIA DEV 1</h:DeviceID>
<h:ElementName>Managed System Media Access Device</h:ElementName>
<h:EnabledDefault>2</h:EnabledDefault>
<h:EnabledState>0</h:EnabledState>
<h:MaxMediaSize>500107862</h:MaxMediaSize>
<h:OperationalStatus>0</h:OperationalStatus>
<h:RequestedState>12</h:RequestedState>
<h:Security>2</h:Security>
<h:SystemCreationClassName>CIM_ComputerSystem</h:SystemCreationClassName>
<h:SystemName>ManagedSystem</h:SystemName>
</h:CIM_MediaAccessDevice>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PhysicalMemory"
xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000E82</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PhysicalMemory</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_PhysicalMemory>
<h:BankLabel>BANK 0</h:BankLabel>
<h:Capacity>17179869184</h:Capacity>
<h:ConfiguredMemoryClockSpeed>2400</h:ConfiguredMemoryClockSpeed>
<h:CreationClassName>CIM_PhysicalMemory</h:CreationClassName>
<h:ElementName>Managed System Memory Chip</h:ElementName>
<h:FormFactor>13</h:FormFactor>
<h:IsSpeedInMhz>true</h:IsSpeedInMhz>
<h:Manufacturer>859B</h:Manufacturer>
<h:MaxMemorySpeed>2400</h:MaxMemorySpeed>
<h:MemoryType>26</h:MemoryType>
<h:PartNumber>CT16G4SFD824A.M16FE </h:PartNumber>
<h:SerialNumber>E0E8D670</h:SerialNumber>
<h:Speed>0</h:Speed>
<h:Tag>9876543210</h:Tag>
</h:CIM_PhysicalMemory>
<h:CIM_PhysicalMemory>
<h:BankLabel>BANK 2</h:BankLabel>
<h:Capacity>17179869184</h:Capacity>
<h:ConfiguredMemoryClockSpeed>2400</h:ConfiguredMemoryClockSpeed>
<h:CreationClassName>CIM_PhysicalMemory</h:CreationClassName>
<h:ElementName>Managed System Memory Chip</h:ElementName>
<h:FormFactor>13</h:FormFactor>
<h:IsSpeedInMhz>true</h:IsSpeedInMhz>
<h:Manufacturer>859B</h:Manufacturer>
<h:MaxMemorySpeed>2400</h:MaxMemorySpeed>
<h:MemoryType>26</h:MemoryType>
<h:PartNumber>CT16G4SFD824A.M16FE </h:PartNumber>
<h:SerialNumber>E0E8D070</h:SerialNumber>
<h:Speed>0</h:Speed>
<h:Tag>9876543210 (#1)</h:Tag>
</h:CIM_PhysicalMemory>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>0</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService/RequestPowerStateChangeResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-0000000003B8</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService</c:ResourceURI>
</a:Header>
<a:Body>
<g:RequestPowerStateChange_OUTPUT>
<g:ReturnValue>0</g:ReturnValue>
</g:RequestPowerStateChange_OUTPUT>
</a:Body>
</a:Envelope>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Processor"
xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000E8B</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Processor</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_Processor>
<h:CPUStatus>1</h:CPUStatus>
<h:CreationClassName>CIM_Processor</h:CreationClassName>
<h:CurrentClockSpeed>2400</h:CurrentClockSpeed>
<h:DeviceID>CPU 0</h:DeviceID>
<h:ElementName>Managed System CPU</h:ElementName>
<h:EnabledState>2</h:EnabledState>
<h:ExternalBusClockSpeed>100</h:ExternalBusClockSpeed>
<h:Family>198</h:Family>
<h:HealthState>0</h:HealthState>
<h:MaxClockSpeed>8300</h:MaxClockSpeed>
<h:OperationalStatus>0</h:OperationalStatus>
<h:OtherFamilyDescription></h:OtherFamilyDescription>
<h:RequestedState>12</h:RequestedState>
<h:Role>Central</h:Role>
<h:Stepping>13</h:Stepping>
<h:SystemCreationClassName>CIM_ComputerSystem</h:SystemCreationClassName>
<h:SystemName>ManagedSystem</h:SystemName>
<h:UpgradeMethod>52</h:UpgradeMethod>
</h:CIM_Processor>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:h="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_SoftwareIdentity"
xmlns:i="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000E9A</b:MessageID>
<c:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_SoftwareIdentity</c:ResourceURI>
</a:Header>
<a:Body>
<g:PullResponse>
<g:Items>
<h:CIM_SoftwareIdentity>
<h:InstanceID>Flash</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>12.0.67</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>Netstack</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>12.0.67</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>AMTApps</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>12.0.67</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>AMT</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>12.0.67</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>Sku</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>16392</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>VendorID</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>8086</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>Build Number</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>1579</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>Recovery Version</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>12.0.67</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>Recovery Build Num</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>1579</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>Legacy Mode</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>False</h:VersionString>
</h:CIM_SoftwareIdentity>
<h:CIM_SoftwareIdentity>
<h:InstanceID>AMT FW Core Version</h:InstanceID>
<h:IsEntity>true</h:IsEntity>
<h:VersionString>12.0.67</h:VersionString>
</h:CIM_SoftwareIdentity>
</g:Items>
<g:EndOfSequence></g:EndOfSequence>
</g:PullResponse>
</a:Body>
</a:Envelope>
+30
View File
@@ -0,0 +1,30 @@
# Intel AMT 12.0.67 (build 1579) on an Intel NUC9 (NUC9V7QNB, i7-9850H), as published in
# Intel's go-wsman-messages (Apache-2.0), pkg/wsman/wsmantesting/responses, commit 00402b9.
# Some values there were replaced by placeholders (AMT_GeneralSettings: "Test Host Name").
#
# file request
AMT_GeneralSettings.get.xml Get AMT_GeneralSettings
AMT_GeneralSettings.enumerate.xml Enumerate AMT_GeneralSettings (shape of every EnumerateResponse)
AMT_EthernetPortSettings.pull.xml Enumerate + Pull AMT_EthernetPortSettings
AMT_SetupAndConfigurationService.get.xml Get AMT_SetupAndConfigurationService
AMT_SetupAndConfigurationService.GetUuid.xml Invoke AMT_SetupAndConfigurationService.GetUuid
AMT_TLSSettingData.pull.xml Enumerate + Pull AMT_TLSSettingData
AMT_RedirectionService.get.xml Get AMT_RedirectionService
AMT_MessageLog.PositionToFirstRecord.xml Invoke AMT_MessageLog.PositionToFirstRecord
AMT_MessageLog.GetRecords.xml Invoke AMT_MessageLog.GetRecords
IPS_KVMRedirectionSettingData.get.xml Get IPS_KVMRedirectionSettingData
CIM_KVMRedirectionSAP.pull.xml Enumerate + Pull CIM_KVMRedirectionSAP
CIM_SoftwareIdentity.pull.xml Enumerate + Pull CIM_SoftwareIdentity
CIM_BIOSElement.pull.xml Enumerate + Pull CIM_BIOSElement
CIM_Chassis.pull.xml Enumerate + Pull CIM_Chassis
CIM_Card.pull.xml Enumerate + Pull CIM_Card
CIM_ComputerSystemPackage.pull.xml Enumerate + Pull CIM_ComputerSystemPackage
CIM_Processor.pull.xml Enumerate + Pull CIM_Processor
CIM_Chip.pull.xml Enumerate + Pull CIM_Chip (first page: a context, no EndOfSequence)
CIM_PhysicalMemory.pull.xml Enumerate + Pull CIM_PhysicalMemory
CIM_MediaAccessDevice.pull.xml Enumerate + Pull CIM_MediaAccessDevice
CIM_AssociatedPowerManagementService.pull.xml Enumerate + Pull CIM_AssociatedPowerManagementService
CIM_PowerManagementService.RequestPowerStateChange.xml Invoke CIM_PowerManagementService.RequestPowerStateChange
#
# Written by hand after DSP0226 (WS-Management) section 14, not recorded:
fault_destination_unreachable.xml the fault for a resource URI the service does not know
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope"
xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:c="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/02/trust"
xmlns:e="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:f="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"
xmlns:g="http://intel.com/wbem/wscim/1/ips-schema/1/IPS_KVMRedirectionSettingData"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>0</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-00000000028D</b:MessageID>
<c:ResourceURI>http://intel.com/wbem/wscim/1/ips-schema/1/IPS_KVMRedirectionSettingData</c:ResourceURI>
</a:Header>
<a:Body>
<g:IPS_KVMRedirectionSettingData>
<g:BackToBackFbMode>false</g:BackToBackFbMode>
<g:DefaultScreen>0</g:DefaultScreen>
<g:ElementName>Intel(r) KVM Redirection Settings</g:ElementName>
<g:EnabledByMEBx>true</g:EnabledByMEBx>
<g:InstanceID>Intel(r) KVM Redirection Settings</g:InstanceID>
<g:Is5900PortEnabled>false</g:Is5900PortEnabled>
<g:OptInPolicy>true</g:OptInPolicy>
<g:OptInPolicyTimeout>300</g:OptInPolicyTimeout>
<g:RFBPassword></g:RFBPassword>
<g:SessionTimeout>3</g:SessionTimeout>
</g:IPS_KVMRedirectionSettingData>
</a:Body>
</a:Envelope>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<a:Envelope xmlns:a="http://www.w3.org/2003/05/soap-envelope" xmlns:b="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:g="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
<a:Header>
<b:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</b:To>
<b:RelatesTo>1</b:RelatesTo>
<b:Action a:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/fault</b:Action>
<b:MessageID>uuid:00000000-8086-8086-8086-000000000001</b:MessageID>
</a:Header>
<a:Body>
<a:Fault>
<a:Code>
<a:Value>a:Sender</a:Value>
<a:Subcode>
<a:Value>b:DestinationUnreachable</a:Value>
</a:Subcode>
</a:Code>
<a:Reason>
<a:Text xml:lang="en-US">No route can be determined to reach the destination role defined by the WS-Addressing To.</a:Text>
</a:Reason>
<a:Detail>
<g:FaultDetail>http://schemas.dmtf.org/wbem/wsman/1/wsman/faultDetail/InvalidResourceURI</g:FaultDetail>
</a:Detail>
</a:Fault>
</a:Body>
</a:Envelope>
View File
Whitespace-only changes.
+100
View File
@@ -0,0 +1,100 @@
"""CPUs, DIMMs and drives of the managed machine, from the recorded NUC9."""
import pytest
from napalm_amt.parsers import parse_cpus, parse_drives, parse_memory
from ..conftest import instances
def test_cpus_take_the_model_from_the_chip():
cpus = parse_cpus(instances("CIM_Processor.pull.xml"), instances("CIM_Chip.pull.xml"))
assert cpus == [
{
"socket": "CPU 0",
"model": "Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz",
"cores": None,
"threads": None,
"status": "OK",
}
]
@pytest.mark.parametrize(
("cpu_status", "status"),
[("1", "OK"), ("4", "OK"), ("2", "Warning"), ("3", "Critical"), ("0", ""), ("", "")],
)
def test_cpu_status_follows_cpustatus(cpu_status, status):
(cpu,) = parse_cpus([{"DeviceID": "CPU 0", "CPUStatus": cpu_status}], [])
assert cpu["status"] == status
assert cpu["model"] == ""
def test_memory():
dimms = parse_memory(instances("CIM_PhysicalMemory.pull.xml"))
assert dimms[0] == {
"slot": "BANK 0",
"size_bytes": 17179869184,
"type": "DDR4",
"speed_mts": 2400,
"part_number": "CT16G4SFD824A.M16FE",
"serial_number": "E0E8D670",
"status": "",
}
assert [d["slot"] for d in dimms] == ["BANK 0", "BANK 2"]
@pytest.mark.parametrize(
("code", "name"),
[("24", "DDR3"), ("26", "DDR4"), ("29", "LPDDR3"), ("34", "DDR5"), ("2", ""), ("", "")],
)
def test_memory_type_follows_smbios(code, name):
(dimm,) = parse_memory([{"BankLabel": "BANK 0", "MemoryType": code}])
assert dimm["type"] == name
def test_memory_without_a_configured_speed_falls_back_to_the_maximum():
(dimm,) = parse_memory(
[{"BankLabel": "A", "ConfiguredMemoryClockSpeed": "0", "MaxMemorySpeed": "2133"}]
)
assert dimm["speed_mts"] == 2133
def test_memory_slot_falls_back_to_the_tag():
(dimm,) = parse_memory([{"Tag": "ChannelA-DIMM0", "Capacity": "8589934592"}])
assert dimm["slot"] == "ChannelA-DIMM0"
assert dimm["size_bytes"] == 8589934592
def test_drives_size_is_in_kilobytes_of_1000():
drives = parse_drives(instances("CIM_MediaAccessDevice.pull.xml"))
# 500107862 KB is the classic 500 GB disk (976773168 sectors of 512 bytes).
assert drives == [
{
"slot": "MEDIA DEV 0",
"model": "",
"serial_number": "",
"size_bytes": 960197124000,
"media": "",
"protocol": "",
"status": "",
},
{
"slot": "MEDIA DEV 1",
"model": "",
"serial_number": "",
"size_bytes": 500107862000,
"media": "",
"protocol": "",
"status": "",
},
]
@pytest.mark.parametrize(
("operational", "status"), [("2", "OK"), ("3", "Warning"), ("6", "Critical"), ("0", "")]
)
def test_drive_status_follows_operational_status(operational, status):
(drive,) = parse_drives([{"DeviceID": "MEDIA DEV 0", "OperationalStatus": operational}])
assert drive["status"] == status
assert drive["size_bytes"] is None
+75
View File
@@ -0,0 +1,75 @@
"""AMT's hardware event log (AMT_MessageLog records), from the recorded NUC9."""
import base64
import pytest
from napalm_amt.parsers import parse_event_record
from ..conftest import instances
def _records():
(out,) = instances("AMT_MessageLog.GetRecords.xml")
return out["RecordArray"]
def test_failed_logins_are_critical():
entry = parse_event_record(_records()[0], "1")
assert entry == {
"id": "1",
"timestamp": 1704511587.0,
"severity": "Critical",
"message": "Intel(r) ME: Authentication failed 10 times. The system may be under attack.",
}
def test_bios_progress_is_ok():
entry = parse_event_record(_records()[1], "2")
assert entry["severity"] == "OK"
assert entry["message"] == "BIOS: PCI resource configuration"
assert entry["timestamp"] == 1694565922.0
def _record(sensor_type, offset, severity, entity, data=(0,) * 8, timestamp=1700000000):
raw = (
timestamp.to_bytes(4, "little")
+ bytes([0xFF, sensor_type, 0x6F, offset, 0x68, severity, 0xFF, entity, 0])
+ bytes(data)
)
return base64.b64encode(raw).decode()
@pytest.mark.parametrize(
("severity", "expected"),
[
(0, "OK"),
(1, "OK"),
(2, "OK"),
(4, "OK"),
(8, "Warning"),
(16, "Critical"),
(32, "Critical"),
],
)
def test_severity(severity, expected):
assert parse_event_record(_record(37, 0, severity, 34), "1")["severity"] == expected
def test_firmware_error():
entry = parse_event_record(_record(15, 0, 16, 34, (0, 1, 0, 0, 0, 0, 0, 0)), "1")
assert entry["message"] == ("BIOS: No system memory is physically installed in the system.")
def test_unknown_sensor_type_is_named():
entry = parse_event_record(_record(200, 0, 2, 2), "1")
assert entry["message"] == "Unknown: sensor type 200"
def test_no_timestamp_is_none():
assert parse_event_record(_record(30, 0, 16, 34, timestamp=0), "1")["timestamp"] is None
def test_a_short_or_broken_record_is_skipped():
assert parse_event_record("AAAA", "1") is None
assert parse_event_record("not base64!", "1") is None
+63
View File
@@ -0,0 +1,63 @@
"""AMT's own network ports (AMT_EthernetPortSettings), from the recorded NUC9."""
from napalm_amt.parsers import parse_ethernet_ports
from ..conftest import instances
def test_the_wired_port_is_kept_and_the_unused_wireless_one_dropped():
# The NUC9 lists a wireless port without a MAC address: there is no WLAN card.
ports = parse_ethernet_ports(instances("AMT_EthernetPortSettings.pull.xml"))
assert ports == [
{
"name": "wired",
"mac_address": "00:be:43:d8:22:a4",
"link_up": True,
"dhcp": True,
"shared_ip": True,
"ip": "",
"prefix_length": None,
}
]
def test_a_static_address_with_its_mask():
(port,) = parse_ethernet_ports(
[
{
"InstanceID": "Intel(r) AMT Ethernet Port Settings 0",
"MACAddress": "6C-4B-90-12-34-56",
"LinkIsUp": "true",
"DHCPEnabled": "false",
"SharedStaticIp": "false",
"IPAddress": "172.22.8.61",
"SubnetMask": "255.255.255.0",
"PhysicalConnectionType": "0",
}
]
)
assert port["mac_address"] == "6c:4b:90:12:34:56"
assert (port["ip"], port["prefix_length"]) == ("172.22.8.61", 24)
assert port["dhcp"] is False
assert port["shared_ip"] is False
def test_a_wireless_port_with_a_card_is_named_wireless():
ports = parse_ethernet_ports(
[
{
"InstanceID": "Intel(r) AMT Ethernet Port Settings 1",
"MACAddress": "a0-88-69-01-02-03",
"PhysicalConnectionType": "3",
"LinkIsUp": "false",
},
]
)
assert [(p["name"], p["link_up"]) for p in ports] == [("wireless", False)]
def test_an_invalid_mask_gives_no_prefix():
(port,) = parse_ethernet_ports(
[{"MACAddress": "00-be-43-d8-22-a4", "IPAddress": "10.0.0.5", "SubnetMask": "255.0.255.0"}]
)
assert port["prefix_length"] is None
+73
View File
@@ -0,0 +1,73 @@
"""The machine's power state and what AMT accepts, from the recorded NUC9."""
import pytest
from napalm_amt.parsers import POWER_REQUESTS, accepted_actions, parse_power_state
from ..conftest import instances
def test_power_state_of_a_running_machine():
(service,) = instances("CIM_AssociatedPowerManagementService.pull.xml")
assert parse_power_state(service) == {
"state": "On",
"available": [10, 8, 5, 11, 4, 7, 14, 12],
}
@pytest.mark.parametrize(
("code", "state"),
[
("2", "On"),
("8", "Off"),
("6", "Off"),
("13", "Off"),
# Asleep or hibernating: the OS is not running, On wakes it.
("3", "Off"),
("4", "Off"),
("7", "Off"),
("99", ""),
("", ""),
],
)
def test_power_state_codes(code, state):
assert parse_power_state({"PowerState": code})["state"] == state
def test_a_single_available_state_is_a_list():
assert parse_power_state({"PowerState": "8", "AvailableRequestedPowerStates": "2"})[
"available"
] == [2]
def test_running_machine_accepts_what_amt_offers_except_on():
assert accepted_actions("On", [10, 8, 5, 11, 4, 7, 14, 12]) == [
"GracefulShutdown",
"ForceOff",
"GracefulRestart",
"ForceRestart",
"PowerCycle",
]
def test_without_graceful_states_amt_offers_no_graceful_actions():
# AMT lists the graceful states only where the host can honour them.
assert accepted_actions("On", [10, 8, 5]) == ["ForceOff", "ForceRestart", "PowerCycle"]
def test_machine_that_is_off_accepts_on_only():
assert accepted_actions("Off", [2]) == ["On"]
assert accepted_actions("Off", []) == ["On"]
def test_unknown_state_accepts_nothing():
assert accepted_actions("", [2, 8, 10]) == []
def test_power_requests_cover_every_role_action():
from napalm_device_types.models import POWER_ACTIONS
assert set(POWER_REQUESTS) == set(POWER_ACTIONS)
assert POWER_REQUESTS["On"] == 2
assert POWER_REQUESTS["ForceOff"] == 8
assert POWER_REQUESTS["ForceRestart"] == 10
+97
View File
@@ -0,0 +1,97 @@
"""What AMT listens on: plain HTTP or TLS, the KVM console and IDE/USB redirection."""
import pytest
from napalm_amt.parsers import parse_management_protocols
from ..conftest import instances
def _protocols(**overrides):
args = {
"tls_settings": instances("AMT_TLSSettingData.pull.xml"),
"redirection": instances("AMT_RedirectionService.get.xml")[0],
"kvm_settings": instances("IPS_KVMRedirectionSettingData.get.xml")[0],
"kvm_saps": instances("CIM_KVMRedirectionSAP.pull.xml"),
}
args.update(overrides)
return parse_management_protocols(**args)
def test_amt_without_tls_listens_on_plain_http():
assert _protocols() == {
"ipmi": {"enabled": False, "port": 623},
"ssh": {"enabled": False, "port": 22},
"http": {"enabled": True, "port": 16992},
"https": {"enabled": False, "port": 16993},
"snmp": {"enabled": False, "port": 161},
# KVM is enabled (MEBx), offline until someone connects; over the redirection
# port, as the standard VNC port 5900 is off.
"kvm": {"enabled": True, "port": 16994},
# IDE-R/USB-R and SOL are both on (EnabledState 32771).
"virtual_media": {"enabled": True, "port": 16994},
}
def test_amt_with_tls_listens_on_16993_and_redirects_over_tls():
tls = [
{
"InstanceID": "Intel(r) AMT 802.3 TLS Settings",
"Enabled": "true",
"AcceptNonSecureConnections": "false",
}
]
protocols = _protocols(tls_settings=tls)
assert protocols["https"] == {"enabled": True, "port": 16993}
assert protocols["http"] == {"enabled": False, "port": 16992}
assert protocols["kvm"]["port"] == 16995
assert protocols["virtual_media"]["port"] == 16995
def test_tls_that_still_accepts_plain_connections_keeps_http_open():
tls = [
{
"InstanceID": "Intel(r) AMT 802.3 TLS Settings",
"Enabled": "true",
"AcceptNonSecureConnections": "true",
}
]
assert _protocols(tls_settings=tls)["http"]["enabled"] is True
def test_the_lms_tls_setting_is_local_and_ignored():
tls = [{"InstanceID": "Intel(r) AMT LMS TLS Settings", "Enabled": "true"}]
assert _protocols(tls_settings=tls)["https"]["enabled"] is False
def test_kvm_on_the_standard_vnc_port():
kvm = {**instances("IPS_KVMRedirectionSettingData.get.xml")[0], "Is5900PortEnabled": "true"}
assert _protocols(kvm_settings=kvm)["kvm"] == {"enabled": True, "port": 5900}
@pytest.mark.parametrize("sap_state", ["3", "4"])
def test_kvm_disabled(sap_state):
saps = [{"EnabledState": sap_state}]
assert _protocols(kvm_saps=saps)["kvm"]["enabled"] is False
def test_kvm_disabled_in_mebx():
kvm = {**instances("IPS_KVMRedirectionSettingData.get.xml")[0], "EnabledByMEBx": "false"}
assert _protocols(kvm_settings=kvm)["kvm"]["enabled"] is False
def test_kvm_unknown_without_any_data():
assert _protocols(kvm_settings={}, kvm_saps=[])["kvm"]["enabled"] is False
@pytest.mark.parametrize(
("state", "enabled"),
[("32768", False), ("32769", True), ("32770", False), ("32771", True), ("", False)],
)
def test_virtual_media_is_ide_redirection(state, enabled):
assert _protocols(redirection={"EnabledState": state})["virtual_media"]["enabled"] is enabled
def test_redirection_listener_off_closes_virtual_media():
redirection = {"EnabledState": "32771", "ListenerEnabled": "false"}
assert _protocols(redirection=redirection)["virtual_media"]["enabled"] is False
+98
View File
@@ -0,0 +1,98 @@
"""The machine's identity and AMT's firmware, from the recorded NUC9 (AMT 12.0.67)."""
import pytest
from napalm_amt.parsers import (
parse_firmware,
parse_platform_guid,
parse_software_identity,
parse_system_identity,
uuid_from_base64,
)
from ..conftest import instances
def test_software_identity_gives_the_amt_version_with_its_build():
identity = parse_software_identity(instances("CIM_SoftwareIdentity.pull.xml"))
assert identity["amt"] == "12.0.67.1579"
assert identity["flash"] == "12.0.67"
def test_software_identity_without_build_keeps_the_version():
identity = parse_software_identity([{"InstanceID": "AMT", "VersionString": "11.8.50"}])
assert identity["amt"] == "11.8.50"
def test_software_identity_of_nothing_is_empty():
assert parse_software_identity([]) == {"amt": "", "flash": ""}
def test_platform_guid_is_the_smbios_uuid():
# AMT sends the SMBIOS UUID's bytes as stored; the first three groups are
# little-endian, as dmidecode and Intel's own console decode them.
(package,) = instances("CIM_ComputerSystemPackage.pull.xml")
assert parse_platform_guid(package["PlatformGUID"]) == "55e3ae13-bfd2-61bb-17a0-88aedd7037ea"
def test_get_uuid_and_platform_guid_agree():
(out,) = instances("AMT_SetupAndConfigurationService.GetUuid.xml")
assert uuid_from_base64(out["UUID"]) == "55e3ae13-bfd2-61bb-17a0-88aedd7037ea"
@pytest.mark.parametrize("guid", ["", "zz", "13AEE355", "0" * 32, "F" * 32])
def test_platform_guid_placeholders_and_garbage_are_empty(guid):
assert parse_platform_guid(guid) == ""
def test_system_identity_comes_from_the_chassis():
identity = parse_system_identity(
instances("CIM_Chassis.pull.xml"), instances("CIM_Card.pull.xml")
)
assert identity == {
"manufacturer": "Intel(R) Client Systems",
"model": "NUC9V7QNX",
"serial_number": "JRQN0243007J",
"board_model": "NUC9V7QNB",
"board_serial": "KNQN0221020W",
}
def test_system_identity_falls_back_to_the_board_for_placeholders():
chassis = [
{
"Manufacturer": "Default string",
"Model": "To Be Filled By O.E.M.",
"SerialNumber": "Default string",
}
]
card = [{"Manufacturer": "LENOVO", "Model": "3102", "SerialNumber": "L1HF8AB01XY"}]
identity = parse_system_identity(chassis, card)
assert identity["manufacturer"] == "LENOVO"
assert identity["model"] == "3102"
assert identity["serial_number"] == "L1HF8AB01XY"
def test_system_identity_of_nothing_is_empty():
assert parse_system_identity([], []) == {
"manufacturer": "",
"model": "",
"serial_number": "",
"board_model": "",
"board_serial": "",
}
def test_firmware_lists_amt_and_the_bios():
firmware = parse_firmware(
parse_software_identity(instances("CIM_SoftwareIdentity.pull.xml")),
instances("CIM_BIOSElement.pull.xml"),
)
assert firmware == [
{"name": "Intel AMT", "version": "12.0.67.1579", "updateable": None},
{"name": "BIOS", "version": "QNCFLX70.0054.2020.0810.2227", "updateable": None},
]
def test_firmware_skips_what_is_unknown():
assert parse_firmware({"amt": "", "flash": ""}, []) == []
+45
View File
@@ -0,0 +1,45 @@
"""What netOrk's discovery scores the driver on.
netOrk lowercases the page (title, Server header, body) and matches each rule's pattern as
a substring; a port that answers adds its PortSpec weight. AMT names itself in its
``Server`` header on every answer, also the 401 of a page that needs a login.
"""
from napalm_device_types.base import rules_max_weight
from napalm_amt import AmtDriver
def _score(text: str) -> float:
text = " ".join(text.lower().split())
return sum(rule.weight for rule in AmtDriver.HTTP_FINGERPRINT if rule.pattern in text)
def test_amt_ports_are_probed():
assert {(s.scheme, s.port) for s in AmtDriver.PORT_SPECS} == {("http", 16992), ("https", 16993)}
def test_server_header_is_recognised():
assert _score("Intel(R) Active Management Technology 11.8.50.3399") >= 10.0
def test_web_ui_landing_page_is_recognised():
page = (
"<html><head><title>Intel&reg; Active Management Technology</title></head>"
"<body>Log On</body></html>"
)
assert _score(page) >= 10.0
def test_other_pages_score_nothing():
assert _score("<title>Loading...</title> Web Switch nginx") == 0.0
def test_an_answering_amt_port_with_its_header_is_a_confident_match():
# One of the two ports answers (16992 without TLS, 16993 with); with the header the
# normalised score must reach netOrk's "high" (0.7).
port = max(s.weight for s in AmtDriver.PORT_SPECS)
total = sum(s.weight for s in AmtDriver.PORT_SPECS) + rules_max_weight(
AmtDriver.HTTP_FINGERPRINT
)
assert (port + _score("Intel(R) Active Management Technology 11.8")) / total >= 0.7
+327
View File
@@ -0,0 +1,327 @@
"""The driver end to end against the recorded NUC9 (AMT 12.0.67), served over mocked HTTP."""
import pytest
import requests
import responses
from napalm.base.exceptions import ConnectionException
from napalm_device_types import ManagementControllerDriver
from napalm_amt import AmtDriver
from .conftest import HOST, URL, load, patched
@pytest.fixture
def driver(amt):
d = AmtDriver(HOST, "admin", "P@ssw0rd")
d.open()
yield d
d.close()
def test_is_a_management_controller():
assert issubclass(AmtDriver, ManagementControllerDriver)
assert AmtDriver.ROLE == "management_controller"
assert AmtDriver.DRIVER_NAME == "amt"
assert AmtDriver.default_port == 16992
def test_does_not_offer_a_controller_reset():
# AMT cannot be restarted over WS-Management; the role method stays absent so
# netOrk does not offer it.
assert not hasattr(AmtDriver, "reboot_host")
def test_tls_follows_the_port():
assert AmtDriver(HOST, "u", "p").transport.url == f"http://{HOST}:16992/wsman"
tls = AmtDriver(HOST, "u", "p", optional_args={"port": 16993, "verify_ssl": False})
assert tls.transport.url == f"https://{HOST}:16993/wsman"
assert tls.transport.session.verify is False
# --- connection ------------------------------------------------------------------------
def test_open_reads_the_general_settings(amt):
d = AmtDriver(HOST, "admin", "P@ssw0rd")
d.open()
assert [r.resource for r in amt.requests] == ["AMT_GeneralSettings"]
assert d.is_alive() == {"is_alive": True}
def test_open_with_wrong_credentials_fails():
with responses.RequestsMock() as rsps:
rsps.add(
responses.POST,
URL,
status=401,
headers={"WWW-Authenticate": 'Digest realm="Digest:0", nonce="n", qop="auth"'},
)
with pytest.raises(ConnectionException, match="credentials"):
AmtDriver(HOST, "admin", "wrong", optional_args={"retries": 0}).open()
def test_open_without_an_answer_fails():
with responses.RequestsMock() as rsps:
rsps.add(responses.POST, URL, body=requests.ConnectionError("refused"))
with pytest.raises(ConnectionException):
AmtDriver(HOST, "admin", "pw", optional_args={"retries": 0}).open()
def test_is_alive_asks_amt(driver, amt):
amt.override("AMT_GeneralSettings", "Get", 500, "<html>gone</html>")
assert driver.is_alive() == {"is_alive": False}
def test_is_alive_before_open_is_false():
assert AmtDriver(HOST, "u", "p").is_alive() == {"is_alive": False}
# --- NAPALM getters --------------------------------------------------------------------
def test_facts(driver):
assert driver.get_facts() == {
"uptime": -1.0,
"vendor": "Intel(R) Client Systems",
"model": "NUC9V7QNX",
"hostname": "Test Host Name",
"fqdn": "Test Host Name.Test Domain Name",
"os_version": "12.0.67.1579",
"serial_number": "JRQN0243007J",
"interface_list": ["wired"],
}
def test_facts_without_an_amt_hostname_use_the_address(amt):
amt.override(
"AMT_GeneralSettings",
"Get",
200,
patched(
"AMT_GeneralSettings.get.xml",
"<g:HostName>Test Host Name</g:HostName>",
"<g:HostName></g:HostName>",
),
)
d = AmtDriver(HOST, "admin", "pw")
d.open()
facts = d.get_facts()
assert facts["hostname"] == HOST
assert facts["fqdn"] == HOST
def test_interfaces(driver):
assert driver.get_interfaces() == {
"wired": {
"is_up": True,
"is_enabled": True,
"description": "Intel AMT, wired, DHCP, shares the host's IP address",
"last_flapped": -1.0,
"speed": -1.0,
"mtu": -1,
"mac_address": "00:be:43:d8:22:a4",
}
}
def test_interfaces_ip_without_an_address_is_empty(driver):
# In DHCP mode the NUC9 reports no address of its own.
assert driver.get_interfaces_ip() == {}
def test_interfaces_ip_with_a_static_address(amt):
pull = patched(
"AMT_EthernetPortSettings.pull.xml",
"<h:DHCPEnabled>true</h:DHCPEnabled>",
"<h:DHCPEnabled>false</h:DHCPEnabled><h:IPAddress>172.22.8.61</h:IPAddress>",
1,
)
amt.override("AMT_EthernetPortSettings", "Pull", 200, pull)
d = AmtDriver(HOST, "admin", "pw")
d.open()
assert d.get_interfaces_ip() == {"wired": {"ipv4": {"172.22.8.61": {"prefix_length": 24}}}}
assert "static" in d.get_interfaces()["wired"]["description"]
def test_environment_is_empty(driver):
# AMT on a client platform reports no temperatures, fans or power supplies.
assert driver.get_environment() == {
"fans": {},
"temperature": {},
"power": {},
"cpu": {},
"memory": {},
}
# --- role methods ----------------------------------------------------------------------
def test_power_state(driver):
assert driver.get_power_state() == {
"state": "On",
"actions": [
"GracefulShutdown",
"ForceOff",
"GracefulRestart",
"ForceRestart",
"PowerCycle",
],
}
def test_set_power_state_asks_amt_for_the_cim_state(driver, amt):
driver.set_power_state("ForceOff")
(request,) = amt.calls("RequestPowerStateChange")
assert "<h:PowerState>8</h:PowerState>" in request.xml
assert '<Selector Name="Name">ManagedSystem</Selector>' in request.xml
def test_set_power_state_refuses_an_unknown_action(driver):
with pytest.raises(ValueError):
driver.set_power_state("Explode")
def test_set_power_state_refuses_on_for_a_running_machine(driver, amt):
with pytest.raises(RuntimeError, match="not accepted"):
driver.set_power_state("On")
assert amt.calls("RequestPowerStateChange") == []
def test_set_power_state_reports_amt_refusing(driver, amt):
amt.override(
"CIM_PowerManagementService",
"RequestPowerStateChange",
200,
patched(
"CIM_PowerManagementService.RequestPowerStateChange.xml",
"<g:ReturnValue>0</g:ReturnValue>",
"<g:ReturnValue>2</g:ReturnValue>",
),
)
with pytest.raises(RuntimeError, match="refused.*2"):
driver.set_power_state("ForceRestart")
def test_set_power_state_accepts_a_started_job(driver, amt):
amt.override(
"CIM_PowerManagementService",
"RequestPowerStateChange",
200,
patched(
"CIM_PowerManagementService.RequestPowerStateChange.xml",
"<g:ReturnValue>0</g:ReturnValue>",
"<g:ReturnValue>4096</g:ReturnValue>",
),
)
driver.set_power_state("PowerCycle")
def test_hardware_inventory(driver):
inventory = driver.get_hardware_inventory()
assert {k: v for k, v in inventory.items() if not isinstance(v, list)} == {
"manufacturer": "Intel(R) Client Systems",
"model": "NUC9V7QNX",
"serial_number": "JRQN0243007J",
"system_uuid": "55e3ae13-bfd2-61bb-17a0-88aedd7037ea",
}
assert [c["model"] for c in inventory["cpus"]] == ["Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz"]
assert len(inventory["memory"]) == 2
assert len(inventory["drives"]) == 2
assert inventory["power_supplies"] == []
assert inventory["nics"] == [
{"name": "wired", "mac_address": "00:be:43:d8:22:a4", "speed_mbps": None, "link_up": True}
]
def test_hardware_inventory_survives_a_class_amt_lacks(driver, amt):
# Older AMT versions know fewer CIM classes; a missing one leaves its part empty.
amt.override(
"CIM_MediaAccessDevice", "Enumerate", 400, load("fault_destination_unreachable.xml")
)
inventory = driver.get_hardware_inventory()
assert inventory["drives"] == []
assert len(inventory["memory"]) == 2
def test_event_log_newest_first(driver, amt):
log = driver.get_event_log()
assert [e["id"] for e in log] == ["1", "2", "3"]
assert log[0]["severity"] == "Critical"
assert log[0]["timestamp"] > log[1]["timestamp"]
(position,) = amt.calls("PositionToFirstRecord")
(records,) = amt.calls("GetRecords")
assert "<h:IterationIdentifier>1</h:IterationIdentifier>" in records.xml
def test_event_log_limit(driver):
assert len(driver.get_event_log(limit=1)) == 1
def test_event_log_empty(driver, amt):
amt.override(
"AMT_MessageLog",
"PositionToFirstRecord",
200,
patched(
"AMT_MessageLog.PositionToFirstRecord.xml",
"<g:ReturnValue>0</g:ReturnValue>",
"<g:ReturnValue>2</g:ReturnValue>",
),
)
assert driver.get_event_log() == []
assert amt.calls("GetRecords") == []
def test_event_log_reads_on_until_no_more_records(driver, amt):
first_page = patched(
"AMT_MessageLog.GetRecords.xml",
"<g:NoMoreRecords>true</g:NoMoreRecords>",
"<g:NoMoreRecords>false</g:NoMoreRecords>",
)
pages = iter([first_page, load("AMT_MessageLog.GetRecords.xml")])
original = amt._answer
def answer(req):
if req.operation == "GetRecords":
return 200, next(pages)
return original(req)
amt._answer = answer
log = driver.get_event_log()
assert len(log) == 6
second = amt.calls("GetRecords")[1]
assert "<h:IterationIdentifier>3</h:IterationIdentifier>" in second.xml
def test_firmware_inventory(driver):
assert driver.get_firmware_inventory() == [
{"name": "Intel AMT", "version": "12.0.67.1579", "updateable": None},
{"name": "BIOS", "version": "QNCFLX70.0054.2020.0810.2227", "updateable": None},
]
def test_management_protocols(driver):
protocols = driver.get_management_protocols()
assert protocols["http"] == {"enabled": True, "port": 16992}
assert protocols["https"] == {"enabled": False, "port": 16993}
assert protocols["kvm"] == {"enabled": True, "port": 16994}
def test_reads_are_cached_per_connection(driver, amt):
driver.get_facts()
driver.get_hardware_inventory()
before = len(amt.requests)
driver.get_facts()
assert len(amt.requests) == before
driver.close()
driver.open()
driver.get_facts()
assert len(amt.requests) > before
def test_getters_before_open_raise():
with pytest.raises(ConnectionException):
AmtDriver(HOST, "u", "p").get_facts()
+118
View File
@@ -0,0 +1,118 @@
"""Recording fixtures from a real AMT: every read the driver makes, nothing that changes it."""
import pytest
import responses
from napalm_amt.record import READS, main, record
from napalm_amt.wsman import WsmanTransport
from .conftest import DMT, HOST, AmtServer
def test_records_every_read_under_the_fixture_names(amt, tmp_path):
record(WsmanTransport(HOST, "admin", "pw"), tmp_path)
names = sorted(p.name for p in tmp_path.iterdir())
assert "AMT_GeneralSettings.get.xml" in names
assert "CIM_PhysicalMemory.pull.xml" in names
assert "AMT_SetupAndConfigurationService.GetUuid.xml" in names
assert "AMT_MessageLog.GetRecords.xml" in names
assert "COMMANDS.txt" in names
assert (tmp_path / "CIM_PhysicalMemory.pull.xml").read_text() == (
DMT / "CIM_PhysicalMemory.pull.xml"
).read_text()
def test_a_second_page_gets_its_own_file(amt, tmp_path):
record(WsmanTransport(HOST, "admin", "pw"), tmp_path)
# CIM_Chip answers in two pages (the second is the mock's empty last page).
assert (tmp_path / "CIM_Chip.pull.xml").exists()
assert (tmp_path / "CIM_Chip.pull.2.xml").exists()
def test_never_switches_power_or_writes(amt, tmp_path):
record(WsmanTransport(HOST, "admin", "pw"), tmp_path)
operations = {r.operation for r in amt.requests}
assert operations <= {
"Get",
"Enumerate",
"Pull",
"GetUuid",
"PositionToFirstRecord",
"GetRecords",
}
def test_a_class_amt_lacks_is_recorded_as_its_fault(amt, tmp_path):
amt.override(
"CIM_MediaAccessDevice",
"Enumerate",
400,
(DMT / "fault_destination_unreachable.xml").read_text(),
)
record(WsmanTransport(HOST, "admin", "pw"), tmp_path)
assert (
"DestinationUnreachable" in (tmp_path / "CIM_MediaAccessDevice.enumerate.xml").read_text()
)
assert "CIM_MediaAccessDevice" in (tmp_path / "COMMANDS.txt").read_text()
def test_the_provisioning_otp_is_redacted(amt, tmp_path):
record(WsmanTransport(HOST, "admin", "pw"), tmp_path)
text = (tmp_path / "AMT_SetupAndConfigurationService.get.xml").read_text()
assert "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" not in text
assert "<g:ProvisioningServerOTP>REDACTED</g:ProvisioningServerOTP>" in text
def test_every_read_names_a_known_class():
from napalm_amt.wsman import resource_uri
for resource, _operation in READS:
resource_uri(resource)
def test_main_reads_the_password_from_a_file(tmp_path, monkeypatch):
secret = tmp_path / "pw"
secret.write_text("P@ssw0rd\n")
out = tmp_path / "out"
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
AmtServer(rsps)
assert (
main(
[
HOST,
"--user",
"admin",
"--password-file",
str(secret),
"--out",
str(out),
"--no-landing-page",
]
)
== 0
)
assert (out / "AMT_GeneralSettings.get.xml").exists()
def test_main_without_a_password_fails(tmp_path, monkeypatch):
monkeypatch.delenv("AMT_PASSWORD", raising=False)
with pytest.raises(SystemExit):
main([HOST, "--out", str(tmp_path)])
def test_landing_page_with_its_headers_is_kept(tmp_path):
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
AmtServer(rsps)
rsps.add(
responses.GET,
f"http://{HOST}:16992/",
status=200,
headers={"Server": "Intel(R) Active Management Technology 11.8.50.3399"},
body="<html><title>Intel&reg; Active Management Technology</title></html>",
)
secret = tmp_path / "pw"
secret.write_text("pw")
main([HOST, "--password-file", str(secret), "--out", str(tmp_path / "out")])
page = (tmp_path / "out" / "landing_page.txt").read_text()
assert "Server: Intel(R) Active Management Technology 11.8.50.3399" in page
assert "<title>Intel&reg; Active Management Technology</title>" in page
+293
View File
@@ -0,0 +1,293 @@
"""The WS-Management client: envelopes, Digest auth, enumeration, methods and faults."""
import pytest
import requests
import responses
from napalm_amt.wsman import (
WsmanAuthError,
WsmanError,
WsmanFault,
WsmanTransport,
resource_uri,
)
from .conftest import HOST, URL, Request, load, patched
def _transport(**kwargs) -> WsmanTransport:
return WsmanTransport(HOST, "admin", "P@ssw0rd", **kwargs)
# --- resource URIs ---------------------------------------------------------------------
@pytest.mark.parametrize(
("resource", "uri"),
[
("AMT_GeneralSettings", "http://intel.com/wbem/wscim/1/amt-schema/1/AMT_GeneralSettings"),
(
"IPS_KVMRedirectionSettingData",
"http://intel.com/wbem/wscim/1/ips-schema/1/IPS_KVMRedirectionSettingData",
),
("CIM_Processor", "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Processor"),
],
)
def test_resource_uri_follows_the_class_prefix(resource, uri):
assert resource_uri(resource) == uri
def test_resource_uri_refuses_an_unknown_prefix():
with pytest.raises(ValueError):
resource_uri("Win32_Processor")
def test_url_is_plain_http_on_16992_and_tls_on_16993():
assert _transport().url == f"http://{HOST}:16992/wsman"
assert _transport(port=16993).url == f"https://{HOST}:16993/wsman"
assert _transport(port=8443, tls=True).url == f"https://{HOST}:8443/wsman"
# --- reading ---------------------------------------------------------------------------
def test_get_returns_the_instance_as_a_dict(amt):
settings = _transport().get("AMT_GeneralSettings")
assert settings["HostName"] == "Test Host Name"
assert settings["AMTNetworkEnabled"] == "1"
request = amt.calls("Get")[0]
assert request.action == "http://schemas.xmlsoap.org/ws/2004/09/transfer/Get"
assert request.resource_uri == resource_uri("AMT_GeneralSettings")
def test_enumerate_pulls_every_instance(amt):
items = _transport().enumerate("CIM_PhysicalMemory")
assert [i["BankLabel"] for i in items] == ["BANK 0", "BANK 2"]
assert [r.operation for r in amt.requests] == ["Enumerate", "Pull"]
assert amt.calls("Pull")[0].context == "ctx-CIM_PhysicalMemory"
def test_enumerate_follows_the_context_until_end_of_sequence(amt):
# CIM_Chip's first page carries a context and no EndOfSequence: pull again.
items = _transport().enumerate("CIM_Chip")
assert [i["Version"] for i in items] == ["Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz"]
assert [r.operation for r in amt.requests] == ["Enumerate", "Pull", "Pull"]
assert amt.calls("Pull")[1].context == "D4020000-0000-0000-0000-000000000000"
def test_repeated_elements_become_lists(amt):
(service,) = _transport().enumerate("CIM_AssociatedPowerManagementService")
assert service["PowerState"] == "2"
assert service["AvailableRequestedPowerStates"] == ["10", "8", "5", "11", "4", "7", "14", "12"]
def test_nested_elements_become_dicts(amt):
(bios,) = _transport().enumerate("CIM_BIOSElement")
assert bios["ReleaseDate"] == {"Datetime": "2020-08-10T00:00:00Z"}
def test_an_empty_element_is_an_empty_string(amt):
kvm = _transport().get("IPS_KVMRedirectionSettingData")
assert kvm["RFBPassword"] == ""
def test_invoke_returns_the_output_parameters(amt):
out = _transport().invoke("AMT_SetupAndConfigurationService", "GetUuid")
assert out == {"UUID": "E67jVdK/u2EXoIiu3XA36g=="}
request = amt.calls("GetUuid")[0]
assert request.action == (
"http://intel.com/wbem/wscim/1/amt-schema/1/AMT_SetupAndConfigurationService/GetUuid"
)
assert (
request.body.find(
"{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_SetupAndConfigurationService}"
"GetUuid_INPUT"
)
is not None
)
def test_invoke_sends_the_input_parameters_in_order(amt):
_transport().invoke(
"AMT_MessageLog", "GetRecords", [("IterationIdentifier", "1"), ("MaxReadRecords", "390")]
)
body = amt.calls("GetRecords")[0].body
ns = "{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_MessageLog}"
params = body.find(f"{ns}GetRecords_INPUT")
assert [(p.tag.removeprefix(ns), p.text) for p in params] == [
("IterationIdentifier", "1"),
("MaxReadRecords", "390"),
]
def test_invoke_accepts_raw_xml_for_references(amt):
ref = "<h:ManagedElement><Address>x</Address></h:ManagedElement>"
_transport().invoke(
"CIM_PowerManagementService", "RequestPowerStateChange", [("PowerState", "8")], raw=ref
)
xml = amt.calls("RequestPowerStateChange")[0].xml
assert "<h:PowerState>8</h:PowerState><h:ManagedElement>" in xml
def test_selectors_go_into_the_header(amt):
_transport().get("AMT_GeneralSettings", selectors={"InstanceID": "Intel(r) AMT: General"})
assert amt.calls("Get")[0].selectors == {"InstanceID": "Intel(r) AMT: General"}
def test_values_are_escaped(amt):
_transport().invoke("AMT_MessageLog", "GetRecords", [("Tag", "<a&b>")])
assert "&lt;a&amp;b&gt;" in amt.calls("GetRecords")[0].xml
def test_message_ids_differ_per_request(amt):
transport = _transport()
transport.get("AMT_GeneralSettings")
transport.get("AMT_GeneralSettings")
ids = [r.xml.split("<a:MessageID>")[1].split("<")[0] for r in amt.requests]
assert len(set(ids)) == 2
# --- errors ----------------------------------------------------------------------------
def test_a_fault_raises_with_its_subcode_and_reason(amt):
with pytest.raises(WsmanFault) as excinfo:
_transport().get("AMT_NoSuchClass")
fault = excinfo.value
assert fault.subcode == "DestinationUnreachable"
assert "No route can be determined" in fault.reason
assert fault.detail.endswith("/InvalidResourceURI")
assert isinstance(fault, WsmanError)
def test_an_unknown_class_in_an_enumeration_raises_the_fault(amt):
with pytest.raises(WsmanFault):
_transport().enumerate("CIM_NoSuchClass")
def test_a_method_returning_an_error_code_raises(amt):
amt.override(
"AMT_MessageLog",
"PositionToFirstRecord",
200,
patched(
"AMT_MessageLog.PositionToFirstRecord.xml",
"<g:ReturnValue>0</g:ReturnValue>",
"<g:ReturnValue>2</g:ReturnValue>",
),
)
with pytest.raises(WsmanError, match="ReturnValue 2"):
_transport().invoke("AMT_MessageLog", "PositionToFirstRecord")
def test_a_method_may_be_allowed_an_error_code(amt):
amt.override(
"AMT_MessageLog",
"PositionToFirstRecord",
200,
patched(
"AMT_MessageLog.PositionToFirstRecord.xml",
"<g:ReturnValue>0</g:ReturnValue>",
"<g:ReturnValue>2</g:ReturnValue>",
),
)
out = _transport().invoke("AMT_MessageLog", "PositionToFirstRecord", check=False)
assert out["ReturnValue"] == "2"
def test_wrong_credentials_raise_an_auth_error():
with responses.RequestsMock() as rsps:
rsps.add(
responses.POST,
URL,
status=401,
headers={"WWW-Authenticate": 'Digest realm="Digest:0", nonce="n", qop="auth"'},
)
with pytest.raises(WsmanAuthError):
_transport(retries=0).get("AMT_GeneralSettings")
def test_a_non_soap_error_raises():
with responses.RequestsMock() as rsps:
rsps.add(responses.POST, URL, status=500, body="<html>busy</html>")
with pytest.raises(WsmanError, match="HTTP 500"):
_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"))
rsps.add(responses.POST, URL, body=load("AMT_GeneralSettings.get.xml"))
assert _transport(retries=1).get("AMT_GeneralSettings")["HostName"] == "Test Host Name"
with responses.RequestsMock() as rsps:
rsps.add(responses.POST, URL, body=requests.ConnectionError("refused"))
with pytest.raises(WsmanError, match="refused"):
_transport(retries=0).get("AMT_GeneralSettings")
# --- Digest authentication -------------------------------------------------------------
def test_digest_challenge_is_answered():
"""AMT accepts only HTTP Digest; the first request draws the challenge."""
seen = []
def handle(request):
seen.append(request.headers.get("Authorization"))
if not request.headers.get("Authorization"):
return (
401,
{
"WWW-Authenticate": 'Digest realm="Digest:F3EB554784E729164447A89F60B641C5", '
'nonce="dGVzdG5vbmNl", stale="false", qop="auth"'
},
"",
)
return 200, {}, load("AMT_GeneralSettings.get.xml")
with responses.RequestsMock() as rsps:
rsps.add_callback(responses.POST, URL, callback=handle)
assert _transport().get("AMT_GeneralSettings")["HostName"] == "Test Host Name"
assert seen[0] is None
assert seen[1].startswith("Digest ")
assert 'username="admin"' in seen[1]
assert 'uri="/wsman"' in seen[1]
assert 'qop="auth"' in seen[1] or "qop=auth" in seen[1]
def test_request_is_a_soap_post():
with responses.RequestsMock() as rsps:
rsps.add(responses.POST, URL, body=load("AMT_GeneralSettings.get.xml"))
_transport().get("AMT_GeneralSettings")
sent = rsps.calls[0].request
assert sent.headers["Content-Type"].startswith("application/soap+xml")
request = Request(sent.body)
assert request.resource == "AMT_GeneralSettings"
# --- recording -------------------------------------------------------------------------
def test_recorder_sees_every_raw_answer(amt):
seen = []
transport = _transport()
transport.recorder = lambda resource, operation, content: seen.append(
(resource, operation, content[:5])
)
transport.enumerate("CIM_PhysicalMemory")
transport.invoke("AMT_SetupAndConfigurationService", "GetUuid")
assert [(r, o) for r, o, _ in seen] == [
("CIM_PhysicalMemory", "Enumerate"),
("CIM_PhysicalMemory", "Pull"),
("AMT_SetupAndConfigurationService", "GetUuid"),
]
assert all(c == b"<?xml" for _, _, c in seen)
def test_recorder_sees_faults_too(amt):
seen = []
transport = _transport()
transport.recorder = lambda resource, operation, content: seen.append(operation)
with pytest.raises(WsmanFault):
transport.get("AMT_NoSuchClass")
assert seen == ["Get"]