]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw_file: expose RGW user-defined attributes 23396/head
authorMatt Benjamin <mbenjamin@redhat.com>
Tue, 31 Jul 2018 14:39:54 +0000 (10:39 -0400)
committerMatt Benjamin <mbenjamin@redhat.com>
Fri, 4 Dec 2020 15:39:42 +0000 (10:39 -0500)
Permit nfsv4.2-style operations on RGW's user-defined attributes
(only).

RGW requires that user-defined attribute keys are prefixed
with RGW_ATTR_META_PREFIX.  Add/remove this prefix automagically
when storing/retrieving.

A special set of read-only,  internal attributes--currently just
user.rgw.etag--will be sent with lsxattrs() result.

Adds librgw_file_xattr fixture with unit tests for setxattrs,
getxattrs, lsxattrs, rmxattrs.

rgw_file: fix exposed_xattr support

Also extends the unit test to cover the missed cases.

Signed-off-by: Matt Benjamin <mbenjamin@redhat.com>
src/include/rados/rgw_file.h
src/rgw/rgw_file.cc
src/rgw/rgw_file.h
src/rgw/rgw_op.cc
src/rgw/rgw_op.h
src/rgw/rgw_op_type.h
src/test/CMakeLists.txt
src/test/librgw_file_xattr.cc [new file with mode: 0644]

index 66cf627aafa1c1b07ccc72ab3f1399d6d8234062..eb2d6dc4d82ea8df4a26274d01ea05d1e7db4276 100644 (file)
@@ -26,8 +26,8 @@ extern "C" {
 #endif
 
 #define LIBRGW_FILE_VER_MAJOR 1
-#define LIBRGW_FILE_VER_MINOR 1
-#define LIBRGW_FILE_VER_EXTRA 7
+#define LIBRGW_FILE_VER_MINOR 2
+#define LIBRGW_FILE_VER_EXTRA 0
 
 #define LIBRGW_FILE_VERSION(maj, min, extra) ((maj << 16) + (min << 8) + extra)
 #define LIBRGW_FILE_VERSION_CODE LIBRGW_FILE_VERSION(LIBRGW_FILE_VER_MAJOR, LIBRGW_FILE_VER_MINOR, LIBRGW_FILE_VER_EXTRA)
@@ -377,6 +377,53 @@ int rgw_fsync(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
 int rgw_commit(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
               uint64_t offset, uint64_t length, uint32_t flags);
 
+/*
+  extended attributes
+ */
+typedef struct rgw_xattrstr
+{
+  char *val;
+  uint32_t len;
+} rgw_xattrstr;
+
+typedef struct rgw_xattr
+{
+  rgw_xattrstr key;
+  rgw_xattrstr val;
+} rgw_xattr;
+
+typedef struct rgw_xattrlist
+{
+  rgw_xattr *xattrs;
+  uint32_t xattr_cnt;
+} rgw_xattrlist;
+
+#define RGW_GETXATTR_FLAG_NONE      0x0000
+
+typedef int (*rgw_getxattr_cb)(rgw_xattrlist *attrs, void *arg,
+                              uint32_t flags);
+
+int rgw_getxattrs(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
+                 rgw_xattrlist *attrs, rgw_getxattr_cb cb, void *cb_arg,
+                 uint32_t flags);
+
+#define RGW_LSXATTR_FLAG_NONE       0x0000
+#define RGW_LSXATTR_FLAG_STOP       0x0001
+
+int rgw_lsxattrs(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
+                rgw_xattrstr *filter_prefix /* unimplemented for now */,
+                rgw_getxattr_cb cb, void *cb_arg, uint32_t flags);
+
+#define RGW_SETXATTR_FLAG_NONE      0x0000
+
+int rgw_setxattrs(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
+                rgw_xattrlist *attrs, uint32_t flags);
+
+#define RGW_RMXATTR_FLAG_NONE       0x0000
+
+int rgw_rmxattrs(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
+                rgw_xattrlist *attrs, uint32_t flags);
+
 #ifdef __cplusplus
 }
 #endif
index 28c1e0e1abc0249173d5fb7455304435f121911f..1a30f11cc55aeec5626e94437bf5161d2ab890d8 100644 (file)
@@ -69,6 +69,33 @@ namespace rgw {
     return 0;
   }
 
+  class XattrHash
+  {
+  public:
+    std::size_t operator()(const rgw_xattrstr& att) const noexcept {
+      return XXH64(att.val, att.len, 5882300);
+    }
+  };
+
+  class XattrEqual
+  {
+  public:
+    bool operator()(const rgw_xattrstr& lhs, const rgw_xattrstr& rhs) const {
+      return ((lhs.len == rhs.len) &&
+             (strncmp(lhs.val, rhs.val, lhs.len) == 0));
+    }
+  };
+
+  /* well-known attributes */
+  static const std::unordered_set<
+    rgw_xattrstr, XattrHash, XattrEqual> rgw_exposed_attrs = {
+    rgw_xattrstr{const_cast<char*>(RGW_ATTR_ETAG), sizeof(RGW_ATTR_ETAG)-1}
+  };
+
+  static inline bool is_exposed_attr(const rgw_xattrstr& k) {
+    return (rgw_exposed_attrs.find(k) != rgw_exposed_attrs.end());
+  }
+
   LookupFHResult RGWLibFS::stat_bucket(RGWFileHandle* parent, const char *path,
                                       RGWLibFS::BucketStats& bs,
                                       uint32_t flags)
@@ -931,7 +958,242 @@ namespace rgw {
     return 0;
   } /* RGWLibFS::setattr */
 
-  /* called under rgw_fh->mtx held */
+  static inline std::string prefix_xattr_keystr(const rgw_xattrstr& key) {
+    std::string keystr;
+    keystr.reserve(sizeof(RGW_ATTR_META_PREFIX) + key.len);
+    keystr += {RGW_ATTR_META_PREFIX};
+    keystr += string{key.val, key.len};
+    return keystr;
+  }
+
+  static inline std::string_view unprefix_xattr_keystr(const std::string& key)
+  {
+    std::string_view svk{key};
+    auto pos = svk.find(RGW_ATTR_META_PREFIX);
+    if (pos == std::string_view::npos) {
+      return std::string_view{""};
+    } else if (pos == 0) {
+      svk.remove_prefix(sizeof(RGW_ATTR_META_PREFIX)-1);
+    }
+    return svk;
+  }
+
+  int RGWLibFS::getxattrs(RGWFileHandle* rgw_fh, rgw_xattrlist *attrs,
+                         rgw_getxattr_cb cb, void *cb_arg,
+                         uint32_t flags)
+  {
+    /* cannot store on fs_root, should not on buckets? */
+    if ((rgw_fh->is_bucket()) ||
+       (rgw_fh->is_root()))  {
+      return -EINVAL;
+    }
+
+    int rc, rc2, rc3;
+    string obj_name{rgw_fh->relative_object_name2()};
+
+    RGWGetAttrsRequest req(cct, rgwlib.get_store()->get_user(user.user_id),
+                          rgw_fh->bucket_name(), obj_name);
+
+    for (uint32_t ix = 0; ix < attrs->xattr_cnt; ++ix) {
+      auto& xattr = attrs->xattrs[ix];
+
+      /* pass exposed attr keys as given, else prefix */
+      std::string k = is_exposed_attr(xattr.key)
+       ? std::string{xattr.key.val, xattr.key.len}
+       : prefix_xattr_keystr(xattr.key);
+
+      req.emplace_key(std::move(k));
+    }
+
+    if (ldlog_p1(get_context(), ceph_subsys_rgw, 15)) {
+      lsubdout(get_context(), rgw, 15)
+       << __func__
+       << " get keys for: "
+       << rgw_fh->object_name()
+       << " keys:"
+       << dendl;
+      for (const auto& attr: req.get_attrs()) {
+       lsubdout(get_context(), rgw, 15)
+         << "\tkey: " << attr.first << dendl;
+      }
+    }
+
+    rc = rgwlib.get_fe()->execute_req(&req);
+    rc2 = req.get_ret();
+    rc3 = ((rc == 0) && (rc2 == 0)) ? 0 : -EIO;
+
+    /* call back w/xattr data */
+    if (rc3 == 0) {
+      const auto& attrs = req.get_attrs();
+      for (const auto& attr : attrs) {
+
+       if (!attr.second.has_value())
+         continue;
+
+       const auto& k = attr.first;
+       const auto& v = attr.second.value();
+
+       /* return exposed attr keys as given, else unprefix --
+        * yes, we could have memoized the exposed check, but
+        * to be efficient it would need to be saved with
+        * RGWGetAttrs::attrs, I think */
+       std::string_view svk =
+         is_exposed_attr(rgw_xattrstr{const_cast<char*>(k.c_str()),
+                                      uint32_t(k.length())})
+         ? k
+         : unprefix_xattr_keystr(k);
+
+       /* skip entries not matching prefix */
+       if (svk.empty())
+         continue;
+
+       rgw_xattrstr xattr_k = { const_cast<char*>(svk.data()),
+                                uint32_t(svk.length())};
+       rgw_xattrstr xattr_v =
+         {const_cast<char*>(const_cast<buffer::list&>(v).c_str()),
+          uint32_t(v.length())};
+       rgw_xattr xattr = { xattr_k, xattr_v };
+       rgw_xattrlist xattrlist = { &xattr, 1 };
+
+       cb(&xattrlist, cb_arg, RGW_GETXATTR_FLAG_NONE);
+      }
+    }
+
+    return rc3;
+  } /* RGWLibFS::getxattrs */
+
+  int RGWLibFS::lsxattrs(
+    RGWFileHandle* rgw_fh, rgw_xattrstr *filter_prefix, rgw_getxattr_cb cb,
+    void *cb_arg, uint32_t flags)
+  {
+    /* cannot store on fs_root, should not on buckets? */
+    if ((rgw_fh->is_bucket()) ||
+       (rgw_fh->is_root()))  {
+      return -EINVAL;
+    }
+
+    int rc, rc2, rc3;
+    string obj_name{rgw_fh->relative_object_name2()};
+
+    RGWGetAttrsRequest req(cct, rgwlib.get_store()->get_user(user.user_id),
+                          rgw_fh->bucket_name(), obj_name);
+
+    rc = rgwlib.get_fe()->execute_req(&req);
+    rc2 = req.get_ret();
+    rc3 = ((rc == 0) && (rc2 == 0)) ? 0 : -EIO;
+
+    /* call back w/xattr data--check for eof */
+    if (rc3 == 0) {
+      const auto& keys = req.get_attrs();
+      for (const auto& k : keys) {
+
+       /* return exposed attr keys as given, else unprefix */
+       std::string_view svk =
+         is_exposed_attr(rgw_xattrstr{const_cast<char*>(k.first.c_str()),
+                                      uint32_t(k.first.length())})
+         ? k.first
+         : unprefix_xattr_keystr(k.first);
+
+       /* skip entries not matching prefix */
+       if (svk.empty())
+         continue;
+
+       rgw_xattrstr xattr_k = { const_cast<char*>(svk.data()),
+                                uint32_t(svk.length())};
+       rgw_xattrstr xattr_v = { nullptr, 0 };
+       rgw_xattr xattr = { xattr_k, xattr_v };
+       rgw_xattrlist xattrlist = { &xattr, 1 };
+
+       auto cbr = cb(&xattrlist, cb_arg, RGW_LSXATTR_FLAG_NONE);
+       if (cbr & RGW_LSXATTR_FLAG_STOP)
+         break;
+      }
+    }
+
+    return rc3;
+  } /* RGWLibFS::lsxattrs */
+
+  int RGWLibFS::setxattrs(RGWFileHandle* rgw_fh, rgw_xattrlist *attrs,
+                         uint32_t flags)
+  {
+    /* cannot store on fs_root, should not on buckets? */
+    if ((rgw_fh->is_bucket()) ||
+       (rgw_fh->is_root()))  {
+      return -EINVAL;
+    }
+
+    int rc, rc2;
+    string obj_name{rgw_fh->relative_object_name2()};
+
+    RGWSetAttrsRequest req(cct, rgwlib.get_store()->get_user(user.user_id),
+                          rgw_fh->bucket_name(), obj_name);
+
+    for (uint32_t ix = 0; ix < attrs->xattr_cnt; ++ix) {
+      auto& xattr = attrs->xattrs[ix];
+      buffer::list attr_bl;
+      /* don't allow storing at RGW_ATTR_META_PREFIX */
+      if (! (xattr.key.len > 0))
+       continue;
+
+      /* reject lexical match with any exposed attr */
+      if (is_exposed_attr(xattr.key))
+       continue;
+
+      string k = prefix_xattr_keystr(xattr.key);
+      attr_bl.append(xattr.val.val, xattr.val.len);
+      req.emplace_attr(k.c_str(), std::move(attr_bl));
+    }
+
+    /* don't send null requests */
+    if (! (req.get_attrs().size() > 0)) {
+      return -EINVAL;
+    }
+
+    rc = rgwlib.get_fe()->execute_req(&req);
+    rc2 = req.get_ret();
+
+    return (((rc == 0) && (rc2 == 0)) ? 0 : -EIO);
+
+  } /* RGWLibFS::setxattrs */
+
+  int RGWLibFS::rmxattrs(RGWFileHandle* rgw_fh, rgw_xattrlist* attrs,
+                        uint32_t flags)
+  {
+    /* cannot store on fs_root, should not on buckets? */
+    if ((rgw_fh->is_bucket()) ||
+       (rgw_fh->is_root()))  {
+      return -EINVAL;
+    }
+
+    int rc, rc2;
+    string obj_name{rgw_fh->relative_object_name2()};
+
+    RGWRMAttrsRequest req(cct, rgwlib.get_store()->get_user(user.user_id),
+                         rgw_fh->bucket_name(), obj_name);
+
+    for (uint32_t ix = 0; ix < attrs->xattr_cnt; ++ix) {
+      auto& xattr = attrs->xattrs[ix];
+      /* don't allow storing at RGW_ATTR_META_PREFIX */
+      if (! (xattr.key.len > 0)) {
+       continue;
+      }
+      string k = prefix_xattr_keystr(xattr.key);
+      req.emplace_key(std::move(k));
+    }
+
+    /* don't send null requests */
+    if (! (req.get_attrs().size() > 0)) {
+      return -EINVAL;
+    }
+
+    rc = rgwlib.get_fe()->execute_req(&req);
+    rc2 = req.get_ret();
+
+    return (((rc == 0) && (rc2 == 0)) ? 0 : -EIO);
+
+  } /* RGWLibFS::rmxattrs */
+
+  /* called with rgw_fh->mtx held */
   void RGWLibFS::update_fh(RGWFileHandle *rgw_fh)
   {
     int rc, rc2;
@@ -2447,4 +2709,46 @@ int rgw_commit(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
   return rgw_fh->commit(offset, length, RGWFileHandle::FLAG_NONE);
 }
 
+/*
+  extended attributes
+ */
+
+int rgw_getxattrs(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
+                 rgw_xattrlist *attrs, rgw_getxattr_cb cb, void *cb_arg,
+                 uint32_t flags)
+{
+  RGWLibFS *fs = static_cast<RGWLibFS*>(rgw_fs->fs_private);
+  RGWFileHandle* rgw_fh = get_rgwfh(fh);
+
+  return fs->getxattrs(rgw_fh, attrs, cb, cb_arg, flags);
+}
+
+int rgw_lsxattrs(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
+                rgw_xattrstr *filter_prefix /* ignored */,
+                rgw_getxattr_cb cb, void *cb_arg, uint32_t flags)
+{
+  RGWLibFS *fs = static_cast<RGWLibFS*>(rgw_fs->fs_private);
+  RGWFileHandle* rgw_fh = get_rgwfh(fh);
+
+  return fs->lsxattrs(rgw_fh, filter_prefix, cb, cb_arg, flags);
+}
+
+int rgw_setxattrs(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
+                 rgw_xattrlist *attrs, uint32_t flags)
+{
+  RGWLibFS *fs = static_cast<RGWLibFS*>(rgw_fs->fs_private);
+  RGWFileHandle* rgw_fh = get_rgwfh(fh);
+
+  return fs->setxattrs(rgw_fh, attrs, flags);
+}
+
+int rgw_rmxattrs(struct rgw_fs *rgw_fs, struct rgw_file_handle *fh,
+                rgw_xattrlist *attrs, uint32_t flags)
+{
+  RGWLibFS *fs = static_cast<RGWLibFS*>(rgw_fs->fs_private);
+  RGWFileHandle* rgw_fh = get_rgwfh(fh);
+
+  return fs->rmxattrs(rgw_fh, attrs, flags);
+}
+
 } /* extern "C" */
index c79293e73416de64eca7c5a150d2b49ffa71d4e0..0971e607ff13b4445abebd3f3c30e5eb65c419bb 100644 (file)
@@ -525,6 +525,14 @@ namespace rgw {
       return full_object_name(true /* omit_bucket */);
     }
 
+    inline std::string relative_object_name2() {
+      std::string rname = full_object_name(true /* omit_bucket */);
+      if (is_dir()) {
+       rname += "/";
+      }
+      return rname;
+    }
+
     inline std::string format_child_name(const std::string& cbasename,
                                          bool is_dir) const {
       std::string child_name{relative_object_name()};
@@ -1179,6 +1187,16 @@ namespace rgw {
     int setattr(RGWFileHandle* rgw_fh, struct stat* st, uint32_t mask,
                uint32_t flags);
 
+    int getxattrs(RGWFileHandle* rgw_fh, rgw_xattrlist* attrs,
+                 rgw_getxattr_cb cb, void *cb_arg, uint32_t flags);
+
+    int lsxattrs(RGWFileHandle* rgw_fh, rgw_xattrstr *filter_prefix,
+                rgw_getxattr_cb cb, void *cb_arg, uint32_t flags);
+
+    int setxattrs(RGWFileHandle* rgw_fh, rgw_xattrlist* attrs, uint32_t flags);
+
+    int rmxattrs(RGWFileHandle* rgw_fh, rgw_xattrlist* attrs, uint32_t flags);
+
     void update_fh(RGWFileHandle *rgw_fh);
 
     LookupFHResult stat_bucket(RGWFileHandle* parent, const char *path,
@@ -2663,6 +2681,62 @@ public:
 
 }; /* RGWCopyObjRequest */
 
+class RGWGetAttrsRequest : public RGWLibRequest,
+                          public RGWGetAttrs /* RGWOp */
+{
+public:
+  const std::string& bucket_name;
+  const std::string& obj_name;
+
+  RGWGetAttrsRequest(CephContext* _cct,
+                    std::unique_ptr<rgw::sal::RGWUser> _user,
+                    const std::string& _bname, const std::string& _oname)
+    : RGWLibRequest(_cct, std::move(_user)), RGWGetAttrs(),
+      bucket_name(_bname), obj_name(_oname) {
+    op = this;
+  }
+
+  const flat_map<std::string, std::optional<buffer::list>>& get_attrs() {
+    return attrs;
+  }
+
+  virtual bool only_bucket() { return false; }
+
+  virtual int op_init() {
+    // assign store, s, and dialect_handler
+    RGWObjectCtx* rados_ctx
+      = static_cast<RGWObjectCtx*>(get_state()->obj_ctx);
+    // framework promises to call op_init after parent init
+    assert(rados_ctx);
+    RGWOp::init(rados_ctx->get_store(), get_state(), this);
+    op = this; // assign self as op: REQUIRED
+    return 0;
+  }
+
+  virtual int header_init() {
+
+    struct req_state* s = get_state();
+    s->info.method = "GET";
+    s->op = OP_GET;
+
+    std::string uri = make_uri(bucket_name, obj_name);
+    s->relative_uri = uri;
+    s->info.request_uri = uri;
+    s->info.effective_uri = uri;
+    s->info.request_params = "";
+    s->info.domain = ""; /* XXX ? */
+
+    return 0;
+  }
+
+  virtual int get_params() {
+    return 0;
+  }
+
+  virtual void send_response() {}
+
+}; /* RGWGetAttrsRequest */
+
 class RGWSetAttrsRequest : public RGWLibRequest,
                           public RGWSetAttrs /* RGWOp */
 {
@@ -2676,6 +2750,10 @@ public:
     op = this;
   }
 
+  const std::map<std::string, buffer::list>& get_attrs() {
+    return attrs;
+  }
+
   bool only_bucket() override { return false; }
 
   int op_init() override {
@@ -2714,6 +2792,62 @@ public:
 
 }; /* RGWSetAttrsRequest */
 
+class RGWRMAttrsRequest : public RGWLibRequest,
+                         public RGWRMAttrs /* RGWOp */
+{
+public:
+  const std::string& bucket_name;
+  const std::string& obj_name;
+
+  RGWRMAttrsRequest(CephContext* _cct,
+                    std::unique_ptr<rgw::sal::RGWUser> _user,
+                    const std::string& _bname, const std::string& _oname)
+    : RGWLibRequest(_cct, std::move(_user)), RGWRMAttrs(),
+      bucket_name(_bname), obj_name(_oname) {
+    op = this;
+  }
+
+  const rgw::sal::RGWAttrs& get_attrs() {
+    return attrs;
+  }
+
+  virtual bool only_bucket() { return false; }
+
+  virtual int op_init() {
+    // assign store, s, and dialect_handler
+    RGWObjectCtx* rados_ctx
+      = static_cast<RGWObjectCtx*>(get_state()->obj_ctx);
+    // framework promises to call op_init after parent init
+    assert(rados_ctx);
+    RGWOp::init(rados_ctx->get_store(), get_state(), this);
+    op = this; // assign self as op: REQUIRED
+    return 0;
+  }
+
+  virtual int header_init() {
+
+    struct req_state* s = get_state();
+    s->info.method = "DELETE";
+    s->op = OP_PUT;
+
+    std::string uri = make_uri(bucket_name, obj_name);
+    s->relative_uri = uri;
+    s->info.request_uri = uri;
+    s->info.effective_uri = uri;
+    s->info.request_params = "";
+    s->info.domain = ""; /* XXX ? */
+
+    return 0;
+  }
+
+  virtual int get_params() {
+    return 0;
+  }
+
+  virtual void send_response() {}
+
+}; /* RGWRMAttrsRequest */
+
 /*
  * Send request to get the rados cluster stats
  */
index d7b9f11180ee8e9f7f6d8072a0f8aa151b8efd1b..a031b037bebccfb04a3aba24f10e73e3809a043e 100644 (file)
@@ -1003,7 +1003,7 @@ void RGWGetObjTags::execute(optional_yield y)
 
   s->object->set_atomic(s->obj_ctx);
 
-  op_ret = s->object->get_obj_attrs(s->obj_ctx, s->yield);
+  op_ret = s->object->get_obj_attrs(s->obj_ctx, y);
   if (op_ret < 0) {
     ldpp_dout(this, 0) << "ERROR: failed to get obj attrs, obj=" << s->object
         << " ret=" << op_ret << dendl;
@@ -1052,7 +1052,7 @@ void RGWPutObjTags::execute(optional_yield y)
   }
 
   s->object->set_atomic(s->obj_ctx);
-  op_ret = s->object->modify_obj_attrs(s->obj_ctx, RGW_ATTR_TAGS, tags_bl, s->yield);
+  op_ret = s->object->modify_obj_attrs(s->obj_ctx, RGW_ATTR_TAGS, tags_bl, y);
   if (op_ret == -ECANCELED){
     op_ret = -ERR_TAG_CONFLICT;
   }
@@ -1092,7 +1092,7 @@ void RGWDeleteObjTags::execute(optional_yield y)
   if (rgw::sal::RGWObject::empty(s->object.get()))
     return;
 
-  op_ret = s->object->delete_obj_attrs(s->obj_ctx, RGW_ATTR_TAGS, s->yield);
+  op_ret = s->object->delete_obj_attrs(s->obj_ctx, RGW_ATTR_TAGS, y);
 }
 
 int RGWGetBucketTags::verify_permission(optional_yield y)
@@ -1138,10 +1138,10 @@ void RGWPutBucketTags::execute(optional_yield y)
     ldpp_dout(this, 0) << "forward_request_to_master returned ret=" << op_ret << dendl;
   }
 
-  op_ret = retry_raced_bucket_write(s->bucket.get(), [this] {
+  op_ret = retry_raced_bucket_write(s->bucket.get(), [this, y] {
     rgw::sal::RGWAttrs attrs = s->bucket->get_attrs();
     attrs[RGW_ATTR_TAGS] = tags_bl;
-    return s->bucket->set_instance_attrs(attrs, s->yield);
+    return s->bucket->set_instance_attrs(attrs, y);
   });
 
 }
@@ -1165,10 +1165,10 @@ void RGWDeleteBucketTags::execute(optional_yield y)
     return;
   }
 
-  op_ret = retry_raced_bucket_write(s->bucket.get(), [this] {
+  op_ret = retry_raced_bucket_write(s->bucket.get(), [this, y] {
     rgw::sal::RGWAttrs attrs = s->bucket->get_attrs();
     attrs.erase(RGW_ATTR_TAGS);
-    op_ret = s->bucket->set_instance_attrs(attrs, s->yield);
+    op_ret = s->bucket->set_instance_attrs(attrs, y);
     if (op_ret < 0) {
       ldpp_dout(this, 0) << "RGWDeleteBucketTags() failed to remove RGW_ATTR_TAGS on bucket="
                         << s->bucket->get_name()
@@ -2811,7 +2811,7 @@ void RGWListBucket::execute(optional_yield y)
 
   rgw::sal::RGWBucket::ListResults results;
 
-  op_ret = s->bucket->list(params, max, results, s->yield);
+  op_ret = s->bucket->list(params, max, results, y);
   if (op_ret >= 0) {
     next_marker = results.next_marker;
     is_truncated = results.is_truncated;
@@ -3200,10 +3200,10 @@ void RGWCreateBucket::execute(optional_yield y)
   }
 
   op_ret = store->ctl()->bucket->link_bucket(s->user->get_id(), s->bucket->get_key(),
-                                          s->bucket->get_creation_time(), s->yield, false);
+                                          s->bucket->get_creation_time(), y, false);
   if (op_ret && !existed && op_ret != -EEXIST) {
     /* if it exists (or previously existed), don't remove it! */
-    op_ret = store->ctl()->bucket->unlink_bucket(s->user->get_id(), s->bucket->get_key(), s->yield);
+    op_ret = store->ctl()->bucket->unlink_bucket(s->user->get_id(), s->bucket->get_key(), y);
     if (op_ret < 0) {
       ldpp_dout(this, 0) << "WARNING: failed to unlink bucket: ret=" << op_ret
                       << dendl;
@@ -3221,7 +3221,7 @@ void RGWCreateBucket::execute(optional_yield y)
     do {
       map<string, bufferlist> battrs;
 
-      op_ret = s->bucket->get_bucket_info(s->yield);
+      op_ret = s->bucket->get_bucket_info(y);
       if (op_ret < 0) {
         return;
       } else if (!s->bucket->is_owner(s->user.get())) {
@@ -3258,7 +3258,7 @@ void RGWCreateBucket::execute(optional_yield y)
       /* This will also set the quota on the bucket. */
       op_ret = store->ctl()->bucket->set_bucket_instance_attrs(s->bucket->get_info(), attrs,
                                                            &s->bucket->get_info().objv_tracker,
-                                                           s->yield);
+                                                           y);
     } while (op_ret == -ECANCELED && tries++ < 20);
 
     /* Restore the proper return code. */
@@ -3319,7 +3319,7 @@ void RGWDeleteBucket::execute(optional_yield y)
      ldpp_dout(this, 1) << "WARNING: failed to sync user stats before bucket delete: op_ret= " << op_ret << dendl;
   }
 
-  op_ret = s->bucket->check_empty(s->yield);
+  op_ret = s->bucket->check_empty(y);
   if (op_ret < 0) {
     return;
   }
@@ -3350,8 +3350,8 @@ void RGWDeleteBucket::execute(optional_yield y)
     }
   }
 
-  op_ret = s->bucket->remove_bucket(false, prefix, delimiter, false, nullptr, s->yield);
-
+  op_ret = s->bucket->remove_bucket(false, prefix, delimiter, false, nullptr,
+                                   y);
   if (op_ret < 0 && op_ret == -ECANCELED) {
       // lost a race, either with mdlog sync or another delete bucket operation.
       // in either case, we've already called ctl.bucket->unlink_bucket()
@@ -3406,13 +3406,13 @@ int RGWPutObj::init_processing(optional_yield y) {
     }
     std::unique_ptr<rgw::sal::RGWBucket> bucket;
     ret = store->get_bucket(s->user.get(), copy_source_tenant_name, copy_source_bucket_name,
-                             &bucket, s->yield);
+                             &bucket, y);
     if (ret < 0) {
       ldpp_dout(this, 5) << __func__ << "(): get_bucket() returned ret=" << ret << dendl;
       return ret;
     }
 
-    ret = bucket->get_bucket_info(s->yield);
+    ret = bucket->get_bucket_info(y);
     if (ret < 0) {
       ldpp_dout(this, 5) << __func__ << "(): get_bucket_info() returned ret=" << ret << dendl;
       return ret;
@@ -7267,6 +7267,98 @@ ssize_t RGWBulkUploadOp::AlignedStreamGetter::get_exactly(const size_t want,
   return len;
 }
 
+int RGWGetAttrs::verify_permission(optional_yield y)
+{
+  s->object->set_atomic(s->obj_ctx);
+
+  auto iam_action = s->object->get_instance().empty() ?
+    rgw::IAM::s3GetObject :
+    rgw::IAM::s3GetObjectVersion;
+
+  if (!verify_object_permission(this, s, iam_action)) {
+    return -EACCES;
+  }
+
+  return 0;
+}
+
+void RGWGetAttrs::pre_exec()
+{
+  rgw_bucket_object_pre_exec(s);
+}
+
+void RGWGetAttrs::execute(optional_yield y)
+{
+  op_ret = get_params();
+  if (op_ret < 0)
+    return;
+
+  s->object->set_atomic(s->obj_ctx);
+
+  op_ret = s->object->get_obj_attrs(s->obj_ctx, s->yield);
+  if (op_ret < 0) {
+    ldpp_dout(this, 0) << "ERROR: failed to get obj attrs, obj=" << s->object
+        << " ret=" << op_ret << dendl;
+    return;
+  }
+
+  /* XXX RGWObject::get_obj_attrs() does not support filtering (yet) */
+  auto& obj_attrs = s->object->get_attrs();
+  if (attrs.size() != 0) {
+    /* return only attrs requested */
+    for (auto& att : attrs) {
+      auto iter = obj_attrs.find(att.first);
+      if (iter != obj_attrs.end()) {
+       att.second = iter->second;
+      }
+    }
+  } else {
+    /* return all attrs */
+    for  (auto& att : obj_attrs) {
+      attrs.insert(get_attrs_t::value_type(att.first, att.second));;
+    }
+  }
+
+  return;
+ }
+
+int RGWRMAttrs::verify_permission(optional_yield y)
+{
+  // This looks to be part of the RGW-NFS machinery and has no S3 or
+  // Swift equivalent.
+  bool perm;
+  if (!rgw::sal::RGWObject::empty(s->object.get())) {
+    perm = verify_object_permission_no_policy(this, s, RGW_PERM_WRITE);
+  } else {
+    perm = verify_bucket_permission_no_policy(this, s, RGW_PERM_WRITE);
+  }
+  if (!perm)
+    return -EACCES;
+
+  return 0;
+}
+
+void RGWRMAttrs::pre_exec()
+{
+  rgw_bucket_object_pre_exec(s);
+}
+
+void RGWRMAttrs::execute(optional_yield y)
+{
+  op_ret = get_params();
+  if (op_ret < 0)
+    return;
+
+  s->object->set_atomic(s->obj_ctx);
+
+  op_ret = s->object->set_obj_attrs(s->obj_ctx, nullptr, &attrs, y);
+  if (op_ret < 0) {
+    ldpp_dout(this, 0) << "ERROR: failed to delete obj attrs, obj=" << s->object
+                      << " ret=" << op_ret << dendl;
+  }
+  return;
+}
+
 int RGWSetAttrs::verify_permission(optional_yield y)
 {
   // This looks to be part of the RGW-NFS machinery and has no S3 or
@@ -7296,16 +7388,17 @@ void RGWSetAttrs::execute(optional_yield y)
 
   if (!rgw::sal::RGWObject::empty(s->object.get())) {
     rgw::sal::RGWAttrs a(attrs);
-    op_ret = s->object->set_obj_attrs(s->obj_ctx, &a, nullptr, s->yield);
+    op_ret = s->object->set_obj_attrs(s->obj_ctx, &a, nullptr, y);
   } else {
     for (auto& iter : attrs) {
       s->bucket_attrs[iter.first] = std::move(iter.second);
     }
-    op_ret = store->ctl()->bucket->set_bucket_instance_attrs(s->bucket->get_info(), attrs,
-                                                         &s->bucket->get_info().objv_tracker,
-                                                         s->yield);
+    op_ret = store->ctl()->bucket->set_bucket_instance_attrs(
+      s->bucket->get_info(), attrs, &s->bucket->get_info().objv_tracker,
+      s->yield);
   }
-}
+
+} /* RGWSetAttrs::execute() */
 
 void RGWGetObjLayout::pre_exec()
 {
@@ -7320,14 +7413,14 @@ void RGWGetObjLayout::execute(optional_yield y)
   std::unique_ptr<rgw::sal::RGWObject::ReadOp> stat_op(s->object->get_read_op(s->obj_ctx));
 
 
-  op_ret = stat_op->prepare(s->yield);
+  op_ret = stat_op->prepare(y);
   if (op_ret < 0) {
     return;
   }
 
   head_obj = stat_op->result.head_obj;
 
-  op_ret = stat_op->get_manifest(&manifest, s->yield);
+  op_ret = stat_op->get_manifest(&manifest, y);
 }
 
 
index be1d802827225165c44c301280efb7e487e1c1d6..33ed580782dff30d00f3b7a41f77a40c597463a2 100644 (file)
@@ -25,6 +25,7 @@
 #include <boost/optional.hpp>
 #include <boost/utility/in_place_factory.hpp>
 #include <boost/function.hpp>
+#include <boost/container/flat_map.hpp>
 
 #include "common/armor.h"
 #include "common/mime.h"
@@ -2129,9 +2130,38 @@ inline void complete_etag(MD5& hash, string *etag)
   *etag = etag_buf_str;
 } /* complete_etag */
 
+using boost::container::flat_map;
+
+class RGWGetAttrs : public RGWOp {
+public:
+    using get_attrs_t = flat_map<std::string, std::optional<buffer::list>>;
+protected:
+  get_attrs_t attrs;
+
+public:
+  RGWGetAttrs()
+  {}
+
+  virtual ~RGWGetAttrs() {}
+
+  void emplace_key(std::string&& key) {
+    attrs.emplace(std::move(key), std::nullopt);
+  }
+
+  int verify_permission(optional_yield y);
+  void pre_exec();
+  void execute(optional_yield y);
+
+  virtual int get_params() = 0;
+  virtual void send_response() = 0;
+  virtual const char* name() const { return "get_attrs"; }
+  virtual RGWOpType get_type() { return RGW_OP_GET_ATTRS; }
+  virtual uint32_t op_mask() { return RGW_OP_TYPE_READ; }
+}; /* RGWGetAttrs */
+
 class RGWSetAttrs : public RGWOp {
 protected:
-  map<string, buffer::list> attrs;
+  map<std::string, buffer::list> attrs;
 
 public:
   RGWSetAttrs() {}
@@ -2152,6 +2182,31 @@ public:
   uint32_t op_mask() override { return RGW_OP_TYPE_WRITE; }
 };
 
+class RGWRMAttrs : public RGWOp {
+protected:
+  rgw::sal::RGWAttrs attrs;
+
+public:
+  RGWRMAttrs()
+  {}
+
+  virtual ~RGWRMAttrs() {}
+
+  void emplace_key(std::string&& key) {
+    attrs.emplace(std::move(key), buffer::list());
+  }
+
+  int verify_permission(optional_yield y);
+  void pre_exec();
+  void execute(optional_yield y);
+
+  virtual int get_params() = 0;
+  virtual void send_response() = 0;
+  virtual const char* name() const { return "rm_attrs"; }
+  virtual RGWOpType get_type() { return RGW_OP_DELETE_ATTRS; }
+  virtual uint32_t op_mask() { return RGW_OP_TYPE_DELETE; }
+}; /* RGWRMAttrs */
+
 class RGWGetObjLayout : public RGWOp {
 protected:
   RGWObjManifest *manifest{nullptr};
index 609868d31b5e5aa99dd187784578fd36dc7a51c5..ff474f00728afff4e7a5f0ce23851c34e4b1fe13 100644 (file)
@@ -42,6 +42,9 @@ enum RGWOpType {
   RGW_OP_LIST_BUCKET_MULTIPARTS,
   RGW_OP_DELETE_MULTI_OBJ,
   RGW_OP_BULK_DELETE,
+  RGW_OP_GET_KEYS,
+  RGW_OP_GET_ATTRS,
+  RGW_OP_DELETE_ATTRS,
   RGW_OP_SET_ATTRS,
   RGW_OP_GET_CROSS_DOMAIN_POLICY,
   RGW_OP_GET_HEALTH_CHECK,
index 0e0dab52704c92fc916806cf71da0ff2403ab142..f66087fb087c49537dfe022a2900bf8f0cfe1d02 100644 (file)
@@ -320,6 +320,20 @@ target_link_libraries(ceph_test_librgw_file_marker
   )
   target_link_libraries(ceph_test_librgw_file_marker spawn)
 
+# ceph_test_librgw_file_xattr (attribute ops)
+add_executable(ceph_test_librgw_file_xattr
+  librgw_file_xattr.cc
+  )
+target_include_directories(ceph_test_librgw_file_xattr SYSTEM PRIVATE "${CMAKE_SOURCE_DIR}/src/rgw")
+target_link_libraries(ceph_test_librgw_file_xattr
+  rgw
+  librados
+  ceph-common
+  ${UNITTEST_LIBS}
+  ${EXTRALIBS}
+  )
+target_link_libraries(ceph_test_librgw_file_xattr spawn)
+
 # ceph_test_rgw_token
 add_executable(ceph_test_rgw_token
   test_rgw_token.cc
diff --git a/src/test/librgw_file_xattr.cc b/src/test/librgw_file_xattr.cc
new file mode 100644 (file)
index 0000000..0cf4047
--- /dev/null
@@ -0,0 +1,434 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation.  See file COPYING.
+ *
+ */
+
+#include <stdint.h>
+#include <tuple>
+#include <iostream>
+#include <vector>
+#include <map>
+#include <random>
+#include <boost/algorithm/string.hpp>
+#include "xxhash.h"
+
+#include "include/rados/librgw.h"
+#include "include/rados/rgw_file.h"
+#include "rgw/rgw_file.h"
+
+#include "gtest/gtest.h"
+#include "common/ceph_argparse.h"
+#include "common/errno.h"
+#include "common/debug.h"
+#include "global/global_init.h"
+#include "include/ceph_assert.h"
+
+#define dout_context g_ceph_context
+#define dout_subsys ceph_subsys_rgw
+
+namespace {
+
+  using namespace rgw;
+
+  string uid("testuser");
+  string access_key("");
+  string secret_key("");
+
+  librgw_t rgw_h = nullptr;
+  struct rgw_fs *fs = nullptr;
+
+  uint32_t owner_uid = 867;
+  uint32_t owner_gid = 5309;
+  uint32_t create_mask = RGW_SETATTR_UID | RGW_SETATTR_GID | RGW_SETATTR_MODE;
+
+  string bucket_name{"v4recov"};
+  string object_path{"node0/clientids"};
+
+  string key1{"black"};
+  string val1{"metallic"};
+
+  struct rgw_file_handle *bucket_fh = nullptr;
+  struct rgw_file_handle *object_fh = nullptr;
+
+  typedef std::tuple<string,uint64_t, struct rgw_file_handle*> fid_type;
+  std::vector<fid_type> fids;
+
+  class obj_rec
+  {
+  public:
+    string name;
+    struct rgw_file_handle* fh;
+    struct rgw_file_handle* parent_fh;
+    RGWFileHandle* rgw_fh; // alias into fh
+
+    struct state {
+      bool readdir;
+      state() : readdir(false) {}
+    } state;
+
+    obj_rec(string _name, struct rgw_file_handle* _fh,
+           struct rgw_file_handle* _parent_fh, RGWFileHandle* _rgw_fh)
+      : name(std::move(_name)), fh(_fh), parent_fh(_parent_fh),
+       rgw_fh(_rgw_fh) {}
+
+    void clear() {
+      fh = nullptr;
+      rgw_fh = nullptr;
+    }
+
+    void sync() {
+      if (fh)
+       rgw_fh = get_rgwfh(fh);
+    }
+
+    friend ostream& operator<<(ostream& os, const obj_rec& rec);
+  };
+
+  ostream& operator<<(ostream& os, const obj_rec& rec)
+  {
+    RGWFileHandle* rgw_fh = rec.rgw_fh;
+    if (rgw_fh) {
+      const char* type = rgw_fh->is_dir() ? "DIR " : "FILE ";
+      os << rec.rgw_fh->full_object_name()
+        << " (" << rec.rgw_fh->object_name() << "): "
+        << type;
+    }
+    return os;
+  }
+
+  typedef std::vector<obj_rec> obj_vec;
+  obj_vec ovec;
+
+  bool do_stat = false;
+  bool do_create = false;
+  bool do_delete = false;
+  bool do_hexdump = false;
+  bool verbose = false;
+
+  struct {
+    int argc;
+    char **argv;
+  } saved_args;
+}
+
+TEST(LibRGW, INIT) {
+  int ret = librgw_create(&rgw_h, saved_args.argc, saved_args.argv);
+  ASSERT_EQ(ret, 0);
+  ASSERT_NE(rgw_h, nullptr);
+}
+
+TEST(LibRGW, MOUNT) {
+  int ret = rgw_mount(rgw_h, uid.c_str(), access_key.c_str(),
+                     secret_key.c_str(), &fs, RGW_MOUNT_FLAG_NONE);
+  ASSERT_EQ(ret, 0);
+  ASSERT_NE(fs, nullptr);
+}
+
+TEST(LibRGW, LOOKUP_BUCKET) {
+  int ret = rgw_lookup(fs, fs->root_fh, bucket_name.c_str(), &bucket_fh,
+                      nullptr, 0, RGW_LOOKUP_FLAG_NONE);
+  ASSERT_EQ(ret, 0);
+}
+
+TEST(LibRGW, CREATE_BUCKET) {
+  if ((! bucket_fh) && do_create) {
+    struct stat st;
+
+    st.st_uid = owner_uid;
+    st.st_gid = owner_gid;
+    st.st_mode = 755;
+
+    int ret = rgw_mkdir(fs, fs->root_fh, bucket_name.c_str(), &st, create_mask,
+                       &bucket_fh, RGW_MKDIR_FLAG_NONE);
+    ASSERT_EQ(ret, 0);
+  }
+}
+
+TEST(LibRGW, CREATE_PATH) {
+
+  if (!bucket_fh)
+    return;
+
+  vector<string> segs;
+  boost::split(segs, object_path, boost::is_any_of("/"));
+
+  struct stat st;
+  st.st_uid = owner_uid;
+  st.st_gid = owner_gid;
+  st.st_mode = 755;
+
+  int ix, ret, sz = segs.size();
+  for (ix = 0; ix < sz; ++ix) {
+    auto& seg = segs[ix];
+    struct rgw_file_handle* parent_fh = (ix > 0) ? ovec[ix-1].fh : bucket_fh;
+    obj_rec dir{seg, nullptr, parent_fh, nullptr};
+    if (do_create) {
+      ret = rgw_mkdir(fs, dir.parent_fh, dir.name.c_str(), &st, create_mask,
+                     &dir.fh, RGW_MKDIR_FLAG_NONE);
+    } else {
+      ret = rgw_lookup(fs, dir.parent_fh, dir.name.c_str(), &dir.fh,
+                      nullptr, 0, RGW_LOOKUP_FLAG_NONE);
+    }
+    ASSERT_EQ(ret, 0);
+    dir.sync();
+    ovec.push_back(dir);
+    if (verbose) {
+      std::cout << "create: " << dir.name << std::endl;
+    }
+  }
+}
+
+TEST(LibRGW, CHECK_PATH_REFS) {
+
+  if (!bucket_fh)
+    return;
+
+  int ix, sz = ovec.size();
+  for (ix = 0; ix < sz; ++ix) {
+    auto& dir = ovec[ix];
+    if (verbose) {
+      std::cout << "name: " << dir.name
+               << " refcnt: " << dir.rgw_fh->get_refcnt()
+               << std::endl;
+    }
+    if (ix == 0) {
+      // sentinel, +1 parent, +1 path
+      ASSERT_EQ(dir.rgw_fh->get_refcnt(), 3U);
+    }
+    if (ix == 1) {
+      // sentinel, +1 path
+      ASSERT_EQ(dir.rgw_fh->get_refcnt(), 2U);
+    }
+  }
+}
+
+TEST(LibRGW, SETXATTR1) {
+
+  if (!bucket_fh)
+    return;
+
+  auto& dir = ovec[ovec.size()-1];
+  rgw_xattrstr xattr_k = { const_cast<char*>(key1.c_str()),
+                          uint32_t(key1.length()) };
+  rgw_xattrstr xattr_v = { const_cast<char*>(val1.c_str()),
+                          uint32_t(val1.length()) };
+
+  rgw_xattr xattr = { xattr_k, xattr_v };
+  rgw_xattrlist xattrlist = { &xattr, 1 };
+
+  int ret = rgw_setxattrs(fs, dir.fh, &xattrlist, RGW_SETXATTR_FLAG_NONE);
+  ASSERT_EQ(ret, 0);
+}
+
+extern "C" {
+  static int getattr_cb(rgw_xattrlist *attrs, void *arg, uint32_t flags)
+  {
+    auto& attrmap =
+      *(static_cast<std::map<std::string, std::string>*>(arg));
+    for (uint32_t ix = 0; ix < attrs->xattr_cnt; ++ix) {
+      auto& xattr = attrs->xattrs[ix];
+      string k{xattr.key.val, xattr.key.len};
+      string v{xattr.val.val, xattr.val.len};
+      if (verbose) {
+       std::cout << __func__
+                 << " attr k: " << k << " v: " << v
+                 << std::endl;
+      }
+      attrmap.insert(std::map<std::string, std::string>::value_type(k, v));
+    }
+    return 0;
+  }
+}
+
+TEST(LibRGW, GETXATTR1) {
+
+  if (!bucket_fh)
+    return;
+
+  using std::get;
+  auto& dir = ovec[ovec.size()-1];
+
+  rgw_xattrstr xattr_k1 =
+    {const_cast<char*>(key1.c_str()), uint32_t(key1.length())};
+  rgw_xattrstr xattr_v1 = {nullptr, 0};
+
+  std::string key2 = "user.rgw.etag";
+  rgw_xattrstr xattr_k2 =
+    {const_cast<char*>(key2.c_str()), uint32_t(key2.length())};
+  rgw_xattrstr xattr_v2 = {nullptr, 0};
+
+  rgw_xattr xattrs[2] = {{xattr_k1, xattr_v1},
+                        {xattr_k2, xattr_v2}};
+
+  /* XXX gcc won't accept static_cast here, don't see why */
+  rgw_xattrlist xattrlist = {reinterpret_cast<rgw_xattr*>(&xattrs), 2};
+
+  std::map<std::string, std::string> out_attrmap;
+
+  int ret = rgw_getxattrs(fs, dir.fh, &xattrlist, getattr_cb, &out_attrmap,
+                         RGW_GETXATTR_FLAG_NONE);
+  ASSERT_EQ(ret, 0);
+  /* check exposed attrs */
+  ASSERT_TRUE(out_attrmap.find("user.rgw.etag") != out_attrmap.end());
+  /* check our user attr */
+  ASSERT_TRUE(out_attrmap.find(key1) != out_attrmap.end());
+}
+
+TEST(LibRGW, LSXATTR1) {
+
+  if (!bucket_fh)
+    return;
+
+  using std::get;
+  auto& dir = ovec[ovec.size()-1];
+
+  rgw_xattrstr filter_prefix = { nullptr, 0}; // XXX ignored, for now
+
+  std::map<std::string, std::string> out_attrmap;
+
+  int ret = rgw_lsxattrs(fs, dir.fh, &filter_prefix, getattr_cb,
+                        &out_attrmap, RGW_LSXATTR_FLAG_NONE);
+  ASSERT_EQ(ret, 0);
+
+  /* check exposed attrs */
+  ASSERT_TRUE(out_attrmap.find("user.rgw.etag") != out_attrmap.end());
+}
+
+TEST(LibRGW, RMXATTR1) {
+
+  if (!bucket_fh)
+    return;
+
+  using std::get;
+  auto& dir = ovec[ovec.size()-1];
+
+  rgw_xattrstr xattr_k = { const_cast<char*>(key1.c_str()),
+                          uint32_t(key1.length()) };
+  rgw_xattrstr xattr_v = { nullptr, 0 };
+
+  rgw_xattr xattr = { xattr_k, xattr_v };
+  rgw_xattrlist xattrlist = { &xattr, 1 };
+
+  int ret = rgw_rmxattrs(fs, dir.fh, &xattrlist, RGW_RMXATTR_FLAG_NONE);
+  ASSERT_EQ(ret, 0);
+}
+
+TEST(LibRGW, LSXATTR2) {
+
+  if (!bucket_fh)
+    return;
+
+  using std::get;
+  auto& dir = ovec[ovec.size()-1];
+
+  rgw_xattrstr filter_prefix = { nullptr, 0 }; // XXX ignored, for now
+
+  std::map<std::string, std::string> out_attrmap;
+
+  int ret = rgw_lsxattrs(fs, dir.fh, &filter_prefix, getattr_cb,
+                        &out_attrmap, RGW_LSXATTR_FLAG_NONE);
+  ASSERT_EQ(ret, 0);
+  /* check exposed attrs */
+  ASSERT_TRUE(out_attrmap.find("user.rgw.etag") != out_attrmap.end());
+  /* verify deletion */
+  ASSERT_TRUE(out_attrmap.find(key1) == out_attrmap.end());
+}
+
+TEST(LibRGW, CLEANUP) {
+  int ret = 0;
+  if (object_fh) {
+    ret = rgw_fh_rele(fs, object_fh, 0 /* flags */);
+    ASSERT_EQ(ret, 0);
+  }
+  if (bucket_fh) {
+    ret = rgw_fh_rele(fs, bucket_fh, 0 /* flags */);
+  }
+  ASSERT_EQ(ret, 0);
+}
+
+TEST(LibRGW, UMOUNT) {
+  if (! fs)
+    return;
+
+  int ret = rgw_umount(fs, RGW_UMOUNT_FLAG_NONE);
+  ASSERT_EQ(ret, 0);
+}
+
+TEST(LibRGW, SHUTDOWN) {
+  librgw_shutdown(rgw_h);
+}
+
+int main(int argc, char *argv[])
+{
+  char *v{nullptr};
+  string val;
+  vector<const char*> args;
+
+  argv_to_vec(argc, const_cast<const char**>(argv), args);
+  env_to_vec(args);
+
+  v = getenv("AWS_ACCESS_KEY_ID");
+  if (v) {
+    access_key = v;
+  }
+
+  v = getenv("AWS_SECRET_ACCESS_KEY");
+  if (v) {
+    secret_key = v;
+  }
+
+  for (auto arg_iter = args.begin(); arg_iter != args.end();) {
+    if (ceph_argparse_witharg(args, arg_iter, &val, "--access",
+                             (char*) nullptr)) {
+      access_key = val;
+    } else if (ceph_argparse_witharg(args, arg_iter, &val, "--secret",
+                                    (char*) nullptr)) {
+      secret_key = val;
+    } else if (ceph_argparse_witharg(args, arg_iter, &val, "--uid",
+                                    (char*) nullptr)) {
+      uid = val;
+    } else if (ceph_argparse_witharg(args, arg_iter, &val, "--bn",
+                                    (char*) nullptr)) {
+      bucket_name = val;
+    } else if (ceph_argparse_flag(args, arg_iter, "--stat",
+                                           (char*) nullptr)) {
+      do_stat = true;
+    } else if (ceph_argparse_flag(args, arg_iter, "--create",
+                                           (char*) nullptr)) {
+      do_create = true;
+    } else if (ceph_argparse_flag(args, arg_iter, "--delete",
+                                           (char*) nullptr)) {
+      do_delete = true;
+    } else if (ceph_argparse_flag(args, arg_iter, "--hexdump",
+                                           (char*) nullptr)) {
+      do_hexdump = true;
+    } else if (ceph_argparse_flag(args, arg_iter, "--verbose",
+                                           (char*) nullptr)) {
+      verbose = true;
+    } else {
+      ++arg_iter;
+    }
+  }
+
+  /* dont accidentally run as anonymous */
+  if ((access_key == "") ||
+      (secret_key == "")) {
+    std::cout << argv[0] << " no AWS credentials, exiting" << std::endl;
+    return EPERM;
+  }
+
+  saved_args.argc = argc;
+  saved_args.argv = argv;
+
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}