mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
rust/pvapconfig: Introduce new tool pvapconfig
pvapconfig is a new tool for automatically configuring the APQNs within an Secure Execution KVM guest with AP pass-through support. Based on a given AP configuration it tries to find a matching APQN and bind and associate it with the correct secret. Signed-off-by: Harald Freudenberger <freude@linux.ibm.com> Reviewed-by: Ingo Franzki <ifranzki@linux.ibm.com> Reviewed-by: Holger Dengler <dengler@linux.ibm.com> Reviewed-by: Steffen Eiden <seiden@linux.ibm.com> Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
This commit is contained in:
committed by
Jan Höppner
parent
0764460eaf
commit
94a38ebc3a
16
rust/Cargo.lock
generated
16
rust/Cargo.lock
generated
@@ -792,6 +792,22 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pvapconfig"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"lazy_static",
|
||||
"openssl",
|
||||
"openssl-sys",
|
||||
"pv",
|
||||
"rand",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pvsecret"
|
||||
version = "0.9.0"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"pv",
|
||||
"pvapconfig",
|
||||
"pvsecret",
|
||||
"utils",
|
||||
]
|
||||
|
||||
@@ -19,7 +19,7 @@ ifneq (${HAVE_CARGO},0)
|
||||
|
||||
ifneq (${HAVE_OPENSSL},0)
|
||||
ifneq (${HAVE_LIBCURL},0)
|
||||
PV_TARGETS := pvsecret
|
||||
PV_TARGETS := pvsecret pvapconfig
|
||||
|
||||
PV_BUILD_TARGETS := $(PV_TARGETS)
|
||||
CARGO_TEST_TARGETS += $(addsuffix .test,pv $(PV_TARGETS))
|
||||
|
||||
19
rust/pvapconfig/Cargo.toml
Normal file
19
rust/pvapconfig/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "pvapconfig"
|
||||
description = "A tool to configure the AP resources inside a SE guest based on UV secrets and an AP config file."
|
||||
authors = ["Harald Freudenberger <freude@linux.ibm.com>"]
|
||||
version = "0.9.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap = { version ="4.1", features = ["derive", "wrap_help"]}
|
||||
lazy_static = "1.1"
|
||||
openssl = { version = "0.10" }
|
||||
openssl-sys = { version = "0.9" }
|
||||
pv = { path = "../pv", features = ["uvsecret", "request"] }
|
||||
rand = "0.8"
|
||||
regex = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
utils = { path = "../utils" }
|
||||
9
rust/pvapconfig/README.md
Normal file
9
rust/pvapconfig/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
<!--
|
||||
Copyright 2023 IBM Corp.
|
||||
s390-tools is free software; you can redistribute it and/or modify
|
||||
it under the terms of the MIT license. See LICENSE for details.
|
||||
-->
|
||||
# pvapconfig
|
||||
## Description
|
||||
**pvapconfig** is used to automatically set up the AP configuration
|
||||
within an IBM Secure Execution guest.
|
||||
174
rust/pvapconfig/man/pvapconfig.1
Normal file
174
rust/pvapconfig/man/pvapconfig.1
Normal file
@@ -0,0 +1,174 @@
|
||||
.\" pvapconfig.1
|
||||
.\"
|
||||
.\" Copyright 2023 IBM Corp.
|
||||
.\" s390-tools is free software; you can redistribute it and/or modify
|
||||
.\" it under the terms of the MIT license. See LICENSE for details.
|
||||
.\"
|
||||
.\" use
|
||||
.\" groff -man -Tutf8 pvapconfig.1
|
||||
.\" or
|
||||
.\" nroff -man pvapconfig.1
|
||||
.\" to process this source
|
||||
.\"
|
||||
.TH PVAPCONFIG 1 "DEC 2023" "s390-tools"
|
||||
.SH NAME
|
||||
pvapconfig \- automatic configure APQNs within an SE KVM guest.
|
||||
.SH SYNOPSIS
|
||||
.TP 9
|
||||
.B pvapconfig [OPTIONS]
|
||||
.SH DESCRIPTION
|
||||
pvapconfig is a tool for automatically configuring the APQNs within an
|
||||
Secure Execution KVM guest with AP pass-through support. Based on a
|
||||
given AP configuration it tries to find matching APQNs and binds and
|
||||
associates them with the given secret(s).
|
||||
|
||||
Here is a description of pvapconfig's process:
|
||||
.TP 3
|
||||
1. Check AP bus: Support for AP bus needs to be available and the AP
|
||||
bus needs to support APSB. APSB is only available within an KVM SE
|
||||
guest with AP pass-through support.
|
||||
.TP 3
|
||||
2. Check Ultravisor: UV support needs to be available and the UV needs
|
||||
to support the
|
||||
.I list secrets
|
||||
feature.
|
||||
.TP 3
|
||||
3. Read in and validate the AP configuration file. By default if not
|
||||
overwritten by the --config option the AP configuration is read from
|
||||
.I /etc/pvapconfig.yaml
|
||||
and syntactically verified. See section CONFIGFILE for details about
|
||||
the syntax and semantic of the configuration file.
|
||||
.TP 3
|
||||
4. Fetch the list of association secrets from the UV. Actually the
|
||||
index of the secret and the secret id for each entry is collected. The
|
||||
secret value is NOT fetched as it is NOT accessible but only usable
|
||||
within the UV firmware.
|
||||
.TP 3
|
||||
5. Gather all APQNs available within this KVM SE guest. Collect
|
||||
information about each APQN like online states, crypto card serial
|
||||
numbers and master key verification patterns (MKVP).
|
||||
.TP 3
|
||||
6. Go through all AP config entries. For each AP config entry try to
|
||||
find an APQN which is already configured (bound/associated) to
|
||||
satisfy this config entry. If such a match is found, the AP config
|
||||
entry is assumed to be fulfilled and marked as done.
|
||||
.TP 3
|
||||
7. All remaining APQNs which do not already satisfy an AP config entry
|
||||
are now examined for their bind and association state and maybe reset
|
||||
to unbound state.
|
||||
.TP 3
|
||||
8. Go through all AP config entries which are still not
|
||||
fulfilled. For each such AP config entry try to search for an APQN
|
||||
which would match to this entry and then prepare this APQN (bind,
|
||||
maybe associate). If successful, mark the AP config entry as done.
|
||||
.TP 3
|
||||
9. Evaluation of the applied AP config entries. Applied means the AP
|
||||
config entry has been fulfilled either in step 6 or in step 8. With
|
||||
the strict option given ALL AP config entries need to apply otherwise
|
||||
an error message is printed and pvapconfig returns with exit failure.
|
||||
If the strict option is not given, it is enough to satisfy at least one
|
||||
AP config entry from the configuration and pvapconfig will return
|
||||
successfully.
|
||||
.SH OPTIONS
|
||||
.TP 8
|
||||
.B -c, --config <configfile>
|
||||
Use <configfile> as the AP config file for pvapconfig. If pvapconfig
|
||||
is run without this option the default configuration file
|
||||
/etc/pvapconfig.yaml is used.
|
||||
.TP 8
|
||||
.B -h, --help
|
||||
Print pvapconfig usage information and exit.
|
||||
.TP 8
|
||||
.B -n, --dry-run
|
||||
Do not bind, unbind or associate APQNs but only process the
|
||||
configuration and the available APQNs and secrets and simulate the
|
||||
bind, unbind or associate action on the chosen APQN. Use it together
|
||||
with the verbose option to see which actions pvapconfig would do if
|
||||
unleashed.
|
||||
.TP 8
|
||||
.B -s, --strict
|
||||
All AP config entries need to be satisfied to have pvapconfig
|
||||
terminate with success. Without this option one applied AP config
|
||||
entry is enough to meet the expectations.
|
||||
.TP 8
|
||||
.B -v, --verbose
|
||||
Print out informational messages about what pvapconfig is actually
|
||||
doing.
|
||||
.TP 8
|
||||
.B -V, --version
|
||||
Print version information and exit.
|
||||
.SH CONFIGFILE
|
||||
The pvapconfig yaml configuration file consists of a list of AP config
|
||||
entries. Each entry may hold this information:
|
||||
.TP 2
|
||||
- mode: AP queue mode information, required, either "EP11" or "Accel".
|
||||
.TP 2
|
||||
- mkvp: AP queue Master Key Verification Pattern (MKVP), required for
|
||||
EP11, hex string optional prepented with 0x. The MKVP hex string value
|
||||
may hold either 16 bytes (32 hex characters) or 32 bytes (64 hex
|
||||
characters) but only the leftmost 16 bytes hold MKVP information and
|
||||
thus the rest is ignored.
|
||||
.TP 2
|
||||
- serialnr: Crypto Card Serial Number, string, optional for EP11,
|
||||
ignored for Accel. As this is a real ASCII string uppercase and
|
||||
lowercase character(s) count different.
|
||||
.TP 2
|
||||
- mingen: Card Minimal Generation, string "CEX4", "CEX5", "CEX6",
|
||||
"CEX7" or "CEX8" for Accelerator, string "CEC8" for EP11, optional. If
|
||||
given specifies the minimal accepted Crypto card generation.
|
||||
.TP 2
|
||||
- secretid: Secret id, hex string with optional 0x prepented, required
|
||||
for EP11, ignored for Accel. Details see the following text.
|
||||
.TP 2
|
||||
- name: ASCII string, optional, but see details below.
|
||||
.TP 2
|
||||
- description: Description of this AP config entry, string, ignored,
|
||||
just for convenience for the reader or editor of the configuration.
|
||||
.PP
|
||||
The secret id uniquely identifies an association secret. However, it
|
||||
is a clumsy hex string of 64 characters which represent the readable
|
||||
sha256 value over the secret's name. So pvapconfig can use the name
|
||||
instead and calculate the secret id from the name. So the rule is:
|
||||
.TP 2
|
||||
- If name and secretid is given, the secretid needs to match to the
|
||||
sha256 hash over the given name for this AP config entry.
|
||||
.TP 2
|
||||
- If only name is given then the secretid is calculated with a sha256
|
||||
hash over the given name.
|
||||
.TP 2
|
||||
- If only the secretid is given, there is nothing more to do but
|
||||
verify that the value is a hex string with 64 characters.
|
||||
.SH LOCKING
|
||||
Pvapconfig needs to have a consistent view of the AP resources
|
||||
during lifetime. There must not run multiple instances of pvapconfig
|
||||
or any manipulations of the AP resources in parallel. To prevent the
|
||||
execution of multiple pvapconfig instances the lock file
|
||||
/run/lock/pvapconfig.lock is established. A second instance of
|
||||
pvapconfig will detect this lock file and terminated with an error
|
||||
message. If for any reason this file still exists as a leftover from a
|
||||
previous pvapconfig crash for example, it needs to get removed by
|
||||
hand. The lock file contains the process id of the pvapconfig process
|
||||
which created this file.
|
||||
.SH RETURN VALUE
|
||||
.TP 8
|
||||
.B 0 - Successful termination.
|
||||
At least one AP config entry has been applied or at least one APQN has
|
||||
been found in a state matching to one AP config entry. If strict
|
||||
option is given, ALL AP config entries have been applied. An AP config
|
||||
entry is applied either by configuring the APQN accordingly or an APQN
|
||||
has been found which already fulfills the constrains.
|
||||
.RE
|
||||
.TP 8
|
||||
.B 1 - Failure.
|
||||
Either some kind of failure happened during processing the
|
||||
configuration or the configuration could not get applied
|
||||
successful. In all cases pvapconfig prints out a message to standard
|
||||
error with details about the failure. Also pvapconfig does NOT reset
|
||||
the APQNs to the state found at the startup when failing to apply the
|
||||
configuration.
|
||||
.SH NOTES
|
||||
For more information and details see the IBM documentation about
|
||||
Confidential Computing "Introducing IBM Secure Execution for Linux"
|
||||
available at https://www.ibm.com/docs/.
|
||||
.SH SEE ALSO
|
||||
\fBpvsecret\fR(1), \fBlszcrypt\fR(8), \fBchzcrypt\fR(8)
|
||||
817
rust/pvapconfig/src/ap.rs
Normal file
817
rust/pvapconfig/src/ap.rs
Normal file
@@ -0,0 +1,817 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp. 2023
|
||||
//
|
||||
//! AP support functions for pvapconfig
|
||||
//
|
||||
|
||||
use crate::helper::*;
|
||||
use regex::Regex;
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use std::slice::Iter;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
const PATH_SYS_BUS_AP: &str = "/sys/bus/ap";
|
||||
const PATH_SYS_BUS_AP_FEATURES: &str = "/sys/bus/ap/features";
|
||||
const PATH_SYS_BUS_AP_BINDINGS: &str = "/sys/bus/ap/bindings";
|
||||
const PATH_SYS_DEVICES_AP: &str = "/sys/devices/ap";
|
||||
|
||||
const RE_CARD_DIR: &str = r"^card([[:xdigit:]]{2})$";
|
||||
const RE_QUEUE_DIR: &str = r"^([[:xdigit:]]{2})\.([[:xdigit:]]{4})$";
|
||||
const RE_CARD_TYPE: &str = r"^CEX([3-8])([ACP])$";
|
||||
const RE_EP11_MKVP: &str = r"WK\s+CUR:\s+(\S+)\s+(\S+)";
|
||||
const RE_CCA_AES_MKVP: &str = r"AES\s+CUR:\s+(\S+)\s+(\S+)";
|
||||
const RE_CCA_APKA_MKVP: &str = r"APKA\s+CUR:\s+(\S+)\s+(\S+)";
|
||||
|
||||
const SYS_BUS_AP_BINDINGS_POLL_MS: u64 = 500;
|
||||
|
||||
const SYS_BUS_AP_BIND_POLL_MS: u64 = 500;
|
||||
const SYS_BUS_AP_BIND_TIMEOUT_MS: u64 = 10000;
|
||||
|
||||
const SYS_BUS_AP_ASSOC_POLL_MS: u64 = 500;
|
||||
const SYS_BUS_AP_ASSOC_TIMEOUT_MS: u64 = 10000;
|
||||
|
||||
/// Check if AP bus support is available.
|
||||
/// Returns Result with Ok(()) or Err(failurestring).
|
||||
pub fn check_ap_bus_support() -> Result<(), String> {
|
||||
if !Path::new(PATH_SYS_BUS_AP).is_dir() {
|
||||
return Err(format!(
|
||||
"AP bus support missing (path {PATH_SYS_BUS_AP} is invalid)."
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if AP bus supports APSB.
|
||||
///
|
||||
/// When APSB support is available returns Result
|
||||
/// with Ok(()) or otherwise Err(failurestring).
|
||||
pub fn ap_bus_has_apsb_support() -> Result<(), String> {
|
||||
if !Path::new(PATH_SYS_BUS_AP_FEATURES).is_file() {
|
||||
return Err(format!(
|
||||
"AP bus features support missing (file {PATH_SYS_BUS_AP_FEATURES} does not exist)."
|
||||
));
|
||||
}
|
||||
let features = sysfs_read_string(PATH_SYS_BUS_AP_FEATURES).map_err(|err| {
|
||||
format!("Failure reading AP bus features from {PATH_SYS_BUS_AP_FEATURES} ({err:?}).")
|
||||
})?;
|
||||
match features.find("APSB") {
|
||||
Some(_) => Ok(()),
|
||||
None => Err("Missing AP bus feature APSB (SE AP pass-through not enabled ?).".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for AP bus set up all it's devices.
|
||||
///
|
||||
/// This function loops until the AP bus reports that
|
||||
/// - all AP queue devices have been constructed
|
||||
/// - and all AP device have been bound to a device driver.
|
||||
/// This may take some time and even loop forever if there
|
||||
/// is something wrong with the kernel modules setup.
|
||||
/// Returns true when AP bus bindings are complete,
|
||||
/// otherwise false and a message is printed.
|
||||
/// When AP bus binding complete is not immediately reached
|
||||
/// and this function needs to loop, about every 5 seconds
|
||||
/// a message is printed "Waiting for ...".
|
||||
pub fn wait_for_ap_bus_bindings_complete() -> bool {
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
match sysfs_read_string(PATH_SYS_BUS_AP_BINDINGS) {
|
||||
Ok(s) => {
|
||||
if s.contains("complete") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"Failure reading AP bus bindings from {} ({:?}).",
|
||||
PATH_SYS_BUS_AP_BINDINGS, err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
thread::sleep(time::Duration::from_millis(SYS_BUS_AP_BINDINGS_POLL_MS));
|
||||
counter += 1;
|
||||
if counter % 10 == 0 {
|
||||
println!("Waiting for AP bus bindings complete.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ApqnMode {
|
||||
Accel,
|
||||
Ep11,
|
||||
Cca,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApqnInfoAccel {
|
||||
// empty
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApqnInfoEp11 {
|
||||
pub serialnr: String,
|
||||
pub mkvp: String, // may be an empty string if no WK set
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApqnInfoCca {
|
||||
pub serialnr: String,
|
||||
pub mkvp_aes: String, // may be an empty string if no MK set
|
||||
pub mkvp_apka: String, // may be an empty string if no MK set
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ApqnInfo {
|
||||
Accel(ApqnInfoAccel),
|
||||
Ep11(ApqnInfoEp11),
|
||||
Cca(ApqnInfoCca),
|
||||
}
|
||||
|
||||
impl ApqnInfo {
|
||||
fn accel_info(_carddir: &str, _queuedir: &str) -> Result<ApqnInfo, String> {
|
||||
Ok(ApqnInfo::Accel(ApqnInfoAccel {}))
|
||||
}
|
||||
|
||||
fn cca_info(carddir: &str, queuedir: &str) -> Result<ApqnInfo, String> {
|
||||
let serialnr = match sysfs_read_string(&format!("{carddir}/serialnr")) {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Failure reading serialnr from {carddir}/serialnr: {:?}.",
|
||||
err
|
||||
))
|
||||
}
|
||||
};
|
||||
let mkvps = match sysfs_read_string(&format!("{carddir}/{queuedir}/mkvps")) {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Failure reading mkvps from {carddir}/{queuedir}/mkvps: {:?}.",
|
||||
err
|
||||
))
|
||||
}
|
||||
};
|
||||
let mut aes_mkvp = String::new();
|
||||
let re_cca_aes_mkvp = Regex::new(RE_CCA_AES_MKVP).unwrap();
|
||||
if !re_cca_aes_mkvp.is_match(&mkvps) {
|
||||
return Err(format!(
|
||||
"APQN {} failure parsing mkvps string '{}'.",
|
||||
queuedir, mkvps
|
||||
));
|
||||
} else {
|
||||
let caps = re_cca_aes_mkvp.captures(&mkvps).unwrap();
|
||||
let valid = caps.get(1).unwrap().as_str().to_lowercase();
|
||||
if valid != "valid" {
|
||||
eprintln!(
|
||||
"Warning: APQN {} has no valid AES master key set.",
|
||||
queuedir
|
||||
);
|
||||
} else {
|
||||
aes_mkvp = caps.get(2).unwrap().as_str().to_lowercase();
|
||||
if aes_mkvp.starts_with("0x") {
|
||||
aes_mkvp = String::from(&aes_mkvp[2..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut apka_mkvp = String::new();
|
||||
let re_cca_apka_mkvp = Regex::new(RE_CCA_APKA_MKVP).unwrap();
|
||||
if !re_cca_apka_mkvp.is_match(&mkvps) {
|
||||
return Err(format!(
|
||||
"APQN {} failure parsing mkvps string '{}'.",
|
||||
queuedir, mkvps
|
||||
));
|
||||
} else {
|
||||
let caps = re_cca_apka_mkvp.captures(&mkvps).unwrap();
|
||||
let valid = caps.get(1).unwrap().as_str().to_lowercase();
|
||||
if valid != "valid" {
|
||||
eprintln!(
|
||||
"Warning: APQN {} has no valid APKA master key set.",
|
||||
queuedir
|
||||
);
|
||||
} else {
|
||||
apka_mkvp = caps.get(2).unwrap().as_str().to_lowercase();
|
||||
if apka_mkvp.starts_with("0x") {
|
||||
apka_mkvp = String::from(&apka_mkvp[2..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ApqnInfo::Cca(ApqnInfoCca {
|
||||
serialnr,
|
||||
mkvp_aes: aes_mkvp,
|
||||
mkvp_apka: apka_mkvp,
|
||||
}))
|
||||
}
|
||||
|
||||
fn ep11_info(carddir: &str, queuedir: &str) -> Result<ApqnInfo, String> {
|
||||
let serialnr = match sysfs_read_string(&format!("{carddir}/serialnr")) {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Failure reading serialnr from {carddir}/serialnr: {:?}.",
|
||||
err
|
||||
))
|
||||
}
|
||||
};
|
||||
let mkvps = match sysfs_read_string(&format!("{carddir}/{queuedir}/mkvps")) {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Failure reading mkvps from {carddir}/{queuedir}/mkvps: {:?}.",
|
||||
err
|
||||
))
|
||||
}
|
||||
};
|
||||
let mut mkvp = String::new();
|
||||
let re_ep11_mkvp = Regex::new(RE_EP11_MKVP).unwrap();
|
||||
if !re_ep11_mkvp.is_match(&mkvps) {
|
||||
return Err(format!(
|
||||
"APQN {} failure parsing mkvps string '{}'.",
|
||||
queuedir, mkvps
|
||||
));
|
||||
} else {
|
||||
let caps = re_ep11_mkvp.captures(&mkvps).unwrap();
|
||||
let valid = caps.get(1).unwrap().as_str().to_lowercase();
|
||||
if valid != "valid" {
|
||||
eprintln!("Warning: APQN {} has no valid wrapping key set.", queuedir);
|
||||
} else {
|
||||
mkvp = caps.get(2).unwrap().as_str().to_lowercase();
|
||||
if mkvp.starts_with("0x") {
|
||||
mkvp = String::from(&mkvp[2..]);
|
||||
}
|
||||
if mkvp.len() > 32 {
|
||||
mkvp = String::from(&mkvp[..32])
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ApqnInfo::Ep11(ApqnInfoEp11 { serialnr, mkvp }))
|
||||
}
|
||||
|
||||
fn info(mode: &ApqnMode, carddir: &str, queuedir: &str) -> Result<ApqnInfo, String> {
|
||||
match mode {
|
||||
ApqnMode::Accel => ApqnInfo::accel_info(carddir, queuedir),
|
||||
ApqnMode::Cca => ApqnInfo::cca_info(carddir, queuedir),
|
||||
ApqnMode::Ep11 => ApqnInfo::ep11_info(carddir, queuedir),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Apqn {
|
||||
pub name: String,
|
||||
pub card: u32,
|
||||
pub domain: u32,
|
||||
pub gen: u32,
|
||||
pub mode: ApqnMode,
|
||||
pub info: Option<ApqnInfo>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Apqn {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "({},{})", self.card, self.domain)
|
||||
}
|
||||
}
|
||||
|
||||
impl Apqn {
|
||||
pub fn bind_state(&self) -> Result<BindState, String> {
|
||||
get_apqn_bind_state(self.card, self.domain)
|
||||
}
|
||||
|
||||
pub fn set_bind_state(&self, state: BindState) -> Result<(), String> {
|
||||
set_apqn_bind_state(self.card, self.domain, state)
|
||||
}
|
||||
|
||||
pub fn associate_state(&self) -> Result<AssocState, String> {
|
||||
get_apqn_associate_state(self.card, self.domain)
|
||||
}
|
||||
|
||||
pub fn set_associate_state(&self, state: AssocState) -> Result<(), String> {
|
||||
set_apqn_associate_state(self.card, self.domain, state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper object around Vector of Apqns
|
||||
pub struct ApqnList(Vec<Apqn>);
|
||||
|
||||
impl ApqnList {
|
||||
#[cfg(test)] // only used in test code
|
||||
pub fn from_apqn_vec(apqns: Vec<Apqn>) -> ApqnList {
|
||||
ApqnList(apqns)
|
||||
}
|
||||
|
||||
#[cfg(test)] // only used in test code
|
||||
pub fn to_apqn_vec(&self) -> Vec<Apqn> {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter<'_, Apqn> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Scan AP bus devices in sysfs and construct the Apqnlist.
|
||||
///
|
||||
/// The list is a vector of struct Apqn for each APQN found in sysfs
|
||||
/// which is online and the card type matches to the regular expression
|
||||
/// RE_CARD_TYPE.
|
||||
/// On success a vector of struct Apqn is returned. This list may be
|
||||
/// empty if there are no APQNs available or do not match to the conditions.
|
||||
/// On failure None is returned.
|
||||
/// Fatal errors which should never happened like unable to compile a
|
||||
/// static regular expression will result in calling panic.
|
||||
/// # Panics
|
||||
/// Panics if the compilation of a static regular expression fails.
|
||||
pub fn gather_apqns() -> Option<ApqnList> {
|
||||
let mut apqns: Vec<Apqn> = Vec::new();
|
||||
let re_card_type = Regex::new(RE_CARD_TYPE).unwrap();
|
||||
let re_queue_dir = Regex::new(RE_QUEUE_DIR).unwrap();
|
||||
let card_dirs =
|
||||
match sysfs_get_list_of_subdirs_matching_regex(PATH_SYS_DEVICES_AP, RE_CARD_DIR) {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"Failure reading AP devices {} ({:?}).",
|
||||
PATH_SYS_DEVICES_AP, err
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
for dir in card_dirs {
|
||||
let path = format!("{PATH_SYS_DEVICES_AP}/{dir}");
|
||||
let card_type = match sysfs_read_string(&format!("{path}/type")) {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
eprintln!("Failure reading card type from {} ({:?}).", path, err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if !re_card_type.is_match(&card_type) {
|
||||
eprintln!("Failure parsing card type string '{}'.", card_type);
|
||||
return None;
|
||||
}
|
||||
let caps = re_card_type.captures(&card_type).unwrap();
|
||||
let gen = caps.get(1).unwrap().as_str().parse::<u32>().unwrap();
|
||||
let mode = match caps.get(2).unwrap().as_str().parse::<char>().unwrap() {
|
||||
'A' => ApqnMode::Accel,
|
||||
'C' => ApqnMode::Cca,
|
||||
'P' => ApqnMode::Ep11,
|
||||
_ => panic!("Code inconsistence between regex RE_CARD_TYPE and evaluation code."),
|
||||
};
|
||||
if pv::misc::pv_guest_bit_set() {
|
||||
// the UV blocks requests to CCA cards within SE guest with
|
||||
// AP pass-through support. However, filter out CCA cards as these
|
||||
// cards cause hangs during information gathering.
|
||||
if mode == ApqnMode::Cca {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let queue_dirs = match sysfs_get_list_of_subdirs_matching_regex(&path, RE_QUEUE_DIR) {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"Failure reading AP queue directories in {} ({:?}).",
|
||||
path, err
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
for queue_dir in queue_dirs {
|
||||
let _online = match sysfs_read_i32(&format!("{path}/{queue_dir}/online")) {
|
||||
Ok(1) => true,
|
||||
_ => continue,
|
||||
};
|
||||
let caps = re_queue_dir.captures(&queue_dir).unwrap();
|
||||
let cardstr = caps.get(1).unwrap().as_str();
|
||||
let card = u32::from_str_radix(cardstr, 16).unwrap();
|
||||
let domstr = caps.get(2).unwrap().as_str();
|
||||
let dom = u32::from_str_radix(domstr, 16).unwrap();
|
||||
// For the mpvk and serialnr to fetch from the APQN within a SE
|
||||
// guest the APQN needs to be bound to the guest. So if the APQN
|
||||
// is not bound, temporarily bind it here until the info has
|
||||
// been retrieved.
|
||||
let mut tempbound = false;
|
||||
if pv::misc::pv_guest_bit_set() {
|
||||
let cbs = match get_apqn_bind_state(card, dom) {
|
||||
Ok(bs) => bs,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"Error: Failure reading APQN ({},{}) bind state: {}",
|
||||
card, dom, err
|
||||
);
|
||||
BindState::NotSupported
|
||||
}
|
||||
};
|
||||
if cbs == BindState::Unbound {
|
||||
let r = set_apqn_bind_state(card, dom, BindState::Bound);
|
||||
if r.is_err() {
|
||||
eprintln!(
|
||||
"Warning: Failure to temp. bind APQN ({},{}): {}",
|
||||
card,
|
||||
dom,
|
||||
r.unwrap_err()
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
tempbound = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
let info = match ApqnInfo::info(&mode, &path, &queue_dir) {
|
||||
Err(err) => {
|
||||
// print the error but continue with info set to None
|
||||
eprintln!(
|
||||
"Warning: Failure to gather info for APQN ({},{}): {}",
|
||||
card, dom, err
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(i) => Some(i),
|
||||
};
|
||||
if tempbound {
|
||||
let r = set_apqn_bind_state(card, dom, BindState::Unbound);
|
||||
if r.is_err() {
|
||||
eprintln!(
|
||||
"Warning: Failure to unbind temp. bound APQN ({},{}): {}",
|
||||
card,
|
||||
dom,
|
||||
r.unwrap_err()
|
||||
);
|
||||
}
|
||||
};
|
||||
apqns.push(Apqn {
|
||||
name: queue_dir.clone(),
|
||||
card,
|
||||
domain: dom,
|
||||
gen,
|
||||
mode: mode.clone(),
|
||||
info,
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(ApqnList(apqns))
|
||||
}
|
||||
|
||||
/// Sort this Apqnlist by card generation:
|
||||
/// newest generation first, older generations last.
|
||||
pub fn sort_by_gen(&mut self) {
|
||||
self.0.sort_unstable_by(|a, b| b.gen.cmp(&a.gen));
|
||||
}
|
||||
|
||||
/// Check MK restriction
|
||||
///
|
||||
/// Within one card there must not exist 2 APQNs with same
|
||||
/// MK setup. This rule only applies to EP11 cards.
|
||||
/// Returns true if this check passed,
|
||||
/// otherwise false and a message is printed.
|
||||
pub fn check_mk_restriction(&self) -> bool {
|
||||
for a1 in self.0.iter() {
|
||||
for a2 in self.0.iter() {
|
||||
if a1.card == a2.card
|
||||
&& a1.domain < a2.domain
|
||||
&& a1.mode == ApqnMode::Ep11
|
||||
&& a1.info.is_some()
|
||||
&& a2.info.is_some()
|
||||
{
|
||||
let i1 = match a1.info.as_ref().unwrap() {
|
||||
ApqnInfo::Ep11(i) => i,
|
||||
_ => continue,
|
||||
};
|
||||
let i2 = match a2.info.as_ref().unwrap() {
|
||||
ApqnInfo::Ep11(i) => i,
|
||||
_ => continue,
|
||||
};
|
||||
if i1.mkvp.is_empty() || i2.mkvp.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if i1.mkvp == i2.mkvp {
|
||||
eprintln!("APQN {} and APQN {} have same MPVK", a1, a2);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum BindState {
|
||||
Bound,
|
||||
Unbound,
|
||||
NotSupported,
|
||||
}
|
||||
|
||||
/// Query bind state for this APQN.
|
||||
///
|
||||
/// Returns a BindState enum as defined above or on failure
|
||||
/// an error string. Does NOT print any error messages.
|
||||
pub fn get_apqn_bind_state(card: u32, dom: u32) -> Result<BindState, String> {
|
||||
let path = format!(
|
||||
"{}/card{:02x}/{:02x}.{:04x}/se_bind",
|
||||
PATH_SYS_DEVICES_AP, card, card, dom
|
||||
);
|
||||
match sysfs_read_string(&path) {
|
||||
Err(err) => Err(format!(
|
||||
"Failure reading se_bind attribute for APQN({},{}): {:?}.",
|
||||
card, dom, err
|
||||
)),
|
||||
Ok(str) => match str.as_str() {
|
||||
"bound" => Ok(BindState::Bound),
|
||||
"unbound" => Ok(BindState::Unbound),
|
||||
"-" => Ok(BindState::NotSupported),
|
||||
_ => Err(format!("Unknown bind state '{str}'.")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind or unbind an APQN.
|
||||
///
|
||||
/// The action is determined by the BindState given in.
|
||||
/// But of course only Bound and Unbound is supported - otherwise
|
||||
/// this function panics!
|
||||
/// The function actively loops over the bind state until
|
||||
/// the requested bind state is reached or a timeout has
|
||||
/// occurred (SYS_BUS_AP_BIND_TIMEOUT_MS).
|
||||
/// On success () is returned, on failure an error string
|
||||
/// is returned. Does NOT print any error messages.
|
||||
/// # Panics
|
||||
/// Panics if a desired bind state other than Bund or Unbound is given.
|
||||
pub fn set_apqn_bind_state(card: u32, dom: u32, state: BindState) -> Result<(), String> {
|
||||
let path = format!(
|
||||
"{}/card{:02x}/{:02x}.{:04x}/se_bind",
|
||||
PATH_SYS_DEVICES_AP, card, card, dom
|
||||
);
|
||||
let r = match state {
|
||||
BindState::Bound => sysfs_write_i32(&path, 1),
|
||||
BindState::Unbound => sysfs_write_i32(&path, 0),
|
||||
_ => panic!("set_apqn_bind_state called with invalid BindState."),
|
||||
};
|
||||
if r.is_err() {
|
||||
return Err(format!(
|
||||
"Failure writing se_bind attribute for APQN({},{}): {:?}.",
|
||||
card,
|
||||
dom,
|
||||
r.unwrap_err()
|
||||
));
|
||||
}
|
||||
let mut ms: u64 = 0;
|
||||
loop {
|
||||
thread::sleep(time::Duration::from_millis(SYS_BUS_AP_BIND_POLL_MS));
|
||||
ms += SYS_BUS_AP_BIND_POLL_MS;
|
||||
if ms >= SYS_BUS_AP_BIND_TIMEOUT_MS {
|
||||
break Err(format!(
|
||||
"Timeout setting APQN({},{}) bind state.",
|
||||
card, dom
|
||||
));
|
||||
}
|
||||
let newstate = match get_apqn_bind_state(card, dom) {
|
||||
Err(err) => return Err(err),
|
||||
Ok(s) => s,
|
||||
};
|
||||
if newstate == state {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum AssocState {
|
||||
Associated(u16),
|
||||
AssociationPending,
|
||||
Unassociated,
|
||||
NotSupported,
|
||||
}
|
||||
|
||||
/// Query association state for this APQN.
|
||||
///
|
||||
/// Returns an AssocState enum as defined above or on failure
|
||||
/// an error string. Does NOT print any error messages.
|
||||
pub fn get_apqn_associate_state(card: u32, dom: u32) -> Result<AssocState, String> {
|
||||
let path = format!(
|
||||
"{}/card{:02x}/{:02x}.{:04x}/se_associate",
|
||||
PATH_SYS_DEVICES_AP, card, card, dom
|
||||
);
|
||||
match sysfs_read_string(&path) {
|
||||
Err(err) => Err(format!(
|
||||
"Failure reading se_associate attribute for APQN({},{}: {:?}",
|
||||
card, dom, err
|
||||
)),
|
||||
Ok(str) => {
|
||||
if let Some(prefix) = str.strip_prefix("associated ") {
|
||||
let value = &prefix.parse::<u16>();
|
||||
match value {
|
||||
Ok(v) => Ok(AssocState::Associated(*v)),
|
||||
Err(_) => Err(format!("Invalid association index in '{str}'.")),
|
||||
}
|
||||
} else {
|
||||
match str.as_str() {
|
||||
"association pending" => Ok(AssocState::AssociationPending),
|
||||
"unassociated" => Ok(AssocState::Unassociated),
|
||||
"-" => Ok(AssocState::NotSupported),
|
||||
_ => Err(format!("Unknown association state '{str}'.")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_apqn_associate_state_associate(card: u32, dom: u32, idx: u16) -> Result<(), String> {
|
||||
let path = format!(
|
||||
"{}/card{:02x}/{:02x}.{:04x}/se_associate",
|
||||
PATH_SYS_DEVICES_AP, card, card, dom
|
||||
);
|
||||
let r = sysfs_write_i32(&path, idx as i32);
|
||||
if r.is_err() {
|
||||
return Err(format!(
|
||||
"Failure writing se_associate attribute for APQN({},{}): {:?}.",
|
||||
card,
|
||||
dom,
|
||||
r.unwrap_err()
|
||||
));
|
||||
}
|
||||
let mut ms: u64 = 0;
|
||||
loop {
|
||||
thread::sleep(time::Duration::from_millis(SYS_BUS_AP_ASSOC_POLL_MS));
|
||||
ms += SYS_BUS_AP_ASSOC_POLL_MS;
|
||||
if ms >= SYS_BUS_AP_ASSOC_TIMEOUT_MS {
|
||||
break Err(format!(
|
||||
"Timeout setting APQN({},{}) association idx {} state.",
|
||||
card, dom, idx
|
||||
));
|
||||
}
|
||||
let newstate = match get_apqn_associate_state(card, dom) {
|
||||
Err(err) => return Err(err),
|
||||
Ok(s) => s,
|
||||
};
|
||||
if let AssocState::Associated(i) = newstate {
|
||||
if idx == i {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Failure: APQN({},{}) is associated with {} but it should be {}.",
|
||||
card, dom, i, idx
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_apqn_associate_state_unbind(card: u32, dom: u32) -> Result<(), String> {
|
||||
let bindpath = format!(
|
||||
"{}/card{:02x}/{:02x}.{:04x}/se_bind",
|
||||
PATH_SYS_DEVICES_AP, card, card, dom
|
||||
);
|
||||
let r = sysfs_write_i32(&bindpath, 0);
|
||||
if r.is_err() {
|
||||
return Err(format!(
|
||||
"Failure writing se_bind attribute for APQN({},{}): {:?}.",
|
||||
card,
|
||||
dom,
|
||||
r.unwrap_err()
|
||||
));
|
||||
}
|
||||
let mut ms: u64 = 0;
|
||||
loop {
|
||||
thread::sleep(time::Duration::from_millis(SYS_BUS_AP_ASSOC_POLL_MS));
|
||||
ms += SYS_BUS_AP_ASSOC_POLL_MS;
|
||||
if ms >= SYS_BUS_AP_ASSOC_TIMEOUT_MS {
|
||||
break Err(format!(
|
||||
"Timeout setting APQN({},{}) association unbind state.",
|
||||
card, dom
|
||||
));
|
||||
}
|
||||
let newstate = match get_apqn_associate_state(card, dom) {
|
||||
Err(err) => return Err(err),
|
||||
Ok(s) => s,
|
||||
};
|
||||
if newstate == AssocState::Unassociated {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Associate or Unassociate an APQN.
|
||||
///
|
||||
/// The action is determined by the AssocState given in.
|
||||
/// But of course only Associated and Unassociated is supported
|
||||
/// otherwise this function panics!
|
||||
/// The function actively loops over the association state until
|
||||
/// the requested state is reached or a timeout has
|
||||
/// occurred (SYS_BUS_AP_ASSOC_TIMEOUT_MS).
|
||||
/// The unassociate is in fact a unbind. So the code triggers
|
||||
/// an unbind and then loops over the sysfs se_associate until
|
||||
/// "unassociated" is reached.
|
||||
/// On success () is returned, on failure an error string
|
||||
/// is returned. Does NOT print any error messages.
|
||||
/// # Panics
|
||||
/// Panics if a desired bind state other than Associated or
|
||||
/// Unassociated is given.
|
||||
pub fn set_apqn_associate_state(card: u32, dom: u32, state: AssocState) -> Result<(), String> {
|
||||
match state {
|
||||
AssocState::Associated(idx) => set_apqn_associate_state_associate(card, dom, idx),
|
||||
AssocState::Unassociated => set_apqn_associate_state_unbind(card, dom),
|
||||
_ => panic!("set_apqn_associate_state called with invalid AssocState."),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
// These tests assume, there is an AP bus available
|
||||
// Also for each APQN which is online, it is assumed
|
||||
// to have a valid master key set up (for Ep11 and CCA).
|
||||
|
||||
#[test]
|
||||
fn test_check_ap_bus_support() {
|
||||
if Path::new(PATH_SYS_BUS_AP).is_dir() {
|
||||
assert!(check_ap_bus_support().is_ok());
|
||||
} else {
|
||||
assert!(check_ap_bus_support().is_err());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_check_ap_bus_apsb_support() {
|
||||
if Path::new(PATH_SYS_BUS_AP).is_dir() {
|
||||
// if we are inside a secure execution guest the
|
||||
// apsb check should succeed. Outside an SE guest
|
||||
// the check should fail.
|
||||
if pv::misc::pv_guest_bit_set() {
|
||||
assert!(ap_bus_has_apsb_support().is_ok());
|
||||
} else {
|
||||
assert!(ap_bus_has_apsb_support().is_err());
|
||||
}
|
||||
} else {
|
||||
assert!(ap_bus_has_apsb_support().is_err());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_wait_for_ap_bus_bindings_complete() {
|
||||
let r = wait_for_ap_bus_bindings_complete();
|
||||
if Path::new(PATH_SYS_BUS_AP).is_dir() {
|
||||
assert!(r);
|
||||
} else {
|
||||
assert!(!r);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_gather_apqns() {
|
||||
let r = ApqnList::gather_apqns();
|
||||
if Path::new(PATH_SYS_BUS_AP).is_dir() {
|
||||
assert!(r.is_some());
|
||||
// fail if no entries found
|
||||
let l = r.unwrap();
|
||||
let v = l.to_apqn_vec();
|
||||
for a in v {
|
||||
match a.mode {
|
||||
ApqnMode::Accel => {
|
||||
// fail if no ApqnInfo is attached
|
||||
assert!(a.info.is_some());
|
||||
}
|
||||
ApqnMode::Ep11 => {
|
||||
// fail if no ApqnInfo is attached
|
||||
assert!(a.info.is_some());
|
||||
let info = a.info.unwrap();
|
||||
let i = match &info {
|
||||
ApqnInfo::Ep11(i) => i,
|
||||
_ => panic!("ApqnInfo attached onto Ep11 APQN is NOT ApqnInfoEp11 ?!?"),
|
||||
};
|
||||
// fail if no serialnr
|
||||
assert!(i.serialnr.len() > 0);
|
||||
// mkvp is either empty (no WK set) or has exact 32 characters
|
||||
assert!(i.mkvp.is_empty() || i.mkvp.len() == 32);
|
||||
}
|
||||
ApqnMode::Cca => {
|
||||
// fail if no ApqnInfo is attached
|
||||
assert!(a.info.is_some());
|
||||
let info = a.info.unwrap();
|
||||
let i = match &info {
|
||||
ApqnInfo::Cca(i) => i,
|
||||
_ => panic!("ApqnInfo attached onto Cca APQN is NOT ApqnInfoCca ?!?"),
|
||||
};
|
||||
// fail if no serialnr
|
||||
assert!(i.serialnr.len() > 0);
|
||||
// aes mkvp is either empty (no MK set) or exact 16 characters
|
||||
assert!(i.mkvp_aes.is_empty() || i.mkvp_aes.len() == 16);
|
||||
// apka mkvp is either empty (no MK set) or exact 16 characters
|
||||
assert!(i.mkvp_apka.is_empty() || i.mkvp_apka.len() == 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert!(r.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
63
rust/pvapconfig/src/cli.rs
Normal file
63
rust/pvapconfig/src/cli.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp. 2023
|
||||
//
|
||||
//! Command line interface for pvapconfig
|
||||
//
|
||||
|
||||
use clap::Parser;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
/// The default pvapconfig config file
|
||||
pub const PATH_DEFAULT_CONFIG_FILE: &str = "/etc/pvapconfig.yaml";
|
||||
|
||||
#[derive(Parser, Clone)]
|
||||
pub struct Cli {
|
||||
/// Provide a custom config file (overwrites default /etc/pvapconfig.yaml).
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
pub config: Option<String>,
|
||||
|
||||
/// Dry run: display the actions but don't actually perform them on the APQNs.
|
||||
#[arg(short = 'n', long = "dry-run")]
|
||||
pub dryrun: bool,
|
||||
|
||||
/// Enforce strict match: All config entries need to be fullfilled.
|
||||
///
|
||||
/// By default it is enough to successfully apply at least one config entry.
|
||||
/// With the strict flag enabled, all config entries within a config file
|
||||
/// need to be applied successful.
|
||||
#[arg(long = "strict")]
|
||||
pub strict: bool,
|
||||
|
||||
/// Provide more detailed output.
|
||||
#[arg(short, long)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// Print version information and exit.
|
||||
#[arg(short = 'V', long)]
|
||||
pub version: bool,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ARGS: Cli = Cli::parse();
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
/// verbose returns true if the verbose command line option
|
||||
/// was given, otherwise false is returned.
|
||||
pub fn verbose(&self) -> bool {
|
||||
self.verbose
|
||||
}
|
||||
|
||||
/// dryrun returns true if the dry-run command line option
|
||||
/// was given, otherwise false is returned.
|
||||
pub fn dryrun(&self) -> bool {
|
||||
self.dryrun
|
||||
}
|
||||
|
||||
/// strict returns true if the strict flag was given, otherwise
|
||||
/// false is returned.
|
||||
pub fn strict(&self) -> bool {
|
||||
self.strict
|
||||
}
|
||||
}
|
||||
391
rust/pvapconfig/src/config.rs
Normal file
391
rust/pvapconfig/src/config.rs
Normal file
@@ -0,0 +1,391 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp. 2023
|
||||
//
|
||||
//! Functions around handling the pvapconfig configuration file
|
||||
//
|
||||
|
||||
use openssl::sha::sha256;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::{self};
|
||||
use std::fs::File;
|
||||
use std::slice::Iter;
|
||||
|
||||
pub const STR_MODE_EP11: &str = "ep11";
|
||||
pub const STR_MODE_ACCEL: &str = "accel";
|
||||
|
||||
const RE_EP11_MKVP_32: &str = r"^(0x)?([[:xdigit:]]{32})$";
|
||||
const RE_EP11_MKVP_64: &str = r"^(0x)?([[:xdigit:]]{64})$";
|
||||
const RE_SERIALNR: &str = r"^(\S{16})$";
|
||||
const RE_EP11_GEN: &str = r"^cex(8)$";
|
||||
const RE_ACCEL_GEN: &str = r"^cex([4-8])$";
|
||||
const RE_SECRETID: &str = r"^(0x)?([[:xdigit:]]{64})$";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct ApConfigEntry {
|
||||
pub name: String, // name and description are unmodified from the config file
|
||||
pub description: String, // accel after validation ep11 after validation
|
||||
pub mode: String, // "accel" "ep11"
|
||||
pub mkvp: String, // empty 32 hex lowercase characters
|
||||
pub serialnr: String, // empty empty or 16 non-whitespace characters
|
||||
pub mingen: String, // empty or "cex4"..."cex8" empty or "cex8"
|
||||
pub secretid: String, // empty 64 hex lowercase characters
|
||||
}
|
||||
|
||||
impl ApConfigEntry {
|
||||
fn validate_secretid(&mut self) -> Result<(), String> {
|
||||
// either secret id or name may be given
|
||||
if self.secretid.is_empty() && self.name.is_empty() {
|
||||
return Err("Neither secretid nor name given.".to_string());
|
||||
}
|
||||
// if name is given, calculate sha256 digest for this name
|
||||
// test for the hash calculated here can be done with openssl:
|
||||
// echo -n "Hello" >in.bin; openssl dgst -sha256 -binary -out out.bin in.bin; hexdump -C out.bin
|
||||
if self.name.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let hash = sha256(self.name.as_bytes());
|
||||
let hashstr = crate::helper::u8_to_hexstring(&hash);
|
||||
// if there is a secretid given, this must match to the hash
|
||||
if !self.secretid.is_empty() {
|
||||
if self.secretid != hashstr {
|
||||
return Err("Mismatch between sha256(name) and secretid.".to_string());
|
||||
}
|
||||
} else {
|
||||
self.secretid = hashstr;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
/// Panics if the compilation of a static regular expression fails.
|
||||
fn validate_ep11_entry(&mut self) -> Result<(), String> {
|
||||
// mkvp is required
|
||||
let mut mkvp = self.mkvp.trim().to_lowercase();
|
||||
if mkvp.is_empty() {
|
||||
return Err("Mkvp value missing.".to_string());
|
||||
}
|
||||
// either 64 hex or 32 hex
|
||||
if Regex::new(RE_EP11_MKVP_64).unwrap().is_match(&mkvp) {
|
||||
// need to cut away the last 32 hex characters
|
||||
mkvp = String::from(&mkvp[..mkvp.len() - 32])
|
||||
} else if Regex::new(RE_EP11_MKVP_32).unwrap().is_match(&mkvp) {
|
||||
// nothing to do here
|
||||
} else {
|
||||
return Err(format!("Mkvp value '{}' is not valid.", &self.mkvp));
|
||||
}
|
||||
self.mkvp = match mkvp.strip_prefix("0x") {
|
||||
Some(rest) => String::from(rest),
|
||||
None => mkvp,
|
||||
};
|
||||
// serialnr is optional
|
||||
let serialnr = self.serialnr.trim().to_string();
|
||||
if !serialnr.is_empty() && !Regex::new(RE_SERIALNR).unwrap().is_match(&serialnr) {
|
||||
return Err(format!("Serialnr value '{}' is not valid.", &self.serialnr));
|
||||
}
|
||||
self.serialnr = serialnr;
|
||||
// mingen is optional, but if given only CEX8 is valid
|
||||
let mingen = self.mingen.trim().to_lowercase();
|
||||
if !mingen.is_empty() && !Regex::new(RE_EP11_GEN).unwrap().is_match(&mingen) {
|
||||
return Err(format!("Mingen value '{}' is not valid.", &self.mingen));
|
||||
}
|
||||
self.mingen = mingen;
|
||||
// secretid or name is required
|
||||
let secretid = self.secretid.trim().to_lowercase();
|
||||
if !secretid.is_empty() && !Regex::new(RE_SECRETID).unwrap().is_match(&secretid) {
|
||||
return Err(format!("Secretid value '{}' is not valid.", &self.secretid));
|
||||
}
|
||||
self.secretid = match secretid.strip_prefix("0x") {
|
||||
Some(rest) => String::from(rest),
|
||||
None => secretid,
|
||||
};
|
||||
// name is optional, ignored here
|
||||
// description is optional, ignored here
|
||||
// but the secretid needs some more validation
|
||||
self.validate_secretid()
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
/// Panics if the compilation of a static regular expression fails.
|
||||
fn validate_accel_entry(&mut self) -> Result<(), String> {
|
||||
// mkvp is ignored
|
||||
self.mkvp.clear();
|
||||
// serialnr is ignored
|
||||
self.serialnr.clear();
|
||||
// mingen is optional, but if given must match to CEX4..CEX8
|
||||
let mingen = self.mingen.trim().to_lowercase();
|
||||
if !mingen.is_empty() && !Regex::new(RE_ACCEL_GEN).unwrap().is_match(&mingen) {
|
||||
return Err(format!("Mingen value '{}' is not valid.", &self.mingen));
|
||||
}
|
||||
self.mingen = mingen;
|
||||
// secretid is ignored
|
||||
self.secretid.clear();
|
||||
// name is optional, ignored here
|
||||
// description is optional, ignored here
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate(&mut self) -> Result<(), String> {
|
||||
// trim name
|
||||
self.name = self.name.trim().to_string();
|
||||
// mode is always required
|
||||
let mode = self.mode.trim().to_lowercase();
|
||||
match mode.as_str() {
|
||||
STR_MODE_EP11 => {
|
||||
self.mode = mode;
|
||||
self.validate_ep11_entry()?;
|
||||
}
|
||||
STR_MODE_ACCEL => {
|
||||
self.mode = mode;
|
||||
self.validate_accel_entry()?;
|
||||
}
|
||||
_ => return Err(format!("Unknown or invalid mode '{}'.", mode)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper object around Vector of ApConfigEntry
|
||||
pub struct ApConfigList(Vec<ApConfigEntry>);
|
||||
|
||||
impl ApConfigList {
|
||||
#[cfg(test)] // only used in test code
|
||||
pub fn from_apconfigentry_vec(apconfigs: Vec<ApConfigEntry>) -> ApConfigList {
|
||||
ApConfigList(apconfigs)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter<'_, ApConfigEntry> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
fn read_yaml_file(fname: &str) -> Result<Vec<ApConfigEntry>, String> {
|
||||
let file = match File::open(fname) {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Failure to open AP config file {}: {:?}",
|
||||
fname, err
|
||||
))
|
||||
}
|
||||
};
|
||||
match serde_yaml::from_reader(file) {
|
||||
Ok(cfg) => Ok(cfg),
|
||||
Err(err) => Err(format!(
|
||||
"Failure parsing AP config file {}: {:?}",
|
||||
fname, err
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(config: &mut [ApConfigEntry]) -> Result<(), String> {
|
||||
for (i, entry) in config.iter_mut().enumerate() {
|
||||
let ename = if !entry.name.trim().is_empty() {
|
||||
format!("AP config entry {} '{}'", i, entry.name.trim())
|
||||
} else {
|
||||
format!("AP config entry {}", i)
|
||||
};
|
||||
if let Err(err) = &entry.validate() {
|
||||
return Err(format!("{}: {}", ename, err));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read in and validate the yaml configuration from a file.
|
||||
/// Returns a Result with Ok(ApConfigList) on success
|
||||
/// or an Err(errorstring) on failure.
|
||||
pub fn read_and_validate_yaml_file(fname: &str) -> Result<ApConfigList, String> {
|
||||
let mut apconfig = ApConfigList::read_yaml_file(fname)?;
|
||||
ApConfigList::validate(&mut apconfig)?;
|
||||
Ok(ApConfigList(apconfig))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
const GOOD_CONFIGS: [&str; 8] = [
|
||||
"# good test 1
|
||||
- name: my Accelerator
|
||||
mode: AcCel
|
||||
mingen: Cex7\n",
|
||||
"# good test 2
|
||||
- name: my Accelerator 2
|
||||
description: Accelerator entry with description
|
||||
mode: Accel\n",
|
||||
"# good test 3
|
||||
- name: my EP11 APQN 1
|
||||
mode: Ep11
|
||||
mkvp: 0xDB3C3B3C3F097DD55EC7EB0E7FDBCB93
|
||||
serialnr: 93AADFK719460083
|
||||
secretid: 0xBC9d46c052BC3574454C5715757274629a283767ed237922cfb8651c0e77320A\n",
|
||||
"# good test 4
|
||||
- name: my EP11 APQN 2
|
||||
mode: EP11
|
||||
mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93
|
||||
serialnr: 93aaDHzu42082261
|
||||
secretid: 0x2ca853f959fc5ce5f1888cb48dae39514a27bb66520ac85f6073a7f678d262c0\n",
|
||||
"# good test 5
|
||||
- name: my EP11 APQN 3
|
||||
mode: EP11
|
||||
mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93db3c3b3c3f097dd55ec7eb0e7fdbcb93
|
||||
serialnr: 93aaDHzu42082261
|
||||
secretid: 0xd146c9ae77cdff25fa87a5b3487587dc29a4e391b315c98570e8fa2e2ec91454\n",
|
||||
"# no name but secretid given
|
||||
- mode: EP11
|
||||
mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93
|
||||
secretid: 0x0767668dd22f23fa675c4641e04bb4e991f443be4df13ce3896b8eeca59fcc10\n",
|
||||
"# no secretid but name given
|
||||
- mode: EP11
|
||||
name: My-EP11-AP-config
|
||||
mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93\n",
|
||||
"# secretid and name given
|
||||
- mode: EP11
|
||||
name: My-EP11-AP-config
|
||||
mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93
|
||||
secretid: 0x0767668dd22f23fa675c4641e04bb4e991f443be4df13ce3896b8eeca59fcc10\n",
|
||||
];
|
||||
|
||||
const BAD_CONFIGS: [&str; 12] = [
|
||||
"# mode missing
|
||||
- name: bad test 1
|
||||
mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93
|
||||
secretid: 0x0767668dd22f23fa675c4641e04bb4e991f443be4df13ce3896b8eeca59fcc10\n",
|
||||
"# invalid mode
|
||||
- name: bad test 2
|
||||
mode: CCA
|
||||
mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93
|
||||
secretid: 0x0767668dd22f23fa675c4641e04bb4e991f443be4df13ce3896b8eeca59fcc10\n",
|
||||
"# Accelerator with wrong CEX3
|
||||
- name: bad test 3
|
||||
mode: Accel
|
||||
mingen: Cex3\n",
|
||||
"# Accelerator with wrong CEX9
|
||||
- name: bad test 4
|
||||
mode: Accel
|
||||
mingen: CEX9\n",
|
||||
"# EP11 with mkvp missing
|
||||
- name: bad test 5
|
||||
mode: EP11
|
||||
serialnr: 93AADHZU42082261\n",
|
||||
"# EP11 with non hex mkvp
|
||||
- name: bad test 6
|
||||
mode: EP11
|
||||
mkvp: 0xabcdefghijklmnopqqponmlkjihgfedcba
|
||||
serialnr: 93AADHZU42082261\n",
|
||||
"# EP11 with mkvp too big
|
||||
- name: bad test 7
|
||||
mode: EP11
|
||||
mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb93aa
|
||||
serialnr: 93AADHZU42082261\n",
|
||||
"# EP11 with mkvp too small
|
||||
- name: bad test 8
|
||||
mode: EP11
|
||||
mkvp: 0xdb3c3b3c3f097dd55ec7eb0e7fdbcb
|
||||
serialnr: 93AADHZU42082261\n",
|
||||
"# EP11 with invalid CEXx
|
||||
- name: bad test 9
|
||||
mode: EP11
|
||||
mingen: CEX7
|
||||
mkvp: 0x00112233445566778899aabbccddeeff
|
||||
serialnr: 93AADHZU42082261\n",
|
||||
"# EP11 with invalid Serialnr
|
||||
- name: bad test 10
|
||||
mode: EP11
|
||||
mkvp: 0x00112233445566778899aabbccddeeff
|
||||
serialnr: 93AADHZU4208226\n",
|
||||
"# EP11 with invalid Serialnr
|
||||
- name: bad test 11
|
||||
mode: EP11
|
||||
mkvp: 0x00112233445566778899aabbccddeeff
|
||||
serialnr: 93AAD ZU42082261\n",
|
||||
"# EP11 with sha256(name) != secretid
|
||||
- name: bad test 12
|
||||
mode: EP11
|
||||
mkvp: 0x00112233445566778899aabbccddeeff
|
||||
serialnr: AABBCCDDEEFFGGHH
|
||||
secretid: 0x2ca853f959fc5ce5f1888cb48dae39514a27bb66520ac85f6073a7f678d262c0\n",
|
||||
];
|
||||
|
||||
const BAD_DESERIALIZE: [&str; 2] = [
|
||||
"/*\ntotal nonsense\n */\n",
|
||||
"# wrong/unknown field
|
||||
- name: de-serialize failure 1
|
||||
type: EP11\n",
|
||||
];
|
||||
|
||||
fn write_yaml_config_to_temp_file(content: &str) -> Result<String, String> {
|
||||
let dir = env::temp_dir();
|
||||
let rnd = rand::random::<u32>();
|
||||
let fname = format!("{}/config-test-{}.yaml", dir.to_str().unwrap(), rnd);
|
||||
let mut f = match File::create(&fname) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Err(format!("Failure creating temp file '{fname}'.")),
|
||||
};
|
||||
match f.write_all(content.as_bytes()) {
|
||||
Ok(_) => Ok(fname),
|
||||
Err(_) => {
|
||||
fs::remove_file(&fname).ok();
|
||||
Err(format!("Failure writing to temp file '{fname}'."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_yaml() {
|
||||
for yaml in GOOD_CONFIGS {
|
||||
let f = write_yaml_config_to_temp_file(yaml).unwrap();
|
||||
let config = ApConfigList::read_and_validate_yaml_file(&f).unwrap();
|
||||
assert!(config.len() > 0);
|
||||
fs::remove_file(&f).ok();
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_bad_yaml() {
|
||||
for yaml in BAD_CONFIGS {
|
||||
let f = write_yaml_config_to_temp_file(yaml).unwrap();
|
||||
let r = ApConfigList::read_and_validate_yaml_file(&f);
|
||||
assert!(r.is_err());
|
||||
fs::remove_file(&f).ok();
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_invalid_deserizalize() {
|
||||
for yaml in BAD_DESERIALIZE {
|
||||
let f = write_yaml_config_to_temp_file(yaml).unwrap();
|
||||
let r = ApConfigList::read_and_validate_yaml_file(&f);
|
||||
assert!(r.is_err());
|
||||
fs::remove_file(&f).ok();
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_sha256() {
|
||||
assert!(
|
||||
crate::helper::u8_to_hexstring(&sha256("Hello".as_bytes()))
|
||||
== "185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"
|
||||
);
|
||||
assert!(
|
||||
crate::helper::u8_to_hexstring(&sha256("SECRET1".as_bytes()))
|
||||
== "03153249db7ce46b0330ffb1a760b59710531af08ec4d7f8424a6870fae49360"
|
||||
);
|
||||
assert!(
|
||||
crate::helper::u8_to_hexstring(&sha256("SECRET2".as_bytes()))
|
||||
== "258499e710e0bd3bb878d6bac7e478b30f3f3e72566989f638c4143d14f6c0b6"
|
||||
);
|
||||
}
|
||||
}
|
||||
272
rust/pvapconfig/src/helper.rs
Normal file
272
rust/pvapconfig/src/helper.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp. 2023
|
||||
//
|
||||
//! Collection of helper functions for pvapconfig
|
||||
//
|
||||
|
||||
use regex::Regex;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const PATH_PVAPCONFIG_LOCK: &str = "/run/lock/pvapconfig.lock";
|
||||
|
||||
/// Convert u8 slice to (lowercase) hex string
|
||||
pub fn u8_to_hexstring(slice: &[u8]) -> String {
|
||||
let s = String::with_capacity(2 * slice.len());
|
||||
slice.iter().fold(s, |acc, e| acc + &format!("{e:02x}"))
|
||||
}
|
||||
|
||||
/// Convert hexstring to u8 vector
|
||||
/// The hexstring may contain whitespaces which are ignored.
|
||||
/// If there are other characters in there or if the number
|
||||
/// of hex characters is uneven panic() is called.
|
||||
/// # Panics
|
||||
/// Panics if the given string contains characters other than
|
||||
/// hex digits and whitespace. Panics if the number of hex digits
|
||||
/// is not even.
|
||||
#[cfg(test)] // currently only used in test code
|
||||
pub fn hexstring_to_u8(hex: &str) -> Vec<u8> {
|
||||
let mut s = String::new();
|
||||
for c in hex.chars() {
|
||||
if c.is_ascii_hexdigit() {
|
||||
s.push(c);
|
||||
} else if c.is_whitespace() {
|
||||
// ignore
|
||||
} else {
|
||||
panic!("Invalid character '{c}'");
|
||||
}
|
||||
}
|
||||
if s.len() % 2 == 1 {
|
||||
panic!("Uneven # of hex characters in '{s}'");
|
||||
}
|
||||
let mut hex_bytes = s.as_bytes().iter().map_while(|b| match b {
|
||||
b'0'..=b'9' => Some(b - b'0'),
|
||||
b'a'..=b'f' => Some(b - b'a' + 10),
|
||||
b'A'..=b'F' => Some(b - b'A' + 10),
|
||||
_ => None,
|
||||
});
|
||||
let mut bytes = Vec::with_capacity(s.len());
|
||||
while let (Some(h), Some(l)) = (hex_bytes.next(), hex_bytes.next()) {
|
||||
bytes.push(h << 4 | l)
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Read sysfs file into string
|
||||
pub fn sysfs_read_string(fname: &str) -> Result<String, Box<dyn Error>> {
|
||||
let mut file = File::open(fname)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
let trimmed_content = String::from(content.trim());
|
||||
Ok(trimmed_content)
|
||||
}
|
||||
|
||||
/// Write string into sysfs file
|
||||
pub fn sysfs_write_string(fname: &str, value: &str) -> Result<(), Box<dyn Error>> {
|
||||
let mut file = OpenOptions::new().write(true).open(fname)?;
|
||||
file.write_all(value.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read sysfs file content and parse as i32 value
|
||||
pub fn sysfs_read_i32(fname: &str) -> Result<i32, Box<dyn Error>> {
|
||||
let content = sysfs_read_string(fname)?;
|
||||
Ok(content.parse::<i32>()?)
|
||||
}
|
||||
|
||||
/// Write an i32 value into a sysfs file
|
||||
pub fn sysfs_write_i32(fname: &str, value: i32) -> Result<(), Box<dyn Error>> {
|
||||
return sysfs_write_string(fname, &value.to_string());
|
||||
}
|
||||
|
||||
/// For a given (sysfs) directory construct a list of all subdirs
|
||||
/// and give it back as a vector of strings. If there is no subdir,
|
||||
/// the vector is empty.
|
||||
pub fn sysfs_get_list_of_subdirs(dname: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
||||
let mut v: Vec<String> = Vec::new();
|
||||
let entries = fs::read_dir(dname)?;
|
||||
for entry in entries.flatten() {
|
||||
let file_type = match entry.file_type() {
|
||||
Ok(ft) => ft,
|
||||
_ => continue,
|
||||
};
|
||||
if !file_type.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let fname = match entry.file_name().into_string() {
|
||||
Ok(s) => s,
|
||||
_ => continue,
|
||||
};
|
||||
v.push(fname);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// For a given (sysfs) directory construct a list of all subdirs which
|
||||
/// match to the given regular expression and give the list back as a
|
||||
/// vector of strings. If there is no subdir, the vector is empty.
|
||||
pub fn sysfs_get_list_of_subdirs_matching_regex(
|
||||
dname: &str,
|
||||
regex: &str,
|
||||
) -> Result<Vec<String>, Box<dyn Error>> {
|
||||
let mut v: Vec<String> = Vec::new();
|
||||
let re = Regex::new(regex)?;
|
||||
let entries = sysfs_get_list_of_subdirs(dname)?;
|
||||
for entry in entries {
|
||||
if re.is_match(&entry) {
|
||||
v.push(entry);
|
||||
}
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// LockFile for inter-process locking
|
||||
///
|
||||
/// Simple class for process locking for pvapconfig.
|
||||
/// The lock concept is simple: The existence of a file is used as the
|
||||
/// locking indicator. If the file exists something is locked, if it does
|
||||
/// not exist something is not locked. In the lock file the PID of the
|
||||
/// process created the file ("owning this file") is written in.
|
||||
/// With the ProcessLock object leaving scope the associated lock file
|
||||
/// is automatically deleted. It is assumed that the creation of a file
|
||||
/// is an atomic operation - that's true for most filesystems but may
|
||||
/// cause problems with network based filesystems.
|
||||
/// Example:
|
||||
/// ```
|
||||
/// let lock = LockFile::lock("/var/lock/process.lock");
|
||||
/// assert!(lock.is_ok());
|
||||
/// let lock2 = LockFile::lock("/var/lock/process.lock");
|
||||
/// assert!(lock2.is_err());
|
||||
/// drop(lock);
|
||||
/// let lock3 = LockFile::lock("/var/lock/process.lock");
|
||||
/// assert!(lock3.is_ok());
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct LockFile {
|
||||
lockfile: PathBuf,
|
||||
}
|
||||
|
||||
impl LockFile {
|
||||
/// Try to establish the lock file.
|
||||
/// Upon success the given file is fresh created and has the pid of this
|
||||
/// process written in. The function returns a new LockFile object
|
||||
/// which has implemented the Drop Trait. So with this object going out
|
||||
/// of scope the lock file is deleted. If establishing the lock file
|
||||
/// fails for any reason (for example the file already exists), the
|
||||
/// function fails with returning an Error string. This function does
|
||||
/// NOT panic if establishing the lock file fails for any reason. If
|
||||
/// the lock file could be esablished but writing in the PID fails, a
|
||||
/// warning is printed but the function continues with returning a
|
||||
/// LockFile object.
|
||||
pub fn try_lock(fname: &str) -> Result<Self, String> {
|
||||
let lockfile = PathBuf::from(fname);
|
||||
let mut file = match OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&lockfile)
|
||||
{
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Failure trying to create lock file {fname}: {err:?}."
|
||||
))
|
||||
}
|
||||
Ok(f) => f,
|
||||
};
|
||||
let _ = file
|
||||
.write(format!("{}", std::process::id()).as_bytes())
|
||||
.map_err(|err| {
|
||||
println!("Warning: could not write PID into lockfile {fname}: {err:?}.")
|
||||
});
|
||||
Ok(LockFile { lockfile })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LockFile {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.lockfile).map_err(|err| {
|
||||
println!(
|
||||
"Warning: could not remove lockfile {}: {err:?}.",
|
||||
self.lockfile.display()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
// Only very simple tests
|
||||
|
||||
const TEST_BYTES: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
|
||||
const TEST_HEXSTR: &str = "0123456789abcdef";
|
||||
|
||||
#[test]
|
||||
fn test_u8_to_hexstring() {
|
||||
let str = u8_to_hexstring(&TEST_BYTES);
|
||||
assert!(str == TEST_HEXSTR);
|
||||
}
|
||||
#[test]
|
||||
fn test_hexstring_to_u8() {
|
||||
let bytes = hexstring_to_u8(TEST_HEXSTR);
|
||||
assert!(bytes.as_slice() == TEST_BYTES);
|
||||
}
|
||||
#[test]
|
||||
fn test_sysfs_read_string() {
|
||||
let r = sysfs_read_string("/proc/cpuinfo");
|
||||
assert!(r.is_ok());
|
||||
}
|
||||
#[test]
|
||||
fn test_sysfs_read_i32() {
|
||||
let r = sysfs_read_i32("/proc/sys/kernel/random/entropy_avail");
|
||||
assert!(r.is_ok());
|
||||
}
|
||||
#[test]
|
||||
fn test_sysfs_get_list_of_subdirs() {
|
||||
let r = sysfs_get_list_of_subdirs("/proc/self");
|
||||
assert!(r.is_ok());
|
||||
let v = r.unwrap();
|
||||
assert!(!v.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn test_sysfs_get_list_of_subdirs_matching_regex() {
|
||||
let r = sysfs_get_list_of_subdirs_matching_regex("/proc/self", "fd.*");
|
||||
assert!(r.is_ok());
|
||||
let v = r.unwrap();
|
||||
assert!(!v.is_empty());
|
||||
for e in v {
|
||||
assert!(e.strip_prefix("fd").is_some());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_sysfs_write_i32() {
|
||||
const TEST_PATH: &str = "/tmp/test_sysfs_write_i32";
|
||||
let mut file = File::create(TEST_PATH).unwrap();
|
||||
let _ = file.write_all(b"XYZ");
|
||||
drop(file);
|
||||
let r = sysfs_read_i32(TEST_PATH);
|
||||
assert!(r.is_err());
|
||||
let r = sysfs_write_i32(TEST_PATH, 999);
|
||||
assert!(r.is_ok());
|
||||
let r = sysfs_read_i32(TEST_PATH);
|
||||
assert!(r.is_ok());
|
||||
let v = r.unwrap();
|
||||
assert!(v == 999);
|
||||
let _ = fs::remove_file(TEST_PATH);
|
||||
}
|
||||
#[test]
|
||||
fn test_lockfile() {
|
||||
let r1 = LockFile::try_lock(PATH_PVAPCONFIG_LOCK);
|
||||
assert!(r1.is_ok());
|
||||
let r2 = LockFile::try_lock(PATH_PVAPCONFIG_LOCK);
|
||||
assert!(r2.is_err());
|
||||
drop(r1);
|
||||
let r3 = LockFile::try_lock(PATH_PVAPCONFIG_LOCK);
|
||||
assert!(r3.is_ok());
|
||||
}
|
||||
}
|
||||
668
rust/pvapconfig/src/main.rs
Normal file
668
rust/pvapconfig/src/main.rs
Normal file
@@ -0,0 +1,668 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp. 2023
|
||||
//
|
||||
//! pvapconfig - Tool to automatically set up the AP configuration
|
||||
//! within an IBM Secure Execution guest.
|
||||
//
|
||||
|
||||
mod ap;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod helper;
|
||||
mod uv;
|
||||
|
||||
use ap::{Apqn, ApqnList};
|
||||
use cli::ARGS;
|
||||
use config::{ApConfigEntry, ApConfigList};
|
||||
use helper::{LockFile, PATH_PVAPCONFIG_LOCK};
|
||||
use pv::uv::{ListableSecretType, SecretList};
|
||||
use std::process::ExitCode;
|
||||
use utils::release_string;
|
||||
|
||||
/// Simple macro for
|
||||
/// if Cli::verbose() {
|
||||
/// print!(...);
|
||||
/// }
|
||||
macro_rules! info {
|
||||
($($arg:tt)*) => {{
|
||||
if ARGS.verbose() {
|
||||
print!($($arg)*);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
/// Simple macro for the main function only
|
||||
/// Does a eprintln of the arguments and then
|
||||
/// return with exit failure.
|
||||
macro_rules! println_and_exit_failure {
|
||||
($($arg:tt)*) => {{
|
||||
eprintln!($($arg)*);
|
||||
return ExitCode::FAILURE;
|
||||
}};
|
||||
}
|
||||
|
||||
/// Simple macro for the main function only
|
||||
/// Check if given object has is_err() true and
|
||||
/// then eprintln the unwrapped error and
|
||||
/// returns with exit failure.
|
||||
macro_rules! on_error_print_and_exit {
|
||||
($r:expr) => {
|
||||
if $r.is_err() {
|
||||
eprintln!("{}", $r.unwrap_err());
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
// handle version option
|
||||
if cli::ARGS.version {
|
||||
println!(
|
||||
"{} version {}\nCopyright IBM Corp. 2023",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
release_string!()
|
||||
);
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
// make sure only one pvapconfig instance is running
|
||||
let r = LockFile::try_lock(PATH_PVAPCONFIG_LOCK);
|
||||
on_error_print_and_exit!(r);
|
||||
let _lockfile = r.unwrap();
|
||||
|
||||
// AP bus check
|
||||
info!("Checking AP bus support and facilities...\n");
|
||||
let r = ap::check_ap_bus_support();
|
||||
on_error_print_and_exit!(r);
|
||||
let r = ap::ap_bus_has_apsb_support();
|
||||
on_error_print_and_exit!(r);
|
||||
info!("AP bus support and facilities are ok.\n");
|
||||
|
||||
// UV check
|
||||
info!("Checking UV support and environment...\n");
|
||||
if !pv::misc::pv_guest_bit_set() {
|
||||
println_and_exit_failure!("Failure: this is not a SE guest.");
|
||||
}
|
||||
let r = uv::has_list_secrets_facility();
|
||||
on_error_print_and_exit!(r);
|
||||
info!("UV support and environment is ok.\n");
|
||||
|
||||
// read configuration
|
||||
let configfile: &str = match &cli::ARGS.config {
|
||||
Some(f) => f,
|
||||
_ => cli::PATH_DEFAULT_CONFIG_FILE,
|
||||
};
|
||||
info!(
|
||||
"Reading AP configuration entries from file '{}'...\n",
|
||||
configfile
|
||||
);
|
||||
let apconfig: ApConfigList = match ApConfigList::read_and_validate_yaml_file(configfile) {
|
||||
Ok(apcfg) => apcfg,
|
||||
Err(err) => println_and_exit_failure!("{}", err),
|
||||
};
|
||||
if apconfig.is_empty() {
|
||||
println!(
|
||||
"No AP configuration entries in config file '{}': Nothing to do.",
|
||||
configfile
|
||||
);
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
info!("Found {} AP configuration entries.\n", apconfig.len());
|
||||
|
||||
// get list of secrets from UV
|
||||
info!("Fetching list of secrets from UV...\n");
|
||||
let secrets: SecretList = match uv::gather_secrets() {
|
||||
Err(e) => println_and_exit_failure!("{}", e),
|
||||
Ok(los) => los,
|
||||
};
|
||||
info!("Fetched {} Secret entries from UV.\n", secrets.len());
|
||||
|
||||
// Warning if no UV secrets given but AP config entries require it
|
||||
let non_accel_apc = apconfig
|
||||
.iter()
|
||||
.filter(|apc| apc.mode != config::STR_MODE_ACCEL)
|
||||
.count();
|
||||
if non_accel_apc > 0 && secrets.is_empty() {
|
||||
println!(
|
||||
"Warning: No UV Secrets given but at least one AP config entry requires a Secret."
|
||||
);
|
||||
}
|
||||
|
||||
info!("Waiting for AP bus bindings complete...\n");
|
||||
if !ap::wait_for_ap_bus_bindings_complete() {
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
info!("Fetching list of available APQNs...\n");
|
||||
let mut apqns: ApqnList = match ApqnList::gather_apqns() {
|
||||
Some(l) => l,
|
||||
None => return ExitCode::FAILURE,
|
||||
};
|
||||
if apqns.is_empty() {
|
||||
info!("List of available APQNs is empty: So there's nothing to do.\n");
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
info!("Found {} APQNs.\n", apqns.len());
|
||||
// check MK restriction
|
||||
if !apqns.check_mk_restriction() {
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
// now the real work
|
||||
info!("Applying AP configuration...\n");
|
||||
let n = match do_ap_config(&mut apqns, &secrets, &apconfig, false) {
|
||||
Err(e) => println_and_exit_failure!("{}", e),
|
||||
Ok(n) => n,
|
||||
};
|
||||
|
||||
if n == 0 {
|
||||
println_and_exit_failure!(
|
||||
"None out of {} AP config entries could be applied.",
|
||||
apconfig.len()
|
||||
);
|
||||
} else if ARGS.strict() && n != apconfig.len() {
|
||||
println_and_exit_failure!(
|
||||
"Strict flag given and only {} out of {} AP config entries have been applied.",
|
||||
n,
|
||||
apconfig.len()
|
||||
);
|
||||
}
|
||||
|
||||
info!(
|
||||
"Successfully applied {} out of {} AP config entries.\n",
|
||||
n,
|
||||
apconfig.len()
|
||||
);
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
/// The real worker function
|
||||
///
|
||||
/// This is the real algorithm which is trying to apply the
|
||||
/// AP configuration read from the config file to the existing
|
||||
/// APQNs with the info from the list of secrets from the UV.
|
||||
/// Returns the nr of AP config entries which are fullfilled
|
||||
/// after the function ended.
|
||||
/// apqns needs to be mutable as the function does a resort
|
||||
/// but content stays the same.
|
||||
fn do_ap_config(
|
||||
apqns: &mut ApqnList,
|
||||
secrets: &SecretList,
|
||||
apconfig: &ApConfigList,
|
||||
fntest: bool,
|
||||
) -> Result<usize, String> {
|
||||
let mut resolved_entries = 0;
|
||||
let mut apconfig_done = vec![false; apconfig.len()];
|
||||
let mut apqn_done = vec![false; apqns.len()];
|
||||
|
||||
// Preparation: Sort APQNs by generation.
|
||||
// All the following steps iterate through the list
|
||||
// of APQNs. So by sorting the APQNs starting with
|
||||
// highest card generation down to the older card
|
||||
// generations we prefer newer card generations over
|
||||
// older card generations.
|
||||
apqns.sort_by_gen();
|
||||
|
||||
// Step 1:
|
||||
// Go through all AP config entries and try to find an APQN
|
||||
// which already matches to this entry. If such an APQN is
|
||||
// found mark the AP config entry as done, and mark the APQN
|
||||
// as used so that entry and APQN will get skipped over in
|
||||
// the next steps.
|
||||
|
||||
for (ci, apc) in apconfig.iter().enumerate() {
|
||||
let cistr = if !apc.name.is_empty() {
|
||||
format!("#{} '{}'", ci + 1, apc.name)
|
||||
} else {
|
||||
format!("#{}", ci + 1)
|
||||
};
|
||||
for (ai, apqn) in apqns.iter().enumerate() {
|
||||
if apqn_done[ai] {
|
||||
continue;
|
||||
}
|
||||
if !config_and_apqn_match(apc, apqn) {
|
||||
continue;
|
||||
}
|
||||
if fntest {
|
||||
continue;
|
||||
}
|
||||
match apqn.mode {
|
||||
ap::ApqnMode::Accel => {
|
||||
// check bind state of this APQN
|
||||
let bind_state_ok = match apqn.bind_state() {
|
||||
Err(err) => {
|
||||
eprintln!("Warning: Failure reading APQN {apqn} bind state: {err}");
|
||||
false
|
||||
}
|
||||
Ok(ap::BindState::Bound) => true,
|
||||
Ok(_) => false,
|
||||
};
|
||||
if !bind_state_ok {
|
||||
continue;
|
||||
}
|
||||
// This APQN matches to the current AP config entry and is already bound.
|
||||
// So this AP config entry is satisfied: mark this config enty as done
|
||||
// and mark this APQN as used.
|
||||
info!("Accelerator APQN {apqn} already satisfies AP config entry {cistr}.\n");
|
||||
apconfig_done[ci] = true;
|
||||
apqn_done[ai] = true;
|
||||
resolved_entries += 1;
|
||||
break;
|
||||
}
|
||||
ap::ApqnMode::Ep11 => {
|
||||
// check association state of this APQN
|
||||
let (assoc_state_ok, assoc_idx) = match apqn.associate_state() {
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"Warning: Failure reading APQN {apqn} associate state: {err}"
|
||||
);
|
||||
(false, 0)
|
||||
}
|
||||
Ok(ap::AssocState::Associated(idx)) => (true, idx),
|
||||
Ok(_) => (false, 0),
|
||||
};
|
||||
if !assoc_state_ok {
|
||||
continue;
|
||||
}
|
||||
// check association index
|
||||
let r = secrets.iter().find(|&se| {
|
||||
se.stype() == ListableSecretType::Association
|
||||
&& se.id().len() == uv::AP_ASSOC_SECRET_ID_SIZE
|
||||
&& se.index() == assoc_idx
|
||||
&& helper::u8_to_hexstring(se.id()) == apc.secretid
|
||||
});
|
||||
if r.is_none() {
|
||||
continue;
|
||||
}
|
||||
// This APQN matches to the current AP config entry and is already
|
||||
// associated with the right secret id. So this AP config entry is
|
||||
// satisfied: mark this config enty as done and mark this APQN as used.
|
||||
info!("EP11 APQN {apqn} already satisfies AP config entry {cistr}.\n");
|
||||
apconfig_done[ci] = true;
|
||||
apqn_done[ai] = true;
|
||||
resolved_entries += 1;
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
// (currently) unknown/unsupported APQN mode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2:
|
||||
// All APQNs NOT marked as done are now examined for their bind
|
||||
// and association state and maybe reset to "unbound".
|
||||
|
||||
for (ai, apqn) in apqns.iter().enumerate() {
|
||||
if apqn_done[ai] || fntest {
|
||||
continue;
|
||||
}
|
||||
match apqn.bind_state() {
|
||||
Err(err) => eprintln!("Warning: Failure reading APQN {apqn} bind state: {err}"),
|
||||
Ok(ap::BindState::Bound) => {
|
||||
info!("Unbind APQN {apqn} as this bind/associate does not match to any AP config entry.\n");
|
||||
if !ARGS.dryrun() {
|
||||
if let Err(err) = apqn.set_bind_state(ap::BindState::Unbound) {
|
||||
return Err(format!("Failure unbinding APQN {apqn}: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
};
|
||||
}
|
||||
|
||||
// Step 3:
|
||||
// Go through all remaining AP config entries and try to fullfill each
|
||||
// by searching for an APQN which would match to this config entry and
|
||||
// then prepare this APQN (bind, maybe associate).
|
||||
for (ci, apc) in apconfig.iter().enumerate() {
|
||||
let cistr = if !apc.name.is_empty() {
|
||||
format!("#{} '{}'", ci + 1, apc.name)
|
||||
} else {
|
||||
format!("#{}", ci + 1)
|
||||
};
|
||||
if apconfig_done[ci] {
|
||||
continue;
|
||||
}
|
||||
for (ai, apqn) in apqns.iter().enumerate() {
|
||||
if apqn_done[ai] {
|
||||
continue;
|
||||
}
|
||||
if !config_and_apqn_match(apc, apqn) {
|
||||
continue;
|
||||
}
|
||||
match apqn.mode {
|
||||
ap::ApqnMode::Accel => {
|
||||
// try to bind this accelerator APQN
|
||||
if ARGS.verbose() || fntest {
|
||||
println!("Bind APQN {apqn} to match to AP config entry {cistr}.");
|
||||
}
|
||||
if !(ARGS.dryrun() || fntest) {
|
||||
if let Err(err) = apqn.set_bind_state(ap::BindState::Bound) {
|
||||
// bind failed, unbind/reset this apqn, return with failure
|
||||
let _ = apqn.set_bind_state(ap::BindState::Unbound);
|
||||
return Err(format!("Failure binding APQN {apqn}: {err}"));
|
||||
}
|
||||
}
|
||||
apconfig_done[ci] = true;
|
||||
apqn_done[ai] = true;
|
||||
resolved_entries += 1;
|
||||
break;
|
||||
}
|
||||
ap::ApqnMode::Ep11 => {
|
||||
// EP11 needs bind and associate, but before doing this let's
|
||||
// check out which secret index to use with the associate
|
||||
let se = match secrets.iter().find(|&se| {
|
||||
se.stype() == ListableSecretType::Association
|
||||
&& se.id().len() == uv::AP_ASSOC_SECRET_ID_SIZE
|
||||
&& helper::u8_to_hexstring(se.id()) == apc.secretid
|
||||
}) {
|
||||
None => {
|
||||
eprintln!("Warning: Secret id '{}' from config entry {} not found in UV secrets list.",
|
||||
apc.secretid, cistr);
|
||||
break;
|
||||
}
|
||||
Some(se) => se,
|
||||
};
|
||||
// try to bind
|
||||
if ARGS.verbose() || fntest {
|
||||
println!(
|
||||
"Bind APQN {apqn} to match to AP config entry {cistr} (step 1/2)."
|
||||
);
|
||||
}
|
||||
if !(ARGS.dryrun() || fntest) {
|
||||
if let Err(err) = apqn.set_bind_state(ap::BindState::Bound) {
|
||||
// bind failed, unbind/reset this apqn, return with failure
|
||||
let _ = apqn.set_bind_state(ap::BindState::Unbound);
|
||||
return Err(format!("Failure binding APQN {}: {}", apqn, err));
|
||||
}
|
||||
}
|
||||
// try to associate
|
||||
if ARGS.verbose() || fntest {
|
||||
println!(
|
||||
"Associate APQN {} with uv secrets index {} to match AP config entry {} (step 2/2).",
|
||||
apqn, se.index(), cistr
|
||||
);
|
||||
}
|
||||
if !(ARGS.dryrun() || fntest) {
|
||||
let apas = ap::AssocState::Associated(se.index());
|
||||
apqn.set_associate_state(apas)
|
||||
.map_err(|err| format!("Failure associating APQN {apqn}: {err}"))?;
|
||||
}
|
||||
apconfig_done[ci] = true;
|
||||
apqn_done[ai] = true;
|
||||
resolved_entries += 1;
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
// (currently) unknown/unsupported APQN mode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved_entries)
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
/// Panics if mingen for an accelerator has not a number as the 4th character.
|
||||
/// Panics if mingen for an ep11 has not a number as the 4th character.
|
||||
/// Please note this can not happen, as mingen is already checked via RE
|
||||
/// during storing the value into mingen.
|
||||
fn config_and_apqn_match(apc: &ApConfigEntry, apqn: &Apqn) -> bool {
|
||||
if apc.mode == config::STR_MODE_ACCEL && apqn.mode == ap::ApqnMode::Accel {
|
||||
// config and apqn are accelerators
|
||||
// maybe check mingen
|
||||
if !apc.mingen.is_empty() {
|
||||
let mingen = &apc.mingen[3..].parse::<u32>().unwrap();
|
||||
if mingen < &apqn.gen {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if apc.mode == config::STR_MODE_EP11 && apqn.mode == ap::ApqnMode::Ep11 {
|
||||
// config and apqn are ep11
|
||||
let info = match &apqn.info {
|
||||
Some(ap::ApqnInfo::Ep11(i)) => i,
|
||||
_ => return false,
|
||||
};
|
||||
// maybe check mingen
|
||||
if !apc.mingen.is_empty() {
|
||||
let mingen = &apc.mingen[3..].parse::<u32>().unwrap();
|
||||
if mingen < &apqn.gen {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// maybe check serialnr
|
||||
if !apc.serialnr.is_empty() && apc.serialnr != info.serialnr {
|
||||
return false;
|
||||
}
|
||||
// check mkvp, currently an ep11 config entry must state an mkvp value
|
||||
// whereas an ep11 info from an APQN may have an empty mkvp value to
|
||||
// indicate that there is no WK set on this APQN.
|
||||
if apc.mkvp != info.mkvp {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use helper::hexstring_to_u8;
|
||||
use pv::uv::SecretEntry;
|
||||
|
||||
// This is more or less only a test for the do_ap_config() function
|
||||
// However, this is THE main functionality of the whole application.
|
||||
|
||||
fn make_test_apqns() -> Vec<Apqn> {
|
||||
let mut v = Vec::new();
|
||||
v.push(ap::Apqn {
|
||||
name: String::from("10.0007"),
|
||||
card: 16,
|
||||
domain: 7,
|
||||
gen: 8,
|
||||
mode: ap::ApqnMode::Accel,
|
||||
info: Option::Some(ap::ApqnInfo::Accel(ap::ApqnInfoAccel {})),
|
||||
});
|
||||
v.push(ap::Apqn {
|
||||
name: String::from("11.0008"),
|
||||
card: 17,
|
||||
domain: 8,
|
||||
gen: 8,
|
||||
mode: ap::ApqnMode::Ep11,
|
||||
info: Option::Some(ap::ApqnInfo::Ep11(ap::ApqnInfoEp11 {
|
||||
serialnr: String::from("93AADFK719460083"),
|
||||
mkvp: String::from("db3c3b3c3f097dd55ec7eb0e7fdbcb93"),
|
||||
})),
|
||||
});
|
||||
v.push(ap::Apqn {
|
||||
name: String::from("12.0009"),
|
||||
card: 18,
|
||||
domain: 9,
|
||||
gen: 8,
|
||||
mode: ap::ApqnMode::Ep11,
|
||||
info: Option::Some(ap::ApqnInfo::Ep11(ap::ApqnInfoEp11 {
|
||||
serialnr: String::from("93AADHZU42082261"),
|
||||
mkvp: String::from("4a27bb66520ac85f6073a7f678d262c0"),
|
||||
})),
|
||||
});
|
||||
v.push(ap::Apqn {
|
||||
name: String::from("12.000a"),
|
||||
card: 18,
|
||||
domain: 10,
|
||||
gen: 8,
|
||||
mode: ap::ApqnMode::Ep11,
|
||||
info: Option::Some(ap::ApqnInfo::Ep11(ap::ApqnInfoEp11 {
|
||||
serialnr: String::from("93AADHZU42082261"),
|
||||
mkvp: String::from("383d2a9ab781f35343554c5b3d9337cd"),
|
||||
})),
|
||||
});
|
||||
v.push(ap::Apqn {
|
||||
name: String::from("13.000d"),
|
||||
card: 19,
|
||||
domain: 13,
|
||||
gen: 8,
|
||||
mode: ap::ApqnMode::Ep11,
|
||||
info: Option::Some(ap::ApqnInfo::Ep11(ap::ApqnInfoEp11 {
|
||||
serialnr: String::from("87HU397G150TZGR"),
|
||||
mkvp: String::new(),
|
||||
})),
|
||||
});
|
||||
v.push(ap::Apqn {
|
||||
name: String::from("13.000f"),
|
||||
card: 19,
|
||||
domain: 15,
|
||||
gen: 8,
|
||||
mode: ap::ApqnMode::Ep11,
|
||||
info: Option::None,
|
||||
});
|
||||
return v;
|
||||
}
|
||||
|
||||
fn make_assoc_secretentry(idx: u16, hexidstr: &str) -> SecretEntry {
|
||||
let id = hexstring_to_u8(hexidstr);
|
||||
let idlen: u32 = id.len().try_into().unwrap();
|
||||
let idarray = <&[u8; 32]>::try_from(id.as_slice()).unwrap();
|
||||
SecretEntry::new(idx, ListableSecretType::Association, *idarray, idlen)
|
||||
}
|
||||
|
||||
fn make_test_secrets() -> Vec<SecretEntry> {
|
||||
let mut v: Vec<SecretEntry> = Vec::new();
|
||||
v.push(make_assoc_secretentry(
|
||||
33,
|
||||
"3333333333333333333333333333333333333333333333333333333333333333",
|
||||
));
|
||||
v.push(make_assoc_secretentry(
|
||||
13,
|
||||
"bc9d46c052bc3574454c5715757274629a283767ed237922cfb8651c0e77320a",
|
||||
));
|
||||
v.push(make_assoc_secretentry(
|
||||
44,
|
||||
"4444444444444444444444444444444444444444444444444444444444444444",
|
||||
));
|
||||
v.push(make_assoc_secretentry(
|
||||
15,
|
||||
"06cdbbac76a595b481110d108154bc05ebbf900a0f16e36a24045998934fb1e9",
|
||||
));
|
||||
v.push(make_assoc_secretentry(
|
||||
17,
|
||||
"6831af07f8c8e7309a3ace9f3b5554d34e3eaa4a27a08fdee469e367c3fa3e9e",
|
||||
));
|
||||
return v;
|
||||
}
|
||||
|
||||
fn make_test_apconfigs() -> Vec<ApConfigEntry> {
|
||||
let mut v: Vec<ApConfigEntry> = Vec::new();
|
||||
v.push(config::ApConfigEntry {
|
||||
name: String::from("test_1"),
|
||||
description: String::from("test_1"),
|
||||
mode: String::from("accel"),
|
||||
mkvp: String::from(""),
|
||||
serialnr: String::from(""),
|
||||
mingen: String::from("cex8"),
|
||||
secretid: String::from(""),
|
||||
});
|
||||
v.push(config::ApConfigEntry {
|
||||
name: String::from("test_2"),
|
||||
description: String::from("test_2"),
|
||||
mode: String::from("ep11"),
|
||||
mkvp: String::from("db3c3b3c3f097dd55ec7eb0e7fdbcb93"),
|
||||
serialnr: String::from("93AADFK719460083"),
|
||||
mingen: String::from("cex8"),
|
||||
secretid: String::from(
|
||||
"bc9d46c052bc3574454c5715757274629a283767ed237922cfb8651c0e77320a",
|
||||
),
|
||||
});
|
||||
v.push(config::ApConfigEntry {
|
||||
name: String::from("test_3"),
|
||||
description: String::from("test_3"),
|
||||
mode: String::from("ep11"),
|
||||
mkvp: String::from("4a27bb66520ac85f6073a7f678d262c0"),
|
||||
serialnr: String::from(""),
|
||||
mingen: String::from("cex8"),
|
||||
secretid: String::from(
|
||||
"06cdbbac76a595b481110d108154bc05ebbf900a0f16e36a24045998934fb1e9",
|
||||
),
|
||||
});
|
||||
v.push(config::ApConfigEntry {
|
||||
name: String::from("test_4"),
|
||||
description: String::from("test_4"),
|
||||
mode: String::from("ep11"),
|
||||
mkvp: String::from("8be1eaf5c44e2fa8b18804551b604b1b"),
|
||||
serialnr: String::from(""),
|
||||
mingen: String::from("cex8"),
|
||||
secretid: String::from(
|
||||
"6831af07f8c8e7309a3ace9f3b5554d34e3eaa4a27a08fdee469e367c3fa3e9e",
|
||||
),
|
||||
});
|
||||
return v;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_do_ap_config_invocation_1() {
|
||||
let test_apqns = make_test_apqns();
|
||||
let mut apqns: Vec<Apqn> = Vec::new();
|
||||
apqns.push(test_apqns[0].clone());
|
||||
let secrets: Vec<SecretEntry> = Vec::new();
|
||||
let secretlist = SecretList::new(secrets.len() as u16, secrets);
|
||||
let test_apconfigs = make_test_apconfigs();
|
||||
let mut apconfig: Vec<ApConfigEntry> = Vec::new();
|
||||
apconfig.push(test_apconfigs[0].clone());
|
||||
let apcfglist = ApConfigList::from_apconfigentry_vec(apconfig);
|
||||
let mut apqnlist = ApqnList::from_apqn_vec(apqns);
|
||||
let r = do_ap_config(&mut apqnlist, &secretlist, &apcfglist, true);
|
||||
assert!(r.is_ok());
|
||||
let n = r.unwrap();
|
||||
assert!(n == 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_do_ap_config_invocation_2() {
|
||||
let test_apqns = make_test_apqns();
|
||||
let mut apqns: Vec<Apqn> = Vec::new();
|
||||
apqns.push(test_apqns[1].clone());
|
||||
let mut secrets = make_test_secrets();
|
||||
while secrets.len() > 2 {
|
||||
secrets.pop();
|
||||
}
|
||||
let secretlist = SecretList::new(secrets.len() as u16, secrets);
|
||||
let test_apconfigs = make_test_apconfigs();
|
||||
let mut apconfig: Vec<ApConfigEntry> = Vec::new();
|
||||
apconfig.push(test_apconfigs[1].clone());
|
||||
let apcfglist = ApConfigList::from_apconfigentry_vec(apconfig);
|
||||
let mut apqnlist = ApqnList::from_apqn_vec(apqns);
|
||||
let r = do_ap_config(&mut apqnlist, &secretlist, &apcfglist, true);
|
||||
assert!(r.is_ok());
|
||||
let n = r.unwrap();
|
||||
assert!(n == 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_do_ap_config_invocation_3() {
|
||||
let test_apqns = make_test_apqns();
|
||||
let mut apqns: Vec<Apqn> = Vec::new();
|
||||
for a in test_apqns.iter() {
|
||||
apqns.push(a.clone());
|
||||
}
|
||||
apqns.reverse();
|
||||
let secrets = make_test_secrets();
|
||||
let secretlist = SecretList::new(secrets.len() as u16, secrets);
|
||||
let test_apconfigs = make_test_apconfigs();
|
||||
let mut apconfig: Vec<ApConfigEntry> = Vec::new();
|
||||
for c in test_apconfigs.iter() {
|
||||
apconfig.push(c.clone());
|
||||
}
|
||||
let apcfglist = ApConfigList::from_apconfigentry_vec(apconfig);
|
||||
let mut apqnlist = ApqnList::from_apqn_vec(apqns);
|
||||
let r = do_ap_config(&mut apqnlist, &secretlist, &apcfglist, true);
|
||||
assert!(r.is_ok());
|
||||
let n = r.unwrap();
|
||||
assert!(n == 3, "n = {} != 3", n);
|
||||
}
|
||||
}
|
||||
105
rust/pvapconfig/src/uv.rs
Normal file
105
rust/pvapconfig/src/uv.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Copyright IBM Corp. 2023
|
||||
//
|
||||
//! UV related functions for pvapconfig
|
||||
//
|
||||
|
||||
use pv::uv::{ListCmd, SecretList, UvDevice, UvcSuccess};
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
/// The byte size of association secret of type 2 in struct SecretEntry
|
||||
pub const AP_ASSOC_SECRET_ID_SIZE: usize = 32;
|
||||
|
||||
const PATH_SYS_FW_UV_FACILITIES: &str = "/sys/firmware/uv/query/facilities";
|
||||
|
||||
const RE_UV_FACILITIES: &str = r"^(0x)?([[:xdigit:]]+)";
|
||||
const RE_UV_FAC_BIT_LIST_SECRETS: u32 = 30;
|
||||
|
||||
/// Check UV facilities to offer the 'list secrets' call.
|
||||
/// Returns a Result with Ok(()) if the 'list secrets' feature
|
||||
/// is available, otherwise an Err(reasonstring) is returned where
|
||||
/// the string denotes a hint which can be displayed.
|
||||
/// # Panics
|
||||
/// Panics if the compilation of a static regular expression fails.
|
||||
/// Panics if RE_UV_FACILITIES does not match.
|
||||
pub fn has_list_secrets_facility() -> Result<(), String> {
|
||||
if !Path::new(PATH_SYS_FW_UV_FACILITIES).is_file() {
|
||||
return Err(format!(
|
||||
"UV facilities sysfs attribute not found (file {} does not exist).",
|
||||
PATH_SYS_FW_UV_FACILITIES
|
||||
));
|
||||
}
|
||||
let facstr = match crate::helper::sysfs_read_string(PATH_SYS_FW_UV_FACILITIES) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Failure reading UV facilities from {PATH_SYS_FW_UV_FACILITIES} ({:?}).",
|
||||
err
|
||||
))
|
||||
}
|
||||
};
|
||||
let re_uv_facilities = Regex::new(RE_UV_FACILITIES).unwrap();
|
||||
if !re_uv_facilities.is_match(&facstr) {
|
||||
Err(format!("Failure parsing UV facilities entry '{facstr}'."))
|
||||
} else {
|
||||
let caps = re_uv_facilities.captures(&facstr).unwrap();
|
||||
let fachex = caps.get(2).unwrap().as_str();
|
||||
let i: usize = RE_UV_FAC_BIT_LIST_SECRETS as usize / 4;
|
||||
if i >= fachex.len() {
|
||||
return Err(format!("Failure parsing UV facilities entry '{fachex}'."));
|
||||
}
|
||||
let nibble = u32::from_str_radix(&fachex[i..i + 1], 16).unwrap();
|
||||
const THEBIT: u32 = 1 << (3 - (RE_UV_FAC_BIT_LIST_SECRETS % 4));
|
||||
if nibble & THEBIT == 0 {
|
||||
return Err("The 'list secret' feature is missing on this UV.".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the list of secrets from the UV.
|
||||
/// Returns Err(errorstring) on error or
|
||||
/// Ok(SecretList) on success.
|
||||
/// The list may be empty if the UV doesn't have any secrets stored.
|
||||
pub fn gather_secrets() -> Result<SecretList, String> {
|
||||
let uv = match UvDevice::open() {
|
||||
Err(e) => return Err(format!("Failed to open UV device: {:?}.", e)),
|
||||
Ok(u) => u,
|
||||
};
|
||||
let mut cmd = ListCmd::default();
|
||||
match uv.send_cmd(&mut cmd).map_err(|e| format!("{e:?}"))? {
|
||||
UvcSuccess::RC_SUCCESS => (),
|
||||
UvcSuccess::RC_MORE_DATA => println!("Warning: There is more data available than expected"),
|
||||
};
|
||||
cmd.try_into().map_err(|e| format!("{e:?}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
// As the name says: check for list secrets feature bit in UV facilities.
|
||||
#[test]
|
||||
fn test_has_list_secrets_facility() {
|
||||
let r = has_list_secrets_facility();
|
||||
if pv::misc::pv_guest_bit_set() {
|
||||
assert!(r.is_ok());
|
||||
} else {
|
||||
assert!(r.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
// Simple invocation of the list_secrets function. Should not fail
|
||||
#[test]
|
||||
fn test_list_secrets() {
|
||||
let r = gather_secrets();
|
||||
if pv::misc::pv_guest_bit_set() {
|
||||
assert!(r.is_ok());
|
||||
} else {
|
||||
assert!(r.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user