From fca7a33aac383adf30a84beb9ed3edbe7d875395 Mon Sep 17 00:00:00 2001 From: Michael Friedrich Date: Tue, 21 Jul 2015 16:10:13 +0200 Subject: [PATCH] Implement config file management for the API refs #9083 fixes #9102 fixes #9103 fixes #9104 fixes #9705 --- doc/4-configuring-icinga-2.md | 10 - etc/icinga2/icinga2.conf | 6 - lib/base/utility.cpp | 33 ++++ lib/base/utility.hpp | 3 +- lib/cli/daemonutility.cpp | 19 ++ lib/remote/CMakeLists.txt | 6 +- lib/remote/configfileshandler.cpp | 96 +++++++++ lib/remote/configfileshandler.hpp | 42 ++++ lib/remote/configmoduleshandler.cpp | 149 ++++++++++++++ lib/remote/configmoduleshandler.hpp | 45 +++++ lib/remote/configmoduleutility.cpp | 297 ++++++++++++++++++++++++++++ lib/remote/configmoduleutility.hpp | 72 +++++++ lib/remote/configstageshandler.cpp | 184 +++++++++++++++++ lib/remote/configstageshandler.hpp | 45 +++++ lib/remote/httpconnection.cpp | 9 +- lib/remote/httputility.cpp | 59 ++++++ lib/remote/httputility.hpp | 45 +++++ 17 files changed, 1100 insertions(+), 20 deletions(-) create mode 100644 lib/remote/configfileshandler.cpp create mode 100644 lib/remote/configfileshandler.hpp create mode 100644 lib/remote/configmoduleshandler.cpp create mode 100644 lib/remote/configmoduleshandler.hpp create mode 100644 lib/remote/configmoduleutility.cpp create mode 100644 lib/remote/configmoduleutility.hpp create mode 100644 lib/remote/configstageshandler.cpp create mode 100644 lib/remote/configstageshandler.hpp create mode 100644 lib/remote/httputility.cpp create mode 100644 lib/remote/httputility.hpp diff --git a/doc/4-configuring-icinga-2.md b/doc/4-configuring-icinga-2.md index a85d83e13..f5686365e 100644 --- a/doc/4-configuring-icinga-2.md +++ b/doc/4-configuring-icinga-2.md @@ -136,16 +136,6 @@ and their generated configuration described in You can put your own configuration files in the [conf.d](4-configuring-icinga-2.md#conf-d) directory. This directive makes sure that all of your own configuration files are included. - /** - * The zones.d directory contains configuration files for satellite - * instances. - */ - include_zones "etc", "zones.d" - -Configuration files for satellite instances are managed in 'zones'. This directive ensures -that all configuration files in the `zones.d` directory are included and that the `zones` -attribute for objects defined in this directory is set appropriately. - ### constants.conf The `constants.conf` configuration file can be used to define global constants. diff --git a/etc/icinga2/icinga2.conf b/etc/icinga2/icinga2.conf index 825ffe19b..e258e7e9a 100644 --- a/etc/icinga2/icinga2.conf +++ b/etc/icinga2/icinga2.conf @@ -48,9 +48,3 @@ include_recursive "repository.d" * directory. Each of these files must have the file extension ".conf". */ include_recursive "conf.d" - -/** - * The zones.d directory contains configuration files for satellite - * instances. - */ -include_zones "etc", "zones.d" diff --git a/lib/base/utility.cpp b/lib/base/utility.cpp index 0c4161140..b1da99df4 100644 --- a/lib/base/utility.cpp +++ b/lib/base/utility.cpp @@ -581,6 +581,39 @@ bool Utility::MkDirP(const String& path, int flags) return ret; } +void Utility::RemoveDirRecursive(const String& path) +{ + std::vector paths; + Utility::GlobRecursive(path, "*", boost::bind(&Utility::CollectPaths, _1, boost::ref(paths)), GlobFile | GlobDirectory); + + /* This relies on the fact that GlobRecursive lists the parent directory + first before recursing into subdirectories. */ + std::reverse(paths.begin(), paths.end()); + + BOOST_FOREACH(const String& path, paths) { + if (remove(path.CStr()) < 0) + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("remove") + << boost::errinfo_errno(errno) + << boost::errinfo_file_name(path)); + } + +#ifndef _WIN32 + if (rmdir(path.CStr()) < 0) +#else /* _WIN32 */ + if (_rmdir(path.CStr()) < 0) +#endif /* _WIN32 */ + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("rmdir") + << boost::errinfo_errno(errno) + << boost::errinfo_file_name(path)); +} + +void Utility::CollectPaths(const String& path, std::vector& paths) +{ + paths.push_back(path); +} + void Utility::CopyFile(const String& source, const String& target) { std::ifstream ifs(source.CStr(), std::ios::binary); diff --git a/lib/base/utility.hpp b/lib/base/utility.hpp index f616801e2..c1ceb273c 100644 --- a/lib/base/utility.hpp +++ b/lib/base/utility.hpp @@ -123,6 +123,7 @@ public: static bool PathExists(const String& path); + static void RemoveDirRecursive(const String& path); static void CopyFile(const String& source, const String& target); static Value LoadJsonFile(const String& path); @@ -130,10 +131,10 @@ public: private: Utility(void); + static void CollectPaths(const String& path, std::vector& paths); static boost::thread_specific_ptr m_ThreadName; static boost::thread_specific_ptr m_RandSeed; - }; } diff --git a/lib/cli/daemonutility.cpp b/lib/cli/daemonutility.cpp index c09de0346..4a1f84dad 100644 --- a/lib/cli/daemonutility.cpp +++ b/lib/cli/daemonutility.cpp @@ -85,10 +85,29 @@ bool DaemonUtility::ValidateConfigFiles(const std::vector& configs, * unfortunately moving it there is somewhat non-trivial. */ success = true; + String zonesEtcDir = Application::GetZonesDir(); + if (!zonesEtcDir.IsEmpty() && Utility::PathExists(zonesEtcDir)) + Utility::Glob(zonesEtcDir + "/*", boost::bind(&IncludeZoneDirRecursive, _1, boost::ref(success)), GlobDirectory); + + if (!success) + return false; + String zonesVarDir = Application::GetLocalStateDir() + "/lib/icinga2/api/zones"; if (Utility::PathExists(zonesVarDir)) Utility::Glob(zonesVarDir + "/*", boost::bind(&IncludeNonLocalZone, _1, boost::ref(success)), GlobDirectory); + if (!success) + return false; + + String modulesVarDir = Application::GetLocalStateDir() + "/lib/icinga2/api/modules"; + if (Utility::PathExists(modulesVarDir)) { + std::vector expressions; + Utility::Glob(modulesVarDir + "/*/include.conf", boost::bind(&ConfigCompiler::CollectIncludes, boost::ref(expressions), _1, ""), GlobFile); + DictExpression expr(expressions); + if (!ExecuteExpression(&expr)) + success = false; + } + if (!success) return false; diff --git a/lib/remote/CMakeLists.txt b/lib/remote/CMakeLists.txt index 125054940..6d3827f02 100644 --- a/lib/remote/CMakeLists.txt +++ b/lib/remote/CMakeLists.txt @@ -22,9 +22,11 @@ mkclass_target(zone.ti zone.tcpp zone.thpp) set(remote_SOURCES apifunction.cpp apilistener.cpp apilistener.thpp apilistener-sync.cpp - apiuser.cpp apiuser.thpp authority.cpp base64.cpp endpoint.cpp endpoint.thpp + apiuser.cpp apiuser.thpp authority.cpp base64.cpp configfileshandler.cpp + configmoduleshandler.cpp configmoduleutility.cpp configstageshandler.cpp + endpoint.cpp endpoint.thpp httpchunkedencoding.cpp httpconnection.cpp httpdemohandler.cpp httphandler.cpp httprequest.cpp httpresponse.cpp - jsonrpc.cpp jsonrpcconnection.cpp jsonrpcconnection-heartbeat.cpp + httputility.cpp jsonrpc.cpp jsonrpcconnection.cpp jsonrpcconnection-heartbeat.cpp messageorigin.cpp zone.cpp zone.thpp url.cpp ) diff --git a/lib/remote/configfileshandler.cpp b/lib/remote/configfileshandler.cpp new file mode 100644 index 000000000..a98ec276e --- /dev/null +++ b/lib/remote/configfileshandler.cpp @@ -0,0 +1,96 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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 "remote/configfileshandler.hpp" +#include "remote/configmoduleutility.hpp" +#include "remote/httputility.hpp" +#include "base/exception.hpp" +#include +#include + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/config/files", ConfigFilesHandler); + +void ConfigFilesHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + if (request.RequestMethod == "GET") + HandleGet(user, request, response); + else + response.SetStatus(400, "Bad request"); +} + +bool ConfigFilesHandler::CanAlsoHandleUrl(const Url::Ptr& url) const +{ + return true; +} + +void ConfigFilesHandler::HandleGet(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + Dictionary::Ptr params = HttpUtility::FetchRequestParameters(request); + + const std::vector& urlPath = request.RequestUrl->GetPath(); + + if (urlPath.size() >= 4) + params->Set("module", urlPath[3]); + + if (urlPath.size() >= 5) + params->Set("stage", urlPath[4]); + + if (urlPath.size() >= 6) { + std::vector tmpPath(urlPath.begin() + 5, urlPath.end()); + params->Set("path", boost::algorithm::join(tmpPath, "/")); + } + + String moduleName = params->Get("module"); + String stageName = params->Get("stage"); + + if (!ConfigModuleUtility::ValidateName(moduleName) || !ConfigModuleUtility::ValidateName(stageName)) { + response.SetStatus(403, "Forbidden"); + return; + } + + String relativePath = params->Get("path"); + + if (ConfigModuleUtility::ContainsDotDot(relativePath)) { + response.SetStatus(403, "Forbidden"); + return; + } + + String path = ConfigModuleUtility::GetModuleDir() + "/" + moduleName + "/" + stageName + "/" + relativePath; + + if (!Utility::PathExists(path)) { + response.SetStatus(404, "File not found"); + return; + } + + try { + std::ifstream fp(path.CStr(), std::ifstream::in | std::ifstream::binary); + fp.exceptions(std::ifstream::badbit); + + String content((std::istreambuf_iterator(fp)), std::istreambuf_iterator()); + response.SetStatus(200, "OK"); + response.AddHeader("Content-Type", "application/octet-stream"); + response.WriteBody(content.CStr(), content.GetLength()); + } catch (const std::exception& ex) { + response.SetStatus(503, "Could not read file"); + } +} + + diff --git a/lib/remote/configfileshandler.hpp b/lib/remote/configfileshandler.hpp new file mode 100644 index 000000000..4a9066439 --- /dev/null +++ b/lib/remote/configfileshandler.hpp @@ -0,0 +1,42 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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. * + ******************************************************************************/ + +#ifndef CONFIGFILESHANDLER_H +#define CONFIGFILESHANDLER_H + +#include "remote/httphandler.hpp" + +namespace icinga +{ + +class I2_REMOTE_API ConfigFilesHandler : public HttpHandler +{ +public: + DECLARE_PTR_TYPEDEFS(ConfigFilesHandler); + + virtual bool CanAlsoHandleUrl(const Url::Ptr& url) const; + virtual void HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); + +private: + void HandleGet(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); +}; + +} + +#endif /* CONFIGFILESHANDLER_H */ diff --git a/lib/remote/configmoduleshandler.cpp b/lib/remote/configmoduleshandler.cpp new file mode 100644 index 000000000..736d97964 --- /dev/null +++ b/lib/remote/configmoduleshandler.cpp @@ -0,0 +1,149 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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 "remote/configmoduleshandler.hpp" +#include "remote/configmoduleutility.hpp" +#include "remote/httputility.hpp" +#include "base/exception.hpp" + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/config/modules", ConfigModulesHandler); + +void ConfigModulesHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + if (request.RequestMethod == "GET") + HandleGet(user, request, response); + else if (request.RequestMethod == "POST") + HandlePost(user, request, response); + else if (request.RequestMethod == "DELETE") + HandleDelete(user, request, response); + else + response.SetStatus(400, "Bad request"); +} + +bool ConfigModulesHandler::CanAlsoHandleUrl(const Url::Ptr& url) const +{ + if (url->GetPath().size() > 4) + return false; + else + return true; +} + +void ConfigModulesHandler::HandleGet(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + std::vector modules = ConfigModuleUtility::GetModules(); + + Array::Ptr results = new Array(); + + BOOST_FOREACH(const String& module, modules) { + Dictionary::Ptr moduleInfo = new Dictionary(); + moduleInfo->Set("name", module); + moduleInfo->Set("stages", Array::FromVector(ConfigModuleUtility::GetStages(module))); + moduleInfo->Set("active-stage", ConfigModuleUtility::GetActiveStage(module)); + results->Add(moduleInfo); + } + + Dictionary::Ptr result = new Dictionary(); + result->Set("results", results); + + response.SetStatus(200, "OK"); + HttpUtility::SendJsonBody(response, result); +} + +void ConfigModulesHandler::HandlePost(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + Dictionary::Ptr params = HttpUtility::FetchRequestParameters(request); + + if (request.RequestUrl->GetPath().size() >= 4) + params->Set("module", request.RequestUrl->GetPath()[3]); + + String moduleName = params->Get("module"); + + if (!ConfigModuleUtility::ValidateName(moduleName)) { + response.SetStatus(403, "Forbidden"); + return; + } + + int code = 200; + String status = "Created module."; + + try { + ConfigModuleUtility::CreateModule(moduleName); + } catch (const std::exception& ex) { + code = 501; + status = "Error: " + DiagnosticInformation(ex); + } + + Dictionary::Ptr result1 = new Dictionary(); + + result1->Set("module", moduleName); + result1->Set("code", code); + result1->Set("status", status); + + Array::Ptr results = new Array(); + results->Add(result1); + + Dictionary::Ptr result = new Dictionary(); + result->Set("results", results); + + response.SetStatus(code, (code == 200) ? "OK" : "Error"); + HttpUtility::SendJsonBody(response, result); +} + +void ConfigModulesHandler::HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + Dictionary::Ptr params = HttpUtility::FetchRequestParameters(request); + + if (request.RequestUrl->GetPath().size() >= 4) + params->Set("module", request.RequestUrl->GetPath()[3]); + + String moduleName = params->Get("module"); + + if (!ConfigModuleUtility::ValidateName(moduleName)) { + response.SetStatus(403, "Forbidden"); + return; + } + + int code = 200; + String status = "Deleted module."; + + try { + ConfigModuleUtility::DeleteModule(moduleName); + } catch (const std::exception& ex) { + code = 501; + status = "Error: " + DiagnosticInformation(ex); + } + + Dictionary::Ptr result1 = new Dictionary(); + + result1->Set("module", moduleName); + result1->Set("code", code); + result1->Set("status", status); + + Array::Ptr results = new Array(); + results->Add(result1); + + Dictionary::Ptr result = new Dictionary(); + result->Set("results", results); + + response.SetStatus(code, (code == 200) ? "OK" : "Error"); + HttpUtility::SendJsonBody(response, result); +} + diff --git a/lib/remote/configmoduleshandler.hpp b/lib/remote/configmoduleshandler.hpp new file mode 100644 index 000000000..8e670b624 --- /dev/null +++ b/lib/remote/configmoduleshandler.hpp @@ -0,0 +1,45 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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. * + ******************************************************************************/ + +#ifndef CONFIGMODULESHANDLER_H +#define CONFIGMODULESHANDLER_H + +#include "remote/httphandler.hpp" + +namespace icinga +{ + +class I2_REMOTE_API ConfigModulesHandler : public HttpHandler +{ +public: + DECLARE_PTR_TYPEDEFS(ConfigModulesHandler); + + virtual bool CanAlsoHandleUrl(const Url::Ptr& url) const; + virtual void HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); + +private: + void HandleGet(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); + void HandlePost(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); + void HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); + +}; + +} + +#endif /* CONFIGMODULESHANDLER_H */ diff --git a/lib/remote/configmoduleutility.cpp b/lib/remote/configmoduleutility.cpp new file mode 100644 index 000000000..b927c87fc --- /dev/null +++ b/lib/remote/configmoduleutility.cpp @@ -0,0 +1,297 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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 "remote/configmoduleutility.hpp" +#include "base/application.hpp" +#include "base/exception.hpp" +#include "base/scriptglobal.hpp" +#include "base/utility.hpp" +#include "boost/foreach.hpp" +#include +#include +#include +#include + +using namespace icinga; + +String ConfigModuleUtility::GetModuleDir(void) +{ + return Application::GetLocalStateDir() + "/lib/icinga2/api/modules"; +} + +void ConfigModuleUtility::CreateModule(const String& name) +{ + String path = GetModuleDir() + "/" + name; + + if (Utility::PathExists(path)) + BOOST_THROW_EXCEPTION(std::invalid_argument("Module already exists.")); + + Utility::MkDirP(path, 0700); + WriteModuleConfig(name); +} + +void ConfigModuleUtility::DeleteModule(const String& name) +{ + String path = GetModuleDir() + "/" + name; + + if (!Utility::PathExists(path)) + BOOST_THROW_EXCEPTION(std::invalid_argument("Module does not exist.")); + + Utility::RemoveDirRecursive(path); + Application::RequestRestart(); +} + +std::vector ConfigModuleUtility::GetModules(void) +{ + std::vector modules; + Utility::Glob(GetModuleDir() + "/*", boost::bind(&ConfigModuleUtility::CollectDirNames, _1, boost::ref(modules)), GlobDirectory); + return modules; +} + +void ConfigModuleUtility::CollectDirNames(const String& path, std::vector& dirs) +{ + String name = Utility::BaseName(path); + dirs.push_back(name); +} + +String ConfigModuleUtility::CreateStage(const String& moduleName, const Dictionary::Ptr& files) +{ + String stageName = Utility::NewUniqueID(); + + String path = GetModuleDir() + "/" + moduleName; + + if (!Utility::PathExists(path)) + BOOST_THROW_EXCEPTION(std::invalid_argument("Module does not exist.")); + + path += "/" + stageName; + + Utility::MkDirP(path, 0700); + WriteStageConfig(moduleName, stageName); + + ObjectLock olock(files); + BOOST_FOREACH(const Dictionary::Pair& kv, files) { + if (ContainsDotDot(kv.first)) + BOOST_THROW_EXCEPTION(std::invalid_argument("Path must not contain '..'.")); + + String filePath = path + "/" + kv.first; + + Log(LogInformation, "ConfigModuleUtility") + << "Updating configuration file: " << filePath; + + //pass the directory and generate a dir tree, if not existing already + 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(); + } + + return stageName; +} + +void ConfigModuleUtility::WriteModuleConfig(const String& moduleName) +{ + String stageName = GetActiveStage(moduleName); + + String includePath = GetModuleDir() + "/" + moduleName + "/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 = GetModuleDir() + "/" + moduleName + "/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] == \"" << moduleName << "\") {\n" + << " if (arr.len() < 2) {\n" + << " log(LogCritical, \"Config\", \"Invalid value for ActiveStageOverride\")\n" + << " } else {\n" + << " ActiveStages[\"" << moduleName << "\"] = arr[1]\n" + << " }\n" + << " }\n" + << "}\n" + << "\n" + << "if (!ActiveStages.contains(\"" << moduleName << "\")) {\n" + << " ActiveStages[\"" << moduleName << "\"] = \"" << stageName << "\"\n" + << "}\n"; + fpActive.close(); +} + +void ConfigModuleUtility::WriteStageConfig(const String& moduleName, const String& stageName) +{ + String path = GetModuleDir() + "/" + moduleName + "/" + stageName + "/include.conf"; + std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); + fp << "include \"../active.conf\"\n" + << "if (ActiveStages[\"" << moduleName << "\"] == \"" << stageName << "\") {\n" + << " include_recursive \"conf.d\"\n" + << " include_zones \"" << moduleName << "\", \"zones.d\"\n" + << "}\n"; + fp.close(); +} + +void ConfigModuleUtility::ActivateStage(const String& moduleName, const String& stageName) +{ + String activeStagePath = GetModuleDir() + "/" + moduleName + "/active-stage"; + std::ofstream fpActiveStage(activeStagePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); + fpActiveStage << stageName; + fpActiveStage.close(); + + WriteModuleConfig(moduleName); +} + +void ConfigModuleUtility::TryActivateStageCallback(const ProcessResult& pr, const String& moduleName, const String& stageName) +{ + String logFile = GetModuleDir() + "/" + moduleName + "/" + stageName + "/startup.log"; + std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); + fpLog << pr.Output; + fpLog.close(); + + String statusFile = GetModuleDir() + "/" + moduleName + "/" + 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) { + ActivateStage(moduleName, stageName); + Application::RequestRestart(); + } else { + Log(LogCritical, "ConfigModuleUtility") + << "Config validation failed for module '" + << moduleName << "' and stage '" << stageName << "'."; + } +} + +void ConfigModuleUtility::AsyncTryActivateStage(const String& moduleName, const String& stageName) +{ + // prepare arguments + Array::Ptr args = new Array(); + args->Add(Application::GetExePath("icinga2")); + args->Add("daemon"); + args->Add("--validate"); + args->Add("--define"); + args->Add("ActiveStageOverride=" + moduleName + ":" + stageName); + + Process::Ptr process = new Process(Process::PrepareCommand(args)); + process->SetTimeout(300); + process->Run(boost::bind(&TryActivateStageCallback, _1, moduleName, stageName)); +} + +void ConfigModuleUtility::DeleteStage(const String& moduleName, const String& stageName) +{ + String path = GetModuleDir() + "/" + moduleName + "/" + stageName; + + if (!Utility::PathExists(path)) + BOOST_THROW_EXCEPTION(std::invalid_argument("Stage does not exist.")); + + if (GetActiveStage(moduleName) == stageName) + BOOST_THROW_EXCEPTION(std::invalid_argument("Active stage cannot be deleted.")); + + Utility::RemoveDirRecursive(path); +} + +std::vector ConfigModuleUtility::GetStages(const String& moduleName) +{ + std::vector stages; + Utility::Glob(GetModuleDir() + "/" + moduleName + "/*", boost::bind(&ConfigModuleUtility::CollectDirNames, _1, boost::ref(stages)), GlobDirectory); + return stages; +} + +String ConfigModuleUtility::GetActiveStage(const String& moduleName) +{ + String path = GetModuleDir() + "/" + moduleName + "/active-stage"; + + std::ifstream fp; + fp.open(path.CStr()); + + String stage; + std::getline(fp, stage.GetData()); + stage.Trim(); + + fp.close(); + + if (fp.fail()) + return ""; + + return stage; +} + + +std::vector > ConfigModuleUtility::GetFiles(const String& moduleName, const String& stageName) +{ + std::vector > paths; + Utility::GlobRecursive(GetModuleDir() + "/" + moduleName + "/" + stageName, "*", boost::bind(&ConfigModuleUtility::CollectPaths, _1, boost::ref(paths)), GlobDirectory | GlobFile); + + return paths; +} + +void ConfigModuleUtility::CollectPaths(const String& path, std::vector >& 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)); +#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)); +#endif /* _WIN32 */ + + paths.push_back(std::make_pair(path, S_ISDIR(statbuf.st_mode))); +} + +bool ConfigModuleUtility::ContainsDotDot(const String& path) +{ + std::vector tokens; + boost::algorithm::split(tokens, path, boost::is_any_of("/\\")); + + BOOST_FOREACH(const String& part, tokens) { + if (part == "..") + return true; + } + + return false; +} + +bool ConfigModuleUtility::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)); +} + + diff --git a/lib/remote/configmoduleutility.hpp b/lib/remote/configmoduleutility.hpp new file mode 100644 index 000000000..99f446894 --- /dev/null +++ b/lib/remote/configmoduleutility.hpp @@ -0,0 +1,72 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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. * + ******************************************************************************/ + +#ifndef CONFIGMODULEUTILITY_H +#define CONFIGMODULEUTILITY_H + +#include "remote/i2-remote.hpp" +#include "base/application.hpp" +#include "base/dictionary.hpp" +#include "base/process.hpp" +#include "base/string.hpp" +#include + +namespace icinga +{ + +/** + * Helper functions. + * + * @ingroup remote + */ +class I2_REMOTE_API ConfigModuleUtility +{ + +public: + static String GetModuleDir(void); + + static void CreateModule(const String& name); + static void DeleteModule(const String& name); + static std::vector GetModules(void); + + static String CreateStage(const String& moduleName, const Dictionary::Ptr& files); + static void DeleteStage(const String& moduleName, const String& stageName); + static std::vector GetStages(const String& moduleName); + static String GetActiveStage(const String& moduleName); + static void ActivateStage(const String& moduleName, const String& stageName); + static void AsyncTryActivateStage(const String& moduleName, const String& stageName); + + static std::vector > GetFiles(const String& moduleName, const String& stageName); + + static bool ContainsDotDot(const String& path); + static bool ValidateName(const String& name); + +private: + static void CollectDirNames(const String& path, std::vector& dirs); + static void CollectPaths(const String& path, std::vector >& paths); + + static void WriteModuleConfig(const String& moduleName); + static void WriteStageConfig(const String& moduleName, const String& stageName); + + static void TryActivateStageCallback(const ProcessResult& pr, const String& moduleName, const String& stageName); +}; + +} + +#endif /* CONFIGMODULEUTILITY_H */ diff --git a/lib/remote/configstageshandler.cpp b/lib/remote/configstageshandler.cpp new file mode 100644 index 000000000..24b12ed69 --- /dev/null +++ b/lib/remote/configstageshandler.cpp @@ -0,0 +1,184 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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 "remote/configstageshandler.hpp" +#include "remote/configmoduleutility.hpp" +#include "remote/httputility.hpp" +#include "base/application.hpp" +#include "base/exception.hpp" +#include + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/config/stages", ConfigStagesHandler); + +void ConfigStagesHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + if (request.RequestMethod == "GET") + HandleGet(user, request, response); + else if (request.RequestMethod == "POST") + HandlePost(user, request, response); + else if (request.RequestMethod == "DELETE") + HandleDelete(user, request, response); + else + response.SetStatus(400, "Bad request"); +} + +bool ConfigStagesHandler::CanAlsoHandleUrl(const Url::Ptr& url) const +{ + if (url->GetPath().size() > 5) + return false; + else + return true; +} + +void ConfigStagesHandler::HandleGet(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + Dictionary::Ptr params = HttpUtility::FetchRequestParameters(request); + + if (request.RequestUrl->GetPath().size() >= 4) + params->Set("module", request.RequestUrl->GetPath()[3]); + + if (request.RequestUrl->GetPath().size() >= 5) + params->Set("stage", request.RequestUrl->GetPath()[4]); + + String moduleName = params->Get("module"); + String stageName = params->Get("stage"); + + if (!ConfigModuleUtility::ValidateName(moduleName) || !ConfigModuleUtility::ValidateName(stageName)) { + response.SetStatus(403, "Forbidden"); + return; + } + + Array::Ptr results = new Array(); + + std::vector > paths = ConfigModuleUtility::GetFiles(moduleName, stageName); + + String prefixPath = ConfigModuleUtility::GetModuleDir() + "/" + moduleName + "/" + stageName + "/"; + + typedef std::pair kv_pair; + BOOST_FOREACH(const kv_pair& kv, paths) { + Dictionary::Ptr stageInfo = new Dictionary(); + stageInfo->Set("type", (kv.second ? "directory" : "file")); + stageInfo->Set("name", kv.first.SubStr(prefixPath.GetLength())); + results->Add(stageInfo); + } + + Dictionary::Ptr result = new Dictionary(); + result->Set("results", results); + + response.SetStatus(200, "OK"); + HttpUtility::SendJsonBody(response, result); +} + +void ConfigStagesHandler::HandlePost(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + Dictionary::Ptr params = HttpUtility::FetchRequestParameters(request); + + if (request.RequestUrl->GetPath().size() >= 4) + params->Set("module", request.RequestUrl->GetPath()[3]); + + String moduleName = params->Get("module"); + + if (!ConfigModuleUtility::ValidateName(moduleName)) { + response.SetStatus(403, "Forbidden"); + return; + } + + Dictionary::Ptr files = params->Get("files"); + + int code = 200; + String status = "Created stage."; + String stageName; + + try { + if (!files) + BOOST_THROW_EXCEPTION(std::invalid_argument("Parameter 'files' must be specified.")); + + stageName = ConfigModuleUtility::CreateStage(moduleName, files); + + /* validate the config. on success, activate stage and reload */ + ConfigModuleUtility::AsyncTryActivateStage(moduleName, stageName); + } catch (const std::exception& ex) { + code = 501; + status = "Error: " + DiagnosticInformation(ex); + } + + Dictionary::Ptr result1 = new Dictionary(); + + result1->Set("module", moduleName); + result1->Set("stage", stageName); + result1->Set("code", code); + result1->Set("status", status); + + Array::Ptr results = new Array(); + results->Add(result1); + + Dictionary::Ptr result = new Dictionary(); + result->Set("results", results); + + response.SetStatus(code, (code == 200) ? "OK" : "Error"); + HttpUtility::SendJsonBody(response, result); +} + +void ConfigStagesHandler::HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + Dictionary::Ptr params = HttpUtility::FetchRequestParameters(request); + + if (request.RequestUrl->GetPath().size() >= 4) + params->Set("module", request.RequestUrl->GetPath()[3]); + + if (request.RequestUrl->GetPath().size() >= 5) + params->Set("stage", request.RequestUrl->GetPath()[4]); + + String moduleName = params->Get("module"); + String stageName = params->Get("stage"); + + if (!ConfigModuleUtility::ValidateName(moduleName) || !ConfigModuleUtility::ValidateName(stageName)) { + response.SetStatus(403, "Forbidden"); + return; + } + + int code = 200; + String status = "Deleted stage."; + + try { + ConfigModuleUtility::DeleteStage(moduleName, stageName); + } catch (const std::exception& ex) { + code = 501; + status = "Error: " + DiagnosticInformation(ex); + } + + Dictionary::Ptr result1 = new Dictionary(); + + result1->Set("module", moduleName); + result1->Set("stage", stageName); + result1->Set("code", code); + result1->Set("status", status); + + Array::Ptr results = new Array(); + results->Add(result1); + + Dictionary::Ptr result = new Dictionary(); + result->Set("results", results); + + response.SetStatus(code, (code == 200) ? "OK" : "Error"); + HttpUtility::SendJsonBody(response, result); +} + diff --git a/lib/remote/configstageshandler.hpp b/lib/remote/configstageshandler.hpp new file mode 100644 index 000000000..ccdb3b7b8 --- /dev/null +++ b/lib/remote/configstageshandler.hpp @@ -0,0 +1,45 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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. * + ******************************************************************************/ + +#ifndef CONFIGSTAGESHANDLER_H +#define CONFIGSTAGESHANDLER_H + +#include "remote/httphandler.hpp" + +namespace icinga +{ + +class I2_REMOTE_API ConfigStagesHandler : public HttpHandler +{ +public: + DECLARE_PTR_TYPEDEFS(ConfigStagesHandler); + + virtual bool CanAlsoHandleUrl(const Url::Ptr& url) const; + virtual void HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); + +private: + void HandleGet(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); + void HandlePost(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); + void HandleDelete(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response); + +}; + +} + +#endif /* CONFIGSTAGESHANDLER_H */ diff --git a/lib/remote/httpconnection.cpp b/lib/remote/httpconnection.cpp index 6aee8a19c..b1056c3e1 100644 --- a/lib/remote/httpconnection.cpp +++ b/lib/remote/httpconnection.cpp @@ -154,7 +154,14 @@ void HttpConnection::ProcessMessageAsync(HttpRequest& request) String msg = "

Unauthorized

"; response.WriteBody(msg.CStr(), msg.GetLength()); } else { - HttpHandler::ProcessRequest(user, request, response); + try { + HttpHandler::ProcessRequest(user, request, response); + } catch (const std::exception& ex) { + response.SetStatus(503, "Unhandled exception"); + response.AddHeader("Content-Type", "text/plain"); + String errorInfo = DiagnosticInformation(ex); + response.WriteBody(errorInfo.CStr(), errorInfo.GetLength()); + } } response.Finish(); diff --git a/lib/remote/httputility.cpp b/lib/remote/httputility.cpp new file mode 100644 index 000000000..86ddef21f --- /dev/null +++ b/lib/remote/httputility.cpp @@ -0,0 +1,59 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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 "remote/httputility.hpp" +#include "base/json.hpp" +#include + +using namespace icinga; + +Dictionary::Ptr HttpUtility::FetchRequestParameters(HttpRequest& request) +{ + Dictionary::Ptr result; + + String body; + char buffer[1024]; + size_t count; + + while ((count = request.ReadBody(buffer, sizeof(buffer))) > 0) + body += String(buffer, buffer + count); + + if (!body.IsEmpty()) + result = JsonDecode(body); + + if (!result) + result = new Dictionary(); + + typedef std::pair kv_pair; + BOOST_FOREACH(const kv_pair& kv, request.RequestUrl->GetQuery()) { + result->Set(kv.first, kv.second); + } + + return result; +} + +void HttpUtility::SendJsonBody(HttpResponse& response, const Value& val) +{ + response.AddHeader("Content-Type", "application/json"); + + String body = JsonEncode(val); + response.WriteBody(body.CStr(), body.GetLength()); +} + + diff --git a/lib/remote/httputility.hpp b/lib/remote/httputility.hpp new file mode 100644 index 000000000..48f27368a --- /dev/null +++ b/lib/remote/httputility.hpp @@ -0,0 +1,45 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2015 Icinga Development Team (http://www.icinga.org) * + * * + * 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. * + ******************************************************************************/ + +#ifndef HTTPUTILITY_H +#define HTTPUTILITY_H + +#include "remote/httprequest.hpp" +#include "remote/httpresponse.hpp" +#include "base/dictionary.hpp" + +namespace icinga +{ + +/** + * Helper functions. + * + * @ingroup remote + */ +class I2_REMOTE_API HttpUtility +{ + +public: + static Dictionary::Ptr FetchRequestParameters(HttpRequest& request); + static void SendJsonBody(HttpResponse& response, const Value& val); +}; + +} + +#endif /* HTTPUTILITY_H */ -- 2.50.1