mirror of
https://github.com/ceph/ceph
synced 2025-02-24 11:37:37 +00:00
exporter: Added https(TLSv13) support
Ceph exporter can run now as http or https server Signed-off-by: Juan Miguel Olmo Martínez <jolmomar@redhat.com>
This commit is contained in:
parent
82e3be89ad
commit
cb660d2783
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
void http_server_thread_entrypoint();
|
276
src/exporter/web_server.cc
Normal file
276
src/exporter/web_server.cc
Normal file
@ -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);
|
||||
}
|
||||
}
|
5
src/exporter/web_server.h
Normal file
5
src/exporter/web_server.h
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
void web_server_thread_entrypoint();
|
Loading…
Reference in New Issue
Block a user