Skip to content

Commit

Permalink
Allow logging to internal disk as a workaround
Browse files Browse the repository at this point in the history
The recommended way to persistent logging during the takeover process is
to provide an external disk or internal disk that's not the target for
balenaOS. This PR adds a mechanism that keeps the log in RAM and then
dumps the logs to the disk running the os. This can be helpful when
running a takeover remotely.

- Adds a new CLI option `--log-to-balenaos`. This is not compatible with
  other logging options takeover provides
- Dumps partial logs at captured if the takeover process fails at any
  point during the different stages

This mechanism is being added to allow running as part of a balenaOS to
balenaOS update whereby the disk partition layout changes across OS versions.

Change-type: minor
Signed-off-by: Rahul Thakoor <[email protected]>
  • Loading branch information
rahul-thakoor committed Nov 30, 2024
1 parent beb21a1 commit 7787d68
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 61 deletions.
23 changes: 21 additions & 2 deletions src/common.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
use finder::Finder;
use mod_logger::Logger;
use std::cmp::min;
use std::ffi::{CStr, CString, OsString};
use std::fs::{read_to_string, OpenOptions};
use std::io::Write;
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use std::process::{exit, Command, ExitStatus, Stdio};
use std::thread::sleep;
use std::time::Duration;

use log::{debug, error, trace, warn};
use libc::LINUX_REBOOT_CMD_RESTART;
use log::{debug, error, info, trace, warn};

use regex::Regex;

pub(crate) mod stage2_config;

pub(crate) mod defs;

pub(crate) mod logging;
pub(crate) mod system;
use system::{is_dir, stat};

Expand Down Expand Up @@ -410,6 +415,16 @@ pub(crate) fn log(text: &str) {
}
}

pub(crate) fn reboot() -> ! {
trace!("reboot entered");
Logger::flush();
sync();
sleep(Duration::from_secs(3));
info!("rebooting");
let _res = unsafe { libc::reboot(LINUX_REBOOT_CMD_RESTART) };
exit(1);
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -420,3 +435,7 @@ mod tests {
assert_eq!(&*c_path.to_string_lossy(), PATH);
}
}

pub fn fake_err(message: &str) -> Result<()> {
Err(Error::with_context(ErrorKind::Upstream, message))
}
176 changes: 176 additions & 0 deletions src/common/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use log::{error, info};
use std::fs::{self, OpenOptions};
use std::{
fs::copy,
path::{Path, PathBuf},
};

use crate::common::defs::BALENA_DATA_MP;
use crate::common::{path_append, Error, ToError};
use crate::{
common::{
debug,
defs::{BALENA_DATA_FSTYPE, NIX_NONE},
disk_util::DEF_BLOCK_SIZE,
error::Result,
loop_device::LoopDevice,
ErrorKind,
},
stage2::get_partition_infos,
};

use nix::{
mount::{mount, umount, MsFlags},
unistd::sync,
};

use self::stage2_config::Stage2Config;

use super::{
defs::{BALENA_PART_MP, OLD_ROOT_MP},
stage2_config,
};

pub const LOG_CONSOLIDATED_TMPFS: &str = "/tmp/consolidated.log";
pub const FALLBACK_LOG_DEST_DIR: &str = "/balenahup";

fn create_dir_if_not_exist(path: &str) -> Result<()> {
let dir_path = Path::new(path);
if !dir_path.is_dir() {
println!("Directory does not exist. Creating: {}", dir_path.display());
fs::create_dir_all(dir_path).upstream_with_context(
format!("Failed to create directory {}", dir_path.display()).as_str(),
)?;
println!("Directory created successfully.");
} else {
println!("Directory already exists: {}", dir_path.display());
}

Ok(())
}

// Helper function to copy a source file to a directory
// it keeps the same file name
pub fn copy_file_to_destination_dir(source_file_path: &str, dest_dir_path: &str) -> Result<()> {
info!(
"copy_file_to_destination_dir! Copying {} from tmpfs to {}",
source_file_path, dest_dir_path
);

let source_file = Path::new(source_file_path);
if source_file.exists() && source_file.is_file() {
let file_name = source_file
.file_name()
.map(|name| name.to_string_lossy().to_string())
.expect("Failed to extract file name from path");

copy(
PathBuf::from(source_file),
path_append(dest_dir_path, format!("/{}", file_name)),
)?;

Ok(())
} else {
Err(Error::with_context(
ErrorKind::FileNotFound,
&format!("source file {} does not exist", source_file_path),
))
}
}

pub fn open_tmpfs_log_file() -> Option<std::fs::File> {
let log_file = match OpenOptions::new()
.append(true) // Append to the file, don't overwrite
.create(true) // Create the file if it does not exist
.open(LOG_CONSOLIDATED_TMPFS)
{
Ok(file) => Some(file),
Err(why) => {
error!("Could not open {}, error {:?}", LOG_CONSOLIDATED_TMPFS, why);
None
}
};
log_file
}

pub fn persist_consolidated_log_to_data_partition(s2_config: &Stage2Config) -> Result<()> {
if Path::new(BALENA_DATA_MP).exists() {
// if data partition is still mounted
// first check expected path before calling pivot root

let dest_dir = format!("{}/{}", BALENA_DATA_MP, FALLBACK_LOG_DEST_DIR);
create_dir_if_not_exist(&dest_dir)?;

copy_file_to_destination_dir(LOG_CONSOLIDATED_TMPFS, dest_dir.as_str())?;
} else if Path::new(path_append(OLD_ROOT_MP, BALENA_DATA_MP).as_os_str()).exists() {
// else if data partition is still mounted but is relative to OLD_ROOT_MP

let dest_dir = format!(
"{}/{}/{}",
OLD_ROOT_MP, BALENA_DATA_MP, FALLBACK_LOG_DEST_DIR
);
create_dir_if_not_exist(&dest_dir)?;
copy_file_to_destination_dir(LOG_CONSOLIDATED_TMPFS, dest_dir.as_str())?;
} else {
// Mount raw data partition
let device = &s2_config.flash_dev;

let (_boot_part, _root_a_part, data_part) = get_partition_infos(device)?;

let mut loop_device = LoopDevice::get_free(true)?;
info!("Create loop device: '{}'", loop_device.get_path().display());
let byte_offset = data_part.start_lba * DEF_BLOCK_SIZE as u64;
let size_limit = data_part.num_sectors * DEF_BLOCK_SIZE as u64;

debug!(
"Setting up device '{}' with offset {}, sizelimit {} on '{}'",
device.display(),
byte_offset,
size_limit,
loop_device.get_path().display()
);

loop_device
.setup(device, Some(byte_offset), Some(size_limit))
.unwrap();
info!(
"Setup device '{}' with offset {}, sizelimit {} on '{}'",
device.display(),
byte_offset,
size_limit,
loop_device.get_path().display()
);

mount(
Some(loop_device.get_path()),
BALENA_PART_MP,
Some(BALENA_DATA_FSTYPE.as_bytes()),
MsFlags::empty(),
NIX_NONE,
)
.upstream_with_context(&format!(
"Failed to mount '{}' to '{}'",
loop_device.get_path().display(),
BALENA_PART_MP,
))?;

info!(
"Mounted data partition as {} on {}",
loop_device.get_path().display(),
BALENA_PART_MP
);

let dest_dir = format!("{}/{}", BALENA_PART_MP, FALLBACK_LOG_DEST_DIR);
create_dir_if_not_exist(&dest_dir)?;

copy_file_to_destination_dir(LOG_CONSOLIDATED_TMPFS, dest_dir.as_str())?;

sync();
umount(BALENA_PART_MP).upstream_with_context("Failed to unmount data partition")?;
info!("Unmounted data partition from {}", BALENA_PART_MP);

loop_device.unset()?;
}

Ok(())
}
9 changes: 9 additions & 0 deletions src/common/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ pub struct Options {
nwmgr_cfg: Option<Vec<PathBuf>>,
#[clap(long, value_name = "DT_SLUG", help = "Device Type slug to change to")]
change_dt_to: Option<String>,
#[clap(
long,
help = "Logs to RAM and then dumps logs to balenaOS disk after flashing"
)]
log_to_balenaos: bool,
}

impl Options {
Expand Down Expand Up @@ -285,4 +290,8 @@ impl Options {
pub fn change_dt_to(&self) -> &Option<String> {
&self.change_dt_to
}

pub fn log_to_balenaos(&self) -> bool {
self.log_to_balenaos
}
}
1 change: 1 addition & 0 deletions src/common/stage2_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub(crate) struct LogDevice {
pub(crate) struct Stage2Config {
pub log_dev: Option<LogDevice>,
pub log_level: String,
pub log_to_balenaos: bool,
pub flash_dev: PathBuf,
pub pretend: bool,
pub umount_parts: Vec<UmountPart>,
Expand Down
Loading

0 comments on commit 7787d68

Please sign in to comment.