mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-26 02:49:02 -04:00
6810 lines
191 KiB
C
6810 lines
191 KiB
C
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2011 Andrea Mazzoleni
|
|
|
|
#include "os/portable.h"
|
|
|
|
#include "elem.h"
|
|
#include "import.h"
|
|
#include "search.h"
|
|
#include "state.h"
|
|
#include "support.h"
|
|
#include "parity.h"
|
|
#include "stream.h"
|
|
#include "handle.h"
|
|
#include "io.h"
|
|
#include "thermal.h"
|
|
#include "raid/raid.h"
|
|
#include "raid/cpu.h"
|
|
|
|
/**
|
|
* Configure the multithread support.
|
|
*
|
|
* Multi thread for write could be either faster or slower, depending
|
|
* on the specific conditions. With multithreads it's likely faster
|
|
* writing to disk, but you'll need to access multiple times the same data,
|
|
* being potentially slower.
|
|
*
|
|
* Multi thread for verify is instead always generally faster,
|
|
* so we enable it if possible.
|
|
*/
|
|
#if HAVE_THREAD
|
|
/* #define HAVE_MT_WRITE 1 */
|
|
#define HAVE_MT_VERIFY 1
|
|
#endif
|
|
|
|
/*
|
|
* Start content-file writeback in bounded windows to avoid dirty-page
|
|
* throttling; the subsequent fsync() remains responsible for durability.
|
|
*
|
|
* Best test-rewrite serialization/write times with four 4 GB content files:
|
|
* ADVISE_SEQUENTIAL: 128s
|
|
* ADVISE_FLUSH: 86s
|
|
* ADVISE_DISCARD_WINDOW: 73s
|
|
* ADVISE_FLUSH_WINDOW: 61s
|
|
*/
|
|
#define CONTENT_WRITE_ADVISE ADVISE_FLUSH_WINDOW
|
|
|
|
const char* lev_name(unsigned l)
|
|
{
|
|
switch (l) {
|
|
case 0 : return "Parity";
|
|
case 1 : return "2-Parity";
|
|
case 2 : return "3-Parity";
|
|
case 3 : return "4-Parity";
|
|
case 4 : return "5-Parity";
|
|
case 5 : return "6-Parity";
|
|
}
|
|
|
|
return "invalid";
|
|
}
|
|
|
|
const char* lev_config_name(unsigned l)
|
|
{
|
|
switch (l) {
|
|
case 0 : return "parity";
|
|
case 1 : return "2-parity";
|
|
case 2 : return "3-parity";
|
|
case 3 : return "4-parity";
|
|
case 4 : return "5-parity";
|
|
case 5 : return "6-parity";
|
|
}
|
|
|
|
return "invalid";
|
|
}
|
|
|
|
static int lev_config_scan(const char* s, unsigned* level, unsigned* mode)
|
|
{
|
|
if (strcmp(s, "parity") == 0 || strcmp(s, "1-parity") == 0) {
|
|
*level = 0;
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(s, "q-parity") == 0 || strcmp(s, "2-parity") == 0) {
|
|
*level = 1;
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(s, "r-parity") == 0 || strcmp(s, "3-parity") == 0) {
|
|
*level = 2;
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(s, "4-parity") == 0) {
|
|
*level = 3;
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(s, "5-parity") == 0) {
|
|
*level = 4;
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(s, "6-parity") == 0) {
|
|
*level = 5;
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(s, "z-parity") == 0) {
|
|
*level = 2;
|
|
if (mode)
|
|
*mode = RAID_MODE_VANDERMONDE_RAID;
|
|
return 0;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
const char* lev_raid_name(unsigned mode, unsigned n)
|
|
{
|
|
switch (n) {
|
|
case 1 : return "par1";
|
|
case 2 : return "par2";
|
|
case 3 : if (mode == RAID_MODE_CAUCHY_RAID || mode == RAID_MODE_CAUCHY_AES)
|
|
return "par3";
|
|
else
|
|
return "parz";
|
|
case 4 : return "par4";
|
|
case 5 : return "par5";
|
|
case 6 : return "par6";
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void state_init(struct snapraid_state* state)
|
|
{
|
|
unsigned l, s, i;
|
|
|
|
memset(&state->opt, 0, sizeof(state->opt));
|
|
state->mapped_device = 0;
|
|
state->snapshot = 0;
|
|
state->filter_hidden = 0;
|
|
state->autosave = 0;
|
|
state->need_write = 0;
|
|
state->written = 0;
|
|
state->checked_read = 0;
|
|
state->block_size = 256 * KIBI; /* default 256 KiB */
|
|
state->raid_mode = RAID_MODE_CAUCHY_RAID;
|
|
state->file_mode = ADVISE_DEFAULT;
|
|
for (l = 0; l < LEV_MAX; ++l) {
|
|
state->parity[l].split_mac = 0;
|
|
for (s = 0; s < SPLIT_MAX; ++s) {
|
|
state->parity[l].split_map[s].path[0] = 0;
|
|
state->parity[l].split_map[s].uuid[0] = 0;
|
|
state->parity[l].split_map[s].content_uuid[0] = 0;
|
|
state->parity[l].split_map[s].fstype[0] = 0;
|
|
state->parity[l].split_map[s].fslabel[0] = 0;
|
|
state->parity[l].split_map[s].size = PARITY_SIZE_INVALID;
|
|
state->parity[l].split_map[s].device = 0;
|
|
}
|
|
state->parity[l].smartctl[0] = 0;
|
|
state->parity[l].smartctl_info[0] = 0;
|
|
for (i = 0; i < SMART_IGNORE_MAX; ++i) {
|
|
state->parity[l].smartignore[i].attr_index = 0;
|
|
state->parity[l].smartignore[i].attr_name[0] = 0;
|
|
}
|
|
state->parity[l].total_blocks = 0;
|
|
state->parity[l].free_blocks = 0;
|
|
state->parity[l].skip_access = 0;
|
|
state->parity[l].tick = 0;
|
|
state->parity[l].cached_blocks = 0;
|
|
state->parity[l].is_excluded_by_filter = 0;
|
|
}
|
|
state->tick_io = 0;
|
|
state->tick_misc = 0;
|
|
state->tick_sched = 0;
|
|
state->tick_raid = 0;
|
|
state->tick_hash = 0;
|
|
state->tick_last = os_tick();
|
|
state->share[0] = 0;
|
|
state->pool[0] = 0;
|
|
state->pool_device = 0;
|
|
state->lockfile[0] = 0;
|
|
state->level = 1; /* default is the lowest protection */
|
|
state->no_conf = 0;
|
|
for (i = 0; i < SMART_IGNORE_MAX; ++i) {
|
|
state->smartignore[i].attr_index = 0;
|
|
state->smartignore[i].attr_name[0] = 0;
|
|
}
|
|
state->rehash_blocks = 0;
|
|
state->bad_blocks = 0;
|
|
state->unsynced_blocks = 0;
|
|
state->unscrubbed_blocks = 0;
|
|
state->thermal_stop_gathering = 0;
|
|
state->thermal_system_temperature = 0;
|
|
state->thermal_ambient_temperature = 0;
|
|
state->thermal_highest_temperature = 0;
|
|
state->thermal_first = 0;
|
|
state->thermal_latest = 0;
|
|
state->thermal_temperature_limit = 0;
|
|
state->thermal_cooldown_time = 0;
|
|
|
|
tommy_list_init(&state->disklist);
|
|
tommy_list_init(&state->extralist);
|
|
tommy_list_init(&state->maplist);
|
|
tommy_list_init(&state->contentlist);
|
|
tommy_list_init(&state->filterlist);
|
|
tommy_list_init(&state->importlist);
|
|
tommy_list_init(&state->thermallist);
|
|
tommy_hashdyn_init(&state->importset);
|
|
tommy_hashdyn_init(&state->previmportset);
|
|
tommy_hashdyn_init(&state->searchset);
|
|
tommy_arrayblkof_init(&state->infoarr, sizeof(snapraid_info));
|
|
tommy_list_init(&state->bucketlist);
|
|
}
|
|
|
|
void state_done(struct snapraid_state* state)
|
|
{
|
|
tommy_list_foreach(&state->disklist, disk_free);
|
|
tommy_list_foreach(&state->extralist, extra_free);
|
|
tommy_list_foreach(&state->maplist, map_free);
|
|
tommy_list_foreach(&state->contentlist, content_free);
|
|
tommy_list_foreach(&state->filterlist, filter_free);
|
|
tommy_list_foreach(&state->importlist, import_file_free);
|
|
tommy_list_foreach(&state->thermallist, thermal_free);
|
|
tommy_hashdyn_foreach(&state->searchset, search_file_free);
|
|
tommy_hashdyn_done(&state->importset);
|
|
tommy_hashdyn_done(&state->previmportset);
|
|
tommy_hashdyn_done(&state->searchset);
|
|
tommy_arrayblkof_done(&state->infoarr);
|
|
tommy_list_foreach(&state->bucketlist, bucket_free);
|
|
}
|
|
|
|
/**
|
|
* Check the configuration.
|
|
*/
|
|
static void state_config_check(struct snapraid_state* state, const char* path, tommy_list* filterlist_disk)
|
|
{
|
|
tommy_node* i;
|
|
unsigned l, s;
|
|
size_t disk_count;
|
|
|
|
/* check total memory budget for block buffers (max 256 MiB) */
|
|
disk_count = tommy_list_count(&state->disklist) + state->level;
|
|
if (disk_count > (256 * 1024) / (state->block_size / KIBI)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "The combination of %zu disks and %u KiB 'blocksize' in '%s' exceeds the 256 MiB limit.\n",
|
|
disk_count, state->block_size / KIBI, path);
|
|
log_fatal(EUSER, "Please reduce the 'blocksize' or the number of disks.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* check for parity level */
|
|
if (state->raid_mode == RAID_MODE_VANDERMONDE_RAID) {
|
|
if (state->level > 3) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Using z-parity limits you to a maximum of 3 parities.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STIO */
|
|
}
|
|
}
|
|
|
|
for (l = 0; l < state->level; ++l) {
|
|
if (state->parity[l].split_mac == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Missing '%s' specification in '%s'\n", lev_config_name(l), path);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
if (tommy_list_empty(&state->contentlist)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Missing 'content' specification in '%s'\n", path);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* check for equal paths */
|
|
for (i = state->contentlist; i != 0; i = i->next) {
|
|
struct snapraid_content* content = i->data;
|
|
|
|
for (l = 0; l < state->level; ++l) {
|
|
for (s = 0; s < state->parity[l].split_mac; ++s) {
|
|
if (pathcmp(state->parity[l].split_map[s].path, content->content) == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Same path used for '%s' and 'content' as '%s'\n", lev_config_name(l), content->content);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* check device of data disks */
|
|
if (!state->opt.skip_device && !state->opt.skip_disk_access) {
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
tommy_node* j;
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
/* skip data disks that are not accessible */
|
|
if (disk->skip_access)
|
|
continue;
|
|
|
|
#ifdef _WIN32
|
|
if (disk->mount_device == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Disk '%s' has a zero serial number.\n", disk->dir);
|
|
log_fatal(ESOFT, "This is not necessarily wrong, but for using SnapRAID\n");
|
|
log_fatal(ESOFT, "it's better to change the serial number of the disk.\n");
|
|
log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
#endif
|
|
|
|
for (j = i->next; j != 0; j = j->next) {
|
|
struct snapraid_disk* other = j->data;
|
|
if (disk->mount_device == other->mount_device) {
|
|
if (state->opt.force_device) {
|
|
/*
|
|
* Note that we just ignore the issue
|
|
* and we DON'T mark the disk to be skipped
|
|
* because we want to use these disks
|
|
*/
|
|
if (!state->opt.no_warnings)
|
|
log_fatal(EUSER, "DANGER! Ignoring that disks '%s' and '%s' are on the same device\n", disk->name, other->name);
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Disks '%s' and '%s' are on the same device.\n", disk->mount_point, other->mount_point);
|
|
#ifdef _WIN32
|
|
log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->mount_device);
|
|
log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n");
|
|
log_fatal(ESOFT, "to change one of the disk serial.\n");
|
|
#endif
|
|
/* in "fix" we allow to continue anyway */
|
|
if (strcmp(state->command, "fix") == 0) {
|
|
log_fatal(ESOFT, "You can '%s' anyway, using 'snapraid --force-device %s'.\n", state->command, state->command);
|
|
}
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!state->opt.skip_parity_access) {
|
|
for (l = 0; l < state->level; ++l) {
|
|
for (s = 0; s < state->parity[l].split_mac; ++s) {
|
|
if (disk->mount_device == state->parity[l].split_map[s].device) {
|
|
if (state->opt.force_device) {
|
|
/*
|
|
* Note that we just ignore the issue
|
|
* and we DON'T mark the disk to be skipped
|
|
* because we want to use these disks
|
|
*/
|
|
if (!state->opt.no_warnings)
|
|
log_fatal(EUSER, "DANGER! Ignoring that disks '%s' and %s '%s' are on the same device\n", disk->mount_point, lev_name(l), state->parity[l].split_map[s].path);
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Disk '%s' and %s '%s' are on the same device.\n", disk->mount_point, lev_name(l), state->parity[l].split_map[s].path);
|
|
#ifdef _WIN32
|
|
log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->mount_device);
|
|
log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n");
|
|
log_fatal(ESOFT, "to change one of the disk serial.\n");
|
|
#endif
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state->pool[0] != 0 && disk->mount_device == state->pool_device) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Disk '%s' and pool '%s' are on the same device.\n", disk->mount_point, state->pool);
|
|
#ifdef _WIN32
|
|
log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->mount_device);
|
|
log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n");
|
|
log_fatal(ESOFT, "to change one of the disk serial.\n");
|
|
#endif
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
|
|
/* check device of parity disks */
|
|
if (!state->opt.skip_device && !state->opt.skip_parity_access) {
|
|
for (l = 0; l < state->level; ++l) {
|
|
for (s = 0; s < state->parity[l].split_mac; ++s) {
|
|
unsigned j, t;
|
|
|
|
/* skip parity disks that are not accessible */
|
|
if (state->parity[l].skip_access)
|
|
continue;
|
|
|
|
#ifdef _WIN32
|
|
if (state->parity[l].split_map[s].device == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Disk '%s' has a zero serial number.\n", state->parity[l].split_map[s].path);
|
|
log_fatal(ESOFT, "This is not necessarily wrong, but for using SnapRAID\n");
|
|
log_fatal(ESOFT, "it's better to change the serial number of the disk.\n");
|
|
log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
#endif
|
|
|
|
for (j = l + 1; j < state->level; ++j) {
|
|
for (t = 0; t < state->parity[j].split_mac; ++t) {
|
|
if (state->parity[l].split_map[s].device == state->parity[j].split_map[t].device) {
|
|
if (state->opt.force_device) {
|
|
/*
|
|
* Note that we just ignore the issue
|
|
* and we DON'T mark the disk to be skipped
|
|
* because we want to use these disks
|
|
*/
|
|
if (!state->opt.no_warnings)
|
|
log_fatal(EUSER, "DANGER! Skipping parities '%s' and '%s' on the same device\n", lev_config_name(l), lev_config_name(j));
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Parity '%s' and '%s' are on the same device.\n", state->parity[l].split_map[s].path, state->parity[j].split_map[t].path);
|
|
#ifdef _WIN32
|
|
log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", state->parity[l].split_map[s].device);
|
|
log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n");
|
|
log_fatal(ESOFT, "to change one of the disk serial.\n");
|
|
#endif
|
|
/* in "fix" we allow to continue anyway */
|
|
if (strcmp(state->command, "fix") == 0) {
|
|
log_fatal(ESOFT, "You can '%s' anyway, using 'snapraid --force-device %s'.\n", state->command, state->command);
|
|
}
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state->pool[0] != 0 && state->pool_device == state->parity[l].split_map[s].device) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Pool '%s' and parity '%s' are on the same device.\n", state->pool, state->parity[l].split_map[s].path);
|
|
#ifdef _WIN32
|
|
log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", state->pool_device);
|
|
log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n");
|
|
log_fatal(ESOFT, "to change one of the disk serial.\n");
|
|
#endif
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* check device of pool disk */
|
|
#ifdef _WIN32
|
|
if (!state->opt.skip_device) {
|
|
if (state->pool[0] != 0 && state->pool_device == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Disk '%s' has a zero serial number.\n", state->pool);
|
|
log_fatal(ESOFT, "This is not necessarily wrong, but for using SnapRAID\n");
|
|
log_fatal(ESOFT, "it's better to change the serial number of the disk.\n");
|
|
log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* count the content files */
|
|
if (!state->opt.skip_device && !state->opt.skip_content_access) {
|
|
unsigned content_count;
|
|
|
|
content_count = 0;
|
|
for (i = state->contentlist; i != 0; i = i->next) {
|
|
tommy_node* j;
|
|
struct snapraid_content* content = i->data;
|
|
|
|
/* check if there are others in the same disk */
|
|
for (j = i->next; j != 0; j = j->next) {
|
|
struct snapraid_content* other = j->data;
|
|
if (content->device == other->device) {
|
|
log_error(ESOFT, "WARNING! Content files on the same disk: '%s' and '%s'.\n", content->content, other->content);
|
|
break;
|
|
}
|
|
}
|
|
if (j != 0) {
|
|
/* skip it */
|
|
continue;
|
|
}
|
|
|
|
++content_count;
|
|
}
|
|
|
|
if (content_count < state->level + 1) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "You must have at least %d 'content' files in different disks.\n", state->level + 1);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
/* check for speed */
|
|
#ifdef CONFIG_X86
|
|
if (!raid_cpu_has_ssse3())
|
|
#endif
|
|
if ((state->raid_mode == RAID_MODE_CAUCHY_RAID || state->raid_mode == RAID_MODE_CAUCHY_AES) && !state->opt.no_warnings) {
|
|
if (state->level == 3) {
|
|
log_info(EUSER, "WARNING! Your CPU doesn't have a fast implementation for triple parity.\n");
|
|
log_info(EUSER, "WARNING! It's recommended to switch to 'z-parity' instead than '3-parity'.\n");
|
|
} else if (state->level > 3) {
|
|
log_info(EUSER, "WARNING! Your CPU doesn't have a fast implementation beyond triple parity.\n");
|
|
log_info(EUSER, "WARNING! It's recommended to reduce the parity levels to triple parity.\n");
|
|
}
|
|
}
|
|
|
|
/* ensure that specified filter disks are valid ones */
|
|
for (i = tommy_list_head(filterlist_disk); i != 0; i = i->next) {
|
|
tommy_node* j;
|
|
struct snapraid_filter* filter = i->data;
|
|
for (j = state->disklist; j != 0; j = j->next) {
|
|
struct snapraid_disk* disk = j->data;
|
|
if (wnmatch(filter->pattern, disk->name) == 0)
|
|
break;
|
|
}
|
|
if (j == 0) {
|
|
for (j = state->extralist; j != 0; j = j->next) {
|
|
struct snapraid_extra* extra = j->data;
|
|
if (wnmatch(filter->pattern, extra->name) == 0)
|
|
break;
|
|
}
|
|
}
|
|
if (j == 0) {
|
|
/* check matching with parity disks */
|
|
for (l = 0; l < state->level; ++l)
|
|
if (wnmatch(filter->pattern, lev_config_name(l)) == 0)
|
|
break;
|
|
if (l == state->level) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Option -d, --filter-disk %s doesn't match any data or parity or extra disk.\n", filter->pattern);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse the smartctl command.
|
|
*
|
|
* It extracts the optional [info: xxx] tag, validates, and returns
|
|
* the cleaned smartctl command and smartctl_info options.
|
|
*/
|
|
int parse_smartctl(const char* custom, char* smartctl, size_t smartctl_size, char* smartctl_info, size_t smartctl_info_size)
|
|
{
|
|
const char* s;
|
|
const char* info_start = 0;
|
|
size_t info_len = 0;
|
|
size_t smartctl_len = 0;
|
|
int arg = 0;
|
|
|
|
/* initialize output buffers */
|
|
smartctl[0] = 0;
|
|
smartctl_info[0] = 0;
|
|
|
|
/* look for "[info:" */
|
|
s = custom;
|
|
while (*s) {
|
|
if (strncmp(s, "[info:", 6) == 0) {
|
|
if (info_start) {
|
|
/* multiple info tags not allowed */
|
|
return -1;
|
|
}
|
|
|
|
info_start = s;
|
|
s += 6;
|
|
|
|
/* skip spaces inside bracket */
|
|
while (isspace((unsigned char)*s))
|
|
++s;
|
|
|
|
/* find matching ']' */
|
|
const char* val_start = s;
|
|
while (*s && *s != ']')
|
|
++s;
|
|
if (*s != ']') {
|
|
/* missing closing bracket */
|
|
return -1;
|
|
}
|
|
|
|
/* trim trailing spaces inside bracket */
|
|
const char* val_end = s;
|
|
while (val_end > val_start && isspace((unsigned char)val_end[-1]))
|
|
--val_end;
|
|
info_len = val_end - val_start;
|
|
if (info_len >= smartctl_info_size) {
|
|
/* info string too long */
|
|
return -1;
|
|
}
|
|
|
|
memcpy(smartctl_info, val_start, info_len);
|
|
smartctl_info[info_len] = 0;
|
|
++s; /* skip ']' */
|
|
} else {
|
|
if (smartctl_len + 1 >= smartctl_size) {
|
|
return -1;
|
|
}
|
|
smartctl[smartctl_len++] = *s++;
|
|
}
|
|
}
|
|
smartctl[smartctl_len] = 0;
|
|
|
|
/* trim extra spaces from the smartctl options */
|
|
strtrim(smartctl);
|
|
|
|
/* validate the cleaned smartctl command */
|
|
s = smartctl;
|
|
while (*s) {
|
|
if (s[0] == '%' && s[1] == 's') {
|
|
if (arg) {
|
|
/* LCOV_EXCL_START */
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
arg = 1;
|
|
++s;
|
|
} else if (s[0] == '%') {
|
|
/* LCOV_EXCL_START */
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
++s;
|
|
}
|
|
|
|
/* validate the smartctl_info command */
|
|
s = smartctl_info;
|
|
while (*s) {
|
|
if (s[0] == '%') {
|
|
/* LCOV_EXCL_START */
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
++s;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void state_config(struct snapraid_state* state, const char* path, const char* command, struct snapraid_option* opt, tommy_list* filterlist_disk)
|
|
{
|
|
STREAM* f;
|
|
unsigned line;
|
|
tommy_node* i;
|
|
unsigned l, s;
|
|
/* copy the options */
|
|
state->opt = *opt;
|
|
|
|
/* if unset, sort by directory order */
|
|
if (!state->opt.force_order)
|
|
state->opt.force_order = SORT_DIR;
|
|
|
|
/* adjust file mode */
|
|
if (state->opt.file_mode != ADVISE_DEFAULT) {
|
|
state->file_mode = state->opt.file_mode;
|
|
} else {
|
|
/* default mode, if nothing is specified */
|
|
state->file_mode = ADVISE_DISCARD;
|
|
}
|
|
|
|
/* store current command */
|
|
state->command = command;
|
|
|
|
log_tag("conf:file:%s\n", esc_tag(path));
|
|
|
|
f = sopen_read(path, 0);
|
|
if (!f) {
|
|
/* LCOV_EXCL_START */
|
|
if (errno == ENOENT) {
|
|
log_fatal(errno, "No configuration file found at '%s'\n", path);
|
|
} else if (errno == EACCES) {
|
|
log_fatal(errno, "You do not have rights to access the configuration file '%s'\n", path);
|
|
} else {
|
|
log_fatal(errno, "Error opening the configuration file '%s'. %s.\n", path, strerror(errno));
|
|
}
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
line = 1;
|
|
while (1) {
|
|
char tag[PATH_MAX];
|
|
char buffer[PATH_MAX];
|
|
ssize_t ret;
|
|
int c;
|
|
unsigned level;
|
|
|
|
/* skip initial spaces */
|
|
sgetspace(f);
|
|
|
|
/* read the command */
|
|
ret = sgettok(f, tag, sizeof(tag));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Error reading the configuration file '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* skip spaces after the command */
|
|
sgetspace(f);
|
|
|
|
if (strcmp(tag, "blocksize") == 0
|
|
/* block_size is the old format of the option */
|
|
|| strcmp(tag, "block_size") == 0) {
|
|
|
|
ret = sgetu32(f, &state->block_size);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'blocksize' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (state->block_size < 1) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Too small 'blocksize' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (state->block_size > 16 * KIBI) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Too big 'blocksize' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
/* check if it's a power of 2 */
|
|
if ((state->block_size & (state->block_size - 1)) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Not power of 2 'blocksize' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
state->block_size *= KIBI;
|
|
} else if (strcmp(tag, "hashsize") == 0
|
|
|| strcmp(tag, "hash_size") == 0 /* v11.0 used incorrectly this one, kept now for backward compatibility */
|
|
) {
|
|
uint32_t hash_size;
|
|
|
|
ret = sgetu32(f, &hash_size);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'hashsize' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (hash_size == 2) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Unsupported 'hashsize 2' specification in '%s' at line %u! The minimum supported hash size is 4.\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (hash_size < 4) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Too small 'hashsize' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (hash_size > HASH_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Too big 'hashsize' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
/* check if it's a power of 2 */
|
|
if ((hash_size & (hash_size - 1)) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Not power of 2 'hashsize' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
BLOCK_HASH_SIZE = hash_size;
|
|
} else if (lev_config_scan(tag, &level, &state->raid_mode) == 0) {
|
|
char device[PATH_MAX];
|
|
char* split_map[SPLIT_MAX + 1];
|
|
unsigned split_mac;
|
|
uint64_t dev;
|
|
int skip_access;
|
|
|
|
if (state->parity[level].split_mac != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Multiple '%s' specification in '%s' at line %u\n", tag, path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetlasttok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid '%s' specification in '%s' at line %u\n", tag, path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty '%s' specification in '%s' at line %u\n", tag, path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
split_mac = strsplit(split_map, SPLIT_MAX + 1, buffer, ",");
|
|
|
|
if (split_mac > SPLIT_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Too many files in '%s' specification in '%s' at line %u\n", tag, path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
skip_access = 0;
|
|
state->parity[level].split_mac = split_mac;
|
|
for (s = 0; s < split_mac; ++s) {
|
|
char uuid[UUID_MAX];
|
|
pathimport(state->parity[level].split_map[s].path, sizeof(state->parity[level].split_map[s].path), split_map[s]);
|
|
|
|
if (!state->opt.skip_parity_access) {
|
|
struct stat st;
|
|
|
|
/* get the device of the directory containing the parity file */
|
|
pathimport(device, sizeof(device), split_map[s]);
|
|
|
|
/* use only the dir, as the parity file may not exist yet */
|
|
pathcut(device);
|
|
|
|
if (stat(device, &st) == 0) {
|
|
dev = st.st_dev;
|
|
|
|
/* read the uuid, if unsupported use an empty one */
|
|
if (devuuid(dev, device, uuid, sizeof(uuid)) != 0) {
|
|
*uuid = 0;
|
|
}
|
|
} else {
|
|
/* if the disk can be skipped */
|
|
if (state->opt.force_device) {
|
|
/* use a fake device, and mark the disk to be skipped */
|
|
dev = 0;
|
|
*uuid = 0;
|
|
skip_access = 1;
|
|
log_fatal(errno, "DANGER! Skipping inaccessible parity disk '%s'...\n", tag);
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error accessing 'parity' dir '%s' specification in '%s' at line %u\n", device, path, line);
|
|
|
|
/* in "fix" we allow to continue anyway */
|
|
if (strcmp(state->command, "fix") == 0) {
|
|
log_fatal(errno, "You can '%s' anyway, using 'snapraid --force-device %s'.\n", state->command, state->command);
|
|
}
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
} else {
|
|
/* use a fake device */
|
|
dev = 0;
|
|
*uuid = 0;
|
|
}
|
|
|
|
state->parity[level].split_map[s].device = dev;
|
|
pathcpy(state->parity[level].split_map[s].uuid, sizeof(state->parity[level].split_map[s].uuid), uuid);
|
|
}
|
|
|
|
/* store the global parity skip_access */
|
|
state->parity[level].skip_access = skip_access;
|
|
|
|
/* adjust the level */
|
|
if (state->level < level + 1)
|
|
state->level = level + 1;
|
|
} else if (strcmp(tag, "share") == 0) {
|
|
if (*state->share) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Multiple 'share' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetlasttok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'share' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'share' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
pathimport(state->share, sizeof(state->share), buffer);
|
|
} else if (strcmp(tag, "pool") == 0) {
|
|
struct stat st;
|
|
|
|
if (*state->pool) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Multiple 'pool' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetlasttok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'pool' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'pool' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
pathimport(state->pool, sizeof(state->pool), buffer);
|
|
|
|
/* get the device of the directory containing the pool tree */
|
|
if (stat(buffer, &st) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error accessing 'pool' dir '%s' specification in '%s' at line %u\n", buffer, path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
state->pool_device = st.st_dev;
|
|
} else if (strcmp(tag, "content") == 0) {
|
|
struct snapraid_content* content;
|
|
char device[PATH_MAX];
|
|
char* slash;
|
|
struct stat st;
|
|
uint64_t dev;
|
|
|
|
ret = sgetlasttok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'content' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (pathcmp(buffer, "/dev/null") == 0 || pathcmp(buffer, "NUL") == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "You cannot use the null device as 'content' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'content' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* check if the content file is already specified */
|
|
for (i = state->contentlist; i != 0; i = i->next) {
|
|
content = i->data;
|
|
if (pathcmp(content->content, buffer) == 0)
|
|
break;
|
|
}
|
|
if (i) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Duplicate 'content' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* get the device of the directory containing the content file */
|
|
pathimport(device, sizeof(device), buffer);
|
|
slash = strrchr(device, '/');
|
|
if (slash)
|
|
slash[1] = 0;
|
|
else
|
|
pathcpy(device, sizeof(device), ".");
|
|
if (stat(device, &st) == 0) {
|
|
dev = st.st_dev;
|
|
} else {
|
|
if (state->opt.skip_content_access) {
|
|
/* use a fake device */
|
|
dev = 0;
|
|
log_fatal(errno, "WARNING! Skipping inaccessible content file '%s'...\n", buffer);
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error accessing 'content' dir '%s' specification in '%s' at line %u\n", device, path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
/* set the lock file at the first accessible content file */
|
|
if (state->lockfile[0] == 0 && dev != 0) {
|
|
pathcpy(state->lockfile, sizeof(state->lockfile), buffer);
|
|
pathcat(state->lockfile, sizeof(state->lockfile), ".lock");
|
|
}
|
|
|
|
content = content_alloc(buffer, dev);
|
|
|
|
tommy_list_insert_tail(&state->contentlist, &content->node, content);
|
|
} else if (strcmp(tag, "data") == 0 || strcmp(tag, "disk") == 0) {
|
|
/* "disk" is the deprecated name up to SnapRAID 9.x */
|
|
char dir[PATH_MAX];
|
|
char device[PATH_MAX];
|
|
char uuid[UUID_MAX];
|
|
struct snapraid_disk* disk;
|
|
uint64_t dev;
|
|
int skip_access;
|
|
|
|
ret = sgettok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'data' name specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'data' name specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
sgetspace(f);
|
|
|
|
ret = sgetlasttok(f, dir, sizeof(dir));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'data' dir specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*dir) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'data' dir specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* get the device of the dir */
|
|
pathimport(device, sizeof(device), dir);
|
|
|
|
/* check if the disk name already exists */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
disk = i->data;
|
|
if (strcmp(disk->name, buffer) == 0)
|
|
break;
|
|
}
|
|
if (i) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Duplicate 'data' name '%s' at line %u\n", buffer, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* if the disk has to be present */
|
|
skip_access = 0;
|
|
if (!state->opt.skip_disk_access) {
|
|
struct stat st;
|
|
|
|
if (stat(device, &st) == 0) {
|
|
dev = st.st_dev;
|
|
|
|
/* read the uuid, if unsupported use an empty one */
|
|
if (devuuid(dev, device, uuid, sizeof(uuid)) != 0) {
|
|
*uuid = 0;
|
|
}
|
|
|
|
/* fake a different UUID when testing */
|
|
if (state->opt.fake_uuid) {
|
|
snprintf(uuid, sizeof(uuid), "fake-uuid-%d", state->opt.fake_uuid);
|
|
--state->opt.fake_uuid;
|
|
}
|
|
} else {
|
|
/* if the disk can be skipped */
|
|
if (state->opt.force_device) {
|
|
/* use a fake device, and mark the disk to be skipped */
|
|
dev = 0;
|
|
*uuid = 0;
|
|
skip_access = 1;
|
|
log_fatal(errno, "DANGER! Skipping inaccessible data disk '%s'...\n", buffer);
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error accessing 'disk' '%s' specification in '%s' at line %u\n", dir, device, line);
|
|
|
|
/* in "fix" we allow to continue anyway */
|
|
if (strcmp(state->command, "fix") == 0) {
|
|
log_fatal(errno, "You can '%s' anyway, using 'snapraid --force-device %s'.\n", state->command, state->command);
|
|
}
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
} else {
|
|
/* use a fake device */
|
|
dev = 0;
|
|
*uuid = 0;
|
|
}
|
|
|
|
disk = disk_alloc(buffer, dir, dev, uuid, skip_access);
|
|
|
|
tommy_list_insert_tail(&state->disklist, &disk->node, disk);
|
|
} else if (strcmp(tag, "extra") == 0) {
|
|
char dir[PATH_MAX];
|
|
char device[PATH_MAX];
|
|
char uuid[UUID_MAX];
|
|
struct snapraid_extra* extra;
|
|
uint64_t dev;
|
|
|
|
ret = sgettok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'extra' name specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'extra' name specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
sgetspace(f);
|
|
|
|
ret = sgetlasttok(f, dir, sizeof(dir));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'extra' dir specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*dir) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'extra' dir specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* get the device of the dir */
|
|
pathimport(device, sizeof(device), dir);
|
|
|
|
/* check if the disk name already exists */
|
|
for (i = state->extralist; i != 0; i = i->next) {
|
|
extra = i->data;
|
|
if (strcmp(extra->name, buffer) == 0)
|
|
break;
|
|
}
|
|
if (i) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Duplicate 'extra' name '%s' at line %u\n", buffer, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* if the disk has to be present */
|
|
if (!state->opt.skip_disk_access) {
|
|
struct stat st;
|
|
|
|
if (stat(device, &st) == 0) {
|
|
dev = st.st_dev;
|
|
|
|
/* read the uuid, if unsupported use an empty one */
|
|
if (devuuid(dev, device, uuid, sizeof(uuid)) != 0) {
|
|
*uuid = 0;
|
|
}
|
|
|
|
/* fake a different UUID when testing */
|
|
if (state->opt.fake_uuid) {
|
|
snprintf(uuid, sizeof(uuid), "fake-uuid-%d", state->opt.fake_uuid);
|
|
--state->opt.fake_uuid;
|
|
}
|
|
} else {
|
|
/* use a fake device, and mark the disk to be skipped */
|
|
dev = 0;
|
|
*uuid = 0;
|
|
}
|
|
} else {
|
|
/* use a fake device */
|
|
dev = 0;
|
|
*uuid = 0;
|
|
}
|
|
|
|
extra = extra_alloc(buffer, dir, dev, uuid);
|
|
|
|
tommy_list_insert_tail(&state->extralist, &extra->node, extra);
|
|
} else if (strcmp(tag, "smartctl") == 0) {
|
|
char custom[PATH_MAX];
|
|
char parsed_smartctl[SMARTCTL_MAX];
|
|
char parsed_smartctl_info[SMARTCTL_MAX];
|
|
|
|
ret = sgettok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'smartctl' name in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'smartctl' name in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
sgetspace(f);
|
|
|
|
ret = sgetlasttok(f, custom, sizeof(custom));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'smartctl' option in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*custom) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'smartctl' option in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (parse_smartctl(custom, parsed_smartctl, sizeof(parsed_smartctl), parsed_smartctl_info, sizeof(parsed_smartctl_info)) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'smartctl' option in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* search for parity */
|
|
if (lev_config_scan(buffer, &level, 0) == 0) {
|
|
if (state->parity[level].smartctl[0] != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Duplicate parity 'smartctl' '%s' at line %u\n", buffer, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
pathcpy(state->parity[level].smartctl, sizeof(state->parity[level].smartctl), parsed_smartctl);
|
|
pathcpy(state->parity[level].smartctl_info, sizeof(state->parity[level].smartctl_info), parsed_smartctl_info);
|
|
} else {
|
|
/* search the disk */
|
|
struct snapraid_disk* found_disk = 0;
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
if (strcmp(disk->name, buffer) == 0) {
|
|
found_disk = disk;
|
|
break;
|
|
}
|
|
}
|
|
struct snapraid_extra* found_extra = 0;
|
|
for (i = state->extralist; i != 0; i = i->next) {
|
|
struct snapraid_extra* extra = i->data;
|
|
if (strcmp(extra->name, buffer) == 0) {
|
|
found_extra = extra;
|
|
break;
|
|
}
|
|
}
|
|
if (found_disk) {
|
|
if (found_disk->smartctl[0] != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Duplicate 'smartctl' name '%s' at line %u\n", buffer, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
pathcpy(found_disk->smartctl, sizeof(found_disk->smartctl), parsed_smartctl);
|
|
pathcpy(found_disk->smartctl_info, sizeof(found_disk->smartctl_info), parsed_smartctl_info);
|
|
} else if (found_extra) {
|
|
if (found_extra->smartctl[0] != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Duplicate 'smartctl' name '%s' at line %u\n", buffer, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
pathcpy(found_extra->smartctl, sizeof(found_extra->smartctl), parsed_smartctl);
|
|
pathcpy(found_extra->smartctl_info, sizeof(found_extra->smartctl_info), parsed_smartctl_info);
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Unknown 'smartctl' name '%s' at line %u\n", buffer, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
} else if (strcmp(tag, "smartignore") == 0) {
|
|
struct smartignore_struct* smart;
|
|
|
|
ret = sgettok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'smartignore' name in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'smartignore' name in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
sgetspace(f);
|
|
|
|
if (strcmp(buffer, "*") == 0) {
|
|
smart = state->smartignore;
|
|
} else if (lev_config_scan(buffer, &level, 0) == 0) { /* search for parity */
|
|
smart = state->parity[level].smartignore;
|
|
} else {
|
|
/* search the disk */
|
|
struct snapraid_disk* found_disk = 0;
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
if (strcmp(disk->name, buffer) == 0) {
|
|
found_disk = disk;
|
|
break;
|
|
}
|
|
}
|
|
struct snapraid_extra* found_extra = 0;
|
|
for (i = state->extralist; i != 0; i = i->next) {
|
|
struct snapraid_extra* extra = i->data;
|
|
if (strcmp(extra->name, buffer) == 0) {
|
|
found_extra = extra;
|
|
break;
|
|
}
|
|
}
|
|
if (found_disk) {
|
|
smart = found_disk->smartignore;
|
|
} else if (found_extra) {
|
|
smart = found_extra->smartignore;
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Unknown 'smartignore' name '%s' at line %u\n", buffer, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
int si = 0;
|
|
|
|
while (1) {
|
|
char* e;
|
|
|
|
ret = sgettok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'smartignore' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer)
|
|
break;
|
|
|
|
if (si == SMART_IGNORE_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Too many 'smartignore' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
int val = strtou(buffer, &e, 0);
|
|
|
|
if (e && !*e) {
|
|
/* numerical form */
|
|
if (val < 1 || val >= 256) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER,
|
|
"Invalid 'smartignore' specification in '%s' at line %u. It should be between 1 and 255.\n",
|
|
path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
smart[si].attr_index = val;
|
|
smart[si].attr_name[0] = 0;
|
|
} else {
|
|
/* name form */
|
|
if (strlen(buffer) >= SMART_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER,
|
|
"Invalid 'smartignore' specification in '%s' at line %u. Attribute name too long.\n",
|
|
path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
smart[si].attr_index = 0;
|
|
pathcpy(smart[si].attr_name, sizeof(smart[si].attr_name), buffer);
|
|
}
|
|
|
|
++si;
|
|
|
|
sgetspace(f);
|
|
}
|
|
|
|
if (si == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'smartignore' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
} else if (strcmp(tag, "temp_limit") == 0) {
|
|
unsigned temp;
|
|
|
|
ret = sgetu32(f, &temp);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'temp_limit' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (temp < 40 || temp > 70) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'temp_limit' temperature specification in '%s' at line %u. It must be between 40 and 70\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
state->thermal_temperature_limit = temp;
|
|
} else if (strcmp(tag, "temp_sleep") == 0) {
|
|
unsigned time;
|
|
|
|
ret = sgetu32(f, &time);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'temp_sleep' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (time < 3 || time > 120) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'temp_sleep' temperature specification in '%s' at line %u. It must be between 3 and 120\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
state->thermal_cooldown_time = time * 60;
|
|
} else if (strcmp(tag, "nohidden") == 0) {
|
|
state->filter_hidden = 1;
|
|
} else if (strcmp(tag, "snapshot") == 0) {
|
|
state->snapshot = 1;
|
|
} else if (strcmp(tag, "exclude") == 0) {
|
|
struct snapraid_filter* filter;
|
|
|
|
ret = sgetlasttok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'exclude' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'exclude' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
filter = filter_alloc_file(-1, "", buffer);
|
|
if (!filter) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'exclude' specification '%s' in '%s' at line %u\n", buffer, path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
tommy_list_insert_tail(&state->filterlist, &filter->node, filter);
|
|
} else if (strcmp(tag, "include") == 0) {
|
|
struct snapraid_filter* filter;
|
|
|
|
ret = sgetlasttok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'include' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'include' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
filter = filter_alloc_file(1, "", buffer);
|
|
if (!filter) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'include' specification '%s' in '%s' at line %u\n", buffer, path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
tommy_list_insert_tail(&state->filterlist, &filter->node, filter);
|
|
} else if (strcmp(tag, "autosave") == 0) {
|
|
char* e;
|
|
|
|
ret = sgetlasttok(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'autosave' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!*buffer) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Empty 'autosave' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
state->autosave = strtoul(buffer, &e, 0);
|
|
|
|
if (!e || *e) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid 'autosave' specification in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* convert to GB */
|
|
state->autosave *= GIGA;
|
|
} else if (tag[0] == 0) {
|
|
/* allow empty lines */
|
|
} else if (tag[0] == '#') {
|
|
ret = sgetline(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid comment in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Invalid command '%s' in '%s' at line %u\n", tag, path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* skip final spaces */
|
|
sgetspace(f);
|
|
|
|
/* next line */
|
|
c = sgeteol(f);
|
|
if (c == EOF) {
|
|
break;
|
|
}
|
|
if (c != '\n') {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Extra data in '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
++line;
|
|
}
|
|
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error reading the configuration file '%s' at line %u\n", path, line);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
sclose(f);
|
|
|
|
state_config_check(state, path, filterlist_disk);
|
|
|
|
/* select the default hash */
|
|
if (state->opt.force_murmur3) {
|
|
state->besthash = HASH_MURMUR3;
|
|
} else if (state->opt.force_spooky2) {
|
|
state->besthash = HASH_SPOOKY2;
|
|
} else {
|
|
state->besthash = membesthash();
|
|
}
|
|
|
|
/* by default use the best hash */
|
|
state->hash = state->besthash;
|
|
|
|
/* by default use a random hash seed */
|
|
if (os_randomize(state->hashseed, HASH_MAX) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Failed to retrieve random values.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* no previous hash by default */
|
|
state->prevhash = HASH_UNDEFINED;
|
|
|
|
/* intentionally not set the prevhashseed, if used valgrind will warn about it */
|
|
|
|
log_tag("blocksize:%u\n", state->block_size);
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
log_tag("data:%s:%s:%s\n", disk->name, esc_tag(disk->mount_point), disk->uuid);
|
|
}
|
|
for (i = state->extralist; i != 0; i = i->next) {
|
|
struct snapraid_extra* extra = i->data;
|
|
log_tag("extra:%s:%s:%s\n", esc_tag(extra->name), esc_tag(extra->dir), extra->uuid);
|
|
}
|
|
|
|
log_tag("mode:%s\n", lev_raid_name(state->raid_mode, state->level));
|
|
for (l = 0; l < state->level; ++l) {
|
|
log_tag("%s:%s:%s\n", lev_config_name(l), esc_tag(state->parity[l].split_map[0].path), state->parity[l].split_map[0].uuid);
|
|
for (s = 1; s < state->parity[l].split_mac; ++s) {
|
|
log_tag("%s/%u:%s:%s\n", lev_config_name(l), s, esc_tag(state->parity[l].split_map[s].path), state->parity[l].split_map[s].uuid);
|
|
}
|
|
}
|
|
if (state->pool[0] != 0)
|
|
log_tag("pool:%s\n", esc_tag(state->pool));
|
|
if (state->share[0] != 0)
|
|
log_tag("share:%s\n", esc_tag(state->share));
|
|
if (state->autosave != 0)
|
|
log_tag("autosave:%" PRIu64 "\n", state->autosave);
|
|
for (i = tommy_list_head(&state->filterlist); i != 0; i = i->next) {
|
|
char out[PATH_MAX];
|
|
struct snapraid_filter* filter = i->data;
|
|
log_tag("filter:%s\n", esc_tag(filter_type(filter, out, sizeof(out))));
|
|
}
|
|
if (state->filter_hidden)
|
|
log_tag("filter:nohidden:\n");
|
|
log_flush();
|
|
}
|
|
|
|
/**
|
|
* Find a disk by name.
|
|
*/
|
|
static struct snapraid_disk* find_disk_by_name(struct snapraid_state* state, const char* name)
|
|
{
|
|
tommy_node* i;
|
|
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
if (strcmp(disk->name, name) == 0)
|
|
return disk;
|
|
}
|
|
|
|
if (state->no_conf) {
|
|
/* without a configuration file, add disks automatically */
|
|
struct snapraid_disk* disk;
|
|
|
|
disk = disk_alloc(name, "DUMMY/", -1, "", 0);
|
|
|
|
tommy_list_insert_tail(&state->disklist, &disk->node, disk);
|
|
|
|
return disk;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Find a disk by UUID.
|
|
*/
|
|
static struct snapraid_disk* find_disk_by_uuid(struct snapraid_state* state, const char* uuid)
|
|
{
|
|
tommy_node* i;
|
|
struct snapraid_disk* found = 0;
|
|
|
|
/*
|
|
* Special test case to find the first matching UUID
|
|
* when testing UUID are all equal or not supported
|
|
* and we should handle this case specifically
|
|
*/
|
|
if (state->opt.match_first_uuid)
|
|
return state->disklist->data;
|
|
|
|
/* LCOV_EXCL_START */
|
|
/* never find an empty uuid */
|
|
if (!*uuid)
|
|
return 0;
|
|
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
if (strcmp(disk->uuid, uuid) == 0) {
|
|
/* never match duplicate UUID */
|
|
if (found)
|
|
return 0;
|
|
found = disk;
|
|
}
|
|
}
|
|
|
|
return found;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/**
|
|
* Update the disk mapping if required.
|
|
*/
|
|
static void state_map(struct snapraid_state* state)
|
|
{
|
|
unsigned hole;
|
|
tommy_node* i;
|
|
unsigned uuid_mismatch;
|
|
size_t diskcount;
|
|
unsigned l, s;
|
|
|
|
/*
|
|
* Remove all the mapping without a disk
|
|
* this happens when a disk is removed from the configuration file
|
|
* From SnapRAID 4.0 mappings are automatically removed if a disk is not used
|
|
* when saving the content file, but we keep this code to import older content files.
|
|
*/
|
|
for (i = state->maplist; i != 0; ) {
|
|
struct snapraid_map* map = i->data;
|
|
struct snapraid_disk* disk;
|
|
|
|
disk = find_disk_by_name(state, map->name);
|
|
|
|
/* go to the next mapping before removing */
|
|
i = i->next;
|
|
|
|
if (disk == 0) {
|
|
/* disk not found, remove the mapping */
|
|
tommy_list_remove_existing(&state->maplist, &map->node);
|
|
map_free(map);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Maps each unmapped disk present in the configuration file in the first available hole
|
|
* this happens when you add disks for the first time in the configuration file
|
|
*/
|
|
hole = 0; /* first position to try */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
struct snapraid_map* map;
|
|
tommy_node* j;
|
|
|
|
/* check if the disk is already mapped */
|
|
for (j = state->maplist; j != 0; j = j->next) {
|
|
map = j->data;
|
|
if (strcmp(disk->name, map->name) == 0) {
|
|
/* mapping found */
|
|
break;
|
|
}
|
|
}
|
|
if (j != 0) {
|
|
/* mapping is present, then copy the free blocks into to disk */
|
|
disk->total_blocks = map->total_blocks;
|
|
disk->free_blocks = map->free_blocks;
|
|
continue;
|
|
}
|
|
|
|
/* mapping not found, search for an hole */
|
|
while (1) {
|
|
for (j = state->maplist; j != 0; j = j->next) {
|
|
map = j->data;
|
|
if (map->position == hole) {
|
|
/* position already used */
|
|
break;
|
|
}
|
|
}
|
|
if (j == 0) {
|
|
/* hole found */
|
|
break;
|
|
}
|
|
|
|
/* try with the next one */
|
|
++hole;
|
|
}
|
|
|
|
/* insert the new mapping */
|
|
map = map_alloc(disk->name, hole, 0, 0, "");
|
|
|
|
tommy_list_insert_tail(&state->maplist, &map->node, map);
|
|
}
|
|
|
|
/* without configuration don't check for number of data disks or uuid changes */
|
|
if (state->no_conf)
|
|
return;
|
|
|
|
/* counter for the number of UUID mismatches */
|
|
uuid_mismatch = 0;
|
|
|
|
/* check if mapping match the disk uuid */
|
|
if (!state->opt.skip_disk_access) {
|
|
for (i = state->maplist; i != 0; i = i->next) {
|
|
struct snapraid_map* map = i->data;
|
|
struct snapraid_disk* disk;
|
|
|
|
disk = find_disk_by_name(state, map->name);
|
|
if (disk == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Mapping not found for '%s'\n", map->name);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (disk->uuid[0] == 0) {
|
|
/* if uuid is not available, skip this one */
|
|
continue;
|
|
}
|
|
|
|
/* if the uuid is changed */
|
|
if (strcmp(disk->uuid, map->content_uuid) != 0) {
|
|
/* mark the disk as with an UUID change */
|
|
disk->has_different_uuid = 1;
|
|
|
|
/* if the previous uuid is available */
|
|
if (map->content_uuid[0] != 0) {
|
|
/* count the number of uuid change */
|
|
++uuid_mismatch;
|
|
log_error(ESOFT, "UUID change for disk '%s' from '%s' to '%s'\n", disk->name, map->content_uuid, disk->uuid);
|
|
} else {
|
|
/*
|
|
* No message here, because having a disk without
|
|
* UUID is the normal state of an empty disk
|
|
*/
|
|
disk->had_empty_uuid = 1;
|
|
}
|
|
|
|
/* write the new state with the new uuid */
|
|
state->need_write = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* check the parity uuid */
|
|
if (!state->opt.skip_parity_access) {
|
|
for (l = 0; l < state->level; ++l) {
|
|
for (s = 0; s < state->parity[l].split_mac; ++s) {
|
|
if (state->parity[l].split_map[s].uuid[0] == 0) {
|
|
/* if uuid is not available, skip this one */
|
|
continue;
|
|
}
|
|
|
|
/* if the uuid is changed */
|
|
if (strcmp(state->parity[l].split_map[s].uuid, state->parity[l].split_map[s].content_uuid) != 0) {
|
|
/* if the previous uuid is available */
|
|
if (state->parity[l].split_map[s].content_uuid[0] != 0) {
|
|
/* count the number of uuid change */
|
|
++uuid_mismatch;
|
|
log_error(ESOFT, "UUID change for parity '%s/%u' from '%s' to '%s'\n", lev_config_name(l), s, state->parity[l].split_map[s].content_uuid, state->parity[l].split_map[s].uuid);
|
|
}
|
|
|
|
/* write the new state with the new uuid */
|
|
state->need_write = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!state->opt.force_uuid && uuid_mismatch > state->level) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Too many disks have changed UUIDs since the last 'sync'.\n");
|
|
log_fatal(ESOFT, "If this happened because you actually replaced them,\n");
|
|
log_fatal(ESOFT, "you can still '%s', using 'snapraid --force-uuid %s'.\n", state->command, state->command);
|
|
log_fatal(ESOFT, "Alternatively, you may have misconfigured the disk mount points,\n");
|
|
log_fatal(ESOFT, "and you need to restore the mount points to the state of the latest sync.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* count the number of data disks, including holes left after removing some */
|
|
diskcount = 0;
|
|
for (i = state->maplist; i != 0; i = i->next) {
|
|
struct snapraid_map* map = i->data;
|
|
|
|
if (map->position + 1 > diskcount)
|
|
diskcount = map->position + 1;
|
|
}
|
|
|
|
/* ensure to don't go over the limit of the RAID engine */
|
|
if (diskcount > RAID_DATA_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ESOFT, "Too many data disks. Maximum allowed is %u.\n", RAID_DATA_MAX);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* now count the real number of data disks, excluding holes left after removing some */
|
|
diskcount = tommy_list_count(&state->maplist);
|
|
|
|
/* recommend number of parities */
|
|
if (!state->opt.no_warnings) {
|
|
/* LCOV_EXCL_START */
|
|
if (diskcount >= 36 && state->level < 6) {
|
|
log_info(EUSER, "WARNING! For %zu disks, it's recommended to use six parity levels.\n", diskcount);
|
|
} else if (diskcount >= 29 && state->level < 5) {
|
|
log_info(EUSER, "WARNING! For %zu disks, it's recommended to use five parity levels.\n", diskcount);
|
|
} else if (diskcount >= 22 && state->level < 4) {
|
|
log_info(EUSER, "WARNING! For %zu disks, it's recommended to use four parity levels.\n", diskcount);
|
|
} else if (diskcount >= 15 && state->level < 3) {
|
|
log_info(EUSER, "WARNING! For %zu disks, it's recommended to use three parity levels.\n", diskcount);
|
|
} else if (diskcount >= 5 && state->level < 2) {
|
|
log_info(EUSER, "WARNING! For %zu disks, it's recommended to use two parity levels.\n", diskcount);
|
|
}
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
void state_refresh(struct snapraid_state* state)
|
|
{
|
|
tommy_node* i;
|
|
unsigned l, s;
|
|
uint64_t bs = (uint64_t)state->block_size;
|
|
|
|
/* for all disks */
|
|
for (i = state->maplist; i != 0; i = i->next) {
|
|
struct snapraid_map* map = i->data;
|
|
struct snapraid_disk* disk;
|
|
uint64_t total_space;
|
|
uint64_t free_space;
|
|
int ret;
|
|
|
|
disk = find_disk_by_name(state, map->name);
|
|
if (disk == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Mapping not found for '%s'\n", map->name);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (disk->skip_access)
|
|
continue;
|
|
|
|
ret = fsinfo(disk->mount_point, 0, 0, &total_space, &free_space, disk->fstype, sizeof(disk->fstype), disk->fslabel, sizeof(disk->fslabel));
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error accessing disk '%s' to retrieve file-system info. %s.\n", disk->mount_point, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* set the new free blocks */
|
|
map->total_blocks = total_space / bs;
|
|
map->free_blocks = free_space / bs;
|
|
|
|
/* also update the disk info */
|
|
disk->total_blocks = map->total_blocks;
|
|
disk->free_blocks = map->free_blocks;
|
|
|
|
log_tag("fsinfo_data_split:%s:%" PRIu64 ":%" PRIu64 ":%s:%s\n", disk->name, disk->total_blocks * bs, disk->free_blocks * bs, disk->fstype, esc_tag(disk->fslabel));
|
|
log_tag("fsinfo_data:%s:%" PRIu64 ":%" PRIu64 "\n", disk->name, disk->total_blocks * bs, disk->free_blocks * bs);
|
|
}
|
|
|
|
/* for all parities */
|
|
for (l = 0; l < state->level; ++l) {
|
|
/* set the new free blocks */
|
|
state->parity[l].total_blocks = 0;
|
|
state->parity[l].free_blocks = 0;
|
|
|
|
if (state->parity[l].skip_access)
|
|
continue;
|
|
|
|
for (s = 0; s < state->parity[l].split_mac; ++s) {
|
|
struct snapraid_split* split = &state->parity[l].split_map[s];
|
|
uint64_t total_space;
|
|
uint64_t free_space;
|
|
int ret;
|
|
|
|
ret = fsinfo(split->path, 0, 0, &total_space, &free_space, split->fstype, sizeof(split->fstype), split->fslabel, sizeof(split->fslabel));
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error accessing file '%s' to retrieve file-system info. %s.\n", split->path, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
#ifdef WIN32
|
|
/*
|
|
* Take into account the space we have to leave free
|
|
* to avoid the waring about low space
|
|
*/
|
|
if (free_space >= WINDOWS_SPACEHOLDER_SIZE)
|
|
free_space -= WINDOWS_SPACEHOLDER_SIZE;
|
|
else
|
|
free_space = 0;
|
|
#endif
|
|
|
|
/* add the new free blocks */
|
|
uint64_t split_total_blocks = total_space / bs;
|
|
uint64_t split_free_blocks = free_space / bs;
|
|
|
|
state->parity[l].total_blocks += split_total_blocks;
|
|
state->parity[l].free_blocks += split_free_blocks;
|
|
|
|
if (s == 0)
|
|
log_tag("fsinfo_parity_split:%s:%" PRIu64 ":%" PRIu64 ":%s:%s\n", lev_config_name(l), split_total_blocks * bs, split_free_blocks * bs, split->fstype, esc_tag(split->fslabel));
|
|
else
|
|
log_tag("fsinfo_parity_split:%s/%u:%" PRIu64 ":%" PRIu64 ":%s:%s\n", lev_config_name(l), s, split_total_blocks * bs, split_free_blocks * bs, split->fstype, esc_tag(split->fslabel));
|
|
}
|
|
|
|
log_tag("fsinfo_parity:%s:%" PRIu64 ":%" PRIu64 "\n", lev_config_name(l), state->parity[l].total_blocks * bs, state->parity[l].free_blocks * bs);
|
|
}
|
|
|
|
/* for all extra disks */
|
|
for (i = state->extralist; i != 0; i = i->next) {
|
|
struct snapraid_extra* extra = i->data;
|
|
uint64_t total_space;
|
|
uint64_t free_space;
|
|
int ret;
|
|
|
|
ret = fsinfo(extra->dir, 0, 0, &total_space, &free_space, extra->fstype, sizeof(extra->fstype), extra->fslabel, sizeof(extra->fslabel));
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error accessing disk '%s' to retrieve file-system info. %s.\n", extra->dir, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("fsinfo_extra:%s:%" PRIu64 ":%" PRIu64 "\n", esc_tag(extra->name), total_space, free_space);
|
|
}
|
|
|
|
/*
|
|
* Note what we don't set need_write = 1, because we don't want
|
|
* to update the content file only for the free space info.
|
|
*/
|
|
}
|
|
|
|
/**
|
|
* Check the content.
|
|
*/
|
|
static void state_content_check(struct snapraid_state* state, const char* path)
|
|
{
|
|
tommy_node* i;
|
|
|
|
/* check that any map has different name and position */
|
|
for (i = state->maplist; i != 0; i = i->next) {
|
|
struct snapraid_map* map = i->data;
|
|
tommy_node* j;
|
|
for (j = i->next; j != 0; j = j->next) {
|
|
struct snapraid_map* other = j->data;
|
|
if (strcmp(map->name, other->name) == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ECONTENT, "Conflicting 'map' disk specification in '%s'\n", path);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (map->position == other->position) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ECONTENT, "Conflicting 'map' index specification in '%s'\n", path);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze if the position is REQUIRED, if parity is unsynced, and if it has
|
|
* DELETED or REBUILD blocks.
|
|
*
|
|
* A DELETED block makes parity invalid, but it counts as unsynced only when at
|
|
* least one disk still has a file at the same position. This is the same logic
|
|
* used by "status" to detect an incomplete sync.
|
|
*/
|
|
static int fs_position_analyze(struct snapraid_state* state, block_off_t pos, int* is_unsynced, int* has_deleted, int* has_rebuild)
|
|
{
|
|
tommy_node* i;
|
|
int one_file = 0;
|
|
int one_invalid = 0;
|
|
int one_deleted = 0;
|
|
int one_rebuild = 0;
|
|
|
|
/* check for each disk */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
struct snapraid_block* block = fs_par2block_find(disk, pos);
|
|
unsigned block_state = block_state_get(block);
|
|
|
|
if (block_has_file(block))
|
|
one_file = 1;
|
|
if (block_has_invalid_parity(block))
|
|
one_invalid = 1;
|
|
if (block_state == BLOCK_STATE_DELETED)
|
|
one_deleted = 1;
|
|
if (block_state == BLOCK_STATE_REBUILD)
|
|
one_rebuild = 1;
|
|
}
|
|
|
|
*is_unsynced = one_file && one_invalid;
|
|
*has_deleted = one_deleted;
|
|
*has_rebuild = one_rebuild;
|
|
|
|
return one_file;
|
|
}
|
|
|
|
/**
|
|
* Check if the info block is REQUIREQ.
|
|
*
|
|
* This is used to ensure that we keep the last check used for scrubbing.
|
|
* and that we add it when importing old context files.
|
|
*
|
|
* Note that you can have position without info blocks, for example
|
|
* if all the blocks are not synced.
|
|
*
|
|
* Note also that not requiring an info block, doesn't mean that if present it
|
|
* can be discarded.
|
|
*/
|
|
static int fs_info_is_required(struct snapraid_state* state, block_off_t pos)
|
|
{
|
|
tommy_node* i;
|
|
|
|
/* check for each disk */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
struct snapraid_block* block = fs_par2block_find(disk, pos);
|
|
|
|
/* if we have at least one synced file, the info is required */
|
|
if (block_state_get(block) == BLOCK_STATE_BLK)
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void fs_position_clear_deleted(struct snapraid_state* state, block_off_t pos)
|
|
{
|
|
tommy_node* i;
|
|
|
|
/* check for each disk if block is really used */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
struct snapraid_block* block = fs_par2block_find(disk, pos);
|
|
|
|
/* if the block is deleted */
|
|
if (block_state_get(block) == BLOCK_STATE_DELETED) {
|
|
/* set it to empty */
|
|
fs_deallocate(disk, pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a block position in a disk is deleted.
|
|
*/
|
|
static inline int fs_is_block_deleted(struct snapraid_disk* disk, block_off_t pos)
|
|
{
|
|
struct snapraid_block* block = fs_par2block_find(disk, pos);
|
|
|
|
return block_state_get(block) == BLOCK_STATE_DELETED;
|
|
}
|
|
|
|
/**
|
|
* Check if a block parity needs sync
|
|
*
|
|
* This is the same logic used in "status" to detect an incomplete "sync",
|
|
* that ignores invalid block, if they are not used by a file in any disk.
|
|
*
|
|
* This means that DELETED blocks won't necessarily imply an invalid parity.
|
|
*/
|
|
static int fs_is_block_unsynced(struct snapraid_state* state, block_off_t pos)
|
|
{
|
|
int one_invalid = 0;
|
|
int one_valid = 0;
|
|
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
struct snapraid_block* block = fs_par2block_find(disk, pos);
|
|
|
|
if (block_has_file(block))
|
|
one_valid = 1;
|
|
if (block_has_invalid_parity(block))
|
|
one_invalid = 1;
|
|
|
|
if (one_invalid && one_valid)
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Flush the file checking the final CRC and abort on error.
|
|
* We exploit the fact that the CRC is always stored in the last 4 bytes.
|
|
*
|
|
* Notice that on corruption, decoding error, or CRC mismatch we intentionally
|
|
* exit with a fatal error rather than automatically falling back to another
|
|
* content file. Automatic fallback could silently mask disk or memory corruption
|
|
* or inadvertently load a stale state. We require the user to explicitly rename
|
|
* or delete the corrupted file so SnapRAID will fall back to the surviving copy
|
|
* on the next run.
|
|
*/
|
|
static void decoding_error(const char* path, STREAM* f)
|
|
{
|
|
unsigned char buf[4];
|
|
uint32_t crc_stored;
|
|
uint32_t crc_computed;
|
|
|
|
if (seof(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ECONTENT, "Unexpected end of content file '%s' at offset %" PRIi64 "\n", path, stell(f));
|
|
log_fatal(ECONTENT, "The content file '%s' is truncated!\n", path);
|
|
log_fatal(ECONTENT, "To recover, rename or delete it and rerun the command.\n");
|
|
log_fatal(ECONTENT, "SnapRAID will automatically fall back to the next healthy copy.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error reading the content file '%s' at offset %" PRIi64 "\n", path, stell(f));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_fatal(ECONTENT, "Error decoding '%s' at offset %" PRIi64 "\n", path, stell(f));
|
|
|
|
if (sdeplete(f, buf) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Failed to flush content file '%s' at offset %" PRIi64 "\n", path, stell(f));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* get the stored crc from the last four bytes */
|
|
crc_stored = buf[0] | (uint32_t)buf[1] << 8 | (uint32_t)buf[2] << 16 | (uint32_t)buf[3] << 24;
|
|
|
|
/* get the computed crc */
|
|
crc_computed = scrc(f);
|
|
|
|
/* adjust the stored crc to include itself */
|
|
crc_stored = crc32c(crc_stored, buf, 4);
|
|
|
|
if (crc_computed != crc_stored) {
|
|
log_fatal(ECONTENT, "CRC mismatch in '%s'\n", path);
|
|
log_fatal(ECONTENT, "The content file '%s' is damaged or corrupted (CRC mismatch)!\n", path);
|
|
log_fatal(ECONTENT, "To recover, rename or delete it and rerun the command.\n");
|
|
log_fatal(ECONTENT, "SnapRAID will automatically fall back to the next healthy copy.\n");
|
|
exit(EXIT_FAILURE);
|
|
} else {
|
|
log_fatal(ECONTENT, "The CRC of the file is correct!\n");
|
|
}
|
|
}
|
|
|
|
enum state_block_hash_mode {
|
|
STATE_BLOCK_HASH_READ,
|
|
STATE_BLOCK_HASH_ZERO,
|
|
STATE_BLOCK_HASH_DISCARD
|
|
};
|
|
|
|
/**
|
|
* Read and initialize a complete run of file blocks.
|
|
*
|
|
* CRC is computed when the stream buffer is filled. Advancing the stream
|
|
* pointer directly over cached hashes therefore preserves the same integrity
|
|
* check performed by sread().
|
|
*/
|
|
static int state_read_block_run(STREAM* f, struct snapraid_file* file, block_off_t file_pos, block_off_t count, unsigned state, enum state_block_hash_mode hash_mode)
|
|
{
|
|
unsigned char* block_ptr = (unsigned char*)file_block(file, file_pos);
|
|
size_t block_stride = block_sizeof();
|
|
unsigned char discard_hash[HASH_MAX];
|
|
|
|
if (hash_mode == STATE_BLOCK_HASH_ZERO) {
|
|
while (count) {
|
|
struct snapraid_block* block = (struct snapraid_block*)block_ptr;
|
|
|
|
block_state_set(block, state);
|
|
hash_zero_set(block->hash);
|
|
|
|
block_ptr += block_stride;
|
|
--count;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
while (count) {
|
|
size_t available = (size_t)(f->end - f->pos);
|
|
block_off_t cached_count = available / BLOCK_HASH_SIZE;
|
|
unsigned char* input;
|
|
|
|
if (cached_count > count)
|
|
cached_count = count;
|
|
|
|
if (cached_count == 0) {
|
|
struct snapraid_block* block = (struct snapraid_block*)block_ptr;
|
|
int ret;
|
|
|
|
block_state_set(block, state);
|
|
if (hash_mode == STATE_BLOCK_HASH_DISCARD) {
|
|
ret = sread(f, discard_hash, BLOCK_HASH_SIZE);
|
|
hash_invalid_set(block->hash);
|
|
} else {
|
|
ret = sread(f, block->hash, BLOCK_HASH_SIZE);
|
|
}
|
|
if (ret < 0)
|
|
return -1;
|
|
|
|
block_ptr += block_stride;
|
|
--count;
|
|
continue;
|
|
}
|
|
|
|
input = f->pos;
|
|
for (block_off_t i = 0; i < cached_count; ++i) {
|
|
struct snapraid_block* block = (struct snapraid_block*)block_ptr;
|
|
|
|
block_state_set(block, state);
|
|
if (hash_mode == STATE_BLOCK_HASH_DISCARD)
|
|
hash_invalid_set(block->hash);
|
|
else
|
|
hash_copy(block->hash, input);
|
|
|
|
input += BLOCK_HASH_SIZE;
|
|
block_ptr += block_stride;
|
|
}
|
|
|
|
f->pos = input;
|
|
count -= cached_count;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Enable or disable lock-free access to the extent trees of all disks.
|
|
* Callers must use this helper in matched, local pairs around operations that
|
|
* are known to be the only extent-tree users.
|
|
*/
|
|
static void fs_single_thread(struct snapraid_state* state, int single_thread)
|
|
{
|
|
#if HAVE_THREAD
|
|
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
disk->single_thread = single_thread;
|
|
}
|
|
#else
|
|
(void)state;
|
|
(void)single_thread;
|
|
#endif
|
|
}
|
|
|
|
static void state_read_content(struct snapraid_state* state, const char* path, STREAM* f)
|
|
{
|
|
block_off_t blockmax;
|
|
uint64_t count_file;
|
|
uint64_t count_hardlink;
|
|
uint64_t count_symlink;
|
|
uint64_t count_dir;
|
|
uint64_t count_bad;
|
|
uint64_t count_rehash;
|
|
uint64_t count_unsynced;
|
|
uint64_t count_unscrubbed;
|
|
int crc_checked;
|
|
int has_block_size;
|
|
int has_blockmax;
|
|
int has_hash_size;
|
|
int has_hash;
|
|
int has_prevhash;
|
|
int has_info;
|
|
int has_invalid_parity;
|
|
char buffer[PATH_MAX];
|
|
int ret;
|
|
tommy_array disk_mapping;
|
|
uint32_t mapping_max;
|
|
tommy_hashdyn bucket_hash;
|
|
|
|
blockmax = 0;
|
|
count_file = 0;
|
|
count_hardlink = 0;
|
|
count_symlink = 0;
|
|
count_dir = 0;
|
|
count_bad = 0;
|
|
count_rehash = 0;
|
|
count_unsynced = 0;
|
|
count_unscrubbed = 0;
|
|
crc_checked = 0;
|
|
has_block_size = 0;
|
|
has_blockmax = 0;
|
|
has_hash_size = 0;
|
|
has_hash = 0;
|
|
has_prevhash = 0;
|
|
has_info = 0;
|
|
has_invalid_parity = 0;
|
|
mapping_max = 0;
|
|
tommy_array_init(&disk_mapping);
|
|
tommy_hashdyn_init(&bucket_hash);
|
|
|
|
ret = sread(f, buffer, 12);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid header!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* File format versions:
|
|
* - SNAPCNT1/SnapRAID 4.0 First version.
|
|
* - SNAPCNT2/SnapRAID 7.0 Adds entries 'M' and 'P', to add free_blocks support.
|
|
* The previous 'm' entry is now deprecated, but supported for importing.
|
|
* Similarly for text file, we add 'mapping' and 'parity' deprecating 'map'.
|
|
* - SNAPCNT3/SnapRAID 11.0 Adds entry 'y' for hash size.
|
|
* - SNAPCNT3/SnapRAID 11.0 Adds entry 'Q' for multi parity file.
|
|
* The previous 'P' entry is now deprecated, but supported for importing.
|
|
* - SNAPCNT4 adds:
|
|
* - entry 'd' for deallocated files;
|
|
* - block type 'r' / BLOCK_STATE_REBUILD for persistent in-place parity
|
|
* rebuilds.
|
|
*/
|
|
if (memcmp(buffer, "SNAPCNT1\n\3\0\0", 12) != 0
|
|
&& memcmp(buffer, "SNAPCNT2\n\3\0\0", 12) != 0
|
|
&& memcmp(buffer, "SNAPCNT3\n\3\0\0", 12) != 0
|
|
&& memcmp(buffer, "SNAPCNT4\n\3\0\0", 12) != 0
|
|
) {
|
|
/* LCOV_EXCL_START */
|
|
if (memcmp(buffer, "SNAPCNT", 7) != 0) {
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid header!\n");
|
|
os_abort();
|
|
} else {
|
|
log_fatal(ECONTENT, "The content file '%s' was generated with a newer version of SnapRAID!\n", path);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
while (1) {
|
|
int c;
|
|
|
|
/* read the command */
|
|
c = sgetc(f);
|
|
if (c == EOF) {
|
|
break;
|
|
}
|
|
|
|
if (c == 'f') {
|
|
/* file */
|
|
char sub[PATH_MAX];
|
|
uint64_t v_size;
|
|
uint64_t v_mtime_sec;
|
|
uint32_t v_mtime_nsec;
|
|
uint64_t v_inode;
|
|
block_off_t v_idx;
|
|
struct snapraid_file* file;
|
|
struct snapraid_disk* disk;
|
|
uint32_t mapping;
|
|
|
|
ret = sgetb32(f, &mapping);
|
|
if (ret < 0 || mapping >= mapping_max) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: File mapping index out of range\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
disk = tommy_array_get(&disk_mapping, mapping);
|
|
|
|
ret = sgetb64(f, &v_size);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* ensure the unsigned serialized size fits in signed data_off_t before conversion. */
|
|
if (v_size > INT64_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Internal inconsistency: Invalid file size %" PRIu64 "!\n", v_size);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (state->block_size == 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Zero blocksize\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!has_blockmax) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Missing 'blockmax' before file in the content file!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* check for impossible file size to avoid to crash for a too big allocation */
|
|
if (v_size / state->block_size > blockmax) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: File size too big!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_mtime_sec);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb32(f, &v_mtime_nsec);
|
|
if (ret < 0 || v_mtime_nsec > 1000000000) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* STAT_NSEC_INVALID is encoded as 0 */
|
|
if (v_mtime_nsec == 0)
|
|
v_mtime_nsec = STAT_NSEC_INVALID;
|
|
else
|
|
--v_mtime_nsec;
|
|
|
|
ret = sgetb64(f, &v_inode);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetbs(f, sub, sizeof(sub));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (!path_is_sub(sub)) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid file '%s'!\n", sub);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* Allocate without initializing blocks. The runs below cover the whole
|
|
* file and set every state and hash before exposing the loaded state.
|
|
*/
|
|
file = file_alloc(state->block_size, sub, v_size, v_mtime_sec, v_mtime_nsec, v_inode);
|
|
|
|
/* insert the file in the file containers */
|
|
if (file->inode != INODE_INVALID)
|
|
tommy_hashdyn_insert(&disk->inodeset, &file->nodeset, file, file_inode_hash(file->inode));
|
|
tommy_hashdyn_insert(&disk->pathset, &file->pathset, file, file_path_hash(file->sub));
|
|
tommy_hashdyn_insert(&disk->stampset, &file->stampset, file, file_stamp_hash(file->size, file->mtime_sec, file->mtime_nsec));
|
|
tommy_list_insert_tail(&disk->filelist, &file->nodelist, file);
|
|
|
|
/* read all the blocks */
|
|
v_idx = 0;
|
|
while (v_idx < file->blockmax) {
|
|
block_off_t v_pos;
|
|
block_off_t v_count;
|
|
block_off_t v_file_pos;
|
|
unsigned v_state;
|
|
enum state_block_hash_mode hash_mode;
|
|
|
|
/* get the "subcommand */
|
|
c = sgetc(f);
|
|
|
|
ret = sgetb64(f, &v_pos);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_count);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (v_count == 0 || v_count > file->blockmax || v_idx > file->blockmax - v_count) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Block number out of range\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (v_count > blockmax || v_pos > blockmax - v_count) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Block size %" PRIu64 "/%" PRIu64 "!\n", blockmax, v_pos + v_count);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
hash_mode = STATE_BLOCK_HASH_READ;
|
|
switch (c) {
|
|
case 'b' :
|
|
v_state = BLOCK_STATE_BLK;
|
|
break;
|
|
case 'r' :
|
|
v_state = BLOCK_STATE_REBUILD;
|
|
break;
|
|
case 'n' :
|
|
/* deprecated NEW blocks are converted to CHG ones with a ZERO hash */
|
|
v_state = BLOCK_STATE_CHG;
|
|
hash_mode = STATE_BLOCK_HASH_ZERO;
|
|
break;
|
|
case 'g' :
|
|
v_state = BLOCK_STATE_CHG;
|
|
break;
|
|
case 'p' :
|
|
if (state->opt.force_nocopy) {
|
|
/* discard stored copy hashes and convert REP blocks to CHG */
|
|
v_state = BLOCK_STATE_CHG;
|
|
hash_mode = STATE_BLOCK_HASH_DISCARD;
|
|
} else {
|
|
v_state = BLOCK_STATE_REP;
|
|
}
|
|
break;
|
|
default :
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid block type!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* all file block states except BLK require a parity update */
|
|
if (v_state != BLOCK_STATE_BLK)
|
|
has_invalid_parity = 1;
|
|
|
|
v_file_pos = v_idx;
|
|
ret = state_read_block_run(f, file, v_file_pos, v_count, v_state, hash_mode);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
v_idx += v_count;
|
|
|
|
/* set the parity association for the whole run */
|
|
fs_allocate(disk, v_pos, file, v_file_pos, v_count);
|
|
}
|
|
|
|
/* stat */
|
|
++count_file;
|
|
} else if (c == 'i') {
|
|
/* "inf" command */
|
|
snapraid_info info;
|
|
block_off_t v_pos;
|
|
uint64_t v_oldest;
|
|
|
|
if (has_info || !has_blockmax) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Duplicate or misplaced 'info' in the content file!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
has_info = 1;
|
|
|
|
ret = sgetb64(f, &v_oldest);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* ensure the unsigned serialized timestamp fits in signed time_t before conversion. */
|
|
if (v_oldest > INT64_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Internal inconsistency: Invalid info oldest timestamp %" PRIu64 "!\n", v_oldest);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
v_pos = 0;
|
|
while (v_pos < blockmax) {
|
|
int bad;
|
|
int rehash;
|
|
int justsynced;
|
|
uint64_t t64;
|
|
uint32_t flag;
|
|
block_off_t v_count;
|
|
|
|
ret = sgetb64(f, &v_count);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (v_count > blockmax || v_pos > blockmax - v_count) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Info size %" PRIu64 "/%" PRIu64 "!\n", blockmax, v_pos + v_count);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb32(f, &flag);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* if there is an info */
|
|
if ((flag & 1) != 0) {
|
|
/* read the time */
|
|
ret = sgetb64(f, &t64);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* ensure the timestamp addition fits in signed time_t without overflow. */
|
|
if (t64 > INT64_MAX - v_oldest) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Internal inconsistency: Invalid info timestamp %" PRIu64 "!\n", t64);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* analyze the flags */
|
|
bad = (flag & 2) != 0;
|
|
rehash = (flag & 4) != 0;
|
|
justsynced = (flag & 8) != 0;
|
|
|
|
if (bad)
|
|
count_bad += v_count;
|
|
if (rehash)
|
|
count_rehash += v_count;
|
|
if (justsynced)
|
|
count_unscrubbed += v_count;
|
|
|
|
if (rehash && state->prevhash == HASH_UNDEFINED) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Missing previous checksum!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
info = info_make(t64 + v_oldest, bad, rehash, justsynced);
|
|
if (!info) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Missing info!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
bucket_insert(&bucket_hash, t64 + v_oldest, v_count, justsynced);
|
|
|
|
/* insert the complete run in the info array */
|
|
info_set_run(&state->infoarr, v_pos, v_count, info);
|
|
v_pos += v_count;
|
|
} else {
|
|
while (v_count) {
|
|
/* ensure that an info is present only for used positions */
|
|
if (fs_info_is_required(state, v_pos)) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Missing info!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* go to next block */
|
|
++v_pos;
|
|
--v_count;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* A synchronized content has no invalid block state, so its unsynced
|
|
* count is certainly zero. Scan the extent maps only when needed.
|
|
*/
|
|
if (has_invalid_parity) {
|
|
for (v_pos = 0; v_pos < blockmax; ++v_pos)
|
|
if (fs_is_block_unsynced(state, v_pos))
|
|
++count_unsynced;
|
|
}
|
|
} else if (c == 'h') {
|
|
/* hole */
|
|
block_off_t v_pos;
|
|
struct snapraid_disk* disk;
|
|
uint32_t mapping;
|
|
|
|
ret = sgetb32(f, &mapping);
|
|
if (ret < 0 || mapping >= mapping_max) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Hole mapping index out of range\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
disk = tommy_array_get(&disk_mapping, mapping);
|
|
|
|
if (!has_blockmax) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Missing 'blockmax' before hole in the content file!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
v_pos = 0;
|
|
while (v_pos < blockmax) {
|
|
block_off_t v_count;
|
|
struct snapraid_file* deleted;
|
|
|
|
ret = sgetb64(f, &v_count);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (v_count == 0 || v_count > blockmax || v_pos > blockmax - v_count) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Hole size %" PRIu64 "/%" PRIu64 "!\n", blockmax, v_pos + v_count);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* get the sub-command */
|
|
c = sgetc(f);
|
|
|
|
switch (c) {
|
|
case 'o' :
|
|
/* if it's a run of deleted blocks */
|
|
has_invalid_parity = 1;
|
|
|
|
/* ensure the converted byte size fits in signed data_off_t without overflow */
|
|
if (v_count > INT64_MAX / (uint64_t)state->block_size) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Internal inconsistency: Invalid deleted block count %" PRIu64 "!\n", v_count);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* Allocate without initializing blocks. This deleted run sets every
|
|
* state and hash before the file is inserted in the extent map.
|
|
*/
|
|
deleted = file_alloc(state->block_size, "<deleted>", v_count * (data_off_t)state->block_size, 0, 0, 0);
|
|
|
|
/* mark the file as deleted */
|
|
file_flag_set(deleted, FILE_IS_DELETED);
|
|
|
|
/* insert it in the list of deleted files */
|
|
tommy_list_insert_tail(&disk->deletedlist, &deleted->nodelist, deleted);
|
|
|
|
/* read all blocks in the deleted run */
|
|
ret = state_read_block_run(f, deleted, 0, v_count, BLOCK_STATE_DELETED, STATE_BLOCK_HASH_READ);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* insert the whole deleted run in the extent map */
|
|
fs_allocate(disk, v_pos, deleted, 0, v_count);
|
|
v_pos += v_count;
|
|
break;
|
|
case 'O' :
|
|
/* go to the next run */
|
|
v_pos += v_count;
|
|
break;
|
|
default :
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid hole type!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
} else if (c == 'd') {
|
|
/* dealloc */
|
|
block_off_t v_count;
|
|
block_off_t v_pos;
|
|
struct snapraid_disk* disk;
|
|
uint32_t mapping;
|
|
|
|
ret = sgetb32(f, &mapping);
|
|
if (ret < 0 || mapping >= mapping_max) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Dealloc mapping index out of range\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
disk = tommy_array_get(&disk_mapping, mapping);
|
|
|
|
ret = sgetb64(f, &v_count);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("content_info:dealloc:%s:%" PRIu64 "\n", disk->name, v_count);
|
|
|
|
for (v_pos = 0; v_pos < v_count; ++v_pos) {
|
|
char sub[PATH_MAX];
|
|
uint64_t v_size;
|
|
uint64_t v_mtime_sec;
|
|
uint32_t v_mtime_nsec;
|
|
|
|
ret = sgetbs(f, sub, sizeof(sub));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (!path_is_sub(sub)) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid dealloc '%s'!\n", sub);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_size);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* ensure the unsigned serialized size fits in signed data_off_t before conversion. */
|
|
if (v_size > INT64_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Internal inconsistency: Invalid deallocated file size %" PRIu64 "!\n", v_size);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_mtime_sec);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb32(f, &v_mtime_nsec);
|
|
if (ret < 0 || v_mtime_nsec > 1000000000) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* STAT_NSEC_INVALID is encoded as 0 */
|
|
if (v_mtime_nsec == 0)
|
|
v_mtime_nsec = STAT_NSEC_INVALID;
|
|
else
|
|
--v_mtime_nsec;
|
|
|
|
/* allocate the file */
|
|
struct snapraid_dealloc* dealloc = dealloc_alloc(state->block_size, sub, v_size, v_mtime_sec, v_mtime_nsec);
|
|
|
|
log_tag("content_info:dealloc_entry:%s:%s:%" PRIu64 ":%" PRIu64 ":%u\n", disk->name, esc_tag(dealloc->sub), dealloc->size, dealloc->mtime_sec, dealloc->mtime_nsec);
|
|
|
|
/* read all hashes */
|
|
for (block_off_t k = 0; k < dealloc->blockmax; ++k) {
|
|
unsigned char* hash = dealloc->blockhash + k * BLOCK_HASH_SIZE;
|
|
|
|
ret = sread(f, hash, BLOCK_HASH_SIZE);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
/* insert the dealloc in the dealloc containers */
|
|
tommy_list_insert_tail(&disk->dealloclist, &dealloc->nodelist, dealloc);
|
|
}
|
|
} else if (c == 's') {
|
|
/* symlink */
|
|
char sub[PATH_MAX];
|
|
char linkto[PATH_MAX];
|
|
struct snapraid_link* slink;
|
|
struct snapraid_disk* disk;
|
|
uint32_t mapping;
|
|
|
|
ret = sgetb32(f, &mapping);
|
|
if (ret < 0 || mapping >= mapping_max) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Symlink mapping index out of range\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
disk = tommy_array_get(&disk_mapping, mapping);
|
|
|
|
ret = sgetbs(f, sub, sizeof(sub));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!path_is_sub(sub)) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid symlink '%s'!\n", sub);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetbs(f, linkto, sizeof(linkto));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* allocate the link as symbolic link */
|
|
slink = link_alloc(sub, linkto, FILE_IS_SYMLINK);
|
|
|
|
/* insert the link in the link containers */
|
|
tommy_hashdyn_insert(&disk->linkset, &slink->nodeset, slink, link_name_hash(slink->sub));
|
|
tommy_list_insert_tail(&disk->linklist, &slink->nodelist, slink);
|
|
|
|
/* stat */
|
|
++count_symlink;
|
|
} else if (c == 'a') {
|
|
/* hardlink */
|
|
char sub[PATH_MAX];
|
|
char linkto[PATH_MAX];
|
|
struct snapraid_link* slink;
|
|
struct snapraid_disk* disk;
|
|
uint32_t mapping;
|
|
|
|
ret = sgetb32(f, &mapping);
|
|
if (ret < 0 || mapping >= mapping_max) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Hardlink mapping index out of range!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
disk = tommy_array_get(&disk_mapping, mapping);
|
|
|
|
ret = sgetbs(f, sub, sizeof(sub));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!path_is_sub(sub)) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid hardlink '%s'!\n", sub);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetbs(f, linkto, sizeof(linkto));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!path_is_sub(linkto)) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid hardlink target '%s' for '%s'!\n", linkto, sub);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* allocate the link as hard link */
|
|
slink = link_alloc(sub, linkto, FILE_IS_HARDLINK);
|
|
|
|
/* insert the link in the link containers */
|
|
tommy_hashdyn_insert(&disk->linkset, &slink->nodeset, slink, link_name_hash(slink->sub));
|
|
tommy_list_insert_tail(&disk->linklist, &slink->nodelist, slink);
|
|
|
|
/* stat */
|
|
++count_hardlink;
|
|
} else if (c == 'r') {
|
|
/* dir */
|
|
char sub[PATH_MAX];
|
|
struct snapraid_dir* dir;
|
|
struct snapraid_disk* disk;
|
|
uint32_t mapping;
|
|
|
|
ret = sgetb32(f, &mapping);
|
|
if (ret < 0 || mapping >= mapping_max) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Dir mapping index out of range!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
disk = tommy_array_get(&disk_mapping, mapping);
|
|
|
|
ret = sgetbs(f, sub, sizeof(sub));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!path_is_sub(sub)) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid dir '%s'!\n", sub);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* allocate the dir */
|
|
dir = dir_alloc(sub);
|
|
|
|
/* insert the dir in the dir containers */
|
|
tommy_hashdyn_insert(&disk->dirset, &dir->nodeset, dir, dir_name_hash(dir->sub));
|
|
tommy_list_insert_tail(&disk->dirlist, &dir->nodelist, dir);
|
|
|
|
/* stat */
|
|
++count_dir;
|
|
} else if (c == 'c') {
|
|
if (has_hash || mapping_max != 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Duplicate or misplaced 'hash' in the content file!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
has_hash = 1;
|
|
|
|
/* get the subcommand */
|
|
c = sgetc(f);
|
|
|
|
switch (c) {
|
|
case 'u' :
|
|
state->hash = HASH_MURMUR3;
|
|
break;
|
|
case 'k' :
|
|
state->hash = HASH_SPOOKY2;
|
|
break;
|
|
default :
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid checksum!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* read the seed */
|
|
ret = sread(f, state->hashseed, HASH_MAX);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
} else if (c == 'C') {
|
|
if (has_prevhash || mapping_max != 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Duplicate or misplaced 'prevhash' in the content file!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
has_prevhash = 1;
|
|
|
|
/* get the sub-command */
|
|
c = sgetc(f);
|
|
|
|
switch (c) {
|
|
case 'u' :
|
|
state->prevhash = HASH_MURMUR3;
|
|
break;
|
|
case 'k' :
|
|
state->prevhash = HASH_SPOOKY2;
|
|
break;
|
|
default :
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid checksum!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* read the seed */
|
|
ret = sread(f, state->prevhashseed, HASH_MAX);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
} else if (c == 'z') {
|
|
uint32_t block_size;
|
|
|
|
if (has_block_size || mapping_max != 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Duplicate or misplaced 'blocksize' in the content file!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
has_block_size = 1;
|
|
|
|
ret = sgetb32(f, &block_size);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (block_size == 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Zero 'blocksize' specification in the content file!\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* without configuration, auto assign the block size */
|
|
if (state->no_conf) {
|
|
state->block_size = block_size;
|
|
}
|
|
|
|
if (block_size != state->block_size) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EUSER, "Mismatching 'blocksize' specification in the content file!\n");
|
|
log_fatal(EUSER, "Please restore the 'blocksize' value in the configuration file to '%u'\n", block_size / KIBI);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
} else if (c == 'y') {
|
|
uint64_t hash_size;
|
|
|
|
if (has_hash_size || mapping_max != 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Duplicate or misplaced 'hashsize' in the content file!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
has_hash_size = 1;
|
|
|
|
ret = sgetb64(f, &hash_size);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (hash_size == 2) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Unsupported 'hashsize 2' specification in the content file! The minimum supported hash size is 4.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (hash_size < 4 || hash_size > HASH_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid 'hashsize' specification in the content file!\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* without configuration, auto assign the hash size */
|
|
if (state->no_conf) {
|
|
BLOCK_HASH_SIZE = hash_size;
|
|
}
|
|
|
|
if (hash_size != BLOCK_HASH_SIZE) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EUSER, "Mismatching 'hashsize' specification in the content file!\n");
|
|
log_fatal(EUSER, "Please restore the 'hashsize' value in the configuration file to '%" PRIu64 "'\n", hash_size);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
} else if (c == 'x') {
|
|
if (has_blockmax || mapping_max != 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Duplicate or misplaced 'blockmax' in the content file!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
has_blockmax = 1;
|
|
|
|
ret = sgetb64(f, &blockmax);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
if (blockmax > BLOCK_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
#if SIZE_MAX == UINT32_MAX
|
|
log_fatal(ESOFT, "Array with %" PRIu64 " blocks in '%s' is too large for a 32-bit build. Use a 64-bit build or increase the block size.\n", blockmax, path);
|
|
#else
|
|
log_fatal(ESOFT, "Array with %" PRIu64 " blocks in '%s' is too large. Increase the block size.\n", blockmax, path);
|
|
#endif
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
} else if (c == 'm' || c == 'M') {
|
|
struct snapraid_map* map;
|
|
char uuid[UUID_MAX];
|
|
uint32_t v_idx;
|
|
block_off_t v_total_blocks;
|
|
block_off_t v_free_blocks;
|
|
struct snapraid_disk* disk;
|
|
|
|
if (mapping_max >= RAID_DATA_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Too many data disk mappings in the content file!\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetbs(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb32(f, &v_idx);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (v_idx >= RAID_DATA_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid disk position '%u' in the content file!\n", v_idx);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* from SnapRAID 7.0 the 'M' command includes the free space */
|
|
if (c == 'M') {
|
|
ret = sgetb64(f, &v_total_blocks);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_free_blocks);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("content_data:%s:%" PRIi64 ":%" PRIi64 "\n",
|
|
esc_tag(buffer),
|
|
v_total_blocks * (uint64_t)state->block_size,
|
|
v_free_blocks * (uint64_t)state->block_size);
|
|
} else {
|
|
v_total_blocks = 0;
|
|
v_free_blocks = 0;
|
|
}
|
|
|
|
/* read the uuid */
|
|
ret = sgetbs(f, uuid, sizeof(uuid));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("content_data_split:%s:%s\n",
|
|
esc_tag(buffer),
|
|
uuid);
|
|
|
|
/* find the disk */
|
|
disk = find_disk_by_name(state, buffer);
|
|
if (!disk) {
|
|
/* search by UUID if renamed */
|
|
disk = find_disk_by_uuid(state, uuid);
|
|
if (disk) {
|
|
log_info(EUSER, "WARNING! Renaming disk '%s' to '%s'\n", buffer, disk->name);
|
|
|
|
/* write the new state with the new name */
|
|
state->need_write = 1;
|
|
}
|
|
}
|
|
if (!disk) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EUSER, "Disk '%s' with uuid '%s' not present in the configuration file!\n", buffer, uuid);
|
|
log_fatal(EUSER, "If you have removed it from the configuration file, please restore it\n");
|
|
/* if it's a command without UUID, it cannot autorename using UUID */
|
|
if (state->opt.skip_disk_access)
|
|
log_fatal(EUSER, "If you have renamed it, run 'sync' to update the new name\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
map = map_alloc(disk->name, v_idx, v_total_blocks, v_free_blocks, uuid);
|
|
|
|
tommy_list_insert_tail(&state->maplist, &map->node, map);
|
|
|
|
/* insert in the mapping vector */
|
|
tommy_array_grow(&disk_mapping, mapping_max + 1);
|
|
tommy_array_set(&disk_mapping, mapping_max, disk);
|
|
++mapping_max;
|
|
} else if (c == 'P') {
|
|
/*
|
|
* From SnapRAID 7.0 the 'P' command includes the free space
|
|
* from SnapRAID 11.0 the 'P' command is deprecated by 'Q'
|
|
*/
|
|
char v_uuid[UUID_MAX];
|
|
uint32_t v_level;
|
|
block_off_t v_total_blocks;
|
|
block_off_t v_free_blocks;
|
|
|
|
ret = sgetb32(f, &v_level);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_total_blocks);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_free_blocks);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetbs(f, v_uuid, sizeof(v_uuid));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (v_level >= LEV_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid parity level '%u' in the configuration file!\n", v_level);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("content_parity:%s:%" PRIi64 ":%" PRIi64 "\n",
|
|
lev_config_name(v_level),
|
|
v_total_blocks * (uint64_t)state->block_size,
|
|
v_free_blocks * (uint64_t)state->block_size);
|
|
|
|
/* auto configure if configuration is missing */
|
|
if (state->no_conf) {
|
|
if (v_level >= state->level)
|
|
state->level = v_level + 1;
|
|
}
|
|
|
|
/* if we use this parity entry */
|
|
if (v_level < state->level) {
|
|
/* if the configuration has more splits, keep them */
|
|
if (state->parity[v_level].split_mac < 1)
|
|
state->parity[v_level].split_mac = 1;
|
|
/* set the parity info */
|
|
if (state->no_conf) {
|
|
pathcpy(state->parity[v_level].split_map[0].uuid, sizeof(state->parity[v_level].split_map[0].uuid), v_uuid);
|
|
}
|
|
pathcpy(state->parity[v_level].split_map[0].content_uuid, sizeof(state->parity[v_level].split_map[0].content_uuid), v_uuid);
|
|
state->parity[v_level].total_blocks = v_total_blocks;
|
|
state->parity[v_level].free_blocks = v_free_blocks;
|
|
|
|
log_tag("content_parity_split:%s:%s:%s:%" PRIi64 "\n",
|
|
lev_config_name(v_level), /* P command always has a single split */
|
|
state->parity[v_level].split_map[0].content_uuid,
|
|
esc_tag(state->parity[v_level].split_map[0].path),
|
|
state->parity[v_level].split_map[0].size);
|
|
}
|
|
} else if (c == 'Q') {
|
|
/* from SnapRAID 11.0 the 'Q' command include size info and multi file support */
|
|
uint32_t v_level;
|
|
block_off_t v_total_blocks;
|
|
block_off_t v_free_blocks;
|
|
uint32_t v_split_mac;
|
|
unsigned s;
|
|
|
|
ret = sgetb32(f, &v_level);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_total_blocks);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_free_blocks);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb32(f, &v_split_mac);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (v_split_mac > SPLIT_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Too many splits '%u' in the content file!\n", v_split_mac);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (v_level >= LEV_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid parity level '%u' in the configuration file!\n", v_level);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* auto configure if configuration is missing */
|
|
if (state->no_conf) {
|
|
if (v_level >= state->level)
|
|
state->level = v_level + 1;
|
|
if (state->parity[v_level].split_mac < v_split_mac)
|
|
state->parity[v_level].split_mac = v_split_mac;
|
|
}
|
|
|
|
/* if we use this parity entry */
|
|
if (v_level < state->level) {
|
|
/* set the parity info */
|
|
state->parity[v_level].total_blocks = v_total_blocks;
|
|
state->parity[v_level].free_blocks = v_free_blocks;
|
|
}
|
|
|
|
log_tag("content_parity:%s:%" PRIi64 ":%" PRIi64 "\n",
|
|
lev_config_name(v_level),
|
|
v_total_blocks * (uint64_t)state->block_size,
|
|
v_free_blocks * (uint64_t)state->block_size);
|
|
|
|
for (s = 0; s < v_split_mac; ++s) {
|
|
char v_path[PATH_MAX];
|
|
char v_uuid[UUID_MAX];
|
|
uint64_t v_size;
|
|
|
|
ret = sgetbs(f, v_path, sizeof(v_path));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetbs(f, v_uuid, sizeof(v_uuid));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
ret = sgetb64(f, &v_size);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* ensure the unsigned serialized size fits in signed data_off_t before conversion. */
|
|
if (v_size > INT64_MAX) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Internal inconsistency: Invalid split file size %" PRIu64 "!\n", v_size);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* if we use this parity entry */
|
|
if (v_level < state->level) {
|
|
/* if this split was removed from the configuration */
|
|
if (s >= state->parity[v_level].split_mac) {
|
|
/* if the file is used, we really need it */
|
|
if (v_size != 0) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EUSER, "Parity '%s' misses used file '%u'!\n", lev_config_name(v_level), s);
|
|
log_fatal(EUSER, "If you have removed it from the configuration file, please restore it\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* otherwise we can drop it */
|
|
log_info(EUSER, "WARNING! Dropping from '%s' unused split '%u'\n", lev_config_name(v_level), s);
|
|
} else {
|
|
char parity_name[64];
|
|
|
|
/* we copy the path and uuid only if without configuration file */
|
|
if (state->no_conf) {
|
|
pathcpy(state->parity[v_level].split_map[s].path, sizeof(state->parity[v_level].split_map[s].path), v_path);
|
|
pathcpy(state->parity[v_level].split_map[s].uuid, sizeof(state->parity[v_level].split_map[s].uuid), v_uuid);
|
|
}
|
|
|
|
/* set the split info */
|
|
pathcpy(state->parity[v_level].split_map[s].content_uuid, sizeof(state->parity[v_level].split_map[s].content_uuid), v_uuid);
|
|
state->parity[v_level].split_map[s].size = v_size;
|
|
|
|
if (s == 0)
|
|
pathcpy(parity_name, sizeof(parity_name), lev_config_name(v_level));
|
|
else
|
|
pathprint(parity_name, sizeof(parity_name), "%s/%u", lev_config_name(v_level), s);
|
|
log_tag("content_parity_split:%s:%s:%s:%" PRIi64 "\n",
|
|
parity_name,
|
|
state->parity[v_level].split_map[s].content_uuid,
|
|
esc_tag(state->parity[v_level].split_map[s].path),
|
|
state->parity[v_level].split_map[s].size);
|
|
}
|
|
}
|
|
}
|
|
} else if (c == 'N') {
|
|
uint32_t crc_stored;
|
|
uint32_t crc_computed;
|
|
|
|
/* get the crc before reading it from the file */
|
|
crc_computed = scrc(f);
|
|
|
|
ret = sgetble32(f, &crc_stored);
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
/* here don't call decoding_error() because it's too late to get the crc */
|
|
log_fatal(errno, "Error reading the CRC in '%s' at offset %" PRIi64 "\n", path, stell(f));
|
|
log_fatal(errno, "The content file '%s' is damaged or corrupted!\n", path);
|
|
log_fatal(errno, "To recover, rename or delete it and rerun the command.\n");
|
|
log_fatal(errno, "SnapRAID will automatically fall back to the next healthy copy.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (crc_stored != crc_computed) {
|
|
/* LCOV_EXCL_START */
|
|
/* here don't call decoding_error() because it's too late to get the crc */
|
|
log_fatal(ECONTENT, "CRC mismatch in '%s'\n", path);
|
|
log_fatal(ECONTENT, "The content file '%s' is damaged or corrupted (CRC mismatch)!\n", path);
|
|
log_fatal(ECONTENT, "To recover, rename or delete it and rerun the command.\n");
|
|
log_fatal(ECONTENT, "SnapRAID will automatically fall back to the next healthy copy.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
crc_checked = 1;
|
|
|
|
/* trailing bytes would lie outside the content boundary verified by the terminal CRC */
|
|
c = sgetc(f);
|
|
if (c != EOF) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ECONTENT, "Unexpected data after CRC in content file '%s' at offset %" PRIi64 "\n", path, stell(f));
|
|
log_fatal(ECONTENT, "The content file '%s' is damaged or corrupted!\n", path);
|
|
log_fatal(ECONTENT, "To recover, rename or delete it and rerun the command.\n");
|
|
log_fatal(ECONTENT, "SnapRAID will automatically fall back to the next healthy copy.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
break;
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(ECONTENT, "Invalid command '%c'!\n", (char)c);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
tommy_array_done(&disk_mapping);
|
|
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error reading the content file '%s' at offset %" PRIi64 "\n", path, stell(f));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!crc_checked) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ECONTENT, "Reached the end of '%s' without finding the expected CRC\n", path);
|
|
log_fatal(ECONTENT, "The content file '%s' is truncated or damaged!\n", path);
|
|
log_fatal(ECONTENT, "To recover, rename or delete it and rerun the command.\n");
|
|
log_fatal(ECONTENT, "SnapRAID will automatically fall back to the next healthy copy.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (!has_blockmax) {
|
|
/* LCOV_EXCL_START */
|
|
decoding_error(path, f);
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Missing 'blockmax' in the content file!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* check the file-system on all disks */
|
|
state_fscheck(state, "after read");
|
|
|
|
/* check that the stored parity size matches the loaded state */
|
|
if (blockmax != parity_allocated_size(state)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Parity size %" PRIu64 "/%" PRIu64 " in '%s' at offset %" PRIi64 "\n", blockmax, parity_allocated_size(state), path, stell(f));
|
|
if (state->opt.skip_content_check) {
|
|
log_fatal(ECONTENT, "Overriding.\n");
|
|
blockmax = parity_allocated_size(state);
|
|
} else {
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
msg_verbose("%8" PRIu64 " files\n", count_file);
|
|
msg_verbose("%8" PRIu64 " hardlinks\n", count_hardlink);
|
|
msg_verbose("%8" PRIu64 " symlinks\n", count_symlink);
|
|
msg_verbose("%8" PRIu64 " empty dirs\n", count_dir);
|
|
|
|
log_tag("content_info:file:%" PRIu64 "\n", count_file);
|
|
log_tag("content_info:hardlink:%" PRIu64 "\n", count_hardlink);
|
|
log_tag("content_info:symlink:%" PRIu64 "\n", count_symlink);
|
|
log_tag("content_info:dir_empty:%" PRIu64 "\n", count_dir);
|
|
|
|
log_tag("content_info:block:%" PRIu64 "\n", blockmax);
|
|
log_tag("content_info:block_bad:%" PRIu64 "\n", count_bad);
|
|
log_tag("content_info:block_rehash:%" PRIu64 "\n", count_rehash);
|
|
log_tag("content_info:block_unsynced:%" PRIu64 "\n", count_unsynced);
|
|
log_tag("content_info:block_unscrubbed:%" PRIu64 "\n", count_unscrubbed);
|
|
|
|
/* store the blocks counters */
|
|
state->rehash_blocks = count_rehash;
|
|
state->bad_blocks = count_bad;
|
|
state->unsynced_blocks = count_unsynced;
|
|
state->unscrubbed_blocks = count_unscrubbed;
|
|
|
|
/* store the bucket info list */
|
|
bucket_to_list(&bucket_hash, &state->bucketlist, &state->bucketcount);
|
|
tommy_hashdyn_done(&bucket_hash);
|
|
}
|
|
|
|
struct state_write_thread_context {
|
|
struct snapraid_state* state;
|
|
#if HAVE_MT_WRITE
|
|
thread_id_t thread;
|
|
#endif
|
|
/* input */
|
|
int content_version;
|
|
block_off_t blockmax;
|
|
time_t info_oldest;
|
|
time_t info_now;
|
|
int info_has_rehash;
|
|
uint64_t count_unsynced;
|
|
STREAM* f;
|
|
int first;
|
|
#if HAVE_MT_WRITE
|
|
const char* content;
|
|
#endif
|
|
|
|
/* output (required to postpone the output to the terminal after the latest write) */
|
|
uint32_t crc;
|
|
uint64_t count_file;
|
|
uint64_t count_hardlink;
|
|
uint64_t count_symlink;
|
|
uint64_t count_dir;
|
|
uint64_t count_bad;
|
|
uint64_t count_rehash;
|
|
uint64_t count_unscrubbed;
|
|
};
|
|
|
|
/**
|
|
* Write all hashes in a contiguous run of file blocks.
|
|
*
|
|
* The source hashes are separated by the block state byte, so they cannot be
|
|
* copied with a single memcpy(). Keep the stream pointer local while filling
|
|
* each buffered group to avoid the per-block stream call and field updates.
|
|
*/
|
|
static int state_write_block_run(STREAM* f, struct snapraid_file* file, block_off_t file_pos, block_off_t count)
|
|
{
|
|
unsigned char* block_ptr = (unsigned char*)file_block(file, file_pos);
|
|
size_t block_stride = block_sizeof();
|
|
|
|
while (count) {
|
|
size_t available = (size_t)(f->end - f->pos);
|
|
block_off_t cached_count = available / BLOCK_HASH_SIZE;
|
|
unsigned char* output;
|
|
|
|
if (cached_count > count)
|
|
cached_count = count;
|
|
|
|
if (cached_count == 0) {
|
|
struct snapraid_block* block = (struct snapraid_block*)block_ptr;
|
|
|
|
if (swrite(block->hash, BLOCK_HASH_SIZE, f) != 0)
|
|
return -1;
|
|
|
|
block_ptr += block_stride;
|
|
--count;
|
|
continue;
|
|
}
|
|
|
|
output = f->pos;
|
|
for (block_off_t i = 0; i < cached_count; ++i) {
|
|
struct snapraid_block* block = (struct snapraid_block*)block_ptr;
|
|
|
|
hash_copy(output, block->hash);
|
|
|
|
output += BLOCK_HASH_SIZE;
|
|
block_ptr += block_stride;
|
|
}
|
|
|
|
f->pos = output;
|
|
count -= cached_count;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void* state_write_thread(void* arg)
|
|
{
|
|
struct state_write_thread_context* context = arg;
|
|
struct snapraid_state* state = context->state;
|
|
block_off_t blockmax = context->blockmax;
|
|
time_t info_oldest = context->info_oldest;
|
|
time_t info_now = context->info_now;
|
|
int info_has_rehash = context->info_has_rehash;
|
|
STREAM* f = context->f;
|
|
uint32_t crc;
|
|
uint64_t t64;
|
|
uint64_t count_file;
|
|
uint64_t count_hardlink;
|
|
uint64_t count_symlink;
|
|
uint64_t count_dir;
|
|
uint64_t count_bad;
|
|
uint64_t count_rehash;
|
|
uint64_t count_unscrubbed;
|
|
tommy_node* i;
|
|
block_off_t begin;
|
|
unsigned l, s;
|
|
tommy_hashdyn bucket_hash;
|
|
#if HAVE_MT_WRITE
|
|
uint64_t start;
|
|
|
|
start = os_tick_ms();
|
|
#endif
|
|
|
|
count_file = 0;
|
|
count_hardlink = 0;
|
|
count_symlink = 0;
|
|
count_dir = 0;
|
|
count_bad = 0;
|
|
count_rehash = 0;
|
|
count_unscrubbed = 0;
|
|
tommy_hashdyn_init(&bucket_hash);
|
|
|
|
/* write header */
|
|
if (context->content_version >= 4)
|
|
swrite("SNAPCNT4\n\3\0\0", 12, f);
|
|
else
|
|
swrite("SNAPCNT3\n\3\0\0", 12, f);
|
|
|
|
/* write block size and block max */
|
|
sputc('z', f);
|
|
sputb32(state->block_size, f);
|
|
sputc('x', f);
|
|
sputb64(blockmax, f);
|
|
|
|
/* hash size */
|
|
sputc('y', f);
|
|
sputb64(BLOCK_HASH_SIZE, f);
|
|
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
sputc('c', f);
|
|
if (state->hash == HASH_MURMUR3) {
|
|
sputc('u', f);
|
|
} else if (state->hash == HASH_SPOOKY2) {
|
|
sputc('k', f);
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Unexpected hash when writing the content file '%s'.\n", serrorfile(f));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
swrite(state->hashseed, HASH_MAX, f);
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* previous hash only present */
|
|
if (state->prevhash != HASH_UNDEFINED) {
|
|
/* if at least one rehash tag found, we have to save the previous hash */
|
|
if (info_has_rehash) {
|
|
sputc('C', f);
|
|
if (state->prevhash == HASH_MURMUR3) {
|
|
sputc('u', f);
|
|
} else if (state->prevhash == HASH_SPOOKY2) {
|
|
sputc('k', f);
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Unexpected prevhash when writing the content file '%s'.\n", serrorfile(f));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
swrite(state->prevhashseed, HASH_MAX, f);
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
|
|
/* for each map */
|
|
for (i = state->maplist; i != 0; i = i->next) {
|
|
struct snapraid_map* map = i->data;
|
|
struct snapraid_disk* disk;
|
|
|
|
/* find the disk for this mapping */
|
|
disk = find_disk_by_name(state, map->name);
|
|
if (!disk) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Unmapped disk '%s'\n", map->name);
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* save the mapping only if disk is mapped */
|
|
if (disk->mapping_idx != -1) {
|
|
const char* uuid;
|
|
|
|
sputc('M', f);
|
|
sputbs(map->name, f);
|
|
sputb32(map->position, f);
|
|
sputb64(map->total_blocks, f);
|
|
sputb64(map->free_blocks, f);
|
|
|
|
/*
|
|
* If the probed uuid is empty, but disk access was skipped (globally
|
|
* or individually), write the last known uuid. Otherwise, write the
|
|
* empty probed uuid.
|
|
*/
|
|
if (disk->uuid[0] == 0 && (state->opt.skip_disk_access || disk->skip_access)) {
|
|
uuid = map->content_uuid;
|
|
} else {
|
|
uuid = disk->uuid;
|
|
}
|
|
sputbs(uuid, f);
|
|
|
|
if (context->first) {
|
|
log_tag("content_data:%s:%" PRIi64 ":%" PRIi64 "\n",
|
|
esc_tag(map->name),
|
|
map->total_blocks * (uint64_t)state->block_size,
|
|
map->free_blocks * (uint64_t)state->block_size);
|
|
log_tag("content_data_split:%s:%s\n",
|
|
esc_tag(map->name),
|
|
uuid);
|
|
}
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
|
|
/* for each parity */
|
|
for (l = 0; l < state->level; ++l) {
|
|
sputc('Q', f);
|
|
sputb32(l, f);
|
|
sputb64(state->parity[l].total_blocks, f);
|
|
sputb64(state->parity[l].free_blocks, f);
|
|
if (context->first) {
|
|
log_tag("content_parity:%s:%" PRIi64 ":%" PRIi64 "\n",
|
|
lev_config_name(l),
|
|
state->parity[l].total_blocks * (uint64_t)state->block_size,
|
|
state->parity[l].free_blocks * (uint64_t)state->block_size);
|
|
}
|
|
sputb32(state->parity[l].split_mac, f);
|
|
for (s = 0; s < state->parity[l].split_mac; ++s) {
|
|
char parity_name[64];
|
|
const char* uuid;
|
|
sputbs(state->parity[l].split_map[s].path, f);
|
|
|
|
/*
|
|
* If the probed uuid is not empty, write it. Otherwise, if parity access was
|
|
* skipped (globally or individually), write the last known uuid read from the
|
|
* content file. Otherwise, write the empty probed uuid.
|
|
*/
|
|
if (state->parity[l].split_map[s].uuid[0] == 0
|
|
&& (state->opt.skip_parity_access || state->parity[l].skip_access)) {
|
|
uuid = state->parity[l].split_map[s].content_uuid;
|
|
} else {
|
|
uuid = state->parity[l].split_map[s].uuid;
|
|
}
|
|
sputbs(uuid, f);
|
|
|
|
sputb64(state->parity[l].split_map[s].size, f);
|
|
if (s == 0)
|
|
pathcpy(parity_name, sizeof(parity_name), lev_config_name(l));
|
|
else
|
|
pathprint(parity_name, sizeof(parity_name), "%s/%u", lev_config_name(l), s);
|
|
if (context->first) {
|
|
log_tag("content_parity_split:%s:%s:%s:%" PRIi64 "\n",
|
|
parity_name,
|
|
uuid,
|
|
esc_tag(state->parity[l].split_map[s].path),
|
|
state->parity[l].split_map[s].size);
|
|
}
|
|
}
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
/* for each disk */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
tommy_node* j;
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
/* if the disk is not mapped, skip it */
|
|
if (disk->mapping_idx < 0)
|
|
continue;
|
|
|
|
/* for each file */
|
|
for (j = disk->filelist; j != 0; j = j->next) {
|
|
struct snapraid_file* file = j->data;
|
|
size_t block_stride = block_sizeof();
|
|
uint64_t size;
|
|
uint64_t mtime_sec;
|
|
int32_t mtime_nsec;
|
|
uint64_t inode;
|
|
|
|
size = file->size;
|
|
mtime_sec = file->mtime_sec;
|
|
mtime_nsec = file->mtime_nsec;
|
|
inode = file->inode;
|
|
|
|
sputc('f', f);
|
|
sputb32(disk->mapping_idx, f);
|
|
sputb64(size, f);
|
|
sputb64(mtime_sec, f);
|
|
/* encode STAT_NSEC_INVALID as 0 */
|
|
if (mtime_nsec == STAT_NSEC_INVALID)
|
|
sputb32(0, f);
|
|
else
|
|
sputb32(mtime_nsec + 1, f);
|
|
sputb64(inode, f);
|
|
sputbs(file->sub, f);
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* for all the blocks of the file */
|
|
begin = 0;
|
|
while (begin < file->blockmax) {
|
|
struct snapraid_block* block = file_block(file, begin);
|
|
unsigned char* end_block_ptr = (unsigned char*)block + block_stride;
|
|
unsigned v_state = block_state_get(block);
|
|
block_off_t extent_count;
|
|
block_off_t v_pos = fs_file2par_get_run(disk, file, begin, &extent_count);
|
|
block_off_t extent_end = begin + extent_count;
|
|
block_off_t v_count;
|
|
|
|
block_off_t end;
|
|
|
|
/* find the end of run of blocks */
|
|
end = begin + 1;
|
|
while (end < file->blockmax) {
|
|
block = (struct snapraid_block*)end_block_ptr;
|
|
if (v_state != block_state_get(block))
|
|
break;
|
|
|
|
/* Resolve the mapping only when crossing an extent boundary. */
|
|
if (end == extent_end) {
|
|
block_off_t next_count;
|
|
block_off_t next_pos = fs_file2par_get_run(disk, file, end, &next_count);
|
|
|
|
if (v_pos + (end - begin) != next_pos)
|
|
break;
|
|
|
|
extent_end += next_count;
|
|
}
|
|
|
|
end_block_ptr += block_stride;
|
|
++end;
|
|
}
|
|
|
|
switch (v_state) {
|
|
case BLOCK_STATE_BLK :
|
|
sputc('b', f);
|
|
break;
|
|
case BLOCK_STATE_REBUILD :
|
|
sputc('r', f);
|
|
break;
|
|
case BLOCK_STATE_CHG :
|
|
sputc('g', f);
|
|
break;
|
|
case BLOCK_STATE_REP :
|
|
sputc('p', f);
|
|
break;
|
|
default :
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Internal inconsistency: State for block %" PRIu64 " state %u\n", v_pos, v_state);
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
sputb64(v_pos, f);
|
|
|
|
v_count = end - begin;
|
|
sputb64(v_count, f);
|
|
|
|
/* write all hashes in the run */
|
|
if (state_write_block_run(f, file, begin, v_count) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* next begin position */
|
|
begin = end;
|
|
}
|
|
|
|
++count_file;
|
|
}
|
|
|
|
/* for each link */
|
|
for (j = disk->linklist; j != 0; j = j->next) {
|
|
struct snapraid_link* slink = j->data;
|
|
|
|
switch (link_flag_get(slink, FILE_IS_LINK_MASK)) {
|
|
case FILE_IS_HARDLINK :
|
|
sputc('a', f);
|
|
++count_hardlink;
|
|
break;
|
|
case FILE_IS_SYMLINK :
|
|
sputc('s', f);
|
|
++count_symlink;
|
|
break;
|
|
}
|
|
|
|
sputb32(disk->mapping_idx, f);
|
|
sputbs(slink->sub, f);
|
|
sputbs(slink->linkto, f);
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
/* for each dir */
|
|
for (j = disk->dirlist; j != 0; j = j->next) {
|
|
struct snapraid_dir* dir = j->data;
|
|
|
|
sputc('r', f);
|
|
sputb32(disk->mapping_idx, f);
|
|
sputbs(dir->sub, f);
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
++count_dir;
|
|
}
|
|
|
|
/* deleted blocks of the disk */
|
|
sputc('h', f);
|
|
sputb32(disk->mapping_idx, f);
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
begin = 0;
|
|
if (tommy_list_empty(&disk->deletedlist)) {
|
|
/* the common case needs only one non-deleted run */
|
|
if (blockmax) {
|
|
sputb64(blockmax, f);
|
|
sputc('O', f);
|
|
}
|
|
} else {
|
|
while (begin < blockmax) {
|
|
int is_deleted;
|
|
block_off_t end;
|
|
|
|
is_deleted = fs_is_block_deleted(disk, begin);
|
|
|
|
/* find the end of run of blocks */
|
|
end = begin + 1;
|
|
while (end < blockmax
|
|
&& is_deleted == fs_is_block_deleted(disk, end)
|
|
) {
|
|
++end;
|
|
}
|
|
|
|
sputb64(end - begin, f);
|
|
|
|
if (is_deleted) {
|
|
/* write the run of deleted blocks with hash */
|
|
sputc('o', f);
|
|
|
|
/* write all the hashes */
|
|
while (begin < end) {
|
|
struct snapraid_block* block = fs_par2block_get(disk, begin);
|
|
|
|
swrite(block->hash, BLOCK_HASH_SIZE, f);
|
|
|
|
++begin;
|
|
}
|
|
} else {
|
|
/* used and empty blocks don't have a hash in this section */
|
|
sputc('O', f);
|
|
begin = end;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* deallocated files of the disk */
|
|
if (!tommy_list_empty(&disk->dealloclist)) {
|
|
sputc('d', f);
|
|
sputb32(disk->mapping_idx, f);
|
|
|
|
block_off_t v_count = tommy_list_count(&disk->dealloclist);
|
|
|
|
sputb64(v_count, f);
|
|
if (context->first)
|
|
log_tag("content_info:dealloc:%s:%" PRIu64 "\n", disk->name, v_count);
|
|
|
|
/* for each file */
|
|
for (j = tommy_list_head(&disk->dealloclist); j != 0; j = j->next) {
|
|
struct snapraid_dealloc* dealloc = j->data;
|
|
sputbs(dealloc->sub, f);
|
|
sputb64(dealloc->size, f);
|
|
sputb64(dealloc->mtime_sec, f);
|
|
|
|
/* encode STAT_NSEC_INVALID as 0 */
|
|
if (dealloc->mtime_nsec == STAT_NSEC_INVALID)
|
|
sputb32(0, f);
|
|
else
|
|
sputb32(dealloc->mtime_nsec + 1, f);
|
|
|
|
if (context->first)
|
|
log_tag("content_info:dealloc_entry:%s:%s:%" PRIu64 ":%" PRIu64 ":%u\n", disk->name, esc_tag(dealloc->sub), dealloc->size, dealloc->mtime_sec, dealloc->mtime_nsec);
|
|
|
|
/* deallocated hashes are already contiguous in memory */
|
|
swrite(dealloc->blockhash, (size_t)dealloc->blockmax * BLOCK_HASH_SIZE, f);
|
|
}
|
|
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
} else {
|
|
if (context->first)
|
|
log_tag("content_info:dealloc:%s:0\n", disk->name);
|
|
}
|
|
}
|
|
|
|
/* write the info for each block */
|
|
sputc('i', f);
|
|
/* ensure to write a 64 bit time */
|
|
t64 = info_oldest;
|
|
sputb64(t64, f);
|
|
begin = 0;
|
|
while (begin < blockmax) {
|
|
snapraid_info info;
|
|
block_off_t end;
|
|
block_off_t count;
|
|
time_t t;
|
|
unsigned flag;
|
|
|
|
/* find the complete run without resolving each array position */
|
|
count = info_get_run(&state->infoarr, begin, blockmax - begin, &info);
|
|
end = begin + count;
|
|
sputb64(count, f);
|
|
|
|
/* if there is info */
|
|
if (info) {
|
|
/* other flags */
|
|
flag = 1; /* info is present */
|
|
if (info_get_bad(info)) {
|
|
flag |= 2;
|
|
if (context->first)
|
|
count_bad += count;
|
|
}
|
|
if (info_get_rehash(info)) {
|
|
flag |= 4;
|
|
if (context->first)
|
|
count_rehash += count;
|
|
}
|
|
if (info_get_justsynced(info)) {
|
|
flag |= 8;
|
|
if (context->first)
|
|
count_unscrubbed += count;
|
|
}
|
|
sputb32(flag, f);
|
|
|
|
t = info_get_time(info);
|
|
|
|
if (context->first)
|
|
bucket_insert(&bucket_hash, t, count, flag & 8);
|
|
|
|
/* truncate any time that is in the future */
|
|
if (t > info_now)
|
|
t = info_now;
|
|
|
|
/* the oldest info is computed only on required blocks, so it may not be the absolute oldest */
|
|
if (t < info_oldest)
|
|
t = 0;
|
|
else
|
|
t -= info_oldest;
|
|
|
|
/* ensure to write a 64 bit time */
|
|
t64 = t;
|
|
sputb64(t64, f);
|
|
} else {
|
|
/* write a special 0 flag to mark missing info */
|
|
sputb32(0, f);
|
|
}
|
|
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* next begin position */
|
|
begin = end;
|
|
}
|
|
|
|
sputc('N', f);
|
|
|
|
/* flush data written to the disk */
|
|
if (sflush(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s' (in flush before crc). %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* get the file crc */
|
|
crc = scrc(f);
|
|
|
|
sputble32(crc, f);
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* set output variables */
|
|
context->crc = crc;
|
|
context->count_file = count_file;
|
|
context->count_hardlink = count_hardlink;
|
|
context->count_symlink = count_symlink;
|
|
context->count_dir = count_dir;
|
|
context->count_bad = count_bad;
|
|
context->count_rehash = count_rehash;
|
|
context->count_unscrubbed = count_unscrubbed;
|
|
|
|
/* store the bucket info list */
|
|
if (context->first)
|
|
bucket_to_list(&bucket_hash, &state->bucketlist, &state->bucketcount);
|
|
|
|
/*
|
|
* Do not need to free the bucket entries in the success path because they
|
|
* are added only if context->first is true, and they are already moved
|
|
* to the list.
|
|
*/
|
|
|
|
tommy_hashdyn_done(&bucket_hash);
|
|
#if HAVE_MT_WRITE
|
|
msg_progress("Saved %s in %" PRIu64 " ms\n", context->content, os_tick_ms() - start);
|
|
#endif
|
|
return 0;
|
|
|
|
bail:
|
|
tommy_hashdyn_foreach(&bucket_hash, bucket_free);
|
|
tommy_hashdyn_done(&bucket_hash);
|
|
return context;
|
|
}
|
|
|
|
static void state_write_content(struct snapraid_state* state, uint32_t* out_crc)
|
|
{
|
|
#if HAVE_MT_WRITE
|
|
int fail;
|
|
int first;
|
|
#else
|
|
STREAM* f;
|
|
unsigned count_content;
|
|
unsigned k;
|
|
struct state_write_thread_context* context;
|
|
void* retval;
|
|
#endif
|
|
tommy_node* i;
|
|
block_off_t blockmax;
|
|
time_t info_oldest;
|
|
time_t info_now;
|
|
time_t content_mtime;
|
|
int info_has_rehash;
|
|
int state_has_rebuild;
|
|
int mapping_idx;
|
|
block_off_t idx;
|
|
uint32_t crc;
|
|
block_off_t count_file;
|
|
block_off_t count_hardlink;
|
|
block_off_t count_symlink;
|
|
block_off_t count_dir;
|
|
block_off_t count_bad;
|
|
block_off_t count_rehash;
|
|
block_off_t count_unsynced;
|
|
block_off_t count_unscrubbed;
|
|
#if !HAVE_MT_WRITE
|
|
uint64_t start;
|
|
#endif
|
|
|
|
/* blocks of all array */
|
|
blockmax = parity_allocated_size(state);
|
|
|
|
/* preparation is the only extent-tree user until serialization starts */
|
|
fs_single_thread(state, 1);
|
|
|
|
/* check the file-system on all disks */
|
|
state_fscheck(state, "before write");
|
|
|
|
/*
|
|
* Clear the info for unused blocks
|
|
* and get some other info
|
|
*/
|
|
info_oldest = 0; /* oldest time in info */
|
|
info_now = time(0); /* get the present time */
|
|
info_has_rehash = 0; /* if there is a rehash info */
|
|
state_has_rebuild = 0;
|
|
count_unsynced = 0;
|
|
for (idx = 0; idx < blockmax; ++idx) {
|
|
int is_unsynced;
|
|
int has_deleted;
|
|
int has_rebuild;
|
|
|
|
/* if the position is used */
|
|
if (fs_position_analyze(state, idx, &is_unsynced, &has_deleted, &has_rebuild)) {
|
|
snapraid_info info = info_get(&state->infoarr, idx);
|
|
|
|
if (has_rebuild)
|
|
state_has_rebuild = 1;
|
|
|
|
if (is_unsynced)
|
|
++count_unsynced;
|
|
|
|
/* only if there is some info to store */
|
|
if (info) {
|
|
time_t info_time = info_get_time(info);
|
|
|
|
if (!info_oldest || info_time < info_oldest)
|
|
info_oldest = info_time;
|
|
|
|
if (info_get_rehash(info))
|
|
info_has_rehash = 1;
|
|
}
|
|
} else {
|
|
/* clear any previous info */
|
|
info_set(&state->infoarr, idx, 0);
|
|
|
|
/* and clear any deleted blocks, avoiding another scan in the common case */
|
|
if (has_deleted)
|
|
fs_position_clear_deleted(state, idx);
|
|
}
|
|
}
|
|
|
|
if (info_has_rehash && state->prevhash == HASH_UNDEFINED) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Rehash blocks found but previous checksum is missing!\n");
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* map disks */
|
|
mapping_idx = 0;
|
|
for (i = state->maplist; i != 0; i = i->next) {
|
|
struct snapraid_map* map = i->data;
|
|
struct snapraid_disk* disk;
|
|
|
|
/* find the disk for this mapping */
|
|
disk = find_disk_by_name(state, map->name);
|
|
if (!disk) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Unmapped disk '%s'\n", map->name);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* save the mapping only for not empty disks */
|
|
if (!fs_is_empty(disk, blockmax)) {
|
|
/* assign the mapping index used to identify disks */
|
|
disk->mapping_idx = mapping_idx;
|
|
++mapping_idx;
|
|
} else {
|
|
/* mark the disk as without mapping */
|
|
disk->mapping_idx = -1;
|
|
}
|
|
}
|
|
|
|
int content_version = 3;
|
|
|
|
/*
|
|
* Force at least version 3 as we want to always store the parity size.
|
|
* If there is a REBUILD block or a dealloc list, force version 4.
|
|
*/
|
|
if (state_has_rebuild) {
|
|
content_version = 4;
|
|
} else {
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
if (!tommy_list_empty(&disk->dealloclist)) {
|
|
content_version = 4;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
fs_single_thread(state, 0);
|
|
|
|
#if !HAVE_MT_WRITE
|
|
start = os_tick_ms();
|
|
#endif
|
|
|
|
#if HAVE_MT_WRITE
|
|
/* start all writing threads */
|
|
first = 1;
|
|
i = tommy_list_head(&state->contentlist);
|
|
while (i) {
|
|
struct snapraid_content* content = i->data;
|
|
struct state_write_thread_context* context;
|
|
char tmp[PATH_MAX];
|
|
STREAM* f;
|
|
|
|
msg_progress("Saving state to %s...\n", content->content);
|
|
log_tag("content_write:%s\n", esc_tag(content->content));
|
|
|
|
pathprint(tmp, sizeof(tmp), "%s.tmp", content->content);
|
|
|
|
/* ensure to delete a previous stale file */
|
|
if (remove(tmp) != 0) {
|
|
if (errno != ENOENT) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error removing the stale content file '%s'. %s.\n", tmp, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
f = sopen_write(tmp, CONTENT_WRITE_ADVISE | STREAM_FLAGS_CRC);
|
|
if (f == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error opening the temporary content file '%s'. %s.\n", tmp, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* allocate the thread context */
|
|
context = malloc_nofail(sizeof(struct state_write_thread_context));
|
|
content->context = context;
|
|
|
|
/* initialize */
|
|
context->state = state;
|
|
context->content_version = content_version;
|
|
context->blockmax = blockmax;
|
|
context->info_oldest = info_oldest;
|
|
context->info_now = info_now;
|
|
context->info_has_rehash = info_has_rehash;
|
|
context->count_unsynced = count_unsynced;
|
|
context->f = f;
|
|
context->first = first;
|
|
context->content = content->content;
|
|
first = 0;
|
|
|
|
thread_create(&context->thread, state_write_thread, context);
|
|
|
|
i = i->next;
|
|
}
|
|
|
|
/* join all threads before starting the durable finalization */
|
|
fail = 0;
|
|
i = tommy_list_head(&state->contentlist);
|
|
while (i) {
|
|
struct snapraid_content* content = i->data;
|
|
struct state_write_thread_context* context = content->context;
|
|
void* retval;
|
|
|
|
thread_join(context->thread, &retval);
|
|
|
|
if (retval) {
|
|
/* LCOV_EXCL_START */
|
|
fail = 1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
i = i->next;
|
|
}
|
|
|
|
/* abort on failure before finalizing any temporary content file */
|
|
if (fail) {
|
|
/* LCOV_EXCL_START */
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* All temporary content files of this publication use the same modification time.
|
|
* SnapRAID guarantees that two state_write() publications cannot occur in the
|
|
* same second due to synchronization delays and the overall execution model.
|
|
*/
|
|
content_mtime = time(0);
|
|
|
|
first = 1;
|
|
crc = 0;
|
|
count_file = 0;
|
|
count_hardlink = 0;
|
|
count_symlink = 0;
|
|
count_dir = 0;
|
|
count_bad = 0;
|
|
count_rehash = 0;
|
|
count_unscrubbed = 0;
|
|
|
|
/* make all temporary content files durable only after all writers have terminated */
|
|
i = tommy_list_head(&state->contentlist);
|
|
while (i) {
|
|
struct snapraid_content* content = i->data;
|
|
struct state_write_thread_context* context = content->context;
|
|
STREAM* f = context->f;
|
|
|
|
/*
|
|
* Use the sequence fflush() -> fsync() -> fclose() -> rename() to ensure
|
|
* than even in a system crash event we have one valid copy of the file.
|
|
*/
|
|
if (sflush(f) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s', in flush(). %s.\n", serrorfile(f), strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (smtime(f, content_mtime, 0) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error setting the modification time of the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
#if HAVE_FSYNC
|
|
if (ssync(f) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s' in sync(). %s.\n", serrorfile(f), strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
#endif
|
|
|
|
if (sclose(f) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error closing the content file. %s.\n", strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (first) {
|
|
first = 0;
|
|
crc = context->crc;
|
|
count_file = context->count_file;
|
|
count_hardlink = context->count_hardlink;
|
|
count_symlink = context->count_symlink;
|
|
count_dir = context->count_dir;
|
|
count_bad = context->count_bad;
|
|
count_rehash = context->count_rehash;
|
|
count_unsynced = context->count_unsynced;
|
|
count_unscrubbed = context->count_unscrubbed;
|
|
} else {
|
|
if (crc != context->crc) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ECONTENT, "Different CRCs writing content streams.\n");
|
|
log_fatal(ECONTENT, "DANGER! Your RAM memory is broken! DO NOT PROCEED UNTIL FIXED!\n");
|
|
log_fatal(ECONTENT, "Try running a memory test like http://www.memtest86.com/\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
free(context);
|
|
|
|
i = i->next;
|
|
}
|
|
#else
|
|
/* count the content files */
|
|
count_content = 0;
|
|
i = tommy_list_head(&state->contentlist);
|
|
while (i) {
|
|
struct snapraid_content* content = i->data;
|
|
msg_progress("Saving state to %s...\n", content->content);
|
|
++count_content;
|
|
i = i->next;
|
|
}
|
|
|
|
/* open all the content files */
|
|
f = sopen_multi_write(count_content, CONTENT_WRITE_ADVISE | STREAM_FLAGS_CRC);
|
|
if (!f) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error opening the content files.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
k = 0;
|
|
i = tommy_list_head(&state->contentlist);
|
|
while (i) {
|
|
struct snapraid_content* content = i->data;
|
|
char tmp[PATH_MAX];
|
|
pathprint(tmp, sizeof(tmp), "%s.tmp", content->content);
|
|
|
|
/* ensure to delete a previous stale file */
|
|
if (remove(tmp) != 0) {
|
|
if (errno != ENOENT) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error removing the stale content file '%s'. %s.\n", tmp, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
if (sopen_multi_file(f, k, tmp) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error opening the temporary content file '%s'. %s.\n", tmp, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
++k;
|
|
i = i->next;
|
|
}
|
|
|
|
/* allocate the thread context */
|
|
context = malloc_nofail(sizeof(struct state_write_thread_context));
|
|
|
|
/* initialize */
|
|
context->state = state;
|
|
context->content_version = content_version;
|
|
context->blockmax = blockmax;
|
|
context->info_oldest = info_oldest;
|
|
context->info_now = info_now;
|
|
context->info_has_rehash = info_has_rehash;
|
|
context->count_unsynced = count_unsynced;
|
|
context->f = f;
|
|
context->first = 1;
|
|
|
|
/* this serializer is the only extent-tree user until it completes */
|
|
fs_single_thread(state, 1);
|
|
retval = state_write_thread(context);
|
|
fs_single_thread(state, 0);
|
|
|
|
/* abort on failure */
|
|
if (retval) {
|
|
/* LCOV_EXCL_START */
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* All temporary content files of this publication use the same modification time.
|
|
* SnapRAID guarantees that two state_write() publications cannot occur in the
|
|
* same second due to synchronization delays and the overall execution model.
|
|
*/
|
|
content_mtime = time(0);
|
|
|
|
/*
|
|
* Use the sequence fflush() -> fsync() -> fclose() -> rename() to ensure
|
|
* than even in a system crash event we have one valid copy of the file.
|
|
*/
|
|
if (sflush(f) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s', in flush(). %s.\n", serrorfile(f), strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (smtime(f, content_mtime, 0) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error setting the modification time of the content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
#if HAVE_FSYNC
|
|
if (ssync(f) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing the content file '%s' in sync(). %s.\n", serrorfile(f), strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
#endif
|
|
|
|
if (sclose(f) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error closing the content file. %s.\n", strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
msg_progress("Saved state in %" PRIu64 " seconds\n", (os_tick_ms() - start) / 1000);
|
|
|
|
crc = context->crc;
|
|
count_file = context->count_file;
|
|
count_hardlink = context->count_hardlink;
|
|
count_symlink = context->count_symlink;
|
|
count_dir = context->count_dir;
|
|
count_bad = context->count_bad;
|
|
count_rehash = context->count_rehash;
|
|
count_unsynced = context->count_unsynced;
|
|
count_unscrubbed = context->count_unscrubbed;
|
|
|
|
free(context);
|
|
#endif
|
|
|
|
msg_verbose("%8" PRIu64 " files\n", count_file);
|
|
msg_verbose("%8" PRIu64 " hardlinks\n", count_hardlink);
|
|
msg_verbose("%8" PRIu64 " symlinks\n", count_symlink);
|
|
msg_verbose("%8" PRIu64 " empty dirs\n", count_dir);
|
|
|
|
log_tag("content_info:file:%" PRIu64 "\n", count_file);
|
|
log_tag("content_info:hardlink:%" PRIu64 "\n", count_hardlink);
|
|
log_tag("content_info:symlink:%" PRIu64 "\n", count_symlink);
|
|
log_tag("content_info:dir_empty:%" PRIu64 "\n", count_dir);
|
|
|
|
log_tag("content_info:block:%" PRIu64 "\n", blockmax);
|
|
log_tag("content_info:block_bad:%" PRIu64 "\n", count_bad);
|
|
log_tag("content_info:block_rehash:%" PRIu64 "\n", count_rehash);
|
|
log_tag("content_info:block_unsynced:%" PRIu64 "\n", count_unsynced);
|
|
log_tag("content_info:block_unscrubbed:%" PRIu64 "\n", count_unscrubbed);
|
|
|
|
/* store the blocks counters */
|
|
state->rehash_blocks = count_rehash;
|
|
state->bad_blocks = count_bad;
|
|
state->unsynced_blocks = count_unsynced;
|
|
state->unscrubbed_blocks = count_unscrubbed;
|
|
|
|
*out_crc = crc;
|
|
}
|
|
|
|
void state_probe(struct snapraid_state* state)
|
|
{
|
|
char path[PATH_MAX];
|
|
struct stat st;
|
|
tommy_node* node;
|
|
|
|
/* iterate over all the available content files and check the first one present */
|
|
node = tommy_list_head(&state->contentlist);
|
|
while (node) {
|
|
struct snapraid_content* content = node->data;
|
|
|
|
pathcpy(path, sizeof(path), content->content);
|
|
|
|
if (stat(path, &st) == 0) {
|
|
/* if found stop the search */
|
|
break;
|
|
}
|
|
|
|
/* next content file */
|
|
node = node->next;
|
|
}
|
|
|
|
/* if not found, assume empty */
|
|
if (!node)
|
|
return;
|
|
|
|
if (!state->no_conf) {
|
|
/* intentionally don't output the content path because it's not really read */
|
|
log_tag("content_info:probe_unixtime:%" PRId64 "\n", (int64_t)st.st_mtime);
|
|
log_flush();
|
|
}
|
|
}
|
|
|
|
void state_read(struct snapraid_state* state)
|
|
{
|
|
STREAM* f;
|
|
char path[PATH_MAX];
|
|
struct stat st;
|
|
tommy_node* node;
|
|
int ret;
|
|
int c;
|
|
uint64_t start;
|
|
|
|
start = os_tick_ms();
|
|
|
|
/*
|
|
* Iterate over all the available content files and load the first one present.
|
|
*
|
|
* Only missing files (ENOENT) trigger automatic failover to the next replica.
|
|
* If a file exists but is corrupted or damaged, reading will fail-fast with
|
|
* exit(EXIT_FAILURE) to alert the user to possible hardware or filesystem corruption.
|
|
* Once the user renames or removes the broken copy, the next run will fall back
|
|
* to the healthy replica and automatically re-create the missing one.
|
|
*/
|
|
f = 0;
|
|
node = tommy_list_head(&state->contentlist);
|
|
while (node) {
|
|
struct snapraid_content* content = node->data;
|
|
pathcpy(path, sizeof(path), content->content);
|
|
|
|
msg_progress("Loading state from %s...\n", path);
|
|
|
|
f = sopen_read(path, ADVISE_SEQUENTIAL | STREAM_FLAGS_CRC);
|
|
if (f != 0) {
|
|
/* if found stop the search */
|
|
break;
|
|
} else {
|
|
/* if it's real error of an existing file, abort */
|
|
if (errno != ENOENT) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error opening the content file '%s'. %s.\n", path, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* otherwise continue */
|
|
if (node->next) {
|
|
log_error(errno, "WARNING! Content file '%s' not found, attempting with another copy...\n", path);
|
|
|
|
/* ensure to rewrite all the content files */
|
|
state->need_write = 1;
|
|
}
|
|
}
|
|
|
|
/* next content file */
|
|
node = node->next;
|
|
}
|
|
|
|
/* if not found, assume empty */
|
|
if (!f) {
|
|
log_info(EUSER, "No content file found. Assuming empty.\n");
|
|
|
|
/* create the initial mapping */
|
|
state_map(state);
|
|
return;
|
|
}
|
|
|
|
/* get the stat of the content file */
|
|
ret = fstat(shandle(f), &st);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error stating the content file '%s'. %s.\n", path, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* go further to check other content files */
|
|
while (node) {
|
|
char other_path[PATH_MAX];
|
|
struct stat other_st;
|
|
struct snapraid_content* content = node->data;
|
|
pathcpy(other_path, sizeof(other_path), content->content);
|
|
|
|
ret = stat(other_path, &other_st);
|
|
if (ret != 0) {
|
|
/* allow missing content files, but not any other kind of error */
|
|
if (errno != ENOENT) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error stating the content file '%s'. %s.\n", other_path, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* ensure to rewrite all the content files */
|
|
state->need_write = 1;
|
|
} else {
|
|
/* different size or modification time means that the replicas are not from the same publication */
|
|
if (other_st.st_size != st.st_size || other_st.st_mtime != st.st_mtime) {
|
|
log_error(ECONTENT, "WARNING! Content files '%s' and '%s' have a different size or modification time!\n", path, other_path);
|
|
log_error(ECONTENT, "Likely the content replicas are from different publications or one of them is broken!\n");
|
|
|
|
/* ensure to rewrite all the content files */
|
|
state->need_write = 1;
|
|
}
|
|
}
|
|
|
|
/* next content file */
|
|
node = node->next;
|
|
}
|
|
|
|
/*
|
|
* Start with a undefined default.
|
|
* it's for compatibility with version 1.0 where MD5 was implicit.
|
|
*/
|
|
state->hash = HASH_UNDEFINED;
|
|
|
|
/* start with a zero seed, it was the default in old versions */
|
|
memset(state->hashseed, 0, HASH_MAX);
|
|
|
|
/* previous hash, start with an undefined value */
|
|
state->prevhash = HASH_UNDEFINED;
|
|
|
|
/* intentionally not set the prevhashseed, if used valgrind will warn about it */
|
|
|
|
/* get the first char to detect the file type */
|
|
c = sgetc(f);
|
|
sungetc(c, f);
|
|
|
|
/* guess the file type from the first char */
|
|
if (c == 'S') {
|
|
/* parsing is the only user of the extent trees until the content is loaded */
|
|
fs_single_thread(state, 1);
|
|
state_read_content(state, path, f);
|
|
fs_single_thread(state, 0);
|
|
} else {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "From SnapRAID v9.0 the text content file is not supported anymore.\n");
|
|
log_fatal(EUSER, "You have first to upgrade to SnapRAID v8.1 to convert it to binary format.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
sclose(f);
|
|
|
|
if (state->hash == HASH_UNDEFINED) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "The checksum to use is not specified.\n");
|
|
log_fatal(EUSER, "This happens because you are likely upgrading from SnapRAID 1.0.\n");
|
|
log_fatal(EUSER, "To use a new SnapRAID you must restart from scratch,\n");
|
|
log_fatal(EUSER, "deleting all the content and parity files.\n");
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
msg_progress("Loaded state in %" PRIu64 " seconds\n", (os_tick_ms() - start) / 1000);
|
|
|
|
if (state->unsynced_blocks)
|
|
msg_progress("WARNING! The latest sync was interrupted!\n");
|
|
|
|
/* update the mapping */
|
|
state_map(state);
|
|
|
|
state_content_check(state, path);
|
|
|
|
if (!state->no_conf) {
|
|
log_tag("content:%s\n", esc_tag(path));
|
|
log_tag("content_info:read_unixtime:%" PRId64 "\n", (int64_t)st.st_mtime);
|
|
log_flush();
|
|
}
|
|
|
|
/* mark that we read the content file, and it passed all the checks */
|
|
state->checked_read = 1;
|
|
}
|
|
|
|
struct state_verify_thread_context {
|
|
struct snapraid_state* state;
|
|
struct snapraid_content* content;
|
|
#if HAVE_MT_VERIFY
|
|
thread_id_t thread;
|
|
#else
|
|
void* retval;
|
|
#endif
|
|
/* input */
|
|
uint32_t crc;
|
|
STREAM* f;
|
|
};
|
|
|
|
static void* state_verify_thread(void* arg)
|
|
{
|
|
struct state_verify_thread_context* context = arg;
|
|
struct snapraid_content* content = context->content;
|
|
STREAM* f = context->f;
|
|
unsigned char buf[4];
|
|
uint32_t crc_stored;
|
|
uint32_t crc_computed;
|
|
uint64_t start;
|
|
|
|
start = os_tick_ms();
|
|
|
|
if (sdeplete(f, buf) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Failed to flush content file '%s'. %s.\n", serrorfile(f), strerror(errno));
|
|
return context;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* get the stored crc from the last four bytes */
|
|
crc_stored = buf[0] | (uint32_t)buf[1] << 8 | (uint32_t)buf[2] << 16 | (uint32_t)buf[3] << 24;
|
|
|
|
if (crc_stored != context->crc) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ECONTENT, "DANGER! Wrong stored CRC in '%s'\n", serrorfile(f));
|
|
return context;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* get the computed crc */
|
|
crc_computed = scrc(f);
|
|
|
|
/* adjust the stored crc to include itself */
|
|
crc_stored = crc32c(crc_stored, buf, 4);
|
|
|
|
if (crc_computed != crc_stored) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(ECONTENT, "DANGER! Wrong file CRC in '%s'\n", serrorfile(f));
|
|
return context;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
msg_progress("Verified %s in %" PRIu64 " seconds\n", content->content, (os_tick_ms() - start) / 1000);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void state_verify_content(struct snapraid_state* state, uint32_t crc)
|
|
{
|
|
tommy_node* i;
|
|
int fail;
|
|
|
|
msg_progress("Verifying...\n");
|
|
|
|
/* start all reading threads */
|
|
i = tommy_list_head(&state->contentlist);
|
|
while (i) {
|
|
struct snapraid_content* content = i->data;
|
|
struct state_verify_thread_context* context;
|
|
char tmp[PATH_MAX];
|
|
STREAM* f;
|
|
|
|
pathprint(tmp, sizeof(tmp), "%s.tmp", content->content);
|
|
f = sopen_read(tmp, ADVISE_SEQUENTIAL | STREAM_FLAGS_CRC);
|
|
if (f == 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error reopening the temporary content file '%s'. %s.\n", tmp, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* allocate the thread context */
|
|
context = malloc_nofail(sizeof(struct state_verify_thread_context));
|
|
content->context = context;
|
|
|
|
/* initialize */
|
|
context->state = state;
|
|
context->content = content;
|
|
context->crc = crc;
|
|
context->f = f;
|
|
|
|
#if HAVE_MT_VERIFY
|
|
thread_create(&context->thread, state_verify_thread, context);
|
|
#else
|
|
context->retval = state_verify_thread(context);
|
|
#endif
|
|
|
|
i = i->next;
|
|
}
|
|
|
|
/* join all thread */
|
|
fail = 0;
|
|
i = tommy_list_head(&state->contentlist);
|
|
while (i) {
|
|
struct snapraid_content* content = i->data;
|
|
struct state_verify_thread_context* context = content->context;
|
|
void* retval;
|
|
|
|
#if HAVE_MT_VERIFY
|
|
thread_join(context->thread, &retval);
|
|
#else
|
|
retval = context->retval;
|
|
#endif
|
|
if (retval) {
|
|
/* LCOV_EXCL_START */
|
|
fail = 1;
|
|
/* LCOV_EXCL_STOP */
|
|
} else {
|
|
STREAM* f = context->f;
|
|
|
|
if (sclose(f) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error closing the content file. %s.\n", strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
free(context);
|
|
|
|
i = i->next;
|
|
}
|
|
|
|
/* abort on failure */
|
|
if (fail) {
|
|
/* LCOV_EXCL_START */
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
static void state_rename_content(struct snapraid_state* state)
|
|
{
|
|
tommy_node* i;
|
|
|
|
#ifndef _WIN32
|
|
/*
|
|
* This sequence relies on POSIX file-system semantics.
|
|
*
|
|
* The temporary content file has already been flushed and fsync'ed in
|
|
* ssync() (using F_FULLFSYNC on macOS to force a physical media flush).
|
|
* rename()/renameat() atomically replaces the old content file, but the
|
|
* directory entry update is not necessarily durable when the rename
|
|
* returns.
|
|
*
|
|
* Open and fsync the parent directory after the rename to ensure that the
|
|
* new content file name is persisted before state_write() returns. This
|
|
* ordering is required for crash consistency because parity modifications
|
|
* may start only after the pre-sync content state has been durably
|
|
* published.
|
|
*
|
|
* On macOS, standard fsync() only pushes data from kernel buffers to the
|
|
* drive's volatile cache without requesting a physical media flush. F_FULLFSYNC
|
|
* forces the drive controller to commit all cached data to permanent storage.
|
|
* Because F_FULLFSYNC is not supported on directory descriptors, the subsequent
|
|
* directory fsync() only flushes to the drive cache and is not guaranteed to be
|
|
* instantly persistent across power loss. However, major database engines such
|
|
* as SQLite and PostgreSQL follow this exact same pattern on Darwin, making it
|
|
* the standard industry practice.
|
|
*
|
|
* See:
|
|
* https://github.com/sqlite/sqlite/blob/f3b9f74d81132426dee1ccc07a67fdad2ccfeaa9/src/os_unix.c#L3928-L3953
|
|
* https://github.com/postgres/postgres/blob/74c77052bc20017076100f0649ca0f44b1b98c29/src/backend/storage/file/fd.c#L835-L852
|
|
*
|
|
* When renameat() is available, perform the rename relative to the same
|
|
* directory descriptor that is subsequently fsync'ed. Use O_DIRECTORY
|
|
* when available, while retaining compatibility with POSIX systems where
|
|
* O_DIRECTORY is not defined.
|
|
*/
|
|
i = tommy_list_head(&state->contentlist);
|
|
while (i) {
|
|
struct snapraid_content* content = i->data;
|
|
char tmp[PATH_MAX];
|
|
char dir[PATH_MAX];
|
|
char* slash;
|
|
int handle;
|
|
|
|
pathcpy(dir, sizeof(dir), content->content);
|
|
|
|
slash = strrchr(dir, '/');
|
|
if (slash)
|
|
slash[1] = 0;
|
|
else
|
|
pathcpy(dir, sizeof(dir), ".");
|
|
|
|
int flags = O_RDONLY;
|
|
#ifdef O_DIRECTORY
|
|
flags |= O_DIRECTORY;
|
|
#endif
|
|
|
|
/* open the directory to get the handle */
|
|
handle = open(dir, flags);
|
|
if (handle < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error opening the directory '%s'. %s.\n", dir, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* now rename the just written copy with the correct name */
|
|
pathprint(tmp, sizeof(tmp), "%s.tmp", content->content);
|
|
#if HAVE_RENAMEAT
|
|
const char* content_name = slash ? content->content + (slash - dir) + 1 : content->content;
|
|
char tmp_name[PATH_MAX];
|
|
|
|
pathprint(tmp_name, sizeof(tmp_name), "%s.tmp", content_name);
|
|
if (renameat(handle, tmp_name, handle, content_name) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error renaming the content file '%s' to '%s'. %s.\n", tmp, content->content, strerror(errno));
|
|
close(handle);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
#else
|
|
if (rename(tmp, content->content) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error renaming the content file '%s' to '%s'. %s.\n", tmp, content->content, strerror(errno));
|
|
close(handle);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
#endif
|
|
|
|
/* sync the directory */
|
|
if (fsync(handle) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error syncing the directory '%s'. %s.\n", dir, strerror(errno));
|
|
close(handle);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (close(handle) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error closing the directory '%s'. %s.\n", dir, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
i = i->next;
|
|
}
|
|
#else
|
|
i = tommy_list_head(&state->contentlist);
|
|
while (i) {
|
|
struct snapraid_content* content = i->data;
|
|
char tmp[PATH_MAX];
|
|
|
|
/* now renames the just written copy with the correct name */
|
|
pathprint(tmp, sizeof(tmp), "%s.tmp", content->content);
|
|
if (rename(tmp, content->content) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error renaming the content file '%s' to '%s' in rename(). %s.\n", tmp, content->content, strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
i = i->next;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* Check if a modification time nanosecond value can be represented in the
|
|
* content format.
|
|
*
|
|
* STAT_NSEC_INVALID is encoded as 0. Valid nanoseconds in the range
|
|
* 0..999999999 are encoded as 1..1000000000.
|
|
*/
|
|
static int state_write_mtime_nsec_valid(int mtime_nsec)
|
|
{
|
|
return mtime_nsec == STAT_NSEC_INVALID || (mtime_nsec >= 0 && mtime_nsec < 1000000000);
|
|
}
|
|
|
|
/*
|
|
* Validate invariants required by the persistent content format.
|
|
*
|
|
* state_write() must never serialize a state that state_read() would reject.
|
|
* In particular, some internal values use signed types or sentinel values but
|
|
* are stored in the content file using unsigned encodings. If such values are
|
|
* outside their valid domain, serializing them may produce a syntactically
|
|
* valid content file that cannot be read back.
|
|
*
|
|
* Keep this check limited to serialization invariants. More complex consistency
|
|
* checks on the filesystem and block mappings are performed separately by
|
|
* state_fscheck().
|
|
*/
|
|
static void state_write_check(struct snapraid_state* state)
|
|
{
|
|
tommy_node* i;
|
|
unsigned l;
|
|
unsigned s;
|
|
block_off_t blockmax;
|
|
|
|
if (state->block_size == 0) {
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Zero block size before state write!\n");
|
|
os_abort();
|
|
}
|
|
|
|
if (state->level == 0 || state->level > LEV_MAX) {
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid parity level count %u!\n", state->level);
|
|
os_abort();
|
|
}
|
|
|
|
blockmax = parity_allocated_size(state);
|
|
if (blockmax > BLOCK_MAX) {
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Parity size %" PRIu64 " is too large!\n", blockmax);
|
|
os_abort();
|
|
}
|
|
|
|
for (l = 0; l < state->level; ++l) {
|
|
if (state->parity[l].split_mac == 0 || state->parity[l].split_mac > SPLIT_MAX) {
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid parity split count %u!\n", state->parity[l].split_mac);
|
|
os_abort();
|
|
}
|
|
|
|
for (s = 0; s < state->parity[l].split_mac; ++s) {
|
|
if (state->parity[l].split_map[s].size < 0) {
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid parity split size %" PRIi64 "!\n", state->parity[l].split_map[s].size);
|
|
os_abort();
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
tommy_node* j;
|
|
|
|
for (j = disk->filelist; j != 0; j = j->next) {
|
|
struct snapraid_file* file = j->data;
|
|
|
|
if (file->size < 0) {
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid file size %" PRIi64 "!\n", file->size);
|
|
os_abort();
|
|
}
|
|
|
|
if (!state_write_mtime_nsec_valid(file->mtime_nsec)) {
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid file mtime nanoseconds %d!\n", file->mtime_nsec);
|
|
os_abort();
|
|
}
|
|
}
|
|
|
|
for (j = disk->dealloclist; j != 0; j = j->next) {
|
|
struct snapraid_dealloc* dealloc = j->data;
|
|
|
|
if (dealloc->size < 0) {
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid deallocated file size %" PRIi64 "!\n", dealloc->size);
|
|
os_abort();
|
|
}
|
|
|
|
if (!state_write_mtime_nsec_valid(dealloc->mtime_nsec)) {
|
|
log_fatal(EINTERNAL, "Internal inconsistency: Invalid deallocated file mtime nanoseconds %d!\n", dealloc->mtime_nsec);
|
|
os_abort();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void state_write(struct snapraid_state* state)
|
|
{
|
|
uint32_t crc;
|
|
time_t now;
|
|
|
|
/* verify that all persistent values are serializable */
|
|
state_write_check(state);
|
|
|
|
/* write all the content files */
|
|
state_write_content(state, &crc);
|
|
|
|
/* verify the just written files */
|
|
state_verify_content(state, crc);
|
|
|
|
/* rename the new files, over the old ones */
|
|
state_rename_content(state);
|
|
|
|
/* log the write time of the content file */
|
|
now = time(0);
|
|
log_tag("content_info:write_unixtime:%" PRId64 "\n", (int64_t)now);
|
|
log_flush();
|
|
|
|
state->need_write = 0; /* no write needed anymore */
|
|
state->checked_read = 0; /* what we wrote is not checked in read */
|
|
state->written = 1;
|
|
}
|
|
|
|
void state_commit(struct snapraid_state* state)
|
|
{
|
|
tommy_node* i;
|
|
|
|
/* for each disk */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
if (!tommy_list_empty(&disk->dealloclist)) {
|
|
/* clear the dealloc list */
|
|
tommy_list_foreach(&disk->dealloclist, dealloc_free);
|
|
tommy_list_init(&disk->dealloclist);
|
|
|
|
/* mark the state as needing write */
|
|
state->need_write = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
void state_skip(struct snapraid_state* state)
|
|
{
|
|
tommy_node* i;
|
|
|
|
/* for each disk */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
tommy_node* j;
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
if (!disk->skip_access)
|
|
continue;
|
|
|
|
/* for each file */
|
|
for (j = tommy_list_head(&disk->filelist); j != 0; j = j->next) {
|
|
struct snapraid_file* file = j->data;
|
|
file_flag_set(file, FILE_IS_EXCLUDED);
|
|
}
|
|
|
|
/* for each link */
|
|
for (j = tommy_list_head(&disk->linklist); j != 0; j = j->next) {
|
|
struct snapraid_link* slink = j->data;
|
|
link_flag_set(slink, FILE_IS_EXCLUDED);
|
|
}
|
|
|
|
/* for each dir */
|
|
for (j = tommy_list_head(&disk->dirlist); j != 0; j = j->next) {
|
|
struct snapraid_dir* dir = j->data;
|
|
dir_flag_set(dir, FILE_IS_EXCLUDED);
|
|
}
|
|
}
|
|
}
|
|
|
|
void state_filter(struct snapraid_state* state, tommy_list* filterlist_file, tommy_list* filterlist_disk, int filter_missing, int filter_error)
|
|
{
|
|
tommy_node* i;
|
|
unsigned l;
|
|
|
|
/* if no filter, include all */
|
|
if (!filter_missing && !filter_error && tommy_list_empty(filterlist_file) && tommy_list_empty(filterlist_disk))
|
|
return;
|
|
|
|
msg_progress("Selecting...\n");
|
|
|
|
for (i = tommy_list_head(filterlist_disk); i != 0; i = i->next) {
|
|
struct snapraid_filter* filter = i->data;
|
|
msg_verbose("\t%s%s\n", filter->pattern, filter->is_disk ? "//" : "");
|
|
}
|
|
for (i = tommy_list_head(filterlist_file); i != 0; i = i->next) {
|
|
struct snapraid_filter* filter = i->data;
|
|
msg_verbose("\t%s%s\n", filter->pattern, filter->is_dir ? "/" : "");
|
|
}
|
|
if (filter_missing)
|
|
msg_verbose("\t<missing>\n");
|
|
if (filter_error)
|
|
msg_verbose("\t<error>\n");
|
|
|
|
/* for each disk */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
tommy_node* j;
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
/* if we filter for presence, we have to access the disk, so better to print something */
|
|
if (filter_missing)
|
|
msg_progress("Scanning disk %s...\n", disk->name);
|
|
|
|
/* for each file */
|
|
for (j = tommy_list_head(&disk->filelist); j != 0; j = j->next) {
|
|
struct snapraid_file* file = j->data;
|
|
|
|
if (filter_path(filterlist_disk, 0, disk->name, file->sub) != 0
|
|
|| filter_path(filterlist_file, 0, disk->name, file->sub) != 0
|
|
|| filter_existence(filter_missing, disk->dir, file->sub) != 0
|
|
|| filter_correctness(filter_error, &state->infoarr, disk, file) != 0
|
|
) {
|
|
file_flag_set(file, FILE_IS_EXCLUDED);
|
|
}
|
|
}
|
|
|
|
/* for each link */
|
|
for (j = tommy_list_head(&disk->linklist); j != 0; j = j->next) {
|
|
struct snapraid_link* slink = j->data;
|
|
|
|
if (filter_path(filterlist_disk, 0, disk->name, slink->sub) != 0
|
|
|| filter_path(filterlist_file, 0, disk->name, slink->sub) != 0
|
|
|| filter_existence(filter_missing, disk->dir, slink->sub) != 0
|
|
) {
|
|
link_flag_set(slink, FILE_IS_EXCLUDED);
|
|
}
|
|
}
|
|
|
|
/* for each empty dir */
|
|
for (j = tommy_list_head(&disk->dirlist); j != 0; j = j->next) {
|
|
struct snapraid_dir* dir = j->data;
|
|
|
|
if (filter_emptydir(filterlist_disk, 0, disk->name, dir->sub) != 0
|
|
|| filter_emptydir(filterlist_file, 0, disk->name, dir->sub) != 0
|
|
|| filter_existence(filter_missing, disk->dir, dir->sub) != 0
|
|
) {
|
|
dir_flag_set(dir, FILE_IS_EXCLUDED);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* if we are filtering by disk, exclude any parity not explicitly included */
|
|
if (!tommy_list_empty(filterlist_disk)) {
|
|
/* for each parity disk */
|
|
for (l = 0; l < state->level; ++l) {
|
|
/* check if the parity is excluded by name */
|
|
if (filter_path(filterlist_disk, 0, lev_config_name(l), 0) != 0) {
|
|
/* excluded the parity from further operation */
|
|
state->parity[l].is_excluded_by_filter = 1;
|
|
}
|
|
}
|
|
} else {
|
|
/* if we are filtering by file, exclude all parity */
|
|
if (filter_missing || !tommy_list_empty(filterlist_file)) {
|
|
/* for each parity disk */
|
|
for (l = 0; l < state->level; ++l) {
|
|
state->parity[l].is_excluded_by_filter = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int state_progress_begin(struct snapraid_state* state, block_off_t blockstart, block_off_t blockmax, block_off_t countmax)
|
|
{
|
|
time_t now;
|
|
|
|
if (state->opt.gui) {
|
|
log_tag("run:begin:%" PRIu64 ":%" PRIu64 ":%" PRIu64 "\n", blockstart, blockmax, countmax);
|
|
log_flush();
|
|
}
|
|
|
|
now = time(0);
|
|
|
|
state->progress_whole_start = now;
|
|
state->progress_tick = 0;
|
|
state->progress_ptr = 0;
|
|
state->progress_wasted = 0;
|
|
|
|
/* start of thermal control */
|
|
if (!state_thermal_begin(state, now))
|
|
return -1;
|
|
|
|
/* stop if requested */
|
|
if (os_signal_interrupt()) {
|
|
/* LCOV_EXCL_START */
|
|
if (!state->opt.gui) {
|
|
msg_status("Not starting due to interruption\n");
|
|
}
|
|
log_tag("sigint:0: SIGINT received\n");
|
|
log_flush();
|
|
return 1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void state_progress_end(struct snapraid_state* state, block_off_t countpos, block_off_t countmax, data_off_t countsize, const char* msg)
|
|
{
|
|
if (state->opt.gui) {
|
|
log_tag("run:end\n");
|
|
log_flush();
|
|
} else if (countmax == 0) {
|
|
if (state->need_write || state->written) {
|
|
msg_status("100%% completed\n");
|
|
} else {
|
|
msg_status("%s", msg);
|
|
}
|
|
} else {
|
|
time_t now;
|
|
time_t elapsed;
|
|
|
|
data_off_t countsize_MB = (countsize + MEGA - 1) / MEGA;
|
|
|
|
now = time(0);
|
|
|
|
elapsed = now - state->progress_whole_start - state->progress_wasted;
|
|
|
|
msg_bar("%u%% completed, %" PRIu64 " MB accessed", muldiv(countpos, 100, countmax), countsize_MB);
|
|
|
|
msg_bar(" in %u:%02u", (unsigned)(elapsed / 3600), (unsigned)((elapsed % 3600) / 60));
|
|
|
|
msg_bar("\n");
|
|
msg_flush();
|
|
}
|
|
}
|
|
|
|
void state_progress_stop(struct snapraid_state* state)
|
|
{
|
|
time_t now;
|
|
|
|
now = time(0);
|
|
|
|
state->progress_interruption = now;
|
|
}
|
|
|
|
void state_progress_restart(struct snapraid_state* state)
|
|
{
|
|
time_t now;
|
|
|
|
now = time(0);
|
|
|
|
/* reset the progress counter */
|
|
state->progress_tick = 0;
|
|
state->progress_ptr = 0;
|
|
|
|
if (now >= state->progress_interruption) /* avoid degenerated cases when the clock is manually adjusted */
|
|
state->progress_wasted += now - state->progress_interruption;
|
|
}
|
|
|
|
/**
|
|
* Set the latest os_tick data in the progress vector.
|
|
*/
|
|
static void state_progress_latest(struct snapraid_state* state)
|
|
{
|
|
tommy_node* i;
|
|
unsigned l;
|
|
|
|
state->progress_tick_misc[state->progress_ptr] = state->tick_misc;
|
|
state->progress_tick_sched[state->progress_ptr] = state->tick_sched;
|
|
state->progress_tick_raid[state->progress_ptr] = state->tick_raid;
|
|
state->progress_tick_hash[state->progress_ptr] = state->tick_hash;
|
|
state->progress_tick_io[state->progress_ptr] = state->tick_io;
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
disk->progress_tick[state->progress_ptr] = disk->tick;
|
|
}
|
|
for (l = 0; l < state->level; ++l)
|
|
state->parity[l].progress_tick[state->progress_ptr] = state->parity[l].tick;
|
|
}
|
|
|
|
/**
|
|
* Number of columns
|
|
*/
|
|
#define GRAPH_COLUMN 78
|
|
|
|
/**
|
|
* Get the reference value, 0 if index is PROGRESS_MAX
|
|
*/
|
|
#define ref(map, index) (index < PROGRESS_MAX ? map[index] : 0)
|
|
|
|
static struct snapraid_thermal* state_thermal_find(struct snapraid_state* state, const char* name)
|
|
{
|
|
tommy_node* i;
|
|
struct snapraid_thermal* found = 0;
|
|
|
|
for (i = tommy_list_head(&state->thermallist); i != 0; i = i->next) {
|
|
struct snapraid_thermal* thermal = i->data;
|
|
if (strcmp(thermal->name, name) == 0) {
|
|
if (found == 0
|
|
/* if multiple matches, return the one with highest temperature */
|
|
|| found->latest_temperature < thermal->latest_temperature
|
|
/* or, if the temperature is equal, the one with the higher r_squared */
|
|
|| (found->latest_temperature == thermal->latest_temperature && found->params.r_squared < thermal->params.r_squared)
|
|
) {
|
|
found = thermal;
|
|
}
|
|
}
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
#define THERMAL_PAD " "
|
|
|
|
static void state_progress_graph(struct snapraid_state* state, struct snapraid_io* io, unsigned current, unsigned oldest)
|
|
{
|
|
uint64_t v;
|
|
uint64_t tick_total;
|
|
size_t bar;
|
|
tommy_node* i;
|
|
unsigned l;
|
|
size_t pad;
|
|
size_t pre;
|
|
|
|
tick_total = 0;
|
|
|
|
tick_total += state->progress_tick_misc[current] - ref(state->progress_tick_misc, oldest);
|
|
tick_total += state->progress_tick_sched[current] - ref(state->progress_tick_sched, oldest);
|
|
tick_total += state->progress_tick_raid[current] - ref(state->progress_tick_raid, oldest);
|
|
tick_total += state->progress_tick_hash[current] - ref(state->progress_tick_hash, oldest);
|
|
tick_total += state->progress_tick_io[current] - ref(state->progress_tick_io, oldest);
|
|
if (!tick_total)
|
|
return;
|
|
|
|
pad = 4;
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
size_t len = strlen(disk->name);
|
|
if (pad < len)
|
|
pad = len;
|
|
}
|
|
for (l = 0; l < state->level; ++l) {
|
|
size_t len = strlen(lev_config_name(l));
|
|
if (pad < len)
|
|
pad = len;
|
|
}
|
|
|
|
/* extra space */
|
|
pad += 1;
|
|
|
|
pre = 4;
|
|
if (pad + pre + 30 < GRAPH_COLUMN)
|
|
bar = GRAPH_COLUMN - pad - pre;
|
|
else
|
|
bar = 30; /* at least a bar of 30 */
|
|
|
|
if (io) {
|
|
const char* legend = "cached blocks (instant, more is better)";
|
|
|
|
/* refresh the cached blocks info */
|
|
io_refresh(io);
|
|
|
|
printf("\n");
|
|
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
v = disk->cached_blocks;
|
|
printr(disk->name, pad);
|
|
printf("%4" PRIu64 " | ", v);
|
|
|
|
if (disk->progress_file && disk->progress_file->sub)
|
|
printf("%s", disk->progress_file->sub);
|
|
else
|
|
printf("-");
|
|
|
|
printf("\n");
|
|
}
|
|
|
|
for (l = 0; l < state->level; ++l) {
|
|
v = state->parity[l].cached_blocks;
|
|
printr(lev_config_name(l), pad);
|
|
printf("%4" PRIu64 " | ", v);
|
|
printc('o', v * bar / io->io_max);
|
|
printf("\n");
|
|
}
|
|
|
|
printc(' ', pad);
|
|
printf(" |_");
|
|
printc('_', bar);
|
|
printf("\n");
|
|
|
|
printc(' ', 5 + pad + 1 + bar / 2 - strlen(legend) / 2);
|
|
printf("%s", legend);
|
|
printf("\n");
|
|
}
|
|
|
|
printf("\n");
|
|
|
|
pre = 4;
|
|
if (state->thermal_temperature_limit != 0)
|
|
pre += strlen(THERMAL_PAD);
|
|
if (pad + pre + 30 < GRAPH_COLUMN)
|
|
bar = GRAPH_COLUMN - pad - pre;
|
|
else
|
|
bar = 30; /* at least a bar of 30 */
|
|
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
v = disk->progress_tick[current] - ref(disk->progress_tick, oldest);
|
|
printr(disk->name, pad);
|
|
|
|
if (state->thermal_temperature_limit != 0) {
|
|
struct snapraid_thermal* thermal = state_thermal_find(state, disk->name);
|
|
if (thermal) {
|
|
if (thermal->params.r_squared >= THERMAL_R_SQUARED_LIMIT)
|
|
printf(" %2u (%2u)", thermal->latest_temperature, (int)thermal->params.t_steady);
|
|
else
|
|
printf(" %2u ", thermal->latest_temperature);
|
|
} else {
|
|
printf(THERMAL_PAD);
|
|
}
|
|
}
|
|
|
|
printf("%3u%% | ", muldiv(v, 100, tick_total));
|
|
printc('*', v * bar / tick_total);
|
|
printf("\n");
|
|
|
|
/* clear the file in progress */
|
|
disk->progress_file = 0;
|
|
}
|
|
|
|
for (l = 0; l < state->level; ++l) {
|
|
v = state->parity[l].progress_tick[current] - ref(state->parity[l].progress_tick, oldest);
|
|
printr(lev_config_name(l), pad);
|
|
|
|
if (state->thermal_temperature_limit != 0) {
|
|
struct snapraid_thermal* thermal = state_thermal_find(state, lev_config_name(l));
|
|
if (thermal) {
|
|
if (thermal->params.r_squared >= THERMAL_R_SQUARED_LIMIT)
|
|
printf(" %2u (%2u)", thermal->latest_temperature, (int)thermal->params.t_steady);
|
|
else
|
|
printf(" %2u ", thermal->latest_temperature);
|
|
} else {
|
|
printf(THERMAL_PAD);
|
|
}
|
|
}
|
|
|
|
printf("%3u%% | ", muldiv(v, 100, tick_total));
|
|
printc('*', v * bar / tick_total);
|
|
printf("\n");
|
|
}
|
|
|
|
v = state->progress_tick_raid[current] - ref(state->progress_tick_raid, oldest);
|
|
printr("raid", pad);
|
|
if (state->thermal_temperature_limit != 0)
|
|
printf(THERMAL_PAD);
|
|
printf("%3u%% | ", muldiv(v, 100, tick_total));
|
|
printc('*', v * bar / tick_total);
|
|
printf("\n");
|
|
|
|
v = state->progress_tick_hash[current] - ref(state->progress_tick_hash, oldest);
|
|
printr("hash", pad);
|
|
if (state->thermal_temperature_limit != 0)
|
|
printf(THERMAL_PAD);
|
|
printf("%3u%% | ", muldiv(v, 100, tick_total));
|
|
printc('*', v * bar / tick_total);
|
|
printf("\n");
|
|
|
|
v = state->progress_tick_sched[current] - ref(state->progress_tick_sched, oldest);
|
|
printr("sched", pad);
|
|
if (state->thermal_temperature_limit != 0)
|
|
printf(THERMAL_PAD);
|
|
printf("%3u%% | ", muldiv(v, 100, tick_total));
|
|
printc('*', v * bar / tick_total);
|
|
printf("\n");
|
|
|
|
v = state->progress_tick_misc[current] - ref(state->progress_tick_misc, oldest);
|
|
printr("misc", pad);
|
|
if (state->thermal_temperature_limit != 0)
|
|
printf(THERMAL_PAD);
|
|
printf("%3u%% | ", muldiv(v, 100, tick_total));
|
|
printc('*', v * bar / tick_total);
|
|
printf("\n");
|
|
|
|
printc(' ', pad);
|
|
if (state->thermal_temperature_limit != 0)
|
|
printf(THERMAL_PAD);
|
|
printf(" |_");
|
|
printc('_', bar);
|
|
printf("\n");
|
|
|
|
if (oldest == PROGRESS_MAX) {
|
|
const char* legend = "wait time (total, less is better)";
|
|
if (state->thermal_temperature_limit != 0)
|
|
printf(THERMAL_PAD);
|
|
printc(' ', 5 + pad + 1 + bar / 2 - strlen(legend) / 2);
|
|
printf("%s", legend);
|
|
printf("\n");
|
|
} else {
|
|
const char* legend_d = "wait time (last %d secs, less is better)";
|
|
if (state->thermal_temperature_limit != 0)
|
|
printf(THERMAL_PAD);
|
|
printc(' ', 5 + pad + 1 + bar / 2 - strlen(legend_d) / 2);
|
|
printf(legend_d, PROGRESS_MAX);
|
|
printf("\n");
|
|
}
|
|
|
|
printf("\n");
|
|
}
|
|
|
|
int state_progress(struct snapraid_state* state, struct snapraid_io* io, block_off_t blockpos, block_off_t countpos, block_off_t countmax, data_off_t countsize)
|
|
{
|
|
time_t now;
|
|
uint64_t thermal_now;
|
|
int pred;
|
|
|
|
now = time(0);
|
|
thermal_now = os_tick_sec();
|
|
|
|
/* thermal measure */
|
|
if (thermal_now > state->thermal_latest + THERMAL_PERIOD_SECONDS || state->opt.fake_device) {
|
|
state->thermal_latest = thermal_now;
|
|
state_thermal(state, now);
|
|
}
|
|
|
|
/* previous position */
|
|
pred = state->progress_ptr + PROGRESS_MAX - 1;
|
|
if (pred >= PROGRESS_MAX)
|
|
pred -= PROGRESS_MAX;
|
|
|
|
/* if the previous measure is different */
|
|
if (state->progress_tick == 0
|
|
|| state->progress_time[pred] != now
|
|
) {
|
|
time_t elapsed;
|
|
unsigned out_perc = 0;
|
|
unsigned out_size_speed = 0;
|
|
unsigned out_block_speed = 0;
|
|
unsigned out_cpu = 0;
|
|
unsigned out_eta = 0;
|
|
int out_temperature = 0;
|
|
int out_steady = 0;
|
|
int out_computed = 0;
|
|
|
|
/* store the new measure */
|
|
state->progress_time[state->progress_ptr] = now;
|
|
state->progress_pos[state->progress_ptr] = countpos;
|
|
state->progress_size[state->progress_ptr] = countsize;
|
|
state_progress_latest(state);
|
|
|
|
elapsed = now - state->progress_whole_start - state->progress_wasted;
|
|
|
|
/* completion percentage */
|
|
if (countmax)
|
|
out_perc = muldiv(countpos, 100, countmax);
|
|
|
|
/* if we have at least 5 measures */
|
|
if (state->progress_tick >= 5
|
|
/* or if we are running in test mode, with at least one measure */
|
|
|| (state->opt.force_progress && state->progress_tick >= 1)
|
|
) {
|
|
int oldest;
|
|
int past;
|
|
time_t delta_time;
|
|
block_off_t delta_pos;
|
|
data_off_t delta_size;
|
|
uint64_t tick_cpu;
|
|
uint64_t tick_total;
|
|
uint64_t delta_tick_cpu;
|
|
uint64_t delta_tick_total;
|
|
uint64_t oldest_tick_cpu;
|
|
uint64_t oldest_tick_total;
|
|
tommy_node* i;
|
|
|
|
/* number of past measures */
|
|
past = state->progress_tick;
|
|
|
|
/*
|
|
* Drop the oldest ones, to promptly
|
|
* skip the startup phase
|
|
*/
|
|
past -= past / 5;
|
|
|
|
/* check how much we can go in the past */
|
|
if (past >= PROGRESS_MAX - 1) {
|
|
/*
|
|
* The vector is filled, so we are already in position
|
|
* to get the possible oldest one
|
|
*/
|
|
oldest = state->progress_ptr + 1;
|
|
} else {
|
|
/* go backward the number of positions selected */
|
|
oldest = state->progress_ptr + PROGRESS_MAX - past;
|
|
}
|
|
if (oldest >= PROGRESS_MAX)
|
|
oldest -= PROGRESS_MAX;
|
|
|
|
tick_cpu = state->progress_tick_misc[state->progress_ptr]
|
|
+ state->progress_tick_sched[state->progress_ptr]
|
|
+ state->progress_tick_raid[state->progress_ptr]
|
|
+ state->progress_tick_hash[state->progress_ptr];
|
|
tick_total = tick_cpu + state->progress_tick_io[state->progress_ptr];
|
|
|
|
oldest_tick_cpu = state->progress_tick_misc[oldest]
|
|
+ state->progress_tick_sched[oldest]
|
|
+ state->progress_tick_raid[oldest]
|
|
+ state->progress_tick_hash[oldest];
|
|
oldest_tick_total = oldest_tick_cpu + state->progress_tick_io[oldest];
|
|
|
|
delta_time = now - state->progress_time[oldest];
|
|
delta_pos = countpos - state->progress_pos[oldest];
|
|
delta_size = countsize - state->progress_size[oldest];
|
|
delta_tick_cpu = tick_cpu - oldest_tick_cpu;
|
|
delta_tick_total = tick_total - oldest_tick_total;
|
|
|
|
/* estimate the speed in MB/s */
|
|
if (delta_time != 0)
|
|
out_size_speed = (unsigned)(delta_size / MEGA / delta_time);
|
|
|
|
/* estimate the speed in block/s */
|
|
if (delta_time != 0)
|
|
out_block_speed = (unsigned)(delta_pos / delta_time);
|
|
|
|
/* estimate the cpu usage percentage */
|
|
if (delta_tick_total != 0)
|
|
out_cpu = muldiv(delta_tick_cpu, 100, delta_tick_total);
|
|
|
|
/* estimate the remaining time in minutes */
|
|
if (delta_pos != 0)
|
|
out_eta = muldiv(countmax - countpos, delta_time, delta_pos);
|
|
|
|
if (state->opt.force_stats) {
|
|
os_clear();
|
|
state_progress_graph(state, io, state->progress_ptr, oldest);
|
|
}
|
|
|
|
if (state->thermal_temperature_limit != 0) {
|
|
/* get the max temperature */
|
|
out_temperature = 0;
|
|
for (i = tommy_list_head(&state->thermallist); i != 0; i = i->next) {
|
|
struct snapraid_thermal* thermal = i->data;
|
|
if (out_temperature < thermal->latest_temperature) {
|
|
out_temperature = thermal->latest_temperature;
|
|
if (thermal->params.r_squared >= THERMAL_R_SQUARED_LIMIT)
|
|
out_steady = thermal->params.t_steady;
|
|
else
|
|
out_steady = 0;
|
|
}
|
|
}
|
|
} else {
|
|
out_temperature = 0;
|
|
out_steady = 0;
|
|
}
|
|
|
|
/* we have the output value */
|
|
out_computed = 1;
|
|
}
|
|
|
|
if (state->opt.gui) {
|
|
char str_eta[32];
|
|
char str_size_speed[32];
|
|
char str_cpu[32];
|
|
char str_temp[32];
|
|
char str_steady[32];
|
|
if (out_computed) {
|
|
snprintf(str_eta, sizeof(str_eta), "%u", out_eta);
|
|
snprintf(str_size_speed, sizeof(str_size_speed), "%u", out_size_speed);
|
|
snprintf(str_cpu, sizeof(str_cpu), "%u", out_cpu);
|
|
} else {
|
|
str_eta[0] = 0;
|
|
str_size_speed[0] = 0;
|
|
str_cpu[0] = 0;
|
|
}
|
|
if (out_temperature) {
|
|
snprintf(str_temp, sizeof(str_temp), "%u", out_temperature);
|
|
} else {
|
|
str_temp[0] = 0;
|
|
}
|
|
if (out_steady) {
|
|
snprintf(str_steady, sizeof(str_steady), "%u", out_steady);
|
|
} else {
|
|
str_steady[0] = 0;
|
|
}
|
|
log_tag("run:pos:%" PRIu64 ":%" PRIu64 ":%" PRIu64 ":%u:%s:%s:%s:%" PRIu64 ":%s:%s\n", blockpos, countpos, countsize, out_perc, str_eta, str_size_speed, str_cpu, (uint64_t)elapsed, str_temp, str_steady);
|
|
log_flush();
|
|
} else {
|
|
msg_bar("%u%%, %u MB", out_perc, (unsigned)(countsize / MEGA));
|
|
if (out_computed) {
|
|
msg_bar(", %u MB/s", out_size_speed);
|
|
msg_bar(", %u stripe/s", out_block_speed);
|
|
msg_bar(", CPU %u%%", out_cpu);
|
|
if (out_temperature != 0) {
|
|
if (out_steady != 0)
|
|
msg_bar(", Tmax %u (%u)", out_temperature, out_steady);
|
|
else
|
|
msg_bar(", Tmax %u", out_temperature);
|
|
}
|
|
unsigned out_eta_minutes = out_eta / 60; /* minutes */
|
|
msg_bar(", %u:%02u ETA", out_eta_minutes / 60, out_eta_minutes % 60);
|
|
}
|
|
msg_bar("\r");
|
|
msg_flush();
|
|
}
|
|
|
|
/* next position to fill */
|
|
++state->progress_ptr;
|
|
if (state->progress_ptr >= PROGRESS_MAX)
|
|
state->progress_ptr -= PROGRESS_MAX;
|
|
|
|
/* one more measure */
|
|
++state->progress_tick;
|
|
}
|
|
|
|
/* stop if requested */
|
|
if (os_signal_interrupt()) {
|
|
/* LCOV_EXCL_START */
|
|
if (!state->opt.gui) {
|
|
msg_bar("\n");
|
|
msg_flush();
|
|
}
|
|
log_fatal(EUSER, "Stopping for interruption at block %" PRIu64 "\n", blockpos);
|
|
log_tag("sigint:%" PRIu64 ": SIGINT received\n", blockpos);
|
|
log_flush();
|
|
return 1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void state_usage_waste(struct snapraid_state* state)
|
|
{
|
|
uint64_t now = os_tick();
|
|
|
|
state->tick_last = now;
|
|
}
|
|
|
|
void state_usage_misc(struct snapraid_state* state)
|
|
{
|
|
uint64_t now = os_tick();
|
|
uint64_t delta = now - state->tick_last;
|
|
|
|
/* increment the time spent in computations */
|
|
state->tick_misc += delta;
|
|
|
|
state->tick_last = now;
|
|
}
|
|
|
|
void state_usage_sched(struct snapraid_state* state)
|
|
{
|
|
uint64_t now = os_tick();
|
|
uint64_t delta = now - state->tick_last;
|
|
|
|
/* increment the time spent in computations */
|
|
state->tick_sched += delta;
|
|
|
|
state->tick_last = now;
|
|
}
|
|
|
|
void state_usage_raid(struct snapraid_state* state)
|
|
{
|
|
uint64_t now = os_tick();
|
|
uint64_t delta = now - state->tick_last;
|
|
|
|
/* increment the time spent in computations */
|
|
state->tick_raid += delta;
|
|
|
|
state->tick_last = now;
|
|
}
|
|
|
|
void state_usage_hash(struct snapraid_state* state)
|
|
{
|
|
uint64_t now = os_tick();
|
|
uint64_t delta = now - state->tick_last;
|
|
|
|
/* increment the time spent in computations */
|
|
state->tick_hash += delta;
|
|
|
|
state->tick_last = now;
|
|
}
|
|
|
|
void state_usage_file(struct snapraid_state* state, struct snapraid_disk* disk, struct snapraid_file* file)
|
|
{
|
|
(void)state;
|
|
|
|
disk->progress_file = file;
|
|
}
|
|
|
|
void state_usage_disk(struct snapraid_state* state, struct snapraid_handle* handle_map, unsigned* waiting_map, unsigned waiting_mac)
|
|
{
|
|
uint64_t now = os_tick();
|
|
uint64_t delta = now - state->tick_last;
|
|
unsigned i;
|
|
|
|
/* increment the time spent in the data disks */
|
|
for (i = 0; i < waiting_mac; ++i) {
|
|
struct snapraid_disk* disk = handle_map[waiting_map[i]].disk;
|
|
|
|
if (!disk)
|
|
continue;
|
|
|
|
disk->tick += delta;
|
|
}
|
|
state->tick_io += delta;
|
|
|
|
state->tick_last = now;
|
|
}
|
|
|
|
void state_usage_parity(struct snapraid_state* state, unsigned* waiting_map, unsigned waiting_mac)
|
|
{
|
|
uint64_t now = os_tick();
|
|
uint64_t delta = now - state->tick_last;
|
|
unsigned i;
|
|
|
|
/* increment the time spent in the parity disk */
|
|
for (i = 0; i < waiting_mac; ++i)
|
|
state->parity[waiting_map[i]].tick += delta;
|
|
state->tick_io += delta;
|
|
|
|
state->tick_last = now;
|
|
}
|
|
|
|
void state_usage_print(struct snapraid_state* state)
|
|
{
|
|
/* set the latest data */
|
|
state_progress_latest(state);
|
|
|
|
if (msg_level < MSG_PROGRESS)
|
|
return;
|
|
|
|
/* print a graph for it */
|
|
state_progress_graph(state, 0, state->progress_ptr, PROGRESS_MAX);
|
|
}
|
|
|
|
void state_fscheck(struct snapraid_state* state, const char* ope)
|
|
{
|
|
tommy_node* i;
|
|
|
|
/* check the file-system on all disks */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
if (fs_check(disk) != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Internal inconsistency: File-system check for disk '%s' %s\n", disk->name, ope);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
|
|
void generate_configuration(const char* path)
|
|
{
|
|
struct snapraid_state state;
|
|
struct snapraid_content* content;
|
|
unsigned l, s;
|
|
tommy_node* j;
|
|
|
|
state_init(&state);
|
|
|
|
/* mark that we are without a configuration file */
|
|
state.no_conf = 1;
|
|
|
|
/* create the dummy content entry */
|
|
content = content_alloc(path, -1);
|
|
|
|
/* adds the content entry */
|
|
tommy_list_insert_tail(&state.contentlist, &content->node, content);
|
|
|
|
/* read the content file */
|
|
state_read(&state);
|
|
|
|
/* output a dummy configuration file */
|
|
printf("# Configuration file generated from %s\n", path);
|
|
printf("\n");
|
|
printf("# Use this blocksize\n");
|
|
printf("blocksize %u\n", state.block_size / KIBI);
|
|
printf("\n");
|
|
printf("# Use this hashsize\n");
|
|
printf("hashsize %zu\n", BLOCK_HASH_SIZE);
|
|
printf("\n");
|
|
for (l = 0; l < state.level; ++l) {
|
|
printf("# Set the correct path for the %s files\n", lev_name(l));
|
|
printf("# You had %u of them:\n", state.parity[l].split_mac);
|
|
for (s = 0; s < state.parity[l].split_mac; ++s) {
|
|
printf("# %u:\n", s);
|
|
printf("# PATH:");
|
|
if (state.parity[l].split_map[s].path[0])
|
|
printf("%s", state.parity[l].split_map[s].path);
|
|
else
|
|
printf("?");
|
|
printf("\n");
|
|
printf("# SIZE:");
|
|
if (state.parity[l].split_map[s].size != PARITY_SIZE_INVALID)
|
|
printf("%" PRIu64, state.parity[l].split_map[s].size);
|
|
else
|
|
printf("?");
|
|
printf("\n");
|
|
printf("# UUID:");
|
|
if (state.parity[l].split_map[s].uuid[0])
|
|
printf("%s", state.parity[l].split_map[s].uuid);
|
|
else
|
|
printf("?");
|
|
printf("\n");
|
|
printf("#\n");
|
|
}
|
|
printf("%s ENTER_HERE_THE_PARITY_FILES_COMMA_SEPARATED\n", lev_config_name(l));
|
|
printf("\n");
|
|
}
|
|
printf("# Add any other content file\n");
|
|
printf("content %s\n", path);
|
|
printf("\n");
|
|
for (j = state.maplist; j; j = j->next) {
|
|
struct snapraid_map* map = j->data;
|
|
struct snapraid_disk* disk;
|
|
printf("# Set the correct dir for disk '%s'\n", map->name);
|
|
if (map->content_uuid[0])
|
|
printf("# Disk '%s' is the one with id '%s'\n", map->name, map->content_uuid);
|
|
disk = find_disk_by_name(&state, map->name);
|
|
if (disk && disk->filelist) {
|
|
struct snapraid_file* file = disk->filelist->data;
|
|
if (file) {
|
|
printf("# and containing: %s\n", fmt_poll(disk, file->sub));
|
|
}
|
|
}
|
|
printf("data %s ENTER_HERE_THE_DIR\n", map->name);
|
|
printf("\n");
|
|
}
|
|
|
|
state_done(&state);
|
|
}
|
|
|
|
/**
|
|
* Establish a durable I/O barrier: drain asynchronous writes, collect their
|
|
* errors, sync parity only if all writes succeeded, then quiesce read-ahead.
|
|
* A failed writer invalidates the whole barrier because parity must not be
|
|
* treated as coherent, or recorded in content state, after a failed write.
|
|
*/
|
|
int state_barrier(struct snapraid_state* state, struct snapraid_io* io, struct snapraid_parity_handle* parity_handle, block_off_t blockcur)
|
|
{
|
|
unsigned l;
|
|
|
|
if (io) {
|
|
int writer_error[IO_WRITER_ERROR_MAX];
|
|
unsigned j;
|
|
|
|
io_flush(io, writer_error);
|
|
|
|
for (j = 0; j < IO_WRITER_ERROR_MAX; ++j) {
|
|
if (writer_error[j]) {
|
|
errno = EIO;
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* flush all parity handles to ensure data is written to disk */
|
|
for (l = 0; l < state->level; ++l) {
|
|
int ret = parity_sync(&parity_handle[l]);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("parity_error_io:%" PRIu64 ":%s: Sync error. %s.\n", blockcur, lev_config_name(l), strerror(errno));
|
|
log_fatal(errno, "DANGER! Unexpected input/output error in disk %s. It isn't possible to continue.\n", lev_config_name(l));
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
/* complete all read-ahead without consuming the results */
|
|
if (io)
|
|
io_quiesce(io);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void state_load_ignore_file(tommy_list* filter_list, const char* path, const char* sub)
|
|
{
|
|
STREAM* f;
|
|
int line;
|
|
|
|
f = sopen_read(path, 0);
|
|
if (!f) {
|
|
/* LCOV_EXCL_START */
|
|
log_error(errno, "Error opening the ignore file '%s'. %s.\n", path, strerror(errno));
|
|
return;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
line = 1;
|
|
while (1) {
|
|
char buffer[PATH_MAX];
|
|
ssize_t ret;
|
|
int c;
|
|
|
|
/* skip initial spaces */
|
|
sgetspace(f);
|
|
|
|
/* get the whole line */
|
|
ret = sgetline(f, buffer, sizeof(buffer));
|
|
if (ret < 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_error(EUSER, "Too long line in '%s' at line %u\n", path, line);
|
|
break;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (buffer[0] == 0) {
|
|
/* allow empty lines */
|
|
} else if (buffer[0] == '#') {
|
|
/* ignore comment lines */
|
|
} else {
|
|
struct snapraid_filter* filter;
|
|
|
|
filter = filter_alloc_file(-1, sub, buffer);
|
|
if (!filter) {
|
|
/* LCOV_EXCL_START */
|
|
log_error(EUSER, "Invalid ignore specification '%s' in '%s' at line %u\n", buffer, path, line);
|
|
break;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
tommy_list_insert_tail(filter_list, &filter->node, filter);
|
|
}
|
|
|
|
/* next line */
|
|
c = sgeteol(f);
|
|
if (c == EOF) {
|
|
break;
|
|
}
|
|
if (c != '\n') {
|
|
/* LCOV_EXCL_START */
|
|
log_error(EUSER, "Extra data in '%s' at line %u\n", path, line);
|
|
break;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
++line;
|
|
}
|
|
|
|
if (serror(f)) {
|
|
/* LCOV_EXCL_START */
|
|
log_error(errno, "Error reading the ignore file '%s' at line %u\n", path, line);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
sclose(f);
|
|
}
|
|
|
|
static int state_snapshot_dir(struct fssnapshot_struct* fss, const char* name, struct snapraid_disk* disk)
|
|
{
|
|
struct stat st;
|
|
|
|
/* fail if it doesn't exists at high level */
|
|
if (fssnapshot_stat(fss, name, &st) != 0)
|
|
return -1;
|
|
|
|
if (disk) {
|
|
char vol[PATH_MAX];
|
|
|
|
if (fssnapshot_path(fss, name, vol, sizeof(vol)) != 0)
|
|
return -1;
|
|
|
|
pathcpy(disk->dir, sizeof(disk->dir), vol);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int state_snapshot_scan(struct snapraid_state* state)
|
|
{
|
|
if (!state->snapshot)
|
|
return 0;
|
|
|
|
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
/* check if it supports snapshot */
|
|
int res = fssnapshot_mount(disk->mount_point, &disk->fss);
|
|
if (res > 0)
|
|
continue;
|
|
if (res < 0) {
|
|
log_fatal(EINTERNAL, "Failed to setup snapshot for disk '%s'.\n", disk->name);
|
|
return -1;
|
|
}
|
|
|
|
msg_progress("Creating disk %s scan snapshot...\n", disk->name);
|
|
|
|
/* delete a potential previous scan snapshot */
|
|
if (state_snapshot_dir(&disk->fss, SNAPSHOT_SCAN, 0) == 0) {
|
|
if (fssnapshot_delete(&disk->fss, SNAPSHOT_SCAN) != 0) {
|
|
log_fatal(errno, "Failed to delete scan snapshot in '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/* create a new snapshot for scanning */
|
|
if (fssnapshot_create(&disk->fss, SNAPSHOT_SCAN) != 0) {
|
|
log_fatal(errno, "Failed to create scan snapshot '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* setup the snapshot in use */
|
|
if (state_snapshot_dir(&disk->fss, SNAPSHOT_SCAN, disk) != 0) {
|
|
log_error(errno, "Error stating scan snapshot '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int state_snapshot_pending(struct snapraid_state* state)
|
|
{
|
|
int error = 0;
|
|
|
|
if (!state->snapshot)
|
|
return 0;
|
|
|
|
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
/* check if it supports snapshot */
|
|
if (disk->fss.magic == 0)
|
|
continue;
|
|
|
|
msg_progress("Creating disk %s pending snapshot...\n", disk->name);
|
|
|
|
/* publish the scan snapshot without dropping the previous recovery source first */
|
|
if (fssnapshot_replace(&disk->fss, SNAPSHOT_SCAN, SNAPSHOT_PENDING) != 0) {
|
|
log_fatal(errno, "Failed to replace pending snapshot in '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
|
|
error = -1;
|
|
continue;
|
|
}
|
|
|
|
/* setup the snapshot in use */
|
|
if (state_snapshot_dir(&disk->fss, SNAPSHOT_PENDING, disk) != 0) {
|
|
log_error(errno, "Error stating pending snapshot '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
|
|
error = -1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
int state_snapshot_commit(struct snapraid_state* state)
|
|
{
|
|
int error = 0;
|
|
|
|
if (!state->snapshot)
|
|
return 0;
|
|
|
|
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
/* check if it supports snapshot */
|
|
if (disk->fss.magic == 0)
|
|
continue;
|
|
|
|
msg_progress("Committing disk %s stable snapshot...\n", disk->name);
|
|
|
|
/* publish pending without dropping the previous stable deallocation source first */
|
|
if (fssnapshot_replace(&disk->fss, SNAPSHOT_PENDING, SNAPSHOT_STABLE) != 0) {
|
|
log_fatal(errno, "Failed to replace stable snapshot in '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
|
|
error = -1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
void state_snapshot_read(struct snapraid_state* state)
|
|
{
|
|
if (!state->snapshot)
|
|
return;
|
|
|
|
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
/* check if it supports snapshot */
|
|
int res = fssnapshot_mount(disk->mount_point, &disk->fss);
|
|
if (res > 0)
|
|
continue;
|
|
if (res < 0) {
|
|
log_error(EUSER, "WARNING! Disk %s snapshot setup or recovery failed, falling back to live filesystem.\n", disk->name);
|
|
log_error(EUSER, "Recovery capability may be reduced if files have changed since the last successful sync.\n");
|
|
continue;
|
|
}
|
|
|
|
if (state_snapshot_dir(&disk->fss, SNAPSHOT_PENDING, disk) == 0) {
|
|
msg_progress("Using disk %s pending snapshot...\n", disk->name);
|
|
} else {
|
|
if (state_snapshot_dir(&disk->fss, SNAPSHOT_STABLE, disk) == 0) {
|
|
msg_progress("Using disk %s stable snapshot...\n", disk->name);
|
|
} else {
|
|
/* fallback to standard mount point */
|
|
log_error(EUSER, "WARNING! Disk %s snapshot missing, falling back to live filesystem.\n", disk->name);
|
|
log_error(EUSER, "Recovery capability may be reduced if files have changed since the last successful sync.\n");
|
|
msg_progress("Using disk %s live filesystem...\n", disk->name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void state_snapshot_write(struct snapraid_state* state, tommy_list* filterlist_disk)
|
|
{
|
|
if (!state->snapshot)
|
|
return;
|
|
|
|
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
/* check if it supports snapshot */
|
|
int res = fssnapshot_mount(disk->mount_point, &disk->fss);
|
|
if (res > 0)
|
|
continue;
|
|
if (res < 0) {
|
|
log_error(EUSER, "WARNING! Disk %s snapshot setup or recovery failed, falling back to live filesystem.\n", disk->name);
|
|
log_error(EUSER, "Recovery capability may be reduced if files have changed since the last successful sync.\n");
|
|
continue;
|
|
}
|
|
|
|
int writable = tommy_list_empty(filterlist_disk) || filter_path(filterlist_disk, 0, disk->name, 0) == 0;
|
|
|
|
if (writable) {
|
|
msg_progress("Using disk %s live filesystem...\n", disk->name);
|
|
} else if (state_snapshot_dir(&disk->fss, SNAPSHOT_PENDING, disk) == 0) {
|
|
msg_progress("Using disk %s pending snapshot...\n", disk->name);
|
|
} else if (state_snapshot_dir(&disk->fss, SNAPSHOT_STABLE, disk) == 0) {
|
|
msg_progress("Using disk %s stable snapshot...\n", disk->name);
|
|
} else {
|
|
/* fallback to standard mount point */
|
|
log_error(EUSER, "WARNING! Disk %s snapshot missing, falling back to live filesystem.\n", disk->name);
|
|
log_error(EUSER, "Recovery capability may be reduced if files have changed since the last successful sync.\n");
|
|
msg_progress("Using disk %s live filesystem...\n", disk->name);
|
|
}
|
|
|
|
/*
|
|
* If sync was interrupted before updating parity, the content file
|
|
* already records BLOCK_STATE_DELETED and pending deallocations, while the
|
|
* deleted files remain physically present in the stable snapshot. Import them
|
|
* into the import index so OLD recovery hypotheses can resolve them without
|
|
* consuming RAID equations, even for disks that use the live filesystem for normal I/O.
|
|
*/
|
|
if (!tommy_list_empty(&disk->dealloclist) && state_snapshot_dir(&disk->fss, SNAPSHOT_STABLE, 0) == 0) {
|
|
char vol[PATH_MAX];
|
|
|
|
msg_progress("Importing disk %s stable snapshot %" PRIu64 " deallocated files...\n", disk->name, (uint64_t)tommy_list_count(&disk->dealloclist));
|
|
|
|
if (fssnapshot_path(&disk->fss, SNAPSHOT_STABLE, vol, sizeof(vol)) == 0)
|
|
state_dealloc(state, vol, &disk->dealloclist);
|
|
}
|
|
}
|
|
}
|
|
|
|
void state_snapshot_cleanup(struct snapraid_state* state)
|
|
{
|
|
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
|
|
/* check if it supports snapshot */
|
|
if (disk->fss.magic == 0)
|
|
continue;
|
|
|
|
fssnapshot_unmount(&disk->fss);
|
|
}
|
|
}
|
|
|