From 66eb268909e4bdc0ceb72e4d3f5aa3e98c254d8c Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Sat, 28 Dec 2024 15:30:21 +0000 Subject: [PATCH] DAOS-10028 client: Add Go bindings for libdaos (Container) Continue the work of converting the raw cgo in the daos tool into proper Go bindings for libdaos. This patch covers most container functionality. NB: Container create and properties will be addressed in follow-on patches. Features: daos_cmd --- src/cart/utils/memcheck-cart.supp | 6 + src/control/cmd/daos/acl.go | 14 +- src/control/cmd/daos/attribute.go | 230 +---- src/control/cmd/daos/container.go | 570 +++--------- src/control/cmd/daos/container_test.go | 307 ++++++ src/control/cmd/daos/filesystem.go | 20 +- src/control/cmd/daos/flags.go | 21 +- src/control/cmd/daos/flags_test.go | 3 +- src/control/cmd/daos/health.go | 46 +- src/control/cmd/daos/object.go | 3 +- src/control/cmd/daos/pool_test.go | 13 +- src/control/cmd/daos/pretty/container.go | 95 ++ src/control/cmd/daos/snapshot.go | 11 +- src/control/cmd/daos/stubbed.go | 7 +- src/control/lib/daos/api/attribute.go | 24 +- src/control/lib/daos/api/container.go | 560 +++++++++++ src/control/lib/daos/api/container_test.go | 880 ++++++++++++++++++ src/control/lib/daos/api/errors.go | 12 +- src/control/lib/daos/api/handle.go | 28 +- src/control/lib/daos/api/handle_test.go | 119 +++ src/control/lib/daos/api/libdaos.go | 52 ++ .../lib/daos/api/libdaos_cont_stubs.go | 291 ++++++ .../lib/daos/api/libdaos_pool_stubs.go | 29 + src/control/lib/daos/api/libdaos_stubs.go | 37 +- src/control/lib/daos/api/libdfs.go | 29 + src/control/lib/daos/api/libdfs_stubs.go | 73 ++ src/control/lib/daos/api/object.go | 53 ++ src/control/lib/daos/api/pool.go | 100 +- src/control/lib/daos/api/pool_test.go | 206 ++-- src/control/lib/daos/api/test_stubs.go | 2 + src/control/lib/daos/api/util.go | 17 +- src/control/lib/daos/cont_prop.go | 290 ++++++ src/control/lib/daos/container.go | 148 ++- src/control/lib/daos/hlc.go | 5 + src/control/lib/daos/libgurt.go | 2 + src/control/lib/daos/object.go | 68 ++ src/control/lib/daos/pool_cont_prop.go | 48 +- src/control/lib/daos/property.go | 185 ++++ src/include/daos/cont_props.h | 5 +- utils/node_local_test.py | 4 +- 40 files changed, 3605 insertions(+), 1008 deletions(-) create mode 100644 src/control/cmd/daos/pretty/container.go create mode 100644 src/control/lib/daos/api/container.go create mode 100644 src/control/lib/daos/api/container_test.go create mode 100644 src/control/lib/daos/api/handle_test.go create mode 100644 src/control/lib/daos/api/libdaos_cont_stubs.go create mode 100644 src/control/lib/daos/api/libdfs.go create mode 100644 src/control/lib/daos/api/libdfs_stubs.go create mode 100644 src/control/lib/daos/api/object.go create mode 100644 src/control/lib/daos/cont_prop.go create mode 100644 src/control/lib/daos/object.go create mode 100644 src/control/lib/daos/property.go diff --git a/src/cart/utils/memcheck-cart.supp b/src/cart/utils/memcheck-cart.supp index 7f0420fb07e..12687079729 100644 --- a/src/cart/utils/memcheck-cart.supp +++ b/src/cart/utils/memcheck-cart.supp @@ -432,6 +432,12 @@ ... fun:indexbytebody } +{ + + Memcheck:Addr16 + ... + fun:indexbytebody +} { Memcheck:Addr32 diff --git a/src/control/cmd/daos/acl.go b/src/control/cmd/daos/acl.go index 34d5b8491f4..e379319eb0d 100644 --- a/src/control/cmd/daos/acl.go +++ b/src/control/cmd/daos/acl.go @@ -1,5 +1,6 @@ // // (C) Copyright 2021-2024 Intel Corporation. +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -42,6 +43,7 @@ import ( "github.com/pkg/errors" "github.com/daos-stack/daos/src/control/lib/control" + "github.com/daos-stack/daos/src/control/lib/daos" ) func getAclStrings(e *C.struct_daos_prop_entry) (out []string) { @@ -154,7 +156,7 @@ func (cmd *containerOverwriteACLCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } @@ -244,7 +246,7 @@ func (cmd *containerUpdateACLCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } @@ -278,7 +280,7 @@ func (cmd *containerDeleteACLCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } @@ -340,7 +342,7 @@ func (cmd *containerGetACLCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RO, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadOnly, ap) if err != nil { return err } @@ -348,7 +350,7 @@ func (cmd *containerGetACLCmd) Execute(args []string) error { acl, err := cmd.getACL(ap) if err != nil { - return errors.Wrapf(err, "failed to query ACL for container %s", cmd.contUUID) + return errors.Wrapf(err, "failed to query ACL for container %s", cmd.ContainerID()) } if cmd.File != "" { @@ -398,7 +400,7 @@ func (cmd *containerSetOwnerCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } diff --git a/src/control/cmd/daos/attribute.go b/src/control/cmd/daos/attribute.go index 677d2beaebe..fc4e45fb553 100644 --- a/src/control/cmd/daos/attribute.go +++ b/src/control/cmd/daos/attribute.go @@ -1,5 +1,5 @@ // -// (C) Copyright 2018-2024 Intel Corporation. +// (C) Copyright 2018-2021 Intel Corporation. // (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent @@ -11,7 +11,6 @@ import ( "context" "fmt" "strings" - "unsafe" "github.com/pkg/errors" @@ -21,11 +20,6 @@ import ( "github.com/daos-stack/daos/src/control/logging" ) -/* -#include "util.h" -*/ -import "C" - type attrType int const ( @@ -150,225 +144,3 @@ func delAttributes(cmd attrCmd, ad attrDeleter, at attrType, id string, names .. return nil } - -// NB: These will be removed in the next patch, which adds the container APIs. -func listDaosAttributes(hdl C.daos_handle_t, at attrType, verbose bool) (daos.AttributeList, error) { - var rc C.int - expectedSize, totalSize := C.size_t(0), C.size_t(0) - - switch at { - case contAttr: - rc = C.daos_cont_list_attr(hdl, nil, &totalSize, nil) - default: - return nil, errors.Errorf("unknown attr type %d", at) - } - if err := daosError(rc); err != nil { - return nil, err - } - - if totalSize < 1 { - return nil, nil - } - - attrNames := []string{} - expectedSize = totalSize - buf := C.malloc(totalSize) - defer C.free(buf) - - switch at { - case contAttr: - rc = C.daos_cont_list_attr(hdl, (*C.char)(buf), &totalSize, nil) - default: - return nil, errors.Errorf("unknown attr type %d", at) - } - if err := daosError(rc); err != nil { - return nil, err - } - - if err := iterStringsBuf(buf, expectedSize, func(name string) { - attrNames = append(attrNames, name) - }); err != nil { - return nil, err - } - - if verbose { - return getDaosAttributes(hdl, at, attrNames) - } - - attrs := make(daos.AttributeList, len(attrNames)) - for i, name := range attrNames { - attrs[i] = &daos.Attribute{Name: name} - } - - return attrs, nil - -} - -// getDaosAttributes fetches the values for the given list of attribute names. -// Uses the bulk attribute fetch API to minimize roundtrips. -func getDaosAttributes(hdl C.daos_handle_t, at attrType, names []string) (daos.AttributeList, error) { - if len(names) == 0 { - attrList, err := listDaosAttributes(hdl, at, false) - if err != nil { - return nil, errors.Wrap(err, "failed to list attributes") - } - names = make([]string, len(attrList)) - for i, attr := range attrList { - names[i] = attr.Name - } - } - numAttr := len(names) - - // First, build a slice of C strings for the attribute names. - attrNames := make([]*C.char, numAttr) - for i, name := range names { - attrNames[i] = C.CString(name) - } - defer func(nameSlice []*C.char) { - for _, name := range nameSlice { - freeString(name) - } - }(attrNames) - - // Next, create a slice of C.size_t entries to hold the sizes of the values. - // We have to do this first in order to know the buffer sizes to allocate - // before fetching the actual values. - attrSizes := make([]C.size_t, numAttr) - var rc C.int - switch at { - case contAttr: - rc = C.daos_cont_get_attr(hdl, C.int(numAttr), &attrNames[0], nil, &attrSizes[0], nil) - default: - return nil, errors.Errorf("unknown attr type %d", at) - } - if err := daosError(rc); err != nil { - return nil, errors.Wrapf(err, "failed to get attribute sizes: %s...", names[0]) - } - - // Now, create a slice of buffers to hold the values. - attrValues := make([]unsafe.Pointer, numAttr) - defer func(valueSlice []unsafe.Pointer) { - for _, value := range valueSlice { - C.free(value) - } - }(attrValues) - for i, size := range attrSizes { - if size < 1 { - return nil, errors.Errorf("failed to get attribute %s: size is %d", names[i], size) - } - - attrValues[i] = C.malloc(size) - } - - // Do the actual fetch of all values in one go. - switch at { - case contAttr: - rc = C.daos_cont_get_attr(hdl, C.int(numAttr), &attrNames[0], &attrValues[0], &attrSizes[0], nil) - default: - return nil, errors.Errorf("unknown attr type %d", at) - } - if err := daosError(rc); err != nil { - return nil, errors.Wrapf(err, "failed to get attribute values: %s...", names[0]) - } - - // Finally, create a slice of attribute structs to hold the results. - // Note that we are copying the values into Go-managed byte slices - // for safety and simplicity so that we can free the C memory as soon - // as this function exits. - attrs := make(daos.AttributeList, numAttr) - for i, name := range names { - attrs[i] = &daos.Attribute{ - Name: name, - Value: C.GoBytes(attrValues[i], C.int(attrSizes[i])), - } - } - - return attrs, nil -} - -// getDaosAttribute fetches the value for the given attribute name. -// NB: For operations involving multiple attributes, the getDaosAttributes() -// function is preferred for efficiency. -func getDaosAttribute(hdl C.daos_handle_t, at attrType, name string) (*daos.Attribute, error) { - attrs, err := getDaosAttributes(hdl, at, []string{name}) - if err != nil { - return nil, err - } - if len(attrs) == 0 { - return nil, errors.Errorf("attribute %q not found", name) - } - return attrs[0], nil -} - -// setDaosAttributes sets the values for the given list of attribute names. -// Uses the bulk attribute set API to minimize roundtrips. -func setDaosAttributes(hdl C.daos_handle_t, at attrType, attrs daos.AttributeList) error { - if len(attrs) == 0 { - return nil - } - - // First, build a slice of C strings for the attribute names. - attrNames := make([]*C.char, len(attrs)) - for i, attr := range attrs { - attrNames[i] = C.CString(attr.Name) - } - defer func(nameSlice []*C.char) { - for _, name := range nameSlice { - freeString(name) - } - }(attrNames) - - // Next, create a slice of C.size_t entries to hold the sizes of the values, - // and a slice of pointers to the actual values. - valSizes := make([]C.size_t, len(attrs)) - valBufs := make([]unsafe.Pointer, len(attrs)) - for i, attr := range attrs { - valSizes[i] = C.size_t(len(attr.Value)) - // NB: We are copying the values into C memory for safety and simplicity. - valBufs[i] = C.malloc(valSizes[i]) - valSlice := (*[1 << 30]byte)(valBufs[i]) - copy(valSlice[:], attr.Value) - } - defer func(bufSlice []unsafe.Pointer) { - for _, buf := range bufSlice { - C.free(buf) - } - }(valBufs) - - attrCount := C.int(len(attrs)) - var rc C.int - switch at { - case contAttr: - rc = C.daos_cont_set_attr(hdl, attrCount, &attrNames[0], &valBufs[0], &valSizes[0], nil) - default: - return errors.Errorf("unknown attr type %d", at) - } - - return daosError(rc) -} - -// setDaosAttribute sets the value for the given attribute name. -// NB: For operations involving multiple attributes, the setDaosAttributes() -// function is preferred for efficiency. -func setDaosAttribute(hdl C.daos_handle_t, at attrType, attr *daos.Attribute) error { - if attr == nil { - return errors.Errorf("nil %T", attr) - } - - return setDaosAttributes(hdl, at, daos.AttributeList{attr}) -} - -func delDaosAttribute(hdl C.daos_handle_t, at attrType, name string) error { - attrName := C.CString(name) - defer freeString(attrName) - - var rc C.int - switch at { - case contAttr: - rc = C.daos_cont_del_attr(hdl, 1, &attrName, nil) - default: - return errors.Errorf("unknown attr type %d", at) - } - - return daosError(rc) -} diff --git a/src/control/cmd/daos/container.go b/src/control/cmd/daos/container.go index 8d8980e8002..a089816cbf4 100644 --- a/src/control/cmd/daos/container.go +++ b/src/control/cmd/daos/container.go @@ -9,20 +9,19 @@ package main import ( "fmt" - "io" "os" "path/filepath" "strings" "unsafe" - "github.com/dustin/go-humanize" "github.com/google/uuid" "github.com/jessevdk/go-flags" "github.com/pkg/errors" "github.com/daos-stack/daos/src/control/cmd/daos/pretty" + "github.com/daos-stack/daos/src/control/common" "github.com/daos-stack/daos/src/control/lib/daos" - "github.com/daos-stack/daos/src/control/lib/txtfmt" + "github.com/daos-stack/daos/src/control/lib/daos/api" "github.com/daos-stack/daos/src/control/lib/ui" "github.com/daos-stack/daos/src/control/logging" ) @@ -67,160 +66,64 @@ type containerCmd struct { type containerBaseCmd struct { poolBaseCmd - contUUID uuid.UUID - contLabel string + container *api.ContainerHandle + // deprecated params -- gradually remove in favor of ContainerHandle + contLabel string + contUUID uuid.UUID cContHandle C.daos_handle_t } -func (cmd *containerBaseCmd) contUUIDPtr() *C.uchar { - if cmd.contUUID == uuid.Nil { - cmd.Error("contUUIDPtr(): nil UUID") - return nil - } - return (*C.uchar)(unsafe.Pointer(&cmd.contUUID[0])) -} - -func containerOpen(poolHdl C.daos_handle_t, contID string, flags uint, query bool) (C.daos_handle_t, *C.daos_cont_info_t, error) { - cContID := C.CString(contID) - defer freeString(cContID) - - var contHdl C.daos_handle_t - var infoPtr *C.daos_cont_info_t - if query { - infoPtr = new(C.daos_cont_info_t) - } - - return contHdl, infoPtr, containerOpenAPI(poolHdl, cContID, C.uint(flags), &contHdl, infoPtr) -} - -func containerOpenAPI(poolHdl C.daos_handle_t, contID *C.char, flags C.uint, contHdl *C.daos_handle_t, contInfo *C.daos_cont_info_t) error { - return daosError(C.daos_cont_open(poolHdl, contID, flags, contHdl, contInfo, nil)) -} - -func containerCloseAPI(contHdl C.daos_handle_t) error { - // Hack for NLT fault injection testing: If the rc - // is -DER_NOMEM, retry once in order to actually - // shut down and release resources. - rc := C.daos_cont_close(contHdl, nil) - if rc == -C.DER_NOMEM { - rc = C.daos_cont_close(contHdl, nil) +func (cmd *containerBaseCmd) openContainer(openFlags daos.ContainerOpenFlag) error { + openFlags |= daos.ContainerOpenFlagForce + if (openFlags & daos.ContainerOpenFlagReadOnly) != 0 { + openFlags |= daos.ContainerOpenFlagReadOnlyMetadata } - return daosError(rc) -} - -func (cmd *containerBaseCmd) openContainer(openFlags C.uint) error { - openFlags |= C.DAOS_COO_FORCE - if (openFlags & C.DAOS_COO_RO) != 0 { - openFlags |= C.DAOS_COO_RO_MDSTATS - } - - var rc C.int + var containerID string switch { case cmd.contLabel != "": - var contInfo C.daos_cont_info_t - cLabel := C.CString(cmd.contLabel) - defer freeString(cLabel) - - cmd.Debugf("opening container: %s", cmd.contLabel) - if err := containerOpenAPI(cmd.cPoolHandle, cLabel, openFlags, &cmd.cContHandle, &contInfo); err != nil { - return err - } - - var err error - cmd.contUUID, err = uuidFromC(contInfo.ci_uuid) - if err != nil { - cmd.closeContainer() - return err - } + containerID = cmd.contLabel case cmd.contUUID != uuid.Nil: - cmd.Debugf("opening container: %s", cmd.contUUID) - cUUIDstr := C.CString(cmd.contUUID.String()) - defer freeString(cUUIDstr) - if err := containerOpenAPI(cmd.cPoolHandle, cUUIDstr, openFlags, &cmd.cContHandle, nil); err != nil { - return err - } + containerID = cmd.contUUID.String() default: return errors.New("no container UUID or label supplied") } - return daosError(rc) -} - -func (cmd *containerBaseCmd) closeContainer() { - cmd.Debugf("closing container: %s", cmd.contUUID) - if err := containerCloseAPI(cmd.cContHandle); err != nil { - cmd.Errorf("container close failed: %s", err) + var err error + var resp *api.ContainerOpenResp + ctx := cmd.MustLogCtx() + if cmd.pool != nil { + resp, err = cmd.pool.OpenContainer(ctx, api.ContainerOpenReq{ + ID: containerID, + Flags: openFlags, + }) + } else { + resp, err = ContainerOpen(ctx, api.ContainerOpenReq{ + ID: containerID, + Flags: openFlags, + SysName: cmd.SysName, + PoolID: cmd.PoolID().String(), + }) } -} - -func queryContainer(poolUUID, contUUID uuid.UUID, poolHandle, contHandle C.daos_handle_t) (*daos.ContainerInfo, error) { - var cInfo C.daos_cont_info_t - var cType [10]C.char - - props, entries, err := allocProps(3) if err != nil { - return nil, err - } - entries[0].dpe_type = C.DAOS_PROP_CO_LAYOUT_TYPE - props.dpp_nr++ - entries[1].dpe_type = C.DAOS_PROP_CO_LABEL - props.dpp_nr++ - entries[2].dpe_type = C.DAOS_PROP_CO_REDUN_FAC - props.dpp_nr++ - defer func() { C.daos_prop_free(props) }() - - rc := C.daos_cont_query(contHandle, &cInfo, props, nil) - if err := daosError(rc); err != nil { - return nil, err + return err } + cmd.container = resp.Connection - ci := convertContainerInfo(poolUUID, contUUID, &cInfo) - lType := C.get_dpe_val(&entries[0]) - C.daos_unparse_ctype(C.ushort(lType), &cType[0]) - ci.Type = C.GoString(&cType[0]) - - if C.get_dpe_str(&entries[1]) == nil { - ci.ContainerLabel = "" - } else { - cStr := C.get_dpe_str(&entries[1]) - ci.ContainerLabel = C.GoString(cStr) + // needed for compat with older code + if err := cmd.container.FillHandle(unsafe.Pointer(&cmd.cContHandle)); err != nil { + cmd.closeContainer() + return err } - ci.RedundancyFactor = uint32(C.get_dpe_val(&entries[2])) - - if lType == C.DAOS_PROP_CO_LAYOUT_POSIX { - var dfs *C.dfs_t - var attr C.dfs_attr_t - var oclass [C.MAX_OBJ_CLASS_NAME_LEN]C.char - var dir_oclass [C.MAX_OBJ_CLASS_NAME_LEN]C.char - var file_oclass [C.MAX_OBJ_CLASS_NAME_LEN]C.char - - rc := C.dfs_mount(poolHandle, contHandle, C.O_RDONLY, &dfs) - if err := dfsError(rc); err != nil { - return nil, errors.Wrap(err, "failed to mount container") - } + return nil +} - rc = C.dfs_query(dfs, &attr) - if err := dfsError(rc); err != nil { - return nil, errors.Wrap(err, "failed to query container") - } - C.daos_oclass_id2name(attr.da_oclass_id, &oclass[0]) - ci.ObjectClass = C.GoString(&oclass[0]) - C.daos_oclass_id2name(attr.da_dir_oclass_id, &dir_oclass[0]) - ci.DirObjectClass = C.GoString(&dir_oclass[0]) - C.daos_oclass_id2name(attr.da_file_oclass_id, &file_oclass[0]) - ci.FileObjectClass = C.GoString(&file_oclass[0]) - ci.CHints = C.GoString(&attr.da_hints[0]) - ci.ChunkSize = uint64(attr.da_chunk_size) - - if err := dfsError(C.dfs_umount(dfs)); err != nil { - return nil, errors.Wrap(err, "failed to unmount container") - } +func (cmd *containerBaseCmd) closeContainer() { + if err := cmd.container.Close(cmd.MustLogCtx()); err != nil { + cmd.Errorf("container close failed: %s", err) } - - return ci, nil } func (cmd *containerBaseCmd) connectPool(flags daos.PoolConnectFlag, ap *C.struct_cmd_args_s) (func(), error) { @@ -313,13 +216,7 @@ func (cmd *containerCreateCmd) Execute(_ []string) (err error) { return err } - if err := cmd.openContainer(C.DAOS_COO_RO); err != nil { - return errors.Wrapf(err, "failed to open new container %s", contID) - } - defer cmd.closeContainer() - - var ci *daos.ContainerInfo - ci, err = queryContainer(cmd.pool.UUID(), cmd.contUUID, cmd.cPoolHandle, cmd.cContHandle) + ci, err := cmd.pool.QueryContainer(cmd.MustLogCtx(), cmd.contUUID.String()) if err != nil { if errors.Cause(err) != daos.NoPermission { return errors.Wrapf(err, "failed to query new container %s", contID) @@ -330,7 +227,7 @@ func (cmd *containerCreateCmd) Execute(_ []string) (err error) { ci = new(daos.ContainerInfo) ci.PoolUUID = cmd.pool.UUID() - ci.Type = cmd.Type.String() + ci.Type = cmd.Type.Type ci.ContainerUUID = cmd.contUUID ci.ContainerLabel = cmd.Args.Label } @@ -340,7 +237,7 @@ func (cmd *containerCreateCmd) Execute(_ []string) (err error) { } var bld strings.Builder - if err := printContainerInfo(&bld, ci, false); err != nil { + if err := pretty.PrintContainerInfo(&bld, ci, false); err != nil { return err } cmd.Info(bld.String()) @@ -408,21 +305,13 @@ func (cmd *containerCreateCmd) contCreate() (string, error) { } if len(cmd.Attrs.ParsedProps) != 0 { - attrs := make(daos.AttributeList, 0, len(cmd.Attrs.ParsedProps)) - for key, val := range cmd.Attrs.ParsedProps { - attrs = append(attrs, &daos.Attribute{ - Name: key, - Value: []byte(val), - }) - } - - if err := cmd.openContainer(C.DAOS_COO_RW); err != nil { + if err := cmd.openContainer(daos.ContainerOpenFlagReadWrite); err != nil { cleanupContainer() return "", errors.Wrapf(err, "failed to open new container %s", contID) } defer cmd.closeContainer() - if err := setDaosAttributes(cmd.cContHandle, contAttr, attrs); err != nil { + if err := setAttributes(cmd, cmd.container, contAttr, cmd.container.ID(), cmd.Attrs.ParsedProps); err != nil { cleanupContainer() return "", errors.Wrapf(err, "failed to set user attributes on new container %s", contID) } @@ -623,113 +512,31 @@ func (cmd *existingContainerCmd) resolveContainer(ap *C.struct_cmd_args_s) (err return nil } -func (cmd *existingContainerCmd) resolveAndConnect(contFlags C.uint, ap *C.struct_cmd_args_s) (cleanFn func(), err error) { - if err = cmd.resolveContainer(ap); err != nil { - return - } - - var cleanupPool func() - cleanupPool, err = cmd.connectPool(daos.PoolConnectFlagReadOnly, ap) - if err != nil { - return +func (cmd *existingContainerCmd) resolveAndOpen(contFlags daos.ContainerOpenFlag, ap *C.struct_cmd_args_s) (func(), error) { + nulCleanFn := func() {} + if err := cmd.resolveContainer(ap); err != nil { + return nulCleanFn, err } - if err = cmd.openContainer(contFlags); err != nil { - cleanupPool() - return + if err := cmd.openContainer(contFlags); err != nil { + return nulCleanFn, err } + cleanup := cmd.closeContainer if ap != nil { - if err = copyUUID(&ap.c_uuid, cmd.contUUID); err != nil { - cleanupPool() - return + if err := copyUUID(&ap.c_uuid, cmd.container.UUID()); err != nil { + cleanup() + return nulCleanFn, err } ap.cont = cmd.cContHandle } - return func() { - cmd.closeContainer() - cleanupPool() - }, nil -} - -func (cmd *existingContainerCmd) getAttr(name string) (*daos.Attribute, error) { - return getDaosAttribute(cmd.cContHandle, contAttr, name) + return cleanup, nil } type containerListCmd struct { poolBaseCmd -} - -func listContainers(hdl C.daos_handle_t) ([]*ContainerID, error) { - extra_cont_margin := C.size_t(16) - - // First call gets the current number of containers. - var ncont C.daos_size_t - rc := C.daos_pool_list_cont(hdl, &ncont, nil, nil) - if err := daosError(rc); err != nil { - return nil, errors.Wrap(err, "pool list containers failed") - } - - // No containers. - if ncont == 0 { - return nil, nil - } - - var cConts *C.struct_daos_pool_cont_info - // Extend ncont with a safety margin to account for containers - // that might have been created since the first API call. - ncont += extra_cont_margin - cConts = (*C.struct_daos_pool_cont_info)(C.calloc(C.sizeof_struct_daos_pool_cont_info, ncont)) - if cConts == nil { - return nil, errors.New("calloc() for containers failed") - } - dpciSlice := (*[1 << 30]C.struct_daos_pool_cont_info)( - unsafe.Pointer(cConts))[:ncont:ncont] - cleanup := func() { - C.free(unsafe.Pointer(cConts)) - } - - rc = C.daos_pool_list_cont(hdl, &ncont, cConts, nil) - if err := daosError(rc); err != nil { - cleanup() - return nil, err - } - - out := make([]*ContainerID, ncont) - for i := range out { - out[i] = new(ContainerID) - out[i].UUID = uuid.Must(uuidFromC(dpciSlice[i].pci_uuid)) - out[i].Label = C.GoString(&dpciSlice[i].pci_label[0]) - } - - C.free(unsafe.Pointer(cConts)) - - return out, nil -} - -func printContainers(out io.Writer, contIDs []*ContainerID) { - if len(contIDs) == 0 { - fmt.Fprintf(out, "No containers.\n") - return - } - - uuidTitle := "UUID" - labelTitle := "Label" - titles := []string{uuidTitle, labelTitle} - - table := []txtfmt.TableRow{} - for _, id := range contIDs { - table = append(table, - txtfmt.TableRow{ - uuidTitle: id.UUID.String(), - labelTitle: id.Label, - }) - } - - tf := txtfmt.NewTableFormatter(titles...) - tf.InitWriter(out) - tf.Format(table) + Verbose bool `short:"v" long:"verbose" description:"Verbose output"` } func (cmd *containerListCmd) Execute(_ []string) error { @@ -739,18 +546,23 @@ func (cmd *containerListCmd) Execute(_ []string) error { } defer cleanup() - contIDs, err := listContainers(cmd.cPoolHandle) + contList, err := cmd.pool.ListContainers(cmd.MustLogCtx(), cmd.Verbose) if err != nil { - return errors.Wrapf(err, - "unable to list containers for pool %s", cmd.PoolID()) + return errors.Wrapf(err, "unable to list container for pool %s", cmd.PoolID()) } if cmd.JSONOutputEnabled() { + // Maintain compatibility with the JSON output from before. + contIDs := make([]ContainerID, len(contList)) + for i, cont := range contList { + contIDs[i].UUID = cont.ContainerUUID + contIDs[i].Label = cont.ContainerLabel + } return cmd.OutputJSON(contIDs, nil) } var bld strings.Builder - printContainers(&bld, contIDs) + pretty.PrintContainers(&bld, cmd.pool.ID(), contList, cmd.Verbose) cmd.Info(bld.String()) return nil @@ -773,52 +585,39 @@ func (cmd *containerDestroyCmd) Execute(_ []string) error { return err } + if cmd.Path == "" { + poolID := cmd.PoolID().String() + contID := cmd.ContainerID().String() + + if contID == "" { + return errors.New("no UUID or label or path for container") + } + if err := api.ContainerDestroy(cmd.MustLogCtx(), cmd.SysName, poolID, contID, cmd.Force); err != nil { + return errors.Wrapf(err, "failed to destroy container %s", contID) + } + + cmd.Infof("Successfully destroyed container %s", contID) + return nil + } + + // TODO: Add API for DUNS. var cleanup func() - cleanup, err = cmd.connectPool(C.DAOS_COO_RW, ap) + cleanup, err = cmd.connectPool(daos.PoolConnectFlagReadWrite, ap) if err != nil { - // Even if we don't have pool-level write permissions, we may - // have delete permissions at the container level. - cleanup, err = cmd.connectPool(C.DAOS_COO_RO, ap) + cleanup, err = cmd.connectPool(daos.PoolConnectFlagReadOnly, ap) if err != nil { return err } } defer cleanup() - cmd.Debugf("destroying container %s (force: %t)", - cmd.ContainerID(), cmd.Force) - - var rc C.int - switch { - case cmd.Path != "": - cPath := C.CString(cmd.Path) - defer freeString(cPath) - rc = C.duns_destroy_path(cmd.cPoolHandle, cPath) - case cmd.ContainerID().HasUUID(): - cUUIDstr := C.CString(cmd.contUUID.String()) - defer freeString(cUUIDstr) - rc = C.daos_cont_destroy(cmd.cPoolHandle, cUUIDstr, - goBool2int(cmd.Force), nil) - case cmd.ContainerID().Label != "": - cLabel := C.CString(cmd.ContainerID().Label) - defer freeString(cLabel) - rc = C.daos_cont_destroy(cmd.cPoolHandle, - cLabel, goBool2int(cmd.Force), nil) - default: - return errors.New("no UUID or label or path for container") - } - - if err := daosError(rc); err != nil { - return errors.Wrapf(err, - "failed to destroy container %s", - cmd.ContainerID()) + cPath := C.CString(cmd.Path) + defer freeString(cPath) + if err := daosError(C.duns_destroy_path(cmd.cPoolHandle, cPath)); err != nil { + return errors.Wrapf(err, "failed to destroy container %s", cmd.ContainerID()) } - if cmd.ContainerID().Empty() { - cmd.Infof("Successfully destroyed container %s", cmd.Path) - } else { - cmd.Infof("Successfully destroyed container %s", cmd.ContainerID()) - } + cmd.Infof("Successfully destroyed container %s", cmd.Path) return nil } @@ -836,7 +635,7 @@ func (cmd *containerListObjectsCmd) Execute(_ []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } @@ -921,60 +720,6 @@ func (cmd *containerStatCmd) Execute(_ []string) error { return nil } -func printContainerInfo(out io.Writer, ci *daos.ContainerInfo, verbose bool) error { - rows := []txtfmt.TableRow{ - {"Container UUID": ci.ContainerUUID.String()}, - } - if ci.ContainerLabel != "" { - rows = append(rows, txtfmt.TableRow{"Container Label": ci.ContainerLabel}) - } - rows = append(rows, txtfmt.TableRow{"Container Type": ci.Type}) - - if verbose { - rows = append(rows, []txtfmt.TableRow{ - {"Pool UUID": ci.PoolUUID.String()}, - {"Container redundancy factor": fmt.Sprintf("%d", ci.RedundancyFactor)}, - {"Number of open handles": fmt.Sprintf("%d", ci.NumHandles)}, - {"Latest open time": fmt.Sprintf("%s (%#x)", daos.HLC(ci.OpenTime), ci.OpenTime)}, - {"Latest close/modify time": fmt.Sprintf("%s (%#x)", daos.HLC(ci.CloseModifyTime), ci.CloseModifyTime)}, - {"Number of snapshots": fmt.Sprintf("%d", ci.NumSnapshots)}, - }...) - - if ci.LatestSnapshot != 0 { - rows = append(rows, txtfmt.TableRow{"Latest Persistent Snapshot": fmt.Sprintf("%#x (%s)", ci.LatestSnapshot, daos.HLC(ci.LatestSnapshot))}) - } - if ci.ObjectClass != "" { - rows = append(rows, txtfmt.TableRow{"Object Class": ci.ObjectClass}) - } - if ci.DirObjectClass != "" { - rows = append(rows, txtfmt.TableRow{"Dir Object Class": ci.DirObjectClass}) - } - if ci.FileObjectClass != "" { - rows = append(rows, txtfmt.TableRow{"File Object Class": ci.FileObjectClass}) - } - if ci.CHints != "" { - rows = append(rows, txtfmt.TableRow{"Hints": ci.CHints}) - } - if ci.ChunkSize > 0 { - rows = append(rows, txtfmt.TableRow{"Chunk Size": humanize.IBytes(ci.ChunkSize)}) - } - } - _, err := fmt.Fprintln(out, txtfmt.FormatEntity("", rows)) - return err -} - -func convertContainerInfo(poolUUID, contUUID uuid.UUID, cInfo *C.daos_cont_info_t) *daos.ContainerInfo { - return &daos.ContainerInfo{ - PoolUUID: poolUUID, - ContainerUUID: contUUID, - LatestSnapshot: uint64(cInfo.ci_lsnapshot), - NumHandles: uint32(cInfo.ci_nhandles), - NumSnapshots: uint32(cInfo.ci_nsnapshots), - OpenTime: uint64(cInfo.ci_md_otime), - CloseModifyTime: uint64(cInfo.ci_md_mtime), - } -} - type containerQueryCmd struct { existingContainerCmd } @@ -986,17 +731,15 @@ func (cmd *containerQueryCmd) Execute(_ []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RO, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadOnly, ap) if err != nil { return err } defer cleanup() - ci, err := queryContainer(cmd.pool.UUID(), cmd.contUUID, cmd.cPoolHandle, cmd.cContHandle) + ci, err := cmd.container.Query(cmd.MustLogCtx()) if err != nil { - return errors.Wrapf(err, - "failed to query container %s", - cmd.contUUID) + return errors.Wrapf(err, "failed to query container %s", cmd.ContainerID()) } if cmd.JSONOutputEnabled() { @@ -1004,7 +747,7 @@ func (cmd *containerQueryCmd) Execute(_ []string) error { } var bld strings.Builder - if err := printContainerInfo(&bld, ci, true); err != nil { + if err := pretty.PrintContainerInfo(&bld, ci, true); err != nil { return err } cmd.Info(bld.String()) @@ -1077,7 +820,7 @@ func (cmd *containerCheckCmd) Execute(_ []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } @@ -1110,33 +853,13 @@ func (cmd *containerListAttrsCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RO, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadOnly, ap) if err != nil { return err } defer cleanup() - attrs, err := listDaosAttributes(cmd.cContHandle, contAttr, cmd.Verbose) - if err != nil { - return errors.Wrapf(err, - "failed to list attributes for container %s", - cmd.ContainerID()) - } - - if cmd.JSONOutputEnabled() { - if cmd.Verbose { - return cmd.OutputJSON(attrs.AsMap(), nil) - } - return cmd.OutputJSON(attrs.AsList(), nil) - } - - var bld strings.Builder - title := fmt.Sprintf("Attributes for container %s:", cmd.ContainerID()) - pretty.PrintAttributes(&bld, title, attrs...) - - cmd.Info(bld.String()) - - return nil + return listAttributes(cmd, cmd.container, contAttr, cmd.container.ID(), cmd.Verbose) } type containerDelAttrCmd struct { @@ -1144,15 +867,20 @@ type containerDelAttrCmd struct { FlagAttr string `long:"attr" short:"a" description:"attribute name (deprecated; use positional argument)"` Args struct { - Attr string `positional-arg-name:""` + Attrs ui.GetPropertiesFlag `positional-arg-name:"key[,key...]"` } `positional-args:"yes"` } func (cmd *containerDelAttrCmd) Execute(args []string) error { if cmd.FlagAttr != "" { - cmd.Args.Attr = cmd.FlagAttr + if len(cmd.Args.Attrs.ParsedProps) > 0 { + return errors.New("cannot specify both --attr and positional arguments") + } + cmd.Args.Attrs.ParsedProps = make(common.StringSet) + cmd.Args.Attrs.ParsedProps.Add(cmd.FlagAttr) + cmd.FlagAttr = "" } - if cmd.Args.Attr == "" { + if len(cmd.Args.Attrs.ParsedProps) == 0 { return errors.New("attribute name is required") } @@ -1162,19 +890,13 @@ func (cmd *containerDelAttrCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } defer cleanup() - if err := delDaosAttribute(cmd.cContHandle, contAttr, cmd.Args.Attr); err != nil { - return errors.Wrapf(err, - "failed to delete attribute %q on container %s", - cmd.Args.Attr, cmd.ContainerID()) - } - - return nil + return delAttributes(cmd, cmd.container, contAttr, cmd.container.ID(), cmd.Args.Attrs.ParsedProps.ToSlice()...) } type containerGetAttrCmd struct { @@ -1191,7 +913,9 @@ func (cmd *containerGetAttrCmd) Execute(args []string) error { if len(cmd.Args.Attrs.ParsedProps) > 0 { return errors.New("cannot specify both --attr and positional arguments") } + cmd.Args.Attrs.ParsedProps = make(common.StringSet) cmd.Args.Attrs.ParsedProps.Add(cmd.FlagAttr) + cmd.FlagAttr = "" } else if len(args) > 0 { if err := cmd.Args.Attrs.UnmarshalFlag(args[len(args)-1]); err != nil { return err @@ -1204,38 +928,13 @@ func (cmd *containerGetAttrCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RO, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadOnly, ap) if err != nil { return err } defer cleanup() - var attrs daos.AttributeList - if len(cmd.Args.Attrs.ParsedProps) == 0 { - attrs, err = listDaosAttributes(cmd.cContHandle, contAttr, true) - } else { - cmd.Debugf("getting attributes: %s", cmd.Args.Attrs.ParsedProps) - attrs, err = getDaosAttributes(cmd.cContHandle, contAttr, cmd.Args.Attrs.ParsedProps.ToSlice()) - } - if err != nil { - return errors.Wrapf(err, "failed to get attributes from container %s", cmd.ContainerID()) - } - - if cmd.JSONOutputEnabled() { - // Maintain compatibility with older behavior. - if len(cmd.Args.Attrs.ParsedProps) == 1 && len(attrs) == 1 { - return cmd.OutputJSON(attrs[0], nil) - } - return cmd.OutputJSON(attrs, nil) - } - - var bld strings.Builder - title := fmt.Sprintf("Attributes for container %s:", cmd.ContainerID()) - pretty.PrintAttributes(&bld, title, attrs...) - - cmd.Info(bld.String()) - - return nil + return getAttributes(cmd, cmd.container, contAttr, cmd.container.ID(), cmd.Args.Attrs.ParsedProps.ToSlice()...) } type containerSetAttrCmd struct { @@ -1258,41 +957,27 @@ func (cmd *containerSetAttrCmd) Execute(args []string) error { } cmd.Args.Attrs.ParsedProps = make(map[string]string) cmd.Args.Attrs.ParsedProps[cmd.FlagAttr] = cmd.FlagValue + cmd.FlagAttr = "" + cmd.FlagValue = "" } else if len(args) > 0 { if err := cmd.Args.Attrs.UnmarshalFlag(args[len(args)-1]); err != nil { return err } } - if len(cmd.Args.Attrs.ParsedProps) == 0 { - return errors.New("attribute name and value are required") - } - ap, deallocCmdArgs, err := allocCmdArgs(cmd.Logger) if err != nil { return err } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } defer cleanup() - attrs := make(daos.AttributeList, 0, len(cmd.Args.Attrs.ParsedProps)) - for key, val := range cmd.Args.Attrs.ParsedProps { - attrs = append(attrs, &daos.Attribute{ - Name: key, - Value: []byte(val), - }) - } - - if err := setDaosAttributes(cmd.cContHandle, contAttr, attrs); err != nil { - return errors.Wrapf(err, "failed to set attributes on container %s", cmd.ContainerID()) - } - - return nil + return setAttributes(cmd, cmd.container, contAttr, cmd.container.ID(), cmd.Args.Attrs.ParsedProps) } type containerGetPropCmd struct { @@ -1329,7 +1014,7 @@ func (cmd *containerGetPropCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RO, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadOnly, ap) if err != nil { return err } @@ -1406,7 +1091,7 @@ func (cmd *containerSetPropCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } @@ -1480,15 +1165,22 @@ func (f *ContainerID) Complete(match string) (comps []flags.Completion) { } defer cleanup() - contIDs, err := listContainers(pf.cPoolHandle) + contList, err := pf.pool.ListContainers(pf.MustLogCtx(), false) if err != nil { return } - for _, id := range contIDs { - if strings.HasPrefix(id.String(), match) { + for _, cont := range contList { + var contId string + if strings.HasPrefix(cont.ContainerLabel, match) { + contId = cont.ContainerLabel + } else if strings.HasPrefix(cont.ContainerUUID.String(), match) { + contId = cont.ContainerUUID.String() + } + + if contId != "" { comps = append(comps, flags.Completion{ - Item: id.String(), + Item: cont.ContainerLabel, }) } } @@ -1565,14 +1257,14 @@ func (cmd *containerEvictCmd) Execute(_ []string) (err error) { } defer deallocCmdArgs() - var co_flags C.uint + var co_flags daos.ContainerOpenFlag if cmd.All { - co_flags = C.DAOS_COO_EVICT_ALL | C.DAOS_COO_EX + co_flags = daos.ContainerOpenFlagEvictAll | daos.ContainerOpenFlagExclusive } else { - co_flags = C.DAOS_COO_EVICT | C.DAOS_COO_RO + co_flags = daos.ContainerOpenFlagEvict | daos.ContainerOpenFlagReadOnly } - cleanup, err := cmd.resolveAndConnect(co_flags, ap) + cleanup, err := cmd.resolveAndOpen(co_flags, ap) if err != nil { return err } diff --git a/src/control/cmd/daos/container_test.go b/src/control/cmd/daos/container_test.go index cbec0cadf81..240c53819f4 100644 --- a/src/control/cmd/daos/container_test.go +++ b/src/control/cmd/daos/container_test.go @@ -1,5 +1,6 @@ // // (C) Copyright 2023 Intel Corporation. +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -7,8 +8,10 @@ package main import ( + "context" "os" "path/filepath" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -16,6 +19,9 @@ import ( "github.com/pkg/errors" "github.com/daos-stack/daos/src/control/common/test" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/lib/daos/api" + "github.com/daos-stack/daos/src/control/lib/ui" "github.com/daos-stack/daos/src/control/logging" ) @@ -189,3 +195,304 @@ func TestDaos_existingContainerCmd_resolveContainer(t *testing.T) { }) } } + +var ( + defaultContInfo *daos.ContainerInfo = &daos.ContainerInfo{ + PoolUUID: defaultPoolInfo.UUID, + ContainerUUID: test.MockPoolUUID(2), + ContainerLabel: "test-container", + } + + contOpenErr error +) + +func ContainerOpen(ctx context.Context, req api.ContainerOpenReq) (*api.ContainerOpenResp, error) { + return &api.ContainerOpenResp{ + Connection: api.MockContainerHandle(), + Info: defaultContInfo, + }, contOpenErr +} + +func TestDaos_containerSetAttrCmd(t *testing.T) { + baseArgs := test.JoinArgs(nil, "container", "set-attr", defaultPoolInfo.Label, defaultContInfo.ContainerLabel) + keysOnlyArg := "key1,key2" + keyValArg := "key1:val1,key2:val2" + + for name, tc := range map[string]struct { + args []string + expErr error + expArgs containerSetAttrCmd + setup func(t *testing.T) + }{ + "invalid flag": { + args: test.JoinArgs(baseArgs, "--bad", keyValArg), + expErr: errors.New("unknown flag"), + }, + "open fails": { + args: test.JoinArgs(baseArgs, keyValArg), + expErr: errors.New("whoops"), + setup: func(t *testing.T) { + prevErr := contOpenErr + t.Cleanup(func() { + contOpenErr = prevErr + }) + contOpenErr = errors.New("whoops") + }, + }, + "missing required arguments": { + args: baseArgs, + expErr: errors.New("attribute name and value are required"), + }, + "malformed required arguments": { + args: test.JoinArgs(baseArgs, keysOnlyArg), + expErr: errors.New("invalid property"), + }, + "success": { + args: test.JoinArgs(baseArgs, keyValArg), + expArgs: containerSetAttrCmd{ + Args: struct { + Attrs ui.SetPropertiesFlag `positional-arg-name:"key:val[,key:val...]"` + }{ + Attrs: ui.SetPropertiesFlag{ + ParsedProps: map[string]string{ + "key1": "val1", + "key2": "val2", + }, + }, + }, + }, + }, + "success (one key, deprecated flag)": { + args: test.JoinArgs(baseArgs, "--attr", "one", "--value", "uno"), + expArgs: containerSetAttrCmd{ + Args: struct { + Attrs ui.SetPropertiesFlag `positional-arg-name:"key:val[,key:val...]"` + }{ + Attrs: ui.SetPropertiesFlag{ + ParsedProps: map[string]string{ + "one": "uno", + }, + }, + }, + }, + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(api.ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + runCmdTest(t, tc.args, tc.expArgs, tc.expErr, "Container.SetAttribute") + }) + } +} + +func TestDaos_containerGetAttrCmd(t *testing.T) { + baseArgs := test.JoinArgs(nil, "container", "get-attr", defaultPoolInfo.Label, defaultContInfo.ContainerLabel) + keysOnlyArg := "key1,key2" + + for name, tc := range map[string]struct { + args []string + expErr error + expArgs containerGetAttrCmd + setup func(t *testing.T) + }{ + "invalid flag": { + args: test.JoinArgs(baseArgs, "--bad"), + expErr: errors.New("unknown flag"), + }, + "missing container ID": { + args: baseArgs[:len(baseArgs)-1], + expErr: errors.New("no container label or UUID supplied"), + }, + "connect fails": { + args: baseArgs, + expErr: errors.New("whoops"), + setup: func(t *testing.T) { + prevErr := contOpenErr + t.Cleanup(func() { + contOpenErr = prevErr + }) + contOpenErr = errors.New("whoops") + }, + }, + "malformed arguments": { + args: test.JoinArgs(baseArgs, strings.ReplaceAll(keysOnlyArg, ",", ":")), + expErr: errors.New("key cannot contain"), + }, + "unknown key(s)": { + args: test.JoinArgs(baseArgs, keysOnlyArg), + expErr: daos.Nonexistent, + }, + "success (one key)": { + args: test.JoinArgs(baseArgs, "one"), + expArgs: containerGetAttrCmd{ + Args: struct { + Attrs ui.GetPropertiesFlag `positional-arg-name:"key[,key...]"` + }{ + Attrs: ui.GetPropertiesFlag{ + ParsedProps: map[string]struct{}{ + "one": {}, + }, + }, + }, + }, + }, + "success (one key, deprecated flag)": { + args: test.JoinArgs(baseArgs, "--attr", "one"), + expArgs: containerGetAttrCmd{ + Args: struct { + Attrs ui.GetPropertiesFlag `positional-arg-name:"key[,key...]"` + }{ + Attrs: ui.GetPropertiesFlag{ + ParsedProps: map[string]struct{}{ + "one": {}, + }, + }, + }, + }, + }, + "success (all keys)": { + args: baseArgs, + expArgs: containerGetAttrCmd{}, + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(api.ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + runCmdTest(t, tc.args, tc.expArgs, tc.expErr, "Container.GetAttribute") + }) + } +} + +func TestDaos_containerDelAttrCmd(t *testing.T) { + baseArgs := test.JoinArgs(nil, "container", "del-attr", defaultPoolInfo.Label, defaultContInfo.ContainerLabel) + keysOnlyArg := "key1,key2" + + for name, tc := range map[string]struct { + args []string + expErr error + expArgs containerDelAttrCmd + setup func(t *testing.T) + }{ + "invalid flag": { + args: test.JoinArgs(baseArgs, "--bad"), + expErr: errors.New("unknown flag"), + }, + "missing required arguments": { + args: baseArgs, + expErr: errors.New("attribute name is required"), + }, + "connect fails": { + args: test.JoinArgs(baseArgs, keysOnlyArg), + expErr: errors.New("whoops"), + setup: func(t *testing.T) { + prevErr := contOpenErr + t.Cleanup(func() { + contOpenErr = prevErr + }) + contOpenErr = errors.New("whoops") + }, + }, + "malformed arguments": { + args: test.JoinArgs(baseArgs, strings.ReplaceAll(keysOnlyArg, ",", ":")), + expErr: errors.New("key cannot contain"), + }, + "success (one key)": { + args: test.JoinArgs(baseArgs, "one"), + expArgs: containerDelAttrCmd{ + Args: struct { + Attrs ui.GetPropertiesFlag `positional-arg-name:"key[,key...]"` + }{ + Attrs: ui.GetPropertiesFlag{ + ParsedProps: map[string]struct{}{ + "one": {}, + }, + }, + }, + }, + }, + "success (deprecated flag)": { + args: test.JoinArgs(baseArgs, "--attr", "one"), + expArgs: containerDelAttrCmd{ + Args: struct { + Attrs ui.GetPropertiesFlag `positional-arg-name:"key[,key...]"` + }{ + Attrs: ui.GetPropertiesFlag{ + ParsedProps: map[string]struct{}{ + "one": {}, + }, + }, + }, + }, + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(api.ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + runCmdTest(t, tc.args, tc.expArgs, tc.expErr, "Container.DeleteAttribute") + }) + } +} + +func TestDaos_containerListAttrCmd(t *testing.T) { + baseArgs := test.JoinArgs(nil, "container", "list-attr", defaultPoolInfo.Label, defaultContInfo.ContainerLabel) + + for name, tc := range map[string]struct { + args []string + expErr error + expArgs containerListAttrsCmd + setup func(t *testing.T) + }{ + "invalid flag": { + args: test.JoinArgs(baseArgs, "--bad"), + expErr: errors.New("unknown flag"), + }, + "missing container ID": { + args: baseArgs[:len(baseArgs)-1], + expErr: errors.New("no container label or UUID supplied"), + }, + "connect fails": { + args: baseArgs, + expErr: errors.New("whoops"), + setup: func(t *testing.T) { + prevErr := contOpenErr + t.Cleanup(func() { + contOpenErr = prevErr + }) + contOpenErr = errors.New("whoops") + }, + }, + "success": { + args: baseArgs, + expArgs: containerListAttrsCmd{}, + }, + "success (verbose, short)": { + args: test.JoinArgs(baseArgs, "-V"), + expArgs: containerListAttrsCmd{ + Verbose: true, + }, + }, + "success (verbose, long)": { + args: test.JoinArgs(baseArgs, "--verbose"), + expArgs: containerListAttrsCmd{ + Verbose: true, + }, + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(api.ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + runCmdTest(t, tc.args, tc.expArgs, tc.expErr, "Container.ListAttributes") + }) + } +} diff --git a/src/control/cmd/daos/filesystem.go b/src/control/cmd/daos/filesystem.go index 6996f33a0eb..107a920221f 100644 --- a/src/control/cmd/daos/filesystem.go +++ b/src/control/cmd/daos/filesystem.go @@ -1,5 +1,6 @@ // // (C) Copyright 2021-2024 Intel Corporation. +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -25,6 +26,8 @@ import ( "unsafe" "github.com/pkg/errors" + + "github.com/daos-stack/daos/src/control/lib/daos" ) func dfsError(rc C.int) error { @@ -191,14 +194,12 @@ func fsModifyAttr(cmd *fsAttrCmd, op uint32, updateAP func(*C.struct_cmd_args_s) } defer deallocCmdArgs() - flags := C.uint(C.DAOS_COO_RW) - ap.fs_op = op if updateAP != nil { updateAP(ap) } - cleanup, err := cmd.resolveAndConnect(flags, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return err } @@ -265,9 +266,8 @@ func (cmd *fsGetAttrCmd) Execute(_ []string) error { defer deallocCmdArgs() ap.fs_op = C.FS_GET_ATTR - flags := C.uint(C.DAOS_COO_RO) - cleanup, err := cmd.resolveAndConnect(flags, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadOnly, ap) if err != nil { return err } @@ -399,10 +399,8 @@ func (cmd *fsFixEntryCmd) Execute(_ []string) error { } defer deallocCmdArgs() - flags := C.uint(C.DAOS_COO_EX) - ap.fs_op = C.FS_CHECK - cleanup, err := cmd.resolveAndConnect(flags, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagExclusive, ap) if err != nil { return errors.Wrapf(err, "failed fs fix-entry") } @@ -439,7 +437,7 @@ func (cmd *fsFixSBCmd) Execute(_ []string) error { defer deallocCmdArgs() ap.fs_op = C.FS_CHECK - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_EX, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagExclusive, ap) if err != nil { return err } @@ -484,7 +482,7 @@ func (cmd *fsFixRootCmd) Execute(_ []string) error { defer deallocCmdArgs() ap.fs_op = C.FS_CHECK - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_EX, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagExclusive, ap) if err != nil { return err } @@ -690,7 +688,7 @@ func (cmd *fsChmodCmd) Execute(_ []string) error { ap.object_mode = cmd.ModeBits.Mode - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(daos.ContainerOpenFlagReadWrite, ap) if err != nil { return errors.Wrapf(err, "failed to connect") } diff --git a/src/control/cmd/daos/flags.go b/src/control/cmd/daos/flags.go index 8478387325c..8e89f4b27d4 100644 --- a/src/control/cmd/daos/flags.go +++ b/src/control/cmd/daos/flags.go @@ -1,5 +1,6 @@ // // (C) Copyright 2021-2024 Intel Corporation. +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -11,6 +12,7 @@ import ( "strconv" "strings" + "github.com/daos-stack/daos/src/control/lib/daos" "github.com/dustin/go-humanize" "github.com/pkg/errors" ) @@ -195,7 +197,7 @@ func (f *ConsModeFlag) String() string { case C.DFS_BALANCED: return "balanced" default: - return fmt.Sprintf("unknown mode %d", f.Mode) + return fmt.Sprintf("unknown mode %d (valid: relaxed, balanced)", f.Mode) } } @@ -210,7 +212,7 @@ func (f *ConsModeFlag) UnmarshalFlag(fv string) error { case "balanced": f.Mode = C.DFS_BALANCED default: - return errors.Errorf("unknown consistency mode %q", fv) + return errors.Errorf("unknown consistency mode %q (valid: relaxed, balanced)", fv) } f.Set = true @@ -219,14 +221,11 @@ func (f *ConsModeFlag) UnmarshalFlag(fv string) error { type ContTypeFlag struct { Set bool - Type C.ushort + Type daos.ContainerLayout } func (f *ContTypeFlag) String() string { - cTypeStr := [16]C.char{} - C.daos_unparse_ctype(f.Type, &cTypeStr[0]) - - return C.GoString(&cTypeStr[0]) + return f.Type.String() } func (f *ContTypeFlag) UnmarshalFlag(fv string) error { @@ -234,12 +233,8 @@ func (f *ContTypeFlag) UnmarshalFlag(fv string) error { return errors.New("empty container type") } - cTypeStr := C.CString(strings.ToUpper(fv)) - defer freeString(cTypeStr) - - C.daos_parse_ctype(cTypeStr, &f.Type) - if f.Type == C.DAOS_PROP_CO_LAYOUT_UNKNOWN { - return errors.Errorf("unknown container type %q", fv) + if err := f.Type.FromString(fv); err != nil { + return err } f.Set = true diff --git a/src/control/cmd/daos/flags_test.go b/src/control/cmd/daos/flags_test.go index 1347637b0d4..7fa86dad393 100644 --- a/src/control/cmd/daos/flags_test.go +++ b/src/control/cmd/daos/flags_test.go @@ -1,5 +1,6 @@ // // (C) Copyright 2021-2024 Intel Corporation. +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -410,7 +411,7 @@ func TestFlags_ContTypeFlag(t *testing.T) { }, "invalid": { arg: "snausages", - expErr: errors.New("unknown container type"), + expErr: errors.New("unknown container layout"), }, "valid": { arg: "pOsIx", diff --git a/src/control/cmd/daos/health.go b/src/control/cmd/daos/health.go index fa71a5b9a08..a91916ffe72 100644 --- a/src/control/cmd/daos/health.go +++ b/src/control/cmd/daos/health.go @@ -8,9 +8,7 @@ package main import ( - "fmt" "strings" - "unsafe" "github.com/google/uuid" @@ -92,7 +90,7 @@ func (cmd *healthCheckCmd) Execute([]string) error { pools, err := api.GetPoolList(ctx, api.GetPoolListReq{ SysName: cmd.SysName, - Query: true, + Query: false, }) if err != nil { cmd.Errorf("failed to get pool list: %v", err) @@ -131,50 +129,14 @@ func (cmd *healthCheckCmd) Execute([]string) error { pool.DisabledRanks = tpi.DisabledRanks pool.DeadRanks = tpi.DeadRanks - /* temporary, until we get the container API bindings */ - var poolHdl C.daos_handle_t - if err := pcResp.Connection.FillHandle(unsafe.Pointer(&poolHdl)); err != nil { - cmd.Errorf("failed to fill handle for pool %s: %v", pool.Label, err) - continue - } - - poolConts, err := listContainers(poolHdl) + poolConts, err := pcResp.Connection.ListContainers(ctx, true) if err != nil { cmd.Errorf("failed to list containers on pool %s: %v", pool.Label, err) continue } - for _, cont := range poolConts { - openFlags := uint(daos.ContainerOpenFlagReadOnly | daos.ContainerOpenFlagForce | daos.ContainerOpenFlagReadOnlyMetadata) - contHdl, contInfo, err := containerOpen(poolHdl, cont.UUID.String(), openFlags, true) - if err != nil { - cmd.Errorf("failed to connect to container %s: %v", cont.Label, err) - ci := &daos.ContainerInfo{ - PoolUUID: pool.UUID, - ContainerUUID: cont.UUID, - ContainerLabel: cont.Label, - Health: fmt.Sprintf("Unknown (%s)", err), - } - systemHealth.Containers[pool.UUID] = append(systemHealth.Containers[pool.UUID], ci) - continue - } - ci := convertContainerInfo(pool.UUID, cont.UUID, contInfo) - ci.ContainerLabel = cont.Label - - props, freeProps, err := getContainerProperties(contHdl, "status") - if err != nil || len(props) == 0 { - cmd.Errorf("failed to get container properties for %s: %v", cont.Label, err) - ci.Health = fmt.Sprintf("Unknown (%s)", err) - } else { - ci.Health = props[0].String() - } - freeProps() - - if err := containerCloseAPI(contHdl); err != nil { - cmd.Errorf("failed to close container %s: %v", cont.Label, err) - } - - systemHealth.Containers[pool.UUID] = append(systemHealth.Containers[pool.UUID], ci) + for _, contInfo := range poolConts { + systemHealth.Containers[pool.UUID] = append(systemHealth.Containers[pool.UUID], contInfo) } } diff --git a/src/control/cmd/daos/object.go b/src/control/cmd/daos/object.go index 141418c43b4..30148cbc640 100644 --- a/src/control/cmd/daos/object.go +++ b/src/control/cmd/daos/object.go @@ -1,5 +1,6 @@ // // (C) Copyright 2021-2024 Intel Corporation. +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -135,7 +136,7 @@ func (cmd *objQueryCmd) Execute(_ []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RO, ap) + cleanup, err := cmd.resolveAndOpen(C.DAOS_COO_RO, ap) if err != nil { return err } diff --git a/src/control/cmd/daos/pool_test.go b/src/control/cmd/daos/pool_test.go index 5f52244788f..f4cf4bd308c 100644 --- a/src/control/cmd/daos/pool_test.go +++ b/src/control/cmd/daos/pool_test.go @@ -105,17 +105,14 @@ func TestDaos_poolListCmd(t *testing.T) { } var ( - defaultPoolConnectResp *api.PoolConnectResp = &api.PoolConnectResp{ - Connection: &api.PoolHandle{}, - Info: defaultPoolInfo, - } - - poolConnectResp *api.PoolConnectResp = defaultPoolConnectResp - poolConnectErr error + poolConnectErr error ) func PoolConnect(ctx context.Context, req api.PoolConnectReq) (*api.PoolConnectResp, error) { - return poolConnectResp, poolConnectErr + return &api.PoolConnectResp{ + Connection: api.MockPoolHandle(), + Info: defaultPoolInfo, + }, poolConnectErr } func TestDaos_poolQueryCmd(t *testing.T) { diff --git a/src/control/cmd/daos/pretty/container.go b/src/control/cmd/daos/pretty/container.go new file mode 100644 index 00000000000..10526bbf0be --- /dev/null +++ b/src/control/cmd/daos/pretty/container.go @@ -0,0 +1,95 @@ +// +// (C) Copyright 2025 Intel Corporation. +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package pretty + +import ( + "fmt" + "io" + + "github.com/dustin/go-humanize" + + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/lib/txtfmt" +) + +// PrintContainerInfo generates a human-readable representation of the supplied +// ContainerInfo struct and writes it to the supplied io.Writer. +func PrintContainerInfo(out io.Writer, ci *daos.ContainerInfo, verbose bool) error { + rows := []txtfmt.TableRow{ + {"Container UUID": ci.ContainerUUID.String()}, + } + if ci.ContainerLabel != "" { + rows = append(rows, txtfmt.TableRow{"Container Label": ci.ContainerLabel}) + } + rows = append(rows, txtfmt.TableRow{"Container Type": ci.Type.String()}) + + if verbose { + rows = append(rows, []txtfmt.TableRow{ + {"Pool UUID": ci.PoolUUID.String()}, + {"Container redundancy factor": fmt.Sprintf("%d", ci.RedundancyFactor)}, + {"Number of open handles": fmt.Sprintf("%d", ci.NumHandles)}, + {"Latest open time": fmt.Sprintf("%s (%#x)", ci.OpenTime, uint64(ci.OpenTime))}, + {"Latest close/modify time": fmt.Sprintf("%s (%#x)", ci.CloseModifyTime, uint64(ci.CloseModifyTime))}, + {"Number of snapshots": fmt.Sprintf("%d", ci.NumSnapshots)}, + }...) + + if ci.LatestSnapshot != 0 { + rows = append(rows, txtfmt.TableRow{"Latest Persistent Snapshot": fmt.Sprintf("%#x (%s)", uint64(ci.LatestSnapshot), ci.LatestSnapshot)}) + } + if ci.ObjectClass != 0 { + rows = append(rows, txtfmt.TableRow{"Object Class": ci.ObjectClass.String()}) + } + if ci.DirObjectClass != 0 { + rows = append(rows, txtfmt.TableRow{"Dir Object Class": ci.DirObjectClass.String()}) + } + if ci.FileObjectClass != 0 { + rows = append(rows, txtfmt.TableRow{"File Object Class": ci.FileObjectClass.String()}) + } + if ci.Hints != "" { + rows = append(rows, txtfmt.TableRow{"Hints": ci.Hints}) + } + if ci.ChunkSize > 0 { + rows = append(rows, txtfmt.TableRow{"Chunk Size": humanize.IBytes(ci.ChunkSize)}) + } + } + _, err := fmt.Fprintln(out, txtfmt.FormatEntity("", rows)) + return err +} + +// PrintContainers generates a human-readable representation of the supplied +// slice of ContainerInfo structs and writes it to the supplied io.Writer. +func PrintContainers(out io.Writer, poolID string, containers []*daos.ContainerInfo, verbose bool) { + if len(containers) == 0 { + fmt.Fprintf(out, "No containers.\n") + return + } + + fmt.Fprintf(out, "Containers in pool %s:\n", poolID) + + uuidTitle := "UUID" + labelTitle := "Label" + layoutTitle := "Layout" + titles := []string{labelTitle} + if verbose { + titles = append(titles, uuidTitle, layoutTitle) + } + + table := []txtfmt.TableRow{} + for _, cont := range containers { + table = append(table, + txtfmt.TableRow{ + uuidTitle: cont.ContainerUUID.String(), + labelTitle: cont.ContainerLabel, + layoutTitle: cont.Type.String(), + }) + } + + tf := txtfmt.NewTableFormatter(titles...) + tf.InitWriter(txtfmt.NewIndentWriter(out)) + tf.Format(table) +} diff --git a/src/control/cmd/daos/snapshot.go b/src/control/cmd/daos/snapshot.go index 626dd7f51ba..ecb122979e7 100644 --- a/src/control/cmd/daos/snapshot.go +++ b/src/control/cmd/daos/snapshot.go @@ -1,5 +1,6 @@ // -// (C) Copyright 2021-2022 Intel Corporation. +// (C) Copyright 2021-2024 Intel Corporation. +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -43,7 +44,7 @@ func (cmd *containerSnapCreateCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(C.DAOS_COO_RW, ap) if err != nil { return err } @@ -103,7 +104,7 @@ func (cmd *containerSnapDestroyCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(C.DAOS_COO_RW, ap) if err != nil { return err } @@ -248,7 +249,7 @@ func (cmd *containerSnapListCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RO, ap) + cleanup, err := cmd.resolveAndOpen(C.DAOS_COO_RO, ap) if err != nil { return err } @@ -284,7 +285,7 @@ func (cmd *containerSnapshotRollbackCmd) Execute(args []string) error { } defer deallocCmdArgs() - cleanup, err := cmd.resolveAndConnect(C.DAOS_COO_RW, ap) + cleanup, err := cmd.resolveAndOpen(C.DAOS_COO_RW, ap) if err != nil { return err } diff --git a/src/control/cmd/daos/stubbed.go b/src/control/cmd/daos/stubbed.go index 000e8be5a20..90a792b198f 100644 --- a/src/control/cmd/daos/stubbed.go +++ b/src/control/cmd/daos/stubbed.go @@ -12,7 +12,8 @@ package main import "github.com/daos-stack/daos/src/control/lib/daos/api" var ( - RunSelfTest = api.RunSelfTest - GetPoolList = api.GetPoolList - PoolConnect = api.PoolConnect + RunSelfTest = api.RunSelfTest + GetPoolList = api.GetPoolList + PoolConnect = api.PoolConnect + ContainerOpen = api.ContainerOpen ) diff --git a/src/control/lib/daos/api/attribute.go b/src/control/lib/daos/api/attribute.go index 79e2630069e..6070e6737d0 100644 --- a/src/control/lib/daos/api/attribute.go +++ b/src/control/lib/daos/api/attribute.go @@ -48,8 +48,8 @@ func listDaosAttributes(hdl C.daos_handle_t, at attrType) ([]string, error) { switch at { case poolAttr: rc = daos_pool_list_attr(hdl, nil, &totalSize, nil) - /*case contAttr: - rc = daos_cont_list_attr(hdl, nil, &totalSize, nil)*/ + case contAttr: + rc = daos_cont_list_attr(hdl, nil, &totalSize, nil) default: return nil, errors.Wrapf(daos.InvalidInput, "unknown attr type %d", at) } @@ -69,8 +69,8 @@ func listDaosAttributes(hdl C.daos_handle_t, at attrType) ([]string, error) { switch at { case poolAttr: rc = daos_pool_list_attr(hdl, (*C.char)(cNamesBuf), &totalSize, nil) - /*case contAttr: - rc = daos_cont_list_attr(hdl, (*C.char)(buf), &totalSize, nil)*/ + case contAttr: + rc = daos_cont_list_attr(hdl, (*C.char)(cNamesBuf), &totalSize, nil) default: return nil, errors.Wrapf(daos.InvalidInput, "unknown attr type %d", at) } @@ -125,8 +125,8 @@ func getDaosAttributes(hdl C.daos_handle_t, at attrType, reqAttrNames []string) switch at { case poolAttr: rc = daos_pool_get_attr(hdl, C.int(numAttr), &cAttrNames[0], nil, &cAttrSizes[0], nil) - /*case contAttr: - rc = daos_cont_get_attr(hdl, C.int(numAttr), &attrNames[0], nil, &attrSizes[0], nil)*/ + case contAttr: + rc = daos_cont_get_attr(hdl, C.int(numAttr), &cAttrNames[0], nil, &cAttrSizes[0], nil) default: return nil, errors.Wrapf(daos.InvalidInput, "unknown attr type %d", at) } @@ -153,8 +153,8 @@ func getDaosAttributes(hdl C.daos_handle_t, at attrType, reqAttrNames []string) switch at { case poolAttr: rc = daos_pool_get_attr(hdl, C.int(numAttr), &cAttrNames[0], &cAttrValues[0], &cAttrSizes[0], nil) - /*case contAttr: - rc = daos_cont_get_attr(hdl, C.int(numAttr), &attrNames[0], &attrValues[0], &attrSizes[0], nil)*/ + case contAttr: + rc = daos_cont_get_attr(hdl, C.int(numAttr), &cAttrNames[0], &cAttrValues[0], &cAttrSizes[0], nil) default: return nil, errors.Wrapf(daos.InvalidInput, "unknown attr type %d", at) } @@ -226,8 +226,8 @@ func setDaosAttributes(hdl C.daos_handle_t, at attrType, attrs daos.AttributeLis switch at { case poolAttr: rc = daos_pool_set_attr(hdl, attrCount, &attrNames[0], &attrValues[0], &attrSizes[0], nil) - /*case contAttr: - rc = daos_cont_set_attr(hdl, attrCount, &attrNames[0], &valBufs[0], &valSizes[0], nil)*/ + case contAttr: + rc = daos_cont_set_attr(hdl, attrCount, &attrNames[0], &attrValues[0], &attrSizes[0], nil) default: return errors.Wrapf(daos.InvalidInput, "unknown attr type %d", at) } @@ -258,8 +258,8 @@ func delDaosAttributes(hdl C.daos_handle_t, at attrType, names []string) error { switch at { case poolAttr: rc = daos_pool_del_attr(hdl, C.int(len(attrNames)), &attrNames[0], nil) - /*case contAttr: - rc = daos_cont_del_attr(hdl, 1, &attrName, nil)*/ + case contAttr: + rc = daos_cont_del_attr(hdl, C.int(len(attrNames)), &attrNames[0], nil) default: return errors.Wrapf(daos.InvalidInput, "unknown attr type %d", at) } diff --git a/src/control/lib/daos/api/container.go b/src/control/lib/daos/api/container.go new file mode 100644 index 00000000000..89a43867573 --- /dev/null +++ b/src/control/lib/daos/api/container.go @@ -0,0 +1,560 @@ +// +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package api + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/pkg/errors" + + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/logging" +) + +/* +#include + +#include +#include +#include +#include + +#include "util.h" + +#cgo LDFLAGS: -ldaos_common +*/ +import "C" + +type ( + // ContainerHandle is an opaque type used to represent a DAOS Container connection. + // NB: A ContainerHandle contains the PoolHandle used to open the container. + ContainerHandle struct { + connHandle + poolConnCleanup func() + PoolHandle *PoolHandle + } +) + +const ( + contHandleKey ctxHdlKey = "contHandle" +) + +// chFromCtx retrieves the ContainerHandle from the supplied context, if available. +func chFromCtx(ctx context.Context) (*ContainerHandle, error) { + if ctx == nil { + return nil, errNilCtx + } + + ch, ok := ctx.Value(contHandleKey).(*ContainerHandle) + if !ok { + return nil, errNoCtxHdl + } + + if !ch.IsValid() { + return nil, ErrInvalidContainerHandle + } + + return ch, nil +} + +// toCtx returns a new context with the ContainerHandle stashed in it. +// NB: Will panic if the context already has a different ContainerHandle stashed. +func (ch *ContainerHandle) toCtx(ctx context.Context) context.Context { + if ch == nil { + return ctx + } + + stashed, _ := chFromCtx(ctx) + if stashed != nil { + if stashed.UUID() == ch.UUID() { + return ctx + } + panic("attempt to stash different ContinerHandle in context") + } + + return context.WithValue(ctx, contHandleKey, ch) +} + +// IsValid returns true if the handle is valid. +func (ch *ContainerHandle) IsValid() bool { + return ch != nil && ch.connHandle.IsValid() && ch.PoolHandle.IsValid() +} + +// UUID returns the DAOS container's UUID. +func (ch *ContainerHandle) UUID() uuid.UUID { + if ch == nil { + return uuid.Nil + } + return ch.connHandle.UUID +} + +func newContainerInfo(poolUUID, contUUID uuid.UUID, cInfo *C.daos_cont_info_t, props *daos.ContainerPropertyList) *daos.ContainerInfo { + if cInfo == nil { + return nil + } + + ci := &daos.ContainerInfo{ + PoolUUID: poolUUID, + ContainerUUID: contUUID, + LatestSnapshot: daos.HLC(cInfo.ci_lsnapshot), + NumHandles: uint32(cInfo.ci_nhandles), + NumSnapshots: uint32(cInfo.ci_nsnapshots), + OpenTime: daos.HLC(cInfo.ci_md_otime), + CloseModifyTime: daos.HLC(cInfo.ci_md_mtime), + } + + if props != nil { + for _, prop := range props.Properties() { + switch prop.Type() { + case daos.ContainerPropLayout: + ci.Type = daos.ContainerLayout(prop.GetValue()) + case daos.ContainerPropLabel: + ci.ContainerLabel = prop.GetString() + case daos.ContainerPropRedunFactor: + ci.RedundancyFactor = uint32(prop.GetValue()) + case daos.ContainerPropStatus: + // Temporary; once we migrate the container properties into the API + // we can use the prop stringer. + statusInt := prop.GetValue() + coStatus := C.struct_daos_co_status{} + + C.daos_prop_val_2_co_status(C.uint64_t(statusInt), &coStatus) + switch coStatus.dcs_status { + case C.DAOS_PROP_CO_HEALTHY: + ci.Health = "HEALTHY" + case C.DAOS_PROP_CO_UNCLEAN: + ci.Health = "UNCLEAN" + } + } + } + } + + return ci +} + +func (ch *ContainerHandle) String() string { + return ch.PoolHandle.String() + fmt.Sprintf(":%s:%t", ch.connHandle.String(), ch.IsValid()) +} + +// Close performs a container close operation to release resources associated +// with the container handle. +func (ch *ContainerHandle) Close(ctx context.Context) error { + if !ch.IsValid() { + return ErrInvalidContainerHandle + } + logging.FromContext(ctx).Debugf("ContainerHandle.Close(%s)", ch) + + if err := daosError(daos_cont_close(ch.daosHandle)); err != nil { + return errors.Wrap(err, "failed to close container") + } + ch.invalidate() + + // If a pool connection was made as part of the container open operation, + // then we should disconnect from the pool now. This should be a no-op if + // the pool connection was already established and stashed in the context. + if ch.poolConnCleanup != nil { + ch.poolConnCleanup() + } + + return nil +} + +// DestroyContainer calls ContainerDestroy() for the specified container, which must +// be served by the pool opened in the handle. +func (ph *PoolHandle) DestroyContainer(ctx context.Context, contID string, force bool) error { + if !ph.IsValid() { + return ErrInvalidPoolHandle + } + logging.FromContext(ctx).Debugf("PoolHandle.DestroyContainer(%s:%s:%t)", ph, contID, force) + + return ContainerDestroy(ph.toCtx(ctx), "", "", contID, force) +} + +// ContainerDestroy destroys the specified container. Setting the force flag +// to true will destroy the container even if it has open handles. +func ContainerDestroy(ctx context.Context, sysName, poolID, contID string, force bool) error { + var poolConn *PoolHandle + var cleanup func() + var err error + + poolConn, cleanup, err = getPoolConn(ctx, sysName, poolID, daos.PoolConnectFlagReadWrite) + if err != nil { + if errors.Is(err, daos.NoPermission) { + // Even if we don't have pool-level write permissions, we may + // have delete permissions at the container level. + // TODO: Is this still correct? Came from the tool code, but quick testing with + // 2.8-ish code seemed to show that trying to a destroy a container without pool + // write permissions would result in a permission denied error. + poolConn, cleanup, err = getPoolConn(ctx, sysName, poolID, daos.PoolConnectFlagReadOnly) + } + if err != nil { + return err + } + } + defer cleanup() + logging.FromContext(ctx).Debugf("ContainerDestroy(%s:%s:%t)", poolConn, contID, force) + + cContID := C.CString(contID) + defer freeString(cContID) + if err := daosError(daos_cont_destroy(poolConn.daosHandle, cContID, goBool2int(force), nil)); err != nil { + return errors.Wrapf(err, "failed to destroy container %q", contID) + } + + return nil +} + +func contFlags2PoolFlags(contFlags daos.ContainerOpenFlag) daos.PoolConnectFlag { + var poolFlags daos.PoolConnectFlag + + if contFlags&daos.ContainerOpenFlagReadOnly != 0 { + poolFlags |= daos.PoolConnectFlagReadOnly + } + if contFlags&daos.ContainerOpenFlagReadWrite != 0 { + poolFlags |= daos.PoolConnectFlagReadWrite + } + if contFlags&daos.ContainerOpenFlagExclusive != 0 { + poolFlags |= daos.PoolConnectFlagExclusive + } + + return poolFlags +} + +// OpenContainer calls ContainerOpen() for the specified container, which must +// be served by the pool opened in the handle. +func (ph *PoolHandle) OpenContainer(ctx context.Context, req ContainerOpenReq) (*ContainerOpenResp, error) { + if !ph.IsValid() { + return nil, ErrInvalidPoolHandle + } + + return ContainerOpen(ph.toCtx(ctx), req) +} + +type ( + // ContainerOpenReq specifies the container open parameters. + ContainerOpenReq struct { + ID string + Flags daos.ContainerOpenFlag + Query bool + SysName string + PoolID string + } + + // ContainerOpenResp contains the handle and optional container information. + ContainerOpenResp struct { + Connection *ContainerHandle + Info *daos.ContainerInfo + } +) + +// ContainerOpen opens the container specified in the open request. +func ContainerOpen(ctx context.Context, req ContainerOpenReq) (_ *ContainerOpenResp, exitErr error) { + if _, err := chFromCtx(ctx); err == nil { + return nil, ErrContextHandleConflict + } + + poolHdl, poolDisc, err := getPoolConn(ctx, req.SysName, req.PoolID, daos.PoolConnectFlagReadOnly) + if err != nil { + return nil, errors.Wrapf(err, "failed to connect to pool %q", req.PoolID) + } + defer func() { + // If the pool connection was made for this Open operation (i.e. was not already set in the context), + // then we need to disconnect from the pool here in order to avoid leaking the pool handle. + if exitErr != nil && !poolHdl.fromCtx { + poolDisc() + } + }() + logging.FromContext(ctx).Debugf("ContainerOpen(%s:%+v)", poolHdl, req) + + if req.ID == "" { + return nil, errors.Wrap(daos.InvalidInput, "no container ID provided") + } + cContID := C.CString(req.ID) + defer freeString(cContID) + if req.Flags == 0 { + req.Flags = daos.ContainerOpenFlagReadOnly + } + + contHdl := &ContainerHandle{ + poolConnCleanup: poolDisc, + PoolHandle: poolHdl, + } + cContInfo := C.daos_cont_info_t{} + + if err := daosError(daos_cont_open(poolHdl.daosHandle, cContID, C.uint(req.Flags), &contHdl.connHandle.daosHandle, &cContInfo, nil)); err != nil { + return nil, errors.Wrapf(err, "failed to open container %q", req.ID) + } + + contHdl.connHandle.UUID, err = uuidFromC(cContInfo.ci_uuid) + if err != nil { + contHdl.poolConnCleanup = func() {} // Handled in defer. + if dcErr := contHdl.Close(ctx); dcErr != nil { + logging.FromContext(ctx).Error(dcErr.Error()) + } + return nil, errors.New("unable to parse container UUID from open") + } + + var contInfo *daos.ContainerInfo + if req.Query { + contInfo, err = contHdl.Query(ctx) + if err != nil { + contHdl.poolConnCleanup = func() {} // Handled in defer. + if dcErr := contHdl.Close(ctx); dcErr != nil { + logging.FromContext(ctx).Error(dcErr.Error()) + } + return nil, errors.Wrapf(err, "failed to query container %q", req.ID) + } + contHdl.connHandle.Label = contInfo.ContainerLabel + } else { + if _, err := uuid.Parse(req.ID); err != nil { + contHdl.connHandle.Label = req.ID + } + contInfo = newContainerInfo(poolHdl.UUID(), contHdl.UUID(), &cContInfo, nil) + } + + logging.FromContext(ctx).Debugf("Opened Container %s)", contHdl) + return &ContainerOpenResp{ + Connection: contHdl, + Info: contInfo, + }, nil +} + +// getContConn retrieves the ContainerHandle set in the context, if available, +// or tries to establish a new connection to the specified container. +func getContConn(ctx context.Context, sysName, poolID, contID string, flags daos.ContainerOpenFlag) (*ContainerHandle, func(), error) { + nulCleanup := func() {} + ch, err := chFromCtx(ctx) + if err == nil { + if contID != "" { + return nil, nulCleanup, errors.Wrap(daos.InvalidInput, "ContainerHandle found in context with non-empty contID") + } + return ch, nulCleanup, nil + } + + resp, err := ContainerOpen(ctx, ContainerOpenReq{ + ID: contID, + Flags: flags, + Query: false, + SysName: sysName, + PoolID: poolID, + }) + if err != nil { + return nil, nulCleanup, err + } + + cleanup := func() { + if err := resp.Connection.Close(ctx); err != nil { + logging.FromContext(ctx).Error(err.Error()) + } + resp.Connection.poolConnCleanup() + } + return resp.Connection, cleanup, nil +} + +func containerQueryDFSAttrs(contConn *ContainerHandle) (*daos.POSIXAttributes, error) { + var pa daos.POSIXAttributes + var dfs *C.dfs_t + var attr C.dfs_attr_t + + if !contConn.IsValid() { + return nil, ErrInvalidContainerHandle + } + + // NB: A relaxed-mode container may be mounted with the DFS_BALANCED + // flag, but the reverse is not true. So we use DFS_BALANCED as the + // default for querying the DFS attributes. + var mountFlags C.int = C.O_RDONLY | C.DFS_BALANCED + rc := dfs_mount(contConn.PoolHandle.daosHandle, contConn.daosHandle, mountFlags, &dfs) + if err := dfsError(rc); err != nil { + return nil, errors.Wrap(err, "failed to mount container") + } + + rc = dfs_query(dfs, &attr) + if err := dfsError(rc); err != nil { + return nil, errors.Wrap(err, "failed to query container") + } + pa.ChunkSize = uint64(attr.da_chunk_size) + pa.ObjectClass = daos.ObjectClass(attr.da_oclass_id) + pa.DirObjectClass = daos.ObjectClass(attr.da_dir_oclass_id) + pa.FileObjectClass = daos.ObjectClass(attr.da_file_oclass_id) + pa.ConsistencyMode = uint32(attr.da_mode) + pa.Hints = C.GoString(&attr.da_hints[0]) + + if err := dfsError(dfs_umount(dfs)); err != nil { + return nil, errors.Wrap(err, "failed to unmount container") + } + + return &pa, nil +} + +// QueryContainer calls ContainerQuery() for the specified container, which must +// be served by the pool opened in the handle. +func (ph *PoolHandle) QueryContainer(ctx context.Context, contID string) (*daos.ContainerInfo, error) { + if !ph.IsValid() { + return nil, ErrInvalidPoolHandle + } + logging.FromContext(ctx).Debugf("PoolHandle.QueryContainer(%s:%s)", ph, contID) + + return ContainerQuery(ph.toCtx(ctx), "", "", contID) +} + +// Query calls ContainerQuery() for the container in the handle. +func (ch *ContainerHandle) Query(ctx context.Context) (*daos.ContainerInfo, error) { + if !ch.IsValid() { + return nil, ErrInvalidContainerHandle + } + logging.FromContext(ctx).Debugf("ContainerHandle.Query(%s)", ch) + + return ContainerQuery(ch.toCtx(ctx), "", "", "") +} + +// ContainerQuery queries the specified container and returns its information. +func ContainerQuery(ctx context.Context, sysName, poolID, contID string) (*daos.ContainerInfo, error) { + queryOpenFlags := daos.ContainerOpenFlagReadOnly | daos.ContainerOpenFlagForce | daos.ContainerOpenFlagReadOnlyMetadata + contConn, cleanup, err := getContConn(ctx, sysName, poolID, contID, queryOpenFlags) + if err != nil { + return nil, err + } + defer cleanup() + logging.FromContext(ctx).Debugf("ContainerQuery(%s)", contConn) + + props, err := daos.NewContainerPropertyList(4) + if err != nil { + return nil, err + } + defer props.Free() + + props.MustAddEntryType(daos.ContainerPropLayout) + props.MustAddEntryType(daos.ContainerPropLabel) + props.MustAddEntryType(daos.ContainerPropRedunFactor) + props.MustAddEntryType(daos.ContainerPropStatus) + + cProps := (*C.daos_prop_t)(props.ToPtr()) + var dci C.daos_cont_info_t + if err := daosError(daos_cont_query(contConn.daosHandle, &dci, cProps, nil)); err != nil { + return nil, errors.Wrap(err, "failed to query container") + } + + ciUUID, err := uuidFromC(dci.ci_uuid) + if err != nil { + return nil, errors.New("unable to parse container UUID from query") + } + if contConn.connHandle.UUID == uuid.Nil { + contConn.connHandle.UUID = ciUUID + } else if contConn.connHandle.UUID != ciUUID { + return nil, errors.Errorf("queried container UUID != handle UUID: %s != %s", ciUUID, contConn.connHandle.UUID) + } + + info := newContainerInfo(contConn.PoolHandle.UUID(), contConn.UUID(), &dci, props) + if info.Type == daos.ContainerLayoutPOSIX { + posixAttrs, err := containerQueryDFSAttrs(contConn) + if err != nil { + return nil, errors.Wrap(err, "failed to query DFS attributes") + } + info.POSIXAttributes = posixAttrs + } + + return info, nil +} + +// ListAttributes calls ContainerListAttributes() for the container in the handle. +func (ch *ContainerHandle) ListAttributes(ctx context.Context) ([]string, error) { + if !ch.IsValid() { + return nil, ErrInvalidContainerHandle + } + return ContainerListAttributes(ch.toCtx(ctx), "", "", "") +} + +// ContainerListAttributes returns a list of user-definable container attribute names. +func ContainerListAttributes(ctx context.Context, sysName, poolID, contID string) ([]string, error) { + contConn, cleanup, err := getContConn(ctx, sysName, poolID, contID, daos.ContainerOpenFlagReadOnlyMetadata) + if err != nil { + return nil, err + } + defer cleanup() + logging.FromContext(ctx).Debugf("ContainerListAttributes(%s)", contConn) + + if err := ctx.Err(); err != nil { + return nil, ctxErr(err) + } + + return listDaosAttributes(contConn.daosHandle, contAttr) +} + +// GetAttributes calls ContainerGetAttributes() for the container in the handle. +func (ch *ContainerHandle) GetAttributes(ctx context.Context, attrNames ...string) (daos.AttributeList, error) { + if !ch.IsValid() { + return nil, ErrInvalidContainerHandle + } + return ContainerGetAttributes(ch.toCtx(ctx), "", "", "", attrNames...) +} + +// ContainerGetAttributes fetches the specified container attributes. If no +// attribute names are provided, all attributes are fetched. +func ContainerGetAttributes(ctx context.Context, sysName, poolID, contID string, names ...string) (daos.AttributeList, error) { + contConn, cleanup, err := getContConn(ctx, sysName, poolID, contID, daos.ContainerOpenFlagReadOnlyMetadata) + if err != nil { + return nil, err + } + defer cleanup() + logging.FromContext(ctx).Debugf("ContainerGetAttributes(%s:%v)", contConn, names) + + if err := ctx.Err(); err != nil { + return nil, ctxErr(err) + } + + return getDaosAttributes(contConn.daosHandle, contAttr, names) +} + +// SetAttributes calls ContainerSetAttributes() for the container in the handle. +func (ch *ContainerHandle) SetAttributes(ctx context.Context, attrs ...*daos.Attribute) error { + if !ch.IsValid() { + return ErrInvalidContainerHandle + } + return ContainerSetAttributes(ch.toCtx(ctx), "", "", "", attrs...) +} + +// ContainerSetAttributes sets the specified container attributes. +func ContainerSetAttributes(ctx context.Context, sysName, poolID, contID string, attrs ...*daos.Attribute) error { + contConn, cleanup, err := getContConn(ctx, sysName, poolID, contID, daos.ContainerOpenFlagReadWrite) + if err != nil { + return err + } + defer cleanup() + logging.FromContext(ctx).Debugf("ContainerSetAttributes(%s:%v)", contConn, attrs) + + if err := ctx.Err(); err != nil { + return ctxErr(err) + } + + return setDaosAttributes(contConn.daosHandle, contAttr, attrs) +} + +// DeleteAttributes calls ContainerDeleteAttributes() for the container in the handle. +func (ch *ContainerHandle) DeleteAttributes(ctx context.Context, attrNames ...string) error { + if !ch.IsValid() { + return ErrInvalidContainerHandle + } + return ContainerDeleteAttributes(ch.toCtx(ctx), "", "", "", attrNames...) +} + +// ContainerDeleteAttributes deletes the specified pool attributes. +func ContainerDeleteAttributes(ctx context.Context, sysName, poolID, contID string, attrNames ...string) error { + contConn, cleanup, err := getContConn(ctx, sysName, poolID, contID, daos.ContainerOpenFlagReadWrite) + if err != nil { + return err + } + defer cleanup() + logging.FromContext(ctx).Debugf("ContainerDeleteAttributes(%s:%+v)", contConn, attrNames) + + if err := ctx.Err(); err != nil { + return ctxErr(err) + } + + return delDaosAttributes(contConn.daosHandle, contAttr, attrNames) +} diff --git a/src/control/lib/daos/api/container_test.go b/src/control/lib/daos/api/container_test.go new file mode 100644 index 00000000000..95cef26d951 --- /dev/null +++ b/src/control/lib/daos/api/container_test.go @@ -0,0 +1,880 @@ +// +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package api + +import ( + "context" + "fmt" + "reflect" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + + "github.com/daos-stack/daos/src/control/build" + "github.com/daos-stack/daos/src/control/common/test" + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/daos-stack/daos/src/control/lib/ranklist" +) + +var ( + testContName = "test-container" +) + +func TestAPI_ContainerOpen(t *testing.T) { + defaultReq := ContainerOpenReq{ + ID: daos_default_ContainerInfo.ContainerLabel, + Flags: daos.ContainerOpenFlagReadWrite, + Query: true, + SysName: build.DefaultSystemName, + PoolID: daos_default_PoolInfo.Label, + } + + for name, tc := range map[string]struct { + setup func(t *testing.T) + ctx context.Context + openReq ContainerOpenReq + checkParams func(t *testing.T) + expResp *ContainerOpenResp + expErr error + }{ + "nil context": { + openReq: defaultReq, + expErr: errNilCtx, + }, + "no contID in req": { + ctx: test.Context(t), + openReq: ContainerOpenReq{ + SysName: defaultReq.SysName, + Flags: defaultReq.Flags, + Query: defaultReq.Query, + PoolID: defaultReq.PoolID, + }, + expErr: errors.Wrap(daos.InvalidInput, "no container ID provided"), + }, + "context already has a connection for a container": { + ctx: func() context.Context { + otherContHdl := &ContainerHandle{ + connHandle: connHandle{ + daosHandle: defaultContHdl(), + UUID: test.MockPoolUUID(99), + Label: "not-the-container-you're-looking-for", + }, + PoolHandle: defaultPoolHandle(), + } + return otherContHdl.toCtx(test.Context(t)) + }(), + expErr: ErrContextHandleConflict, + }, + "daos_cont_open() fails": { + setup: func(t *testing.T) { + daos_cont_open_RC = _Ctype_int(daos.IOError) + }, + ctx: test.Context(t), + openReq: defaultReq, + expErr: errors.New("failed to open container"), + }, + "daos_cont_open() succeeds (no pool in context)": { + ctx: test.Context(t), + openReq: defaultReq, + checkParams: func(t *testing.T) { + test.CmpAny(t, "pool connect count", 1, daos_pool_connect_Count) + test.CmpAny(t, "contID", defaultReq.ID, daos_cont_open_SetContainerID) + test.CmpAny(t, "flags", defaultReq.Flags, daos_cont_open_SetFlags) + }, + expResp: &ContainerOpenResp{ + Connection: &ContainerHandle{ + connHandle: connHandle{ + Label: daos_default_ContainerInfo.ContainerLabel, + UUID: daos_default_ContainerInfo.ContainerUUID, + daosHandle: defaultContHdl(), + }, + PoolHandle: defaultPoolHandle(), + }, + Info: defaultContainerInfo(), + }, + }, + "daos_cont_open() succeeds (pool in context)": { + ctx: defaultPoolHandle().toCtx(test.Context(t)), + openReq: ContainerOpenReq{ + ID: defaultReq.ID, + Flags: defaultReq.Flags, + Query: defaultReq.Query, + }, + checkParams: func(t *testing.T) { + test.CmpAny(t, "pool connect count", 0, daos_pool_connect_Count) + test.CmpAny(t, "contID", defaultReq.ID, daos_cont_open_SetContainerID) + test.CmpAny(t, "flags", defaultReq.Flags, daos_cont_open_SetFlags) + }, + expResp: &ContainerOpenResp{ + Connection: &ContainerHandle{ + connHandle: connHandle{ + Label: daos_default_ContainerInfo.ContainerLabel, + UUID: daos_default_ContainerInfo.ContainerUUID, + daosHandle: defaultContHdl(), + }, + PoolHandle: defaultPoolHandle(), + }, + Info: defaultContainerInfo(), + }, + }, + "Open with UUID and query enabled": { + ctx: test.Context(t), + openReq: ContainerOpenReq{ + ID: daos_default_ContainerInfo.ContainerUUID.String(), + Flags: defaultReq.Flags, + Query: true, + PoolID: defaultReq.PoolID, + }, + expResp: &ContainerOpenResp{ + Connection: &ContainerHandle{ + connHandle: connHandle{ + Label: daos_default_ContainerInfo.ContainerLabel, + UUID: daos_default_ContainerInfo.ContainerUUID, + daosHandle: defaultContHdl(), + }, + PoolHandle: defaultPoolHandle(), + }, + Info: defaultContainerInfo(), + }, + }, + "Open with UUID and query enabled -- query fails": { + setup: func(t *testing.T) { + daos_cont_query_RC = _Ctype_int(daos.IOError) + }, + ctx: test.Context(t), + openReq: ContainerOpenReq{ + ID: daos_default_ContainerInfo.ContainerUUID.String(), + Flags: defaultReq.Flags, + Query: true, + PoolID: defaultReq.PoolID, + }, + checkParams: func(t *testing.T) { + // Make sure we don't leak handles when handling errors. + test.CmpAny(t, "container close count", 1, daos_cont_close_Count) + test.CmpAny(t, "pool disconnect count", 1, daos_pool_disconnect_Count) + }, + expErr: daos.IOError, + }, + "Open with UUID and query disabled": { + ctx: test.Context(t), + openReq: ContainerOpenReq{ + ID: daos_default_ContainerInfo.ContainerUUID.String(), + Flags: defaultReq.Flags, + Query: false, + PoolID: defaultReq.PoolID, + }, + expResp: &ContainerOpenResp{ + Connection: &ContainerHandle{ + connHandle: connHandle{ + UUID: daos_default_ContainerInfo.ContainerUUID, + daosHandle: defaultContHdl(), + }, + PoolHandle: defaultPoolHandle(), + }, + Info: func() *daos.ContainerInfo { + ci := defaultContainerInfo() + ci.ContainerLabel = "" + ci.Type = 0 + ci.Health = "" + ci.POSIXAttributes = nil + return ci + }(), + }, + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + if tc.checkParams != nil { + defer tc.checkParams(t) + } + + gotResp, gotErr := ContainerOpen(mustLogCtx(tc.ctx, t), tc.openReq) + test.CmpErr(t, tc.expErr, gotErr) + if tc.expErr != nil { + return + } + + cmpOpts := cmp.Options{ + cmp.Comparer(func(a, b *ContainerHandle) bool { + return a != nil && b != nil && a.String() == b.String() + }), + } + test.CmpAny(t, "ContainerOpenResp", tc.expResp, gotResp, cmpOpts...) + }) + } +} + +func TestAPI_ContainerDestroy(t *testing.T) { + for name, tc := range map[string]struct { + setup func(t *testing.T) + ctx context.Context + poolID string + contID string + checkParams func(t *testing.T) + expErr error + }{ + "nil context": { + expErr: errNilCtx, + }, + "pool_connect with read/write flag fails, but succeeds with readonly": { + setup: func(t *testing.T) { + daos_pool_connect_RCList = []_Ctype_int{ + _Ctype_int(daos.NoPermission), + 0, + } + }, + ctx: test.Context(t), + poolID: testPoolName, + contID: testContName, + }, + "daos_cont_destroy fails": { + setup: func(t *testing.T) { + daos_cont_destroy_RC = _Ctype_int(daos.IOError) + }, + ctx: test.Context(t), + poolID: testPoolName, + contID: testContName, + checkParams: func(t *testing.T) { + test.CmpAny(t, "pool disconnect count", 1, daos_pool_disconnect_Count) + }, + expErr: errors.New("failed to destroy container"), + }, + "daos_cont_destroy succeeds": { + ctx: test.Context(t), + poolID: testPoolName, + contID: testContName, + checkParams: func(t *testing.T) { + test.CmpAny(t, "pool disconnect count", 1, daos_pool_disconnect_Count) + }, + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + if tc.checkParams != nil { + defer tc.checkParams(t) + } + + gotErr := ContainerDestroy(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.contID, false) + test.CmpErr(t, tc.expErr, gotErr) + }) + } +} + +func TestAPI_getContConn(t *testing.T) { + otherContHdl := &ContainerHandle{ + connHandle: connHandle{ + daosHandle: defaultContHdl(), + UUID: test.MockPoolUUID(99), + Label: "not-the-container-you're-looking-for", + }, + PoolHandle: defaultPoolHandle(), + } + + for name, tc := range map[string]struct { + setup func(t *testing.T) + ctx context.Context + poolID string + contID string + flags daos.ContainerOpenFlag + checkParams func(t *testing.T) + expHdl *ContainerHandle + expErr error + }{ + "nil context": { + expErr: errNilCtx, + }, + "pool handle not in context, no poolID": { + ctx: test.Context(t), + contID: testContName, + expErr: errors.Wrap(daos.InvalidInput, "no pool ID provided"), + }, + "pool not in context; pool connect fails": { + ctx: test.Context(t), + setup: func(t *testing.T) { + daos_pool_connect_RC = _Ctype_int(daos.IOError) + }, + poolID: testPoolName, + contID: testContName, + expErr: errors.Wrap(daos.IOError, "failed to connect to pool"), + }, + "pool handle in context with non-empty poolID": { + ctx: defaultPoolHandle().toCtx(test.Context(t)), + poolID: testPoolName, + expErr: errors.New("PoolHandle found in context with non-empty poolID"), + }, + "container handle not in context, no contID": { + ctx: defaultPoolHandle().toCtx(test.Context(t)), + expErr: errors.Wrap(daos.InvalidInput, "no container ID provided"), + }, + "container handle in context with non-empty contID": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + contID: testContName, + expErr: errors.New("non-empty contID"), + }, + "context already has a connection for a container": { + ctx: otherContHdl.toCtx(test.Context(t)), + checkParams: func(t *testing.T) { + test.CmpAny(t, "pool connect count", 0, daos_pool_connect_Count) + test.CmpAny(t, "cont open count", 0, daos_cont_open_Count) + }, + expHdl: otherContHdl, + }, + "pool handle from context; container open fails": { + ctx: defaultPoolHandle().toCtx(test.Context(t)), + setup: func(t *testing.T) { + daos_cont_open_RC = _Ctype_int(daos.IOError) + }, + contID: testContName, + checkParams: func(t *testing.T) { + // Pool was already connected, so it shouldn't be disconnected in this + // error handler. + test.CmpAny(t, "pool disconnect count", 0, daos_pool_disconnect_Count) + }, + expErr: errors.New("failed to open container"), + }, + "pool handle from Connect(); container open fails": { + setup: func(t *testing.T) { + daos_cont_open_RC = _Ctype_int(daos.IOError) + }, + ctx: test.Context(t), + poolID: testPoolName, + contID: testContName, + checkParams: func(t *testing.T) { + // Pool needed to be connected, so it should be disconnected in this + // error handler. + test.CmpAny(t, "pool disconnect count", 1, daos_pool_disconnect_Count) + }, + expErr: errors.New("failed to open container"), + }, + "pool handle from context; container handle from Open()": { + ctx: defaultPoolHandle().toCtx(test.Context(t)), + contID: testContName, + checkParams: func(t *testing.T) { + test.CmpAny(t, "pool connect count", 0, daos_pool_connect_Count) + + test.CmpAny(t, "cont open count", 1, daos_cont_open_Count) + test.CmpAny(t, "contID", testContName, daos_cont_open_SetContainerID) + test.CmpAny(t, "open flags", daos.ContainerOpenFlagReadOnly, daos_cont_open_SetFlags) + }, + expHdl: defaultContainerHandle(), + }, + "pool handle from Connect(); container handle from Open()": { + ctx: test.Context(t), + poolID: testPoolName, + contID: testContName, + checkParams: func(t *testing.T) { + test.CmpAny(t, "pool connect count", 1, daos_pool_connect_Count) + test.CmpAny(t, "poolID", testPoolName, daos_pool_connect_SetPoolID) + test.CmpAny(t, "sysName", build.DefaultSystemName, daos_pool_connect_SetSys) + test.CmpAny(t, "connect flags", daos.PoolConnectFlagReadOnly, daos_pool_connect_SetFlags) + test.CmpAny(t, "pool query", daos.PoolQueryMask(0), daos_pool_connect_QueryMask) + + test.CmpAny(t, "cont open count", 1, daos_cont_open_Count) + test.CmpAny(t, "contID", testContName, daos_cont_open_SetContainerID) + test.CmpAny(t, "open flags", daos.ContainerOpenFlagReadOnly, daos_cont_open_SetFlags) + }, + expHdl: defaultContainerHandle(), + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + if tc.checkParams != nil { + defer tc.checkParams(t) + } + + ph, cleanup, gotErr := getContConn(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.contID, tc.flags) + test.CmpErr(t, tc.expErr, gotErr) + if tc.expErr != nil { + return + } + t.Cleanup(cleanup) + + cmpOpts := cmp.Options{ + cmp.Comparer(func(a, b *ContainerHandle) bool { + return a != nil && b != nil && a.String() == b.String() + }), + } + test.CmpAny(t, "ContainerHandle", tc.expHdl, ph, cmpOpts...) + }) + } +} + +func TestAPI_containerQueryDFSAttrs(t *testing.T) { + for name, tc := range map[string]struct { + setup func(t *testing.T) + hdl *ContainerHandle + expAttrs *daos.POSIXAttributes + expErr error + }{ + "nil handle": { + expErr: ErrInvalidContainerHandle, + }, + "dfs_mount fails": { + setup: func(t *testing.T) { + dfs_mount_RC = 22 + }, + hdl: defaultContainerHandle(), + expErr: errors.New("DFS error"), + }, + "dfs_query fails": { + setup: func(t *testing.T) { + dfs_query_RC = 22 + }, + hdl: defaultContainerHandle(), + expErr: errors.New("DFS error"), + }, + "dfs_umount fails": { + setup: func(t *testing.T) { + dfs_umount_RC = 22 + }, + hdl: defaultContainerHandle(), + expErr: errors.New("DFS error"), + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + gotAttrs, gotErr := containerQueryDFSAttrs(tc.hdl) + test.CmpErr(t, tc.expErr, gotErr) + if tc.expErr != nil { + return + } + + test.CmpAny(t, "DFS attributes", tc.expAttrs, gotAttrs) + }) + } +} + +func TestAPI_ContainerQuery(t *testing.T) { + for name, tc := range map[string]struct { + setup func(t *testing.T) + ctx context.Context + poolID string + contID string + checkParams func(t *testing.T) + expResp *daos.ContainerInfo + expErr error + }{ + "nil context": { + expErr: errNilCtx, + }, + "daos_cont_query() fails": { + setup: func(t *testing.T) { + daos_cont_query_RC = _Ctype_int(daos.IOError) + }, + ctx: test.Context(t), + poolID: daos_default_PoolInfo.Label, + contID: daos_default_ContainerInfo.ContainerLabel, + expErr: errors.Wrap(daos.IOError, "failed to query container"), + }, + "DFS query fails on POSIX container": { + setup: func(t *testing.T) { + dfs_query_RC = 22 + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expErr: errors.New("DFS error"), + }, + "success": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expResp: defaultContainerInfo(), + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + if tc.checkParams != nil { + defer tc.checkParams(t) + } + + gotResp, err := ContainerQuery(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.contID) + test.CmpErr(t, tc.expErr, err) + if tc.expErr != nil { + return + } + + cmpOpts := cmp.Options{ + cmp.Comparer(func(a, b ranklist.RankSet) bool { + return a.String() == b.String() + }), + } + test.CmpAny(t, "ContainerQuery() ContainerInfo", tc.expResp, gotResp, cmpOpts...) + }) + } +} + +func TestAPI_ContainerListAttributes(t *testing.T) { + for name, tc := range map[string]struct { + setup func(t *testing.T) + ctx context.Context + poolID string + contID string + expNames []string + expErr error + }{ + "nil context": { + expErr: errNilCtx, + }, + "daos_cont_list_attr() fails (get buf size)": { + setup: func(t *testing.T) { + daos_cont_list_attr_RC = _Ctype_int(daos.IOError) + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expErr: errors.Wrap(daos.IOError, "failed to list container attributes"), + }, + "daos_cont_list_attr() fails (fetch names)": { + setup: func(t *testing.T) { + daos_cont_list_attr_RCList = []_Ctype_int{ + 0, + _Ctype_int(daos.IOError), + } + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expErr: errors.Wrap(daos.IOError, "failed to list container attributes"), + }, + "no attributes set": { + setup: func(t *testing.T) { + daos_cont_list_attr_AttrList = nil + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + }, + "success": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expNames: []string{ + daos_default_AttrList[0].Name, + daos_default_AttrList[1].Name, + daos_default_AttrList[2].Name, + }, + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + gotNames, err := ContainerListAttributes(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.contID) + test.CmpErr(t, tc.expErr, err) + if tc.expErr != nil { + return + } + + test.CmpAny(t, "ContainerListAttributes()", tc.expNames, gotNames) + }) + } +} + +func TestAPI_ContainerGetAttributes(t *testing.T) { + for name, tc := range map[string]struct { + setup func(t *testing.T) + ctx context.Context + poolID string + contID string + attrNames []string + checkParams func(t *testing.T) + expAttrs daos.AttributeList + expErr error + }{ + "nil context": { + expErr: errNilCtx, + }, + "daos_cont_list_attr() fails": { + setup: func(t *testing.T) { + daos_cont_list_attr_RC = _Ctype_int(daos.IOError) + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expErr: errors.Wrap(daos.IOError, "failed to list container attributes"), + }, + "daos_cont_get_attr() fails (sizes)": { + setup: func(t *testing.T) { + daos_cont_get_attr_RC = _Ctype_int(daos.IOError) + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expErr: errors.Wrap(daos.IOError, "failed to get container attribute sizes"), + }, + "daos_cont_get_attr() fails (values)": { + setup: func(t *testing.T) { + daos_cont_get_attr_RCList = []_Ctype_int{ + 0, + _Ctype_int(daos.IOError), + } + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expErr: errors.Wrap(daos.IOError, "failed to get container attribute values"), + }, + "empty requested attribute name": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + attrNames: test.JoinArgs(nil, "a", ""), + expErr: errors.Errorf("empty container attribute name at index 1"), + }, + "no attributes set; attributes requested": { + setup: func(t *testing.T) { + daos_cont_get_attr_AttrList = nil + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + attrNames: test.JoinArgs(nil, "foo"), + checkParams: func(t *testing.T) { + test.CmpAny(t, "req attr names", map[string]struct{}{"foo": {}}, daos_cont_get_attr_ReqNames) + }, + expErr: errors.Wrap(daos.Nonexistent, "failed to get container attribute sizes"), + }, + "unknown attribute requested": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + attrNames: test.JoinArgs(nil, "foo"), + checkParams: func(t *testing.T) { + test.CmpAny(t, "req attr names", map[string]struct{}{"foo": {}}, daos_cont_get_attr_ReqNames) + }, + expErr: errors.Wrap(daos.Nonexistent, "failed to get container attribute sizes"), + }, + "no attributes set; no attributes requested": { + setup: func(t *testing.T) { + daos_cont_list_attr_AttrList = nil + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + }, + "success; all attributes": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expAttrs: daos_default_AttrList, + }, + "success; requested attributes": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + attrNames: test.JoinArgs(nil, daos_default_AttrList[0].Name, daos_default_AttrList[2].Name), + checkParams: func(t *testing.T) { + reqNames := test.JoinArgs(nil, daos_default_AttrList[0].Name, daos_default_AttrList[2].Name) + sort.Strings(reqNames) + gotNames := daos_test_get_mappedNames(daos_cont_get_attr_ReqNames) + sort.Strings(gotNames) + test.CmpAny(t, "req attr names", reqNames, gotNames) + }, + expAttrs: daos.AttributeList{ + daos_default_AttrList[0], + daos_default_AttrList[2], + }, + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + if tc.checkParams != nil { + defer tc.checkParams(t) + } + + gotAttrs, err := ContainerGetAttributes(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.contID, tc.attrNames...) + test.CmpErr(t, tc.expErr, err) + if tc.expErr != nil { + return + } + + test.CmpAny(t, "ContainerGetAttributes() daos.AttributeList", tc.expAttrs, gotAttrs) + }) + } +} + +func TestAPI_ContainerSetAttributes(t *testing.T) { + for name, tc := range map[string]struct { + setup func(t *testing.T) + ctx context.Context + poolID string + contID string + toSet daos.AttributeList + expErr error + }{ + "nil context": { + expErr: errNilCtx, + }, + "no attributes to set": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expErr: errors.Wrap(daos.InvalidInput, "no container attributes provided"), + }, + "nil toSet attribute": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + toSet: append(daos_default_AttrList, nil), + expErr: errors.Wrap(daos.InvalidInput, "nil container attribute at index 3"), + }, + "toSet attribute with empty name": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + toSet: append(daos_default_AttrList, &daos.Attribute{Name: ""}), + expErr: errors.Wrap(daos.InvalidInput, "empty container attribute name at index 3"), + }, + "toSet attribute with empty value": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + toSet: append(daos_default_AttrList, &daos.Attribute{Name: "empty"}), + expErr: errors.Wrap(daos.InvalidInput, "empty container attribute value at index 3"), + }, + "daos_cont_set_attr() fails": { + setup: func(t *testing.T) { + daos_cont_set_attr_RC = _Ctype_int(daos.IOError) + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + toSet: daos_default_AttrList, + expErr: errors.Wrap(daos.IOError, "failed to set container attributes"), + }, + "success": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + toSet: daos_default_AttrList, + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + err := ContainerSetAttributes(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.contID, tc.toSet...) + test.CmpErr(t, tc.expErr, err) + if tc.expErr != nil { + return + } + + test.CmpAny(t, "ContainerSetAttributes() daos.AttributeList", tc.toSet, daos_cont_set_attr_AttrList) + }) + } +} + +func TestAPI_ContainerDeleteAttributes(t *testing.T) { + for name, tc := range map[string]struct { + setup func(t *testing.T) + ctx context.Context + poolID string + contID string + toDelete []string + expErr error + }{ + "nil context": { + expErr: errNilCtx, + }, + "no attributes to delete": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + expErr: errors.Wrap(daos.InvalidInput, "no container attribute names provided"), + }, + "empty name in toDelete list": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + toDelete: test.JoinArgs(nil, "foo", "", "bar"), + expErr: errors.Wrap(daos.InvalidInput, "empty container attribute name at index 1"), + }, + "daos_cont_det_attr() fails": { + setup: func(t *testing.T) { + daos_cont_del_attr_RC = _Ctype_int(daos.IOError) + }, + ctx: defaultContainerHandle().toCtx(test.Context(t)), + toDelete: test.JoinArgs(nil, daos_default_AttrList[0].Name), + expErr: errors.Wrap(daos.IOError, "failed to delete container attributes"), + }, + "success": { + ctx: defaultContainerHandle().toCtx(test.Context(t)), + toDelete: test.JoinArgs(nil, daos_default_AttrList[0].Name), + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + if tc.setup != nil { + tc.setup(t) + } + + err := ContainerDeleteAttributes(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.contID, tc.toDelete...) + test.CmpErr(t, tc.expErr, err) + if tc.expErr != nil { + return + } + + test.CmpAny(t, "ContainerDeleteAttributes() AttrNames", tc.toDelete, daos_cont_del_attr_AttrNames) + }) + } +} + +func TestAPI_ContainerHandleMethods(t *testing.T) { + thType := reflect.TypeOf(defaultContainerHandle()) + for i := 0; i < thType.NumMethod(); i++ { + method := thType.Method(i) + methArgs := make([]reflect.Value, 0) + ctx := test.Context(t) + var expResults int + + switch method.Name { + case "Close": + expResults = 1 + case "Query": + expResults = 2 + case "ListAttributes": + expResults = 2 + case "GetAttributes": + methArgs = append(methArgs, reflect.ValueOf(daos_default_AttrList[0].Name)) + expResults = 2 + case "SetAttributes": + methArgs = append(methArgs, reflect.ValueOf(daos_default_AttrList[0])) + expResults = 1 + case "DeleteAttributes": + methArgs = append(methArgs, reflect.ValueOf(daos_default_AttrList[0].Name)) + expResults = 1 + case "FillHandle", "IsValid", "String", "UUID", "ID": + // No tests for these. The main point of this suite is to ensure that the + // convenience wrappers handle inputs as expected. + continue + default: + // If you're here, you need to add a case to test your new method. + t.Fatalf("unhandled method %q", method.Name) + } + + // Not intended to be exhaustive; just verify that they accept the parameters + // we expect and return something sensible for errors. + for name, tc := range map[string]struct { + setup func(t *testing.T) + th *ContainerHandle + expErr error + }{ + fmt.Sprintf("%s: nil handle", method.Name): { + th: nil, + expErr: ErrInvalidContainerHandle, + }, + fmt.Sprintf("%s: success", method.Name): { + th: defaultContainerHandle(), + }, + } { + t.Run(name, func(t *testing.T) { + thArg := reflect.ValueOf(tc.th) + if tc.th == nil { + thArg = reflect.New(thType).Elem() + } + ctxArg := reflect.ValueOf(ctx) + testArgs := append([]reflect.Value{thArg, ctxArg}, methArgs...) + t.Logf("\nargs: %+v", testArgs) + + retVals := method.Func.Call(testArgs) + if len(retVals) != expResults { + t.Fatalf("expected %d return values, got %d", expResults, len(retVals)) + } + + if err, ok := retVals[len(retVals)-1].Interface().(error); ok { + test.CmpErr(t, tc.expErr, err) + } else { + test.CmpErr(t, tc.expErr, nil) + } + }) + } + } +} diff --git a/src/control/lib/daos/api/errors.go b/src/control/lib/daos/api/errors.go index 623b61243c0..33755b6d962 100644 --- a/src/control/lib/daos/api/errors.go +++ b/src/control/lib/daos/api/errors.go @@ -21,13 +21,13 @@ import ( import "C" var ( - ErrNoSystemRanks = errors.New("no ranks in system") - ErrContextHandleConflict = errors.New("context already contains a handle for a different pool or container") - ErrInvalidPoolHandle = errors.New("pool handle is nil or invalid") + ErrNoSystemRanks = errors.New("no ranks in system") + ErrContextHandleConflict = errors.New("context already contains a handle for a different pool or container") + ErrInvalidPoolHandle = errors.New("pool handle is nil or invalid") + ErrInvalidContainerHandle = errors.New("container handle is nil or invalid") - errInvalidContainerHandle = errors.New("container handle is nil or invalid") - errNilCtx = errors.New("nil context") - errNoCtxHdl = errors.New("no handle in context") + errNilCtx = errors.New("nil context") + errNoCtxHdl = errors.New("no handle in context") ) // dfsError converts a return code from a DFS API diff --git a/src/control/lib/daos/api/handle.go b/src/control/lib/daos/api/handle.go index e729b7401e5..34981c71ec6 100644 --- a/src/control/lib/daos/api/handle.go +++ b/src/control/lib/daos/api/handle.go @@ -24,7 +24,9 @@ import ( import "C" const ( - MissingPoolLabel = "" + // MissingPoolLabel defines a default label set when a pool connection was made by UUID. + MissingPoolLabel = "" + // MissingContainerLabel defines a default label set when a container connection was made by UUID. MissingContainerLabel = "" ) @@ -37,6 +39,7 @@ type ( UUID uuid.UUID Label string daosHandle C.daos_handle_t + fromCtx bool } ) @@ -57,9 +60,12 @@ func (ch *connHandle) invalidate() { // it is provided for compatibility with older code that calls // into libdaos directly. func (ch *connHandle) FillHandle(cHandle unsafe.Pointer) error { - if ch == nil || cHandle == nil { + if !ch.IsValid() { return errors.New("invalid handle") } + if cHandle == nil { + return errors.New("nil DAOS handle pointer") + } (*C.daos_handle_t)(cHandle).cookie = ch.daosHandle.cookie return nil @@ -70,11 +76,22 @@ func (ch *connHandle) IsValid() bool { if ch == nil { return false } + if (ch.Label == "" || ch.Label == MissingPoolLabel || ch.Label == MissingContainerLabel) && ch.UUID == uuid.Nil { + return false + } + return bool(daos_handle_is_valid(ch.daosHandle)) } // ID returns the label if available, otherwise the UUID. func (ch *connHandle) ID() string { + if ch == nil { + return "" + } + if !ch.IsValid() { + return "" + } + id := ch.Label if id == "" || id == MissingPoolLabel || id == MissingContainerLabel { id = ch.UUID.String() @@ -84,6 +101,13 @@ func (ch *connHandle) ID() string { } func (ch *connHandle) String() string { + if ch == nil { + return ":false" + } + if !ch.IsValid() { + return ":false" + } + id := ch.Label if id == "" || id == MissingPoolLabel || id == MissingContainerLabel { id = logging.ShortUUID(ch.UUID) diff --git a/src/control/lib/daos/api/handle_test.go b/src/control/lib/daos/api/handle_test.go new file mode 100644 index 00000000000..131c6b7116d --- /dev/null +++ b/src/control/lib/daos/api/handle_test.go @@ -0,0 +1,119 @@ +// +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package api + +import ( + "testing" + "unsafe" + + "github.com/daos-stack/daos/src/control/common/test" + "github.com/daos-stack/daos/src/control/logging" + "github.com/pkg/errors" +) + +func TestAPI_connHandle(t *testing.T) { + testUUID := test.MockPoolUUID(42) + + for name, tc := range map[string]struct { + ch *connHandle + expIsValid bool + expID string + expString string + }{ + "nil handle": { + expIsValid: false, + expID: "", + expString: ":false", + }, + "valid handle (label+uuid)": { + ch: &connHandle{ + daosHandle: *defaultPoolHdl(), + UUID: testUUID, + Label: testPoolName, + }, + expIsValid: true, + expID: testPoolName, + expString: testPoolName + ":true", + }, + "valid handle (uuid)": { + ch: &connHandle{ + daosHandle: *defaultPoolHdl(), + UUID: testUUID, + }, + expIsValid: true, + expID: testUUID.String(), + expString: logging.ShortUUID(testUUID) + ":true", + }, + "invalid handle (no uuid or label)": { + ch: &connHandle{ + daosHandle: *defaultPoolHdl(), + }, + expIsValid: false, + expID: "", + expString: ":false", + }, + "invalid handle (daos handle zeroed)": { + ch: &connHandle{ + daosHandle: _Ctype_daos_handle_t{}, + UUID: testUUID, + Label: testPoolName, + }, + expIsValid: false, + expID: "", + expString: ":false", + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + + test.CmpAny(t, "IsValid()", tc.expIsValid, tc.ch.IsValid()) + test.CmpAny(t, "ID()", tc.expID, tc.ch.ID()) + test.CmpAny(t, "String()", tc.expString, tc.ch.String()) + if tc.ch == nil || !tc.expIsValid { + return + } + + tc.ch.invalidate() + test.CmpAny(t, "IsValid()", false, tc.ch.IsValid()) + test.CmpAny(t, "ID()", "", tc.ch.ID()) + test.CmpAny(t, "String()", ":false", tc.ch.String()) + }) + } +} + +func TestAPI_connHandle_FillHandle(t *testing.T) { + for name, tc := range map[string]struct { + th *connHandle + ptr unsafe.Pointer + expErr error + }{ + "nil handle": { + expErr: errors.New("invalid handle"), + }, + "nil pointer": { + th: &defaultPoolHandle().connHandle, + expErr: errors.New("nil DAOS handle pointer"), + }, + "valid handle": { + th: &defaultPoolHandle().connHandle, + ptr: unsafe.Pointer(&_Ctype_daos_handle_t{}), + }, + } { + t.Run(name, func(t *testing.T) { + t.Cleanup(ResetTestStubs) + + gotErr := tc.th.FillHandle(tc.ptr) + test.CmpErr(t, tc.expErr, gotErr) + if tc.expErr != nil { + return + } + + cHdl := (*_Ctype_daos_handle_t)(tc.ptr) + test.CmpAny(t, "cookie", tc.th.daosHandle.cookie, cHdl.cookie) + }) + } +} diff --git a/src/control/lib/daos/api/libdaos.go b/src/control/lib/daos/api/libdaos.go index 426507b98ad..920563ae0e7 100644 --- a/src/control/lib/daos/api/libdaos.go +++ b/src/control/lib/daos/api/libdaos.go @@ -88,6 +88,58 @@ func daos_pool_del_attr(poolHdl C.daos_handle_t, n C.int, name **C.char, ev *C.s return C.daos_pool_del_attr(poolHdl, n, name, ev) } +func daos_pool_list_cont(poolHdl C.daos_handle_t, nCont *C.daos_size_t, conts *C.struct_daos_pool_cont_info, ev *C.struct_daos_event) C.int { + return C.daos_pool_list_cont(poolHdl, nCont, conts, ev) +} + func daos_mgmt_list_pools(sysName *C.char, poolCount *C.daos_size_t, pools *C.daos_mgmt_pool_info_t, ev *C.struct_daos_event) C.int { return C.daos_mgmt_list_pools(sysName, poolCount, pools, ev) } + +func daos_cont_open(poolHdl C.daos_handle_t, contID *C.char, flags C.uint, contHdl *C.daos_handle_t, contInfo *C.daos_cont_info_t, ev *C.struct_daos_event) C.int { + return C.daos_cont_open(poolHdl, contID, flags, contHdl, contInfo, ev) +} + +func daos_cont_destroy(poolHdl C.daos_handle_t, contID *C.char, force C.int, ev *C.struct_daos_event) C.int { + return C.daos_cont_destroy(poolHdl, contID, force, ev) +} + +func daos_cont_close(contHdl C.daos_handle_t) C.int { + // Hack for NLT fault injection testing: If the rc + // is -DER_NOMEM, retry once in order to actually + // shut down and release resources. + rc := C.daos_cont_close(contHdl, nil) + if rc == -C.DER_NOMEM { + rc = C.daos_cont_close(contHdl, nil) + } + + return rc +} + +func daos_cont_query(contHdl C.daos_handle_t, contInfo *C.daos_cont_info_t, props *C.daos_prop_t, ev *C.struct_daos_event) C.int { + return C.daos_cont_query(contHdl, contInfo, props, ev) +} + +func daos_cont_list_attr(contHdl C.daos_handle_t, buf *C.char, size *C.size_t, ev *C.struct_daos_event) C.int { + return C.daos_cont_list_attr(contHdl, buf, size, ev) +} + +func daos_cont_get_attr(contHdl C.daos_handle_t, n C.int, names **C.char, values *unsafe.Pointer, sizes *C.size_t, ev *C.struct_daos_event) C.int { + return C.daos_cont_get_attr(contHdl, n, names, values, sizes, ev) +} + +func daos_cont_set_attr(contHdl C.daos_handle_t, n C.int, names **C.char, values *unsafe.Pointer, sizes *C.size_t, ev *C.struct_daos_event) C.int { + return C.daos_cont_set_attr(contHdl, n, names, values, sizes, ev) +} + +func daos_cont_del_attr(contHdl C.daos_handle_t, n C.int, name **C.char, ev *C.struct_daos_event) C.int { + return C.daos_cont_del_attr(contHdl, n, name, ev) +} + +func daos_oclass_name2id(name *C.char) C.daos_oclass_id_t { + return C.daos_oclass_id_t(C.daos_oclass_name2id(name)) +} + +func daos_oclass_id2name(id C.daos_oclass_id_t, name *C.char) C.int { + return C.daos_oclass_id2name(id, name) +} diff --git a/src/control/lib/daos/api/libdaos_cont_stubs.go b/src/control/lib/daos/api/libdaos_cont_stubs.go new file mode 100644 index 00000000000..831c562fef6 --- /dev/null +++ b/src/control/lib/daos/api/libdaos_cont_stubs.go @@ -0,0 +1,291 @@ +// +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// +//go:build test_stubs +// +build test_stubs + +package api + +import ( + "unsafe" + + "github.com/daos-stack/daos/src/control/common/test" + "github.com/daos-stack/daos/src/control/lib/daos" +) + +/* +#include +#include + +#include "util.h" +*/ +import "C" + +func daos_gci2cci(gci *daos.ContainerInfo) *C.daos_cont_info_t { + return &C.daos_cont_info_t{ + ci_uuid: uuidToC(gci.ContainerUUID), + ci_lsnapshot: C.uint64_t(gci.LatestSnapshot), + ci_nhandles: C.uint32_t(gci.NumHandles), + ci_nsnapshots: C.uint32_t(gci.NumSnapshots), + ci_md_otime: C.uint64_t(gci.OpenTime), + ci_md_mtime: C.uint64_t(gci.CloseModifyTime), + } +} + +// defaultContainerInfo should be used to get a copy of the default container info. +func defaultContainerInfo() *daos.ContainerInfo { + return copyContainerInfo(&daos_default_ContainerInfo) +} + +func copyContainerInfo(in *daos.ContainerInfo) *daos.ContainerInfo { + if in == nil { + return nil + } + + out := new(daos.ContainerInfo) + *out = *in + + return out +} + +var ( + daos_default_cont_open_Handle C.daos_handle_t = C.daos_handle_t{cookie: 24} + + daos_default_ContainerInfo daos.ContainerInfo = daos.ContainerInfo{ + PoolUUID: daos_default_PoolInfo.UUID, + ContainerUUID: test.MockPoolUUID(2), + ContainerLabel: "test-container", + LatestSnapshot: 1, + NumHandles: 2, + NumSnapshots: 3, + OpenTime: 4, + CloseModifyTime: 5, + Type: daos.ContainerLayoutPOSIX, + Health: "HEALTHY", + POSIXAttributes: &daos.POSIXAttributes{ + ChunkSize: 1024, + ObjectClass: 1, + DirObjectClass: 2, + FileObjectClass: 3, + Hints: "what's a hint?", + }, + } +) + +// defaultContHandle should be used to get a copy of the default daos handle for a container. +func defaultContHdl() C.daos_handle_t { + newHdl := C.daos_handle_t{cookie: daos_default_cont_open_Handle.cookie} + return newHdl +} + +// defaultContainerHandle should be used to get a copy of the default container handle. +func defaultContainerHandle() *ContainerHandle { + return &ContainerHandle{ + connHandle: connHandle{ + UUID: daos_default_ContainerInfo.ContainerUUID, + Label: daos_default_ContainerInfo.ContainerLabel, + daosHandle: defaultContHdl(), + }, + PoolHandle: defaultPoolHandle(), + } +} + +// MockContainerHandle returns a valid ContainerHandle suitable for use in tests. +func MockContainerHandle() *ContainerHandle { + return defaultContainerHandle() +} + +func reset_daos_cont_stubs() { + reset_daos_cont_destroy() + reset_daos_cont_open() + reset_daos_cont_close() + reset_daos_cont_query() + reset_daos_cont_list_attr() + reset_daos_cont_get_attr() + reset_daos_cont_set_attr() + reset_daos_cont_del_attr() +} + +var ( + daos_cont_destroy_Count int + daos_cont_destroy_RC C.int = 0 +) + +func reset_daos_cont_destroy() { + daos_cont_destroy_Count = 0 + daos_cont_destroy_RC = 0 +} + +func daos_cont_destroy(poolHdl C.daos_handle_t, contID *C.char, force C.int, ev *C.struct_daos_event) C.int { + daos_cont_destroy_Count++ + return daos_cont_destroy_RC +} + +var ( + daos_cont_open_SetContainerID string + daos_cont_open_SetFlags daos.ContainerOpenFlag + daos_cont_open_Handle = defaultContHdl() + daos_cont_open_ContainerInfo = defaultContainerInfo() + daos_cont_open_Count int + daos_cont_open_RC C.int = 0 +) + +func reset_daos_cont_open() { + daos_cont_open_SetContainerID = "" + daos_cont_open_SetFlags = 0 + daos_cont_open_Handle = defaultContHdl() + daos_cont_open_ContainerInfo = defaultContainerInfo() + daos_cont_open_Count = 0 + daos_cont_open_RC = 0 +} + +func daos_cont_open(poolHdl C.daos_handle_t, contID *C.char, flags C.uint, contHdl *C.daos_handle_t, contInfo *C.daos_cont_info_t, ev *C.struct_daos_event) C.int { + daos_cont_open_Count++ + if daos_cont_open_RC != 0 { + return daos_cont_open_RC + } + + // capture the parameters set by the test + daos_cont_open_SetContainerID = C.GoString(contID) + daos_cont_open_SetFlags = daos.ContainerOpenFlag(flags) + + // set the return values + contHdl.cookie = daos_cont_open_Handle.cookie + if contInfo != nil { + *contInfo = *daos_gci2cci(daos_cont_open_ContainerInfo) + } + + return daos_cont_open_RC +} + +var ( + daos_cont_close_Count int + daos_cont_close_RC C.int = 0 +) + +func reset_daos_cont_close() { + daos_cont_close_Count = 0 + daos_cont_close_RC = 0 +} + +func daos_cont_close(contHdl C.daos_handle_t) C.int { + daos_cont_close_Count++ + if daos_cont_close_RC != 0 { + return daos_cont_close_RC + } + + return daos_cont_close_RC +} + +var ( + daos_cont_query_ContainerInfo = defaultContainerInfo() + daos_cont_query_RC C.int = 0 +) + +func reset_daos_cont_query() { + daos_cont_query_ContainerInfo = defaultContainerInfo() + daos_cont_query_RC = 0 +} + +func daos_cont_query(contHdl C.daos_handle_t, contInfo *C.daos_cont_info_t, props *C.daos_prop_t, ev *C.struct_daos_event) C.int { + if daos_cont_query_RC != 0 { + return daos_cont_query_RC + } + + if contInfo != nil { + *contInfo = *daos_gci2cci(daos_cont_query_ContainerInfo) + } + + if props != nil { + propSlice := unsafe.Slice(props.dpp_entries, props.dpp_nr) + for i := range propSlice { + switch propSlice[i].dpe_type { + case C.DAOS_PROP_CO_LABEL: + cLabel := C.CString(daos_cont_query_ContainerInfo.ContainerLabel) + C.set_dpe_str(&propSlice[i], cLabel) + case C.DAOS_PROP_CO_REDUN_FAC: + C.set_dpe_val(&propSlice[i], C.uint64_t(daos_cont_query_ContainerInfo.RedundancyFactor)) + case C.DAOS_PROP_CO_LAYOUT_TYPE: + C.set_dpe_val(&propSlice[i], C.uint64_t(daos_cont_query_ContainerInfo.Type)) + case C.DAOS_PROP_CO_STATUS: + if daos_cont_query_ContainerInfo.Health == "HEALTHY" { + C.set_dpe_val(&propSlice[i], C.daos_prop_co_status_val(C.DAOS_PROP_CO_HEALTHY, 0, 0)) + } else { + C.set_dpe_val(&propSlice[i], C.daos_prop_co_status_val(C.DAOS_PROP_CO_UNCLEAN, 0, 0)) + } + } + } + } + + return daos_cont_query_RC +} + +var ( + daos_cont_list_attr_AttrList daos.AttributeList = daos_default_AttrList + daos_cont_list_attr_CallCount int + daos_cont_list_attr_RCList []C.int + daos_cont_list_attr_RC C.int = 0 +) + +func reset_daos_cont_list_attr() { + daos_cont_list_attr_AttrList = daos_default_AttrList + daos_cont_list_attr_CallCount = 0 + daos_cont_list_attr_RCList = nil + daos_cont_list_attr_RC = 0 +} + +func daos_cont_list_attr(contHdl C.daos_handle_t, buf *C.char, size *C.size_t, ev *C.struct_daos_event) C.int { + return list_attrs(buf, size, daos_cont_list_attr_RCList, &daos_cont_list_attr_CallCount, daos_cont_list_attr_RC, daos_cont_list_attr_AttrList) +} + +var ( + daos_cont_get_attr_SetN int + daos_cont_get_attr_ReqNames map[string]struct{} + daos_cont_get_attr_CallCount int + daos_cont_get_attr_RCList []C.int + daos_cont_get_attr_AttrList daos.AttributeList = daos_default_AttrList + daos_cont_get_attr_RC C.int = 0 +) + +func reset_daos_cont_get_attr() { + daos_cont_get_attr_SetN = 0 + daos_cont_get_attr_ReqNames = nil + daos_cont_get_attr_CallCount = 0 + daos_cont_get_attr_RCList = nil + daos_cont_get_attr_AttrList = daos_default_AttrList + daos_cont_get_attr_RC = 0 +} + +func daos_cont_get_attr(contHdl C.daos_handle_t, n C.int, names **C.char, values *unsafe.Pointer, sizes *C.size_t, ev *C.struct_daos_event) C.int { + return get_attr(n, names, values, sizes, daos_cont_get_attr_RCList, &daos_cont_get_attr_CallCount, daos_cont_get_attr_RC, daos_cont_get_attr_AttrList, &daos_cont_get_attr_SetN, &daos_cont_get_attr_ReqNames) +} + +var ( + daos_cont_set_attr_AttrList daos.AttributeList + daos_cont_set_attr_RC C.int = 0 +) + +func reset_daos_cont_set_attr() { + daos_cont_set_attr_AttrList = nil + daos_cont_set_attr_RC = 0 +} + +func daos_cont_set_attr(contHdl C.daos_handle_t, n C.int, names **C.char, values *unsafe.Pointer, sizes *C.size_t, ev *C.struct_daos_event) C.int { + return set_attr(n, names, values, sizes, daos_cont_set_attr_RC, &daos_cont_set_attr_AttrList) +} + +var ( + daos_cont_del_attr_AttrNames []string + daos_cont_del_attr_RC C.int = 0 +) + +func reset_daos_cont_del_attr() { + daos_cont_del_attr_AttrNames = nil + daos_cont_del_attr_RC = 0 +} + +func daos_cont_del_attr(contHdl C.daos_handle_t, n C.int, name **C.char, ev *C.struct_daos_event) C.int { + return del_attr(n, name, daos_cont_del_attr_RC, &daos_cont_del_attr_AttrNames) +} diff --git a/src/control/lib/daos/api/libdaos_pool_stubs.go b/src/control/lib/daos/api/libdaos_pool_stubs.go index 308c4533a8e..ea262dc20b0 100644 --- a/src/control/lib/daos/api/libdaos_pool_stubs.go +++ b/src/control/lib/daos/api/libdaos_pool_stubs.go @@ -175,6 +175,14 @@ var ( }, } + daos_default_PoolHandle PoolHandle = PoolHandle{ + connHandle: connHandle{ + UUID: daos_default_PoolInfo.UUID, + Label: daos_default_PoolInfo.Label, + daosHandle: *defaultPoolHdl(), + }, + } + daos_default_PoolQueryTargetInfo daos.PoolQueryTargetInfo = daos.PoolQueryTargetInfo{ Type: daos.PoolQueryTargetType(1), State: daos.PoolTargetStateUp, @@ -192,11 +200,24 @@ var ( } ) +// defaultPoolHdl should be used to get a copy of the daos handle for the default pool. func defaultPoolHdl() *C.daos_handle_t { newHdl := C.daos_handle_t{cookie: daos_default_pool_connect_Handle.cookie} return &newHdl } +// defaultPoolHandle should be used to get a copy of the default pool handle. +func defaultPoolHandle() *PoolHandle { + newHandle := new(PoolHandle) + *newHandle = daos_default_PoolHandle + return newHandle +} + +// MockPoolHandle returns a valid PoolHandle suitable for use in tests. +func MockPoolHandle() *PoolHandle { + return defaultPoolHandle() +} + func reset_daos_pool_stubs() { reset_daos_pool_connect() reset_daos_pool_disconnect() @@ -219,6 +240,7 @@ var ( daos_pool_connect_Handle *C.daos_handle_t = defaultPoolHdl() daos_pool_connect_Info *daos.PoolInfo = defaultPoolInfo() daos_pool_connect_Count int = 0 + daos_pool_connect_RCList []C.int = nil daos_pool_connect_RC C.int = 0 ) @@ -230,11 +252,18 @@ func reset_daos_pool_connect() { daos_pool_connect_Handle = defaultPoolHdl() daos_pool_connect_Info = defaultPoolInfo() daos_pool_connect_Count = 0 + daos_pool_connect_RCList = nil daos_pool_connect_RC = 0 } func daos_pool_connect(poolID *C.char, sys *C.char, flags C.uint32_t, poolHdl *C.daos_handle_t, poolInfo *C.daos_pool_info_t, ev *C.struct_daos_event) C.int { daos_pool_connect_Count++ + if len(daos_pool_connect_RCList) > 0 { + rc := daos_pool_connect_RCList[daos_pool_connect_Count-1] + if rc != 0 { + return rc + } + } if daos_pool_connect_RC != 0 { return daos_pool_connect_RC } diff --git a/src/control/lib/daos/api/libdaos_stubs.go b/src/control/lib/daos/api/libdaos_stubs.go index 20ae8301b9b..ce60f5a39cb 100644 --- a/src/control/lib/daos/api/libdaos_stubs.go +++ b/src/control/lib/daos/api/libdaos_stubs.go @@ -19,6 +19,7 @@ import ( /* #include #include +#include #include "util.h" @@ -37,12 +38,9 @@ func daos_fini() {} func dc_agent_fini() {} -var ( - daos_handle_is_valid_Bool C.bool = true -) - func daos_handle_is_valid(handle C.daos_handle_t) C.bool { - return daos_handle_is_valid_Bool + // No real beneft to stubbing this out... + return C.daos_handle_is_valid(handle) } var ( @@ -126,3 +124,32 @@ func daos_mgmt_put_sys_info(sys_info *C.struct_daos_sys_info) { C.free(unsafe.Pointer(sys_info.dsi_ms_ranks)) } } + +var ( + daos_oclass_name2id_Default C.daos_oclass_id_t = C.OC_UNKNOWN + daos_oclass_name2id_Map = map[string]C.daos_oclass_id_t{} +) + +func daos_oclass_name2id(cName *C.char) C.daos_oclass_id_t { + name := C.GoString(cName) + if id, ok := daos_oclass_name2id_Map[name]; ok { + return id + } + + return daos_oclass_name2id_Default +} + +var ( + daos_oclass_id2name_Default = C.OC_UNKNOWN + daos_oclass_id2name_Map = map[C.daos_oclass_id_t]string{} +) + +func daos_oclass_id2name(id C.daos_oclass_id_t, cName *C.char) C.int { + if name, ok := daos_oclass_id2name_Map[id]; ok { + nameSlice := unsafe.Slice(cName, len(name)) + for i, c := range name { + nameSlice[i] = C.char(c) + } + } + return 0 +} diff --git a/src/control/lib/daos/api/libdfs.go b/src/control/lib/daos/api/libdfs.go new file mode 100644 index 00000000000..c048cdec657 --- /dev/null +++ b/src/control/lib/daos/api/libdfs.go @@ -0,0 +1,29 @@ +// +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// +//go:build !test_stubs +// +build !test_stubs + +package api + +/* +#include +#include + +#cgo LDFLAGS: -ldfs +*/ +import "C" + +func dfs_mount(poolHdl C.daos_handle_t, contHdl C.daos_handle_t, flags C.int, dfs **C.dfs_t) C.int { + return C.dfs_mount(poolHdl, contHdl, flags, dfs) +} + +func dfs_umount(dfs *C.dfs_t) C.int { + return C.dfs_umount(dfs) +} + +func dfs_query(dfs *C.dfs_t, attrs *C.dfs_attr_t) C.int { + return C.dfs_query(dfs, attrs) +} diff --git a/src/control/lib/daos/api/libdfs_stubs.go b/src/control/lib/daos/api/libdfs_stubs.go new file mode 100644 index 00000000000..7b8f5e21a55 --- /dev/null +++ b/src/control/lib/daos/api/libdfs_stubs.go @@ -0,0 +1,73 @@ +// +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// +//go:build test_stubs +// +build test_stubs + +package api + +/* +#include +#include +*/ +import "C" +import ( + "github.com/daos-stack/daos/src/control/lib/daos" +) + +func reset_dfs_stubs() { + reset_dfs_mount() + reset_dfs_umount() + reset_dfs_query() +} + +var ( + dfs_mount_RC C.int = 0 +) + +func reset_dfs_mount() { + dfs_mount_RC = 0 +} + +func dfs_mount(poolHdl C.daos_handle_t, contHdl C.daos_handle_t, flags C.int, dfs **C.dfs_t) C.int { + return dfs_mount_RC +} + +var ( + dfs_umount_RC C.int = 0 +) + +func reset_dfs_umount() { + dfs_umount_RC = 0 +} + +func dfs_umount(dfs *C.dfs_t) C.int { + return dfs_umount_RC +} + +var ( + dfs_query_Attrs daos.POSIXAttributes = *daos_default_ContainerInfo.POSIXAttributes + dfs_query_RC C.int = 0 +) + +func reset_dfs_query() { + dfs_query_Attrs = *daos_default_ContainerInfo.POSIXAttributes + dfs_query_RC = 0 +} + +func dfs_query(dfs *C.dfs_t, attrs *C.dfs_attr_t) C.int { + if dfs_query_RC != 0 { + return dfs_query_RC + } + + attrs.da_chunk_size = C.uint64_t(dfs_query_Attrs.ChunkSize) + attrs.da_dir_oclass_id = C.uint32_t(dfs_query_Attrs.DirObjectClass) + attrs.da_file_oclass_id = C.uint32_t(dfs_query_Attrs.FileObjectClass) + attrs.da_oclass_id = C.uint32_t(dfs_query_Attrs.ObjectClass) + attrs.da_mode = C.uint32_t(dfs_query_Attrs.ConsistencyMode) + C.strcpy(&attrs.da_hints[0], C.CString(dfs_query_Attrs.Hints)) + + return dfs_query_RC +} diff --git a/src/control/lib/daos/api/object.go b/src/control/lib/daos/api/object.go new file mode 100644 index 00000000000..03a9f6f6365 --- /dev/null +++ b/src/control/lib/daos/api/object.go @@ -0,0 +1,53 @@ +// +// (C) Copyright 2024-2025 Intel Corporation. +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package api + +import ( + "strings" + + "github.com/pkg/errors" + + "github.com/daos-stack/daos/src/control/lib/daos" +) + +/* +#include +#include +*/ +import "C" + +func init() { + // Any user of this package will automatically benefit from these + // helpers in the daos package. + daos.SetObjectClassHelpers(ObjectClassName, ObjectClassFromName) +} + +// ObjectClassFromName returns a new ObjectClass for the given name. +func ObjectClassFromName(name string) (daos.ObjectClass, error) { + upperName := strings.ToUpper(name) + cStr := C.CString(upperName) + defer freeString(cStr) + + id := daos_oclass_name2id(cStr) + if id == C.OC_UNKNOWN { + return daos.ObjectClass(C.OC_UNKNOWN), errors.Wrapf(daos.InvalidInput, "invalid object class %q", name) + } + + return daos.ObjectClass(id), nil +} + +// ObjectClassNameFromID returns a new ObjectClass for the given ID. +func ObjectClassName(class daos.ObjectClass) string { + var oclass [10]C.char + + if rc := daos_oclass_id2name(C.daos_oclass_id_t(class), &oclass[0]); rc != 0 { + return errors.Wrapf(daos.InvalidInput, "invalid object class %d", class).Error() + } + + return C.GoString(&oclass[0]) +} diff --git a/src/control/lib/daos/api/pool.go b/src/control/lib/daos/api/pool.go index 9f81dfd7547..2ee266e9ac1 100644 --- a/src/control/lib/daos/api/pool.go +++ b/src/control/lib/daos/api/pool.go @@ -58,10 +58,19 @@ func phFromCtx(ctx context.Context) (*PoolHandle, error) { if !ok { return nil, errNoCtxHdl } + ph.fromCtx = true return ph, nil } +// IsValid returns true if the pool handle is valid. +func (ph *PoolHandle) IsValid() bool { + if ph == nil { + return false + } + return ph.connHandle.IsValid() +} + // toCtx returns a new context with the PoolHandle stashed in it. // NB: Will panic if the context already has a different PoolHandle stashed. func (ph *PoolHandle) toCtx(ctx context.Context) context.Context { @@ -180,7 +189,7 @@ func poolInfoFromProps(pi *daos.PoolInfo, propEntries []C.struct_daos_prop_entry // connection and that it is safe to release resources allocated for // the connection. func (ph *PoolHandle) Disconnect(ctx context.Context) error { - if ph == nil { + if !ph.IsValid() { return ErrInvalidPoolHandle } logging.FromContext(ctx).Debugf("PoolHandle.Disconnect(%s)", ph) @@ -307,8 +316,7 @@ func getPoolConn(ctx context.Context, sysName, poolID string, flags daos.PoolCon } cleanup := func() { - err := resp.Connection.Disconnect(ctx) - if err != nil { + if err := resp.Connection.Disconnect(ctx); err != nil { logging.FromContext(ctx).Error(err.Error()) } } @@ -317,7 +325,7 @@ func getPoolConn(ctx context.Context, sysName, poolID string, flags daos.PoolCon // Query is a convenience wrapper around the PoolQuery() function. func (ph *PoolHandle) Query(ctx context.Context, mask daos.PoolQueryMask) (*daos.PoolInfo, error) { - if ph == nil { + if !ph.IsValid() { return nil, ErrInvalidPoolHandle } return PoolQuery(ph.toCtx(ctx), "", "", mask) @@ -423,7 +431,7 @@ func newPoolTargetInfo(ptinfo *C.daos_target_info_t) *daos.PoolQueryTargetInfo { // QueryTargets is a convenience wrapper around the PoolQueryTargets() function. func (ph *PoolHandle) QueryTargets(ctx context.Context, rank ranklist.Rank, targets *ranklist.RankSet) ([]*daos.PoolQueryTargetInfo, error) { - if ph == nil { + if !ph.IsValid() { return nil, ErrInvalidPoolHandle } return PoolQueryTargets(ph.toCtx(ctx), "", "", rank, targets) @@ -472,7 +480,7 @@ func PoolQueryTargets(ctx context.Context, sysName, poolID string, rank ranklist // ListAttributes is a convenience wrapper around the PoolListAttributes() function. func (ph *PoolHandle) ListAttributes(ctx context.Context) ([]string, error) { - if ph == nil { + if !ph.IsValid() { return nil, ErrInvalidPoolHandle } return PoolListAttributes(ph.toCtx(ctx), "", "") @@ -496,7 +504,7 @@ func PoolListAttributes(ctx context.Context, sysName, poolID string) ([]string, // GetAttributes is a convenience wrapper around the PoolGetAttributes() function. func (ph *PoolHandle) GetAttributes(ctx context.Context, attrNames ...string) (daos.AttributeList, error) { - if ph == nil { + if !ph.IsValid() { return nil, ErrInvalidPoolHandle } return PoolGetAttributes(ph.toCtx(ctx), "", "", attrNames...) @@ -521,7 +529,7 @@ func PoolGetAttributes(ctx context.Context, sysName, poolID string, names ...str // SetAttributes is a convenience wrapper around the PoolSetAttributes() function. func (ph *PoolHandle) SetAttributes(ctx context.Context, attrs ...*daos.Attribute) error { - if ph == nil { + if !ph.IsValid() { return ErrInvalidPoolHandle } return PoolSetAttributes(ph.toCtx(ctx), "", "", attrs...) @@ -545,7 +553,7 @@ func PoolSetAttributes(ctx context.Context, sysName, poolID string, attrs ...*da // DeleteAttributes is a convenience wrapper around the PoolDeleteAttributes() function. func (ph *PoolHandle) DeleteAttributes(ctx context.Context, attrNames ...string) error { - if ph == nil { + if !ph.IsValid() { return ErrInvalidPoolHandle } return PoolDeleteAttributes(ph.toCtx(ctx), "", "", attrNames...) @@ -567,6 +575,80 @@ func PoolDeleteAttributes(ctx context.Context, sysName, poolID string, attrNames return delDaosAttributes(poolConn.daosHandle, poolAttr, attrNames) } +func (ph *PoolHandle) ListContainers(ctx context.Context, query bool) ([]*daos.ContainerInfo, error) { + if !ph.IsValid() { + return nil, ErrInvalidPoolHandle + } + + return PoolListContainers(ph.toCtx(ctx), "", "", query) +} + +// PoolListContainers returns a list of information about containers in the pool. +func PoolListContainers(ctx context.Context, sysName, poolID string, query bool) ([]*daos.ContainerInfo, error) { + poolConn, disconnect, err := getPoolConn(ctx, sysName, poolID, daos.PoolConnectFlagReadOnly) + if err != nil { + return nil, err + } + defer disconnect() + logging.FromContext(ctx).Debugf("PoolListContainers(%s:%t)", poolConn, query) + + if err := ctx.Err(); err != nil { + return nil, ctxErr(err) + } + + extra_cont_margin := C.size_t(16) + + // First call gets the current number of containers. + var ncont C.daos_size_t + rc := daos_pool_list_cont(poolConn.daosHandle, &ncont, nil, nil) + if err := daosError(rc); err != nil { + return nil, errors.Wrap(err, "pool list containers failed") + } + + // No containers. + if ncont == 0 { + return nil, nil + } + + var cConts *C.struct_daos_pool_cont_info + // Extend ncont with a safety margin to account for containers + // that might have been created since the first API call. + ncont += extra_cont_margin + cConts = (*C.struct_daos_pool_cont_info)(C.calloc(C.sizeof_struct_daos_pool_cont_info, ncont)) + if cConts == nil { + return nil, errors.New("calloc() for containers failed") + } + dpciSlice := unsafe.Slice(cConts, ncont) + defer func() { + C.free(unsafe.Pointer(cConts)) + }() + + rc = daos_pool_list_cont(poolConn.daosHandle, &ncont, cConts, nil) + if err := daosError(rc); err != nil { + return nil, err + } + + out := make([]*daos.ContainerInfo, ncont) + for i := range out { + out[i] = new(daos.ContainerInfo) + out[i].ContainerUUID = uuid.Must(uuidFromC(dpciSlice[i].pci_uuid)) + out[i].ContainerLabel = C.GoString(&dpciSlice[i].pci_label[0]) + } + + if query { + for i := range out { + qc, err := poolConn.QueryContainer(ctx, out[i].ContainerUUID.String()) + if err != nil { + logging.FromContext(ctx).Errorf("failed to query container %s: %s", out[i].Name(), err) + continue + } + out[i] = qc + } + } + + return out, nil +} + type ( // GetPoolListReq defines the parameters for a GetPoolList request. GetPoolListReq struct { diff --git a/src/control/lib/daos/api/pool_test.go b/src/control/lib/daos/api/pool_test.go index 448d90b6da2..62b7c55c674 100644 --- a/src/control/lib/daos/api/pool_test.go +++ b/src/control/lib/daos/api/pool_test.go @@ -21,7 +21,6 @@ import ( "github.com/daos-stack/daos/src/control/common/test" "github.com/daos-stack/daos/src/control/lib/daos" "github.com/daos-stack/daos/src/control/lib/ranklist" - "github.com/daos-stack/daos/src/control/logging" ) var ( @@ -95,7 +94,7 @@ func TestAPI_PoolConnect(t *testing.T) { connHandle: connHandle{ Label: daos_default_PoolInfo.Label, UUID: daos_default_PoolInfo.UUID, - daosHandle: daos_default_pool_connect_Handle, + daosHandle: *defaultPoolHdl(), }, }, Info: defaultPoolInfo(), @@ -114,7 +113,7 @@ func TestAPI_PoolConnect(t *testing.T) { connHandle: connHandle{ Label: daos_default_PoolInfo.Label, UUID: daos_default_PoolInfo.UUID, - daosHandle: daos_default_pool_connect_Handle, + daosHandle: *defaultPoolHdl(), }, }, Info: defaultPoolInfo(), @@ -149,7 +148,7 @@ func TestAPI_PoolConnect(t *testing.T) { connHandle: connHandle{ Label: MissingPoolLabel, UUID: daos_default_PoolInfo.UUID, - daosHandle: daos_default_pool_connect_Handle, + daosHandle: *defaultPoolHdl(), }, }, Info: func() *daos.PoolInfo { @@ -165,14 +164,12 @@ func TestAPI_PoolConnect(t *testing.T) { if tc.setup != nil { tc.setup(t) } - log, buf := logging.NewTestLogger(name) - defer test.ShowBufferOnFailure(t, buf) if tc.checkParams != nil { defer tc.checkParams(t) } - gotResp, gotErr := PoolConnect(mustLogCtx(tc.ctx, log), tc.connReq) + gotResp, gotErr := PoolConnect(mustLogCtx(tc.ctx, t), tc.connReq) test.CmpErr(t, tc.expErr, gotErr) if tc.expErr != nil { return @@ -192,23 +189,6 @@ func TestAPI_PoolConnect(t *testing.T) { } } -var ( - testCtxPoolHandle = &PoolHandle{ - connHandle: connHandle{ - UUID: test.MockPoolUUID(43), - Label: "test-ctx-pool", - }, - } - - testConnPoolHandle = &PoolHandle{ - connHandle: connHandle{ - daosHandle: daos_default_pool_connect_Handle, - UUID: daos_default_PoolInfo.UUID, - Label: daos_default_PoolInfo.Label, - }, - } -) - func TestAPI_getPoolConn(t *testing.T) { for name, tc := range map[string]struct { setup func(t *testing.T) @@ -220,18 +200,26 @@ func TestAPI_getPoolConn(t *testing.T) { expErr error }{ "pool handle in context with non-empty ID": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), poolID: testPoolName, expErr: errors.New("PoolHandle found in context with non-empty poolID"), }, "pool handle in context": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), - expHdl: testCtxPoolHandle, + ctx: defaultPoolHandle().toCtx(test.Context(t)), + expHdl: defaultPoolHandle(), }, "pool handle not in context, no poolID": { ctx: test.Context(t), expErr: errors.Wrap(daos.InvalidInput, "no pool ID provided"), }, + "pool not in context; pool connect fails": { + ctx: test.Context(t), + setup: func(t *testing.T) { + daos_pool_connect_RC = -_Ctype_int(daos.IOError) + }, + poolID: testPoolName, + expErr: errors.Wrap(daos.IOError, "failed to connect to pool"), + }, "pool handle from Connect()": { ctx: test.Context(t), poolID: daos_default_PoolInfo.Label, @@ -241,7 +229,7 @@ func TestAPI_getPoolConn(t *testing.T) { test.CmpAny(t, "flags", daos.PoolConnectFlagReadOnly, daos_pool_connect_SetFlags) test.CmpAny(t, "query", daos.PoolQueryMask(0), daos_pool_connect_QueryMask) }, - expHdl: testConnPoolHandle, + expHdl: defaultPoolHandle(), }, } { t.Run(name, func(t *testing.T) { @@ -249,8 +237,6 @@ func TestAPI_getPoolConn(t *testing.T) { if tc.setup != nil { tc.setup(t) } - log, buf := logging.NewTestLogger(name) - defer test.ShowBufferOnFailure(t, buf) ctx := tc.ctx if ctx == nil { @@ -261,7 +247,7 @@ func TestAPI_getPoolConn(t *testing.T) { defer tc.checkParams(t) } - ph, cleanup, gotErr := getPoolConn(mustLogCtx(ctx, log), "", tc.poolID, tc.flags) + ph, cleanup, gotErr := getPoolConn(mustLogCtx(ctx, t), "", tc.poolID, tc.flags) test.CmpErr(t, tc.expErr, gotErr) if tc.expErr != nil { return @@ -291,15 +277,6 @@ func TestAPI_PoolQuery(t *testing.T) { "nil context": { expErr: errNilCtx, }, - "pool handle in context with non-empty ID": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), - poolID: testPoolName, - expErr: errors.New("PoolHandle found in context with non-empty poolID"), - }, - "pool handle not in context, no poolID": { - ctx: test.Context(t), - expErr: errors.Wrap(daos.InvalidInput, "no pool ID provided"), - }, "daos_pool_query() fails": { setup: func(t *testing.T) { daos_pool_query_RC = -_Ctype_int(daos.IOError) @@ -318,7 +295,7 @@ func TestAPI_PoolQuery(t *testing.T) { expErr: errors.Wrap(daos.IOError, "failed to query pool"), }, "unspecified query mask": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expResp: func() *daos.PoolInfo { out := defaultPoolInfo() out.QueryMask = daos.DefaultPoolQueryMask @@ -327,7 +304,7 @@ func TestAPI_PoolQuery(t *testing.T) { }(), }, "default query mask": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), queryMask: daos.DefaultPoolQueryMask, expResp: func() *daos.PoolInfo { out := defaultPoolInfo() @@ -337,7 +314,7 @@ func TestAPI_PoolQuery(t *testing.T) { }(), }, "health-only query mask": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), queryMask: daos.HealthOnlyPoolQueryMask, expResp: func() *daos.PoolInfo { out := defaultPoolInfo() @@ -348,7 +325,7 @@ func TestAPI_PoolQuery(t *testing.T) { }(), }, "enabled ranks": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), queryMask: daos.MustNewPoolQueryMask(daos.PoolQueryOptionEnabledEngines), expResp: func() *daos.PoolInfo { out := defaultPoolInfo() @@ -359,7 +336,7 @@ func TestAPI_PoolQuery(t *testing.T) { }(), }, "enabled & disabled ranks": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), queryMask: daos.MustNewPoolQueryMask(daos.PoolQueryOptionEnabledEngines, daos.PoolQueryOptionDisabledEngines), expResp: func() *daos.PoolInfo { out := defaultPoolInfo() @@ -369,7 +346,7 @@ func TestAPI_PoolQuery(t *testing.T) { }(), }, "space-only": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), queryMask: daos.MustNewPoolQueryMask(daos.PoolQueryOptionSpace), expResp: func() *daos.PoolInfo { out := defaultPoolInfo() @@ -385,14 +362,12 @@ func TestAPI_PoolQuery(t *testing.T) { if tc.setup != nil { tc.setup(t) } - log, buf := logging.NewTestLogger(name) - defer test.ShowBufferOnFailure(t, buf) if tc.checkParams != nil { defer tc.checkParams(t) } - gotResp, err := PoolQuery(mustLogCtx(tc.ctx, log), "", tc.poolID, tc.queryMask) + gotResp, err := PoolQuery(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.queryMask) test.CmpErr(t, tc.expErr, err) if tc.expErr != nil { return @@ -424,15 +399,6 @@ func TestAPI_PoolQueryTargets(t *testing.T) { "nil context": { expErr: errNilCtx, }, - "pool handle in context with non-empty ID": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), - poolID: testPoolName, - expErr: errors.New("PoolHandle found in context with non-empty poolID"), - }, - "pool handle not in context, no poolID": { - ctx: test.Context(t), - expErr: errors.Wrap(daos.InvalidInput, "no pool ID provided"), - }, "daos_pool_query() fails": { setup: func(t *testing.T) { daos_pool_query_RC = -_Ctype_int(daos.IOError) @@ -527,14 +493,12 @@ func TestAPI_PoolQueryTargets(t *testing.T) { if tc.setup != nil { tc.setup(t) } - log, buf := logging.NewTestLogger(name) - defer test.ShowBufferOnFailure(t, buf) if tc.checkParams != nil { defer tc.checkParams(t) } - gotResp, err := PoolQueryTargets(mustLogCtx(tc.ctx, log), "", tc.poolID, tc.rank, tc.targets) + gotResp, err := PoolQueryTargets(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.rank, tc.targets) test.CmpErr(t, tc.expErr, err) if tc.expErr != nil { return @@ -561,20 +525,11 @@ func TestAPI_PoolListAttributes(t *testing.T) { "nil context": { expErr: errNilCtx, }, - "pool handle in context with non-empty ID": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), - poolID: testPoolName, - expErr: errors.New("PoolHandle found in context with non-empty poolID"), - }, - "pool handle not in context, no poolID": { - ctx: test.Context(t), - expErr: errors.Wrap(daos.InvalidInput, "no pool ID provided"), - }, "daos_pool_list_attr() fails (get buf size)": { setup: func(t *testing.T) { daos_pool_list_attr_RC = -_Ctype_int(daos.IOError) }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expErr: errors.Wrap(daos.IOError, "failed to list pool attributes"), }, "daos_pool_list_attr() fails (fetch names)": { @@ -584,17 +539,17 @@ func TestAPI_PoolListAttributes(t *testing.T) { -_Ctype_int(daos.IOError), } }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expErr: errors.Wrap(daos.IOError, "failed to list pool attributes"), }, "no attributes set": { setup: func(t *testing.T) { daos_pool_list_attr_AttrList = nil }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), }, "success": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expNames: []string{ daos_default_AttrList[0].Name, daos_default_AttrList[1].Name, @@ -607,10 +562,8 @@ func TestAPI_PoolListAttributes(t *testing.T) { if tc.setup != nil { tc.setup(t) } - log, buf := logging.NewTestLogger(name) - defer test.ShowBufferOnFailure(t, buf) - gotNames, err := PoolListAttributes(mustLogCtx(tc.ctx, log), "", tc.poolID) + gotNames, err := PoolListAttributes(mustLogCtx(tc.ctx, t), "", tc.poolID) test.CmpErr(t, tc.expErr, err) if tc.expErr != nil { return @@ -634,27 +587,18 @@ func TestAPI_PoolGetAttributes(t *testing.T) { "nil context": { expErr: errNilCtx, }, - "pool handle in context with non-empty ID": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), - poolID: testPoolName, - expErr: errors.New("PoolHandle found in context with non-empty poolID"), - }, - "pool handle not in context, no poolID": { - ctx: test.Context(t), - expErr: errors.Wrap(daos.InvalidInput, "no pool ID provided"), - }, "daos_pool_list_attr() fails": { setup: func(t *testing.T) { daos_pool_list_attr_RC = -_Ctype_int(daos.IOError) }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expErr: errors.Wrap(daos.IOError, "failed to list pool attributes"), }, "daos_pool_get_attr() fails (sizes)": { setup: func(t *testing.T) { daos_pool_get_attr_RC = -_Ctype_int(daos.IOError) }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expErr: errors.Wrap(daos.IOError, "failed to get pool attribute sizes"), }, "daos_pool_get_attr() fails (values)": { @@ -664,11 +608,11 @@ func TestAPI_PoolGetAttributes(t *testing.T) { -_Ctype_int(daos.IOError), } }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expErr: errors.Wrap(daos.IOError, "failed to get pool attribute values"), }, "empty requested attribute name": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), attrNames: test.JoinArgs(nil, "a", ""), expErr: errors.Errorf("empty pool attribute name at index 1"), }, @@ -676,7 +620,7 @@ func TestAPI_PoolGetAttributes(t *testing.T) { setup: func(t *testing.T) { daos_pool_get_attr_AttrList = nil }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), attrNames: test.JoinArgs(nil, "foo"), checkParams: func(t *testing.T) { test.CmpAny(t, "req attr names", map[string]struct{}{"foo": {}}, daos_pool_get_attr_ReqNames) @@ -684,7 +628,7 @@ func TestAPI_PoolGetAttributes(t *testing.T) { expErr: errors.Wrap(daos.Nonexistent, "failed to get pool attribute sizes"), }, "unknown attribute requested": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), attrNames: test.JoinArgs(nil, "foo"), checkParams: func(t *testing.T) { test.CmpAny(t, "req attr names", map[string]struct{}{"foo": {}}, daos_pool_get_attr_ReqNames) @@ -695,14 +639,14 @@ func TestAPI_PoolGetAttributes(t *testing.T) { setup: func(t *testing.T) { daos_pool_list_attr_AttrList = nil }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), }, "success; all attributes": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expAttrs: daos_default_AttrList, }, "success; requested attributes": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), attrNames: test.JoinArgs(nil, daos_default_AttrList[0].Name, daos_default_AttrList[2].Name), checkParams: func(t *testing.T) { reqNames := test.JoinArgs(nil, daos_default_AttrList[0].Name, daos_default_AttrList[2].Name) @@ -722,14 +666,12 @@ func TestAPI_PoolGetAttributes(t *testing.T) { if tc.setup != nil { tc.setup(t) } - log, buf := logging.NewTestLogger(name) - defer test.ShowBufferOnFailure(t, buf) if tc.checkParams != nil { defer tc.checkParams(t) } - gotAttrs, err := PoolGetAttributes(mustLogCtx(tc.ctx, log), "", tc.poolID, tc.attrNames...) + gotAttrs, err := PoolGetAttributes(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.attrNames...) test.CmpErr(t, tc.expErr, err) if tc.expErr != nil { return @@ -751,31 +693,22 @@ func TestAPI_PoolSetAttributes(t *testing.T) { "nil context": { expErr: errNilCtx, }, - "pool handle in context with non-empty ID": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), - poolID: testPoolName, - expErr: errors.New("PoolHandle found in context with non-empty poolID"), - }, - "pool handle not in context, no poolID": { - ctx: test.Context(t), - expErr: errors.Wrap(daos.InvalidInput, "no pool ID provided"), - }, "no attributes to set": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expErr: errors.Wrap(daos.InvalidInput, "no pool attributes provided"), }, "nil toSet attribute": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), toSet: append(daos_default_AttrList, nil), expErr: errors.Wrap(daos.InvalidInput, "nil pool attribute at index 3"), }, "toSet attribute with empty name": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), toSet: append(daos_default_AttrList, &daos.Attribute{Name: ""}), expErr: errors.Wrap(daos.InvalidInput, "empty pool attribute name at index 3"), }, "toSet attribute with empty value": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), toSet: append(daos_default_AttrList, &daos.Attribute{Name: "empty"}), expErr: errors.Wrap(daos.InvalidInput, "empty pool attribute value at index 3"), }, @@ -783,12 +716,12 @@ func TestAPI_PoolSetAttributes(t *testing.T) { setup: func(t *testing.T) { daos_pool_set_attr_RC = -_Ctype_int(daos.IOError) }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), toSet: daos_default_AttrList, expErr: errors.Wrap(daos.IOError, "failed to set pool attributes"), }, "success": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), toSet: daos_default_AttrList, }, } { @@ -797,10 +730,8 @@ func TestAPI_PoolSetAttributes(t *testing.T) { if tc.setup != nil { tc.setup(t) } - log, buf := logging.NewTestLogger(name) - defer test.ShowBufferOnFailure(t, buf) - err := PoolSetAttributes(mustLogCtx(tc.ctx, log), "", tc.poolID, tc.toSet...) + err := PoolSetAttributes(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.toSet...) test.CmpErr(t, tc.expErr, err) if tc.expErr != nil { return @@ -822,21 +753,12 @@ func TestAPI_PoolDeleteAttributes(t *testing.T) { "nil context": { expErr: errNilCtx, }, - "pool handle in context with non-empty ID": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), - poolID: testPoolName, - expErr: errors.New("PoolHandle found in context with non-empty poolID"), - }, - "pool handle not in context, no poolID": { - ctx: test.Context(t), - expErr: errors.Wrap(daos.InvalidInput, "no pool ID provided"), - }, "no attributes to delete": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), expErr: errors.Wrap(daos.InvalidInput, "no pool attribute names provided"), }, "empty name in toDelete list": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), toDelete: test.JoinArgs(nil, "foo", "", "bar"), expErr: errors.Wrap(daos.InvalidInput, "empty pool attribute name at index 1"), }, @@ -844,12 +766,12 @@ func TestAPI_PoolDeleteAttributes(t *testing.T) { setup: func(t *testing.T) { daos_pool_del_attr_RC = -_Ctype_int(daos.IOError) }, - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), toDelete: test.JoinArgs(nil, daos_default_AttrList[0].Name), expErr: errors.Wrap(daos.IOError, "failed to delete pool attributes"), }, "success": { - ctx: testCtxPoolHandle.toCtx(test.Context(t)), + ctx: defaultPoolHandle().toCtx(test.Context(t)), toDelete: test.JoinArgs(nil, daos_default_AttrList[0].Name), }, } { @@ -858,10 +780,8 @@ func TestAPI_PoolDeleteAttributes(t *testing.T) { if tc.setup != nil { tc.setup(t) } - log, buf := logging.NewTestLogger(name) - defer test.ShowBufferOnFailure(t, buf) - err := PoolDeleteAttributes(mustLogCtx(tc.ctx, log), "", tc.poolID, tc.toDelete...) + err := PoolDeleteAttributes(mustLogCtx(tc.ctx, t), "", tc.poolID, tc.toDelete...) test.CmpErr(t, tc.expErr, err) if tc.expErr != nil { return @@ -873,9 +793,7 @@ func TestAPI_PoolDeleteAttributes(t *testing.T) { } func TestAPI_PoolHandleMethods(t *testing.T) { - testHandle := &PoolHandle{} - - thType := reflect.TypeOf(testHandle) + thType := reflect.TypeOf(defaultPoolHandle()) for i := 0; i < thType.NumMethod(); i++ { method := thType.Method(i) methArgs := make([]reflect.Value, 0) @@ -901,6 +819,18 @@ func TestAPI_PoolHandleMethods(t *testing.T) { case "DeleteAttributes": methArgs = append(methArgs, reflect.ValueOf(daos_default_AttrList[0].Name)) expResults = 1 + case "ListContainers": + methArgs = append(methArgs, reflect.ValueOf(true)) + expResults = 2 + case "DestroyContainer": + methArgs = append(methArgs, reflect.ValueOf("foo"), reflect.ValueOf(true)) + expResults = 1 + case "QueryContainer": + methArgs = append(methArgs, reflect.ValueOf("foo")) + expResults = 2 + case "OpenContainer": + methArgs = append(methArgs, reflect.ValueOf(ContainerOpenReq{ID: "foo"})) + expResults = 2 case "FillHandle", "IsValid", "String", "UUID", "ID": // No tests for these. The main point of this suite is to ensure that the // convenience wrappers handle inputs as expected. @@ -922,7 +852,7 @@ func TestAPI_PoolHandleMethods(t *testing.T) { expErr: ErrInvalidPoolHandle, }, fmt.Sprintf("%s: success", method.Name): { - th: testHandle, + th: defaultPoolHandle(), }, } { t.Run(name, func(t *testing.T) { @@ -1057,14 +987,12 @@ func TestAPI_GetPoolList(t *testing.T) { if tc.setup != nil { tc.setup(t) } - log, buf := logging.NewTestLogger(name) - defer test.ShowBufferOnFailure(t, buf) if tc.checkParams != nil { defer tc.checkParams(t) } - gotPools, err := GetPoolList(mustLogCtx(tc.ctx, log), tc.req) + gotPools, err := GetPoolList(mustLogCtx(tc.ctx, t), tc.req) test.CmpErr(t, tc.expErr, err) if tc.expErr != nil { return diff --git a/src/control/lib/daos/api/test_stubs.go b/src/control/lib/daos/api/test_stubs.go index 26ddd02da0a..2b50207f9b1 100644 --- a/src/control/lib/daos/api/test_stubs.go +++ b/src/control/lib/daos/api/test_stubs.go @@ -31,4 +31,6 @@ func UnlockTestStubs() { // to reset state between tests. func ResetTestStubs() { reset_daos_pool_stubs() + reset_daos_cont_stubs() + reset_dfs_stubs() } diff --git a/src/control/lib/daos/api/util.go b/src/control/lib/daos/api/util.go index 9c461cd5c16..59acadf9bae 100644 --- a/src/control/lib/daos/api/util.go +++ b/src/control/lib/daos/api/util.go @@ -1,12 +1,21 @@ +// +// (C) Copyright 2024 Intel Corporation. +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + package api import ( "context" + "testing" "unsafe" "github.com/google/uuid" "github.com/pkg/errors" + "github.com/daos-stack/daos/src/control/common/test" "github.com/daos-stack/daos/src/control/lib/daos" "github.com/daos-stack/daos/src/control/lib/ranklist" "github.com/daos-stack/daos/src/control/logging" @@ -105,10 +114,16 @@ func ranklistFromGo(rs *ranklist.RankSet) *C.d_rank_list_t { return rl } -func mustLogCtx(parent context.Context, log logging.Logger) context.Context { +func mustLogCtx(parent context.Context, t *testing.T) context.Context { if parent == nil { return nil } + + log, buf := logging.NewTestLogger(t.Name()) + t.Cleanup(func() { + test.ShowBufferOnFailure(t, buf) + }) + ctx, err := logging.ToContext(parent, log) if err != nil { panic(err) diff --git a/src/control/lib/daos/cont_prop.go b/src/control/lib/daos/cont_prop.go new file mode 100644 index 00000000000..9515281713a --- /dev/null +++ b/src/control/lib/daos/cont_prop.go @@ -0,0 +1,290 @@ +// +// (C) Copyright 2019-2025 Intel Corporation. +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package daos + +import ( + "fmt" + "strings" + "unsafe" + + "github.com/pkg/errors" +) + +/* +#cgo LDFLAGS: -ldaos_common -lgurt -lcart +#include +#include +*/ +import "C" + +const ( + // PropEntryAllocedOID is the highest allocated OID. + PropEntryAllocedOID = C.DAOS_PROP_ENTRY_ALLOCED_OID + // PropEntryChecksum is the checksum property. + PropEntryChecksum = C.DAOS_PROP_ENTRY_CKSUM + // PropEntryChecksumSize is the checksum size property. + PropEntryChecksumSize = C.DAOS_PROP_ENTRY_CKSUM_SIZE + // PropEntryCompression is the compression property. + PropEntryCompression = C.DAOS_PROP_ENTRY_COMPRESS + // PropEntryDedupe is the dedupe property. + PropEntryDedupe = C.DAOS_PROP_ENTRY_DEDUP + // PropEntryDedupThreshold is the dedupe threshold property. + PropEntryDedupeThreshold = C.DAOS_PROP_ENTRY_DEDUP_THRESHOLD + // PropEntryECCellSize is the EC cell size property. + PropEntryECCellSize = C.DAOS_PROP_ENTRY_EC_CELL_SZ + // PropEntryECPerfDomainAff is the EC performance domain affinity property. + PropEntryECPerfDomainAff = C.DAOS_PROP_ENTRY_EC_PDA + // PropEntryEncryption is the encryption property. + PropEntryEncryption = C.DAOS_PROP_ENTRY_ENCRYPT + // PropEntryGlobalVersion is the global version property. + PropEntryGlobalVersion = C.DAOS_PROP_ENTRY_GLOBAL_VERSION + // PropEntryObjectVersion is the object layout version property. + PropEntryObjectVersion = C.DAOS_PROP_ENTRY_OBJ_VERSION + // PropEntryGroup is the group property. + PropEntryGroup = C.DAOS_PROP_ENTRY_GROUP + // PropEntryLabel is the label property. + PropEntryLabel = C.DAOS_PROP_ENTRY_LABEL + // PropEntryLayout is the layout property. + PropEntryLayoutType = C.DAOS_PROP_ENTRY_LAYOUT_TYPE + // PropEntryLayoutVersion is the layout version property. + PropEntryLayoutVersion = C.DAOS_PROP_ENTRY_LAYOUT_VER + // PropEntryOwner is the owner property. + PropEntryOwner = C.DAOS_PROP_ENTRY_OWNER + // PropEntryRedunFactor is the redundancy factor property. + PropEntryRedunFactor = C.DAOS_PROP_ENTRY_REDUN_FAC + // PropEntryRedunLevel is the redundancy level property. + PropEntryRedunLevel = C.DAOS_PROP_ENTRY_REDUN_LVL + // PropEntryRedunPerfDomainAff is the redundancy performance domain affinity property. + PropEntryRedunPerfDomainAff = C.DAOS_PROP_ENTRY_RP_PDA + // PropEntrySnapshotMax is the snapshot max property. + PropEntrySnapshotMax = C.DAOS_PROP_ENTRY_SNAPSHOT_MAX + // PropEntryServerChecksum is the server checksum property. + PropEntryServerChecksum = C.DAOS_PROP_ENTRY_SRV_CKSUM + // PropEntryStatus is the status property. + PropEntryStatus = C.DAOS_PROP_ENTRY_STATUS +) + +type ContainerPropType C.uint + +const ( + containerPropMin ContainerPropType = C.DAOS_PROP_CO_MIN + ContainerPropLabel ContainerPropType = C.DAOS_PROP_CO_LABEL + ContainerPropLayout ContainerPropType = C.DAOS_PROP_CO_LAYOUT_TYPE + ContainerPropLayoutVersion ContainerPropType = C.DAOS_PROP_CO_LAYOUT_VER + ContainerPropChecksumEnabled ContainerPropType = C.DAOS_PROP_CO_CSUM + ContainerPropChecksumSize ContainerPropType = C.DAOS_PROP_CO_CSUM_CHUNK_SIZE + ContainerPropChecksumSrvVrfy ContainerPropType = C.DAOS_PROP_CO_CSUM_SERVER_VERIFY + ContainerPropRedunFactor ContainerPropType = C.DAOS_PROP_CO_REDUN_FAC + ContainerPropRedunLevel ContainerPropType = C.DAOS_PROP_CO_REDUN_LVL + ContainerPropMaxSnapshots ContainerPropType = C.DAOS_PROP_CO_SNAPSHOT_MAX + ContainerPropACL ContainerPropType = C.DAOS_PROP_CO_ACL + ContainerPropCompression ContainerPropType = C.DAOS_PROP_CO_COMPRESS + ContainerPropEncrypted ContainerPropType = C.DAOS_PROP_CO_ENCRYPT + ContainerPropOwner ContainerPropType = C.DAOS_PROP_CO_OWNER + ContainerPropGroup ContainerPropType = C.DAOS_PROP_CO_OWNER_GROUP + ContainerPropDedupEnabled ContainerPropType = C.DAOS_PROP_CO_DEDUP + ContainerPropDedupThreshold ContainerPropType = C.DAOS_PROP_CO_DEDUP_THRESHOLD + ContainerPropRootObjects ContainerPropType = C.DAOS_PROP_CO_ROOTS + ContainerPropStatus ContainerPropType = C.DAOS_PROP_CO_STATUS + ContainerPropHighestOid ContainerPropType = C.DAOS_PROP_CO_ALLOCED_OID + ContainerPropEcCellSize ContainerPropType = C.DAOS_PROP_CO_EC_CELL_SZ + ContainerPropEcPerfDom ContainerPropType = C.DAOS_PROP_CO_EC_PDA + ContainerPropEcPerfDomAff ContainerPropType = C.DAOS_PROP_CO_RP_PDA + ContainerPropGlobalVersion ContainerPropType = C.DAOS_PROP_CO_GLOBAL_VERSION + ContainerPropScubberDisabled ContainerPropType = C.DAOS_PROP_CO_SCRUBBER_DISABLED + ContainerPropObjectVersion ContainerPropType = C.DAOS_PROP_CO_OBJ_VERSION + ContainerPropPerfDomain ContainerPropType = C.DAOS_PROP_CO_PERF_DOMAIN + containerPropMax ContainerPropType = C.DAOS_PROP_CO_MAX +) + +func (cpt ContainerPropType) String() string { + switch cpt { + case ContainerPropLabel: + return C.DAOS_PROP_ENTRY_LABEL + case ContainerPropLayout: + return C.DAOS_PROP_ENTRY_LAYOUT_TYPE + case ContainerPropLayoutVersion: + return C.DAOS_PROP_ENTRY_LAYOUT_VER + case ContainerPropChecksumEnabled: + return C.DAOS_PROP_ENTRY_CKSUM + case ContainerPropChecksumSize: + return C.DAOS_PROP_ENTRY_CKSUM_SIZE + case ContainerPropChecksumSrvVrfy: + return C.DAOS_PROP_ENTRY_SRV_CKSUM + case ContainerPropRedunFactor: + return C.DAOS_PROP_ENTRY_REDUN_FAC + case ContainerPropRedunLevel: + return C.DAOS_PROP_ENTRY_REDUN_LVL + case ContainerPropMaxSnapshots: + return C.DAOS_PROP_ENTRY_SNAPSHOT_MAX + case ContainerPropACL: + return C.DAOS_PROP_ENTRY_ACL + case ContainerPropCompression: + return C.DAOS_PROP_ENTRY_COMPRESS + case ContainerPropEncrypted: + return C.DAOS_PROP_ENTRY_ENCRYPT + case ContainerPropOwner: + return C.DAOS_PROP_ENTRY_OWNER + case ContainerPropGroup: + return C.DAOS_PROP_ENTRY_GROUP + case ContainerPropDedupEnabled: + return C.DAOS_PROP_ENTRY_DEDUP + case ContainerPropDedupThreshold: + return C.DAOS_PROP_ENTRY_DEDUP_THRESHOLD + case ContainerPropRootObjects: + return C.DAOS_PROP_ENTRY_ROOT_OIDS + case ContainerPropStatus: + return C.DAOS_PROP_ENTRY_STATUS + case ContainerPropHighestOid: + return C.DAOS_PROP_ENTRY_ALLOCED_OID + case ContainerPropEcCellSize: + return C.DAOS_PROP_ENTRY_EC_CELL_SZ + case ContainerPropEcPerfDom: + return C.DAOS_PROP_ENTRY_EC_PDA + case ContainerPropEcPerfDomAff: + return C.DAOS_PROP_ENTRY_RP_PDA + case ContainerPropGlobalVersion: + return C.DAOS_PROP_ENTRY_GLOBAL_VERSION + case ContainerPropScubberDisabled: + return C.DAOS_PROP_ENTRY_SCRUB_DISABLED + case ContainerPropObjectVersion: + return C.DAOS_PROP_ENTRY_OBJ_VERSION + case ContainerPropPerfDomain: + return C.DAOS_PROP_ENTRY_PERF_DOMAIN + default: + return fmt.Sprintf("unknown container property type %d", cpt) + } +} + +func (cpt *ContainerPropType) FromString(in string) error { + switch strings.TrimSpace(strings.ToLower(in)) { + case C.DAOS_PROP_ENTRY_LABEL: + *cpt = ContainerPropLabel + case C.DAOS_PROP_ENTRY_LAYOUT_TYPE: + *cpt = ContainerPropLayout + case C.DAOS_PROP_ENTRY_LAYOUT_VER: + *cpt = ContainerPropLayoutVersion + case C.DAOS_PROP_ENTRY_CKSUM: + *cpt = ContainerPropChecksumEnabled + case C.DAOS_PROP_ENTRY_CKSUM_SIZE: + *cpt = ContainerPropChecksumSize + case C.DAOS_PROP_ENTRY_SRV_CKSUM: + *cpt = ContainerPropChecksumSrvVrfy + case C.DAOS_PROP_ENTRY_REDUN_FAC: + *cpt = ContainerPropRedunFactor + case C.DAOS_PROP_ENTRY_REDUN_LVL: + *cpt = ContainerPropRedunLevel + case C.DAOS_PROP_ENTRY_SNAPSHOT_MAX: + *cpt = ContainerPropMaxSnapshots + case C.DAOS_PROP_ENTRY_ACL: + *cpt = ContainerPropACL + case C.DAOS_PROP_ENTRY_COMPRESS: + *cpt = ContainerPropCompression + case C.DAOS_PROP_ENTRY_ENCRYPT: + *cpt = ContainerPropEncrypted + case C.DAOS_PROP_ENTRY_OWNER: + *cpt = ContainerPropOwner + case C.DAOS_PROP_ENTRY_GROUP: + *cpt = ContainerPropGroup + case C.DAOS_PROP_ENTRY_DEDUP: + *cpt = ContainerPropDedupEnabled + case C.DAOS_PROP_ENTRY_DEDUP_THRESHOLD: + *cpt = ContainerPropDedupThreshold + case C.DAOS_PROP_ENTRY_ROOT_OIDS: + *cpt = ContainerPropRootObjects + case C.DAOS_PROP_ENTRY_STATUS: + *cpt = ContainerPropStatus + case C.DAOS_PROP_ENTRY_ALLOCED_OID: + *cpt = ContainerPropHighestOid + case C.DAOS_PROP_ENTRY_EC_CELL_SZ: + *cpt = ContainerPropEcCellSize + case C.DAOS_PROP_ENTRY_EC_PDA: + *cpt = ContainerPropEcPerfDom + case C.DAOS_PROP_ENTRY_RP_PDA: + *cpt = ContainerPropEcPerfDomAff + case C.DAOS_PROP_ENTRY_GLOBAL_VERSION: + *cpt = ContainerPropGlobalVersion + case C.DAOS_PROP_ENTRY_SCRUB_DISABLED: + *cpt = ContainerPropScubberDisabled + case C.DAOS_PROP_ENTRY_OBJ_VERSION: + *cpt = ContainerPropObjectVersion + case C.DAOS_PROP_ENTRY_PERF_DOMAIN: + *cpt = ContainerPropPerfDomain + default: + return fmt.Errorf("unknown container property type %q", in) + } + + return nil +} + +func NewContainerPropertyList(count uint) (*ContainerPropertyList, error) { + pl, err := newPropertyList(count) + if err != nil { + return nil, err + } + + return &ContainerPropertyList{ + propertyList: *pl, + }, nil +} + +func ContainerPropertyListFromPtr(ptr unsafe.Pointer) (*ContainerPropertyList, error) { + pl, err := propertyListFromPtr(ptr) + if err != nil { + return nil, err + } + + return &ContainerPropertyList{ + propertyList: *pl, + }, nil +} + +func (cpl *ContainerPropertyList) MustAddEntryType(propType ContainerPropType) { + if err := cpl.AddEntryType(propType); err != nil { + panic(err) + } +} + +func (cpl *ContainerPropertyList) AddEntryType(propType ContainerPropType) error { + if propType < containerPropMin || propType > containerPropMax { + return errors.Wrapf(InvalidInput, "invalid container property type %d", propType) + } + + if int(cpl.cProps.dpp_nr) == len(cpl.entries) { + return errors.Errorf("property list is full (%d/%d entries)", len(cpl.entries), cap(cpl.entries)) + } + + cpl.entries[cpl.cProps.dpp_nr].dpe_type = C.uint(propType) + cpl.cProps.dpp_nr++ + + return nil +} + +func (cpl *ContainerPropertyList) Properties() (props []*ContainerProperty) { + for i := range cpl.entries { + props = append(props, &ContainerProperty{ + property: property{ + idx: C.int(i), + entry: &cpl.entries[i], + }, + }) + } + return +} + +func (cp *ContainerProperty) Type() ContainerPropType { + return ContainerPropType(cp.entry.dpe_type) +} + +func (cp *ContainerProperty) Name() string { + return cp.Type().String() +} + +func (cp *ContainerProperty) String() string { + return fmt.Sprintf("%s: %s", cp.Name(), cp.property.String()) +} diff --git a/src/control/lib/daos/container.go b/src/control/lib/daos/container.go index 31c2992f81b..7671baace91 100644 --- a/src/control/lib/daos/container.go +++ b/src/control/lib/daos/container.go @@ -1,57 +1,127 @@ // // (C) Copyright 2024 Intel Corporation. +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // package daos -import "github.com/google/uuid" +import ( + "encoding/json" + + "github.com/google/uuid" + "github.com/pkg/errors" +) /* #include #include +#include + +#cgo LDFLAGS: -ldaos_common */ import "C" +type ( + // ContainerLayout represents the layout of a container. + ContainerLayout uint16 + // ContainerQueryOption is used to supply container open options. + ContainerOpenFlag uint +) + const ( // ContainerOpenFlagReadOnly opens the container in read-only mode. - ContainerOpenFlagReadOnly = C.DAOS_COO_RO + ContainerOpenFlagReadOnly ContainerOpenFlag = C.DAOS_COO_RO // ContainerOpenFlagReadWrite opens the container in read-write mode. - ContainerOpenFlagReadWrite = C.DAOS_COO_RW + ContainerOpenFlagReadWrite ContainerOpenFlag = C.DAOS_COO_RW // ContainerOpenFlagExclusive opens the container in exclusive read-write mode. - ContainerOpenFlagExclusive = C.DAOS_COO_EX + ContainerOpenFlagExclusive ContainerOpenFlag = C.DAOS_COO_EX // ContainerOpenFlagForce skips container health checks. - ContainerOpenFlagForce = C.DAOS_COO_FORCE + ContainerOpenFlagForce ContainerOpenFlag = C.DAOS_COO_FORCE // ContainerOpenFlagReadOnlyMetadata skips container metadata updates. - ContainerOpenFlagReadOnlyMetadata = C.DAOS_COO_RO_MDSTATS + ContainerOpenFlagReadOnlyMetadata ContainerOpenFlag = C.DAOS_COO_RO_MDSTATS // ContainerOpenFlagEvict evicts the current user's open handles. - ContainerOpenFlagEvict = C.DAOS_COO_EVICT + ContainerOpenFlagEvict ContainerOpenFlag = C.DAOS_COO_EVICT // ContainerOpenFlagEvictAll evicts all open handles. - ContainerOpenFlagEvictAll = C.DAOS_COO_EVICT_ALL + ContainerOpenFlagEvictAll ContainerOpenFlag = C.DAOS_COO_EVICT_ALL + + // ContainerLayoutUnknown represents an unknown container layout. + ContainerLayoutUnknown ContainerLayout = C.DAOS_PROP_CO_LAYOUT_UNKNOWN + // ContainerLayoutPOSIX represents a POSIX container layout. + ContainerLayoutPOSIX ContainerLayout = C.DAOS_PROP_CO_LAYOUT_POSIX + // ContainerLayoutHDF5 represents an HDF5 container layout. + ContainerLayoutHDF5 ContainerLayout = C.DAOS_PROP_CO_LAYOUT_HDF5 + // ContainerLayoutPython represents a Python container layout. + ContainerLayoutPython ContainerLayout = C.DAOS_PROP_CO_LAYOUT_PYTHON + // ContainerLayoutSpark represents a Spark container layout. + ContainerLayoutSpark ContainerLayout = C.DAOS_PROP_CO_LAYOUT_SPARK + // ContainerLayoutDatabase represents a database container layout. + ContainerLayoutDatabase ContainerLayout = C.DAOS_PROP_CO_LAYOUT_DATABASE + // ContainerLayoutRoot represents a root container layout. + ContainerLayoutRoot ContainerLayout = C.DAOS_PROP_CO_LAYOUT_ROOT + // ContainerLayoutSeismic represents a seismic container layout. + ContainerLayoutSeismic ContainerLayout = C.DAOS_PROP_CO_LAYOUT_SEISMIC + // ContainerLayoutMeteo represents a meteo container layout. + ContainerLayoutMeteo ContainerLayout = C.DAOS_PROP_CO_LAYOUT_METEO ) -// ContainerInfo contains information about the Container. -type ContainerInfo struct { - PoolUUID uuid.UUID `json:"pool_uuid"` - ContainerUUID uuid.UUID `json:"container_uuid"` - ContainerLabel string `json:"container_label,omitempty"` - LatestSnapshot uint64 `json:"latest_snapshot"` - RedundancyFactor uint32 `json:"redundancy_factor"` - NumHandles uint32 `json:"num_handles"` - NumSnapshots uint32 `json:"num_snapshots"` - OpenTime uint64 `json:"open_time"` - CloseModifyTime uint64 `json:"close_modify_time"` - Type string `json:"container_type"` - ObjectClass string `json:"object_class,omitempty"` - DirObjectClass string `json:"dir_object_class,omitempty"` - FileObjectClass string `json:"file_object_class,omitempty"` - CHints string `json:"hints,omitempty"` - ChunkSize uint64 `json:"chunk_size,omitempty"` - Health string `json:"health,omitempty"` // FIXME (DAOS-10028): Should be derived from props +// FromString converts a string to a ContainerLayout. +func (l *ContainerLayout) FromString(in string) error { + cStr := C.CString(in) + defer freeString(cStr) + C.daos_parse_ctype(cStr, (*C.uint16_t)(l)) + + if *l == ContainerLayoutUnknown { + return errors.Errorf("unknown container layout %q", in) + } + + return nil +} + +func (l ContainerLayout) String() string { + var cType [10]C.char + C.daos_unparse_ctype(C.ushort(l), &cType[0]) + return C.GoString(&cType[0]) +} + +func (l ContainerLayout) MarshalJSON() ([]byte, error) { + return []byte(`"` + l.String() + `"`), nil } +func (l *ContainerLayout) UnmarshalJSON(data []byte) error { + return l.FromString(string(data[1 : len(data)-1])) +} + +type ( + // POSIXAttributes contains extended information about POSIX-layout containers. + POSIXAttributes struct { + ChunkSize uint64 `json:"chunk_size,omitempty"` + ObjectClass ObjectClass `json:"object_class,omitempty"` + DirObjectClass ObjectClass `json:"dir_object_class,omitempty"` + FileObjectClass ObjectClass `json:"file_object_class,omitempty"` + ConsistencyMode uint32 `json:"cons_mode,omitempty"` + Hints string `json:"hints,omitempty"` + } + + // ContainerInfo contains information about the Container. + ContainerInfo struct { + PoolUUID uuid.UUID `json:"pool_uuid"` + ContainerUUID uuid.UUID `json:"container_uuid"` + ContainerLabel string `json:"container_label,omitempty"` + LatestSnapshot HLC `json:"latest_snapshot"` + RedundancyFactor uint32 `json:"redundancy_factor"` + NumHandles uint32 `json:"num_handles"` + NumSnapshots uint32 `json:"num_snapshots"` + OpenTime HLC `json:"open_time"` + CloseModifyTime HLC `json:"close_modify_time"` + Type ContainerLayout `json:"container_type"` + Health string `json:"health"` + *POSIXAttributes `json:",omitempty"` + } +) + // Name returns an identifier for the container (Label, if set, falling back to UUID). func (ci *ContainerInfo) Name() string { if ci.ContainerLabel == "" { @@ -59,3 +129,29 @@ func (ci *ContainerInfo) Name() string { } return ci.ContainerLabel } + +func (ci *ContainerInfo) String() string { + return ci.Name() +} + +func (ci *ContainerInfo) MarshalJSON() ([]byte, error) { + checkZeroHLC := func(hlc HLC) string { + if hlc.IsZero() { + return "" + } + return hlc.String() + } + + type toJSON ContainerInfo + return json.Marshal(&struct { + toJSON + LatestSnapshot string `json:"latest_snapshot,omitempty"` + OpenTime string `json:"open_time,omitempty"` + CloseModifyTime string `json:"close_modify_time,omitempty"` + }{ + toJSON: toJSON(*ci), + LatestSnapshot: checkZeroHLC(ci.LatestSnapshot), + OpenTime: checkZeroHLC(ci.OpenTime), + CloseModifyTime: checkZeroHLC(ci.CloseModifyTime), + }) +} diff --git a/src/control/lib/daos/hlc.go b/src/control/lib/daos/hlc.go index fadb21a71a8..3eb25f50831 100644 --- a/src/control/lib/daos/hlc.go +++ b/src/control/lib/daos/hlc.go @@ -1,5 +1,6 @@ // // (C) Copyright 2022 Intel Corporation. +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -43,6 +44,10 @@ func (hlc HLC) ToTime() time.Time { return time.Unix(0, hlc.Nanoseconds()) } +func (hlc HLC) IsZero() bool { + return hlc == 0 || hlc.String() == ZeroHLCDate +} + func (hlc HLC) MarshalJSON() ([]byte, error) { return []byte(`"` + common.FormatTime(hlc.ToTime()) + `"`), nil } diff --git a/src/control/lib/daos/libgurt.go b/src/control/lib/daos/libgurt.go index 3e1485b968f..c1862fcd961 100644 --- a/src/control/lib/daos/libgurt.go +++ b/src/control/lib/daos/libgurt.go @@ -1,4 +1,6 @@ // +// (C) Copyright 2024 Intel Corporation. +// // SPDX-License-Identifier: BSD-2-Clause-Patent // //go:build !test_stubs diff --git a/src/control/lib/daos/object.go b/src/control/lib/daos/object.go new file mode 100644 index 00000000000..777ea57ed97 --- /dev/null +++ b/src/control/lib/daos/object.go @@ -0,0 +1,68 @@ +// +// (C) Copyright 2024-2025 Intel Corporation. +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package daos + +import ( + "fmt" + + "github.com/pkg/errors" +) + +/* +#include +*/ +import "C" + +// NB: We can't use the oclass_name2id() and oclass_id2name() helpers +// in this package because they are client-only symbols and we don't +// want this package to depend directly on libdaos. :/ +// +// TODO(?): Move the oclass helpers into the common library? + +var ( + objClass2String objClassIDStringer = func(oc ObjectClass) string { + return fmt.Sprintf("0x%x", oc) + } + objClassName2Class objClassNameResolver = func(name string) (ObjectClass, error) { + return ObjectClass(0), errors.New("no object class resolver set; can't resolve class name") + } +) + +// SetObjectClassHelpers provides a mechanism for injecting the object API +// methods into this package without introducing a hard dependency on libdaos. +// Generally only called from object.go in the api package. +func SetObjectClassHelpers(stringer objClassIDStringer, resolver objClassNameResolver) { + objClass2String = stringer + objClassName2Class = resolver +} + +type ( + objClassNameResolver func(string) (ObjectClass, error) + objClassIDStringer func(ObjectClass) string + + // ObjectClass represents an object class. + ObjectClass C.daos_oclass_id_t +) + +// FromString resolves a string to an ObjectClass. +func (oc *ObjectClass) FromString(name string) error { + class, err := objClassName2Class(name) + if err != nil { + return err + } + *oc = class + return nil +} + +func (oc ObjectClass) String() string { + return objClass2String(oc) +} + +func (oc ObjectClass) MarshalJSON() ([]byte, error) { + return []byte(`"` + oc.String() + `"`), nil +} diff --git a/src/control/lib/daos/pool_cont_prop.go b/src/control/lib/daos/pool_cont_prop.go index 09b7cb40e85..348ef964ae7 100644 --- a/src/control/lib/daos/pool_cont_prop.go +++ b/src/control/lib/daos/pool_cont_prop.go @@ -1,6 +1,7 @@ // // (C) Copyright 2019-2023 Intel Corporation. // (C) Copyright 2025 Hewlett Packard Enterprise Development LP +// (C) Copyright 2025 Google LLC // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -28,53 +29,6 @@ const ( MaxLabelLength = C.DAOS_PROP_LABEL_MAX_LEN ) -const ( - // PropEntryAllocedOID is the highest allocated OID. - PropEntryAllocedOID = C.DAOS_PROP_ENTRY_ALLOCED_OID - // PropEntryChecksum is the checksum property. - PropEntryChecksum = C.DAOS_PROP_ENTRY_CKSUM - // PropEntryChecksumSize is the checksum size property. - PropEntryChecksumSize = C.DAOS_PROP_ENTRY_CKSUM_SIZE - // PropEntryCompression is the compression property. - PropEntryCompression = C.DAOS_PROP_ENTRY_COMPRESS - // PropEntryDedupe is the dedupe property. - PropEntryDedupe = C.DAOS_PROP_ENTRY_DEDUP - // PropEntryDedupThreshold is the dedupe threshold property. - PropEntryDedupeThreshold = C.DAOS_PROP_ENTRY_DEDUP_THRESHOLD - // PropEntryECCellSize is the EC cell size property. - PropEntryECCellSize = C.DAOS_PROP_ENTRY_EC_CELL_SZ - // PropEntryECPerfDomainAff is the EC performance domain affinity property. - PropEntryECPerfDomainAff = C.DAOS_PROP_ENTRY_EC_PDA - // PropEntryEncryption is the encryption property. - PropEntryEncryption = C.DAOS_PROP_ENTRY_ENCRYPT - // PropEntryGlobalVersion is the global version property. - PropEntryGlobalVersion = C.DAOS_PROP_ENTRY_GLOBAL_VERSION - // PropEntryObjectVersion is the object layout version property. - PropEntryObjectVersion = C.DAOS_PROP_ENTRY_OBJ_VERSION - // PropEntryGroup is the group property. - PropEntryGroup = C.DAOS_PROP_ENTRY_GROUP - // PropEntryLabel is the label property. - PropEntryLabel = C.DAOS_PROP_ENTRY_LABEL - // PropEntryLayout is the layout property. - PropEntryLayoutType = C.DAOS_PROP_ENTRY_LAYOUT_TYPE - // PropEntryLayoutVersion is the layout version property. - PropEntryLayoutVersion = C.DAOS_PROP_ENTRY_LAYOUT_VER - // PropEntryOwner is the owner property. - PropEntryOwner = C.DAOS_PROP_ENTRY_OWNER - // PropEntryRedunFactor is the redundancy factor property. - PropEntryRedunFactor = C.DAOS_PROP_ENTRY_REDUN_FAC - // PropEntryRedunLevel is the redundancy level property. - PropEntryRedunLevel = C.DAOS_PROP_ENTRY_REDUN_LVL - // PropEntryRedunPerfDomainAff is the redundancy performance domain affinity property. - PropEntryRedunPerfDomainAff = C.DAOS_PROP_ENTRY_RP_PDA - // PropEntrySnapshotMax is the snapshot max property. - PropEntrySnapshotMax = C.DAOS_PROP_ENTRY_SNAPSHOT_MAX - // PropEntryServerChecksum is the server checksum property. - PropEntryServerChecksum = C.DAOS_PROP_ENTRY_SRV_CKSUM - // PropEntryStatus is the status property. - PropEntryStatus = C.DAOS_PROP_ENTRY_STATUS -) - const ( // PoolPropertyMin before any pool property PoolPropertyMin = C.DAOS_PROP_PO_MIN diff --git a/src/control/lib/daos/property.go b/src/control/lib/daos/property.go new file mode 100644 index 00000000000..efdaa0989a1 --- /dev/null +++ b/src/control/lib/daos/property.go @@ -0,0 +1,185 @@ +// +// (C) Copyright 2024 Intel Corporation. +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package daos + +import ( + "fmt" + "unsafe" + + "github.com/pkg/errors" +) + +/* +#include + +static inline char * +get_dpe_str(struct daos_prop_entry *dpe) +{ + if (dpe == NULL) + return NULL; + + return dpe->dpe_str; +} + +static inline uint64_t +get_dpe_val(struct daos_prop_entry *dpe) +{ + if (dpe == NULL) + return 0; + + return dpe->dpe_val; +} + +static inline void * +get_dpe_val_ptr(struct daos_prop_entry *dpe) +{ + if (dpe == NULL) + return NULL; + + return dpe->dpe_val_ptr; +} + +static inline bool +dpe_is_negative(struct daos_prop_entry *dpe) +{ + if (dpe == NULL) + return 0; + + return dpe->dpe_flags & DAOS_PROP_ENTRY_NOT_SET; +} + +static inline void +set_dpe_str(struct daos_prop_entry *dpe, d_string_t str) +{ + if (dpe == NULL) + return; + + dpe->dpe_str = str; +} + +static inline void +set_dpe_val(struct daos_prop_entry *dpe, uint64_t val) +{ + if (dpe == NULL) + return; + + dpe->dpe_val = val; +} + +static inline void +set_dpe_val_ptr(struct daos_prop_entry *dpe, void *val_ptr) +{ + if (dpe == NULL) + return; + + dpe->dpe_val_ptr = val_ptr; +} +*/ +import "C" + +type ( + property struct { + idx C.int + entry *C.struct_daos_prop_entry + } + + propertyList struct { + cProps *C.daos_prop_t + entries []C.struct_daos_prop_entry + } + + ContainerProperty struct { + property + } + + ContainerPropertyList struct { + propertyList + } +) + +func (p *property) GetString() string { + return C.GoString(C.get_dpe_str(p.entry)) +} + +func (p *property) GetValue() uint64 { + return uint64(C.get_dpe_val(p.entry)) +} + +func (p *property) GetValuePtr() unsafe.Pointer { + return unsafe.Pointer(C.get_dpe_val_ptr(p.entry)) +} + +func (p *property) SetString(str string) { + C.set_dpe_str(p.entry, C.d_string_t(C.CString(str))) +} + +func (p *property) SetValue(val uint64) { + C.set_dpe_val(p.entry, C.uint64_t(val)) +} + +func (p *property) SetValuePtr(val unsafe.Pointer) { + C.set_dpe_val_ptr(p.entry, val) +} + +func (p *property) String() string { + propStr := p.GetString() + if propStr == "" { + ptr := p.GetValuePtr() + if ptr == nil { + propStr = fmt.Sprintf("%d", p.GetValue()) + } else { + propStr = fmt.Sprintf("%p", ptr) + } + } + + return propStr +} + +func newPropertyList(count uint) (*propertyList, error) { + props := C.daos_prop_alloc(C.uint(count)) + if props == nil { + return nil, errors.Wrap(NoMemory, "failed to allocate property list") + } + + // Set to zero initially, will be incremented as properties are added. + props.dpp_nr = 0 + + return &propertyList{ + cProps: props, + entries: unsafe.Slice(props.dpp_entries, count), + }, nil +} + +func propertyListFromPtr(ptr unsafe.Pointer) (*propertyList, error) { + if ptr == nil { + return nil, errors.Wrap(InvalidInput, "nil pointer") + } + + props := (*C.daos_prop_t)(ptr) + return &propertyList{ + cProps: props, + entries: unsafe.Slice(props.dpp_entries, props.dpp_nr), + }, nil +} + +func (pl *propertyList) Free() { + C.daos_prop_free(pl.cProps) +} + +func (pl *propertyList) Properties() (props []*property) { + for i := range pl.entries { + props = append(props, &property{ + idx: C.int(i), + entry: &pl.entries[i], + }) + } + return +} + +func (pl *propertyList) ToPtr() unsafe.Pointer { + return unsafe.Pointer(pl.cProps) +} diff --git a/src/include/daos/cont_props.h b/src/include/daos/cont_props.h index 10cca999477..169fec0850f 100644 --- a/src/include/daos/cont_props.h +++ b/src/include/daos/cont_props.h @@ -1,5 +1,5 @@ /** - * (C) Copyright 2020-2023 Intel Corporation. + * (C) Copyright 2020-2024 Intel Corporation. * * SPDX-License-Identifier: BSD-2-Clause-Patent */ @@ -33,6 +33,9 @@ #define DAOS_PROP_ENTRY_GLOBAL_VERSION "global_version" #define DAOS_PROP_ENTRY_OBJ_VERSION "obj_version" #define DAOS_PROP_ENTRY_PERF_DOMAIN "perf_domain" +#define DAOS_PROP_ENTRY_ACL "acl" +#define DAOS_PROP_ENTRY_SCRUB_DISABLED "scrub_disabled" +#define DAOS_PROP_ENTRY_ROOT_OIDS "root_oids" /** DAOS deprecated property entry names keeped for backward compatibility */ #define DAOS_PROP_ENTRY_REDUN_FAC_OLD "rf" diff --git a/utils/node_local_test.py b/utils/node_local_test.py index 5d1fd2dfba8..96af834e949 100755 --- a/utils/node_local_test.py +++ b/utils/node_local_test.py @@ -4384,7 +4384,7 @@ def test_daos_fs_fix(self): assert rc.returncode != 0 output = rc.stderr.decode('utf-8') line = output.splitlines() - if line[-1] != 'ERROR: daos: failed fs fix-entry: DER_BUSY(-1012): Device or resource busy': + if 'DER_BUSY(-1012): Device or resource busy' not in line[-1]: raise NLTestFail('daos fs fix-entry /test_dir/f1') # stop dfuse @@ -5805,7 +5805,7 @@ def _explain(): if self._aft.check_stderr: stderr = self._stderr.decode('utf-8').rstrip() if stderr != '' and not stderr.endswith('(-1009): Out of memory') and \ - not stderr.endswith(': errno 12 (Cannot allocate memory)') and \ + not stderr.endswith(': DFS error 12: Cannot allocate memory') and \ 'error parsing command line arguments' not in stderr and \ self.stdout != self._aft.expected_stdout: if self.stdout != b'':