Commit 7c6ff16a authored by jan.koester's avatar jan.koester
Browse files

Initial import of storaged

Software RAID (md) and LVM2 as a sysbus service, moved out of webctl's
raid and lvm modules: md v1.2 superblocks and LVM2 metadata written
directly, arrays run with the md ioctls, LVs as device-mapper devices.
Device writes run as jobs in child processes; at start it assembles
arrays and activates LVs. API in include/storaged/api.h.

Co-Authored-By: default avatarClaude Opus 5.5 <noreply@anthropic.com>
parents
Loading
Loading
Loading
Loading

CMakeLists.txt

0 → 100644
+41 −0
Changes for CMakeLists.txt: 41 added lines, 0 removed lines.
Original line number Diff line number Diff line
cmake_minimum_required(VERSION 3.16)
project(storaged VERSION 0.1.0 LANGUAGES CXX)

# storaged: software RAID (md) and LVM2 without mdadm and the lvm2 tools.
# md v1.2 superblocks and LVM2 labels and metadata are read and written
# here, arrays are run with the md ioctls and logical volumes are
# device-mapper devices. At start it assembles the arrays and activates
# the logical volumes it finds; it is controlled over sysbus as the
# service "storaged" (include/storaged/api.h) -- webctl's raid and lvm
# pages are its clients. Whatever writes to a device runs in a child
# process of its own: a disk that doesn't answer stalls that, not storaged.
#
#   storaged   the daemon (root)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()

include(GNUInstallDirs)

find_package(sysbus REQUIRED)

# needs the kernel's <linux/raid/md_p.h>, <linux/raid/md_u.h> and
# <linux/dm-ioctl.h>
add_executable(storaged
    src/main.cpp
    src/result.cpp
    src/jobs.cpp
    src/blockdev.cpp
    src/raid.cpp
    src/lvm.cpp
)
target_include_directories(storaged PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(storaged PRIVATE sysbus::sysbus)

install(TARGETS storaged RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
install(DIRECTORY include/storaged DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
set(STORAGED_UNITDIR "/etc/syslite/services" CACHE PATH "syslite unit directory")
install(FILES dist/storaged.service DESTINATION ${STORAGED_UNITDIR})

dist/storaged.service

0 → 100644
+10 −0
Changes for dist/storaged.service: 10 added lines, 0 removed lines.
Original line number Diff line number Diff line
[Unit]
Description=storaged: software RAID (md) and LVM2

[Service]
Type=simple
ExecStart=/usr/sbin/storaged
Restart=always

[Install]
WantedBy=multi-user.target

include/storaged/api.h

0 → 100644
+141 −0
Changes for include/storaged/api.h: 141 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

/* storaged on sysbus: the service "storaged", reached with
 * sysbus::Client::callService(storaged::Service, method, fields).
 * Every method needs root (the bus passes the caller's uid along): the
 * lists open and probe block devices too. Error messages are in English,
 * or in German if the call has a field "lang" starting with "de".
 * Flags are yes|no, sizes in bytes.
 *
 * Lists are flat: a record starts with its key (array, member, found,
 * device, defect, job, vg, pv, lv) and the fields after it belong to it
 * until the next record starts. member records belong to the array
 * before them, pv and lv records to the vg before them. "dev" (a kernel
 * device name such as sdb or md0) may repeat within a record.
 *
 * Software RAID (md), metadata 1.2, written and assembled by storaged
 * itself with the md ioctls:
 *   ListArrays -> per array: array (mdN), level, state, metadata,
 *       raid_disks, degraded, size, component_size, chunk, bitmap,
 *       sync_action, sync_percent?, sync_speed? (KiB/s), in_use, uuid?,
 *       label?; then per member: member (device), state, slot (-1: none)
 *     per set of members found on free devices: found (uuid), label,
 *       level, raid_disks, present, active_array? (it runs already as),
 *       dev (repeated)
 *     the devices nothing uses, defects and the RAID jobs (see below)
 *   CreateArray {level (0 1 5 6 10), dev..., spare...?, chunk? (KiB),
 *       confirm} -> array
 *   AddSpare {array, dev, confirm}      md rebuilds onto it when a member
 *                                       is missing
 *   RemoveMember {array, dev}           fails an active member first; its
 *                                       superblock is wiped
 *   CheckArray {array, stop?}           scrub (sync_action check/idle)
 *   StopArray {array}                   members keep their superblocks
 *   DeleteArray {array, confirm}        stop + wipe all superblocks
 *   AssembleArray {uuid} -> array       a set found on free devices
 *
 * LVM2 (on-disk format and device-mapper, without the lvm2 tools; only
 * linear/striped LVs in VGs without locking, system ID or missing PVs
 * are changed):
 *   ListVolumeGroups -> per VG: vg (name), id, extent_size, read_only?
 *       (why it can't be changed here), size, free; then per PV: pv (id),
 *       dev? (missing PVs have none), size, used; per visible LV: lv
 *       (name), size, path, active, open, plain, dev (repeated: the PVs it
 *       is on)
 *     the devices nothing uses (md arrays included, PVs of a VG not),
 *     defects and the LVM jobs
 *   CreateVG {name, dev..., confirm}
 *   ExtendVG {vg, dev, confirm}         a new PV
 *   ReduceVG {vg, dev}                  a PV no LV uses; label wiped
 *   RemoveVG {vg}                       without LVs; labels wiped
 *   CreateLV {vg, name, size | all} -> path      linear
 *   ExtendLV {vg, lv, size | all}       grows by size, also online
 *   RemoveLV {vg, lv, confirm}
 *   Activate / Deactivate {vg, lv?}     without lv: every plain LV
 *
 * Free devices, in both lists: device (name), size, model, serial,
 * partition, parent? (of a partition), content_type?, content_label?
 * (what blkid would name; it is lost), partitions? (a disk's: lost too).
 * Devices that are offline or don't answer: defect (name), size, model,
 * problem.
 *
 * Jobs: whatever writes to devices runs in a child process of its own, so
 * a disk that doesn't answer stalls that, not storaged. Done within about
 * three seconds, a method replies as listed; otherwise the reply is
 * background = yes and the job, which runs on. While one such job runs,
 * the next is refused (Busy). At start storaged assembles every set of
 * members it finds and activates every LV of every complete VG (for half
 * a minute, as arrays come up), as jobs that don't block others.
 *   job (id), area (raid|lvm), title, state (running|done|failed),
 *   seconds, finished (unix time, 0 while running), error?
 *   ListJobs {area?} -> the jobs of the last ten minutes */

#pragma once

namespace storaged {

constexpr const char *Service = "storaged";

namespace method {
constexpr const char *ListArrays = "ListArrays";
constexpr const char *CreateArray = "CreateArray";
constexpr const char *AddSpare = "AddSpare";
constexpr const char *RemoveMember = "RemoveMember";
constexpr const char *CheckArray = "CheckArray";
constexpr const char *StopArray = "StopArray";
constexpr const char *DeleteArray = "DeleteArray";
constexpr const char *AssembleArray = "AssembleArray";

constexpr const char *ListVolumeGroups = "ListVolumeGroups";
constexpr const char *CreateVG = "CreateVG";
constexpr const char *ExtendVG = "ExtendVG";
constexpr const char *ReduceVG = "ReduceVG";
constexpr const char *RemoveVG = "RemoveVG";
constexpr const char *CreateLV = "CreateLV";
constexpr const char *ExtendLV = "ExtendLV";
constexpr const char *RemoveLV = "RemoveLV";
constexpr const char *Activate = "Activate";
constexpr const char *Deactivate = "Deactivate";

constexpr const char *ListJobs = "ListJobs";
} // namespace method

namespace signal {
// fields: job, area, state -- a job ended (done or failed)
constexpr const char *JobFinished = "JobFinished";
} // namespace signal

// Error names besides sysbus's own (InvalidArgs, Failed, AccessDenied,
// UnknownMethod).
namespace error {
constexpr const char *NotFound = "storaged.Error.NotFound";        // no such array, VG, LV, ...
constexpr const char *Busy = "storaged.Error.Busy";                // in use, or another job runs
constexpr const char *Unavailable = "storaged.Error.Unavailable";  // the kernel lacks md / device-mapper
} // namespace error

} // namespace storaged

src/blockdev.cpp

0 → 100644
+644 −0

File added.

Preview size limit exceeded, changes collapsed.

src/blockdev.h

0 → 100644
+159 −0
Changes for src/blockdev.h: 159 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

/* Block devices for raid and lvm: which ones nothing uses, what is on
 * them, and O_DIRECT I/O on them. */

#pragma once

#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <set>
#include <string>
#include <vector>

#include <sys/types.h>

namespace storaged::blk {

const uint64_t MiB = 1 << 20;
const size_t Block = 4096;  // O_DIRECT unit

// ---- files ----

std::string readLine(const std::string &path);  // first line, trailing blanks cut
bool writeSys(const std::string &path, const std::string &value);  // errno on failure
bool exists(const std::string &path);
std::vector<std::string> dirEntries(const std::string &path);  // sorted, no dot files
bool startsWith(const std::string &s, const std::string &p);
std::string sysErr(const std::string &what, int e);
std::string hostName();
void randomBytes(uint8_t *p, size_t n);
std::string hex(const uint8_t *p, size_t n);

// ---- O_DIRECT I/O ----

// page-aligned, zeroed buffer
struct Buf {
    void *p = nullptr;
    size_t n;
    explicit Buf(size_t size);
    ~Buf();
    Buf(const Buf &) = delete;
    Buf &operator=(const Buf &) = delete;
    uint8_t *data() { return (uint8_t *)p; }
};

bool readAt(int fd, void *buf, size_t n, uint64_t off);
bool writeAt(int fd, const void *buf, size_t n, uint64_t off);
bool zeroAt(int fd, uint64_t off, uint64_t len);
uint64_t devBytes(int fd);  // BLKGETSIZE64, 0 if unknown

// ---- devices ----

struct Dev {
    std::string name, parent, model, serial;
    dev_t dev = 0;
    uint64_t bytes = 0;
    bool partition = false, readOnly = false;
};

bool parseDev(const std::string &s, dev_t &d);  // "8:16"

// candidates for storage: no ram/zram, optical, floppy, dm or empty
// devices; md arrays (and their partitions) only withMd
std::vector<Dev> blockDevices(bool withMd);

// mounted (also by a mount source that isn't the superblock's dev_t, as
// with btrfs) or swap
std::set<dev_t> usedByMounts();
bool hasHolders(const std::string &name);
// claimed in the kernel (mount, md, dm, swap, another O_EXCL); guarded
bool claimed(const std::string &name);

// ---- devices that don't answer ----
//
// A dying disk often doesn't fail, it doesn't answer: a read can hang for
// minutes, and uninterruptibly. Whatever touches devices just to show them
// runs in child processes with a time limit (runGuarded); one that runs
// over is left behind -- it can't be killed while the kernel waits -- and
// its key (the device) counts as not responding until that child is gone.
// Nothing touches it meanwhile.

struct Guarded {
    bool done = false;   // finished in time
    std::string out;     // what the task returned
};
// the tasks in parallel child processes, at most timeoutMs in all; a task
// whose key doesn't respond isn't started
std::vector<Guarded> runGuarded(const std::vector<std::pair<std::string, std::function<std::string()>>> &tasks,
                                int timeoutMs = 5000);
bool notResponding(const std::string &key);

// why the kernel won't talk to a device ("" if it will): its SCSI/NVMe
// state isn't running/live (for a partition: its disk's)
std::string offlineReason(const std::string &name);

struct Defect {
    Dev dev;
    std::string problem;     // "offline: <state>" or "not responding"
};

// the devices nothing uses, from minBytes up and writable; a disk only
// if none of its partitions is used either. Devices that are offline or
// don't answer aren't among them; *defective gets them.
std::vector<Dev> freeDevices(bool withMd, uint64_t minBytes, std::vector<Defect> *defective = nullptr);

// number of partitions the kernel knows on a disk
int partitionCount(const std::string &name);

// what a device holds, for the warning before it is overwritten:
// type as blkid names it ("ext4", "linux_raid_member", "LVM2_member",
// "gpt", ...), label (file system label, md array or LVM VG name)
struct Content {
    std::string type, label;
    std::string uuid;  // file systems and swap, as blkid prints it
};
Content probe(int fd, uint64_t bytes);  // fd opened with O_DIRECT, unguarded
Content probe(const std::string &name);  // guarded: type "" when it didn't answer

// many devices at once, guarded and in parallel: claimed and content each
struct Look {
    bool answered = false;
    std::string problem;     // when not: "offline: <state>", "not responding"
    bool claimed = false;
    Content content;
};
std::map<std::string, Look> inspect(const std::vector<std::string> &names);

// writes over a device's start and end (1 MiB each): partition tables,
// file system, md and LVM signatures
bool wipeEnds(int fd, uint64_t bytes);

} // namespace storaged::blk
Loading