Skip to content

Commit 02abede

Browse files
committed
feat: add support for hugepages
Signed-off-by: Piyush Jena <jepiyush@amazon.com>
1 parent c848537 commit 02abede

4 files changed

Lines changed: 194 additions & 3 deletions

File tree

bottlerocket-settings-models/modeled-types/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ serde = { workspace = true, features = ["derive"] }
2424
serde_json.workspace = true
2525
serde_plain.workspace = true
2626
snafu.workspace = true
27+
test-case.workspace = true
2728
url.workspace = true
2829
x509-parser.workspace = true
2930

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use crate::{error, NonNegativeInteger};
2+
use bottlerocket_string_impls_for::string_impls_for;
3+
use lazy_static::lazy_static;
4+
use regex::Regex;
5+
use serde::{Deserialize, Serialize};
6+
use snafu::{ensure, OptionExt};
7+
use std::collections::HashMap;
8+
9+
/// HugepagesSettings contains hugepages and transparent hugepages related settings.
10+
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
11+
pub struct HugepagesSettings {
12+
#[serde(flatten)]
13+
pub hugepages_config: HashMap<HugepageSize, HugepageConfig>,
14+
}
15+
16+
/// HugepageConfig contains all the knobs relevant to 1 type of Hugepage.
17+
/// We only have 1 knob now - count which contains the requested number of hugepages of a particular type.
18+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19+
pub struct HugepageConfig {
20+
pub count: NonNegativeInteger,
21+
}
22+
23+
lazy_static! {
24+
// A positive integer followed by an IEC binary unit (Ki, Mi, Gi, Ti).
25+
static ref HUGEPAGE_SIZE_RE: Regex =
26+
Regex::new(r"^(?P<value>[1-9][0-9]*)(?P<unit>Ki|Mi|Gi|Ti)$").unwrap();
27+
}
28+
29+
/// HugepageSize represents an explicit HugeTLB page size in IEC binary unit, e.g. `2Mi` or `1Gi`.
30+
/// We chose validated string instead of enum to accomodate arch specific allowed values.
31+
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
32+
pub struct HugepageSize {
33+
inner: String,
34+
}
35+
36+
impl HugepageSize {
37+
/// Size in kibibytes, as used by the kernel's sysfs `hugepages-<N>kB` directories.
38+
pub fn as_kib(&self) -> Option<u64> {
39+
if let Some(nbytes) = Self::parse(&self.inner) {
40+
return Some(nbytes / 1024);
41+
}
42+
43+
None
44+
}
45+
46+
/// Returns number of bytes if `input` is well-formed and does not overflow `u64`.
47+
fn parse(input: &str) -> Option<u64> {
48+
if let Some(captured) = HUGEPAGE_SIZE_RE.captures(input) {
49+
let Ok(value) = captured["value"].parse::<u64>() else {
50+
return None;
51+
};
52+
53+
let multiplier: u64 = match &captured["unit"] {
54+
"Ki" => 1 << 10,
55+
"Mi" => 1 << 20,
56+
"Gi" => 1 << 30,
57+
"Ti" => 1 << 40,
58+
_ => return None,
59+
};
60+
61+
return value.checked_mul(multiplier);
62+
}
63+
64+
None
65+
}
66+
}
67+
68+
/// Validate format and the power-of-two invariant before we accept the input.
69+
impl TryFrom<&str> for HugepageSize {
70+
type Error = error::Error;
71+
72+
fn try_from(input: &str) -> Result<Self, Self::Error> {
73+
let bytes = HugepageSize::parse(input).context(error::InvalidHugepageSizeSnafu {
74+
input,
75+
msg: "must be a positive integer with an IEC binary unit (Ki, Mi, Gi, Ti) \
76+
in u64, e.g. \"2Mi\" or \"1Gi\"",
77+
})?;
78+
79+
ensure!(
80+
bytes.is_power_of_two(),
81+
error::InvalidHugepageSizeSnafu {
82+
input,
83+
msg: "huge page size must be a power of two",
84+
}
85+
);
86+
87+
Ok(HugepageSize {
88+
inner: input.to_string(),
89+
})
90+
}
91+
}
92+
93+
string_impls_for!(HugepageSize, "HugepageSize");
94+
95+
#[cfg(test)]
96+
mod test_hugepage_size {
97+
use super::HugepageSize;
98+
use std::convert::TryFrom;
99+
use test_case::test_case;
100+
101+
#[test_case("64Ki", 64 ; "64 KB hugepages")]
102+
#[test_case("2Mi", 2048; "2 MB hugepages")]
103+
#[test_case("512Mi", 524_288 ; "512 MB")]
104+
#[test_case("1Gi", 1_048_576 ; "1 GB hugepages")]
105+
#[test_case("16Gi", 16 * 1_048_576 ; "16 GB")]
106+
fn valid(input: &str, kib: u64) {
107+
let h = HugepageSize::try_from(input);
108+
assert!(h.is_ok());
109+
assert_eq!(h.unwrap().as_kib(), Some(kib), "{input} kib");
110+
}
111+
112+
#[test_case(""; "empty")]
113+
#[test_case("2"; "no unit")]
114+
#[test_case("2M"; "SI-style suffix not accepted")]
115+
#[test_case("2mi"; "wrong case")]
116+
#[test_case("Mi"; "no mantissa")]
117+
#[test_case("0Mi"; "zero")]
118+
#[test_case("3Mi"; "not a power of two")]
119+
#[test_case("1.5Gi"; "not an integer")]
120+
#[test_case("-2Mi"; "negative")]
121+
#[test_case("2 Mi"; "whitespace")]
122+
#[test_case("2Pi"; "unsupported unit")]
123+
fn invalid(input: &str) {
124+
assert!(
125+
HugepageSize::try_from(input).is_err(),
126+
"expected '{input}' to be rejected"
127+
);
128+
}
129+
}
130+
131+
#[cfg(test)]
132+
mod test_hugepages_settings {
133+
use super::{HugepageConfig, HugepageSize, HugepagesSettings};
134+
use crate::NonNegativeInteger;
135+
use std::convert::TryFrom;
136+
137+
#[test]
138+
fn pool_count_table_deserializes_and_round_trips() {
139+
let settings: HugepagesSettings = serde_json::from_str(r#"{"1Gi": {"count": 2}}"#).unwrap();
140+
let key = HugepageSize::try_from("1Gi").unwrap();
141+
let pool = settings.hugepages_config.get(&key).expect("1Gi pool");
142+
assert_eq!(*pool.count, 2);
143+
144+
// Round-trips back to the same nested shape.
145+
let json = serde_json::to_value(&settings).unwrap();
146+
assert_eq!(json, serde_json::json!({"1Gi": {"count": 2}}));
147+
}
148+
149+
#[test]
150+
fn construct_pool_directly() {
151+
let pool = HugepageConfig {
152+
count: NonNegativeInteger::try_from(8).unwrap(),
153+
};
154+
assert_eq!(*pool.count, 8);
155+
}
156+
}

bottlerocket-settings-models/modeled-types/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ pub mod error {
5757
#[snafu(display("Invalid version string '{}'", input))]
5858
InvalidVersion { input: String },
5959

60+
#[snafu(display("Invalid hugepages size '{}': '{}'", input, msg))]
61+
InvalidHugepageSize { input: String, msg: String },
62+
6063
#[snafu(display("{} must match '{}', given: {}", thing, pattern, input))]
6164
Pattern {
6265
thing: String,
@@ -200,11 +203,13 @@ macro_rules! require {
200203

201204
// Must be after macro definition
202205
mod ecs;
206+
mod hugepages;
203207
mod kubernetes;
204208
mod oci_defaults;
205209
mod shared;
206210

207211
pub use ecs::*;
212+
pub use hugepages::*;
208213
pub use kubernetes::*;
209214
pub use oci_defaults::*;
210215
pub use shared::*;

bottlerocket-settings-models/settings-extensions/kernel/src/lib.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
//! The kernel settings can be used to configure settings related to the kernel, e.g.
1+
//! The kernel settings can be used to configure settings related to the kernel, e.g.
22
//! kernel modules
33
use bottlerocket_model_derive::model;
4-
use bottlerocket_modeled_types::{KmodKey, Lockdown, SysctlKey};
4+
use bottlerocket_modeled_types::{HugepagesSettings, KmodKey, Lockdown, SysctlKey};
55
use bottlerocket_settings_sdk::{GenerateResult, SettingsModel};
66
use std::collections::HashMap;
77
use std::convert::Infallible;
@@ -12,6 +12,7 @@ struct KernelSettingsV1 {
1212
modules: HashMap<KmodKey, KmodSetting>,
1313
// Values are almost always a single line and often just an integer... but not always.
1414
sysctl: HashMap<SysctlKey, String>,
15+
hugepages: HugepagesSettings,
1516
}
1617

1718
#[model]
@@ -52,6 +53,7 @@ impl SettingsModel for KernelSettingsV1 {
5253
#[cfg(test)]
5354
mod test {
5455
use super::*;
56+
use bottlerocket_modeled_types::{HugepageConfig, HugepageSize, NonNegativeInteger};
5557

5658
#[test]
5759
fn test_generate_kernel() {
@@ -63,6 +65,7 @@ mod test {
6365
lockdown: None,
6466
modules: None,
6567
sysctl: None,
68+
hugepages: None,
6669
})
6770
)
6871
}
@@ -72,7 +75,11 @@ mod test {
7275
let test_json = r#"{
7376
"lockdown": "integrity",
7477
"modules": {"foo": {"allowed": true, "autoload": true}},
75-
"sysctl": {"key": "value"}
78+
"sysctl": {"key": "value"},
79+
"hugepages": {
80+
"2Mi": {"count": 512},
81+
"1Gi": {"count": 4}
82+
}
7683
}"#;
7784

7885
let kernel: KernelSettingsV1 = serde_json::from_str(test_json).unwrap();
@@ -91,13 +98,35 @@ mod test {
9198
sysctl.insert(SysctlKey::try_from("key").unwrap(), String::from("value"));
9299
let sysctl = Some(sysctl);
93100

101+
let mut hugepages_config = HashMap::new();
102+
hugepages_config.insert(
103+
HugepageSize::try_from("2Mi").unwrap(),
104+
HugepageConfig {
105+
count: NonNegativeInteger::try_from(512).unwrap(),
106+
},
107+
);
108+
hugepages_config.insert(
109+
HugepageSize::try_from("1Gi").unwrap(),
110+
HugepageConfig {
111+
count: NonNegativeInteger::try_from(4).unwrap(),
112+
},
113+
);
114+
115+
let hugepages = Some(HugepagesSettings { hugepages_config });
116+
94117
assert_eq!(
95118
kernel,
96119
KernelSettingsV1 {
97120
lockdown: Some(Lockdown::try_from("integrity").unwrap()),
98121
modules,
99122
sysctl,
123+
hugepages,
100124
}
101125
);
126+
127+
let roundtrip = serde_json::to_value(&kernel).unwrap();
128+
let hugepages_out = &roundtrip["hugepages"];
129+
assert_eq!(hugepages_out["2Mi"]["count"], serde_json::json!(512));
130+
assert_eq!(hugepages_out["1Gi"]["count"], serde_json::json!(4));
102131
}
103132
}

0 commit comments

Comments
 (0)