Implement 'console' cli command using the API

fixes #10387
This commit is contained in:
Michael Friedrich 2015-11-02 16:35:21 +01:00
parent a51bc4010a
commit 5d46f661ea
17 changed files with 780 additions and 321 deletions

View File

@ -27,7 +27,7 @@ endif()
add_executable(icinga-studio MACOSX_BUNDLE WIN32 icinga-studio.cpp add_executable(icinga-studio MACOSX_BUNDLE WIN32 icinga-studio.cpp
forms.cpp aboutform.cpp connectform.cpp mainform.cpp forms.cpp aboutform.cpp connectform.cpp mainform.cpp
icinga.icns apiclient.cpp ${WindowsSources}) icinga.icns ${WindowsSources})
include_directories(${Boost_INCLUDE_DIRS}) include_directories(${Boost_INCLUDE_DIRS})
target_link_libraries(icinga-studio ${Boost_LIBRARIES} ${wxWidgets_LIBRARIES} base remote) target_link_libraries(icinga-studio ${Boost_LIBRARIES} ${wxWidgets_LIBRARIES} base remote)

View File

@ -142,7 +142,6 @@ void MainForm::ObjectsCompletionHandler(boost::exception_ptr eptr, const std::ve
m_PropertyGrid->Clear(); m_PropertyGrid->Clear();
if (eptr) { if (eptr) {
try { try {
boost::rethrow_exception(eptr); boost::rethrow_exception(eptr);
} catch (const std::exception& ex) { } catch (const std::exception& ex) {

View File

@ -20,7 +20,7 @@
#ifndef MAINFORM_H #ifndef MAINFORM_H
#define MAINFORM_H #define MAINFORM_H
#include "icinga-studio/apiclient.hpp" #include "remote/apiclient.hpp"
#include "remote/url.hpp" #include "remote/url.hpp"
#include "base/exception.hpp" #include "base/exception.hpp"
#include "icinga-studio/forms.h" #include "icinga-studio/forms.h"

View File

@ -63,7 +63,7 @@ static Object::Ptr SerializeObject(const Object::Ptr& input, int attributeTypes)
for (int i = 0; i < type->GetFieldCount(); i++) { for (int i = 0; i < type->GetFieldCount(); i++) {
Field field = type->GetFieldInfo(i); Field field = type->GetFieldInfo(i);
if ((field.Attributes & attributeTypes) == 0) if (attributeTypes != 0 && (field.Attributes & attributeTypes) == 0)
continue; continue;
fields->Set(field.Name, Serialize(input->GetField(i), attributeTypes)); fields->Set(field.Name, Serialize(input->GetField(i), attributeTypes));

View File

@ -19,7 +19,11 @@
#include "cli/consolecommand.hpp" #include "cli/consolecommand.hpp"
#include "config/configcompiler.hpp" #include "config/configcompiler.hpp"
#include "remote/apiclient.hpp"
#include "remote/consolehandler.hpp"
#include "remote/url.hpp"
#include "base/configwriter.hpp" #include "base/configwriter.hpp"
#include "base/serializer.hpp"
#include "base/json.hpp" #include "base/json.hpp"
#include "base/console.hpp" #include "base/console.hpp"
#include "base/application.hpp" #include "base/application.hpp"
@ -37,6 +41,8 @@ using namespace icinga;
namespace po = boost::program_options; namespace po = boost::program_options;
static ScriptFrame *l_ScriptFrame; static ScriptFrame *l_ScriptFrame;
static ApiClient::Ptr l_ApiClient;
static String l_Session;
REGISTER_CLICOMMAND("console", ConsoleCommand); REGISTER_CLICOMMAND("console", ConsoleCommand);
@ -60,89 +66,41 @@ void ConsoleCommand::InitParameters(boost::program_options::options_description&
{ {
visibleDesc.add_options() visibleDesc.add_options()
("connect,c", po::value<std::string>(), "connect to an Icinga 2 instance") ("connect,c", po::value<std::string>(), "connect to an Icinga 2 instance")
("eval,e", po::value<std::string>(), "evaluate expression and terminate")
("sandbox", "enable sandbox mode") ("sandbox", "enable sandbox mode")
; ;
} }
#ifdef HAVE_EDITLINE #ifdef HAVE_EDITLINE
static void AddSuggestion(std::vector<String>& matches, const String& word, const String& suggestion) char *ConsoleCommand::ConsoleCompleteHelper(const char *word, int state)
{
if (suggestion.Find(word) != 0)
return;
matches.push_back(suggestion);
}
static char *ConsoleCompleteHelper(const char *word, int state)
{ {
static std::vector<String> matches; static std::vector<String> matches;
String aword = word;
if (state == 0) { if (state == 0) {
if (!l_ApiClient)
matches = ConsoleHandler::GetAutocompletionSuggestions(word, *l_ScriptFrame);
else {
boost::mutex mutex;
boost::condition_variable cv;
bool ready = false;
Array::Ptr suggestions;
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)));
{
boost::mutex::scoped_lock lock(mutex);
while (!ready)
cv.wait(lock);
}
matches.clear(); matches.clear();
BOOST_FOREACH(const String& keyword, ConfigWriter::GetKeywords()) { ObjectLock olock(suggestions);
AddSuggestion(matches, word, keyword); std::copy(suggestions->Begin(), suggestions->End(), std::back_inserter(matches));
}
{
ObjectLock olock(l_ScriptFrame->Locals);
BOOST_FOREACH(const Dictionary::Pair& kv, l_ScriptFrame->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 = 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>()) {
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<Dictionary>(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 */ }
} }
} }
@ -164,30 +122,53 @@ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::stri
int next_line = 1; int next_line = 1;
#ifdef HAVE_EDITLINE #ifdef HAVE_EDITLINE
rl_completion_entry_function = ConsoleCompleteHelper; rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper;
rl_completion_append_character = '\0'; rl_completion_append_character = '\0';
#endif /* HAVE_EDITLINE */ #endif /* HAVE_EDITLINE */
String addr, session; String addr;
ScriptFrame scriptFrame; ScriptFrame scriptFrame;
l_ScriptFrame = &scriptFrame; l_ScriptFrame = &scriptFrame;
l_Session = Utility::NewUniqueID();
if (vm.count("sandbox"))
scriptFrame.Sandboxed = true;
if (!vm.count("eval"))
std::cout << "Icinga 2 (version: " << Application::GetAppVersion() << ")\n";
const char *addrEnv = getenv("ICINGA2_API_URL");
if (addrEnv)
addr = addrEnv;
if (vm.count("connect")) { if (vm.count("connect")) {
addr = vm["connect"].as<std::string>(); addr = vm["connect"].as<std::string>();
session = Utility::NewUniqueID();
} }
if (vm.count("sandbox")) { if (!addr.IsEmpty()) {
if (vm.count("connect")) { Url::Ptr url;
Log(LogCritical, "ConsoleCommand", "Sandbox mode cannot be used together with --connect.");
try {
url = new Url(addr);
} catch (const std::exception& ex) {
Log(LogCritical, "ConsoleCommand", ex.what());
return EXIT_FAILURE; 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()) { while (std::cin.good()) {
String fileName = "<" + Convert::ToString(next_line) + ">"; String fileName = "<" + Convert::ToString(next_line) + ">";
@ -197,6 +178,9 @@ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::stri
std::string command; std::string command;
incomplete: incomplete:
std::string line;
if (!vm.count("eval")) {
#ifdef HAVE_EDITLINE #ifdef HAVE_EDITLINE
std::ostringstream promptbuf; std::ostringstream promptbuf;
std::ostream& os = promptbuf; std::ostream& os = promptbuf;
@ -222,35 +206,59 @@ incomplete:
add_history(cline); add_history(cline);
std::string line = cline; line = cline;
free(cline); free(cline);
#else /* HAVE_EDITLINE */ #else /* HAVE_EDITLINE */
std::string line;
std::getline(std::cin, line); std::getline(std::cin, line);
#endif /* HAVE_EDITLINE */ #endif /* HAVE_EDITLINE */
} else
line = vm["eval"].as<std::string>();
if (!command.empty()) if (!command.empty())
command += "\n"; command += "\n";
command += line; command += line;
if (addr.IsEmpty()) {
Expression *expr = NULL; Expression *expr = NULL;
try { try {
lines[fileName] = command; lines[fileName] = command;
expr = ConfigCompiler::CompileText(fileName, command); Value result;
if (expr) { if (!l_ApiClient) {
Value result = expr->Evaluate(scriptFrame); expr = ConfigCompiler::CompileText(fileName, command);
result = Serialize(expr->Evaluate(scriptFrame), 0);
} else {
boost::mutex mutex;
boost::condition_variable cv;
bool ready = false;
boost::exception_ptr eptr;
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)));
{
boost::mutex::scoped_lock lock(mutex);
while (!ready)
cv.wait(lock);
}
if (eptr)
boost::rethrow_exception(eptr);
}
if (!vm.count("eval")) {
std::cout << ConsoleColorTag(Console_ForegroundCyan); std::cout << ConsoleColorTag(Console_ForegroundCyan);
if (!result.IsObject() || result.IsObjectType<Array>() || result.IsObjectType<Dictionary>()) ConfigWriter::EmitValue(std::cout, 1, result);
std::cout << JsonEncode(result);
else
std::cout << result;
std::cout << ConsoleColorTag(Console_Normal) << "\n"; std::cout << ConsoleColorTag(Console_Normal) << "\n";
} else {
std::cout << JsonEncode(result) << "\n";
break;
} }
} catch (const ScriptError& ex) { } catch (const ScriptError& ex) {
if (ex.IsIncompleteExpression()) { if (ex.IsIncompleteExpression()) {
@ -297,48 +305,64 @@ incomplete:
} }
std::cout << ex.what() << "\n"; std::cout << ex.what() << "\n";
if (vm.count("eval"))
return EXIT_FAILURE;
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
std::cout << "Error: " << DiagnosticInformation(ex) << "\n"; std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
if (vm.count("eval"))
return EXIT_FAILURE;
} }
delete expr; delete expr;
} else {
Socket::Ptr socket;
#ifndef _WIN32
if (addr.FindFirstOf("/") != String::NPos) {
UnixSocket::Ptr usocket = new UnixSocket();
usocket->Connect(addr);
socket = usocket;
} else {
#endif /* _WIN32 */
Log(LogCritical, "ConsoleCommand", "Sorry, TCP sockets aren't supported yet.");
return 1;
#ifndef _WIN32
}
#endif /* _WIN32 */
String query = "SCRIPT " + session + "\n" + line + "\n\n";
NetworkStream::Ptr ns = new NetworkStream(socket);
ns->Write(query.CStr(), query.GetLength());
String result;
char buf[1024];
while (!ns->IsEof()) {
size_t rc = ns->Read(buf, sizeof(buf), true);
result += String(buf, buf + rc);
} }
if (result.GetLength() < 16) { return EXIT_SUCCESS;
Log(LogCritical, "ConsoleCommand", "Received invalid response from Livestatus.");
continue;
} }
std::cout << result.SubStr(16) << "\n"; 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();
}
} }

View File

@ -21,6 +21,7 @@
#define CONSOLECOMMAND_H #define CONSOLECOMMAND_H
#include "cli/clicommand.hpp" #include "cli/clicommand.hpp"
#include "base/exception.hpp"
namespace icinga namespace icinga
{ {
@ -41,6 +42,22 @@ public:
virtual void InitParameters(boost::program_options::options_description& visibleDesc, virtual void InitParameters(boost::program_options::options_description& visibleDesc,
boost::program_options::options_description& hiddenDesc) const override; boost::program_options::options_description& hiddenDesc) const override;
virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override; virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& 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 */
}; };
} }

View File

@ -31,7 +31,6 @@
#include "livestatus/orfilter.hpp" #include "livestatus/orfilter.hpp"
#include "livestatus/andfilter.hpp" #include "livestatus/andfilter.hpp"
#include "icinga/externalcommandprocessor.hpp" #include "icinga/externalcommandprocessor.hpp"
#include "config/configcompiler.hpp"
#include "base/debug.hpp" #include "base/debug.hpp"
#include "base/convert.hpp" #include "base/convert.hpp"
#include "base/objectlock.hpp" #include "base/objectlock.hpp"
@ -52,36 +51,6 @@ using namespace icinga;
static int l_ExternalCommands = 0; static int l_ExternalCommands = 0;
static boost::mutex l_QueryMutex; static boost::mutex l_QueryMutex;
static std::map<String, LivestatusScriptFrame> 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<String> cleanup_keys;
typedef std::pair<String, LivestatusScriptFrame> 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<String>& lines, const String& compat_log_path) LivestatusQuery::LivestatusQuery(const std::vector<String>& lines, const String& compat_log_path)
: m_KeepAlive(false), m_OutputFormat("csv"), m_ColumnHeaders(true), m_Limit(-1), m_ErrorCode(0), : m_KeepAlive(false), m_OutputFormat("csv"), m_ColumnHeaders(true), m_Limit(-1), m_ErrorCode(0),
@ -123,16 +92,6 @@ LivestatusQuery::LivestatusQuery(const std::vector<String>& lines, const String&
if (m_Verb == "COMMAND") { if (m_Verb == "COMMAND") {
m_KeepAlive = true; m_KeepAlive = true;
m_Command = target; 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") { } else if (m_Verb == "GET") {
m_Table = target; m_Table = target;
} else { } else {
@ -625,54 +584,6 @@ void LivestatusQuery::ExecuteCommandHelper(const Stream::Ptr& stream)
SendResponse(stream, LivestatusErrorOK, ""); 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) void LivestatusQuery::ExecuteErrorHelper(const Stream::Ptr& stream)
{ {
Log(LogDebug, "LivestatusQuery") Log(LogDebug, "LivestatusQuery")
@ -720,8 +631,6 @@ bool LivestatusQuery::Execute(const Stream::Ptr& stream)
ExecuteGetHelper(stream); ExecuteGetHelper(stream);
else if (m_Verb == "COMMAND") else if (m_Verb == "COMMAND")
ExecuteCommandHelper(stream); ExecuteCommandHelper(stream);
else if (m_Verb == "SCRIPT")
ExecuteScriptHelper(stream);
else if (m_Verb == "ERROR") else if (m_Verb == "ERROR")
ExecuteErrorHelper(stream); ExecuteErrorHelper(stream);
else else

View File

@ -40,18 +40,6 @@ enum LivestatusError
LivestatusErrorQuery = 452 LivestatusErrorQuery = 452
}; };
struct LivestatusScriptFrame
{
double Seen;
int NextLine;
std::map<String, String> Lines;
Dictionary::Ptr Locals;
LivestatusScriptFrame(void)
: Seen(0), NextLine(1)
{ }
};
/** /**
* @ingroup livestatus * @ingroup livestatus
*/ */
@ -106,7 +94,6 @@ private:
void ExecuteGetHelper(const Stream::Ptr& stream); void ExecuteGetHelper(const Stream::Ptr& stream);
void ExecuteCommandHelper(const Stream::Ptr& stream); void ExecuteCommandHelper(const Stream::Ptr& stream);
void ExecuteScriptHelper(const Stream::Ptr& stream);
void ExecuteErrorHelper(const Stream::Ptr& stream); void ExecuteErrorHelper(const Stream::Ptr& stream);
void SendResponse(const Stream::Ptr& stream, int code, const String& data); void SendResponse(const Stream::Ptr& stream, int code, const String& data);

View File

@ -21,10 +21,10 @@ mkclass_target(endpoint.ti endpoint.tcpp endpoint.thpp)
mkclass_target(zone.ti zone.tcpp zone.thpp) mkclass_target(zone.ti zone.tcpp zone.thpp)
set(remote_SOURCES set(remote_SOURCES
actionshandler.cpp apiaction.cpp actionshandler.cpp apiaction.cpp apiclient.cpp
apifunction.cpp apilistener.cpp apilistener.thpp apilistener-configsync.cpp apifunction.cpp apilistener.cpp apilistener.thpp apilistener-configsync.cpp
apilistener-filesync.cpp apiuser.cpp apiuser.thpp authority.cpp base64.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 configstageshandler.cpp createobjecthandler.cpp deleteobjecthandler.cpp
endpoint.cpp endpoint.thpp eventshandler.cpp eventqueue.cpp filterutility.cpp endpoint.cpp endpoint.thpp eventshandler.cpp eventqueue.cpp filterutility.cpp
httpchunkedencoding.cpp httpclientconnection.cpp httpserverconnection.cpp httphandler.cpp httprequest.cpp httpresponse.cpp httpchunkedencoding.cpp httpclientconnection.cpp httpserverconnection.cpp httphandler.cpp httprequest.cpp httpresponse.cpp

View File

@ -17,7 +17,7 @@
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * * 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 "remote/base64.hpp"
#include "base/json.hpp" #include "base/json.hpp"
#include "base/logger.hpp" #include "base/logger.hpp"
@ -116,7 +116,6 @@ void ApiClient::GetObjects(const String& pluralType, const ObjectsCompletionCall
path.push_back("objects"); path.push_back("objects");
path.push_back(pluralType); path.push_back(pluralType);
url->SetPath(path); url->SetPath(path);
String qp;
std::map<String, std::vector<String> > params; std::map<String, std::vector<String> > params;
@ -206,3 +205,159 @@ void ApiClient::ObjectsHttpCompletionCallback(HttpRequest& request,
callback(boost::current_exception(), std::vector<ApiObject::Ptr>()); callback(boost::current_exception(), std::vector<ApiObject::Ptr>());
} }
} }
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<String> path;
path.push_back("v1");
path.push_back("console");
path.push_back("execute-script");
url->SetPath(path);
std::map<String, std::vector<String> > 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<HttpRequest> 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<String> path;
path.push_back("v1");
path.push_back("console");
path.push_back("auto-complete-script");
url->SetPath(path);
std::map<String, std::vector<String> > 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<HttpRequest> 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());
}
}

View File

@ -96,6 +96,14 @@ public:
const std::vector<String>& names = std::vector<String>(), const std::vector<String>& names = std::vector<String>(),
const std::vector<String>& attrs = std::vector<String>()) const; const std::vector<String>& attrs = std::vector<String>()) const;
typedef boost::function<void(boost::exception_ptr, const Value&)> ExecuteScriptCompletionCallback;
void ExecuteScript(const String& session, const String& command, bool sandboxed,
const ExecuteScriptCompletionCallback& callback) const;
typedef boost::function<void(boost::exception_ptr, const Array::Ptr&)> AutocompleteScriptCompletionCallback;
void AutocompleteScript(const String& session, const String& command, bool sandboxed,
const AutocompleteScriptCompletionCallback& callback) const;
private: private:
HttpClientConnection::Ptr m_Connection; HttpClientConnection::Ptr m_Connection;
String m_User; String m_User;
@ -105,6 +113,10 @@ private:
HttpResponse& response, const TypesCompletionCallback& callback); HttpResponse& response, const TypesCompletionCallback& callback);
static void ObjectsHttpCompletionCallback(HttpRequest& request, static void ObjectsHttpCompletionCallback(HttpRequest& request,
HttpResponse& response, const ObjectsCompletionCallback& callback); 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);
}; };
} }

View File

@ -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 <boost/algorithm/string.hpp>
#include <set>
using namespace icinga;
REGISTER_URLHANDLER("/v1/console", ConsoleHandler);
static int l_ExternalCommands = 0;
static boost::mutex l_QueryMutex;
static std::map<String, ApiScriptFrame> 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<String> cleanup_keys;
typedef std::pair<String, ApiScriptFrame> 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<String>& matches, const String& word, const String& suggestion)
{
if (suggestion.Find(word) != 0)
return;
matches.push_back(suggestion);
}
std::vector<String> ConsoleHandler::GetAutocompletionSuggestions(const String& word, ScriptFrame& frame)
{
std::vector<String> 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>()) {
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<Dictionary>(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;
}

View File

@ -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<String, String> 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<String> 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 */

View File

@ -192,7 +192,7 @@ bool HttpResponse::Parse(StreamReadContext& src, bool may_wait)
if (srs != StatusNewItem) if (srs != StatusNewItem)
return false; return false;
Log(LogInformation, "HttpResponse") Log(LogNotice, "HttpResponse")
<< "Read " << size << " bytes"; << "Read " << size << " bytes";
m_Body->Write(data, size); m_Body->Write(data, size);

View File

@ -177,12 +177,14 @@ void Url::SetScheme(const String& scheme)
m_Scheme = 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; m_Username = username;
}
void Url::SetPassword(const String& password)
{
m_Password = password; m_Password = password;
m_Host = host;
m_Port = port;
} }
void Url::SetHost(const String& host) void Url::SetHost(const String& host)

View File

@ -59,12 +59,13 @@ public:
String GetFragment(void) const; String GetFragment(void) const;
void SetScheme(const String& scheme); void SetScheme(const String& scheme);
void SetAuthority(const String& username, const String& password, void SetUsername(const String& username);
const String& host, const String& port); void SetPassword(const String& password);
void SetHost(const String& host); void SetHost(const String& host);
void SetPort(const String& port); void SetPort(const String& port);
void SetPath(const std::vector<String>& path); void SetPath(const std::vector<String>& path);
void SetQuery(const std::map<String, std::vector<String> >& query); void SetQuery(const std::map<String, std::vector<String> >& query);
void AddQueryElement(const String& name, const String& query); void AddQueryElement(const String& name, const String& query);
void SetQueryElements(const String& name, const std::vector<String>& query); void SetQueryElements(const String& name, const std::vector<String>& query);
void SetFragment(const String& fragment); void SetFragment(const String& fragment);

View File

@ -47,7 +47,10 @@ BOOST_AUTO_TEST_CASE(get_and_set)
{ {
Url::Ptr url = new Url(); Url::Ptr url = new Url();
url->SetScheme("ftp"); 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<String> p = boost::assign::list_of("path")("to")("münchen"); std::vector<String> p = boost::assign::list_of("path")("to")("münchen");
url->SetPath(p); url->SetPath(p);
BOOST_CHECK(url->Format(true) == "ftp://Horst:Seehofer@koenigreich.bayern:1918/path/to/m%C3%BCnchen"); BOOST_CHECK(url->Format(true) == "ftp://Horst:Seehofer@koenigreich.bayern:1918/path/to/m%C3%BCnchen");