diff --git a/profiles/pci/power_management/profiles.toml b/profiles/pci/power_management/profiles.toml new file mode 100644 index 0000000..7484cbd --- /dev/null +++ b/profiles/pci/power_management/profiles.toml @@ -0,0 +1,15 @@ +[intel-lpmd] +desc = 'Intel Low Power Mode Daemon for supported hybrid Intel CPUs' +class_ids = "0600" +vendor_ids = "8086" +device_ids = "*" +cpu_family = "6" +cpu_models = "151 154 183 186 191 170 172 189 204" +priority = 5 +packages = 'intel-lpmd' +post_install = """ + systemctl enable --now intel_lpmd.service +""" +post_remove = """ + systemctl disable intel_lpmd.service +""" diff --git a/src/data.rs b/src/data.rs index 15b430f..23ff0ff 100644 --- a/src/data.rs +++ b/src/data.rs @@ -248,6 +248,13 @@ pub fn get_all_devices_of_profile(devices: &ListOfDevicesT, profile: &Profile) - } } + if profile.cpu_family.is_some() { + match crate::hwd_misc::get_cpu_info() { + Some(cpu_info) if matches_cpu_filter(profile, &cpu_info) => {}, + _ => return vec![], + } + } + if let Some(gc_versions) = &profile.gc_versions { if let Some(hwd_gc_versions) = crate::hwd_misc::get_gc_versions() { return get_all_devices_from_gc_versions(devices, &hwd_gc_versions, gc_versions); @@ -320,6 +327,20 @@ pub fn get_all_devices_of_profile(devices: &ListOfDevicesT, profile: &Profile) - found_indices } +fn matches_cpu_filter(profile: &Profile, cpu_info: &crate::hwd_misc::CpuInfo) -> bool { + if let Some(cpu_family) = &profile.cpu_family { + if cpu_info.family != *cpu_family { + return false; + } + } + if let Some(cpu_models) = &profile.cpu_models { + if !cpu_models.contains(&cpu_info.model) { + return false; + } + } + true +} + fn add_profile_sorted(profiles: &mut Vec>, new_profile: &Profile) { if profiles.iter().any(|x| new_profile.name == x.name) { return; @@ -772,6 +793,63 @@ mod tests { ] } + fn cpu_test_profile( + cpu_family: Option<&str>, + cpu_models: Option>, + ) -> crate::profile::Profile { + crate::profile::Profile { + cpu_family: cpu_family.map(str::to_string), + cpu_models: cpu_models + .map(|v| v.into_iter().map(str::to_string).collect()), + ..Default::default() + } + } + + #[test] + fn cpu_filter_matches_family_and_model() { + let cpu_info = + crate::hwd_misc::CpuInfo { vendor: "GenuineIntel".into(), family: "6".into(), model: "154".into() }; + let profile = cpu_test_profile(Some("6"), Some(vec!["151", "154", "183"])); + + assert!(data::matches_cpu_filter(&profile, &cpu_info)); + } + + #[test] + fn cpu_filter_rejects_wrong_family() { + let cpu_info = + crate::hwd_misc::CpuInfo { vendor: "AuthenticAMD".into(), family: "25".into(), model: "80".into() }; + let profile = cpu_test_profile(Some("6"), Some(vec!["151", "154"])); + + assert!(!data::matches_cpu_filter(&profile, &cpu_info)); + } + + #[test] + fn cpu_filter_rejects_wrong_model() { + let cpu_info = + crate::hwd_misc::CpuInfo { vendor: "GenuineIntel".into(), family: "6".into(), model: "142".into() }; + let profile = cpu_test_profile(Some("6"), Some(vec!["151", "154"])); + + assert!(!data::matches_cpu_filter(&profile, &cpu_info)); + } + + #[test] + fn cpu_filter_family_only_matches() { + let cpu_info = + crate::hwd_misc::CpuInfo { vendor: "GenuineIntel".into(), family: "6".into(), model: "999".into() }; + let profile = cpu_test_profile(Some("6"), None); + // no cpu_models filter — any model in family 6 should match + assert!(data::matches_cpu_filter(&profile, &cpu_info)); + } + + #[test] + fn cpu_filter_no_filter_matches_all() { + let cpu_info = + crate::hwd_misc::CpuInfo { vendor: "AuthenticAMD".into(), family: "25".into(), model: "80".into() }; + let profile = cpu_test_profile(None, None); + // no cpu_family, no cpu_models + assert!(data::matches_cpu_filter(&profile, &cpu_info)); + } + #[test] fn get_devices_from_gc_versions() { let devices = test_data(); diff --git a/src/hwd_misc.rs b/src/hwd_misc.rs index bb4964c..c741a01 100644 --- a/src/hwd_misc.rs +++ b/src/hwd_misc.rs @@ -14,6 +14,42 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +pub struct CpuInfo { + pub vendor: String, + pub family: String, + pub model: String, +} + +#[must_use] +pub fn get_cpu_info() -> Option { + let content = std::fs::read_to_string("/proc/cpuinfo").ok()?; + parse_cpu_info(&content) +} + +fn parse_cpu_info(content: &str) -> Option { + let mut vendor = None; + let mut family = None; + let mut model = None; + + for line in content.lines() { + if let Some((key, value)) = line.split_once(':') { + let key = key.trim(); + let value = value.trim(); + match key { + "vendor_id" if vendor.is_none() => vendor = Some(value.to_owned()), + "cpu family" if family.is_none() => family = Some(value.to_owned()), + "model" if model.is_none() => model = Some(value.to_owned()), + _ => {}, + } + } + if vendor.is_some() && family.is_some() && model.is_some() { + break; + } + } + + Some(CpuInfo { vendor: vendor?, family: family?, model: model? }) +} + #[must_use] pub fn get_sysfs_busid_from_amdgpu_path(amdgpu_path: &str) -> &str { amdgpu_path.split('/') @@ -111,4 +147,42 @@ mod tests { assert_eq!(hwd_misc::get_sysfs_busid_from_amdgpu_path("/sys/bus/pci/drivers/amdgpu/"), ""); } + + #[test] + fn parse_cpu_info_intel() { + let cpuinfo = "\ +processor\t: 0 +vendor_id\t: GenuineIntel +cpu family\t: 6 +model\t\t: 154 +model name\t: 12th Gen Intel(R) Core(TM) i7-1260P +stepping\t: 4 +microcode\t: 0x432 +"; + let info = super::parse_cpu_info(cpuinfo).unwrap(); + assert_eq!(info.vendor, "GenuineIntel"); + assert_eq!(info.family, "6"); + assert_eq!(info.model, "154"); + } + + #[test] + fn parse_cpu_info_amd() { + let cpuinfo = "\ +processor\t: 0 +vendor_id\t: AuthenticAMD +cpu family\t: 25 +model\t\t: 80 +model name\t: AMD Ryzen 7 5800X 8-Core Processor +stepping\t: 2 +"; + let info = super::parse_cpu_info(cpuinfo).unwrap(); + assert_eq!(info.vendor, "AuthenticAMD"); + assert_eq!(info.family, "25"); + assert_eq!(info.model, "80"); + } + + #[test] + fn parse_cpu_info_empty() { + assert!(super::parse_cpu_info("").is_none()); + } } diff --git a/src/profile.rs b/src/profile.rs index 0181216..173338d 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -46,20 +46,40 @@ pub struct Profile { pub device_name_pattern: Option, pub hwd_product_name_pattern: Option, pub gc_versions: Option>, + pub cpu_family: Option, + pub cpu_models: Option>, pub hwd_ids: Vec, } impl Default for Profile { fn default() -> Self { - Self::new() + Self { + is_ai_sdk: false, + prof_path: String::new(), + name: String::new(), + desc: String::new(), + priority: 0, + packages: String::new(), + post_install: String::new(), + post_remove: String::new(), + pre_install: String::new(), + pre_remove: String::new(), + conditional_packages: String::new(), + device_name_pattern: None, + hwd_product_name_pattern: None, + gc_versions: None, + cpu_family: None, + cpu_models: None, + hwd_ids: vec![Default::default()], + } } } impl Profile { #[must_use] pub fn new() -> Self { - Self { hwd_ids: vec![Default::default()], ..Default::default() } + Self::default() } } @@ -224,13 +244,18 @@ fn parse_profile(node: &toml::Table, profile_name: &str) -> Result { hwd_product_name_pattern: node .get("hwd_product_name_pattern") .and_then(|x| x.as_str().map(str::to_string)), - gc_versions: node.get("gc_versions").and_then(|x| { - x.as_str() - .map(str::split_ascii_whitespace) - .map(|x| x.map(str::to_string).collect::>()) - }), + gc_versions: parse_whitespace_list(node, "gc_versions"), + cpu_family: node.get("cpu_family").and_then(|x| x.as_str().map(str::to_string)), + cpu_models: parse_whitespace_list(node, "cpu_models"), }; + if profile.cpu_models.is_some() && profile.cpu_family.is_none() { + let msg = + format!("profile '{profile_name}' specifies cpu_models without cpu_family"); + eprintln!("Warning: skipping profile '{profile_name}': {msg}"); + anyhow::bail!(msg); + } + let conf_devids = node.get("device_ids").and_then(|x| x.as_str()).unwrap_or(""); let conf_vendorids = node.get("vendor_ids").and_then(|x| x.as_str()).unwrap_or(""); let conf_classids = node.get("class_ids").and_then(|x| x.as_str()).unwrap_or(""); @@ -302,6 +327,10 @@ fn parse_ids_file(file_path: &str) -> Result { Ok(parsed_ids.split_ascii_whitespace().collect::>().join(" ")) } +fn parse_whitespace_list(node: &toml::Table, key: &str) -> Option> { + node.get(key)?.as_str().map(|s| s.split_ascii_whitespace().map(str::to_string).collect()) +} + fn merge_table_left(lhs: &mut toml::Table, rhs: &toml::Table) { for (rhs_key, rhs_val) in rhs { // rhs key not found in lhs - direct move @@ -414,7 +443,13 @@ fn profile_into_toml(profile: &Profile) -> toml::Table { table.insert("hwd_product_name_pattern".to_owned(), product_name_pattern.clone().into()); } if let Some(gc_versions) = &profile.gc_versions { - table.insert("gc_versions".to_owned(), gc_versions.clone().into()); + table.insert("gc_versions".to_owned(), gc_versions.join(" ").into()); + } + if let Some(cpu_family) = &profile.cpu_family { + table.insert("cpu_family".to_owned(), cpu_family.clone().into()); + } + if let Some(cpu_models) = &profile.cpu_models { + table.insert("cpu_models".to_owned(), cpu_models.join(" ").into()); } let last_hwd_id = profile.hwd_ids.last().unwrap(); @@ -686,4 +721,46 @@ mod tests { assert_eq!(orig_content, expected_output); } + + #[test] + fn cpu_profile_parse_test() { + let prof_path = "tests/profiles/cpu-profile-test.toml"; + let parsed_profiles = parse_profiles(prof_path); + assert!(parsed_profiles.is_ok()); + let parsed_profiles = parsed_profiles.unwrap(); + + assert_eq!(parsed_profiles.len(), 1); + assert_eq!(parsed_profiles[0].name, "intel-lpmd"); + assert_eq!( + parsed_profiles[0].desc, + "Intel Low Power Mode Daemon for supported hybrid Intel CPUs" + ); + assert_eq!(parsed_profiles[0].priority, 5); + assert_eq!(parsed_profiles[0].packages, "intel-lpmd"); + assert_eq!(parsed_profiles[0].cpu_family, Some("6".to_owned())); + assert_eq!( + parsed_profiles[0].cpu_models, + Some(vec![ + "151".to_owned(), + "154".to_owned(), + "183".to_owned(), + "186".to_owned(), + "191".to_owned(), + "170".to_owned(), + "172".to_owned(), + "189".to_owned(), + "204".to_owned(), + ]) + ); + assert!(!parsed_profiles[0].post_install.is_empty()); + assert!(!parsed_profiles[0].post_remove.is_empty()); + } + + #[test] + fn cpu_models_without_family_is_rejected() { + let prof_path = "tests/profiles/cpu-models-no-family-test.toml"; + let parsed_profiles = parse_profiles(prof_path); + // Should fail because cpu_models is set without cpu_family + assert!(parsed_profiles.is_err() || parsed_profiles.unwrap().is_empty()); + } } diff --git a/tests/profiles/cpu-models-no-family-test.toml b/tests/profiles/cpu-models-no-family-test.toml new file mode 100644 index 0000000..39584dd --- /dev/null +++ b/tests/profiles/cpu-models-no-family-test.toml @@ -0,0 +1,8 @@ +[bad-profile] +desc = 'Invalid profile with cpu_models but no cpu_family' +class_ids = "0600" +vendor_ids = "8086" +device_ids = "*" +cpu_models = "151 154 183" +priority = 5 +packages = 'test-pkg' diff --git a/tests/profiles/cpu-profile-test.toml b/tests/profiles/cpu-profile-test.toml new file mode 100644 index 0000000..7484cbd --- /dev/null +++ b/tests/profiles/cpu-profile-test.toml @@ -0,0 +1,15 @@ +[intel-lpmd] +desc = 'Intel Low Power Mode Daemon for supported hybrid Intel CPUs' +class_ids = "0600" +vendor_ids = "8086" +device_ids = "*" +cpu_family = "6" +cpu_models = "151 154 183 186 191 170 172 189 204" +priority = 5 +packages = 'intel-lpmd' +post_install = """ + systemctl enable --now intel_lpmd.service +""" +post_remove = """ + systemctl disable intel_lpmd.service +"""