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