Skip to content

Commit 56f0a62

Browse files
behoubarst0git
authored andcommitted
feat(client): add support for global configuration file
Prior to this commit, users had to create a separate configuration file for each process involved in the coordinated checkpoint. This commit enables the usage of a single config file for multiple processes. The global config should be stored inside `/etc/criu/criu-coordinator.json` and it uses a slightly different format from the per-process config. Example of global config: ```json { "address": "127.0.0.1", "port": "8080", "log-file": "/var/log/criu-coordinator.log", "dependencies": { "A": ["B", "C"], "B": ["C", "A"], "C": ["A"] } } ``` Where `dependencies` field is a map of IDs (e.g: container IDs) to a list of dependencies. Signed-off-by: Kouame Behouba Manasse <[email protected]>
1 parent ba0d09c commit 56f0a62

File tree

3 files changed

+190
-49
lines changed

3 files changed

+190
-49
lines changed

src/client.rs

Lines changed: 180 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ use std::io::{Read, Write};
2121
use std::net::{TcpStream, Shutdown};
2222
use std::path::Path;
2323
use std::process::exit;
24-
use std::str;
24+
use std::{fs, str};
2525
use json::object;
2626
use log::*;
27-
use config::Config;
2827

29-
use crate::constants::MESSAGE_ACK;
30-
use crate::pipeline::streamer::streamer;
31-
use std::{collections::HashMap, path::PathBuf};
3228
use crate::cli::{DEFAULT_ADDRESS, DEFAULT_PORT};
3329
use crate::constants::*;
30+
use crate::pipeline::streamer::streamer;
31+
use std::{collections::HashMap, env, path::PathBuf};
32+
33+
use config::Config;
3434

3535
const BUFFER_SIZE: usize = 32768 * 4;
3636

@@ -44,6 +44,16 @@ pub struct ClientConfig {
4444
}
4545

4646
impl ClientConfig {
47+
fn new(log_file: String, address: String, port: String, id: String, dependencies: String) -> Self {
48+
ClientConfig {
49+
log_file,
50+
address,
51+
port,
52+
id,
53+
dependencies,
54+
}
55+
}
56+
4757
pub fn get_log_file(&self) -> &str {
4858
&self.log_file
4959
}
@@ -71,55 +81,183 @@ const CONFIG_KEY_ADDR: &str = "address";
7181
const CONFIG_KEY_PORT: &str = "port";
7282
const CONFIG_KEY_LOG: &str = "log-file";
7383

74-
pub fn load_config_file<P: AsRef<Path>>(images_dir: P) -> ClientConfig {
84+
pub fn load_config_file<P: AsRef<Path>>(images_dir: P, action: &str) -> ClientConfig {
7585
let images_dir = images_dir.as_ref();
76-
let mut config_file = images_dir.join(Path::new(CONFIG_FILE));
77-
if !config_file.is_file() {
78-
// The following allows us to load global config files from /etc/criu.
79-
// This is useful for example when we want to use the same config file
80-
// for multiple containers.
81-
let config_dir = PathBuf::from("/etc/criu");
82-
config_file = config_dir.join(Path::new(CONFIG_FILE));
83-
if !config_file.is_file() {
84-
panic!("config file does not exist")
85-
}
86-
}
86+
let local_config_file = images_dir.join(Path::new(CONFIG_FILE));
8787

88-
let settings = Config::builder().add_source(config::File::from(config_file)).build().unwrap();
89-
let settings_map = settings.try_deserialize::<HashMap<String, String>>().unwrap();
88+
// Handle per-process configuration workflow
89+
if local_config_file.is_file() {
90+
// Example of per-process config file:
91+
// {
92+
// "id": "A",
93+
// "dependencies": "B:C",
94+
// "address": "127.0.0.1",
95+
// "port": "8080",
96+
// "log-file": "/var/log/criu-coordinator.log"
97+
// }
98+
let settings = Config::builder().add_source(config::File::from(local_config_file)).build().unwrap();
99+
let settings_map = settings.try_deserialize::<HashMap<String, String>>().unwrap();
90100

91-
if !settings_map.contains_key(CONFIG_KEY_ID) {
92-
panic!("id missing in config file")
101+
return ClientConfig::new(
102+
settings_map.get(CONFIG_KEY_LOG).cloned().unwrap_or_else(|| "-".to_string()),
103+
settings_map.get(CONFIG_KEY_ADDR).cloned().unwrap_or_else(|| DEFAULT_ADDRESS.to_string()),
104+
settings_map.get(CONFIG_KEY_PORT).cloned().unwrap_or_else(|| DEFAULT_PORT.to_string()),
105+
settings_map.get(CONFIG_KEY_ID).unwrap().clone(),
106+
settings_map.get(CONFIG_KEY_DEPS).cloned().unwrap_or_default(),
107+
);
93108
}
94-
let id = settings_map.get(CONFIG_KEY_ID).unwrap();
95109

96-
let mut dependencies = String::new();
97-
if settings_map.contains_key(CONFIG_KEY_DEPS) {
98-
dependencies = settings_map.get(CONFIG_KEY_DEPS).unwrap().to_string();
99-
}
110+
// The following allows us to load global config files from /etc/criu.
111+
// This is useful for example when we want to use the same config file
112+
// for multiple containers.
113+
// Example of global config file:
114+
// {
115+
// "address": "127.0.0.1",
116+
// "port": "8080",
117+
// "log-file": "/var/log/criu-coordinator.log",
118+
// "dependencies": {
119+
// "A": ["B", "C"],
120+
// "B": ["C", "A"],
121+
// "C": ["A"]
122+
// }
123+
// }
124+
// Where dependencies is a map of IDs (e.g: container IDs) to a list of dependencies.
125+
let global_config_file = PathBuf::from("/etc/criu").join(Path::new(CONFIG_FILE));
100126

101-
let mut address = DEFAULT_ADDRESS;
102-
if settings_map.contains_key(CONFIG_KEY_ADDR) {
103-
address = settings_map.get(CONFIG_KEY_ADDR).unwrap();
127+
if !global_config_file.is_file() {
128+
panic!("Global config file {:?} is not found", global_config_file);
104129
}
105130

106-
let mut port = DEFAULT_PORT;
107-
if settings_map.contains_key(CONFIG_KEY_PORT) {
108-
port = settings_map.get(CONFIG_KEY_PORT).unwrap();
109-
}
131+
let global_settings = Config::builder().add_source(config::File::from(global_config_file)).build().unwrap();
132+
let global_map = global_settings.try_deserialize::<HashMap<String, config::Value>>().unwrap();
133+
134+
let address = global_map.get(CONFIG_KEY_ADDR).map(|v| v.clone().into_string().unwrap()).unwrap_or_else(|| DEFAULT_ADDRESS.to_string());
135+
let port = global_map.get(CONFIG_KEY_PORT).map(|v| v.clone().into_string().unwrap()).unwrap_or_else(|| DEFAULT_PORT.to_string());
136+
let log_file = global_map.get(CONFIG_KEY_LOG).map(|v| v.clone().into_string().unwrap()).unwrap_or_else(|| "-".to_string());
137+
138+
if is_dump_action(action) {
139+
let pid_str = env::var(ENV_INIT_PID)
140+
.unwrap_or_else(|_| panic!("{} not set", ENV_INIT_PID));
141+
let pid: u32 = pid_str.parse().expect("Invalid PID");
142+
143+
144+
let deps_map: HashMap<String, Vec<String>> = global_map
145+
.get(CONFIG_KEY_DEPS)
146+
.unwrap_or_else(|| panic!("'{}' map is missing in global config", CONFIG_KEY_DEPS))
147+
.clone().into_table().unwrap()
148+
.into_iter().map(|(k, v)| {
149+
let deps = v.into_array().unwrap().into_iter().map(|val| val.into_string().unwrap()).collect();
150+
(k, deps)
151+
}).collect();
152+
153+
// We first try to find a container ID.
154+
let id = match find_container_id_from_pid(pid) {
155+
Ok(container_id) => container_id,
156+
Err(_) => {
157+
// If the PID is not in a container cgroup, we consider it a regular process.
158+
// We identify it by its process name from /proc/<pid>/comm.
159+
let process_name_path = format!("/proc/{pid}/comm");
160+
if let Ok(name) = fs::read_to_string(process_name_path) {
161+
name.trim().to_string()
162+
} else {
163+
// Fallback to using the PID as the ID if comm is unreadable
164+
pid.to_string()
165+
}
166+
}
167+
};
168+
169+
let dependencies = find_dependencies_in_global_config(&deps_map, &id).unwrap();
170+
171+
// Write the local config for each container during dump
172+
if action == ACTION_PRE_DUMP || action == ACTION_PRE_STREAM {
173+
write_checkpoint_config(images_dir, &id, &dependencies);
174+
}
110175

111-
let mut log_file = "-";
112-
if settings_map.contains_key(CONFIG_KEY_LOG) {
113-
log_file = settings_map.get(CONFIG_KEY_LOG).unwrap();
176+
ClientConfig::new(
177+
log_file,
178+
address,
179+
port,
180+
id,
181+
dependencies,
182+
)
183+
} else { // Restore action
184+
if !local_config_file.is_file() {
185+
panic!("Restore action initiated, but no {CONFIG_FILE} found in the image directory {:?}", images_dir);
186+
}
187+
188+
let local_settings = Config::builder().add_source(config::File::from(local_config_file)).build().unwrap();
189+
let local_map = local_settings.try_deserialize::<HashMap<String, String>>().unwrap();
190+
191+
ClientConfig::new(
192+
log_file,
193+
address,
194+
port,
195+
local_map.get(CONFIG_KEY_ID).unwrap().clone(),
196+
local_map.get(CONFIG_KEY_DEPS).cloned().unwrap_or_default(),
197+
)
114198
}
199+
}
200+
201+
/// Find containers dependencies by matching the discovered ID as a prefix of a key in the map
202+
fn find_dependencies_in_global_config(
203+
deps_map: &HashMap<String, Vec<String>>,
204+
id: &str,
205+
) -> Result<String, String> {
206+
let deps = deps_map
207+
.iter()
208+
.find(|(key, _)| id.starts_with(*key))
209+
.map(|(_, deps)| deps.join(":"))
210+
.ok_or_else(|| {
211+
format!("No dependency entry found for container ID matching '{id}'")
212+
})?;
213+
Ok(deps)
214+
}
215+
216+
/// Find a container ID from the host PID by inspecting the process's cgroup.
217+
fn find_container_id_from_pid(pid: u32) -> Result<String, String> {
218+
let cgroup_path = format!("/proc/{pid}/cgroup");
219+
let cgroup_content = fs::read_to_string(&cgroup_path)
220+
.map_err(|e| format!("Failed to read {cgroup_path}: {e}"))?;
115221

116-
ClientConfig {
117-
log_file: log_file.to_string(),
118-
address: address.to_string(),
119-
port: port.to_string(),
120-
id: id.to_string(),
121-
dependencies,
222+
let mut container_id: Option<String> = None;
223+
for line in cgroup_content.lines() {
224+
if line.len() < 64 {
225+
continue;
226+
}
227+
for i in 0..=(line.len() - 64) {
228+
let potential_id = &line[i..i + 64];
229+
if potential_id.chars().all(|c| c.is_ascii_hexdigit()) {
230+
let is_start = i == 0 || !line.chars().nth(i - 1).unwrap().is_ascii_hexdigit();
231+
let is_end = (i + 64 == line.len())
232+
|| !line.chars().nth(i + 64).unwrap().is_ascii_hexdigit();
233+
if is_start && is_end {
234+
container_id = Some(potential_id.to_string());
235+
}
236+
}
237+
}
122238
}
239+
240+
container_id.ok_or_else(|| {
241+
format!("Could not find container ID from cgroup file for PID {pid}")
242+
})
243+
}
244+
245+
/// Write per-checkpoint configuration file into the checkpoint images directory.
246+
fn write_checkpoint_config(img_dir: &Path, id: &str, dependencies: &str) {
247+
let config_path = img_dir.join(CONFIG_FILE);
248+
let content = format!("{{\n \"id\": \"{id}\",\n \"dependencies\": \"{dependencies}\"\n}}",);
249+
250+
fs::write(&config_path, content)
251+
.unwrap_or_else(|_| panic!("Failed to write checkpoint config file to {:?}", config_path))
252+
}
253+
254+
255+
pub fn is_dump_action(action: &str) -> bool {
256+
matches!(action, ACTION_PRE_DUMP | ACTION_NETWORK_LOCK | ACTION_POST_DUMP | ACTION_PRE_STREAM)
257+
}
258+
259+
pub fn is_restore_action(action: &str) -> bool {
260+
matches!(action, ACTION_PRE_RESTORE | ACTION_POST_RESTORE | ACTION_NETWORK_UNLOCK | ACTION_POST_RESUME)
123261
}
124262

125263
pub fn run_client(address: &str, port: u16, id: &str, deps: &str, action: &str, images_dir: &Path, enable_streaming: bool) {

src/constants.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ pub const ACTION_ADD_DEPENDENCIES: &str = "add-dependencies";
3232
pub const ENV_ACTION: &str = "CRTOOLS_SCRIPT_ACTION";
3333
/// ENV_IMAGE_DIR specifies path as used a base directory for CRIU images.
3434
pub const ENV_IMAGE_DIR: &str = "CRTOOLS_IMAGE_DIR";
35+
/// ENV_INIT_PID specifies the PID of the process to be checkpointed by CRIU.
36+
pub const ENV_INIT_PID: &str = "CRTOOLS_INIT_PID";
3537

3638
/// Unix socket used for "criu dump".
3739
pub const IMG_STREAMER_CAPTURE_SOCKET_NAME: &str = "streamer-capture.sock";

src/main.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod pipeline;
2525
mod logger;
2626

2727
use constants::*;
28+
2829
use std::{env, path::PathBuf, process::exit, fs, os::unix::prelude::FileTypeExt};
2930

3031
use clap::{CommandFactory, Parser};
@@ -36,15 +37,19 @@ use client::run_client;
3637
use server::run_server;
3738
use logger::init_logger;
3839

39-
use crate::client::load_config_file;
40+
use crate::client::{load_config_file, is_dump_action, is_restore_action};
41+
4042

4143
fn main() {
4244
if let Ok(action) = env::var(ENV_ACTION) {
45+
if !is_dump_action(&action) && !is_restore_action(&action) {
46+
exit(0)
47+
}
4348

4449
let images_dir = PathBuf::from(env::var(ENV_IMAGE_DIR)
4550
.unwrap_or_else(|_| panic!("Missing {} environment variable", ENV_IMAGE_DIR)));
4651

47-
let client_config = load_config_file(&images_dir);
52+
let client_config = load_config_file(&images_dir, &action);
4853

4954
// Ignore all action hooks other than "pre-stream", "pre-dump" and "pre-restore".
5055
let enable_streaming = match action.as_str() {
@@ -61,11 +66,7 @@ fn main() {
6166
Err(_) => false
6267
}
6368
},
64-
ACTION_PRE_RESTORE => false,
65-
ACTION_POST_DUMP => false,
66-
ACTION_NETWORK_LOCK => false,
67-
ACTION_NETWORK_UNLOCK => false,
68-
_ => exit(0)
69+
_ => false,
6970
};
7071

7172
init_logger(Some(&images_dir), client_config.get_log_file().to_string());

0 commit comments

Comments
 (0)