]> granicus.if.org Git - icinga2/blob - lib/cli/agentwizardcommand.cpp
Cli Agent Setup: Provide hint with actual CN for master ticket cmd
[icinga2] / lib / cli / agentwizardcommand.cpp
1 /******************************************************************************
2  * Icinga 2                                                                   *
3  * Copyright (C) 2012-2014 Icinga Development Team (http://www.icinga.org)    *
4  *                                                                            *
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.                     *
9  *                                                                            *
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.                               *
14  *                                                                            *
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  ******************************************************************************/
19
20 #include "cli/agentwizardcommand.hpp"
21 #include "cli/agentutility.hpp"
22 #include "cli/pkiutility.hpp"
23 #include "cli/featureutility.hpp"
24 #include "base/logger.hpp"
25 #include "base/console.hpp"
26 #include "base/application.hpp"
27 #include "base/tlsutility.hpp"
28 #include <boost/foreach.hpp>
29 #include <boost/algorithm/string/join.hpp>
30 #include <boost/algorithm/string/replace.hpp>
31 #include <boost/algorithm/string/case_conv.hpp>
32 #include <iostream>
33 #include <string>
34 #include <fstream>
35 #include <vector>
36
37 using namespace icinga;
38 namespace po = boost::program_options;
39
40 REGISTER_CLICOMMAND("agent/wizard", AgentWizardCommand);
41
42 String AgentWizardCommand::GetDescription(void) const
43 {
44         return "Wizard for Icinga 2 agent setup.";
45 }
46
47 String AgentWizardCommand::GetShortDescription(void) const
48 {
49         return "wizard for agent setup";
50 }
51
52 /**
53  * The entry point for the "agent wizard" CLI command.
54  *
55  * @returns An exit status.
56  */
57 int AgentWizardCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
58 {
59         /*
60          * The wizard will get all information from the user,
61          * and then call all required functions.
62          */
63
64         std::cout << "Welcome to the Icinga 2 Setup Wizard!\n"
65             << "\n"
66             << "We'll guide you through all required configuration details.\n"
67             << "\n"
68             << "If you have questions, please consult the documentation at http://docs.icinga.org\n"
69             << "or join the community support channels at https://support.icinga.org\n"
70             << "\n\n";
71
72         //TODO: Add sort of bash completion to path input?
73
74         /* 0. master or agent setup?
75          * 1. Ticket
76          * 2. Master information for autosigning
77          * 3. Trusted cert location
78          * 4. CN to use (defaults to FQDN)
79          * 5. Local CA
80          * 6. New self signed certificate
81          * 7. Request signed certificate from master
82          * 8. copy key information to /etc/icinga2/pki
83          * 9. enable ApiListener feature
84          * 10. generate zones.conf with endpoints and zone objects
85          * 11. set NodeName = cn in constants.conf
86          * 12. reload icinga2, or tell the user to
87          */
88
89         std::string answer;
90         bool is_agent_setup = true;
91
92         std::cout << "Please specify if this is an agent setup ('no' installs a master setup) [Y/n]: ";
93         std::getline (std::cin, answer);
94
95         boost::algorithm::to_lower(answer);
96
97         if (Utility::Match("^n*", answer))
98                 is_agent_setup = false;
99
100
101         if (is_agent_setup) {
102                 /* agent setup part */
103                 std::cout << "Starting the Agent setup routine...\n";
104
105                 /* CN */
106                 std::cout << "Please specifiy the common name (CN) [" << Utility::GetFQDN() << "]: ";
107
108                 std::getline(std::cin, answer);
109                 boost::algorithm::to_lower(answer);
110
111                 if (answer.empty())
112                         answer = Utility::GetFQDN();
113
114                 String cn = answer;
115                 cn.Trim();
116
117                 //TODO: Ask for endpoint config instead, and use that for master_host/port
118                 std::vector<std::string> endpoints;
119
120                 String endpoint_buffer;
121
122                 std::cout << "Please specify the master endpoint(s) this agent should connect to:\n";
123                 String master_endpoint_name;
124
125 wizard_endpoint_loop_start:
126
127                 std::cout << "Master CN: ";
128
129                 std::getline(std::cin, answer);
130                 boost::algorithm::to_lower(answer);
131
132                 if(answer.empty()) {
133                         Log(LogWarning, "cli", "Master CN is required! Please retry.");
134                         goto wizard_endpoint_loop_start;
135                 }
136
137                 endpoint_buffer = answer;
138                 endpoint_buffer.Trim();
139
140                 std::cout << "Master endpoint host: ";
141
142                 std::getline(std::cin, answer);
143                 boost::algorithm::to_lower(answer);
144
145                 if (!answer.empty()) {
146                         String tmp = answer;
147                         tmp.Trim();
148                         endpoint_buffer += "," + tmp;
149                         master_endpoint_name = tmp; //store the endpoint name for later
150                 }
151
152                 std::cout << "Master endpoint port: ";
153
154                 std::getline(std::cin, answer);
155                 boost::algorithm::to_lower(answer);
156
157                 if (!answer.empty()) {
158                         String tmp = answer;
159                         tmp.Trim();
160                         endpoint_buffer += "," + answer;
161                 }
162
163
164                 endpoints.push_back(endpoint_buffer);
165
166                 std::cout << "Add more master endpoints? [y/N]";
167                 std::getline (std::cin, answer);
168
169                 boost::algorithm::to_lower(answer);
170
171                 if (Utility::Match("^y*", answer))
172                         goto wizard_endpoint_loop_start;
173
174
175                 std::cout << "Please specify the master connection for auto-signing:\n";
176
177 wizard_master_host:
178                 std::cout << "Host [" << master_endpoint_name << "]: ";
179
180                 std::getline(std::cin, answer);
181                 boost::algorithm::to_lower(answer);
182
183                 if (answer.empty() && !master_endpoint_name.IsEmpty())
184                         answer = master_endpoint_name;
185
186                 if (answer.empty() && master_endpoint_name.IsEmpty())
187                         goto wizard_master_host;
188
189                 String master_host = answer;
190                 master_host.Trim();
191
192                 std::cout << "Port [5665]: ";
193
194                 std::getline(std::cin, answer);
195                 boost::algorithm::to_lower(answer);
196
197                 if (answer.empty())
198                         answer = "5665";
199
200                 String master_port = answer;
201                 master_port.Trim();
202
203                 /* workaround for fetching the master cert - TODO */
204                 String agent_cert = PkiUtility::GetPkiPath() + "/" + cn + ".crt";
205                 String agent_key = PkiUtility::GetPkiPath() + "/" + cn + ".key";
206
207                 //new-ca, new-cert
208                 PkiUtility::NewCa();
209
210                 if (PkiUtility::NewCert(cn, agent_key, Empty, agent_cert) > 0) {
211                         Log(LogCritical, "cli")
212                             << "Failed to create new self-signed certificate for CN '" << cn << "'. Please try again.";
213                         return 1;
214                 }
215
216                 /* store ca in /etc/icinga2/pki */
217                 //TODO FIX chown
218                 String ca = PkiUtility::GetLocalCaPath() + "/ca.crt";
219                 String pki_path = PkiUtility::GetPkiPath();
220
221                 String target_ca = pki_path + "/ca.crt";
222
223                 Utility::CopyFile(ca, target_ca);
224
225                 //save-cert and store the master certificate somewhere
226
227                 std::cout << "Generating self-signed certifiate:\n";
228
229
230                 std::cout << "Fetching public certificate from master ("
231                     << master_host << ", " << master_port << "):\n";
232
233                 String trusted_cert = PkiUtility::GetPkiPath() + "/trusted-master.crt";
234
235                 if (PkiUtility::SaveCert(master_host, master_port, agent_key, agent_cert, trusted_cert) > 0) {
236                         Log(LogCritical, "cli")
237                             << "Failed to fetch trusted master certificate. Please try again.";
238                         return 1;
239                 }
240
241                 std::cout << "Stored trusted master certificate in '" << trusted_cert << "'.\n";
242
243 wizard_ticket:
244                 std::cout << "Please specify the request ticket generated on your Icinga 2 master."
245                     << " (Hint: '# icinga2 pki ticket --cn " << cn << "'):\n";
246
247                 std::getline(std::cin, answer);
248                 boost::algorithm::to_lower(answer);
249
250                 if (answer.empty())
251                         goto wizard_ticket;
252
253                 String ticket = answer;
254                 ticket.Trim();
255
256                 std::cout << "Processing self-signed certificate request. Ticket '" << ticket << "'.\n";
257
258                 if (PkiUtility::RequestCertificate(master_host, master_port, agent_key, agent_cert, ca, trusted_cert, ticket) > 0) {
259                         Log(LogCritical, "cli")
260                             << "Failed to fetch signed certificate from master '" << master_host << ", "
261                             << master_port <<"'. Please try again.";
262                         return 1;
263                 }
264
265                 /* apilistener config */
266                 std::cout << "Please specify the API bind host/port (optional):\n";
267                 std::cout << "Bind Host []: ";
268
269                 std::getline(std::cin, answer);
270                 boost::algorithm::to_lower(answer);
271
272                 String bind_host = answer;
273                 bind_host.Trim();
274
275                 std::cout << "Bind Port []: ";
276
277                 std::getline(std::cin, answer);
278                 boost::algorithm::to_lower(answer);
279
280                 String bind_port = answer;
281                 bind_port.Trim();
282
283                 std::cout << "Enabling the APIlistener feature.\n";
284
285                 std::vector<std::string> enable;
286                 enable.push_back("api");
287                 FeatureUtility::EnableFeatures(enable);
288
289                 String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
290                 AgentUtility::CreateBackupFile(apipath);
291
292                 String apipathtmp = apipath + ".tmp";
293
294                 std::ofstream fp;
295                 fp.open(apipathtmp.CStr(), std::ofstream::out | std::ofstream::trunc);
296
297                 fp << "/**\n"
298                     << " * The API listener is used for distributed monitoring setups.\n"
299                     << " */\n"
300                     << "object ApiListener \"api\" {\n"
301                     << "  cert_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".crt\"\n"
302                     << "  key_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".key\"\n"
303                     << "  ca_path = SysconfDir + \"/icinga2/pki/ca.crt\"\n";
304
305                 if (!bind_host.IsEmpty())
306                         fp << "  bind_host = \"" << bind_host << "\"\n";
307                 if (!bind_port.IsEmpty())
308                         fp << "  bind_port = " << bind_port << "\n";
309
310                 fp << "\n"
311                     << "  ticket_salt = TicketSalt\n"
312                     << "}\n";
313
314                 fp.close();
315
316 #ifdef _WIN32
317                 _unlink(apipath.CStr());
318 #endif /* _WIN32 */
319
320                 if (rename(apipathtmp.CStr(), apipath.CStr()) < 0) {
321                         BOOST_THROW_EXCEPTION(posix_error()
322                             << boost::errinfo_api_function("rename")
323                             << boost::errinfo_errno(errno)
324                             << boost::errinfo_file_name(apipathtmp));
325                 }
326
327                 /* apilistener config */
328                 std::cout << "Generating local zones.conf.\n";
329
330                 AgentUtility::GenerateAgentIcingaConfig(endpoints, cn);
331
332                 if (cn != Utility::GetFQDN()) {
333                         Log(LogWarning, "cli")
334                             << "CN '" << cn << "' does not match the default FQDN '" << Utility::GetFQDN() << "'. Requires update for NodeName constant in constants.conf!";
335                 }
336
337                 std::cout << "Updating constants.conf\n";
338
339                 AgentUtility::CreateBackupFile(Application::GetSysconfDir() + "/icinga2/constants.conf");
340
341                 AgentUtility::UpdateConstant("NodeName", cn);
342
343         } else {
344                 /* master setup */
345                 std::cout << "Starting the Master setup routine...\n";
346
347                 /* CN */
348                 std::cout << "Please specifiy the common name (CN) (leave blank for default FQDN): ";
349
350                 std::getline(std::cin, answer);
351                 boost::algorithm::to_lower(answer);
352
353                 if (answer.empty())
354                         answer = Utility::GetFQDN();
355
356                 String cn = answer;
357                 cn.Trim();
358
359                 if (PkiUtility::NewCa() > 0) {
360                         Log(LogWarning, "cli", "Found CA, skipping and using the existing one.");
361                 }
362
363                 String pki_path = PkiUtility::GetPkiPath();
364
365                 if (!Utility::MkDirP(pki_path, 0700)) {
366                         Log(LogCritical, "cli")
367                             << "Could not create local pki directory '" << pki_path << "'.";
368                         return 1;
369                 }
370
371                 String key = pki_path + "/" + cn + ".key";
372                 String csr = pki_path + "/" + cn + ".csr";
373
374                 if (PkiUtility::NewCert(cn, key, csr, "") > 0) {
375                         Log(LogCritical, "cli", "Failed to create self-signed certificate");
376                         return 1;
377                 }
378
379                 /* Sign the CSR with the CA key */
380
381                 String cert = pki_path + "/" + cn + ".crt";
382
383                 if (PkiUtility::SignCsr(csr, cert) != 0) {
384                         Log(LogCritical, "cli", "Could not sign CSR.");
385                         return 1;
386                 }
387
388                 /* Copy CA certificate to /etc/icinga2/pki */
389
390                 String ca = PkiUtility::GetLocalCaPath() + "/ca.crt";
391                 String target_ca = pki_path + "/ca.crt";
392
393                 Log(LogInformation, "cli")
394                     << "Copying CA certificate to '" << target_ca << "'.";
395
396                 /* does not overwrite existing files! */
397                 Utility::CopyFile(ca, target_ca);
398
399                 //TODO: Fix permissions for CA dir (root -> icinga)
400
401                 AgentUtility::GenerateAgentMasterIcingaConfig(cn);
402
403                 /* apilistener config */
404                 std::cout << "Please specify the API bind host/port (optional):\n";
405                 std::cout << "Bind Host []: ";
406
407                 std::getline(std::cin, answer);
408                 boost::algorithm::to_lower(answer);
409
410                 String bind_host = answer;
411                 bind_host.Trim();
412
413                 std::cout << "Bind Port []: ";
414
415                 std::getline(std::cin, answer);
416                 boost::algorithm::to_lower(answer);
417
418                 String bind_port = answer;
419                 bind_port.Trim();
420
421                 std::cout << "Enabling the APIlistener feature.\n";
422
423                 std::vector<std::string> enable;
424                 enable.push_back("api");
425                 FeatureUtility::EnableFeatures(enable);
426
427                 String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
428                 AgentUtility::CreateBackupFile(apipath);
429
430                 String apipathtmp = apipath + ".tmp";
431
432                 std::ofstream fp;
433                 fp.open(apipathtmp.CStr(), std::ofstream::out | std::ofstream::trunc);
434
435                 fp << "/**\n"
436                     << " * The API listener is used for distributed monitoring setups.\n"
437                     << " */\n"
438                     << "object ApiListener \"api\" {\n"
439                     << "  cert_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".crt\"\n"
440                     << "  key_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".key\"\n"
441                     << "  ca_path = SysconfDir + \"/icinga2/pki/ca.crt\"\n";
442
443                 if (!bind_host.IsEmpty())
444                         fp << "  bind_host = \"" << bind_host << "\"\n";
445                 if (!bind_port.IsEmpty())
446                         fp << "  bind_port = " << bind_port << "\n";
447
448                 fp << "\n"
449                     << "  ticket_salt = TicketSalt\n"
450                     << "}\n";
451
452                 fp.close();
453
454 #ifdef _WIN32
455                 _unlink(apipath.CStr());
456 #endif /* _WIN32 */
457
458                 if (rename(apipathtmp.CStr(), apipath.CStr()) < 0) {
459                         BOOST_THROW_EXCEPTION(posix_error()
460                             << boost::errinfo_api_function("rename")
461                             << boost::errinfo_errno(errno)
462                             << boost::errinfo_file_name(apipathtmp));
463                 }
464
465                 /* update constants.conf with NodeName = CN + TicketSalt = random value */
466                 if (cn != Utility::GetFQDN()) {
467                         Log(LogWarning, "cli")
468                                 << "CN '" << cn << "' does not match the default FQDN '" << Utility::GetFQDN() << "'. Requires update for NodeName constant in constants.conf!";
469                 }
470
471                 Log(LogInformation, "cli", "Updating constants.conf.");
472
473                 AgentUtility::CreateBackupFile(Application::GetSysconfDir() + "/icinga2/constants.conf");
474
475                 AgentUtility::UpdateConstant("NodeName", cn);
476
477                 String salt = RandomString(16);
478
479                 AgentUtility::UpdateConstant("TicketSalt", salt);
480
481                 Log(LogInformation, "cli")
482                     << "Edit the api feature config file '" << apipath << "' and set a secure 'ticket_salt' attribute.";
483         }
484
485         std::cout << "Now restart your Icinga 2 agent to finish the installation!\n";
486
487         std::cout << "If you encounter problems or bugs, please do not hesitate to\n"
488             << "get in touch with the community at https://support.icinga.org" << std::endl;
489
490         return 0;
491 }