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:
commit
0052ba7884
61 files changed
+4303
No files matched your search
@@ -0,0 +1,7 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
.venv/
|
||||
.pytest_cache/
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
"""NAPALM driver for Intel Active Management Technology (AMT) over WS-Management."""
|
||||
|
||||
from napalm_amt.driver import AmtDriver
|
||||
|
||||
__all__ = ["AmtDriver"]
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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])}",
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 []
|
||||
@@ -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},
|
||||
}
|
||||
@@ -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
|
||||
Whitespace-only changes.
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
Whitespace-only changes.
@@ -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>
|
||||
+20
@@ -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>
|
||||
+50
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+22
@@ -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
@@ -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>
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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": ""}, []) == []
|
||||
@@ -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® 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
|
||||
@@ -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()
|
||||
@@ -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® 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® Active Management Technology</title>" in page
|
||||
@@ -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 "<a&b>" 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"]
|
||||
Reference in new issue
Block a user