diff --git a/icinga-studio/CMakeLists.txt b/icinga-studio/CMakeLists.txt index d05076de6..33f466e33 100644 --- a/icinga-studio/CMakeLists.txt +++ b/icinga-studio/CMakeLists.txt @@ -27,7 +27,7 @@ endif() add_executable(icinga-studio MACOSX_BUNDLE WIN32 icinga-studio.cpp forms.cpp aboutform.cpp connectform.cpp mainform.cpp - icinga.icns apiclient.cpp ${WindowsSources}) + icinga.icns ${WindowsSources}) include_directories(${Boost_INCLUDE_DIRS}) target_link_libraries(icinga-studio ${Boost_LIBRARIES} ${wxWidgets_LIBRARIES} base remote) diff --git a/icinga-studio/mainform.cpp b/icinga-studio/mainform.cpp index 5362e2423..0044402dd 100644 --- a/icinga-studio/mainform.cpp +++ b/icinga-studio/mainform.cpp @@ -142,7 +142,6 @@ void MainForm::ObjectsCompletionHandler(boost::exception_ptr eptr, const std::ve m_PropertyGrid->Clear(); if (eptr) { - try { boost::rethrow_exception(eptr); } catch (const std::exception& ex) { diff --git a/icinga-studio/mainform.hpp b/icinga-studio/mainform.hpp index 2e244a71f..af7cd1873 100644 --- a/icinga-studio/mainform.hpp +++ b/icinga-studio/mainform.hpp @@ -20,7 +20,7 @@ #ifndef MAINFORM_H #define MAINFORM_H -#include "icinga-studio/apiclient.hpp" +#include "remote/apiclient.hpp" #include "remote/url.hpp" #include "base/exception.hpp" #include "icinga-studio/forms.h" diff --git a/lib/base/serializer.cpp b/lib/base/serializer.cpp index 4f95ffb72..bfcb90a5d 100644 --- a/lib/base/serializer.cpp +++ b/lib/base/serializer.cpp @@ -63,7 +63,7 @@ static Object::Ptr SerializeObject(const Object::Ptr& input, int attributeTypes) for (int i = 0; i < type->GetFieldCount(); i++) { Field field = type->GetFieldInfo(i); - if ((field.Attributes & attributeTypes) == 0) + if (attributeTypes != 0 && (field.Attributes & attributeTypes) == 0) continue; fields->Set(field.Name, Serialize(input->GetField(i), attributeTypes)); diff --git a/lib/cli/consolecommand.cpp b/lib/cli/consolecommand.cpp index 801c5f8ee..6fb0bd25c 100644 --- a/lib/cli/consolecommand.cpp +++ b/lib/cli/consolecommand.cpp @@ -19,7 +19,11 @@ #include "cli/consolecommand.hpp" #include "config/configcompiler.hpp" +#include "remote/apiclient.hpp" +#include "remote/consolehandler.hpp" +#include "remote/url.hpp" #include "base/configwriter.hpp" +#include "base/serializer.hpp" #include "base/json.hpp" #include "base/console.hpp" #include "base/application.hpp" @@ -37,6 +41,8 @@ using namespace icinga; namespace po = boost::program_options; static ScriptFrame *l_ScriptFrame; +static ApiClient::Ptr l_ApiClient; +static String l_Session; REGISTER_CLICOMMAND("console", ConsoleCommand); @@ -60,89 +66,41 @@ void ConsoleCommand::InitParameters(boost::program_options::options_description& { visibleDesc.add_options() ("connect,c", po::value(), "connect to an Icinga 2 instance") + ("eval,e", po::value(), "evaluate expression and terminate") ("sandbox", "enable sandbox mode") ; } #ifdef HAVE_EDITLINE -static void AddSuggestion(std::vector& matches, const String& word, const String& suggestion) -{ - if (suggestion.Find(word) != 0) - return; - - matches.push_back(suggestion); -} - -static char *ConsoleCompleteHelper(const char *word, int state) +char *ConsoleCommand::ConsoleCompleteHelper(const char *word, int state) { static std::vector matches; - String aword = word; if (state == 0) { - matches.clear(); + if (!l_ApiClient) + matches = ConsoleHandler::GetAutocompletionSuggestions(word, *l_ScriptFrame); + else { + boost::mutex mutex; + boost::condition_variable cv; + bool ready = false; + Array::Ptr suggestions; - BOOST_FOREACH(const String& keyword, ConfigWriter::GetKeywords()) { - AddSuggestion(matches, word, keyword); - } + l_ApiClient->AutocompleteScript(l_Session, word, l_ScriptFrame->Sandboxed, + boost::bind(&ConsoleCommand::AutocompleteScriptCompletionHandler, + boost::ref(mutex), boost::ref(cv), boost::ref(ready), + _1, _2, + boost::ref(suggestions))); - { - ObjectLock olock(l_ScriptFrame->Locals); - BOOST_FOREACH(const Dictionary::Pair& kv, l_ScriptFrame->Locals) { - AddSuggestion(matches, word, kv.first); + { + boost::mutex::scoped_lock lock(mutex); + while (!ready) + cv.wait(lock); } - } - { - ObjectLock olock(ScriptGlobal::GetGlobals()); - BOOST_FOREACH(const Dictionary::Pair& kv, ScriptGlobal::GetGlobals()) { - AddSuggestion(matches, word, kv.first); - } - } + matches.clear(); - String::SizeType cperiod = aword.RFind("."); - - if (cperiod != -1) { - String pword = aword.SubStr(0, cperiod); - - Value value; - - try { - Expression *expr = ConfigCompiler::CompileText("temp", pword); - - if (expr) - value = expr->Evaluate(*l_ScriptFrame); - - if (value.IsObjectType()) { - Dictionary::Ptr dict = value; - - ObjectLock olock(dict); - BOOST_FOREACH(const Dictionary::Pair& kv, dict) { - AddSuggestion(matches, word, pword + "." + kv.first); - } - } - - Type::Ptr type = value.GetReflectionType(); - - for (int i = 0; i < type->GetFieldCount(); i++) { - Field field = type->GetFieldInfo(i); - - AddSuggestion(matches, word, pword + "." + field.Name); - } - - while (type) { - Object::Ptr prototype = type->GetPrototype(); - Dictionary::Ptr dict = dynamic_pointer_cast(prototype); - - if (dict) { - ObjectLock olock(dict); - BOOST_FOREACH(const Dictionary::Pair& kv, dict) { - AddSuggestion(matches, word, pword + "." + kv.first); - } - } - - type = type->GetBaseType(); - } - } catch (...) { /* Ignore the exception */ } + ObjectLock olock(suggestions); + std::copy(suggestions->Begin(), suggestions->End(), std::back_inserter(matches)); } } @@ -164,30 +122,53 @@ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector(); - session = Utility::NewUniqueID(); } - if (vm.count("sandbox")) { - if (vm.count("connect")) { - Log(LogCritical, "ConsoleCommand", "Sandbox mode cannot be used together with --connect."); + if (!addr.IsEmpty()) { + Url::Ptr url; + + try { + url = new Url(addr); + } catch (const std::exception& ex) { + Log(LogCritical, "ConsoleCommand", ex.what()); return EXIT_FAILURE; } - scriptFrame.Sandboxed = true; - } + const char *usernameEnv = getenv("ICINGA2_API_USERNAME"); + const char *passwordEnv = getenv("ICINGA2_API_PASSWORD"); - std::cout << "Icinga (version: " << Application::GetAppVersion() << ")\n"; + if (usernameEnv) + url->SetUsername(usernameEnv); + if (passwordEnv) + url->SetPassword(passwordEnv); + + if (url->GetPort().IsEmpty()) + url->SetPort("5665"); + + l_ApiClient = new ApiClient(url->GetHost(), url->GetPort(), url->GetUsername(), url->GetPassword()); + } while (std::cin.good()) { String fileName = "<" + Convert::ToString(next_line) + ">"; @@ -197,148 +178,191 @@ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector "; - else - os << " .. "; - -#ifdef HAVE_EDITLINE - String prompt = promptbuf.str(); - - char *cline; - cline = readline(prompt.CStr()); - - if (!cline) - break; - - add_history(cline); - - std::string line = cline; - - free(cline); -#else /* HAVE_EDITLINE */ std::string line; - std::getline(std::cin, line); + + if (!vm.count("eval")) { +#ifdef HAVE_EDITLINE + std::ostringstream promptbuf; + std::ostream& os = promptbuf; +#else /* HAVE_EDITLINE */ + std::ostream& os = std::cout; #endif /* HAVE_EDITLINE */ + os << fileName; + + if (!continuation) + os << " => "; + else + os << " .. "; + +#ifdef HAVE_EDITLINE + String prompt = promptbuf.str(); + + char *cline; + cline = readline(prompt.CStr()); + + if (!cline) + break; + + add_history(cline); + + line = cline; + + free(cline); +#else /* HAVE_EDITLINE */ + std::getline(std::cin, line); +#endif /* HAVE_EDITLINE */ + } else + line = vm["eval"].as(); + if (!command.empty()) command += "\n"; command += line; - if (addr.IsEmpty()) { - Expression *expr = NULL; + Expression *expr = NULL; - try { - lines[fileName] = command; + try { + lines[fileName] = command; + Value result; + + if (!l_ApiClient) { expr = ConfigCompiler::CompileText(fileName, command); - - if (expr) { - Value result = expr->Evaluate(scriptFrame); - std::cout << ConsoleColorTag(Console_ForegroundCyan); - if (!result.IsObject() || result.IsObjectType() || result.IsObjectType()) - std::cout << JsonEncode(result); - else - std::cout << result; - std::cout << ConsoleColorTag(Console_Normal) << "\n"; - } - } catch (const ScriptError& ex) { - if (ex.IsIncompleteExpression()) { - continuation = true; - goto incomplete; - } - - DebugInfo di = ex.GetDebugInfo(); - - if (lines.find(di.Path) != lines.end()) { - String text = lines[di.Path]; - - std::vector ulines; - boost::algorithm::split(ulines, text, boost::is_any_of("\n")); - - for (int i = 1; i <= ulines.size(); i++) { - int start, len; - - if (i == di.FirstLine) - start = di.FirstColumn; - else - start = 0; - - if (i == di.LastLine) - len = di.LastColumn - di.FirstColumn + 1; - else - len = ulines[i - 1].GetLength(); - - int offset; - - if (di.Path != fileName) { - std::cout << di.Path << ": " << ulines[i - 1] << "\n"; - offset = 2; - } else - offset = 4; - - if (i >= di.FirstLine && i <= di.LastLine) { - std::cout << String(di.Path.GetLength() + offset, ' '); - std::cout << String(start, ' ') << String(len, '^') << "\n"; - } - } - } else { - ShowCodeFragment(std::cout, di); - } - - std::cout << ex.what() << "\n"; - } catch (const std::exception& ex) { - std::cout << "Error: " << DiagnosticInformation(ex) << "\n"; - } - - delete expr; - } else { - Socket::Ptr socket; - -#ifndef _WIN32 - if (addr.FindFirstOf("/") != String::NPos) { - UnixSocket::Ptr usocket = new UnixSocket(); - usocket->Connect(addr); - socket = usocket; + result = Serialize(expr->Evaluate(scriptFrame), 0); } else { -#endif /* _WIN32 */ - Log(LogCritical, "ConsoleCommand", "Sorry, TCP sockets aren't supported yet."); - return 1; -#ifndef _WIN32 - } -#endif /* _WIN32 */ + boost::mutex mutex; + boost::condition_variable cv; + bool ready = false; + boost::exception_ptr eptr; - String query = "SCRIPT " + session + "\n" + line + "\n\n"; + l_ApiClient->ExecuteScript(l_Session, command, scriptFrame.Sandboxed, + boost::bind(&ConsoleCommand::ExecuteScriptCompletionHandler, + boost::ref(mutex), boost::ref(cv), boost::ref(ready), + _1, _2, + boost::ref(result), boost::ref(eptr))); - NetworkStream::Ptr ns = new NetworkStream(socket); - ns->Write(query.CStr(), query.GetLength()); + { + boost::mutex::scoped_lock lock(mutex); + while (!ready) + cv.wait(lock); + } - String result; - char buf[1024]; - - while (!ns->IsEof()) { - size_t rc = ns->Read(buf, sizeof(buf), true); - result += String(buf, buf + rc); + if (eptr) + boost::rethrow_exception(eptr); } - if (result.GetLength() < 16) { - Log(LogCritical, "ConsoleCommand", "Received invalid response from Livestatus."); - continue; + if (!vm.count("eval")) { + std::cout << ConsoleColorTag(Console_ForegroundCyan); + ConfigWriter::EmitValue(std::cout, 1, result); + std::cout << ConsoleColorTag(Console_Normal) << "\n"; + } else { + std::cout << JsonEncode(result) << "\n"; + break; + } + } catch (const ScriptError& ex) { + if (ex.IsIncompleteExpression()) { + continuation = true; + goto incomplete; } - std::cout << result.SubStr(16) << "\n"; + DebugInfo di = ex.GetDebugInfo(); + + if (lines.find(di.Path) != lines.end()) { + String text = lines[di.Path]; + + std::vector ulines; + boost::algorithm::split(ulines, text, boost::is_any_of("\n")); + + for (int i = 1; i <= ulines.size(); i++) { + int start, len; + + if (i == di.FirstLine) + start = di.FirstColumn; + else + start = 0; + + if (i == di.LastLine) + len = di.LastColumn - di.FirstColumn + 1; + else + len = ulines[i - 1].GetLength(); + + int offset; + + if (di.Path != fileName) { + std::cout << di.Path << ": " << ulines[i - 1] << "\n"; + offset = 2; + } else + offset = 4; + + if (i >= di.FirstLine && i <= di.LastLine) { + std::cout << String(di.Path.GetLength() + offset, ' '); + std::cout << String(start, ' ') << String(len, '^') << "\n"; + } + } + } else { + ShowCodeFragment(std::cout, di); + } + + std::cout << ex.what() << "\n"; + + if (vm.count("eval")) + return EXIT_FAILURE; + } catch (const std::exception& ex) { + std::cout << "Error: " << DiagnosticInformation(ex) << "\n"; + + if (vm.count("eval")) + return EXIT_FAILURE; + } + + delete expr; + } + + return EXIT_SUCCESS; +} + +void ConsoleCommand::ExecuteScriptCompletionHandler(boost::mutex& mutex, boost::condition_variable& cv, + bool& ready, boost::exception_ptr eptr, const Value& result, Value& resultOut, boost::exception_ptr& eptrOut) +{ + if (eptr) { + try { + boost::rethrow_exception(eptr); + } catch (const ScriptError& ex) { + eptrOut = boost::current_exception(); + } catch (const std::exception& ex) { + Log(LogCritical, "ConsoleCommand") + << "HTTP query failed: " << ex.what(); + Application::Exit(EXIT_FAILURE); } } - return 0; + resultOut = result; + + { + boost::mutex::scoped_lock lock(mutex); + ready = true; + cv.notify_all(); + } +} + +void ConsoleCommand::AutocompleteScriptCompletionHandler(boost::mutex& mutex, boost::condition_variable& cv, + bool& ready, boost::exception_ptr eptr, const Array::Ptr& result, Array::Ptr& resultOut) +{ + if (eptr) { + try { + boost::rethrow_exception(eptr); + } catch (const std::exception& ex) { + Log(LogCritical, "ConsoleCommand") + << "HTTP query failed: " << ex.what(); + Application::Exit(EXIT_FAILURE); + } + } + + resultOut = result; + + { + boost::mutex::scoped_lock lock(mutex); + ready = true; + cv.notify_all(); + } } diff --git a/lib/cli/consolecommand.hpp b/lib/cli/consolecommand.hpp index b5795b964..f144e9a04 100644 --- a/lib/cli/consolecommand.hpp +++ b/lib/cli/consolecommand.hpp @@ -21,6 +21,7 @@ #define CONSOLECOMMAND_H #include "cli/clicommand.hpp" +#include "base/exception.hpp" namespace icinga { @@ -41,6 +42,22 @@ public: virtual void InitParameters(boost::program_options::options_description& visibleDesc, boost::program_options::options_description& hiddenDesc) const override; virtual int Run(const boost::program_options::variables_map& vm, const std::vector& ap) const override; + +private: + mutable boost::mutex m_Mutex; + mutable boost::condition_variable m_CV; + mutable bool m_CommandReady; + + static void ExecuteScriptCompletionHandler(boost::mutex& mutex, boost::condition_variable& cv, + bool& ready, boost::exception_ptr eptr, const Value& result, Value& resultOut, + boost::exception_ptr& eptrOut); + static void AutocompleteScriptCompletionHandler(boost::mutex& mutex, boost::condition_variable& cv, + bool& ready, boost::exception_ptr eptr, const Array::Ptr& result, Array::Ptr& resultOut); + +#ifdef HAVE_EDITLINE + static char *ConsoleCompleteHelper(const char *word, int state); +#endif /* HAVE_EDITLINE */ + }; } diff --git a/lib/livestatus/livestatusquery.cpp b/lib/livestatus/livestatusquery.cpp index c216da6bc..0e048d715 100644 --- a/lib/livestatus/livestatusquery.cpp +++ b/lib/livestatus/livestatusquery.cpp @@ -31,7 +31,6 @@ #include "livestatus/orfilter.hpp" #include "livestatus/andfilter.hpp" #include "icinga/externalcommandprocessor.hpp" -#include "config/configcompiler.hpp" #include "base/debug.hpp" #include "base/convert.hpp" #include "base/objectlock.hpp" @@ -52,36 +51,6 @@ using namespace icinga; static int l_ExternalCommands = 0; static boost::mutex l_QueryMutex; -static std::map l_LivestatusScriptFrames; -static Timer::Ptr l_FrameCleanupTimer; -static boost::mutex l_LivestatusScriptMutex; - -static void ScriptFrameCleanupHandler(void) -{ - boost::mutex::scoped_lock lock(l_LivestatusScriptMutex); - - std::vector cleanup_keys; - - typedef std::pair KVPair; - - BOOST_FOREACH(const KVPair& kv, l_LivestatusScriptFrames) { - if (kv.second.Seen < Utility::GetTime() - 1800) - cleanup_keys.push_back(kv.first); - } - - BOOST_FOREACH(const String& key, cleanup_keys) - l_LivestatusScriptFrames.erase(key); -} - -static void InitScriptFrameCleanup(void) -{ - l_FrameCleanupTimer = new Timer(); - l_FrameCleanupTimer->OnTimerExpired.connect(boost::bind(ScriptFrameCleanupHandler)); - l_FrameCleanupTimer->SetInterval(30); - l_FrameCleanupTimer->Start(); -} - -INITIALIZE_ONCE(InitScriptFrameCleanup); LivestatusQuery::LivestatusQuery(const std::vector& lines, const String& compat_log_path) : m_KeepAlive(false), m_OutputFormat("csv"), m_ColumnHeaders(true), m_Limit(-1), m_ErrorCode(0), @@ -123,16 +92,6 @@ LivestatusQuery::LivestatusQuery(const std::vector& lines, const String& if (m_Verb == "COMMAND") { m_KeepAlive = true; m_Command = target; - } else if (m_Verb == "SCRIPT") { - m_Session = target; - - for (unsigned int i = 1; i < lines.size(); i++) { - if (m_Command != "") - m_Command += "\n"; - m_Command += lines[i]; - } - - return; } else if (m_Verb == "GET") { m_Table = target; } else { @@ -625,54 +584,6 @@ void LivestatusQuery::ExecuteCommandHelper(const Stream::Ptr& stream) SendResponse(stream, LivestatusErrorOK, ""); } -void LivestatusQuery::ExecuteScriptHelper(const Stream::Ptr& stream) -{ - Log(LogInformation, "LivestatusQuery") - << "Executing expression: " << m_Command; - - m_ResponseHeader = "fixed16"; - - LivestatusScriptFrame& lsf = l_LivestatusScriptFrames[m_Session]; - lsf.Seen = Utility::GetTime(); - - if (!lsf.Locals) - lsf.Locals = new Dictionary(); - - String fileName = "<" + Convert::ToString(lsf.NextLine) + ">"; - lsf.NextLine++; - - lsf.Lines[fileName] = m_Command; - - Expression *expr = NULL; - Value result; - try { - expr = ConfigCompiler::CompileText(fileName, m_Command); - ScriptFrame frame; - frame.Locals = lsf.Locals; - frame.Self = lsf.Locals; - result = expr->Evaluate(frame); - } catch (const ScriptError& ex) { - delete expr; - - DebugInfo di = ex.GetDebugInfo(); - - std::ostringstream msgbuf; - - msgbuf << di.Path << ": " << lsf.Lines[di.Path] << "\n" - << String(di.Path.GetLength() + 2, ' ') - << String(di.FirstColumn, ' ') << String(di.LastColumn - di.FirstColumn + 1, '^') << "\n" - << ex.what() << "\n"; - - SendResponse(stream, LivestatusErrorQuery, msgbuf.str()); - return; - } catch (...) { - delete expr; - throw; - } - delete expr; - SendResponse(stream, LivestatusErrorOK, JsonEncode(Serialize(result, FAEphemeral | FAState | FAConfig), true)); -} - void LivestatusQuery::ExecuteErrorHelper(const Stream::Ptr& stream) { Log(LogDebug, "LivestatusQuery") @@ -720,8 +631,6 @@ bool LivestatusQuery::Execute(const Stream::Ptr& stream) ExecuteGetHelper(stream); else if (m_Verb == "COMMAND") ExecuteCommandHelper(stream); - else if (m_Verb == "SCRIPT") - ExecuteScriptHelper(stream); else if (m_Verb == "ERROR") ExecuteErrorHelper(stream); else diff --git a/lib/livestatus/livestatusquery.hpp b/lib/livestatus/livestatusquery.hpp index 822bde239..7ad31760a 100644 --- a/lib/livestatus/livestatusquery.hpp +++ b/lib/livestatus/livestatusquery.hpp @@ -40,18 +40,6 @@ enum LivestatusError LivestatusErrorQuery = 452 }; -struct LivestatusScriptFrame -{ - double Seen; - int NextLine; - std::map Lines; - Dictionary::Ptr Locals; - - LivestatusScriptFrame(void) - : Seen(0), NextLine(1) - { } -}; - /** * @ingroup livestatus */ @@ -92,7 +80,7 @@ private: /* Parameters for invalid queries. */ int m_ErrorCode; String m_ErrorMessage; - + unsigned long m_LogTimeFrom; unsigned long m_LogTimeUntil; String m_CompatLogPath; @@ -106,12 +94,11 @@ private: void ExecuteGetHelper(const Stream::Ptr& stream); void ExecuteCommandHelper(const Stream::Ptr& stream); - void ExecuteScriptHelper(const Stream::Ptr& stream); void ExecuteErrorHelper(const Stream::Ptr& stream); void SendResponse(const Stream::Ptr& stream, int code, const String& data); void PrintFixed16(const Stream::Ptr& stream, int code, const String& data); - + static Filter::Ptr ParseFilter(const String& params, unsigned long& from, unsigned long& until); }; diff --git a/lib/remote/CMakeLists.txt b/lib/remote/CMakeLists.txt index 11243f782..e0328dabc 100644 --- a/lib/remote/CMakeLists.txt +++ b/lib/remote/CMakeLists.txt @@ -21,10 +21,10 @@ mkclass_target(endpoint.ti endpoint.tcpp endpoint.thpp) mkclass_target(zone.ti zone.tcpp zone.thpp) set(remote_SOURCES - actionshandler.cpp apiaction.cpp + actionshandler.cpp apiaction.cpp apiclient.cpp apifunction.cpp apilistener.cpp apilistener.thpp apilistener-configsync.cpp apilistener-filesync.cpp apiuser.cpp apiuser.thpp authority.cpp base64.cpp - configfileshandler.cpp configpackageshandler.cpp configpackageutility.cpp configobjectutility.cpp + consolehandler.cpp configfileshandler.cpp configpackageshandler.cpp configpackageutility.cpp configobjectutility.cpp configstageshandler.cpp createobjecthandler.cpp deleteobjecthandler.cpp endpoint.cpp endpoint.thpp eventshandler.cpp eventqueue.cpp filterutility.cpp httpchunkedencoding.cpp httpclientconnection.cpp httpserverconnection.cpp httphandler.cpp httprequest.cpp httpresponse.cpp diff --git a/icinga-studio/apiclient.cpp b/lib/remote/apiclient.cpp similarity index 57% rename from icinga-studio/apiclient.cpp rename to lib/remote/apiclient.cpp index 2a2321238..11dd39e97 100644 --- a/icinga-studio/apiclient.cpp +++ b/lib/remote/apiclient.cpp @@ -17,7 +17,7 @@ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * ******************************************************************************/ -#include "icinga-studio/apiclient.hpp" +#include "remote/apiclient.hpp" #include "remote/base64.hpp" #include "base/json.hpp" #include "base/logger.hpp" @@ -116,7 +116,6 @@ void ApiClient::GetObjects(const String& pluralType, const ObjectsCompletionCall path.push_back("objects"); path.push_back(pluralType); url->SetPath(path); - String qp; std::map > params; @@ -206,3 +205,159 @@ void ApiClient::ObjectsHttpCompletionCallback(HttpRequest& request, callback(boost::current_exception(), std::vector()); } } + +void ApiClient::ExecuteScript(const String& session, const String& command, bool sandboxed, + const ExecuteScriptCompletionCallback& callback) const +{ + Url::Ptr url = new Url(); + url->SetScheme("https"); + url->SetHost(m_Connection->GetHost()); + url->SetPort(m_Connection->GetPort()); + + std::vector path; + path.push_back("v1"); + path.push_back("console"); + path.push_back("execute-script"); + url->SetPath(path); + + std::map > params; + params["session"].push_back(session); + params["command"].push_back(command); + params["sandboxed"].push_back(sandboxed ? "1" : "0"); + url->SetQuery(params); + + try { + boost::shared_ptr req = m_Connection->NewRequest(); + req->RequestMethod = "POST"; + req->RequestUrl = url; + req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password)); + m_Connection->SubmitRequest(req, boost::bind(ExecuteScriptHttpCompletionCallback, _1, _2, callback)); + } catch (const std::exception& ex) { + callback(boost::current_exception(), Empty); + } +} + +void ApiClient::ExecuteScriptHttpCompletionCallback(HttpRequest& request, + HttpResponse& response, const ExecuteScriptCompletionCallback& callback) +{ + Dictionary::Ptr result; + + String body; + char buffer[1024]; + size_t count; + + while ((count = response.ReadBody(buffer, sizeof(buffer))) > 0) + body += String(buffer, buffer + count); + + try { + if (response.StatusCode < 200 || response.StatusCode > 299) { + std::string message = "HTTP request failed; Code: " + Convert::ToString(response.StatusCode) + "; Body: " + body; + + BOOST_THROW_EXCEPTION(ScriptError(message)); + } + + result = JsonDecode(body); + + Array::Ptr results = result->Get("results"); + Value result; + bool incompleteExpression = false; + String errorMessage = "Unexpected result from API."; + + if (results && results->GetLength() > 0) { + Dictionary::Ptr resultInfo = results->Get(0); + errorMessage = resultInfo->Get("status"); + + if (resultInfo->Get("code") >= 200 && resultInfo->Get("code") <= 299) { + result = resultInfo->Get("result"); + } else { + DebugInfo di; + Dictionary::Ptr debugInfo = resultInfo->Get("debug_info"); + if (debugInfo) { + di.Path = debugInfo->Get("path"); + di.FirstLine = debugInfo->Get("first_line"); + di.FirstColumn = debugInfo->Get("first_column"); + di.LastLine = debugInfo->Get("last_line"); + di.LastColumn = debugInfo->Get("last_column"); + } + bool incompleteExpression = resultInfo->Get("incomplete_expression"); + BOOST_THROW_EXCEPTION(ScriptError(errorMessage, di, incompleteExpression)); + } + } + + callback(boost::exception_ptr(), result); + } catch (const std::exception& ex) { + callback(boost::current_exception(), Empty); + } +} + +void ApiClient::AutocompleteScript(const String& session, const String& command, bool sandboxed, + const AutocompleteScriptCompletionCallback& callback) const +{ + Url::Ptr url = new Url(); + url->SetScheme("https"); + url->SetHost(m_Connection->GetHost()); + url->SetPort(m_Connection->GetPort()); + + std::vector path; + path.push_back("v1"); + path.push_back("console"); + path.push_back("auto-complete-script"); + url->SetPath(path); + + std::map > params; + params["session"].push_back(session); + params["command"].push_back(command); + params["sandboxed"].push_back(sandboxed ? "1" : "0"); + url->SetQuery(params); + + try { + boost::shared_ptr req = m_Connection->NewRequest(); + req->RequestMethod = "POST"; + req->RequestUrl = url; + req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password)); + m_Connection->SubmitRequest(req, boost::bind(AutocompleteScriptHttpCompletionCallback, _1, _2, callback)); + } catch (const std::exception& ex) { + callback(boost::current_exception(), Array::Ptr()); + } +} + +void ApiClient::AutocompleteScriptHttpCompletionCallback(HttpRequest& request, + HttpResponse& response, const AutocompleteScriptCompletionCallback& callback) +{ + Dictionary::Ptr result; + + String body; + char buffer[1024]; + size_t count; + + while ((count = response.ReadBody(buffer, sizeof(buffer))) > 0) + body += String(buffer, buffer + count); + + try { + if (response.StatusCode < 200 || response.StatusCode > 299) { + std::string message = "HTTP request failed; Code: " + Convert::ToString(response.StatusCode) + "; Body: " + body; + + BOOST_THROW_EXCEPTION(ScriptError(message)); + } + + result = JsonDecode(body); + + Array::Ptr results = result->Get("results"); + Array::Ptr suggestions; + String errorMessage = "Unexpected result from API."; + + if (results && results->GetLength() > 0) { + Dictionary::Ptr resultInfo = results->Get(0); + errorMessage = resultInfo->Get("status"); + + if (resultInfo->Get("code") >= 200 && resultInfo->Get("code") <= 299) + suggestions = resultInfo->Get("suggestions"); + else + BOOST_THROW_EXCEPTION(ScriptError(errorMessage)); + } + + callback(boost::exception_ptr(), suggestions); + } catch (const std::exception& ex) { + callback(boost::current_exception(), Array::Ptr()); + } +} diff --git a/icinga-studio/apiclient.hpp b/lib/remote/apiclient.hpp similarity index 80% rename from icinga-studio/apiclient.hpp rename to lib/remote/apiclient.hpp index a4ab2283c..273714bcb 100644 --- a/icinga-studio/apiclient.hpp +++ b/lib/remote/apiclient.hpp @@ -96,6 +96,14 @@ public: const std::vector& names = std::vector(), const std::vector& attrs = std::vector()) const; + typedef boost::function ExecuteScriptCompletionCallback; + void ExecuteScript(const String& session, const String& command, bool sandboxed, + const ExecuteScriptCompletionCallback& callback) const; + + typedef boost::function AutocompleteScriptCompletionCallback; + void AutocompleteScript(const String& session, const String& command, bool sandboxed, + const AutocompleteScriptCompletionCallback& callback) const; + private: HttpClientConnection::Ptr m_Connection; String m_User; @@ -105,6 +113,10 @@ private: HttpResponse& response, const TypesCompletionCallback& callback); static void ObjectsHttpCompletionCallback(HttpRequest& request, HttpResponse& response, const ObjectsCompletionCallback& callback); + static void ExecuteScriptHttpCompletionCallback(HttpRequest& request, + HttpResponse& response, const ExecuteScriptCompletionCallback& callback); + static void AutocompleteScriptHttpCompletionCallback(HttpRequest& request, + HttpResponse& response, const AutocompleteScriptCompletionCallback& callback); }; } diff --git a/lib/remote/consolehandler.cpp b/lib/remote/consolehandler.cpp new file mode 100644 index 000000000..5169fefd5 --- /dev/null +++ b/lib/remote/consolehandler.cpp @@ -0,0 +1,290 @@ +/****************************************************************************** + * 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/consolehandler.hpp" +#include "remote/httputility.hpp" +#include "remote/filterutility.hpp" +#include "config/configcompiler.hpp" +#include "base/configtype.hpp" +#include "base/configwriter.hpp" +#include "base/scriptglobal.hpp" +#include "base/logger.hpp" +#include "base/serializer.hpp" +#include "base/timer.hpp" +#include "base/initialize.hpp" +#include +#include + +using namespace icinga; + +REGISTER_URLHANDLER("/v1/console", ConsoleHandler); + +static int l_ExternalCommands = 0; +static boost::mutex l_QueryMutex; +static std::map l_ApiScriptFrames; +static Timer::Ptr l_FrameCleanupTimer; +static boost::mutex l_ApiScriptMutex; + +static void ScriptFrameCleanupHandler(void) +{ + boost::mutex::scoped_lock lock(l_ApiScriptMutex); + + std::vector cleanup_keys; + + typedef std::pair KVPair; + + BOOST_FOREACH(const KVPair& kv, l_ApiScriptFrames) { + if (kv.second.Seen < Utility::GetTime() - 1800) + cleanup_keys.push_back(kv.first); + } + + BOOST_FOREACH(const String& key, cleanup_keys) + l_ApiScriptFrames.erase(key); +} + +static void InitScriptFrameCleanup(void) +{ + l_FrameCleanupTimer = new Timer(); + l_FrameCleanupTimer->OnTimerExpired.connect(boost::bind(ScriptFrameCleanupHandler)); + l_FrameCleanupTimer->SetInterval(30); + l_FrameCleanupTimer->Start(); +} + +INITIALIZE_ONCE(InitScriptFrameCleanup); + +bool ConsoleHandler::HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) +{ + if (request.RequestUrl->GetPath().size() > 3) + return false; + + if (request.RequestMethod != "POST") + return false; + + QueryDescription qd; + Dictionary::Ptr params = HttpUtility::FetchRequestParameters(request); + + String methodName = request.RequestUrl->GetPath()[2]; + + String permission = "console/" + methodName; + FilterUtility::CheckPermission(user, permission); + + String session = HttpUtility::GetLastParameter(params, "session"); + + if (session.IsEmpty()) + session = Utility::NewUniqueID(); + + String command = HttpUtility::GetLastParameter(params, "command"); + + bool sandboxed = HttpUtility::GetLastParameter(params, "sandboxed"); + + if (methodName == "execute-script") { + return ExecuteScriptHelper(request, response, command, session, sandboxed); + } else if (methodName == "auto-complete-script") { + return AutocompleteScriptHelper(request, response, command, session, sandboxed); + } + + return true; +} + +bool ConsoleHandler::ExecuteScriptHelper(HttpRequest& request, HttpResponse& response, + const String& command, const String& session, bool sandboxed) +{ + Log(LogInformation, "Console") + << "Executing expression: " << command; + + ApiScriptFrame& lsf = l_ApiScriptFrames[session]; + lsf.Seen = Utility::GetTime(); + + if (!lsf.Locals) + lsf.Locals = new Dictionary(); + + String fileName = "<" + Convert::ToString(lsf.NextLine) + ">"; + lsf.NextLine++; + + lsf.Lines[fileName] = command; + + Array::Ptr results = new Array(); + Dictionary::Ptr resultInfo = new Dictionary(); + Expression *expr = NULL; + Value exprResult; + + try { + expr = ConfigCompiler::CompileText(fileName, command); + + ScriptFrame frame; + frame.Locals = lsf.Locals; + frame.Self = lsf.Locals; + frame.Sandboxed = sandboxed; + + exprResult = expr->Evaluate(frame); + + resultInfo->Set("code", 200); + resultInfo->Set("status", "Executed successfully."); + resultInfo->Set("result", Serialize(exprResult, 0)); + } catch (const ScriptError& ex) { + DebugInfo di = ex.GetDebugInfo(); + + std::ostringstream msgbuf; + + msgbuf << di.Path << ": " << lsf.Lines[di.Path] << "\n" + << String(di.Path.GetLength() + 2, ' ') + << String(di.FirstColumn, ' ') << String(di.LastColumn - di.FirstColumn + 1, '^') << "\n" + << ex.what() << "\n"; + + resultInfo->Set("code", 500); + resultInfo->Set("status", String(msgbuf.str())); + resultInfo->Set("incomplete_expression", ex.IsIncompleteExpression()); + + Dictionary::Ptr debugInfo = new Dictionary(); + debugInfo->Set("path", di.Path); + debugInfo->Set("first_line", di.FirstLine); + debugInfo->Set("first_column", di.FirstColumn); + debugInfo->Set("last_line", di.LastLine); + debugInfo->Set("last_column", di.LastColumn); + resultInfo->Set("debug_info", debugInfo); + } catch (...) { + delete expr; + throw; + } + delete expr; + + results->Add(resultInfo); + + Dictionary::Ptr result = new Dictionary(); + result->Set("results", results); + + response.SetStatus(200, "OK"); + HttpUtility::SendJsonBody(response, result); + + return true; +} + +bool ConsoleHandler::AutocompleteScriptHelper(HttpRequest& request, HttpResponse& response, + const String& command, const String& session, bool sandboxed) +{ + Log(LogInformation, "Console") + << "Auto-completing expression: " << command; + + ApiScriptFrame& lsf = l_ApiScriptFrames[session]; + lsf.Seen = Utility::GetTime(); + + if (!lsf.Locals) + lsf.Locals = new Dictionary(); + + Array::Ptr results = new Array(); + Dictionary::Ptr resultInfo = new Dictionary(); + + ScriptFrame frame; + frame.Locals = lsf.Locals; + frame.Self = lsf.Locals; + frame.Sandboxed = sandboxed; + + resultInfo->Set("code", 200); + resultInfo->Set("status", "Auto-completed successfully."); + resultInfo->Set("suggestions", Array::FromVector(GetAutocompletionSuggestions(command, frame))); + + results->Add(resultInfo); + + Dictionary::Ptr result = new Dictionary(); + result->Set("results", results); + + response.SetStatus(200, "OK"); + HttpUtility::SendJsonBody(response, result); + + return true; +} + +static void AddSuggestion(std::vector& matches, const String& word, const String& suggestion) +{ + if (suggestion.Find(word) != 0) + return; + + matches.push_back(suggestion); +} + +std::vector ConsoleHandler::GetAutocompletionSuggestions(const String& word, ScriptFrame& frame) +{ + std::vector matches; + + BOOST_FOREACH(const String& keyword, ConfigWriter::GetKeywords()) { + AddSuggestion(matches, word, keyword); + } + + { + ObjectLock olock(frame.Locals); + BOOST_FOREACH(const Dictionary::Pair& kv, frame.Locals) { + AddSuggestion(matches, word, kv.first); + } + } + + { + ObjectLock olock(ScriptGlobal::GetGlobals()); + BOOST_FOREACH(const Dictionary::Pair& kv, ScriptGlobal::GetGlobals()) { + AddSuggestion(matches, word, kv.first); + } + } + + String::SizeType cperiod = word.RFind("."); + + if (cperiod != -1) { + String pword = word.SubStr(0, cperiod); + + Value value; + + try { + Expression *expr = ConfigCompiler::CompileText("temp", pword); + + if (expr) + value = expr->Evaluate(frame); + + if (value.IsObjectType()) { + Dictionary::Ptr dict = value; + + ObjectLock olock(dict); + BOOST_FOREACH(const Dictionary::Pair& kv, dict) { + AddSuggestion(matches, word, pword + "." + kv.first); + } + } + + Type::Ptr type = value.GetReflectionType(); + + for (int i = 0; i < type->GetFieldCount(); i++) { + Field field = type->GetFieldInfo(i); + + AddSuggestion(matches, word, pword + "." + field.Name); + } + + while (type) { + Object::Ptr prototype = type->GetPrototype(); + Dictionary::Ptr dict = dynamic_pointer_cast(prototype); + + if (dict) { + ObjectLock olock(dict); + BOOST_FOREACH(const Dictionary::Pair& kv, dict) { + AddSuggestion(matches, word, pword + "." + kv.first); + } + } + + type = type->GetBaseType(); + } + } catch (...) { /* Ignore the exception */ } + } + + return matches; +} diff --git a/lib/remote/consolehandler.hpp b/lib/remote/consolehandler.hpp new file mode 100644 index 000000000..146a7470b --- /dev/null +++ b/lib/remote/consolehandler.hpp @@ -0,0 +1,60 @@ +/****************************************************************************** + * 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 CONSOLEHANDLER_H +#define CONSOLEHANDLER_H + +#include "remote/httphandler.hpp" +#include "base/scriptframe.hpp" + +namespace icinga +{ + +struct I2_REMOTE_API ApiScriptFrame +{ + double Seen; + int NextLine; + std::map Lines; + Dictionary::Ptr Locals; + + ApiScriptFrame(void) + : Seen(0), NextLine(1) + { } +}; + +class I2_REMOTE_API ConsoleHandler : public HttpHandler +{ +public: + DECLARE_PTR_TYPEDEFS(ConsoleHandler); + + virtual bool HandleRequest(const ApiUser::Ptr& user, HttpRequest& request, HttpResponse& response) override; + + static std::vector GetAutocompletionSuggestions(const String& word, ScriptFrame& frame); + +private: + static bool ExecuteScriptHelper(HttpRequest& request, HttpResponse& response, + const String& command, const String& session, bool sandboxed); + static bool AutocompleteScriptHelper(HttpRequest& request, HttpResponse& response, + const String& command, const String& session, bool sandboxed); + +}; + +} + +#endif /* CONSOLEHANDLER_H */ diff --git a/lib/remote/httpresponse.cpp b/lib/remote/httpresponse.cpp index dbd841e2a..411057372 100644 --- a/lib/remote/httpresponse.cpp +++ b/lib/remote/httpresponse.cpp @@ -192,7 +192,7 @@ bool HttpResponse::Parse(StreamReadContext& src, bool may_wait) if (srs != StatusNewItem) return false; - Log(LogInformation, "HttpResponse") + Log(LogNotice, "HttpResponse") << "Read " << size << " bytes"; m_Body->Write(data, size); diff --git a/lib/remote/url.cpp b/lib/remote/url.cpp index 537f221dd..05a54aa98 100644 --- a/lib/remote/url.cpp +++ b/lib/remote/url.cpp @@ -177,12 +177,14 @@ void Url::SetScheme(const String& scheme) m_Scheme = scheme; } -void Url::SetAuthority(const String& username, const String& password, const String& host, const String& port) +void Url::SetUsername(const String& username) { m_Username = username; +} + +void Url::SetPassword(const String& password) +{ m_Password = password; - m_Host = host; - m_Port = port; } void Url::SetHost(const String& host) diff --git a/lib/remote/url.hpp b/lib/remote/url.hpp index c416b4009..118126342 100644 --- a/lib/remote/url.hpp +++ b/lib/remote/url.hpp @@ -59,12 +59,13 @@ public: String GetFragment(void) const; void SetScheme(const String& scheme); - void SetAuthority(const String& username, const String& password, - const String& host, const String& port); + void SetUsername(const String& username); + void SetPassword(const String& password); void SetHost(const String& host); void SetPort(const String& port); void SetPath(const std::vector& path); void SetQuery(const std::map >& query); + void AddQueryElement(const String& name, const String& query); void SetQueryElements(const String& name, const std::vector& query); void SetFragment(const String& fragment); diff --git a/test/remote-url.cpp b/test/remote-url.cpp index 21cd99c66..afea891b2 100644 --- a/test/remote-url.cpp +++ b/test/remote-url.cpp @@ -47,7 +47,10 @@ BOOST_AUTO_TEST_CASE(get_and_set) { Url::Ptr url = new Url(); url->SetScheme("ftp"); - url->SetAuthority("Horst", "Seehofer", "koenigreich.bayern", "1918"); + url->SetUsername("Horst"); + url->SetPassword("Seehofer"); + url->SetHost("koenigreich.bayern"); + url->SetPort("1918"); std::vector p = boost::assign::list_of("path")("to")("münchen"); url->SetPath(p); BOOST_CHECK(url->Format(true) == "ftp://Horst:Seehofer@koenigreich.bayern:1918/path/to/m%C3%BCnchen");