]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
rgw: implement set account quota for admin REST APIs
authornliu204 <NLIU204@bloomberg.net>
Wed, 10 Sep 2025 21:41:43 +0000 (17:41 -0400)
committernliu204 <NLIU204@bloomberg.net>
Thu, 16 Oct 2025 15:23:31 +0000 (11:23 -0400)
This commit implements the functionality of setting bucket- and account-level quotas with PUT /admin/account

tracker: https://tracker.ceph.com/issues/72527
Signed-off-by: Nicholas Liu <nliu204@bloomberg.net>
src/rgw/rgw_rest_account.cc
src/test/CMakeLists.txt
src/test/test_rgw_admin_account_quota_set.cc [new file with mode: 0644]
src/test/test_rgw_admin_helper.cc [new file with mode: 0644]
src/test/test_rgw_admin_helper.h [new file with mode: 0644]

index 7cfb7b911379a43106e28868fc1b3735cd4bf27b..e57c33eb8d41d624502ca62d5e04efe58c98923f 100644 (file)
@@ -220,6 +220,84 @@ void RGWOp_Account_Delete::execute(optional_yield y)
                                 s->err.message, flusher, y);
 }
 
+class RGWOp_Account_Quota_Set : public RGWRESTOp {
+public:
+  int check_caps(const RGWUserCaps& caps) override {
+    return caps.check_cap("accounts", RGW_CAP_WRITE);
+  }
+
+  void execute(optional_yield y) override;
+
+  const char* name() const override { return "set_account_quota_info"; }
+};
+
+/**
+ * @brief Sets quota limits for an RGW account
+ * 
+ * @param y Optional yield context for coroutine-based async operations
+ * 
+ * REST Endpoint:
+ *   PUT /admin/account
+ * 
+ * Query Parameters:
+ *   - quota: (required) Subresource to trigger the put account quota operation
+ *   - id: (required) Account ID to set quota for
+ *   - quota-type (required): Type of quota to set - "account" or "bucket" 
+ *   - max-size: (optional) Maximum storage size in bytes
+ *   - max-objects: (optional) Maximum number of objects (-1 for unlimited)
+ *   - enabled: (optional) Enable/disable quota enforcement (true/false)
+ * 
+ * Example Usage:
+ *   PUT /admin/account?quota&id=RGW123&quota-type=account&max-size=1073741824&enabled=true
+ * 
+ */
+
+void RGWOp_Account_Quota_Set::execute(optional_yield y)
+{
+  bufferlist data;
+  op_ret = rgw_forward_request_to_master(this, *s->penv.site, s->user->get_id(),
+                                         &data, nullptr, s->info, s->err, y);
+  if (op_ret < 0) {
+    ldpp_dout(this, 0) << "forward_request_to_master returned ret=" << op_ret << dendl;
+    return;
+  }
+
+  rgw::account::AdminOpState op_state;
+  bool has_account_id = false;
+  RESTArgs::get_string(s, "id", "", &op_state.account_id, &has_account_id);
+  bool has_quota_scope = false;
+  RESTArgs::get_string(s, "quota-type", "", &op_state.quota_scope, &has_quota_scope);
+
+  if (!has_account_id || !has_quota_scope || (op_state.quota_scope != "account" && op_state.quota_scope != "bucket")) {
+    op_ret = -EINVAL;
+    return;
+  }
+
+  int32_t quota_max_size = 0;
+  bool has_quota_max_size = false;
+  RESTArgs::get_int32(s, "max-size", 0, &quota_max_size, &has_quota_max_size);
+  if (has_quota_max_size) {
+    op_state.quota_max_size = quota_max_size;
+  }
+
+  int32_t quota_max_objects = 0;
+  bool has_quota_max_objects = false;
+  RESTArgs::get_int32(s, "max-objects", 0, &quota_max_objects, &has_quota_max_objects);
+  if (has_quota_max_objects) {
+    op_state.quota_max_objects = quota_max_objects;
+  }
+
+  bool quota_enabled = false;
+  bool has_quota_enabled = false;
+  RESTArgs::get_bool(s, "enabled", false, &quota_enabled, &has_quota_enabled);
+  if (has_quota_enabled) {
+    op_state.quota_enabled = quota_enabled;
+  }
+
+  op_ret = rgw::account::modify(this, driver, op_state,
+                                s->err.message, flusher, y);
+}
+
 RGWOp* RGWHandler_Account::op_post()
 {
   return new RGWOp_Account_Create;
@@ -227,6 +305,8 @@ RGWOp* RGWHandler_Account::op_post()
 
 RGWOp* RGWHandler_Account::op_put()
 {
+  if (s->info.args.sub_resource_exists("quota"))
+    return new RGWOp_Account_Quota_Set;
   return new RGWOp_Account_Modify;
 }
 
index a22c7ccb236f07fc7618547cce7eeb9ff6a9e17d..3d85003de6f967d0c4fe68b258c7437ab88d039b 100644 (file)
@@ -225,6 +225,33 @@ if(${WITH_RADOSGW})
     ${EXPAT_LIBRARIES}
     ${CMAKE_DL_LIBS} ${UNITTEST_LIBS})
 
+  # ceph_test_cls_rgw_admin_account_quota_set
+  set(test_cls_rgw_admin_account_quota_set_srcs 
+    test_rgw_admin_account_quota_set.cc
+    test_rgw_admin_helper.cc
+  )
+  add_executable(ceph_test_cls_rgw_admin_account_quota_set
+    ${test_cls_rgw_admin_account_quota_set_srcs}
+    )
+  target_link_libraries(ceph_test_cls_rgw_admin_account_quota_set
+    librados
+    ${rgw_libs}
+    global
+    cls_version_client
+    cls_log_client
+    cls_refcount_client
+    cls_rgw_client
+    cls_user_client
+    cls_lock_client
+    ${BLKID_LIBRARIES}
+    ${CURL_LIBRARIES}
+    ${EXPAT_LIBRARIES}
+    ${CMAKE_DL_LIBS} ${UNITTEST_LIBS} ${CRYPTO_LIBS})
+
+  install(TARGETS
+    ceph_test_cls_rgw_admin_account_quota_set
+    DESTINATION ${CMAKE_INSTALL_BINDIR})
+
   # ceph_test_cls_rgw_meta
   set(test_cls_rgw_meta_srcs test_rgw_admin_meta.cc)
   add_executable(ceph_test_cls_rgw_meta
diff --git a/src/test/test_rgw_admin_account_quota_set.cc b/src/test/test_rgw_admin_account_quota_set.cc
new file mode 100644 (file)
index 0000000..dfc448e
--- /dev/null
@@ -0,0 +1,217 @@
+// -*- 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) 2013 eNovance SAS <licensing@enovance.com>
+ *
+ * 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 "common/Finisher.h"
+#include "common/ceph_argparse.h"
+#include "global/global_init.h"
+#include <gtest/gtest.h>
+#include "test_rgw_admin_helper.h"
+
+
+using namespace std;
+using admin_helper::g_test;
+
+int default_quota_max = -1;
+string account_id = "RGW00000000000000001";
+string account_name = CEPH_UID;
+string uid = CEPH_UID;
+string display_name = "CEPH";
+
+TEST(TestRGWAdmin, account_quota_put_accounts_no_access){
+  JSONParser parser;
+  RGWUserInfo uinfo;
+  RGWAccountInfo ainfo;
+  string request_params = "id=" + account_id + "&max-size=999&max-objects=999&enabled=true";
+
+  ASSERT_EQ(0, admin_helper::account_create(account_id, account_name));
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  ASSERT_EQ(0, admin_helper::user_create(uid, display_name, true, account_id, false));
+  ASSERT_EQ(0, admin_helper::user_info(uid, display_name, uinfo));
+
+  // assert default values
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_size);
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_objects);
+  EXPECT_FALSE(ainfo.quota.enabled);
+
+  // assert a user with no access gets 403 unauthorized
+  g_test->send_request(string("PUT"), "/admin/account?quota&quota-type=account&" + request_params);
+  EXPECT_EQ(403U, g_test->get_resp_code());
+
+  ASSERT_EQ(0, admin_helper::user_rm(uid, display_name));
+  ASSERT_EQ(0, admin_helper::account_rm(account_id));
+}
+
+TEST(TestRGWAdmin, account_quota_put){
+  JSONParser parser;
+  RGWUserInfo uinfo;
+  RGWAccountInfo ainfo;
+
+  string request_params = "id=" + account_id + "&max-size=999&max-objects=999&enabled=true";
+
+  ASSERT_EQ(0, admin_helper::account_create(account_id, account_name));
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  ASSERT_EQ(0, admin_helper::user_create(uid, display_name, true, account_id, true));
+  ASSERT_EQ(0, admin_helper::user_info(uid, display_name, uinfo));
+
+  // assert default values
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_size);
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_objects);
+  EXPECT_FALSE(ainfo.quota.enabled);
+
+  g_test->send_request(string("PUT"), "/admin/account?quota&quota-type=account&" + request_params);
+  EXPECT_EQ(200U, g_test->get_resp_code());
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  // assert quotas are set correctly at the account level
+  EXPECT_EQ(999U, ainfo.quota.max_size);
+  EXPECT_EQ(999U, ainfo.quota.max_objects);
+  EXPECT_TRUE(ainfo.quota.enabled);
+
+  // assert bucket quota remains the default
+  EXPECT_EQ(default_quota_max, ainfo.bucket_quota.max_size);
+  EXPECT_EQ(default_quota_max, ainfo.bucket_quota.max_objects);
+  EXPECT_FALSE(ainfo.bucket_quota.enabled);
+
+  g_test->send_request(string("PUT"), "/admin/account?quota&quota-type=bucket&" + request_params);
+  EXPECT_EQ(200U, g_test->get_resp_code());
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  // assert quotas are set correctly at the bucket level
+  EXPECT_EQ(999U, ainfo.bucket_quota.max_size);
+  EXPECT_EQ(999U, ainfo.bucket_quota.max_objects);
+  EXPECT_TRUE(ainfo.bucket_quota.enabled);
+
+  ASSERT_EQ(0, admin_helper::user_rm(uid, display_name));
+  ASSERT_EQ(0, admin_helper::account_rm(account_id));
+}
+
+TEST(TestRGWAdmin, account_quota_put_partial){
+  JSONParser parser;
+  RGWUserInfo uinfo;
+  RGWAccountInfo ainfo;
+
+  ASSERT_EQ(0, admin_helper::account_create(account_id, account_name));
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  ASSERT_EQ(0, admin_helper::user_create(uid, display_name, true, account_id, true));
+  ASSERT_EQ(0, admin_helper::user_info(uid, display_name, uinfo));
+
+  // assert default values
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_size);
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_objects);
+  EXPECT_FALSE(ainfo.quota.enabled);
+
+  // assert not having anything changed for account quota (max-objects, etc) maintains the default values
+  g_test->send_request(string("PUT"), "/admin/account?quota&quota-type=account&id=" + account_id);
+  EXPECT_EQ(200U, g_test->get_resp_code());
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_size);
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_objects);
+  EXPECT_FALSE(ainfo.quota.enabled);
+
+  // assert having one field changed leaves the others unchanged
+  g_test->send_request(string("PUT"), "/admin/account?quota&quota-type=account&id=" + account_id + "&max-size=100");
+  EXPECT_EQ(200U, g_test->get_resp_code());
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  EXPECT_EQ(100U, ainfo.quota.max_size);
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_objects);
+  EXPECT_FALSE(ainfo.quota.enabled);
+
+  // assert not having anything changed for bucket quota (max-objects, etc) maintains the default values
+  g_test->send_request(string("PUT"), "/admin/account?quota&quota-type=bucket&id=" + account_id);
+  EXPECT_EQ(200U, g_test->get_resp_code());
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  EXPECT_EQ(default_quota_max, ainfo.bucket_quota.max_size);
+  EXPECT_EQ(default_quota_max, ainfo.bucket_quota.max_objects);
+  EXPECT_FALSE(ainfo.bucket_quota.enabled);
+
+  // assert having one field changed leaves the others unchanged
+  g_test->send_request(string("PUT"), "/admin/account?quota&quota-type=account&id=" + account_id + "&enabled=true");
+  EXPECT_EQ(200U, g_test->get_resp_code());
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  EXPECT_EQ(default_quota_max, ainfo.bucket_quota.max_size);
+  EXPECT_EQ(default_quota_max, ainfo.bucket_quota.max_objects);
+  EXPECT_TRUE(ainfo.quota.enabled);
+
+  ASSERT_EQ(0, admin_helper::user_rm(uid, display_name));
+  ASSERT_EQ(0, admin_helper::account_rm(account_id));
+}
+
+TEST(TestRGWAdmin, account_quota_put_invalid_args){
+  JSONParser parser;
+  RGWUserInfo uinfo;
+  RGWAccountInfo ainfo;
+
+  ASSERT_EQ(0, admin_helper::account_create(account_id, account_name));
+  ASSERT_EQ(0, admin_helper::account_info(account_id, ainfo));
+
+  ASSERT_EQ(0, admin_helper::user_create(uid, display_name, true, account_id, true));
+  ASSERT_EQ(0, admin_helper::user_info(uid, display_name, uinfo));
+
+  // assert default values
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_size);
+  EXPECT_EQ(default_quota_max, ainfo.quota.max_objects);
+  EXPECT_FALSE(ainfo.quota.enabled);
+
+  // assert trying to set quota with no quota-type returns a 400 error
+  g_test->send_request(string("PUT"), "/admin/account?quota&id=" + account_id);
+  EXPECT_EQ(400U, g_test->get_resp_code());
+
+  // assert trying to set quota with invalid quota type returns a 400 error
+  g_test->send_request(string("PUT"), "/admin/account?quota&quota-type=invalid&id=" + account_id);
+  EXPECT_EQ(400U, g_test->get_resp_code());
+
+  // assert trying to set quota with no account id returns a 400 error
+  g_test->send_request(string("PUT"), "/admin/account?quota&quota-type=account");
+  EXPECT_EQ(400U, g_test->get_resp_code());
+
+  ASSERT_EQ(0, admin_helper::user_rm(uid, display_name));
+  ASSERT_EQ(0, admin_helper::account_rm(account_id));
+}
+
+int main(int argc, char *argv[]){
+  auto args = argv_to_vec(argc, argv);
+
+  auto cct = global_init(NULL, args, CEPH_ENTITY_TYPE_CLIENT,
+                        CODE_ENVIRONMENT_UTILITY,
+                        CINIT_FLAG_NO_DEFAULT_CONFIG_FILE);
+  common_init_finish(g_ceph_context);
+  g_test = new admin_helper::test_helper();
+  Finisher *finisher = new Finisher(g_ceph_context);
+#ifdef GTEST
+  ::testing::InitGoogleTest(&argc, argv);
+#endif
+  finisher->start();
+
+  if(g_test->extract_input(argc, argv) < 0){
+    admin_helper::print_usage(argv[0]);
+    return -1;
+  }
+#ifdef GTEST
+  int r = RUN_ALL_TESTS();
+  if (r == 0) {
+    cout << "There are no failures in the test case\n";
+  } else {
+    cout << "There are some failures\n";
+  }
+#endif
+  finisher->stop();
+  return 0;
+}
\ No newline at end of file
diff --git a/src/test/test_rgw_admin_helper.cc b/src/test/test_rgw_admin_helper.cc
new file mode 100644 (file)
index 0000000..114ffd3
--- /dev/null
@@ -0,0 +1,642 @@
+// -*- 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) 2013 eNovance SAS <licensing@enovance.com>
+ *
+ * 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 <fstream>
+#include <iostream>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include "common/ceph_crypto.h"
+#include "common/ceph_json.h"
+#include "common/code_environment.h"
+#include "common/armor.h"
+#include "include/str_list.h"
+#include "test_rgw_admin_helper.h"
+
+using namespace std;
+
+namespace admin_helper
+{
+    test_helper *g_test;
+
+    void print_usage(char *exec)
+    {
+        cout << "Usage: " << exec << " <Options>\n";
+        cout << "Options:\n"
+                "-g <gw-ip> - The ip address of the gateway\n"
+                "-p <gw-port> - The port number of the gateway\n"
+                "-c <ceph.conf> - Absolute path of ceph config file\n"
+                "-rgw-admin <path/to/radosgw-admin> - radosgw-admin absolute path\n";
+    }
+
+    test_helper::test_helper() : curl_inst(0), resp_data(NULL), resp_code(0)
+    {
+        curl_global_init(CURL_GLOBAL_ALL);
+    }
+
+    test_helper::~test_helper()
+    {
+        curl_global_cleanup();
+    }
+
+    int test_helper::send_request(string method, string res,
+                                  size_t (*read_function)(void *, size_t, size_t, void *),
+                                  void *ud,
+                                  size_t length)
+    {
+        string url;
+        string auth, date;
+        url.append(string("http://") + host);
+        if (port.length() > 0)
+            url.append(string(":") + port);
+        url.append(res);
+        curl_inst = curl_easy_init();
+        if (curl_inst)
+        {
+            curl_easy_setopt(curl_inst, CURLOPT_URL, url.c_str());
+            curl_easy_setopt(curl_inst, CURLOPT_CUSTOMREQUEST, method.c_str());
+            curl_easy_setopt(curl_inst, CURLOPT_VERBOSE, CURL_VERBOSE);
+            curl_easy_setopt(curl_inst, CURLOPT_HEADERFUNCTION, admin_helper::write_header);
+            curl_easy_setopt(curl_inst, CURLOPT_WRITEHEADER, (void *)this);
+            curl_easy_setopt(curl_inst, CURLOPT_WRITEFUNCTION, admin_helper::write_data);
+            curl_easy_setopt(curl_inst, CURLOPT_WRITEDATA, (void *)this);
+            if (read_function)
+            {
+                curl_easy_setopt(curl_inst, CURLOPT_READFUNCTION, read_function);
+                curl_easy_setopt(curl_inst, CURLOPT_READDATA, (void *)ud);
+                curl_easy_setopt(curl_inst, CURLOPT_UPLOAD, 1L);
+                curl_easy_setopt(curl_inst, CURLOPT_INFILESIZE_LARGE, (curl_off_t)length);
+            }
+
+            get_date(date);
+            string http_date;
+            http_date.append(string("Date: ") + date);
+
+            string s3auth;
+            if (admin_helper::get_s3_auth(method, creds, date, res, s3auth) < 0)
+                return -1;
+            auth.append(string("Authorization: AWS ") + s3auth);
+
+            struct curl_slist *slist = NULL;
+            slist = curl_slist_append(slist, auth.c_str());
+            slist = curl_slist_append(slist, http_date.c_str());
+            for (list<string>::iterator it = extra_hdrs.begin();
+                 it != extra_hdrs.end(); ++it)
+            {
+                slist = curl_slist_append(slist, (*it).c_str());
+            }
+            if (read_function)
+                curl_slist_append(slist, "Expect:");
+            curl_easy_setopt(curl_inst, CURLOPT_HTTPHEADER, slist);
+
+            response.erase(response.begin(), response.end());
+            extra_hdrs.erase(extra_hdrs.begin(), extra_hdrs.end());
+            CURLcode res = curl_easy_perform(curl_inst);
+            if (res != CURLE_OK)
+            {
+                cout << "Curl perform failed for " << url << ", res: " << curl_easy_strerror(res) << "\n";
+                return -1;
+            }
+            curl_slist_free_all(slist);
+        }
+        curl_easy_cleanup(curl_inst);
+        return 0;
+    }
+
+    string &test_helper::get_response(string hdr)
+    {
+        return response[hdr];
+    }
+
+    void test_helper::set_extra_header(string hdr)
+    {
+        extra_hdrs.push_back(hdr);
+    }
+
+    void test_helper::set_response(char *r)
+    {
+        string sr(r), h, v;
+        size_t off = sr.find(": ");
+        if (off != string::npos)
+        {
+            h.assign(sr, 0, off);
+            v.assign(sr, off + 2, sr.find("\r\n") - (off + 2));
+        }
+        else
+        {
+            /*Could be the status code*/
+            if (sr.find("HTTP/") != string::npos)
+            {
+                h.assign(HTTP_RESPONSE_STR);
+                off = sr.find(" ");
+                v.assign(sr, off + 1, sr.find("\r\n") - (off + 1));
+                resp_code = atoi((v.substr(0, 3)).c_str());
+            }
+        }
+        response[h] = v;
+    }
+
+    void test_helper::set_response_data(char *data, size_t len)
+    {
+        if (resp_data)
+            delete resp_data;
+        resp_data = new string(data, len);
+    }
+    string &test_helper::get_rgw_admin_path()
+    {
+        return rgw_admin_path;
+    }
+    string &test_helper::get_ceph_conf_path()
+    {
+        return conf_path;
+    }
+    void test_helper::set_creds(string &c)
+    {
+        creds = c;
+    }
+    const string *test_helper::get_response_data() { return resp_data; }
+    unsigned test_helper::get_resp_code() { return resp_code; }
+
+    int test_helper::extract_input(int argc, char *argv[])
+    {
+#define ERR_CHECK_NEXT_PARAM(o)  \
+    if (((int)loop + 1) >= argc) \
+        return -1;               \
+    else                         \
+        o = argv[loop + 1];
+
+        for (unsigned loop = 1; loop < (unsigned)argc; loop += 2)
+        {
+            if (strcmp(argv[loop], "-g") == 0)
+            {
+                ERR_CHECK_NEXT_PARAM(host);
+            }
+            else if (strcmp(argv[loop], "-p") == 0)
+            {
+                ERR_CHECK_NEXT_PARAM(port);
+            }
+            else if (strcmp(argv[loop], "-c") == 0)
+            {
+                ERR_CHECK_NEXT_PARAM(conf_path);
+            }
+            else if (strcmp(argv[loop], "-rgw-admin") == 0)
+            {
+                ERR_CHECK_NEXT_PARAM(rgw_admin_path);
+            }
+            else
+                return -1;
+        }
+        if (host.empty() || rgw_admin_path.empty())
+            return -1;
+        return 0;
+    }
+
+    size_t write_header(void *ptr, size_t size, size_t nmemb, void *ud)
+    {
+        test_helper *h = static_cast<test_helper *>(ud);
+        h->set_response((char *)ptr);
+        return size * nmemb;
+    }
+
+    size_t write_data(void *ptr, size_t size, size_t nmemb, void *ud)
+    {
+        test_helper *h = static_cast<test_helper *>(ud);
+        h->set_response_data((char *)ptr, size * nmemb);
+        return size * nmemb;
+    }
+
+    inline void buf_to_hex(const unsigned char *buf, int len, char *str)
+    {
+        int i;
+        str[0] = '\0';
+        for (i = 0; i < len; i++)
+        {
+            sprintf(&str[i * 2], "%02x", (int)buf[i]);
+        }
+    }
+
+    void calc_hmac_sha1(const char *key, int key_len,
+                        const char *msg, int msg_len, char *dest)
+    /* destination should be CEPH_CRYPTO_HMACSHA1_DIGESTSIZE bytes long */
+    {
+        ceph::crypto::HMACSHA1 hmac((const unsigned char *)key, key_len);
+        hmac.Update((const unsigned char *)msg, msg_len);
+        hmac.Final((unsigned char *)dest);
+
+        char hex_str[(CEPH_CRYPTO_HMACSHA1_DIGESTSIZE * 2) + 1];
+        admin_helper::buf_to_hex((unsigned char *)dest, CEPH_CRYPTO_HMACSHA1_DIGESTSIZE, hex_str);
+    }
+
+    int get_s3_auth(const string &method, string creds, const string &date, string res, string &out)
+    {
+        string aid, secret, auth_hdr;
+        string tmp_res;
+        size_t off = creds.find(":");
+        out = "";
+        if (off != string::npos)
+        {
+            aid.assign(creds, 0, off);
+            secret.assign(creds, off + 1, string::npos);
+
+            /*sprintf(auth_hdr, "%s\n\n\n%s\n%s", req_type, date, res);*/
+            char hmac_sha1[CEPH_CRYPTO_HMACSHA1_DIGESTSIZE];
+            char b64[65]; /* 64 is really enough */
+            size_t off = res.find("?");
+            if (off == string::npos)
+                tmp_res = res;
+            else
+                tmp_res.assign(res, 0, off);
+            auth_hdr.append(method + string("\n\n\n") + date + string("\n") + tmp_res);
+            admin_helper::calc_hmac_sha1(secret.c_str(), secret.length(),
+                                         auth_hdr.c_str(), auth_hdr.length(), hmac_sha1);
+            int ret = ceph_armor(b64, b64 + 64, hmac_sha1,
+                                 hmac_sha1 + CEPH_CRYPTO_HMACSHA1_DIGESTSIZE);
+            if (ret < 0)
+            {
+                cout << "ceph_armor failed\n";
+                return -1;
+            }
+            b64[ret] = 0;
+            out.append(aid + string(":") + b64);
+        }
+        else
+            return -1;
+        return 0;
+    }
+
+    void get_date(string &d)
+    {
+        struct timeval tv;
+        char date[64];
+        struct tm tm;
+        char *days[] = {(char *)"Sun", (char *)"Mon", (char *)"Tue",
+                        (char *)"Wed", (char *)"Thu", (char *)"Fri",
+                        (char *)"Sat"};
+        char *months[] = {(char *)"Jan", (char *)"Feb", (char *)"Mar",
+                          (char *)"Apr", (char *)"May", (char *)"Jun",
+                          (char *)"Jul", (char *)"Aug", (char *)"Sep",
+                          (char *)"Oct", (char *)"Nov", (char *)"Dec"};
+        gettimeofday(&tv, NULL);
+        gmtime_r(&tv.tv_sec, &tm);
+        sprintf(date, "%s, %d %s %d %d:%d:%d GMT",
+                days[tm.tm_wday],
+                tm.tm_mday, months[tm.tm_mon],
+                tm.tm_year + 1900,
+                tm.tm_hour, tm.tm_min, 0 /*tm.tm_sec*/);
+        d = date;
+    }
+
+    int run_rgw_admin(string &cmd, string &resp)
+    {
+        pid_t pid;
+        pid = fork();
+        if (pid == 0)
+        {
+            /* child */
+            list<string> l;
+            get_str_list(cmd, " \t", l);
+            char *argv[l.size()];
+            unsigned loop = 1;
+
+            argv[0] = (char *)"radosgw-admin";
+            for (list<string>::iterator it = l.begin();
+                 it != l.end(); ++it)
+            {
+                argv[loop++] = (char *)(*it).c_str();
+            }
+            argv[loop] = NULL;
+            if (!freopen(RGW_ADMIN_RESP_PATH, "w+", stdout))
+            {
+                cout << "Unable to open stdout file" << std::endl;
+            }
+            execv((g_test->get_rgw_admin_path()).c_str(), argv);
+        }
+        else if (pid > 0)
+        {
+            int status;
+            waitpid(pid, &status, 0);
+            if (WIFEXITED(status))
+            {
+                if (WEXITSTATUS(status) != 0)
+                {
+                    cout << "Child exited with status " << WEXITSTATUS(status) << std::endl;
+                    return -1;
+                }
+            }
+            ifstream in;
+            struct stat st;
+
+            if (stat(RGW_ADMIN_RESP_PATH, &st) < 0)
+            {
+                cout << "Error stating the admin response file, errno " << errno << std::endl;
+                return -1;
+            }
+            else
+            {
+                char *data = (char *)malloc(st.st_size + 1);
+                in.open(RGW_ADMIN_RESP_PATH);
+                in.read(data, st.st_size);
+                in.close();
+                data[st.st_size] = 0;
+                resp = data;
+                free(data);
+                unlink(RGW_ADMIN_RESP_PATH);
+                /* cout << "radosgw-admin " << cmd << ": " << resp << std::endl;*/
+            }
+        }
+        else
+            return -1;
+        return 0;
+    }
+
+    int get_creds(string &json, string &creds)
+    {
+        JSONParser parser;
+        if (!parser.parse(json.c_str(), json.length()))
+        {
+            cout << "Error parsing create user response" << std::endl;
+            return -1;
+        }
+
+        RGWUserInfo info;
+        decode_json_obj(info, &parser);
+        creds = "";
+        for (map<string, RGWAccessKey>::iterator it = info.access_keys.begin();
+             it != info.access_keys.end(); ++it)
+        {
+            RGWAccessKey _k = it->second;
+            /*cout << "accesskeys [ " << it->first << " ] = " <<
+              "{ " << _k.id << ", " << _k.key << ", " << _k.subuser << "}" << std::endl;*/
+            creds.append(it->first + string(":") + _k.key);
+            break;
+        }
+        return 0;
+    }
+
+    int account_create(string &account_id, string &account_name)
+    {
+        stringstream ss;
+        ss << "-c " << g_test->get_ceph_conf_path() << " account create --account-id=" << account_id << " --account-name=" << account_name;
+        string cmd = ss.str();
+        string out;
+        if (run_rgw_admin(cmd, out) != 0)
+        {
+            cout << "Error creating account" << std::endl;
+            return -1;
+        }
+        return 0;
+    }
+
+    int user_create(string &uid, string &display_name, bool set_creds, const string &account_id, bool is_admin)
+    {
+        stringstream ss;
+        string creds;
+        ss << "-c " << g_test->get_ceph_conf_path() << " user create --uid=" << uid
+           << " --display-name=" << display_name;
+        if (!account_id.empty()){
+            ss << " --account-id=" << account_id;
+        }
+        if (is_admin)
+        {
+            ss << " --admin";
+        }
+        string out;
+        string cmd = ss.str();
+        if (run_rgw_admin(cmd, out) != 0)
+        {
+            cout << "Error creating user" << std::endl;
+            return -1;
+        }
+        get_creds(out, creds);
+        if (set_creds)
+            g_test->set_creds(creds);
+        return 0;
+    }
+
+    int account_info(string &account_id, RGWAccountInfo &a_info)
+    {
+        stringstream ss;
+        ss << "-c " << g_test->get_ceph_conf_path() << " account get --account-id=" << account_id;
+        string cmd = ss.str();
+        string out;
+        if (run_rgw_admin(cmd, out) != 0)
+        {
+            cout << "Error reading account information" << std::endl;
+            return -1;
+        }
+        JSONParser parser;
+        if (!parser.parse(out.c_str(), out.length()))
+        {
+            cout << "Error parsing account info response" << std::endl;
+            return -1;
+        }
+        decode_json_obj(a_info, &parser);
+        return 0;
+    }
+
+    int user_info(string &uid, string &display_name, RGWUserInfo &uinfo)
+    {
+        stringstream ss;
+        ss << "-c " << g_test->get_ceph_conf_path() << " user info --uid=" << uid
+           << " --display-name=" << display_name;
+
+        string out;
+        string cmd = ss.str();
+        if (run_rgw_admin(cmd, out) != 0)
+        {
+            cout << "Error reading user information" << std::endl;
+            return -1;
+        }
+        JSONParser parser;
+        if (!parser.parse(out.c_str(), out.length()))
+        {
+            cout << "Error parsing create user response" << std::endl;
+            return -1;
+        }
+        decode_json_obj(uinfo, &parser);
+        return 0;
+    }
+
+    int account_rm(string &account_id)
+    {
+        stringstream ss;
+        ss << "-c " << g_test->get_ceph_conf_path() << " account rm --account-id=" << account_id;
+        string cmd = ss.str();
+        string out;
+        if (run_rgw_admin(cmd, out) != 0)
+        {
+            cout << "Error removing account" << std::endl;
+            return -1;
+        }
+        return 0;
+    }
+
+    int user_rm(string &uid, string &display_name)
+    {
+        stringstream ss;
+        ss << "-c " << g_test->get_ceph_conf_path() << " user rm --uid=" << uid
+           << " --display-name=" << display_name;
+
+        string out;
+        string cmd = ss.str();
+        if (run_rgw_admin(cmd, out) != 0)
+        {
+            cout << "Error removing user" << std::endl;
+            return -1;
+        }
+        return 0;
+    }
+
+    int caps_add(const string caps_name, const string uid, const char *perm)
+    {
+        stringstream ss;
+
+        ss << "-c " << g_test->get_ceph_conf_path() << " caps add --caps=" << caps_name << "=" << perm << " --uid=" << uid;
+        string out;
+        string cmd = ss.str();
+        if (run_rgw_admin(cmd, out) != 0)
+        {
+            cout << "Error adding caps to user" << std::endl;
+            return -1;
+        }
+        return 0;
+    }
+
+    int caps_rm(const string caps_name, const string uid, const char *perm)
+    {
+        stringstream ss;
+
+        ss << "-c " << g_test->get_ceph_conf_path() << " caps rm --caps=" << caps_name << "=" << perm << " --uid=" << uid;
+        string out;
+        string cmd = ss.str();
+        if (run_rgw_admin(cmd, out) != 0)
+        {
+            cout << "Error removing caps from user" << std::endl;
+            return -1;
+        }
+        return 0;
+    }
+
+    int compare_access_keys(RGWAccessKey &k1, RGWAccessKey &k2)
+    {
+        if (k1.id.compare(k2.id) != 0)
+            return -1;
+        if (k1.key.compare(k2.key) != 0)
+            return -1;
+        if (k1.subuser.compare(k2.subuser) != 0)
+            return -1;
+
+        return 0;
+    }
+
+    int compare_user_info(RGWUserInfo &i1, RGWUserInfo &i2, const string meta_caps)
+    {
+        int rv;
+
+        if ((rv = i1.user_id.id.compare(i2.user_id.id)) != 0)
+            return rv;
+        if ((rv = i1.display_name.compare(i2.display_name)) != 0)
+            return rv;
+        if ((rv = i1.user_email.compare(i2.user_email)) != 0)
+            return rv;
+        if (i1.access_keys.size() != i2.access_keys.size())
+            return -1;
+        for (map<string, RGWAccessKey>::iterator it = i1.access_keys.begin();
+             it != i1.access_keys.end(); ++it)
+        {
+            RGWAccessKey k1, k2;
+            k1 = it->second;
+            if (i2.access_keys.count(it->first) == 0)
+                return -1;
+            k2 = i2.access_keys[it->first];
+            if (compare_access_keys(k1, k2) != 0)
+                return -1;
+        }
+        if (i1.swift_keys.size() != i2.swift_keys.size())
+            return -1;
+        for (map<string, RGWAccessKey>::iterator it = i1.swift_keys.begin();
+             it != i1.swift_keys.end(); ++it)
+        {
+            RGWAccessKey k1, k2;
+            k1 = it->second;
+            if (i2.swift_keys.count(it->first) == 0)
+                return -1;
+            k2 = i2.swift_keys[it->first];
+            if (compare_access_keys(k1, k2) != 0)
+                return -1;
+        }
+        if (i1.subusers.size() != i2.subusers.size())
+            return -1;
+        for (map<string, RGWSubUser>::iterator it = i1.subusers.begin();
+             it != i1.subusers.end(); ++it)
+        {
+            RGWSubUser k1, k2;
+            k1 = it->second;
+            if (!i2.subusers.count(it->first))
+                return -1;
+            k2 = i2.subusers[it->first];
+            if (k1.name.compare(k2.name) != 0)
+                return -1;
+            if (k1.perm_mask != k2.perm_mask)
+                return -1;
+        }
+        if (i1.suspended != i2.suspended)
+            return -1;
+        if (i1.max_buckets != i2.max_buckets)
+            return -1;
+        uint32_t p1, p2;
+        p1 = p2 = RGW_CAP_ALL;
+        if (i1.caps.check_cap(meta_caps, p1) != 0)
+            return -1;
+        if (i2.caps.check_cap(meta_caps, p2) != 0)
+            return -1;
+        return 0;
+    }
+
+    size_t read_dummy_post(void *ptr, size_t s, size_t n, void *ud)
+    {
+        int dummy = 0;
+        memcpy(ptr, &dummy, sizeof(dummy));
+        return sizeof(dummy);
+    }
+
+    int parse_json_resp(JSONParser &parser)
+    {
+        string *resp;
+        resp = (string *)g_test->get_response_data();
+        if (!resp)
+            return -1;
+        if (!parser.parse(resp->c_str(), resp->length()))
+        {
+            cout << "Error parsing create user response" << std::endl;
+            return -1;
+        }
+        return 0;
+    }
+
+    size_t meta_read_json(void *ptr, size_t s, size_t n, void *ud)
+    {
+        stringstream *ss = (stringstream *)ud;
+        size_t len = ss->str().length();
+        if (s * n < len)
+        {
+            cout << "Cannot copy json data, as len is not enough\n";
+            return 0;
+        }
+        memcpy(ptr, (void *)ss->str().c_str(), len);
+        return len;
+    }
+};
diff --git a/src/test/test_rgw_admin_helper.h b/src/test/test_rgw_admin_helper.h
new file mode 100644 (file)
index 0000000..587b684
--- /dev/null
@@ -0,0 +1,89 @@
+// -*- 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) 2013 eNovance SAS <licensing@enovance.com>
+ *
+ * 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 <map>
+#include <list>
+extern "C"{
+#include <curl/curl.h>
+}
+#include <string.h>
+#include "rgw_common.h"
+
+using namespace std;
+
+#define CURL_VERBOSE 0
+#define HTTP_RESPONSE_STR "RespCode"
+#define CEPH_CRYPTO_HMACSHA1_DIGESTSIZE 20
+#define RGW_ADMIN_RESP_PATH "/tmp/.test_rgw_admin_resp"
+#define CEPH_UID "ceph"
+
+namespace admin_helper
+{
+    class test_helper
+    {
+    private:
+        string host;
+        string port;
+        string creds;
+        string rgw_admin_path;
+        string conf_path;
+        CURL *curl_inst;
+        map<string, string> response;
+        list<string> extra_hdrs;
+        string *resp_data;
+        unsigned resp_code;
+
+    public:
+        test_helper();
+        ~test_helper();
+        int send_request(string method, string uri,
+                         size_t (*function)(void *, size_t, size_t, void *) = 0,
+                         void *ud = 0, size_t length = 0);
+        int extract_input(int argc, char *argv[]);
+        string &get_response(string hdr);
+        void set_extra_header(string hdr);
+        void set_response(char *val);
+        void set_response_data(char *data, size_t len);
+        string &get_rgw_admin_path();
+        string &get_ceph_conf_path();
+        void set_creds(string &c);
+        const string *get_response_data();
+        unsigned get_resp_code();
+    };
+
+    size_t write_header(void *ptr, size_t size, size_t nmemb, void *ud);
+    size_t write_data(void *ptr, size_t size, size_t nmemb, void *ud);
+    inline void buf_to_hex(const unsigned char *buf, int len, char *str);
+    void calc_hmac_sha1(const char *key, int key_len,
+                        const char *msg, int msg_len, char *dest);
+    int get_s3_auth(const string &method, string creds, const string &date, string res, string &out);
+    void get_date(string &d);
+    void print_usage(char *exec);
+    int run_rgw_admin(string &cmd, string &resp);
+    int account_create(string &account_id, string &account_name);
+    int user_create(string &uid, string &display_name, bool set_creds = true, const string &account_id = "", bool is_admin = false);
+    int account_info(string &account_id, RGWAccountInfo &a_info);
+    int user_info(string &uid, string &display_name, RGWUserInfo &uinfo);
+    int account_rm(string &account_id);
+    int user_rm(string &uid, string &display_name);
+    int get_creds(string &json, string &creds);
+    int caps_add(const string caps_name, const string uid, const char *perm);
+    int caps_rm(const string caps_name, const string uid, const char *perm);
+    int compare_access_keys(RGWAccessKey &k1, RGWAccessKey &k2);
+    int compare_user_info(RGWUserInfo &i1, RGWUserInfo &i2, const string meta_caps);
+    size_t read_dummy_post(void *ptr, size_t s, size_t n, void *ud);
+    int parse_json_resp(JSONParser &parser);
+    size_t meta_read_json(void *ptr, size_t s, size_t n, void *ud);
+    extern admin_helper::test_helper *g_test;
+};