Skip to content

Commit d6659b1

Browse files
committed
provisioning: Add new Provisioner
This is a highly WIP effort to utilise the 3 main crates together cohesively, and identify usable strategies based upon the existing storage pool. We will ofc need to refine to load in the partition table headers in order to respect the first and last usable LBAs in each disk. Signed-off-by: Ikey Doherty <[email protected]>
1 parent cdb4b42 commit d6659b1

File tree

5 files changed

+242
-8
lines changed

5 files changed

+242
-8
lines changed

Cargo.lock

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/provisioning/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ edition = "2021"
77
miette = { workspace = true, features = ["fancy"] }
88

99
[dependencies]
10+
disks = { path = "../disks" }
11+
partitioning = { path = "../partitioning" }
1012
kdl = { workspace = true, features = ["span"] }
1113
miette = { workspace = true }
1214
itertools = { workspace = true }
1315
phf = { workspace = true, features = ["macros"] }
16+
test-log.workspace = true
1417
thiserror.workspace = true
18+
log.workspace = true

crates/provisioning/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use itertools::{Either, Itertools};
88
use kdl::{KdlDocument, KdlNode};
99
use miette::{Diagnostic, NamedSource, Severity};
1010

11+
mod provisioner;
12+
pub use provisioner::*;
13+
1114
mod errors;
1215
pub use errors::*;
1316

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers
2+
// SPDX-FileCopyrightText: Copyright © 2025 AerynOS Developers
3+
//
4+
// SPDX-License-Identifier: MPL-2.0
5+
6+
use std::collections::HashMap;
7+
8+
use disks::BlockDevice;
9+
use log::{debug, info, trace, warn};
10+
use partitioning::{
11+
planner::Planner,
12+
strategy::{AllocationStrategy, PartitionRequest, SizeRequirement, Strategy},
13+
};
14+
15+
use crate::{commands::Command, Constraints, StrategyDefinition};
16+
17+
/// Provisioner
18+
pub struct Provisioner {
19+
/// Pool of devices
20+
devices: Vec<BlockDevice>,
21+
22+
/// Strategy configurations
23+
configs: HashMap<String, StrategyDefinition>,
24+
}
25+
26+
/// Compiled plan
27+
pub struct Plan<'a> {
28+
pub strategy: &'a StrategyDefinition,
29+
pub device_assignments: HashMap<String, DevicePlan<'a>>,
30+
}
31+
32+
#[derive(Debug, Clone)]
33+
pub struct DevicePlan<'a> {
34+
device: &'a BlockDevice,
35+
planner: Planner,
36+
strategy: Strategy,
37+
}
38+
39+
impl Default for Provisioner {
40+
fn default() -> Self {
41+
Self::new()
42+
}
43+
}
44+
45+
impl Provisioner {
46+
/// Create a new provisioner
47+
pub fn new() -> Self {
48+
debug!("Creating new provisioner");
49+
Self {
50+
devices: Vec::new(),
51+
configs: HashMap::new(),
52+
}
53+
}
54+
55+
/// Add a strategy configuration
56+
pub fn add_strategy(&mut self, config: StrategyDefinition) {
57+
info!("Adding strategy: {}", config.name);
58+
self.configs.insert(config.name.clone(), config);
59+
}
60+
61+
// Add a device to the provisioner pool
62+
pub fn push_device(&mut self, device: BlockDevice) {
63+
debug!("Adding device to pool: {:?}", device);
64+
self.devices.push(device)
65+
}
66+
67+
// Build an inheritance chain for a strategy
68+
fn strategy_parents<'a>(&'a self, strategy: &'a StrategyDefinition) -> Vec<&'a StrategyDefinition> {
69+
trace!("Building inheritance chain for strategy: {}", strategy.name);
70+
let mut chain = vec![];
71+
if let Some(parent) = &strategy.inherits {
72+
if let Some(parent) = self.configs.get(parent) {
73+
chain.extend(self.strategy_parents(parent));
74+
}
75+
}
76+
chain.push(strategy);
77+
chain
78+
}
79+
80+
/// Attempt all strategies on the pool of devices
81+
pub fn plan(&self) -> Vec<Plan> {
82+
info!("Planning device provisioning");
83+
let mut plans = Vec::new();
84+
for strategy in self.configs.values() {
85+
debug!("Attempting strategy: {}", strategy.name);
86+
self.create_plans_for_strategy(strategy, &mut HashMap::new(), &mut plans);
87+
}
88+
debug!("Generated {} plans", plans.len());
89+
plans
90+
}
91+
92+
fn create_plans_for_strategy<'a>(
93+
&'a self,
94+
strategy: &'a StrategyDefinition,
95+
device_assignments: &mut HashMap<String, DevicePlan<'a>>,
96+
plans: &mut Vec<Plan<'a>>,
97+
) {
98+
trace!("Creating plans for strategy: {}", strategy.name);
99+
let chain = self.strategy_parents(strategy);
100+
101+
for command in chain.iter().flat_map(|s| &s.commands) {
102+
match command {
103+
Command::FindDisk(command) => {
104+
// Skip if already assigned
105+
if device_assignments.contains_key(&command.name) {
106+
trace!("Disk {} already assigned, skipping", command.name);
107+
continue;
108+
}
109+
110+
// Find matching devices that haven't been assigned yet
111+
let matching_devices: Vec<_> = self
112+
.devices
113+
.iter()
114+
.filter(|d| match command.constraints.as_ref() {
115+
Some(Constraints::AtLeast(n)) => d.size() >= *n,
116+
Some(Constraints::Exact(n)) => d.size() == *n,
117+
Some(Constraints::Range { min, max }) => d.size() >= *min && d.size() <= *max,
118+
_ => true,
119+
})
120+
.filter(|d| {
121+
!device_assignments
122+
.values()
123+
.any(|assigned| std::ptr::eq(assigned.device, *d))
124+
})
125+
.collect();
126+
127+
debug!("Found {} matching devices for {}", matching_devices.len(), command.name);
128+
129+
// Branch for each matching device
130+
for device in matching_devices {
131+
trace!("Creating plan branch for device: {:?}", device);
132+
let mut new_assignments = device_assignments.clone();
133+
new_assignments.insert(
134+
command.name.clone(),
135+
DevicePlan {
136+
device,
137+
planner: Planner::new(device),
138+
strategy: Strategy::new(AllocationStrategy::LargestFree),
139+
},
140+
);
141+
self.create_plans_for_strategy(strategy, &mut new_assignments, plans);
142+
}
143+
144+
return;
145+
}
146+
Command::CreatePartitionTable(command) => {
147+
if let Some(device_plan) = device_assignments.get_mut(&command.disk) {
148+
debug!("Creating partition table on disk {}", command.disk);
149+
device_plan.strategy = Strategy::new(AllocationStrategy::InitializeWholeDisk);
150+
} else {
151+
warn!("Could not find disk {} to create partition table", command.disk);
152+
}
153+
}
154+
Command::CreatePartition(command) => {
155+
if let Some(device_plan) = device_assignments.get_mut(&command.disk) {
156+
debug!("Adding partition request for disk {}", command.disk);
157+
device_plan.strategy.add_request(PartitionRequest {
158+
size: match &command.constraints {
159+
Constraints::AtLeast(n) => SizeRequirement::AtLeast(*n),
160+
Constraints::Exact(n) => SizeRequirement::Exact(*n),
161+
Constraints::Range { min, max } => SizeRequirement::Range { min: *min, max: *max },
162+
_ => SizeRequirement::Remaining,
163+
},
164+
});
165+
} else {
166+
warn!("Could not find disk {} to create partition", command.disk);
167+
}
168+
}
169+
}
170+
}
171+
172+
// OK lets now apply amy mutations to the device assignments
173+
for (disk_name, device_plan) in device_assignments.iter_mut() {
174+
debug!("Applying device plan for disk {}", disk_name);
175+
if let Err(e) = device_plan.strategy.apply(&mut device_plan.planner) {
176+
warn!("Failed to apply strategy for disk {}: {:?}", disk_name, e);
177+
}
178+
}
179+
180+
// All commands processed successfully - create a plan
181+
debug!("Creating final plan for strategy {}", strategy.name);
182+
plans.push(Plan {
183+
strategy,
184+
device_assignments: device_assignments.clone(),
185+
});
186+
}
187+
}
188+
189+
#[cfg(test)]
190+
mod tests {
191+
use disks::mock::MockDisk;
192+
use test_log::test;
193+
194+
use crate::Parser;
195+
196+
use super::*;
197+
198+
#[test]
199+
fn test_use_whole_disk() {
200+
let test_strategies = Parser::new_for_path("tests/use_whole_disk.kdl").unwrap();
201+
let def = test_strategies.strategies;
202+
let device = BlockDevice::mock_device(MockDisk::new(150 * 1024 * 1024 * 1024));
203+
let mut provisioner = Provisioner::new();
204+
provisioner.push_device(device);
205+
for def in def {
206+
provisioner.add_strategy(def);
207+
}
208+
209+
let plans = provisioner.plan();
210+
assert_eq!(plans.len(), 2);
211+
212+
let plan = &plans[0];
213+
assert_eq!(plan.device_assignments.len(), 1);
214+
215+
for plan in plans {
216+
eprintln!("Plan: {}", plan.strategy.name);
217+
for (disk, device_plan) in plan.device_assignments.iter() {
218+
println!("strategy for {disk} is now: {}", device_plan.strategy.describe());
219+
println!("After: {}", device_plan.planner.describe_changes());
220+
}
221+
}
222+
}
223+
}

crates/provisioning/tests/use_whole_disk.kdl

+8-8
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,26 @@ strategy name="whole_disk" summary="Wipe and use an entire disk" {
1414
// Create the ESP
1515
create-partition disk="root_disk" role="boot" id="esp" {
1616
constraints {
17-
min (GB)1
18-
max (GB)2
17+
min (GIB)1
18+
max (GIB)2
1919
}
2020
type (GUID)"ESP"
2121
}
2222

2323
// Create xbootldr
2424
create-partition disk="root_disk" role="extended-boot" id="xbootldr" {
2525
constraints {
26-
min (GB)2
27-
max (GB)4
26+
min (GIB)2
27+
max (GIB)4
2828
}
2929
type (GUID)"LinuxExtendedBoot"
3030
}
3131

3232
// Create a partition for rootfs
3333
create-partition disk="root_disk" id="root" {
3434
constraints {
35-
min (GB)30
36-
max (GB)120
35+
min (GIB)30
36+
max (GIB)120
3737
}
3838
type (GUID)"LinuxRoot"
3939
}
@@ -48,8 +48,8 @@ strategy name="whole_disk_with_swap" inherits="whole_disk" \
4848
// Create a swap partition in addition to the base strategy
4949
create-partition disk="root_disk" id="swap" role="swap" {
5050
constraints {
51-
min (GB)4
52-
max (GB)8
51+
min (GIB)4
52+
max (GIB)8
5353
}
5454
type (GUID)"LinuxSwap"
5555
}

0 commit comments

Comments
 (0)