]> granicus.if.org Git - icinga2/blob - lib/cli/repositoryutility.cpp
Cli: 'repository host remove' cleans service directory, add pending changes helper
[icinga2] / lib / cli / repositoryutility.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/repositoryutility.hpp"
21 #include "cli/clicommand.hpp"
22 #include "base/logger.hpp"
23 #include "base/application.hpp"
24 #include "base/convert.hpp"
25 #include "base/json.hpp"
26 #include "base/netstring.hpp"
27 #include "base/tlsutility.hpp"
28 #include "base/stdiostream.hpp"
29 #include "base/debug.hpp"
30 #include "base/objectlock.hpp"
31 #include "base/console.hpp"
32 #include <boost/foreach.hpp>
33 #include <boost/algorithm/string/join.hpp>
34 #include <boost/algorithm/string/replace.hpp>
35 #include <boost/algorithm/string/split.hpp>
36 #include <boost/algorithm/string/classification.hpp>
37 #include <boost/algorithm/string/case_conv.hpp>
38 #include <boost/regex.hpp>
39 #include <fstream>
40 #include <iostream>
41
42 using namespace icinga;
43
44 Dictionary::Ptr RepositoryUtility::GetArgumentAttributes(const std::vector<std::string>& arguments)
45 {
46         Dictionary::Ptr attrs = make_shared<Dictionary>();
47
48         BOOST_FOREACH(const String& kv, arguments) {
49                 std::vector<String> tokens;
50                 boost::algorithm::split(tokens, kv, boost::is_any_of("="));
51
52                 if (tokens.size() != 2) {
53                         Log(LogWarning, "cli")
54                             << "Cannot parse passed attributes: " << boost::algorithm::join(tokens, "=");
55                         continue;
56                 }
57
58                 Value value;
59
60                 try {
61                         value = Convert::ToDouble(tokens[1]);
62                 } catch (...) {
63                         value = tokens[1];
64                 }
65
66                 attrs->Set(tokens[0], value);
67         }
68
69         return attrs;
70 }
71
72 String RepositoryUtility::GetRepositoryConfigPath(void)
73 {
74         return Application::GetSysconfDir() + "/icinga2/repository.d";
75 }
76
77 String RepositoryUtility::GetRepositoryObjectConfigPath(const String& type, const Dictionary::Ptr& object)
78 {
79         String path = GetRepositoryConfigPath() + "/";
80
81         if (type == "Host")
82                 path += "hosts";
83         else if (type == "Service")
84                 path += "hosts/" + object->Get("host_name");
85         else if (type == "Zone")
86                 path += "zones";
87         else if (type == "Endpoints")
88                 path += "endpoints";
89
90         return path;
91 }
92
93 bool RepositoryUtility::FilterRepositoryObjects(const String& type, const String& path)
94 {
95         if (type == "Host") {
96                 boost::regex expr("hosts/[^/]*.conf", boost::regex::icase);
97                 boost::smatch what;
98                 return boost::regex_search(path.GetData(), what, expr);
99         }
100         else if (type == "Service")
101                 return Utility::Match("*hosts/*/*.conf", path);
102         else if (type == "Zone")
103                 return Utility::Match("*zones/*.conf", path);
104         else if (type == "Endpoints")
105                 return Utility::Match("*endpoints/*.conf", path);
106
107         return false;
108 }
109
110 String RepositoryUtility::GetRepositoryObjectConfigFilePath(const String& type, const Dictionary::Ptr& object)
111 {
112         String path = GetRepositoryObjectConfigPath(type, object);
113
114         path += "/" + object->Get("name") + ".conf";
115
116         return path;
117 }
118
119 String RepositoryUtility::GetRepositoryChangeLogPath(void)
120 {
121         return Application::GetLocalStateDir() + "/lib/icinga2/repository/changes";
122 }
123
124 /* printers */
125 void RepositoryUtility::PrintObjects(std::ostream& fp, const String& type)
126 {
127         std::vector<String> objects = GetObjects(); //full path
128
129         BOOST_FOREACH(const String& object, objects) {
130                 if (!FilterRepositoryObjects(type, object)) {
131                         Log(LogDebug, "cli")
132                             << "Ignoring object '" << object << "'. Type '" << type << "' does not match.";
133                         continue;
134                 }
135
136                 fp << "Object Path: " << object << "\n";
137
138                 Dictionary::Ptr obj = GetObjectFromRepository(object); //TODO: config parser not implemented yet!
139
140                 if (obj)
141                         fp << JsonEncode(obj);
142         }
143 }
144
145 void RepositoryUtility::PrintChangeLog(std::ostream& fp)
146 {
147         Array::Ptr changelog = make_shared<Array>();
148
149         GetChangeLog(boost::bind(RepositoryUtility::CollectChange, _1, boost::ref(changelog)));
150
151         ObjectLock olock(changelog);
152
153         std::cout << "Changes to be committed:\n\n";
154
155         BOOST_FOREACH(const Value& entry, changelog) {
156                 FormatChangelogEntry(std::cout, entry);
157         }
158 }
159
160 /* modify objects and write changelog */
161 bool RepositoryUtility::AddObject(const String& name, const String& type, const Dictionary::Ptr& attrs)
162 {
163         /* add a new changelog entry by timestamp */
164         String path = GetRepositoryChangeLogPath() + "/" + Convert::ToString(static_cast<long>(Utility::GetTime())) + "-" + SHA256(name) + ".change";
165
166         Dictionary::Ptr change = make_shared<Dictionary>();
167
168         change->Set("timestamp", Utility::GetTime());
169         change->Set("name", name);
170         change->Set("type", type);
171         change->Set("command", "add");
172         change->Set("attrs", attrs);
173
174         return WriteObjectToRepositoryChangeLog(path, change);
175 }
176
177 bool RepositoryUtility::RemoveObject(const String& name, const String& type, const Dictionary::Ptr& attrs)
178 {
179         /* add a new changelog entry by timestamp */
180         String path = GetRepositoryChangeLogPath() + "/" + Convert::ToString(static_cast<long>(Utility::GetTime())) + "-" + SHA256(name) + ".change";
181
182         Dictionary::Ptr change = make_shared<Dictionary>();
183
184         change->Set("timestamp", Utility::GetTime());
185         change->Set("name", name);
186         change->Set("type", type);
187         change->Set("command", "remove");
188         change->Set("attrs", attrs); //required for service->host_name
189
190         return WriteObjectToRepositoryChangeLog(path, change);
191 }
192
193 bool RepositoryUtility::SetObjectAttribute(const String& name, const String& type, const String& attr, const Value& val)
194 {
195         //TODO: Implement modification commands
196         return true;
197 }
198
199 bool RepositoryUtility::ClearChangeLog(void)
200 {
201         GetChangeLog(boost::bind(RepositoryUtility::ClearChange, _1, _2));
202
203         return true;
204 }
205
206 bool RepositoryUtility::ChangeLogHasPendingChanges(void)
207 {
208         Array::Ptr changelog = make_shared<Array>();
209         GetChangeLog(boost::bind(RepositoryUtility::CollectChange, _1, boost::ref(changelog)));
210
211         return changelog->GetLength() > 0;
212 }
213
214 /* commit changelog */
215 bool RepositoryUtility::CommitChangeLog(void)
216 {
217         GetChangeLog(boost::bind(RepositoryUtility::CommitChange, _1, _2));
218
219         return true;
220 }
221
222 /* write/read from changelog repository */
223 bool RepositoryUtility::WriteObjectToRepositoryChangeLog(const String& path, const Dictionary::Ptr& item)
224 {
225         Log(LogInformation, "cli", "Dumping changelog items to file '" + path + "'");
226
227         Utility::MkDirP(Utility::DirName(path), 0750);
228
229         String tempPath = path + ".tmp";
230
231         std::ofstream fp(tempPath.CStr(), std::ofstream::out | std::ostream::trunc);
232         fp << JsonEncode(item);
233         fp.close();
234
235 #ifdef _WIN32
236         _unlink(path.CStr());
237 #endif /* _WIN32 */
238
239         if (rename(tempPath.CStr(), path.CStr()) < 0) {
240                 BOOST_THROW_EXCEPTION(posix_error()
241                     << boost::errinfo_api_function("rename")
242                     << boost::errinfo_errno(errno)
243                     << boost::errinfo_file_name(tempPath));
244         }
245
246         return true;
247 }
248
249 Dictionary::Ptr RepositoryUtility::GetObjectFromRepositoryChangeLog(const String& filename)
250 {
251         std::fstream fp;
252         fp.open(filename.CStr(), std::ifstream::in);
253
254         if (!fp)
255                 return Dictionary::Ptr();
256
257         String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
258
259         fp.close();
260
261         return JsonDecode(content);
262 }
263
264 /* internal implementation when changes are committed */
265 bool RepositoryUtility::AddObjectInternal(const String& name, const String& type, const Dictionary::Ptr& attrs)
266 {
267         String path = GetRepositoryObjectConfigPath(type, attrs) + "/" + name + ".conf";
268
269         return WriteObjectToRepository(path, name, type, attrs);
270 }
271
272 bool RepositoryUtility::RemoveObjectInternal(const String& name, const String& type, const Dictionary::Ptr& attrs)
273 {
274         String path = GetRepositoryObjectConfigPath(type, attrs) + "/" + name + ".conf";
275         bool success = RemoveObjectFileInternal(path);
276
277         /* special treatment for hosts -> remove the services too */
278         if (type == "Host") {
279                 path = GetRepositoryObjectConfigPath(type, attrs) + "/" + name;
280
281                 std::vector<String> files;
282                 Utility::GlobRecursive(path, "*.conf",
283                     boost::bind(&RepositoryUtility::CollectObjects, _1, boost::ref(files)), GlobFile);
284
285                 BOOST_FOREACH(const String& file, files) {
286                         RemoveObjectFileInternal(file);
287                 }
288 #ifndef _WIN32
289                 rmdir(path.CStr());
290 #else
291                 _rmdir(path.CStr());
292 #endif /* _WIN32 */
293
294         }
295
296         return success;
297 }
298
299 bool RepositoryUtility::RemoveObjectFileInternal(const String& path)
300 {
301         if (!Utility::PathExists(path) ) {
302                 Log(LogCritical, "cli", "Cannot remove '" + path + "'. Does not exist.");
303                 return false;
304         }
305
306         if (unlink(path.CStr()) < 0) {
307                 Log(LogCritical, "cli", "Cannot remove path '" + path +
308                     "'. Failed with error code " + Convert::ToString(errno) + ", \"" + Utility::FormatErrorNumber(errno) + "\".");
309                 return false;
310         }
311
312         return true;
313 }
314
315 bool RepositoryUtility::SetObjectAttributeInternal(const String& name, const String& type, const String& key, const Value& val, const Dictionary::Ptr& attrs)
316 {
317         //Fixme
318         String path = GetRepositoryObjectConfigPath(type, attrs) + "/" + name + ".conf";
319
320         Dictionary::Ptr obj = GetObjectFromRepository(path); //TODO
321
322         if (!obj) {
323                 Log(LogCritical, "cli")
324                     << "Can't get object " << name << " from repository.\n";
325                 return false;
326         }
327
328         obj->Set(key, val);
329
330         std::cout << "Writing object '" << name << "' to path '" << path << "'.\n";
331
332         //TODO: Create a patch file
333         if(!WriteObjectToRepository(path, name, type, obj)) {
334                 Log(LogCritical, "cli")
335                     << "Can't write object " << name << " to repository.\n";
336                 return false;
337         }
338
339         return true;
340 }
341
342 bool RepositoryUtility::WriteObjectToRepository(const String& path, const String& name, const String& type, const Dictionary::Ptr& item)
343 {
344         Log(LogInformation, "cli")
345             << "Dumping config object '" << name << "' to file '" << path << "'";
346
347         Utility::MkDirP(Utility::DirName(path), 0755);
348
349         String tempPath = path + ".tmp";
350
351         std::ofstream fp(tempPath.CStr(), std::ofstream::out | std::ostream::trunc);
352         SerializeObject(fp, name, type, item);
353         fp << std::endl;
354         fp.close();
355
356 #ifdef _WIN32
357         _unlink(path.CStr());
358 #endif /* _WIN32 */
359
360         if (rename(tempPath.CStr(), path.CStr()) < 0) {
361                 BOOST_THROW_EXCEPTION(posix_error()
362                     << boost::errinfo_api_function("rename")
363                     << boost::errinfo_errno(errno)
364                     << boost::errinfo_file_name(tempPath));
365         }
366
367         return true;
368 }
369
370 Dictionary::Ptr RepositoryUtility::GetObjectFromRepository(const String& filename)
371 {
372         //TODO: Parse existing configuration objects
373         return Dictionary::Ptr();
374 }
375
376
377 /*
378  * collect functions
379  */
380 std::vector<String> RepositoryUtility::GetObjects(void)
381 {
382         std::vector<String> objects;
383         String path = GetRepositoryConfigPath();
384
385         Utility::GlobRecursive(path, "*.conf",
386             boost::bind(&RepositoryUtility::CollectObjects, _1, boost::ref(objects)), GlobFile);
387
388         return objects;
389 }
390
391 void RepositoryUtility::CollectObjects(const String& object_file, std::vector<String>& objects)
392 {
393         Log(LogDebug, "cli")
394             << "Adding object: '" << object_file << "'.";
395
396         objects.push_back(object_file);
397 }
398
399
400 bool RepositoryUtility::GetChangeLog(const boost::function<void (const Dictionary::Ptr&, const String&)>& callback)
401 {
402         std::vector<String> changelog;
403         String path = GetRepositoryChangeLogPath() + "/";
404
405         Utility::Glob(path + "/*.change",
406             boost::bind(&RepositoryUtility::CollectChangeLog, _1, boost::ref(changelog)), GlobFile);
407
408         /* sort by timestamp ascending */
409         std::sort(changelog.begin(), changelog.end());
410
411         BOOST_FOREACH(const String& entry, changelog) {
412                 String file = path + entry + ".change";
413                 Dictionary::Ptr change = GetObjectFromRepositoryChangeLog(file);
414
415                 Log(LogDebug, "cli")
416                     << "Collecting entry " << entry << "\n";
417
418                 if (change)
419                         callback(change, file);
420         }
421
422         return true;
423 }
424
425 void RepositoryUtility::CollectChangeLog(const String& change_file, std::vector<String>& changelog)
426 {
427         String file = Utility::BaseName(change_file);
428         boost::algorithm::replace_all(file, ".change", "");
429
430         Log(LogDebug, "cli")
431             << "Adding change file: '" << file << "'.";
432
433         changelog.push_back(file);
434 }
435
436 void RepositoryUtility::CollectChange(const Dictionary::Ptr& change, Array::Ptr& changes)
437 {
438         changes->Add(change);
439 }
440
441 /*
442  * Commit Changelog entry
443  */
444 void RepositoryUtility::CommitChange(const Dictionary::Ptr& change, const String& path)
445 {
446         Log(LogDebug, "cli")
447            << "Got change " << change->Get("name");
448
449         String name = change->Get("name");
450         String type = change->Get("type");
451         String command = change->Get("command");
452         Dictionary::Ptr attrs;
453
454         if (change->Contains("attrs")) {
455                 attrs = change->Get("attrs");
456         }
457
458         bool success = false;
459
460         if (command == "add") {
461                 success = AddObjectInternal(name, type, attrs);
462         }
463         else if (command == "remove") {
464                 success = RemoveObjectInternal(name, type, attrs);
465         }
466
467         if (success) {
468                 Log(LogNotice, "cli")
469                     << "Removing changelog file '" << path << "'.";
470                 RemoveObjectFileInternal(path);
471         }
472 }
473
474 /*
475  * Clear Changelog entry
476  */
477 void RepositoryUtility::ClearChange(const Dictionary::Ptr& change, const String& path)
478 {
479         Log(LogDebug, "cli")
480            << "Clearing change " << change->Get("name");
481
482         Log(LogInformation, "cli")
483            << "Removing changelog file '" << path << "'.";
484
485         RemoveObjectFileInternal(path);
486 }
487
488 /*
489  * Print Changelog helpers
490  */
491 void RepositoryUtility::FormatChangelogEntry(std::ostream& fp, const Dictionary::Ptr& change)
492 {
493         if (!change)
494                 return;
495
496         if (change->Get("command") == "add")
497                 fp << "Adding";
498         if (change->Get("command") == "remove")
499                 fp << "Removing";
500
501         String type = change->Get("type");
502         boost::algorithm::to_lower(type);
503         Dictionary::Ptr attrs = change->Get("attrs");
504
505         fp << " " << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << type << ConsoleColorTag(Console_Normal) << " '";
506         fp << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << change->Get("name") << ConsoleColorTag(Console_Normal) << "'";
507
508         if (!attrs || attrs->GetLength() == 0) {
509                 fp << "\n";
510                 return;
511         }
512
513         fp << " with attributes: \n";
514
515         BOOST_FOREACH(const Dictionary::Pair& kv, attrs) {
516                 /* skip the name */
517                 if (kv.first == "name")
518                         continue;
519
520                 fp << std::setw(4) << " " << ConsoleColorTag(Console_ForegroundGreen) << kv.first << ConsoleColorTag(Console_Normal) << " = ";
521                 FormatValue(fp, kv.second);
522                 fp << "\n";
523         }
524 }
525
526 /*
527  * print helpers for configuration
528  * TODO: Move into a separate class
529  */
530 void RepositoryUtility::SerializeObject(std::ostream& fp, const String& name, const String& type, const Dictionary::Ptr& object)
531 {
532         fp << "object " << type << " \"" << name << "\" {\n";
533
534         if (!object) {
535                 fp << "}\n";
536                 return;
537         }
538
539         if (object->Contains("import")) {
540                 Array::Ptr imports = object->Get("import");
541
542                 ObjectLock olock(imports);
543                 BOOST_FOREACH(const String& import, imports) {
544                         fp << "\t" << "import \"" << import << "\"\n";
545                 }
546         }
547
548         BOOST_FOREACH(const Dictionary::Pair& kv, object) {
549                 if (kv.first == "import" || kv.first == "name") {
550                         continue;
551                 } else {
552                         fp << "\t" << kv.first << " = ";
553                         FormatValue(fp, kv.second);
554                 }
555                 fp << "\n";
556         }
557         fp << "}\n";
558 }
559
560 void RepositoryUtility::FormatValue(std::ostream& fp, const Value& val)
561 {
562         if (val.IsObjectType<Array>()) {
563                 FormatArray(fp, val);
564                 return;
565         }
566
567         if (val.IsString()) {
568                 fp << "\"" << Convert::ToString(val) << "\"";
569                 return;
570         }
571
572         fp << Convert::ToString(val);
573 }
574
575 void RepositoryUtility::FormatArray(std::ostream& fp, const Array::Ptr& arr)
576 {
577         bool first = true;
578
579         fp << "[ ";
580
581         if (arr) {
582                 ObjectLock olock(arr);
583                 BOOST_FOREACH(const Value& value, arr) {
584                         if (first)
585                                 first = false;
586                         else
587                                 fp << ", ";
588
589                         FormatValue(fp, value);
590                 }
591         }
592
593         if (!first)
594                 fp << " ";
595
596         fp << "]";
597 }