// SPDX-License-Identifier: GPL-2.0 #include "bcachefs.h" #include "disk_groups.h" #include "super-io.h" #include static int group_cmp(const void *_l, const void *_r) { const struct bch_disk_group *l = _l; const struct bch_disk_group *r = _r; return ((BCH_GROUP_DELETED(l) > BCH_GROUP_DELETED(r)) - (BCH_GROUP_DELETED(l) < BCH_GROUP_DELETED(r))) ?: ((BCH_GROUP_PARENT(l) > BCH_GROUP_PARENT(r)) - (BCH_GROUP_PARENT(l) < BCH_GROUP_PARENT(r))) ?: strncmp(l->label, r->label, sizeof(l->label)); } static const char *bch2_sb_disk_groups_validate(struct bch_sb *sb, struct bch_sb_field *f) { struct bch_sb_field_disk_groups *groups = field_to_type(f, disk_groups); struct bch_disk_group *g, *sorted = NULL; struct bch_sb_field_members *mi; struct bch_member *m; unsigned i, nr_groups, len; const char *err = NULL; mi = bch2_sb_get_members(sb); groups = bch2_sb_get_disk_groups(sb); nr_groups = disk_groups_nr(groups); for (m = mi->members; m < mi->members + sb->nr_devices; m++) { unsigned g; if (!BCH_MEMBER_GROUP(m)) continue; g = BCH_MEMBER_GROUP(m) - 1; if (g >= nr_groups || BCH_GROUP_DELETED(&groups->entries[g])) return "disk has invalid group"; } if (!nr_groups) return NULL; for (g = groups->entries; g < groups->entries + nr_groups; g++) { if (BCH_GROUP_DELETED(g)) continue; len = strnlen(g->label, sizeof(g->label)); if (!len) { err = "group with empty label"; goto err; } } sorted = kmalloc_array(nr_groups, sizeof(*sorted), GFP_KERNEL); if (!sorted) return "cannot allocate memory"; memcpy(sorted, groups->entries, nr_groups * sizeof(*sorted)); sort(sorted, nr_groups, sizeof(*sorted), group_cmp, NULL); for (i = 0; i + 1 < nr_groups; i++) if (!BCH_GROUP_DELETED(sorted + i) && !group_cmp(sorted + i, sorted + i + 1)) { err = "duplicate groups"; goto err; } err = NULL; err: kfree(sorted); return err; } static void bch2_sb_disk_groups_to_text(struct printbuf *out, struct bch_sb *sb, struct bch_sb_field *f) { struct bch_sb_field_disk_groups *groups = field_to_type(f, disk_groups); struct bch_disk_group *g; unsigned nr_groups = disk_groups_nr(groups); for (g = groups->entries; g < groups->entries + nr_groups; g++) { if (g != groups->entries) pr_buf(out, " "); if (BCH_GROUP_DELETED(g)) pr_buf(out, "[deleted]"); else pr_buf(out, "[parent %llu name %s]", BCH_GROUP_PARENT(g), g->label); } } const struct bch_sb_field_ops bch_sb_field_ops_disk_groups = { .validate = bch2_sb_disk_groups_validate, .to_text = bch2_sb_disk_groups_to_text }; int bch2_sb_disk_groups_to_cpu(struct bch_fs *c) { struct bch_sb_field_members *mi; struct bch_sb_field_disk_groups *groups; struct bch_disk_groups_cpu *cpu_g, *old_g; unsigned i, g, nr_groups; lockdep_assert_held(&c->sb_lock); mi = bch2_sb_get_members(c->disk_sb.sb); groups = bch2_sb_get_disk_groups(c->disk_sb.sb); nr_groups = disk_groups_nr(groups); if (!groups) return 0; cpu_g = kzalloc(sizeof(*cpu_g) + sizeof(cpu_g->entries[0]) * nr_groups, GFP_KERNEL); if (!cpu_g) return -ENOMEM; cpu_g->nr = nr_groups; for (i = 0; i < nr_groups; i++) { struct bch_disk_group *src = &groups->entries[i]; struct bch_disk_group_cpu *dst = &cpu_g->entries[i]; dst->deleted = BCH_GROUP_DELETED(src); dst->parent = BCH_GROUP_PARENT(src); } for (i = 0; i < c->disk_sb.sb->nr_devices; i++) { struct bch_member *m = mi->members + i; struct bch_disk_group_cpu *dst = &cpu_g->entries[BCH_MEMBER_GROUP(m)]; if (!bch2_member_exists(m)) continue; g = BCH_MEMBER_GROUP(m); while (g) { dst = &cpu_g->entries[g - 1]; __set_bit(i, dst->devs.d); g = dst->parent; } } old_g = rcu_dereference_protected(c->disk_groups, lockdep_is_held(&c->sb_lock)); rcu_assign_pointer(c->disk_groups, cpu_g); if (old_g) kfree_rcu(old_g, rcu); return 0; } const struct bch_devs_mask *bch2_target_to_mask(struct bch_fs *c, unsigned target) { struct target t = target_decode(target); switch (t.type) { case TARGET_NULL: return NULL; case TARGET_DEV: { struct bch_dev *ca = t.dev < c->sb.nr_devices ? rcu_dereference(c->devs[t.dev]) : NULL; return ca ? &ca->self : NULL; } case TARGET_GROUP: { struct bch_disk_groups_cpu *g = rcu_dereference(c->disk_groups); return t.group < g->nr && !g->entries[t.group].deleted ? &g->entries[t.group].devs : NULL; } default: BUG(); } } bool bch2_dev_in_target(struct bch_fs *c, unsigned dev, unsigned target) { struct target t = target_decode(target); switch (t.type) { case TARGET_NULL: return false; case TARGET_DEV: return dev == t.dev; case TARGET_GROUP: { struct bch_disk_groups_cpu *g; const struct bch_devs_mask *m; bool ret; rcu_read_lock(); g = rcu_dereference(c->disk_groups); m = t.group < g->nr && !g->entries[t.group].deleted ? &g->entries[t.group].devs : NULL; ret = m ? test_bit(dev, m->d) : false; rcu_read_unlock(); return ret; } default: BUG(); } } static int __bch2_disk_group_find(struct bch_sb_field_disk_groups *groups, unsigned parent, const char *name, unsigned namelen) { unsigned i, nr_groups = disk_groups_nr(groups); if (!namelen || namelen > BCH_SB_LABEL_SIZE) return -EINVAL; for (i = 0; i < nr_groups; i++) { struct bch_disk_group *g = groups->entries + i; if (BCH_GROUP_DELETED(g)) continue; if (!BCH_GROUP_DELETED(g) && BCH_GROUP_PARENT(g) == parent && strnlen(g->label, sizeof(g->label)) == namelen && !memcmp(name, g->label, namelen)) return i; } return -1; } static int __bch2_disk_group_add(struct bch_sb_handle *sb, unsigned parent, const char *name, unsigned namelen) { struct bch_sb_field_disk_groups *groups = bch2_sb_get_disk_groups(sb->sb); unsigned i, nr_groups = disk_groups_nr(groups); struct bch_disk_group *g; if (!namelen || namelen > BCH_SB_LABEL_SIZE) return -EINVAL; for (i = 0; i < nr_groups && !BCH_GROUP_DELETED(&groups->entries[i]); i++) ; if (i == nr_groups) { unsigned u64s = (sizeof(struct bch_sb_field_disk_groups) + sizeof(struct bch_disk_group) * (nr_groups + 1)) / sizeof(u64); groups = bch2_sb_resize_disk_groups(sb, u64s); if (!groups) return -ENOSPC; nr_groups = disk_groups_nr(groups); } BUG_ON(i >= nr_groups); g = &groups->entries[i]; memcpy(g->label, name, namelen); if (namelen < sizeof(g->label)) g->label[namelen] = '\0'; SET_BCH_GROUP_DELETED(g, 0); SET_BCH_GROUP_PARENT(g, parent); SET_BCH_GROUP_DATA_ALLOWED(g, ~0); return i; } int bch2_disk_path_find(struct bch_sb_handle *sb, const char *name) { struct bch_sb_field_disk_groups *groups = bch2_sb_get_disk_groups(sb->sb); int v = -1; do { const char *next = strchrnul(name, '.'); unsigned len = next - name; if (*next == '.') next++; v = __bch2_disk_group_find(groups, v + 1, name, len); name = next; } while (*name && v >= 0); return v; } int bch2_disk_path_find_or_create(struct bch_sb_handle *sb, const char *name) { struct bch_sb_field_disk_groups *groups; unsigned parent = 0; int v = -1; do { const char *next = strchrnul(name, '.'); unsigned len = next - name; if (*next == '.') next++; groups = bch2_sb_get_disk_groups(sb->sb); v = __bch2_disk_group_find(groups, parent, name, len); if (v < 0) v = __bch2_disk_group_add(sb, parent, name, len); if (v < 0) return v; parent = v + 1; name = next; } while (*name && v >= 0); return v; } void bch2_disk_path_to_text(struct printbuf *out, struct bch_sb_handle *sb, unsigned v) { struct bch_sb_field_disk_groups *groups = bch2_sb_get_disk_groups(sb->sb); struct bch_disk_group *g; unsigned nr = 0; u16 path[32]; while (1) { if (nr == ARRAY_SIZE(path)) goto inval; if (v >= disk_groups_nr(groups)) goto inval; g = groups->entries + v; if (BCH_GROUP_DELETED(g)) goto inval; path[nr++] = v; if (!BCH_GROUP_PARENT(g)) break; v = BCH_GROUP_PARENT(g) - 1; } while (nr) { v = path[--nr]; g = groups->entries + v; bch_scnmemcpy(out, g->label, strnlen(g->label, sizeof(g->label))); if (nr) pr_buf(out, "."); } return; inval: pr_buf(out, "invalid group %u", v); } int bch2_dev_group_set(struct bch_fs *c, struct bch_dev *ca, const char *name) { struct bch_member *mi; int v = -1; mutex_lock(&c->sb_lock); if (!strlen(name) || !strcmp(name, "none")) goto write_sb; v = bch2_disk_path_find_or_create(&c->disk_sb, name); if (v < 0) { mutex_unlock(&c->sb_lock); return v; } write_sb: mi = &bch2_sb_get_members(c->disk_sb.sb)->members[ca->dev_idx]; SET_BCH_MEMBER_GROUP(mi, v + 1); bch2_write_super(c); mutex_unlock(&c->sb_lock); return 0; } int bch2_opt_target_parse(struct bch_fs *c, const char *buf, u64 *v) { struct bch_dev *ca; int g; if (!strlen(buf) || !strcmp(buf, "none")) { *v = 0; return 0; } /* Is it a device? */ ca = bch2_dev_lookup(c, buf); if (!IS_ERR(ca)) { *v = dev_to_target(ca->dev_idx); percpu_ref_put(&ca->ref); return 0; } mutex_lock(&c->sb_lock); g = bch2_disk_path_find(&c->disk_sb, buf); mutex_unlock(&c->sb_lock); if (g >= 0) { *v = group_to_target(g); return 0; } return -EINVAL; } void bch2_opt_target_to_text(struct printbuf *out, struct bch_fs *c, u64 v) { struct target t = target_decode(v); switch (t.type) { case TARGET_NULL: pr_buf(out, "none"); break; case TARGET_DEV: { struct bch_dev *ca; rcu_read_lock(); ca = t.dev < c->sb.nr_devices ? rcu_dereference(c->devs[t.dev]) : NULL; if (ca && percpu_ref_tryget(&ca->io_ref)) { pr_buf(out, "/dev/%pg", ca->disk_sb.bdev); percpu_ref_put(&ca->io_ref); } else if (ca) { pr_buf(out, "offline device %u", t.dev); } else { pr_buf(out, "invalid device %u", t.dev); } rcu_read_unlock(); break; } case TARGET_GROUP: mutex_lock(&c->sb_lock); bch2_disk_path_to_text(out, &c->disk_sb, t.group); mutex_unlock(&c->sb_lock); break; default: BUG(); } }