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:
Harald Freudenberger
2023-12-01 12:10:20 +01:00
committed by Jan Höppner
parent 0764460eaf
commit 94a38ebc3a
12 changed files with 2536 additions and 1 deletions
+817
View 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
View 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
View 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
View 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
View 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
View 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());
}
}
}