diff --git a/src/control/cmd/daos/acl.go b/src/control/cmd/daos/acl.go index 34d5b8491f4..f105c7a33bd 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 // @@ -154,7 +155,7 @@ func (cmd *containerOverwriteACLCmd) 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 } @@ -244,7 +245,7 @@ func (cmd *containerUpdateACLCmd) 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 } @@ -278,7 +279,7 @@ func (cmd *containerDeleteACLCmd) 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 } @@ -340,7 +341,7 @@ func (cmd *containerGetACLCmd) 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 } @@ -348,7 +349,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 +399,7 @@ func (cmd *containerSetOwnerCmd) 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/attribute.go b/src/control/cmd/daos/attribute.go index 677d2beaebe..0549834d094 100644 --- a/src/control/cmd/daos/attribute.go +++ b/src/control/cmd/daos/attribute.go @@ -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..80994c10711 100644 --- a/src/control/cmd/daos/container.go +++ b/src/control/cmd/daos/container.go @@ -9,20 +9,18 @@ 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/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,20 +65,14 @@ 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) @@ -116,111 +108,55 @@ func (cmd *containerBaseCmd) openContainer(openFlags C.uint) error { 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: daos.ContainerOpenFlag(openFlags), + Query: false, + }) + } else { + resp, err = api.ContainerOpen(ctx, api.ContainerOpenReq{ + ID: containerID, + Flags: daos.ContainerOpenFlag(openFlags), + Query: false, + 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 { + if ccErr := cmd.container.Close(ctx); ccErr != nil { + logging.FromContext(ctx).Errorf("failed to close %s in cleanup: %s", cmd.container, ccErr) + } + 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() { + cmd.Debugf("closing container: %s", cmd.contUUID) + if err := containerCloseAPI(cmd.cContHandle); 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 +249,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 +260,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 +270,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 +338,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 { 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 +545,36 @@ 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 +func (cmd *existingContainerCmd) resolveAndOpen(contFlags C.uint, ap *C.struct_cmd_args_s) (func(), error) { + nulCleanFn := func() {} + if err := cmd.resolveContainer(ap); err != nil { + return nulCleanFn, err } - var cleanupPool func() - cleanupPool, err = cmd.connectPool(daos.PoolConnectFlagReadOnly, ap) - if err != nil { - return + if err := cmd.openContainer(contFlags); err != nil { + return nulCleanFn, err } - if err = cmd.openContainer(contFlags); err != nil { - cleanupPool() - return + cleanup := func() { + if err := cmd.container.Close(cmd.MustLogCtx()); err != nil { + cmd.Errorf("failed to close container %s: %+v", cmd.container, err) + } } 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 +584,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,45 +623,32 @@ func (cmd *containerDestroyCmd) Execute(_ []string) error { return err } - var cleanup func() - cleanup, err = cmd.connectPool(C.DAOS_COO_RW, 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) - if err != nil { - 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(), "", poolID, contID, cmd.Force); err != nil { + return errors.Wrapf(err, "failed to destroy container %s", contID) } - } - defer cleanup() - cmd.Debugf("destroying container %s (force: %t)", - cmd.ContainerID(), cmd.Force) + cmd.Infof("Successfully destroyed container %s", contID) + return nil + } - 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") + // TODO: Add API for DUNS. + cleanup, err := cmd.connectPool(C.DAOS_COO_RO, ap) + if err != nil { + return err } + defer cleanup() - 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() { @@ -836,7 +673,7 @@ func (cmd *containerListObjectsCmd) Execute(_ []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 } @@ -921,60 +758,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 +769,15 @@ func (cmd *containerQueryCmd) 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 } 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 +785,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 +858,7 @@ func (cmd *containerCheckCmd) Execute(_ []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 } @@ -1110,33 +891,13 @@ func (cmd *containerListAttrsCmd) 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 } 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 { @@ -1162,19 +923,13 @@ func (cmd *containerDelAttrCmd) 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 } 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.Attr) } type containerGetAttrCmd struct { @@ -1204,38 +959,13 @@ func (cmd *containerGetAttrCmd) 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 } 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 { @@ -1264,35 +994,19 @@ func (cmd *containerSetAttrCmd) Execute(args []string) error { } } - 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(C.DAOS_COO_RW, 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 +1043,7 @@ func (cmd *containerGetPropCmd) 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 } @@ -1406,7 +1120,7 @@ func (cmd *containerSetPropCmd) 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 } @@ -1480,15 +1194,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, }) } } @@ -1572,7 +1293,7 @@ func (cmd *containerEvictCmd) Execute(_ []string) (err error) { co_flags = C.DAOS_COO_EVICT | C.DAOS_COO_RO } - 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/filesystem.go b/src/control/cmd/daos/filesystem.go index 6996f33a0eb..0715bd0e023 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 // @@ -198,7 +199,7 @@ func fsModifyAttr(cmd *fsAttrCmd, op uint32, updateAP func(*C.struct_cmd_args_s) updateAP(ap) } - cleanup, err := cmd.resolveAndConnect(flags, ap) + cleanup, err := cmd.resolveAndOpen(flags, ap) if err != nil { return err } @@ -267,7 +268,7 @@ func (cmd *fsGetAttrCmd) Execute(_ []string) error { ap.fs_op = C.FS_GET_ATTR flags := C.uint(C.DAOS_COO_RO) - cleanup, err := cmd.resolveAndConnect(flags, ap) + cleanup, err := cmd.resolveAndOpen(flags, ap) if err != nil { return err } @@ -402,7 +403,7 @@ func (cmd *fsFixEntryCmd) Execute(_ []string) error { flags := C.uint(C.DAOS_COO_EX) ap.fs_op = C.FS_CHECK - cleanup, err := cmd.resolveAndConnect(flags, ap) + cleanup, err := cmd.resolveAndOpen(flags, ap) if err != nil { return errors.Wrapf(err, "failed fs fix-entry") } @@ -439,7 +440,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(C.DAOS_COO_EX, ap) if err != nil { return err } @@ -484,7 +485,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(C.DAOS_COO_EX, ap) if err != nil { return err } @@ -690,7 +691,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(C.DAOS_COO_RW, 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..e5aee6da732 100644 --- a/src/control/cmd/daos/flags_test.go +++ b/src/control/cmd/daos/flags_test.go @@ -410,7 +410,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/pretty/container.go b/src/control/cmd/daos/pretty/container.go new file mode 100644 index 00000000000..c6a5f2f91b7 --- /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.ID != 0 { + rows = append(rows, txtfmt.TableRow{"Object Class": ci.ObjectClass.String()}) + } + if ci.DirObjectClass.ID != 0 { + rows = append(rows, txtfmt.TableRow{"Dir Object Class": ci.DirObjectClass.String()}) + } + if ci.FileObjectClass.ID != 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/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..ef0547e1ac5 --- /dev/null +++ b/src/control/lib/daos/api/container.go @@ -0,0 +1,522 @@ +// +// (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 + } + + 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) +} + +// 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), + } + + 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 +} + +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 + } +) + +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 == nil { + 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 == nil { + 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 { + // 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 == nil { + return nil, ErrInvalidPoolHandle + } + + return ContainerOpen(ph.toCtx(ctx), req) +} + +// ContainerOpen opens the container specified in the open request. +func ContainerOpen(ctx context.Context, req ContainerOpenReq) (*ContainerOpenResp, error) { + poolHdl, poolDisc, err := getPoolConn(ctx, req.SysName, req.PoolID, contFlags2PoolFlags(req.Flags)) + if err != nil { + return nil, errors.Wrapf(err, "failed to connect to pool %q", req.PoolID) + } + 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, + } + + if err := daosError(daos_cont_open(poolHdl.daosHandle, cContID, C.uint(req.Flags), &contHdl.connHandle.daosHandle, nil, nil)); err != nil { + return nil, errors.Wrapf(err, "failed to open container %q", req.ID) + } + + var contInfo *daos.ContainerInfo + if req.Query { + contInfo, err = contHdl.Query(ctx) + if err != nil { + return nil, errors.Wrapf(err, "failed to query container %q", req.ID) + } + contHdl.connHandle.UUID = contInfo.ContainerUUID + contHdl.connHandle.Label = contInfo.ContainerLabel + } else { + if contUUID, err := uuid.Parse(req.ID); err == nil { + contHdl.connHandle.UUID = contUUID + } else { + contHdl.connHandle.Label = req.ID + } + contInfo = newContainerInfo(poolHdl.UUID(), contHdl.UUID(), nil, nil) + } + + logging.FromContext(ctx).Debugf("Opened Container %s)", contHdl) + return &ContainerOpenResp{ + Connection: contHdl, + Info: contInfo, + }, nil +} + +func containerQueryDFSAttrs(contConn *ContainerHandle) (*daos.POSIXAttributes, error) { + var pa daos.POSIXAttributes + var dfs *C.dfs_t + var attr C.dfs_attr_t + + // 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 = newObjectClass(attr.da_oclass_id) + pa.DirObjectClass = newObjectClass(attr.da_dir_oclass_id) + pa.FileObjectClass = newObjectClass(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 +} + +// 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() { + err := resp.Connection.Close(ctx) + if err != nil { + logging.FromContext(ctx).Error(err.Error()) + } + resp.Connection.poolConnCleanup() + } + return resp.Connection, cleanup, 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 == nil { + 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 == nil { + 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 == nil { + 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 == nil { + 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 == nil { + 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 == nil { + 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..bb1258eb651 --- /dev/null +++ b/src/control/lib/daos/api/container_test.go @@ -0,0 +1,440 @@ +// +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package api + +import ( + "context" + "fmt" + "reflect" + "sort" + "testing" + + "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/logging" +) + +var ( + testContName = "test-container" + + testCtxContHandle = &ContainerHandle{ + connHandle: connHandle{ + UUID: test.MockPoolUUID(43), + Label: "test-ctx-cont", + }, + PoolHandle: testCtxPoolHandle, + } +) + +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, + }, + "cont handle in context with non-empty ID": { + ctx: testCtxContHandle.toCtx(test.Context(t)), + poolID: testPoolName, + contID: testContName, + expErr: errors.New("ContainerHandle found in context with non-empty contID"), + }, + "cont handle not in context, no contID": { + ctx: test.Context(t), + poolID: testPoolName, + expErr: errors.Wrap(daos.InvalidInput, "no container ID provided"), + }, + "daos_cont_list_attr() fails (get buf size)": { + setup: func(t *testing.T) { + daos_cont_list_attr_RC = -_Ctype_int(daos.IOError) + }, + ctx: testCtxContHandle.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: testCtxContHandle.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: testCtxContHandle.toCtx(test.Context(t)), + }, + "success": { + ctx: testCtxContHandle.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) + } + log, buf := logging.NewTestLogger(name) + defer test.ShowBufferOnFailure(t, buf) + + gotNames, err := ContainerListAttributes(mustLogCtx(tc.ctx, log), "", 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, + }, + "cont handle in context with non-empty ID": { + ctx: testCtxContHandle.toCtx(test.Context(t)), + poolID: testPoolName, + contID: testContName, + expErr: errors.New("ContainerHandle found in context with non-empty contID"), + }, + "cont handle not in context, no contID": { + ctx: test.Context(t), + poolID: testPoolName, + expErr: errors.Wrap(daos.InvalidInput, "no container ID provided"), + }, + "daos_cont_list_attr() fails": { + setup: func(t *testing.T) { + daos_cont_list_attr_RC = -_Ctype_int(daos.IOError) + }, + ctx: testCtxContHandle.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: testCtxContHandle.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: testCtxContHandle.toCtx(test.Context(t)), + expErr: errors.Wrap(daos.IOError, "failed to get container attribute values"), + }, + "empty requested attribute name": { + ctx: testCtxContHandle.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: testCtxContHandle.toCtx(test.Context(t)), + attrNames: test.JoinArgs(nil, "foo"), + checkParams: func(t *testing.T) { + test.CmpAny(t, "req attr names", test.JoinArgs(nil, "foo"), daos_cont_get_attr_ReqNames) + }, + expErr: errors.Wrap(daos.Nonexistent, "failed to get container attribute sizes"), + }, + "unknown attribute requested": { + ctx: testCtxContHandle.toCtx(test.Context(t)), + attrNames: test.JoinArgs(nil, "foo"), + checkParams: func(t *testing.T) { + test.CmpAny(t, "req attr names", test.JoinArgs(nil, "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: testCtxContHandle.toCtx(test.Context(t)), + }, + "success; all attributes": { + ctx: testCtxContHandle.toCtx(test.Context(t)), + expAttrs: daos_default_AttrList, + }, + "success; requested attributes": { + ctx: testCtxContHandle.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) + } + log, buf := logging.NewTestLogger(name) + defer test.ShowBufferOnFailure(t, buf) + + gotAttrs, err := ContainerGetAttributes(mustLogCtx(tc.ctx, log), "", tc.poolID, tc.contID, tc.attrNames...) + test.CmpErr(t, tc.expErr, err) + if tc.expErr != nil { + return + } + + if tc.checkParams != nil { + tc.checkParams(t) + } + + 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, + }, + "cont handle in context with non-empty ID": { + ctx: testCtxContHandle.toCtx(test.Context(t)), + poolID: testPoolName, + contID: testContName, + expErr: errors.New("ContainerHandle found in context with non-empty contID"), + }, + "cont handle not in context, no contID": { + ctx: test.Context(t), + poolID: testPoolName, + expErr: errors.Wrap(daos.InvalidInput, "no container ID provided"), + }, + "no attributes to set": { + ctx: testCtxContHandle.toCtx(test.Context(t)), + expErr: errors.Wrap(daos.InvalidInput, "no container attributes provided"), + }, + "nil toSet attribute": { + ctx: testCtxContHandle.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: testCtxContHandle.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: testCtxContHandle.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: testCtxContHandle.toCtx(test.Context(t)), + toSet: daos_default_AttrList, + expErr: errors.Wrap(daos.IOError, "failed to set container attributes"), + }, + "success": { + ctx: testCtxContHandle.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) + } + log, buf := logging.NewTestLogger(name) + defer test.ShowBufferOnFailure(t, buf) + + err := ContainerSetAttributes(mustLogCtx(tc.ctx, log), "", 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, + }, + "cont handle in context with non-empty ID": { + ctx: testCtxContHandle.toCtx(test.Context(t)), + poolID: testPoolName, + contID: testContName, + expErr: errors.New("ContainerHandle found in context with non-empty contID"), + }, + "cont handle not in context, no contID": { + ctx: test.Context(t), + poolID: testPoolName, + expErr: errors.Wrap(daos.InvalidInput, "no container ID provided"), + }, + "no attributes to delete": { + ctx: testCtxContHandle.toCtx(test.Context(t)), + expErr: errors.Wrap(daos.InvalidInput, "no container attribute names provided"), + }, + "empty name in toDelete list": { + ctx: testCtxContHandle.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: testCtxContHandle.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: testCtxContHandle.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) + } + log, buf := logging.NewTestLogger(name) + defer test.ShowBufferOnFailure(t, buf) + + err := ContainerDeleteAttributes(mustLogCtx(tc.ctx, log), "", 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) { + testHandle := &ContainerHandle{} + + thType := reflect.TypeOf(testHandle) + for i := 0; i < thType.NumMethod(); i++ { + method := thType.Method(i) + methArgs := make([]reflect.Value, 0) + 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: testHandle, + }, + } { + t.Run(name, func(t *testing.T) { + thArg := reflect.ValueOf(tc.th) + if tc.th == nil { + thArg = reflect.New(thType).Elem() + } + ctxArg := reflect.ValueOf(test.Context(t)) + 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 f82afb52a83..6cc93f733d7 100644 --- a/src/control/lib/daos/api/errors.go +++ b/src/control/lib/daos/api/errors.go @@ -21,12 +21,12 @@ import ( import "C" var ( - ErrNoSystemRanks = errors.New("no ranks in system") - ErrInvalidPoolHandle = errors.New("pool handle is nil or invalid") + ErrNoSystemRanks = errors.New("no ranks in system") + 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/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..b46debfc5ca --- /dev/null +++ b/src/control/lib/daos/api/libdaos_cont_stubs.go @@ -0,0 +1,217 @@ +// +// (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, + } +) + +func defaultContHdl() *C.daos_handle_t { + newHdl := C.daos_handle_t{cookie: daos_default_cont_open_Handle.cookie} + return &newHdl +} + +var ( + daos_cont_destroy_RC C.int = 0 +) + +func reset_daos_cont_destroy() { + 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 { + 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_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_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 { + 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_RC C.int = 0 +) + +func reset_daos_cont_close() { + daos_cont_close_RC = 0 +} + +func daos_cont_close(contHdl C.daos_handle_t) C.int { + if daos_cont_close_RC != 0 { + return daos_cont_close_RC + } + + return daos_cont_close_RC +} + +var ( + daos_cont_query_RC C.int = 0 +) + +func reset_daos_cont_query() { + 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 + } + + 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_stubs.go b/src/control/lib/daos/api/libdaos_stubs.go index e7c00a4076d..2fc3e512902 100644 --- a/src/control/lib/daos/api/libdaos_stubs.go +++ b/src/control/lib/daos/api/libdaos_stubs.go @@ -19,7 +19,7 @@ import ( /* #include #include -#include +#include #include "util.h" @@ -36,7 +36,22 @@ func ResetTestStubs() { reset_daos_pool_get_attr() reset_daos_pool_set_attr() reset_daos_pool_del_attr() + reset_daos_pool_list_cont() + reset_daos_mgmt_list_pools() + + 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() + + reset_dfs_mount() + reset_dfs_umount() + reset_dfs_query() } var ( @@ -140,3 +155,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..3933cef5dfc --- /dev/null +++ b/src/control/lib/daos/api/libdfs_stubs.go @@ -0,0 +1,51 @@ +// +// (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" + +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_RC C.int = 0 +) + +func reset_dfs_query() { + dfs_query_RC = 0 +} + +func dfs_query(dfs *C.dfs_t, attrs *C.dfs_attr_t) C.int { + 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..638c9424d63 --- /dev/null +++ b/src/control/lib/daos/api/object.go @@ -0,0 +1,56 @@ +// +// (C) Copyright 2024-2025 Intel Corporation. +// (C) Copyright 2025 Hewlett Packard Enterprise Development LP +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package api + +import ( + "strings" + + "github.com/daos-stack/daos/src/control/lib/daos" + "github.com/pkg/errors" +) + +/* +#include +#include +*/ +import "C" + +// 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 nil, errors.Wrapf(daos.InvalidInput, "invalid object class %q", name) + } + + return &daos.ObjectClass{ID: uint32(id), Name: upperName}, nil +} + +// ObjectClassFromID returns a new ObjectClass for the given ID. +func ObjectClassFromID(id uint32) (*daos.ObjectClass, error) { + var oclass [10]C.char + + if rc := daos_oclass_id2name(C.daos_oclass_id_t(id), &oclass[0]); rc != 0 { + return nil, errors.Wrapf(daos.InvalidInput, "invalid object class %d", id) + } + + return &daos.ObjectClass{ID: uint32(id), Name: C.GoString(&oclass[0])}, nil +} + +// newObjectClass returns a new ObjectClass for the given ID regardless of +// whether or not the ID resolves to a name. Internal use only. +func newObjectClass(id C.uint32_t) daos.ObjectClass { + oc, err := ObjectClassFromID(uint32(id)) + if err != nil { + return daos.ObjectClass{ID: uint32(id), Name: "UNKNOWN"} + } + return *oc +} diff --git a/src/control/lib/daos/api/pool.go b/src/control/lib/daos/api/pool.go index c4dad3d337f..76f4ea9594e 100644 --- a/src/control/lib/daos/api/pool.go +++ b/src/control/lib/daos/api/pool.go @@ -560,6 +560,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 == nil { + 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 93bf48a30d3..0f8e573fc42 100644 --- a/src/control/lib/daos/api/pool_test.go +++ b/src/control/lib/daos/api/pool_test.go @@ -882,6 +882,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. diff --git a/src/control/lib/daos/api/util.go b/src/control/lib/daos/api/util.go index 9c461cd5c16..a0178ddc785 100644 --- a/src/control/lib/daos/api/util.go +++ b/src/control/lib/daos/api/util.go @@ -1,3 +1,9 @@ +// +// (C) Copyright 2024 Intel Corporation. +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + package api import ( 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..72ceeea39d8 100644 --- a/src/control/lib/daos/container.go +++ b/src/control/lib/daos/container.go @@ -1,57 +1,128 @@ // // (C) Copyright 2024 Intel Corporation. +// (C) Copyright 2025 Google LLC +// (C) Copyright 2025 Hewlett Packard Enterprise Development LP // // 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 +130,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..6068073e318 --- /dev/null +++ b/src/control/lib/daos/object.go @@ -0,0 +1,54 @@ +// +// (C) Copyright 2024-2025 Intel Corporation. +// (C) Copyright 2025 Google LLC +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// + +package daos + +import ( + "fmt" +) + +/* +#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 on libdaos. :/ +// +// TODO(?): Move the oclass helpers into the common library? + +const ( + UnknownObjectClassName = "UNKNOWN" +) + +type ( + // ObjectClass represents an object class. + ObjectClass struct { + ID uint32 + Name string + } +) + +func (oc ObjectClass) String() string { + if oc.Name != "" { + return oc.Name + } + return fmt.Sprintf("0%x", oc.ID) +} + +func (oc ObjectClass) MarshalJSON() ([]byte, error) { + name := oc.Name + if name == "" { + if oc.ID != 0 { + name = fmt.Sprintf("0x%x", oc.ID) + } else { + name = UnknownObjectClassName + } + } + return []byte(`"` + name + `"`), nil +} diff --git a/src/control/lib/daos/pool_cont_prop.go b/src/control/lib/daos/pool_cont_prop.go index 6d937b8e5b5..6cf62a1211b 100644 --- a/src/control/lib/daos/pool_cont_prop.go +++ b/src/control/lib/daos/pool_cont_prop.go @@ -1,5 +1,5 @@ // -// (C) Copyright 2019-2023 Intel Corporation. +// (C) Copyright 2019-2024 Intel Corporation. // // SPDX-License-Identifier: BSD-2-Clause-Patent // @@ -27,53 +27,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"