From a4081f1445d5b7dc53fcebbb6c6220ef462ddaec Mon Sep 17 00:00:00 2001 From: Gunnar Beutner Date: Mon, 6 Oct 2014 14:21:18 +0200 Subject: [PATCH] Implement support for CLI commands fixes #7246 --- debian/icinga2-common.icinga2.init | 6 +- debian/icinga2-common.install | 1 + etc/CMakeLists.txt | 5 + etc/bash_completion.d/icinga2 | 11 + etc/initsystem/icinga2.init.d.cmake | 4 +- etc/initsystem/icinga2.service.cmake | 2 +- icinga-app/icinga.cpp | 551 +++++---------------------- icinga2.spec | 1 + lib/CMakeLists.txt | 1 + lib/base/CMakeLists.txt | 2 +- lib/base/application.cpp | 2 + lib/base/clicommand.cpp | 211 ++++++++++ lib/base/clicommand.hpp | 74 ++++ lib/base/utility.cpp | 2 +- lib/cli/CMakeLists.txt | 42 ++ lib/cli/cainitcommand.cpp | 54 +++ lib/cli/cainitcommand.hpp | 48 +++ lib/cli/daemoncommand.cpp | 446 ++++++++++++++++++++++ lib/cli/daemoncommand.hpp | 48 +++ 19 files changed, 1057 insertions(+), 454 deletions(-) create mode 100644 etc/bash_completion.d/icinga2 create mode 100644 lib/base/clicommand.cpp create mode 100644 lib/base/clicommand.hpp create mode 100644 lib/cli/CMakeLists.txt create mode 100644 lib/cli/cainitcommand.cpp create mode 100644 lib/cli/cainitcommand.hpp create mode 100644 lib/cli/daemoncommand.cpp create mode 100644 lib/cli/daemoncommand.hpp diff --git a/debian/icinga2-common.icinga2.init b/debian/icinga2-common.icinga2.init index 5876b6dee..e0d3e2711 100644 --- a/debian/icinga2-common.icinga2.init +++ b/debian/icinga2-common.icinga2.init @@ -53,7 +53,7 @@ check_run () { } check_config () { - $DAEMON --validate -u "$DAEMON_USER" -g "$DAEMON_GROUP" -c "$DAEMON_CONFIG" + $DAEMON run --validate -u "$DAEMON_USER" -g "$DAEMON_GROUP" -c "$DAEMON_CONFIG" } # @@ -69,7 +69,7 @@ do_start() start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ - -c "$DAEMON_CONFIG" -u "$DAEMON_USER" -g "$DAEMON_GROUP" -d $DAEMON_ARGS \ + run -c "$DAEMON_CONFIG" -u "$DAEMON_USER" -g "$DAEMON_GROUP" -d $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend @@ -84,7 +84,7 @@ do_foreground() start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test \ || return 1 start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON -- \ - -c "$DAEMON_CONFIG" -u "$DAEMON_USER" -g "$DAEMON_GROUP" $DAEMON_ARGS \ + run -c "$DAEMON_CONFIG" -u "$DAEMON_USER" -g "$DAEMON_GROUP" $DAEMON_ARGS \ || return 2 } diff --git a/debian/icinga2-common.install b/debian/icinga2-common.install index ed6c8436d..9380cb74e 100644 --- a/debian/icinga2-common.install +++ b/debian/icinga2-common.install @@ -1,6 +1,7 @@ debian/config/apt.conf etc/icinga2/conf.d/hosts/localhost debian/tmp/etc/icinga2 debian/tmp/etc/logrotate.d +debian/tmp/etc/bash_completion.d tools/syntax/* usr/share/icinga2-common/syntax usr/bin/icinga2-build* usr/bin/icinga2-sign-key diff --git a/etc/CMakeLists.txt b/etc/CMakeLists.txt index ef9411026..0145a39c5 100644 --- a/etc/CMakeLists.txt +++ b/etc/CMakeLists.txt @@ -68,6 +68,11 @@ if(NOT WIN32) install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E create_symlink ../features-available/checker.conf \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_SYSCONFDIR}/icinga2/features-enabled/checker.conf\")") install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E create_symlink ../features-available/notification.conf \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_SYSCONFDIR}/icinga2/features-enabled/notification.conf\")") install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E create_symlink ../features-available/mainlog.conf \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_SYSCONFDIR}/icinga2/features-enabled/mainlog.conf\")") + + install( + FILES bash_completion.d/icinga2 + DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/bash_completion.d + ) else() install( FILES icinga2/features-enabled/checker.conf icinga2/features-enabled/notification.conf diff --git a/etc/bash_completion.d/icinga2 b/etc/bash_completion.d/icinga2 new file mode 100644 index 000000000..0af84850c --- /dev/null +++ b/etc/bash_completion.d/icinga2 @@ -0,0 +1,11 @@ +_icinga2() +{ + local cur opts + opts="${COMP_WORDS[*]}" + cur="${COMP_WORDS[COMP_CWORD]}" + COMPREPLY=($(icinga2 --autocomplete ${COMP_WORDS[*]:1} < /dev/null)) + return 0 +} + +complete -F _icinga2 icinga2 + diff --git a/etc/initsystem/icinga2.init.d.cmake b/etc/initsystem/icinga2.init.d.cmake index ad61fb276..08b434c56 100644 --- a/etc/initsystem/icinga2.init.d.cmake +++ b/etc/initsystem/icinga2.init.d.cmake @@ -48,7 +48,7 @@ start() { printf "Starting Icinga 2: " @CMAKE_INSTALL_FULL_SBINDIR@/icinga2-prepare-dirs $SYSCONFIGFILE - if ! $DAEMON -c $ICINGA2_CONFIG_FILE -d -e $ICINGA2_ERROR_LOG -u $ICINGA2_USER -g $ICINGA2_GROUP > $ICINGA2_STARTUP_LOG 2>&1; then + if ! $DAEMON daemon -c $ICINGA2_CONFIG_FILE -d -e $ICINGA2_ERROR_LOG -u $ICINGA2_USER -g $ICINGA2_GROUP > $ICINGA2_STARTUP_LOG 2>&1; then echo "Error starting Icinga. Check '$ICINGA2_STARTUP_LOG' for details." exit 1 else @@ -111,7 +111,7 @@ reload() { checkconfig() { printf "Checking configuration: " - if ! $DAEMON -c $ICINGA2_CONFIG_FILE -C -u $ICINGA2_USER -g $ICINGA2_GROUP > $ICINGA2_STARTUP_LOG 2>&1; then + if ! $DAEMON daemon -c $ICINGA2_CONFIG_FILE -C -u $ICINGA2_USER -g $ICINGA2_GROUP > $ICINGA2_STARTUP_LOG 2>&1; then if [ "x$1" = "x" ]; then cat $ICINGA2_STARTUP_LOG echo "Icinga 2 detected configuration errors. Check '$ICINGA2_STARTUP_LOG' for details." diff --git a/etc/initsystem/icinga2.service.cmake b/etc/initsystem/icinga2.service.cmake index 815d57338..d97027024 100644 --- a/etc/initsystem/icinga2.service.cmake +++ b/etc/initsystem/icinga2.service.cmake @@ -6,7 +6,7 @@ After=syslog.target postgresql.service mariadb.service carbon-cache.service Type=forking EnvironmentFile=@ICINGA2_SYSCONFIGFILE@ ExecStartPre=@CMAKE_INSTALL_FULL_SBINDIR@/icinga2-prepare-dirs @ICINGA2_SYSCONFIGFILE@ -ExecStart=@CMAKE_INSTALL_FULL_SBINDIR@/icinga2 -c ${ICINGA2_CONFIG_FILE} -d -e ${ICINGA2_ERROR_LOG} -u ${ICINGA2_USER} -g ${ICINGA2_GROUP} +ExecStart=@CMAKE_INSTALL_FULL_SBINDIR@/icinga2 daemon -c ${ICINGA2_CONFIG_FILE} -d -e ${ICINGA2_ERROR_LOG} -u ${ICINGA2_USER} -g ${ICINGA2_GROUP} PIDFile=@ICINGA2_RUNDIR@/icinga2/icinga2.pid ExecReload=/bin/kill -HUP $MAINPID diff --git a/icinga-app/icinga.cpp b/icinga-app/icinga.cpp index 9a9bff17f..771ff4665 100644 --- a/icinga-app/icinga.cpp +++ b/icinga-app/icinga.cpp @@ -28,249 +28,20 @@ #include "base/convert.hpp" #include "base/scriptvariable.hpp" #include "base/context.hpp" +#include "base/clicommand.hpp" #include "config.h" #include #include #include -#include - -#ifndef _WIN32 -# include -# include -# include -#endif /* _WIN32 */ using namespace icinga; namespace po = boost::program_options; -static po::variables_map g_AppParams; - #ifdef _WIN32 SERVICE_STATUS l_SvcStatus; SERVICE_STATUS_HANDLE l_SvcStatusHandle; #endif /* _WIN32 */ -static String LoadAppType(const String& typeSpec) -{ - Log(LogInformation, "icinga-app", "Loading application type: " + typeSpec); - - String::SizeType index = typeSpec.FindFirstOf('/'); - - if (index == String::NPos) - return typeSpec; - - String library = typeSpec.SubStr(0, index); - - (void) Utility::LoadExtensionLibrary(library); - - return typeSpec.SubStr(index + 1); -} - -static void IncludeZoneDirRecursive(const String& path) -{ - String zoneName = Utility::BaseName(path); - Utility::GlobRecursive(path, "*.conf", boost::bind(&ConfigCompiler::CompileFile, _1, zoneName), GlobFile); -} - -static void IncludeNonLocalZone(const String& zonePath) -{ - String etcPath = Application::GetZonesDir() + "/" + Utility::BaseName(zonePath); - - if (Utility::PathExists(etcPath)) - return; - - IncludeZoneDirRecursive(zonePath); -} - -static bool LoadConfigFiles(const String& appType, const String& objectsFile = String()) -{ - ConfigCompilerContext::GetInstance()->Reset(); - - if (g_AppParams.count("config") > 0) { - BOOST_FOREACH(const String& configPath, g_AppParams["config"].as >()) { - ConfigCompiler::CompileFile(configPath); - } - } else if (!g_AppParams.count("no-config")) - ConfigCompiler::CompileFile(Application::GetSysconfDir() + "/icinga2/icinga2.conf"); - - /* Load cluster config files - this should probably be in libremote but - * unfortunately moving it there is somewhat non-trivial. */ - String zonesEtcDir = Application::GetZonesDir(); - if (!zonesEtcDir.IsEmpty() && Utility::PathExists(zonesEtcDir)) - Utility::Glob(zonesEtcDir + "/*", &IncludeZoneDirRecursive, GlobDirectory); - - String zonesVarDir = Application::GetLocalStateDir() + "/lib/icinga2/api/zones"; - if (Utility::PathExists(zonesVarDir)) - Utility::Glob(zonesVarDir + "/*", &IncludeNonLocalZone, GlobDirectory); - - String name, fragment; - BOOST_FOREACH(boost::tie(name, fragment), ConfigFragmentRegistry::GetInstance()->GetItems()) { - ConfigCompiler::CompileText(name, fragment); - } - - ConfigItemBuilder::Ptr builder = make_shared(); - builder->SetType(appType); - builder->SetName("application"); - ConfigItem::Ptr item = builder->Compile(); - item->Register(); - - bool result = ConfigItem::ValidateItems(objectsFile); - - int warnings = 0, errors = 0; - - BOOST_FOREACH(const ConfigCompilerMessage& message, ConfigCompilerContext::GetInstance()->GetMessages()) { - std::ostringstream locbuf; - ShowCodeFragment(locbuf, message.Location, true); - String location = locbuf.str(); - - String logmsg; - - if (!location.IsEmpty()) - logmsg = "Location:\n" + location; - - logmsg += String("\nConfig ") + (message.Error ? "error" : "warning") + ": " + message.Text; - - if (message.Error) { - Log(LogCritical, "config", logmsg); - errors++; - } else { - Log(LogWarning, "config", logmsg); - warnings++; - } - } - - if (warnings > 0 || errors > 0) { - LogSeverity severity; - - if (errors == 0) - severity = LogWarning; - else - severity = LogCritical; - - Log(severity, "config", Convert::ToString(errors) + " errors, " + Convert::ToString(warnings) + " warnings."); - } - - if (!result) - return false; - - return true; -} - -#ifndef _WIN32 -static void SigHupHandler(int) -{ - Application::RequestRestart(); -} -#endif /* _WIN32 */ - -static bool Daemonize(void) -{ -#ifndef _WIN32 - pid_t pid = fork(); - if (pid == -1) { - return false; - } - - if (pid) { - // systemd requires that the pidfile of the daemon is written before the forking - // process terminates. So wait till either the forked daemon has written a pidfile or died. - - int status; - int ret; - pid_t readpid; - do { - Utility::Sleep(0.1); - - readpid = Application::ReadPidFile(Application::GetPidPath()); - ret = waitpid(pid, &status, WNOHANG); - } while (readpid != pid && ret == 0); - - if (ret == pid) { - Log(LogCritical, "icinga-app", "The daemon could not be started. See log output for details."); - exit(EXIT_FAILURE); - } else if (ret == -1) { - std::ostringstream msgbuf; - msgbuf << "waitpid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; - Log(LogCritical, "icinga-app", msgbuf.str()); - exit(EXIT_FAILURE); - } - - exit(0); - } -#endif /* _WIN32 */ - - return true; -} - -static bool SetDaemonIO(const String& stderrFile) -{ -#ifndef _WIN32 - int fdnull = open("/dev/null", O_RDWR); - if (fdnull >= 0) { - if (fdnull != 0) - dup2(fdnull, 0); - - if (fdnull != 1) - dup2(fdnull, 1); - - if (fdnull > 1) - close(fdnull); - } - - const char *errPath = "/dev/null"; - - if (!stderrFile.IsEmpty()) - errPath = stderrFile.CStr(); - - int fderr = open(errPath, O_WRONLY | O_APPEND); - - if (fderr < 0 && errno == ENOENT) - fderr = open(errPath, O_CREAT | O_WRONLY | O_APPEND, 0600); - - if (fderr > 0) { - if (fderr != 2) - dup2(fderr, 2); - - if (fderr > 2) - close(fderr); - } - - pid_t sid = setsid(); - if (sid == -1) { - return false; - } -#endif - - return true; -} - -/** - * Terminate another process and wait till it has ended - * - * @params target PID of the process to end - */ -static void TerminateAndWaitForEnd(pid_t target) -{ -#ifndef _WIN32 - // allow 30 seconds timeout - double timeout = Utility::GetTime() + 30; - - int ret = kill(target, SIGTERM); - - while (Utility::GetTime() < timeout && (ret == 0 || errno != ESRCH)) { - Utility::Sleep(0.1); - ret = kill(target, 0); - } - - // timeout and the process still seems to live: kill it - if (ret == 0 || errno != ESRCH) - kill(target, SIGKILL); - -#else - // TODO: implement this for Win32 -#endif /* _WIN32 */ -} - int Main(void) { int argc = Application::GetArgC(); @@ -329,46 +100,39 @@ int Main(void) Application::DeclareZonesDir(Application::GetSysconfDir() + "/icinga2/zones.d"); Application::DeclareApplicationType("icinga/IcingaApplication"); - po::options_description desc("Supported options"); + LogSeverity logLevel = Logger::GetConsoleLogSeverity(); + Logger::SetConsoleLogSeverity(LogWarning); + + Utility::LoadExtensionLibrary("cli"); + + po::options_description desc("Global options"); + desc.add_options() ("help", "show this help message") ("version,V", "show version information") + ("define,D", po::value >(), "define a constant") ("library,l", po::value >(), "load a library") ("include,I", po::value >(), "add include search directory") - ("define,D", po::value >(), "define a constant") - ("config,c", po::value >(), "parse a configuration file") - ("no-config,z", "start without a configuration file") - ("validate,C", "exit after validating the configuration") ("log-level,x", po::value(), "specify the log level for the console log") - ("errorlog,e", po::value(), "log fatal errors to the specified log file (only works in combination with --daemonize)") -#ifndef _WIN32 - ("reload-internal", po::value(), "used internally to implement config reload: do not call manually, send SIGHUP instead") - ("daemonize,d", "detach from the controlling terminal") - ("user,u", po::value(), "user to run Icinga as") - ("group,g", po::value(), "group to run Icinga as") -# ifdef RLIMIT_STACK - ("no-stack-rlimit", "don't attempt to set RLIMIT_STACK") -# endif /* RLIMIT_STACK */ -#else /* _WIN32 */ - ("scm", "run as a Windows service (must be the first argument if specified)") - ("scm-install", "installs Icinga 2 as a Windows service (must be the first argument if specified") - ("scm-uninstall", "uninstalls the Icinga 2 Windows service (must be the first argument if specified") -#endif /* _WIN32 */ - ; + ("no-stack-rlimit", "used internally, do not specify manually") + ("autocomplete", "auto-complete arguments"); + + String cmdname; + CLICommand::Ptr command; + bool autocomplete; + po::variables_map vm; try { - po::store(po::parse_command_line(argc, argv, desc), g_AppParams); + CLICommand::ParseCommand(argc, argv, desc, vm, cmdname, command, autocomplete); } catch (const std::exception& ex) { std::ostringstream msgbuf; msgbuf << "Error while parsing command-line options: " << ex.what(); - Log(LogCritical, "icinga-app", msgbuf.str()); + Log(LogCritical, "cli_daemon", msgbuf.str()); return EXIT_FAILURE; } - po::notify(g_AppParams); - - if (g_AppParams.count("define")) { - BOOST_FOREACH(const String& define, g_AppParams["define"].as >()) { + if (vm.count("define")) { + BOOST_FOREACH(const String& define, vm["define"].as >()) { String key, value; size_t pos = define.FindFirstOf('='); if (pos != String::NPos) { @@ -381,214 +145,109 @@ int Main(void) ScriptVariable::Set(key, value); } } - + Application::DeclareStatePath(Application::GetLocalStateDir() + "/lib/icinga2/icinga2.state"); Application::DeclareObjectsPath(Application::GetLocalStateDir() + "/cache/icinga2/icinga2.debug"); Application::DeclarePidPath(Application::GetRunDir() + "/icinga2/icinga2.pid"); -#ifndef _WIN32 - if (g_AppParams.count("group")) { - String group = g_AppParams["group"].as(); - - errno = 0; - struct group *gr = getgrnam(group.CStr()); - - if (!gr) { - if (errno == 0) { - std::ostringstream msgbuf; - msgbuf << "Invalid group specified: " + group; - Log(LogCritical, "icinga-app", msgbuf.str()); - return EXIT_FAILURE; - } else { - std::ostringstream msgbuf; - msgbuf << "getgrnam() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; - Log(LogCritical, "icinga-app", msgbuf.str()); - return EXIT_FAILURE; - } - } - - if (!g_AppParams.count("reload-internal") && setgroups(0, NULL) < 0) { - std::ostringstream msgbuf; - msgbuf << "setgroups() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; - Log(LogCritical, "icinga-app", msgbuf.str()); - return EXIT_FAILURE; - } + ConfigCompiler::AddIncludeSearchDir(Application::GetIncludeConfDir()); - if (setgid(gr->gr_gid) < 0) { - std::ostringstream msgbuf; - msgbuf << "setgid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; - Log(LogCritical, "icinga-app", msgbuf.str()); - return EXIT_FAILURE; + if (!autocomplete && vm.count("include")) { + BOOST_FOREACH(const String& includePath, vm["include"].as >()) { + ConfigCompiler::AddIncludeSearchDir(includePath); } } + + Logger::SetConsoleLogSeverity(logLevel); - if (g_AppParams.count("user")) { - String user = g_AppParams["user"].as(); - - errno = 0; - struct passwd *pw = getpwnam(user.CStr()); - - if (!pw) { - if (errno == 0) { - std::ostringstream msgbuf; - msgbuf << "Invalid user specified: " + user; - Log(LogCritical, "icinga-app", msgbuf.str()); - return EXIT_FAILURE; - } else { - std::ostringstream msgbuf; - msgbuf << "getpwnam() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; - Log(LogCritical, "icinga-app", msgbuf.str()); - return EXIT_FAILURE; + if (!autocomplete) { + if (vm.count("log-level")) { + String severity = vm["log-level"].as(); + + LogSeverity logLevel = LogInformation; + try { + logLevel = Logger::StringToSeverity(severity); + } catch (std::exception&) { + /* use the default */ + Log(LogWarning, "icinga", "Invalid log level set. Using default 'information'."); } + + Logger::SetConsoleLogSeverity(logLevel); } - - // also activate the additional groups the configured user is member of - if (!g_AppParams.count("reload-internal") && initgroups(user.CStr(), pw->pw_gid) < 0) { - std::ostringstream msgbuf; - msgbuf << "initgroups() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; - Log(LogCritical, "icinga-app", msgbuf.str()); - return EXIT_FAILURE; - } - - if (setuid(pw->pw_uid) < 0) { - std::ostringstream msgbuf; - msgbuf << "setuid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; - Log(LogCritical, "icinga-app", msgbuf.str()); - return EXIT_FAILURE; - } - } -#endif /* _WIN32 */ - - if (g_AppParams.count("log-level")) { - String severity = g_AppParams["log-level"].as(); - - LogSeverity logLevel = LogInformation; - try { - logLevel = Logger::StringToSeverity(severity); - } catch (std::exception&) { - /* use the default */ - Log(LogWarning, "icinga", "Invalid log level set. Using default 'information'."); - } - - Logger::SetConsoleLogSeverity(logLevel); - } - - if (g_AppParams.count("help") || g_AppParams.count("version")) { - String appName = Utility::BaseName(argv[0]); - - if (appName.GetLength() > 3 && appName.SubStr(0, 3) == "lt-") - appName = appName.SubStr(3, appName.GetLength() - 3); - - std::cout << appName << " " << "- The Icinga 2 network monitoring daemon."; - - if (g_AppParams.count("version")) { - std::cout << " (Version: " << Application::GetVersion() << ")"; - std::cout << std::endl - << "Copyright (c) 2012-2014 Icinga Development Team (http://www.icinga.org)" << std::endl - << "License GPLv2+: GNU GPL version 2 or later " << std::endl - << "This is free software: you are free to change and redistribute it." << std::endl - << "There is NO WARRANTY, to the extent permitted by law."; + + if (vm.count("library")) { + BOOST_FOREACH(const String& libraryName, vm["library"].as >()) { + (void)Utility::LoadExtensionLibrary(libraryName); + } } - - std::cout << std::endl; - - if (g_AppParams.count("version")) { + + if (!command || vm.count("help") || vm.count("version")) { + String appName = Utility::BaseName(Application::GetArgV()[0]); + + if (appName.GetLength() > 3 && appName.SubStr(0, 3) == "lt-") + appName = appName.SubStr(3, appName.GetLength() - 3); + + std::cout << appName << " " << "- The Icinga 2 network monitoring daemon."; + + if (!command || vm.count("help")) { + std::cout << std::endl << std::endl + << "Usage:" << std::endl + << " " << argv[0] << " "; + + if (cmdname.IsEmpty()) + std::cout << ""; + else + std::cout << cmdname; + + std::cout << " []"; + + if (command) { + std::cout << std::endl << std::endl + << command->GetDescription(); + } + } + + if (vm.count("version")) { + std::cout << " (Version: " << Application::GetVersion() << ")"; + std::cout << std::endl + << "Copyright (c) 2012-2014 Icinga Development Team (http://www.icinga.org)" << std::endl + << "License GPLv2+: GNU GPL version 2 or later " << std::endl + << "This is free software: you are free to change and redistribute it." << std::endl + << "There is NO WARRANTY, to the extent permitted by law."; + } + std::cout << std::endl; - - Application::DisplayInfoMessage(true); - - return EXIT_SUCCESS; - } - } - - if (g_AppParams.count("help")) { - std::cout << std::endl - << desc << std::endl - << "Report bugs at " << std::endl - << "Icinga home page: " << std::endl; - return EXIT_SUCCESS; - } - - ScriptVariable::Set("UseVfork", true, false, true); - - Application::MakeVariablesConstant(); - - Log(LogInformation, "icinga-app", "Icinga application loader (version: " + Application::GetVersion() + ")"); - - String appType = LoadAppType(Application::GetApplicationType()); - - if (g_AppParams.count("library")) { - BOOST_FOREACH(const String& libraryName, g_AppParams["library"].as >()) { - (void)Utility::LoadExtensionLibrary(libraryName); - } - } - - ConfigCompiler::AddIncludeSearchDir(Application::GetIncludeConfDir()); - - if (g_AppParams.count("include")) { - BOOST_FOREACH(const String& includePath, g_AppParams["include"].as >()) { - ConfigCompiler::AddIncludeSearchDir(includePath); - } - } - - if (!g_AppParams.count("validate") && !g_AppParams.count("reload-internal")) { - pid_t runningpid = Application::ReadPidFile(Application::GetPidPath()); - if (runningpid > 0) { - Log(LogCritical, "icinga-app", "Another instance of Icinga already running with PID " + Convert::ToString(runningpid)); - return EXIT_FAILURE; + + if (vm.count("version")) { + std::cout << std::endl; + + Application::DisplayInfoMessage(true); + + return EXIT_SUCCESS; + } } - } - - if (!LoadConfigFiles(appType, Application::GetObjectsPath())) - return EXIT_FAILURE; - - if (g_AppParams.count("validate")) { - Log(LogInformation, "icinga-app", "Finished validating the configuration file(s)."); - return EXIT_SUCCESS; - } - - if(g_AppParams.count("reload-internal")) { - int parentpid = g_AppParams["reload-internal"].as(); - Log(LogInformation, "icinga-app", "Terminating previous instance of Icinga (PID " + Convert::ToString(parentpid) + ")"); - TerminateAndWaitForEnd(parentpid); - Log(LogInformation, "icinga-app", "Previous instance has ended, taking over now."); - } - - if (g_AppParams.count("daemonize")) { - if (!g_AppParams.count("reload-internal")) { - // no additional fork neccessary on reload - try { - Daemonize(); - } catch (std::exception&) { - Log(LogCritical, "icinga-app", "Daemonize failed. Exiting."); - return EXIT_FAILURE; + + if (!command || vm.count("help")) { + if (!command) { + std::cout << std::endl; + CLICommand::ShowCommands(argc, argv, NULL, false); } + + std::cout << std::endl + << desc << std::endl + << "Report bugs at " << std::endl + << "Icinga home page: " << std::endl; + return EXIT_SUCCESS; } } - // activate config only after daemonization: it starts threads and that is not compatible with fork() - if (!ConfigItem::ActivateItems()) { - Log(LogCritical, "icinga-app", "Error activating configuration."); - return EXIT_FAILURE; - } - - if (g_AppParams.count("daemonize")) { - String errorLog; - if (g_AppParams.count("errorlog")) - errorLog = g_AppParams["errorlog"].as(); - - SetDaemonIO(errorLog); - Logger::DisableConsoleLog(); - } - -#ifndef _WIN32 - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = &SigHupHandler; - sigaction(SIGHUP, &sa, NULL); -#endif /* _WIN32 */ + int rc = 1; - int rc = Application::GetInstance()->Run(); + if (autocomplete) { + CLICommand::ShowCommands(argc, argv, &desc, true); + rc = 0; + } else if (command) + rc = command->Run(vm); #ifndef _DEBUG Application::Exit(rc); diff --git a/icinga2.spec b/icinga2.spec index adb6578d8..63d25068b 100644 --- a/icinga2.spec +++ b/icinga2.spec @@ -506,6 +506,7 @@ exit 0 %doc COPYING COPYING.Exceptions README.md NEWS AUTHORS ChangeLog tools/syntax %attr(0750,%{icinga_user},%{icingacmd_group}) %dir %{_localstatedir}/log/%{name} %config(noreplace) %{_sysconfdir}/logrotate.d/%{name} +%{_sysconfdir}/bash_completion.d/%{name} %attr(0750,%{icinga_user},%{icinga_group}) %dir %{_localstatedir}/spool/%{name} %attr(0750,%{icinga_user},%{icinga_group}) %dir %{_localstatedir}/spool/%{name}/perfdata %attr(0750,%{icinga_user},%{icinga_group}) %dir %{_localstatedir}/spool/%{name}/tmp diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 9a70c2377..ca35ac0e6 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -16,6 +16,7 @@ # Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. add_subdirectory(base) +add_subdirectory(cli) add_subdirectory(config) add_subdirectory(icinga) add_subdirectory(db_ido) diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt index 3707587ef..2374c6716 100644 --- a/lib/base/CMakeLists.txt +++ b/lib/base/CMakeLists.txt @@ -23,7 +23,7 @@ mkclass_target(streamlogger.ti streamlogger.thpp) mkclass_target(sysloglogger.ti sysloglogger.thpp) set(base_SOURCES - application.cpp application.thpp array.cpp configerror.cpp context.cpp + application.cpp application.thpp array.cpp clicommand.cpp configerror.cpp context.cpp convert.cpp debuginfo.cpp dictionary.cpp dynamicobject.cpp dynamicobject.thpp dynamictype.cpp exception.cpp fifo.cpp filelogger.cpp filelogger.thpp logger.cpp logger.thpp netstring.cpp networkstream.cpp object.cpp objectlock.cpp process.cpp diff --git a/lib/base/application.cpp b/lib/base/application.cpp index 8d5365bc7..062db1aae 100644 --- a/lib/base/application.cpp +++ b/lib/base/application.cpp @@ -135,8 +135,10 @@ void Application::InitializeBase(void) maxfds = 65536; for (rlim_t i = 3; i < maxfds; i++) { +#ifdef _DEBUG if (close(i) >= 0) std::cerr << "Closed FD " << i << " which we inherited from our parent process." << std::endl; +#endif /* _DEBUG */ } } #endif /* _WIN32 */ diff --git a/lib/base/clicommand.cpp b/lib/base/clicommand.cpp new file mode 100644 index 000000000..696eab613 --- /dev/null +++ b/lib/base/clicommand.cpp @@ -0,0 +1,211 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2014 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 "base/clicommand.hpp" +#include "base/logger_fwd.hpp" +#include +#include +#include +#include +#include +#include + +using namespace icinga; +namespace po = boost::program_options; + +boost::mutex l_RegistryMutex; +std::map, CLICommand::Ptr> l_Registry; + +CLICommand::Ptr CLICommand::GetByName(const std::vector& name) +{ + boost::mutex::scoped_lock lock(l_RegistryMutex); + + std::map, CLICommand::Ptr>::const_iterator it = l_Registry.find(name); + + if (it == l_Registry.end()) + return CLICommand::Ptr(); + + return it->second; +} + +void CLICommand::Register(const std::vector& name, const CLICommand::Ptr& function) +{ + boost::mutex::scoped_lock lock(l_RegistryMutex); + l_Registry[name] = function; +} + +void CLICommand::Unregister(const std::vector& name) +{ + boost::mutex::scoped_lock lock(l_RegistryMutex); + l_Registry.erase(name); +} + +RegisterCLICommandHelper::RegisterCLICommandHelper(const String& name, const CLICommand::Ptr& command) +{ + std::vector vname; + boost::algorithm::split(vname, name, boost::is_any_of("/")); + CLICommand::Register(vname, command); +} + +bool CLICommand::ParseCommand(int argc, char **argv, po::options_description& desc, po::variables_map& vm, + String& cmdname, CLICommand::Ptr& command, bool& autocomplete) +{ + boost::mutex::scoped_lock lock(l_RegistryMutex); + + typedef std::map, CLICommand::Ptr>::value_type CLIKeyValue; + + std::vector best_match; + int arg_end = 1; + + BOOST_FOREACH(const CLIKeyValue& kv, l_Registry) { + const std::vector& vname = kv.first; + + for (int i = 0, k = 1; i < vname.size() && k < argc; i++, k++) { + if (strcmp(argv[k], "--no-stack-rlimit") == 0 || strcmp(argv[k], "--autocomplete") == 0) { + if (strcmp(argv[k], "--autocomplete") == 0) { + autocomplete = true; + return false; + } + + i--; + continue; + } + + if (vname[i] != argv[k]) + break; + + if (i >= best_match.size()) + best_match.push_back(vname[i]); + + if (i == vname.size() - 1) { + cmdname = boost::algorithm::join(vname, " "); + command = kv.second; + arg_end = k; + goto found_command; + } + } + } + +found_command: + lock.unlock(); + + po::options_description ldesc("Command options"); + + if (command) + command->InitParameters(ldesc); + + desc.add(ldesc); + + po::store(po::parse_command_line(argc - arg_end, argv + arg_end, desc), vm); + po::notify(vm); + + return true; +} + +void CLICommand::ShowCommands(int argc, char **argv, po::options_description *desc, bool autocomplete) +{ + boost::mutex::scoped_lock lock(l_RegistryMutex); + + typedef std::map, CLICommand::Ptr>::value_type CLIKeyValue; + + std::vector best_match; + int arg_begin = 0; + CLICommand::Ptr command; + + BOOST_FOREACH(const CLIKeyValue& kv, l_Registry) { + const std::vector& vname = kv.first; + + arg_begin = 0; + + for (int i = 0, k = 1; i < vname.size() && k < argc; i++, k++) { + if (strcmp(argv[k], "--no-stack-rlimit") == 0 || strcmp(argv[k], "--autocomplete") == 0) { + i--; + arg_begin++; + continue; + } + + if (vname[i] != argv[k]) + break; + + if (i >= best_match.size()) { + best_match.push_back(vname[i]); + } + + if (i == vname.size() - 1) { + command = kv.second; + break; + } + } + } + + if (!autocomplete) + std::cout << "Supported commands: " << std::endl; + + BOOST_FOREACH(const CLIKeyValue& kv, l_Registry) { + const std::vector& vname = kv.first; + + if (vname.size() < best_match.size()) + continue; + + bool match = true; + + for (int i = 0; i < best_match.size(); i++) { + if (vname[i] != best_match[i]) { + match = false; + break; + } + } + + if (!match) + continue; + + if (autocomplete) { + if (best_match.size() < vname.size()) { + String cname = vname[best_match.size()]; + String pname; + + if (arg_begin + best_match.size() + 1 < argc) + pname = argv[arg_begin + best_match.size() + 1]; + + if (cname.Find(pname) == 0) + std::cout << vname[best_match.size()] << " "; + } + } else + std::cout << " * " << boost::algorithm::join(vname, " ") << " (" << kv.second->GetShortDescription() << ")" << std::endl; + } + + if (command && autocomplete) { + po::options_description ldesc("Command options"); + + if (command) + command->InitParameters(ldesc); + + desc->add(ldesc); + + BOOST_FOREACH(const shared_ptr& odesc, desc->options()) { + String cname = "--" + odesc->long_name(); + String pname = argv[argc - 1]; + + if (cname.Find(pname) == 0) + std::cout << cname << " "; + } + } + + return; +} diff --git a/lib/base/clicommand.hpp b/lib/base/clicommand.hpp new file mode 100644 index 000000000..ee7d1c6d6 --- /dev/null +++ b/lib/base/clicommand.hpp @@ -0,0 +1,74 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2014 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 CLICOMMAND_H +#define CLICOMMAND_H + +#include "base/i2-base.hpp" +#include "base/value.hpp" +#include "base/utility.hpp" +#include +#include + +namespace icinga +{ + +/** + * A CLI command. + * + * @ingroup base + */ +class I2_BASE_API CLICommand : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(CLICommand); + + virtual String GetDescription(void) const = 0; + virtual String GetShortDescription(void) const = 0; + virtual void InitParameters(boost::program_options::options_description& desc) const = 0; + virtual int Run(const boost::program_options::variables_map& vm) const = 0; + + static CLICommand::Ptr GetByName(const std::vector& name); + static void Register(const std::vector& name, const CLICommand::Ptr& command); + static void Unregister(const std::vector& name); + + static bool ParseCommand(int argc, char **argv, boost::program_options::options_description& desc, + boost::program_options::variables_map& vm, String& cmdname, CLICommand::Ptr& command, bool& autocomplete); + static void ShowCommands(int argc, char **argv, boost::program_options::options_description *desc, bool autocomplete); +}; + +/** + * Helper class for registering CLICommand implementation classes. + * + * @ingroup base + */ +class I2_BASE_API RegisterCLICommandHelper +{ +public: + RegisterCLICommandHelper(const String& name, const CLICommand::Ptr& command); +}; + +#define REGISTER_CLICOMMAND(name, klass) \ + namespace { namespace UNIQUE_NAME(cli) { \ + I2_EXPORT icinga::RegisterCLICommandHelper l_RegisterCLICommand(name, make_shared()); \ + } } + +} + +#endif /* CLICOMMAND_H */ diff --git a/lib/base/utility.cpp b/lib/base/utility.cpp index fc348bcea..eeadf1178 100644 --- a/lib/base/utility.cpp +++ b/lib/base/utility.cpp @@ -1001,7 +1001,7 @@ String Utility::GetFQDN(void) addrinfo *result; int rc = getaddrinfo(hostname.CStr(), NULL, &hints, &result); - if (rc < 0) + if (rc != 0) result = NULL; String canonicalName; diff --git a/lib/cli/CMakeLists.txt b/lib/cli/CMakeLists.txt new file mode 100644 index 000000000..ca9a1147b --- /dev/null +++ b/lib/cli/CMakeLists.txt @@ -0,0 +1,42 @@ +# Icinga 2 +# Copyright (C) 2012-2014 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. + +set(cli_SOURCES + cainitcommand.cpp daemoncommand.cpp +) + +if(ICINGA2_UNITY_BUILD) + mkunity_target(cli cli_SOURCES) +endif() + +add_library(cli SHARED ${cli_SOURCES}) + +target_link_libraries(cli ${Boost_LIBRARIES} base config) + +set_target_properties ( + cli PROPERTIES + INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/icinga2 + FOLDER Lib +) + +install( + TARGETS cli + RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/icinga2 +) + + diff --git a/lib/cli/cainitcommand.cpp b/lib/cli/cainitcommand.cpp new file mode 100644 index 000000000..947e16397 --- /dev/null +++ b/lib/cli/cainitcommand.cpp @@ -0,0 +1,54 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2014 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 "cli/cainitcommand.hpp" +#include "base/logger_fwd.hpp" +#include "base/clicommand.hpp" + +using namespace icinga; + +REGISTER_CLICOMMAND("ca/init", CAInitCommand); + +String CAInitCommand::GetDescription(void) const +{ + return "Sets up a new Certificate Authority."; +} + +String CAInitCommand::GetShortDescription(void) const +{ + return "sets up a new CA"; +} + +void CAInitCommand::InitParameters(boost::program_options::options_description& desc) const +{ + /* Command doesn't support any parameters. */ +} + +/** + * The entry point for the "ca init" CLI command. + * + * @returns An exit status. + */ +int CAInitCommand::Run(const boost::program_options::variables_map& vm) const +{ + Log(LogNotice, "cli", "Test!"); + Log(LogInformation, "cli", "Hello World!"); + + return 0; +} diff --git a/lib/cli/cainitcommand.hpp b/lib/cli/cainitcommand.hpp new file mode 100644 index 000000000..5723b7254 --- /dev/null +++ b/lib/cli/cainitcommand.hpp @@ -0,0 +1,48 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2014 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 CAINITCOMMAND_H +#define CAINITCOMMAND_H + +#include "base/qstring.hpp" +#include "base/clicommand.hpp" + +namespace icinga +{ + +/** + * The "ca init" command. + * + * @ingroup cli + */ +class CAInitCommand : public CLICommand +{ +public: + DECLARE_PTR_TYPEDEFS(CAInitCommand); + + virtual String GetDescription(void) const; + virtual String GetShortDescription(void) const; + virtual void InitParameters(boost::program_options::options_description& desc) const; + virtual int Run(const boost::program_options::variables_map& vm) const; + +}; + +} + +#endif /* CAINITCOMMAND_H */ diff --git a/lib/cli/daemoncommand.cpp b/lib/cli/daemoncommand.cpp new file mode 100644 index 000000000..5acd2e212 --- /dev/null +++ b/lib/cli/daemoncommand.cpp @@ -0,0 +1,446 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2014 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 "cli/daemoncommand.hpp" +#include "config/configcompilercontext.hpp" +#include "config/configcompiler.hpp" +#include "config/configitembuilder.hpp" +#include "base/logger_fwd.hpp" +#include "base/clicommand.hpp" +#include "base/application.hpp" +#include "base/logger.hpp" +#include "base/timer.hpp" +#include "base/utility.hpp" +#include "base/exception.hpp" +#include "base/convert.hpp" +#include "base/scriptvariable.hpp" +#include "base/context.hpp" +#include "config.h" +#include +#include +#include +#include + +#ifndef _WIN32 +# include +# include +# include +#endif /* _WIN32 */ + +using namespace icinga; +namespace po = boost::program_options; + +static po::variables_map g_AppParams; + +REGISTER_CLICOMMAND("daemon", DaemonCommand); + +static String LoadAppType(const String& typeSpec) +{ + Log(LogInformation, "cli", "Loading application type: " + typeSpec); + + String::SizeType index = typeSpec.FindFirstOf('/'); + + if (index == String::NPos) + return typeSpec; + + String library = typeSpec.SubStr(0, index); + + (void) Utility::LoadExtensionLibrary(library); + + return typeSpec.SubStr(index + 1); +} + +static void IncludeZoneDirRecursive(const String& path) +{ + String zoneName = Utility::BaseName(path); + Utility::GlobRecursive(path, "*.conf", boost::bind(&ConfigCompiler::CompileFile, _1, zoneName), GlobFile); +} + +static void IncludeNonLocalZone(const String& zonePath) +{ + String etcPath = Application::GetZonesDir() + "/" + Utility::BaseName(zonePath); + + if (Utility::PathExists(etcPath)) + return; + + IncludeZoneDirRecursive(zonePath); +} + +static bool LoadConfigFiles(const boost::program_options::variables_map& vm, const String& appType, const String& objectsFile = String()) +{ + ConfigCompilerContext::GetInstance()->Reset(); + + if (vm.count("config") > 0) { + BOOST_FOREACH(const String& configPath, vm["config"].as >()) { + ConfigCompiler::CompileFile(configPath); + } + } else if (!vm.count("no-config")) + ConfigCompiler::CompileFile(Application::GetSysconfDir() + "/icinga2/icinga2.conf"); + + /* Load cluster config files - this should probably be in libremote but + * unfortunately moving it there is somewhat non-trivial. */ + String zonesEtcDir = Application::GetZonesDir(); + if (!zonesEtcDir.IsEmpty() && Utility::PathExists(zonesEtcDir)) + Utility::Glob(zonesEtcDir + "/*", &IncludeZoneDirRecursive, GlobDirectory); + + String zonesVarDir = Application::GetLocalStateDir() + "/lib/icinga2/api/zones"; + if (Utility::PathExists(zonesVarDir)) + Utility::Glob(zonesVarDir + "/*", &IncludeNonLocalZone, GlobDirectory); + + String name, fragment; + BOOST_FOREACH(boost::tie(name, fragment), ConfigFragmentRegistry::GetInstance()->GetItems()) { + ConfigCompiler::CompileText(name, fragment); + } + + ConfigItemBuilder::Ptr builder = make_shared(); + builder->SetType(appType); + builder->SetName("application"); + ConfigItem::Ptr item = builder->Compile(); + item->Register(); + + bool result = ConfigItem::ValidateItems(objectsFile); + + int warnings = 0, errors = 0; + + BOOST_FOREACH(const ConfigCompilerMessage& message, ConfigCompilerContext::GetInstance()->GetMessages()) { + std::ostringstream locbuf; + ShowCodeFragment(locbuf, message.Location, true); + String location = locbuf.str(); + + String logmsg; + + if (!location.IsEmpty()) + logmsg = "Location:\n" + location; + + logmsg += String("\nConfig ") + (message.Error ? "error" : "warning") + ": " + message.Text; + + if (message.Error) { + Log(LogCritical, "config", logmsg); + errors++; + } else { + Log(LogWarning, "config", logmsg); + warnings++; + } + } + + if (warnings > 0 || errors > 0) { + LogSeverity severity; + + if (errors == 0) + severity = LogWarning; + else + severity = LogCritical; + + Log(severity, "config", Convert::ToString(errors) + " errors, " + Convert::ToString(warnings) + " warnings."); + } + + if (!result) + return false; + + return true; +} + +#ifndef _WIN32 +static void SigHupHandler(int) +{ + Application::RequestRestart(); +} +#endif /* _WIN32 */ + +static bool Daemonize(void) +{ +#ifndef _WIN32 + pid_t pid = fork(); + if (pid == -1) { + return false; + } + + if (pid) { + // systemd requires that the pidfile of the daemon is written before the forking + // process terminates. So wait till either the forked daemon has written a pidfile or died. + + int status; + int ret; + pid_t readpid; + do { + Utility::Sleep(0.1); + + readpid = Application::ReadPidFile(Application::GetPidPath()); + ret = waitpid(pid, &status, WNOHANG); + } while (readpid != pid && ret == 0); + + if (ret == pid) { + Log(LogCritical, "cli", "The daemon could not be started. See log output for details."); + exit(EXIT_FAILURE); + } else if (ret == -1) { + std::ostringstream msgbuf; + msgbuf << "waitpid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; + Log(LogCritical, "cli", msgbuf.str()); + exit(EXIT_FAILURE); + } + + exit(0); + } +#endif /* _WIN32 */ + + return true; +} + +static bool SetDaemonIO(const String& stderrFile) +{ +#ifndef _WIN32 + int fdnull = open("/dev/null", O_RDWR); + if (fdnull >= 0) { + if (fdnull != 0) + dup2(fdnull, 0); + + if (fdnull != 1) + dup2(fdnull, 1); + + if (fdnull > 1) + close(fdnull); + } + + const char *errPath = "/dev/null"; + + if (!stderrFile.IsEmpty()) + errPath = stderrFile.CStr(); + + int fderr = open(errPath, O_WRONLY | O_APPEND); + + if (fderr < 0 && errno == ENOENT) + fderr = open(errPath, O_CREAT | O_WRONLY | O_APPEND, 0600); + + if (fderr > 0) { + if (fderr != 2) + dup2(fderr, 2); + + if (fderr > 2) + close(fderr); + } + + pid_t sid = setsid(); + if (sid == -1) { + return false; + } +#endif + + return true; +} + +/** + * Terminate another process and wait till it has ended + * + * @params target PID of the process to end + */ +static void TerminateAndWaitForEnd(pid_t target) +{ +#ifndef _WIN32 + // allow 30 seconds timeout + double timeout = Utility::GetTime() + 30; + + int ret = kill(target, SIGTERM); + + while (Utility::GetTime() < timeout && (ret == 0 || errno != ESRCH)) { + Utility::Sleep(0.1); + ret = kill(target, 0); + } + + // timeout and the process still seems to live: kill it + if (ret == 0 || errno != ESRCH) + kill(target, SIGKILL); + +#else + // TODO: implement this for Win32 +#endif /* _WIN32 */ +} + +String DaemonCommand::GetDescription(void) const +{ + return "Starts Icinga 2."; +} + +String DaemonCommand::GetShortDescription(void) const +{ + return "starts Icinga 2"; +} + +void DaemonCommand::InitParameters(boost::program_options::options_description& desc) const +{ + desc.add_options() + ("config,c", po::value >(), "parse a configuration file") + ("no-config,z", "start without a configuration file") + ("validate,C", "exit after validating the configuration") + ("errorlog,e", po::value(), "log fatal errors to the specified log file (only works in combination with --daemonize)") +#ifndef _WIN32 + ("reload-internal", po::value(), "used internally to implement config reload: do not call manually, send SIGHUP instead") + ("daemonize,d", "detach from the controlling terminal") + ("user,u", po::value(), "user to run Icinga as") + ("group,g", po::value(), "group to run Icinga as") +#endif /* _WIN32 */ + ; +} + +/** + * The entry point for the "daemon" CLI command. + * + * @returns An exit status. + */ +int DaemonCommand::Run(const po::variables_map& vm) const +{ +#ifndef _WIN32 + if (vm.count("group")) { + String group = vm["group"].as(); + + errno = 0; + struct group *gr = getgrnam(group.CStr()); + + if (!gr) { + if (errno == 0) { + std::ostringstream msgbuf; + msgbuf << "Invalid group specified: " + group; + Log(LogCritical, "cli", msgbuf.str()); + return EXIT_FAILURE; + } else { + std::ostringstream msgbuf; + msgbuf << "getgrnam() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; + Log(LogCritical, "cli", msgbuf.str()); + return EXIT_FAILURE; + } + } + + if (!vm.count("reload-internal") && setgroups(0, NULL) < 0) { + std::ostringstream msgbuf; + msgbuf << "setgroups() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; + Log(LogCritical, "cli", msgbuf.str()); + return EXIT_FAILURE; + } + + if (setgid(gr->gr_gid) < 0) { + std::ostringstream msgbuf; + msgbuf << "setgid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; + Log(LogCritical, "cli", msgbuf.str()); + return EXIT_FAILURE; + } + } + + if (vm.count("user")) { + String user = vm["user"].as(); + + errno = 0; + struct passwd *pw = getpwnam(user.CStr()); + + if (!pw) { + if (errno == 0) { + std::ostringstream msgbuf; + msgbuf << "Invalid user specified: " + user; + Log(LogCritical, "cli", msgbuf.str()); + return EXIT_FAILURE; + } else { + std::ostringstream msgbuf; + msgbuf << "getpwnam() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; + Log(LogCritical, "cli", msgbuf.str()); + return EXIT_FAILURE; + } + } + + // also activate the additional groups the configured user is member of + if (!vm.count("reload-internal") && initgroups(user.CStr(), pw->pw_gid) < 0) { + std::ostringstream msgbuf; + msgbuf << "initgroups() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; + Log(LogCritical, "cli", msgbuf.str()); + return EXIT_FAILURE; + } + + if (setuid(pw->pw_uid) < 0) { + std::ostringstream msgbuf; + msgbuf << "setuid() failed with error code " << errno << ", \"" << Utility::FormatErrorNumber(errno) << "\""; + Log(LogCritical, "cli", msgbuf.str()); + return EXIT_FAILURE; + } + } +#endif /* _WIN32 */ + + ScriptVariable::Set("UseVfork", true, false, true); + + Application::MakeVariablesConstant(); + + Log(LogInformation, "cli", "Icinga application loader (version: " + Application::GetVersion() + ")"); + + String appType = LoadAppType(Application::GetApplicationType()); + + if (!vm.count("validate") && !vm.count("reload-internal")) { + pid_t runningpid = Application::ReadPidFile(Application::GetPidPath()); + if (runningpid > 0) { + Log(LogCritical, "cli", "Another instance of Icinga already running with PID " + Convert::ToString(runningpid)); + return EXIT_FAILURE; + } + } + + if (!LoadConfigFiles(vm, appType, Application::GetObjectsPath())) + return EXIT_FAILURE; + + if (vm.count("validate")) { + Log(LogInformation, "cli", "Finished validating the configuration file(s)."); + return EXIT_SUCCESS; + } + + if(vm.count("reload-internal")) { + int parentpid = vm["reload-internal"].as(); + Log(LogInformation, "cli", "Terminating previous instance of Icinga (PID " + Convert::ToString(parentpid) + ")"); + TerminateAndWaitForEnd(parentpid); + Log(LogInformation, "cli", "Previous instance has ended, taking over now."); + } + + if (vm.count("daemonize")) { + if (!vm.count("reload-internal")) { + // no additional fork neccessary on reload + try { + Daemonize(); + } catch (std::exception&) { + Log(LogCritical, "cli", "Daemonize failed. Exiting."); + return EXIT_FAILURE; + } + } + } + + // activate config only after daemonization: it starts threads and that is not compatible with fork() + if (!ConfigItem::ActivateItems()) { + Log(LogCritical, "cli", "Error activating configuration."); + return EXIT_FAILURE; + } + + if (vm.count("daemonize")) { + String errorLog; + if (vm.count("errorlog")) + errorLog = vm["errorlog"].as(); + + SetDaemonIO(errorLog); + Logger::DisableConsoleLog(); + } + +#ifndef _WIN32 + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = &SigHupHandler; + sigaction(SIGHUP, &sa, NULL); +#endif /* _WIN32 */ + + return Application::GetInstance()->Run(); +} diff --git a/lib/cli/daemoncommand.hpp b/lib/cli/daemoncommand.hpp new file mode 100644 index 000000000..6dee2fc0c --- /dev/null +++ b/lib/cli/daemoncommand.hpp @@ -0,0 +1,48 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2014 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 DAEMONCOMMAND_H +#define DAEMONCOMMAND_H + +#include "base/clicommand.hpp" +#include "base/qstring.hpp" +#include + +namespace icinga +{ + +/** + * The "daemon" CLI command. + * + * @ingroup cli + */ +class DaemonCommand : public CLICommand +{ +public: + DECLARE_PTR_TYPEDEFS(DaemonCommand); + + virtual String GetDescription(void) const; + virtual String GetShortDescription(void) const; + virtual void InitParameters(boost::program_options::options_description& desc) const; + virtual int Run(const boost::program_options::variables_map& vm) const; +}; + +} + +#endif /* DAEMONCOMMAND_H */ -- 2.40.0