]> git.sesse.net Git - bcachefs-tools-debian/blob - libbcachefs.c
fix list_journal for nochanges
[bcachefs-tools-debian] / libbcachefs.c
1 #include <ctype.h>
2 #include <dirent.h>
3 #include <errno.h>
4 #include <fcntl.h>
5 #include <libgen.h>
6 #include <stdbool.h>
7 #include <stdint.h>
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <string.h>
11 #include <sys/stat.h>
12 #include <sys/sysmacros.h>
13 #include <sys/types.h>
14 #include <time.h>
15 #include <unistd.h>
16
17 #include <uuid/uuid.h>
18
19 #include "libbcachefs.h"
20 #include "crypto.h"
21 #include "libbcachefs/bcachefs_format.h"
22 #include "libbcachefs/btree_cache.h"
23 #include "libbcachefs/checksum.h"
24 #include "libbcachefs/disk_groups.h"
25 #include "libbcachefs/journal_seq_blacklist.h"
26 #include "libbcachefs/opts.h"
27 #include "libbcachefs/replicas.h"
28 #include "libbcachefs/super-io.h"
29 #include "tools-util.h"
30
31 #define NSEC_PER_SEC    1000000000L
32
33 static void init_layout(struct bch_sb_layout *l,
34                         unsigned block_size,
35                         unsigned sb_size,
36                         u64 sb_start, u64 sb_end)
37 {
38         u64 sb_pos = sb_start;
39         unsigned i;
40
41         memset(l, 0, sizeof(*l));
42
43         l->magic                = BCHFS_MAGIC;
44         l->layout_type          = 0;
45         l->nr_superblocks       = 2;
46         l->sb_max_size_bits     = ilog2(sb_size);
47
48         /* Create two superblocks in the allowed range: */
49         for (i = 0; i < l->nr_superblocks; i++) {
50                 if (sb_pos != BCH_SB_SECTOR)
51                         sb_pos = round_up(sb_pos, block_size);
52
53                 l->sb_offset[i] = cpu_to_le64(sb_pos);
54                 sb_pos += sb_size;
55         }
56
57         if (sb_pos > sb_end)
58                 die("insufficient space for superblocks: start %llu end %llu > %llu size %u",
59                     sb_start, sb_pos, sb_end, sb_size);
60 }
61
62 /* minimum size filesystem we can create, given a bucket size: */
63 static u64 min_size(unsigned bucket_size)
64 {
65         return BCH_MIN_NR_NBUCKETS * bucket_size;
66 }
67
68 u64 bch2_pick_bucket_size(struct bch_opts opts, struct dev_opts *dev)
69 {
70         u64 bucket_size;
71
72         if (dev->size < min_size(opts.block_size))
73                 die("cannot format %s, too small (%llu bytes, min %llu)",
74                     dev->path, dev->size, min_size(opts.block_size));
75
76         /* Bucket size must be >= block size: */
77         bucket_size = opts.block_size;
78
79         /* Bucket size must be >= btree node size: */
80         if (opt_defined(opts, btree_node_size))
81                 bucket_size = max_t(unsigned, bucket_size,
82                                          opts.btree_node_size);
83
84         /* Want a bucket size of at least 128k, if possible: */
85         bucket_size = max(bucket_size, 128ULL << 10);
86
87         if (dev->size >= min_size(bucket_size)) {
88                 unsigned scale = max(1,
89                         ilog2(dev->size / min_size(bucket_size)) / 4);
90
91                 scale = rounddown_pow_of_two(scale);
92
93                 /* max bucket size 1 mb */
94                 bucket_size = min(bucket_size * scale, 1ULL << 20);
95         } else {
96                 do {
97                         bucket_size /= 2;
98                 } while (dev->size < min_size(bucket_size));
99         }
100
101         return bucket_size;
102 }
103
104 void bch2_check_bucket_size(struct bch_opts opts, struct dev_opts *dev)
105 {
106         if (dev->bucket_size < opts.block_size)
107                 die("Bucket size (%llu) cannot be smaller than block size (%u)",
108                     dev->bucket_size, opts.block_size);
109
110         if (opt_defined(opts, btree_node_size) &&
111             dev->bucket_size < opts.btree_node_size)
112                 die("Bucket size (%llu) cannot be smaller than btree node size (%u)",
113                     dev->bucket_size, opts.btree_node_size);
114
115         if (dev->nbuckets < BCH_MIN_NR_NBUCKETS)
116                 die("Not enough buckets: %llu, need %u (bucket size %llu)",
117                     dev->nbuckets, BCH_MIN_NR_NBUCKETS, dev->bucket_size);
118
119         if (dev->bucket_size > (u32) U16_MAX << 9)
120                 die("Bucket size (%llu) too big (max %u)",
121                     dev->bucket_size, (u32) U16_MAX << 9);
122 }
123
124 static unsigned parse_target(struct bch_sb_handle *sb,
125                              struct dev_opts *devs, size_t nr_devs,
126                              const char *s)
127 {
128         struct dev_opts *i;
129         int idx;
130
131         if (!s)
132                 return 0;
133
134         for (i = devs; i < devs + nr_devs; i++)
135                 if (!strcmp(s, i->path))
136                         return dev_to_target(i - devs);
137
138         idx = bch2_disk_path_find(sb, s);
139         if (idx >= 0)
140                 return group_to_target(idx);
141
142         die("Invalid target %s", s);
143         return 0;
144 }
145
146 struct bch_sb *bch2_format(struct bch_opt_strs  fs_opt_strs,
147                            struct bch_opts      fs_opts,
148                            struct format_opts   opts,
149                            struct dev_opts      *devs,
150                            size_t               nr_devs)
151 {
152         struct bch_sb_handle sb = { NULL };
153         struct dev_opts *i;
154         unsigned max_dev_block_size = 0;
155         unsigned opt_id;
156         u64 min_bucket_size = U64_MAX;
157
158         for (i = devs; i < devs + nr_devs; i++)
159                 max_dev_block_size = max(max_dev_block_size, get_blocksize(i->bdev->bd_buffered_fd));
160
161         /* calculate block size: */
162         if (!opt_defined(fs_opts, block_size)) {
163                 opt_set(fs_opts, block_size, max_dev_block_size);
164         } else if (fs_opts.block_size < max_dev_block_size)
165                 die("blocksize too small: %u, must be greater than device blocksize %u",
166                     fs_opts.block_size, max_dev_block_size);
167
168         /* get device size, if it wasn't specified: */
169         for (i = devs; i < devs + nr_devs; i++)
170                 if (!i->size)
171                         i->size = get_size(i->bdev->bd_buffered_fd);
172
173         /* calculate bucket sizes: */
174         for (i = devs; i < devs + nr_devs; i++)
175                 min_bucket_size = min(min_bucket_size,
176                         i->bucket_size ?: bch2_pick_bucket_size(fs_opts, i));
177
178         for (i = devs; i < devs + nr_devs; i++)
179                 if (!i->bucket_size)
180                         i->bucket_size = min_bucket_size;
181
182         for (i = devs; i < devs + nr_devs; i++) {
183                 i->nbuckets = i->size / i->bucket_size;
184                 bch2_check_bucket_size(fs_opts, i);
185         }
186
187         /* calculate btree node size: */
188         if (!opt_defined(fs_opts, btree_node_size)) {
189                 /* 256k default btree node size */
190                 opt_set(fs_opts, btree_node_size, 256 << 10);
191
192                 for (i = devs; i < devs + nr_devs; i++)
193                         fs_opts.btree_node_size =
194                                 min_t(unsigned, fs_opts.btree_node_size,
195                                       i->bucket_size);
196         }
197
198         if (uuid_is_null(opts.uuid.b))
199                 uuid_generate(opts.uuid.b);
200
201         if (bch2_sb_realloc(&sb, 0))
202                 die("insufficient memory");
203
204         sb.sb->version          = le16_to_cpu(opts.version);
205         sb.sb->version_min      = le16_to_cpu(opts.version);
206         sb.sb->magic            = BCHFS_MAGIC;
207         sb.sb->user_uuid        = opts.uuid;
208         sb.sb->nr_devices       = nr_devs;
209
210         if (opts.version == bcachefs_metadata_version_current)
211                 sb.sb->features[0] |= cpu_to_le64(BCH_SB_FEATURES_ALL);
212
213         uuid_generate(sb.sb->uuid.b);
214
215         if (opts.label)
216                 memcpy(sb.sb->label,
217                        opts.label,
218                        min(strlen(opts.label), sizeof(sb.sb->label)));
219
220         for (opt_id = 0;
221              opt_id < bch2_opts_nr;
222              opt_id++) {
223                 u64 v;
224
225                 v = bch2_opt_defined_by_id(&fs_opts, opt_id)
226                         ? bch2_opt_get_by_id(&fs_opts, opt_id)
227                         : bch2_opt_get_by_id(&bch2_opts_default, opt_id);
228
229                 __bch2_opt_set_sb(sb.sb, &bch2_opt_table[opt_id], v);
230         }
231
232         struct timespec now;
233         if (clock_gettime(CLOCK_REALTIME, &now))
234                 die("error getting current time: %m");
235
236         sb.sb->time_base_lo     = cpu_to_le64(now.tv_sec * NSEC_PER_SEC + now.tv_nsec);
237         sb.sb->time_precision   = cpu_to_le32(1);
238
239         /* Member info: */
240         struct bch_sb_field_members_v2 *mi =
241                 bch2_sb_field_resize(&sb, members_v2,
242                         (sizeof(*mi) + sizeof(struct bch_member) *
243                         nr_devs) / sizeof(u64));
244         mi->member_bytes = cpu_to_le16(sizeof(struct bch_member));
245         for (i = devs; i < devs + nr_devs; i++) {
246                 struct bch_member *m = bch2_members_v2_get_mut(sb.sb, (i - devs));
247
248                 uuid_generate(m->uuid.b);
249                 m->nbuckets     = cpu_to_le64(i->nbuckets);
250                 m->first_bucket = 0;
251                 m->bucket_size  = cpu_to_le16(i->bucket_size >> 9);
252
253                 SET_BCH_MEMBER_DISCARD(m,       i->discard);
254                 SET_BCH_MEMBER_DATA_ALLOWED(m,  i->data_allowed);
255                 SET_BCH_MEMBER_DURABILITY(m,    i->durability + 1);
256         }
257
258         /* Disk labels*/
259         for (i = devs; i < devs + nr_devs; i++) {
260                 struct bch_member *m;
261                 int idx;
262
263                 if (!i->label)
264                         continue;
265
266                 idx = bch2_disk_path_find_or_create(&sb, i->label);
267                 if (idx < 0)
268                         die("error creating disk path: %s", strerror(-idx));
269
270                 /*
271                  * Recompute mi and m after each sb modification: its location
272                  * in memory may have changed due to reallocation.
273                  */
274                 m = bch2_members_v2_get_mut(sb.sb, (i - devs));
275                 SET_BCH_MEMBER_GROUP(m, idx + 1);
276         }
277
278         SET_BCH_SB_FOREGROUND_TARGET(sb.sb,
279                 parse_target(&sb, devs, nr_devs, fs_opt_strs.foreground_target));
280         SET_BCH_SB_BACKGROUND_TARGET(sb.sb,
281                 parse_target(&sb, devs, nr_devs, fs_opt_strs.background_target));
282         SET_BCH_SB_PROMOTE_TARGET(sb.sb,
283                 parse_target(&sb, devs, nr_devs, fs_opt_strs.promote_target));
284         SET_BCH_SB_METADATA_TARGET(sb.sb,
285                 parse_target(&sb, devs, nr_devs, fs_opt_strs.metadata_target));
286
287         /* Crypt: */
288         if (opts.encrypted) {
289                 struct bch_sb_field_crypt *crypt =
290                         bch2_sb_field_resize(&sb, crypt, sizeof(*crypt) / sizeof(u64));
291
292                 bch_sb_crypt_init(sb.sb, crypt, opts.passphrase);
293                 SET_BCH_SB_ENCRYPTION_TYPE(sb.sb, 1);
294         }
295
296         bch2_sb_members_cpy_v2_v1(&sb);
297
298         for (i = devs; i < devs + nr_devs; i++) {
299                 u64 size_sectors = i->size >> 9;
300
301                 sb.sb->dev_idx = i - devs;
302
303                 if (!i->sb_offset) {
304                         i->sb_offset    = BCH_SB_SECTOR;
305                         i->sb_end       = size_sectors;
306                 }
307
308                 init_layout(&sb.sb->layout, fs_opts.block_size,
309                             opts.superblock_size,
310                             i->sb_offset, i->sb_end);
311
312                 /*
313                  * Also create a backup superblock at the end of the disk:
314                  *
315                  * If we're not creating a superblock at the default offset, it
316                  * means we're being run from the migrate tool and we could be
317                  * overwriting existing data if we write to the end of the disk:
318                  */
319                 if (i->sb_offset == BCH_SB_SECTOR) {
320                         struct bch_sb_layout *l = &sb.sb->layout;
321                         u64 backup_sb = size_sectors - (1 << l->sb_max_size_bits);
322
323                         backup_sb = rounddown(backup_sb, i->bucket_size >> 9);
324                         l->sb_offset[l->nr_superblocks++] = cpu_to_le64(backup_sb);
325                 }
326
327                 if (i->sb_offset == BCH_SB_SECTOR) {
328                         /* Zero start of disk */
329                         static const char zeroes[BCH_SB_SECTOR << 9];
330
331                         xpwrite(i->bdev->bd_buffered_fd, zeroes, BCH_SB_SECTOR << 9, 0,
332                                 "zeroing start of disk");
333                 }
334
335                 bch2_super_write(i->bdev->bd_buffered_fd, sb.sb);
336                 close(i->bdev->bd_buffered_fd);
337         }
338
339         return sb.sb;
340 }
341
342 void bch2_super_write(int fd, struct bch_sb *sb)
343 {
344         struct nonce nonce = { 0 };
345
346         unsigned i;
347         for (i = 0; i < sb->layout.nr_superblocks; i++) {
348                 sb->offset = sb->layout.sb_offset[i];
349
350                 if (sb->offset == BCH_SB_SECTOR) {
351                         /* Write backup layout */
352                         xpwrite(fd, &sb->layout, sizeof(sb->layout),
353                                 BCH_SB_LAYOUT_SECTOR << 9,
354                                 "backup layout");
355                 }
356
357                 sb->csum = csum_vstruct(NULL, BCH_SB_CSUM_TYPE(sb), nonce, sb);
358                 xpwrite(fd, sb, vstruct_bytes(sb),
359                         le64_to_cpu(sb->offset) << 9,
360                         "superblock");
361         }
362
363         fsync(fd);
364 }
365
366 struct bch_sb *__bch2_super_read(int fd, u64 sector)
367 {
368         struct bch_sb sb, *ret;
369
370         xpread(fd, &sb, sizeof(sb), sector << 9);
371
372         if (memcmp(&sb.magic, &BCACHE_MAGIC, sizeof(sb.magic)) &&
373             memcmp(&sb.magic, &BCHFS_MAGIC, sizeof(sb.magic)))
374                 die("not a bcachefs superblock");
375
376         size_t bytes = vstruct_bytes(&sb);
377
378         ret = malloc(bytes);
379
380         xpread(fd, ret, bytes, sector << 9);
381
382         return ret;
383 }
384
385 /* ioctl interface: */
386
387 /* Global control device: */
388 int bcachectl_open(void)
389 {
390         return xopen("/dev/bcachefs-ctl", O_RDWR);
391 }
392
393 /* Filesystem handles (ioctl, sysfs dir): */
394
395 #define SYSFS_BASE "/sys/fs/bcachefs/"
396
397 void bcache_fs_close(struct bchfs_handle fs)
398 {
399         close(fs.ioctl_fd);
400         close(fs.sysfs_fd);
401 }
402
403 struct bchfs_handle bcache_fs_open(const char *path)
404 {
405         struct bchfs_handle ret;
406
407         if (!uuid_parse(path, ret.uuid.b)) {
408                 /* It's a UUID, look it up in sysfs: */
409                 char *sysfs = mprintf(SYSFS_BASE "%s", path);
410                 ret.sysfs_fd = xopen(sysfs, O_RDONLY);
411
412                 char *minor = read_file_str(ret.sysfs_fd, "minor");
413                 char *ctl = mprintf("/dev/bcachefs%s-ctl", minor);
414                 ret.ioctl_fd = xopen(ctl, O_RDWR);
415
416                 free(sysfs);
417                 free(minor);
418                 free(ctl);
419         } else {
420                 /* It's a path: */
421                 ret.ioctl_fd = open(path, O_RDONLY);
422                 if (ret.ioctl_fd < 0)
423                         die("Error opening filesystem at %s: %m", path);
424
425                 struct bch_ioctl_query_uuid uuid;
426                 if (ioctl(ret.ioctl_fd, BCH_IOCTL_QUERY_UUID, &uuid) < 0)
427                         die("error opening %s: not a bcachefs filesystem", path);
428
429                 ret.uuid = uuid.uuid;
430
431                 char uuid_str[40];
432                 uuid_unparse(uuid.uuid.b, uuid_str);
433
434                 char *sysfs = mprintf(SYSFS_BASE "%s", uuid_str);
435                 ret.sysfs_fd = xopen(sysfs, O_RDONLY);
436                 free(sysfs);
437         }
438
439         return ret;
440 }
441
442 /*
443  * Given a path to a block device, open the filesystem it belongs to; also
444  * return the device's idx:
445  */
446 struct bchfs_handle bchu_fs_open_by_dev(const char *path, int *idx)
447 {
448         struct bch_opts opts = bch2_opts_empty();
449         char buf[1024], *uuid_str;
450
451         struct stat stat = xstat(path);
452
453         if (S_ISBLK(stat.st_mode)) {
454                 char *sysfs = mprintf("/sys/dev/block/%u:%u/bcachefs",
455                                       major(stat.st_dev),
456                                       minor(stat.st_dev));
457
458                 ssize_t len = readlink(sysfs, buf, sizeof(buf));
459                 free(sysfs);
460
461                 if (len <= 0)
462                         goto read_super;
463
464                 char *p = strrchr(buf, '/');
465                 if (!p || sscanf(p + 1, "dev-%u", idx) != 1)
466                         die("error parsing sysfs");
467
468                 *p = '\0';
469                 p = strrchr(buf, '/');
470                 uuid_str = p + 1;
471         } else {
472 read_super:
473                 opt_set(opts, noexcl,   true);
474                 opt_set(opts, nochanges, true);
475
476                 struct bch_sb_handle sb;
477                 int ret = bch2_read_super(path, &opts, &sb);
478                 if (ret)
479                         die("Error opening %s: %s", path, strerror(-ret));
480
481                 *idx = sb.sb->dev_idx;
482                 uuid_str = buf;
483                 uuid_unparse(sb.sb->user_uuid.b, uuid_str);
484
485                 bch2_free_super(&sb);
486         }
487
488         return bcache_fs_open(uuid_str);
489 }
490
491 int bchu_dev_path_to_idx(struct bchfs_handle fs, const char *dev_path)
492 {
493         int idx;
494         struct bchfs_handle fs2 = bchu_fs_open_by_dev(dev_path, &idx);
495
496         if (memcmp(&fs.uuid, &fs2.uuid, sizeof(fs.uuid)))
497                 idx = -1;
498         bcache_fs_close(fs2);
499         return idx;
500 }
501
502 int bchu_data(struct bchfs_handle fs, struct bch_ioctl_data cmd)
503 {
504         int progress_fd = xioctl(fs.ioctl_fd, BCH_IOCTL_DATA, &cmd);
505
506         while (1) {
507                 struct bch_ioctl_data_event e;
508
509                 if (read(progress_fd, &e, sizeof(e)) != sizeof(e))
510                         die("error reading from progress fd %m");
511
512                 if (e.type)
513                         continue;
514
515                 if (e.p.data_type == U8_MAX)
516                         break;
517
518                 printf("\33[2K\r");
519
520                 printf("%llu%% complete: current position %s",
521                        e.p.sectors_total
522                        ? e.p.sectors_done * 100 / e.p.sectors_total
523                        : 0,
524                        bch2_data_types[e.p.data_type]);
525
526                 switch (e.p.data_type) {
527                 case BCH_DATA_btree:
528                 case BCH_DATA_user:
529                         printf(" %s:%llu:%llu",
530                                bch2_btree_id_str(e.p.btree_id),
531                                e.p.pos.inode,
532                                e.p.pos.offset);
533                 }
534
535                 fflush(stdout);
536                 sleep(1);
537         }
538         printf("\nDone\n");
539
540         close(progress_fd);
541         return 0;
542 }
543
544 /* option parsing */
545
546 void bch2_opt_strs_free(struct bch_opt_strs *opts)
547 {
548         unsigned i;
549
550         for (i = 0; i < bch2_opts_nr; i++) {
551                 free(opts->by_id[i]);
552                 opts->by_id[i] = NULL;
553         }
554 }
555
556 struct bch_opt_strs bch2_cmdline_opts_get(int *argc, char *argv[],
557                                           unsigned opt_types)
558 {
559         struct bch_opt_strs opts;
560         unsigned i = 1;
561
562         memset(&opts, 0, sizeof(opts));
563
564         while (i < *argc) {
565                 char *optstr = strcmp_prefix(argv[i], "--");
566                 char *valstr = NULL, *p;
567                 int optid, nr_args = 1;
568
569                 if (!optstr) {
570                         i++;
571                         continue;
572                 }
573
574                 optstr = strdup(optstr);
575
576                 p = optstr;
577                 while (isalpha(*p) || *p == '_')
578                         p++;
579
580                 if (*p == '=') {
581                         *p = '\0';
582                         valstr = p + 1;
583                 }
584
585                 optid = bch2_opt_lookup(optstr);
586                 if (optid < 0 ||
587                     !(bch2_opt_table[optid].flags & opt_types)) {
588                         i++;
589                         goto next;
590                 }
591
592                 if (!valstr &&
593                     bch2_opt_table[optid].type != BCH_OPT_BOOL) {
594                         nr_args = 2;
595                         valstr = argv[i + 1];
596                 }
597
598                 if (!valstr)
599                         valstr = "1";
600
601                 opts.by_id[optid] = strdup(valstr);
602
603                 *argc -= nr_args;
604                 memmove(&argv[i],
605                         &argv[i + nr_args],
606                         sizeof(char *) * (*argc - i));
607                 argv[*argc] = NULL;
608 next:
609                 free(optstr);
610         }
611
612         return opts;
613 }
614
615 struct bch_opts bch2_parse_opts(struct bch_opt_strs strs)
616 {
617         struct bch_opts opts = bch2_opts_empty();
618         struct printbuf err = PRINTBUF;
619         unsigned i;
620         int ret;
621         u64 v;
622
623         for (i = 0; i < bch2_opts_nr; i++) {
624                 if (!strs.by_id[i])
625                         continue;
626
627                 ret = bch2_opt_parse(NULL,
628                                      &bch2_opt_table[i],
629                                      strs.by_id[i], &v, &err);
630                 if (ret < 0)
631                         die("Invalid option %s", err.buf);
632
633                 bch2_opt_set_by_id(&opts, i, v);
634         }
635
636         printbuf_exit(&err);
637         return opts;
638 }
639
640 #define newline(c)              \
641         do {                    \
642                 printf("\n");   \
643                 c = 0;          \
644         } while(0)
645 void bch2_opts_usage(unsigned opt_types)
646 {
647         const struct bch_option *opt;
648         unsigned i, c = 0, helpcol = 30;
649
650
651
652         for (opt = bch2_opt_table;
653              opt < bch2_opt_table + bch2_opts_nr;
654              opt++) {
655                 if (!(opt->flags & opt_types))
656                         continue;
657
658                 c += printf("      --%s", opt->attr.name);
659
660                 switch (opt->type) {
661                 case BCH_OPT_BOOL:
662                         break;
663                 case BCH_OPT_STR:
664                         c += printf("=(");
665                         for (i = 0; opt->choices[i]; i++) {
666                                 if (i)
667                                         c += printf("|");
668                                 c += printf("%s", opt->choices[i]);
669                         }
670                         c += printf(")");
671                         break;
672                 default:
673                         c += printf("=%s", opt->hint);
674                         break;
675                 }
676
677                 if (opt->help) {
678                         const char *l = opt->help;
679
680                         if (c >= helpcol)
681                                 newline(c);
682
683                         while (1) {
684                                 const char *n = strchrnul(l, '\n');
685
686                                 while (c < helpcol) {
687                                         putchar(' ');
688                                         c++;
689                                 }
690                                 printf("%.*s", (int) (n - l), l);
691                                 newline(c);
692
693                                 if (!*n)
694                                         break;
695                                 l = n + 1;
696                         }
697                 } else {
698                         newline(c);
699                 }
700         }
701 }
702
703 dev_names bchu_fs_get_devices(struct bchfs_handle fs)
704 {
705         DIR *dir = fdopendir(fs.sysfs_fd);
706         struct dirent *d;
707         dev_names devs;
708
709         darray_init(&devs);
710
711         while ((errno = 0), (d = readdir(dir))) {
712                 struct dev_name n = { 0, NULL, NULL };
713
714                 if (sscanf(d->d_name, "dev-%u", &n.idx) != 1)
715                         continue;
716
717                 char *block_attr = mprintf("dev-%u/block", n.idx);
718
719                 char sysfs_block_buf[4096];
720                 ssize_t r = readlinkat(fs.sysfs_fd, block_attr,
721                                        sysfs_block_buf, sizeof(sysfs_block_buf));
722                 if (r > 0) {
723                         sysfs_block_buf[r] = '\0';
724                         n.dev = strdup(basename(sysfs_block_buf));
725                 }
726
727                 free(block_attr);
728
729                 char *label_attr = mprintf("dev-%u/label", n.idx);
730                 n.label = read_file_str(fs.sysfs_fd, label_attr);
731                 free(label_attr);
732
733                 char *durability_attr = mprintf("dev-%u/durability", n.idx);
734                 n.durability = read_file_u64(fs.sysfs_fd, durability_attr);
735                 free(durability_attr);
736
737                 darray_push(&devs, n);
738         }
739
740         closedir(dir);
741
742         return devs;
743 }