1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2
3 #include "remote/configobjectutility.hpp"
4 #include "remote/configpackageutility.hpp"
5 #include "remote/apilistener.hpp"
6 #include "config/configcompiler.hpp"
7 #include "config/configitem.hpp"
8 #include "base/configwriter.hpp"
9 #include "base/exception.hpp"
10 #include "base/dependencygraph.hpp"
11 #include "base/tlsutility.hpp"
12 #include "base/utility.hpp"
13 #include <boost/algorithm/string/case_conv.hpp>
14 #include <boost/filesystem.hpp>
15 #include <boost/system/error_code.hpp>
16 #include <fstream>
17 #include <utility>
18
19 using namespace icinga;
20
GetConfigDir()21 String ConfigObjectUtility::GetConfigDir()
22 {
23 String prefix = ConfigPackageUtility::GetPackageDir() + "/_api/";
24 String activeStage = ConfigPackageUtility::GetActiveStage("_api");
25
26 if (activeStage.IsEmpty())
27 RepairPackage("_api");
28
29 return prefix + activeStage;
30 }
31
GetObjectConfigPath(const Type::Ptr & type,const String & fullName)32 String ConfigObjectUtility::GetObjectConfigPath(const Type::Ptr& type, const String& fullName)
33 {
34 String typeDir = type->GetPluralName();
35 boost::algorithm::to_lower(typeDir);
36
37 /* This may throw an exception the caller above must handle. */
38 String prefix = GetConfigDir() + "/conf.d/" + type->GetPluralName().ToLower() + "/";
39
40 String escapedName = EscapeName(fullName);
41
42 String longPath = prefix + escapedName + ".conf";
43
44 /*
45 * The long path may cause trouble due to exceeding the allowed filename length of the filesystem. Therefore, the
46 * preferred solution would be to use the truncated and hashed version as returned at the end of this function.
47 * However, for compatibility reasons, we have to keep the old long version in some cases. Notably, this could lead
48 * to the creation of objects that can't be synced to child nodes if they are running an older version. Thus, for
49 * now, the fix is only enabled for comments and downtimes, as these are the object types for which the issue is
50 * most likely triggered but can't be worked around easily (you'd have to rename the host and/or service in order to
51 * be able to schedule a downtime or add an acknowledgement, which is not feasible) and the impact of not syncing
52 * these objects through the whole cluster is limited. For other object types, we currently prefer to fail the
53 * creation early so that configuration inconsistencies throughout the cluster are avoided.
54 */
55 if ((type->GetName() != "Comment" && type->GetName() != "Downtime") || Utility::PathExists(longPath)) {
56 return std::move(longPath);
57 }
58
59 /* Maximum length 80 bytes object name + 3 bytes "..." + 40 bytes SHA1 (hex-encoded) */
60 return prefix + Utility::TruncateUsingHash<80+3+40>(escapedName) + ".conf";
61 }
62
RepairPackage(const String & package)63 void ConfigObjectUtility::RepairPackage(const String& package)
64 {
65 /* Try to fix the active stage, whenever we find a directory in there.
66 * This automatically heals packages < 2.11 which remained broken.
67 */
68 String dir = ConfigPackageUtility::GetPackageDir() + "/" + package + "/";
69
70 namespace fs = boost::filesystem;
71
72 /* Use iterators to workaround VS builds on Windows. */
73 fs::path path(dir.Begin(), dir.End());
74
75 fs::recursive_directory_iterator end;
76
77 String foundActiveStage;
78
79 for (fs::recursive_directory_iterator it(path); it != end; it++) {
80 boost::system::error_code ec;
81
82 const fs::path d = *it;
83 if (fs::is_directory(d, ec)) {
84 /* Extract the relative directory name. */
85 foundActiveStage = d.stem().string();
86
87 break; // Use the first found directory.
88 }
89 }
90
91 if (!foundActiveStage.IsEmpty()) {
92 Log(LogInformation, "ConfigObjectUtility")
93 << "Repairing config package '" << package << "' with stage '" << foundActiveStage << "'.";
94
95 ConfigPackageUtility::ActivateStage(package, foundActiveStage);
96 } else {
97 BOOST_THROW_EXCEPTION(std::invalid_argument("Cannot repair package '" + package + "', please check the troubleshooting docs."));
98 }
99 }
100
CreateStorage()101 void ConfigObjectUtility::CreateStorage()
102 {
103 std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
104
105 /* For now, we only use _api as our creation target. */
106 String package = "_api";
107
108 if (!ConfigPackageUtility::PackageExists(package)) {
109 Log(LogNotice, "ConfigObjectUtility")
110 << "Package " << package << " doesn't exist yet, creating it.";
111
112 ConfigPackageUtility::CreatePackage(package);
113
114 String stage = ConfigPackageUtility::CreateStage(package);
115 ConfigPackageUtility::ActivateStage(package, stage);
116 }
117 }
118
EscapeName(const String & name)119 String ConfigObjectUtility::EscapeName(const String& name)
120 {
121 return Utility::EscapeString(name, "<>:\"/\\|?*", true);
122 }
123
CreateObjectConfig(const Type::Ptr & type,const String & fullName,bool ignoreOnError,const Array::Ptr & templates,const Dictionary::Ptr & attrs)124 String ConfigObjectUtility::CreateObjectConfig(const Type::Ptr& type, const String& fullName,
125 bool ignoreOnError, const Array::Ptr& templates, const Dictionary::Ptr& attrs)
126 {
127 auto *nc = dynamic_cast<NameComposer *>(type.get());
128 Dictionary::Ptr nameParts;
129 String name;
130
131 if (nc) {
132 nameParts = nc->ParseName(fullName);
133 name = nameParts->Get("name");
134 } else
135 name = fullName;
136
137 Dictionary::Ptr allAttrs = new Dictionary();
138
139 if (attrs) {
140 attrs->CopyTo(allAttrs);
141
142 ObjectLock olock(attrs);
143 for (const Dictionary::Pair& kv : attrs) {
144 int fid = type->GetFieldId(kv.first.SubStr(0, kv.first.FindFirstOf(".")));
145
146 if (fid < 0)
147 BOOST_THROW_EXCEPTION(ScriptError("Invalid attribute specified: " + kv.first));
148
149 Field field = type->GetFieldInfo(fid);
150
151 if (!(field.Attributes & FAConfig) || kv.first == "name")
152 BOOST_THROW_EXCEPTION(ScriptError("Attribute is marked for internal use only and may not be set: " + kv.first));
153 }
154 }
155
156 if (nameParts)
157 nameParts->CopyTo(allAttrs);
158
159 allAttrs->Remove("name");
160
161 /* update the version for config sync */
162 allAttrs->Set("version", Utility::GetTime());
163
164 std::ostringstream config;
165 ConfigWriter::EmitConfigItem(config, type->GetName(), name, false, ignoreOnError, templates, allAttrs);
166 ConfigWriter::EmitRaw(config, "\n");
167
168 return config.str();
169 }
170
CreateObject(const Type::Ptr & type,const String & fullName,const String & config,const Array::Ptr & errors,const Array::Ptr & diagnosticInformation,const Value & cookie)171 bool ConfigObjectUtility::CreateObject(const Type::Ptr& type, const String& fullName,
172 const String& config, const Array::Ptr& errors, const Array::Ptr& diagnosticInformation, const Value& cookie)
173 {
174 CreateStorage();
175
176 {
177 auto configType (dynamic_cast<ConfigType*>(type.get()));
178
179 if (configType && configType->GetObject(fullName)) {
180 errors->Add("Object '" + fullName + "' already exists.");
181 return false;
182 }
183 }
184
185 String path;
186
187 try {
188 path = GetObjectConfigPath(type, fullName);
189 } catch (const std::exception& ex) {
190 errors->Add("Config package broken: " + DiagnosticInformation(ex, false));
191 return false;
192 }
193
194 Utility::MkDirP(Utility::DirName(path), 0700);
195
196 std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::trunc);
197 fp << config;
198 fp.close();
199
200 std::unique_ptr<Expression> expr = ConfigCompiler::CompileFile(path, String(), "_api");
201
202 try {
203 ActivationScope ascope;
204
205 ScriptFrame frame(true);
206 expr->Evaluate(frame);
207 expr.reset();
208
209 WorkQueue upq;
210 upq.SetName("ConfigObjectUtility::CreateObject");
211
212 std::vector<ConfigItem::Ptr> newItems;
213
214 /*
215 * Disable logging for object creation, but do so ourselves later on.
216 * Duplicate the error handling for better logging and debugging here.
217 */
218 if (!ConfigItem::CommitItems(ascope.GetContext(), upq, newItems, true)) {
219 if (errors) {
220 Log(LogNotice, "ConfigObjectUtility")
221 << "Failed to commit config item '" << fullName << "'. Aborting and removing config path '" << path << "'.";
222
223 Utility::Remove(path);
224
225 for (const boost::exception_ptr& ex : upq.GetExceptions()) {
226 errors->Add(DiagnosticInformation(ex, false));
227
228 if (diagnosticInformation)
229 diagnosticInformation->Add(DiagnosticInformation(ex));
230 }
231 }
232
233 return false;
234 }
235
236 /*
237 * Activate the config object.
238 * uq, items, runtimeCreated, silent, withModAttrs, cookie
239 * IMPORTANT: Forward the cookie aka origin in order to prevent sync loops in the same zone!
240 */
241 if (!ConfigItem::ActivateItems(newItems, true, false, false, cookie)) {
242 if (errors) {
243 Log(LogNotice, "ConfigObjectUtility")
244 << "Failed to activate config object '" << fullName << "'. Aborting and removing config path '" << path << "'.";
245
246 Utility::Remove(path);
247
248 for (const boost::exception_ptr& ex : upq.GetExceptions()) {
249 errors->Add(DiagnosticInformation(ex, false));
250
251 if (diagnosticInformation)
252 diagnosticInformation->Add(DiagnosticInformation(ex));
253 }
254 }
255
256 return false;
257 }
258
259 /* if (type != Comment::TypeInstance && type != Downtime::TypeInstance)
260 * Does not work since this would require libicinga, which has a dependency on libremote
261 * Would work if these libs were static.
262 */
263 if (type->GetName() != "Comment" && type->GetName() != "Downtime")
264 ApiListener::UpdateObjectAuthority();
265
266 // At this stage we should have a config object already. If not, it was ignored before.
267 auto *ctype = dynamic_cast<ConfigType *>(type.get());
268 ConfigObject::Ptr obj = ctype->GetObject(fullName);
269
270 if (obj) {
271 Log(LogInformation, "ConfigObjectUtility")
272 << "Created and activated object '" << fullName << "' of type '" << type->GetName() << "'.";
273 } else {
274 Log(LogNotice, "ConfigObjectUtility")
275 << "Object '" << fullName << "' was not created but ignored due to errors.";
276 }
277
278 } catch (const std::exception& ex) {
279 Utility::Remove(path);
280
281 if (errors)
282 errors->Add(DiagnosticInformation(ex, false));
283
284 if (diagnosticInformation)
285 diagnosticInformation->Add(DiagnosticInformation(ex));
286
287 return false;
288 }
289
290 return true;
291 }
292
DeleteObjectHelper(const ConfigObject::Ptr & object,bool cascade,const Array::Ptr & errors,const Array::Ptr & diagnosticInformation,const Value & cookie)293 bool ConfigObjectUtility::DeleteObjectHelper(const ConfigObject::Ptr& object, bool cascade,
294 const Array::Ptr& errors, const Array::Ptr& diagnosticInformation, const Value& cookie)
295 {
296 std::vector<Object::Ptr> parents = DependencyGraph::GetParents(object);
297
298 Type::Ptr type = object->GetReflectionType();
299
300 String name = object->GetName();
301
302 if (!parents.empty() && !cascade) {
303 if (errors) {
304 errors->Add("Object '" + name + "' of type '" + type->GetName() +
305 "' cannot be deleted because other objects depend on it. "
306 "Use cascading delete to delete it anyway.");
307 }
308
309 return false;
310 }
311
312 for (const Object::Ptr& pobj : parents) {
313 ConfigObject::Ptr parentObj = dynamic_pointer_cast<ConfigObject>(pobj);
314
315 if (!parentObj)
316 continue;
317
318 DeleteObjectHelper(parentObj, cascade, errors, diagnosticInformation, cookie);
319 }
320
321 ConfigItem::Ptr item = ConfigItem::GetByTypeAndName(type, name);
322
323 try {
324 /* mark this object for cluster delete event */
325 object->SetExtension("ConfigObjectDeleted", true);
326
327 /*
328 * Trigger deactivation signal for DB IDO and runtime object delections.
329 * IMPORTANT: Specify the cookie aka origin in order to prevent sync loops
330 * in the same zone!
331 */
332 object->Deactivate(true, cookie);
333
334 if (item)
335 item->Unregister();
336 else
337 object->Unregister();
338
339 } catch (const std::exception& ex) {
340 if (errors)
341 errors->Add(DiagnosticInformation(ex, false));
342
343 if (diagnosticInformation)
344 diagnosticInformation->Add(DiagnosticInformation(ex));
345
346 return false;
347 }
348
349 String path;
350
351 try {
352 path = GetObjectConfigPath(object->GetReflectionType(), name);
353 } catch (const std::exception& ex) {
354 errors->Add("Config package broken: " + DiagnosticInformation(ex, false));
355 return false;
356 }
357
358 Utility::Remove(path);
359
360 Log(LogInformation, "ConfigObjectUtility")
361 << "Deleted object '" << name << "' of type '" << type->GetName() << "'.";
362
363 return true;
364 }
365
DeleteObject(const ConfigObject::Ptr & object,bool cascade,const Array::Ptr & errors,const Array::Ptr & diagnosticInformation,const Value & cookie)366 bool ConfigObjectUtility::DeleteObject(const ConfigObject::Ptr& object, bool cascade, const Array::Ptr& errors,
367 const Array::Ptr& diagnosticInformation, const Value& cookie)
368 {
369 if (object->GetPackage() != "_api") {
370 if (errors)
371 errors->Add("Object cannot be deleted because it was not created using the API.");
372
373 return false;
374 }
375
376 return DeleteObjectHelper(object, cascade, errors, diagnosticInformation, cookie);
377 }
378