diff options
Diffstat (limited to 'lib/cli')
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 */ |