Skip to content

Commit aa40caa

Browse files
committed
chore: Consume config directly without untarring (Host)
Config files are passed from the HostOS into the GuestOS using a config media (aka virtual USB stick) which the GuestOS sees as a block device (usually under `/dev/sda`) containing a vfat filesystem. Previously config files were not directly stored in the filesystem, but they were tarred into a single ic-bootstrap.tar file which was then written to the config media. The tarring step is unnecessary and makes accessing the config files more difficult since the files have to be untarred first. Furthermore, tars can contain unwanted entries, such as symlinks, devices etc. which can be misused by a malicious host (these are not supported by vfat). The migration consists of 3 steps: 1) Prepare GuestOS to read files directly from the config media and fall back to `ic-bootstrap.tar` when it exists for backwards compatibility. (#8234) 2) (this PR) Once 1) has been rolled out to all nodes, stop tarring in HostOS. 3) Once 2) has been rolled out to all nodes, remove fallback from GuestOS.
1 parent e63daa7 commit aa40caa

File tree

1 file changed

+54
-86
lines changed

1 file changed

+54
-86
lines changed

rs/ic_os/config/src/hostos/guestos_bootstrap_image.rs

Lines changed: 54 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,19 @@ impl BootstrapOptions {
4545
///
4646
/// Takes all the configuration options specified in BootstrapOptions and packages them into
4747
/// a disk image that can be mounted by the GuestOS. The image contains a FAT filesystem with
48-
/// a single file named 'ic-bootstrap.tar' that includes all configuration files.
48+
/// the configuration files.
4949
pub fn build_bootstrap_config_image(&self, out_file: &Path) -> Result<()> {
5050
let tmp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
5151

52-
// Create bootstrap tar
53-
let tar_path = tmp_dir.path().join("ic-bootstrap.tar");
54-
self.build_bootstrap_tar(&tar_path)?;
52+
// Create bootstrap directory with all files
53+
let bootstrap_dir = tmp_dir.path().join("bootstrap");
54+
fs::create_dir(&bootstrap_dir).context("Failed to create bootstrap directory")?;
55+
self.populate_bootstrap_dir(&bootstrap_dir)?;
5556

56-
let tar_size = fs::metadata(&tar_path)
57-
.context("Failed to get tar file metadata")?
58-
.len();
57+
let dir_size = fs_extra::dir::get_size(&bootstrap_dir)?;
58+
// image size = 2 * directory size + 1 MB
59+
let image_size = dir_size * 2 + 1024 * 1024;
5960

60-
// Calculate the disk image size (2 * tar_size + 1MB)
61-
let image_size = 2 * tar_size + 1_048_576;
62-
63-
// Create an empty file of the calculated size
6461
let file = File::create(out_file).context("Failed to create output file")?;
6562
file.set_len(image_size)
6663
.context("Failed to set output file size")?;
@@ -77,57 +74,43 @@ impl BootstrapOptions {
7774
bail!("Failed to format disk image");
7875
}
7976

80-
// Copy the tar file to the disk image
81-
if !Command::new("mcopy")
82-
.arg("-i")
83-
.arg(out_file)
84-
.arg("-o")
85-
.arg(&tar_path)
86-
.arg("::")
87-
.status()
88-
.context("Failed to execute mcopy command")?
89-
.success()
90-
{
91-
bail!("Failed to copy tar to disk image");
92-
}
77+
// Copy all files from bootstrap directory to the disk image
78+
Self::copy_dir_to_vfat(&bootstrap_dir, out_file)?;
9379

9480
Ok(())
9581
}
9682

97-
/// Build a bootstrap tar file with this configuration.
98-
fn build_bootstrap_tar(&self, out_file: &Path) -> Result<()> {
99-
// Create temporary directory for bootstrap files
100-
let bootstrap_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
101-
83+
/// Populate a directory with bootstrap files.
84+
fn populate_bootstrap_dir(&self, bootstrap_dir: &Path) -> Result<()> {
10285
if let Some(guestos_config) = &self.guestos_config {
103-
serialize_and_write_config(&bootstrap_dir.path().join("config.json"), guestos_config)
86+
serialize_and_write_config(&bootstrap_dir.join("config.json"), guestos_config)
10487
.context("Failed to write guestos config to config.json")?;
10588
}
10689

10790
if let Some(node_operator_private_key) = &self.node_operator_private_key {
10891
fs::copy(
10992
node_operator_private_key,
110-
bootstrap_dir.path().join("node_operator_private_key.pem"),
93+
bootstrap_dir.join("node_operator_private_key.pem"),
11194
)
11295
.context("Failed to copy node operator private key")?;
11396
}
11497

11598
if let Some(ic_crypto) = &self.ic_crypto {
116-
Self::copy_dir_recursively(ic_crypto, &bootstrap_dir.path().join("ic_crypto"))
99+
Self::copy_dir_recursively(ic_crypto, &bootstrap_dir.join("ic_crypto"))
117100
.context("Failed to copy IC crypto directory")?;
118101
}
119102

120103
if let Some(ic_state) = &self.ic_state
121104
&& ic_state.exists()
122105
{
123-
Self::copy_dir_recursively(ic_state, &bootstrap_dir.path().join("ic_state"))
106+
Self::copy_dir_recursively(ic_state, &bootstrap_dir.join("ic_state"))
124107
.context("Failed to copy IC state directory")?;
125108
}
126109

127110
if let Some(ic_registry_local_store) = &self.ic_registry_local_store {
128111
Self::copy_dir_recursively(
129112
ic_registry_local_store,
130-
&bootstrap_dir.path().join("ic_registry_local_store"),
113+
&bootstrap_dir.join("ic_registry_local_store"),
131114
)
132115
.context("Failed to copy registry local store")?;
133116
}
@@ -137,33 +120,39 @@ impl BootstrapOptions {
137120
if let Some(nns_public_key_override) = &self.nns_public_key_override {
138121
fs::copy(
139122
nns_public_key_override,
140-
bootstrap_dir.path().join("nns_public_key_override.pem"),
123+
bootstrap_dir.join("nns_public_key_override.pem"),
141124
)
142125
.context("Failed to copy NNS public key override")?;
143126
}
144127

145128
if let Some(accounts_ssh_authorized_keys) = &self.accounts_ssh_authorized_keys {
146-
let target_dir = bootstrap_dir.path().join("accounts_ssh_authorized_keys");
129+
let target_dir = bootstrap_dir.join("accounts_ssh_authorized_keys");
147130
Self::copy_dir_recursively(accounts_ssh_authorized_keys, &target_dir)
148131
.context("Failed to copy SSH authorized keys")?;
149132
}
150133
}
151134

152-
if !Command::new("tar")
153-
.arg("cf")
154-
.arg(out_file)
155-
.arg("--sort=name")
156-
.arg("--owner=root:0")
157-
.arg("--group=root:0")
158-
.arg("--mtime=UTC 1970-01-01 00:00:00")
159-
.arg("-C")
160-
.arg(bootstrap_dir.path())
161-
.arg(".")
162-
.status()
163-
.context("Failed to execute tar command")?
164-
.success()
165-
{
166-
bail!("Failed to create tar file");
135+
Ok(())
136+
}
137+
138+
/// Copy all files from a directory to a vfat image using mcopy.
139+
fn copy_dir_to_vfat(src_dir: &Path, vfat_image: &Path) -> Result<()> {
140+
let all_files = fs::read_dir(src_dir)?
141+
.map(|entry| Ok(entry?.path()))
142+
.collect::<Result<Vec<_>>>()
143+
.context("Failed to collect config directory entries")?;
144+
145+
let output = Command::new("/usr/bin/mcopy")
146+
.arg("-i")
147+
.arg(vfat_image)
148+
.arg("-s")
149+
.args(all_files)
150+
.arg("::/")
151+
.output()
152+
.context("Failed to execute mcopy")?;
153+
154+
if !output.status.success() {
155+
bail!("Failed to copy directory contents to vfat image. {output:?}");
167156
}
168157

169158
Ok(())
@@ -191,28 +180,14 @@ impl BootstrapOptions {
191180
#[cfg(test)]
192181
mod tests {
193182
use super::*;
194-
195-
#[test]
196-
fn test_build_bootstrap_config_image_succeeds_with_default_options() {
197-
let tmp_dir = tempfile::tempdir().unwrap();
198-
let out_file = tmp_dir.path().join("bootstrap.tar");
199-
200-
assert!(
201-
BootstrapOptions::default()
202-
.build_bootstrap_config_image(&out_file)
203-
.is_ok()
204-
);
205-
}
183+
use config_types::{DeploymentEnvironment, ICOSSettings, Ipv6Config, NetworkSettings};
206184

207185
#[test]
208186
fn test_build_bootstrap_image() -> Result<()> {
209187
let tmp_dir = tempfile::tempdir()?;
210188
let out_file = tmp_dir.path().join("bootstrap.img");
211189

212-
BootstrapOptions {
213-
..Default::default()
214-
}
215-
.build_bootstrap_config_image(&out_file)?;
190+
BootstrapOptions::default().build_bootstrap_config_image(&out_file)?;
216191

217192
assert!(out_file.exists());
218193
assert!(fs::metadata(&out_file)?.len() > 0);
@@ -222,7 +197,7 @@ mod tests {
222197

223198
#[test]
224199
#[cfg(feature = "dev")]
225-
fn test_build_bootstrap_tar_with_all_options() -> Result<()> {
200+
fn test_populate_bootstrap_dir_with_all_options() -> Result<()> {
226201
use config_types::{
227202
DeploymentEnvironment, GuestOSUpgradeConfig, GuestVMType, ICOSSettings, Ipv6Config,
228203
NetworkSettings,
@@ -231,7 +206,8 @@ mod tests {
231206
use std::str::FromStr;
232207

233208
let tmp_dir = tempfile::tempdir()?;
234-
let out_file = tmp_dir.path().join("bootstrap.tar");
209+
let bootstrap_dir = tmp_dir.path().join("bootstrap");
210+
fs::create_dir(&bootstrap_dir)?;
235211

236212
// Create test files and directories
237213
let test_files_dir = tmp_dir.path().join("test_files");
@@ -297,44 +273,36 @@ mod tests {
297273
ic_registry_local_store: Some(registry_dir),
298274
};
299275

300-
// Build and extract tar
301-
bootstrap_options.build_bootstrap_tar(&out_file)?;
302-
let extract_dir = tmp_dir.path().join("extract");
303-
fs::create_dir(&extract_dir)?;
304-
Command::new("tar")
305-
.arg("xf")
306-
.arg(&out_file)
307-
.arg("-C")
308-
.arg(&extract_dir)
309-
.status()?;
276+
// Populate bootstrap directory
277+
bootstrap_options.populate_bootstrap_dir(&bootstrap_dir)?;
310278

311279
// Verify all copied files and directories
312280
assert_eq!(
313-
fs::read_to_string(extract_dir.join("config.json"))?,
281+
fs::read_to_string(bootstrap_dir.join("config.json"))?,
314282
serde_json::to_string_pretty(&guestos_config)?
315283
);
316284
assert_eq!(
317-
fs::read_to_string(extract_dir.join("nns_public_key_override.pem"))?,
285+
fs::read_to_string(bootstrap_dir.join("nns_public_key_override.pem"))?,
318286
"test_nns_key"
319287
);
320288
assert_eq!(
321-
fs::read_to_string(extract_dir.join("node_operator_private_key.pem"))?,
289+
fs::read_to_string(bootstrap_dir.join("node_operator_private_key.pem"))?,
322290
"test_node_key"
323291
);
324292
assert_eq!(
325-
fs::read_to_string(extract_dir.join("accounts_ssh_authorized_keys/key1"))?,
293+
fs::read_to_string(bootstrap_dir.join("accounts_ssh_authorized_keys/key1"))?,
326294
"ssh_key1"
327295
);
328296
assert_eq!(
329-
fs::read_to_string(extract_dir.join("ic_crypto/test"))?,
297+
fs::read_to_string(bootstrap_dir.join("ic_crypto/test"))?,
330298
"crypto_data"
331299
);
332300
assert_eq!(
333-
fs::read_to_string(extract_dir.join("ic_state/test"))?,
301+
fs::read_to_string(bootstrap_dir.join("ic_state/test"))?,
334302
"state_data"
335303
);
336304
assert_eq!(
337-
fs::read_to_string(extract_dir.join("ic_registry_local_store/test"))?,
305+
fs::read_to_string(bootstrap_dir.join("ic_registry_local_store/test"))?,
338306
"registry_data"
339307
);
340308

0 commit comments

Comments
 (0)