icinga2/lib/remote/configpackageutility.cpp
Michael Friedrich 6cce9c0fdd API: Automatically repair broken packages
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
2019-05-10 12:48:34 +02:00

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;
}