summaryrefslogtreecommitdiffstats
path: root/lib/cli
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/cli/CMakeLists.txt49
-rw-r--r--lib/cli/apisetupcommand.cpp59
-rw-r--r--lib/cli/apisetupcommand.hpp31
-rw-r--r--lib/cli/apisetuputility.cpp205
-rw-r--r--lib/cli/apisetuputility.hpp39
-rw-r--r--lib/cli/calistcommand.cpp89
-rw-r--r--lib/cli/calistcommand.hpp33
-rw-r--r--lib/cli/caremovecommand.cpp93
-rw-r--r--lib/cli/caremovecommand.hpp30
-rw-r--r--lib/cli/carestorecommand.cpp88
-rw-r--r--lib/cli/carestorecommand.hpp30
-rw-r--r--lib/cli/casigncommand.cpp108
-rw-r--r--lib/cli/casigncommand.hpp30
-rw-r--r--lib/cli/clicommand.cpp373
-rw-r--r--lib/cli/clicommand.hpp79
-rw-r--r--lib/cli/consolecommand.cpp723
-rw-r--r--lib/cli/consolecommand.hpp61
-rw-r--r--lib/cli/daemoncommand.cpp882
-rw-r--r--lib/cli/daemoncommand.hpp31
-rw-r--r--lib/cli/daemonutility.cpp285
-rw-r--r--lib/cli/daemonutility.hpp27
-rw-r--r--lib/cli/editline.hpp19
-rw-r--r--lib/cli/featuredisablecommand.cpp55
-rw-r--r--lib/cli/featuredisablecommand.hpp33
-rw-r--r--lib/cli/featureenablecommand.cpp50
-rw-r--r--lib/cli/featureenablecommand.hpp32
-rw-r--r--lib/cli/featurelistcommand.cpp34
-rw-r--r--lib/cli/featurelistcommand.hpp28
-rw-r--r--lib/cli/featureutility.cpp243
-rw-r--r--lib/cli/featureutility.hpp42
-rw-r--r--lib/cli/i2-cli.hpp14
-rw-r--r--lib/cli/internalsignalcommand.cpp67
-rw-r--r--lib/cli/internalsignalcommand.hpp33
-rw-r--r--lib/cli/nodesetupcommand.cpp559
-rw-r--r--lib/cli/nodesetupcommand.hpp36
-rw-r--r--lib/cli/nodeutility.cpp378
-rw-r--r--lib/cli/nodeutility.hpp49
-rw-r--r--lib/cli/nodewizardcommand.cpp815
-rw-r--r--lib/cli/nodewizardcommand.hpp36
-rw-r--r--lib/cli/objectlistcommand.cpp145
-rw-r--r--lib/cli/objectlistcommand.hpp36
-rw-r--r--lib/cli/objectlistutility.cpp155
-rw-r--r--lib/cli/objectlistutility.hpp34
-rw-r--r--lib/cli/pkinewcacommand.cpp29
-rw-r--r--lib/cli/pkinewcacommand.hpp29
-rw-r--r--lib/cli/pkinewcertcommand.cpp66
-rw-r--r--lib/cli/pkinewcertcommand.hpp32
-rw-r--r--lib/cli/pkirequestcommand.cpp93
-rw-r--r--lib/cli/pkirequestcommand.hpp32
-rw-r--r--lib/cli/pkisavecertcommand.cpp89
-rw-r--r--lib/cli/pkisavecertcommand.hpp32
-rw-r--r--lib/cli/pkisigncsrcommand.cpp56
-rw-r--r--lib/cli/pkisigncsrcommand.hpp32
-rw-r--r--lib/cli/pkiticketcommand.cpp55
-rw-r--r--lib/cli/pkiticketcommand.hpp31
-rw-r--r--lib/cli/pkiverifycommand.cpp226
-rw-r--r--lib/cli/pkiverifycommand.hpp32
-rw-r--r--lib/cli/variablegetcommand.cpp75
-rw-r--r--lib/cli/variablegetcommand.hpp34
-rw-r--r--lib/cli/variablelistcommand.cpp52
-rw-r--r--lib/cli/variablelistcommand.hpp34
-rw-r--r--lib/cli/variableutility.cpp76
-rw-r--r--lib/cli/variableutility.hpp31
63 files changed, 7374 insertions, 0 deletions
diff --git a/lib/cli/CMakeLists.txt b/lib/cli/CMakeLists.txt
new file mode 100644
index 0000000..bbdf801
--- /dev/null
+++ b/lib/cli/CMakeLists.txt
@@ -0,0 +1,49 @@
+# Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+
+
+set(cli_SOURCES
+ i2-cli.hpp
+ apisetupcommand.cpp apisetupcommand.hpp
+ apisetuputility.cpp apisetuputility.hpp
+ calistcommand.cpp calistcommand.hpp
+ caremovecommand.cpp caremovecommand.hpp
+ carestorecommand.cpp carestorecommand.hpp
+ casigncommand.cpp casigncommand.hpp
+ clicommand.cpp clicommand.hpp
+ consolecommand.cpp consolecommand.hpp
+ daemoncommand.cpp daemoncommand.hpp
+ daemonutility.cpp daemonutility.hpp
+ editline.hpp
+ featuredisablecommand.cpp featuredisablecommand.hpp
+ featureenablecommand.cpp featureenablecommand.hpp
+ featurelistcommand.cpp featurelistcommand.hpp
+ featureutility.cpp featureutility.hpp
+ internalsignalcommand.cpp internalsignalcommand.hpp
+ nodesetupcommand.cpp nodesetupcommand.hpp
+ nodeutility.cpp nodeutility.hpp
+ nodewizardcommand.cpp nodewizardcommand.hpp
+ objectlistcommand.cpp objectlistcommand.hpp
+ objectlistutility.cpp objectlistutility.hpp
+ pkinewcacommand.cpp pkinewcacommand.hpp
+ pkinewcertcommand.cpp pkinewcertcommand.hpp
+ pkirequestcommand.cpp pkirequestcommand.hpp
+ pkisavecertcommand.cpp pkisavecertcommand.hpp
+ pkisigncsrcommand.cpp pkisigncsrcommand.hpp
+ pkiticketcommand.cpp pkiticketcommand.hpp
+ pkiverifycommand.cpp pkiverifycommand.hpp
+ variablegetcommand.cpp variablegetcommand.hpp
+ variablelistcommand.cpp variablelistcommand.hpp
+ variableutility.cpp variableutility.hpp
+)
+
+if(ICINGA2_UNITY_BUILD)
+ mkunity_target(cli cli cli_SOURCES)
+endif()
+
+add_library(cli OBJECT ${cli_SOURCES})
+
+add_dependencies(cli base config icinga remote)
+
+set_target_properties (
+ cli PROPERTIES
+ FOLDER Lib
+)
diff --git a/lib/cli/apisetupcommand.cpp b/lib/cli/apisetupcommand.cpp
new file mode 100644
index 0000000..81b9d8d
--- /dev/null
+++ b/lib/cli/apisetupcommand.cpp
@@ -0,0 +1,59 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/apisetupcommand.hpp"
+#include "cli/apisetuputility.hpp"
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("api/setup", ApiSetupCommand);
+
+String ApiSetupCommand::GetDescription() const
+{
+ return "Setup for Icinga 2 API.";
+}
+
+String ApiSetupCommand::GetShortDescription() const
+{
+ return "setup for API";
+}
+
+ImpersonationLevel ApiSetupCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+void ApiSetupCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("cn", po::value<std::string>(), "The certificate's common name");
+}
+
+/**
+ * The entry point for the "api setup" CLI command.
+ *
+ * @returns An exit status.
+ */
+int ApiSetupCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String cn;
+
+ if (vm.count("cn")) {
+ cn = vm["cn"].as<std::string>();
+ } else {
+ cn = VariableUtility::GetVariable("NodeName");
+
+ if (cn.IsEmpty())
+ cn = Utility::GetFQDN();
+ }
+
+ if (!ApiSetupUtility::SetupMaster(cn, true))
+ return 1;
+
+ return 0;
+}
diff --git a/lib/cli/apisetupcommand.hpp b/lib/cli/apisetupcommand.hpp
new file mode 100644
index 0000000..be2693d
--- /dev/null
+++ b/lib/cli/apisetupcommand.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APISETUPCOMMAND_H
+#define APISETUPCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "api setup" command.
+ *
+ * @ingroup cli
+ */
+class ApiSetupCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ApiSetupCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+};
+
+}
+
+#endif /* APISETUPCOMMAND_H */
diff --git a/lib/cli/apisetuputility.cpp b/lib/cli/apisetuputility.cpp
new file mode 100644
index 0000000..8bdd767
--- /dev/null
+++ b/lib/cli/apisetuputility.cpp
@@ -0,0 +1,205 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/apisetuputility.hpp"
+#include "cli/nodeutility.hpp"
+#include "cli/featureutility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/case_conv.hpp>
+#include <iostream>
+#include <string>
+#include <fstream>
+#include <vector>
+
+using namespace icinga;
+
+String ApiSetupUtility::GetConfdPath()
+{
+ return Configuration::ConfigDir + "/conf.d";
+}
+
+String ApiSetupUtility::GetApiUsersConfPath()
+{
+ return ApiSetupUtility::GetConfdPath() + "/api-users.conf";
+}
+
+bool ApiSetupUtility::SetupMaster(const String& cn, bool prompt_restart)
+{
+ if (!SetupMasterCertificates(cn))
+ return false;
+
+ if (!SetupMasterApiUser())
+ return false;
+
+ if (!SetupMasterEnableApi())
+ return false;
+
+ if (!SetupMasterUpdateConstants(cn))
+ return false;
+
+ if (prompt_restart) {
+ std::cout << "Done.\n\n";
+ std::cout << "Now restart your Icinga 2 daemon to finish the installation!\n\n";
+ }
+
+ return true;
+}
+
+bool ApiSetupUtility::SetupMasterCertificates(const String& cn)
+{
+ Log(LogInformation, "cli", "Generating new CA.");
+
+ if (PkiUtility::NewCa() > 0)
+ Log(LogWarning, "cli", "Found CA, skipping and using the existing one.");
+
+ String pki_path = ApiListener::GetCertsDir();
+ Utility::MkDirP(pki_path, 0700);
+
+ String user = Configuration::RunAsUser;
+ String group = Configuration::RunAsGroup;
+
+ if (!Utility::SetFileOwnership(pki_path, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << pki_path << "'.";
+ }
+
+ String key = pki_path + "/" + cn + ".key";
+ String csr = pki_path + "/" + cn + ".csr";
+
+ if (Utility::PathExists(key)) {
+ Log(LogInformation, "cli")
+ << "Private key file '" << key << "' already exists, not generating new certificate.";
+ return true;
+ }
+
+ Log(LogInformation, "cli")
+ << "Generating new CSR in '" << csr << "'.";
+
+ if (Utility::PathExists(key))
+ NodeUtility::CreateBackupFile(key, true);
+ if (Utility::PathExists(csr))
+ NodeUtility::CreateBackupFile(csr);
+
+ if (PkiUtility::NewCert(cn, key, csr, "") > 0) {
+ Log(LogCritical, "cli", "Failed to create certificate signing request.");
+ return false;
+ }
+
+ /* Sign the CSR with the CA key */
+ String cert = pki_path + "/" + cn + ".crt";
+
+ Log(LogInformation, "cli")
+ << "Signing CSR with CA and writing certificate to '" << cert << "'.";
+
+ if (Utility::PathExists(cert))
+ NodeUtility::CreateBackupFile(cert);
+
+ if (PkiUtility::SignCsr(csr, cert) != 0) {
+ Log(LogCritical, "cli", "Could not sign CSR.");
+ return false;
+ }
+
+ /* Copy CA certificate to /etc/icinga2/pki */
+ String ca_path = ApiListener::GetCaDir();
+ String ca = ca_path + "/ca.crt";
+ String ca_key = ca_path + "/ca.key";
+ String target_ca = pki_path + "/ca.crt";
+
+ Log(LogInformation, "cli")
+ << "Copying CA certificate to '" << target_ca << "'.";
+
+ if (Utility::PathExists(target_ca))
+ NodeUtility::CreateBackupFile(target_ca);
+
+ /* does not overwrite existing files! */
+ Utility::CopyFile(ca, target_ca);
+
+ /* fix permissions: root -> icinga daemon user */
+ for (const String& file : { ca_path, ca, ca_key, target_ca, key, csr, cert }) {
+ if (!Utility::SetFileOwnership(file, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << file << "'.";
+ }
+ }
+
+ return true;
+}
+
+bool ApiSetupUtility::SetupMasterApiUser()
+{
+ if (!Utility::PathExists(GetConfdPath())) {
+ Log(LogWarning, "cli")
+ << "Path '" << GetConfdPath() << "' do not exist.";
+ Log(LogInformation, "cli")
+ << "Creating path '" << GetConfdPath() << "'.";
+
+ Utility::MkDirP(GetConfdPath(), 0755);
+ }
+
+ String api_username = "root"; // TODO make this available as cli parameter?
+ String api_password = RandomString(8);
+ String apiUsersPath = GetConfdPath() + "/api-users.conf";
+
+ if (Utility::PathExists(apiUsersPath)) {
+ Log(LogInformation, "cli")
+ << "API user config file '" << apiUsersPath << "' already exists, not creating config file.";
+ return true;
+ }
+
+ Log(LogInformation, "cli")
+ << "Adding new ApiUser '" << api_username << "' in '" << apiUsersPath << "'.";
+
+ NodeUtility::CreateBackupFile(apiUsersPath);
+
+ AtomicFile fp (apiUsersPath, 0644);
+
+ fp << "/**\n"
+ << " * The ApiUser objects are used for authentication against the API.\n"
+ << " */\n"
+ << "object ApiUser \"" << api_username << "\" {\n"
+ << " password = \"" << api_password << "\"\n"
+ << " // client_cn = \"\"\n"
+ << "\n"
+ << " permissions = [ \"*\" ]\n"
+ << "}\n";
+
+ fp.Commit();
+
+ return true;
+}
+
+bool ApiSetupUtility::SetupMasterEnableApi()
+{
+ /*
+ * Ensure the api-users.conf file is included, when conf.d inclusion is disabled.
+ */
+ if (!NodeUtility::GetConfigurationIncludeState("\"conf.d\"", true))
+ NodeUtility::UpdateConfiguration("\"conf.d/api-users.conf\"", true, false);
+
+ /*
+ * Enable the API feature
+ */
+ Log(LogInformation, "cli", "Enabling the 'api' feature.");
+
+ FeatureUtility::EnableFeatures({ "api" });
+
+ return true;
+}
+
+bool ApiSetupUtility::SetupMasterUpdateConstants(const String& cn)
+{
+ NodeUtility::UpdateConstant("NodeName", cn);
+ NodeUtility::UpdateConstant("ZoneName", cn);
+
+ return true;
+}
diff --git a/lib/cli/apisetuputility.hpp b/lib/cli/apisetuputility.hpp
new file mode 100644
index 0000000..d361446
--- /dev/null
+++ b/lib/cli/apisetuputility.hpp
@@ -0,0 +1,39 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef APISETUPUTILITY_H
+#define APISETUPUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "base/value.hpp"
+#include "base/string.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class ApiSetupUtility
+{
+public:
+ static bool SetupMaster(const String& cn, bool prompt_restart = false);
+
+ static bool SetupMasterCertificates(const String& cn);
+ static bool SetupMasterApiUser();
+ static bool SetupMasterEnableApi();
+ static bool SetupMasterUpdateConstants(const String& cn);
+
+ static String GetConfdPath();
+ static String GetApiUsersConfPath();
+
+private:
+ ApiSetupUtility();
+};
+
+}
+
+#endif /* APISETUPUTILITY_H */
diff --git a/lib/cli/calistcommand.cpp b/lib/cli/calistcommand.cpp
new file mode 100644
index 0000000..f693ad7
--- /dev/null
+++ b/lib/cli/calistcommand.cpp
@@ -0,0 +1,89 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/calistcommand.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/json.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("ca/list", CAListCommand);
+
+/**
+ * Provide a long CLI description sentence.
+ *
+ * @return text
+ */
+String CAListCommand::GetDescription() const
+{
+ return "Lists pending certificate signing requests.";
+}
+
+/**
+ * Provide a short CLI description.
+ *
+ * @return text
+ */
+String CAListCommand::GetShortDescription() const
+{
+ return "lists pending certificate signing requests";
+}
+
+/**
+ * Initialize available CLI parameters.
+ *
+ * @param visibleDesc Register visible parameters.
+ * @param hiddenDesc Register hidden parameters.
+ */
+void CAListCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("all", "List all certificate signing requests, including signed. Note: Old requests are automatically cleaned by Icinga after 1 week.")
+ ("removed", "List all removed CSRs (for use with 'ca restore')")
+ ("json", "encode output as JSON");
+}
+
+/**
+ * The entry point for the "ca list" CLI command.
+ *
+ * @return An exit status.
+ */
+int CAListCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ Dictionary::Ptr requests = PkiUtility::GetCertificateRequests(vm.count("removed"));
+
+ if (vm.count("json"))
+ std::cout << JsonEncode(requests);
+ else {
+ ObjectLock olock(requests);
+
+ std::cout << "Fingerprint | Timestamp | Signed | Subject\n";
+ std::cout << "-----------------------------------------------------------------|--------------------------|--------|--------\n";
+
+ for (auto& kv : requests) {
+ Dictionary::Ptr request = kv.second;
+
+ /* Skip signed requests by default. */
+ if (!vm.count("all") && request->Contains("cert_response"))
+ continue;
+
+ std::cout << kv.first
+ << " | "
+/* << Utility::FormatDateTime("%Y/%m/%d %H:%M:%S", request->Get("timestamp")) */
+ << request->Get("timestamp")
+ << " | "
+ << (request->Contains("cert_response") ? "*" : " ") << " "
+ << " | "
+ << request->Get("subject")
+ << "\n";
+ }
+ }
+
+ return 0;
+}
diff --git a/lib/cli/calistcommand.hpp b/lib/cli/calistcommand.hpp
new file mode 100644
index 0000000..ddf44d4
--- /dev/null
+++ b/lib/cli/calistcommand.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CALISTCOMMAND_H
+#define CALISTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "ca list" command.
+ *
+ * @ingroup cli
+ */
+class CAListCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CAListCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+private:
+ static void PrintRequest(const String& requestFile);
+};
+
+}
+
+#endif /* CALISTCOMMAND_H */
diff --git a/lib/cli/caremovecommand.cpp b/lib/cli/caremovecommand.cpp
new file mode 100644
index 0000000..d894494
--- /dev/null
+++ b/lib/cli/caremovecommand.cpp
@@ -0,0 +1,93 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/caremovecommand.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "remote/apilistener.hpp"
+
+using namespace icinga;
+
+REGISTER_CLICOMMAND("ca/remove", CARemoveCommand);
+
+/**
+ * Provide a long CLI description sentence.
+ *
+ * @return text
+ */
+String CARemoveCommand::GetDescription() const
+{
+ return "Removes an outstanding certificate request.";
+}
+
+/**
+ * Provide a short CLI description.
+ *
+ * @return text
+ */
+String CARemoveCommand::GetShortDescription() const
+{
+ return "removes an outstanding certificate request";
+}
+
+/**
+ * Define minimum arguments without key parameter.
+ *
+ * @return number of arguments
+ */
+int CARemoveCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+/**
+ * Impersonate as Icinga user.
+ *
+ * @return impersonate level
+ */
+ImpersonationLevel CARemoveCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "ca remove" CLI command.
+ *
+ * @returns An exit status.
+ */
+int CARemoveCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String fingerPrint = ap[0];
+ String requestFile = ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".json";
+
+ if (!Utility::PathExists(requestFile)) {
+ Log(LogCritical, "cli")
+ << "No request exists for fingerprint '" << fingerPrint << "'.";
+ return 1;
+ }
+
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestFile);
+ std::shared_ptr<X509> certRequest = StringToCertificate(request->Get("cert_request"));
+
+ if (!certRequest) {
+ Log(LogCritical, "cli", "Certificate request is invalid. Could not parse X.509 certificate for the 'cert_request' attribute.");
+ return 1;
+ }
+
+ String cn = GetCertificateCN(certRequest);
+
+ if (request->Contains("cert_response")) {
+ Log(LogCritical, "cli")
+ << "Certificate request for CN '" << cn << "' already signed, removal is not possible.";
+ return 1;
+ }
+
+ Utility::SaveJsonFile(ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".removed", 0600, request);
+
+ Utility::Remove(requestFile);
+
+ Log(LogInformation, "cli")
+ << "Certificate request for CN " << cn << " removed.";
+
+ return 0;
+}
diff --git a/lib/cli/caremovecommand.hpp b/lib/cli/caremovecommand.hpp
new file mode 100644
index 0000000..2da92d3
--- /dev/null
+++ b/lib/cli/caremovecommand.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CAREMOVECOMMAND_H
+#define CAREMOVECOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "ca remove" command.
+ *
+ * @ingroup cli
+ */
+class CARemoveCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CARemoveCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* CAREMOVECOMMAND_H */
diff --git a/lib/cli/carestorecommand.cpp b/lib/cli/carestorecommand.cpp
new file mode 100644
index 0000000..5020368
--- /dev/null
+++ b/lib/cli/carestorecommand.cpp
@@ -0,0 +1,88 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/carestorecommand.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "remote/apilistener.hpp"
+
+using namespace icinga;
+
+REGISTER_CLICOMMAND("ca/restore", CARestoreCommand);
+
+/**
+ * Provide a long CLI description sentence.
+ *
+ * @return text
+ */
+String CARestoreCommand::GetDescription() const
+{
+ return "Restores a previously removed certificate request.";
+}
+
+/**
+ * Provide a short CLI description.
+ *
+ * @return text
+ */
+String CARestoreCommand::GetShortDescription() const
+{
+ return "restores a removed certificate request";
+}
+
+/**
+ * Define minimum arguments without key parameter.
+ *
+ * @return number of arguments
+ */
+int CARestoreCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+/**
+ * Impersonate as Icinga user.
+ *
+ * @return impersonate level
+ */
+ImpersonationLevel CARestoreCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "ca restore" CLI command.
+ *
+ * @returns An exit status.
+ */
+int CARestoreCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String fingerPrint = ap[0];
+ String removedRequestFile = ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".removed";
+
+ if (!Utility::PathExists(removedRequestFile)) {
+ Log(LogCritical, "cli")
+ << "Cannot find removed fingerprint '" << fingerPrint << "', bailing out.";
+ return 1;
+ }
+
+ Dictionary::Ptr request = Utility::LoadJsonFile(removedRequestFile);
+ std::shared_ptr<X509> certRequest = StringToCertificate(request->Get("cert_request"));
+
+ if (!certRequest) {
+ Log(LogCritical, "cli", "Certificate request is invalid. Could not parse X.509 certificate for the 'cert_request' attribute.");
+ /* Purge the file when we know that it is broken. */
+ Utility::Remove(removedRequestFile);
+ return 1;
+ }
+
+ Utility::SaveJsonFile(ApiListener::GetCertificateRequestsDir() + "/" + fingerPrint + ".json", 0600, request);
+
+ Utility::Remove(removedRequestFile);
+
+ Log(LogInformation, "cli")
+ << "Restored certificate request for CN '" << GetCertificateCN(certRequest) << "', sign it with:\n"
+ << "\"icinga2 ca sign " << fingerPrint << "\"";
+
+ return 0;
+}
diff --git a/lib/cli/carestorecommand.hpp b/lib/cli/carestorecommand.hpp
new file mode 100644
index 0000000..74a27df
--- /dev/null
+++ b/lib/cli/carestorecommand.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CARESTORECOMMAND_H
+#define CARESTORECOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "ca restore" command.
+ *
+ * @ingroup cli
+ */
+class CARestoreCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CARestoreCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* CASTORECOMMAND_H */
diff --git a/lib/cli/casigncommand.cpp b/lib/cli/casigncommand.cpp
new file mode 100644
index 0000000..96d2c2c
--- /dev/null
+++ b/lib/cli/casigncommand.cpp
@@ -0,0 +1,108 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/casigncommand.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "remote/apilistener.hpp"
+
+using namespace icinga;
+
+REGISTER_CLICOMMAND("ca/sign", CASignCommand);
+
+/**
+ * Provide a long CLI description sentence.
+ *
+ * @return text
+ */
+String CASignCommand::GetDescription() const
+{
+ return "Signs an outstanding certificate request.";
+}
+
+/**
+ * Provide a short CLI description.
+ *
+ * @return text
+ */
+String CASignCommand::GetShortDescription() const
+{
+ return "signs an outstanding certificate request";
+}
+
+/**
+ * Define minimum arguments without key parameter.
+ *
+ * @return number of arguments
+ */
+int CASignCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+/**
+ * Impersonate as Icinga user.
+ *
+ * @return impersonate level
+ */
+ImpersonationLevel CASignCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "ca sign" CLI command.
+ *
+ * @return An exit status.
+ */
+int CASignCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String requestFile = ApiListener::GetCertificateRequestsDir() + "/" + ap[0] + ".json";
+
+ if (!Utility::PathExists(requestFile)) {
+ Log(LogCritical, "cli")
+ << "No request exists for fingerprint '" << ap[0] << "'.";
+ return 1;
+ }
+
+ Dictionary::Ptr request = Utility::LoadJsonFile(requestFile);
+
+ if (!request)
+ return 1;
+
+ String certRequestText = request->Get("cert_request");
+
+ std::shared_ptr<X509> certRequest = StringToCertificate(certRequestText);
+
+ if (!certRequest) {
+ Log(LogCritical, "cli", "Certificate request is invalid. Could not parse X.509 certificate for the 'cert_request' attribute.");
+ return 1;
+ }
+
+ std::shared_ptr<X509> certResponse = CreateCertIcingaCA(certRequest);
+
+ BIO *out = BIO_new(BIO_s_mem());
+ X509_NAME_print_ex(out, X509_get_subject_name(certRequest.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
+
+ char *data;
+ long length;
+ length = BIO_get_mem_data(out, &data);
+
+ String subject = String(data, data + length);
+ BIO_free(out);
+
+ if (!certResponse) {
+ Log(LogCritical, "cli")
+ << "Could not sign certificate for '" << subject << "'.";
+ return 1;
+ }
+
+ request->Set("cert_response", CertificateToString(certResponse));
+
+ Utility::SaveJsonFile(requestFile, 0600, request);
+
+ Log(LogInformation, "cli")
+ << "Signed certificate for '" << subject << "'.";
+
+ return 0;
+}
diff --git a/lib/cli/casigncommand.hpp b/lib/cli/casigncommand.hpp
new file mode 100644
index 0000000..0089af7
--- /dev/null
+++ b/lib/cli/casigncommand.hpp
@@ -0,0 +1,30 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CASIGNCOMMAND_H
+#define CASIGNCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "ca sign" command.
+ *
+ * @ingroup cli
+ */
+class CASignCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CASignCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* CASIGNCOMMAND_H */
diff --git a/lib/cli/clicommand.cpp b/lib/cli/clicommand.cpp
new file mode 100644
index 0000000..cfdce09
--- /dev/null
+++ b/lib/cli/clicommand.cpp
@@ -0,0 +1,373 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/clicommand.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/type.hpp"
+#include "base/serializer.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/trim.hpp>
+#include <boost/program_options.hpp>
+#include <algorithm>
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+std::vector<String> icinga::GetBashCompletionSuggestions(const String& type, const String& word)
+{
+ std::vector<String> result;
+
+#ifndef _WIN32
+ String bashArg = "compgen -A " + Utility::EscapeShellArg(type) + " " + Utility::EscapeShellArg(word);
+ String cmd = "bash -c " + Utility::EscapeShellArg(bashArg);
+
+ FILE *fp = popen(cmd.CStr(), "r");
+
+ char line[4096];
+ while (fgets(line, sizeof(line), fp)) {
+ String wline = line;
+ boost::algorithm::trim_right_if(wline, boost::is_any_of("\r\n"));
+ result.push_back(wline);
+ }
+
+ pclose(fp);
+
+ /* Append a slash if there's only one suggestion and it's a directory */
+ if ((type == "file" || type == "directory") && result.size() == 1) {
+ String path = result[0];
+
+ struct stat statbuf;
+ if (lstat(path.CStr(), &statbuf) >= 0) {
+ if (S_ISDIR(statbuf.st_mode)) {
+ result.clear(),
+ result.push_back(path + "/");
+ }
+ }
+ }
+#endif /* _WIN32 */
+
+ return result;
+}
+
+std::vector<String> icinga::GetFieldCompletionSuggestions(const Type::Ptr& type, const String& word)
+{
+ std::vector<String> result;
+
+ for (int i = 0; i < type->GetFieldCount(); i++) {
+ Field field = type->GetFieldInfo(i);
+
+ if (field.Attributes & FANoUserView)
+ continue;
+
+ if (strcmp(field.TypeName, "int") != 0 && strcmp(field.TypeName, "double") != 0
+ && strcmp(field.TypeName, "bool") != 0 && strcmp(field.TypeName, "String") != 0)
+ continue;
+
+ String fname = field.Name;
+
+ String suggestion = fname + "=";
+
+ if (suggestion.Find(word) == 0)
+ result.push_back(suggestion);
+ }
+
+ return result;
+}
+
+int CLICommand::GetMinArguments() const
+{
+ return 0;
+}
+
+int CLICommand::GetMaxArguments() const
+{
+ return GetMinArguments();
+}
+
+bool CLICommand::IsHidden() const
+{
+ return false;
+}
+
+bool CLICommand::IsDeprecated() const
+{
+ return false;
+}
+
+std::mutex& CLICommand::GetRegistryMutex()
+{
+ static std::mutex mtx;
+ return mtx;
+}
+
+std::map<std::vector<String>, CLICommand::Ptr>& CLICommand::GetRegistry()
+{
+ static std::map<std::vector<String>, CLICommand::Ptr> registry;
+ return registry;
+}
+
+CLICommand::Ptr CLICommand::GetByName(const std::vector<String>& name)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+
+ auto it = GetRegistry().find(name);
+
+ if (it == GetRegistry().end())
+ return nullptr;
+
+ return it->second;
+}
+
+void CLICommand::Register(const std::vector<String>& name, const CLICommand::Ptr& function)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+ GetRegistry()[name] = function;
+}
+
+void CLICommand::Unregister(const std::vector<String>& name)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+ GetRegistry().erase(name);
+}
+
+std::vector<String> CLICommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ return std::vector<String>();
+}
+
+std::vector<String> CLICommand::GetPositionalSuggestions(const String& word) const
+{
+ return std::vector<String>();
+}
+
+void CLICommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{ }
+
+ImpersonationLevel CLICommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+bool CLICommand::ParseCommand(int argc, char **argv, po::options_description& visibleDesc,
+ po::options_description& hiddenDesc,
+ po::positional_options_description& positionalDesc,
+ po::variables_map& vm, String& cmdname, CLICommand::Ptr& command, bool autocomplete)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+
+ typedef std::map<std::vector<String>, CLICommand::Ptr>::value_type CLIKeyValue;
+
+ std::vector<String> best_match;
+ int arg_end = 0;
+ bool tried_command = false;
+
+ for (const CLIKeyValue& kv : GetRegistry()) {
+ const std::vector<String>& vname = kv.first;
+
+ std::vector<String>::size_type i;
+ int k;
+ for (i = 0, k = 1; i < vname.size() && k < argc; i++, k++) {
+ if (strncmp(argv[k], "-", 1) == 0 || strncmp(argv[k], "--", 2) == 0) {
+ i--;
+ continue;
+ }
+
+ tried_command = true;
+
+ if (vname[i] != argv[k])
+ break;
+
+ if (i >= best_match.size())
+ best_match.push_back(vname[i]);
+
+ if (i == vname.size() - 1) {
+ cmdname = boost::algorithm::join(vname, " ");
+ command = kv.second;
+ arg_end = k;
+ goto found_command;
+ }
+ }
+ }
+
+found_command:
+ lock.unlock();
+
+ if (command) {
+ po::options_description vdesc("Command options");
+ command->InitParameters(vdesc, hiddenDesc);
+ visibleDesc.add(vdesc);
+ }
+
+ if (autocomplete || (tried_command && !command))
+ return true;
+
+ po::options_description adesc;
+ adesc.add(visibleDesc);
+ adesc.add(hiddenDesc);
+
+ if (command && command->IsDeprecated()) {
+ std::cerr << ConsoleColorTag(Console_ForegroundRed | Console_Bold)
+ << "Warning: CLI command '" << cmdname << "' is DEPRECATED! Please read the Changelog."
+ << ConsoleColorTag(Console_Normal) << std::endl << std::endl;
+ }
+
+ po::store(po::command_line_parser(argc - arg_end, argv + arg_end).options(adesc).positional(positionalDesc).run(), vm);
+ po::notify(vm);
+
+ return true;
+}
+
+void CLICommand::ShowCommands(int argc, char **argv, po::options_description *visibleDesc,
+ po::options_description *hiddenDesc,
+ ArgumentCompletionCallback globalArgCompletionCallback,
+ bool autocomplete, int autoindex)
+{
+ std::unique_lock<std::mutex> lock(GetRegistryMutex());
+
+ typedef std::map<std::vector<String>, CLICommand::Ptr>::value_type CLIKeyValue;
+
+ std::vector<String> best_match;
+ int arg_begin = 0;
+ CLICommand::Ptr command;
+
+ for (const CLIKeyValue& kv : GetRegistry()) {
+ const std::vector<String>& vname = kv.first;
+
+ arg_begin = 0;
+
+ std::vector<String>::size_type i;
+ int k;
+ for (i = 0, k = 1; i < vname.size() && k < argc; i++, k++) {
+ if (strcmp(argv[k], "--no-stack-rlimit") == 0 || strcmp(argv[k], "--autocomplete") == 0 || strcmp(argv[k], "--scm") == 0) {
+ i--;
+ arg_begin++;
+ continue;
+ }
+
+ if (autocomplete && static_cast<int>(i) >= autoindex - 1)
+ break;
+
+ if (vname[i] != argv[k])
+ break;
+
+ if (i >= best_match.size()) {
+ best_match.push_back(vname[i]);
+ }
+
+ if (i == vname.size() - 1) {
+ command = kv.second;
+ break;
+ }
+ }
+ }
+
+ String aword;
+
+ if (autocomplete) {
+ if (autoindex < argc)
+ aword = argv[autoindex];
+
+ if (autoindex - 1 > static_cast<int>(best_match.size()) && !command)
+ return;
+ } else
+ std::cout << "Supported commands: " << std::endl;
+
+ for (const CLIKeyValue& kv : GetRegistry()) {
+ const std::vector<String>& vname = kv.first;
+
+ if (vname.size() < best_match.size() || kv.second->IsHidden())
+ continue;
+
+ bool match = true;
+
+ for (std::vector<String>::size_type i = 0; i < best_match.size(); i++) {
+ if (vname[i] != best_match[i]) {
+ match = false;
+ break;
+ }
+ }
+
+ if (!match)
+ continue;
+
+ if (autocomplete) {
+ String cname;
+
+ if (autoindex - 1 < static_cast<int>(vname.size())) {
+ cname = vname[autoindex - 1];
+
+ if (cname.Find(aword) == 0)
+ std::cout << cname << "\n";
+ }
+ } else {
+ std::cout << " * " << boost::algorithm::join(vname, " ")
+ << " (" << kv.second->GetShortDescription() << ")"
+ << (kv.second->IsDeprecated() ? " (DEPRECATED)" : "") << std::endl;
+ }
+ }
+
+ if (!autocomplete)
+ std::cout << std::endl;
+
+ if (command && autocomplete) {
+ String aname, prefix, pword;
+ const po::option_description *odesc;
+
+ if (autoindex - 2 >= 0 && strcmp(argv[autoindex - 1], "=") == 0 && strstr(argv[autoindex - 2], "--") == argv[autoindex - 2]) {
+ aname = argv[autoindex - 2] + 2;
+ pword = aword;
+ } else if (autoindex - 1 >= 0 && argv[autoindex - 1][0] == '-' && argv[autoindex - 1][1] == '-') {
+ aname = argv[autoindex - 1] + 2;
+ pword = aword;
+
+ if (pword == "=")
+ pword = "";
+ } else if (autoindex - 1 >= 0 && argv[autoindex - 1][0] == '-' && argv[autoindex - 1][1] != '-') {
+ aname = argv[autoindex - 1];
+ pword = aword;
+
+ if (pword == "=")
+ pword = "";
+ } else if (aword.GetLength() > 1 && aword[0] == '-' && aword[1] != '-') {
+ aname = aword.SubStr(0, 2);
+ prefix = aname;
+ pword = aword.SubStr(2);
+ } else {
+ goto complete_option;
+ }
+
+ odesc = visibleDesc->find_nothrow(aname, false);
+
+ if (!odesc)
+ return;
+
+ if (odesc->semantic()->min_tokens() == 0)
+ goto complete_option;
+
+ for (const String& suggestion : globalArgCompletionCallback(odesc->long_name(), pword)) {
+ std::cout << prefix << suggestion << "\n";
+ }
+
+ for (const String& suggestion : command->GetArgumentSuggestions(odesc->long_name(), pword)) {
+ std::cout << prefix << suggestion << "\n";
+ }
+
+ return;
+
+complete_option:
+ for (const boost::shared_ptr<po::option_description>& odesc : visibleDesc->options()) {
+ String cname = "--" + odesc->long_name();
+
+ if (cname.Find(aword) == 0)
+ std::cout << cname << "\n";
+ }
+
+ for (const String& suggestion : command->GetPositionalSuggestions(aword)) {
+ std::cout << suggestion << "\n";
+ }
+ }
+
+ return;
+}
diff --git a/lib/cli/clicommand.hpp b/lib/cli/clicommand.hpp
new file mode 100644
index 0000000..ce58b54
--- /dev/null
+++ b/lib/cli/clicommand.hpp
@@ -0,0 +1,79 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CLICOMMAND_H
+#define CLICOMMAND_H
+
+#include "cli/i2-cli.hpp"
+#include "base/value.hpp"
+#include "base/utility.hpp"
+#include "base/type.hpp"
+#include <vector>
+#include <boost/program_options.hpp>
+
+namespace icinga
+{
+
+std::vector<String> GetBashCompletionSuggestions(const String& type, const String& word);
+std::vector<String> GetFieldCompletionSuggestions(const Type::Ptr& type, const String& word);
+
+enum ImpersonationLevel
+{
+ ImpersonateNone,
+ ImpersonateRoot,
+ ImpersonateIcinga
+};
+
+/**
+ * A CLI command.
+ *
+ * @ingroup base
+ */
+class CLICommand : public Object
+{
+public:
+ DECLARE_PTR_TYPEDEFS(CLICommand);
+
+ typedef std::vector<String>(*ArgumentCompletionCallback)(const String&, const String&);
+
+ virtual String GetDescription() const = 0;
+ virtual String GetShortDescription() const = 0;
+ virtual int GetMinArguments() const;
+ virtual int GetMaxArguments() const;
+ virtual bool IsHidden() const;
+ virtual bool IsDeprecated() const;
+ virtual void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const;
+ virtual ImpersonationLevel GetImpersonationLevel() const;
+ virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const = 0;
+ virtual std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const;
+ virtual std::vector<String> GetPositionalSuggestions(const String& word) const;
+
+ static CLICommand::Ptr GetByName(const std::vector<String>& name);
+ static void Register(const std::vector<String>& name, const CLICommand::Ptr& command);
+ static void Unregister(const std::vector<String>& name);
+
+ static bool ParseCommand(int argc, char **argv, boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc,
+ boost::program_options::positional_options_description& positionalDesc,
+ boost::program_options::variables_map& vm, String& cmdname, CLICommand::Ptr& command, bool autocomplete);
+
+ static void ShowCommands(int argc, char **argv,
+ boost::program_options::options_description *visibleDesc = nullptr,
+ boost::program_options::options_description *hiddenDesc = nullptr,
+ ArgumentCompletionCallback globalArgCompletionCallback = nullptr,
+ bool autocomplete = false, int autoindex = -1);
+
+private:
+ static std::mutex& GetRegistryMutex();
+ static std::map<std::vector<String>, CLICommand::Ptr>& GetRegistry();
+};
+
+#define REGISTER_CLICOMMAND(name, klass) \
+ INITIALIZE_ONCE([]() { \
+ std::vector<String> vname = String(name).Split("/"); \
+ CLICommand::Register(vname, new klass()); \
+ })
+
+}
+
+#endif /* CLICOMMAND_H */
diff --git a/lib/cli/consolecommand.cpp b/lib/cli/consolecommand.cpp
new file mode 100644
index 0000000..78906bb
--- /dev/null
+++ b/lib/cli/consolecommand.cpp
@@ -0,0 +1,723 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/consolecommand.hpp"
+#include "config/configcompiler.hpp"
+#include "remote/consolehandler.hpp"
+#include "remote/url.hpp"
+#include "base/configwriter.hpp"
+#include "base/serializer.hpp"
+#include "base/json.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/objectlock.hpp"
+#include "base/unixsocket.hpp"
+#include "base/utility.hpp"
+#include "base/networkstream.hpp"
+#include "base/defer.hpp"
+#include "base/io-engine.hpp"
+#include "base/stream.hpp"
+#include "base/tcpsocket.hpp" /* include global icinga::Connect */
+#include <base/base64.hpp>
+#include "base/exception.hpp"
+#include <boost/asio/ssl/context.hpp>
+#include <boost/beast/core/flat_buffer.hpp>
+#include <boost/beast/http/field.hpp>
+#include <boost/beast/http/message.hpp>
+#include <boost/beast/http/parser.hpp>
+#include <boost/beast/http/read.hpp>
+#include <boost/beast/http/status.hpp>
+#include <boost/beast/http/string_body.hpp>
+#include <boost/beast/http/verb.hpp>
+#include <boost/beast/http/write.hpp>
+#include <iostream>
+#include <fstream>
+
+
+#ifdef HAVE_EDITLINE
+#include "cli/editline.hpp"
+#endif /* HAVE_EDITLINE */
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+static ScriptFrame *l_ScriptFrame;
+static Url::Ptr l_Url;
+static Shared<AsioTlsStream>::Ptr l_TlsStream;
+static String l_Session;
+
+REGISTER_CLICOMMAND("console", ConsoleCommand);
+
+INITIALIZE_ONCE(&ConsoleCommand::StaticInitialize);
+
+extern "C" void dbg_spawn_console()
+{
+ ScriptFrame frame(true);
+ ConsoleCommand::RunScriptConsole(frame);
+}
+
+extern "C" void dbg_inspect_value(const Value& value)
+{
+ ConfigWriter::EmitValue(std::cout, 1, Serialize(value, 0));
+ std::cout << std::endl;
+}
+
+extern "C" void dbg_inspect_object(Object *obj)
+{
+ Object::Ptr objr = obj;
+ dbg_inspect_value(objr);
+}
+
+extern "C" void dbg_eval(const char *text)
+{
+ std::unique_ptr<Expression> expr;
+
+ try {
+ ScriptFrame frame(true);
+ expr = ConfigCompiler::CompileText("<dbg>", text);
+ Value result = Serialize(expr->Evaluate(frame), 0);
+ dbg_inspect_value(result);
+ } catch (const std::exception& ex) {
+ std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
+ }
+}
+
+extern "C" void dbg_eval_with_value(const Value& value, const char *text)
+{
+ std::unique_ptr<Expression> expr;
+
+ try {
+ ScriptFrame frame(true);
+ frame.Locals = new Dictionary({
+ { "arg", value }
+ });
+ expr = ConfigCompiler::CompileText("<dbg>", text);
+ Value result = Serialize(expr->Evaluate(frame), 0);
+ dbg_inspect_value(result);
+ } catch (const std::exception& ex) {
+ std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
+ }
+}
+
+extern "C" void dbg_eval_with_object(Object *object, const char *text)
+{
+ std::unique_ptr<Expression> expr;
+
+ try {
+ ScriptFrame frame(true);
+ frame.Locals = new Dictionary({
+ { "arg", object }
+ });
+ expr = ConfigCompiler::CompileText("<dbg>", text);
+ Value result = Serialize(expr->Evaluate(frame), 0);
+ dbg_inspect_value(result);
+ } catch (const std::exception& ex) {
+ std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
+ }
+}
+
+void ConsoleCommand::BreakpointHandler(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di)
+{
+ static std::mutex mutex;
+ std::unique_lock<std::mutex> lock(mutex);
+
+ if (!Application::GetScriptDebuggerEnabled())
+ return;
+
+ if (ex && ex->IsHandledByDebugger())
+ return;
+
+ std::cout << "Breakpoint encountered.\n";
+
+ if (ex) {
+ std::cout << "Exception: " << DiagnosticInformation(*ex) << "\n";
+ ex->SetHandledByDebugger(true);
+ } else
+ ShowCodeLocation(std::cout, di);
+
+ std::cout << "You can inspect expressions (such as variables) by entering them at the prompt.\n"
+ << "To leave the debugger and continue the program use \"$continue\".\n"
+ << "For further commands see \"$help\".\n";
+
+#ifdef HAVE_EDITLINE
+ rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper;
+ rl_completion_append_character = '\0';
+#endif /* HAVE_EDITLINE */
+
+ ConsoleCommand::RunScriptConsole(frame);
+}
+
+void ConsoleCommand::StaticInitialize()
+{
+ Expression::OnBreakpoint.connect(&ConsoleCommand::BreakpointHandler);
+}
+
+String ConsoleCommand::GetDescription() const
+{
+ return "Interprets Icinga script expressions.";
+}
+
+String ConsoleCommand::GetShortDescription() const
+{
+ return "Icinga console";
+}
+
+ImpersonationLevel ConsoleCommand::GetImpersonationLevel() const
+{
+ return ImpersonateNone;
+}
+
+void ConsoleCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("connect,c", po::value<std::string>(), "connect to an Icinga 2 instance")
+ ("eval,e", po::value<std::string>(), "evaluate expression and terminate")
+ ("file,r", po::value<std::string>(), "evaluate a file and terminate")
+ ("syntax-only", "only validate syntax (requires --eval or --file)")
+ ("sandbox", "enable sandbox mode")
+ ;
+}
+
+#ifdef HAVE_EDITLINE
+char *ConsoleCommand::ConsoleCompleteHelper(const char *word, int state)
+{
+ static std::vector<String> matches;
+
+ if (state == 0) {
+ if (!l_Url)
+ matches = ConsoleHandler::GetAutocompletionSuggestions(word, *l_ScriptFrame);
+ else {
+ Array::Ptr suggestions;
+
+ /* Remote debug console. */
+ try {
+ suggestions = AutoCompleteScript(l_Session, word, l_ScriptFrame->Sandboxed);
+ } catch (...) {
+ return nullptr; //Errors are just ignored here.
+ }
+
+ matches.clear();
+
+ ObjectLock olock(suggestions);
+ std::copy(suggestions->Begin(), suggestions->End(), std::back_inserter(matches));
+ }
+ }
+
+ if (state >= static_cast<int>(matches.size()))
+ return nullptr;
+
+ return strdup(matches[state].CStr());
+}
+#endif /* HAVE_EDITLINE */
+
+/**
+ * The entry point for the "console" CLI command.
+ *
+ * @returns An exit status.
+ */
+int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::string>& ap) const
+{
+#ifdef HAVE_EDITLINE
+ rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper;
+ rl_completion_append_character = '\0';
+#endif /* HAVE_EDITLINE */
+
+ String addr, session;
+ ScriptFrame scriptFrame(true);
+
+ session = Utility::NewUniqueID();
+
+ if (vm.count("sandbox"))
+ scriptFrame.Sandboxed = true;
+
+ scriptFrame.Self = scriptFrame.Locals;
+
+ if (!vm.count("eval") && !vm.count("file"))
+ std::cout << "Icinga 2 (version: " << Application::GetAppVersion() << ")\n"
+ << "Type $help to view available commands.\n";
+
+ String addrEnv = Utility::GetFromEnvironment("ICINGA2_API_URL");
+ if (!addrEnv.IsEmpty())
+ addr = addrEnv;
+
+ /* Initialize remote connect parameters. */
+ if (vm.count("connect")) {
+ addr = vm["connect"].as<std::string>();
+
+ try {
+ l_Url = new Url(addr);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ConsoleCommand", ex.what());
+ return EXIT_FAILURE;
+ }
+
+ String usernameEnv = Utility::GetFromEnvironment("ICINGA2_API_USERNAME");
+ String passwordEnv = Utility::GetFromEnvironment("ICINGA2_API_PASSWORD");
+
+ if (!usernameEnv.IsEmpty())
+ l_Url->SetUsername(usernameEnv);
+ if (!passwordEnv.IsEmpty())
+ l_Url->SetPassword(passwordEnv);
+
+ if (l_Url->GetPort().IsEmpty())
+ l_Url->SetPort("5665");
+
+ /* User passed --connect and wants to run the expression via REST API.
+ * Evaluate this now before any user input happens.
+ */
+ try {
+ l_TlsStream = ConsoleCommand::Connect();
+ } catch (const std::exception& ex) {
+ return EXIT_FAILURE;
+ }
+ }
+
+ String command;
+ bool syntaxOnly = false;
+
+ if (vm.count("syntax-only")) {
+ if (vm.count("eval") || vm.count("file"))
+ syntaxOnly = true;
+ else {
+ std::cerr << "The option --syntax-only can only be used in combination with --eval or --file." << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+
+ String commandFileName;
+
+ if (vm.count("eval"))
+ command = vm["eval"].as<std::string>();
+ else if (vm.count("file")) {
+ commandFileName = vm["file"].as<std::string>();
+
+ try {
+ std::ifstream fp(commandFileName.CStr());
+ fp.exceptions(std::ifstream::failbit | std::ifstream::badbit);
+ command = String(std::istreambuf_iterator<char>(fp), std::istreambuf_iterator<char>());
+ } catch (const std::exception&) {
+ std::cerr << "Could not read file '" << commandFileName << "'." << std::endl;
+ return EXIT_FAILURE;
+ }
+ }
+
+ return RunScriptConsole(scriptFrame, addr, session, command, commandFileName, syntaxOnly);
+}
+
+int ConsoleCommand::RunScriptConsole(ScriptFrame& scriptFrame, const String& connectAddr, const String& session,
+ const String& commandOnce, const String& commandOnceFileName, bool syntaxOnly)
+{
+ std::map<String, String> lines;
+ int next_line = 1;
+
+#ifdef HAVE_EDITLINE
+ String homeEnv = Utility::GetFromEnvironment("HOME");
+
+ String historyPath;
+ std::fstream historyfp;
+
+ if (!homeEnv.IsEmpty()) {
+ historyPath = String(homeEnv) + "/.icinga2_history";
+
+ historyfp.open(historyPath.CStr(), std::fstream::in);
+
+ String line;
+ while (std::getline(historyfp, line.GetData()))
+ add_history(line.CStr());
+
+ historyfp.close();
+ }
+#endif /* HAVE_EDITLINE */
+
+ l_ScriptFrame = &scriptFrame;
+ l_Session = session;
+
+ while (std::cin.good()) {
+ String fileName;
+
+ if (commandOnceFileName.IsEmpty())
+ fileName = "<" + Convert::ToString(next_line) + ">";
+ else
+ fileName = commandOnceFileName;
+
+ next_line++;
+
+ bool continuation = false;
+ std::string command;
+
+incomplete:
+ std::string line;
+
+ if (commandOnce.IsEmpty()) {
+#ifdef HAVE_EDITLINE
+ std::ostringstream promptbuf;
+ std::ostream& os = promptbuf;
+#else /* HAVE_EDITLINE */
+ std::ostream& os = std::cout;
+#endif /* HAVE_EDITLINE */
+
+ os << fileName;
+
+ if (!continuation)
+ os << " => ";
+ else
+ os << " .. ";
+
+#ifdef HAVE_EDITLINE
+ String prompt = promptbuf.str();
+
+ char *cline;
+ cline = readline(prompt.CStr());
+
+ if (!cline)
+ break;
+
+ if (commandOnce.IsEmpty() && cline[0] != '\0') {
+ add_history(cline);
+
+ if (!historyPath.IsEmpty()) {
+ historyfp.open(historyPath.CStr(), std::fstream::out | std::fstream::app);
+ historyfp << cline << "\n";
+ historyfp.close();
+ }
+ }
+
+ line = cline;
+
+ free(cline);
+#else /* HAVE_EDITLINE */
+ std::getline(std::cin, line);
+#endif /* HAVE_EDITLINE */
+ } else
+ line = commandOnce;
+
+ if (!line.empty() && line[0] == '$') {
+ if (line == "$continue" || line == "$quit" || line == "$exit")
+ break;
+ else if (line == "$help")
+ std::cout << "Welcome to the Icinga 2 debug console.\n"
+ "Usable commands:\n"
+ " $continue Continue running Icinga 2 (script debugger).\n"
+ " $quit, $exit Stop debugging and quit the console.\n"
+ " $help Print this help.\n\n"
+ "For more information on how to use this console, please consult the documentation at https://icinga.com/docs\n";
+ else
+ std::cout << "Unknown debugger command: " << line << "\n";
+
+ continue;
+ }
+
+ if (!command.empty())
+ command += "\n";
+
+ command += line;
+
+ std::unique_ptr<Expression> expr;
+
+ try {
+ lines[fileName] = command;
+
+ Value result;
+
+ /* Local debug console. */
+ if (connectAddr.IsEmpty()) {
+ expr = ConfigCompiler::CompileText(fileName, command);
+
+ /* This relies on the fact that - for syntax errors - CompileText()
+ * returns an AST where the top-level expression is a 'throw'. */
+ if (!syntaxOnly || dynamic_cast<ThrowExpression *>(expr.get())) {
+ if (syntaxOnly)
+ std::cerr << " => " << command << std::endl;
+ result = Serialize(expr->Evaluate(scriptFrame), 0);
+ } else
+ result = true;
+ } else {
+ /* Remote debug console. */
+ try {
+ result = ExecuteScript(l_Session, command, scriptFrame.Sandboxed);
+ } catch (const ScriptError&) {
+ /* Re-throw the exception for the outside try-catch block. */
+ boost::rethrow_exception(boost::current_exception());
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "ConsoleCommand")
+ << "HTTP query failed: " << ex.what();
+
+#ifdef HAVE_EDITLINE
+ /* Ensures that the terminal state is reset */
+ rl_deprep_terminal();
+#endif /* HAVE_EDITLINE */
+
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (commandOnce.IsEmpty()) {
+ std::cout << ConsoleColorTag(Console_ForegroundCyan);
+ ConfigWriter::EmitValue(std::cout, 1, result);
+ std::cout << ConsoleColorTag(Console_Normal) << "\n";
+ } else {
+ std::cout << JsonEncode(result) << "\n";
+ break;
+ }
+ } catch (const ScriptError& ex) {
+ if (ex.IsIncompleteExpression() && commandOnce.IsEmpty()) {
+ continuation = true;
+ goto incomplete;
+ }
+
+ DebugInfo di = ex.GetDebugInfo();
+
+ if (commandOnceFileName.IsEmpty() && lines.find(di.Path) != lines.end()) {
+ String text = lines[di.Path];
+
+ std::vector<String> ulines = text.Split("\n");
+
+ for (decltype(ulines.size()) i = 1; i <= ulines.size(); i++) {
+ int start, len;
+
+ if (i == (decltype(i))di.FirstLine)
+ start = di.FirstColumn;
+ else
+ start = 0;
+
+ if (i == (decltype(i))di.LastLine)
+ len = di.LastColumn - di.FirstColumn + 1;
+ else
+ len = ulines[i - 1].GetLength();
+
+ int offset;
+
+ if (di.Path != fileName) {
+ std::cout << di.Path << ": " << ulines[i - 1] << "\n";
+ offset = 2;
+ } else
+ offset = 4;
+
+ if (i >= (decltype(i))di.FirstLine && i <= (decltype(i))di.LastLine) {
+ std::cout << String(di.Path.GetLength() + offset, ' ');
+ std::cout << String(start, ' ') << String(len, '^') << "\n";
+ }
+ }
+ } else {
+ ShowCodeLocation(std::cout, di);
+ }
+
+ std::cout << ex.what() << "\n";
+
+ if (!commandOnce.IsEmpty())
+ return EXIT_FAILURE;
+ } catch (const std::exception& ex) {
+ std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
+
+ if (!commandOnce.IsEmpty())
+ return EXIT_FAILURE;
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
+
+/**
+ * Connects to host:port and performs a TLS shandshake
+ *
+ * @returns AsioTlsStream pointer for future HTTP connections.
+ */
+Shared<AsioTlsStream>::Ptr ConsoleCommand::Connect()
+{
+ Shared<boost::asio::ssl::context>::Ptr sslContext;
+
+ try {
+ sslContext = MakeAsioSslContext(Empty, Empty, Empty); //TODO: Add support for cert, key, ca parameters
+ } catch(const std::exception& ex) {
+ Log(LogCritical, "DebugConsole")
+ << "Cannot make SSL context: " << ex.what();
+ throw;
+ }
+
+ String host = l_Url->GetHost();
+ String port = l_Url->GetPort();
+
+ Shared<AsioTlsStream>::Ptr stream = Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *sslContext, host);
+
+ try {
+ icinga::Connect(stream->lowest_layer(), host, port);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "DebugConsole")
+ << "Cannot connect to REST API on host '" << host << "' port '" << port << "': " << ex.what();
+ throw;
+ }
+
+ auto& tlsStream (stream->next_layer());
+
+ try {
+ tlsStream.handshake(tlsStream.client);
+ } catch (const std::exception& ex) {
+ Log(LogWarning, "DebugConsole")
+ << "TLS handshake with host '" << host << "' failed: " << ex.what();
+ throw;
+ }
+
+ return stream;
+}
+
+/**
+ * Sends the request via REST API and returns the parsed response.
+ *
+ * @param tlsStream Caller must prepare TLS stream/handshake.
+ * @param url Fully prepared Url object.
+ * @return A dictionary decoded from JSON.
+ */
+Dictionary::Ptr ConsoleCommand::SendRequest()
+{
+ namespace beast = boost::beast;
+ namespace http = beast::http;
+
+ l_TlsStream = ConsoleCommand::Connect();
+
+ Defer s ([&]() {
+ l_TlsStream->next_layer().shutdown();
+ });
+
+ http::request<http::string_body> request(http::verb::post, std::string(l_Url->Format(false)), 10);
+
+ request.set(http::field::user_agent, "Icinga/DebugConsole/" + Application::GetAppVersion());
+ request.set(http::field::host, l_Url->GetHost() + ":" + l_Url->GetPort());
+
+ request.set(http::field::accept, "application/json");
+ request.set(http::field::authorization, "Basic " + Base64::Encode(l_Url->GetUsername() + ":" + l_Url->GetPassword()));
+
+ try {
+ http::write(*l_TlsStream, request);
+ l_TlsStream->flush();
+ } catch (const std::exception &ex) {
+ Log(LogWarning, "DebugConsole")
+ << "Cannot write HTTP request to REST API at URL '" << l_Url->Format(true) << "': " << ex.what();
+ throw;
+ }
+
+ http::parser<false, http::string_body> parser;
+ beast::flat_buffer buf;
+
+ try {
+ http::read(*l_TlsStream, buf, parser);
+ } catch (const std::exception &ex) {
+ Log(LogWarning, "DebugConsole")
+ << "Failed to parse HTTP response from REST API at URL '" << l_Url->Format(true) << "': " << ex.what();
+ throw;
+ }
+
+ auto &response(parser.get());
+
+ /* Handle HTTP errors first. */
+ if (response.result() != http::status::ok) {
+ String message = "HTTP request failed; Code: " + Convert::ToString(response.result())
+ + "; Body: " + response.body();
+ BOOST_THROW_EXCEPTION(ScriptError(message));
+ }
+
+ Dictionary::Ptr jsonResponse;
+ auto &body(response.body());
+
+ //Log(LogWarning, "Console")
+ // << "Got response: " << response.body();
+
+ try {
+ jsonResponse = JsonDecode(body);
+ } catch (...) {
+ String message = "Cannot parse JSON response body: " + response.body();
+ BOOST_THROW_EXCEPTION(ScriptError(message));
+ }
+
+ return jsonResponse;
+}
+
+/**
+ * Executes the DSL script via HTTP and returns HTTP and user errors.
+ *
+ * @param session Local session handler.
+ * @param command The DSL string.
+ * @param sandboxed Whether to run this sandboxed.
+ * @return Result value, also contains user errors.
+ */
+Value ConsoleCommand::ExecuteScript(const String& session, const String& command, bool sandboxed)
+{
+ /* Extend the url parameters for the request. */
+ l_Url->SetPath({"v1", "console", "execute-script"});
+
+ l_Url->SetQuery({
+ {"session", session},
+ {"command", command},
+ {"sandboxed", sandboxed ? "1" : "0"}
+ });
+
+ Dictionary::Ptr jsonResponse = SendRequest();
+
+ /* Extract the result, and handle user input errors too. */
+ Array::Ptr results = jsonResponse->Get("results");
+ Value result;
+
+ if (results && results->GetLength() > 0) {
+ Dictionary::Ptr resultInfo = results->Get(0);
+
+ if (resultInfo->Get("code") >= 200 && resultInfo->Get("code") <= 299) {
+ result = resultInfo->Get("result");
+ } else {
+ String errorMessage = resultInfo->Get("status");
+
+ DebugInfo di;
+ Dictionary::Ptr debugInfo = resultInfo->Get("debug_info");
+
+ if (debugInfo) {
+ di.Path = debugInfo->Get("path");
+ di.FirstLine = debugInfo->Get("first_line");
+ di.FirstColumn = debugInfo->Get("first_column");
+ di.LastLine = debugInfo->Get("last_line");
+ di.LastColumn = debugInfo->Get("last_column");
+ }
+
+ bool incompleteExpression = resultInfo->Get("incomplete_expression");
+ BOOST_THROW_EXCEPTION(ScriptError(errorMessage, di, incompleteExpression));
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Executes the auto completion script via HTTP and returns HTTP and user errors.
+ *
+ * @param session Local session handler.
+ * @param command The auto completion string.
+ * @param sandboxed Whether to run this sandboxed.
+ * @return Result value, also contains user errors.
+ */
+Array::Ptr ConsoleCommand::AutoCompleteScript(const String& session, const String& command, bool sandboxed)
+{
+ /* Extend the url parameters for the request. */
+ l_Url->SetPath({ "v1", "console", "auto-complete-script" });
+
+ l_Url->SetQuery({
+ {"session", session},
+ {"command", command},
+ {"sandboxed", sandboxed ? "1" : "0"}
+ });
+
+ Dictionary::Ptr jsonResponse = SendRequest();
+
+ /* Extract the result, and handle user input errors too. */
+ Array::Ptr results = jsonResponse->Get("results");
+ Array::Ptr suggestions;
+
+ if (results && results->GetLength() > 0) {
+ Dictionary::Ptr resultInfo = results->Get(0);
+
+ if (resultInfo->Get("code") >= 200 && resultInfo->Get("code") <= 299) {
+ suggestions = resultInfo->Get("suggestions");
+ } else {
+ String errorMessage = resultInfo->Get("status");
+ BOOST_THROW_EXCEPTION(ScriptError(errorMessage));
+ }
+ }
+
+ return suggestions;
+}
diff --git a/lib/cli/consolecommand.hpp b/lib/cli/consolecommand.hpp
new file mode 100644
index 0000000..631ec21
--- /dev/null
+++ b/lib/cli/consolecommand.hpp
@@ -0,0 +1,61 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef CONSOLECOMMAND_H
+#define CONSOLECOMMAND_H
+
+#include "cli/clicommand.hpp"
+#include "base/exception.hpp"
+#include "base/scriptframe.hpp"
+#include "base/tlsstream.hpp"
+#include "remote/url.hpp"
+#include <condition_variable>
+
+
+namespace icinga
+{
+
+/**
+ * The "console" CLI command.
+ *
+ * @ingroup cli
+ */
+class ConsoleCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ConsoleCommand);
+
+ static void StaticInitialize();
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+ static int RunScriptConsole(ScriptFrame& scriptFrame, const String& connectAddr = String(),
+ const String& session = String(), const String& commandOnce = String(), const String& commandOnceFileName = String(),
+ bool syntaxOnly = false);
+
+private:
+ mutable std::mutex m_Mutex;
+ mutable std::condition_variable m_CV;
+
+ static Shared<AsioTlsStream>::Ptr Connect();
+
+ static Value ExecuteScript(const String& session, const String& command, bool sandboxed);
+ static Array::Ptr AutoCompleteScript(const String& session, const String& command, bool sandboxed);
+
+ static Dictionary::Ptr SendRequest();
+
+#ifdef HAVE_EDITLINE
+ static char *ConsoleCompleteHelper(const char *word, int state);
+#endif /* HAVE_EDITLINE */
+
+ static void BreakpointHandler(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di);
+
+};
+
+}
+
+#endif /* CONSOLECOMMAND_H */
diff --git a/lib/cli/daemoncommand.cpp b/lib/cli/daemoncommand.cpp
new file mode 100644
index 0000000..3a9ce8c
--- /dev/null
+++ b/lib/cli/daemoncommand.cpp
@@ -0,0 +1,882 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/daemoncommand.hpp"
+#include "cli/daemonutility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/configobjectslock.hpp"
+#include "remote/configobjectutility.hpp"
+#include "config/configcompiler.hpp"
+#include "config/configcompilercontext.hpp"
+#include "config/configitembuilder.hpp"
+#include "base/atomic.hpp"
+#include "base/defer.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/process.hpp"
+#include "base/timer.hpp"
+#include "base/utility.hpp"
+#include "base/exception.hpp"
+#include "base/convert.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/context.hpp"
+#include "config.h"
+#include <cstdint>
+#include <cstring>
+#include <boost/program_options.hpp>
+#include <iostream>
+#include <fstream>
+
+#ifdef _WIN32
+#include <windows.h>
+#else /* _WIN32 */
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#endif /* _WIN32 */
+
+#ifdef HAVE_SYSTEMD
+#include <systemd/sd-daemon.h>
+#endif /* HAVE_SYSTEMD */
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+static po::variables_map g_AppParams;
+
+REGISTER_CLICOMMAND("daemon", DaemonCommand);
+
+static inline
+void NotifyStatus(const char* status)
+{
+#ifdef HAVE_SYSTEMD
+ (void)sd_notifyf(0, "STATUS=%s", status);
+#endif /* HAVE_SYSTEMD */
+}
+
+/*
+ * Daemonize(). On error, this function logs by itself and exits (i.e. does not return).
+ *
+ * Implementation note: We're only supposed to call exit() in one of the forked processes.
+ * The other process calls _exit(). This prevents issues with exit handlers like atexit().
+ */
+static void Daemonize() noexcept
+{
+#ifndef _WIN32
+ try {
+ Application::UninitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to stop thread pool before daemonizing, unexpected error: " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+
+ pid_t pid = fork();
+ if (pid == -1) {
+ Log(LogCritical, "cli")
+ << "fork() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ exit(EXIT_FAILURE);
+ }
+
+ if (pid) {
+ // systemd requires that the pidfile of the daemon is written before the forking
+ // process terminates. So wait till either the forked daemon has written a pidfile or died.
+
+ int status;
+ int ret;
+ pid_t readpid;
+ do {
+ Utility::Sleep(0.1);
+
+ readpid = Application::ReadPidFile(Configuration::PidPath);
+ ret = waitpid(pid, &status, WNOHANG);
+ } while (readpid != pid && ret == 0);
+
+ if (ret == pid) {
+ Log(LogCritical, "cli", "The daemon could not be started. See log output for details.");
+ _exit(EXIT_FAILURE);
+ } else if (ret == -1) {
+ Log(LogCritical, "cli")
+ << "waitpid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ _exit(EXIT_FAILURE);
+ }
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ Log(LogDebug, "Daemonize()")
+ << "Child process with PID " << Utility::GetPid() << " continues; re-initializing base.";
+
+ // Detach from controlling terminal
+ pid_t sid = setsid();
+ if (sid == -1) {
+ Log(LogCritical, "cli")
+ << "setsid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+ exit(EXIT_FAILURE);
+ }
+
+ try {
+ Application::InitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to re-initialize thread pool after daemonizing: " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+#endif /* _WIN32 */
+}
+
+static void CloseStdIO(const String& stderrFile)
+{
+#ifndef _WIN32
+ int fdnull = open("/dev/null", O_RDWR);
+ if (fdnull >= 0) {
+ if (fdnull != 0)
+ dup2(fdnull, 0);
+
+ if (fdnull != 1)
+ dup2(fdnull, 1);
+
+ if (fdnull > 1)
+ close(fdnull);
+ }
+
+ const char *errPath = "/dev/null";
+
+ if (!stderrFile.IsEmpty())
+ errPath = stderrFile.CStr();
+
+ int fderr = open(errPath, O_WRONLY | O_APPEND);
+
+ if (fderr < 0 && errno == ENOENT)
+ fderr = open(errPath, O_CREAT | O_WRONLY | O_APPEND, 0600);
+
+ if (fderr >= 0) {
+ if (fderr != 2)
+ dup2(fderr, 2);
+
+ if (fderr > 2)
+ close(fderr);
+ }
+#endif
+}
+
+String DaemonCommand::GetDescription() const
+{
+ return "Starts Icinga 2.";
+}
+
+String DaemonCommand::GetShortDescription() const
+{
+ return "starts Icinga 2";
+}
+
+void DaemonCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("config,c", po::value<std::vector<std::string> >(), "parse a configuration file")
+ ("no-config,z", "start without a configuration file")
+ ("validate,C", "exit after validating the configuration")
+ ("dump-objects", "write icinga2.debug cache file for icinga2 object list")
+ ("errorlog,e", po::value<std::string>(), "log fatal errors to the specified log file (only works in combination with --daemonize or --close-stdio)")
+#ifndef _WIN32
+ ("daemonize,d", "detach from the controlling terminal")
+ ("close-stdio", "do not log to stdout (or stderr) after startup")
+#endif /* _WIN32 */
+ ;
+}
+
+std::vector<String> DaemonCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "config" || argument == "errorlog")
+ return GetBashCompletionSuggestions("file", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+#ifndef _WIN32
+// The PID of the Icinga umbrella process
+pid_t l_UmbrellaPid = 0;
+
+// Whether the umbrella process allowed us to continue working beyond config validation
+static Atomic<bool> l_AllowedToWork (false);
+#endif /* _WIN32 */
+
+#ifdef I2_DEBUG
+/**
+ * Determine whether the developer wants to delay the worker process to attach a debugger to it.
+ *
+ * @return Internal.DebugWorkerDelay double
+ */
+static double GetDebugWorkerDelay()
+{
+ Namespace::Ptr internal = ScriptGlobal::Get("Internal", &Empty);
+
+ Value vdebug;
+ if (internal && internal->Get("DebugWorkerDelay", &vdebug))
+ return Convert::ToDouble(vdebug);
+
+ return 0.0;
+}
+#endif /* I2_DEBUG */
+
+static String l_ObjectsPath;
+
+/**
+ * Do the actual work (config loading, ...)
+ *
+ * @param configs Files to read config from
+ * @param closeConsoleLog Whether to close the console log after config loading
+ * @param stderrFile Where to log errors
+ *
+ * @return Exit code
+ */
+static inline
+int RunWorker(const std::vector<std::string>& configs, bool closeConsoleLog = false, const String& stderrFile = String())
+{
+
+#ifdef I2_DEBUG
+ double delay = GetDebugWorkerDelay();
+
+ if (delay > 0.0) {
+ Log(LogInformation, "RunWorker")
+ << "DEBUG: Current PID: " << Utility::GetPid() << ". Sleeping for " << delay << " seconds to allow lldb/gdb -p <PID> attachment.";
+
+ Utility::Sleep(delay);
+ }
+#endif /* I2_DEBUG */
+
+ Log(LogInformation, "cli", "Loading configuration file(s).");
+ NotifyStatus("Loading configuration file(s)...");
+
+ {
+ std::vector<ConfigItem::Ptr> newItems;
+
+ if (!DaemonUtility::LoadConfigFiles(configs, newItems, l_ObjectsPath, Configuration::VarsPath)) {
+ Log(LogCritical, "cli", "Config validation failed. Re-run with 'icinga2 daemon -C' after fixing the config.");
+ NotifyStatus("Config validation failed.");
+ return EXIT_FAILURE;
+ }
+
+#ifndef _WIN32
+ Log(LogNotice, "cli")
+ << "Notifying umbrella process (PID " << l_UmbrellaPid << ") about the config loading success";
+
+ (void)kill(l_UmbrellaPid, SIGUSR2);
+
+ Log(LogNotice, "cli")
+ << "Waiting for the umbrella process to let us doing the actual work";
+
+ NotifyStatus("Waiting for the umbrella process to let us doing the actual work...");
+
+ if (closeConsoleLog) {
+ CloseStdIO(stderrFile);
+ Logger::DisableConsoleLog();
+ }
+
+ while (!l_AllowedToWork.load()) {
+ Utility::Sleep(0.2);
+ }
+
+ Log(LogNotice, "cli")
+ << "The umbrella process let us continuing";
+#endif /* _WIN32 */
+
+ NotifyStatus("Restoring the previous program state...");
+
+ /* restore the previous program state */
+ try {
+ ConfigObject::RestoreObjects(Configuration::StatePath);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to restore state file: " << DiagnosticInformation(ex);
+
+ NotifyStatus("Failed to restore state file.");
+
+ return EXIT_FAILURE;
+ }
+
+ NotifyStatus("Activating config objects...");
+
+ // activate config only after daemonization: it starts threads and that is not compatible with fork()
+ if (!ConfigItem::ActivateItems(newItems, false, true, true)) {
+ Log(LogCritical, "cli", "Error activating configuration.");
+
+ NotifyStatus("Error activating configuration.");
+
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* Create the internal API object storage. Do this here too with setups without API. */
+ ConfigObjectUtility::CreateStorage();
+
+ /* Remove ignored Downtime/Comment objects. */
+ try {
+ String configDir = ConfigObjectUtility::GetConfigDir();
+ ConfigItem::RemoveIgnoredItems(configDir);
+ } catch (const std::exception& ex) {
+ Log(LogNotice, "cli")
+ << "Cannot clean ignored downtimes/comments: " << ex.what();
+ }
+
+ ApiListener::UpdateObjectAuthority();
+
+ NotifyStatus("Startup finished.");
+
+ return Application::GetInstance()->Run();
+}
+
+#ifndef _WIN32
+// The signals to block temporarily in StartUnixWorker().
+static const sigset_t l_UnixWorkerSignals = ([]() -> sigset_t {
+ sigset_t s;
+
+ (void)sigemptyset(&s);
+ (void)sigaddset(&s, SIGUSR1);
+ (void)sigaddset(&s, SIGUSR2);
+ (void)sigaddset(&s, SIGINT);
+ (void)sigaddset(&s, SIGTERM);
+ (void)sigaddset(&s, SIGHUP);
+
+ return s;
+})();
+
+// The PID of the seamless worker currently being started by StartUnixWorker()
+static Atomic<pid_t> l_CurrentlyStartingUnixWorkerPid (-1);
+
+// The state of the seamless worker currently being started by StartUnixWorker()
+static Atomic<bool> l_CurrentlyStartingUnixWorkerReady (false);
+
+// The last temination signal we received
+static Atomic<int> l_TermSignal (-1);
+
+// Whether someone requested to re-load config (and we didn't handle that request, yet)
+static Atomic<bool> l_RequestedReload (false);
+
+// Whether someone requested to re-open logs (and we didn't handle that request, yet)
+static Atomic<bool> l_RequestedReopenLogs (false);
+
+/**
+ * Umbrella process' signal handlers
+ */
+static void UmbrellaSignalHandler(int num, siginfo_t *info, void*)
+{
+ switch (num) {
+ case SIGUSR1:
+ // Someone requested to re-open logs
+ l_RequestedReopenLogs.store(true);
+ break;
+ case SIGUSR2:
+ if (!l_CurrentlyStartingUnixWorkerReady.load()
+ && (info->si_pid == 0 || info->si_pid == l_CurrentlyStartingUnixWorkerPid.load()) ) {
+ // The seamless worker currently being started by StartUnixWorker() successfully loaded its config
+ l_CurrentlyStartingUnixWorkerReady.store(true);
+ }
+ break;
+ case SIGINT:
+ case SIGTERM:
+ // Someone requested our termination
+
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_handler = SIG_DFL;
+
+ (void)sigaction(num, &sa, nullptr);
+ }
+
+ l_TermSignal.store(num);
+ break;
+ case SIGHUP:
+ // Someone requested to re-load config
+ l_RequestedReload.store(true);
+ break;
+ default:
+ // Programming error (or someone has broken the userspace)
+ VERIFY(!"Caught unexpected signal");
+ }
+}
+
+/**
+ * Seamless worker's signal handlers
+ */
+static void WorkerSignalHandler(int num, siginfo_t *info, void*)
+{
+ switch (num) {
+ case SIGUSR1:
+ // Catches SIGUSR1 as long as the actual handler (logrotate)
+ // has not been installed not to let SIGUSR1 terminate the process
+ break;
+ case SIGUSR2:
+ if (info->si_pid == 0 || info->si_pid == l_UmbrellaPid) {
+ // The umbrella process allowed us to continue working beyond config validation
+ l_AllowedToWork.store(true);
+ }
+ break;
+ case SIGINT:
+ case SIGTERM:
+ if (info->si_pid == 0 || info->si_pid == l_UmbrellaPid) {
+ // The umbrella process requested our termination
+ Application::RequestShutdown();
+ }
+ break;
+ default:
+ // Programming error (or someone has broken the userspace)
+ VERIFY(!"Caught unexpected signal");
+ }
+}
+
+#ifdef HAVE_SYSTEMD
+// When we last notified the watchdog.
+static Atomic<double> l_LastNotifiedWatchdog (0);
+
+/**
+ * Notify the watchdog if not notified during the last 2.5s.
+ */
+static void NotifyWatchdog()
+{
+ double now = Utility::GetTime();
+
+ if (now - l_LastNotifiedWatchdog.load() >= 2.5) {
+ sd_notify(0, "WATCHDOG=1");
+ l_LastNotifiedWatchdog.store(now);
+ }
+}
+#endif /* HAVE_SYSTEMD */
+
+/**
+ * Starts seamless worker process doing the actual work (config loading, ...)
+ *
+ * @param configs Files to read config from
+ * @param closeConsoleLog Whether to close the console log after config loading
+ * @param stderrFile Where to log errors
+ *
+ * @return The worker's PID on success, -1 on fork(2) failure, -2 if the worker couldn't load its config
+ */
+static pid_t StartUnixWorker(const std::vector<std::string>& configs, bool closeConsoleLog = false, const String& stderrFile = String())
+{
+ Log(LogNotice, "cli")
+ << "Spawning seamless worker process doing the actual work";
+
+ try {
+ Application::UninitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to stop thread pool before forking, unexpected error: " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Block the signal handlers we'd like to change in the child process until we changed them.
+ * Block SIGUSR2 handler until we've set l_CurrentlyStartingUnixWorkerPid.
+ */
+ (void)sigprocmask(SIG_BLOCK, &l_UnixWorkerSignals, nullptr);
+
+ pid_t pid = fork();
+
+ switch (pid) {
+ case -1:
+ Log(LogCritical, "cli")
+ << "fork() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\"";
+
+ try {
+ Application::InitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to re-initialize thread pool after forking (parent): " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+
+ (void)sigprocmask(SIG_UNBLOCK, &l_UnixWorkerSignals, nullptr);
+ return -1;
+
+ case 0:
+ try {
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_handler = SIG_DFL;
+
+ (void)sigaction(SIGUSR1, &sa, nullptr);
+ }
+
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_handler = SIG_IGN;
+
+ (void)sigaction(SIGHUP, &sa, nullptr);
+ }
+
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_sigaction = &WorkerSignalHandler;
+ sa.sa_flags = SA_RESTART | SA_SIGINFO;
+
+ (void)sigaction(SIGUSR1, &sa, nullptr);
+ (void)sigaction(SIGUSR2, &sa, nullptr);
+ (void)sigaction(SIGINT, &sa, nullptr);
+ (void)sigaction(SIGTERM, &sa, nullptr);
+ }
+
+ (void)sigprocmask(SIG_UNBLOCK, &l_UnixWorkerSignals, nullptr);
+
+ try {
+ Application::InitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to re-initialize thread pool after forking (child): " << DiagnosticInformation(ex);
+ _exit(EXIT_FAILURE);
+ }
+
+ try {
+ Process::InitializeSpawnHelper();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to initialize process spawn helper after forking (child): " << DiagnosticInformation(ex);
+ _exit(EXIT_FAILURE);
+ }
+
+ _exit(RunWorker(configs, closeConsoleLog, stderrFile));
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli") << "Exception in main process: " << DiagnosticInformation(ex);
+ _exit(EXIT_FAILURE);
+ } catch (...) {
+ _exit(EXIT_FAILURE);
+ }
+
+ default:
+ l_CurrentlyStartingUnixWorkerPid.store(pid);
+ (void)sigprocmask(SIG_UNBLOCK, &l_UnixWorkerSignals, nullptr);
+
+ Log(LogNotice, "cli")
+ << "Spawned worker process (PID " << pid << "), waiting for it to load its config";
+
+ // Wait for the newly spawned process to either load its config or fail.
+ for (;;) {
+#ifdef HAVE_SYSTEMD
+ NotifyWatchdog();
+#endif /* HAVE_SYSTEMD */
+
+ if (waitpid(pid, nullptr, WNOHANG) > 0) {
+ Log(LogNotice, "cli")
+ << "Worker process couldn't load its config";
+
+ pid = -2;
+ break;
+ }
+
+ if (l_CurrentlyStartingUnixWorkerReady.load()) {
+ Log(LogNotice, "cli")
+ << "Worker process successfully loaded its config";
+ break;
+ }
+
+ Utility::Sleep(0.2);
+ }
+
+ // Reset flags for the next time
+ l_CurrentlyStartingUnixWorkerPid.store(-1);
+ l_CurrentlyStartingUnixWorkerReady.store(false);
+
+ try {
+ Application::InitializeBase();
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Failed to re-initialize thread pool after forking (parent): " << DiagnosticInformation(ex);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ return pid;
+}
+
+/**
+ * Workaround to instantiate Application (which is abstract) in DaemonCommand#Run()
+ */
+class PidFileManagementApp : public Application
+{
+public:
+ inline int Main() override
+ {
+ return EXIT_FAILURE;
+ }
+};
+#endif /* _WIN32 */
+
+/**
+ * The entry point for the "daemon" CLI command.
+ *
+ * @returns An exit status.
+ */
+int DaemonCommand::Run(const po::variables_map& vm, const std::vector<std::string>& ap) const
+{
+#ifdef _WIN32
+ SetConsoleOutputCP(65001);
+#endif /* _WIN32 */
+
+ Logger::EnableTimestamp();
+
+ Log(LogInformation, "cli")
+ << "Icinga application loader (version: " << Application::GetAppVersion()
+#ifdef I2_DEBUG
+ << "; debug"
+#endif /* I2_DEBUG */
+ << ")";
+
+ std::vector<std::string> configs;
+ if (vm.count("config") > 0)
+ configs = vm["config"].as<std::vector<std::string> >();
+ else if (!vm.count("no-config")) {
+ /* The implicit string assignment is needed for Windows builds. */
+ String configDir = Configuration::ConfigDir;
+ configs.push_back(configDir + "/icinga2.conf");
+ }
+
+ if (vm.count("dump-objects")) {
+ if (!vm.count("validate")) {
+ Log(LogCritical, "cli", "--dump-objects is not allowed without -C");
+ return EXIT_FAILURE;
+ }
+
+ l_ObjectsPath = Configuration::ObjectsPath;
+ }
+
+ if (vm.count("validate")) {
+ Log(LogInformation, "cli", "Loading configuration file(s).");
+
+ std::vector<ConfigItem::Ptr> newItems;
+
+ if (!DaemonUtility::LoadConfigFiles(configs, newItems, l_ObjectsPath, Configuration::VarsPath)) {
+ Log(LogCritical, "cli", "Config validation failed. Re-run with 'icinga2 daemon -C' after fixing the config.");
+ return EXIT_FAILURE;
+ }
+
+ Log(LogInformation, "cli", "Finished validating the configuration file(s).");
+ return EXIT_SUCCESS;
+ }
+
+ {
+ pid_t runningpid = Application::ReadPidFile(Configuration::PidPath);
+ if (runningpid > 0) {
+ Log(LogCritical, "cli")
+ << "Another instance of Icinga already running with PID " << runningpid;
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (vm.count("daemonize")) {
+ // this subroutine either succeeds, or logs an error
+ // and terminates the process (does not return).
+ Daemonize();
+ }
+
+#ifndef _WIN32
+ /* The Application manages the PID file,
+ * but on *nix this process doesn't load any config
+ * so there's no central Application instance.
+ */
+ PidFileManagementApp app;
+
+ try {
+ app.UpdatePidFile(Configuration::PidPath);
+ } catch (const std::exception&) {
+ Log(LogCritical, "Application")
+ << "Cannot update PID file '" << Configuration::PidPath << "'. Aborting.";
+ return EXIT_FAILURE;
+ }
+
+ Defer closePidFile ([&app]() {
+ app.ClosePidFile(true);
+ });
+#endif /* _WIN32 */
+
+ if (vm.count("daemonize")) {
+ // After disabling the console log, any further errors will go to the configured log only.
+ // Let's try to make this clear and say good bye.
+ Log(LogInformation, "cli", "Closing console log.");
+
+ String errorLog;
+ if (vm.count("errorlog"))
+ errorLog = vm["errorlog"].as<std::string>();
+
+ CloseStdIO(errorLog);
+ Logger::DisableConsoleLog();
+ }
+
+#ifdef _WIN32
+ try {
+ return RunWorker(configs);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli") << "Exception in main process: " << DiagnosticInformation(ex);
+ return EXIT_FAILURE;
+ } catch (...) {
+ return EXIT_FAILURE;
+ }
+#else /* _WIN32 */
+ l_UmbrellaPid = getpid();
+ Application::SetUmbrellaProcess(l_UmbrellaPid);
+
+ {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+
+ sa.sa_sigaction = &UmbrellaSignalHandler;
+ sa.sa_flags = SA_NOCLDSTOP | SA_RESTART | SA_SIGINFO;
+
+ (void)sigaction(SIGUSR1, &sa, nullptr);
+ (void)sigaction(SIGUSR2, &sa, nullptr);
+ (void)sigaction(SIGINT, &sa, nullptr);
+ (void)sigaction(SIGTERM, &sa, nullptr);
+ (void)sigaction(SIGHUP, &sa, nullptr);
+ }
+
+ bool closeConsoleLog = !vm.count("daemonize") && vm.count("close-stdio");
+
+ String errorLog;
+ if (vm.count("errorlog"))
+ errorLog = vm["errorlog"].as<std::string>();
+
+ // The PID of the current seamless worker
+ pid_t currentWorker = StartUnixWorker(configs, closeConsoleLog, errorLog);
+
+ if (currentWorker < 0) {
+ return EXIT_FAILURE;
+ }
+
+ if (closeConsoleLog) {
+ // After disabling the console log, any further errors will go to the configured log only.
+ // Let's try to make this clear and say good bye.
+ Log(LogInformation, "cli", "Closing console log.");
+
+ CloseStdIO(errorLog);
+ Logger::DisableConsoleLog();
+ }
+
+ // Immediately allow the first (non-reload) worker to continue working beyond config validation
+ (void)kill(currentWorker, SIGUSR2);
+
+#ifdef HAVE_SYSTEMD
+ sd_notify(0, "READY=1");
+#endif /* HAVE_SYSTEMD */
+
+ // Whether we already forwarded a termination signal to the seamless worker
+ bool requestedTermination = false;
+
+ // Whether we already notified systemd about our termination
+ bool notifiedTermination = false;
+
+ for (;;) {
+#ifdef HAVE_SYSTEMD
+ NotifyWatchdog();
+#endif /* HAVE_SYSTEMD */
+
+ if (!requestedTermination) {
+ int termSig = l_TermSignal.load();
+ if (termSig != -1) {
+ Log(LogNotice, "cli")
+ << "Got signal " << termSig << ", forwarding to seamless worker (PID " << currentWorker << ")";
+
+ (void)kill(currentWorker, termSig);
+ requestedTermination = true;
+
+#ifdef HAVE_SYSTEMD
+ if (!notifiedTermination) {
+ notifiedTermination = true;
+ sd_notify(0, "STOPPING=1");
+ }
+#endif /* HAVE_SYSTEMD */
+ }
+ }
+
+ if (l_RequestedReload.exchange(false)) {
+ Log(LogInformation, "Application")
+ << "Got reload command: Starting new instance.";
+
+#ifdef HAVE_SYSTEMD
+ sd_notify(0, "RELOADING=1");
+#endif /* HAVE_SYSTEMD */
+
+ // The old process is still active, yet.
+ // Its config changes would not be visible to the new one after config load.
+ ConfigObjectsExclusiveLock lock;
+
+ pid_t nextWorker = StartUnixWorker(configs);
+
+ switch (nextWorker) {
+ case -1:
+ break;
+ case -2:
+ Log(LogCritical, "Application", "Found error in config: reloading aborted");
+ Application::SetLastReloadFailed(Utility::GetTime());
+ break;
+ default:
+ Log(LogInformation, "Application")
+ << "Reload done, old process shutting down. Child process with PID '" << nextWorker << "' is taking over.";
+
+ NotifyStatus("Shutting down old instance...");
+
+ Application::SetLastReloadFailed(0);
+ (void)kill(currentWorker, SIGTERM);
+
+ {
+ double start = Utility::GetTime();
+
+ while (waitpid(currentWorker, nullptr, 0) == -1 && errno == EINTR) {
+ #ifdef HAVE_SYSTEMD
+ NotifyWatchdog();
+ #endif /* HAVE_SYSTEMD */
+ }
+
+ Log(LogNotice, "cli")
+ << "Waited for " << Utility::FormatDuration(Utility::GetTime() - start) << " on old process to exit.";
+ }
+
+ // Old instance shut down, allow the new one to continue working beyond config validation
+ (void)kill(nextWorker, SIGUSR2);
+
+ NotifyStatus("Shut down old instance.");
+
+ currentWorker = nextWorker;
+ }
+
+#ifdef HAVE_SYSTEMD
+ sd_notify(0, "READY=1");
+#endif /* HAVE_SYSTEMD */
+
+ }
+
+ if (l_RequestedReopenLogs.exchange(false)) {
+ Log(LogNotice, "cli")
+ << "Got signal " << SIGUSR1 << ", forwarding to seamless worker (PID " << currentWorker << ")";
+
+ (void)kill(currentWorker, SIGUSR1);
+ }
+
+ {
+ int status;
+ if (waitpid(currentWorker, &status, WNOHANG) > 0) {
+ Log(LogNotice, "cli")
+ << "Seamless worker (PID " << currentWorker << ") stopped, stopping as well";
+
+#ifdef HAVE_SYSTEMD
+ if (!notifiedTermination) {
+ notifiedTermination = true;
+ sd_notify(0, "STOPPING=1");
+ }
+#endif /* HAVE_SYSTEMD */
+
+ // If killed by signal, forward it via the exit code (to be as seamless as possible)
+ return WIFSIGNALED(status) ? 128 + WTERMSIG(status) : WEXITSTATUS(status);
+ }
+ }
+
+ Utility::Sleep(0.2);
+ }
+#endif /* _WIN32 */
+}
diff --git a/lib/cli/daemoncommand.hpp b/lib/cli/daemoncommand.hpp
new file mode 100644
index 0000000..da8a34b
--- /dev/null
+++ b/lib/cli/daemoncommand.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DAEMONCOMMAND_H
+#define DAEMONCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "daemon" CLI command.
+ *
+ * @ingroup cli
+ */
+class DaemonCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(DaemonCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* DAEMONCOMMAND_H */
diff --git a/lib/cli/daemonutility.cpp b/lib/cli/daemonutility.cpp
new file mode 100644
index 0000000..9e910f3
--- /dev/null
+++ b/lib/cli/daemonutility.cpp
@@ -0,0 +1,285 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/daemonutility.hpp"
+#include "base/configobject.hpp"
+#include "base/exception.hpp"
+#include "base/utility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/scriptglobal.hpp"
+#include "config/configcompiler.hpp"
+#include "config/configcompilercontext.hpp"
+#include "config/configitembuilder.hpp"
+#include "icinga/dependency.hpp"
+#include <set>
+
+using namespace icinga;
+
+static bool ExecuteExpression(Expression *expression)
+{
+ if (!expression)
+ return false;
+
+ try {
+ ScriptFrame frame(true);
+ expression->Evaluate(frame);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "config", DiagnosticInformation(ex));
+ return false;
+ }
+
+ return true;
+}
+
+static bool IncludeZoneDirRecursive(const String& path, const String& package, bool& success)
+{
+ String zoneName = Utility::BaseName(path);
+
+ /* We don't have an activated zone object yet. We may forcefully guess from configitems
+ * to not include this specific synced zones directory.
+ */
+ if(!ConfigItem::GetByTypeAndName(Type::GetByName("Zone"), zoneName)) {
+ return false;
+ }
+
+ /* register this zone path for cluster config sync */
+ ConfigCompiler::RegisterZoneDir("_etc", path, zoneName);
+
+ std::vector<std::unique_ptr<Expression> > expressions;
+ Utility::GlobRecursive(path, "*.conf", [&expressions, zoneName, package](const String& file) {
+ ConfigCompiler::CollectIncludes(expressions, file, zoneName, package);
+ }, GlobFile);
+
+ DictExpression expr(std::move(expressions));
+ if (!ExecuteExpression(&expr))
+ success = false;
+
+ return true;
+}
+
+static bool IncludeNonLocalZone(const String& zonePath, const String& package, bool& success)
+{
+ /* Note: This include function must not call RegisterZoneDir().
+ * We do not need to copy it for cluster config sync. */
+
+ String zoneName = Utility::BaseName(zonePath);
+
+ /* We don't have an activated zone object yet. We may forcefully guess from configitems
+ * to not include this specific synced zones directory.
+ */
+ if(!ConfigItem::GetByTypeAndName(Type::GetByName("Zone"), zoneName)) {
+ return false;
+ }
+
+ /* Check whether this node already has an authoritative config version
+ * from zones.d in etc or api package directory, or a local marker file)
+ */
+ if (ConfigCompiler::HasZoneConfigAuthority(zoneName) || Utility::PathExists(zonePath + "/.authoritative")) {
+ Log(LogNotice, "config")
+ << "Ignoring non local config include for zone '" << zoneName << "': We already have an authoritative copy included.";
+ return true;
+ }
+
+ std::vector<std::unique_ptr<Expression> > expressions;
+ Utility::GlobRecursive(zonePath, "*.conf", [&expressions, zoneName, package](const String& file) {
+ ConfigCompiler::CollectIncludes(expressions, file, zoneName, package);
+ }, GlobFile);
+
+ DictExpression expr(std::move(expressions));
+ if (!ExecuteExpression(&expr))
+ success = false;
+
+ return true;
+}
+
+static void IncludePackage(const String& packagePath, bool& success)
+{
+ /* Note: Package includes will register their zones
+ * for config sync inside their generated config. */
+ String packageName = Utility::BaseName(packagePath);
+
+ if (Utility::PathExists(packagePath + "/include.conf")) {
+ std::unique_ptr<Expression> expr = ConfigCompiler::CompileFile(packagePath + "/include.conf",
+ String(), packageName);
+
+ if (!ExecuteExpression(&*expr))
+ success = false;
+ }
+}
+
+bool DaemonUtility::ValidateConfigFiles(const std::vector<std::string>& configs, const String& objectsFile)
+{
+ bool success;
+
+ Namespace::Ptr systemNS = ScriptGlobal::Get("System");
+ VERIFY(systemNS);
+
+ Namespace::Ptr internalNS = ScriptGlobal::Get("Internal");
+ VERIFY(internalNS);
+
+ if (!objectsFile.IsEmpty())
+ ConfigCompilerContext::GetInstance()->OpenObjectsFile(objectsFile);
+
+ if (!configs.empty()) {
+ for (const String& configPath : configs) {
+ try {
+ std::unique_ptr<Expression> expression = ConfigCompiler::CompileFile(configPath, String(), "_etc");
+ success = ExecuteExpression(&*expression);
+ if (!success)
+ return false;
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli", "Could not compile config files: " + DiagnosticInformation(ex, false));
+ Application::Exit(1);
+ }
+ }
+ }
+
+ /* Load cluster config files from /etc/icinga2/zones.d.
+ * This should probably be in libremote but
+ * unfortunately moving it there is somewhat non-trivial. */
+ success = true;
+
+ /* Only load zone directory if we're not in staging validation. */
+ if (!internalNS->Contains("ZonesStageVarDir")) {
+ String zonesEtcDir = Configuration::ZonesDir;
+ if (!zonesEtcDir.IsEmpty() && Utility::PathExists(zonesEtcDir)) {
+ std::set<String> zoneEtcDirs;
+ Utility::Glob(zonesEtcDir + "/*", [&zoneEtcDirs](const String& zoneEtcDir) { zoneEtcDirs.emplace(zoneEtcDir); }, GlobDirectory);
+
+ bool hasSuccess = true;
+
+ while (!zoneEtcDirs.empty() && hasSuccess) {
+ hasSuccess = false;
+
+ for (auto& zoneEtcDir : zoneEtcDirs) {
+ if (IncludeZoneDirRecursive(zoneEtcDir, "_etc", success)) {
+ zoneEtcDirs.erase(zoneEtcDir);
+ hasSuccess = true;
+ break;
+ }
+ }
+ }
+
+ for (auto& zoneEtcDir : zoneEtcDirs) {
+ Log(LogWarning, "config")
+ << "Ignoring directory '" << zoneEtcDir << "' for unknown zone '" << Utility::BaseName(zoneEtcDir) << "'.";
+ }
+ }
+
+ if (!success)
+ return false;
+ }
+
+ /* Load package config files - they may contain additional zones which
+ * are authoritative on this node and are checked in HasZoneConfigAuthority(). */
+ String packagesVarDir = Configuration::DataDir + "/api/packages";
+ if (Utility::PathExists(packagesVarDir))
+ Utility::Glob(packagesVarDir + "/*", [&success](const String& packagePath) { IncludePackage(packagePath, success); }, GlobDirectory);
+
+ if (!success)
+ return false;
+
+ /* Load cluster synchronized configuration files. This can be overridden for staged sync validations. */
+ String zonesVarDir = Configuration::DataDir + "/api/zones";
+
+ /* Cluster config sync stage validation needs this. */
+ if (internalNS->Contains("ZonesStageVarDir")) {
+ zonesVarDir = internalNS->Get("ZonesStageVarDir");
+
+ Log(LogNotice, "DaemonUtility")
+ << "Overriding zones var directory with '" << zonesVarDir << "' for cluster config sync staging.";
+ }
+
+
+ if (Utility::PathExists(zonesVarDir)) {
+ std::set<String> zoneVarDirs;
+ Utility::Glob(zonesVarDir + "/*", [&zoneVarDirs](const String& zoneVarDir) { zoneVarDirs.emplace(zoneVarDir); }, GlobDirectory);
+
+ bool hasSuccess = true;
+
+ while (!zoneVarDirs.empty() && hasSuccess) {
+ hasSuccess = false;
+
+ for (auto& zoneVarDir : zoneVarDirs) {
+ if (IncludeNonLocalZone(zoneVarDir, "_cluster", success)) {
+ zoneVarDirs.erase(zoneVarDir);
+ hasSuccess = true;
+ break;
+ }
+ }
+ }
+
+ for (auto& zoneEtcDir : zoneVarDirs) {
+ Log(LogWarning, "config")
+ << "Ignoring directory '" << zoneEtcDir << "' for unknown zone '" << Utility::BaseName(zoneEtcDir) << "'.";
+ }
+ }
+
+ if (!success)
+ return false;
+
+ /* This is initialized inside the IcingaApplication class. */
+ Value vAppType;
+ VERIFY(systemNS->Get("ApplicationType", &vAppType));
+
+ Type::Ptr appType = Type::GetByName(vAppType);
+
+ if (ConfigItem::GetItems(appType).empty()) {
+ ConfigItemBuilder builder;
+ builder.SetType(appType);
+ builder.SetName("app");
+ builder.AddExpression(new ImportDefaultTemplatesExpression());
+ ConfigItem::Ptr item = builder.Compile();
+ item->Register();
+ }
+
+ return true;
+}
+
+bool DaemonUtility::LoadConfigFiles(const std::vector<std::string>& configs,
+ std::vector<ConfigItem::Ptr>& newItems,
+ const String& objectsFile, const String& varsfile)
+{
+ ActivationScope ascope;
+
+ if (!DaemonUtility::ValidateConfigFiles(configs, objectsFile)) {
+ ConfigCompilerContext::GetInstance()->CancelObjectsFile();
+ return false;
+ }
+
+ // After evaluating the top-level statements of the config files (happening in ValidateConfigFiles() above),
+ // prevent further modification of the global scope. This allows for a faster execution of the following steps
+ // as Freeze() disables locking as it's not necessary on a read-only data structure anymore.
+ ScriptGlobal::GetGlobals()->Freeze();
+
+ WorkQueue upq(25000, Configuration::Concurrency);
+ upq.SetName("DaemonUtility::LoadConfigFiles");
+ bool result = ConfigItem::CommitItems(ascope.GetContext(), upq, newItems);
+
+ if (result) {
+ try {
+ Dependency::AssertNoCycles();
+ } catch (...) {
+ Log(LogCritical, "config")
+ << DiagnosticInformation(boost::current_exception(), false);
+
+ result = false;
+ }
+ }
+
+ if (!result) {
+ ConfigCompilerContext::GetInstance()->CancelObjectsFile();
+ return false;
+ }
+
+ try {
+ ScriptGlobal::WriteToFile(varsfile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli", "Could not write vars file: " + DiagnosticInformation(ex, false));
+ Application::Exit(1);
+ }
+
+ ConfigCompilerContext::GetInstance()->FinishObjectsFile();
+
+ return true;
+}
diff --git a/lib/cli/daemonutility.hpp b/lib/cli/daemonutility.hpp
new file mode 100644
index 0000000..963bfba
--- /dev/null
+++ b/lib/cli/daemonutility.hpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef DAEMONUTILITY_H
+#define DAEMONUTILITY_H
+
+#include "cli/i2-cli.hpp"
+#include "config/configitem.hpp"
+#include "base/string.hpp"
+#include <boost/program_options.hpp>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class DaemonUtility
+{
+public:
+ static bool ValidateConfigFiles(const std::vector<std::string>& configs, const String& objectsFile = String());
+ static bool LoadConfigFiles(const std::vector<std::string>& configs, std::vector<ConfigItem::Ptr>& newItems,
+ const String& objectsFile = String(), const String& varsfile = String());
+};
+
+}
+
+#endif /* DAEMONULITIY_H */
diff --git a/lib/cli/editline.hpp b/lib/cli/editline.hpp
new file mode 100644
index 0000000..f97525e
--- /dev/null
+++ b/lib/cli/editline.hpp
@@ -0,0 +1,19 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef EDITLINE_H
+#define EDITLINE_H
+
+extern "C" {
+
+char *readline(const char *prompt);
+int add_history(const char *line);
+void rl_deprep_terminal();
+
+typedef char *ELFunction(const char *, int);
+
+extern char rl_completion_append_character;
+extern ELFunction *rl_completion_entry_function;
+
+}
+
+#endif /* EDITLINE_H */
diff --git a/lib/cli/featuredisablecommand.cpp b/lib/cli/featuredisablecommand.cpp
new file mode 100644
index 0000000..95a4a26
--- /dev/null
+++ b/lib/cli/featuredisablecommand.cpp
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/featuredisablecommand.hpp"
+#include "cli/featureutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("feature/disable", FeatureDisableCommand);
+
+String FeatureDisableCommand::GetDescription() const
+{
+ return "Disables specified Icinga 2 feature.";
+}
+
+String FeatureDisableCommand::GetShortDescription() const
+{
+ return "disables specified feature";
+}
+
+std::vector<String> FeatureDisableCommand::GetPositionalSuggestions(const String& word) const
+{
+ return FeatureUtility::GetFieldCompletionSuggestions(word, false);
+}
+
+int FeatureDisableCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+int FeatureDisableCommand::GetMaxArguments() const
+{
+ return -1;
+}
+
+ImpersonationLevel FeatureDisableCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "feature disable" CLI command.
+ *
+ * @returns An exit status.
+ */
+int FeatureDisableCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (ap.empty()) {
+ Log(LogCritical, "cli", "Cannot disable feature(s). Name(s) are missing!");
+ return 0;
+ }
+
+ return FeatureUtility::DisableFeatures(ap);
+}
diff --git a/lib/cli/featuredisablecommand.hpp b/lib/cli/featuredisablecommand.hpp
new file mode 100644
index 0000000..b24655d
--- /dev/null
+++ b/lib/cli/featuredisablecommand.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FEATUREDISABLECOMMAND_H
+#define FEATUREDISABLECOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "feature disable" command.
+ *
+ * @ingroup cli
+ */
+class FeatureDisableCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(FeatureDisableCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ int GetMaxArguments() const override;
+ std::vector<String> GetPositionalSuggestions(const String& word) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* FEATUREDISABLECOMMAND_H */
diff --git a/lib/cli/featureenablecommand.cpp b/lib/cli/featureenablecommand.cpp
new file mode 100644
index 0000000..0cf9066
--- /dev/null
+++ b/lib/cli/featureenablecommand.cpp
@@ -0,0 +1,50 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/featureenablecommand.hpp"
+#include "cli/featureutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("feature/enable", FeatureEnableCommand);
+
+String FeatureEnableCommand::GetDescription() const
+{
+ return "Enables specified Icinga 2 feature.";
+}
+
+String FeatureEnableCommand::GetShortDescription() const
+{
+ return "enables specified feature";
+}
+
+std::vector<String> FeatureEnableCommand::GetPositionalSuggestions(const String& word) const
+{
+ return FeatureUtility::GetFieldCompletionSuggestions(word, true);
+}
+
+int FeatureEnableCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+int FeatureEnableCommand::GetMaxArguments() const
+{
+ return -1;
+}
+
+ImpersonationLevel FeatureEnableCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "feature enable" CLI command.
+ *
+ * @returns An exit status.
+ */
+int FeatureEnableCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ return FeatureUtility::EnableFeatures(ap);
+}
diff --git a/lib/cli/featureenablecommand.hpp b/lib/cli/featureenablecommand.hpp
new file mode 100644
index 0000000..fc91778
--- /dev/null
+++ b/lib/cli/featureenablecommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FEATUREENABLECOMMAND_H
+#define FEATUREENABLECOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "feature enable" command.
+ *
+ * @ingroup cli
+ */
+class FeatureEnableCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(FeatureEnableCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ int GetMaxArguments() const override;
+ std::vector<String> GetPositionalSuggestions(const String& word) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* FEATUREENABLECOMMAND_H */
diff --git a/lib/cli/featurelistcommand.cpp b/lib/cli/featurelistcommand.cpp
new file mode 100644
index 0000000..2aad4a9
--- /dev/null
+++ b/lib/cli/featurelistcommand.cpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/featurelistcommand.hpp"
+#include "cli/featureutility.hpp"
+#include "base/logger.hpp"
+#include "base/convert.hpp"
+#include "base/console.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("feature/list", FeatureListCommand);
+
+String FeatureListCommand::GetDescription() const
+{
+ return "Lists all available Icinga 2 features.";
+}
+
+String FeatureListCommand::GetShortDescription() const
+{
+ return "lists all available features";
+}
+
+/**
+ * The entry point for the "feature list" CLI command.
+ *
+ * @returns An exit status.
+ */
+int FeatureListCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ return FeatureUtility::ListFeatures();
+}
diff --git a/lib/cli/featurelistcommand.hpp b/lib/cli/featurelistcommand.hpp
new file mode 100644
index 0000000..cae1d74
--- /dev/null
+++ b/lib/cli/featurelistcommand.hpp
@@ -0,0 +1,28 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FEATURELISTCOMMAND_H
+#define FEATURELISTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "feature list" command.
+ *
+ * @ingroup cli
+ */
+class FeatureListCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(FeatureListCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* FEATURELISTCOMMAND_H */
diff --git a/lib/cli/featureutility.cpp b/lib/cli/featureutility.cpp
new file mode 100644
index 0000000..3523868
--- /dev/null
+++ b/lib/cli/featureutility.cpp
@@ -0,0 +1,243 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/featureutility.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/utility.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+
+using namespace icinga;
+
+String FeatureUtility::GetFeaturesAvailablePath()
+{
+ return Configuration::ConfigDir + "/features-available";
+}
+
+String FeatureUtility::GetFeaturesEnabledPath()
+{
+ return Configuration::ConfigDir + "/features-enabled";
+}
+
+std::vector<String> FeatureUtility::GetFieldCompletionSuggestions(const String& word, bool enable)
+{
+ std::vector<String> cache;
+ std::vector<String> suggestions;
+
+ GetFeatures(cache, enable);
+
+ std::sort(cache.begin(), cache.end());
+
+ for (const String& suggestion : cache) {
+ if (suggestion.Find(word) == 0)
+ suggestions.push_back(suggestion);
+ }
+
+ return suggestions;
+}
+
+int FeatureUtility::EnableFeatures(const std::vector<std::string>& features)
+{
+ String features_available_dir = GetFeaturesAvailablePath();
+ String features_enabled_dir = GetFeaturesEnabledPath();
+
+ if (!Utility::PathExists(features_available_dir) ) {
+ Log(LogCritical, "cli")
+ << "Cannot parse available features. Path '" << features_available_dir << "' does not exist.";
+ return 1;
+ }
+
+ if (!Utility::PathExists(features_enabled_dir) ) {
+ Log(LogCritical, "cli")
+ << "Cannot enable features. Path '" << features_enabled_dir << "' does not exist.";
+ return 1;
+ }
+
+ std::vector<std::string> errors;
+
+ for (const String& feature : features) {
+ String source = features_available_dir + "/" + feature + ".conf";
+
+ if (!Utility::PathExists(source) ) {
+ Log(LogCritical, "cli")
+ << "Cannot enable feature '" << feature << "'. Source file '" << source + "' does not exist.";
+ errors.push_back(feature);
+ continue;
+ }
+
+ String target = features_enabled_dir + "/" + feature + ".conf";
+
+ if (Utility::PathExists(target) ) {
+ Log(LogWarning, "cli")
+ << "Feature '" << feature << "' already enabled.";
+ continue;
+ }
+
+ std::cout << "Enabling feature " << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << feature
+ << ConsoleColorTag(Console_Normal) << ". Make sure to restart Icinga 2 for these changes to take effect.\n";
+
+#ifndef _WIN32
+ String relativeSource = "../features-available/" + feature + ".conf";
+
+ if (symlink(relativeSource.CStr(), target.CStr()) < 0) {
+ Log(LogCritical, "cli")
+ << "Cannot enable feature '" << feature << "'. Linking source '" << relativeSource << "' to target file '" << target
+ << "' failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\".";
+ errors.push_back(feature);
+ continue;
+ }
+#else /* _WIN32 */
+ std::ofstream fp;
+ fp.open(target.CStr());
+ fp << "include \"../features-available/" << feature << ".conf\"" << std::endl;
+ fp.close();
+
+ if (fp.fail()) {
+ Log(LogCritical, "cli")
+ << "Cannot enable feature '" << feature << "'. Failed to open file '" << target << "'.";
+ errors.push_back(feature);
+ continue;
+ }
+#endif /* _WIN32 */
+ }
+
+ if (!errors.empty()) {
+ Log(LogCritical, "cli")
+ << "Cannot enable feature(s): " << boost::algorithm::join(errors, " ");
+ errors.clear();
+ return 1;
+ }
+
+ return 0;
+}
+
+int FeatureUtility::DisableFeatures(const std::vector<std::string>& features)
+{
+ String features_enabled_dir = GetFeaturesEnabledPath();
+
+ if (!Utility::PathExists(features_enabled_dir) ) {
+ Log(LogCritical, "cli")
+ << "Cannot disable features. Path '" << features_enabled_dir << "' does not exist.";
+ return 0;
+ }
+
+ std::vector<std::string> errors;
+
+ for (const String& feature : features) {
+ String target = features_enabled_dir + "/" + feature + ".conf";
+
+ if (!Utility::PathExists(target) ) {
+ Log(LogWarning, "cli")
+ << "Feature '" << feature << "' already disabled.";
+ continue;
+ }
+
+ if (unlink(target.CStr()) < 0) {
+ Log(LogCritical, "cli")
+ << "Cannot disable feature '" << feature << "'. Unlinking target file '" << target
+ << "' failed with error code " << errno << ", \"" + Utility::FormatErrorNumber(errno) << "\".";
+ errors.push_back(feature);
+ continue;
+ }
+
+ std::cout << "Disabling feature " << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << feature
+ << ConsoleColorTag(Console_Normal) << ". Make sure to restart Icinga 2 for these changes to take effect.\n";
+ }
+
+ if (!errors.empty()) {
+ Log(LogCritical, "cli")
+ << "Cannot disable feature(s): " << boost::algorithm::join(errors, " ");
+ errors.clear();
+ return 1;
+ }
+
+ return 0;
+}
+
+int FeatureUtility::ListFeatures(std::ostream& os)
+{
+ std::vector<String> disabled_features;
+ std::vector<String> enabled_features;
+
+ if (!FeatureUtility::GetFeatures(disabled_features, true))
+ return 1;
+
+ os << ConsoleColorTag(Console_ForegroundRed | Console_Bold) << "Disabled features: " << ConsoleColorTag(Console_Normal)
+ << boost::algorithm::join(disabled_features, " ") << "\n";
+
+ if (!FeatureUtility::GetFeatures(enabled_features, false))
+ return 1;
+
+ os << ConsoleColorTag(Console_ForegroundGreen | Console_Bold) << "Enabled features: " << ConsoleColorTag(Console_Normal)
+ << boost::algorithm::join(enabled_features, " ") << "\n";
+
+ return 0;
+}
+
+bool FeatureUtility::GetFeatures(std::vector<String>& features, bool get_disabled)
+{
+ /* request all disabled features */
+ if (get_disabled) {
+ /* disable = available-enabled */
+ String available_pattern = GetFeaturesAvailablePath() + "/*.conf";
+ std::vector<String> available;
+ Utility::Glob(available_pattern, [&available](const String& featureFile) { CollectFeatures(featureFile, available); }, GlobFile);
+
+ String enabled_pattern = GetFeaturesEnabledPath() + "/*.conf";
+ std::vector<String> enabled;
+ Utility::Glob(enabled_pattern, [&enabled](const String& featureFile) { CollectFeatures(featureFile, enabled); }, GlobFile);
+
+ std::sort(available.begin(), available.end());
+ std::sort(enabled.begin(), enabled.end());
+ std::set_difference(
+ available.begin(), available.end(),
+ enabled.begin(), enabled.end(),
+ std::back_inserter(features)
+ );
+ } else {
+ /* all enabled features */
+ String enabled_pattern = GetFeaturesEnabledPath() + "/*.conf";
+
+ Utility::Glob(enabled_pattern, [&features](const String& featureFile) { CollectFeatures(featureFile, features); }, GlobFile);
+ }
+
+ return true;
+}
+
+bool FeatureUtility::CheckFeatureEnabled(const String& feature)
+{
+ return CheckFeatureInternal(feature, false);
+}
+
+bool FeatureUtility::CheckFeatureDisabled(const String& feature)
+{
+ return CheckFeatureInternal(feature, true);
+}
+
+bool FeatureUtility::CheckFeatureInternal(const String& feature, bool check_disabled)
+{
+ std::vector<String> features;
+
+ if (!FeatureUtility::GetFeatures(features, check_disabled))
+ return false;
+
+ for (const String& check_feature : features) {
+ if (check_feature == feature)
+ return true;
+ }
+
+ return false;
+}
+
+void FeatureUtility::CollectFeatures(const String& feature_file, std::vector<String>& features)
+{
+ String feature = Utility::BaseName(feature_file);
+ boost::algorithm::replace_all(feature, ".conf", "");
+
+ Log(LogDebug, "cli")
+ << "Adding feature: " << feature;
+ features.push_back(feature);
+}
diff --git a/lib/cli/featureutility.hpp b/lib/cli/featureutility.hpp
new file mode 100644
index 0000000..9cb2128
--- /dev/null
+++ b/lib/cli/featureutility.hpp
@@ -0,0 +1,42 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef FEATUREUTILITY_H
+#define FEATUREUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/string.hpp"
+#include <vector>
+#include <iostream>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class FeatureUtility
+{
+public:
+ static String GetFeaturesAvailablePath();
+ static String GetFeaturesEnabledPath();
+
+ static std::vector<String> GetFieldCompletionSuggestions(const String& word, bool enable);
+
+ static int EnableFeatures(const std::vector<std::string>& features);
+ static int DisableFeatures(const std::vector<std::string>& features);
+ static int ListFeatures(std::ostream& os = std::cout);
+
+ static bool GetFeatures(std::vector<String>& features, bool enable);
+ static bool CheckFeatureEnabled(const String& feature);
+ static bool CheckFeatureDisabled(const String& feature);
+
+private:
+ FeatureUtility();
+ static void CollectFeatures(const String& feature_file, std::vector<String>& features);
+ static bool CheckFeatureInternal(const String& feature, bool check_disabled);
+};
+
+}
+
+#endif /* FEATUREUTILITY_H */
diff --git a/lib/cli/i2-cli.hpp b/lib/cli/i2-cli.hpp
new file mode 100644
index 0000000..86e5ddd
--- /dev/null
+++ b/lib/cli/i2-cli.hpp
@@ -0,0 +1,14 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef I2CLI_H
+#define I2CLI_H
+
+/**
+ * @defgroup cli CLI commands
+ *
+ * The CLI library implements Icinga's command-line interface.
+ */
+
+#include "base/i2-base.hpp"
+
+#endif /* I2CLI_H */
diff --git a/lib/cli/internalsignalcommand.cpp b/lib/cli/internalsignalcommand.cpp
new file mode 100644
index 0000000..b097965
--- /dev/null
+++ b/lib/cli/internalsignalcommand.cpp
@@ -0,0 +1,67 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/internalsignalcommand.hpp"
+#include "base/logger.hpp"
+#include <signal.h>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("internal/signal", InternalSignalCommand);
+
+String InternalSignalCommand::GetDescription() const
+{
+ return "Send signal as Icinga user";
+}
+
+String InternalSignalCommand::GetShortDescription() const
+{
+ return "Send signal as Icinga user";
+}
+
+ImpersonationLevel InternalSignalCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+bool InternalSignalCommand::IsHidden() const
+{
+ return true;
+}
+
+void InternalSignalCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("pid,p", po::value<int>(), "Target PID")
+ ("sig,s", po::value<String>(), "Signal (POSIX string) to send")
+ ;
+}
+
+/**
+ * The entry point for the "internal signal" CLI command.
+ *
+ * @returns An exit status.
+ */
+int InternalSignalCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+#ifndef _WIN32
+ String signal = vm["sig"].as<String>();
+
+ /* Thank POSIX */
+ if (signal == "SIGKILL")
+ return kill(vm["pid"].as<int>(), SIGKILL);
+ if (signal == "SIGINT")
+ return kill(vm["pid"].as<int>(), SIGINT);
+ if (signal == "SIGCHLD")
+ return kill(vm["pid"].as<int>(), SIGCHLD);
+ if (signal == "SIGHUP")
+ return kill(vm["pid"].as<int>(), SIGHUP);
+
+ Log(LogCritical, "cli") << "Unsupported signal \"" << signal << "\"";
+#else
+ Log(LogCritical, "cli", "Unsupported action on Windows.");
+#endif /* _Win32 */
+ return 1;
+}
+
diff --git a/lib/cli/internalsignalcommand.hpp b/lib/cli/internalsignalcommand.hpp
new file mode 100644
index 0000000..d599b80
--- /dev/null
+++ b/lib/cli/internalsignalcommand.hpp
@@ -0,0 +1,33 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef INTERNALSIGNALCOMMAND_H
+#define INTERNALSIGNALCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "internal signal" command.
+ *
+ * @ingroup cli
+ */
+class InternalSignalCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(InternalSignalCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ bool IsHidden() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* INTERNALSIGNALCOMMAND_H */
diff --git a/lib/cli/nodesetupcommand.cpp b/lib/cli/nodesetupcommand.cpp
new file mode 100644
index 0000000..2a685b5
--- /dev/null
+++ b/lib/cli/nodesetupcommand.cpp
@@ -0,0 +1,559 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/nodesetupcommand.hpp"
+#include "cli/nodeutility.hpp"
+#include "cli/featureutility.hpp"
+#include "cli/apisetuputility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+
+#include <iostream>
+#include <fstream>
+#include <vector>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("node/setup", NodeSetupCommand);
+
+String NodeSetupCommand::GetDescription() const
+{
+ return "Sets up an Icinga 2 node.";
+}
+
+String NodeSetupCommand::GetShortDescription() const
+{
+ return "set up node";
+}
+
+void NodeSetupCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("zone", po::value<std::string>(), "The name of the local zone")
+ ("endpoint", po::value<std::vector<std::string> >(), "Connect to remote endpoint; syntax: cn[,host,port]")
+ ("parent_host", po::value<std::string>(), "The name of the parent host for auto-signing the csr; syntax: host[,port]")
+ ("parent_zone", po::value<std::string>(), "The name of the parent zone")
+ ("listen", po::value<std::string>(), "Listen on host,port")
+ ("ticket", po::value<std::string>(), "Generated ticket number for this request (optional)")
+ ("trustedcert", po::value<std::string>(), "Trusted parent certificate file as connection verification (received via 'pki save-cert')")
+ ("cn", po::value<std::string>(), "The certificate's common name")
+ ("accept-config", "Accept config from parent node")
+ ("accept-commands", "Accept commands from parent node")
+ ("master", "Use setup for a master instance")
+ ("global_zones", po::value<std::vector<std::string> >(), "The names of the additional global zones to 'global-templates' and 'director-global'.")
+ ("disable-confd", "Disables the conf.d directory during the setup");
+
+ hiddenDesc.add_options()
+ ("master_zone", po::value<std::string>(), "DEPRECATED: The name of the master zone")
+ ("master_host", po::value<std::string>(), "DEPRECATED: The name of the master host for auto-signing the csr; syntax: host[,port]");
+}
+
+std::vector<String> NodeSetupCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "key" || argument == "cert" || argument == "trustedcert")
+ return GetBashCompletionSuggestions("file", word);
+ else if (argument == "host")
+ return GetBashCompletionSuggestions("hostname", word);
+ else if (argument == "port")
+ return GetBashCompletionSuggestions("service", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+ImpersonationLevel NodeSetupCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+/**
+ * The entry point for the "node setup" CLI command.
+ *
+ * @returns An exit status.
+ */
+int NodeSetupCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!ap.empty()) {
+ Log(LogWarning, "cli")
+ << "Ignoring parameters: " << boost::algorithm::join(ap, " ");
+ }
+
+ if (vm.count("master"))
+ return SetupMaster(vm, ap);
+ else
+ return SetupNode(vm, ap);
+}
+
+int NodeSetupCommand::SetupMaster(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap)
+{
+ /* Ignore not required parameters */
+ if (vm.count("ticket"))
+ Log(LogWarning, "cli", "Master for Node setup: Ignoring --ticket");
+
+ if (vm.count("endpoint"))
+ Log(LogWarning, "cli", "Master for Node setup: Ignoring --endpoint");
+
+ if (vm.count("trustedcert"))
+ Log(LogWarning, "cli", "Master for Node setup: Ignoring --trustedcert");
+
+ String cn = Utility::GetFQDN();
+
+ if (vm.count("cn"))
+ cn = vm["cn"].as<std::string>();
+
+ /* Setup command hardcodes this as FQDN */
+ String endpointName = cn;
+
+ /* Allow to specify zone name. */
+ String zoneName = "master";
+
+ if (vm.count("zone"))
+ zoneName = vm["zone"].as<std::string>();
+
+ /* check whether the user wants to generate a new certificate or not */
+ String existingPath = ApiListener::GetCertsDir() + "/" + cn + ".crt";
+
+ Log(LogInformation, "cli")
+ << "Checking in existing certificates for common name '" << cn << "'...";
+
+ if (Utility::PathExists(existingPath)) {
+ Log(LogWarning, "cli")
+ << "Certificate '" << existingPath << "' for CN '" << cn << "' already exists. Not generating new certificate.";
+ } else {
+ Log(LogInformation, "cli")
+ << "Certificates not yet generated. Running 'api setup' now.";
+
+ ApiSetupUtility::SetupMasterCertificates(cn);
+ }
+
+ Log(LogInformation, "cli", "Generating master configuration for Icinga 2.");
+ ApiSetupUtility::SetupMasterApiUser();
+
+ if (!FeatureUtility::CheckFeatureEnabled("api")) {
+ ApiSetupUtility::SetupMasterEnableApi();
+ } else {
+ Log(LogInformation, "cli")
+ << "'api' feature already enabled.\n";
+ }
+
+ /* write zones.conf and update with zone + endpoint information */
+ Log(LogInformation, "cli", "Generating zone and object configuration.");
+
+ std::vector<String> globalZones { "global-templates", "director-global" };
+ std::vector<std::string> setupGlobalZones;
+
+ if (vm.count("global_zones"))
+ setupGlobalZones = vm["global_zones"].as<std::vector<std::string> >();
+
+ for (decltype(setupGlobalZones.size()) i = 0; i < setupGlobalZones.size(); i++) {
+ if (std::find(globalZones.begin(), globalZones.end(), setupGlobalZones[i]) != globalZones.end()) {
+ Log(LogCritical, "cli")
+ << "The global zone '" << setupGlobalZones[i] << "' is already specified.";
+ return 1;
+ }
+ }
+
+ globalZones.insert(globalZones.end(), setupGlobalZones.begin(), setupGlobalZones.end());
+
+ /* Generate master configuration. */
+ NodeUtility::GenerateNodeMasterIcingaConfig(endpointName, zoneName, globalZones);
+
+ /* Update the ApiListener config. */
+ Log(LogInformation, "cli", "Updating the APIListener feature.");
+
+ String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
+ NodeUtility::CreateBackupFile(apipath);
+
+ AtomicFile fp (apipath, 0644);
+
+ fp << "/**\n"
+ << " * The API listener is used for distributed monitoring setups.\n"
+ << " */\n"
+ << "object ApiListener \"api\" {\n";
+
+ if (vm.count("listen")) {
+ std::vector<String> tokens = String(vm["listen"].as<std::string>()).Split(",");
+
+ if (tokens.size() > 0)
+ fp << " bind_host = \"" << tokens[0] << "\"\n";
+ if (tokens.size() > 1)
+ fp << " bind_port = " << tokens[1] << "\n";
+ }
+
+ fp << "\n";
+
+ if (vm.count("accept-config"))
+ fp << " accept_config = true\n";
+ else
+ fp << " accept_config = false\n";
+
+ if (vm.count("accept-commands"))
+ fp << " accept_commands = true\n";
+ else
+ fp << " accept_commands = false\n";
+
+ fp << "\n"
+ << " ticket_salt = TicketSalt\n"
+ << "}\n";
+
+ fp.Commit();
+
+ /* update constants.conf with NodeName = CN + TicketSalt = random value */
+ if (endpointName != Utility::GetFQDN()) {
+ Log(LogWarning, "cli")
+ << "CN/Endpoint name '" << endpointName << "' does not match the default FQDN '" << Utility::GetFQDN() << "'. Requires update for NodeName constant in constants.conf!";
+ }
+
+ NodeUtility::UpdateConstant("NodeName", endpointName);
+ NodeUtility::UpdateConstant("ZoneName", zoneName);
+
+ String salt = RandomString(16);
+
+ NodeUtility::UpdateConstant("TicketSalt", salt);
+
+ Log(LogInformation, "cli")
+ << "Edit the api feature config file '" << apipath << "' and set a secure 'ticket_salt' attribute.";
+
+ if (vm.count("disable-confd")) {
+ /* Disable conf.d inclusion */
+ if (NodeUtility::UpdateConfiguration("\"conf.d\"", false, true)) {
+ Log(LogInformation, "cli")
+ << "Disabled conf.d inclusion";
+ } else {
+ Log(LogWarning, "cli")
+ << "Tried to disable conf.d inclusion but failed, possibly it's already disabled.";
+ }
+
+ /* Include api-users.conf */
+ String apiUsersFilePath = ApiSetupUtility::GetApiUsersConfPath();
+
+ if (Utility::PathExists(apiUsersFilePath)) {
+ NodeUtility::UpdateConfiguration("\"conf.d/api-users.conf\"", true, false);
+ } else {
+ Log(LogWarning, "cli")
+ << "Included file doesn't exist " << apiUsersFilePath;
+ }
+ }
+
+ /* tell the user to reload icinga2 */
+ Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
+
+ return 0;
+}
+
+int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap)
+{
+ /* require at least one endpoint. Ticket is optional. */
+ if (!vm.count("endpoint")) {
+ Log(LogCritical, "cli", "You need to specify at least one endpoint (--endpoint).");
+ return 1;
+ }
+
+ if (!vm.count("zone")) {
+ Log(LogCritical, "cli", "You need to specify the local zone (--zone).");
+ return 1;
+ }
+
+ /* Deprecation warnings. TODO: Remove in 2.10.0. */
+ if (vm.count("master_zone"))
+ Log(LogWarning, "cli", "The 'master_zone' parameter has been deprecated. Use 'parent_zone' instead.");
+ if (vm.count("master_host"))
+ Log(LogWarning, "cli", "The 'master_host' parameter has been deprecated. Use 'parent_host' instead.");
+
+ String ticket;
+
+ if (vm.count("ticket"))
+ ticket = vm["ticket"].as<std::string>();
+
+ if (ticket.IsEmpty()) {
+ Log(LogInformation, "cli")
+ << "Requesting certificate without a ticket.";
+ } else {
+ Log(LogInformation, "cli")
+ << "Requesting certificate with ticket '" << ticket << "'.";
+ }
+
+ /* Decide whether to directly connect to the parent node for CSR signing, or leave it to the user. */
+ bool connectToParent = false;
+ String parentHost;
+ String parentPort = "5665";
+ std::shared_ptr<X509> trustedParentCert;
+
+ /* TODO: remove master_host in 2.10.0. */
+ if (!vm.count("master_host") && !vm.count("parent_host")) {
+ connectToParent = false;
+
+ Log(LogWarning, "cli")
+ << "Node to master/satellite connection setup skipped. Please configure your parent node to\n"
+ << "connect to this node by setting the 'host' attribute for the node Endpoint object.\n";
+ } else {
+ connectToParent = true;
+
+ String parentHostInfo;
+
+ if (vm.count("parent_host"))
+ parentHostInfo = vm["parent_host"].as<std::string>();
+ else if (vm.count("master_host")) /* TODO: Remove in 2.10.0. */
+ parentHostInfo = vm["master_host"].as<std::string>();
+
+ std::vector<String> tokens = parentHostInfo.Split(",");
+
+ if (tokens.size() == 1 || tokens.size() == 2)
+ parentHost = tokens[0];
+
+ if (tokens.size() == 2)
+ parentPort = tokens[1];
+
+ Log(LogInformation, "cli")
+ << "Verifying parent host connection information: host '" << parentHost << "', port '" << parentPort << "'.";
+
+ }
+
+ /* retrieve CN and pass it (defaults to FQDN) */
+ String cn = Utility::GetFQDN();
+
+ if (vm.count("cn"))
+ cn = vm["cn"].as<std::string>();
+
+ Log(LogInformation, "cli")
+ << "Using the following CN (defaults to FQDN): '" << cn << "'.";
+
+ /* pki request a signed certificate from the master */
+ String certsDir = ApiListener::GetCertsDir();
+ Utility::MkDirP(certsDir, 0700);
+
+ String user = Configuration::RunAsUser;
+ String group = Configuration::RunAsGroup;
+
+ if (!Utility::SetFileOwnership(certsDir, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << certsDir << "'. Verify it yourself!";
+ }
+
+ String key = certsDir + "/" + cn + ".key";
+ String cert = certsDir + "/" + cn + ".crt";
+ String ca = certsDir + "/ca.crt";
+
+ if (Utility::PathExists(key))
+ NodeUtility::CreateBackupFile(key, true);
+ if (Utility::PathExists(cert))
+ NodeUtility::CreateBackupFile(cert);
+
+ if (PkiUtility::NewCert(cn, key, String(), cert) != 0) {
+ Log(LogCritical, "cli", "Failed to generate new self-signed certificate.");
+ return 1;
+ }
+
+ /* fix permissions: root -> icinga daemon user */
+ if (!Utility::SetFileOwnership(key, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << key << "'. Verify it yourself!";
+ }
+
+ /* Send a signing request to the parent immediately, or leave it to the user. */
+ if (connectToParent) {
+ /* In contrast to `node wizard` the user must manually fetch
+ * the trustedParentCert to prove the trust relationship (fetched with 'pki save-cert').
+ */
+ if (!vm.count("trustedcert")) {
+ Log(LogCritical, "cli")
+ << "Please pass the trusted cert retrieved from the parent node (master or satellite)\n"
+ << "(Hint: 'icinga2 pki save-cert --host <parenthost> --port <5665> --key local.key --cert local.crt --trustedcert trusted-parent.crt').";
+ return 1;
+ }
+
+ String trustedCert = vm["trustedcert"].as<std::string>();
+
+ try{
+ trustedParentCert = GetX509Certificate(trustedCert);
+ } catch (const std::exception&) {
+ Log(LogCritical, "cli")
+ << "Can't read trusted cert at '" << trustedCert << "'.";
+ return 1;
+ }
+
+ try {
+ if (IsCa(trustedParentCert)) {
+ Log(LogCritical, "cli")
+ << "The trusted parent certificate is NOT a client certificate. It seems you passed the 'ca.crt' CA certificate via '--trustedcert' parameter.";
+ return 1;
+ }
+ } catch (const std::exception&) {
+ /* Swallow the error and do not run the check on unsupported OpenSSL platforms. */
+ }
+
+ Log(LogInformation, "cli")
+ << "Verifying trusted certificate file '" << vm["trustedcert"].as<std::string>() << "'.";
+
+ Log(LogInformation, "cli", "Requesting a signed certificate from the parent Icinga node.");
+
+ if (PkiUtility::RequestCertificate(parentHost, parentPort, key, cert, ca, trustedParentCert, ticket) > 0) {
+ Log(LogCritical, "cli")
+ << "Failed to fetch signed certificate from parent Icinga node '"
+ << parentHost << ", "
+ << parentPort << "'. Please try again.";
+ return 1;
+ }
+ } else {
+ /* We cannot retrieve the parent certificate.
+ * Tell the user to manually copy the ca.crt file
+ * into DataDir + "/certs"
+ */
+ Log(LogWarning, "cli")
+ << "\nNo connection to the parent node was specified.\n\n"
+ << "Please copy the public CA certificate from your master/satellite\n"
+ << "into '" << ca << "' before starting Icinga 2.\n";
+
+ if (Utility::PathExists(ca)) {
+ Log(LogInformation, "cli")
+ << "\nFound public CA certificate in '" << ca << "'.\n"
+ << "Please verify that it is the same as on your master/satellite.\n";
+ }
+ }
+
+ if (!Utility::SetFileOwnership(ca, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << ca << "'. Verify it yourself!";
+ }
+
+ /* fix permissions (again) when updating the signed certificate */
+ if (!Utility::SetFileOwnership(cert, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << cert << "'. Verify it yourself!";
+ }
+
+ /* disable the notifications feature */
+ Log(LogInformation, "cli", "Disabling the Notification feature.");
+
+ FeatureUtility::DisableFeatures({ "notification" });
+
+ /* enable the ApiListener config */
+
+ Log(LogInformation, "cli", "Updating the ApiListener feature.");
+
+ FeatureUtility::EnableFeatures({ "api" });
+
+ String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
+ NodeUtility::CreateBackupFile(apipath);
+
+ AtomicFile fp (apipath, 0644);
+
+ fp << "/**\n"
+ << " * The API listener is used for distributed monitoring setups.\n"
+ << " */\n"
+ << "object ApiListener \"api\" {\n";
+
+ if (vm.count("listen")) {
+ std::vector<String> tokens = String(vm["listen"].as<std::string>()).Split(",");
+
+ if (tokens.size() > 0)
+ fp << " bind_host = \"" << tokens[0] << "\"\n";
+ if (tokens.size() > 1)
+ fp << " bind_port = " << tokens[1] << "\n";
+ }
+
+ fp << "\n";
+
+ if (vm.count("accept-config"))
+ fp << " accept_config = true\n";
+ else
+ fp << " accept_config = false\n";
+
+ if (vm.count("accept-commands"))
+ fp << " accept_commands = true\n";
+ else
+ fp << " accept_commands = false\n";
+
+ fp << "\n"
+ << "}\n";
+
+ fp.Commit();
+
+ /* Generate zones configuration. */
+ Log(LogInformation, "cli", "Generating zone and object configuration.");
+
+ /* Setup command hardcodes this as FQDN */
+ String endpointName = cn;
+
+ /* Allow to specify zone name. */
+ String zoneName = vm["zone"].as<std::string>();
+
+ /* Allow to specify the parent zone name. */
+ String parentZoneName = "master";
+
+ if (vm.count("parent_zone"))
+ parentZoneName = vm["parent_zone"].as<std::string>();
+
+ std::vector<String> globalZones { "global-templates", "director-global" };
+ std::vector<std::string> setupGlobalZones;
+
+ if (vm.count("global_zones"))
+ setupGlobalZones = vm["global_zones"].as<std::vector<std::string> >();
+
+ for (decltype(setupGlobalZones.size()) i = 0; i < setupGlobalZones.size(); i++) {
+ if (std::find(globalZones.begin(), globalZones.end(), setupGlobalZones[i]) != globalZones.end()) {
+ Log(LogCritical, "cli")
+ << "The global zone '" << setupGlobalZones[i] << "' is already specified.";
+ return 1;
+ }
+ }
+
+ globalZones.insert(globalZones.end(), setupGlobalZones.begin(), setupGlobalZones.end());
+
+ /* Generate node configuration. */
+ NodeUtility::GenerateNodeIcingaConfig(endpointName, zoneName, parentZoneName, vm["endpoint"].as<std::vector<std::string> >(), globalZones);
+
+ /* update constants.conf with NodeName = CN */
+ if (endpointName != Utility::GetFQDN()) {
+ Log(LogWarning, "cli")
+ << "CN/Endpoint name '" << endpointName << "' does not match the default FQDN '"
+ << Utility::GetFQDN() << "'. Requires an update for the NodeName constant in constants.conf!";
+ }
+
+ NodeUtility::UpdateConstant("NodeName", endpointName);
+ NodeUtility::UpdateConstant("ZoneName", zoneName);
+
+ if (!ticket.IsEmpty()) {
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+ AtomicFile af (ticketPath, 0600);
+
+ if (!Utility::SetFileOwnership(af.GetTempFilename(), user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group
+ << "' on file '" << ticketPath << "'. Verify it yourself!";
+ }
+
+ af << ticket;
+ af.Commit();
+ }
+
+ /* If no parent connection was made, the user must supply the ca.crt before restarting Icinga 2.*/
+ if (!connectToParent) {
+ Log(LogWarning, "cli")
+ << "No connection to the parent node was specified.\n\n"
+ << "Please copy the public CA certificate from your master/satellite\n"
+ << "into '" << ca << "' before starting Icinga 2.\n";
+ } else {
+ Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
+ }
+
+ if (vm.count("disable-confd")) {
+ /* Disable conf.d inclusion */
+ NodeUtility::UpdateConfiguration("\"conf.d\"", false, true);
+ }
+
+ /* tell the user to reload icinga2 */
+ Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
+
+ return 0;
+}
diff --git a/lib/cli/nodesetupcommand.hpp b/lib/cli/nodesetupcommand.hpp
new file mode 100644
index 0000000..d25d21e
--- /dev/null
+++ b/lib/cli/nodesetupcommand.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NODESETUPCOMMAND_H
+#define NODESETUPCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "node setup" command.
+ *
+ * @ingroup cli
+ */
+class NodeSetupCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(NodeSetupCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+private:
+ static int SetupMaster(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap);
+ static int SetupNode(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap);
+};
+
+}
+
+#endif /* NODESETUPCOMMAND_H */
diff --git a/lib/cli/nodeutility.cpp b/lib/cli/nodeutility.cpp
new file mode 100644
index 0000000..523532a
--- /dev/null
+++ b/lib/cli/nodeutility.cpp
@@ -0,0 +1,378 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/nodeutility.hpp"
+#include "cli/clicommand.hpp"
+#include "cli/variableutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/convert.hpp"
+#include "base/utility.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/json.hpp"
+#include "base/netstring.hpp"
+#include "base/stdiostream.hpp"
+#include "base/debug.hpp"
+#include "base/objectlock.hpp"
+#include "base/console.hpp"
+#include "base/exception.hpp"
+#include "base/configwriter.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+
+using namespace icinga;
+
+String NodeUtility::GetConstantsConfPath()
+{
+ return Configuration::ConfigDir + "/constants.conf";
+}
+
+String NodeUtility::GetZonesConfPath()
+{
+ return Configuration::ConfigDir + "/zones.conf";
+}
+
+/*
+ * Node Setup helpers
+ */
+
+int NodeUtility::GenerateNodeIcingaConfig(const String& endpointName, const String& zoneName,
+ const String& parentZoneName, const std::vector<std::string>& endpoints,
+ const std::vector<String>& globalZones)
+{
+ Array::Ptr config = new Array();
+
+ Array::Ptr myParentZoneMembers = new Array();
+
+ for (const String& endpoint : endpoints) {
+ /* extract all --endpoint arguments and store host,port info */
+ std::vector<String> tokens = endpoint.Split(",");
+
+ Dictionary::Ptr myParentEndpoint = new Dictionary();
+
+ if (tokens.size() > 1) {
+ String host = tokens[1].Trim();
+
+ if (!host.IsEmpty())
+ myParentEndpoint->Set("host", host);
+ }
+
+ if (tokens.size() > 2) {
+ String port = tokens[2].Trim();
+
+ if (!port.IsEmpty())
+ myParentEndpoint->Set("port", port);
+ }
+
+ String myEndpointName = tokens[0].Trim();
+ myParentEndpoint->Set("__name", myEndpointName);
+ myParentEndpoint->Set("__type", "Endpoint");
+
+ /* save endpoint in master zone */
+ myParentZoneMembers->Add(myEndpointName);
+
+ config->Add(myParentEndpoint);
+ }
+
+ /* add the parent zone to the config */
+ config->Add(new Dictionary({
+ { "__name", parentZoneName },
+ { "__type", "Zone" },
+ { "endpoints", myParentZoneMembers }
+ }));
+
+ /* store the local generated node configuration */
+ config->Add(new Dictionary({
+ { "__name", endpointName },
+ { "__type", "Endpoint" }
+ }));
+
+ config->Add(new Dictionary({
+ { "__name", zoneName },
+ { "__type", "Zone" },
+ { "parent", parentZoneName },
+ { "endpoints", new Array({ endpointName }) }
+ }));
+
+ for (const String& globalzone : globalZones) {
+ config->Add(new Dictionary({
+ { "__name", globalzone },
+ { "__type", "Zone" },
+ { "global", true }
+ }));
+ }
+
+ /* Write the newly generated configuration. */
+ NodeUtility::WriteNodeConfigObjects(GetZonesConfPath(), config);
+
+ return 0;
+}
+
+int NodeUtility::GenerateNodeMasterIcingaConfig(const String& endpointName, const String& zoneName,
+ const std::vector<String>& globalZones)
+{
+ Array::Ptr config = new Array();
+
+ /* store the local generated node master configuration */
+ config->Add(new Dictionary({
+ { "__name", endpointName },
+ { "__type", "Endpoint" }
+ }));
+
+ config->Add(new Dictionary({
+ { "__name", zoneName },
+ { "__type", "Zone" },
+ { "endpoints", new Array({ endpointName }) }
+ }));
+
+ for (const String& globalzone : globalZones) {
+ config->Add(new Dictionary({
+ { "__name", globalzone },
+ { "__type", "Zone" },
+ { "global", true }
+ }));
+ }
+
+ /* Write the newly generated configuration. */
+ NodeUtility::WriteNodeConfigObjects(GetZonesConfPath(), config);
+
+ return 0;
+}
+
+bool NodeUtility::WriteNodeConfigObjects(const String& filename, const Array::Ptr& objects)
+{
+ Log(LogInformation, "cli")
+ << "Dumping config items to file '" << filename << "'.";
+
+ /* create a backup first */
+ CreateBackupFile(filename);
+
+ String path = Utility::DirName(filename);
+
+ Utility::MkDirP(path, 0755);
+
+ String user = Configuration::RunAsUser;
+ String group = Configuration::RunAsGroup;
+
+ if (!Utility::SetFileOwnership(path, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user << "' group '" << group << "' on path '" << path << "'. Verify it yourself!";
+ }
+
+ AtomicFile fp (filename, 0644);
+
+ fp << "/*\n";
+ fp << " * Generated by Icinga 2 node setup commands\n";
+ fp << " * on " << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime()) << "\n";
+ fp << " */\n\n";
+
+ ObjectLock olock(objects);
+ for (const Dictionary::Ptr& object : objects) {
+ SerializeObject(fp, object);
+ }
+
+ fp << std::endl;
+ fp.Commit();
+
+ return true;
+}
+
+
+/*
+ * We generally don't overwrite files without backup before
+ */
+bool NodeUtility::CreateBackupFile(const String& target, bool isPrivate)
+{
+ if (!Utility::PathExists(target))
+ return false;
+
+ String backup = target + ".orig";
+
+ if (Utility::PathExists(backup)) {
+ Log(LogInformation, "cli")
+ << "Backup file '" << backup << "' already exists. Skipping backup.";
+ return false;
+ }
+
+ Utility::CopyFile(target, backup);
+
+#ifndef _WIN32
+ if (isPrivate)
+ chmod(backup.CStr(), 0600);
+#endif /* _WIN32 */
+
+ Log(LogInformation, "cli")
+ << "Created backup file '" << backup << "'.";
+
+ return true;
+}
+
+void NodeUtility::SerializeObject(std::ostream& fp, const Dictionary::Ptr& object)
+{
+ fp << "object ";
+ ConfigWriter::EmitIdentifier(fp, object->Get("__type"), false);
+ fp << " ";
+ ConfigWriter::EmitValue(fp, 0, object->Get("__name"));
+ fp << " {\n";
+
+ ObjectLock olock(object);
+ for (const Dictionary::Pair& kv : object) {
+ if (kv.first == "__type" || kv.first == "__name")
+ continue;
+
+ fp << "\t";
+ ConfigWriter::EmitIdentifier(fp, kv.first, true);
+ fp << " = ";
+ ConfigWriter::EmitValue(fp, 1, kv.second);
+ fp << "\n";
+ }
+
+ fp << "}\n\n";
+}
+
+/*
+* Returns true if the include is found, otherwise false
+*/
+bool NodeUtility::GetConfigurationIncludeState(const String& value, bool recursive) {
+ String configurationFile = Configuration::ConfigDir + "/icinga2.conf";
+
+ Log(LogInformation, "cli")
+ << "Reading '" << configurationFile << "'.";
+
+ std::ifstream ifp(configurationFile.CStr());
+
+ String affectedInclude = value;
+
+ if (recursive)
+ affectedInclude = "include_recursive " + affectedInclude;
+ else
+ affectedInclude = "include " + affectedInclude;
+
+ bool isIncluded = false;
+
+ std::string line;
+
+ while(std::getline(ifp, line)) {
+ /*
+ * Trying to find if the inclusion is enabled.
+ * First hit breaks out of the loop.
+ */
+
+ if (line.compare(0, affectedInclude.GetLength(), affectedInclude) == 0) {
+ isIncluded = true;
+
+ /*
+ * We can safely break out here, since an enabled include always win.
+ */
+ break;
+ }
+ }
+
+ ifp.close();
+
+ return isIncluded;
+}
+
+/*
+ * include = false, will comment out the include statement
+ * include = true, will add an include statement or uncomment a statement if one is existing
+ * resursive = false, will search for a non-resursive include statement
+ * recursive = true, will search for a resursive include statement
+ * Returns true on success, false if option was not found
+ */
+bool NodeUtility::UpdateConfiguration(const String& value, bool include, bool recursive)
+{
+ String configurationFile = Configuration::ConfigDir + "/icinga2.conf";
+
+ Log(LogInformation, "cli")
+ << "Updating '" << value << "' include in '" << configurationFile << "'.";
+
+ NodeUtility::CreateBackupFile(configurationFile);
+
+ std::ifstream ifp(configurationFile.CStr());
+ AtomicFile ofp (configurationFile, 0644);
+
+ String affectedInclude = value;
+
+ if (recursive)
+ affectedInclude = "include_recursive " + affectedInclude;
+ else
+ affectedInclude = "include " + affectedInclude;
+
+ bool found = false;
+
+ std::string line;
+
+ while (std::getline(ifp, line)) {
+ if (include) {
+ if (line.find("//" + affectedInclude) != std::string::npos || line.find("// " + affectedInclude) != std::string::npos) {
+ found = true;
+ ofp << "// Added by the node setup CLI command on "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime())
+ << "\n" + affectedInclude + "\n";
+ } else if (line.find(affectedInclude) != std::string::npos) {
+ found = true;
+
+ Log(LogInformation, "cli")
+ << "Include statement '" + affectedInclude + "' already set.";
+
+ ofp << line << "\n";
+ } else {
+ ofp << line << "\n";
+ }
+ } else {
+ if (line.find(affectedInclude) != std::string::npos) {
+ found = true;
+ ofp << "// Disabled by the node setup CLI command on "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime())
+ << "\n// " + affectedInclude + "\n";
+ } else {
+ ofp << line << "\n";
+ }
+ }
+ }
+
+ if (include && !found) {
+ ofp << "// Added by the node setup CLI command on "
+ << Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", Utility::GetTime())
+ << "\n" + affectedInclude + "\n";
+ }
+
+ ifp.close();
+ ofp.Commit();
+
+ return (found || include);
+}
+
+void NodeUtility::UpdateConstant(const String& name, const String& value)
+{
+ String constantsConfPath = NodeUtility::GetConstantsConfPath();
+
+ Log(LogInformation, "cli")
+ << "Updating '" << name << "' constant in '" << constantsConfPath << "'.";
+
+ NodeUtility::CreateBackupFile(constantsConfPath);
+
+ std::ifstream ifp(constantsConfPath.CStr());
+ AtomicFile ofp (constantsConfPath, 0644);
+
+ bool found = false;
+
+ std::string line;
+ while (std::getline(ifp, line)) {
+ if (line.find("const " + name + " = ") != std::string::npos) {
+ ofp << "const " + name + " = \"" + value + "\"\n";
+ found = true;
+ } else
+ ofp << line << "\n";
+ }
+
+ if (!found)
+ ofp << "const " + name + " = \"" + value + "\"\n";
+
+ ifp.close();
+ ofp.Commit();
+}
diff --git a/lib/cli/nodeutility.hpp b/lib/cli/nodeutility.hpp
new file mode 100644
index 0000000..7016b6b
--- /dev/null
+++ b/lib/cli/nodeutility.hpp
@@ -0,0 +1,49 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NODEUTILITY_H
+#define NODEUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "base/value.hpp"
+#include "base/string.hpp"
+#include <vector>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class NodeUtility
+{
+public:
+ static String GetConstantsConfPath();
+ static String GetZonesConfPath();
+
+ static bool CreateBackupFile(const String& target, bool isPrivate = false);
+
+ static bool WriteNodeConfigObjects(const String& filename, const Array::Ptr& objects);
+
+ static bool GetConfigurationIncludeState(const String& value, bool recursive);
+ static bool UpdateConfiguration(const String& value, bool include, bool recursive);
+ static void UpdateConstant(const String& name, const String& value);
+
+ /* node setup helpers */
+ static int GenerateNodeIcingaConfig(const String& endpointName, const String& zoneName,
+ const String& parentZoneName, const std::vector<std::string>& endpoints,
+ const std::vector<String>& globalZones);
+ static int GenerateNodeMasterIcingaConfig(const String& endpointName, const String& zoneName,
+ const std::vector<String>& globalZones);
+
+private:
+ NodeUtility();
+
+ static void SerializeObject(std::ostream& fp, const Dictionary::Ptr& object);
+};
+
+}
+
+#endif /* NODEUTILITY_H */
diff --git a/lib/cli/nodewizardcommand.cpp b/lib/cli/nodewizardcommand.cpp
new file mode 100644
index 0000000..3a3cd42
--- /dev/null
+++ b/lib/cli/nodewizardcommand.cpp
@@ -0,0 +1,815 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/nodewizardcommand.hpp"
+#include "cli/nodeutility.hpp"
+#include "cli/featureutility.hpp"
+#include "cli/apisetuputility.hpp"
+#include "remote/apilistener.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/atomic-file.hpp"
+#include "base/logger.hpp"
+#include "base/console.hpp"
+#include "base/application.hpp"
+#include "base/tlsutility.hpp"
+#include "base/scriptglobal.hpp"
+#include "base/exception.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/case_conv.hpp>
+#include <iostream>
+#include <string>
+#include <fstream>
+#include <vector>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("node/wizard", NodeWizardCommand);
+
+String NodeWizardCommand::GetDescription() const
+{
+ return "Wizard for Icinga 2 node setup.";
+}
+
+String NodeWizardCommand::GetShortDescription() const
+{
+ return "wizard for node setup";
+}
+
+ImpersonationLevel NodeWizardCommand::GetImpersonationLevel() const
+{
+ return ImpersonateIcinga;
+}
+
+int NodeWizardCommand::GetMaxArguments() const
+{
+ return -1;
+}
+
+void NodeWizardCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("verbose", "increase log level");
+}
+
+/**
+ * The entry point for the "node wizard" CLI command.
+ *
+ * @returns An exit status.
+ */
+int NodeWizardCommand::Run(const boost::program_options::variables_map& vm,
+ const std::vector<std::string>& ap) const
+{
+ if (!vm.count("verbose"))
+ Logger::SetConsoleLogSeverity(LogCritical);
+
+ /*
+ * The wizard will get all information from the user,
+ * and then call all required functions.
+ */
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundBlue)
+ << "Welcome to the Icinga 2 Setup Wizard!\n"
+ << "\n"
+ << "We will guide you through all required configuration details.\n"
+ << "\n"
+ << ConsoleColorTag(Console_Normal);
+
+ /* 0. master or node setup?
+ * 1. Ticket
+ * 2. Master information for autosigning
+ * 3. Trusted cert location
+ * 4. CN to use (defaults to FQDN)
+ * 5. Local CA
+ * 6. New self signed certificate
+ * 7. Request signed certificate from master
+ * 8. copy key information to /var/lib/icinga2/certs
+ * 9. enable ApiListener feature
+ * 10. generate zones.conf with endpoints and zone objects
+ * 11. set NodeName = cn and ZoneName in constants.conf
+ * 12. disable conf.d directory?
+ * 13. reload icinga2, or tell the user to
+ */
+
+ std::string answer;
+ /* master or satellite/agent setup */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify if this is an agent/satellite setup "
+ << "('n' installs a master setup)" << ConsoleColorTag(Console_Normal)
+ << " [Y/n]: ";
+ std::getline (std::cin, answer);
+
+ boost::algorithm::to_lower(answer);
+
+ String choice = answer;
+
+ std::cout << "\n";
+
+ int res = 0;
+
+ if (choice.Contains("n"))
+ res = MasterSetup();
+ else
+ res = AgentSatelliteSetup();
+
+ if (res != 0)
+ return res;
+
+ std::cout << "\n";
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Done.\n\n"
+ << ConsoleColorTag(Console_Normal);
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundRed)
+ << "Now restart your Icinga 2 daemon to finish the installation!\n"
+ << ConsoleColorTag(Console_Normal);
+
+ return 0;
+}
+
+int NodeWizardCommand::AgentSatelliteSetup() const
+{
+ std::string answer;
+ String choice;
+ bool connectToParent = false;
+
+ std::cout << "Starting the Agent/Satellite setup routine...\n\n";
+
+ /* CN */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the common name (CN)"
+ << ConsoleColorTag(Console_Normal)
+ << " [" << Utility::GetFQDN() << "]: ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = Utility::GetFQDN();
+
+ String cn = answer;
+ cn = cn.Trim();
+
+ std::vector<std::string> endpoints;
+
+ String endpointBuffer;
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "\nPlease specify the parent endpoint(s) (master or satellite) where this node should connect to:"
+ << ConsoleColorTag(Console_Normal) << "\n";
+ String parentEndpointName;
+
+wizard_endpoint_loop_start:
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Master/Satellite Common Name" << ConsoleColorTag(Console_Normal)
+ << " (CN from your master/satellite node): ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ Log(LogWarning, "cli", "Master/Satellite CN is required! Please retry.");
+ goto wizard_endpoint_loop_start;
+ }
+
+ endpointBuffer = answer;
+ endpointBuffer = endpointBuffer.Trim();
+
+ std::cout << "\nDo you want to establish a connection to the parent node "
+ << ConsoleColorTag(Console_Bold) << "from this node?"
+ << ConsoleColorTag(Console_Normal) << " [Y/n]: ";
+
+ std::getline (std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ String parentEndpointPort = "5665";
+
+ if (choice.Contains("n")) {
+ connectToParent = false;
+
+ Log(LogWarning, "cli", "Node to master/satellite connection setup skipped");
+ std::cout << "Connection setup skipped. Please configure your parent node to\n"
+ << "connect to this node by setting the 'host' attribute for the node Endpoint object.\n";
+
+ } else {
+ connectToParent = true;
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the master/satellite connection information:"
+ << ConsoleColorTag(Console_Normal) << "\n"
+ << ConsoleColorTag(Console_Bold) << "Master/Satellite endpoint host"
+ << ConsoleColorTag(Console_Normal) << " (IP address or FQDN): ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ Log(LogWarning, "cli", "Please enter the parent endpoint (master/satellite) connection information.");
+ goto wizard_endpoint_loop_start;
+ }
+
+ String tmp = answer;
+ tmp = tmp.Trim();
+
+ endpointBuffer += "," + tmp;
+ parentEndpointName = tmp;
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Master/Satellite endpoint port" << ConsoleColorTag(Console_Normal)
+ << " [" << parentEndpointPort << "]: ";
+
+ std::getline(std::cin, answer);
+
+ if (!answer.empty())
+ parentEndpointPort = answer;
+
+ endpointBuffer += "," + parentEndpointPort.Trim();
+ }
+
+ endpoints.push_back(endpointBuffer);
+
+ std::cout << ConsoleColorTag(Console_Bold) << "\nAdd more master/satellite endpoints?"
+ << ConsoleColorTag(Console_Normal) << " [y/N]: ";
+ std::getline (std::cin, answer);
+
+ boost::algorithm::to_lower(answer);
+
+ choice = answer;
+
+ if (choice.Contains("y"))
+ goto wizard_endpoint_loop_start;
+
+ /* Extract parent node information. */
+ String parentHost, parentPort;
+
+ for (const String& endpoint : endpoints) {
+ std::vector<String> tokens = endpoint.Split(",");
+
+ if (tokens.size() > 1)
+ parentHost = tokens[1];
+
+ if (tokens.size() > 2)
+ parentPort = tokens[2];
+ }
+
+ /* workaround for fetching the master cert */
+ String certsDir = ApiListener::GetCertsDir();
+ Utility::MkDirP(certsDir, 0700);
+
+ String user = Configuration::RunAsUser;
+ String group = Configuration::RunAsGroup;
+
+ if (!Utility::SetFileOwnership(certsDir, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group
+ << "' on file '" << certsDir << "'. Verify it yourself!";
+ }
+
+ String nodeCert = certsDir + "/" + cn + ".crt";
+ String nodeKey = certsDir + "/" + cn + ".key";
+
+ if (Utility::PathExists(nodeKey))
+ NodeUtility::CreateBackupFile(nodeKey, true);
+ if (Utility::PathExists(nodeCert))
+ NodeUtility::CreateBackupFile(nodeCert);
+
+ if (PkiUtility::NewCert(cn, nodeKey, Empty, nodeCert) > 0) {
+ Log(LogCritical, "cli")
+ << "Failed to create new self-signed certificate for CN '"
+ << cn << "'. Please try again.";
+ return 1;
+ }
+
+ /* fix permissions: root -> icinga daemon user */
+ if (!Utility::SetFileOwnership(nodeKey, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group
+ << "' on file '" << nodeKey << "'. Verify it yourself!";
+ }
+
+ std::shared_ptr<X509> trustedParentCert;
+
+ /* Check whether we should connect to the parent node and present its trusted certificate. */
+ if (connectToParent) {
+ //save-cert and store the master certificate somewhere
+ Log(LogInformation, "cli")
+ << "Fetching public certificate from master ("
+ << parentHost << ", " << parentPort << "):\n";
+
+ trustedParentCert = PkiUtility::FetchCert(parentHost, parentPort);
+ if (!trustedParentCert) {
+ Log(LogCritical, "cli", "Peer did not present a valid certificate.");
+ return 1;
+ }
+
+ std::cout << ConsoleColorTag(Console_Bold) << "Parent certificate information:\n"
+ << ConsoleColorTag(Console_Normal) << PkiUtility::GetCertificateInformation(trustedParentCert)
+ << ConsoleColorTag(Console_Bold) << "\nIs this information correct?"
+ << ConsoleColorTag(Console_Normal) << " [y/N]: ";
+
+ std::getline (std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ if (answer != "y") {
+ Log(LogWarning, "cli", "Process aborted.");
+ return 1;
+ }
+
+ Log(LogInformation, "cli", "Received trusted parent certificate.\n");
+ }
+
+wizard_ticket:
+ String nodeCA = certsDir + "/ca.crt";
+ String ticket;
+
+ /* Check whether we can connect to the parent node and fetch the client and CA certificate. */
+ if (connectToParent) {
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "\nPlease specify the request ticket generated on your Icinga 2 master "
+ << ConsoleColorTag(Console_Normal) << "(optional)"
+ << ConsoleColorTag(Console_Bold) << "."
+ << ConsoleColorTag(Console_Normal) << "\n"
+ << " (Hint: # icinga2 pki ticket --cn '" << cn << "'): ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ std::cout << ConsoleColorTag(Console_Bold) << "\n"
+ << "No ticket was specified. Please approve the certificate signing request manually\n"
+ << "on the master (see 'icinga2 ca list' and 'icinga2 ca sign --help' for details)."
+ << ConsoleColorTag(Console_Normal) << "\n";
+ }
+
+ ticket = answer;
+ ticket = ticket.Trim();
+
+ if (ticket.IsEmpty()) {
+ Log(LogInformation, "cli")
+ << "Requesting certificate without a ticket.";
+ } else {
+ Log(LogInformation, "cli")
+ << "Requesting certificate with ticket '" << ticket << "'.";
+ }
+
+ if (Utility::PathExists(nodeCA))
+ NodeUtility::CreateBackupFile(nodeCA);
+ if (Utility::PathExists(nodeCert))
+ NodeUtility::CreateBackupFile(nodeCert);
+
+ if (PkiUtility::RequestCertificate(parentHost, parentPort, nodeKey,
+ nodeCert, nodeCA, trustedParentCert, ticket) > 0) {
+ Log(LogCritical, "cli")
+ << "Failed to fetch signed certificate from master '"
+ << parentHost << ", "
+ << parentPort << "'. Please try again.";
+ goto wizard_ticket;
+ }
+
+ /* fix permissions (again) when updating the signed certificate */
+ if (!Utility::SetFileOwnership(nodeCert, user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group << "' on file '"
+ << nodeCert << "'. Verify it yourself!";
+ }
+ } else {
+ /* We cannot retrieve the parent certificate.
+ * Tell the user to manually copy the ca.crt file
+ * into DataDir + "/certs"
+ */
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "\nNo connection to the parent node was specified.\n\n"
+ << "Please copy the public CA certificate from your master/satellite\n"
+ << "into '" << nodeCA << "' before starting Icinga 2.\n"
+ << ConsoleColorTag(Console_Normal);
+
+ if (Utility::PathExists(nodeCA)) {
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "\nFound public CA certificate in '" << nodeCA << "'.\n"
+ << "Please verify that it is the same as on your master/satellite.\n"
+ << ConsoleColorTag(Console_Normal);
+ }
+
+ }
+
+ /* apilistener config */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the API bind host/port "
+ << ConsoleColorTag(Console_Normal) << "(optional)"
+ << ConsoleColorTag(Console_Bold) << ":\n";
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Bind Host" << ConsoleColorTag(Console_Normal) << " []: ";
+
+ std::getline(std::cin, answer);
+
+ String bindHost = answer;
+ bindHost = bindHost.Trim();
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Bind Port" << ConsoleColorTag(Console_Normal) << " []: ";
+
+ std::getline(std::cin, answer);
+
+ String bindPort = answer;
+ bindPort = bindPort.Trim();
+
+ std::cout << ConsoleColorTag(Console_Bold) << "\n"
+ << "Accept config from parent node?" << ConsoleColorTag(Console_Normal)
+ << " [y/N]: ";
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ String acceptConfig = choice.Contains("y") ? "true" : "false";
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Accept commands from parent node?" << ConsoleColorTag(Console_Normal)
+ << " [y/N]: ";
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ String acceptCommands = choice.Contains("y") ? "true" : "false";
+
+ std::cout << "\n";
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Reconfiguring Icinga...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ /* disable the notifications feature on agent/satellite nodes */
+ Log(LogInformation, "cli", "Disabling the Notification feature.");
+
+ FeatureUtility::DisableFeatures({ "notification" });
+
+ Log(LogInformation, "cli", "Enabling the ApiListener feature.");
+
+ FeatureUtility::EnableFeatures({ "api" });
+
+ String apiConfPath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
+ NodeUtility::CreateBackupFile(apiConfPath);
+
+ AtomicFile fp (apiConfPath, 0644);
+
+ fp << "/**\n"
+ << " * The API listener is used for distributed monitoring setups.\n"
+ << " */\n"
+ << "object ApiListener \"api\" {\n"
+ << " accept_config = " << acceptConfig << "\n"
+ << " accept_commands = " << acceptCommands << "\n";
+
+ if (!bindHost.IsEmpty())
+ fp << " bind_host = \"" << bindHost << "\"\n";
+ if (!bindPort.IsEmpty())
+ fp << " bind_port = " << bindPort << "\n";
+
+ fp << "}\n";
+
+ fp.Commit();
+
+ /* Zones configuration. */
+ Log(LogInformation, "cli", "Generating local zones.conf.");
+
+ /* Setup command hardcodes this as FQDN */
+ String endpointName = cn;
+
+ /* Different local zone name. */
+ std::cout << "\nLocal zone name [" + endpointName + "]: ";
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = endpointName;
+
+ String zoneName = answer;
+ zoneName = zoneName.Trim();
+
+ /* Different parent zone name. */
+ std::cout << "Parent zone name [master]: ";
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = "master";
+
+ String parentZoneName = answer;
+ parentZoneName = parentZoneName.Trim();
+
+ /* Global zones. */
+ std::vector<String> globalZones { "global-templates", "director-global" };
+
+ std::cout << "\nDefault global zones: " << boost::algorithm::join(globalZones, " ");
+ std::cout << "\nDo you want to specify additional global zones? [y/N]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+wizard_global_zone_loop_start:
+ if (choice.Contains("y")) {
+ std::cout << "\nPlease specify the name of the global Zone: ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ std::cout << "\nName of the global Zone is required! Please retry.";
+ goto wizard_global_zone_loop_start;
+ }
+
+ String globalZoneName = answer;
+ globalZoneName = globalZoneName.Trim();
+
+ if (std::find(globalZones.begin(), globalZones.end(), globalZoneName) != globalZones.end()) {
+ std::cout << "The global zone '" << globalZoneName << "' is already specified."
+ << " Please retry.";
+ goto wizard_global_zone_loop_start;
+ }
+
+ globalZones.push_back(globalZoneName);
+
+ std::cout << "\nDo you want to specify another global zone? [y/N]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ if (choice.Contains("y"))
+ goto wizard_global_zone_loop_start;
+ } else
+ Log(LogInformation, "cli", "No additional global Zones have been specified");
+
+ /* Generate node configuration. */
+ NodeUtility::GenerateNodeIcingaConfig(endpointName, zoneName, parentZoneName, endpoints, globalZones);
+
+ if (endpointName != Utility::GetFQDN()) {
+ Log(LogWarning, "cli")
+ << "CN/Endpoint name '" << endpointName << "' does not match the default FQDN '"
+ << Utility::GetFQDN() << "'. Requires update for NodeName constant in constants.conf!";
+ }
+
+ NodeUtility::UpdateConstant("NodeName", endpointName);
+ NodeUtility::UpdateConstant("ZoneName", zoneName);
+
+ if (!ticket.IsEmpty()) {
+ String ticketPath = ApiListener::GetCertsDir() + "/ticket";
+ AtomicFile af (ticketPath, 0600);
+
+ if (!Utility::SetFileOwnership(af.GetTempFilename(), user, group)) {
+ Log(LogWarning, "cli")
+ << "Cannot set ownership for user '" << user
+ << "' group '" << group
+ << "' on file '" << ticketPath << "'. Verify it yourself!";
+ }
+
+ af << ticket;
+ af.Commit();
+ }
+
+ /* If no parent connection was made, the user must supply the ca.crt before restarting Icinga 2.*/
+ if (!connectToParent) {
+ Log(LogWarning, "cli")
+ << "No connection to the parent node was specified.\n\n"
+ << "Please copy the public CA certificate from your master/satellite\n"
+ << "into '" << nodeCA << "' before starting Icinga 2.\n";
+ } else {
+ Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
+ }
+
+ /* Disable conf.d inclusion */
+ std::cout << "\nDo you want to disable the inclusion of the conf.d directory [Y/n]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ if (choice.Contains("n"))
+ Log(LogInformation, "cli")
+ << "conf.d directory has not been disabled.";
+ else {
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Disabling the inclusion of the conf.d directory...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ if (!NodeUtility::UpdateConfiguration("\"conf.d\"", false, true)) {
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundRed)
+ << "Failed to disable the conf.d inclusion, it may already have been disabled.\n"
+ << ConsoleColorTag(Console_Normal);
+ }
+
+ /* Satellite/Agents should not include the api-users.conf file.
+ * The configuration should instead be managed via config sync or automation tools.
+ */
+ }
+
+ return 0;
+}
+
+int NodeWizardCommand::MasterSetup() const
+{
+ std::string answer;
+ String choice;
+
+ std::cout << ConsoleColorTag(Console_Bold) << "Starting the Master setup routine...\n\n";
+
+ /* CN */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the common name" << ConsoleColorTag(Console_Normal)
+ << " (CN) [" << Utility::GetFQDN() << "]: ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = Utility::GetFQDN();
+
+ String cn = answer;
+ cn = cn.Trim();
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Reconfiguring Icinga...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ /* check whether the user wants to generate a new certificate or not */
+ String existing_path = ApiListener::GetCertsDir() + "/" + cn + ".crt";
+
+ std::cout << ConsoleColorTag(Console_Normal)
+ << "Checking for existing certificates for common name '" << cn << "'...\n";
+
+ if (Utility::PathExists(existing_path)) {
+ std::cout << "Certificate '" << existing_path << "' for CN '"
+ << cn << "' already existing. Skipping certificate generation.\n";
+ } else {
+ std::cout << "Certificates not yet generated. Running 'api setup' now.\n";
+ ApiSetupUtility::SetupMasterCertificates(cn);
+ }
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Generating master configuration for Icinga 2.\n"
+ << ConsoleColorTag(Console_Normal);
+
+ ApiSetupUtility::SetupMasterApiUser();
+
+ if (!FeatureUtility::CheckFeatureEnabled("api"))
+ ApiSetupUtility::SetupMasterEnableApi();
+ else
+ std::cout << "'api' feature already enabled.\n";
+
+ /* Setup command hardcodes this as FQDN */
+ String endpointName = cn;
+
+ /* Different zone name. */
+ std::cout << "\nMaster zone name [master]: ";
+ std::getline(std::cin, answer);
+
+ if (answer.empty())
+ answer = "master";
+
+ String zoneName = answer;
+ zoneName = zoneName.Trim();
+
+ /* Global zones. */
+ std::vector<String> globalZones { "global-templates", "director-global" };
+
+ std::cout << "\nDefault global zones: " << boost::algorithm::join(globalZones, " ");
+ std::cout << "\nDo you want to specify additional global zones? [y/N]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+wizard_global_zone_loop_start:
+ if (choice.Contains("y")) {
+ std::cout << "\nPlease specify the name of the global Zone: ";
+
+ std::getline(std::cin, answer);
+
+ if (answer.empty()) {
+ std::cout << "\nName of the global Zone is required! Please retry.";
+ goto wizard_global_zone_loop_start;
+ }
+
+ String globalZoneName = answer;
+ globalZoneName = globalZoneName.Trim();
+
+ if (std::find(globalZones.begin(), globalZones.end(), globalZoneName) != globalZones.end()) {
+ std::cout << "The global zone '" << globalZoneName << "' is already specified."
+ << " Please retry.";
+ goto wizard_global_zone_loop_start;
+ }
+
+ globalZones.push_back(globalZoneName);
+
+ std::cout << "\nDo you want to specify another global zone? [y/N]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ if (choice.Contains("y"))
+ goto wizard_global_zone_loop_start;
+ } else
+ Log(LogInformation, "cli", "No additional global Zones have been specified");
+
+ /* Generate master configuration. */
+ NodeUtility::GenerateNodeMasterIcingaConfig(endpointName, zoneName, globalZones);
+
+ /* apilistener config */
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Please specify the API bind host/port "
+ << ConsoleColorTag(Console_Normal) << "(optional)"
+ << ConsoleColorTag(Console_Bold) << ":\n";
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Bind Host" << ConsoleColorTag(Console_Normal) << " []: ";
+
+ std::getline(std::cin, answer);
+
+ String bindHost = answer;
+ bindHost = bindHost.Trim();
+
+ std::cout << ConsoleColorTag(Console_Bold)
+ << "Bind Port" << ConsoleColorTag(Console_Normal) << " []: ";
+
+ std::getline(std::cin, answer);
+
+ String bindPort = answer;
+ bindPort = bindPort.Trim();
+
+ /* api feature is always enabled, check above */
+ String apiConfPath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
+ NodeUtility::CreateBackupFile(apiConfPath);
+
+ AtomicFile fp (apiConfPath, 0644);
+
+ fp << "/**\n"
+ << " * The API listener is used for distributed monitoring setups.\n"
+ << " */\n"
+ << "object ApiListener \"api\" {\n";
+
+ if (!bindHost.IsEmpty())
+ fp << " bind_host = \"" << bindHost << "\"\n";
+ if (!bindPort.IsEmpty())
+ fp << " bind_port = " << bindPort << "\n";
+
+ fp << "\n"
+ << " ticket_salt = TicketSalt\n"
+ << "}\n";
+
+ fp.Commit();
+
+ /* update constants.conf with NodeName = CN + TicketSalt = random value */
+ if (cn != Utility::GetFQDN()) {
+ Log(LogWarning, "cli")
+ << "CN '" << cn << "' does not match the default FQDN '"
+ << Utility::GetFQDN() << "'. Requires an update for the NodeName constant in constants.conf!";
+ }
+
+ Log(LogInformation, "cli", "Updating constants.conf.");
+
+ NodeUtility::CreateBackupFile(NodeUtility::GetConstantsConfPath());
+
+ NodeUtility::UpdateConstant("NodeName", endpointName);
+ NodeUtility::UpdateConstant("ZoneName", zoneName);
+
+ String salt = RandomString(16);
+
+ NodeUtility::UpdateConstant("TicketSalt", salt);
+
+ /* Disable conf.d inclusion */
+ std::cout << "\nDo you want to disable the inclusion of the conf.d directory [Y/n]: ";
+
+ std::getline(std::cin, answer);
+ boost::algorithm::to_lower(answer);
+ choice = answer;
+
+ if (choice.Contains("n"))
+ Log(LogInformation, "cli")
+ << "conf.d directory has not been disabled.";
+ else {
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Disabling the inclusion of the conf.d directory...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ if (!NodeUtility::UpdateConfiguration("\"conf.d\"", false, true)) {
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundRed)
+ << "Failed to disable the conf.d inclusion, it may already have been disabled.\n"
+ << ConsoleColorTag(Console_Normal);
+ }
+
+ /* Include api-users.conf */
+ String apiUsersFilePath = Configuration::ConfigDir + "/conf.d/api-users.conf";
+
+ std::cout << ConsoleColorTag(Console_Bold | Console_ForegroundGreen)
+ << "Checking if the api-users.conf file exists...\n"
+ << ConsoleColorTag(Console_Normal);
+
+ if (Utility::PathExists(apiUsersFilePath)) {
+ NodeUtility::UpdateConfiguration("\"conf.d/api-users.conf\"", true, false);
+ } else {
+ Log(LogWarning, "cli")
+ << "Included file '" << apiUsersFilePath << "' does not exist.";
+ }
+ }
+
+ return 0;
+}
diff --git a/lib/cli/nodewizardcommand.hpp b/lib/cli/nodewizardcommand.hpp
new file mode 100644
index 0000000..dfda70c
--- /dev/null
+++ b/lib/cli/nodewizardcommand.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef NODEWIZARDCOMMAND_H
+#define NODEWIZARDCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "node wizard" command.
+ *
+ * @ingroup cli
+ */
+class NodeWizardCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(NodeWizardCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMaxArguments() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+ ImpersonationLevel GetImpersonationLevel() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+
+private:
+ int AgentSatelliteSetup() const;
+ int MasterSetup() const;
+};
+
+}
+
+#endif /* NODEWIZARDCOMMAND_H */
diff --git a/lib/cli/objectlistcommand.cpp b/lib/cli/objectlistcommand.cpp
new file mode 100644
index 0000000..3bcb315
--- /dev/null
+++ b/lib/cli/objectlistcommand.cpp
@@ -0,0 +1,145 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/objectlistcommand.hpp"
+#include "cli/objectlistutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/convert.hpp"
+#include "base/configobject.hpp"
+#include "base/configtype.hpp"
+#include "base/json.hpp"
+#include "base/netstring.hpp"
+#include "base/stdiostream.hpp"
+#include "base/debug.hpp"
+#include "base/objectlock.hpp"
+#include "base/console.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+#include <iomanip>
+#include <sys/stat.h>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("object/list", ObjectListCommand);
+
+String ObjectListCommand::GetDescription() const
+{
+ return "Lists all Icinga 2 objects.";
+}
+
+String ObjectListCommand::GetShortDescription() const
+{
+ return "lists all objects";
+}
+
+void ObjectListCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("count,c", "display object counts by types")
+ ("name,n", po::value<std::string>(), "filter by name matches")
+ ("type,t", po::value<std::string>(), "filter by type matches");
+}
+
+static time_t GetCtime(const String& path)
+{
+#ifdef _WIN32
+ struct _stat statbuf;
+ int rc = _stat(path.CStr(), &statbuf);
+#else /* _WIN32 */
+ struct stat statbuf;
+ int rc = stat(path.CStr(), &statbuf);
+#endif /* _WIN32 */
+
+ return rc ? 0 : statbuf.st_ctime;
+}
+
+/**
+ * The entry point for the "object list" CLI command.
+ *
+ * @returns An exit status.
+ */
+int ObjectListCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String objectfile = Configuration::ObjectsPath;
+
+ if (!Utility::PathExists(objectfile)) {
+ Log(LogCritical, "cli")
+ << "Cannot open objects file '" << Configuration::ObjectsPath << "'.";
+ Log(LogCritical, "cli", "Run 'icinga2 daemon -C --dump-objects' to validate config and generate the cache file.");
+ return 1;
+ }
+
+ std::fstream fp;
+ fp.open(objectfile.CStr(), std::ios_base::in);
+
+ StdioStream::Ptr sfp = new StdioStream(&fp, false);
+ unsigned long objects_count = 0;
+ std::map<String, int> type_count;
+
+ String name_filter, type_filter;
+
+ if (vm.count("name"))
+ name_filter = vm["name"].as<std::string>();
+ if (vm.count("type"))
+ type_filter = vm["type"].as<std::string>();
+
+ bool first = true;
+
+ String message;
+ StreamReadContext src;
+ for (;;) {
+ StreamReadStatus srs = NetString::ReadStringFromStream(sfp, &message, src);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ ObjectListUtility::PrintObject(std::cout, first, message, type_count, name_filter, type_filter);
+ objects_count++;
+ }
+
+ sfp->Close();
+ fp.close();
+
+ if (vm.count("count")) {
+ if (!first)
+ std::cout << "\n";
+
+ PrintTypeCounts(std::cout, type_count);
+ std::cout << "\n";
+ }
+
+ Log(LogNotice, "cli")
+ << "Parsed " << objects_count << " objects.";
+
+ auto objectsPathCtime (GetCtime(Configuration::ObjectsPath));
+ auto varsPathCtime (GetCtime(Configuration::VarsPath));
+
+ if (objectsPathCtime < varsPathCtime) {
+ Log(LogWarning, "cli")
+ << "This data is " << Utility::FormatDuration(varsPathCtime - objectsPathCtime)
+ << " older than the last Icinga config (re)load. It may be outdated. Consider running 'icinga2 daemon -C --dump-objects' first.";
+ }
+
+ return 0;
+}
+
+void ObjectListCommand::PrintTypeCounts(std::ostream& fp, const std::map<String, int>& type_count)
+{
+ typedef std::map<String, int>::value_type TypeCount;
+
+ for (const TypeCount& kv : type_count) {
+ fp << "Found " << kv.second << " " << kv.first << " object";
+
+ if (kv.second != 1)
+ fp << "s";
+
+ fp << ".\n";
+ }
+}
diff --git a/lib/cli/objectlistcommand.hpp b/lib/cli/objectlistcommand.hpp
new file mode 100644
index 0000000..bafe3ec
--- /dev/null
+++ b/lib/cli/objectlistcommand.hpp
@@ -0,0 +1,36 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTLISTCOMMAND_H
+#define OBJECTLISTCOMMAND_H
+
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "cli/clicommand.hpp"
+#include <ostream>
+
+namespace icinga
+{
+
+/**
+ * The "object list" command.
+ *
+ * @ingroup cli
+ */
+class ObjectListCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(ObjectListCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+private:
+ static void PrintTypeCounts(std::ostream& fp, const std::map<String, int>& type_count);
+};
+
+}
+
+#endif /* OBJECTLISTCOMMAND_H */
diff --git a/lib/cli/objectlistutility.cpp b/lib/cli/objectlistutility.cpp
new file mode 100644
index 0000000..a8135d9
--- /dev/null
+++ b/lib/cli/objectlistutility.cpp
@@ -0,0 +1,155 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/objectlistutility.hpp"
+#include "base/json.hpp"
+#include "base/utility.hpp"
+#include "base/console.hpp"
+#include "base/objectlock.hpp"
+#include "base/convert.hpp"
+#include <iostream>
+#include <iomanip>
+
+using namespace icinga;
+
+bool ObjectListUtility::PrintObject(std::ostream& fp, bool& first, const String& message, std::map<String, int>& type_count, const String& name_filter, const String& type_filter)
+{
+ Dictionary::Ptr object = JsonDecode(message);
+
+ Dictionary::Ptr properties = object->Get("properties");
+
+ String internal_name = properties->Get("__name");
+ String name = object->Get("name");
+ String type = object->Get("type");
+
+ if (!name_filter.IsEmpty() && !Utility::Match(name_filter, name) && !Utility::Match(name_filter, internal_name))
+ return false;
+ if (!type_filter.IsEmpty() && !Utility::Match(type_filter, type))
+ return false;
+
+ if (first)
+ first = false;
+ else
+ fp << "\n";
+
+ Dictionary::Ptr debug_hints = object->Get("debug_hints");
+
+ fp << "Object '" << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << internal_name << ConsoleColorTag(Console_Normal) << "'";
+ fp << " of type '" << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << type << ConsoleColorTag(Console_Normal) << "':\n";
+
+ Array::Ptr di = object->Get("debug_info");
+
+ if (di) {
+ fp << ConsoleColorTag(Console_ForegroundCyan) << " % declared in '" << di->Get(0) << "', lines "
+ << di->Get(1) << ":" << di->Get(2) << "-" << di->Get(3) << ":" << di->Get(4) << ConsoleColorTag(Console_Normal) << "\n";
+ }
+
+ PrintProperties(fp, properties, debug_hints, 2);
+
+ type_count[type]++;
+ return true;
+}
+
+void ObjectListUtility::PrintProperties(std::ostream& fp, const Dictionary::Ptr& props, const Dictionary::Ptr& debug_hints, int indent)
+{
+ /* get debug hint props */
+ Dictionary::Ptr debug_hint_props;
+ if (debug_hints)
+ debug_hint_props = debug_hints->Get("properties");
+
+ int offset = 2;
+
+ ObjectLock olock(props);
+ for (const Dictionary::Pair& kv : props)
+ {
+ String key = kv.first;
+ Value val = kv.second;
+
+ /* key & value */
+ fp << std::setw(indent) << " " << "* " << ConsoleColorTag(Console_ForegroundGreen) << key << ConsoleColorTag(Console_Normal);
+
+ /* extract debug hints for key */
+ Dictionary::Ptr debug_hints_fwd;
+ if (debug_hint_props)
+ debug_hints_fwd = debug_hint_props->Get(key);
+
+ /* print dicts recursively */
+ if (val.IsObjectType<Dictionary>()) {
+ fp << "\n";
+ PrintHints(fp, debug_hints_fwd, indent + offset);
+ PrintProperties(fp, val, debug_hints_fwd, indent + offset);
+ } else {
+ fp << " = ";
+ PrintValue(fp, val);
+ fp << "\n";
+ PrintHints(fp, debug_hints_fwd, indent + offset);
+ }
+ }
+}
+
+void ObjectListUtility::PrintHints(std::ostream& fp, const Dictionary::Ptr& debug_hints, int indent)
+{
+ if (!debug_hints)
+ return;
+
+ Array::Ptr messages = debug_hints->Get("messages");
+
+ if (messages) {
+ ObjectLock olock(messages);
+
+ for (const Value& msg : messages)
+ {
+ PrintHint(fp, msg, indent);
+ }
+ }
+}
+
+void ObjectListUtility::PrintHint(std::ostream& fp, const Array::Ptr& msg, int indent)
+{
+ fp << std::setw(indent) << " " << ConsoleColorTag(Console_ForegroundCyan) << "% " << msg->Get(0) << " modified in '" << msg->Get(1) << "', lines "
+ << msg->Get(2) << ":" << msg->Get(3) << "-" << msg->Get(4) << ":" << msg->Get(5) << ConsoleColorTag(Console_Normal) << "\n";
+}
+
+void ObjectListUtility::PrintValue(std::ostream& fp, const Value& val)
+{
+ if (val.IsObjectType<Array>()) {
+ PrintArray(fp, val);
+ return;
+ }
+
+ if (val.IsString()) {
+ fp << "\"" << Convert::ToString(val) << "\"";
+ return;
+ }
+
+ if (val.IsEmpty()) {
+ fp << "null";
+ return;
+ }
+
+ fp << Convert::ToString(val);
+}
+
+void ObjectListUtility::PrintArray(std::ostream& fp, const Array::Ptr& arr)
+{
+ bool first = true;
+
+ fp << "[ ";
+
+ if (arr) {
+ ObjectLock olock(arr);
+ for (const Value& value : arr)
+ {
+ if (first)
+ first = false;
+ else
+ fp << ", ";
+
+ PrintValue(fp, value);
+ }
+ }
+
+ if (!first)
+ fp << " ";
+
+ fp << "]";
+}
diff --git a/lib/cli/objectlistutility.hpp b/lib/cli/objectlistutility.hpp
new file mode 100644
index 0000000..ee1b97c
--- /dev/null
+++ b/lib/cli/objectlistutility.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef OBJECTLISTUTILITY_H
+#define OBJECTLISTUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "base/value.hpp"
+#include "base/string.hpp"
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class ObjectListUtility
+{
+public:
+ static bool PrintObject(std::ostream& fp, bool& first, const String& message, std::map<String, int>& type_count, const String& name_filter, const String& type_filter);
+
+private:
+ static void PrintProperties(std::ostream& fp, const Dictionary::Ptr& props, const Dictionary::Ptr& debug_hints, int indent);
+ static void PrintHints(std::ostream& fp, const Dictionary::Ptr& debug_hints, int indent);
+ static void PrintHint(std::ostream& fp, const Array::Ptr& msg, int indent);
+ static void PrintValue(std::ostream& fp, const Value& val);
+ static void PrintArray(std::ostream& fp, const Array::Ptr& arr);
+};
+
+}
+
+#endif /* OBJECTLISTUTILITY_H */
diff --git a/lib/cli/pkinewcacommand.cpp b/lib/cli/pkinewcacommand.cpp
new file mode 100644
index 0000000..eba08c6
--- /dev/null
+++ b/lib/cli/pkinewcacommand.cpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkinewcacommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+
+REGISTER_CLICOMMAND("pki/new-ca", PKINewCACommand);
+
+String PKINewCACommand::GetDescription() const
+{
+ return "Sets up a new Certificate Authority.";
+}
+
+String PKINewCACommand::GetShortDescription() const
+{
+ return "sets up a new CA";
+}
+
+/**
+ * The entry point for the "pki new-ca" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKINewCACommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ return PkiUtility::NewCa();
+}
diff --git a/lib/cli/pkinewcacommand.hpp b/lib/cli/pkinewcacommand.hpp
new file mode 100644
index 0000000..5b1bff6
--- /dev/null
+++ b/lib/cli/pkinewcacommand.hpp
@@ -0,0 +1,29 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKINEWCACOMMAND_H
+#define PKINEWCACOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki new-ca" command.
+ *
+ * @ingroup cli
+ */
+class PKINewCACommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKINewCACommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKINEWCACOMMAND_H */
diff --git a/lib/cli/pkinewcertcommand.cpp b/lib/cli/pkinewcertcommand.cpp
new file mode 100644
index 0000000..5201d92
--- /dev/null
+++ b/lib/cli/pkinewcertcommand.cpp
@@ -0,0 +1,66 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkinewcertcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/new-cert", PKINewCertCommand);
+
+String PKINewCertCommand::GetDescription() const
+{
+ return "Creates a new Certificate Signing Request, a self-signed X509 certificate or both.";
+}
+
+String PKINewCertCommand::GetShortDescription() const
+{
+ return "creates a new CSR";
+}
+
+void PKINewCertCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("cn", po::value<std::string>(), "Common Name")
+ ("key", po::value<std::string>(), "Key file path (output)")
+ ("csr", po::value<std::string>(), "CSR file path (optional, output)")
+ ("cert", po::value<std::string>(), "Certificate file path (optional, output)");
+}
+
+std::vector<String> PKINewCertCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "key" || argument == "csr" || argument == "cert")
+ return GetBashCompletionSuggestions("file", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki new-cert" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKINewCertCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("cn")) {
+ Log(LogCritical, "cli", "Common name (--cn) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("key")) {
+ Log(LogCritical, "cli", "Key file path (--key) must be specified.");
+ return 1;
+ }
+
+ String csr, cert;
+
+ if (vm.count("csr"))
+ csr = vm["csr"].as<std::string>();
+
+ if (vm.count("cert"))
+ cert = vm["cert"].as<std::string>();
+
+ return PkiUtility::NewCert(vm["cn"].as<std::string>(), vm["key"].as<std::string>(), csr, cert);
+}
diff --git a/lib/cli/pkinewcertcommand.hpp b/lib/cli/pkinewcertcommand.hpp
new file mode 100644
index 0000000..0c39bb6
--- /dev/null
+++ b/lib/cli/pkinewcertcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKINEWCERTCOMMAND_H
+#define PKINEWCERTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki new-cert" command.
+ *
+ * @ingroup cli
+ */
+class PKINewCertCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKINewCertCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKINEWCERTCOMMAND_H */
diff --git a/lib/cli/pkirequestcommand.cpp b/lib/cli/pkirequestcommand.cpp
new file mode 100644
index 0000000..d2b79f0
--- /dev/null
+++ b/lib/cli/pkirequestcommand.cpp
@@ -0,0 +1,93 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkirequestcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+#include "base/tlsutility.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/request", PKIRequestCommand);
+
+String PKIRequestCommand::GetDescription() const
+{
+ return "Sends a PKI request to Icinga 2.";
+}
+
+String PKIRequestCommand::GetShortDescription() const
+{
+ return "requests a certificate";
+}
+
+void PKIRequestCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("key", po::value<std::string>(), "Key file path (input)")
+ ("cert", po::value<std::string>(), "Certificate file path (input + output)")
+ ("ca", po::value<std::string>(), "CA file path (output)")
+ ("trustedcert", po::value<std::string>(), "Trusted certificate file path (input)")
+ ("host", po::value<std::string>(), "Icinga 2 host")
+ ("port", po::value<std::string>(), "Icinga 2 port")
+ ("ticket", po::value<std::string>(), "Icinga 2 PKI ticket");
+}
+
+std::vector<String> PKIRequestCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "key" || argument == "cert" || argument == "ca" || argument == "trustedcert")
+ return GetBashCompletionSuggestions("file", word);
+ else if (argument == "host")
+ return GetBashCompletionSuggestions("hostname", word);
+ else if (argument == "port")
+ return GetBashCompletionSuggestions("service", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki request" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKIRequestCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("host")) {
+ Log(LogCritical, "cli", "Icinga 2 host (--host) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("key")) {
+ Log(LogCritical, "cli", "Key input file path (--key) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("cert")) {
+ Log(LogCritical, "cli", "Certificate output file path (--cert) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("ca")) {
+ Log(LogCritical, "cli", "CA certificate output file path (--ca) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("trustedcert")) {
+ Log(LogCritical, "cli", "Trusted certificate input file path (--trustedcert) must be specified.");
+ return 1;
+ }
+
+ String port = "5665";
+ String ticket;
+
+ if (vm.count("port"))
+ port = vm["port"].as<std::string>();
+
+ if (vm.count("ticket"))
+ ticket = vm["ticket"].as<std::string>();
+
+ return PkiUtility::RequestCertificate(vm["host"].as<std::string>(), port, vm["key"].as<std::string>(),
+ vm["cert"].as<std::string>(), vm["ca"].as<std::string>(), GetX509Certificate(vm["trustedcert"].as<std::string>()),
+ ticket);
+}
diff --git a/lib/cli/pkirequestcommand.hpp b/lib/cli/pkirequestcommand.hpp
new file mode 100644
index 0000000..6e2a393
--- /dev/null
+++ b/lib/cli/pkirequestcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKIREQUESTCOMMAND_H
+#define PKIREQUESTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki request" command.
+ *
+ * @ingroup cli
+ */
+class PKIRequestCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKIRequestCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKIREQUESTCOMMAND_H */
diff --git a/lib/cli/pkisavecertcommand.cpp b/lib/cli/pkisavecertcommand.cpp
new file mode 100644
index 0000000..befd0ee
--- /dev/null
+++ b/lib/cli/pkisavecertcommand.cpp
@@ -0,0 +1,89 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkisavecertcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+#include "base/tlsutility.hpp"
+#include "base/console.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/save-cert", PKISaveCertCommand);
+
+String PKISaveCertCommand::GetDescription() const
+{
+ return "Saves another Icinga 2 instance's certificate.";
+}
+
+String PKISaveCertCommand::GetShortDescription() const
+{
+ return "saves another Icinga 2 instance's certificate";
+}
+
+void PKISaveCertCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("trustedcert", po::value<std::string>(), "Trusted certificate file path (output)")
+ ("host", po::value<std::string>(), "Parent Icinga instance to fetch the public TLS certificate from")
+ ("port", po::value<std::string>()->default_value("5665"), "Icinga 2 port");
+
+ hiddenDesc.add_options()
+ ("key", po::value<std::string>())
+ ("cert", po::value<std::string>());
+}
+
+std::vector<String> PKISaveCertCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "trustedcert")
+ return GetBashCompletionSuggestions("file", word);
+ else if (argument == "host")
+ return GetBashCompletionSuggestions("hostname", word);
+ else if (argument == "port")
+ return GetBashCompletionSuggestions("service", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki save-cert" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKISaveCertCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("host")) {
+ Log(LogCritical, "cli", "Icinga 2 host (--host) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("trustedcert")) {
+ Log(LogCritical, "cli", "Trusted certificate output file path (--trustedcert) must be specified.");
+ return 1;
+ }
+
+ String host = vm["host"].as<std::string>();
+ String port = vm["port"].as<std::string>();
+
+ Log(LogInformation, "cli")
+ << "Retrieving TLS certificate for '" << host << ":" << port << "'.";
+
+ std::shared_ptr<X509> cert = PkiUtility::FetchCert(host, port);
+
+ if (!cert) {
+ Log(LogCritical, "cli", "Failed to fetch certificate from host.");
+ return 1;
+ }
+
+ std::cout << PkiUtility::GetCertificateInformation(cert) << "\n";
+ std::cout << ConsoleColorTag(Console_ForegroundRed)
+ << "***\n"
+ << "*** You have to ensure that this certificate actually matches the parent\n"
+ << "*** instance's certificate in order to avoid man-in-the-middle attacks.\n"
+ << "***\n\n"
+ << ConsoleColorTag(Console_Normal);
+
+ return PkiUtility::WriteCert(cert, vm["trustedcert"].as<std::string>());
+}
diff --git a/lib/cli/pkisavecertcommand.hpp b/lib/cli/pkisavecertcommand.hpp
new file mode 100644
index 0000000..c552eef
--- /dev/null
+++ b/lib/cli/pkisavecertcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKISAVECERTCOMMAND_H
+#define PKISAVECERTCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki save-cert" command.
+ *
+ * @ingroup cli
+ */
+class PKISaveCertCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKISaveCertCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKISAVECERTCOMMAND_H */
diff --git a/lib/cli/pkisigncsrcommand.cpp b/lib/cli/pkisigncsrcommand.cpp
new file mode 100644
index 0000000..ce1427b
--- /dev/null
+++ b/lib/cli/pkisigncsrcommand.cpp
@@ -0,0 +1,56 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkisigncsrcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/logger.hpp"
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/sign-csr", PKISignCSRCommand);
+
+String PKISignCSRCommand::GetDescription() const
+{
+ return "Reads a Certificate Signing Request from stdin and prints a signed certificate on stdout.";
+}
+
+String PKISignCSRCommand::GetShortDescription() const
+{
+ return "signs a CSR";
+}
+
+void PKISignCSRCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("csr", po::value<std::string>(), "CSR file path (input)")
+ ("cert", po::value<std::string>(), "Certificate file path (output)");
+}
+
+std::vector<String> PKISignCSRCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "csr" || argument == "cert")
+ return GetBashCompletionSuggestions("file", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki sign-csr" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKISignCSRCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("csr")) {
+ Log(LogCritical, "cli", "Certificate signing request file path (--csr) must be specified.");
+ return 1;
+ }
+
+ if (!vm.count("cert")) {
+ Log(LogCritical, "cli", "Certificate file path (--cert) must be specified.");
+ return 1;
+ }
+
+ return PkiUtility::SignCsr(vm["csr"].as<std::string>(), vm["cert"].as<std::string>());
+}
diff --git a/lib/cli/pkisigncsrcommand.hpp b/lib/cli/pkisigncsrcommand.hpp
new file mode 100644
index 0000000..a66fd39
--- /dev/null
+++ b/lib/cli/pkisigncsrcommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKISIGNCSRCOMMAND_H
+#define PKISIGNCSRCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki sign-csr" command.
+ *
+ * @ingroup cli
+ */
+class PKISignCSRCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKISignCSRCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKISIGNCSRCOMMAND_H */
diff --git a/lib/cli/pkiticketcommand.cpp b/lib/cli/pkiticketcommand.cpp
new file mode 100644
index 0000000..82f3586
--- /dev/null
+++ b/lib/cli/pkiticketcommand.cpp
@@ -0,0 +1,55 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkiticketcommand.hpp"
+#include "remote/pkiutility.hpp"
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/ticket", PKITicketCommand);
+
+String PKITicketCommand::GetDescription() const
+{
+ return "Generates an Icinga 2 ticket";
+}
+
+String PKITicketCommand::GetShortDescription() const
+{
+ return "generates a ticket";
+}
+
+void PKITicketCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("cn", po::value<std::string>(), "Certificate common name")
+ ("salt", po::value<std::string>(), "Ticket salt");
+}
+
+/**
+ * The entry point for the "pki ticket" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKITicketCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (!vm.count("cn")) {
+ Log(LogCritical, "cli", "Common name (--cn) must be specified.");
+ return 1;
+ }
+
+ String salt = VariableUtility::GetVariable("TicketSalt");
+
+ if (vm.count("salt"))
+ salt = vm["salt"].as<std::string>();
+
+ if (salt.IsEmpty()) {
+ Log(LogCritical, "cli", "Ticket salt (--salt) must be specified.");
+ return 1;
+ }
+
+ return PkiUtility::GenTicket(vm["cn"].as<std::string>(), salt, std::cout);
+}
diff --git a/lib/cli/pkiticketcommand.hpp b/lib/cli/pkiticketcommand.hpp
new file mode 100644
index 0000000..500ce86
--- /dev/null
+++ b/lib/cli/pkiticketcommand.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef PKITICKETCOMMAND_H
+#define PKITICKETCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki ticket" command.
+ *
+ * @ingroup cli
+ */
+class PKITicketCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKITicketCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKITICKETCOMMAND_H */
diff --git a/lib/cli/pkiverifycommand.cpp b/lib/cli/pkiverifycommand.cpp
new file mode 100644
index 0000000..963903a
--- /dev/null
+++ b/lib/cli/pkiverifycommand.cpp
@@ -0,0 +1,226 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#include "cli/pkiverifycommand.hpp"
+#include "icinga/service.hpp"
+#include "remote/pkiutility.hpp"
+#include "base/tlsutility.hpp"
+#include "base/logger.hpp"
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("pki/verify", PKIVerifyCommand);
+
+String PKIVerifyCommand::GetDescription() const
+{
+ return "Verify TLS certificates: CN, signed by CA, is CA; Print certificate";
+}
+
+String PKIVerifyCommand::GetShortDescription() const
+{
+ return "verify TLS certificates: CN, signed by CA, is CA; Print certificate";
+}
+
+void PKIVerifyCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("cn", po::value<std::string>(), "Common Name (optional). Use with '--cert' to check the CN in the certificate.")
+ ("cert", po::value<std::string>(), "Certificate file path (optional). Standalone: print certificate. With '--cacert': Verify against CA.")
+ ("cacert", po::value<std::string>(), "CA certificate file path (optional). If passed standalone, verifies whether this is a CA certificate")
+ ("crl", po::value<std::string>(), "CRL file path (optional). Check the certificate against this revocation list when verifying against CA.");
+}
+
+std::vector<String> PKIVerifyCommand::GetArgumentSuggestions(const String& argument, const String& word) const
+{
+ if (argument == "cert" || argument == "cacert" || argument == "crl")
+ return GetBashCompletionSuggestions("file", word);
+ else
+ return CLICommand::GetArgumentSuggestions(argument, word);
+}
+
+/**
+ * The entry point for the "pki verify" CLI command.
+ *
+ * @returns An exit status.
+ */
+int PKIVerifyCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String cn, certFile, caCertFile, crlFile;
+
+ if (vm.count("cn"))
+ cn = vm["cn"].as<std::string>();
+
+ if (vm.count("cert"))
+ certFile = vm["cert"].as<std::string>();
+
+ if (vm.count("cacert"))
+ caCertFile = vm["cacert"].as<std::string>();
+
+ if (vm.count("crl"))
+ crlFile = vm["crl"].as<std::string>();
+
+ /* Verify CN in certificate. */
+ if (!cn.IsEmpty() && !certFile.IsEmpty()) {
+ std::shared_ptr<X509> cert;
+ try {
+ cert = GetX509Certificate(certFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read certificate file '" << certFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ Log(LogInformation, "cli")
+ << "Verifying common name (CN) '" << cn << " in certificate '" << certFile << "'.";
+
+ std::cout << PkiUtility::GetCertificateInformation(cert) << "\n";
+
+ String certCN = GetCertificateCN(cert);
+
+ if (cn == certCN) {
+ Log(LogInformation, "cli")
+ << "OK: CN '" << cn << "' matches certificate CN '" << certCN << "'.";
+
+ return ServiceOK;
+ } else {
+ Log(LogCritical, "cli")
+ << "CRITICAL: CN '" << cn << "' does NOT match certificate CN '" << certCN << "'.";
+
+ return ServiceCritical;
+ }
+ }
+
+ /* Verify certificate. */
+ if (!certFile.IsEmpty() && !caCertFile.IsEmpty()) {
+ std::shared_ptr<X509> cert;
+ try {
+ cert = GetX509Certificate(certFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read certificate file '" << certFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ std::shared_ptr<X509> cacert;
+ try {
+ cacert = GetX509Certificate(caCertFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read CA certificate file '" << caCertFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ Log(LogInformation, "cli")
+ << "Verifying certificate '" << certFile << "'";
+
+ std::cout << PkiUtility::GetCertificateInformation(cert) << "\n";
+
+ Log(LogInformation, "cli")
+ << " with CA certificate '" << caCertFile << "'.";
+
+ std::cout << PkiUtility::GetCertificateInformation(cacert) << "\n";
+
+ String certCN = GetCertificateCN(cert);
+
+ bool signedByCA;
+
+ try {
+ signedByCA = VerifyCertificate(cacert, cert, crlFile);
+ } catch (const std::exception& ex) {
+ Log logmsg (LogCritical, "cli");
+ logmsg << "CRITICAL: Certificate with CN '" << certCN << "' is NOT signed by CA: ";
+ if (const unsigned long *openssl_code = boost::get_error_info<errinfo_openssl_error>(ex)) {
+ logmsg << X509_verify_cert_error_string(*openssl_code) << " (code " << *openssl_code << ")";
+ } else {
+ logmsg << DiagnosticInformation(ex, false);
+ }
+
+ return ServiceCritical;
+ }
+
+ if (signedByCA) {
+ Log(LogInformation, "cli")
+ << "OK: Certificate with CN '" << certCN << "' is signed by CA.";
+
+ return ServiceOK;
+ } else {
+ Log(LogCritical, "cli")
+ << "CRITICAL: Certificate with CN '" << certCN << "' is NOT signed by CA.";
+
+ return ServiceCritical;
+ }
+ }
+
+
+ /* Standalone CA checks. */
+ if (certFile.IsEmpty() && !caCertFile.IsEmpty()) {
+ std::shared_ptr<X509> cacert;
+ try {
+ cacert = GetX509Certificate(caCertFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read CA certificate file '" << caCertFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ Log(LogInformation, "cli")
+ << "Checking whether certificate '" << caCertFile << "' is a valid CA certificate.";
+
+ std::cout << PkiUtility::GetCertificateInformation(cacert) << "\n";
+
+ if (IsCa(cacert)) {
+ Log(LogInformation, "cli")
+ << "OK: CA certificate file '" << caCertFile << "' was verified successfully.\n";
+
+ return ServiceOK;
+ } else {
+ Log(LogCritical, "cli")
+ << "CRITICAL: The file '" << caCertFile << "' does not seem to be a CA certificate file.\n";
+
+ return ServiceCritical;
+ }
+ }
+
+ /* Print certificate */
+ if (!certFile.IsEmpty()) {
+ std::shared_ptr<X509> cert;
+ try {
+ cert = GetX509Certificate(certFile);
+ } catch (const std::exception& ex) {
+ Log(LogCritical, "cli")
+ << "Cannot read certificate file '" << certFile << "'. Please ensure that it exists and is readable.";
+
+ return ServiceCritical;
+ }
+
+ Log(LogInformation, "cli")
+ << "Printing certificate '" << certFile << "'";
+
+ std::cout << PkiUtility::GetCertificateInformation(cert) << "\n";
+
+ return ServiceOK;
+ }
+
+ /* Error handling. */
+ if (!cn.IsEmpty() && certFile.IsEmpty()) {
+ Log(LogCritical, "cli")
+ << "The '--cn' parameter requires the '--cert' parameter.";
+
+ return ServiceCritical;
+ }
+
+ if (cn.IsEmpty() && certFile.IsEmpty() && caCertFile.IsEmpty()) {
+ Log(LogInformation, "cli")
+ << "Please add the '--help' parameter to see all available options.";
+
+ return ServiceOK;
+ }
+
+ return ServiceOK;
+}
diff --git a/lib/cli/pkiverifycommand.hpp b/lib/cli/pkiverifycommand.hpp
new file mode 100644
index 0000000..8e4b9db
--- /dev/null
+++ b/lib/cli/pkiverifycommand.hpp
@@ -0,0 +1,32 @@
+/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+#ifndef PKIVERIFYCOMMAND_H
+#define PKIVERIFYCOMMAND_H
+
+#include "cli/clicommand.hpp"
+
+namespace icinga
+{
+
+/**
+ * The "pki verify" command.
+ *
+ * @ingroup cli
+ */
+class PKIVerifyCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(PKIVerifyCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ std::vector<String> GetArgumentSuggestions(const String& argument, const String& word) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+};
+
+}
+
+#endif /* PKIVERIFYCOMMAND_H */
diff --git a/lib/cli/variablegetcommand.cpp b/lib/cli/variablegetcommand.cpp
new file mode 100644
index 0000000..c05ac96
--- /dev/null
+++ b/lib/cli/variablegetcommand.cpp
@@ -0,0 +1,75 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/variablegetcommand.hpp"
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/convert.hpp"
+#include "base/configobject.hpp"
+#include "base/configtype.hpp"
+#include "base/json.hpp"
+#include "base/netstring.hpp"
+#include "base/stdiostream.hpp"
+#include "base/debug.hpp"
+#include "base/objectlock.hpp"
+#include "base/console.hpp"
+#include "base/scriptglobal.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("variable/get", VariableGetCommand);
+
+String VariableGetCommand::GetDescription() const
+{
+ return "Prints the value of an Icinga 2 variable.";
+}
+
+String VariableGetCommand::GetShortDescription() const
+{
+ return "gets a variable";
+}
+
+void VariableGetCommand::InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const
+{
+ visibleDesc.add_options()
+ ("current", "Uses the current value (i.e. from the running process, rather than from the vars file)");
+}
+
+int VariableGetCommand::GetMinArguments() const
+{
+ return 1;
+}
+
+/**
+ * The entry point for the "variable get" CLI command.
+ *
+ * @returns An exit status.
+ */
+int VariableGetCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ if (vm.count("current")) {
+ std::cout << ScriptGlobal::Get(ap[0], &Empty) << "\n";
+ return 0;
+ }
+
+ String varsfile = Configuration::VarsPath;
+
+ if (!Utility::PathExists(varsfile)) {
+ Log(LogCritical, "cli")
+ << "Cannot open variables file '" << varsfile << "'.";
+ Log(LogCritical, "cli", "Run 'icinga2 daemon -C' to validate config and generate the cache file.");
+ return 1;
+ }
+
+ Value value = VariableUtility::GetVariable(ap[0]);
+
+ std::cout << value << "\n";
+
+ return 0;
+}
diff --git a/lib/cli/variablegetcommand.hpp b/lib/cli/variablegetcommand.hpp
new file mode 100644
index 0000000..9479b3a
--- /dev/null
+++ b/lib/cli/variablegetcommand.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VARIABLEGETCOMMAND_H
+#define VARIABLEGETCOMMAND_H
+
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "cli/clicommand.hpp"
+#include <ostream>
+
+namespace icinga
+{
+
+/**
+ * The "variable get" command.
+ *
+ * @ingroup cli
+ */
+class VariableGetCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(VariableGetCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int GetMinArguments() const override;
+ void InitParameters(boost::program_options::options_description& visibleDesc,
+ boost::program_options::options_description& hiddenDesc) const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+};
+
+}
+
+#endif /* VARIABLEGETCOMMAND_H */
diff --git a/lib/cli/variablelistcommand.cpp b/lib/cli/variablelistcommand.cpp
new file mode 100644
index 0000000..b7ba1be
--- /dev/null
+++ b/lib/cli/variablelistcommand.cpp
@@ -0,0 +1,52 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/variablelistcommand.hpp"
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/convert.hpp"
+#include "base/configobject.hpp"
+#include "base/debug.hpp"
+#include "base/objectlock.hpp"
+#include "base/console.hpp"
+#include <boost/algorithm/string/join.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <fstream>
+#include <iostream>
+
+using namespace icinga;
+namespace po = boost::program_options;
+
+REGISTER_CLICOMMAND("variable/list", VariableListCommand);
+
+String VariableListCommand::GetDescription() const
+{
+ return "Lists all Icinga 2 variables.";
+}
+
+String VariableListCommand::GetShortDescription() const
+{
+ return "lists all variables";
+}
+
+/**
+ * The entry point for the "variable list" CLI command.
+ *
+ * @returns An exit status.
+ */
+int VariableListCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
+{
+ String varsfile = Configuration::VarsPath;
+
+ if (!Utility::PathExists(varsfile)) {
+ Log(LogCritical, "cli")
+ << "Cannot open variables file '" << varsfile << "'.";
+ Log(LogCritical, "cli", "Run 'icinga2 daemon -C' to validate config and generate the cache file.");
+ return 1;
+ }
+
+ VariableUtility::PrintVariables(std::cout);
+
+ return 0;
+}
+
diff --git a/lib/cli/variablelistcommand.hpp b/lib/cli/variablelistcommand.hpp
new file mode 100644
index 0000000..909d9eb
--- /dev/null
+++ b/lib/cli/variablelistcommand.hpp
@@ -0,0 +1,34 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VARIABLELISTCOMMAND_H
+#define VARIABLELISTCOMMAND_H
+
+#include "base/dictionary.hpp"
+#include "base/array.hpp"
+#include "cli/clicommand.hpp"
+#include <ostream>
+
+namespace icinga
+{
+
+/**
+ * The "variable list" command.
+ *
+ * @ingroup cli
+ */
+class VariableListCommand final : public CLICommand
+{
+public:
+ DECLARE_PTR_TYPEDEFS(VariableListCommand);
+
+ String GetDescription() const override;
+ String GetShortDescription() const override;
+ int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
+
+private:
+ static void PrintVariable(std::ostream& fp, const String& message);
+};
+
+}
+
+#endif /* VARIABLELISTCOMMAND_H */
diff --git a/lib/cli/variableutility.cpp b/lib/cli/variableutility.cpp
new file mode 100644
index 0000000..398c9a0
--- /dev/null
+++ b/lib/cli/variableutility.cpp
@@ -0,0 +1,76 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#include "cli/variableutility.hpp"
+#include "base/logger.hpp"
+#include "base/application.hpp"
+#include "base/utility.hpp"
+#include "base/stdiostream.hpp"
+#include "base/netstring.hpp"
+#include "base/json.hpp"
+#include "remote/jsonrpc.hpp"
+#include <fstream>
+
+using namespace icinga;
+
+Value VariableUtility::GetVariable(const String& name)
+{
+ String varsfile = Configuration::VarsPath;
+
+ std::fstream fp;
+ fp.open(varsfile.CStr(), std::ios_base::in);
+
+ StdioStream::Ptr sfp = new StdioStream(&fp, false);
+
+ String message;
+ StreamReadContext src;
+ for (;;) {
+ StreamReadStatus srs = NetString::ReadStringFromStream(sfp, &message, src);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ Dictionary::Ptr variable = JsonDecode(message);
+
+ if (variable->Get("name") == name) {
+ return variable->Get("value");
+ }
+ }
+
+ return Empty;
+}
+
+void VariableUtility::PrintVariables(std::ostream& outfp)
+{
+ String varsfile = Configuration::VarsPath;
+
+ std::fstream fp;
+ fp.open(varsfile.CStr(), std::ios_base::in);
+
+ StdioStream::Ptr sfp = new StdioStream(&fp, false);
+ unsigned long variables_count = 0;
+
+ String message;
+ StreamReadContext src;
+ for (;;) {
+ StreamReadStatus srs = NetString::ReadStringFromStream(sfp, &message, src);
+
+ if (srs == StatusEof)
+ break;
+
+ if (srs != StatusNewItem)
+ continue;
+
+ Dictionary::Ptr variable = JsonDecode(message);
+ outfp << variable->Get("name") << " = " << variable->Get("value") << "\n";
+ variables_count++;
+ }
+
+ sfp->Close();
+ fp.close();
+
+ Log(LogNotice, "cli")
+ << "Parsed " << variables_count << " variables.";
+}
diff --git a/lib/cli/variableutility.hpp b/lib/cli/variableutility.hpp
new file mode 100644
index 0000000..69869b2
--- /dev/null
+++ b/lib/cli/variableutility.hpp
@@ -0,0 +1,31 @@
+/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
+
+#ifndef VARIABLEUTILITY_H
+#define VARIABLEUTILITY_H
+
+#include "base/i2-base.hpp"
+#include "cli/i2-cli.hpp"
+#include "base/dictionary.hpp"
+#include "base/string.hpp"
+#include <ostream>
+
+namespace icinga
+{
+
+/**
+ * @ingroup cli
+ */
+class VariableUtility
+{
+public:
+ static Value GetVariable(const String& name);
+ static void PrintVariables(std::ostream& outfp);
+
+private:
+ VariableUtility();
+
+};
+
+}
+
+#endif /* VARIABLEUTILITY_H */