Exemple #1
0
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());
	url->SetPath({ "v1", "console", "execute-script" });

	std::map<String, std::vector<String> > params;
	params["session"].push_back(session);
	params["command"].push_back(command);
	params["sandboxed"].emplace_back(sandboxed ? "1" : "0");
	url->SetQuery(params);

	try {
		std::shared_ptr<HttpRequest> req = m_Connection->NewRequest();
		req->RequestMethod = "POST";
		req->RequestUrl = url;
		req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password));
		req->AddHeader("Accept", "application/json");
		m_Connection->SubmitRequest(req, std::bind(ExecuteScriptHttpCompletionCallback, _1, _2, callback));
	} catch (const std::exception&) {
		callback(boost::current_exception(), Empty);
	}
}
void ApiClient::GetObjects(const String& pluralType, const ObjectsCompletionCallback& callback,
    const std::vector<String>& names, const std::vector<String>& attrs, const std::vector<String>& joins, bool all_joins) 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("objects");
	path.push_back(pluralType);
	url->SetPath(path);

	std::map<String, std::vector<String> > params;

	for (const String& name : names) {
		params[pluralType.ToLower()].push_back(name);
	}

	for (const String& attr : attrs) {
		params["attrs"].push_back(attr);
	}

	for (const String& join : joins) {
		params["joins"].push_back(join);
	}

	params["all_joins"].push_back(all_joins ? "1" : "0");

	url->SetQuery(params);

	try {
		boost::shared_ptr<HttpRequest> req = m_Connection->NewRequest();
		req->RequestMethod = "GET";
		req->RequestUrl = url;
		req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password));
		req->AddHeader("Accept", "application/json");
		m_Connection->SubmitRequest(req, boost::bind(ObjectsHttpCompletionCallback, _1, _2, callback));
	} catch (const std::exception& ex) {
		callback(boost::current_exception(), std::vector<ApiObject::Ptr>());
	}
}
void ApiClient::GetTypes(const TypesCompletionCallback& 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("types");
	url->SetPath(path);

	try {
		boost::shared_ptr<HttpRequest> req = m_Connection->NewRequest();
		req->RequestMethod = "GET";
		req->RequestUrl = url;
		req->AddHeader("Authorization", "Basic " + Base64::Encode(m_User + ":" + m_Password));
		req->AddHeader("Accept", "application/json");
		m_Connection->SubmitRequest(req, boost::bind(TypesHttpCompletionCallback, _1, _2, callback));
	} catch (const std::exception& ex) {
		callback(boost::current_exception(), std::vector<ApiType::Ptr>());
	}
}
Exemple #4
0
void InfluxdbWriter::Flush(void)
{
	Stream::Ptr stream = Connect();

	// Unable to connect, play it safe and lose the data points
	// to avoid a memory leak
	if (!stream.get()) {
		m_DataBuffer->Clear();
		return;
	}

	Url::Ptr url = new Url();
	url->SetScheme(GetSslEnable() ? "https" : "http");
	url->SetHost(GetHost());
	url->SetPort(GetPort());

	std::vector<String> path;
	path.push_back("write");
	url->SetPath(path);

	url->AddQueryElement("db", GetDatabase());
	url->AddQueryElement("precision", "s");
	if (!GetUsername().IsEmpty())
		url->AddQueryElement("u", GetUsername());
	if (!GetPassword().IsEmpty())
		url->AddQueryElement("p", GetPassword());

	// Ensure you hold a lock against m_DataBuffer so that things
	// don't go missing after creating the body and clearing the buffer
	String body = Utility::Join(m_DataBuffer, '\n', false);
	m_DataBuffer->Clear();

	HttpRequest req(stream);
	req.RequestMethod = "POST";
	req.RequestUrl = url;

	try {
		req.WriteBody(body.CStr(), body.GetLength());
		req.Finish();
	} catch (const std::exception&) {
		Log(LogWarning, "InfluxdbWriter")
		    << "Cannot write to TCP socket on host '" << GetHost() << "' port '" << GetPort() << "'.";
		return;
	}

	HttpResponse resp(stream, req);
	StreamReadContext context;

	try {
		resp.Parse(context, true);
	} catch (const std::exception&) {
		Log(LogWarning, "InfluxdbWriter")
		    << "Cannot read from TCP socket from host '" << GetHost() << "' port '" << GetPort() << "'.";
		return;
	}

	if (resp.StatusCode != 204) {
		Log(LogWarning, "InfluxdbWriter")
		    << "Unexpected response code " << resp.StatusCode;
	}
}
Exemple #5
0
/**
 * The entry point for the "console" CLI command.
 *
 * @returns An exit status.
 */
int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::string>& ap) const
{
#ifdef HAVE_EDITLINE
	rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper;
	rl_completion_append_character = '\0';
#endif /* HAVE_EDITLINE */

	String addr, session;
	ScriptFrame scriptFrame(true);

	session = Utility::NewUniqueID();

	if (vm.count("sandbox"))
		scriptFrame.Sandboxed = true;

	scriptFrame.Self = scriptFrame.Locals;

	if (!vm.count("eval") && !vm.count("file"))
		std::cout << "Icinga 2 (version: " << Application::GetAppVersion() << ")\n"
			<< "Type $help to view available commands.\n";

	String addrEnv = Utility::GetFromEnvironment("ICINGA2_API_URL");
	if (!addrEnv.IsEmpty())
		addr = addrEnv;

	/* Initialize remote connect parameters. */
	if (vm.count("connect")) {
		addr = vm["connect"].as<std::string>();

		try {
			l_Url = new Url(addr);
		} catch (const std::exception& ex) {
			Log(LogCritical, "ConsoleCommand", ex.what());
			return EXIT_FAILURE;
		}

		String usernameEnv = Utility::GetFromEnvironment("ICINGA2_API_USERNAME");
		String passwordEnv = Utility::GetFromEnvironment("ICINGA2_API_PASSWORD");

		if (!usernameEnv.IsEmpty())
			l_Url->SetUsername(usernameEnv);
		if (!passwordEnv.IsEmpty())
			l_Url->SetPassword(passwordEnv);

		if (l_Url->GetPort().IsEmpty())
			l_Url->SetPort("5665");

		/* User passed --connect and wants to run the expression via REST API.
		 * Evaluate this now before any user input happens.
		 */
		try {
			l_TlsStream = ConsoleCommand::Connect();
		} catch (const std::exception& ex) {
			return EXIT_FAILURE;
		}
	}

	String command;
	bool syntaxOnly = false;

	if (vm.count("syntax-only")) {
		if (vm.count("eval") || vm.count("file"))
			syntaxOnly = true;
		else {
			std::cerr << "The option --syntax-only can only be used in combination with --eval or --file." << std::endl;
			return EXIT_FAILURE;
		}
	}

	String commandFileName;

	if (vm.count("eval"))
		command = vm["eval"].as<std::string>();
	else if (vm.count("file")) {
		commandFileName = vm["file"].as<std::string>();

		try {
			std::ifstream fp(commandFileName.CStr());
			fp.exceptions(std::ifstream::failbit | std::ifstream::badbit);
			command = String(std::istreambuf_iterator<char>(fp), std::istreambuf_iterator<char>());
		} catch (const std::exception&) {
			std::cerr << "Could not read file '" << commandFileName << "'." << std::endl;
			return EXIT_FAILURE;
		}
	}

	return RunScriptConsole(scriptFrame, addr, session, command, commandFileName, syntaxOnly);
}
Exemple #6
0
int ConsoleCommand::RunScriptConsole(ScriptFrame& scriptFrame, const String& addr, const String& session, const String& commandOnce, const String& commandOnceFileName, bool syntaxOnly)
{
	std::map<String, String> lines;
	int next_line = 1;

#ifdef HAVE_EDITLINE
	char *homeEnv = getenv("HOME");

	String historyPath;
	std::fstream historyfp;

	if (homeEnv) {
		historyPath = String(homeEnv) + "/.icinga2_history";

		historyfp.open(historyPath.CStr(), std::fstream::in);

		String line;
		while (std::getline(historyfp, line.GetData()))
			add_history(line.CStr());

		historyfp.close();
	}
#endif /* HAVE_EDITLINE */

	l_ScriptFrame = &scriptFrame;
	l_Session = session;

	if (!addr.IsEmpty()) {
		Url::Ptr url;

		try {
			url = new Url(addr);
		} catch (const std::exception& ex) {
			Log(LogCritical, "ConsoleCommand", ex.what());
			return EXIT_FAILURE;
		}

		const char *usernameEnv = getenv("ICINGA2_API_USERNAME");
		const char *passwordEnv = getenv("ICINGA2_API_PASSWORD");

		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;

		if (commandOnceFileName.IsEmpty())
			fileName = "<" + Convert::ToString(next_line) + ">";
		else
			fileName = commandOnceFileName;

		next_line++;

		bool continuation = false;
		std::string command;

incomplete:
		std::string line;

		if (commandOnce.IsEmpty()) {
#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;

			if (commandOnce.IsEmpty() && cline[0] != '\0') {
				add_history(cline);

				if (!historyPath.IsEmpty()) {
					historyfp.open(historyPath.CStr(), std::fstream::out | std::fstream::app);
					historyfp << cline << "\n";
					historyfp.close();
				}
			}

			line = cline;

			free(cline);
#else /* HAVE_EDITLINE */
			std::getline(std::cin, line);
#endif /* HAVE_EDITLINE */
		} else
			line = commandOnce;

		if (!line.empty() && line[0] == '$') {
			if (line == "$continue" || line == "$quit" || line == "$exit")
				break;
			else if (line == "$help")
				std::cout << "Welcome to the Icinga 2 debug console.\n"
					"Usable commands:\n"
					"  $continue      Continue running Icinga 2 (script debugger).\n"
					"  $quit, $exit   Stop debugging and quit the console.\n"
					"  $help          Print this help.\n\n"
					"For more information on how to use this console, please consult the documentation at https://icinga.com/docs\n";
			else
				std::cout << "Unknown debugger command: " << line << "\n";

			continue;
		}

		if (!command.empty())
			command += "\n";

		command += line;

		std::unique_ptr<Expression> expr;

		try {
			lines[fileName] = command;

			Value result;

			if (!l_ApiClient) {
				expr = ConfigCompiler::CompileText(fileName, command);

				/* This relies on the fact that - for syntax errors - CompileText()
				 * returns an AST where the top-level expression is a 'throw'. */
				if (!syntaxOnly || dynamic_cast<ThrowExpression *>(expr.get())) {
					if (syntaxOnly)
						std::cerr << "    => " << command << std::endl;
					result = Serialize(expr->Evaluate(scriptFrame), 0);
				} else
					result = true;
			} else {
				boost::mutex mutex;
				boost::condition_variable cv;
				bool ready = false;
				boost::exception_ptr eptr;

				l_ApiClient->ExecuteScript(l_Session, command, scriptFrame.Sandboxed,
					std::bind(&ConsoleCommand::ExecuteScriptCompletionHandler,
					std::ref(mutex), std::ref(cv), std::ref(ready),
					_1, _2,
					std::ref(result), std::ref(eptr)));

				{
					boost::mutex::scoped_lock lock(mutex);
					while (!ready)
						cv.wait(lock);
				}

				if (eptr)
					boost::rethrow_exception(eptr);
			}

			if (commandOnce.IsEmpty()) {
				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() && commandOnce.IsEmpty()) {
				continuation = true;
				goto incomplete;
			}

			DebugInfo di = ex.GetDebugInfo();

			if (commandOnceFileName.IsEmpty() && lines.find(di.Path) != lines.end()) {
				String text = lines[di.Path];

				std::vector<String> ulines = text.Split("\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 {
				ShowCodeLocation(std::cout, di);
			}

			std::cout << ex.what() << "\n";

			if (!commandOnce.IsEmpty())
				return EXIT_FAILURE;
		} catch (const std::exception& ex) {
			std::cout << "Error: " << DiagnosticInformation(ex) << "\n";

			if (!commandOnce.IsEmpty())
				return EXIT_FAILURE;
		}
	}

	return EXIT_SUCCESS;
}
void InfluxdbWriter::Flush()
{
	String body = boost::algorithm::join(m_DataBuffer, "\n");
	m_DataBuffer.clear();

	Stream::Ptr stream;

	try {
		stream = Connect();
	} catch (const std::exception& ex) {
		Log(LogWarning, "InfluxDbWriter")
			<< "Flush failed, cannot connect to InfluxDB.";
		return;
	}

	if (!stream)
		return;

	Url::Ptr url = new Url();
	url->SetScheme(GetSslEnable() ? "https" : "http");
	url->SetHost(GetHost());
	url->SetPort(GetPort());

	std::vector<String> path;
	path.emplace_back("write");
	url->SetPath(path);

	url->AddQueryElement("db", GetDatabase());
	url->AddQueryElement("precision", "s");
	if (!GetUsername().IsEmpty())
		url->AddQueryElement("u", GetUsername());
	if (!GetPassword().IsEmpty())
		url->AddQueryElement("p", GetPassword());

	HttpRequest req(stream);
	req.RequestMethod = "POST";
	req.RequestUrl = url;

	try {
		req.WriteBody(body.CStr(), body.GetLength());
		req.Finish();
	} catch (const std::exception& ex) {
		Log(LogWarning, "InfluxdbWriter")
			<< "Cannot write to TCP socket on host '" << GetHost() << "' port '" << GetPort() << "'.";
		throw ex;
	}

	HttpResponse resp(stream, req);
	StreamReadContext context;

	try {
		while (resp.Parse(context, true) && !resp.Complete)
			; /* Do nothing */
	} catch (const std::exception& ex) {
		Log(LogWarning, "InfluxdbWriter")
			<< "Failed to parse HTTP response from host '" << GetHost() << "' port '" << GetPort() << "': " << DiagnosticInformation(ex);
		throw ex;
	}

	if (!resp.Complete) {
		Log(LogWarning, "InfluxdbWriter")
			<< "Failed to read a complete HTTP response from the InfluxDB server.";
		return;
	}

	if (resp.StatusCode != 204) {
		Log(LogWarning, "InfluxdbWriter")
			<< "Unexpected response code: " << resp.StatusCode;

		String contentType = resp.Headers->Get("content-type");
		if (contentType != "application/json") {
			Log(LogWarning, "InfluxdbWriter")
				<< "Unexpected Content-Type: " << contentType;
			return;
		}

		size_t responseSize = resp.GetBodySize();
		boost::scoped_array<char> buffer(new char[responseSize + 1]);
		resp.ReadBody(buffer.get(), responseSize);
		buffer.get()[responseSize] = '\0';

		Dictionary::Ptr jsonResponse;
		try {
			jsonResponse = JsonDecode(buffer.get());
		} catch (...) {
			Log(LogWarning, "InfluxdbWriter")
				<< "Unable to parse JSON response:\n" << buffer.get();
			return;
		}

		String error = jsonResponse->Get("error");

		Log(LogCritical, "InfluxdbWriter")
			<< "InfluxDB error message:\n" << error;

		return;
	}
}
void ElasticsearchWriter::SendRequest(const String& body)
{
	namespace beast = boost::beast;
	namespace http = beast::http;

	Url::Ptr url = new Url();

	url->SetScheme(GetEnableTls() ? "https" : "http");
	url->SetHost(GetHost());
	url->SetPort(GetPort());

	std::vector<String> path;

	/* Specify the index path. Best practice is a daily rotation.
	 * Example: http://localhost:9200/icinga2-2017.09.11?pretty=1
	 */
	path.emplace_back(GetIndex() + "-" + Utility::FormatDateTime("%Y.%m.%d", Utility::GetTime()));

	/* ES 6 removes multiple _type mappings: https://www.elastic.co/guide/en/elasticsearch/reference/6.x/removal-of-types.html
	 * Best practice is to statically define 'doc', as ES 5.X does not allow types starting with '_'.
	 */
	path.emplace_back("doc");

	/* Use the bulk message format. */
	path.emplace_back("_bulk");

	url->SetPath(path);

	OptionalTlsStream stream;

	try {
		stream = Connect();
	} catch (const std::exception& ex) {
		Log(LogWarning, "ElasticsearchWriter")
			<< "Flush failed, cannot connect to Elasticsearch: " << DiagnosticInformation(ex, false);
		return;
	}

	Defer s ([&stream]() {
		if (stream.first) {
			stream.first->next_layer().shutdown();
		}
	});

	http::request<http::string_body> request (http::verb::post, std::string(url->Format(true)), 10);

	request.set(http::field::user_agent, "Icinga/" + Application::GetAppVersion());
	request.set(http::field::host, url->GetHost() + ":" + url->GetPort());

	/* Specify required headers by Elasticsearch. */
	request.set(http::field::accept, "application/json");

	/* Use application/x-ndjson for bulk streams. While ES
	 * is able to handle application/json, the newline separator
	 * causes problems with Logstash (#6609).
	 */
	request.set(http::field::content_type, "application/x-ndjson");

	/* Send authentication if configured. */
	String username = GetUsername();
	String password = GetPassword();

	if (!username.IsEmpty() && !password.IsEmpty())
		request.set(http::field::authorization, "Basic " + Base64::Encode(username + ":" + password));

	request.body() = body;
	request.set(http::field::content_length, request.body().size());

	/* Don't log the request body to debug log, this is already done above. */
	Log(LogDebug, "ElasticsearchWriter")
		<< "Sending " << request.method_string() << " request" << ((!username.IsEmpty() && !password.IsEmpty()) ? " with basic auth" : "" )
		<< " to '" << url->Format() << "'.";

	try {
		if (stream.first) {
			http::write(*stream.first, request);
			stream.first->flush();
		} else {
			http::write(*stream.second, request);
			stream.second->flush();
		}
	} catch (const std::exception&) {
		Log(LogWarning, "ElasticsearchWriter")
			<< "Cannot write to HTTP API on host '" << GetHost() << "' port '" << GetPort() << "'.";
		throw;
	}

	http::parser<false, http::string_body> parser;
	beast::flat_buffer buf;

	try {
		if (stream.first) {
			http::read(*stream.first, buf, parser);
		} else {
			http::read(*stream.second, buf, parser);
		}
	} catch (const std::exception& ex) {
		Log(LogWarning, "ElasticsearchWriter")
			<< "Failed to parse HTTP response from host '" << GetHost() << "' port '" << GetPort() << "': " << DiagnosticInformation(ex, false);
		throw;
	}

	auto& response (parser.get());

	if (response.result_int() > 299) {
		if (response.result() == http::status::unauthorized) {
			/* More verbose error logging with Elasticsearch is hidden behind a proxy. */
			if (!username.IsEmpty() && !password.IsEmpty()) {
				Log(LogCritical, "ElasticsearchWriter")
					<< "401 Unauthorized. Please ensure that the user '" << username
					<< "' is able to authenticate against the HTTP API/Proxy.";
			} else {
				Log(LogCritical, "ElasticsearchWriter")
					<< "401 Unauthorized. The HTTP API requires authentication but no username/password has been configured.";
			}

			return;
		}

		std::ostringstream msgbuf;
		msgbuf << "Unexpected response code " << response.result_int() << " from URL '" << url->Format() << "'";

		auto& contentType (response[http::field::content_type]);

		if (contentType != "application/json" && contentType != "application/json; charset=utf-8") {
			msgbuf << "; Unexpected Content-Type: '" << contentType << "'";
		}

		auto& body (response.body());

#ifdef I2_DEBUG
		msgbuf << "; Response body: '" << body << "'";
#endif /* I2_DEBUG */

		Dictionary::Ptr jsonResponse;

		try {
			jsonResponse = JsonDecode(body);
		} catch (...) {
			Log(LogWarning, "ElasticsearchWriter")
				<< "Unable to parse JSON response:\n" << body;
			return;
		}

		String error = jsonResponse->Get("error");

		Log(LogCritical, "ElasticsearchWriter")
			<< "Error: '" << error << "'. " << msgbuf.str();
	}
}