Compare commits

...

2 Commits

Author SHA1 Message Date
Vitaliy Filippov a121b875b4 WIP Implement resizing partitions created with vitastor-disk
Test / test_rebalance_verify_ec (push) Has been skipped Details
Test / test_rebalance_verify_ec_imm (push) Has been skipped Details
Test / test_dd (push) Has been skipped Details
Test / test_root_node (push) Has been skipped Details
Test / test_switch_primary (push) Has been skipped Details
Test / test_write (push) Has been skipped Details
Test / test_write_xor (push) Has been skipped Details
Test / test_write_no_same (push) Has been skipped Details
Test / test_heal_pg_size_2 (push) Has been skipped Details
Test / test_heal_ec (push) Has been skipped Details
Test / test_heal_antietcd (push) Has been skipped Details
Test / test_heal_csum_32k_dmj (push) Has been skipped Details
Test / test_heal_csum_32k_dj (push) Has been skipped Details
Test / test_heal_csum_32k (push) Has been skipped Details
Test / test_heal_csum_4k_dmj (push) Has been skipped Details
Test / test_heal_csum_4k_dj (push) Has been skipped Details
Test / test_heal_csum_4k (push) Has been skipped Details
Test / test_snapshot_pool2 (push) Has been skipped Details
Test / test_osd_tags (push) Has been skipped Details
Test / test_enospc (push) Has been skipped Details
Test / test_enospc_xor (push) Has been skipped Details
Test / test_enospc_imm (push) Has been skipped Details
Test / test_enospc_imm_xor (push) Has been skipped Details
Test / test_scrub (push) Has been skipped Details
Test / test_scrub_zero_osd_2 (push) Has been skipped Details
Test / test_scrub_xor (push) Has been skipped Details
Test / test_scrub_pg_size_3 (push) Has been skipped Details
Test / test_scrub_pg_size_6_pg_minsize_4_osd_count_6_ec (push) Has been skipped Details
Test / test_scrub_ec (push) Has been skipped Details
Test / test_nfs (push) Has been skipped Details
2024-10-12 00:42:03 +03:00
Vitaliy Filippov 70acd8137e Extract check_existing_partition(), get_device_size() 2024-10-12 00:40:39 +03:00
8 changed files with 359 additions and 52 deletions

View File

@ -5,7 +5,8 @@ project(vitastor)
# vitastor-disk # vitastor-disk
add_executable(vitastor-disk add_executable(vitastor-disk
disk_tool.cpp disk_simple_offsets.cpp disk_tool.cpp disk_simple_offsets.cpp
disk_tool_journal.cpp disk_tool_meta.cpp disk_tool_prepare.cpp disk_tool_resize.cpp disk_tool_udev.cpp disk_tool_utils.cpp disk_tool_upgrade.cpp disk_tool_journal.cpp disk_tool_meta.cpp disk_tool_prepare.cpp disk_tool_resize.cpp
disk_tool_resize_auto.cpp disk_tool_udev.cpp disk_tool_utils.cpp disk_tool_upgrade.cpp
../util/crc32c.c ../util/str_util.cpp ../util/json_util.cpp ../../json11/json11.cpp ../util/rw_blocking.cpp ../util/allocator.cpp ../util/ringloop.cpp ../blockstore/blockstore_disk.cpp ../util/crc32c.c ../util/str_util.cpp ../util/json_util.cpp ../../json11/json11.cpp ../util/rw_blocking.cpp ../util/allocator.cpp ../util/ringloop.cpp ../blockstore/blockstore_disk.cpp
) )
target_link_libraries(vitastor-disk target_link_libraries(vitastor-disk

View File

@ -92,8 +92,22 @@ static const char *help_text =
" \n" " \n"
" Requires the `sfdisk` utility.\n" " Requires the `sfdisk` utility.\n"
"\n" "\n"
"vitastor-disk resize <ALL_OSD_PARAMETERS> <NEW_LAYOUT> [--iodepth 32]\n" "vitastor-disk resize <osd_num>|<osd_device> [OPTIONS]\n"
" Resize data area and/or rewrite/move journal and metadata\n" " Resize data area and/or move journal and metadata:\n"
" --move-journal TARGET move journal to TARGET\n"
" --move-meta TARGET move metadata to TARGET\n"
" --journal-size NEW_SIZE resize journal to NEW_SIZE\n"
" --data-size NEW_SIZE resize data device to NEW_SIZE\n"
" --dry-run only show new layout, do not apply it\n"
" \n"
" NEW_SIZE may include k/m/g/t suffixes.\n"
" TARGET may be one of:\n"
" <partition> move journal/metadata to an existing GPT partition\n"
" <raw_device> create a GPT partition on <raw_device> and move journal/metadata to it\n"
" \"\" (empty string) move journal/metadata back to the data device\n"
"\n"
"vitastor-disk raw-resize <ALL_OSD_PARAMETERS> <NEW_LAYOUT> [--iodepth 32]\n"
" Resize data area and/or rewrite/move journal and metadata (manual format).\n"
" ALL_OSD_PARAMETERS must include all (at least all disk-related)\n" " ALL_OSD_PARAMETERS must include all (at least all disk-related)\n"
" parameters from OSD command line (i.e. from systemd unit or superblock).\n" " parameters from OSD command line (i.e. from systemd unit or superblock).\n"
" NEW_LAYOUT may include new disk layout parameters:\n" " NEW_LAYOUT may include new disk layout parameters:\n"
@ -236,6 +250,10 @@ int main(int argc, char *argv[])
{ {
self.options["force"] = "1"; self.options["force"] = "1";
} }
else if (!strcmp(argv[i], "--dry-run") || !strcmp(argv[i], "--dry_run"))
{
self.options["dry_run"] = "1";
}
else if (!strcmp(argv[i], "--allow-data-loss")) else if (!strcmp(argv[i], "--allow-data-loss"))
{ {
self.options["allow_data_loss"] = "1"; self.options["allow_data_loss"] = "1";
@ -243,7 +261,7 @@ int main(int argc, char *argv[])
else if (argv[i][0] == '-' && argv[i][1] == '-' && i < argc-1) else if (argv[i][0] == '-' && argv[i][1] == '-' && i < argc-1)
{ {
char *key = argv[i]+2; char *key = argv[i]+2;
self.options[key] = argv[++i]; self.options[str_replace(key, "-", "_")] = argv[++i];
} }
else else
{ {
@ -375,7 +393,16 @@ int main(int argc, char *argv[])
} }
else if (!strcmp(cmd[0], "resize")) else if (!strcmp(cmd[0], "resize"))
{ {
return self.resize_data(); if (cmd.size() != 2)
{
fprintf(stderr, "Exactly 1 OSD number or OSD device path argument is required\n");
return 1;
}
return self.resize_data(cmd[1]);
}
else if (!strcmp(cmd[0], "raw-resize"))
{
return self.raw_resize();
} }
else if (!strcmp(cmd[0], "simple-offsets")) else if (!strcmp(cmd[0], "simple-offsets"))
{ {

View File

@ -98,7 +98,11 @@ struct disk_tool_t
int write_json_journal(json11::Json entries); int write_json_journal(json11::Json entries);
int write_json_meta(json11::Json meta); int write_json_meta(json11::Json meta);
int resize_data(); int resize_data(std::string device);
int resize_parse_move_journal(std::map<std::string, std::string> & move_options);
int resize_parse_move_meta(std::map<std::string, std::string> & move_options);
int raw_resize();
int resize_parse_params(); int resize_parse_params();
void resize_init(blockstore_meta_header_v2_t *hdr); void resize_init(blockstore_meta_header_v2_t *hdr);
int resize_remap_blocks(); int resize_remap_blocks();
@ -121,6 +125,7 @@ struct disk_tool_t
uint32_t write_osd_superblock(std::string device, json11::Json params); uint32_t write_osd_superblock(std::string device, json11::Json params);
int prepare_one(std::map<std::string, std::string> options, int is_hdd = -1); int prepare_one(std::map<std::string, std::string> options, int is_hdd = -1);
int check_existing_partition(const std::string & dev);
int prepare(std::vector<std::string> devices); int prepare(std::vector<std::string> devices);
std::vector<vitastor_dev_info_t> collect_devices(const std::vector<std::string> & devices); std::vector<vitastor_dev_info_t> collect_devices(const std::vector<std::string> & devices);
json11::Json add_partitions(vitastor_dev_info_t & devinfo, std::vector<std::string> sizes); json11::Json add_partitions(vitastor_dev_info_t & devinfo, std::vector<std::string> sizes);
@ -135,6 +140,7 @@ void disk_tool_simple_offsets(json11::Json cfg, bool json_output);
uint64_t sscanf_json(const char *fmt, const json11::Json & str); uint64_t sscanf_json(const char *fmt, const json11::Json & str);
void fromhexstr(const std::string & from, int bytes, uint8_t *to); void fromhexstr(const std::string & from, int bytes, uint8_t *to);
int disable_cache(std::string dev); int disable_cache(std::string dev);
uint64_t get_device_size(const std::string & dev);
std::string get_parent_device(std::string dev); std::string get_parent_device(std::string dev);
int shell_exec(const std::vector<std::string> & cmd, const std::string & in, std::string *out, std::string *err); int shell_exec(const std::vector<std::string> & cmd, const std::string & in, std::string *out, std::string *err);
int write_zero(int fd, uint64_t offset, uint64_t size); int write_zero(int fd, uint64_t offset, uint64_t size);

View File

@ -54,24 +54,9 @@ int disk_tool_t::prepare_one(std::map<std::string, std::string> options, int is_
} }
if (i == 0 && is_hdd == -1) if (i == 0 && is_hdd == -1)
is_hdd = trim(read_file("/sys/block/"+parent_dev+"/queue/rotational")) == "1"; is_hdd = trim(read_file("/sys/block/"+parent_dev+"/queue/rotational")) == "1";
std::string out; if (check_existing_partition(dev) != 0)
if (shell_exec({ "wipefs", dev }, "", &out, NULL) != 0 || out != "")
{
fprintf(stderr, "%s contains data, not creating OSD without --force. wipefs shows:\n%s", dev.c_str(), out.c_str());
return 1; return 1;
} }
json11::Json sb = read_osd_superblock(dev, false);
if (!sb.is_null())
{
fprintf(stderr, "%s already contains Vitastor OSD superblock, not creating OSD without --force\n", dev.c_str());
return 1;
}
if (fix_partition_type(dev) != 0)
{
fprintf(stderr, "%s has incorrect type and we failed to change it to Vitastor type\n", dev.c_str());
return 1;
}
}
} }
for (auto dev: std::vector<std::string>{"data", "meta", "journal"}) for (auto dev: std::vector<std::string>{"data", "meta", "journal"})
{ {
@ -222,6 +207,28 @@ int disk_tool_t::prepare_one(std::map<std::string, std::string> options, int is_
return 0; return 0;
} }
int disk_tool_t::check_existing_partition(const std::string & dev)
{
std::string out;
if (shell_exec({ "wipefs", dev }, "", &out, NULL) != 0 || out != "")
{
fprintf(stderr, "%s contains data, not creating OSD without --force. wipefs shows:\n%s", dev.c_str(), out.c_str());
return 1;
}
json11::Json sb = read_osd_superblock(dev, false);
if (!sb.is_null())
{
fprintf(stderr, "%s already contains Vitastor OSD superblock, not creating OSD without --force\n", dev.c_str());
return 1;
}
if (fix_partition_type(dev) != 0)
{
fprintf(stderr, "%s has incorrect type and we failed to change it to Vitastor type\n", dev.c_str());
return 1;
}
return 0;
}
std::vector<vitastor_dev_info_t> disk_tool_t::collect_devices(const std::vector<std::string> & devices) std::vector<vitastor_dev_info_t> disk_tool_t::collect_devices(const std::vector<std::string> & devices)
{ {
std::vector<vitastor_dev_info_t> devinfo; std::vector<vitastor_dev_info_t> devinfo;
@ -233,34 +240,17 @@ std::vector<vitastor_dev_info_t> disk_tool_t::collect_devices(const std::vector<
fprintf(stderr, "%s does not start with /dev/, ignoring\n", dev.c_str()); fprintf(stderr, "%s does not start with /dev/, ignoring\n", dev.c_str());
continue; continue;
} }
struct stat dev_st, sys_st; struct stat sys_st;
if (stat(dev.c_str(), &dev_st) < 0) uint64_t dev_size = get_device_size(dev);
if (!dev_size)
{ {
if (errno == ENOENT) return {};
}
else if (dev_size == UINT64_MAX)
{ {
fprintf(stderr, "%s does not exist, skipping\n", dev.c_str()); fprintf(stderr, "%s does not exist, skipping\n", dev.c_str());
continue; continue;
} }
fprintf(stderr, "Error checking %s: %s\n", dev.c_str(), strerror(errno));
return {};
}
uint64_t dev_size = dev_st.st_size;
if (S_ISBLK(dev_st.st_mode))
{
int fd = open(dev.c_str(), O_DIRECT|O_RDWR);
if (fd < 0)
{
fprintf(stderr, "Failed to open %s: %s\n", dev.c_str(), strerror(errno));
return {};
}
if (ioctl(fd, BLKGETSIZE64, &dev_size) < 0)
{
fprintf(stderr, "Failed to get %s size: %s\n", dev.c_str(), strerror(errno));
close(fd);
return {};
}
close(fd);
}
if (stat(("/sys/block/"+dev.substr(5)).c_str(), &sys_st) < 0) if (stat(("/sys/block/"+dev.substr(5)).c_str(), &sys_st) < 0)
{ {
if (errno == ENOENT) if (errno == ENOENT)

View File

@ -18,7 +18,7 @@ struct resizer_data_moving_t
uint64_t old_loc, new_loc; uint64_t old_loc, new_loc;
}; };
int disk_tool_t::resize_data() int disk_tool_t::raw_resize()
{ {
int r; int r;
// Parse parameters // Parse parameters

View File

@ -0,0 +1,241 @@
// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 (see README.md for details)
#include "disk_tool.h"
#include "rw_blocking.h"
#include "str_util.h"
#include "json_util.h"
int disk_tool_t::resize_data(std::string device)
{
if (options.find("move_journal") == options.end() &&
options.find("move_data") == options.end() &&
options.find("journal_size") == options.end() &&
options.find("data_size") == options.end())
{
fprintf(stderr, "None of --move-journal, --move-data, --journal-size, --data-size options are specified - nothing to do!\n");
return 1;
}
if (stoull_full(device))
device = "/dev/vitastor/osd"+device+"-data";
json11::Json sb = read_osd_superblock(device, true, false);
if (sb.is_null())
return 1;
std::map sb_params = json_to_string_map(sb["params"].object_items());
blockstore_disk_t dsk;
try
{
dsk.parse_config(sb_params);
dsk.data_io = dsk.meta_io = dsk.journal_io = "cached";
dsk.open_data();
dsk.open_meta();
dsk.open_journal();
dsk.calc_lengths(true);
}
catch (std::exception & e)
{
dsk.close_all();
fprintf(stderr, "%s\n", e.what());
return 1;
}
dsk.close_all();
new_data_offset = dsk.data_offset;
new_meta_offset = dsk.meta_offset;
new_journal_len = dsk.journal_len;
if (options.find("journal_size") != options.end())
{
new_journal_len = parse_size(options["journal_size"]);
if (options.find("move_journal") == options.end())
options["move_journal"] = dsk.journal_device == dsk.data_device ? "" : dsk.journal_device;
}
std::map<std::string, std::string> move_options;
if (options.find("move_journal") != options.end())
{
if (resize_parse_move_journal(move_options) != 0)
return 1;
}
if (options.find("move_meta") != options.end())
{
if (resize_parse_move_meta(move_options) != 0)
return 1;
}
if (options.find("data_size") != options.end())
{
auto new_data_dev_size = options["data_size"] == "max"
? dsk.data_device_size : parse_size(options["data_size"]);
if (new_data_dev_size-dsk.data_offset != dsk.data_len)
move_options["new_data_len"] = std::to_string(new_data_dev_size-new_data_offset);
}
if (new_data_offset != dsk.data_offset)
move_options["new_data_offset"] = std::to_string(new_data_offset);
if (new_meta_offset != dsk.meta_offset)
move_options["new_meta_offset"] = std::to_string(new_meta_offset);
auto orig_options = std::move(options);
options = sb_params;
for (auto & kv: move_options)
options[kv.first] = kv.second;
if (orig_options.find("dry_run") != orig_options.end())
{
if (json)
printf("%s\n", json11::Json(options).dump().c_str());
else
{
std::string cmd = "vitastor-disk resize";
for (auto & kv: options)
cmd += " --"+kv.first+" "+kv.second;
printf("%s\n", cmd.c_str());
}
return 0;
}
return raw_resize();
}
int disk_tool_t::resize_parse_move_journal(std::map<std::string, std::string> & move_options)
{
if (options["move_journal"] == "")
{
// move back to the data device
// but first check if not already there :)
if (dsk.journal_device == dsk.data_device && new_journal_len == dsk.journal_len)
{
// already there
fprintf(stderr, "journal is already on data device and has the same size\n");
return 0;
}
move_options["new_journal_device"] = dsk.data_device;
move_options["new_journal_offset"] = "4096";
move_options["new_journal_len"] = std::to_string(new_journal_len);
new_data_offset += new_journal_len;
if (dsk.meta_device == dsk.data_device)
new_meta_offset += new_journal_len;
}
else
{
std::string real_dev = realpath_str(options["move_journal"], false);
if (real_dev == "")
return 1;
std::string parent_dev = get_parent_device(real_dev);
if (parent_dev == "")
return 1;
if (parent_dev == real_dev)
{
// whole disk - create partition
std::string old_real_dev = realpath_str(dsk.journal_device);
if (old_real_dev == "")
return 1;
if (options.find("force") == options.end() &&
get_parent_device(old_real_dev) == parent_dev)
{
// already there
fprintf(stderr, "journal is already on a partition of %s, add --force to create a new partition\n", options["move_journal"].c_str());
return 0;
}
auto devinfos = collect_devices({ real_dev });
if (devinfos.size() == 0)
return 1;
std::vector<std::string> sizes;
sizes.push_back(std::to_string((new_journal_len+1024*1024-1)/1024/1024)+"MiB");
auto new_parts = add_partitions(devinfos[0], sizes);
if (!new_parts.array_items().size())
return 1;
options["move_journal"] = "/dev/disk/by-partuuid/"+new_parts[0]["uuid"].string_value();
}
else if (options["move_journal"].substr(0, 22) != "/dev/disk/by-partuuid/")
{
// Partitions should be identified by GPT partition UUID
fprintf(stderr, "%s does not start with /dev/disk/by-partuuid/. Partitions should be identified by GPT partition UUIDs\n", options["move_journal"].c_str());
return 1;
}
else
{
// already a partition - check that it's a GPT partition with correct type
if (options.find("force") == options.end() &&
check_existing_partition(real_dev) != 0)
{
return 1;
}
}
new_journal_len = get_device_size(options["move_journal"]) - 4096;
move_options["new_journal_device"] = options["move_journal"];
move_options["new_journal_offset"] = "4096";
move_options["new_journal_len"] = std::to_string(new_journal_len);
if (dsk.journal_device == dsk.data_device)
new_data_offset -= dsk.journal_len;
}
return 0;
}
int disk_tool_t::resize_parse_move_meta(std::map<std::string, std::string> & move_options)
{
if (options["move_meta"] == "")
{
// move back to the data device
// but first check if not already there :)
if (dsk.meta_device == dsk.data_device)
{
// already there
fprintf(stderr, "metadata is already on data device\n");
return 0;
}
auto new_journal_device = move_options.find("new_journal_device") != move_options.end()
? move_options["new_journal_device"] : dsk.journal_device;
move_options["new_meta_device"] = dsk.data_device;
move_options["new_meta_offset"] = std::to_string(new_journal_device == dsk.data_device
? 4096+new_journal_len : 4096);
move_options["new_meta_len"] = std::to_string(dsk.meta_len);
new_data_offset += dsk.meta_len;
}
else
{
std::string real_dev = realpath_str(options["move_meta"], false);
if (real_dev == "")
return 1;
std::string parent_dev = get_parent_device(real_dev);
if (parent_dev == "")
return 1;
if (parent_dev == real_dev)
{
// whole disk - create partition
std::string old_real_dev = realpath_str(dsk.meta_device);
if (old_real_dev == "")
return 1;
if (options.find("force") == options.end() &&
get_parent_device(old_real_dev) == parent_dev)
{
// already there
fprintf(stderr, "metadata is already on a partition of %s\n", options["move_meta"].c_str());
return 0;
}
auto devinfos = collect_devices({ real_dev });
if (devinfos.size() == 0)
return 1;
std::vector<std::string> sizes;
sizes.push_back(std::to_string((dsk.meta_len+1024*1024-1)/1024/1024)+"MiB");
auto new_parts = add_partitions(devinfos[0], sizes);
if (!new_parts.array_items().size())
return 1;
options["move_meta"] = "/dev/disk/by-partuuid/"+new_parts[0]["uuid"].string_value();
}
else if (options["move_meta"].substr(0, 22) != "/dev/disk/by-partuuid/")
{
// Partitions should be identified by GPT partition UUID
fprintf(stderr, "%s does not start with /dev/disk/by-partuuid/. Partitions should be identified by GPT partition UUIDs\n", options["move_meta"].c_str());
return 1;
}
else
{
// already a partition - check that it's a GPT partition with correct type
if (options.find("force") == options.end() &&
check_existing_partition(real_dev) != 0)
{
return 1;
}
}
move_options["new_meta_len"] = std::to_string(get_device_size(options["move_meta"]) - 4096);
move_options["new_meta_device"] = options["move_meta"];
move_options["new_meta_offset"] = "4096";
if (dsk.meta_device == dsk.data_device)
new_data_offset -= dsk.meta_len;
}
return 0;
}

View File

@ -101,7 +101,7 @@ int disk_tool_t::upgrade_simple_unit(std::string unit)
resizer.options = options; resizer.options = options;
for (auto & kv: resize) for (auto & kv: resize)
resizer.options[kv.first] = std::to_string(kv.second); resizer.options[kv.first] = std::to_string(kv.second);
if (resizer.resize_data() != 0) if (resizer.raw_resize() != 0)
{ {
// FIXME: Resize with backup or journal // FIXME: Resize with backup or journal
fprintf( fprintf(

View File

@ -117,6 +117,38 @@ int disable_cache(std::string dev)
return 0; return 0;
} }
uint64_t get_device_size(const std::string & dev)
{
struct stat dev_st;
if (stat(dev.c_str(), &dev_st) < 0)
{
if (errno == ENOENT)
{
return UINT64_MAX;
}
fprintf(stderr, "Error checking %s: %s\n", dev.c_str(), strerror(errno));
return 0;
}
uint64_t dev_size = dev_st.st_size;
if (S_ISBLK(dev_st.st_mode))
{
int fd = open(dev.c_str(), O_DIRECT|O_RDWR);
if (fd < 0)
{
fprintf(stderr, "Failed to open %s: %s\n", dev.c_str(), strerror(errno));
return 0;
}
if (ioctl(fd, BLKGETSIZE64, &dev_size) < 0)
{
fprintf(stderr, "Failed to get %s size: %s\n", dev.c_str(), strerror(errno));
close(fd);
return 0;
}
close(fd);
}
return dev_size;
}
std::string get_parent_device(std::string dev) std::string get_parent_device(std::string dev)
{ {
if (dev.substr(0, 5) != "/dev/") if (dev.substr(0, 5) != "/dev/")
@ -125,16 +157,26 @@ std::string get_parent_device(std::string dev)
return ""; return "";
} }
dev = dev.substr(5); dev = dev.substr(5);
// check if it's a partition - partitions aren't present in /sys/block/
struct stat st;
auto chk = "/sys/block/"+dev;
if (stat(chk.c_str(), &st) == 0)
{
// present in /sys/block/ - not a partition
return dev;
}
else if (errno != ENOENT)
{
fprintf(stderr, "Failed to stat %s: %s\n", chk.c_str(), strerror(errno));
return "";
}
int i = dev.size(); int i = dev.size();
while (i > 0 && isdigit(dev[i-1])) while (i > 0 && isdigit(dev[i-1]))
i--; i--;
if (i >= 1 && dev[i-1] == '-') // dm-0, dm-1 if (i >= 2 && dev[i-1] == 'p' && isdigit(dev[i-2])) // nvme0n1p1
return dev;
else if (i >= 2 && dev[i-1] == 'p' && isdigit(dev[i-2])) // nvme0n1p1
i--; i--;
// Check that such block device exists // Check that such block device exists
struct stat st; chk = "/sys/block/"+dev.substr(0, i);
auto chk = "/sys/block/"+dev.substr(0, i);
if (stat(chk.c_str(), &st) < 0) if (stat(chk.c_str(), &st) < 0)
{ {
if (errno != ENOENT) if (errno != ENOENT)