hb_volume.erl - Physical Disk & Volume Management
Overview
Purpose: Manage physical disks, partitions, encryption, and volume mounting
Module: hb_volume
Pattern: OS command wrappers with error handling and security features
This module provides operations for partitioning, formatting, mounting, and managing encrypted volumes on Linux systems. Designed for setting up and migrating HyperBEAM node storage across physical disks with LUKS encryption support.
Dependencies
- Erlang/OTP:
os,file,filelib,string,re - HyperBEAM:
hb_store_lmdb,hb_json,hb_opts - System:
sudo,fdisk,parted,mkfs,cryptsetup,mount
Public Functions Overview
%% Disk Information
-spec list_partitions() -> {ok, map()} | {error, binary()}.
%% Partition Management
-spec create_partition(Device, PartType) -> {ok, map()} | {error, binary()}.
%% Disk Operations (LUKS encryption)
-spec format_disk(Partition, EncKey) -> {ok, map()} | {error, binary()}.
-spec mount_disk(Partition, EncKey, MountPoint, VolumeName) -> {ok, map()} | {error, binary()}.
%% Store Migration
-spec change_node_store(StorePath, CurrentStore) -> {ok, map()} | {error, binary()}.
%% Utilities
-spec check_for_device(Device) -> boolean().Public Functions
1. list_partitions/0
-spec list_partitions() -> {ok, map()} | {error, binary()}.Description: List all available partitions and disks using fdisk -l. Parses output to extract disk information including size, model, and sector details.
{ok, #{
<<"status">> => 200,
<<"content-type">> => <<"application/json">>,
<<"body">> => JSON % Encoded array of disk objects
}}#{
<<"device">> => <<"/dev/sdb">>,
<<"size">> => <<"931.5 GiB">>,
<<"bytes">> => 1000204886016,
<<"sectors">> => 1953525168,
<<"model">> => <<"Samsung SSD 860">>,
<<"sector_size">> => #{
<<"logical">> => <<"512 bytes">>,
<<"physical">> => <<"512 bytes">>
}
}-module(hb_volume_list_test).
-include_lib("eunit/include/eunit.hrl").
list_partitions_test() ->
case hb_volume:list_partitions() of
{ok, Result} ->
?assert(maps:is_key(<<"status">>, Result)),
?assertEqual(200, maps:get(<<"status">>, Result)),
Body = hb_json:decode(maps:get(<<"body">>, Result)),
?assert(maps:is_key(<<"disks">>, Body));
{error, _} ->
% May fail if not run with sudo
ok
end.2. create_partition/2
-spec create_partition(Device, PartType) -> {ok, map()} | {error, binary()}
when
Device :: binary(),
PartType :: binary().Description: Create a GPT partition table and partition on a device. Uses parted to create the partition spanning the entire disk.
Device: Device path (e.g.,<<"/dev/sdb">>)PartType: Partition type (e.g.,<<"ext4">>,<<"xfs">>)
-module(hb_volume_create_partition_test).
-include_lib("eunit/include/eunit.hrl").
% NOTE: This is a destructive operation - only test on dedicated test devices
create_partition_validation_test() ->
% Test with undefined device
?assertEqual(
{error, <<"Device path not specified">>},
hb_volume:create_partition(undefined, <<"ext4">>)
).
% Integration test would require physical device
% create_partition_integration_test() ->
% Device = <<"/dev/sdb">>, % WARNING: Will erase device!
% {ok, Result} = hb_volume:create_partition(Device, <<"ext4">>),
% ?assertEqual(200, maps:get(<<"status">>, Result)).3. format_disk/2
-spec format_disk(Partition, EncKey) -> {ok, map()} | {error, binary()}
when
Partition :: binary(),
EncKey :: binary().Description: Format a partition with LUKS encryption using cryptsetup luksFormat.
Partition: Partition path (e.g.,<<"/dev/sdb1">>)EncKey: Encryption key/passphrase for LUKS
-module(hb_volume_format_test).
-include_lib("eunit/include/eunit.hrl").
format_disk_validation_test() ->
% Test with undefined partition
?assertEqual(
{error, <<"Partition path not specified">>},
hb_volume:format_disk(undefined, <<"test-key">>)
),
% Test with undefined key
?assertEqual(
{error, <<"Encryption key not specified">>},
hb_volume:format_disk(<<"/dev/sdb1">>, undefined)
).4. mount_disk/4
-spec mount_disk(Partition, EncKey, MountPoint, VolumeName) -> {ok, map()} | {error, binary()}
when
Partition :: binary(),
EncKey :: binary(),
MountPoint :: binary(),
VolumeName :: binary().Description: Mount a LUKS-encrypted partition. Opens the encrypted device with cryptsetup luksOpen, then mounts to the specified mount point.
Partition: Partition path (e.g.,<<"/dev/sdb1">>)EncKey: Encryption key/passphrase for LUKSMountPoint: Directory to mount to (e.g.,<<"/encrypted">>)VolumeName: Name for decrypted volume (e.g.,<<"encrypted_data">>)
- Write key to secure temp file
- Open LUKS volume with
cryptsetup luksOpen - Create mount point directory
- Mount decrypted device
-module(hb_volume_mount_test).
-include_lib("eunit/include/eunit.hrl").
mount_disk_validation_test() ->
% Test with undefined partition
?assertEqual(
{error, <<"Partition path not specified">>},
hb_volume:mount_disk(undefined, <<"key">>, <<"/mnt">>, <<"vol">>)
),
% Test with undefined key
?assertEqual(
{error, <<"Encryption key not specified">>},
hb_volume:mount_disk(<<"/dev/sdb1">>, undefined, <<"/mnt">>, <<"vol">>)
),
% Test with undefined mount point
?assertEqual(
{error, <<"Mount point not specified">>},
hb_volume:mount_disk(<<"/dev/sdb1">>, <<"key">>, undefined, <<"vol">>)
).5. change_node_store/2
-spec change_node_store(StorePath, CurrentStore) -> {ok, map()} | {error, binary()}
when
StorePath :: binary(),
CurrentStore :: list().Description: Update HyperBEAM node store configuration to use a new path. Creates the directory and updates all store configs to prefix paths with the new location.
Parameters:StorePath: New base path for stores (e.g.,<<"/encrypted">>)CurrentStore: Current store configuration list
{ok, #{
<<"status">> => 200,
<<"message">> => <<"Node store updated to use encrypted disk.">>,
<<"store_path">> => StorePath,
<<"store">> => UpdatedStoreConfig
}}-module(hb_volume_change_store_test).
-include_lib("eunit/include/eunit.hrl").
change_node_store_validation_test() ->
% Test with undefined store path
?assertEqual(
{error, <<"Store path not specified">>},
hb_volume:change_node_store(undefined, [])
).
change_node_store_updates_config_test() ->
% Test config update with valid path
StoreConfig = [#{<<"store-module">> => hb_store_fs, <<"name">> => <<"cache">>}],
{ok, Result} = hb_volume:change_node_store(<<"/encrypted">>, StoreConfig),
?assertEqual(200, maps:get(<<"status">>, Result)),
?assertEqual(<<"/encrypted">>, maps:get(<<"store_path">>, Result)).6. check_for_device/1
-spec check_for_device(Device) -> boolean()
when
Device :: binary().Description: Check if a device exists on the system using ls.
-module(hb_volume_check_test).
-include_lib("eunit/include/eunit.hrl").
check_for_device_test() ->
?assertEqual(true, hb_volume:check_for_device(<<"/dev/null">>)),
?assertEqual(false, hb_volume:check_for_device(<<"/dev/nonexistent_123">>)).Common Patterns
%% List available disks
{ok, Response} = hb_volume:list_partitions(),
Body = hb_json:decode(maps:get(<<"body">>, Response)),
Disks = maps:get(<<"disks">>, Body).
%% Full encrypted disk setup
Device = <<"/dev/sdb">>,
EncKey = <<"my-secure-passphrase">>,
% 1. Create partition
{ok, _} = hb_volume:create_partition(Device, <<"ext4">>),
% 2. Format with LUKS encryption
{ok, _} = hb_volume:format_disk(<<"/dev/sdb1">>, EncKey),
% 3. Mount encrypted volume
{ok, _} = hb_volume:mount_disk(
<<"/dev/sdb1">>,
EncKey,
<<"/encrypted">>,
<<"encrypted_data">>
),
% 4. Update store config to use new mount point
CurrentStore = hb_opts:get(store),
{ok, _} = hb_volume:change_node_store(<<"/encrypted">>, CurrentStore).
%% Check device before operations
case hb_volume:check_for_device(Device) of
true -> proceed_with_setup(Device);
false -> {error, device_not_found}
end.LUKS Encryption Details
Encryption Process
% 1. Setup LUKS on partition (via format_disk/2)
"sudo cryptsetup luksFormat --batch-mode --key-file /root/tmp/key /dev/sdb1"
% 2. Open encrypted device (via mount_disk/4)
"sudo cryptsetup luksOpen --key-file /root/tmp/key /dev/sdb1 volume_name"
% 3. Mount mapped device
"sudo mount /dev/mapper/volume_name /encrypted"Security Features
- Passphrase passed via stdin (not command line)
- Temporary key files created in
/root/tmp - Automatic cleanup of key files
- Exception-safe key file deletion
Store Migration
Configuration Update
% Updates store paths from old to new mount point
update_store_config(StoreConfig, <<"/encrypted">>)
% For filesystem stores:
#{<<"name">> => <<"cache">>}
→ #{<<"name">> => <<"/encrypted/cache">>}
% For LMDB stores:
% 1. Stops current store
% 2. Updates path
% 3. Starts new store
#{<<"name">> => <<"cache/lmdb">>}
→ #{<<"name">> => <<"/encrypted/cache/lmdb">>}
% For gateway stores:
% Recursively updates nested storesError Handling
Common Errors
% Device not found
{error, <<"Device not found">>}
% Partition creation failed
{error, <<"Failed to create partition: ...", ErrorDetail/binary>>}
% Format failed
{error, <<"Failed to format disk: ...", ErrorDetail/binary>>}
% Mount failed
{error, <<"Failed to mount device: ...", ErrorDetail/binary>>}
% Encryption failed
{error, <<"Failed to setup encryption: ...", ErrorDetail/binary>>}Error Detection
% Check command output for error keywords
check_command_errors(Output, ["Error", "failed", "cannot"])Security Considerations
- Sudo Required: All operations require sudo privileges
- Passphrase Handling: Never logged, passed via stdin
- Temporary Files: Key files cleaned up even on exceptions
- Secure Location: Key files in
/root/tmp(root-only access) - Batch Mode: LUKS operations use
--batch-modeto avoid prompts
System Requirements
Required Commands
sudo- Root privilege executionfdisk- Partition listingparted- Partition creationmkfs.ext4/mkfs.xfs- Filesystem formattingcryptsetup- LUKS encryptionmount- Filesystem mountingls- Device checking
Permissions
Operations require:
- Sudo access
- Write access to
/root/tmp - Permission to modify block devices
- Permission to create mount points
Disk Information Parsing
fdisk Output Format
Disk /dev/sdb: 931.5 GiB, 1000204886016 bytes, 1953525168 sectors
Disk model: Samsung SSD 860
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytesParsed Structure
#{
<<"device">> => <<"/dev/sdb">>,
<<"size">> => <<"931.5 GiB">>,
<<"bytes">> => 1000204886016,
<<"sectors">> => 1953525168,
<<"model">> => <<"Samsung SSD 860">>,
<<"units">> => <<"sectors of 1 * 512 = 512 bytes">>,
<<"sector_size">> => #{
<<"logical">> => <<"512 bytes">>,
<<"physical">> => <<"512 bytes">>
},
<<"io_size">> => #{
<<"minimum">> => <<"512 bytes">>,
<<"optimal">> => <<"512 bytes">>
}
}Testing Utilities
with_secure_key_file/2
Internal helper for secure temporary key file management:
% Creates temporary key file
% Executes function with file path
% Ensures cleanup even on exceptions
with_secure_key_file(EncryptionKey, fun(KeyFile) ->
% Use KeyFile for operations
cryptsetup_command(KeyFile)
end)References
- LUKS - Linux Unified Key Setup (disk encryption)
- parted - GNU partition editor
- cryptsetup - LUKS setup utility
- fdisk - Partition table manipulator
Notes
- Destructive Operations: Partition and format operations erase data
- Sudo Required: All operations need root privileges
- LUKS Encryption: Industry-standard Linux disk encryption
- Store Migration: Automatically updates HyperBEAM store paths
- Error Recovery: Failed operations may leave partial state
- Device Naming: Partition typically gets device name + "1" (e.g., /dev/sdb1)
- Mount Points: Creates mount point directories if needed
- Regex Parsing: Uses regex to parse fdisk/parted output
- RAM Disks: Explicitly excludes /dev/ram devices from listing
- Security: Passphrase never appears in process listings