summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/common/options/ceph-exporter.yaml.in14
-rw-r--r--src/exporter/CMakeLists.txt6
-rw-r--r--src/exporter/ceph_exporter.cc10
-rw-r--r--src/exporter/http_server.cc169
-rw-r--r--src/exporter/http_server.h5
-rw-r--r--src/exporter/web_server.cc276
-rw-r--r--src/exporter/web_server.h5
7 files changed, 307 insertions, 178 deletions
diff --git a/src/common/options/ceph-exporter.yaml.in b/src/common/options/ceph-exporter.yaml.in
index 798a185e96b..c4b24ee43d4 100644
--- a/src/common/options/ceph-exporter.yaml.in
+++ b/src/common/options/ceph-exporter.yaml.in
@@ -25,6 +25,20 @@ options:
default: 9926
services:
- ceph-exporter
+- name: exporter_cert_file
+ type: str
+ level: advanced
+ desc: Certificate file for TLS.
+ default:
+ services:
+ - ceph-exporter
+- name: exporter_key_file
+ type: str
+ level: advanced
+ desc: Key certificate file for TLS.
+ default:
+ services:
+ - ceph-exporter
- name: exporter_prio_limit
type: int
level: advanced
diff --git a/src/exporter/CMakeLists.txt b/src/exporter/CMakeLists.txt
index 0c0c03bf91d..0127cc53913 100644
--- a/src/exporter/CMakeLists.txt
+++ b/src/exporter/CMakeLists.txt
@@ -1,10 +1,12 @@
set(exporter_srcs
ceph_exporter.cc
DaemonMetricCollector.cc
- http_server.cc
+ web_server.cc
util.cc
)
add_executable(ceph-exporter ${exporter_srcs})
target_link_libraries(ceph-exporter
- global-static ceph-common)
+ global-static
+ ceph-common
+ OpenSSL::SSL)
install(TARGETS ceph-exporter DESTINATION bin)
diff --git a/src/exporter/ceph_exporter.cc b/src/exporter/ceph_exporter.cc
index 70650ff87c6..2e2c16bb085 100644
--- a/src/exporter/ceph_exporter.cc
+++ b/src/exporter/ceph_exporter.cc
@@ -1,7 +1,7 @@
#include "common/ceph_argparse.h"
#include "common/config.h"
#include "exporter/DaemonMetricCollector.h"
-#include "exporter/http_server.h"
+#include "exporter/web_server.h"
#include "global/global_init.h"
#include "global/global_context.h"
@@ -18,6 +18,8 @@ static void usage() {
" --sock-dir: The path to ceph daemons socket files dir\n"
" --addrs: Host ip address where exporter is deployed\n"
" --port: Port to deploy exporter on. Default is 9926\n"
+ " --cert-file: Path to the certificate file to use https\n"
+ " --key-file: Path to the certificate key file to use https\n"
" --prio-limit: Only perf counters greater than or equal to prio-limit are fetched. Default: 5\n"
" --stats-period: Time to wait before sending requests again to exporter server (seconds). Default: 5s"
<< std::endl;
@@ -48,6 +50,10 @@ int main(int argc, char **argv) {
cct->_conf.set_val("exporter_addr", val);
} else if (ceph_argparse_witharg(args, i, &val, "--port", (char *)NULL)) {
cct->_conf.set_val("exporter_http_port", val);
+ } else if (ceph_argparse_witharg(args, i, &val, "--cert-file", (char *)NULL)) {
+ cct->_conf.set_val("exporter_cert_file", val);
+ } else if (ceph_argparse_witharg(args, i, &val, "--key-file", (char *)NULL)) {
+ cct->_conf.set_val("exporter_key_file", val);
} else if (ceph_argparse_witharg(args, i, &val, "--prio-limit", (char *)NULL)) {
cct->_conf.set_val("exporter_prio_limit", val);
} else if (ceph_argparse_witharg(args, i, &val, "--stats-period", (char *)NULL)) {
@@ -58,7 +64,7 @@ int main(int argc, char **argv) {
}
common_init_finish(g_ceph_context);
- boost::thread server_thread(http_server_thread_entrypoint);
+ boost::thread server_thread(web_server_thread_entrypoint);
DaemonMetricCollector &collector = collector_instance();
collector.main();
server_thread.join();
diff --git a/src/exporter/http_server.cc b/src/exporter/http_server.cc
deleted file mode 100644
index 3eb48a2a1f0..00000000000
--- a/src/exporter/http_server.cc
+++ /dev/null
@@ -1,169 +0,0 @@
-#include "http_server.h"
-#include "common/debug.h"
-#include "common/hostname.h"
-#include "global/global_init.h"
-#include "global/global_context.h"
-#include "exporter/DaemonMetricCollector.h"
-
-#include <boost/asio/ip/tcp.hpp>
-#include <boost/beast/core.hpp>
-#include <boost/beast/http.hpp>
-#include <boost/beast/version.hpp>
-#include <boost/thread/thread.hpp>
-#include <chrono>
-#include <cstdlib>
-#include <ctime>
-#include <iostream>
-#include <map>
-#include <memory>
-#include <string>
-
-#define dout_context g_ceph_context
-#define dout_subsys ceph_subsys_ceph_exporter
-
-namespace beast = boost::beast; // from <boost/beast.hpp>
-namespace http = beast::http; // from <boost/beast/http.hpp>
-namespace net = boost::asio; // from <boost/asio.hpp>
-using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
-
-class http_connection : public std::enable_shared_from_this<http_connection> {
-public:
- http_connection(tcp::socket socket) : socket_(std::move(socket)) {}
-
- // Initiate the asynchronous operations associated with the connection.
- void start() {
- read_request();
- check_deadline();
- }
-
-private:
- tcp::socket socket_;
- beast::flat_buffer buffer_{8192};
- http::request<http::dynamic_body> request_;
- http::response<http::string_body> response_;
-
- net::steady_timer deadline_{socket_.get_executor(), std::chrono::seconds(60)};
-
- // Asynchronously receive a complete request message.
- void read_request() {
- auto self = shared_from_this();
-
- http::async_read(socket_, buffer_, request_,
- [self](beast::error_code ec, std::size_t bytes_transferred) {
- boost::ignore_unused(bytes_transferred);
- if (ec) {
- dout(1) << "ERROR: " << ec.message() << dendl;
- return;
- }
- else {
- self->process_request();
- }
- });
- }
-
- // Determine what needs to be done with the request message.
- void process_request() {
- response_.version(request_.version());
- response_.keep_alive(request_.keep_alive());
-
- switch (request_.method()) {
- case http::verb::get:
- response_.result(http::status::ok);
- create_response();
- break;
-
- default:
- // We return responses indicating an error if
- // we do not recognize the request method.
- response_.result(http::status::method_not_allowed);
- response_.set(http::field::content_type, "text/plain");
- std::string body("Invalid request-method '" +
- std::string(request_.method_string()) + "'");
- response_.body() = body;
- break;
- }
-
- write_response();
- }
-
- // Construct a response message based on the program state.
- void create_response() {
- if (request_.target() == "/") {
- response_.set(http::field::content_type, "text/html; charset=utf-8");
- std::string body("<html>\n"
- "<head><title>Ceph Exporter</title></head>\n"
- "<body>\n"
- "<h1>Ceph Exporter</h1>\n"
- "<p><a href='/metrics'>Metrics</a></p>"
- "</body>\n"
- "</html>\n");
- response_.body() = body;
- } else if (request_.target() == "/metrics") {
- response_.set(http::field::content_type, "text/plain; charset=utf-8");
- DaemonMetricCollector &collector = collector_instance();
- std::string metrics = collector.get_metrics();
- response_.body() = metrics;
- } else {
- response_.result(http::status::method_not_allowed);
- response_.set(http::field::content_type, "text/plain");
- response_.body() = "File not found \n";
- }
- }
-
- // Asynchronously transmit the response message.
- void write_response() {
- auto self = shared_from_this();
-
- response_.prepare_payload();
-
- http::async_write(socket_, response_,
- [self](beast::error_code ec, std::size_t) {
- self->socket_.shutdown(tcp::socket::shutdown_send, ec);
- self->deadline_.cancel();
- if (ec) {
- dout(1) << "ERROR: " << ec.message() << dendl;
- return;
- }
- });
- }
-
- // Check whether we have spent enough time on this connection.
- void check_deadline() {
- auto self = shared_from_this();
-
- deadline_.async_wait([self](beast::error_code ec) {
- if (!ec) {
- // Close socket to cancel any outstanding operation.
- self->socket_.close(ec);
- }
- });
- }
-};
-
-// "Loop" forever accepting new connections.
-void http_server(tcp::acceptor &acceptor, tcp::socket &socket) {
- acceptor.async_accept(socket, [&](beast::error_code ec) {
- if (!ec)
- std::make_shared<http_connection>(std::move(socket))->start();
- http_server(acceptor, socket);
- });
-}
-
-void http_server_thread_entrypoint() {
- try {
- std::string exporter_addr = g_conf().get_val<std::string>("exporter_addr");
- auto const address = net::ip::make_address(exporter_addr);
- unsigned short port = g_conf().get_val<int64_t>("exporter_http_port");
-
- net::io_context ioc{1};
-
- tcp::acceptor acceptor{ioc, {address, port}};
- tcp::socket socket{ioc};
- http_server(acceptor, socket);
- dout(1) << "Http server running on " << exporter_addr << ":" << port << dendl;
- ioc.run();
- } catch (std::exception const &e) {
- dout(1) << "Error: " << e.what() << dendl;
- exit(EXIT_FAILURE);
- }
-}
diff --git a/src/exporter/http_server.h b/src/exporter/http_server.h
deleted file mode 100644
index 0d0502f57c8..00000000000
--- a/src/exporter/http_server.h
+++ /dev/null
@@ -1,5 +0,0 @@
-#pragma once
-
-#include <string>
-
-void http_server_thread_entrypoint();
diff --git a/src/exporter/web_server.cc b/src/exporter/web_server.cc
new file mode 100644
index 00000000000..96cc02b389f
--- /dev/null
+++ b/src/exporter/web_server.cc
@@ -0,0 +1,276 @@
+#include "web_server.h"
+#include "common/debug.h"
+#include "common/hostname.h"
+#include "global/global_init.h"
+#include "global/global_context.h"
+#include "exporter/DaemonMetricCollector.h"
+
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ssl.hpp> // SSL/TLS
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/version.hpp>
+#include <boost/thread/thread.hpp>
+#include <chrono>
+#include <cstdlib>
+#include <ctime>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <string>
+
+#define dout_context g_ceph_context
+#define dout_subsys ceph_subsys_ceph_exporter
+
+namespace beast = boost::beast; // from <boost/beast.hpp>
+namespace http = beast::http; // from <boost/beast/http.hpp>
+namespace net = boost::asio; // from <boost/asio.hpp>
+namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
+using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
+
+// Base class for common functionality
+class web_connection {
+public:
+ virtual ~web_connection() = default;
+ virtual void start() = 0; // Pure virtual function to start the connection
+
+protected:
+ beast::flat_buffer buffer_{8192};
+ http::request<http::dynamic_body> request_;
+ http::response<http::string_body> response_;
+ net::steady_timer deadline_;
+
+ web_connection(net::any_io_executor executor, std::chrono::seconds timeout)
+ : deadline_(executor, timeout) {}
+
+ // Common request processing logic
+ void process_request() {
+ response_.version(request_.version());
+ response_.keep_alive(request_.keep_alive());
+
+ switch (request_.method()) {
+ case http::verb::get:
+ response_.result(http::status::ok);
+ create_response();
+ break;
+
+ default:
+ response_.result(http::status::method_not_allowed);
+ response_.set(http::field::content_type, "text/plain");
+ std::string body("Invalid request-method '" + std::string(request_.method_string()) + "'\n");
+ response_.body() = body;
+ break;
+ }
+ write_response();
+ }
+
+ // Construct a response message based on the request target
+ void create_response() {
+ if (request_.target() == "/") {
+ response_.result(http::status::moved_permanently);
+ response_.set(http::field::location, "/metrics");
+ } else if (request_.target() == "/metrics") {
+ response_.set(http::field::content_type, "text/plain; charset=utf-8");
+ DaemonMetricCollector &collector = collector_instance();
+ std::string metrics = collector.get_metrics();
+ response_.body() = metrics;
+ } else {
+ response_.result(http::status::method_not_allowed);
+ response_.set(http::field::content_type, "text/plain");
+ response_.body() = "File not found \n";
+ }
+ }
+
+ // Asynchronously transmit the response message
+ virtual void write_response() = 0;
+
+ // Check whether we have spent enough time on this connection
+ void check_deadline(std::shared_ptr<web_connection> self) {
+ deadline_.async_wait([self](beast::error_code ec) {
+ if (!ec) {
+ self->close_connection(ec);
+ }
+ });
+ }
+
+ // Bad requests error mgmt (http req->https srv and https req ->http srv)
+ void handle_bad_request(beast::error_code ec) {
+ response_.version(request_.version());
+ response_.keep_alive(request_.keep_alive());
+ response_.result(http::status::method_not_allowed);
+ response_.set(http::field::content_type, "text/plain");
+ std::string body = "Ceph exporter.\nRequest Error: " + ec.message();
+ response_.body() = body;
+
+ write_response();
+ }
+
+ virtual void close_connection(beast::error_code& ec) = 0;
+};
+
+// Derived class for HTTP connections
+class http_connection : public web_connection, public std::enable_shared_from_this<http_connection> {
+public:
+ explicit http_connection(tcp::socket socket)
+ : web_connection(socket.get_executor(), std::chrono::seconds(60)), socket_(std::move(socket)) {}
+
+ void start() override {
+ read_request(shared_from_this());
+ check_deadline(shared_from_this());
+ }
+
+private:
+ tcp::socket socket_;
+
+ void read_request(std::shared_ptr<http_connection> self) {
+ http::async_read(socket_, buffer_, request_,
+ [self](beast::error_code ec, std::size_t bytes_transferred) {
+ boost::ignore_unused(bytes_transferred);
+ if (ec) {
+ dout(1) << "ERROR: " << ec.message() << dendl;
+ self->handle_bad_request(ec);
+ return;
+ }
+ self->process_request();
+ });
+ }
+
+ void write_response() override {
+ auto self = shared_from_this();
+ response_.prepare_payload();
+ http::async_write(socket_, response_,
+ [self](beast::error_code ec, std::size_t) {
+ self->socket_.shutdown(tcp::socket::shutdown_send, ec);
+ self->deadline_.cancel();
+ if (ec) {
+ dout(1) << "ERROR: " << ec.message() << dendl;
+ return;
+ }
+ });
+ }
+
+ void close_connection(beast::error_code& ec) override {
+ socket_.close(ec);
+ }
+};
+
+// Derived class for HTTPS connections
+class https_connection : public web_connection, public std::enable_shared_from_this<https_connection> {
+public:
+ explicit https_connection(ssl::stream<tcp::socket> socket)
+ : web_connection(socket.get_executor(), std::chrono::seconds(60)), socket_(std::move(socket)) {}
+
+ void start() override {
+ auto self = shared_from_this();
+ socket_.async_handshake(ssl::stream_base::server,
+ [self](beast::error_code ec) {
+ if (!ec) {
+ self->read_request(self);
+ } else {
+ dout(1) << "ERROR: SSL Handshake failed: " << ec.message() << dendl;
+ self->handle_bad_request(ec);
+ }
+ });
+ check_deadline(self);
+ }
+
+private:
+ ssl::stream<tcp::socket> socket_;
+
+ void read_request(std::shared_ptr<https_connection> self) {
+ http::async_read(socket_, buffer_, request_,
+ [self](beast::error_code ec, std::size_t bytes_transferred) {
+ boost::ignore_unused(bytes_transferred);
+ if (ec) {
+ dout(1) << "ERROR: " << ec.message() << dendl;
+ return;
+ }
+ self->process_request();
+ });
+ }
+
+ void write_response() override {
+ auto self = shared_from_this();
+ response_.prepare_payload();
+ http::async_write(socket_, response_,
+ [self](beast::error_code ec, std::size_t) {
+ self->socket_.async_shutdown([self](beast::error_code ec) {
+ self->deadline_.cancel();
+ if (ec) {
+ dout(1) << "ERROR: " << ec.message() << dendl;
+ }
+ });
+ });
+ }
+
+ void close_connection(beast::error_code& ec) override {
+ socket_.lowest_layer().close(ec);
+ }
+
+};
+
+void http_server(tcp::acceptor &acceptor, tcp::socket &socket) {
+ acceptor.async_accept(socket, [&](beast::error_code ec) {
+ if (!ec) {
+ std::make_shared<http_connection>(std::move(socket))->start();
+ }
+ http_server(acceptor, socket);
+ });
+}
+
+void https_server(tcp::acceptor &acceptor, ssl::context &ssl_ctx) {
+ acceptor.async_accept([&](beast::error_code ec, tcp::socket socket) {
+ if (!ec) {
+ std::make_shared<https_connection>(ssl::stream<tcp::socket>(std::move(socket), ssl_ctx))->start();
+ }
+ https_server(acceptor, ssl_ctx);
+ });
+}
+
+void run_http_server(const std::string& exporter_addr, short unsigned int port) {
+ net::io_context ioc{1};
+ tcp::acceptor acceptor{ioc, {net::ip::make_address(exporter_addr), port}};
+ tcp::socket socket{ioc};
+
+ http_server(acceptor, socket);
+
+ dout(1) << "HTTP server running on " << exporter_addr << ":" << port << dendl;
+ ioc.run();
+}
+
+void run_https_server(const std::string& exporter_addr, short unsigned int port, const std::string& cert_file, const std::string& key_file) {
+ net::io_context ioc{1};
+ ssl::context ssl_ctx(ssl::context::tlsv13);
+
+ ssl_ctx.use_certificate_chain_file(cert_file);
+ ssl_ctx.use_private_key_file(key_file, ssl::context::pem);
+
+ tcp::acceptor acceptor{ioc, {net::ip::make_address(exporter_addr), port}};
+ https_server(acceptor, ssl_ctx);
+
+ dout(1) << "HTTPS server running on " << exporter_addr << ":" << port << dendl;
+ ioc.run();
+}
+
+void web_server_thread_entrypoint() {
+ try {
+ std::string exporter_addr = g_conf().get_val<std::string>("exporter_addr");
+ short unsigned int port = g_conf().get_val<int64_t>("exporter_http_port");
+ std::string cert_file = g_conf().get_val<std::string>("exporter_cert_file");
+ std::string key_file = g_conf().get_val<std::string>("exporter_key_file");
+
+ if (cert_file.empty() && key_file.empty()) {
+ run_http_server(exporter_addr, port);
+ } else {
+ try {
+ run_https_server(exporter_addr, port, cert_file, key_file);
+ } catch (const std::exception &e) {
+ dout(1) << "Failed to start HTTPS server: " << e.what() << dendl;
+ exit(EXIT_FAILURE);
+ }
+ }
+ } catch (std::exception const &e) {
+ dout(1) << "Error: " << e.what() << dendl;
+ exit(EXIT_FAILURE);
+ }
+}
diff --git a/src/exporter/web_server.h b/src/exporter/web_server.h
new file mode 100644
index 00000000000..c3339a8d43a
--- /dev/null
+++ b/src/exporter/web_server.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include <string>
+
+void web_server_thread_entrypoint();