/* Copyright (c) 2018 Martin Wilck, SUSE Linux GmbH This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "nvme-lib.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "util.h" #include "vector.h" #include "generic.h" #include "foreign.h" #include "debug.h" #include "structs.h" #include "sysfs.h" static const char nvme_vendor[] = "NVMe"; static const char N_A[] = "n/a"; const char *THIS; struct nvme_map; struct nvme_pathgroup { struct gen_pathgroup gen; struct _vector pathvec; }; struct nvme_path { struct gen_path gen; struct udev_device *udev; struct udev_device *ctl; struct nvme_map *map; bool seen; /* * The kernel works in failover mode. * Each path has a separate path group. */ struct nvme_pathgroup pg; }; struct nvme_map { struct gen_multipath gen; struct udev_device *udev; struct udev_device *subsys; dev_t devt; struct _vector pgvec; int nr_live; int ana_supported; }; #define NAME_LEN 64 /* buffer length for temp attributes */ #define const_gen_mp_to_nvme(g) ((const struct nvme_map*)(g)) #define gen_mp_to_nvme(g) ((struct nvme_map*)(g)) #define nvme_mp_to_gen(n) &((n)->gen) #define const_gen_pg_to_nvme(g) ((const struct nvme_pathgroup*)(g)) #define gen_pg_to_nvme(g) ((struct nvme_pathgroup*)(g)) #define nvme_pg_to_gen(n) &((n)->gen) #define const_gen_path_to_nvme(g) ((const struct nvme_path*)(g)) #define gen_path_to_nvme(g) ((struct nvme_path*)(g)) #define nvme_path_to_gen(n) &((n)->gen) #define nvme_pg_to_path(x) (VECTOR_SLOT(&((x)->pathvec), 0)) #define nvme_path_to_pg(x) &((x)->pg) static void cleanup_nvme_path(struct nvme_path *path) { condlog(5, "%s: %p %p", __func__, path, path->udev); if (path->udev) udev_device_unref(path->udev); vector_reset(&path->pg.pathvec); /* ctl is implicitly referenced by udev, no need to unref */ free(path); } static void cleanup_nvme_map(struct nvme_map *map) { struct nvme_pathgroup *pg; struct nvme_path *path; int i; vector_foreach_slot_backwards(&map->pgvec, pg, i) { path = nvme_pg_to_path(pg); condlog(5, "%s: %d %p", __func__, i, path); cleanup_nvme_path(path); vector_del_slot(&map->pgvec, i); } vector_reset(&map->pgvec); if (map->udev) udev_device_unref(map->udev); /* subsys is implicitly referenced by udev, no need to unref */ free(map); } static const struct _vector* nvme_mp_get_pgs(const struct gen_multipath *gmp) { const struct nvme_map *nvme = const_gen_mp_to_nvme(gmp); /* This is all used under the lock, no need to copy */ return &nvme->pgvec; } static void nvme_mp_rel_pgs(__attribute__((unused)) const struct gen_multipath *gmp, __attribute__((unused)) const struct _vector *v) { /* empty */ } static void rstrip(char *str) { int n; for (n = strlen(str) - 1; n >= 0 && str[n] == ' '; n--); str[n+1] = '\0'; } static int snprint_nvme_map(const struct gen_multipath *gmp, char *buff, int len, char wildcard) { const struct nvme_map *nvm = const_gen_mp_to_nvme(gmp); char fld[NAME_LEN]; const char *val; switch (wildcard) { case 'd': return snprintf(buff, len, "%s", udev_device_get_sysname(nvm->udev)); case 'n': return snprintf(buff, len, "%s:nsid.%s", udev_device_get_sysattr_value(nvm->subsys, "subsysnqn"), udev_device_get_sysattr_value(nvm->udev, "nsid")); case 'w': return snprintf(buff, len, "%s", udev_device_get_sysattr_value(nvm->udev, "wwid")); case 'N': return snprintf(buff, len, "%u", nvm->nr_live); case 'S': return snprintf(buff, len, "%s", udev_device_get_sysattr_value(nvm->udev, "size")); case 'v': return snprintf(buff, len, "%s", nvme_vendor); case 's': case 'p': snprintf(fld, sizeof(fld), "%s", udev_device_get_sysattr_value(nvm->subsys, "model")); rstrip(fld); if (wildcard == 'p') return snprintf(buff, len, "%s", fld); return snprintf(buff, len, "%s,%s,%s", nvme_vendor, fld, udev_device_get_sysattr_value(nvm->subsys, "firmware_rev")); case 'e': return snprintf(buff, len, "%s", udev_device_get_sysattr_value(nvm->subsys, "firmware_rev")); case 'r': val = udev_device_get_sysattr_value(nvm->udev, "ro"); if (val[0] == 1) return snprintf(buff, len, "%s", "ro"); else return snprintf(buff, len, "%s", "rw"); case 'G': return snprintf(buff, len, "%s", THIS); case 'h': if (nvm->ana_supported == YNU_YES) return snprintf(buff, len, "ANA"); default: break; } return snprintf(buff, len, N_A); } static const struct _vector* nvme_pg_get_paths(const struct gen_pathgroup *gpg) { const struct nvme_pathgroup *gp = const_gen_pg_to_nvme(gpg); /* This is all used under the lock, no need to copy */ return &gp->pathvec; } static void nvme_pg_rel_paths(__attribute__((unused)) const struct gen_pathgroup *gpg, __attribute__((unused)) const struct _vector *v) { /* empty */ } static int snprint_hcil(const struct nvme_path *np, char *buf, int len) { unsigned int nvmeid, ctlid, nsid; int rc; const char *sysname = udev_device_get_sysname(np->udev); rc = sscanf(sysname, "nvme%uc%un%u", &nvmeid, &ctlid, &nsid); if (rc != 3) { condlog(1, "%s: failed to scan %s", __func__, sysname); rc = snprintf(buf, len, "(ERR:%s)", sysname); } else rc = snprintf(buf, len, "%u:%u:%u", nvmeid, ctlid, nsid); return (rc < len ? rc : len); } static int snprint_nvme_path(const struct gen_path *gp, char *buff, int len, char wildcard) { const struct nvme_path *np = const_gen_path_to_nvme(gp); dev_t devt; char fld[NAME_LEN]; struct udev_device *pci; switch (wildcard) { case 'w': return snprintf(buff, len, "%s", udev_device_get_sysattr_value(np->udev, "wwid")); case 'd': return snprintf(buff, len, "%s", udev_device_get_sysname(np->udev)); case 'i': return snprint_hcil(np, buff, len); case 'D': devt = udev_device_get_devnum(np->udev); return snprintf(buff, len, "%u:%u", major(devt), minor(devt)); case 'o': if (sysfs_attr_get_value(np->ctl, "state", fld, sizeof(fld)) > 0) return snprintf(buff, len, "%s", fld); break; case 'T': if (sysfs_attr_get_value(np->udev, "ana_state", fld, sizeof(fld)) > 0) return snprintf(buff, len, "%s", fld); break; case 'p': if (sysfs_attr_get_value(np->udev, "ana_state", fld, sizeof(fld)) > 0) { rstrip(fld); if (!strcmp(fld, "optimized")) return snprintf(buff, len, "%d", 50); else if (!strcmp(fld, "non-optimized")) return snprintf(buff, len, "%d", 10); else return snprintf(buff, len, "%d", 0); } break; case 's': snprintf(fld, sizeof(fld), "%s", udev_device_get_sysattr_value(np->ctl, "model")); rstrip(fld); return snprintf(buff, len, "%s,%s,%s", nvme_vendor, fld, udev_device_get_sysattr_value(np->ctl, "firmware_rev")); case 'S': return snprintf(buff, len, "%s", udev_device_get_sysattr_value(np->udev, "size")); case 'z': return snprintf(buff, len, "%s", udev_device_get_sysattr_value(np->ctl, "serial")); case 'm': return snprintf(buff, len, "%s", udev_device_get_sysname(np->map->udev)); case 'N': case 'R': return snprintf(buff, len, "%s:%s", udev_device_get_sysattr_value(np->ctl, "transport"), udev_device_get_sysattr_value(np->ctl, "address")); case 'G': return snprintf(buff, len, "[%s]", THIS); case 'a': pci = udev_device_get_parent_with_subsystem_devtype(np->ctl, "pci", NULL); if (pci != NULL) return snprintf(buff, len, "PCI:%s", udev_device_get_sysname(pci)); /* fall through */ default: break; } return snprintf(buff, len, "%s", N_A); return 0; } static int snprint_nvme_pg(const struct gen_pathgroup *gmp, char *buff, int len, char wildcard) { const struct nvme_pathgroup *pg = const_gen_pg_to_nvme(gmp); const struct nvme_path *path = nvme_pg_to_path(pg); switch (wildcard) { case 't': return snprint_nvme_path(nvme_path_to_gen(path), buff, len, 'T'); case 'p': return snprint_nvme_path(nvme_path_to_gen(path), buff, len, 'p'); default: return snprintf(buff, len, N_A); } } static int nvme_style(__attribute__((unused)) const struct gen_multipath* gm, char *buf, int len, __attribute__((unused)) int verbosity) { int n = snprintf(buf, len, "%%w [%%G]:%%d %%s"); return (n < len ? n : len - 1); } static const struct gen_multipath_ops nvme_map_ops = { .get_pathgroups = nvme_mp_get_pgs, .rel_pathgroups = nvme_mp_rel_pgs, .style = nvme_style, .snprint = snprint_nvme_map, }; static const struct gen_pathgroup_ops nvme_pg_ops __attribute__((unused)) = { .get_paths = nvme_pg_get_paths, .rel_paths = nvme_pg_rel_paths, .snprint = snprint_nvme_pg, }; static const struct gen_path_ops nvme_path_ops __attribute__((unused)) = { .snprint = snprint_nvme_path, }; struct context { pthread_mutex_t mutex; vector mpvec; struct udev *udev; }; void lock(struct context *ctx) { pthread_mutex_lock(&ctx->mutex); } void unlock(void *arg) { struct context *ctx = arg; pthread_mutex_unlock(&ctx->mutex); } static int _delete_all(struct context *ctx) { struct nvme_map *nm; int n = VECTOR_SIZE(ctx->mpvec), i; if (n == 0) return FOREIGN_IGNORED; vector_foreach_slot_backwards(ctx->mpvec, nm, i) { vector_del_slot(ctx->mpvec, i); cleanup_nvme_map(nm); } return FOREIGN_OK; } int delete_all(struct context *ctx) { int rc; condlog(5, "%s called for \"%s\"", __func__, THIS); lock(ctx); pthread_cleanup_push(unlock, ctx); rc = _delete_all(ctx); pthread_cleanup_pop(1); return rc; } void cleanup(struct context *ctx) { (void)delete_all(ctx); lock(ctx); /* * Locking is not strictly necessary here, locking in foreign.c * makes sure that no other code is called with this ctx any more. * But this should make static checkers feel better. */ pthread_cleanup_push(unlock, ctx); if (ctx->udev) udev_unref(ctx->udev); if (ctx->mpvec) vector_free(ctx->mpvec); ctx->mpvec = NULL; ctx->udev = NULL; pthread_cleanup_pop(1); pthread_mutex_destroy(&ctx->mutex); free(ctx); } struct context *init(unsigned int api, const char *name) { struct context *ctx; if (api > LIBMP_FOREIGN_API) { condlog(0, "%s: api version mismatch: %08x > %08x\n", __func__, api, LIBMP_FOREIGN_API); return NULL; } if ((ctx = calloc(1, sizeof(*ctx)))== NULL) return NULL; pthread_mutex_init(&ctx->mutex, NULL); ctx->udev = udev_new(); if (ctx->udev == NULL) goto err; ctx->mpvec = vector_alloc(); if (ctx->mpvec == NULL) goto err; THIS = name; return ctx; err: cleanup(ctx); return NULL; } static struct nvme_map *_find_nvme_map_by_devt(const struct context *ctx, dev_t devt) { struct nvme_map *nm; int i; if (ctx->mpvec == NULL) return NULL; vector_foreach_slot(ctx->mpvec, nm, i) { if (nm->devt == devt) return nm; } return NULL; } static struct nvme_path * _find_path_by_syspath(struct nvme_map *map, const char *syspath) { struct nvme_pathgroup *pg; char real[PATH_MAX]; const char *ppath; int i; ppath = realpath(syspath, real); if (ppath == NULL) { condlog(1, "%s: %s: error in realpath", __func__, THIS); ppath = syspath; } vector_foreach_slot(&map->pgvec, pg, i) { struct nvme_path *path = nvme_pg_to_path(pg); if (!strcmp(ppath, udev_device_get_syspath(path->udev))) return path; } condlog(4, "%s: %s: %s not found", __func__, THIS, ppath); return NULL; } static void _udev_device_unref(void *p) { udev_device_unref(p); } static void _udev_enumerate_unref(void *p) { udev_enumerate_unref(p); } static int _dirent_controller(const struct dirent *di) { static const char nvme_prefix[] = "nvme"; const char *p; #ifdef _DIRENT_HAVE_D_TYPE if (di->d_type != DT_LNK) return 0; #endif if (strncmp(di->d_name, nvme_prefix, sizeof(nvme_prefix) - 1)) return 0; p = di->d_name + sizeof(nvme_prefix) - 1; if (*p == '\0' || !isdigit(*p)) return 0; for (++p; *p != '\0'; ++p) if (!isdigit(*p)) return 0; return 1; } /* Find the block device for a given nvme controller */ struct udev_device *get_ctrl_blkdev(const struct context *ctx, struct udev_device *ctrl) { struct udev_list_entry *item; struct udev_device *blkdev = NULL; struct udev_enumerate *enm = udev_enumerate_new(ctx->udev); if (enm == NULL) return NULL; pthread_cleanup_push(_udev_enumerate_unref, enm); if (udev_enumerate_add_match_parent(enm, ctrl) < 0) goto out; if (udev_enumerate_add_match_subsystem(enm, "block")) goto out; if (udev_enumerate_scan_devices(enm) < 0) { condlog(1, "%s: %s: error enumerating devices", __func__, THIS); goto out; } for (item = udev_enumerate_get_list_entry(enm); item != NULL; item = udev_list_entry_get_next(item)) { struct udev_device *tmp; tmp = udev_device_new_from_syspath(ctx->udev, udev_list_entry_get_name(item)); if (tmp == NULL) continue; if (!strcmp(udev_device_get_devtype(tmp), "disk")) { blkdev = tmp; break; } else udev_device_unref(tmp); } if (blkdev == NULL) condlog(1, "%s: %s: failed to get blockdev for %s", __func__, THIS, udev_device_get_sysname(ctrl)); else condlog(5, "%s: %s: got %s", __func__, THIS, udev_device_get_sysname(blkdev)); out: pthread_cleanup_pop(1); return blkdev; } static void test_ana_support(struct nvme_map *map, struct udev_device *ctl) { const char *dev_t; char sys_path[64]; long fd; int rc; if (map->ana_supported != YNU_UNDEF) return; dev_t = udev_device_get_sysattr_value(ctl, "dev"); if (safe_sprintf(sys_path, "/dev/char/%s", dev_t)) return; fd = open(sys_path, O_RDONLY); if (fd == -1) { condlog(2, "%s: error opening %s", __func__, sys_path); return; } pthread_cleanup_push(close_fd, (void *)fd); rc = nvme_id_ctrl_ana(fd, NULL); if (rc < 0) condlog(2, "%s: error in nvme_id_ctrl: %s", __func__, strerror(errno)); else { map->ana_supported = (rc == 1 ? YNU_YES : YNU_NO); condlog(3, "%s: NVMe ctrl %s: ANA %s supported", __func__, dev_t, rc == 1 ? "is" : "is not"); } pthread_cleanup_pop(1); } static void _find_controllers(struct context *ctx, struct nvme_map *map) { char pathbuf[PATH_MAX], realbuf[PATH_MAX]; struct dirent **di = NULL; struct scandir_result sr; struct udev_device *subsys; struct nvme_pathgroup *pg; struct nvme_path *path; int r, i, n; if (map == NULL || map->udev == NULL) return; vector_foreach_slot(&map->pgvec, pg, i) { path = nvme_pg_to_path(pg); path->seen = false; } subsys = udev_device_get_parent_with_subsystem_devtype(map->udev, "nvme-subsystem", NULL); if (subsys == NULL) { condlog(1, "%s: %s: BUG: no NVME subsys for %s", __func__, THIS, udev_device_get_sysname(map->udev)); return; } n = snprintf(pathbuf, sizeof(pathbuf), "%s", udev_device_get_syspath(subsys)); r = scandir(pathbuf, &di, _dirent_controller, alphasort); if (r == 0) { condlog(3, "%s: %s: no controllers for %s", __func__, THIS, udev_device_get_sysname(map->udev)); return; } else if (r < 0) { condlog(1, "%s: %s: error %d scanning controllers of %s", __func__, THIS, errno, udev_device_get_sysname(map->udev)); return; } sr.di = di; sr.n = r; pthread_cleanup_push_cast(free_scandir_result, &sr); for (i = 0; i < r; i++) { char *fn = di[i]->d_name; struct udev_device *ctrl, *udev; if (safe_snprintf(pathbuf + n, sizeof(pathbuf) - n, "/%s", fn)) continue; if (realpath(pathbuf, realbuf) == NULL) { condlog(3, "%s: %s: realpath: %s", __func__, THIS, strerror(errno)); continue; } condlog(4, "%s: %s: found %s", __func__, THIS, realbuf); ctrl = udev_device_new_from_syspath(ctx->udev, realbuf); if (ctrl == NULL) { condlog(1, "%s: %s: failed to get udev device for %s", __func__, THIS, realbuf); continue; } pthread_cleanup_push(_udev_device_unref, ctrl); udev = get_ctrl_blkdev(ctx, ctrl); /* * We give up the reference to the nvme device here and get * it back from the child below. * This way we don't need to worry about unreffing it. */ pthread_cleanup_pop(1); if (udev == NULL) continue; path = _find_path_by_syspath(map, udev_device_get_syspath(udev)); if (path != NULL) { path->seen = true; condlog(4, "%s: %s already known", __func__, fn); continue; } path = calloc(1, sizeof(*path)); if (path == NULL) continue; path->gen.ops = &nvme_path_ops; path->udev = udev; path->seen = true; path->map = map; path->ctl = udev_device_get_parent_with_subsystem_devtype (udev, "nvme", NULL); if (path->ctl == NULL) { condlog(1, "%s: %s: failed to get controller for %s", __func__, THIS, fn); cleanup_nvme_path(path); continue; } test_ana_support(map, path->ctl); path->pg.gen.ops = &nvme_pg_ops; if (vector_alloc_slot(&path->pg.pathvec) == NULL) { cleanup_nvme_path(path); continue; } vector_set_slot(&path->pg.pathvec, path); if (vector_alloc_slot(&map->pgvec) == NULL) { cleanup_nvme_path(path); continue; } vector_set_slot(&map->pgvec, &path->pg); condlog(3, "%s: %s: new path %s added to %s", __func__, THIS, udev_device_get_sysname(udev), udev_device_get_sysname(map->udev)); } pthread_cleanup_pop(1); map->nr_live = 0; vector_foreach_slot_backwards(&map->pgvec, pg, i) { path = nvme_pg_to_path(pg); if (!path->seen) { condlog(1, "path %d not found in %s any more", i, udev_device_get_sysname(map->udev)); vector_del_slot(&map->pgvec, i); cleanup_nvme_path(path); } else { static const char live_state[] = "live"; char state[16]; if ((sysfs_attr_get_value(path->ctl, "state", state, sizeof(state)) > 0) && !strncmp(state, live_state, sizeof(live_state) - 1)) map->nr_live++; } } condlog(3, "%s: %s: map %s has %d/%d live paths", __func__, THIS, udev_device_get_sysname(map->udev), map->nr_live, VECTOR_SIZE(&map->pgvec)); } static int _add_map(struct context *ctx, struct udev_device *ud, struct udev_device *subsys) { dev_t devt = udev_device_get_devnum(ud); struct nvme_map *map; if (_find_nvme_map_by_devt(ctx, devt) != NULL) return FOREIGN_OK; map = calloc(1, sizeof(*map)); if (map == NULL) return FOREIGN_ERR; map->devt = devt; map->udev = udev_device_ref(ud); /* * subsys is implicitly referenced by map->udev, * no need to take a reference here. */ map->subsys = subsys; map->gen.ops = &nvme_map_ops; if (vector_alloc_slot(ctx->mpvec) == NULL) { cleanup_nvme_map(map); return FOREIGN_ERR; } vector_set_slot(ctx->mpvec, map); _find_controllers(ctx, map); return FOREIGN_CLAIMED; } int add(struct context *ctx, struct udev_device *ud) { struct udev_device *subsys; int rc; condlog(5, "%s called for \"%s\"", __func__, THIS); if (ud == NULL) return FOREIGN_ERR; if (strcmp("disk", udev_device_get_devtype(ud))) return FOREIGN_IGNORED; subsys = udev_device_get_parent_with_subsystem_devtype(ud, "nvme-subsystem", NULL); if (subsys == NULL) return FOREIGN_IGNORED; lock(ctx); pthread_cleanup_push(unlock, ctx); rc = _add_map(ctx, ud, subsys); pthread_cleanup_pop(1); if (rc == FOREIGN_CLAIMED) condlog(3, "%s: %s: added map %s", __func__, THIS, udev_device_get_sysname(ud)); else if (rc != FOREIGN_OK) condlog(1, "%s: %s: retcode %d adding %s", __func__, THIS, rc, udev_device_get_sysname(ud)); return rc; } int change(__attribute__((unused)) struct context *ctx, __attribute__((unused)) struct udev_device *ud) { condlog(5, "%s called for \"%s\"", __func__, THIS); return FOREIGN_IGNORED; } static int _delete_map(struct context *ctx, struct udev_device *ud) { int k; struct nvme_map *map; dev_t devt = udev_device_get_devnum(ud); map = _find_nvme_map_by_devt(ctx, devt); if (map ==NULL) return FOREIGN_IGNORED; k = find_slot(ctx->mpvec, map); if (k == -1) return FOREIGN_ERR; else vector_del_slot(ctx->mpvec, k); cleanup_nvme_map(map); return FOREIGN_OK; } int delete(struct context *ctx, struct udev_device *ud) { int rc; condlog(5, "%s called for \"%s\"", __func__, THIS); if (ud == NULL) return FOREIGN_ERR; lock(ctx); pthread_cleanup_push(unlock, ctx); rc = _delete_map(ctx, ud); pthread_cleanup_pop(1); if (rc == FOREIGN_OK) condlog(3, "%s: %s: map %s deleted", __func__, THIS, udev_device_get_sysname(ud)); else if (rc != FOREIGN_IGNORED) condlog(1, "%s: %s: retcode %d deleting map %s", __func__, THIS, rc, udev_device_get_sysname(ud)); return rc; } void _check(struct context *ctx) { struct gen_multipath *gm; int i; vector_foreach_slot(ctx->mpvec, gm, i) { struct nvme_map *map = gen_mp_to_nvme(gm); _find_controllers(ctx, map); } } void check(struct context *ctx) { condlog(4, "%s called for \"%s\"", __func__, THIS); lock(ctx); pthread_cleanup_push(unlock, ctx); _check(ctx); pthread_cleanup_pop(1); return; } /* * It's safe to pass our internal pointer, this is only used under the lock. */ const struct _vector *get_multipaths(const struct context *ctx) { condlog(5, "%s called for \"%s\"", __func__, THIS); return ctx->mpvec; } void release_multipaths(__attribute__((unused)) const struct context *ctx, __attribute__((unused)) const struct _vector *mpvec) { condlog(5, "%s called for \"%s\"", __func__, THIS); /* NOP */ } /* * It's safe to pass our internal pointer, this is only used under the lock. */ const struct _vector * get_paths(const struct context *ctx) { vector paths = NULL; const struct gen_multipath *gm; int i; condlog(5, "%s called for \"%s\"", __func__, THIS); vector_foreach_slot(ctx->mpvec, gm, i) { const struct nvme_map *nm = const_gen_mp_to_nvme(gm); paths = vector_convert(paths, &nm->pgvec, struct nvme_pathgroup, nvme_pg_to_path); } return paths; } void release_paths(__attribute__((unused)) const struct context *ctx, const struct _vector *mpvec) { condlog(5, "%s called for \"%s\"", __func__, THIS); vector_free_const(mpvec); } /* compile-time check whether all methods are present and correctly typed */ #define _METHOD_INIT(x) .x = x static struct foreign __methods __attribute__((unused)) = { _METHOD_INIT(init), _METHOD_INIT(cleanup), _METHOD_INIT(change), _METHOD_INIT(delete), _METHOD_INIT(delete_all), _METHOD_INIT(check), _METHOD_INIT(lock), _METHOD_INIT(unlock), _METHOD_INIT(get_multipaths), _METHOD_INIT(release_multipaths), _METHOD_INIT(get_paths), _METHOD_INIT(release_paths), };