Skip to content

Commit

Permalink
nydus-image: introduce new subcommand export
Browse files Browse the repository at this point in the history
Introduce new subcommand `export` to nydus-image, which will be used
to export RAFS filesystems as raw block device images or tar files.

Signed-off-by: Jiang Liu <gerry@linux.alibaba.com>
  • Loading branch information
jiangliu committed Mar 20, 2023
1 parent 1e45b15 commit 7c2861a
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 7 deletions.
6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ nydus-api = { version = "0.2.2", path = "api", features = ["handler"] }
nydus-app = { version = "0.3.2", path = "app" }
nydus-error = { version = "0.2.3", path = "error" }
nydus-rafs = { version = "0.2.2", path = "rafs", features = ["builder"] }
nydus-service = { version = "0.2.0", path = "service" }
nydus-service = { version = "0.2.0", path = "service", features = ["block-device"] }
nydus-storage = { version = "0.6.2", path = "storage" }
nydus-utils = { version = "0.4.1", path = "utils" }

Expand Down Expand Up @@ -89,9 +89,7 @@ virtiofs = [
"vm-memory",
"vmm-sys-util",
]
block-nbd = [
"nydus-service/block-nbd"
]
block-device = ["nydus-service/block-device"]

backend-http-proxy = ["nydus-storage/backend-http-proxy"]
backend-localdisk = ["nydus-storage/backend-localdisk"]
Expand Down
129 changes: 128 additions & 1 deletion service/src/block_device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@
//! device, so it can be directly mounted by Linux EROFS fs driver.
use std::cmp::min;
use std::fs::OpenOptions;
use std::io::Result;
use std::path::PathBuf;
use std::sync::Arc;

use dbs_allocator::{Constraint, IntervalTree, NodeState, Range};
use nydus_api::BlobCacheEntry;
use nydus_rafs::metadata::layout::v6::{
EROFS_BLOCK_BITS_12, EROFS_BLOCK_BITS_9, EROFS_BLOCK_SIZE_4096, EROFS_BLOCK_SIZE_512,
};
use nydus_storage::utils::alloc_buf;
use tokio_uring::buf::IoBufMut;

use crate::blob_cache::{BlobCacheMgr, BlobConfig, DataBlob, MetaBlob};
use crate::blob_cache::{generate_blob_key, BlobCacheMgr, BlobConfig, DataBlob, MetaBlob};

const BLOCK_DEVICE_EXPORT_BATCH_SIZE: usize = 0x80000;

enum BlockRange {
Hole,
Expand Down Expand Up @@ -272,6 +278,127 @@ impl BlockDevice {

(Ok(total_size), buf)
}

/// Export a RAFS filesystem as a raw block disk image.
pub fn export(
blob_entry: BlobCacheEntry,
output: Option<String>,
localfs_dir: Option<String>,
_threads: u32,
) -> Result<()> {
let cache_mgr = Arc::new(BlobCacheMgr::new());
cache_mgr.add_blob_entry(&blob_entry).map_err(|e| {
eother!(format!(
"block_device: failed to add blob into CacheMgr, {}",
e
))
})?;
let blob_id = generate_blob_key(&blob_entry.domain_id, &blob_entry.blob_id);
let block_device = BlockDevice::new(blob_id.clone(), cache_mgr.clone()).map_err(|e| {
eother!(format!(
"block_device: failed to create block device object, {}",
e
))
})?;
let block_device = Arc::new(block_device);

let path = match output {
Some(v) => PathBuf::from(v),
None => {
let path = match block_device.cache_mgr.get_config(&blob_id) {
Some(BlobConfig::MetaBlob(meta)) => meta.path().to_path_buf(),
_ => return Err(enoent!("block_device: failed to get meta blob")),
};
if !path.is_file() {
return Err(eother!(format!(
"block_device: meta blob {} is not a file",
path.display()
)));
}
let name = path
.file_name()
.ok_or_else(|| {
eother!(format!(
"block_device: failed to get file name from {}",
path.display()
))
})?
.to_str()
.ok_or_else(|| {
eother!(format!(
"block_device: failed to get file name from {}",
path.display()
))
})?;
let dir = localfs_dir
.ok_or_else(|| einval!("block_device: parameter `localfs_dir` is missing"))?;
let path = PathBuf::from(dir);
path.join(name.to_string() + ".disk")
}
};

let output_file = OpenOptions::new()
.create_new(true)
.read(true)
.write(true)
.open(&path)
.map_err(|e| {
eother!(format!(
"block_device: failed to create output file {}, {}",
path.display(),
e
))
})?;
let output_file = Arc::new(tokio_uring::fs::File::from_std(output_file));

tokio_uring::start(async move {
Self::do_export(block_device.clone(), output_file, 0, block_device.blocks()).await
})?;

Ok(())
}

async fn do_export(
block_device: Arc<BlockDevice>,
output_file: Arc<tokio_uring::fs::File>,
start: u32,
mut blocks: u32,
) -> Result<()> {
let batch_size = BLOCK_DEVICE_EXPORT_BATCH_SIZE as u32 / block_device.block_size() as u32;
let mut pos = start;
let mut buf = alloc_buf(BLOCK_DEVICE_EXPORT_BATCH_SIZE);

while blocks > 0 {
let count = min(batch_size, blocks);
let (res, buf1) = block_device.async_read(pos, count, buf).await;
let sz = res?;
if sz != count as usize * block_device.block_size() as usize {
return Err(eio!(
"block_device: failed to read data, got less data than requested"
));
}
buf = buf1;

if sz != buf.len() {
buf.resize(sz, 0);
}
let (res, buf2) = output_file
.write_at(buf, block_device.blocks_to_size(pos))
.await;
let sz1 = res?;
if sz1 != sz {
return Err(eio!(
"block_device: failed to write data to disk image file, written less data than requested"
));
}
buf = buf2;

pos += count;
blocks -= count;
}

Ok(())
}
}

#[cfg(test)]
Expand Down
127 changes: 125 additions & 2 deletions src/bin/nydus-image/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@ use std::convert::TryFrom;
use std::fs::{self, metadata, DirEntry, File, OpenOptions};
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;

use anyhow::{bail, Context, Result};
use clap::parser::ValueSource;
use clap::{Arg, ArgAction, ArgMatches, Command as App};
use nix::unistd::{getegid, geteuid};
use nydus::get_build_time_info;
use nydus_api::{BuildTimeInfo, ConfigV2, LocalFsConfig};
use nydus::{get_build_time_info, SubCmdArgs};
use nydus_api::{BlobCacheEntry, BuildTimeInfo, ConfigV2, LocalFsConfig};
use nydus_app::setup_logging;
use nydus_rafs::builder::{
parse_chunk_dict_arg, ArtifactStorage, BlobCompactor, BlobManager, BootstrapManager,
BuildContext, BuildOutput, Builder, ConversionType, DirectoryBuilder, Feature, Features,
HashChunkDict, Merger, Prefetch, PrefetchPolicy, StargzBuilder, TarballBuilder, WhiteoutSpec,
};
use nydus_rafs::metadata::{RafsSuper, RafsSuperConfig, RafsVersion};
use nydus_service::block_device::BlockDevice;
use nydus_service::{validate_threads_configuration, ServiceArgs};
use nydus_storage::backend::localfs::LocalFs;
use nydus_storage::backend::BlobBackend;
use nydus_storage::device::BlobFeatures;
Expand Down Expand Up @@ -429,6 +432,55 @@ fn prepare_cmd_args(bti_string: &'static str) -> App {
.arg(arg_output_json.clone()),
);

let app = app.subcommand(
App::new("export")
.about("Export RAFS filesystems as raw block disk images or tar files")
.arg(
Arg::new("block")
.long("block")
.action(ArgAction::SetTrue)
.required(true)
.help("Export RAFS filesystems as raw block disk images")
)
.arg(
Arg::new("bootstrap")
.long("bootstrap")
.short('B')
.help("Bootstrap of the RAFS filesystem to be exported")
.requires("localfs-dir")
)
.arg(Arg::new("config")
.long("config")
.short('C')
.help("Configuration file containing a `BlobCacheEntry` object")
.required(false))
.arg(
Arg::new("localfs-dir")
.long("localfs-dir")
.short('D')
.help(
"Path to the `localfs` working directory, which also enables the `localfs` storage backend"
)
.requires("bootstrap")
.conflicts_with("config"),
)
.arg(
Arg::new("threads")
.long("threads")
.default_value("4")
.help("Number of worker threads to execute export operation, valid values: [1-32]")
.value_parser(thread_validator)
.required(false),
)
.arg(
Arg::new("output")
.long("output")
.short('O')
.help("File path for saving the exported content")
.required_unless_present("localfs-dir")
)
);

let app = app.subcommand(
App::new("inspect")
.about("Inspect RAFS filesystem metadata in interactive or request mode")
Expand Down Expand Up @@ -626,6 +678,8 @@ fn main() -> Result<()> {
Command::merge(matches, &build_info)
} else if let Some(matches) = cmd.subcommand_matches("check") {
Command::check(matches, &build_info)
} else if let Some(matches) = cmd.subcommand_matches("export") {
Command::export(&cmd, matches, &build_info)
} else if let Some(matches) = cmd.subcommand_matches("inspect") {
Command::inspect(matches)
} else if let Some(matches) = cmd.subcommand_matches("stat") {
Expand Down Expand Up @@ -1130,6 +1184,16 @@ impl Command {
Ok(())
}

fn export(args: &ArgMatches, subargs: &ArgMatches, build_info: &BuildTimeInfo) -> Result<()> {
let subargs = SubCmdArgs::new(args, subargs);
if subargs.is_present("block") {
Self::export_block(&subargs, build_info)?;
} else {
bail!("unknown export type");
}
Ok(())
}

fn inspect(matches: &ArgMatches) -> Result<()> {
let bootstrap_path = Self::get_bootstrap(matches)?;
let mut config = Self::get_configuration(matches)?;
Expand Down Expand Up @@ -1445,4 +1509,63 @@ impl Command {
);
Ok(())
}

fn export_block(subargs: &SubCmdArgs, _bti: &BuildTimeInfo) -> Result<()> {
let mut localfs_dir = None;
let mut entry = if let Some(dir) = subargs.value_of("localfs-dir") {
// Safe to unwrap because `--block` requires `--bootstrap`.
let bootstrap = subargs.value_of("bootstrap").unwrap();
let config = r#"
{
"type": "bootstrap",
"id": "disk-default",
"domain_id": "block-nbd",
"config_v2": {
"version": 2,
"id": "block-nbd-factory",
"backend": {
"type": "localfs",
"localfs": {
"dir": "LOCAL_FS_DIR"
}
},
"cache": {
"type": "filecache",
"filecache": {
"work_dir": "LOCAL_FS_DIR"
}
},
"metadata_path": "META_FILE_PATH"
}
}"#;
let config = config
.replace("LOCAL_FS_DIR", dir)
.replace("META_FILE_PATH", bootstrap);
localfs_dir = Some(dir.to_string());
BlobCacheEntry::from_str(&config)?
} else if let Some(v) = subargs.value_of("config") {
BlobCacheEntry::from_file(v)?
} else {
bail!("both option `-C/--config` and `-D/--localfs-dir` are missing");
};
if !entry.prepare_configuration_info() {
bail!("invalid blob cache entry configuration information");
}
if entry.validate() == false {
bail!("invalid blob cache entry configuration information");
}

let threads: u32 = subargs
.value_of("threads")
.map(|n| n.parse().unwrap_or(1))
.unwrap_or(1);
let output = subargs.value_of("output").map(|v| v.to_string());

BlockDevice::export(entry, output, localfs_dir, threads)
.context("failed to export RAFS filesystem as raw block device image")
}
}

fn thread_validator(v: &str) -> std::result::Result<String, String> {
validate_threads_configuration(v).map(|s| s.to_string())
}

0 comments on commit 7c2861a

Please sign in to comment.