vmm/vmm_config/
memory_hotplug.rs

1// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use serde::{Deserialize, Serialize};
5
6use crate::devices::virtio::mem::{
7    VIRTIO_MEM_DEFAULT_BLOCK_SIZE_MIB, VIRTIO_MEM_DEFAULT_SLOT_SIZE_MIB,
8};
9
10/// Errors associated with memory hotplug configuration.
11#[derive(Debug, thiserror::Error, displaydoc::Display)]
12pub enum MemoryHotplugConfigError {
13    /// Block size must not be lower than {0} MiB
14    BlockSizeTooSmall(usize),
15    /// Block size must be a power of 2
16    BlockSizeNotPowerOfTwo,
17    /// Slot size must not be lower than {0} MiB
18    SlotSizeTooSmall(usize),
19    /// Slot size must be a multiple of block size ({0} MiB)
20    SlotSizeNotMultipleOfBlockSize(usize),
21    /// Total size must not be lower than slot size ({0} MiB)
22    TotalSizeTooSmall(usize),
23    /// Total size must be a multiple of slot size ({0} MiB)
24    TotalSizeNotMultipleOfSlotSize(usize),
25}
26
27fn default_block_size_mib() -> usize {
28    VIRTIO_MEM_DEFAULT_BLOCK_SIZE_MIB
29}
30
31fn default_slot_size_mib() -> usize {
32    VIRTIO_MEM_DEFAULT_SLOT_SIZE_MIB
33}
34
35/// Configuration for memory hotplug device.
36#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
37#[serde(deny_unknown_fields)]
38pub struct MemoryHotplugConfig {
39    /// Total memory size in MiB that can be hotplugged.
40    pub total_size_mib: usize,
41    /// Block size in MiB. A block is the smallest unit the guest can hot(un)plug
42    #[serde(default = "default_block_size_mib")]
43    pub block_size_mib: usize,
44    /// Slot size in MiB. A slot is the smallest unit the host can (de)attach memory
45    #[serde(default = "default_slot_size_mib")]
46    pub slot_size_mib: usize,
47}
48
49impl MemoryHotplugConfig {
50    /// Validates the configuration.
51    pub fn validate(&self) -> Result<(), MemoryHotplugConfigError> {
52        let min_block_size_mib = VIRTIO_MEM_DEFAULT_BLOCK_SIZE_MIB;
53        if self.block_size_mib < min_block_size_mib {
54            return Err(MemoryHotplugConfigError::BlockSizeTooSmall(
55                min_block_size_mib,
56            ));
57        }
58        if !self.block_size_mib.is_power_of_two() {
59            return Err(MemoryHotplugConfigError::BlockSizeNotPowerOfTwo);
60        }
61
62        let min_slot_size_mib = VIRTIO_MEM_DEFAULT_SLOT_SIZE_MIB;
63        if self.slot_size_mib < min_slot_size_mib {
64            return Err(MemoryHotplugConfigError::SlotSizeTooSmall(
65                min_slot_size_mib,
66            ));
67        }
68        if !self.slot_size_mib.is_multiple_of(self.block_size_mib) {
69            return Err(MemoryHotplugConfigError::SlotSizeNotMultipleOfBlockSize(
70                self.block_size_mib,
71            ));
72        }
73
74        if self.total_size_mib < self.slot_size_mib {
75            return Err(MemoryHotplugConfigError::TotalSizeTooSmall(
76                self.slot_size_mib,
77            ));
78        }
79        if !self.total_size_mib.is_multiple_of(self.slot_size_mib) {
80            return Err(MemoryHotplugConfigError::TotalSizeNotMultipleOfSlotSize(
81                self.slot_size_mib,
82            ));
83        }
84
85        Ok(())
86    }
87}
88
89/// Configuration for memory hotplug device.
90#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
91#[serde(deny_unknown_fields)]
92pub struct MemoryHotplugSizeUpdate {
93    /// Requested size in MiB to resize the hotpluggable memory to.
94    pub requested_size_mib: usize,
95}
96
97#[cfg(test)]
98mod tests {
99    use serde_json;
100
101    use super::*;
102
103    #[test]
104    fn test_valid_config() {
105        let config = MemoryHotplugConfig {
106            total_size_mib: 1024,
107            block_size_mib: 2,
108            slot_size_mib: 128,
109        };
110        config.validate().unwrap();
111    }
112
113    #[test]
114    fn test_block_size_too_small() {
115        let config = MemoryHotplugConfig {
116            total_size_mib: 1024,
117            block_size_mib: 1,
118            slot_size_mib: 128,
119        };
120        match config.validate() {
121            Err(MemoryHotplugConfigError::BlockSizeTooSmall(min)) => assert_eq!(min, 2),
122            _ => panic!("Expected InvalidBlockSizeTooSmall error"),
123        }
124    }
125
126    #[test]
127    fn test_block_size_not_power_of_two() {
128        let config = MemoryHotplugConfig {
129            total_size_mib: 1024,
130            block_size_mib: 3,
131            slot_size_mib: 128,
132        };
133        match config.validate() {
134            Err(MemoryHotplugConfigError::BlockSizeNotPowerOfTwo) => {}
135            _ => panic!("Expected InvalidBlockSizePowerOfTwo error"),
136        }
137    }
138
139    #[test]
140    fn test_slot_size_too_small() {
141        let config = MemoryHotplugConfig {
142            total_size_mib: 1024,
143            block_size_mib: 2,
144            slot_size_mib: 1,
145        };
146        match config.validate() {
147            Err(MemoryHotplugConfigError::SlotSizeTooSmall(min)) => assert_eq!(min, 128),
148            _ => panic!("Expected InvalidSlotSizeTooSmall error"),
149        }
150    }
151
152    #[test]
153    fn test_slot_size_not_multiple_of_block_size() {
154        let config = MemoryHotplugConfig {
155            total_size_mib: 1024,
156            block_size_mib: 4,
157            slot_size_mib: 130,
158        };
159        match config.validate() {
160            Err(MemoryHotplugConfigError::SlotSizeNotMultipleOfBlockSize(block_size)) => {
161                assert_eq!(block_size, 4)
162            }
163            _ => panic!("Expected InvalidSlotSizeMultiple error"),
164        }
165    }
166
167    #[test]
168    fn test_total_size_too_small() {
169        let config = MemoryHotplugConfig {
170            total_size_mib: 64,
171            block_size_mib: 2,
172            slot_size_mib: 128,
173        };
174        match config.validate() {
175            Err(MemoryHotplugConfigError::TotalSizeTooSmall(slot_size)) => {
176                assert_eq!(slot_size, 128)
177            }
178            _ => panic!("Expected InvalidTotalSizeTooSmall error"),
179        }
180    }
181
182    #[test]
183    fn test_total_size_not_multiple_of_slot_size() {
184        let config = MemoryHotplugConfig {
185            total_size_mib: 1000,
186            block_size_mib: 2,
187            slot_size_mib: 128,
188        };
189        match config.validate() {
190            Err(MemoryHotplugConfigError::TotalSizeNotMultipleOfSlotSize(slot_size)) => {
191                assert_eq!(slot_size, 128)
192            }
193            _ => panic!("Expected InvalidTotalSizeMultiple error"),
194        }
195    }
196
197    #[test]
198    fn test_defaults() {
199        assert_eq!(default_block_size_mib(), 2);
200        assert_eq!(default_slot_size_mib(), 128);
201
202        let json = r#"{
203            "total_size_mib": 1024
204        }"#;
205        let deserialized: MemoryHotplugConfig = serde_json::from_str(json).unwrap();
206        assert_eq!(
207            deserialized,
208            MemoryHotplugConfig {
209                total_size_mib: 1024,
210                block_size_mib: 2,
211                slot_size_mib: 128,
212            }
213        );
214    }
215
216    #[test]
217    fn test_serde() {
218        let config = MemoryHotplugConfig {
219            total_size_mib: 1024,
220            block_size_mib: 4,
221            slot_size_mib: 256,
222        };
223        let json = serde_json::to_string(&config).unwrap();
224        let deserialized: MemoryHotplugConfig = serde_json::from_str(&json).unwrap();
225        assert_eq!(config, deserialized);
226    }
227}