EZIO is a high-performance disk imaging tool for rapidly deploying dozens to hundreds of machines over a LAN. It distributes disk images peer-to-peer with the BitTorrent protocol and writes directly to raw disk partitions, achieving far faster deployment than traditional multicast — and it gets faster as you add more clients, because every client also seeds.
It uses partclone to capture only the used filesystem blocks, so images stay small without losing fidelity.
Just want to deploy? EZIO ships inside Clonezilla as its Lite Server Mode (since Clonezilla 2.6.0-31). That is the easiest way to use EZIO — no manual build required. See Easiest Path: Clonezilla.
Use Clonezilla Live (>= 2.6.0-31) and pick Lite Server Mode. EZIO is built in and driven through Clonezilla's menus — no compilation, no manual torrent handling. Recommended for most users.
The full workflow is: build EZIO -> capture an image with partclone -> create a torrent -> seed on the source -> download on each target.
# 1. Build (see Installation for dependencies)
mkdir build && cd build && cmake ../ && make && cd ..
# 2. Capture a partition into a partclone image + torrent.info
sudo partclone.extfs -c -T -s /dev/sda1 -O image_dir/ --buffer_size 16777216
# 3. Turn it into a torrent
utils/partclone_create_torrent.py \
-c CloneZilla -p sda1 \
-i image_dir/torrent.info -o sda1.torrent \
-t 'http://<tracker-host>:6969/announce'
# 4. Generate the gRPC Python stubs once (needed by the utils/ scripts)
cd utils && ./ezio_create_proto_py.sh && cd ..
# --- On the SOURCE machine: seed the disk ---
sudo ./build/ezio # start the daemon
utils/ezio_add_torrent.py -S sda1.torrent /dev/sda1 # -S = seeding mode
# --- On each TARGET machine: clone to disk ---
sudo ./build/ezio # start the daemon
utils/ezio_add_torrent.py sda1.torrent /dev/sda1 # download + write to disk
That's it. As targets download they automatically seed to each other. Monitor progress with utils/ezio_cli.py or the TUI utils/ezio_ui.py.
Note:
ezioruns in the foreground and blocks until it is shut down, so theezio_add_torrent.py/ monitoring commands must be run from a separate terminal. Stop the daemon withutils/ezio_shutdown.py(or letezio_uiauto-exit once seeding is done).
Tip:
eziowrites directly to raw devices, so it needs root (or CAP_DAC_OVERRIDE) for/dev/sdXaccess.
sudo apt install build-essential cmake libboost-all-dev \
libtorrent-rasterbar-dev libgrpc-dev libgrpc++-dev \
libprotobuf-dev protobuf-compiler-grpc libspdlog-dev clang-format
Python helper scripts in utils/ need:
pip install -r utils/requirements.txt
mkdir build && cd build
cmake ../
make
This produces the ezio binary in build/; the examples below run it as ./build/ezio.
docker build . -t ezio-latest-img
Run clang-format before committing:
find . -maxdepth 1 -name "*.cpp" -o -name "*.hpp" | grep -v "./tmp/" | xargs clang-format -i
ezio daemonezio is the disk I/O daemon. It exposes a gRPC control interface; you add and manage torrents through the utils/ scripts. It runs in the foreground until shut down, so start it in its own terminal and run the scripts below from another.
Allowed Options:
-h [ --help ] show help
-F [ --file ] read/write a regular file instead of a raw disk
-l [ --listen ] arg gRPC listen address:port (default 127.0.0.1:50051)
--cache-size arg unified cache size in MB (default 512)
--aio-threads arg threads for disk I/O and hashing (default 16)
--slow-start enable session-wide slow-start upload ramp (default off)
--slow-start-period arg slow-start step period in seconds (default 10)
-p [ --port ] arg BitTorrent peer listen port (default 6881)
-v [ --version ] show version
When --slow-start is enabled, the seeder caps its session-wide upload at 10 MB/s on launch and steps it up by 10 MB/s every --slow-start-period seconds until it reaches 100 MB/s, after which the limit is removed (unlimited). This eases a fleet of leechers into a deploy instead of slamming them with full bandwidth from the first second. The ramp is purely time-based and open-loop (it starts at daemon launch, not when peers connect).
The Python scripts need generated protobuf stubs:
cd utils && ./ezio_create_proto_py.sh && cd ..
Capture a partition with partclone (this also emits torrent.info):
# -T also writes the BT image; use -t if you only want torrent.info
sudo partclone.extfs -c -T -s /dev/sda1 -O image_dir/ --buffer_size 16777216
Then build the .torrent:
utils/partclone_create_torrent.py \
-c CloneZilla -p sda1 \
-i image_dir/torrent.info -o sda1.torrent \
-t 'http://<tracker-host>:6969/announce'
A single script, ezio_add_torrent.py, handles both. Add -S to seed.
# Seed an existing disk (source machine)
sudo ./build/ezio
utils/ezio_add_torrent.py -S sda1.torrent /dev/sda1
# Seed from a saved image file instead of a raw disk
sudo ./build/ezio -F
utils/ezio_add_torrent.py -S sda1.torrent /path/to/sda1.img
# Download and write to disk (target machine)
sudo ./build/ezio
utils/ezio_add_torrent.py sda1.torrent /dev/sda1
# Download to a file (proxy / save the image)
sudo ./build/ezio -F
utils/ezio_add_torrent.py sda1.torrent /path/to/save/sda1.img
ezio_add_torrent.py options:
torrent torrent file path
path destination disk or file
-a, --address gRPC server (default 127.0.0.1:50051)
-S, --seeding start in seeding mode
-s, --sequential sequential download
-c, --connections max total connections (default 3)
-u, --uploads max upload connections (default 2)
utils/ezio_cli.py # status / progress in the terminal
utils/ezio_ui.py # full TUI (sorting, filtering, color, help via 'h')
utils/ezio_pause_torrent.py / ezio_resume_torrent.py / ezio_shutdown.py
For WAN or bottlenecked links, proxy the torrent through a regular BitTorrent client such as qBittorrent, and keep internal peers from connecting directly to outside peers.
Control verbosity at runtime with SPDLOG_LEVEL:
SPDLOG_LEVEL=debug ./ezio # everything
SPDLOG_LEVEL=warn ./ezio # warnings + errors
SPDLOG_LEVEL=info,raw_disk_io=debug ./ezio # per-component
Levels: trace, debug, info (default), warn, error, critical, off.
| Option | Default | Purpose |
|---|---|---|
--aio-threads |
16 | Threads for disk I/O and SHA-1 hashing |
--cache-size |
512 | Lock-free unified cache size, MB |
Memory budget: cache (--cache-size, default 512 MB) + fixed buffer pools (512 MB read + 512 MB write) = ~1.5 GB by default.
A larger cache improves the hit rate (seeders re-serve hot pieces from RAM instead of re-reading the disk), but EZIO must coexist with the OS and the running deployment. A good rule of thumb is to set --cache-size to about 1/4 of system RAM, leaving the rest for buffer pools, the OS page cache, and headroom.
| System RAM | Suggested --cache-size |
|---|---|
| 4 GB | 1024 (1 GB) |
| 8 GB | 2048 (2 GB) |
| 16 GB | 4096 (4 GB) |
| 32 GB | 8192 (8 GB) |
./ezio --cache-size 4096 # ~16 GB machine
Remember the two 512 MB buffer pools (~1 GB) are always allocated on top of the cache, so keep total EZIO usage comfortably below available RAM.
./ezio --aio-threads 2 # HDD: fewer threads, less seek thrash
./ezio # SATA SSD: default 16 is fine
./ezio --aio-threads 32 # NVMe: high parallelism, can saturate 10GbE
Combine options as needed:
./ezio --aio-threads 32 --cache-size 1024 --listen 0.0.0.0:50051
Monitor with iostat/iotop (disk) and iftop/nload (network). Homogeneous deployments (all the same disk type) perform best.
EZIO implements a custom libtorrent disk I/O interface in raw_disk_io.cpp/.hpp that reads/writes /dev/sdX partitions directly, skipping the filesystem layer. Block offsets are computed on the fly, so EZIO streams data straight to the target disk — no need to buffer the whole image in RAM or stage it in temporary storage first. Images of any size deploy without extra disk space.
A 512 MB (configurable) write-through cache with a zero-mutex design:
hash(storage_index, piece_index) % aio_threads routes every operation on a piece to the same I/O thread, guaranteeing a piece's async_read runs after any pending async_write for it.store_buffer used by stock libtorrent.The disk offset (hex) is stored as the file path and the length as the file size, so BitTorrent can seek directly to the right disk position:
{
'announce': 'http://tracker.example.com/announce',
'info': {
'name': 'root',
'piece length': 262144,
'files': [
{'path': ['0000000000000000'], 'length': 4096}, // offset + length
{'path': ['0000000000020000'], 'length': 8192},
...
],
'pieces': '...'
}
}
Traditional multicast must synchronize all clients before starting, retransmits to the whole group on any single failure, and lets one broken client stall the deployment. With BitTorrent, clients seed as they download (load spreads across peers), failures cost only a single 16 KB block, and there's no global sync barrier. Supported filesystems include ext2/3/4, xfs, btrfs, f2fs, reiserfs, NTFS, FAT, HFS+, UFS and more; unsupported ones fall back to sector-by-sector dd.
EZIO vs. Clonezilla multicast. Lower is better; EZIO pulls ahead as clients scale.
pzstd, BT image is raw)| Clients | Unicast (s) | EZIO (s) | Multicast (s) | EZIO / Multicast |
|---|---|---|---|---|
| 1 | 474 | 675 | 390 | 1.731 |
| 2 | 948 | 1273 | 474 | 2.686 |
| 4 | 1896 | 1331 | 638 | 2.086 |
| 8 | 3792 | 1412 | 980 | 1.441 |
| 16 | 7584 | 1005 | 1454 | 0.691 |
| 24 | 11376 | 1048 | 1992 | 0.526 |
| 32 | 15168 | 1143 | 2203 | 0.519 |
At 16+ clients EZIO is roughly 2x faster than multicast and the gap keeps widening.
A recent end-to-end run of the current build, deploying a real partclone image directly onto raw NVMe partitions across separate hosts on a LAN.
/dev/nvme0n1p1 (source and target)--aio-threads 16, prefetch 16 blocks; cache size as notedblkdiscard on each target between runs to avoid SLC-cache skewTwo metrics per leecher: download speed as reported by ezio_ui (network in), and disk-write average (bytes written to NVMe / elapsed). Download outruns disk-write because the cache absorbs network bursts ahead of the flush to disk.
| Scenario | Cache | Time | Download speed (median / peak) | Disk-write avg |
|---|---|---|---|---|
| 1-on-1 | 512 MB | ~166 s | ~511 / 526 MB/s | ~374 MiB/s |
| 1-on-1 | 4 GB | ~116 s | ~836 / 874 MB/s | ~536 MiB/s |
| 1-to-3 | 512 MB | ~219 s | ~370 / 381 MB/s per leecher | ~283 MiB/s per leecher |
| 1-to-3 | 4 GB | ~210 s | ~375 / 444 MB/s per leecher | ~294 MiB/s per leecher |
A larger cache helps the 1-on-1 case most: once the seeder's cache is warm it serves the whole image from RAM, pushing download to ~840 MB/s and nearly halving the time. With 3 leechers the gain is small — the peers already offload the seeder by sharing pieces with each other, so the seeder reads only ~one image (~63 GiB) from disk per run regardless of leecher count, and aggregate delivered throughput (~1.1 GB/s across 3 leechers) scales with the number of peers.
Note: These are small-scale lab figures and understate EZIO's real advantage. BitTorrent's strength is scale — the more clients you deploy to, the more they seed to each other and the further EZIO pulls ahead of unicast or multicast (see the scaling table above). With only 1-3 nodes you are seeing close to its worst case, not its best.
A 1-on-1 run with the partition image served from and written to RAM (tmpfs), exercising the real raw-disk I/O path end to end:
| Scenario | Best | Typical |
|---|---|---|
| 1-on-1 (all-RAM) | ~1046 MiB/s (24.6 s) | ~1011 MiB/s |
Result is stable across --cache-size (512 MB–2 GB) and --aio-threads (8–32).
dd copy.Special thanks to the National Center for High-performance Computing (NCHC), Taiwan, for test hardware and support.
GNU General Public License v2.0.