mirror of
https://github.com/Icinga/icinga2.git
synced 2025-04-08 17:05:25 +02:00
This partially reverts #7150 and avoids exceptions inside the flow. Each time an empty active stage is detected, Icinga tries to repair it from the the given directory tree. Also, the code now takes into account that it should create the package storage on startup, whether within the API object, or if disabled, inside the application. Caching the active stages for packages in memory only is in effect with the API feature being enabled. This is useful for other deployed config packages, not only the internal one. fixes #7173 refs #7150 refs #7119 fixes #6959
396 lines
12 KiB
C++
396 lines
12 KiB
C++
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
|
|
|
|
#include "remote/configpackageutility.hpp"
|
|
#include "remote/apilistener.hpp"
|
|
#include "base/application.hpp"
|
|
#include "base/exception.hpp"
|
|
#include "base/utility.hpp"
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/regex.hpp>
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
|
|
using namespace icinga;
|
|
|
|
String ConfigPackageUtility::GetPackageDir()
|
|
{
|
|
return Configuration::DataDir + "/api/packages";
|
|
}
|
|
|
|
void ConfigPackageUtility::CreatePackage(const String& name)
|
|
{
|
|
String path = GetPackageDir() + "/" + name;
|
|
|
|
if (Utility::PathExists(path))
|
|
BOOST_THROW_EXCEPTION(std::invalid_argument("Package already exists."));
|
|
|
|
Utility::MkDirP(path, 0700);
|
|
WritePackageConfig(name);
|
|
}
|
|
|
|
void ConfigPackageUtility::DeletePackage(const String& name)
|
|
{
|
|
String path = GetPackageDir() + "/" + name;
|
|
|
|
if (!Utility::PathExists(path))
|
|
BOOST_THROW_EXCEPTION(std::invalid_argument("Package does not exist."));
|
|
|
|
ApiListener::Ptr listener = ApiListener::GetInstance();
|
|
|
|
/* config packages without API make no sense. */
|
|
if (!listener)
|
|
BOOST_THROW_EXCEPTION(std::invalid_argument("No ApiListener instance configured."));
|
|
|
|
listener->RemoveActivePackageStage(name);
|
|
|
|
Utility::RemoveDirRecursive(path);
|
|
Application::RequestRestart();
|
|
}
|
|
|
|
std::vector<String> ConfigPackageUtility::GetPackages()
|
|
{
|
|
String packageDir = GetPackageDir();
|
|
|
|
std::vector<String> packages;
|
|
|
|
/* Package directory does not exist, no packages have been created thus far. */
|
|
if (!Utility::PathExists(packageDir))
|
|
return packages;
|
|
|
|
Utility::Glob(packageDir + "/*", std::bind(&ConfigPackageUtility::CollectDirNames,
|
|
_1, std::ref(packages)), GlobDirectory);
|
|
|
|
return packages;
|
|
}
|
|
|
|
void ConfigPackageUtility::CollectDirNames(const String& path, std::vector<String>& dirs)
|
|
{
|
|
dirs.emplace_back(Utility::BaseName(path));
|
|
}
|
|
|
|
bool ConfigPackageUtility::PackageExists(const String& name)
|
|
{
|
|
return Utility::PathExists(GetPackageDir() + "/" + name);
|
|
}
|
|
|
|
String ConfigPackageUtility::CreateStage(const String& packageName, const Dictionary::Ptr& files)
|
|
{
|
|
String stageName = Utility::NewUniqueID();
|
|
|
|
String path = GetPackageDir() + "/" + packageName;
|
|
|
|
if (!Utility::PathExists(path))
|
|
BOOST_THROW_EXCEPTION(std::invalid_argument("Package does not exist."));
|
|
|
|
path += "/" + stageName;
|
|
|
|
Utility::MkDirP(path, 0700);
|
|
Utility::MkDirP(path + "/conf.d", 0700);
|
|
Utility::MkDirP(path + "/zones.d", 0700);
|
|
WriteStageConfig(packageName, stageName);
|
|
|
|
bool foundDotDot = false;
|
|
|
|
if (files) {
|
|
ObjectLock olock(files);
|
|
for (const Dictionary::Pair& kv : files) {
|
|
if (ContainsDotDot(kv.first)) {
|
|
foundDotDot = true;
|
|
break;
|
|
}
|
|
|
|
String filePath = path + "/" + kv.first;
|
|
|
|
Log(LogInformation, "ConfigPackageUtility")
|
|
<< "Updating configuration file: " << filePath;
|
|
|
|
// Pass the directory and generate a dir tree, if it does not already exist
|
|
Utility::MkDirP(Utility::DirName(filePath), 0750);
|
|
std::ofstream fp(filePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
|
|
fp << kv.second;
|
|
fp.close();
|
|
}
|
|
}
|
|
|
|
if (foundDotDot) {
|
|
Utility::RemoveDirRecursive(path);
|
|
BOOST_THROW_EXCEPTION(std::invalid_argument("Path must not contain '..'."));
|
|
}
|
|
|
|
return stageName;
|
|
}
|
|
|
|
void ConfigPackageUtility::WritePackageConfig(const String& packageName)
|
|
{
|
|
String stageName = GetActiveStage(packageName);
|
|
|
|
String includePath = GetPackageDir() + "/" + packageName + "/include.conf";
|
|
std::ofstream fpInclude(includePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
|
|
fpInclude << "include \"*/include.conf\"\n";
|
|
fpInclude.close();
|
|
|
|
String activePath = GetPackageDir() + "/" + packageName + "/active.conf";
|
|
std::ofstream fpActive(activePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
|
|
fpActive << "if (!globals.contains(\"ActiveStages\")) {\n"
|
|
<< " globals.ActiveStages = {}\n"
|
|
<< "}\n"
|
|
<< "\n"
|
|
<< "if (globals.contains(\"ActiveStageOverride\")) {\n"
|
|
<< " var arr = ActiveStageOverride.split(\":\")\n"
|
|
<< " if (arr[0] == \"" << packageName << "\") {\n"
|
|
<< " if (arr.len() < 2) {\n"
|
|
<< " log(LogCritical, \"Config\", \"Invalid value for ActiveStageOverride\")\n"
|
|
<< " } else {\n"
|
|
<< " ActiveStages[\"" << packageName << "\"] = arr[1]\n"
|
|
<< " }\n"
|
|
<< " }\n"
|
|
<< "}\n"
|
|
<< "\n"
|
|
<< "if (!ActiveStages.contains(\"" << packageName << "\")) {\n"
|
|
<< " ActiveStages[\"" << packageName << "\"] = \"" << stageName << "\"\n"
|
|
<< "}\n";
|
|
fpActive.close();
|
|
}
|
|
|
|
void ConfigPackageUtility::WriteStageConfig(const String& packageName, const String& stageName)
|
|
{
|
|
String path = GetPackageDir() + "/" + packageName + "/" + stageName + "/include.conf";
|
|
std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
|
|
fp << "include \"../active.conf\"\n"
|
|
<< "if (ActiveStages[\"" << packageName << "\"] == \"" << stageName << "\") {\n"
|
|
<< " include_recursive \"conf.d\"\n"
|
|
<< " include_zones \"" << packageName << "\", \"zones.d\"\n"
|
|
<< "}\n";
|
|
fp.close();
|
|
}
|
|
|
|
void ConfigPackageUtility::ActivateStage(const String& packageName, const String& stageName)
|
|
{
|
|
SetActiveStage(packageName, stageName);
|
|
|
|
WritePackageConfig(packageName);
|
|
}
|
|
|
|
void ConfigPackageUtility::TryActivateStageCallback(const ProcessResult& pr, const String& packageName, const String& stageName, bool reload)
|
|
{
|
|
String logFile = GetPackageDir() + "/" + packageName + "/" + stageName + "/startup.log";
|
|
std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
|
|
fpLog << pr.Output;
|
|
fpLog.close();
|
|
|
|
String statusFile = GetPackageDir() + "/" + packageName + "/" + stageName + "/status";
|
|
std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
|
|
fpStatus << pr.ExitStatus;
|
|
fpStatus.close();
|
|
|
|
/* validation went fine, activate stage and reload */
|
|
if (pr.ExitStatus == 0) {
|
|
{
|
|
boost::mutex::scoped_lock lock(GetStaticPackageMutex());
|
|
|
|
ActivateStage(packageName, stageName);
|
|
}
|
|
|
|
if (reload)
|
|
Application::RequestRestart();
|
|
} else {
|
|
Log(LogCritical, "ConfigPackageUtility")
|
|
<< "Config validation failed for package '"
|
|
<< packageName << "' and stage '" << stageName << "'.";
|
|
}
|
|
}
|
|
|
|
void ConfigPackageUtility::AsyncTryActivateStage(const String& packageName, const String& stageName, bool reload)
|
|
{
|
|
VERIFY(Application::GetArgC() >= 1);
|
|
|
|
// prepare arguments
|
|
Array::Ptr args = new Array({
|
|
Application::GetExePath(Application::GetArgV()[0]),
|
|
});
|
|
|
|
// copy all arguments of parent process
|
|
for (int i = 1; i < Application::GetArgC(); i++) {
|
|
String argV = Application::GetArgV()[i];
|
|
|
|
if (argV == "-d" || argV == "--daemonize")
|
|
continue;
|
|
|
|
args->Add(argV);
|
|
}
|
|
|
|
// add arguments for validation
|
|
args->Add("--validate");
|
|
args->Add("--define");
|
|
args->Add("ActiveStageOverride=" + packageName + ":" + stageName);
|
|
|
|
Process::Ptr process = new Process(Process::PrepareCommand(args));
|
|
process->SetTimeout(Application::GetReloadTimeout());
|
|
process->Run(std::bind(&TryActivateStageCallback, _1, packageName, stageName, reload));
|
|
}
|
|
|
|
void ConfigPackageUtility::DeleteStage(const String& packageName, const String& stageName)
|
|
{
|
|
String path = GetPackageDir() + "/" + packageName + "/" + stageName;
|
|
|
|
if (!Utility::PathExists(path))
|
|
BOOST_THROW_EXCEPTION(std::invalid_argument("Stage does not exist."));
|
|
|
|
if (GetActiveStage(packageName) == stageName)
|
|
BOOST_THROW_EXCEPTION(std::invalid_argument("Active stage cannot be deleted."));
|
|
|
|
Utility::RemoveDirRecursive(path);
|
|
}
|
|
|
|
std::vector<String> ConfigPackageUtility::GetStages(const String& packageName)
|
|
{
|
|
std::vector<String> stages;
|
|
Utility::Glob(GetPackageDir() + "/" + packageName + "/*", std::bind(&ConfigPackageUtility::CollectDirNames, _1, std::ref(stages)), GlobDirectory);
|
|
return stages;
|
|
}
|
|
|
|
String ConfigPackageUtility::GetActiveStageFromFile(const String& packageName)
|
|
{
|
|
/* Lock the transaction, reading this only happens on startup or when something really is broken. */
|
|
boost::mutex::scoped_lock lock(GetStaticActiveStageMutex());
|
|
|
|
String path = GetPackageDir() + "/" + packageName + "/active-stage";
|
|
|
|
std::ifstream fp;
|
|
fp.open(path.CStr());
|
|
|
|
String stage;
|
|
std::getline(fp, stage.GetData());
|
|
|
|
fp.close();
|
|
|
|
if (fp.fail())
|
|
return ""; /* Don't use exceptions here. The caller must deal with empty stages at this point. Happens on initial package creation for example. */
|
|
|
|
return stage.Trim();
|
|
}
|
|
|
|
void ConfigPackageUtility::SetActiveStageToFile(const String& packageName, const String& stageName)
|
|
{
|
|
boost::mutex::scoped_lock lock(GetStaticActiveStageMutex());
|
|
|
|
String activeStagePath = GetPackageDir() + "/" + packageName + "/active-stage";
|
|
|
|
std::ofstream fpActiveStage(activeStagePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); //TODO: fstream exceptions
|
|
fpActiveStage << stageName;
|
|
fpActiveStage.close();
|
|
}
|
|
|
|
String ConfigPackageUtility::GetActiveStage(const String& packageName)
|
|
{
|
|
String activeStage;
|
|
|
|
ApiListener::Ptr listener = ApiListener::GetInstance();
|
|
|
|
/* If we don't have an API feature, just use the file storage without caching this.
|
|
* This happens when ScheduledDowntime objects generate Downtime objects.
|
|
* TODO: Make the API a first class citizen.
|
|
*/
|
|
if (!listener)
|
|
return GetActiveStageFromFile(packageName);
|
|
|
|
/* First use runtime state. */
|
|
try {
|
|
activeStage = listener->GetActivePackageStage(packageName);
|
|
} catch (const std::exception& ex) {
|
|
/* Fallback to reading the file, happens on restarts. */
|
|
activeStage = GetActiveStageFromFile(packageName);
|
|
|
|
/* When we've read something, correct memory. */
|
|
if (!activeStage.IsEmpty())
|
|
listener->SetActivePackageStage(packageName, activeStage);
|
|
}
|
|
|
|
return activeStage;
|
|
}
|
|
|
|
void ConfigPackageUtility::SetActiveStage(const String& packageName, const String& stageName)
|
|
{
|
|
/* Update the marker on disk for restarts. */
|
|
SetActiveStageToFile(packageName, stageName);
|
|
|
|
ApiListener::Ptr listener = ApiListener::GetInstance();
|
|
|
|
/* No API, no caching. */
|
|
if (!listener)
|
|
return;
|
|
|
|
listener->SetActivePackageStage(packageName, stageName);
|
|
}
|
|
|
|
std::vector<std::pair<String, bool> > ConfigPackageUtility::GetFiles(const String& packageName, const String& stageName)
|
|
{
|
|
std::vector<std::pair<String, bool> > paths;
|
|
Utility::GlobRecursive(GetPackageDir() + "/" + packageName + "/" + stageName, "*", std::bind(&ConfigPackageUtility::CollectPaths, _1, std::ref(paths)), GlobDirectory | GlobFile);
|
|
|
|
return paths;
|
|
}
|
|
|
|
void ConfigPackageUtility::CollectPaths(const String& path, std::vector<std::pair<String, bool> >& paths)
|
|
{
|
|
#ifndef _WIN32
|
|
struct stat statbuf;
|
|
int rc = lstat(path.CStr(), &statbuf);
|
|
if (rc < 0)
|
|
BOOST_THROW_EXCEPTION(posix_error()
|
|
<< boost::errinfo_api_function("lstat")
|
|
<< boost::errinfo_errno(errno)
|
|
<< boost::errinfo_file_name(path));
|
|
|
|
paths.emplace_back(path, S_ISDIR(statbuf.st_mode));
|
|
#else /* _WIN32 */
|
|
struct _stat statbuf;
|
|
int rc = _stat(path.CStr(), &statbuf);
|
|
if (rc < 0)
|
|
BOOST_THROW_EXCEPTION(posix_error()
|
|
<< boost::errinfo_api_function("_stat")
|
|
<< boost::errinfo_errno(errno)
|
|
<< boost::errinfo_file_name(path));
|
|
|
|
paths.emplace_back(path, ((statbuf.st_mode & S_IFMT) == S_IFDIR));
|
|
#endif /* _WIN32 */
|
|
}
|
|
|
|
bool ConfigPackageUtility::ContainsDotDot(const String& path)
|
|
{
|
|
std::vector<String> tokens = path.Split("/\\");
|
|
|
|
for (const String& part : tokens) {
|
|
if (part == "..")
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ConfigPackageUtility::ValidateName(const String& name)
|
|
{
|
|
if (name.IsEmpty())
|
|
return false;
|
|
|
|
/* check for path injection */
|
|
if (ContainsDotDot(name))
|
|
return false;
|
|
|
|
boost::regex expr("^[^a-zA-Z0-9_\\-]*$", boost::regex::icase);
|
|
boost::smatch what;
|
|
return (!boost::regex_search(name.GetData(), what, expr));
|
|
}
|
|
|
|
boost::mutex& ConfigPackageUtility::GetStaticPackageMutex()
|
|
{
|
|
static boost::mutex mutex;
|
|
return mutex;
|
|
}
|
|
|
|
boost::mutex& ConfigPackageUtility::GetStaticActiveStageMutex()
|
|
{
|
|
static boost::mutex mutex;
|
|
return mutex;
|
|
}
|