diff --git a/ceph.spec.in b/ceph.spec.in index 1a22b12f5c4..0e18f2b656d 100644 --- a/ceph.spec.in +++ b/ceph.spec.in @@ -614,6 +614,7 @@ fi %{_bindir}/ceph_filestore_dump %{_bindir}/ceph_streamtest %{_bindir}/ceph_test_cfuse_cache_invalidate +%{_bindir}/ceph_test_cls_hello %{_bindir}/ceph_test_cls_lock %{_bindir}/ceph_test_cls_log %{_bindir}/ceph_test_cls_rbd diff --git a/debian/ceph-test.install b/debian/ceph-test.install index 764e6d1d573..c5a5e0a9774 100644 --- a/debian/ceph-test.install +++ b/debian/ceph-test.install @@ -17,6 +17,7 @@ usr/bin/ceph_smalliobenchfs usr/bin/ceph_smalliobenchrbd usr/bin/ceph_streamtest usr/bin/ceph_test_cfuse_cache_invalidate +usr/bin/ceph_test_cls_hello usr/bin/ceph_test_cls_lock usr/bin/ceph_test_cls_log usr/bin/ceph_test_cls_rbd diff --git a/qa/workunits/cls/test_cls_hello.sh b/qa/workunits/cls/test_cls_hello.sh new file mode 100755 index 00000000000..0a2e096207b --- /dev/null +++ b/qa/workunits/cls/test_cls_hello.sh @@ -0,0 +1,5 @@ +#!/bin/sh -e + +ceph_test_cls_hello + +exit 0 diff --git a/src/Makefile.am b/src/Makefile.am index e93bbb8f537..de1b81fb4ba 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -531,6 +531,18 @@ endif ## rados object classes +radoslibdir = $(libdir)/rados-classes +radoslib_LTLIBRARIES = + +# hello world class +libcls_hello_la_SOURCES = cls/hello/cls_hello.cc +libcls_hello_la_CFLAGS = ${AM_CFLAGS} +libcls_hello_la_CXXFLAGS= ${AM_CXXFLAGS} +libcls_hello_la_LIBADD = $(PTHREAD_LIBS) $(EXTRALIBS) +libcls_hello_la_LDFLAGS = ${AM_LDFLAGS} -version-info 1:0:0 -export-symbols-regex '.*__cls_.*' + +radoslib_LTLIBRARIES += libcls_hello.la + # rbd: rados block device class libcls_rbd_la_SOURCES = cls/rbd/cls_rbd.cc libcls_rbd_la_CFLAGS = ${AM_CFLAGS} @@ -538,9 +550,7 @@ libcls_rbd_la_CXXFLAGS= ${AM_CXXFLAGS} libcls_rbd_la_LIBADD = $(PTHREAD_LIBS) $(EXTRALIBS) libcls_rbd_la_LDFLAGS = ${AM_LDFLAGS} -version-info 1:0:0 -export-symbols-regex '.*__cls_.*' -radoslibdir = $(libdir)/rados-classes -radoslib_LTLIBRARIES = libcls_rbd.la - +radoslib_LTLIBRARIES += libcls_rbd.la # lock class libcls_lock_la_SOURCES = cls/lock/cls_lock.cc @@ -548,7 +558,6 @@ libcls_lock_la_CFLAGS = ${AM_CFLAGS} libcls_lock_la_CXXFLAGS= ${AM_CXXFLAGS} libcls_lock_la_LIBADD = $(PTHREAD_LIBS) $(EXTRALIBS) libcls_lock_la_LDFLAGS = ${AM_LDFLAGS} -version-info 1:0:0 -export-symbols-regex '.*__cls_.*' - radoslib_LTLIBRARIES += libcls_lock.la # refcount class @@ -1082,6 +1091,11 @@ ceph_test_cls_lock_LDADD = libcls_lock_client.a librados.la ${UNITTEST_STATIC_L ceph_test_cls_lock_CXXFLAGS = ${AM_CXXFLAGS} ${UNITTEST_CXXFLAGS} bin_DEBUGPROGRAMS += ceph_test_cls_lock +ceph_test_cls_hello_SOURCES = test/cls_hello/test_cls_hello.cc test/librados/test.cc +ceph_test_cls_hello_LDADD = ${UNITTEST_LDADD} ${UNITTEST_STATIC_LDADD} $(LIBGLOBAL_LDA) $(CRYPTO_LIBS) librados.la +ceph_test_cls_hello_CXXFLAGS = ${AM_CXXFLAGS} ${UNITTEST_CXXFLAGS} +bin_DEBUGPROGRAMS += ceph_test_cls_hello + if WITH_RADOSGW ceph_test_cls_rgw_SOURCES = test/cls_rgw/test_cls_rgw.cc \ diff --git a/src/cls/hello/cls_hello.cc b/src/cls/hello/cls_hello.cc new file mode 100644 index 00000000000..0d5c78b1617 --- /dev/null +++ b/src/cls/hello/cls_hello.cc @@ -0,0 +1,307 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +/* + * This is a simple example RADOS class, designed to be usable as a + * template from implementing new methods. + * + * Our goal here is to illustrate the interface between the OSD and + * the class and demonstrate what kinds of things a class can do. + * + * Note that any *real* class will probably have a much more + * sophisticated protocol dealing with the in and out data buffers. + * For an example of the model that we've settled on for handling that + * in a clean way, please refer to cls_lock or cls_version for + * relatively simple examples of how the parameter encoding can be + * encoded in a way that allows for forward and backward compatibility + * between client vs class revisions. + */ + +/* + * A quick note about bufferlists: + * + * The bufferlist class allows memory buffers to be concatenated, + * truncated, spliced, "copied," encoded/embedded, and decoded. For + * most operations no actual data is ever copied, making bufferlists + * very convenient for efficiently passing data around. + * + * bufferlist is actually a typedef of buffer::list, and is defined in + * include/buffer.h (and implemented in common/buffer.cc). + */ + +#include +#include +#include +#include + +#include "objclass/objclass.h" + +CLS_VER(1,0) +CLS_NAME(hello) + +cls_handle_t h_class; +cls_method_handle_t h_say_hello; +cls_method_handle_t h_record_hello; +cls_method_handle_t h_replay; +cls_method_handle_t h_writes_dont_return_data; +cls_method_handle_t h_turn_it_to_11; +cls_method_handle_t h_bad_reader; +cls_method_handle_t h_bad_writer; + +/** + * say hello - a "read" method that does not depend on the object + * + * This is an example of a method that does some computation and + * returns data to the caller, without depending on the local object + * content. + */ +static int say_hello(cls_method_context_t hctx, bufferlist *in, bufferlist *out) +{ + // see if the input data from the client matches what this method + // expects to receive. your class can fill this buffer with what it + // wants. + if (in->length() > 100) + return -EINVAL; + + // we generate our reply + out->append("Hello, "); + if (in->length() == 0) + out->append("world"); + else + out->append(*in); + out->append("!"); + + // this return value will be returned back to the librados caller + return 0; +} + +/** + * record hello - a "write" method that creates an object + * + * This method modifies a local object (in this case, by creating it + * if it doesn't exist). We make multiple write calls (write, + * setxattr) which are accumulated and applied as an atomic + * transaction. + */ +static int record_hello(cls_method_context_t hctx, bufferlist *in, bufferlist *out) +{ + // we can write arbitrary stuff to the ceph-osd debug log. each log + // message is accompanied by an integer log level. smaller is + // "louder". how much of this makes it into the log is controlled + // by the debug_cls option on the ceph-osd, similar to how other log + // levels are controlled. this message, at level 20, will generally + // not be seen by anyone unless debug_cls is set at 20 or higher. + CLS_LOG(20, "in record_hello"); + + // see if the input data from the client matches what this method + // expects to receive. your class can fill this buffer with what it + // wants. + if (in->length() > 100) + return -EINVAL; + + // only say hello to non-existent objects + if (cls_cxx_stat(hctx, NULL, NULL) == 0) + return -EEXIST; + + bufferlist content; + content.append("Hello, "); + if (in->length() == 0) + content.append("world"); + else + content.append(*in); + content.append("!"); + + // create/write the object + int r = cls_cxx_write_full(hctx, &content); + if (r < 0) + return r; + + // also make note of who said it + entity_inst_t origin; + cls_get_request_origin(hctx, &origin); + ostringstream ss; + ss << origin; + bufferlist attrbl; + attrbl.append(ss.str()); + r = cls_cxx_setxattr(hctx, "said_by", &attrbl); + if (r < 0) + return r; + + // For write operations, there are two possible outcomes: + // + // * For a failure, we return a negative error code. The out + // buffer can contain any data that we want, and that data will + // be returned to the caller. No change is made to the object. + // + // * For a success, we must return 0 and *no* data in the out + // buffer. This is becaues the OSD does not log write result + // codes or output buffers and we need a replayed/resent + // operation (e.g., after a TCP disconnect) to be idempotent. + // + // If a class returns a positive value or puts data in the out + // buffer, the OSD code will ignore it and return 0 to the + // client. + return 0; +} + +static int writes_dont_return_data(cls_method_context_t hctx, bufferlist *in, bufferlist *out) +{ + // make some change to the object + bufferlist attrbl; + attrbl.append("bar"); + int r = cls_cxx_setxattr(hctx, "foo", &attrbl); + if (r < 0) + return r; + + if (in->length() > 0) { + // note that if we return anything < 0 (an error), this + // operation/transaction will abort, and the setattr above will + // never happen. however, we *can* return data on error. + out->append("too much input data!"); + return -EINVAL; + } + + // try to return some data. note that this *won't* reach the + // client! see the matching test case in test_cls_hello.cc. + out->append("you will never see this"); + + // if we try to return anything > 0 here the client will see 0. + return 42; +} + + +/** + * replay - a "read" method to get a previously recorded hello + * + * This is a read method that will retrieve a previously recorded + * hello statement. + */ +static int replay(cls_method_context_t hctx, bufferlist *in, bufferlist *out) +{ + // read contents out of the on-disk object. our behavior can be a + // function of either the request alone, or the request and the + // on-disk state, depending on whether the RD flag is specified when + // registering the method (see the __cls__init function below). + int r = cls_cxx_read(hctx, 0, 1100, out); + if (r < 0) + return r; + + // note that our return value need not be the length of the returned + // data; it can be whatever value we want: positive, zero or + // negative (this is a read). + return 0; +} + +/** + * turn_it_to_11 - a "write" method that mutates existing object data + * + * A write method can depend on previous object content (i.e., perform + * a read/modify/write operation). This atomically transitions the + * object state from the old content to the new content. + */ +static int turn_it_to_11(cls_method_context_t hctx, bufferlist *in, bufferlist *out) +{ + // see if the input data from the client matches what this method + // expects to receive. your class can fill this buffer with what it + // wants. + if (in->length() != 0) + return -EINVAL; + + bufferlist previous; + int r = cls_cxx_read(hctx, 0, 1100, &previous); + if (r < 0) + return r; + + std::string str(previous.c_str(), previous.length()); + std::transform(str.begin(), str.end(), str.begin(), ::toupper); + previous.clear(); + previous.append(str); + + // replace previous byte data content (write_full == truncate(0) + write) + r = cls_cxx_write_full(hctx, &previous); + if (r < 0) + return r; + + // record who did it + entity_inst_t origin; + cls_get_request_origin(hctx, &origin); + ostringstream ss; + ss << origin; + bufferlist attrbl; + attrbl.append(ss.str()); + r = cls_cxx_setxattr(hctx, "amplified_by", &attrbl); + if (r < 0) + return r; + + // return value is 0 for success; out buffer is empty. + return 0; +} + +/** + * example method that does not behave + * + * This method is registered as WR but tries to read + */ +static int bad_reader(cls_method_context_t hctx, bufferlist *in, bufferlist *out) +{ + return cls_cxx_read(hctx, 0, 100, out); +} + +/** + * example method that does not behave + * + * This method is registered as RD but tries to write + */ +static int bad_writer(cls_method_context_t hctx, bufferlist *in, bufferlist *out) +{ + return cls_cxx_write_full(hctx, in); +} + + +/** + * initialize class + * + * We do two things here: we register the new class, and then register + * all of the class's methods. + */ +void __cls_init() +{ + // this log message, at level 0, will always appear in the ceph-osd + // log file. + CLS_LOG(0, "loading cls_hello"); + + cls_register("hello", &h_class); + + // There are two flags we specify for methods: + // + // RD : whether this method (may) read prior object state + // WR : whether this method (may) write or update the object + // + // A method can be RD, WR, neither, or both. If a method does + // neither, the data it returns to the caller is a function of the + // request and not the object contents. + + cls_register_cxx_method(h_class, "say_hello", + CLS_METHOD_RD, + say_hello, &h_say_hello); + cls_register_cxx_method(h_class, "record_hello", + CLS_METHOD_WR, + record_hello, &h_record_hello); + cls_register_cxx_method(h_class, "writes_dont_return_data", + CLS_METHOD_WR, + writes_dont_return_data, &h_writes_dont_return_data); + cls_register_cxx_method(h_class, "replay", + CLS_METHOD_RD, + replay, &h_replay); + + // RD | WR is a read-modify-write method. + cls_register_cxx_method(h_class, "turn_it_to_11", + CLS_METHOD_RD | CLS_METHOD_WR, + turn_it_to_11, &h_turn_it_to_11); + + // counter-examples + cls_register_cxx_method(h_class, "bad_reader", CLS_METHOD_WR, + bad_reader, &h_bad_reader); + cls_register_cxx_method(h_class, "bad_writer", CLS_METHOD_RD, + bad_writer, &h_bad_writer); +} diff --git a/src/test/cls_hello/test_cls_hello.cc b/src/test/cls_hello/test_cls_hello.cc new file mode 100644 index 00000000000..58ecb97f9f6 --- /dev/null +++ b/src/test/cls_hello/test_cls_hello.cc @@ -0,0 +1,133 @@ +// -*- 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 Inktank + * + * 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 +#include + +#include "include/rados/librados.hpp" +#include "test/librados/test.h" +#include "gtest/gtest.h" + +using namespace librados; + +TEST(ClsHello, SayHello) { + Rados cluster; + std::string pool_name = get_temp_pool_name(); + ASSERT_EQ("", create_one_pool_pp(pool_name, cluster)); + IoCtx ioctx; + cluster.ioctx_create(pool_name.c_str(), ioctx); + + bufferlist in, out; + ASSERT_EQ(-ENOENT, ioctx.exec("myobject", "hello", "say_hello", in, out)); + ASSERT_EQ(0, ioctx.write_full("myobject", in)); + ASSERT_EQ(0, ioctx.exec("myobject", "hello", "say_hello", in, out)); + ASSERT_EQ(std::string("Hello, world!"), std::string(out.c_str(), out.length())); + + out.clear(); + in.append("Tester"); + ASSERT_EQ(0, ioctx.exec("myobject", "hello", "say_hello", in, out)); + ASSERT_EQ(std::string("Hello, Tester!"), std::string(out.c_str(), out.length())); + + out.clear(); + in.clear(); + char buf[4096]; + memset(buf, 1, sizeof(buf)); + in.append(buf, sizeof(buf)); + ASSERT_EQ(-EINVAL, ioctx.exec("myobject", "hello", "say_hello", in, out)); + + ASSERT_EQ(0, destroy_one_pool_pp(pool_name, cluster)); +} + +TEST(ClsHello, RecordHello) { + Rados cluster; + std::string pool_name = get_temp_pool_name(); + ASSERT_EQ("", create_one_pool_pp(pool_name, cluster)); + IoCtx ioctx; + cluster.ioctx_create(pool_name.c_str(), ioctx); + + bufferlist in, out; + ASSERT_EQ(0, ioctx.exec("myobject", "hello", "record_hello", in, out)); + ASSERT_EQ(-EEXIST, ioctx.exec("myobject", "hello", "record_hello", in, out)); + + in.append("Tester"); + ASSERT_EQ(0, ioctx.exec("myobject2", "hello", "record_hello", in, out)); + ASSERT_EQ(-EEXIST, ioctx.exec("myobject2", "hello", "record_hello", in, out)); + ASSERT_EQ(0u, out.length()); + + in.clear(); + out.clear(); + ASSERT_EQ(0, ioctx.exec("myobject", "hello", "replay", in, out)); + ASSERT_EQ(std::string("Hello, world!"), std::string(out.c_str(), out.length())); + out.clear(); + ASSERT_EQ(0, ioctx.exec("myobject2", "hello", "replay", in, out)); + ASSERT_EQ(std::string("Hello, Tester!"), std::string(out.c_str(), out.length())); + + ASSERT_EQ(0, destroy_one_pool_pp(pool_name, cluster)); +} + +TEST(ClsHello, WriteReturnData) { + Rados cluster; + std::string pool_name = get_temp_pool_name(); + ASSERT_EQ("", create_one_pool_pp(pool_name, cluster)); + IoCtx ioctx; + cluster.ioctx_create(pool_name.c_str(), ioctx); + + bufferlist in, out; + ASSERT_EQ(0, ioctx.exec("myobject", "hello", "writes_dont_return_data", in, out)); + ASSERT_EQ(std::string(), std::string(out.c_str(), out.length())); + + char buf[4096]; + memset(buf, 1, sizeof(buf)); + in.append(buf, sizeof(buf)); + ASSERT_EQ(-EINVAL, ioctx.exec("myobject2", "hello", "writes_dont_return_data", in, out)); + ASSERT_EQ(std::string("too much input data!"), std::string(out.c_str(), out.length())); + ASSERT_EQ(-ENOENT, ioctx.getxattr("myobject2", "foo", out)); + + ASSERT_EQ(0, destroy_one_pool_pp(pool_name, cluster)); +} + +TEST(ClsHello, Loud) { + Rados cluster; + std::string pool_name = get_temp_pool_name(); + ASSERT_EQ("", create_one_pool_pp(pool_name, cluster)); + IoCtx ioctx; + cluster.ioctx_create(pool_name.c_str(), ioctx); + + bufferlist in, out; + ASSERT_EQ(0, ioctx.exec("myobject", "hello", "record_hello", in, out)); + ASSERT_EQ(0, ioctx.exec("myobject", "hello", "replay", in, out)); + ASSERT_EQ(std::string("Hello, world!"), std::string(out.c_str(), out.length())); + + ASSERT_EQ(0, ioctx.exec("myobject", "hello", "turn_it_to_11", in, out)); + ASSERT_EQ(0, ioctx.exec("myobject", "hello", "replay", in, out)); + ASSERT_EQ(std::string("HELLO, WORLD!"), std::string(out.c_str(), out.length())); + + ASSERT_EQ(0, destroy_one_pool_pp(pool_name, cluster)); +} + +TEST(ClsHello, BadMethods) { + Rados cluster; + std::string pool_name = get_temp_pool_name(); + ASSERT_EQ("", create_one_pool_pp(pool_name, cluster)); + IoCtx ioctx; + cluster.ioctx_create(pool_name.c_str(), ioctx); + + bufferlist in, out; + + ASSERT_EQ(0, ioctx.write_full("myobject", in)); + ASSERT_EQ(-EIO, ioctx.exec("myobject", "hello", "bad_reader", in, out)); + ASSERT_EQ(-EIO, ioctx.exec("myobject", "hello", "bad_writer", in, out)); + + ASSERT_EQ(0, destroy_one_pool_pp(pool_name, cluster)); +}