2014-10-20 18:37:19 +02:00
|
|
|
/******************************************************************************
|
|
|
|
* Icinga 2 *
|
2016-01-12 08:29:59 +01:00
|
|
|
* Copyright (C) 2012-2016 Icinga Development Team (https://www.icinga.org/) *
|
2014-10-20 18:37:19 +02:00
|
|
|
* *
|
|
|
|
* This program is free software; you can redistribute it and/or *
|
|
|
|
* modify it under the terms of the GNU General Public License *
|
|
|
|
* as published by the Free Software Foundation; either version 2 *
|
|
|
|
* of the License, or (at your option) any later version. *
|
|
|
|
* *
|
|
|
|
* This program is distributed in the hope that it will be useful, *
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
|
|
* GNU General Public License for more details. *
|
|
|
|
* *
|
|
|
|
* You should have received a copy of the GNU General Public License *
|
|
|
|
* along with this program; if not, write to the Free Software Foundation *
|
|
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
|
|
|
|
******************************************************************************/
|
|
|
|
|
|
|
|
#include "cli/repositoryutility.hpp"
|
|
|
|
#include "cli/clicommand.hpp"
|
|
|
|
#include "base/logger.hpp"
|
|
|
|
#include "base/application.hpp"
|
|
|
|
#include "base/convert.hpp"
|
2014-12-14 11:33:45 +01:00
|
|
|
#include "base/scriptglobal.hpp"
|
2014-10-26 19:59:49 +01:00
|
|
|
#include "base/json.hpp"
|
2014-10-20 18:37:19 +02:00
|
|
|
#include "base/netstring.hpp"
|
2014-10-27 17:55:58 +01:00
|
|
|
#include "base/tlsutility.hpp"
|
2014-10-20 18:37:19 +02:00
|
|
|
#include "base/stdiostream.hpp"
|
|
|
|
#include "base/debug.hpp"
|
|
|
|
#include "base/objectlock.hpp"
|
|
|
|
#include "base/console.hpp"
|
2014-12-09 14:55:29 +01:00
|
|
|
#include "base/serializer.hpp"
|
2015-03-28 11:04:42 +01:00
|
|
|
#include "base/exception.hpp"
|
2014-10-20 18:37:19 +02:00
|
|
|
#include <boost/foreach.hpp>
|
|
|
|
#include <boost/algorithm/string/join.hpp>
|
2014-10-23 19:07:14 +02:00
|
|
|
#include <boost/algorithm/string/replace.hpp>
|
2014-10-24 12:42:57 +02:00
|
|
|
#include <boost/algorithm/string/split.hpp>
|
|
|
|
#include <boost/algorithm/string/classification.hpp>
|
2014-10-27 14:53:00 +01:00
|
|
|
#include <boost/algorithm/string/case_conv.hpp>
|
2014-10-27 13:01:21 +01:00
|
|
|
#include <boost/regex.hpp>
|
2014-10-20 18:37:19 +02:00
|
|
|
#include <fstream>
|
|
|
|
#include <iostream>
|
|
|
|
|
|
|
|
using namespace icinga;
|
|
|
|
|
2014-10-24 12:42:57 +02:00
|
|
|
Dictionary::Ptr RepositoryUtility::GetArgumentAttributes(const std::vector<std::string>& arguments)
|
|
|
|
{
|
2014-11-08 21:17:16 +01:00
|
|
|
Dictionary::Ptr attrs = new Dictionary();
|
2014-10-24 12:42:57 +02:00
|
|
|
|
|
|
|
BOOST_FOREACH(const String& kv, arguments) {
|
|
|
|
std::vector<String> tokens;
|
|
|
|
boost::algorithm::split(tokens, kv, boost::is_any_of("="));
|
|
|
|
|
2014-10-27 15:33:36 +01:00
|
|
|
if (tokens.size() != 2) {
|
2014-10-24 12:42:57 +02:00
|
|
|
Log(LogWarning, "cli")
|
|
|
|
<< "Cannot parse passed attributes: " << boost::algorithm::join(tokens, "=");
|
2014-10-27 15:33:36 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
Value value;
|
|
|
|
|
|
|
|
try {
|
|
|
|
value = Convert::ToDouble(tokens[1]);
|
|
|
|
} catch (...) {
|
|
|
|
value = tokens[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
attrs->Set(tokens[0], value);
|
2014-10-24 12:42:57 +02:00
|
|
|
}
|
|
|
|
|
2014-10-27 15:33:36 +01:00
|
|
|
return attrs;
|
2014-10-24 12:42:57 +02:00
|
|
|
}
|
|
|
|
|
2014-10-24 15:27:20 +02:00
|
|
|
String RepositoryUtility::GetRepositoryConfigPath(void)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
|
|
|
return Application::GetSysconfDir() + "/icinga2/repository.d";
|
|
|
|
}
|
|
|
|
|
2014-10-24 15:27:20 +02:00
|
|
|
String RepositoryUtility::GetRepositoryObjectConfigPath(const String& type, const Dictionary::Ptr& object)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-10-24 15:27:20 +02:00
|
|
|
String path = GetRepositoryConfigPath() + "/";
|
|
|
|
|
2014-10-27 13:01:21 +01:00
|
|
|
if (type == "Host")
|
2014-10-24 15:27:20 +02:00
|
|
|
path += "hosts";
|
2014-10-20 18:37:19 +02:00
|
|
|
else if (type == "Service")
|
2014-12-10 13:20:16 +01:00
|
|
|
path += "hosts/" + EscapeName(object->Get("host_name"));
|
2014-10-20 18:37:19 +02:00
|
|
|
else if (type == "Zone")
|
2014-10-24 15:27:20 +02:00
|
|
|
path += "zones";
|
2014-10-28 10:54:29 +01:00
|
|
|
else if (type == "Endpoint")
|
2014-10-24 15:27:20 +02:00
|
|
|
path += "endpoints";
|
|
|
|
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
2014-10-27 13:01:21 +01:00
|
|
|
bool RepositoryUtility::FilterRepositoryObjects(const String& type, const String& path)
|
|
|
|
{
|
|
|
|
if (type == "Host") {
|
|
|
|
boost::regex expr("hosts/[^/]*.conf", boost::regex::icase);
|
|
|
|
boost::smatch what;
|
|
|
|
return boost::regex_search(path.GetData(), what, expr);
|
|
|
|
}
|
|
|
|
else if (type == "Service")
|
|
|
|
return Utility::Match("*hosts/*/*.conf", path);
|
|
|
|
else if (type == "Zone")
|
|
|
|
return Utility::Match("*zones/*.conf", path);
|
|
|
|
else if (type == "Endpoints")
|
|
|
|
return Utility::Match("*endpoints/*.conf", path);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-10-24 15:27:20 +02:00
|
|
|
String RepositoryUtility::GetRepositoryObjectConfigFilePath(const String& type, const Dictionary::Ptr& object)
|
|
|
|
{
|
|
|
|
String path = GetRepositoryObjectConfigPath(type, object);
|
|
|
|
|
2014-12-10 13:20:16 +01:00
|
|
|
path += "/" + EscapeName(object->Get("name")) + ".conf";
|
2014-10-24 15:27:20 +02:00
|
|
|
|
|
|
|
return path;
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
String RepositoryUtility::GetRepositoryChangeLogPath(void)
|
|
|
|
{
|
2014-10-24 15:27:20 +02:00
|
|
|
return Application::GetLocalStateDir() + "/lib/icinga2/repository/changes";
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
2014-12-04 17:22:09 +01:00
|
|
|
void RepositoryUtility::CreateRepositoryPath(const String& path)
|
|
|
|
{
|
|
|
|
if (!Utility::PathExists(path))
|
|
|
|
Utility::MkDirP(path, 0750);
|
|
|
|
|
2014-12-14 11:33:45 +01:00
|
|
|
String user = ScriptGlobal::Get("RunAsUser");
|
2014-12-18 16:55:45 +01:00
|
|
|
String group = ScriptGlobal::Get("RunAsGroup");
|
2014-12-04 17:22:09 +01:00
|
|
|
|
2014-12-18 16:55:45 +01:00
|
|
|
if (!Utility::SetFileOwnership(path, user, group)) {
|
|
|
|
Log(LogWarning, "cli")
|
|
|
|
<< "Cannot set ownership for user '" << user << "' group '" << group << "' on path '" << path << "'. Verify it yourself!";
|
2014-12-04 17:22:09 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
/* printers */
|
2014-10-20 18:37:19 +02:00
|
|
|
void RepositoryUtility::PrintObjects(std::ostream& fp, const String& type)
|
|
|
|
{
|
2014-10-24 15:27:20 +02:00
|
|
|
std::vector<String> objects = GetObjects(); //full path
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
BOOST_FOREACH(const String& object, objects) {
|
2014-10-27 13:01:21 +01:00
|
|
|
if (!FilterRepositoryObjects(type, object)) {
|
|
|
|
Log(LogDebug, "cli")
|
|
|
|
<< "Ignoring object '" << object << "'. Type '" << type << "' does not match.";
|
|
|
|
continue;
|
|
|
|
}
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 20:16:44 +01:00
|
|
|
String file = Utility::BaseName(object);
|
|
|
|
boost::algorithm::replace_all(file, ".conf", "");
|
2014-12-10 13:20:16 +01:00
|
|
|
file = UnescapeName(file);
|
2014-10-27 13:01:21 +01:00
|
|
|
|
2014-10-27 20:16:44 +01:00
|
|
|
fp << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << type << ConsoleColorTag(Console_Normal)
|
|
|
|
<< " '" << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << file << ConsoleColorTag(Console_Normal) << "'";
|
|
|
|
|
|
|
|
String prefix = Utility::DirName(object);
|
|
|
|
|
|
|
|
if (type == "Service") {
|
|
|
|
std::vector<String> tokens;
|
|
|
|
boost::algorithm::split(tokens, prefix, boost::is_any_of("/"));
|
|
|
|
|
2014-12-10 13:20:16 +01:00
|
|
|
String host_name = UnescapeName(tokens[tokens.size()-1]);
|
2014-10-27 20:16:44 +01:00
|
|
|
fp << " (on " << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << "Host" << ConsoleColorTag(Console_Normal)
|
|
|
|
<< " '" << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << host_name << ConsoleColorTag(Console_Normal) << "')";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
fp << "\n";
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
void RepositoryUtility::PrintChangeLog(std::ostream& fp)
|
|
|
|
{
|
2014-11-08 21:17:16 +01:00
|
|
|
Array::Ptr changelog = new Array();
|
2014-10-27 14:53:00 +01:00
|
|
|
|
2014-11-08 14:54:36 +01:00
|
|
|
GetChangeLog(boost::bind(RepositoryUtility::CollectChange, _1, changelog));
|
2014-10-27 14:53:00 +01:00
|
|
|
|
|
|
|
ObjectLock olock(changelog);
|
|
|
|
|
|
|
|
std::cout << "Changes to be committed:\n\n";
|
|
|
|
|
|
|
|
BOOST_FOREACH(const Value& entry, changelog) {
|
|
|
|
FormatChangelogEntry(std::cout, entry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-30 23:32:13 +01:00
|
|
|
class RepositoryValidationUtils : public ValidationUtils
|
2014-10-28 11:54:56 +01:00
|
|
|
{
|
|
|
|
public:
|
2014-11-30 23:32:13 +01:00
|
|
|
virtual bool ValidateName(const String& type, const String& name) const
|
2014-10-28 11:54:56 +01:00
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
/* modify objects and write changelog */
|
2015-02-20 21:01:07 +01:00
|
|
|
bool RepositoryUtility::AddObject(const std::vector<String>& object_paths, const String& name, const String& type,
|
|
|
|
const Dictionary::Ptr& attrs, const Array::Ptr& changes, bool check_config)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-10-28 19:55:46 +01:00
|
|
|
String pattern;
|
|
|
|
|
|
|
|
if (type == "Service")
|
2014-12-10 13:20:16 +01:00
|
|
|
pattern = EscapeName(attrs->Get("host_name")) + "/" + EscapeName(name) + ".conf";
|
2014-10-28 19:55:46 +01:00
|
|
|
else
|
2014-12-10 13:20:16 +01:00
|
|
|
pattern = EscapeName(name) + ".conf";
|
2014-10-28 19:55:46 +01:00
|
|
|
|
|
|
|
BOOST_FOREACH(const String& object_path, object_paths) {
|
|
|
|
if (object_path.Contains(pattern)) {
|
|
|
|
Log(LogWarning, "cli")
|
|
|
|
<< type << " '" << name << "' already exists. Skipping creation.";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-20 18:37:19 +02:00
|
|
|
/* add a new changelog entry by timestamp */
|
2014-10-28 10:54:29 +01:00
|
|
|
String path = GetRepositoryChangeLogPath() + "/" + Convert::ToString(Utility::GetTime()) + "-" + type + "-" + SHA256(name) + ".change";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-11-08 21:17:16 +01:00
|
|
|
Dictionary::Ptr change = new Dictionary();
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
change->Set("timestamp", Utility::GetTime());
|
|
|
|
change->Set("name", name);
|
|
|
|
change->Set("type", type);
|
|
|
|
change->Set("command", "add");
|
2014-10-27 17:55:58 +01:00
|
|
|
change->Set("attrs", attrs);
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-11-30 23:32:13 +01:00
|
|
|
Type::Ptr utype = Type::GetByName(type);
|
|
|
|
ASSERT(utype);
|
|
|
|
|
2015-02-20 21:01:07 +01:00
|
|
|
if (check_config) {
|
2014-11-30 23:32:13 +01:00
|
|
|
try {
|
2015-10-13 09:15:06 +02:00
|
|
|
ConfigObject::Ptr object = static_pointer_cast<ConfigObject>(utype->Instantiate());
|
2015-12-17 10:20:41 +01:00
|
|
|
/* temporarly set the object type for validation */
|
2015-11-20 15:52:11 +01:00
|
|
|
attrs->Set("type", utype->GetName());
|
2014-11-30 23:32:13 +01:00
|
|
|
Deserialize(object, attrs, false, FAConfig);
|
2015-10-13 09:15:06 +02:00
|
|
|
object->SetName(name);
|
2014-11-30 23:32:13 +01:00
|
|
|
|
|
|
|
RepositoryValidationUtils utils;
|
2015-08-15 20:28:05 +02:00
|
|
|
static_pointer_cast<ConfigObject>(object)->Validate(FAConfig, utils);
|
2015-12-17 10:20:41 +01:00
|
|
|
|
|
|
|
attrs->Remove("type");
|
2015-10-13 09:15:06 +02:00
|
|
|
} catch (const ValidationError& ex) {
|
2014-11-30 23:32:13 +01:00
|
|
|
Log(LogCritical, "config", DiagnosticInformation(ex));
|
|
|
|
return false;
|
2014-12-18 15:11:57 +01:00
|
|
|
}
|
2014-10-28 11:54:56 +01:00
|
|
|
}
|
|
|
|
|
2015-02-19 13:15:28 +01:00
|
|
|
if (CheckChangeExists(change, changes)) {
|
2014-10-31 21:08:11 +01:00
|
|
|
Log(LogWarning, "cli")
|
|
|
|
<< "Change '" << change->Get("command") << "' for type '"
|
|
|
|
<< change->Get("type") << "' and name '" << change->Get("name")
|
|
|
|
<< "' already exists.";
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-02-19 13:15:28 +01:00
|
|
|
/* store the cached change */
|
|
|
|
changes->Add(change);
|
|
|
|
|
2014-10-20 18:37:19 +02:00
|
|
|
return WriteObjectToRepositoryChangeLog(path, change);
|
|
|
|
}
|
|
|
|
|
2015-02-19 13:15:28 +01:00
|
|
|
bool RepositoryUtility::RemoveObject(const String& name, const String& type, const Dictionary::Ptr& attrs, const Array::Ptr& changes)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
|
|
|
/* add a new changelog entry by timestamp */
|
2014-10-28 10:54:29 +01:00
|
|
|
String path = GetRepositoryChangeLogPath() + "/" + Convert::ToString(Utility::GetTime()) + "-" + type + "-" + SHA256(name) + ".change";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-11-08 21:17:16 +01:00
|
|
|
Dictionary::Ptr change = new Dictionary();
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
change->Set("timestamp", Utility::GetTime());
|
|
|
|
change->Set("name", name);
|
|
|
|
change->Set("type", type);
|
|
|
|
change->Set("command", "remove");
|
2014-10-27 17:55:58 +01:00
|
|
|
change->Set("attrs", attrs); //required for service->host_name
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2015-02-19 13:15:28 +01:00
|
|
|
if (CheckChangeExists(change, changes)) {
|
2014-10-31 21:08:11 +01:00
|
|
|
Log(LogWarning, "cli")
|
|
|
|
<< "Change '" << change->Get("command") << "' for type '"
|
|
|
|
<< change->Get("type") << "' and name '" << change->Get("name")
|
|
|
|
<< "' already exists.";
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-02-19 13:15:28 +01:00
|
|
|
/* store the cached change */
|
|
|
|
changes->Add(change);
|
|
|
|
|
2014-10-20 18:37:19 +02:00
|
|
|
return WriteObjectToRepositoryChangeLog(path, change);
|
|
|
|
}
|
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
bool RepositoryUtility::SetObjectAttribute(const String& name, const String& type, const String& attr, const Value& val)
|
|
|
|
{
|
|
|
|
//TODO: Implement modification commands
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-02-19 13:15:28 +01:00
|
|
|
bool RepositoryUtility::CheckChangeExists(const Dictionary::Ptr& change, const Array::Ptr& changes)
|
2014-10-31 21:08:11 +01:00
|
|
|
{
|
2014-11-08 14:57:44 +01:00
|
|
|
Dictionary::Ptr attrs = change->Get("attrs");
|
|
|
|
|
2015-02-19 13:15:28 +01:00
|
|
|
ObjectLock olock(changes);
|
|
|
|
BOOST_FOREACH(const Dictionary::Ptr& entry, changes) {
|
2014-10-31 21:08:11 +01:00
|
|
|
if (entry->Get("type") != change->Get("type"))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (entry->Get("name") != change->Get("name"))
|
|
|
|
continue;
|
|
|
|
|
2014-11-08 14:57:44 +01:00
|
|
|
Dictionary::Ptr their_attrs = entry->Get("attrs");
|
|
|
|
|
|
|
|
if (entry->Get("type") == "Service" && attrs->Get("host_name") != their_attrs->Get("host_name"))
|
|
|
|
continue;
|
|
|
|
|
2014-10-31 21:08:11 +01:00
|
|
|
if (entry->Get("command") != change->Get("command"))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
/* only works for add/remove commands (no set) */
|
2014-11-08 14:57:44 +01:00
|
|
|
if (change->Get("command") == "add" || change->Get("command") == "remove")
|
2014-10-31 21:08:11 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-10-27 17:55:58 +01:00
|
|
|
bool RepositoryUtility::ClearChangeLog(void)
|
|
|
|
{
|
|
|
|
GetChangeLog(boost::bind(RepositoryUtility::ClearChange, _1, _2));
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2014-10-27 19:13:33 +01:00
|
|
|
bool RepositoryUtility::ChangeLogHasPendingChanges(void)
|
|
|
|
{
|
2014-11-08 21:17:16 +01:00
|
|
|
Array::Ptr changelog = new Array();
|
2014-11-08 14:54:36 +01:00
|
|
|
GetChangeLog(boost::bind(RepositoryUtility::CollectChange, _1, changelog));
|
2014-10-27 19:13:33 +01:00
|
|
|
|
|
|
|
return changelog->GetLength() > 0;
|
|
|
|
}
|
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
/* commit changelog */
|
2014-10-20 18:37:19 +02:00
|
|
|
bool RepositoryUtility::CommitChangeLog(void)
|
|
|
|
{
|
2014-10-27 11:02:14 +01:00
|
|
|
GetChangeLog(boost::bind(RepositoryUtility::CommitChange, _1, _2));
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
/* write/read from changelog repository */
|
|
|
|
bool RepositoryUtility::WriteObjectToRepositoryChangeLog(const String& path, const Dictionary::Ptr& item)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-10-27 14:53:00 +01:00
|
|
|
Log(LogInformation, "cli", "Dumping changelog items to file '" + path + "'");
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-12-04 17:22:09 +01:00
|
|
|
CreateRepositoryPath(Utility::DirName(path));
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
String tempPath = path + ".tmp";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-12-18 16:55:45 +01:00
|
|
|
std::ofstream fp(tempPath.CStr(), std::ofstream::out | std::ostream::trunc);
|
|
|
|
fp << JsonEncode(item);
|
|
|
|
fp.close();
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
#ifdef _WIN32
|
|
|
|
_unlink(path.CStr());
|
|
|
|
#endif /* _WIN32 */
|
|
|
|
|
|
|
|
if (rename(tempPath.CStr(), path.CStr()) < 0) {
|
|
|
|
BOOST_THROW_EXCEPTION(posix_error()
|
|
|
|
<< boost::errinfo_api_function("rename")
|
|
|
|
<< boost::errinfo_errno(errno)
|
|
|
|
<< boost::errinfo_file_name(tempPath));
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
2014-10-27 14:53:00 +01:00
|
|
|
|
|
|
|
return true;
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
Dictionary::Ptr RepositoryUtility::GetObjectFromRepositoryChangeLog(const String& filename)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-10-27 14:53:00 +01:00
|
|
|
std::fstream fp;
|
|
|
|
fp.open(filename.CStr(), std::ifstream::in);
|
|
|
|
|
|
|
|
if (!fp)
|
|
|
|
return Dictionary::Ptr();
|
|
|
|
|
|
|
|
String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
|
|
|
|
|
|
|
|
fp.close();
|
|
|
|
|
|
|
|
return JsonDecode(content);
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/* internal implementation when changes are committed */
|
2014-10-27 17:55:58 +01:00
|
|
|
bool RepositoryUtility::AddObjectInternal(const String& name, const String& type, const Dictionary::Ptr& attrs)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-12-10 13:20:16 +01:00
|
|
|
String path = GetRepositoryObjectConfigPath(type, attrs) + "/" + EscapeName(name) + ".conf";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 17:55:58 +01:00
|
|
|
return WriteObjectToRepository(path, name, type, attrs);
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
2014-10-27 17:55:58 +01:00
|
|
|
bool RepositoryUtility::RemoveObjectInternal(const String& name, const String& type, const Dictionary::Ptr& attrs)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-12-10 13:20:16 +01:00
|
|
|
String path = GetRepositoryObjectConfigPath(type, attrs) + "/" + EscapeName(name) + ".conf";
|
2014-10-28 15:45:01 +01:00
|
|
|
|
|
|
|
if (!Utility::PathExists(path)) {
|
2014-10-31 20:35:05 +01:00
|
|
|
Log(LogWarning, "cli")
|
2014-10-28 15:45:01 +01:00
|
|
|
<< type << " '" << name << "' does not exist.";
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2014-10-27 19:13:33 +01:00
|
|
|
bool success = RemoveObjectFileInternal(path);
|
|
|
|
|
2014-10-31 21:08:11 +01:00
|
|
|
if (success)
|
|
|
|
Log(LogInformation, "cli")
|
|
|
|
<< "Removing config object '" << name << "' in file '" << path << "'";
|
|
|
|
|
2014-10-27 19:13:33 +01:00
|
|
|
/* special treatment for hosts -> remove the services too */
|
|
|
|
if (type == "Host") {
|
|
|
|
path = GetRepositoryObjectConfigPath(type, attrs) + "/" + name;
|
|
|
|
|
2014-10-28 15:23:51 +01:00
|
|
|
/* if path does not exist, this host does not have any services */
|
|
|
|
if (!Utility::PathExists(path)) {
|
|
|
|
Log(LogNotice, "cli")
|
|
|
|
<< type << " '" << name << "' does not have any services configured.";
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
2014-10-27 19:13:33 +01:00
|
|
|
std::vector<String> files;
|
2014-10-28 15:23:51 +01:00
|
|
|
|
2014-10-27 19:13:33 +01:00
|
|
|
Utility::GlobRecursive(path, "*.conf",
|
|
|
|
boost::bind(&RepositoryUtility::CollectObjects, _1, boost::ref(files)), GlobFile);
|
|
|
|
|
2014-10-28 15:23:51 +01:00
|
|
|
|
2014-10-27 19:13:33 +01:00
|
|
|
BOOST_FOREACH(const String& file, files) {
|
|
|
|
RemoveObjectFileInternal(file);
|
|
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
|
|
rmdir(path.CStr());
|
|
|
|
#else
|
|
|
|
_rmdir(path.CStr());
|
|
|
|
#endif /* _WIN32 */
|
|
|
|
|
|
|
|
}
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 19:13:33 +01:00
|
|
|
return success;
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
bool RepositoryUtility::RemoveObjectFileInternal(const String& path)
|
|
|
|
{
|
|
|
|
if (!Utility::PathExists(path) ) {
|
|
|
|
Log(LogCritical, "cli", "Cannot remove '" + path + "'. Does not exist.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (unlink(path.CStr()) < 0) {
|
2014-10-27 19:13:33 +01:00
|
|
|
Log(LogCritical, "cli", "Cannot remove path '" + path +
|
2014-10-20 18:37:19 +02:00
|
|
|
"'. Failed with error code " + Convert::ToString(errno) + ", \"" + Utility::FormatErrorNumber(errno) + "\".");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2014-10-27 17:55:58 +01:00
|
|
|
bool RepositoryUtility::SetObjectAttributeInternal(const String& name, const String& type, const String& key, const Value& val, const Dictionary::Ptr& attrs)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-10-28 15:45:01 +01:00
|
|
|
//TODO
|
2014-12-10 13:20:16 +01:00
|
|
|
String path = GetRepositoryObjectConfigPath(type, attrs) + "/" + EscapeName(name) + ".conf";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 13:01:21 +01:00
|
|
|
Dictionary::Ptr obj = GetObjectFromRepository(path); //TODO
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
if (!obj) {
|
|
|
|
Log(LogCritical, "cli")
|
|
|
|
<< "Can't get object " << name << " from repository.\n";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-10-24 15:27:20 +02:00
|
|
|
obj->Set(key, val);
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
std::cout << "Writing object '" << name << "' to path '" << path << "'.\n";
|
|
|
|
|
|
|
|
//TODO: Create a patch file
|
2015-02-11 15:47:45 +01:00
|
|
|
if (!WriteObjectToRepository(path, name, type, obj)) {
|
2014-10-20 18:37:19 +02:00
|
|
|
Log(LogCritical, "cli")
|
|
|
|
<< "Can't write object " << name << " to repository.\n";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool RepositoryUtility::WriteObjectToRepository(const String& path, const String& name, const String& type, const Dictionary::Ptr& item)
|
|
|
|
{
|
2014-10-27 17:55:58 +01:00
|
|
|
Log(LogInformation, "cli")
|
2014-10-31 21:08:11 +01:00
|
|
|
<< "Writing config object '" << name << "' to file '" << path << "'";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-12-04 17:22:09 +01:00
|
|
|
CreateRepositoryPath(Utility::DirName(path));
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
String tempPath = path + ".tmp";
|
|
|
|
|
2014-12-18 16:55:45 +01:00
|
|
|
std::ofstream fp(tempPath.CStr(), std::ofstream::out | std::ostream::trunc);
|
|
|
|
SerializeObject(fp, name, type, item);
|
2014-10-20 18:37:19 +02:00
|
|
|
fp << std::endl;
|
2014-12-18 16:55:45 +01:00
|
|
|
fp.close();
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
#ifdef _WIN32
|
2014-10-23 19:07:14 +02:00
|
|
|
_unlink(path.CStr());
|
2014-10-20 18:37:19 +02:00
|
|
|
#endif /* _WIN32 */
|
|
|
|
|
|
|
|
if (rename(tempPath.CStr(), path.CStr()) < 0) {
|
|
|
|
BOOST_THROW_EXCEPTION(posix_error()
|
|
|
|
<< boost::errinfo_api_function("rename")
|
|
|
|
<< boost::errinfo_errno(errno)
|
|
|
|
<< boost::errinfo_file_name(tempPath));
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Dictionary::Ptr RepositoryUtility::GetObjectFromRepository(const String& filename)
|
|
|
|
{
|
|
|
|
//TODO: Parse existing configuration objects
|
|
|
|
return Dictionary::Ptr();
|
|
|
|
}
|
|
|
|
|
2014-12-10 13:20:16 +01:00
|
|
|
String RepositoryUtility::EscapeName(const String& name)
|
|
|
|
{
|
2015-06-26 15:37:47 +02:00
|
|
|
return Utility::EscapeString(name, "<>:\"/\\|?*", true);
|
2014-12-10 13:20:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
String RepositoryUtility::UnescapeName(const String& name)
|
|
|
|
{
|
|
|
|
return Utility::UnescapeString(name);
|
|
|
|
}
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* collect functions
|
|
|
|
*/
|
2014-10-24 15:27:20 +02:00
|
|
|
std::vector<String> RepositoryUtility::GetObjects(void)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-10-24 15:27:20 +02:00
|
|
|
std::vector<String> objects;
|
2014-10-27 13:01:21 +01:00
|
|
|
String path = GetRepositoryConfigPath();
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 13:01:21 +01:00
|
|
|
Utility::GlobRecursive(path, "*.conf",
|
2014-10-24 15:27:20 +02:00
|
|
|
boost::bind(&RepositoryUtility::CollectObjects, _1, boost::ref(objects)), GlobFile);
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-24 15:27:20 +02:00
|
|
|
return objects;
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void RepositoryUtility::CollectObjects(const String& object_file, std::vector<String>& objects)
|
|
|
|
{
|
2014-10-24 15:27:20 +02:00
|
|
|
Log(LogDebug, "cli")
|
|
|
|
<< "Adding object: '" << object_file << "'.";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-24 15:27:20 +02:00
|
|
|
objects.push_back(object_file);
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-10-27 11:02:14 +01:00
|
|
|
bool RepositoryUtility::GetChangeLog(const boost::function<void (const Dictionary::Ptr&, const String&)>& callback)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
|
|
|
std::vector<String> changelog;
|
|
|
|
String path = GetRepositoryChangeLogPath() + "/";
|
|
|
|
|
2015-10-13 09:15:06 +02:00
|
|
|
Utility::MkDirP(path, 0700);
|
|
|
|
|
2014-10-24 15:27:20 +02:00
|
|
|
Utility::Glob(path + "/*.change",
|
|
|
|
boost::bind(&RepositoryUtility::CollectChangeLog, _1, boost::ref(changelog)), GlobFile);
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
/* sort by timestamp ascending */
|
|
|
|
std::sort(changelog.begin(), changelog.end());
|
|
|
|
|
|
|
|
BOOST_FOREACH(const String& entry, changelog) {
|
2014-10-27 11:02:14 +01:00
|
|
|
String file = path + entry + ".change";
|
|
|
|
Dictionary::Ptr change = GetObjectFromRepositoryChangeLog(file);
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
Log(LogDebug, "cli")
|
2014-10-20 18:37:19 +02:00
|
|
|
<< "Collecting entry " << entry << "\n";
|
|
|
|
|
|
|
|
if (change)
|
2014-10-27 11:02:14 +01:00
|
|
|
callback(change, file);
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void RepositoryUtility::CollectChangeLog(const String& change_file, std::vector<String>& changelog)
|
|
|
|
{
|
|
|
|
String file = Utility::BaseName(change_file);
|
|
|
|
boost::algorithm::replace_all(file, ".change", "");
|
|
|
|
|
2014-10-27 17:55:58 +01:00
|
|
|
Log(LogDebug, "cli")
|
|
|
|
<< "Adding change file: '" << file << "'.";
|
|
|
|
|
2014-10-20 18:37:19 +02:00
|
|
|
changelog.push_back(file);
|
|
|
|
}
|
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
void RepositoryUtility::CollectChange(const Dictionary::Ptr& change, Array::Ptr& changes)
|
|
|
|
{
|
|
|
|
changes->Add(change);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2014-10-27 17:55:58 +01:00
|
|
|
* Commit Changelog entry
|
2014-10-27 14:53:00 +01:00
|
|
|
*/
|
2014-10-27 11:02:14 +01:00
|
|
|
void RepositoryUtility::CommitChange(const Dictionary::Ptr& change, const String& path)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-10-27 14:53:00 +01:00
|
|
|
Log(LogDebug, "cli")
|
2014-10-20 18:37:19 +02:00
|
|
|
<< "Got change " << change->Get("name");
|
|
|
|
|
|
|
|
String name = change->Get("name");
|
|
|
|
String type = change->Get("type");
|
|
|
|
String command = change->Get("command");
|
2014-10-27 17:55:58 +01:00
|
|
|
Dictionary::Ptr attrs;
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 17:55:58 +01:00
|
|
|
if (change->Contains("attrs")) {
|
|
|
|
attrs = change->Get("attrs");
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
2014-10-27 11:02:14 +01:00
|
|
|
bool success = false;
|
|
|
|
|
2014-10-20 18:37:19 +02:00
|
|
|
if (command == "add") {
|
2014-10-27 17:55:58 +01:00
|
|
|
success = AddObjectInternal(name, type, attrs);
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
else if (command == "remove") {
|
2014-10-27 17:55:58 +01:00
|
|
|
success = RemoveObjectInternal(name, type, attrs);
|
2014-10-27 11:02:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (success) {
|
2014-10-27 14:53:00 +01:00
|
|
|
Log(LogNotice, "cli")
|
2014-10-27 11:02:14 +01:00
|
|
|
<< "Removing changelog file '" << path << "'.";
|
|
|
|
RemoveObjectFileInternal(path);
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-27 17:55:58 +01:00
|
|
|
/*
|
|
|
|
* Clear Changelog entry
|
|
|
|
*/
|
|
|
|
void RepositoryUtility::ClearChange(const Dictionary::Ptr& change, const String& path)
|
|
|
|
{
|
|
|
|
Log(LogDebug, "cli")
|
|
|
|
<< "Clearing change " << change->Get("name");
|
|
|
|
|
|
|
|
Log(LogInformation, "cli")
|
|
|
|
<< "Removing changelog file '" << path << "'.";
|
|
|
|
|
|
|
|
RemoveObjectFileInternal(path);
|
|
|
|
}
|
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
/*
|
|
|
|
* Print Changelog helpers
|
|
|
|
*/
|
|
|
|
void RepositoryUtility::FormatChangelogEntry(std::ostream& fp, const Dictionary::Ptr& change)
|
2014-10-20 18:37:19 +02:00
|
|
|
{
|
2014-10-27 14:53:00 +01:00
|
|
|
if (!change)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (change->Get("command") == "add")
|
|
|
|
fp << "Adding";
|
|
|
|
if (change->Get("command") == "remove")
|
|
|
|
fp << "Removing";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-10-27 14:53:00 +01:00
|
|
|
String type = change->Get("type");
|
|
|
|
boost::algorithm::to_lower(type);
|
2014-10-27 17:55:58 +01:00
|
|
|
Dictionary::Ptr attrs = change->Get("attrs");
|
2014-10-27 14:53:00 +01:00
|
|
|
|
2014-10-28 19:16:48 +01:00
|
|
|
fp << " " << ConsoleColorTag(Console_ForegroundMagenta | Console_Bold) << type << ConsoleColorTag(Console_Normal) << " '";
|
2014-10-31 21:08:11 +01:00
|
|
|
fp << ConsoleColorTag(Console_ForegroundBlue | Console_Bold) << change->Get("name") << ConsoleColorTag(Console_Normal) << "'\n";
|
2014-10-27 14:53:00 +01:00
|
|
|
|
2014-12-04 17:22:09 +01:00
|
|
|
ObjectLock olock(attrs);
|
2014-10-27 14:53:00 +01:00
|
|
|
BOOST_FOREACH(const Dictionary::Pair& kv, attrs) {
|
|
|
|
/* skip the name */
|
2014-10-28 15:45:01 +01:00
|
|
|
if (kv.first == "name" || kv.first == "__name")
|
2014-10-27 14:53:00 +01:00
|
|
|
continue;
|
|
|
|
|
|
|
|
fp << std::setw(4) << " " << ConsoleColorTag(Console_ForegroundGreen) << kv.first << ConsoleColorTag(Console_Normal) << " = ";
|
|
|
|
FormatValue(fp, kv.second);
|
|
|
|
fp << "\n";
|
|
|
|
}
|
|
|
|
}
|
2014-10-20 18:37:19 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* print helpers for configuration
|
|
|
|
* TODO: Move into a separate class
|
|
|
|
*/
|
|
|
|
void RepositoryUtility::SerializeObject(std::ostream& fp, const String& name, const String& type, const Dictionary::Ptr& object)
|
|
|
|
{
|
2016-01-21 18:14:53 +01:00
|
|
|
fp << "object " << type << " \"" << EscapeIcingaString(name) << "\" {\n";
|
2014-10-23 19:06:02 +02:00
|
|
|
|
2014-10-23 20:42:56 +02:00
|
|
|
if (!object) {
|
|
|
|
fp << "}\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-10-24 12:42:57 +02:00
|
|
|
if (object->Contains("import")) {
|
|
|
|
Array::Ptr imports = object->Get("import");
|
2014-10-23 20:42:56 +02:00
|
|
|
|
2014-10-24 12:42:57 +02:00
|
|
|
ObjectLock olock(imports);
|
|
|
|
BOOST_FOREACH(const String& import, imports) {
|
|
|
|
fp << "\t" << "import \"" << import << "\"\n";
|
2014-10-23 20:42:56 +02:00
|
|
|
}
|
|
|
|
}
|
2014-10-23 19:06:02 +02:00
|
|
|
|
2014-12-04 17:22:09 +01:00
|
|
|
ObjectLock xlock(object);
|
2014-10-20 18:37:19 +02:00
|
|
|
BOOST_FOREACH(const Dictionary::Pair& kv, object) {
|
2014-10-28 19:43:37 +01:00
|
|
|
if (kv.first == "import" || kv.first == "name" || kv.first == "__name") {
|
2014-10-23 19:06:02 +02:00
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
fp << "\t" << kv.first << " = ";
|
|
|
|
FormatValue(fp, kv.second);
|
|
|
|
}
|
2014-10-20 18:37:19 +02:00
|
|
|
fp << "\n";
|
|
|
|
}
|
|
|
|
fp << "}\n";
|
|
|
|
}
|
|
|
|
|
2016-01-21 18:14:53 +01:00
|
|
|
String RepositoryUtility::EscapeIcingaString(const String& str)
|
|
|
|
{
|
|
|
|
String result = str;
|
|
|
|
boost::algorithm::replace_all(result, "\\", "\\\\");
|
|
|
|
boost::algorithm::replace_all(result, "\n", "\\n");
|
|
|
|
boost::algorithm::replace_all(result, "\t", "\\t");
|
|
|
|
boost::algorithm::replace_all(result, "\r", "\\r");
|
|
|
|
boost::algorithm::replace_all(result, "\b", "\\b");
|
|
|
|
boost::algorithm::replace_all(result, "\f", "\\f");
|
|
|
|
boost::algorithm::replace_all(result, "\"", "\\\"");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2014-10-20 18:37:19 +02:00
|
|
|
void RepositoryUtility::FormatValue(std::ostream& fp, const Value& val)
|
|
|
|
{
|
2014-12-18 16:55:45 +01:00
|
|
|
if (val.IsObjectType<Array>()) {
|
|
|
|
FormatArray(fp, val);
|
|
|
|
return;
|
|
|
|
}
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-12-18 16:55:45 +01:00
|
|
|
if (val.IsString()) {
|
2016-01-21 18:14:53 +01:00
|
|
|
fp << "\"" << EscapeIcingaString(val) << "\"";
|
2014-12-18 16:55:45 +01:00
|
|
|
return;
|
|
|
|
}
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2016-01-21 18:14:53 +01:00
|
|
|
fp << EscapeIcingaString(val);
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void RepositoryUtility::FormatArray(std::ostream& fp, const Array::Ptr& arr)
|
|
|
|
{
|
2014-12-18 16:55:45 +01:00
|
|
|
bool first = true;
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-12-18 16:55:45 +01:00
|
|
|
fp << "[ ";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-12-18 16:55:45 +01:00
|
|
|
if (arr) {
|
|
|
|
ObjectLock olock(arr);
|
|
|
|
BOOST_FOREACH(const Value& value, arr) {
|
|
|
|
if (first)
|
|
|
|
first = false;
|
|
|
|
else
|
|
|
|
fp << ", ";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-12-18 16:55:45 +01:00
|
|
|
FormatValue(fp, value);
|
|
|
|
}
|
|
|
|
}
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-12-18 16:55:45 +01:00
|
|
|
if (!first)
|
|
|
|
fp << " ";
|
2014-10-20 18:37:19 +02:00
|
|
|
|
2014-12-18 16:55:45 +01:00
|
|
|
fp << "]";
|
2014-10-20 18:37:19 +02:00
|
|
|
}
|