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