]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
exporter: Added https(TLSv13) support 58440/head
authorJuan Miguel Olmo Martínez <jolmomar@redhat.com>
Fri, 5 Jul 2024 08:09:25 +0000 (10:09 +0200)
committerJuan Miguel Olmo Martínez <jolmomar@redhat.com>
Wed, 10 Jul 2024 07:17:42 +0000 (09:17 +0200)
Ceph exporter can run now as http or https server

Signed-off-by: Juan Miguel Olmo Martínez <jolmomar@redhat.com>
src/common/options/ceph-exporter.yaml.in
src/exporter/CMakeLists.txt
src/exporter/ceph_exporter.cc
src/exporter/http_server.cc [deleted file]
src/exporter/http_server.h [deleted file]
src/exporter/web_server.cc [new file with mode: 0644]
src/exporter/web_server.h [new file with mode: 0644]

index 798a185e96bcc1ec6e205d8a66e196071bf570e1..c4b24ee43d4b055c01f85061ad73e8ea1e11b2c4 100644 (file)
@@ -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
index 0c0c03bf91db081dde81bf020a2c30e3535d5fa6..0127cc53913a84fee385f59177cf79eec48e1a98 100644 (file)
@@ -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)
index 70650ff87c677faa979b71bb7d991f4534bb91af..2e2c16bb0859fa822e31c2e4ac086f0d7205b5f5 100644 (file)
@@ -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 (file)
index 3eb48a2..0000000
+++ /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 (file)
index 0d0502f..0000000
+++ /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 (file)
index 0000000..96cc02b
--- /dev/null
@@ -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 (file)
index 0000000..c3339a8
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+
+#include <string>
+
+void web_server_thread_entrypoint();