| // SPDX-License-Identifier: GPL-2.0 | |
| #include "bcachefs.h" | |
| #include "disk_groups.h" | |
| #include "sb-members.h" | |
| #include "super-io.h" | |
| #include <linux/sort.h> | |
| 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 int bch2_sb_disk_groups_validate(struct bch_sb *sb, struct bch_sb_field *f, | |
| enum bch_validate_flags flags, struct printbuf *err) | |
| { | |
| struct bch_sb_field_disk_groups *groups = | |
| field_to_type(f, disk_groups); | |
| struct bch_disk_group *g, *sorted = NULL; | |
| unsigned nr_groups = disk_groups_nr(groups); | |
| unsigned i, len; | |
| int ret = 0; | |
| for (i = 0; i < sb->nr_devices; i++) { | |
| struct bch_member m = bch2_sb_member_get(sb, i); | |
| unsigned group_id; | |
| if (!BCH_MEMBER_GROUP(&m)) | |
| continue; | |
| group_id = BCH_MEMBER_GROUP(&m) - 1; | |
| if (group_id >= nr_groups) { | |
| prt_printf(err, "disk %u has invalid label %u (have %u)", | |
| i, group_id, nr_groups); | |
| return -BCH_ERR_invalid_sb_disk_groups; | |
| } | |
| if (BCH_GROUP_DELETED(&groups->entries[group_id])) { | |
| prt_printf(err, "disk %u has deleted label %u", i, group_id); | |
| return -BCH_ERR_invalid_sb_disk_groups; | |
| } | |
| } | |
| if (!nr_groups) | |
| return 0; | |
| for (i = 0; i < nr_groups; i++) { | |
| g = groups->entries + i; | |
| if (BCH_GROUP_DELETED(g)) | |
| continue; | |
| len = strnlen(g->label, sizeof(g->label)); | |
| if (!len) { | |
| prt_printf(err, "label %u empty", i); | |
| return -BCH_ERR_invalid_sb_disk_groups; | |
| } | |
| } | |
| sorted = kmalloc_array(nr_groups, sizeof(*sorted), GFP_KERNEL); | |
| if (!sorted) | |
| return -BCH_ERR_ENOMEM_disk_groups_validate; | |
| memcpy(sorted, groups->entries, nr_groups * sizeof(*sorted)); | |
| sort(sorted, nr_groups, sizeof(*sorted), group_cmp, NULL); | |
| for (g = sorted; g + 1 < sorted + nr_groups; g++) | |
| if (!BCH_GROUP_DELETED(g) && | |
| !group_cmp(&g[0], &g[1])) { | |
| prt_printf(err, "duplicate label %llu.%.*s", | |
| BCH_GROUP_PARENT(g), | |
| (int) sizeof(g->label), g->label); | |
| ret = -BCH_ERR_invalid_sb_disk_groups; | |
| goto err; | |
| } | |
| err: | |
| kfree(sorted); | |
| return ret; | |
| } | |
| 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) | |
| prt_printf(out, " "); | |
| if (BCH_GROUP_DELETED(g)) | |
| prt_printf(out, "[deleted]"); | |
| else | |
| prt_printf(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_disk_groups *groups; | |
| struct bch_disk_groups_cpu *cpu_g, *old_g; | |
| unsigned i, g, nr_groups; | |
| lockdep_assert_held(&c->sb_lock); | |
| groups = bch2_sb_field_get(c->disk_sb.sb, disk_groups); | |
| nr_groups = disk_groups_nr(groups); | |
| if (!groups) | |
| return 0; | |
| cpu_g = kzalloc(struct_size(cpu_g, entries, nr_groups), GFP_KERNEL); | |
| if (!cpu_g) | |
| return bch_err_throw(c, ENOMEM_disk_groups_to_cpu); | |
| 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); | |
| memcpy(dst->label, src->label, sizeof(dst->label)); | |
| } | |
| for (i = 0; i < c->disk_sb.sb->nr_devices; i++) { | |
| struct bch_member m = bch2_sb_member_get(c->disk_sb.sb, i); | |
| struct bch_disk_group_cpu *dst; | |
| if (!bch2_member_alive(&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); | |
| guard(rcu)(); | |
| 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 g && 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 = rcu_dereference(c->disk_groups); | |
| const struct bch_devs_mask *m = | |
| g && t.group < g->nr && !g->entries[t.group].deleted | |
| ? &g->entries[t.group].devs | |
| : NULL; | |
| return m ? test_bit(dev, m->d) : false; | |
| } | |
| 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_field_get(sb->sb, disk_groups); | |
| 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_field_resize(sb, disk_groups, u64s); | |
| if (!groups) | |
| return -BCH_ERR_ENOSPC_disk_label_add; | |
| 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_field_get(sb->sb, disk_groups); | |
| 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_field_get(sb->sb, disk_groups); | |
| 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; | |
| } | |
| static void __bch2_disk_path_to_text(struct printbuf *out, struct bch_disk_groups_cpu *g, | |
| unsigned v) | |
| { | |
| u16 path[32]; | |
| unsigned nr = 0; | |
| while (1) { | |
| if (nr == ARRAY_SIZE(path)) | |
| goto invalid; | |
| if (v >= (g ? g->nr : 0)) | |
| goto invalid; | |
| struct bch_disk_group_cpu *e = g->entries + v; | |
| if (e->deleted) | |
| goto invalid; | |
| path[nr++] = v; | |
| if (!e->parent) | |
| break; | |
| v = e->parent - 1; | |
| } | |
| while (nr) { | |
| struct bch_disk_group_cpu *e = g->entries + path[--nr]; | |
| prt_printf(out, "%.*s", (int) sizeof(e->label), e->label); | |
| if (nr) | |
| prt_printf(out, "."); | |
| } | |
| return; | |
| invalid: | |
| prt_printf(out, "invalid label %u", v); | |
| } | |
| void bch2_disk_groups_to_text(struct printbuf *out, struct bch_fs *c) | |
| { | |
| bch2_printbuf_make_room(out, 4096); | |
| out->atomic++; | |
| guard(rcu)(); | |
| struct bch_disk_groups_cpu *g = rcu_dereference(c->disk_groups); | |
| for (unsigned i = 0; i < (g ? g->nr : 0); i++) { | |
| prt_printf(out, "%2u: ", i); | |
| if (g->entries[i].deleted) { | |
| prt_printf(out, "[deleted]"); | |
| goto next; | |
| } | |
| __bch2_disk_path_to_text(out, g, i); | |
| prt_printf(out, " devs"); | |
| for_each_member_device_rcu(c, ca, &g->entries[i].devs) | |
| prt_printf(out, " %s", ca->name); | |
| next: | |
| prt_newline(out); | |
| } | |
| out->atomic--; | |
| } | |
| void bch2_disk_path_to_text(struct printbuf *out, struct bch_fs *c, unsigned v) | |
| { | |
| out->atomic++; | |
| guard(rcu)(); | |
| __bch2_disk_path_to_text(out, rcu_dereference(c->disk_groups), v), | |
| --out->atomic; | |
| } | |
| void bch2_disk_path_to_text_sb(struct printbuf *out, struct bch_sb *sb, unsigned v) | |
| { | |
| struct bch_sb_field_disk_groups *groups = | |
| bch2_sb_field_get(sb, disk_groups); | |
| 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; | |
| prt_printf(out, "%.*s", (int) sizeof(g->label), g->label); | |
| if (nr) | |
| prt_printf(out, "."); | |
| } | |
| return; | |
| inval: | |
| prt_printf(out, "invalid label %u", v); | |
| } | |
| int __bch2_dev_group_set(struct bch_fs *c, struct bch_dev *ca, const char *name) | |
| { | |
| lockdep_assert_held(&c->sb_lock); | |
| if (!strlen(name) || !strcmp(name, "none")) { | |
| struct bch_member *mi = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); | |
| SET_BCH_MEMBER_GROUP(mi, 0); | |
| } else { | |
| int v = bch2_disk_path_find_or_create(&c->disk_sb, name); | |
| if (v < 0) | |
| return v; | |
| struct bch_member *mi = bch2_members_v2_get_mut(c->disk_sb.sb, ca->dev_idx); | |
| SET_BCH_MEMBER_GROUP(mi, v + 1); | |
| } | |
| return bch2_sb_disk_groups_to_cpu(c); | |
| } | |
| int bch2_dev_group_set(struct bch_fs *c, struct bch_dev *ca, const char *name) | |
| { | |
| int ret; | |
| mutex_lock(&c->sb_lock); | |
| ret = __bch2_dev_group_set(c, ca, name) ?: | |
| bch2_write_super(c); | |
| mutex_unlock(&c->sb_lock); | |
| return ret; | |
| } | |
| int bch2_opt_target_parse(struct bch_fs *c, const char *val, u64 *res, | |
| struct printbuf *err) | |
| { | |
| struct bch_dev *ca; | |
| int g; | |
| if (!val) | |
| return -EINVAL; | |
| if (!c) | |
| return -BCH_ERR_option_needs_open_fs; | |
| if (!strlen(val) || !strcmp(val, "none")) { | |
| *res = 0; | |
| return 0; | |
| } | |
| /* Is it a device? */ | |
| ca = bch2_dev_lookup(c, val); | |
| if (!IS_ERR(ca)) { | |
| *res = dev_to_target(ca->dev_idx); | |
| bch2_dev_put(ca); | |
| return 0; | |
| } | |
| mutex_lock(&c->sb_lock); | |
| g = bch2_disk_path_find(&c->disk_sb, val); | |
| mutex_unlock(&c->sb_lock); | |
| if (g >= 0) { | |
| *res = group_to_target(g); | |
| return 0; | |
| } | |
| return -EINVAL; | |
| } | |
| void bch2_target_to_text(struct printbuf *out, struct bch_fs *c, unsigned v) | |
| { | |
| struct target t = target_decode(v); | |
| switch (t.type) { | |
| case TARGET_NULL: | |
| prt_printf(out, "none"); | |
| return; | |
| case TARGET_DEV: { | |
| out->atomic++; | |
| guard(rcu)(); | |
| struct bch_dev *ca = t.dev < c->sb.nr_devices | |
| ? rcu_dereference(c->devs[t.dev]) | |
| : NULL; | |
| if (ca && ca->disk_sb.bdev) | |
| prt_printf(out, "/dev/%s", ca->name); | |
| else if (ca) | |
| prt_printf(out, "offline device %u", t.dev); | |
| else | |
| prt_printf(out, "invalid device %u", t.dev); | |
| out->atomic--; | |
| return; | |
| } | |
| case TARGET_GROUP: | |
| bch2_disk_path_to_text(out, c, t.group); | |
| return; | |
| default: | |
| BUG(); | |
| } | |
| } | |
| static void bch2_target_to_text_sb(struct printbuf *out, struct bch_sb *sb, unsigned v) | |
| { | |
| struct target t = target_decode(v); | |
| switch (t.type) { | |
| case TARGET_NULL: | |
| prt_printf(out, "none"); | |
| break; | |
| case TARGET_DEV: { | |
| struct bch_member m = bch2_sb_member_get(sb, t.dev); | |
| if (bch2_member_exists(sb, t.dev)) { | |
| prt_printf(out, "Device "); | |
| pr_uuid(out, m.uuid.b); | |
| prt_printf(out, " (%u)", t.dev); | |
| } else { | |
| prt_printf(out, "Bad device %u", t.dev); | |
| } | |
| break; | |
| } | |
| case TARGET_GROUP: | |
| bch2_disk_path_to_text_sb(out, sb, t.group); | |
| break; | |
| default: | |
| BUG(); | |
| } | |
| } | |
| void bch2_opt_target_to_text(struct printbuf *out, | |
| struct bch_fs *c, | |
| struct bch_sb *sb, | |
| u64 v) | |
| { | |
| if (c) | |
| bch2_target_to_text(out, c, v); | |
| else | |
| bch2_target_to_text_sb(out, sb, v); | |
| } |