diff --git a/lib/cli/agentsetupcommand.cpp b/lib/cli/agentsetupcommand.cpp index 82cf1e6a0..5c7221125 100644 --- a/lib/cli/agentsetupcommand.cpp +++ b/lib/cli/agentsetupcommand.cpp @@ -165,6 +165,8 @@ int AgentSetupCommand::SetupMaster(const boost::program_options::variables_map& /* does not overwrite existing files! */ Utility::CopyFile(ca, target_ca); + //TODO: Fix permissions for CA dir (root -> icinga) + /* read zones.conf and update with zone + endpoint information */ Log(LogInformation, "cli", "Generating zone and object configuration."); diff --git a/lib/cli/agentutility.cpp b/lib/cli/agentutility.cpp index 7a10fc9ec..922a32f13 100644 --- a/lib/cli/agentutility.cpp +++ b/lib/cli/agentutility.cpp @@ -252,17 +252,25 @@ int AgentUtility::GenerateAgentIcingaConfig(const std::vector& endp Dictionary::Ptr my_master_endpoint = make_shared(); - if (tokens.size() > 1) - my_master_endpoint->Set("host", tokens[1]); + if (tokens.size() > 1) { + String host = tokens[1]; + host.Trim(); + my_master_endpoint->Set("host", host); + } - if (tokens.size() > 2) - my_master_endpoint->Set("port", tokens[2]); + if (tokens.size() > 2) { + String port = tokens[2]; + port.Trim(); + my_master_endpoint->Set("port", port); + } - my_master_endpoint->Set("__name", tokens[0]); + String cn = tokens[0]; + cn.Trim(); + my_master_endpoint->Set("__name", cn); my_master_endpoint->Set("__type", "Endpoint"); /* save endpoint in master zone */ - my_master_zone_members->Add(tokens[0]); + my_master_zone_members->Add(cn); my_config->Add(my_master_endpoint); } diff --git a/lib/cli/agentwizardcommand.cpp b/lib/cli/agentwizardcommand.cpp index 222b6bf0b..0ab2b7184 100644 --- a/lib/cli/agentwizardcommand.cpp +++ b/lib/cli/agentwizardcommand.cpp @@ -18,12 +18,19 @@ ******************************************************************************/ #include "cli/agentwizardcommand.hpp" +#include "cli/agentutility.hpp" +#include "cli/pkiutility.hpp" +#include "cli/featureutility.hpp" #include "base/logger.hpp" +#include "base/console.hpp" #include "base/application.hpp" +#include "base/tlsutility.hpp" #include #include #include +#include #include +#include #include #include @@ -49,8 +56,6 @@ String AgentWizardCommand::GetShortDescription(void) const */ int AgentWizardCommand::Run(const boost::program_options::variables_map& vm, const std::vector& ap) const { - Log(LogWarning, "cli", "TODO: Not implemented yet."); - /* * The wizard will get all information from the user, * and then call all required functions. @@ -66,7 +71,6 @@ int AgentWizardCommand::Run(const boost::program_options::variables_map& vm, con //TODO: Add sort of bash completion to path input? - /* 0. master or agent setup? * 1. Ticket * 2. Master information for autosigning @@ -82,5 +86,406 @@ int AgentWizardCommand::Run(const boost::program_options::variables_map& vm, con * 12. reload icinga2, or tell the user to */ + std::string answer; + bool is_agent_setup = true; + + std::cout << "Please specify if this is an agent setup ('no' installs a master setup) [Y/n]: "; + std::getline (std::cin, answer); + + boost::algorithm::to_lower(answer); + + if (Utility::Match("^n*", answer)) + is_agent_setup = false; + + + if (is_agent_setup) { + /* agent setup part */ + std::cout << "Starting the Agent setup routine...\n"; + + /* CN */ + std::cout << "Please specifiy the common name (CN) [" << Utility::GetFQDN() << "]: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + if (answer.empty()) + answer = Utility::GetFQDN(); + + String cn = answer; + cn.Trim(); + + //TODO: Ask for endpoint config instead, and use that for master_host/port + std::vector endpoints; + + String endpoint_buffer; + + std::cout << "Please specify the master endpoint(s) this agent should connect to:\n"; + String master_endpoint_name; + +wizard_endpoint_loop_start: + + std::cout << "Master CN: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + if(answer.empty()) { + Log(LogWarning, "cli", "Master CN is required! Please retry."); + goto wizard_endpoint_loop_start; + } + + endpoint_buffer = answer; + endpoint_buffer.Trim(); + + std::cout << "Master endpoint host: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + if (!answer.empty()) { + String tmp = answer; + tmp.Trim(); + endpoint_buffer += "," + tmp; + master_endpoint_name = tmp; //store the endpoint name for later + } + + std::cout << "Master endpoint port: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + if (!answer.empty()) { + String tmp = answer; + tmp.Trim(); + endpoint_buffer += "," + answer; + } + + + endpoints.push_back(endpoint_buffer); + + std::cout << "Add more master endpoints? [y/N]"; + std::getline (std::cin, answer); + + boost::algorithm::to_lower(answer); + + if (Utility::Match("^y*", answer)) + goto wizard_endpoint_loop_start; + + + std::cout << "Please specify the master connection for auto-signing:\n"; + +wizard_master_host: + std::cout << "Host [" << master_endpoint_name << "]: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + if (answer.empty() && !master_endpoint_name.IsEmpty()) + answer = master_endpoint_name; + + if (answer.empty() && master_endpoint_name.IsEmpty()) + goto wizard_master_host; + + String master_host = answer; + master_host.Trim(); + + std::cout << "Port [5665]: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + if (answer.empty()) + answer = "5665"; + + String master_port = answer; + master_port.Trim(); + + /* workaround for fetching the master cert - TODO */ + String agent_cert = PkiUtility::GetPkiPath() + "/" + cn + ".crt"; + String agent_key = PkiUtility::GetPkiPath() + "/" + cn + ".key"; + + //new-ca, new-cert + PkiUtility::NewCa(); + + if (PkiUtility::NewCert(cn, agent_key, Empty, agent_cert) > 0) { + Log(LogCritical, "cli") + << "Failed to create new self-signed certificate for CN '" << cn << "'. Please try again."; + return 1; + } + + /* store ca in /etc/icinga2/pki */ + //TODO FIX chown + String ca = PkiUtility::GetLocalCaPath() + "/ca.crt"; + String pki_path = PkiUtility::GetPkiPath(); + + String target_ca = pki_path + "/ca.crt"; + + Utility::CopyFile(ca, target_ca); + + //save-cert and store the master certificate somewhere + + std::cout << "Generating self-signed certifiate:\n"; + + + std::cout << "Fetching public certificate from master (" + << master_host << ", " << master_port << "):\n"; + + String trusted_cert = PkiUtility::GetPkiPath() + "/trusted-master.crt"; + + if (PkiUtility::SaveCert(master_host, master_port, agent_key, agent_cert, trusted_cert) > 0) { + Log(LogCritical, "cli") + << "Failed to fetch trusted master certificate. Please try again."; + return 1; + } + + std::cout << "Stored trusted master certificate in '" << trusted_cert << "'.\n"; + +wizard_ticket: + std::cout << "Please specify the request ticket generated on your Icinga 2 master.\n" + << "(Hint: '# icinga2 pki ticket --cn '): "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + if (answer.empty()) + goto wizard_ticket; + + String ticket = answer; + ticket.Trim(); + + std::cout << "Processing self-signed certificate request. Ticket '" << ticket << "'.\n"; + + if (PkiUtility::RequestCertificate(master_host, master_port, agent_key, agent_cert, ca, trusted_cert, ticket) > 0) { + Log(LogCritical, "cli") + << "Failed to fetch signed certificate from master '" << master_host << ", " + << master_port <<"'. Please try again."; + return 1; + } + + /* apilistener config */ + std::cout << "Please specify the API bind host/port (optional):\n"; + std::cout << "Bind Host []: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + String bind_host = answer; + bind_host.Trim(); + + std::cout << "Bind Port []: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + String bind_port = answer; + bind_port.Trim(); + + std::cout << "Enabling the APIlistener feature.\n"; + + std::vector enable; + enable.push_back("api"); + FeatureUtility::EnableFeatures(enable); + + String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf"; + AgentUtility::CreateBackupFile(apipath); + + String apipathtmp = apipath + ".tmp"; + + std::ofstream fp; + fp.open(apipathtmp.CStr(), std::ofstream::out | std::ofstream::trunc); + + fp << "/**\n" + << " * The API listener is used for distributed monitoring setups.\n" + << " */\n" + << "object ApiListener \"api\" {\n" + << " cert_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".crt\"\n" + << " key_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".key\"\n" + << " ca_path = SysconfDir + \"/icinga2/pki/ca.crt\"\n"; + + if (!bind_host.IsEmpty()) + fp << " bind_host = \"" << bind_host << "\"\n"; + if (!bind_port.IsEmpty()) + fp << " bind_port = " << bind_port << "\n"; + + fp << "\n" + << " ticket_salt = TicketSalt\n" + << "}\n"; + + fp.close(); + +#ifdef _WIN32 + _unlink(apipath.CStr()); +#endif /* _WIN32 */ + + if (rename(apipathtmp.CStr(), apipath.CStr()) < 0) { + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("rename") + << boost::errinfo_errno(errno) + << boost::errinfo_file_name(apipathtmp)); + } + + /* apilistener config */ + std::cout << "Generating local zones.conf.\n"; + + AgentUtility::GenerateAgentIcingaConfig(endpoints, cn); + + if (cn != Utility::GetFQDN()) { + Log(LogWarning, "cli") + << "CN '" << cn << "' does not match the default FQDN '" << Utility::GetFQDN() << "'. Requires update for NodeName constant in constants.conf!"; + } + + std::cout << "Updating constants.conf\n"; + + AgentUtility::CreateBackupFile(Application::GetSysconfDir() + "/icinga2/constants.conf"); + + AgentUtility::UpdateConstant("NodeName", cn); + + } else { + /* master setup */ + std::cout << "Starting the Master setup routine...\n"; + + /* CN */ + std::cout << "Please specifiy the common name (CN) (leave blank for default FQDN): "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + if (answer.empty()) + answer = Utility::GetFQDN(); + + String cn = answer; + cn.Trim(); + + if (PkiUtility::NewCa() > 0) { + Log(LogWarning, "cli", "Found CA, skipping and using the existing one."); + } + + String pki_path = PkiUtility::GetPkiPath(); + + if (!Utility::MkDirP(pki_path, 0700)) { + Log(LogCritical, "cli") + << "Could not create local pki directory '" << pki_path << "'."; + return 1; + } + + String key = pki_path + "/" + cn + ".key"; + String csr = pki_path + "/" + cn + ".csr"; + + if (PkiUtility::NewCert(cn, key, csr, "") > 0) { + Log(LogCritical, "cli", "Failed to create self-signed certificate"); + return 1; + } + + /* Sign the CSR with the CA key */ + + String cert = pki_path + "/" + cn + ".crt"; + + if (PkiUtility::SignCsr(csr, cert) != 0) { + Log(LogCritical, "cli", "Could not sign CSR."); + return 1; + } + + /* Copy CA certificate to /etc/icinga2/pki */ + + String ca = PkiUtility::GetLocalCaPath() + "/ca.crt"; + String target_ca = pki_path + "/ca.crt"; + + Log(LogInformation, "cli") + << "Copying CA certificate to '" << target_ca << "'."; + + /* does not overwrite existing files! */ + Utility::CopyFile(ca, target_ca); + + //TODO: Fix permissions for CA dir (root -> icinga) + + AgentUtility::GenerateAgentMasterIcingaConfig(cn); + + /* apilistener config */ + std::cout << "Please specify the API bind host/port (optional):\n"; + std::cout << "Bind Host []: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + String bind_host = answer; + bind_host.Trim(); + + std::cout << "Bind Port []: "; + + std::getline(std::cin, answer); + boost::algorithm::to_lower(answer); + + String bind_port = answer; + bind_port.Trim(); + + std::cout << "Enabling the APIlistener feature.\n"; + + std::vector enable; + enable.push_back("api"); + FeatureUtility::EnableFeatures(enable); + + String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf"; + AgentUtility::CreateBackupFile(apipath); + + String apipathtmp = apipath + ".tmp"; + + std::ofstream fp; + fp.open(apipathtmp.CStr(), std::ofstream::out | std::ofstream::trunc); + + fp << "/**\n" + << " * The API listener is used for distributed monitoring setups.\n" + << " */\n" + << "object ApiListener \"api\" {\n" + << " cert_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".crt\"\n" + << " key_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".key\"\n" + << " ca_path = SysconfDir + \"/icinga2/pki/ca.crt\"\n"; + + if (!bind_host.IsEmpty()) + fp << " bind_host = \"" << bind_host << "\"\n"; + if (!bind_port.IsEmpty()) + fp << " bind_port = " << bind_port << "\n"; + + fp << "\n" + << " ticket_salt = TicketSalt\n" + << "}\n"; + + fp.close(); + +#ifdef _WIN32 + _unlink(apipath.CStr()); +#endif /* _WIN32 */ + + if (rename(apipathtmp.CStr(), apipath.CStr()) < 0) { + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("rename") + << boost::errinfo_errno(errno) + << boost::errinfo_file_name(apipathtmp)); + } + + /* 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 update for NodeName constant in constants.conf!"; + } + + Log(LogInformation, "cli", "Updating constants.conf."); + + AgentUtility::CreateBackupFile(Application::GetSysconfDir() + "/icinga2/constants.conf"); + + AgentUtility::UpdateConstant("NodeName", cn); + + String salt = RandomString(16); + + AgentUtility::UpdateConstant("TicketSalt", salt); + + Log(LogInformation, "cli") + << "Edit the api feature config file '" << apipath << "' and set a secure 'ticket_salt' attribute."; + } + + std::cout << "Now restart your Icinga 2 agent to finish the installation!\n"; + + std::cout << "If you encounter problems or bugs, please do not hesitate to\n" + << "get in touch with the community at https://support.icinga.org" << std::endl; + return 0; } diff --git a/lib/cli/pkiutility.cpp b/lib/cli/pkiutility.cpp index 6e07ac579..140a30619 100644 --- a/lib/cli/pkiutility.cpp +++ b/lib/cli/pkiutility.cpp @@ -245,6 +245,11 @@ int PkiUtility::RequestCertificate(const String& host, const String& port, const break; } + if (!response->Contains("result")) { + Log(LogCritical, "cli", "Request certificate did not return a valid result. Check the master log for details!"); + return 1; + } + Dictionary::Ptr result = response->Get("result"); if (result->Contains("error")) {