Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

Commit a35a624

Browse files
committed
feat: parse configuration domain
1 parent f8292a2 commit a35a624

4 files changed

Lines changed: 258 additions & 15 deletions

File tree

docs/docs/explanations/architecture/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ This is the only service that uses the `/xenith` directory. The directory is str
3939
/snapshots
4040
# contains all disk snapshots
4141
/templates
42-
debian-default.hcl # packer image template
42+
debian-default.pkr.hcl # packer image template
4343
template-variables.hcl # generated image variables
4444

4545
/windows11-default

xenith-domain-management/src/configuration/disk.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,12 @@ impl Display for Disk {
8888
}
8989
}
9090

91-
impl TryFrom<PathBuf> for Disk {
91+
impl TryFrom<&PathBuf> for Disk {
9292
type Error = std::io::Error;
9393

94-
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
94+
fn try_from(path: &PathBuf) -> Result<Self, Self::Error> {
9595
// Check if the file exists
96-
if !path.exists() {
97-
return Err(std::io::Error::new(
98-
std::io::ErrorKind::NotFound,
99-
format!("File not found: {}", path.display()),
100-
));
101-
}
96+
path.try_exists()?;
10297

10398
// Check if the file is a regular file
10499
if !path.is_file() {
@@ -136,7 +131,7 @@ impl TryFrom<PathBuf> for Disk {
136131

137132
Ok(Self {
138133
name,
139-
path,
134+
path: path.clone(),
140135
size,
141136
format,
142137
})

xenith-domain-management/src/configuration/domain.rs

Lines changed: 177 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
1818

1919
use std::fmt::Display;
20-
use std::path::PathBuf;
20+
use std::path::{Path, PathBuf};
2121

2222
use serde::{Deserialize, Serialize};
2323

24-
use crate::configuration::Disk;
25-
use crate::configuration::Template;
24+
use crate::error::ConfigurationError;
25+
26+
use super::Disk;
27+
use super::Template;
2628

2729
/// Domain configuration
2830
#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
@@ -40,6 +42,8 @@ pub struct Domain {
4042
/// List of disks for the domain
4143
/// This is the list of disks that are used by the domain.
4244
disks: Vec<Disk>,
45+
/// List of disk snapshots for the domain
46+
snapshots: Vec<Disk>,
4347
/// Packer templates for the domain
4448
templates: Option<Template>,
4549
}
@@ -50,17 +54,96 @@ impl Domain {
5054
path: PathBuf,
5155
configuration_file: Option<PathBuf>,
5256
disks: Vec<Disk>,
57+
disks_snapshots: Vec<Disk>,
5358
templates: Option<Template>,
5459
) -> Self {
5560
Self {
5661
name,
5762
path,
5863
configuration_file,
5964
disks,
65+
snapshots: disks_snapshots,
6066
templates,
6167
}
6268
}
6369

70+
pub fn new_empty(name: String, path: PathBuf) -> Self {
71+
Self {
72+
name,
73+
path,
74+
configuration_file: None,
75+
disks: vec![],
76+
snapshots: vec![],
77+
templates: None,
78+
}
79+
}
80+
81+
fn parse_configuration_file(&self) -> Result<Option<PathBuf>, ConfigurationError> {
82+
let path = self.get_configuration_file_path();
83+
84+
// check if the file exists and is not empty
85+
if path.exists() && path.is_file() {
86+
let metadata = path.metadata().map_err(ConfigurationError::Parsing)?;
87+
88+
if metadata.len() > 0 {
89+
return Ok(Some(path));
90+
}
91+
}
92+
93+
Ok(None)
94+
}
95+
96+
fn parse_disks_in_directory(&self, path: &Path) -> Result<Vec<Disk>, ConfigurationError> {
97+
let mut disks = vec![];
98+
99+
if !path.exists() {
100+
return Ok(disks);
101+
}
102+
103+
// iterate over the disks directory, and parse each disk
104+
for entry in path.read_dir().map_err(ConfigurationError::Parsing)? {
105+
let entry = entry.map_err(ConfigurationError::Parsing)?;
106+
let path = entry.path();
107+
108+
if path.is_file() {
109+
let disk = Disk::try_from(&path).map_err(ConfigurationError::Parsing)?;
110+
disks.push(disk);
111+
}
112+
}
113+
114+
Ok(disks)
115+
}
116+
117+
fn parse_templates_in_directory(
118+
&self,
119+
path: &PathBuf,
120+
) -> Result<Option<Template>, ConfigurationError> {
121+
if path.exists() {
122+
let template = Template::try_from(path).map_err(ConfigurationError::Parsing)?;
123+
return Ok(Some(template));
124+
}
125+
126+
Ok(None)
127+
}
128+
129+
fn parse_disks(&self) -> Result<Vec<Disk>, ConfigurationError> {
130+
let disks_path = self.get_disks_path();
131+
132+
self.parse_disks_in_directory(&disks_path)
133+
}
134+
135+
fn parse_snapshots(&self) -> Result<Vec<Disk>, ConfigurationError> {
136+
let snapshots_path = self.get_snapshots_path();
137+
138+
self.parse_disks_in_directory(&snapshots_path)
139+
}
140+
141+
fn parse_templates(&self) -> Result<Option<Template>, ConfigurationError> {
142+
let templates_path = self.get_templates_path();
143+
144+
self.parse_templates_in_directory(&templates_path)
145+
}
146+
64147
/// Get the domain name
65148
/// This is the name of the domain that is used to create
66149
/// the domain in libvirt.
@@ -98,6 +181,10 @@ impl Domain {
98181
self.configuration_file.as_ref()
99182
}
100183

184+
pub fn get_configuration_file_path(&self) -> PathBuf {
185+
self.path.join("config.xml")
186+
}
187+
101188
/// Get the disks for the domain
102189
/// This is the list of disks that are used by the domain.
103190
/// It is used to create the domain in libvirt.
@@ -109,6 +196,24 @@ impl Domain {
109196
&self.disks
110197
}
111198

199+
pub fn get_disks_path(&self) -> PathBuf {
200+
self.path.join("disks")
201+
}
202+
203+
/// Get the disk snapshots for the domain
204+
/// This is the list of disk snapshots that are used by the domain.
205+
///
206+
/// # Returns
207+
///
208+
/// * `&Vec<Disk>` - The list of disk snapshots for the domain.
209+
pub fn get_snapshots(&self) -> &Vec<Disk> {
210+
&self.snapshots
211+
}
212+
213+
pub fn get_snapshots_path(&self) -> PathBuf {
214+
self.get_disks_path().join("snapshots")
215+
}
216+
112217
/// Get the Packer templates for the domain
113218
/// It is used to create the domain disk image.
114219
///
@@ -120,20 +225,85 @@ impl Domain {
120225
pub fn get_templates(&self) -> Option<&Template> {
121226
self.templates.as_ref()
122227
}
228+
229+
pub fn get_templates_path(&self) -> PathBuf {
230+
self.path.join("templates")
231+
}
123232
}
124233

125234
impl Display for Domain {
126235
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127236
write!(
128237
f,
129-
"Domain({{ name: {}, path: {}, disks: {:?} }})",
238+
"Domain({{ name: {}, path: {}, disks: {:?}, snapshots: {:?} }})",
130239
self.name,
131240
self.path.display(),
132-
self.disks
241+
self.disks,
242+
self.snapshots,
133243
)
134244
}
135245
}
136246

247+
impl TryFrom<&PathBuf> for Domain {
248+
type Error = ConfigurationError;
249+
250+
/// Try to create a Domain from a path.
251+
///
252+
/// The given path should be the path to the domain directory configuration.
253+
fn try_from(path: &PathBuf) -> Result<Self, Self::Error> {
254+
// Check if the file exists
255+
path.try_exists().map_err(ConfigurationError::Parsing)?;
256+
257+
// Check if the file is a regular file
258+
if path.is_file() {
259+
return Err(ConfigurationError::Parsing(std::io::Error::new(
260+
std::io::ErrorKind::InvalidInput,
261+
format!("Not a directory: {}", path.display()),
262+
)));
263+
}
264+
265+
// Check if the file is not empty
266+
let metadata = path.metadata().map_err(ConfigurationError::Parsing)?;
267+
metadata.len().eq(&0).then(|| {
268+
ConfigurationError::Parsing(std::io::Error::new(
269+
std::io::ErrorKind::InvalidInput,
270+
format!("File is empty: {}", path.display()),
271+
))
272+
});
273+
274+
let name = path
275+
.file_name()
276+
.ok_or_else(|| {
277+
std::io::Error::new(
278+
std::io::ErrorKind::InvalidData,
279+
format!("No file name for path: {}", path.display()),
280+
)
281+
})
282+
.map_err(ConfigurationError::Parsing)?
283+
.to_string_lossy()
284+
.to_string();
285+
286+
let domain = Domain::new_empty(name.clone(), path.clone());
287+
288+
let config_file = domain.parse_configuration_file()?;
289+
290+
let disks = domain.parse_disks()?;
291+
292+
let snapshots = domain.parse_snapshots()?;
293+
294+
let templates = domain.parse_templates()?;
295+
296+
Ok(Domain {
297+
name,
298+
path: path.clone(),
299+
configuration_file: config_file,
300+
disks,
301+
snapshots,
302+
templates,
303+
})
304+
}
305+
}
306+
137307
#[cfg(test)]
138308
mod tests {
139309

@@ -153,6 +323,7 @@ mod tests {
153323
PathBuf::from("/xenith/domains/test_domain"),
154324
Some(PathBuf::from("/xenith/domains/test_domain/config.xml")),
155325
vec![disk.clone()],
326+
vec![],
156327
None,
157328
);
158329

@@ -166,6 +337,7 @@ mod tests {
166337
Some(&PathBuf::from("/xenith/domains/test_domain/config.xml"))
167338
);
168339
assert_eq!(domain.get_disks(), &vec![disk]);
340+
assert_eq!(domain.get_snapshots(), &vec![]);
169341
assert!(domain.get_templates().is_none());
170342
}
171343
}

xenith-domain-management/src/configuration/template.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,82 @@ impl Display for Template {
5757
}
5858
}
5959

60+
impl TryFrom<&PathBuf> for Template {
61+
type Error = std::io::Error;
62+
63+
/// Creates a new Template from a directory path.
64+
/// The directory must contain a file with the extension `.pkr.hcl` for the image template
65+
/// and an optional file with the extension `.hcl` for the variables.
66+
fn try_from(path: &PathBuf) -> Result<Self, Self::Error> {
67+
// Check if the file exists
68+
path.try_exists()?;
69+
70+
// Check if the file is a directory
71+
if path.is_file() {
72+
return Err(std::io::Error::new(
73+
std::io::ErrorKind::InvalidInput,
74+
format!("Not a regular file: {}", path.display()),
75+
));
76+
}
77+
78+
// Check if the directory is empty
79+
if path.read_dir()?.next().is_none() {
80+
return Err(std::io::Error::new(
81+
std::io::ErrorKind::InvalidInput,
82+
format!("Directory is empty: {}", path.display()),
83+
));
84+
}
85+
86+
let mut image_template = None;
87+
let mut variables = None;
88+
89+
let mut hcl_files = vec![];
90+
for entry in path.read_dir()? {
91+
let entry = entry?;
92+
let path = entry.path();
93+
let extension = path.extension().and_then(|s| s.to_str());
94+
95+
// Check if the file is a regular file
96+
if !path.is_file() {
97+
continue;
98+
}
99+
100+
if extension != Some("hcl") {
101+
continue;
102+
}
103+
104+
hcl_files.push(path);
105+
}
106+
107+
// If `.pkr.hcl` file is found, set it as the image template
108+
for file in hcl_files.iter() {
109+
let file_name = file
110+
.file_name()
111+
.and_then(|s| s.to_str())
112+
.unwrap_or_default();
113+
114+
if file_name.contains(".pkr.hcl") {
115+
image_template = Some(file.clone());
116+
} else if file_name.contains(".hcl") {
117+
variables = Some(file.clone());
118+
}
119+
}
120+
121+
if image_template.is_none() {
122+
return Err(std::io::Error::new(
123+
std::io::ErrorKind::NotFound,
124+
format!("No image template found in directory: {}", path.display()),
125+
));
126+
}
127+
let image_template = image_template.unwrap();
128+
129+
Ok(Template {
130+
image_template,
131+
variables,
132+
})
133+
}
134+
}
135+
60136
#[cfg(test)]
61137
mod tests {
62138
use super::*;

0 commit comments

Comments
 (0)