]> granicus.if.org Git - icinga2/blob - lib/cli/repositoryutility.cpp
Cli: Fix formatting of 'repository <type> list' command
[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                 String file = Utility::BaseName(object);
137                 boost::algorithm::replace_all(file, ".conf", "");
138
139                 fp << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << type << ConsoleColorTag(Console_Normal)
140                    << " '" << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << file << ConsoleColorTag(Console_Normal) << "'";
141
142                 String prefix = Utility::DirName(object);
143
144                 if (type == "Service") {
145                         std::vector<String> tokens;
146                         boost::algorithm::split(tokens, prefix, boost::is_any_of("/"));
147
148                         String host_name = tokens[tokens.size()-1];
149                         fp << " (on " << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << "Host" << ConsoleColorTag(Console_Normal)
150                            << " '" << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << host_name << ConsoleColorTag(Console_Normal) << "')";
151
152                 }
153
154                 fp << "\n";
155
156                 /*
157                 Dictionary::Ptr obj = GetObjectFromRepository(object); //TODO: config parser not implemented yet!
158
159                 if (obj)
160                         fp << JsonEncode(obj);
161                 */
162         }
163 }
164
165 void RepositoryUtility::PrintChangeLog(std::ostream& fp)
166 {
167         Array::Ptr changelog = make_shared<Array>();
168
169         GetChangeLog(boost::bind(RepositoryUtility::CollectChange, _1, boost::ref(changelog)));
170
171         ObjectLock olock(changelog);
172
173         std::cout << "Changes to be committed:\n\n";
174
175         BOOST_FOREACH(const Value& entry, changelog) {
176                 FormatChangelogEntry(std::cout, entry);
177         }
178 }
179
180 /* modify objects and write changelog */
181 bool RepositoryUtility::AddObject(const String& name, const String& type, const Dictionary::Ptr& attrs)
182 {
183         /* add a new changelog entry by timestamp */
184         String path = GetRepositoryChangeLogPath() + "/" + Convert::ToString(static_cast<long>(Utility::GetTime())) + "-" + SHA256(name) + ".change";
185
186         Dictionary::Ptr change = make_shared<Dictionary>();
187
188         change->Set("timestamp", Utility::GetTime());
189         change->Set("name", name);
190         change->Set("type", type);
191         change->Set("command", "add");
192         change->Set("attrs", attrs);
193
194         return WriteObjectToRepositoryChangeLog(path, change);
195 }
196
197 bool RepositoryUtility::RemoveObject(const String& name, const String& type, const Dictionary::Ptr& attrs)
198 {
199         /* add a new changelog entry by timestamp */
200         String path = GetRepositoryChangeLogPath() + "/" + Convert::ToString(static_cast<long>(Utility::GetTime())) + "-" + SHA256(name) + ".change";
201
202         Dictionary::Ptr change = make_shared<Dictionary>();
203
204         change->Set("timestamp", Utility::GetTime());
205         change->Set("name", name);
206         change->Set("type", type);
207         change->Set("command", "remove");
208         change->Set("attrs", attrs); //required for service->host_name
209
210         return WriteObjectToRepositoryChangeLog(path, change);
211 }
212
213 bool RepositoryUtility::SetObjectAttribute(const String& name, const String& type, const String& attr, const Value& val)
214 {
215         //TODO: Implement modification commands
216         return true;
217 }
218
219 bool RepositoryUtility::ClearChangeLog(void)
220 {
221         GetChangeLog(boost::bind(RepositoryUtility::ClearChange, _1, _2));
222
223         return true;
224 }
225
226 bool RepositoryUtility::ChangeLogHasPendingChanges(void)
227 {
228         Array::Ptr changelog = make_shared<Array>();
229         GetChangeLog(boost::bind(RepositoryUtility::CollectChange, _1, boost::ref(changelog)));
230
231         return changelog->GetLength() > 0;
232 }
233
234 /* commit changelog */
235 bool RepositoryUtility::CommitChangeLog(void)
236 {
237         GetChangeLog(boost::bind(RepositoryUtility::CommitChange, _1, _2));
238
239         return true;
240 }
241
242 /* write/read from changelog repository */
243 bool RepositoryUtility::WriteObjectToRepositoryChangeLog(const String& path, const Dictionary::Ptr& item)
244 {
245         Log(LogInformation, "cli", "Dumping changelog items to file '" + path + "'");
246
247         Utility::MkDirP(Utility::DirName(path), 0750);
248
249         String tempPath = path + ".tmp";
250
251         std::ofstream fp(tempPath.CStr(), std::ofstream::out | std::ostream::trunc);
252         fp << JsonEncode(item);
253         fp.close();
254
255 #ifdef _WIN32
256         _unlink(path.CStr());
257 #endif /* _WIN32 */
258
259         if (rename(tempPath.CStr(), path.CStr()) < 0) {
260                 BOOST_THROW_EXCEPTION(posix_error()
261                     << boost::errinfo_api_function("rename")
262                     << boost::errinfo_errno(errno)
263                     << boost::errinfo_file_name(tempPath));
264         }
265
266         return true;
267 }
268
269 Dictionary::Ptr RepositoryUtility::GetObjectFromRepositoryChangeLog(const String& filename)
270 {
271         std::fstream fp;
272         fp.open(filename.CStr(), std::ifstream::in);
273
274         if (!fp)
275                 return Dictionary::Ptr();
276
277         String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
278
279         fp.close();
280
281         return JsonDecode(content);
282 }
283
284 /* internal implementation when changes are committed */
285 bool RepositoryUtility::AddObjectInternal(const String& name, const String& type, const Dictionary::Ptr& attrs)
286 {
287         String path = GetRepositoryObjectConfigPath(type, attrs) + "/" + name + ".conf";
288
289         return WriteObjectToRepository(path, name, type, attrs);
290 }
291
292 bool RepositoryUtility::RemoveObjectInternal(const String& name, const String& type, const Dictionary::Ptr& attrs)
293 {
294         String path = GetRepositoryObjectConfigPath(type, attrs) + "/" + name + ".conf";
295         bool success = RemoveObjectFileInternal(path);
296
297         /* special treatment for hosts -> remove the services too */
298         if (type == "Host") {
299                 path = GetRepositoryObjectConfigPath(type, attrs) + "/" + name;
300
301                 std::vector<String> files;
302                 Utility::GlobRecursive(path, "*.conf",
303                     boost::bind(&RepositoryUtility::CollectObjects, _1, boost::ref(files)), GlobFile);
304
305                 BOOST_FOREACH(const String& file, files) {
306                         RemoveObjectFileInternal(file);
307                 }
308 #ifndef _WIN32
309                 rmdir(path.CStr());
310 #else
311                 _rmdir(path.CStr());
312 #endif /* _WIN32 */
313
314         }
315
316         return success;
317 }
318
319 bool RepositoryUtility::RemoveObjectFileInternal(const String& path)
320 {
321         if (!Utility::PathExists(path) ) {
322                 Log(LogCritical, "cli", "Cannot remove '" + path + "'. Does not exist.");
323                 return false;
324         }
325
326         if (unlink(path.CStr()) < 0) {
327                 Log(LogCritical, "cli", "Cannot remove path '" + path +
328                     "'. Failed with error code " + Convert::ToString(errno) + ", \"" + Utility::FormatErrorNumber(errno) + "\".");
329                 return false;
330         }
331
332         return true;
333 }
334
335 bool RepositoryUtility::SetObjectAttributeInternal(const String& name, const String& type, const String& key, const Value& val, const Dictionary::Ptr& attrs)
336 {
337         //Fixme
338         String path = GetRepositoryObjectConfigPath(type, attrs) + "/" + name + ".conf";
339
340         Dictionary::Ptr obj = GetObjectFromRepository(path); //TODO
341
342         if (!obj) {
343                 Log(LogCritical, "cli")
344                     << "Can't get object " << name << " from repository.\n";
345                 return false;
346         }
347
348         obj->Set(key, val);
349
350         std::cout << "Writing object '" << name << "' to path '" << path << "'.\n";
351
352         //TODO: Create a patch file
353         if(!WriteObjectToRepository(path, name, type, obj)) {
354                 Log(LogCritical, "cli")
355                     << "Can't write object " << name << " to repository.\n";
356                 return false;
357         }
358
359         return true;
360 }
361
362 bool RepositoryUtility::WriteObjectToRepository(const String& path, const String& name, const String& type, const Dictionary::Ptr& item)
363 {
364         Log(LogInformation, "cli")
365             << "Dumping config object '" << name << "' to file '" << path << "'";
366
367         Utility::MkDirP(Utility::DirName(path), 0755);
368
369         String tempPath = path + ".tmp";
370
371         std::ofstream fp(tempPath.CStr(), std::ofstream::out | std::ostream::trunc);
372         SerializeObject(fp, name, type, item);
373         fp << std::endl;
374         fp.close();
375
376 #ifdef _WIN32
377         _unlink(path.CStr());
378 #endif /* _WIN32 */
379
380         if (rename(tempPath.CStr(), path.CStr()) < 0) {
381                 BOOST_THROW_EXCEPTION(posix_error()
382                     << boost::errinfo_api_function("rename")
383                     << boost::errinfo_errno(errno)
384                     << boost::errinfo_file_name(tempPath));
385         }
386
387         return true;
388 }
389
390 Dictionary::Ptr RepositoryUtility::GetObjectFromRepository(const String& filename)
391 {
392         //TODO: Parse existing configuration objects
393         return Dictionary::Ptr();
394 }
395
396
397 /*
398  * collect functions
399  */
400 std::vector<String> RepositoryUtility::GetObjects(void)
401 {
402         std::vector<String> objects;
403         String path = GetRepositoryConfigPath();
404
405         Utility::GlobRecursive(path, "*.conf",
406             boost::bind(&RepositoryUtility::CollectObjects, _1, boost::ref(objects)), GlobFile);
407
408         return objects;
409 }
410
411 void RepositoryUtility::CollectObjects(const String& object_file, std::vector<String>& objects)
412 {
413         Log(LogDebug, "cli")
414             << "Adding object: '" << object_file << "'.";
415
416         objects.push_back(object_file);
417 }
418
419
420 bool RepositoryUtility::GetChangeLog(const boost::function<void (const Dictionary::Ptr&, const String&)>& callback)
421 {
422         std::vector<String> changelog;
423         String path = GetRepositoryChangeLogPath() + "/";
424
425         Utility::Glob(path + "/*.change",
426             boost::bind(&RepositoryUtility::CollectChangeLog, _1, boost::ref(changelog)), GlobFile);
427
428         /* sort by timestamp ascending */
429         std::sort(changelog.begin(), changelog.end());
430
431         BOOST_FOREACH(const String& entry, changelog) {
432                 String file = path + entry + ".change";
433                 Dictionary::Ptr change = GetObjectFromRepositoryChangeLog(file);
434
435                 Log(LogDebug, "cli")
436                     << "Collecting entry " << entry << "\n";
437
438                 if (change)
439                         callback(change, file);
440         }
441
442         return true;
443 }
444
445 void RepositoryUtility::CollectChangeLog(const String& change_file, std::vector<String>& changelog)
446 {
447         String file = Utility::BaseName(change_file);
448         boost::algorithm::replace_all(file, ".change", "");
449
450         Log(LogDebug, "cli")
451             << "Adding change file: '" << file << "'.";
452
453         changelog.push_back(file);
454 }
455
456 void RepositoryUtility::CollectChange(const Dictionary::Ptr& change, Array::Ptr& changes)
457 {
458         changes->Add(change);
459 }
460
461 /*
462  * Commit Changelog entry
463  */
464 void RepositoryUtility::CommitChange(const Dictionary::Ptr& change, const String& path)
465 {
466         Log(LogDebug, "cli")
467            << "Got change " << change->Get("name");
468
469         String name = change->Get("name");
470         String type = change->Get("type");
471         String command = change->Get("command");
472         Dictionary::Ptr attrs;
473
474         if (change->Contains("attrs")) {
475                 attrs = change->Get("attrs");
476         }
477
478         bool success = false;
479
480         if (command == "add") {
481                 success = AddObjectInternal(name, type, attrs);
482         }
483         else if (command == "remove") {
484                 success = RemoveObjectInternal(name, type, attrs);
485         }
486
487         if (success) {
488                 Log(LogNotice, "cli")
489                     << "Removing changelog file '" << path << "'.";
490                 RemoveObjectFileInternal(path);
491         }
492 }
493
494 /*
495  * Clear Changelog entry
496  */
497 void RepositoryUtility::ClearChange(const Dictionary::Ptr& change, const String& path)
498 {
499         Log(LogDebug, "cli")
500            << "Clearing change " << change->Get("name");
501
502         Log(LogInformation, "cli")
503            << "Removing changelog file '" << path << "'.";
504
505         RemoveObjectFileInternal(path);
506 }
507
508 /*
509  * Print Changelog helpers
510  */
511 void RepositoryUtility::FormatChangelogEntry(std::ostream& fp, const Dictionary::Ptr& change)
512 {
513         if (!change)
514                 return;
515
516         if (change->Get("command") == "add")
517                 fp << "Adding";
518         if (change->Get("command") == "remove")
519                 fp << "Removing";
520
521         String type = change->Get("type");
522         boost::algorithm::to_lower(type);
523         Dictionary::Ptr attrs = change->Get("attrs");
524
525         fp << " " << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << type << ConsoleColorTag(Console_Normal) << " '";
526         fp << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << change->Get("name") << ConsoleColorTag(Console_Normal) << "'";
527
528         if (!attrs || attrs->GetLength() == 0) {
529                 fp << "\n";
530                 return;
531         }
532
533         fp << " with attributes: \n";
534
535         BOOST_FOREACH(const Dictionary::Pair& kv, attrs) {
536                 /* skip the name */
537                 if (kv.first == "name")
538                         continue;
539
540                 fp << std::setw(4) << " " << ConsoleColorTag(Console_ForegroundGreen) << kv.first << ConsoleColorTag(Console_Normal) << " = ";
541                 FormatValue(fp, kv.second);
542                 fp << "\n";
543         }
544 }
545
546 /*
547  * print helpers for configuration
548  * TODO: Move into a separate class
549  */
550 void RepositoryUtility::SerializeObject(std::ostream& fp, const String& name, const String& type, const Dictionary::Ptr& object)
551 {
552         fp << "object " << type << " \"" << name << "\" {\n";
553
554         if (!object) {
555                 fp << "}\n";
556                 return;
557         }
558
559         if (object->Contains("import")) {
560                 Array::Ptr imports = object->Get("import");
561
562                 ObjectLock olock(imports);
563                 BOOST_FOREACH(const String& import, imports) {
564                         fp << "\t" << "import \"" << import << "\"\n";
565                 }
566         }
567
568         BOOST_FOREACH(const Dictionary::Pair& kv, object) {
569                 if (kv.first == "import" || kv.first == "name") {
570                         continue;
571                 } else {
572                         fp << "\t" << kv.first << " = ";
573                         FormatValue(fp, kv.second);
574                 }
575                 fp << "\n";
576         }
577         fp << "}\n";
578 }
579
580 void RepositoryUtility::FormatValue(std::ostream& fp, const Value& val)
581 {
582         if (val.IsObjectType<Array>()) {
583                 FormatArray(fp, val);
584                 return;
585         }
586
587         if (val.IsString()) {
588                 fp << "\"" << Convert::ToString(val) << "\"";
589                 return;
590         }
591
592         fp << Convert::ToString(val);
593 }
594
595 void RepositoryUtility::FormatArray(std::ostream& fp, const Array::Ptr& arr)
596 {
597         bool first = true;
598
599         fp << "[ ";
600
601         if (arr) {
602                 ObjectLock olock(arr);
603                 BOOST_FOREACH(const Value& value, arr) {
604                         if (first)
605                                 first = false;
606                         else
607                                 fp << ", ";
608
609                         FormatValue(fp, value);
610                 }
611         }
612
613         if (!first)
614                 fp << " ";
615
616         fp << "]";
617 }