1 /******************************************************************************
3 * Copyright (C) 2012-2018 Icinga Development Team (https://icinga.com/) *
5 * This program is free software; you can redistribute it and/or *
6 * modify it under the terms of the GNU General Public License *
7 * as published by the Free Software Foundation; either version 2 *
8 * of the License, or (at your option) any later version. *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the Free Software Foundation *
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
18 ******************************************************************************/
20 #include "cli/nodesetupcommand.hpp"
21 #include "cli/nodeutility.hpp"
22 #include "cli/featureutility.hpp"
23 #include "cli/apisetuputility.hpp"
24 #include "remote/apilistener.hpp"
25 #include "remote/pkiutility.hpp"
26 #include "base/logger.hpp"
27 #include "base/console.hpp"
28 #include "base/application.hpp"
29 #include "base/tlsutility.hpp"
30 #include "base/scriptglobal.hpp"
31 #include "base/exception.hpp"
32 #include <boost/algorithm/string/join.hpp>
33 #include <boost/algorithm/string/replace.hpp>
39 using namespace icinga;
40 namespace po = boost::program_options;
42 REGISTER_CLICOMMAND("node/setup", NodeSetupCommand);
44 String NodeSetupCommand::GetDescription() const
46 return "Sets up an Icinga 2 node.";
49 String NodeSetupCommand::GetShortDescription() const
54 void NodeSetupCommand::InitParameters(boost::program_options::options_description& visibleDesc,
55 boost::program_options::options_description& hiddenDesc) const
57 visibleDesc.add_options()
58 ("zone", po::value<std::string>(), "The name of the local zone")
59 ("endpoint", po::value<std::vector<std::string> >(), "Connect to remote endpoint; syntax: cn[,host,port]")
60 ("parent_host", po::value<std::string>(), "The name of the parent host for auto-signing the csr; syntax: host[,port]")
61 ("parent_zone", po::value<std::string>(), "The name of the parent zone")
62 ("listen", po::value<std::string>(), "Listen on host,port")
63 ("ticket", po::value<std::string>(), "Generated ticket number for this request (optional)")
64 ("trustedcert", po::value<std::string>(), "Trusted master certificate file")
65 ("cn", po::value<std::string>(), "The certificate's common name")
66 ("accept-config", "Accept config from master")
67 ("accept-commands", "Accept commands from master")
68 ("master", "Use setup for a master instance")
69 ("global_zones", po::value<std::vector<std::string> >(), "The names of the additional global zones to 'global-templates' and 'director-global'.")
70 ("disable-confd", "Disables the conf.d directory during the setup");
72 hiddenDesc.add_options()
73 ("master_zone", po::value<std::string>(), "DEPRECATED: The name of the master zone")
74 ("master_host", po::value<std::string>(), "DEPRECATED: The name of the master host for auto-signing the csr; syntax: host[,port]");
77 std::vector<String> NodeSetupCommand::GetArgumentSuggestions(const String& argument, const String& word) const
79 if (argument == "key" || argument == "cert" || argument == "trustedcert")
80 return GetBashCompletionSuggestions("file", word);
81 else if (argument == "host")
82 return GetBashCompletionSuggestions("hostname", word);
83 else if (argument == "port")
84 return GetBashCompletionSuggestions("service", word);
86 return CLICommand::GetArgumentSuggestions(argument, word);
89 ImpersonationLevel NodeSetupCommand::GetImpersonationLevel() const
91 return ImpersonateRoot;
95 * The entry point for the "node setup" CLI command.
97 * @returns An exit status.
99 int NodeSetupCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
102 Log(LogWarning, "cli")
103 << "Ignoring parameters: " << boost::algorithm::join(ap, " ");
106 if (vm.count("master"))
107 return SetupMaster(vm, ap);
109 return SetupNode(vm, ap);
112 int NodeSetupCommand::SetupMaster(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap)
114 /* Ignore not required parameters */
115 if (vm.count("ticket"))
116 Log(LogWarning, "cli", "Master for Node setup: Ignoring --ticket");
118 if (vm.count("endpoint"))
119 Log(LogWarning, "cli", "Master for Node setup: Ignoring --endpoint");
121 if (vm.count("trustedcert"))
122 Log(LogWarning, "cli", "Master for Node setup: Ignoring --trustedcert");
124 String cn = Utility::GetFQDN();
127 cn = vm["cn"].as<std::string>();
129 /* Setup command hardcodes this as FQDN */
130 String endpointName = cn;
132 /* Allow to specify zone name. */
133 String zoneName = "master";
135 if (vm.count("zone"))
136 zoneName = vm["zone"].as<std::string>();
138 /* check whether the user wants to generate a new certificate or not */
139 String existingPath = ApiListener::GetCertsDir() + "/" + cn + ".crt";
141 Log(LogInformation, "cli")
142 << "Checking in existing certificates for common name '" << cn << "'...";
144 if (Utility::PathExists(existingPath)) {
145 Log(LogWarning, "cli")
146 << "Certificate '" << existingPath << "' for CN '" << cn << "' already exists. Not generating new certificate.";
148 Log(LogInformation, "cli")
149 << "Certificates not yet generated. Running 'api setup' now.";
151 ApiSetupUtility::SetupMasterCertificates(cn);
154 Log(LogInformation, "cli", "Generating master configuration for Icinga 2.");
155 ApiSetupUtility::SetupMasterApiUser();
157 if (!FeatureUtility::CheckFeatureEnabled("api")) {
158 ApiSetupUtility::SetupMasterEnableApi();
160 Log(LogInformation, "cli")
161 << "'api' feature already enabled.\n";
164 /* write zones.conf and update with zone + endpoint information */
165 Log(LogInformation, "cli", "Generating zone and object configuration.");
167 std::vector<String> globalZones { "global-templates", "director-global" };
168 std::vector<std::string> setupGlobalZones;
170 if (vm.count("global_zones"))
171 setupGlobalZones = vm["global_zones"].as<std::vector<std::string> >();
173 for (int i = 0; i < setupGlobalZones.size(); i++) {
174 if (std::find(globalZones.begin(), globalZones.end(), setupGlobalZones[i]) != globalZones.end()) {
175 Log(LogCritical, "cli")
176 << "The global zone '" << setupGlobalZones[i] << "' is already specified.";
181 globalZones.insert(globalZones.end(), setupGlobalZones.begin(), setupGlobalZones.end());
183 /* Generate master configuration. */
184 NodeUtility::GenerateNodeMasterIcingaConfig(endpointName, zoneName, globalZones);
186 /* Update the ApiListener config. */
187 Log(LogInformation, "cli", "Updating the APIListener feature.");
189 String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
190 NodeUtility::CreateBackupFile(apipath);
193 String tempApiPath = Utility::CreateTempFile(apipath + ".XXXXXX", 0644, fp);
196 << " * The API listener is used for distributed monitoring setups.\n"
198 << "object ApiListener \"api\" {\n";
200 if (vm.count("listen")) {
201 std::vector<String> tokens = String(vm["listen"].as<std::string>()).Split(",");
203 if (tokens.size() > 0)
204 fp << " bind_host = \"" << tokens[0] << "\"\n";
205 if (tokens.size() > 1)
206 fp << " bind_port = " << tokens[1] << "\n";
211 if (vm.count("accept-config"))
212 fp << " accept_config = true\n";
214 fp << " accept_config = false\n";
216 if (vm.count("accept-commands"))
217 fp << " accept_commands = true\n";
219 fp << " accept_commands = false\n";
222 << " ticket_salt = TicketSalt\n"
228 _unlink(apipath.CStr());
231 if (rename(tempApiPath.CStr(), apipath.CStr()) < 0) {
232 BOOST_THROW_EXCEPTION(posix_error()
233 << boost::errinfo_api_function("rename")
234 << boost::errinfo_errno(errno)
235 << boost::errinfo_file_name(tempApiPath));
238 /* update constants.conf with NodeName = CN + TicketSalt = random value */
239 if (endpointName != Utility::GetFQDN()) {
240 Log(LogWarning, "cli")
241 << "CN/Endpoint name '" << endpointName << "' does not match the default FQDN '" << Utility::GetFQDN() << "'. Requires update for NodeName constant in constants.conf!";
244 NodeUtility::UpdateConstant("NodeName", endpointName);
245 NodeUtility::UpdateConstant("ZoneName", zoneName);
247 String salt = RandomString(16);
249 NodeUtility::UpdateConstant("TicketSalt", salt);
251 Log(LogInformation, "cli")
252 << "Edit the api feature config file '" << apipath << "' and set a secure 'ticket_salt' attribute.";
254 if (vm.count("disable-confd")) {
255 /* Disable conf.d inclusion */
256 if (NodeUtility::UpdateConfiguration("\"conf.d\"", false, true)) {
257 Log(LogInformation, "cli")
258 << "Disabled conf.d inclusion";
260 Log(LogWarning, "cli")
261 << "Tried to disable conf.d inclusion but failed, possibly it's already disabled.";
264 /* Include api-users.conf */
265 String apiUsersFilePath = ApiSetupUtility::GetApiUsersConfPath();
267 if (Utility::PathExists(apiUsersFilePath)) {
268 NodeUtility::UpdateConfiguration("\"conf.d/api-users.conf\"", true, false);
270 Log(LogWarning, "cli")
271 << "Included file doesn't exist " << apiUsersFilePath;
275 /* tell the user to reload icinga2 */
276 Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
281 int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap)
283 /* require at least one endpoint. Ticket is optional. */
284 if (!vm.count("endpoint")) {
285 Log(LogCritical, "cli", "You need to specify at least one endpoint (--endpoint).");
289 if (!vm.count("zone")) {
290 Log(LogCritical, "cli", "You need to specify the local zone (--zone).");
294 /* Deprecation warnings. TODO: Remove in 2.10.0. */
295 if (vm.count("master_zone"))
296 Log(LogWarning, "cli", "The 'master_zone' parameter has been deprecated. Use 'parent_zone' instead.");
297 if (vm.count("master_host"))
298 Log(LogWarning, "cli", "The 'master_host' parameter has been deprecated. Use 'parent_host' instead.");
302 if (vm.count("ticket"))
303 ticket = vm["ticket"].as<std::string>();
305 if (ticket.IsEmpty()) {
306 Log(LogInformation, "cli")
307 << "Requesting certificate without a ticket.";
309 Log(LogInformation, "cli")
310 << "Requesting certificate with ticket '" << ticket << "'.";
313 /* Decide whether to directly connect to the parent node for CSR signing, or leave it to the user. */
314 bool connectToParent = false;
316 String parentPort = "5665";
317 std::shared_ptr<X509> trustedParentCert;
319 /* TODO: remove master_host in 2.10.0. */
320 if (!vm.count("master_host") && !vm.count("parent_host")) {
321 connectToParent = false;
323 Log(LogWarning, "cli")
324 << "Node to master/satellite connection setup skipped. Please configure your parent node to\n"
325 << "connect to this node by setting the 'host' attribute for the node Endpoint object.\n";
327 connectToParent = true;
329 String parentHostInfo;
331 if (vm.count("parent_host"))
332 parentHostInfo = vm["parent_host"].as<std::string>();
333 else if (vm.count("master_host")) /* TODO: Remove in 2.10.0. */
334 parentHostInfo = vm["master_host"].as<std::string>();
336 std::vector<String> tokens = parentHostInfo.Split(",");
338 if (tokens.size() == 1 || tokens.size() == 2)
339 parentHost = tokens[0];
341 if (tokens.size() == 2)
342 parentPort = tokens[1];
344 Log(LogInformation, "cli")
345 << "Verifying parent host connection information: host '" << parentHost << "', port '" << parentPort << "'.";
349 /* retrieve CN and pass it (defaults to FQDN) */
350 String cn = Utility::GetFQDN();
353 cn = vm["cn"].as<std::string>();
355 Log(LogInformation, "cli")
356 << "Using the following CN (defaults to FQDN): '" << cn << "'.";
358 /* pki request a signed certificate from the master */
359 String certsDir = ApiListener::GetCertsDir();
360 Utility::MkDirP(certsDir, 0700);
362 String user = Configuration::RunAsUser;
363 String group = Configuration::RunAsGroup;
365 if (!Utility::SetFileOwnership(certsDir, user, group)) {
366 Log(LogWarning, "cli")
367 << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << certsDir << "'. Verify it yourself!";
370 String key = certsDir + "/" + cn + ".key";
371 String cert = certsDir + "/" + cn + ".crt";
372 String ca = certsDir + "/ca.crt";
374 if (Utility::PathExists(key))
375 NodeUtility::CreateBackupFile(key, true);
376 if (Utility::PathExists(cert))
377 NodeUtility::CreateBackupFile(cert);
379 if (PkiUtility::NewCert(cn, key, String(), cert) != 0) {
380 Log(LogCritical, "cli", "Failed to generate new self-signed certificate.");
384 /* fix permissions: root -> icinga daemon user */
385 if (!Utility::SetFileOwnership(key, user, group)) {
386 Log(LogWarning, "cli")
387 << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << key << "'. Verify it yourself!";
390 /* Send a signing request to the parent immediately, or leave it to the user. */
391 if (connectToParent) {
392 /* In contrast to `node wizard` the user must manually fetch
393 * the trustedParentCert to prove the trust relationship (fetched with 'pki save-cert').
395 if (!vm.count("trustedcert")) {
396 Log(LogCritical, "cli")
397 << "Please pass the trusted cert retrieved from the parent node (master or satellite)\n"
398 << "(Hint: 'icinga2 pki save-cert --host <masterhost> --port <5665> --key local.key --cert local.crt --trustedcert parent.crt').";
402 trustedParentCert = GetX509Certificate(vm["trustedcert"].as<std::string>());
404 Log(LogInformation, "cli")
405 << "Verifying trusted certificate file '" << vm["trustedcert"].as<std::string>() << "'.";
407 Log(LogInformation, "cli", "Requesting a signed certificate from the parent Icinga node.");
409 if (PkiUtility::RequestCertificate(parentHost, parentPort, key, cert, ca, trustedParentCert, ticket) > 0) {
410 Log(LogCritical, "cli")
411 << "Failed to fetch signed certificate from parent Icinga node '"
412 << parentHost << ", "
413 << parentPort << "'. Please try again.";
417 /* We cannot retrieve the parent certificate.
418 * Tell the user to manually copy the ca.crt file
419 * into DataDir + "/certs"
421 Log(LogWarning, "cli")
422 << "\nNo connection to the parent node was specified.\n\n"
423 << "Please copy the public CA certificate from your master/satellite\n"
424 << "into '" << ca << "' before starting Icinga 2.\n";
426 if (Utility::PathExists(ca)) {
427 Log(LogInformation, "cli")
428 << "\nFound public CA certificate in '" << ca << "'.\n"
429 << "Please verify that it is the same as on your master/satellite.\n";
433 if (!Utility::SetFileOwnership(ca, user, group)) {
434 Log(LogWarning, "cli")
435 << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << ca << "'. Verify it yourself!";
438 /* fix permissions (again) when updating the signed certificate */
439 if (!Utility::SetFileOwnership(cert, user, group)) {
440 Log(LogWarning, "cli")
441 << "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << cert << "'. Verify it yourself!";
444 /* disable the notifications feature */
445 Log(LogInformation, "cli", "Disabling the Notification feature.");
447 FeatureUtility::DisableFeatures({ "notification" });
449 /* enable the ApiListener config */
451 Log(LogInformation, "cli", "Updating the ApiListener feature.");
453 FeatureUtility::EnableFeatures({ "api" });
455 String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
456 NodeUtility::CreateBackupFile(apipath);
459 String tempApiPath = Utility::CreateTempFile(apipath + ".XXXXXX", 0644, fp);
462 << " * The API listener is used for distributed monitoring setups.\n"
464 << "object ApiListener \"api\" {\n";
466 if (vm.count("listen")) {
467 std::vector<String> tokens = String(vm["listen"].as<std::string>()).Split(",");
469 if (tokens.size() > 0)
470 fp << " bind_host = \"" << tokens[0] << "\"\n";
471 if (tokens.size() > 1)
472 fp << " bind_port = " << tokens[1] << "\n";
477 if (vm.count("accept-config"))
478 fp << " accept_config = true\n";
480 fp << " accept_config = false\n";
482 if (vm.count("accept-commands"))
483 fp << " accept_commands = true\n";
485 fp << " accept_commands = false\n";
493 _unlink(apipath.CStr());
496 if (rename(tempApiPath.CStr(), apipath.CStr()) < 0) {
497 BOOST_THROW_EXCEPTION(posix_error()
498 << boost::errinfo_api_function("rename")
499 << boost::errinfo_errno(errno)
500 << boost::errinfo_file_name(tempApiPath));
504 /* Generate zones configuration. */
505 Log(LogInformation, "cli", "Generating zone and object configuration.");
507 /* Setup command hardcodes this as FQDN */
508 String endpointName = cn;
510 /* Allow to specify zone name. */
511 String zoneName = vm["zone"].as<std::string>();
513 /* Allow to specify the parent zone name. */
514 String parentZoneName = "master";
516 if (vm.count("parent_zone"))
517 parentZoneName = vm["parent_zone"].as<std::string>();
519 std::vector<String> globalZones { "global-templates", "director-global" };
520 std::vector<std::string> setupGlobalZones;
522 if (vm.count("global_zones"))
523 setupGlobalZones = vm["global_zones"].as<std::vector<std::string> >();
525 for (int i = 0; i < setupGlobalZones.size(); i++) {
526 if (std::find(globalZones.begin(), globalZones.end(), setupGlobalZones[i]) != globalZones.end()) {
527 Log(LogCritical, "cli")
528 << "The global zone '" << setupGlobalZones[i] << "' is already specified.";
533 globalZones.insert(globalZones.end(), setupGlobalZones.begin(), setupGlobalZones.end());
535 /* Generate node configuration. */
536 NodeUtility::GenerateNodeIcingaConfig(endpointName, zoneName, parentZoneName, vm["endpoint"].as<std::vector<std::string> >(), globalZones);
538 /* update constants.conf with NodeName = CN */
539 if (endpointName != Utility::GetFQDN()) {
540 Log(LogWarning, "cli")
541 << "CN/Endpoint name '" << endpointName << "' does not match the default FQDN '"
542 << Utility::GetFQDN() << "'. Requires an update for the NodeName constant in constants.conf!";
545 NodeUtility::UpdateConstant("NodeName", endpointName);
546 NodeUtility::UpdateConstant("ZoneName", zoneName);
548 if (!ticket.IsEmpty()) {
549 String ticketPath = ApiListener::GetCertsDir() + "/ticket";
551 String tempTicketPath = Utility::CreateTempFile(ticketPath + ".XXXXXX", 0600, fp);
553 if (!Utility::SetFileOwnership(tempTicketPath, user, group)) {
554 Log(LogWarning, "cli")
555 << "Cannot set ownership for user '" << user
556 << "' group '" << group
557 << "' on file '" << tempTicketPath << "'. Verify it yourself!";
565 _unlink(ticketPath.CStr());
568 if (rename(tempTicketPath.CStr(), ticketPath.CStr()) < 0) {
569 BOOST_THROW_EXCEPTION(posix_error()
570 << boost::errinfo_api_function("rename")
571 << boost::errinfo_errno(errno)
572 << boost::errinfo_file_name(tempTicketPath));
576 /* If no parent connection was made, the user must supply the ca.crt before restarting Icinga 2.*/
577 if (!connectToParent) {
578 Log(LogWarning, "cli")
579 << "No connection to the parent node was specified.\n\n"
580 << "Please copy the public CA certificate from your master/satellite\n"
581 << "into '" << ca << "' before starting Icinga 2.\n";
583 Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
586 if (vm.count("disable-confd")) {
587 /* Disable conf.d inclusion */
588 NodeUtility::UpdateConfiguration("\"conf.d\"", false, true);
591 /* tell the user to reload icinga2 */
592 Log(LogInformation, "cli", "Make sure to restart Icinga 2.");