|  | // 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); | 
|  | } |