From 59b9a7a44870b341a229695a2459e68964157c68 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 6 Jun 2013 12:27:28 +0200 Subject: [PATCH 01/10] Vagrant: Add openldap stack w/ example data refs #4218 --- .vagrant-puppet/manifests/default.pp | 32 ++++++++++- .../modules/openldap/files/db.ldif | 18 ++++++ .../modules/openldap/files/dit.ldif | 4 ++ .../modules/openldap/files/users.ldif | 43 ++++++++++++++ .../modules/openldap/manifests/init.pp | 25 ++++++++ README.md | 57 +++++++++++++++++++ 6 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 .vagrant-puppet/modules/openldap/files/db.ldif create mode 100644 .vagrant-puppet/modules/openldap/files/dit.ldif create mode 100644 .vagrant-puppet/modules/openldap/files/users.ldif create mode 100644 .vagrant-puppet/modules/openldap/manifests/init.pp diff --git a/.vagrant-puppet/manifests/default.pp b/.vagrant-puppet/manifests/default.pp index 58fcec798..b287d137b 100644 --- a/.vagrant-puppet/manifests/default.pp +++ b/.vagrant-puppet/manifests/default.pp @@ -1,6 +1,7 @@ include apache include mysql include pgsql +include openldap Exec { path => '/bin:/usr/bin:/sbin' } @@ -24,8 +25,8 @@ $icinga_packages = [ 'gcc', 'glibc', 'glibc-common', 'gd', 'gd-devel', 'libdbi-dbd-mysql', 'libdbi-dbd-pgsql' ] package { $icinga_packages: ensure => installed } -php::extension { ['php-mysql', 'php-pgsql']: - require => [Class['mysql'], Class['pgsql']] +php::extension { ['php-mysql', 'php-pgsql', 'php-ldap']: + require => [Class['mysql'], Class['pgsql'], Class['openldap']] } group { 'icinga-cmd': @@ -212,3 +213,30 @@ file { '/usr/local/icinga-mysql/etc/modules/mk-livestatus.cfg': require => Cmmi['mk-livestatus'], notify => [Service['icinga-mysql'], Service['ido2db-mysql']] } + +file { 'openldap/db.ldif': + path => '/usr/share/openldap-servers/db.ldif', + source => 'puppet:///modules/openldap/db.ldif', + require => Class['openldap'] +} + +file { 'openldap/dit.ldif': + path => '/usr/share/openldap-servers/dit.ldif', + source => 'puppet:///modules/openldap/dit.ldif', + require => Class['openldap'] +} + +file { 'openldap/users.ldif': + path => '/usr/share/openldap-servers/users.ldif', + source => 'puppet:///modules/openldap/users.ldif', + require => Class['openldap'] +} + +exec { 'populate-openldap': + # TODO: Split the command and use unless instead of trying to populate openldap everytime + command => 'sudo ldapadd -c -Y EXTERNAL -H ldapi:/// -f /usr/share/openldap-servers/db.ldif || true && \ + sudo ldapadd -c -D cn=admin,dc=icinga,dc=org -x -w admin -f /usr/share/openldap-servers/dit.ldif || true && \ + sudo ldapadd -c -D cn=admin,dc=icinga,dc=org -x -w admin -f /usr/share/openldap-servers/users.ldif || true', + require => [Service['slapd'], File['openldap/db.ldif'], + File['openldap/dit.ldif'], File['openldap/users.ldif']] +} diff --git a/.vagrant-puppet/modules/openldap/files/db.ldif b/.vagrant-puppet/modules/openldap/files/db.ldif new file mode 100644 index 000000000..eab9b7bac --- /dev/null +++ b/.vagrant-puppet/modules/openldap/files/db.ldif @@ -0,0 +1,18 @@ +dn: olcDatabase={0}config,cn=config +changetype: modify +replace: olcRootPW +olcRootPW: {SSHA}N/2WMqT8q7cElh7KUQz+p9TJbjmKv/u9 +- +replace: olcRootDN +olcRootDN: cn=admin,cn=config + +dn: olcDatabase={2}bdb,cn=config +changetype: modify +replace: olcRootPW +olcRootPW: {SSHA}MxMpLBo2/TSymoIBf/Sb5iQac7Wwiur5 +- +replace: olcSuffix +olcSuffix: dc=icinga,dc=org +- +replace: olcRootDN +olcRootDN: cn=admin,dc=icinga,dc=org diff --git a/.vagrant-puppet/modules/openldap/files/dit.ldif b/.vagrant-puppet/modules/openldap/files/dit.ldif new file mode 100644 index 000000000..275f339c6 --- /dev/null +++ b/.vagrant-puppet/modules/openldap/files/dit.ldif @@ -0,0 +1,4 @@ +dn: dc=icinga,dc=org +dc: icinga +objectClass: top +objectClass: domain diff --git a/.vagrant-puppet/modules/openldap/files/users.ldif b/.vagrant-puppet/modules/openldap/files/users.ldif new file mode 100644 index 000000000..ba68348ee --- /dev/null +++ b/.vagrant-puppet/modules/openldap/files/users.ldif @@ -0,0 +1,43 @@ +dn: ou=people,dc=icinga,dc=org +ou: people +objectclass: organizationalunit + +dn: ou=groups,dc=icinga,dc=org +ou: groups +objectclass: organizationalunit + +dn: cn=John Doe,ou=people,dc=icinga,dc=org +objectclass: inetOrgPerson +cn: John Doe +sn: Doe +uid: jdoe +userpassword: password + +dn: cn=Jane Smith,ou=people,dc=icinga,dc=org +objectclass: inetOrgPerson +cn: Jane Smith +sn: Smith +uid: jsmith +userpassword: password + +dn: cn=John Q. Public,ou=people,dc=icinga,dc=org +objectclass: inetOrgPerson +cn: John Q. Public +sn: Public +uid: jqpublic +userpassword: password + +dn: cn=Richard Roe,ou=people,dc=icinga,dc=org +objectclass: inetOrgPerson +cn: Richard Roe +sn: Roe +uid: jroe +userpassword: password + +dn: cn=Users,ou=groups,dc=icinga,dc=org +objectClass: groupOfUniqueNames +cn: Users +uniqueMember: uid=jdoe,ou=people,dc=icinga,dc=rg +uniqueMember: uid=jsmith,ou=people,dc=icinga,dc=rg +uniqueMember: uid=jqpublic,ou=people,dc=icinga,dc=rg +uniqueMember: uid=jroe,ou=people,dc=icinga,dc=rg diff --git a/.vagrant-puppet/modules/openldap/manifests/init.pp b/.vagrant-puppet/modules/openldap/manifests/init.pp new file mode 100644 index 000000000..e9f3c504b --- /dev/null +++ b/.vagrant-puppet/modules/openldap/manifests/init.pp @@ -0,0 +1,25 @@ +# Class: openldap +# +# This class installs the openldap servers and clients software. +# +# Parameters: +# +# Actions: +# +# Requires: +# +# Sample Usage: +# +# include openldap +# +class openldap { + + package { ['openldap-servers', 'openldap-clients']: + ensure => installed + } + + service { 'slapd': + ensure => running, + require => Package['openldap-servers'] + } +} diff --git a/README.md b/README.md index f1b180655..ea811dc9e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ have to do is install Vagrant and run: vagrant up +> **Note** that the first boot of the vm takes a fairly long time because +> you'll download a plain CentOS base box and Vagrant will automatically +> provision the environment on the first go. + After you should be able to browse [localhost:8080/icinga2-web](http://localhost:8080/icinga2-web). ### Environment @@ -98,10 +102,63 @@ For **logging into** the Icinga classic web interface use user *icingaadmin* wit MK Livestatus is added to the Icinga installation using a MySQL database. **Installation path**: + * `/usr/local/icinga-mysql/bin/unixcat` * `/usr/local/icinga-mysql/lib/mk-livestatus/livecheck` * `/usr/local/icinga-mysql/lib/mk-livestatus/livestatus.o` * `/usr/local/icinga-mysql/etc/modules/mk-livestatus.cfg` * `/usr/local/icinga-mysql/var/rw/live` +**Example usage**: + echo "GET hosts" | /usr/local/icinga-mysql/bin/unixcat /usr/local/icinga-mysql/var/rw/live + +#### LDAP example data + +The environment includes a openldap server with example data. *Domain* suffix is **dc=icinga,dc=org**. +Administrator (*rootDN*) of the slapd configuration database is **cn=admin,cn=config** and the +administrator (*rootDN*) of our database instance is **cn=admin,dc=icinga,dc=org**. Both share +the *password* `admin`. + +Examples to query the slapd configuration database: + + ldapsearch -x -W -LLL -D cn=admin,cn=config -b cn=config dn + ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b cn=config dn + +Examples to query our database instance: + + ldapsearch -x -W -LLL -D cn=admin,dc=icinga,dc=org -b dc=icinga,dc=org dn + ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b dc=icinga,dc=org dn + +This is what the **dc=icinga,dc=org** *DIT* looks like: + +> dn: dc=icinga,dc=org +> +> dn: ou=people,dc=icinga,dc=org +> +> dn: ou=groups,dc=icinga,dc=org +> +> dn: cn=Users,ou=groups,dc=icinga,dc=org +> cn: Users +> uniqueMember: uid=jdoe,ou=people,dc=icinga,dc=rg +> uniqueMember: uid=jsmith,ou=people,dc=icinga,dc=rg +> uniqueMember: uid=jqpublic,ou=people,dc=icinga,dc=rg +> uniqueMember: uid=jroe,ou=people,dc=icinga,dc=rg +> +> dn: cn=John Doe,ou=people,dc=icinga,dc=org +> cn: John Doe +> uid: jdoe +> +> dn: cn=Jane Smith,ou=people,dc=icinga,dc=org +> cn: Jane Smith +> uid: jsmith +> +> dn: cn=John Q. Public,ou=people,dc=icinga,dc=org +> cn: John Q. Public +> uid: jqpublic +> +> dn: cn=Richard Roe,ou=people,dc=icinga,dc=org +> cn: Richard Roe +> uid: jroe + +All users share the password `password`. From a1298e6edd26ea788c2ab4cf813ab8a2a93ee115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Thu, 6 Jun 2013 16:05:55 +0200 Subject: [PATCH 02/10] Add dummy test case for casperjs The dummy test only checks whether our build server has the correct title when called. Also the i2w-config.js toolkit is provided to help testing correct paths refs #4213 --- test/frontend/cases/static-page-test.js | 19 +++++ test/frontend/casperjs.config | 5 ++ test/frontend/i2w-config.js | 107 ++++++++++++++++++++++++ test/frontend/regression/test.txt | 1 + 4 files changed, 132 insertions(+) create mode 100644 test/frontend/cases/static-page-test.js create mode 100644 test/frontend/casperjs.config create mode 100644 test/frontend/i2w-config.js create mode 100755 test/frontend/regression/test.txt diff --git a/test/frontend/cases/static-page-test.js b/test/frontend/cases/static-page-test.js new file mode 100644 index 000000000..b77496f86 --- /dev/null +++ b/test/frontend/cases/static-page-test.js @@ -0,0 +1,19 @@ +/** +* +* This test simply checks the icinga build server and tests +* if the title is correct +**/ +i2w = require('./i2w-config'); + +var casper = i2w.getTestEnv(); + +casper.start("http://build.icinga.org/jenkins"); + +casper.then(function() { + this.test.assertTitle("icinga-web test [Jenkins]", "The jenkins page"); +}); + +casper.run(function() { + this.test.done(); +}); + diff --git a/test/frontend/casperjs.config b/test/frontend/casperjs.config new file mode 100644 index 000000000..69c9d4a3b --- /dev/null +++ b/test/frontend/casperjs.config @@ -0,0 +1,5 @@ +{ + "host": "localhost", + "port": 80, + "path": "icinga2-web" +} diff --git a/test/frontend/i2w-config.js b/test/frontend/i2w-config.js new file mode 100644 index 000000000..7d54a1a00 --- /dev/null +++ b/test/frontend/i2w-config.js @@ -0,0 +1,107 @@ +/** +* Tools for setting up the casperjs tests +* mainly setting host, port and path path +**/ + +// load config files +var fs = require('fs'); +var env = require('system').env; +var args = require('system').args; +var utils = require('utils'); + + +var configFile = fs.absolute('./casperjs.config'); +var host = null; +var port = null; +var path = null; +var verbose = false; + + +if (typeof(env.CASPERJS_HOST) === "string") + host = env.CASPERJS_HOST; +if (typeof(env.CASPERJS_PORT) === "string") + port = parseInt(env.CASPERJS_PORT, 10); +if (typeof(env.CASPERJS_PATH) === "string") + path = env.CASPERJS_PATH; + + +for (var i=0;i Date: Thu, 6 Jun 2013 16:07:32 +0200 Subject: [PATCH 03/10] Add testrunner for frontend tests This runner should conform to our runner specification refs #4213 refs #4244 --- test/frontend/runtest.sh | 115 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100755 test/frontend/runtest.sh diff --git a/test/frontend/runtest.sh b/test/frontend/runtest.sh new file mode 100755 index 000000000..bbe84f300 --- /dev/null +++ b/test/frontend/runtest.sh @@ -0,0 +1,115 @@ +#!/bin/sh + +set -o nounset + +pushd `dirname $0` > /dev/null +DIR=`pwd` +popd > /dev/null +CASPER=$(which casperjs) +INCLUDE="" +EXCLUDE="" +VERBOSE=0 +BUILD=0 + +if [ ! -x $CASPER ]; then + echo "CasperJS is not installed but required to run frontend tests\n"\ +"Take a look at http://casperjs.org/installation.html to see how the installation works for your system" + exit 1 +fi; + +PARAM="0" +for arg in $@;do + if [ ! "$PARAM" == "0" ]; then + export $PARAM=$arg + PARAM="0" + continue + fi; + case $arg in + --verbose) + VERBOSE=1 + ;; + --include) + PARAM="INCLUDE" + continue + ;; + --exclude) + PARAM="EXCLUDE" + continue + ;; + --build) + BUILD=1 + continue + ;; + **) + if [ "$arg" != "--help" ]; then + echo "Unknown option $arg" + fi; + echo "Testrunner for interface tests: ./$0 [--verbose] [--include %include%] [--exclude %exclude%] [--build]" + echo "\t\t --verbose \t\t\t Print verbose output when testing" + echo "\t\t --include %filelist%\t\t Include only files matching this patterns" + echo "\t\t --exclude %filelist%\t\t Exclude files matching this patterns" + echo "\t\t --build \t\t\t Write test results to ../../build/log/casper_results.xml" + echo "\t\t --help \t\t\t Print this message" + exit 1 + + esac; +done; + +EXEC="$CASPER test" + +# +# If build is set, the results are written for our jenkins server +# +if [ $BUILD -eq 1 ];then + mkdir -p $DIR/../../build/log + EXEC="$EXEC --xunit=$DIR/../../build/log/casper_results.xml" +fi; +if [ "$PARAM" != "0" ]; then + echo "Missing parameter for $PARAM" + exit 1 +fi; + +cd $DIR +FILELIST="" +# +# Default : Run regression and cases directory +# +if [ "$INCLUDE" == "" -a "$EXCLUDE" == "" ];then + FILELIST="./cases ./regression" +fi; + +# +# Include patterns set with the --include directive +# +if [ "$INCLUDE" != "" ];then + NAME="\(" + GLUE="" + for INC in $INCLUDE;do + NAME="$NAME${GLUE}${INC}.*js" + GLUE="\|" + done; + NAME=$NAME"\)$" + FILELIST=`find . | grep "$NAME"` +fi; + +# +# Exclude patterns that match the include directive +# +if [ "$EXCLUDE" != "" ];then + NAME="\(" + GLUE="" + for EXC in $EXCLUDE;do + NAME="$NAME${GLUE}${EXC}.*js" + GLUE="\|" + done; + NAME=$NAME"\)$" + if [ "$FILELIST" == "" ]; then + FILELIST=`find .|grep ".*js$"` + fi + FILELIST=`echo $FILELIST | grep -v "$NAME"` +fi; + +echo $EXEC $FILELIST +$EXEC $FILELIST + +exit 0 From 26644b091b90c8ae565063afa9aff5156552ade2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Thu, 6 Jun 2013 16:13:16 +0200 Subject: [PATCH 04/10] Renamed runtest to runtests.sh --- test/frontend/{runtest.sh => runtests.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/frontend/{runtest.sh => runtests.sh} (100%) diff --git a/test/frontend/runtest.sh b/test/frontend/runtests.sh similarity index 100% rename from test/frontend/runtest.sh rename to test/frontend/runtests.sh From b253c4c0699715ae88c1b56103a80a23cacb7763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Thu, 6 Jun 2013 16:13:16 +0200 Subject: [PATCH 05/10] Renamed runtest to runtests.sh refs #4213 --- test/frontend/{runtest.sh => runtests.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/frontend/{runtest.sh => runtests.sh} (100%) diff --git a/test/frontend/runtest.sh b/test/frontend/runtests.sh similarity index 100% rename from test/frontend/runtest.sh rename to test/frontend/runtests.sh From e9bd0648ab484bc664484b8b2654fa83a5c5e326 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 6 Jun 2013 17:06:48 +0200 Subject: [PATCH 06/10] Vagrant/LDAP: Fix members of group `Users` refs #4218 --- .vagrant-puppet/modules/openldap/files/users.ldif | 10 +++++----- README.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.vagrant-puppet/modules/openldap/files/users.ldif b/.vagrant-puppet/modules/openldap/files/users.ldif index ba68348ee..f5ad802a3 100644 --- a/.vagrant-puppet/modules/openldap/files/users.ldif +++ b/.vagrant-puppet/modules/openldap/files/users.ldif @@ -31,13 +31,13 @@ dn: cn=Richard Roe,ou=people,dc=icinga,dc=org objectclass: inetOrgPerson cn: Richard Roe sn: Roe -uid: jroe +uid: rroe userpassword: password dn: cn=Users,ou=groups,dc=icinga,dc=org objectClass: groupOfUniqueNames cn: Users -uniqueMember: uid=jdoe,ou=people,dc=icinga,dc=rg -uniqueMember: uid=jsmith,ou=people,dc=icinga,dc=rg -uniqueMember: uid=jqpublic,ou=people,dc=icinga,dc=rg -uniqueMember: uid=jroe,ou=people,dc=icinga,dc=rg +uniqueMember: cn=Jon Doe,ou=people,dc=icinga,dc=org +uniqueMember: cn=Jane Smith,ou=people,dc=icinga,dc=org +uniqueMember: cn=John Q. Public,ou=people,dc=icinga,dc=org +uniqueMember: cn=Richard Roe,ou=people,dc=icinga,dc=org diff --git a/README.md b/README.md index ea811dc9e..371d1b3da 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,10 @@ This is what the **dc=icinga,dc=org** *DIT* looks like: > > dn: cn=Users,ou=groups,dc=icinga,dc=org > cn: Users -> uniqueMember: uid=jdoe,ou=people,dc=icinga,dc=rg -> uniqueMember: uid=jsmith,ou=people,dc=icinga,dc=rg -> uniqueMember: uid=jqpublic,ou=people,dc=icinga,dc=rg -> uniqueMember: uid=jroe,ou=people,dc=icinga,dc=rg +> uniqueMember: cn=Jon Doe,ou=people,dc=icinga,dc=org +> uniqueMember: cn=Jane Smith,ou=people,dc=icinga,dc=org +> uniqueMember: cn=John Q. Public,ou=people,dc=icinga,dc=org +> uniqueMember: cn=Richard Roe,ou=people,dc=icinga,dc=org > > dn: cn=John Doe,ou=people,dc=icinga,dc=org > cn: John Doe @@ -159,6 +159,6 @@ This is what the **dc=icinga,dc=org** *DIT* looks like: > > dn: cn=Richard Roe,ou=people,dc=icinga,dc=org > cn: Richard Roe -> uid: jroe +> uid: rroe All users share the password `password`. From 3e4ae41e8ab322f60c2b5bfb76b65f1243c6416a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Fri, 7 Jun 2013 10:34:39 +0200 Subject: [PATCH 07/10] Rename runtests.sh to runtest As we have different executables for executing the runtests scripts, runtests.sh is now renamed to runtests in order to stay consistent over all testrunners refs #4213 refs #4244 --- test/frontend/{runtests.sh => runtests} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/frontend/{runtests.sh => runtests} (99%) diff --git a/test/frontend/runtests.sh b/test/frontend/runtests similarity index 99% rename from test/frontend/runtests.sh rename to test/frontend/runtests index bbe84f300..ecfc1babc 100755 --- a/test/frontend/runtests.sh +++ b/test/frontend/runtests @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env sh set -o nounset From 6f80d410b8a3b140973f21f323ed80340d1c9559 Mon Sep 17 00:00:00 2001 From: Marius Hein Date: Fri, 7 Jun 2013 15:09:31 +0200 Subject: [PATCH 08/10] Integrate javascript frontend/component tests Fix output and make smaller refs 4213 --- test/frontend/runtests | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/frontend/runtests b/test/frontend/runtests index ecfc1babc..4e913ea72 100755 --- a/test/frontend/runtests +++ b/test/frontend/runtests @@ -44,14 +44,14 @@ for arg in $@;do if [ "$arg" != "--help" ]; then echo "Unknown option $arg" fi; - echo "Testrunner for interface tests: ./$0 [--verbose] [--include %include%] [--exclude %exclude%] [--build]" - echo "\t\t --verbose \t\t\t Print verbose output when testing" - echo "\t\t --include %filelist%\t\t Include only files matching this patterns" - echo "\t\t --exclude %filelist%\t\t Exclude files matching this patterns" - echo "\t\t --build \t\t\t Write test results to ../../build/log/casper_results.xml" - echo "\t\t --help \t\t\t Print this message" + printf "%b" "Testrunner for interface tests\n\n" + printf "%b" "Usage: ./$0 [--verbose] [--include %include%] [--exclude %exclude%] [--build]\n\n" + printf "%b" " --verbose \t\t\t Print verbose output when testing\n" + printf "%b" " --include %filelist%\t\t Include only files matching this patterns\n" + printf "%b" " --exclude %filelist%\t\t Exclude files matching this patterns\n" + printf "%b" " --build \t\t\t Write test results to ../../build/log/casper_results.xml\n" + printf "%b" " --help \t\t\t Print this message\n\n" exit 1 - esac; done; From 54ccb9b12adf03245122691d49909f4a6a01a2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Fri, 7 Jun 2013 11:44:37 +0200 Subject: [PATCH 09/10] Move libraries from incubator to working tree for evaluation Add all untested files from incubator's library/Icinga to working tree library/Icinga refs #4257 --- .../Application/ApplicationBootstrap.php | 268 +++++++++ library/Icinga/Application/Benchmark.php | 293 ++++++++++ library/Icinga/Application/Cli.php | 32 ++ library/Icinga/Application/Config.php | 76 +++ library/Icinga/Application/EmbeddedWeb.php | 36 ++ library/Icinga/Application/Icinga.php | 26 + library/Icinga/Application/LegacyWeb.php | 30 + library/Icinga/Application/Loader.php | 131 +++++ .../Icinga/Application/Modules/Manager.php | 249 +++++++++ library/Icinga/Application/Modules/Module.php | 139 +++++ library/Icinga/Application/Platform.php | 67 +++ .../Icinga/Application/TranslationHelper.php | 147 +++++ library/Icinga/Application/Web.php | 199 +++++++ library/Icinga/Application/functions.php | 31 ++ library/Icinga/Authentication/Auth.php | 83 +++ library/Icinga/Authentication/Backend.php | 26 + library/Icinga/Authentication/Backend/Db.php | 0 .../Authentication/Backend/LegacyWeb1x.php | 0 library/Icinga/Authentication/Group.php | 0 .../Icinga/Authentication/GroupBackend.php | 0 .../Icinga/Authentication/GroupMembership.php | 0 .../Icinga/Authentication/LdapUserBackend.php | 57 ++ library/Icinga/Authentication/Storable.php | 203 +++++++ library/Icinga/Authentication/User.php | 70 +++ library/Icinga/Authentication/UserBackend.php | 28 + library/Icinga/Backend.php | 38 ++ library/Icinga/Backend/Combo.php | 53 ++ library/Icinga/Backend/Ido.php | 292 ++++++++++ .../Icinga/Backend/Ido/GroupsummaryQuery.php | 191 +++++++ .../Backend/Ido/HostgroupsummaryQuery.php | 14 + library/Icinga/Backend/Ido/Query.php | 521 ++++++++++++++++++ .../Backend/Ido/ServicegroupsummaryQuery.php | 14 + .../Icinga/Backend/Ido/ServicelistQuery.php | 95 ++++ .../Icinga/Backend/Ido/StatehistoryQuery.php | 86 +++ library/Icinga/Backend/Livestatus.php | 45 ++ library/Icinga/Backend/Livestatus/Query.php | 213 +++++++ .../Backend/Livestatus/ServicelistQuery.php | 46 ++ .../Icinga/Backend/MonitoringObjectList.php | 110 ++++ library/Icinga/Data/AbstractQuery.php | 376 +++++++++++++ library/Icinga/Data/ArrayDatasource.php | 100 ++++ library/Icinga/Data/ArrayQuery.php | 85 +++ library/Icinga/Data/DatasourceInterface.php | 14 + library/Icinga/Data/Db/Connection.php | 113 ++++ library/Icinga/Data/Db/Query.php | 130 +++++ library/Icinga/Data/Filter.php | 54 ++ library/Icinga/Objects/Host.php | 8 + library/Icinga/Objects/Object.php | 61 ++ library/Icinga/Objects/Service.php | 7 + library/Icinga/Pdf/File.php | 84 +++ library/Icinga/Protocol/Ldap/LdapUtils.php | 1 + .../Icinga/Protocol/Livestatus/Connection.php | 249 +++++++++ library/Icinga/Protocol/Livestatus/Query.php | 210 +++++++ library/Icinga/Protocol/Nrpe/Connection.php | 106 ++++ library/Icinga/Protocol/Nrpe/Packet.php | 68 +++ library/Icinga/Util/Format.php | 71 +++ library/Icinga/Web/Cookie.php | 8 + library/Icinga/Web/Form.php | 79 +++ library/Icinga/Web/Hook/Grapher.php | 90 +++ library/Icinga/Web/Hook/Toptray.php | 30 + library/Icinga/Web/ModuleActionController.php | 87 +++ library/Icinga/Web/Notification.php | 113 ++++ .../Web/Paginator/Adapter/QueryAdapter.php | 62 +++ .../ScrollingStyle/SlidingWithBorder.php | 63 +++ library/Icinga/Web/Session.php | 156 ++++++ library/Icinga/Web/Widget.php | 53 ++ library/Icinga/Web/Widget/AbstractWidget.php | 162 ++++++ library/Icinga/Web/Widget/Form.php | 43 ++ library/Icinga/Web/Widget/Tab.php | 119 ++++ library/Icinga/Web/Widget/Tabs.php | 195 +++++++ 69 files changed, 6906 insertions(+) create mode 100755 library/Icinga/Application/ApplicationBootstrap.php create mode 100755 library/Icinga/Application/Benchmark.php create mode 100644 library/Icinga/Application/Cli.php create mode 100755 library/Icinga/Application/Config.php create mode 100644 library/Icinga/Application/EmbeddedWeb.php create mode 100644 library/Icinga/Application/Icinga.php create mode 100644 library/Icinga/Application/LegacyWeb.php create mode 100755 library/Icinga/Application/Loader.php create mode 100644 library/Icinga/Application/Modules/Manager.php create mode 100644 library/Icinga/Application/Modules/Module.php create mode 100644 library/Icinga/Application/Platform.php create mode 100644 library/Icinga/Application/TranslationHelper.php create mode 100644 library/Icinga/Application/Web.php create mode 100755 library/Icinga/Application/functions.php create mode 100644 library/Icinga/Authentication/Auth.php create mode 100644 library/Icinga/Authentication/Backend.php create mode 100644 library/Icinga/Authentication/Backend/Db.php create mode 100644 library/Icinga/Authentication/Backend/LegacyWeb1x.php create mode 100644 library/Icinga/Authentication/Group.php create mode 100644 library/Icinga/Authentication/GroupBackend.php create mode 100644 library/Icinga/Authentication/GroupMembership.php create mode 100644 library/Icinga/Authentication/LdapUserBackend.php create mode 100644 library/Icinga/Authentication/Storable.php create mode 100644 library/Icinga/Authentication/User.php create mode 100644 library/Icinga/Authentication/UserBackend.php create mode 100755 library/Icinga/Backend.php create mode 100755 library/Icinga/Backend/Combo.php create mode 100755 library/Icinga/Backend/Ido.php create mode 100755 library/Icinga/Backend/Ido/GroupsummaryQuery.php create mode 100755 library/Icinga/Backend/Ido/HostgroupsummaryQuery.php create mode 100755 library/Icinga/Backend/Ido/Query.php create mode 100755 library/Icinga/Backend/Ido/ServicegroupsummaryQuery.php create mode 100755 library/Icinga/Backend/Ido/ServicelistQuery.php create mode 100644 library/Icinga/Backend/Ido/StatehistoryQuery.php create mode 100755 library/Icinga/Backend/Livestatus.php create mode 100644 library/Icinga/Backend/Livestatus/Query.php create mode 100644 library/Icinga/Backend/Livestatus/ServicelistQuery.php create mode 100755 library/Icinga/Backend/MonitoringObjectList.php create mode 100644 library/Icinga/Data/AbstractQuery.php create mode 100644 library/Icinga/Data/ArrayDatasource.php create mode 100644 library/Icinga/Data/ArrayQuery.php create mode 100644 library/Icinga/Data/DatasourceInterface.php create mode 100644 library/Icinga/Data/Db/Connection.php create mode 100644 library/Icinga/Data/Db/Query.php create mode 100644 library/Icinga/Data/Filter.php create mode 100755 library/Icinga/Objects/Host.php create mode 100755 library/Icinga/Objects/Object.php create mode 100755 library/Icinga/Objects/Service.php create mode 100644 library/Icinga/Pdf/File.php create mode 100755 library/Icinga/Protocol/Livestatus/Connection.php create mode 100755 library/Icinga/Protocol/Livestatus/Query.php create mode 100644 library/Icinga/Protocol/Nrpe/Connection.php create mode 100644 library/Icinga/Protocol/Nrpe/Packet.php create mode 100644 library/Icinga/Util/Format.php create mode 100644 library/Icinga/Web/Cookie.php create mode 100644 library/Icinga/Web/Form.php create mode 100644 library/Icinga/Web/Hook/Grapher.php create mode 100755 library/Icinga/Web/Hook/Toptray.php create mode 100644 library/Icinga/Web/ModuleActionController.php create mode 100644 library/Icinga/Web/Notification.php create mode 100755 library/Icinga/Web/Paginator/Adapter/QueryAdapter.php create mode 100755 library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php create mode 100755 library/Icinga/Web/Session.php create mode 100644 library/Icinga/Web/Widget.php create mode 100644 library/Icinga/Web/Widget/AbstractWidget.php create mode 100644 library/Icinga/Web/Widget/Form.php create mode 100644 library/Icinga/Web/Widget/Tab.php create mode 100644 library/Icinga/Web/Widget/Tabs.php diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php new file mode 100755 index 000000000..58550516c --- /dev/null +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -0,0 +1,268 @@ + + * use Icinga\Application\Cli; + * Cli::start(); + * + * + * Usage example for Icinga Web application: + * + * use Icinga\Application\Web; + * Web::start()->dispatch(); + * + * + * Usage example for Icinga-Web 1.x compatibility mode: + * + * use Icinga\Application\LegacyWeb; + * LegacyWeb::start()->setIcingaWebBasedir(ICINGAWEB_BASEDIR)->dispatch(); + * + * + * @copyright Copyright (c) 2013 Icinga-Web Team + * @author Icinga-Web Team + * @package Icinga\Application + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +abstract class ApplicationBootstrap +{ + protected $loader; + protected $libdir; + protected $config; + protected $configFile; + protected $appdir; + protected $moduleManager; + protected $isCli = false; + protected $isWeb = false; + + /** + * Constructor + * + * The constructor is protected to avoid incorrect usage + * + * @return void + */ + protected function __construct($configFile = null) + { + $this->checkPrerequisites(); + $this->libdir = realpath(dirname(dirname(dirname(__FILE__)))); + + require $this->libdir . '/Icinga/Application/Loader.php'; + if (! defined('ICINGA_LIBDIR')) { + define('ICINGA_LIBDIR', $this->libdir); + } + // TODO: Make appdir configurable for packagers + $this->appdir = realpath(dirname($this->libdir) . '/application'); + if (! defined('ICINGA_APPDIR')) { + define('ICINGA_APPDIR', $this->appdir); + } + + $this->loader = Loader::register(); + $this->registerZendAutoloader(); + Benchmark::measure('Bootstrap, autoloader registered'); + + Icinga::setApp($this); + + // Unfortunately this is needed to get the Zend Plugin loader working: + set_include_path( + implode( + PATH_SEPARATOR, + array($this->libdir, get_include_path()) + ) + ); + + if ($configFile === null) { + $configFile = dirname($this->libdir) . '/config/icinga.ini'; + } + $this->configFile = $configFile; + require_once dirname(__FILE__) . '/functions.php'; + } + + abstract protected function bootstrap(); + + public function moduleManager() + { + if ($this->moduleManager === null) { + $this->moduleManager = new ModuleManager($this); + } + return $this->moduleManager; + } + + public function getLoader() + { + return $this->loader; + } + + protected function loadEnabledModules() + { + $this->moduleManager()->loadEnabledModules(); + return $this; + } + + public function isCli() + { + return $this->isCli; + } + + public function isWeb() + { + return $this->isWeb; + } + + public function getApplicationDir($subdir = null) + { + $dir = $this->appdir; + if ($subdir !== null) { + $dir .= '/' . ltrim($subdir, '/'); + } + return $dir; + } + + public function hasModule($name) + { + return $this->moduleManager()->hasLoaded($name); + } + + public function getModule($name) + { + return $this->moduleManager()->getModule($name); + } + + public function loadModule($name) + { + return $this->moduleManager()->loadModule($name); + } + + public function getConfig() + { + return $this->config; + } + + public static function start($config = null) + { + $class = get_called_class(); + $obj = new $class(); + $obj->bootstrap(); + return $obj; + } + + /** + * Register the Zend Autoloader + * + * @return self + */ + protected function registerZendAutoloader() + { + require_once 'Zend/Loader/Autoloader.php'; + ZendLoader::getInstance(); + return $this; + } + + /** + * Check whether we have all we need + * + * Pretty useless right now as a namespaces class would not work + * with PHP 5.3 + * + * @return self + */ + protected function checkPrerequisites() + { + if (version_compare(phpversion(), '5.3.0', '<') === true) { + die('PHP > 5.3.0 required'); + } + return $this; + } + + /** + * Check whether a given PHP extension is available + * + * @return boolean + */ + protected function hasExtension($name) + { + if (!extension_loaded($name)) { + if (! @ dl($name)) { + throw new ConfigurationError( + sprintf( + 'The PHP extension %s is not available', + $name + ) + ); + } + } + } + + /** + * Load Configuration + * + * @return self + */ + protected function loadConfig() + { + // TODO: add an absolutely failsafe config loader + if (! @is_readable($this->configFile)) { + throw new \Exception('Cannot read config file: ' . $this->configFile); + } + $this->config = Config::getInstance($this->configFile); + return $this; + } + + + /** + * Configure cache settings + * + * TODO: Right now APC is hardcoded, make this configurable + * + * @return self + */ + protected function configureCache() + { + // TODO: Provide Zend_Cache_Frontend_File for statusdat + //$this->cache = \Zend_Cache::factory('Core', 'Apc'); + return $this; + } + + /** + * Error handling configuration + * + * @return self + */ + protected function configureErrorHandling() + { + if ($this->config->global->environment == 'development') { + error_reporting(E_ALL | E_NOTICE); + ini_set('display_startup_errors', 1); + ini_set('display_errors', 1); + } + Logger::create($this->config->logging); + return $this; + } + + /** + * Set timezone settings + * + * @return self + */ + protected function setTimezone() + { + date_default_timezone_set( + $this->config->{'global'}->get('timezone', 'UTC') + ); + return $this; + } +} diff --git a/library/Icinga/Application/Benchmark.php b/library/Icinga/Application/Benchmark.php new file mode 100755 index 000000000..33455f300 --- /dev/null +++ b/library/Icinga/Application/Benchmark.php @@ -0,0 +1,293 @@ + + * Benchmark::measure('Program started'); + * // ...do something... + * Benchmark::measure('Task finieshed'); + * Benchmark::dump(); + * + */ +class Benchmark +{ + const TIME = 0x01; + const MEMORY = 0x02; + + protected static $instance; + protected $start; + protected $measures = array(); + + /** + * Add a measurement to your benchmark + * + * The same identifier can also be used multiple times + * + * @param string A comment identifying the current measurement + * @return void + */ + public static function measure($message) + { + self::getInstance()->measures[] = (object) array( + 'timestamp' => microtime(true), + 'memory_real' => memory_get_usage(true), + 'memory' => memory_get_usage(), + 'message' => $message + ); + } + + /** + * Throws all measurements away + * + * This empties your measurement table and allows you to restart your + * benchmark from scratch + * + * @return void + */ + public static function reset() + { + self::$instance = null; + } + + /** + * Rerieve benchmark start time + * + * This will give you the timestamp of your first measurement + * + * @return float + */ + public static function getStartTime() + { + return self::getInstance()->start; + } + + /** + * Dump benchmark data + * + * Will dump a text table if running on CLI and a simple HTML table + * otherwise. Use Benchmark::TIME and Benchmark::MEMORY to choose whether + * you prefer to show either time or memory or both in your output + * + * @param int Whether to get time and/or memory summary + * @return string + */ + public static function dump($what = null) + { + if (Icinga::app()->isCli()) { + echo self::renderToText($what); + } else { + echo self::renderToHtml($what); + } + } + + /** + * Render benchmark data to a simple text table + * + * Use Benchmark::TIME and Icinga::MEMORY to choose whether you prefer to + * show either time or memory or both in your output + * + * @param int Whether to get time and/or memory summary + * @return string + */ + public static function renderToText($what = null) + { + $data = self::prepareDataForRendering($what); + $sep = '+'; + $title = '|'; + foreach ($data->columns as & $col) { + $col->format = ' %' + . ($col->align === 'right' ? '' : '-') + . $col->maxlen . 's |'; + + $sep .= str_repeat('-', $col->maxlen) . '--+'; + $title .= sprintf($col->format, $col->title); + } + + $out = $sep . "\n" . $title . "\n" . $sep . "\n"; + foreach ($data->rows as & $row) { + $r = '|'; + foreach ($data->columns as $key => & $col) { + $r .= sprintf($col->format, $row[$key]); + } + $out .= $r . "\n"; + } + + $out .= $sep . "\n"; + return $out; + } + + /** + * Render benchmark data to a simple HTML table + * + * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer + * to show either time or memory or both in your output + * + * @param int Whether to get time and/or memory summary + * @return string + */ + public static function renderToHtml($what = null) + { + $data = self::prepareDataForRendering($what); + + // TODO: Move formatting to CSS file + $style = 'font-family: monospace; font-size: 1.5em; width: 100%'; + $html = '' . "\n" . ''; + foreach ($data->columns as & $col) { + $html .= sprintf( + '', + $col->align, + htmlspecialchars($col->title) + ); + } + $html .= "\n"; + + foreach ($data->rows as & $row) { + $html .= ''; + foreach ($data->columns as $key => & $col) { + $html .= sprintf( + '', + $col->align, + $row[$key] + ); + } + $html .= "\n"; + } + $html .= "
%s
%s
\n"; + return $html; + } + + /** + * Prepares benchmark data for output + * + * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer + * to have either time or memory or both in your output + * + * @param int Whether to get time and/or memory summary + * @return array + */ + protected static function prepareDataForRendering($what = null) + { + if ($what === null) { + $what = self::TIME | self::MEMORY; + } + + $columns = array( + (object) array( + 'title' => 'Time', + 'align' => 'left', + 'maxlen' => 4 + ), + (object) array( + 'title' => 'Description', + 'align' => 'left', + 'maxlen' => 11 + ) + ); + if ($what & self::TIME) { + $columns[] = (object) array( + 'title' => 'Off (ms)', + 'align' => 'right', + 'maxlen' => 11 + ); + $columns[] = (object) array( + 'title' => 'Dur (ms)', + 'align' => 'right', + 'maxlen' => 13 + ); + } + if ($what & self::MEMORY) { + $columns[] = (object) array( + 'title' => 'Mem (diff)', + 'align' => 'right', + 'maxlen' => 10 + ); + $columns[] = (object) array( + 'title' => 'Mem (total)', + 'align' => 'right', + 'maxlen' => 11 + ); + } + + $bench = self::getInstance(); + $last = $bench->start; + $rows = array(); + $lastmem = 0; + foreach ($bench->measures as $m) { + $micro = sprintf( + '%03d', + round(($m->timestamp - floor($m->timestamp)) * 1000) + ); + $vals = array( + date('H:i:s', $m->timestamp) . '.' . $micro, + $m->message + ); + + if ($what & self::TIME) { + $m->relative = $m->timestamp - $bench->start; + $m->offset = $m->timestamp - $last; + $last = $m->timestamp; + $vals[] = sprintf('%0.3f', $m->relative * 1000); + $vals[] = sprintf('%0.3f', $m->offset * 1000); + } + + if ($what & self::MEMORY) { + $mem = $m->memory - $lastmem; + $lastmem = $m->memory; + $vals[] = Format::bytes($mem); + $vals[] = Format::bytes($m->memory); + } + + $row = & $rows[]; + foreach ($vals as $col => $val) { + $row[$col] = $val; + $columns[$col]->maxlen = max( + strlen($val), + $columns[$col]->maxlen + ); + } + } + + return (object) array( + 'columns' => $columns, + 'rows' => $rows + ); + } + + /** + * Singleton + * + * Benchmark is run only once, but you are not allowed to directly access + * the getInstance() method + * + * @return self + */ + protected static function getInstance() + { + if (self::$instance === null) { + self::$instance = new Benchmark(); + self::$instance->start = microtime(true); + } + + return self::$instance; + } + + /** + * Constructor + * + * Singleton usage is enforced, the only way to instantiate Benchmark is by + * starting your measurements + * + * @return void + */ + protected function __construct() + { + } +} diff --git a/library/Icinga/Application/Cli.php b/library/Icinga/Application/Cli.php new file mode 100644 index 000000000..6b6937e57 --- /dev/null +++ b/library/Icinga/Application/Cli.php @@ -0,0 +1,32 @@ +assertRunningOnCli(); + return $this->loadConfig() + ->configureErrorHandling() + ->setTimezone(); + } + + /** + * Fail if Icinga has not been called on CLI + * + * @throws Exception + * @return void + */ + private static function assertRunningOnCli() + { + if (Platform::isCli()) { + return; + } + throw new Exception('Icinga is not running on CLI'); + } +} diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php new file mode 100755 index 000000000..6b72df715 --- /dev/null +++ b/library/Icinga/Application/Config.php @@ -0,0 +1,76 @@ +$what === null) { + return array(); + } else { + return array_keys($this->$what->toArray()); + } + } + + public function getConfigDir() + { + return $this->configDir; + } + + public function __construct($filename, $section = null, $options = false) + { + $options['allowModifications'] = true; + $this->configDir = dirname($filename); + return parent::__construct($filename, $section, $options); + } + + public function getModuleConfig($key, $module) + { + $manager = Icinga::app()->moduleManager(); + $res = null; + if ($manager->hasInstalled($module)) { + $filename = $manager->getModuleConfigDir($module) . "/$key.ini"; + if (file_exists($filename)) { + return $this->$key = new Config($filename); + } + } + return $res; + } + + public function __get($key) + { + $res = parent::__get($key); + if ($res === null) { + $app = Icinga::app(); + if ($app->hasModule($key)) { + $filename = $app->getModule($key)->getConfigDir() . "/$key.ini"; + } else { + $filename = $this->configDir . '/' . $key . '.ini'; + } + if (file_exists($filename)) { + $res = $this->$key = new Config($filename); + } + } + return $res; + } + + public static function getInstance($configFile = null) + { + if (self::$instance === null) { + if ($configFile === null) { + $configFile = dirname(dirname(dirname(dirname(__FILE__)))) + . '/config/icinga.ini'; + } + self::$instance = new Config($configFile); + } + return self::$instance; + } +} diff --git a/library/Icinga/Application/EmbeddedWeb.php b/library/Icinga/Application/EmbeddedWeb.php new file mode 100644 index 000000000..b02d8f0d3 --- /dev/null +++ b/library/Icinga/Application/EmbeddedWeb.php @@ -0,0 +1,36 @@ + + * use Icinga\Application\EmbeddedWeb; + * EmbeddedWeb::start(); + * + * + * @copyright Copyright (c) 2013 Icinga-Web Team + * @author Icinga-Web Team + * @package Icinga\Application + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class EmbeddedWeb extends ApplicationBootstrap +{ + protected function bootstrap() + { + return $this->loadConfig() + ->configureErrorHandling() + ->setTimezone() + ->loadEnabledModules(); + } +} diff --git a/library/Icinga/Application/Icinga.php b/library/Icinga/Application/Icinga.php new file mode 100644 index 000000000..e88ff3360 --- /dev/null +++ b/library/Icinga/Application/Icinga.php @@ -0,0 +1,26 @@ +setupIcingaLegacyWrapper(); + } + + /** + * Get the Icinga-Web 1.x base path + * + * @throws Exception + * @return self + */ + public function getLecacyBasedir() + { + return $this->legacyBasedir; + } +} diff --git a/library/Icinga/Application/Loader.php b/library/Icinga/Application/Loader.php new file mode 100755 index 000000000..7b25f7ded --- /dev/null +++ b/library/Icinga/Application/Loader.php @@ -0,0 +1,131 @@ + + * Icinga\Application\Loader::register(); + * + * + * @copyright Copyright (c) 2013 Icinga-Web Team + * @author Icinga-Web Team + * @package Icinga\Application + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Loader +{ + const NS = '\\'; + protected $moduleDirs = array(); + + private static $instance; + + /** + * Register the Icinga autoloader + * + * You could also call getInstance(), this alias function is here to make + * code look better + * + * @return self + */ + public static function register() + { + return self::getInstance(); + } + + /** + * Singleton + * + * Registers the Icinga autoloader if not already been done + * + * @return self + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new Loader(); + self::$instance->registerAutoloader(); + } + return self::$instance; + } + + public function addModule($name, $dir) + { + $this->moduleDirs[ucfirst($name)] = $dir; + return $this; + } + + /** + * Class loader + * + * Ignores all but classes in the Icinga namespace. + * + * @return boolean + */ + public function loadClass($class) + { + if (strpos($class, 'Icinga' . self::NS) === false) { + return false; + } + $file = str_replace(self::NS, '/', $class) . '.php'; + $file = ICINGA_LIBDIR . '/' . $file; + if (! @is_file($file)) { + $parts = preg_split('~\\\~', $class); + array_shift($parts); + $module = $parts[0]; + if (array_key_exists($module, $this->moduleDirs)) { + $file = $this->moduleDirs[$module] + . '/' + . implode('/', $parts) . '.php'; + if (@is_file($file)) { + require_once $file; + return true; + } + } + // Log::debug('File ' . $file . ' not found'); + return false; + } + require_once $file; + return true; + } + + /** + * Effectively registers the autoloader the PHP/SPL way + * + * @return void + */ + protected function registerAutoloader() + { + // Not adding ourselves to include_path right now, MAY be faster + /*set_include_path(implode(PATH_SEPARATOR, array( + realpath(dirname(dirname(__FILE__))), + get_include_path(), + )));*/ + spl_autoload_register(array($this, 'loadClass')); + } + + /** + * Constructor + * + * Singleton usage is enforced, you are also not allowed to overwrite this + * function + * + * @return void + */ + final private function __construct() + { + } +} diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php new file mode 100644 index 000000000..45f4c89aa --- /dev/null +++ b/library/Icinga/Application/Modules/Manager.php @@ -0,0 +1,249 @@ +app = $app; + $this->prepareEssentials(); + $this->detectEnabledModules(); + } + + protected function prepareEssentials() + { + $this->enableDir = $this->app->getConfig()->getConfigDir() + . '/enabledModules'; + + if (! file_exists($this->enableDir) || ! is_dir($this->enableDir)) { + throw new ProgrammingError( + sprintf( + 'Missing module directory: %s', + $this->enableDir + ) + ); + } + } + + protected function detectEnabledModules() + { + $fh = opendir($this->enableDir); + + while (false !== ($file = readdir($fh))) { + + if ($file[0] === '.') { + continue; + } + + $link = $this->enableDir . '/' . $file; + if (! is_link($link)) { + continue; + } + + $dir = realpath($link); + if (! file_exists($dir) || ! is_dir($dir)) { + continue; + } + + $this->enabledDirs[$file] = $dir; + } + } + + public function loadEnabledModules() + { + foreach ($this->listEnabledModules() as $name) { + $this->loadModule($name); + } + return $this; + } + + public function loadModule($name) + { + if ($this->hasLoaded($name)) { + return $this; + } + $module = new Module($this->app, $name, $this->getModuleDir($name)); + $module->register(); + $this->loadedModules[$name] = $module; + return $this; + } + + public function enableModule($name) + { + if (! $this->hasInstalled($name)) { + throw new ConfigurationError( + sprintf( + "Cannot enable module '%s' as it isn't installed", + $name + ) + ); + return $this; + } + $target = $this->installedBaseDirs[$name]; + $link = $this->enableDir . '/' . $name; + if (! is_writable($this->enableDir)) { + Notification::error("I do not have permissions to enable modules"); + return $this; + } + if (@symlink($target, $link)) { + Notification::success("The module $name has been enabled"); + } else { + Notification::error("Enabling module $name failed"); + } + return $this; + } + + public function disableModule($name) + { + if (! $this->hasEnabled($name)) { + return $this; + } + if (! is_writable($this->enableDir)) { + Notification::error("I do not have permissions to disable modules"); + return $this; + } + $link = $this->enableDir . '/' . $name; + if (file_exists($link) && is_link($link)) { + if (@unlink($link)) { + Notification::success("The module $name has been disabled"); + } else { + Notification::error("Disabling module $name failed"); + } + } + return $this; + } + + public function getModuleConfigDir($name) + { + return $this->getModuleDir($name, '/config'); + } + + public function getModuleDir($name, $subdir = '') + { + if ($this->hasEnabled($name)) { + return $this->enabledDirs[$name]. $subdir; + } + + if ($this->hasInstalled($name)) { + return $this->installedBaseDirs[$name] . $subdir; + } + + throw new ProgrammingError( + sprintf( + 'Trying to access uninstalled module dir: %s', + $name + ) + ); + } + + public function hasInstalled($name) + { + if ($this->installedBaseDirs === null) { + $this->detectInstalledModules(); + } + return array_key_exists($name, $this->installedBaseDirs); + } + + public function hasEnabled($name) + { + return array_key_exists($name, $this->enabledDirs); + } + + public function hasLoaded($name) + { + return array_key_exists($name, $this->loadedModules); + } + + public function getLoadedModules() + { + return $this->loadedModules; + } + + public function getModule($name) + { + if (! $this->hasLoaded($name)) { + throw new ProgrammingError( + sprintf( + 'Cannot access module %s as it hasn\'t been loaded', + $name + ) + ); + } + return $this->loadedModules[$name]; + } + + public function getModuleInfo() + { + $installed = $this->listInstalledModules(); + $info = array(); + foreach ($installed as $name) { + $info[] = (object) array( + 'name' => $name, + 'path' => $this->installedBaseDirs[$name], + 'enabled' => $this->hasEnabled($name), + 'loaded' => $this->hasLoaded($name) + ); + } + return $info; + } + + public function select() + { + $ds = new ArrayDatasource($this->getModuleInfo()); + return $ds->select(); + } + + public function listEnabledModules() + { + return array_keys($this->enabledDirs); + } + + public function listLoadedModules() + { + return array_keys($this->loadedModules); + } + + public function listInstalledModules() + { + if ($this->installedBaseDirs === null) { + $this->detectInstalledModules(); + } + return array_keys($this->installedBaseDirs); + } + + public function detectInstalledModules() + { + // TODO: Allow multiple paths for installed modules (e.g. web vs pkg) + $basedir = realpath(ICINGA_APPDIR . '/../modules'); + $fh = @opendir($basedir); + if ($fh === false) { + return $this; + } + + while ($name = readdir($fh)) { + if ($name[0] === '.') { + continue; + } + if (is_dir($basedir . '/' . $name)) { + $this->installedBaseDirs[$name] = $basedir . '/' . $name; + } + } + } +} diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php new file mode 100644 index 000000000..abc4a56a6 --- /dev/null +++ b/library/Icinga/Application/Modules/Module.php @@ -0,0 +1,139 @@ +app = $app; + $this->name = $name; + $this->basedir = $basedir; + $this->cssdir = $basedir . '/public/css'; + $this->libdir = $basedir . '/library'; + $this->configdir = $basedir . '/config'; + $this->localedir = $basedir . '/application/locale'; + $this->controllerdir = $basedir . '/application/controllers'; + $this->registerscript = $basedir . '/register.php'; + } + + public function register() + { + $this->registerLibrary() + ->registerWebIntegration() + ->runRegisterScript(); + return true; + } + + public function hasCss() + { + return file_exists($this->getCssFilename()); + } + + public function getCssFilename() + { + return $this->cssdir . '/module.less'; + } + + public function getBaseDir() + { + return $this->basedir; + } + + public function getConfigDir() + { + return $this->configdir; + } + + protected function registerLibrary() + { + if (file_exists($this->libdir) && is_dir($this->libdir)) { + $this->app->getLoader()->addModule($this->name, $this->libdir); + } + return $this; + } + + protected function registerLocales() + { + if (file_exists($this->localedir) && is_dir($this->localedir)) { + bindtextdomain($this->name, $this->localedir); + } + return $this; + } + + protected function registerWebIntegration() + { + if (! $this->app->isWeb()) { + return $this; + } + + if (file_exists($this->controllerdir) && is_dir($this->controllerdir)) { + $this->app->frontController()->addControllerDirectory( + $this->controllerdir, + $this->name + ); + } + + $this->registerLocales() + ->registerRoutes() + ->registerMenuEntries(); + return $this; + } + + protected function registerMenuEntries() + { + $cfg = $this->app + ->getConfig() + ->getModuleConfig('menu', $this->name); + + $view = $this->app->getView(); + if ($cfg) { + $view->view->navigation = $cfg->merge($view->view->navigation); + } + return $this; + } + + protected function registerRoutes() + { + $this->app->frontController()->getRouter()->addRoute( + $this->name . '_jsprovider', + new Route( + 'js/' . $this->name . '/:file', + array( + 'controller' => 'static', + 'action' =>'javascript', + 'moduleName' => $this->name + ) + ) + ); + return $this; + } + + protected function runRegisterScript() + { + if (file_exists($this->registerscript) + && is_readable($this->registerscript)) { + include($this->registerscript); + } + return $this; + } + + protected function registerHook($name, $class) + { + Hook::register($name, $class); + return $this; + } +} diff --git a/library/Icinga/Application/Platform.php b/library/Icinga/Application/Platform.php new file mode 100644 index 000000000..f17dad4f8 --- /dev/null +++ b/library/Icinga/Application/Platform.php @@ -0,0 +1,67 @@ +moduledir = $bootstrap->getModuleDir(); + if ($module) { + $this->basedir = $bootstrap->getModuleDir($module) . '/application'; + } else { + $this->basedir = $bootstrap->getApplicationDir(); + } + $this->locale = $locale; + $this->module = $module; + $this->targetfile = $this->basedir + . '/locale/' + . $this->locale + . '/LC_MESSAGES/' + . ($module ? $module : 'icinga') + . '.po'; + $target_dir = dirname($this->targetfile); + if (! is_dir($target_dir)) { + mkdir($target_dir, 0755, true); + } + } + + public function __destruct() + { + if ($this->tmpfile !== null) { + unlink($this->tmpfile); + } + if ($this->potfile !== null) { + unlink($this->potfile); + } + } + + public function extractTexts() + { + $tmpdir = sys_get_temp_dir(); + $this->potfile = tempnam($tmpdir, 'IcingaPot_'); + $cmd = '/usr/bin/xgettext' + . ' --language=PHP' + . ' --from-code=iso-8859-15' + . ' --keyword=' + . ($this->module ? '_mt:2' : '_t') + . ' --sort-output' + . ' --force-po' + . ' --package-name=Icinga' + . ' --package-version=0.1' + . ' --copyright-holder="Icinga Team"' + . ' --msgid-bugs-address="dev@icinga.org"' + . ' --files-from=' . $this->tmpfile + . ' --output=' . $this->potfile + ; + `$cmd`; + $this->fixPotfile(); + $this->mergeOldTranslations(); + return $this; + } + + protected function fixPotfile() + { + $content = file_get_contents($this->potfile); + $fh = fopen($this->potfile, 'w'); + foreach (preg_split('~\n~', $content) as $line) { + // if (preg_match('~^"Language:~', $line)) continue; + if (preg_match('~^"Content-Type:~', $line)) { + $line = '"Content-Type: text/plain; charset=utf-8\n"'; + } + fwrite($fh, $line . "\n"); + } + fclose($fh); + } + + protected function mergeOldTranslations() + { + if (is_file($this->targetfile)) { + $cmd = sprintf( + '/usr/bin/msgmerge %s %s -o %s 2>&1', + $this->targetfile, + $this->potfile, + $this->targetfile . '.new' + ); + `$cmd`; + rename($this->targetfile . '.new', $this->targetfile); + } else { + file_put_contents($this->targetfile, file_get_contents($this->potfile)); + } + } + + public function createTemporaryFileList() + { + $tmpdir = sys_get_temp_dir(); + $this->tmpfile = tempnam($tmpdir, 'IcingaTranslation_'); + $tmp_fh = fopen($this->tmpfile, 'w'); + if (! $tmp_fh) { + throw new \Exception('Unable to create ' . $this->tmpfile); + } + if ($this->module) { + $blacklist = array(); + } else { + $blacklist = array( + $this->moduledir + ); + } + $this->getSourceFileNames($this->basedir, $tmp_fh, $blacklist); + $this->getSourceFileNames(ICINGA_LIBDIR, $tmp_fh, $blacklist); + fclose($tmp_fh); + return $this; + } + + protected function getSourceFileNames($dir, & $fh, $blacklist = array()) + { + $dh = opendir($dir); + if (! $dh) { + throw new \Exception("Unable to read files from $dir"); + } + $subdirs = array(); + while ($filename = readdir($dh)) { + if ($filename[0] === '.') { + continue; + } + $fullname = $dir . '/' . $filename; + if (preg_match('~\.(?:php|phtml)$~', $filename)) { + fwrite($fh, "$fullname\n"); + } elseif (is_dir($fullname)) { + if (in_array($fullname, $blacklist)) { + continue; + } + $subdirs[] = $fullname; + } + } + closedir($dh); + foreach ($subdirs as $dir) { + $this->getSourceFileNames($dir, $fh, $blacklist); + } + } +} diff --git a/library/Icinga/Application/Web.php b/library/Icinga/Application/Web.php new file mode 100644 index 000000000..cc7e278ed --- /dev/null +++ b/library/Icinga/Application/Web.php @@ -0,0 +1,199 @@ + + * use Icinga\Application\EmbeddedWeb; + * EmbeddedWeb::start(); + * + * + * @copyright Copyright (c) 2013 Icinga-Web Team + * @author Icinga-Web Team + * @package Icinga\Application + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Web extends ApplicationBootstrap +{ + protected $view; + protected $frontController; + protected $isWeb = true; + + protected function bootstrap() + { + return $this->loadConfig() + ->configureErrorHandling() + ->setTimezone() + ->configureSession() + ->configureCache() + ->prepareZendMvc() + ->loadTranslations() + ->loadEnabledModules() + ->setupSpecialRoutes() + ->configurePagination(); + } + + protected function setupSpecialRoutes() + { + // TODO: Find a better solution + $this->frontController->getRouter()->addRoute( + 'module_overview', + new Route( + 'js/modules/list.js', + array( + 'controller' =>'static', + 'action' =>'modulelist', + ) + ) + ); + return $this; + } + + public function frontController() + { + // TODO: ProgrammingError if null + return $this->frontController; + } + + public function getView() + { + // TODO: ProgrammingError if null + return $this->view; + } + + public function dispatch() + { + $this->dispatchFrontController(); + } + + /** + * Configure web session settings + * + * @return self + */ + protected function configureSession() + { + Session::setOptions( + array( + // strict requires Zend_Session::start() + 'strict' => true, + 'cookie_secure' => false, + 'name' => $this->config->{'global'}->get( + 'session_cookie', + 'ICINGA_SID' + ), + + // Obsolete once moved to Icinga\Web\Session: + 'cookie_httponly' => true, + 'use_only_cookies' => true, + 'hash_function' => true, + 'hash_bits_per_character' => 5, + ) + ); + return $this; + } + + protected function loadTranslations() + { + $locale = Session::getInstance()->language; + if (! $locale) { + $locale = 'en_US'; + } + putenv('LC_ALL=' . $locale . '.UTF-8'); + setlocale(LC_ALL, $locale . '.UTF-8'); + bindtextdomain('icinga', ICINGA_APPDIR . '/locale'); + textdomain('icinga'); + return $this; + } + + protected function dispatchFrontController() + { + Session::getInstance(); + $this->frontController->dispatch(); + return $this; + } + + /** + * Prepare Zend MVC Base + * + * @return self + */ + protected function prepareZendMvc() + { + // TODO: Replace Zend_Application: + Layout::startMvc( + array( + 'layout' => 'layout', + 'layoutPath' => $this->appdir . '/layouts/scripts' + ) + ); + + return $this->prepareFrontController() + ->prepareView(); + } + + protected function prepareFrontController() + { + $this->frontController = FrontController::getInstance() + ->setControllerDirectory($this->appdir . '/controllers') + // TODO: Create config option for Load balancers etc: + // ->setBaseurl() + ->setParams( + array( + 'displayExceptions' => 1 + ) + ); + return $this; + } + + protected function prepareView() + { + $view = ActionHelper::getStaticHelper('viewRenderer'); + $view->initView(); + + $view->view->addHelperPath($this->appdir . '/views/helpers'); + // TODO: find out how to avoid this additional helper path: + $view->view->addHelperPath($this->appdir . '/views/helpers/layout'); + + $view->view->setEncoding('UTF-8'); + $view->view->headTitle()->prepend( + $this->config->{'global'}->get('project', 'Icinga') + ); + $view->view->headTitle()->setSeparator(' :: '); + $view->view->navigation = $this->config->menu; + + $this->view = $view; + return $this; + } + + /** + * Configure pagination settings + * + * @return self + */ + protected function configurePagination() + { + Paginator::addScrollingStylePrefixPath( + 'Icinga_Web_Paginator_ScrollingStyle', + 'Icinga/Web/Paginator/ScrollingStyle' + ); + + Paginator::setDefaultScrollingStyle('SlidingWithBorder'); + PaginationControl::setDefaultViewPartial( + array('mixedPagination.phtml','default') + ); + return $this; + } +} diff --git a/library/Icinga/Application/functions.php b/library/Icinga/Application/functions.php new file mode 100755 index 000000000..f4f01a06b --- /dev/null +++ b/library/Icinga/Application/functions.php @@ -0,0 +1,31 @@ +session = new SessionNamespace('IcingaAuth'); + } + + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new Auth(); + } + return self::$instance; + } + + public function isAuthenticated() + { + if ($this->userInfo === null) { + if ($sessionInfo = $this->session->userInfo) { + $this->userInfo = $sessionInfo; + } + } + return is_object($this->userInfo) && ! empty($this->userInfo->username); + } + + public function getUsername() + { + $this->assertIsAuthenticated(); + return $this->userInfo->username; + } + + public function getEmail() + { + $this->assertIsAuthenticated(); + return $this->userInfo->email; + } + + public function setAuthenticatedUser(User $user) + { + $this->userInfo = (object) array( + 'username' => $user->username, + 'permissions' => $user->getPermissionList(), + 'email' => $user->email, + ); + $this->session->userInfo = $this->userInfo; + } + + public function forgetAuthentication() + { + unset($this->session->userInfo); + $this->userInfo = null; + } + + public function hasPermission($route, $flags = 0x01) + { + $this->assertBeingAuthenticated(); + if (! array_key_exists($route, $this->userInfo->permissions)) { + return false; + } + + return $this->userInfo->permissions[$route] & $flags === $flags; + } + + protected function assertIsAuthenticated() + { + if (! $this->isAuthenticated()) { + throw new Exception\ProgrammingError( + 'Cannot fetch properties of a non-authenticated user' + ); + } + } +} diff --git a/library/Icinga/Authentication/Backend.php b/library/Icinga/Authentication/Backend.php new file mode 100644 index 000000000..19534478f --- /dev/null +++ b/library/Icinga/Authentication/Backend.php @@ -0,0 +1,26 @@ +config = $config; + $userbackend = ucwords(strtolower($config->users->backend)); + $class = '\\Icinga\\Authentication\\' . $userbackend . 'UserBackend'; + $this->userBackend = new $class($config->users); + } + + public function hasUsername($username) + { + return $this->userBackend->hasUsername($username); + } + + public function authenticate($username, $password = null) + { + return $this->userBackend->authenticate($username, $password); + } +} diff --git a/library/Icinga/Authentication/Backend/Db.php b/library/Icinga/Authentication/Backend/Db.php new file mode 100644 index 000000000..e69de29bb diff --git a/library/Icinga/Authentication/Backend/LegacyWeb1x.php b/library/Icinga/Authentication/Backend/LegacyWeb1x.php new file mode 100644 index 000000000..e69de29bb diff --git a/library/Icinga/Authentication/Group.php b/library/Icinga/Authentication/Group.php new file mode 100644 index 000000000..e69de29bb diff --git a/library/Icinga/Authentication/GroupBackend.php b/library/Icinga/Authentication/GroupBackend.php new file mode 100644 index 000000000..e69de29bb diff --git a/library/Icinga/Authentication/GroupMembership.php b/library/Icinga/Authentication/GroupMembership.php new file mode 100644 index 000000000..e69de29bb diff --git a/library/Icinga/Authentication/LdapUserBackend.php b/library/Icinga/Authentication/LdapUserBackend.php new file mode 100644 index 000000000..171d2f6f4 --- /dev/null +++ b/library/Icinga/Authentication/LdapUserBackend.php @@ -0,0 +1,57 @@ +connection = new Ldap\Connection($this->config); + } + + public function hasUsername($username) + { + if (! $username) { + return false; + } + return $this->connection->fetchOne( + $this->selectUsername($username) + ) === $username; + } + + protected function stripAsterisks($string) + { + return str_replace('*', '', $string); + } + + protected function selectUsername($username) + { + return $this->connection->select() + ->from('user', array('sAMAccountName')) + ->where('sAMAccountName', $this->stripAsterisks($username)); + } + + public function authenticate($username, $password = null) + { + if (empty($username) || empty($password)) { + return false; + } + if (! $this->connection->testCredentials( + $this->connection->fetchDN($this->selectUsername($username)), + $password + )) { + return false; + } + $user = User::create( + $this, + array( + 'username' => $username, + ) + ); + return $user; + } +} diff --git a/library/Icinga/Authentication/Storable.php b/library/Icinga/Authentication/Storable.php new file mode 100644 index 000000000..a861bd817 --- /dev/null +++ b/library/Icinga/Authentication/Storable.php @@ -0,0 +1,203 @@ + + * @author Icinga-Web Team + * @package Icinga\Application + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +abstract class Storable +{ + protected $key; + + /** + * Current Storable properties + */ + protected $props; + + /** + * Default property values for this Storable + * + * All allowed properties have to be defined here, otherwise they will be + * rejected + */ + protected $defaultProps = array(); + + /** + * Properties as they have been once loaded from backend + */ + protected $storedProps = array(); + + /** + * Whether this storable has been stored in the current state + */ + protected $stored = false; + + /** + * Create a new Storable instance, with data loaded from backend + * + * You should NEVER directly use this function unless you are absolutely + * sure on what you are doing. + * + * @param Backend The backend used to load this object from + * @param Array Property array + * @return Storable + */ + public static function create(UserBackend $backend, $props = array()) + { + $class = get_called_class(); + $object = new $class($props); + return $object; + } + + /** + * Override this function for custom cross-value checks before storing it + * + * @return boolean Whether the Storable is valid + */ + public function isValid() + { + return true; + } + + /** + * The constructor is protected, you should never override it + * + * Use the available hooks for all the things you need to do at construction + * time + * + * @param Array Property array + * @return void + */ + final protected function __construct($properties = array()) + { + $this->assertKeyHasBeenDefined(); + $this->props = $this->defaultProps; + foreach ($properties as $key => $val) { + $this->set($key, $val); + } + $this->assertKeyExists(); + } + + + /** + * Get property value, fail unless it exists + * + * @param string Property name + * @return mixed + */ + public function get($key) + { + $this->assertPropertyExists($key); + return $this->props[$key]; + return $this; + } + + /** + * Set property value, fail unless it exists + * + * @param string Property name + * @param mixed New property value + * @return Storable + */ + protected function set($key, $val) + { + $this->assertPropertyExists($key); + $this->props[$key] = $val; + return $this; + } + + /** + * Getter + * + * @param string Property name + * @return mixed + */ + public function __get($key) + { + return $this->get($key); + } + + /** + * Setter + * + * @param string Property name + * @param mixed New property value + * @return void + */ + public function __set($key, $val) + { + $this->set($key, $val); + } + + /** + * Whether the given property name exist + * + * @param string Property name + * @return boolean + */ + public function __isset($key) + { + return array_key_exists($key, $this->props); + } + + /** + * Makes sure that the Storable got it's unique key + * + * @throws \Exception + * @return Storable + */ + protected function assertKeyExists() + { + return $this->assertPropertyExists($this->key); + } + + /** + * Makes sure the given property is allowed + * + * @throws \Exception + * @return Storable + */ + protected function assertPropertyExists($key) + { + if (! array_key_exists($key, $this->props)) { + throw new \Exception( + sprintf( + 'Storable (%s) has no "%s" property', + get_class($this), + $key + ) + ); + } + return $this; + } + + /** + * Makes sure that the class inheriting Storable defined it's key column + * + * @throws \Exception + * @return Storable + */ + protected function assertKeyHasBeenDefined() + { + if ($this->key === null) { + throw new \Exception( + 'Implementation error, Storable needs a valid key' + ); + } + return $this; + } +} diff --git a/library/Icinga/Authentication/User.php b/library/Icinga/Authentication/User.php new file mode 100644 index 000000000..ccabfcb3e --- /dev/null +++ b/library/Icinga/Authentication/User.php @@ -0,0 +1,70 @@ + + * @author Icinga-Web Team + * @package Icinga\Application + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class User extends Storable +{ + protected $defaultProps = array( + 'username' => null, + 'password' => null, + 'first_name' => null, + 'last_name' => null, + 'email' => null, + ); + protected $permissions = array(); + protected $backend; + protected $groups; + protected $key = 'username'; + + public function listGroups() + { + if ($this->groups === null) { + $this->loadGroups(); + } + } + + protected function loadGroups() + { + // Whatever + } + + public function isMemberOf(Group $group) + { + + } + + public function getPermissionList() + { + return $this->permissions; + } + + public function hasPermission($uri, $permission) + { + + } + + public function grantPermission($uri, $permission) + { + + } + + public function revokePermission($uri, $permission) + { + + } +} diff --git a/library/Icinga/Authentication/UserBackend.php b/library/Icinga/Authentication/UserBackend.php new file mode 100644 index 000000000..259310ce1 --- /dev/null +++ b/library/Icinga/Authentication/UserBackend.php @@ -0,0 +1,28 @@ +config = $config; + $this->init(); + } + + protected function init() + { + } + + public function hasUsername($username) + { + return false; + } + + public function authenticate($username, $password = null) + { + return false; + } +} diff --git a/library/Icinga/Backend.php b/library/Icinga/Backend.php new file mode 100755 index 000000000..5839335d5 --- /dev/null +++ b/library/Icinga/Backend.php @@ -0,0 +1,38 @@ +backends; + if ($name === null) { + $name = Session::getInstance()->backend; + } + if ($name === null) { + $name = array_shift(array_keys($config->toArray())); + } + if (isset($config->backends->$name)) { + $config = $config->backends->$name; + $type = $config->type; + $type[0] = strtoupper($type[0]); + $class = '\\Icinga\\Backend\\' . $type; + self::$instances[$name] = new $class($config); + } else { + throw new \Exception(sprintf( + 'Got no config for backend %s', + $name + )); + } + } + return self::$instances[$name]; + } +} + diff --git a/library/Icinga/Backend/Combo.php b/library/Icinga/Backend/Combo.php new file mode 100755 index 000000000..22fff81e5 --- /dev/null +++ b/library/Icinga/Backend/Combo.php @@ -0,0 +1,53 @@ +listMyBackends(); + $query = null; + $msg = ''; + while ($query === null) { + try { + $backend_name = array_shift($backends); + $msg .= "Trying $backend_name"; + $backend = Backend::getInstance($backend_name); + if ($backend->hasView($view)) { + $query = $backend->from($view, $fields); + } + } catch (\Exception $e) { + $msg .= ' Failed: ' . $e->getMessage() . "\n"; + } + + if ($query !== null) $msg .= " Succeeded.\n"; + + if ($query === null && empty($backends)) { + throw new \Exception('All backends failed: ' . nl2br($msg)); + } + } + return $query; + } + + public function hasView($virtual_table) + { + $backends = $this->listMyBackends(); + while ($backend_name = array_shift($backends)) { + if (Backend::getInstance($backend_name)->hasView($virtual_table)) { + return true; + } + } + return false; + } + + protected function listMyBackends() + { + return preg_split('~,\s*~', $this->config->backends, -1, PREG_SPLIT_NO_EMPTY); + } +} + diff --git a/library/Icinga/Backend/Ido.php b/library/Icinga/Backend/Ido.php new file mode 100755 index 000000000..569f63d14 --- /dev/null +++ b/library/Icinga/Backend/Ido.php @@ -0,0 +1,292 @@ + + * CREATE INDEX web2_index ON icinga_scheduleddowntime (object_id, is_in_effect); + * CREATE INDEX web2_index ON icinga_comments (object_id); + * CREATE INDEX web2_index ON icinga_objects (object_id, is_active); -- (not sure yet) + * + * + * Other possible (history-related) indexes, still subject to tests: + * CREATE INDEX web2_index ON icinga_statehistory (object_id, state_time DESC); + * CREATE INDEX web2_index ON icinga_notifications (object_id, instance_id, start_time DESC); + * CREATE INDEX web2_index ON icinga_downtimehistory (object_id, actual_start_time, actual_end_time); + * + * @copyright Copyright (c) 2013 Icinga-Web Team + * @author Icinga-Web Team + * @package Icinga\Application + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Ido extends AbstractBackend +{ + protected $db; + protected $dbtype; + protected $prefix = 'icinga_'; + + /** + * Backend initialization starts here + * + * return void + */ + protected function init() + { + $this->connect(); + } + + /** + * Get our Zend_Db connection + * + * return \Zend_Db_Adapter_Abstract + */ + public function getAdapter() + { + return $this->db; + } + + public function getDbType() + { + return $this->dbtype; + } + + /** + * Get our IDO table prefix + * + * return string + */ + public function getPrefix() + { + return $this->prefix; + } + + // TODO: Move elsewhere. Really? Reasons may be: other backends need IDO + // access, even in environments running state details without IDO + protected function connect() + { + $this->dbtype = $this->config->get('dbtype', 'mysql'); + $options = array( + \Zend_Db::AUTO_QUOTE_IDENTIFIERS => false, + \Zend_Db::CASE_FOLDING => \Zend_Db::CASE_LOWER + ); + $drv_options = array( + \PDO::ATTR_TIMEOUT => 2, + // TODO: Check whether LC is useful. Zend_Db does fetchNum for Oci: + \PDO::ATTR_CASE => \PDO::CASE_LOWER + // TODO: ATTR_ERRMODE => ERRMODE_EXCEPTION vs ERRMODE_SILENT + ); + switch ($this->dbtype) { + case 'mysql': + $adapter = 'Pdo_Mysql'; + $drv_options[\PDO::MYSQL_ATTR_INIT_COMMAND] = + "SET SESSION SQL_MODE='STRICT_ALL_TABLES,NO_ZERO_IN_DATE," + . "NO_ZERO_DATE,NO_ENGINE_SUBSTITUTION';"; + // Not using ONLY_FULL_GROUP_BY as of performance impact + // TODO: NO_ZERO_IN_DATE as been added with 5.1.11. Is it + // ignored by other versions? + $port = $this->config->get('port', 3306); + break; + case 'pgsql': + $adapter = 'Pdo_Pgsql'; + $port = $this->config->get('port', 5432); + break; + case 'oracle': + $adapter = 'Pdo_Oci'; + // $adapter = 'Oracle'; + $port = $this->config->get('port', 1521); +// $drv_options[\PDO::ATTR_STRINGIFY_FETCHES] = true; + + +if ($adapter === 'Oracle') { + +putenv('ORACLE_SID=XE'); +putenv('ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe'); +putenv('PATH=$PATH:$ORACLE_HOME/bin'); +putenv('ORACLE_BASE=/u01/app/oracle'); +putenv('NLS_LANG=AMERICAN_AMERICA.UTF8'); + +} + + + $this->prefix = ''; + break; + default: + throw new \Exception(sprintf( + 'Backend "%s" is not supported', $type + )); + } + $attributes = array( + 'host' => $this->config->host, + 'port' => $port, + 'username' => $this->config->user, + 'password' => $this->config->pass, + 'dbname' => $this->config->db, + 'options' => $options, + 'driver_options' => $drv_options + ); + if ($this->dbtype === 'oracle') { + $attributes['persistent'] = true; + } + $this->db = \Zend_Db::factory($adapter, $attributes); + if ($adapter === 'Oracle') { + $this->db->setLobAsString(false); + } + $this->db->setFetchMode(\Zend_Db::FETCH_OBJ); + // $this->db->setFetchMode(\Zend_Db::FETCH_ASSOC); + + } + + + + /// *** TODO: EVERYTHING BELOW THIS LINE WILL BE MOVED AWAY *** /// + + + // UGLY temporary host fetch + public function fetchHost($host) + { + $object = \Icinga\Objects\Service::fromBackend( + $this->select() + ->from('servicelist') + ->where('so.name1 = ?', $host) + ->fetchRow() + ); + $object->customvars = $this->fetchCustomvars($host); + return $object; + } + + // UGLY temporary service fetch + public function fetchService($host, $service) + { + $object = \Icinga\Objects\Service::fromBackend( + $this->select() + ->from('servicelist') + ->where('so.name1 = ?', $host) + ->where('so.name2 = ?', $service) + ->fetchRow() + ); + $object->customvars = $this->fetchCustomvars($host, $service); + return $object; + } + + public function fetchCustomvars($host, $service = null) + { + if ($this->dbtype === 'oracle') return (object) array(); + + $select = $this->db->select()->from( + array('cv' => $this->prefix . 'customvariablestatus'), + array( + // 'host_name' => 'cvo.name1', + // 'service_description' => 'cvo.name2', + 'name' => 'cv.varname', + 'value' => 'cv.varvalue', + ) + )->join( + array('cvo' => $this->prefix . 'objects'), + 'cvo.object_id = cv.object_id', + array() + ); + $select->where('name1 = ?', $host); + if ($service === null) { + $select->where('objecttype_id = 1'); + } else { + $select->where('objecttype_id = 1'); + $select->where('name2 = ?', $service); + } + $select->where('is_active = 1')->order('cv.varname'); + return (object) $this->db->fetchPairs($select); + } + + // TODO: Move to module! + + public function fetchHardStatesForBpHosts($hosts) + { + return $this->fetchStatesForBp($hosts, 'last_hard_state'); + } + + public function fetchSoftStatesForBpHosts($hosts) + { + return $this->fetchStatesForBp($hosts, 'current_state'); + } + + public function fetchStatesForBp($hosts, $state_column = 'last_hard_state') + { + $select_hosts = $this->db->select()->from( + array('hs' => $this->prefix . 'hoststatus'), + array( + 'state' => 'hs.' . $state_column, + 'ack' => 'hs.problem_has_been_acknowledged', + 'in_downtime' => 'CASE WHEN (d.object_id IS NULL) THEN 0 ELSE 1 END', + 'combined' => 'hs.current_state << 2 + hs.problem_has_been_acknowledged << 1 + CASE WHEN (d.object_id IS NULL) THEN 0 ELSE 1 END' + ) + )->joinRight( + array('o' => $this->prefix . 'objects'), + 'hs.host_object_id = o.object_id', + array( + 'object_id' => 'o.object_id', + 'hostname' => 'o.name1', + 'service' => '(NULL)' + ) + )->joinLeft( + array('d' => $this->prefix . 'scheduleddowntime'), + 'o.object_id = d.object_id' + . ' AND d.was_started = 1' + . ' AND d.scheduled_end_time > NOW()' + . ' AND d.actual_start_time < NOW()', + array() + )->where('o.name1 IN (?)', $hosts) + ->where('o.objecttype_id = 1') + ->where('o.is_active = 1'); + + $select_services = $this->db->select()->from( + array('ss' => $this->prefix . 'servicestatus'), + array( + 'state' => 'ss.' . $state_column, + 'ack' => 'ss.problem_has_been_acknowledged', + 'in_downtime' => 'CASE WHEN (d.object_id IS NULL) THEN 0 ELSE 1 END', + 'combined' => 'ss.current_state << 2 + ss.problem_has_been_acknowledged << 1 + CASE WHEN (d.object_id IS NULL) THEN 0 ELSE 1 END' + ) + )->joinRight( + array('o' => $this->prefix . 'objects'), + 'ss.service_object_id = o.object_id', + array( + 'object_id' => 'o.object_id', + 'hostname' => 'o.name1', + 'service' => 'o.name2' + ) + )->joinLeft( + array('d' => $this->prefix . 'scheduleddowntime'), + 'o.object_id = d.object_id' + . ' AND d.was_started = 1' + . ' AND d.scheduled_end_time > NOW()' + . ' AND d.actual_start_time < NOW()', + array() + )->where('o.name1 IN (?)', $hosts) + ->where('o.is_active = 1') + ->where('o.objecttype_id = 2'); + + $union = $this->db->select()->union( + array( + '(' . $select_hosts . ')', // ZF-4338 :-( + '(' . $select_services . ')', + ), + // At least on MySQL UNION ALL seems to be faster than UNION in + // most situations, as it doesn't care about duplicates + \Zend_Db_Select::SQL_UNION_ALL + )->order('hostname')->order('service'); + + return $this->db->fetchAll($union); + } + + +} + diff --git a/library/Icinga/Backend/Ido/GroupsummaryQuery.php b/library/Icinga/Backend/Ido/GroupsummaryQuery.php new file mode 100755 index 000000000..78aeb76e7 --- /dev/null +++ b/library/Icinga/Backend/Ido/GroupsummaryQuery.php @@ -0,0 +1,191 @@ + 'SUM(CASE WHEN state = 0 THEN 1 ELSE 0 END)', + 'critical' => 'SUM(CASE WHEN state = 2 AND downtime = 0 AND ack = 0 THEN 1 ELSE 0 END)', + 'critical_dt' => 'SUM(CASE WHEN state = 2 AND downtime = 1 THEN 1 ELSE 0 END)', + 'critical_ack' => 'SUM(CASE WHEN state = 2 AND downtime = 0 AND ack = 1 THEN 1 ELSE 0 END)', + 'unknown' => 'SUM(CASE WHEN state = 3 AND downtime = 0 AND ack = 0 THEN 1 ELSE 0 END)', + 'unknown_dt' => 'SUM(CASE WHEN state = 3 AND downtime = 1 THEN 1 ELSE 0 END)', + 'unknown_ack' => 'SUM(CASE WHEN state = 3 AND downtime = 0 AND ack = 1 THEN 1 ELSE 0 END)', + 'warning' => 'SUM(CASE WHEN state = 1 AND downtime = 0 AND ack = 0 THEN 1 ELSE 0 END)', + 'warning_dt' => 'SUM(CASE WHEN state = 1 AND downtime = 1 THEN 1 ELSE 0 END)', + 'warning_ack' => 'SUM(CASE WHEN state = 1 AND downtime = 0 AND ack = 1 THEN 1 ELSE 0 END)', + 'last_state_change' => 'UNIX_TIMESTAMP(MAX(last_state_change))', + ); + + protected $order_columns = array( + 'state' => array( + 'ASC' => array( + 'ok ASC', + 'warning_dt ASC', + 'warning_ack ASC', + 'warning ASC', + 'unknown_dt ASC', + 'unknown_ack ASC', + 'unknown ASC', + 'critical_dt ASC', + 'critical_ack ASC', + 'critical ASC', + ), + 'DESC' => array( + 'critical DESC', + 'unknown DESC', + 'warning DESC', + 'critical_ack DESC', + 'critical_dt DESC', + 'unknown_ack DESC', + 'unknown_dt DESC', + 'warning_ack DESC', + 'warning_dt DESC', + 'ok DESC', + ), + 'default' => 'DESC' + ) + ); + + abstract protected function addSummaryJoins($query); + + protected function init() + { + parent::init(); + if ($this->dbtype === 'oracle') { + $this->columns['last_state_change'] = 'localts2unixts(MAX(last_state_change))'; + } + } + + protected function createQuery() + { + $this->columns[$this->name_alias] = $this->name_alias; + $this->order_columns['state']['ASC'][] = $this->name_alias . ' ASC'; + $this->order_columns['state']['DESC'][] = $this->name_alias . ' DESC'; + $this->order_columns['name'] = array( + 'ASC' => array( $this->name_alias . ' ASC'), + 'DESC' => array( $this->name_alias . ' DESC'), + 'default' => 'ASC' + ); + $sub_query = $this->createSubQuery(); + // $sub_query->group($this->sub_group_column); + // $sub_query->columns(array($this->name_alias => 'MAX(' . $this->sub_group_column . ')')); + $sub_query->columns(array($this->name_alias => $this->sub_group_column )); + $this->addSummaryJoins($sub_query); + $query = $this->db->select()->from( + array('sub' => $sub_query), + array() + ); + $query->group($this->name_alias); + $this->sub_query = $sub_query; + return $query; + } + + + protected function createCountQuery() + { + $this->sub_count_query = $this->createCountSubQuery(); + $this->sub_count_query->group($this->sub_group_column); + $this->addSummaryJoins($this->sub_count_query); + $count = $this->db->select()->from( + array('cnt' => $this->sub_count_query), + array() + ); + return $count; + } + + protected function createSubQuery() + { + + $query = $this->db->select() + ->from( + array('so' => $this->prefix . 'objects'), + array( + // MAX seems to be useless, but is required as of the GROUP below + // 'state' => 'MAX(ss.current_state)', + 'state' => 'ss.current_state', + // 'ack' => 'MAX(ss.problem_has_been_acknowledged)', + 'ack' => 'ss.problem_has_been_acknowledged', + // 'downtime' => 'MAX(CASE WHEN (dt.object_id IS NULL) THEN 0 ELSE 1 END)', + // 'downtime' => 'MAX(CASE WHEN (scheduled_downtime_depth = 0) THEN 0 ELSE 1 END)', + 'downtime' => 'CASE WHEN (scheduled_downtime_depth = 0) THEN 0 ELSE 1 END', + // 'last_state_change' => 'MAX(ss.last_state_change)', + 'last_state_change' => 'ss.last_state_change', + ) + )->joinLeft( + array('ss' => $this->prefix . 'servicestatus'), + "so.$this->object_id = ss.service_object_id", + array() + )->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = ss.service_object_id', + array() + )/*->joinLeft( + array('dt' => $this->prefix . 'scheduleddowntime'), + "so.$this->object_id = dt.object_id" + . ' AND dt.is_in_effect = 1', + array() + )->joinLeft( + array('co' => $this->prefix . 'comments'), + "so.$this->object_id = co.object_id", + array() + )*/ + ->where('so.is_active = 1') + ->where('so.objecttype_id = 2') + // Group is required as there could be multiple comments: + // ->group('so.' . $this->object_id) + ; + return $query; + } + + protected function createCountSubQuery() + { + return $this->db->select() + ->from( + array('so' => $this->prefix . 'objects'), + array('state' => 'MAX(ss.current_state)') + )->joinLeft( + array('ss' => $this->prefix . 'servicestatus'), + "so.$this->object_id = ss.service_object_id", + array() + )->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = ss.service_object_id', + array() + ); + } + + public function where($column, $value = null) + { + if ($column === 'problems') { + if ($value === 'true') { + $this->query->having('(SUM(CASE WHEN state = 2 AND downtime = 0 AND ack = 0 THEN 1 ELSE 0 END) + +SUM(CASE WHEN state = 2 AND downtime = 1 THEN 1 ELSE 0 END) + +SUM(CASE WHEN state = 2 AND downtime = 0 AND ack = 1 THEN 1 ELSE 0 END) + +SUM(CASE WHEN state = 3 AND downtime = 0 AND ack = 0 THEN 1 ELSE 0 END) + +SUM(CASE WHEN state = 3 AND downtime = 1 THEN 1 ELSE 0 END) + +SUM(CASE WHEN state = 3 AND downtime = 0 AND ack = 1 THEN 1 ELSE 0 END) + +SUM(CASE WHEN state = 1 AND downtime = 0 AND ack = 0 THEN 1 ELSE 0 END) + +SUM(CASE WHEN state = 1 AND downtime = 1 THEN 1 ELSE 0 END) + +SUM(CASE WHEN state = 1 AND downtime = 0 AND ack = 1 THEN 1 ELSE 0 END)) > 0'); + $this->sub_count_query->where('ss.current_state > 0'); + $this->sub_query->where('ss.current_state > 0'); + } + } elseif ($column === 'search') { + if ($value) { + // $this->sub_query->where($this->name_alias . ' LIKE ?', '%' . $value . '%'); + $this->sub_query->where($this->sub_group_column . ' LIKE ?', '%' . $value . '%'); + $this->sub_count_query->where($this->sub_group_column . ' LIKE ?', '%' . $value . '%'); + } + } else { + parent::where($column, $value); + } + return $this; + } +} + diff --git a/library/Icinga/Backend/Ido/HostgroupsummaryQuery.php b/library/Icinga/Backend/Ido/HostgroupsummaryQuery.php new file mode 100755 index 000000000..7eb374e42 --- /dev/null +++ b/library/Icinga/Backend/Ido/HostgroupsummaryQuery.php @@ -0,0 +1,14 @@ +joinServiceHostgroups($query); + } +} + diff --git a/library/Icinga/Backend/Ido/Query.php b/library/Icinga/Backend/Ido/Query.php new file mode 100755 index 000000000..0de186ca9 --- /dev/null +++ b/library/Icinga/Backend/Ido/Query.php @@ -0,0 +1,521 @@ + array( + 'ASC' => array( + 'host_name ASC', + 'service_description ASC' + ), + 'DESC' => array( + 'host_name DESC', + 'service_description ASC' + ), + 'default' => 'ASC' + ), + 'host_address' => array( + 'ASC' => array( + 'host_ipv4 ASC', + 'service_description ASC' + ), + 'DESC' => array( + 'host_ipv4 ASC', + 'service_description ASC' + ), + 'default' => 'ASC' + ), + 'service' => array( + 'ASC' => array( + 'service_description ASC' + ), + 'DESC' => array( + 'service_description DESC' + ), + 'default' => 'ASC' + ), + 'service_state_change' => array( + 'ASC' => array( + 'ss.last_state_change ASC' + ), + 'DESC' => array( + 'ss.last_state_change DESC' + ), + 'default' => 'DESC' + ), + 'service_state' => array( + 'ASC' => array( + 'CASE WHEN (ss.current_state = 3) THEN 2 WHEN (ss.current_state = 2) THEN 3 ELSE ss.current_state END DESC', // TODO: distinct severity in a better way + 'ss.problem_has_been_acknowledged ASC', + // 'CASE WHEN (ss.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END ASC', + 'service_in_downtime ASC', // TODO: Check if all dbs allow sorting by alias + 'ss.last_state_change DESC', + 'so.name1 ASC', + 'so.name2 ASC' + ), + 'DESC' => array( + 'CASE WHEN (ss.current_state = 3) THEN 2 WHEN (ss.current_state = 2) THEN 3 ELSE ss.current_state END ASC', + 'ss.problem_has_been_acknowledged ASC', + // 'CASE WHEN (ss.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END ASC', + 'service_in_downtime ASC', + 'ss.last_state_change DESC' + ), + 'default' => 'ASC' + ) + ); + + abstract protected function createQuery(); + + public function dump() + { + $this->finalize(); + return "QUERY\n=====\n" + . $this->query + . "\n\nCOUNT\n=====\n" + . $this->count_query + . "\n\n"; + } + + public function getCountQueryObject() + { + return $this->finalize()->count_query; + } + + public function getQueryObject() + { + return $this->finalize()->query; + } + + protected function createCountQuery() + { + return clone($this->query); + } + + protected function init() + { + $this->db = $this->backend->getAdapter(); + $this->dbtype = $this->backend->getDbType(); + if ($this->dbtype === 'oracle') { + $this->object_id = $this->hostgroup_id = $this->servicegroup_id = 'id'; + } + $this->prefix = $this->backend->getPrefix(); + $this->query = $this->createQuery(); + $this->count_query = $this->createCountQuery(); + } + + protected function finalize() + { + if ($this->finalized) return $this; + $this->finalized = true; + $this->query->columns($this->columns); + if ($this->count_columns === null) { + $this->count_columns = array('cnt' => 'COUNT(*)'); + } + if (! $this->ordered) { + $this->order(); + } + $this->count_query->columns($this->count_columns); + return $this; + } + + protected function prepareServiceStatesQuery() + { + $query = $this->db->select() + ->from( + array('hs' => $this->prefix . 'hoststatus'), + array() + )->join( + array('h' => $this->prefix . 'hosts'), + 'hs.host_object_id = h.host_object_id', + array() + )->join( + array('s' => $this->prefix . 'services'), + 's.host_object_id = h.host_object_id', + array() + )->join( + array('so' => $this->prefix . 'objects'), + "so.$this->object_id = s.service_object_id AND so.is_active = 1", + array() + )->joinLeft( + array('ss' => $this->prefix . 'servicestatus'), + "so.$this->object_id = ss.service_object_id", + array() + ); + // $this->joinServiceDowntimes($query); + // $query->group('so.object_id'); + return $query; + } + + protected function prepareServicesCount() + { + // TODO: Depends on filter, some cols could be avoided + $query = $this->db->select()->from( + array('hs' => $this->prefix . 'hoststatus'), + array() + )->join( + array('h' => $this->prefix . 'hosts'), + 'hs.host_object_id = h.host_object_id', + array() + )->join( + array('s' => $this->prefix . 'services'), + 's.host_object_id = h.host_object_id', + array() + )->join( + array('so' => $this->prefix . 'objects'), + "so.$this->object_id = s.service_object_id AND so.is_active = 1", + "COUNT(so.$this->object_id)" + )->joinLeft( + array('ss' => $this->prefix . 'servicestatus'), + "so.$this->object_id = ss.service_object_id", + array() + ); + // $this->joinServiceDowntimes($query); + return $query; + } + + protected function joinHostgroups($query = null) + { + if ($query === null) $query = $this->query; + + $query->join( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = h.host_object_id', + array() + )->join( + array('hg' => $this->prefix . 'hostgroups'), + "hgm.hostgroup_id = hg.$this->hostgroup_id", + array() + ); + + return $this; + } + + protected function joinServiceHostgroups($query) + { + if ($query === null) $query = $this->query; + + $query->join( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->join( + array('hg' => $this->prefix . 'hostgroups'), + "hgm.hostgroup_id = hg.$this->hostgroup_id", + array() + ); + + return $this; + } + + protected function joinServicegroups($query) + { + if ($query === null) $query = $this->query; + + $query->join( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->join( + array('sg' => $this->prefix . 'servicegroups'), + "sgm.servicegroup_id = sg.$this->servicegroup_id", + array() + ); + + return $this; + } + + protected function joinServiceDowntimes($query) + { + $query->joinLeft( + array('dt' => $this->prefix . 'scheduleddowntime'), + "so.$this->object_id = dt.object_id" + . ' AND dt.is_in_effect = 1', + array() + ); + // NDO compat (doesn't work correctly like this): + // $now = "'" . date('Y-m-d H:i:s') . "'"; + // . ' AND dt.was_started = 1' + // . ' AND dt.scheduled_end_time > ' . $now + // . ' AND dt.actual_start_time < ' . $now, + return $query; + } + + public function where($column, $value = null) + { + // Ugly temporary hack: + foreach (array($this->query, $this->count_query) as $query) { + if ($column === 'search') { + if ($this->dbtype === 'mysql') { + $query->where($this->db->quoteInto( + 'so.name2 COLLATE latin1_general_ci LIKE ?' + . ' OR so.name1 COLLATE latin1_general_ci LIKE ?', + '%' . $value . '%', + '%' . $value . '%' + )); + } else { + $query->where($this->db->quoteInto( + 'LOWER(so.name2) LIKE ?' + . ' OR LOWER(so.name1) LIKE ?', + '%' . strtolower($value) . '%', + '%' . strtolower($value) . '%' + )); + } + continue; + } + // TODO: Check if this also works based on column: + if ($column === 'hostgroups') { + $this->appendHostgroupLimit($query, $value); + continue; + } + if (preg_match('~^_([^_]+)_(.+)$~', $column, $m)) { + switch($m[1]) { + case 'host': + $this->appendHostCustomVarLimit($query, $m[2], $value); + break; + case 'service': + $this->appendServiceCustomVarLimit($query, $m[2], $value); + break; + } + continue; + } + //$column = preg_replace('~^current_state~', 'ss.current_state', $column); + if (array_key_exists($column, $this->available_columns)) { + $column = $this->available_columns[$column]; + } + $query->where($this->prepareFilterStringForColumn($column, $value)); + } + + /*->orWhere('last_state_change > ?', $new)*/ + return $this; + } + + public function order($column = '', $dir = null) + { + $this->ordered = true; + return $this->applyOrder($column, $dir); + } + + protected function applyOrder($order = '', $order_dir = null) + { + if (! array_key_exists($order, $this->order_columns)) { + $order = key($this->order_columns); + } + + if ($order_dir === null) { + $order_dir = $this->order_columns[$order]['default']; + } + foreach ($this->order_columns[$order][$order_dir] as $col) { + $this->query->order($col); + } + return $this; + } + + protected function addServiceComments($query = null) + { + if ($query === null) { + $query = $this->query; + } + $query->joinLeft( + array('co' => $this->prefix . 'comments'), + "so.$this->object_id = co.object_id", + array() + ) + + ->group('so.object_id') + + ; + return $this; + } + + /** + * $column = col + * $value = abc,cde,cd*,!egh,!*hh* + * -> (col IN ('abc', 'cde') OR col LIKE 'cd%') AND (col != 'egh' AND col NOT LIKE '%hh%') + */ + protected function prepareFilterStringForColumn($column, $value) + { + $filter = ''; + $filters = array(); + + $or = array(); + $and = array(); + + if (strpos($value, ',') !== false) { + $value = preg_split('~,~', $value, -1, PREG_SPLIT_NO_EMPTY); + } + if (! is_array($value)) { + $value = array($value); + } + + // Go through all given values + foreach ($value as $val) { + // Value starting with - means negation + if ($val[0] === '-') { + $val = substr($val, 1); + if (strpos($val, '*') === false) { + $and[] = $this->db->quoteInto($column . ' != ?', $val); + } else { + $and[] = $this->db->quoteInto( + $column . ' NOT LIKE ?', + str_replace('*', '%', $val) + ); + } + // Starting with + enforces AND + } elseif ($val[0] === '+') { + $val = substr($val, 1); + if (strpos($val, '*') === false) { + $and[] = $this->db->quoteInto($column . ' = ?', $val); + } else { + $and[] = $this->db->quoteInto( + $column . ' LIKE ?', + str_replace('*', '%', $val) + ); + } + // All others ar ORs: + } else { + if (strpos($val, '*') === false) { + $or[] = $this->db->quoteInto($column . ' = ?', $val); + } else { + $or[] = $this->db->quoteInto( + $column . ' LIKE ?', + str_replace('*', '%', $val) + ); + } + } + } + + if (! empty($or)) { $filters[] = implode(' OR ', $or); } + if (! empty($and)) { $filters[] = implode(' AND ', $and); } + if (! empty($filters)) { + $filter = '(' . implode(') AND (', $filters) . ')'; + } + + return $filter; + } + + protected function addCustomVarColumn($query, $alias, $name, $filter = null) + { + // TODO: Improve this: + if (! preg_match('~^[a-zA-Z0-9_]+$~', $name)) { + throw new \Exception(sprintf( + 'Got invalid custom var: "%s"', + $name + )); + } + $qobj = spl_object_hash($query); + if (! array_key_exists($qobj, $this->custom_cols)) { + $this->custom_cols[$qobj] = array(); + } + + if (array_key_exists($alias, $this->custom_cols[$qobj])) { + if ($name !== $this->custom_cols[$qobj][$alias]) { + throw new \Exception(sprintf( + 'Cannot add CV alias "%s" twice with different target', + $alias + )); + } + return $this; + } + $query->join( + // TODO: Allow multiple limits with different aliases + array($alias => $this->prefix . 'customvariablestatus'), + 's.host_object_id = ' . $alias . '.object_id' + . ' AND ' . $alias . '.varname = ' + . $this->db->quote(strtoupper($name)) + //. ($filter === null ? '' : ' AND ' . $filter), + , + array() + ); + $this->custom_cols[$qobj][$alias] = $name; + return $this; + } + + protected function appendHostCustomVarLimit($query, $key, $value) + { + $alias = 'hcv_' . strtolower($key); + $filter = $this->prepareFilterStringForColumn($alias . '.varvalue', $value); + $this->addCustomVarColumn($query, $alias, $key); + $query->where($filter); + return $query; + } + + protected function appendHostgroupLimit($query, $hostgroups) + { + return $query->join( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->join( + array('hg' => $this->prefix . 'hostgroups'), + "hgm.hostgroup_id = hg.$this->hostgroup_id", + array() + ) + ->where('hg.alias IN (?)', $hostgroups); + } + + public function count() + { + return $this->db->fetchOne( + $this->finalize()->count_query + ); + } + + public function fetchAll() + { + return $this->db->fetchAll($this->finalize()->query); + } + + public function fetchRow() + { + return $this->db->fetchRow($this->finalize()->query); + } + + public function fetchOne() + { + return $this->db->fetchOne($this->finalize()->query); + } + + public function fetchPairs() + { + return $this->db->fetchPairs($this->finalize()->query); + } + + /** + * Sets a limit count and offset to the query + * + * @param int $count Number of rows to return + * @param int $offset Row offset to start from + * @return \Icinga\Backend\Query This Query object + */ + public function limit($count = null, $offset = null) + { + $this->query->limit($count, $offset); + return $this; + } + + public function __toString() + { + $this->finalize(); + return (string) $this->query; + } +} + diff --git a/library/Icinga/Backend/Ido/ServicegroupsummaryQuery.php b/library/Icinga/Backend/Ido/ServicegroupsummaryQuery.php new file mode 100755 index 000000000..49a735f59 --- /dev/null +++ b/library/Icinga/Backend/Ido/ServicegroupsummaryQuery.php @@ -0,0 +1,14 @@ +joinServicegroups($query); + } +} + diff --git a/library/Icinga/Backend/Ido/ServicelistQuery.php b/library/Icinga/Backend/Ido/ServicelistQuery.php new file mode 100755 index 000000000..993f2f9eb --- /dev/null +++ b/library/Icinga/Backend/Ido/ServicelistQuery.php @@ -0,0 +1,95 @@ + 'so.name1', + 'host_display_name' => 'h.display_name', + 'host_alias' => 'h.alias', + 'host_address' => 'h.address', + 'host_ipv4' => 'INET_ATON(h.address)', + + 'host_icon_image' => 'h.icon_image', + + // Host state + 'host_state' => 'hs.current_state', + 'host_output' => 'hs.output', + 'host_perfdata' => 'hs.perfdata', + 'host_acknowledged' => 'hs.problem_has_been_acknowledged', + 'host_does_active_checks' => 'hs.active_checks_enabled', + 'host_accepts_passive_checks' => 'hs.passive_checks_enabled', + 'host_last_state_change' => 'UNIX_TIMESTAMP(hs.last_state_change)', + + // Service config + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name', + + // Service state + 'current_state' => 'ss.current_state', + 'service_state' => 'ss.current_state', + 'service_output' => 'ss.output', + 'service_perfdata' => 'ss.perfdata', + 'service_acknowledged' => 'ss.problem_has_been_acknowledged', + 'service_in_downtime' => 'CASE WHEN (ss.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END', + 'service_handled' => 'CASE WHEN ss.problem_has_been_acknowledged + ss.scheduled_downtime_depth > 0 THEN 1 ELSE 0 END', + 'service_does_active_checks' => 'ss.active_checks_enabled', + 'service_accepts_passive_checks' => 'ss.passive_checks_enabled', + 'service_last_state_change' => 'UNIX_TIMESTAMP(ss.last_state_change)', + + // Service comments + //'service_downtimes_with_info' => "IF(dt.object_id IS NULL, NULL, GROUP_CONCAT(CONCAT('[', dt.author_name, '] ', dt.comment_data) ORDER BY dt.entry_time DESC SEPARATOR '|'))", + //'service_comments_with_info' => "IF(co.object_id IS NULL, NULL, GROUP_CONCAT(CONCAT('[', co.author_name, '] ', co.comment_data) ORDER BY co.entry_time DESC SEPARATOR '|'))", + // SLA Example: + // 'sla' => "icinga_availability(so.object_id," + // . " '2012-12-01 00:00:00', '2012-12-31 23:59:59')", + ); + + protected function init() + { + parent::init(); + if ($this->dbtype === 'oracle') { + $this->columns['host_last_state_change'] = + 'localts2unixts(ss.last_state_change)'; + $this->columns['service_last_state_change'] = + 'localts2unixts(ss.last_state_change)'; + } + } + + public function where($column, $value = null) + { + // Ugly temporary hack: + if ($column === 'problems') { + if ($value === true || $value === 'true') { + parent::where('current_state', '-0'); + } elseif ($value === false || $value === 'false') { + parent::where('current_state', '0'); + } + return $this; + } + + if ($column === 'handled') $column = 'service_handled'; + parent::where($column, $value); + return $this; + } + + protected function createQuery() + { + $query = $this->prepareServiceStatesQuery(); + if ($this->dbtype === 'mysql') { + // $this->addServiceComments($query); + } else { + $this->columns['host_ipv4'] = 'h.address'; + $this->columns['service_downtimes_with_info'] = '(NULL)'; + $this->columns['service_comments_with_info'] = '(NULL)'; + } + return $query; + } + + protected function createCountQuery() + { + return $this->prepareServicesCount(); + } +} + diff --git a/library/Icinga/Backend/Ido/StatehistoryQuery.php b/library/Icinga/Backend/Ido/StatehistoryQuery.php new file mode 100644 index 000000000..623754ddf --- /dev/null +++ b/library/Icinga/Backend/Ido/StatehistoryQuery.php @@ -0,0 +1,86 @@ + 'sho.name1', + 'service_description' => 'sho.name2', + 'object_type' => "CASE WHEN sho.objecttype_id = 1 THEN 'host' ELSE 'service' END", + 'timestamp' => 'UNIX_TIMESTAMP(sh.state_time)', + 'state' => 'sh.state', + 'last_state' => 'sh.last_state', + 'last_hard_state' => 'sh.last_hard_state', + 'attempt' => 'sh.current_check_attempt', + 'max_attempts' => 'sh.max_check_attempts', + 'output' => 'sh.output', // no long_output in browse + + ); + protected $order_columns = array( + 'timestamp' => array( + 'ASC' => array( + 'state_time ASC', + ), + 'DESC' => array( + 'state_time DESC', + ), + 'default' => 'DESC' + ) + ); + + protected function init() + { + parent::init(); + if ($this->dbtype === 'oracle') { + $this->columns['timestamp'] = + 'localts2unixts(sh.state_time)'; + } + } + + public function where($column, $value = null) + { + if ($column === 'problems') { + if ($value === 'true') { + foreach (array($this->query, $this->count_query) as $query) { + $query->where('sh.state > 0'); + } + } + return $this; + } + if ($column === 'host') { + foreach (array($this->query, $this->count_query) as $query) { + $query->where('sho.name1 = ?', $value); + } + return $this; + } + if ($column === 'service') { + foreach (array($this->query, $this->count_query) as $query) { + $query->where('sho.name2 = ?', $value); + } + return $this; + } + + parent::where($column, $value); + return $this; + } + + protected function createQuery() + { + $query = $this->db->select()->from( + array('sh' => $this->prefix . 'statehistory'), + array() + // )->join( + )->joinLeft( // LEFT is bullshit but greatly helps MySQL + // Problem -> has to be INNER once permissions are in effect + // Therefore this should probably be "flexible" or handled in another + // way + array('sho' => $this->prefix . 'objects'), + 'sho.' . $this->object_id . ' = sh.object_id AND sho.is_active = 1', + array() + ); + + return $query; + } +} + diff --git a/library/Icinga/Backend/Livestatus.php b/library/Icinga/Backend/Livestatus.php new file mode 100755 index 000000000..e06ffad90 --- /dev/null +++ b/library/Icinga/Backend/Livestatus.php @@ -0,0 +1,45 @@ + + * @author Icinga-Web Team + * @package Icinga\Application + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Livestatus extends AbstractBackend +{ + protected $connection; + + /** + * Backend initialization starts here + * + * return void + */ + protected function init() + { + $this->connection = new Connection($this->config->socket); + } + + /** + * Get our Livestatus connection + * + * return \Icinga\Protocol\Livestatus\Connection + */ + public function getConnection() + { + return $this->connection; + } +} + diff --git a/library/Icinga/Backend/Livestatus/Query.php b/library/Icinga/Backend/Livestatus/Query.php new file mode 100644 index 000000000..92bc80aa7 --- /dev/null +++ b/library/Icinga/Backend/Livestatus/Query.php @@ -0,0 +1,213 @@ + array( + 'ASC' => array( + 'host_name ASC', + 'service_description ASC' + ), + 'DESC' => array( + 'host_name DESC', + 'service_description ASC' + ), + 'default' => 'ASC' + ), + 'host_address' => array( + 'ASC' => array( + 'host_ipv4 ASC', + 'service_description ASC' + ), + 'DESC' => array( + 'host_ipv4 ASC', + 'service_description ASC' + ), + 'default' => 'ASC' + ), + 'service' => array( + 'ASC' => array( + 'service_description ASC' + ), + 'DESC' => array( + 'service_description DESC' + ), + 'default' => 'ASC' + ), + 'service_state_change' => array( + 'ASC' => array( + 'ss.last_state_change ASC' + ), + 'DESC' => array( + 'ss.last_state_change DESC' + ), + 'default' => 'DESC' + ), + 'service_state' => array( + 'ASC' => array( + 'IF (ss.current_state = 3, 2, IF(ss.current_state = 2, 3, ss.current_state)) DESC', + 'ss.problem_has_been_acknowledged ASC', + 'IF(dt.object_id IS NULL, 0, 1) ASC', + 'ss.last_state_change DESC' + ), + 'DESC' => array( + 'IF (ss.current_state = 3, 2, IF(ss.current_state = 3, 2, ss.current_state)) DESC', + 'ss.problem_has_been_acknowledged ASC', + 'IF(dt.object_id IS NULL, 0, 1) ASC', + 'ss.last_state_change DESC' + ), + 'default' => 'ASC' + ) + ); + + abstract protected function createQuery(); + + protected function init() + { + $this->connection = $this->backend->getConnection(); + $this->query = $this->createQuery(); + } + + public function where($column, $value = null) + { + if ($column === 'problems') { + if ($value === 'true') { + $this->query->where('state > 0'); + } elseif ($value === 'false') { + $this->query->where('state = 0'); + } + return $this; + } + if ($column === 'handled') { + if ($value === 'true') { + // TODO: Not yet + } elseif ($value === 'false') { + // TODO: Not yet + } + return $this; + } + + // Ugly temporary hack: + $colcheck = preg_replace('~[\s=><].+$~', '', $column); + if (array_key_exists($colcheck, $this->available_columns)) { + $query->where(preg_replace( + '~' . $colcheck . '~', + $this->available_columns[$colcheck], + $column + ), $value); + } else { + $this->query->where($column, $value); + } + + return $this; + } + + protected function finalize() + { + return $this; + + if ($this->finalized) return $this; + $this->finalized = true; + $this->query->columns($this->columns); + if ($this->count_columns === null) { + $this->count_columns = array('cnt' => 'COUNT(*)'); + } + if (! $this->ordered) { + $this->order(); + } + $this->count_query->columns($this->count_columns); + return $this; + } + + public function applyFilters($filters = array()) + { + foreach ($filters as $key => $val) { + $this->where($key, $val); + } + return $this; + } + + public function order($column = '', $dir = null) + { + $this->ordered = true; + return $this->applyOrder($column, $dir); + } + + protected function applyOrder($order = '', $order_dir = null) + { + return $this; + + if (! array_key_exists($order, $this->order_columns)) { + $order = key($this->order_columns); + } + + if ($order_dir === null) { + $order_dir = $this->order_columns[$order]['default']; + } + foreach ($this->order_columns[$order][$order_dir] as $col) { + $this->query->order($col); + } + return $this; + } + + public function count() + { + return $this->connection->count( + $this->finalize()->query + ); + } + + public function fetchAll() + { + return $this->connection->fetchAll($this->finalize()->query); + } + + public function fetchRow() + { + return $this->db->fetchRow($this->finalize()->query); + } + + public function fetchOne() + { + return $this->db->fetchOne($this->finalize()->query); + } + + public function fetchPairs() + { + return $this->db->fetchPairs($this->finalize()->query); + } + + /** + * Sets a limit count and offset to the query + * + * @param int $count Number of rows to return + * @param int $offset Row offset to start from + * @return \Icinga\Backend\Query This Query object + */ + public function limit($count = null, $offset = null) + { + $this->query->limit($count, $offset); + return $this; + } + + + // For debugging and testing only: + public function __toString() + { + $this->finalize(); + return (string) $this->query; + } + +} + diff --git a/library/Icinga/Backend/Livestatus/ServicelistQuery.php b/library/Icinga/Backend/Livestatus/ServicelistQuery.php new file mode 100644 index 000000000..708422d5e --- /dev/null +++ b/library/Icinga/Backend/Livestatus/ServicelistQuery.php @@ -0,0 +1,46 @@ + 'host_address', // TODO + 'host_icon_image', + + // Host state + 'host_state', + 'host_output' => 'host_plugin_output', + 'host_perfdata' => 'host_perf_data', + 'host_acknowledged', + 'host_does_active_checks' => 'host_active_checks_enabled', + 'host_accepts_passive_checks' => 'host_accept_passive_checks', + 'host_last_state_change', + + // Service config + 'service_description' => 'description', + 'service_display_name' => 'display_name', + + // Service state + 'service_state' => 'state', + 'service_output' => 'plugin_output', + 'service_perfdata' => 'perf_data', + 'service_acknowledged' => 'acknowledged', + 'service_does_active_checks' => 'active_checks_enabled', + 'service_accepts_passive_checks' => 'accept_passive_checks', + 'service_last_state_change' => 'last_state_change', + + // Service comments + 'comments_with_info', + 'downtimes_with_info', + ); + + protected function createQuery() + { + return $this->connection->select()->from('services', $this->available_columns); + } +} + diff --git a/library/Icinga/Backend/MonitoringObjectList.php b/library/Icinga/Backend/MonitoringObjectList.php new file mode 100755 index 000000000..85081e944 --- /dev/null +++ b/library/Icinga/Backend/MonitoringObjectList.php @@ -0,0 +1,110 @@ +dataSet = $dataset; + $this->position = 0; + $this->dataView = $dataView; + } + + public function count() + { + return count($this->dataSet); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + */ + public function current() + { + if ($this->dataView) + return $this; + return $this->dataSet[$this->position]; + } + + /** + * (PHP 5 >= 5.0.0)
+ * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + */ + public function next() + { + $this->position++; + } + + /** + * (PHP 5 >= 5.0.0)
+ * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + */ + public function key() + { + return $this->position; + } + + /** + * (PHP 5 >= 5.0.0)
+ * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + */ + public function valid() + { + return $this->position < count($this->dataSet); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + */ + public function rewind() + { + $this->position = 0; + } + + public function __isset($name) + { + return $this->dataView->exists($this->dataSet[$this->position],$name); + } + + function __get($name) + { + return $this->dataView->get($this->dataSet[$this->position],$name); + + + } + + function __set($name, $value) + { + throw new \Exception("Setting is currently not available for objects"); + } + +} diff --git a/library/Icinga/Data/AbstractQuery.php b/library/Icinga/Data/AbstractQuery.php new file mode 100644 index 000000000..5fb141f62 --- /dev/null +++ b/library/Icinga/Data/AbstractQuery.php @@ -0,0 +1,376 @@ +ds = $ds; + + if ($columns === null) { + $columns = $this->getDefaultColumns(); + } + if ($columns !== null) { + $this->columns($columns); + } + + $this->init(); + } + + protected function getDefaultColumns() + { + return null; + } + + /** + * Choose a table and the colums you are interested in + * + * Query will return all available columns if none are given here + * + * @return self + */ + public function from($table, $columns = null) + { + $this->table = $table; + if ($columns !== null) { + $this->columns($columns); + } + return $this; + } + + public function columns($columns) + { + if (is_array($columns)) { + $this->columns = $columns; + } else { + $this->columns = array($columns); + } + + return $this; + } + + /** + * Use once or multiple times to filter result set + * + * Multiple where calls will be combined by a logical AND operation + * + * @param string $key Column or backend-specific search expression + * @param string $val Search value, must be escaped automagically + * + * @return self + */ + public function where($key, $val = null) + { + $this->filters[] = array($key, $val); + return $this; + } + + /** + * Sort query result by the given column name + * + * Sort direction can be ascending (self::SORT_ASC, being the default) + * or descending (self::SORT_DESC). + * + * Preferred usage: + * + * $query->sort('column_name ASC') + * + * + * @param string $col Column, may contain direction separated by space + * @param int $dir Sort direction + * + * @return self + */ + public function order($col, $dir = null) + { + if ($dir === null) { + if (($pos = strpos($col, ' ')) === false) { + $dir = $this->getDefaultSortDir($col); + } else { + $dir = strtoupper(substr($col, $pos + 1)); + if ($dir === 'DESC') { + $dir = self::SORT_DESC; + } else { + $dir = self::SORT_ASC; + } + $col = substr($col, 0, $pos); + } + } else { + if (strtoupper($dir) === 'DESC') { + $dir = self::SORT_DESC; + } else { + $dir = self::SORT_ASC; + } + } + $this->order_columns[] = array($col, $dir); + return $this; + } + + protected function getDefaultSortDir($col) + { + return self::SORT_ASC; + } + + /** + * Limit the result set + * + * @param int $count Return not more than that many rows + * @param int $offset Result starts with this row + * + * @return self + */ + // Nur wenn keine stats, sonst im RAM!! + // Offset gibt es nicht, muss simuliert werden + public function limit($count = null, $offset = null) + { + if (! preg_match('~^\d+~', $count . $offset)) { + throw new Exception\ProgrammingError( + sprintf( + 'Got invalid limit: %s, %s', + $count, + $offset + ) + ); + } + $this->limit_count = (int) $count; + $this->limit_offset = (int) $offset; + return $this; + } + + /** + * Wheter at least one order column has been applied to this Query + * + * @return bool + */ + public function hasOrder() + { + return ! empty($this->order_columns); + } + + /** + * Wheter a limit has been applied to this Query + * + * @return bool + */ + public function hasLimit() + { + return $this->limit_count !== null; + } + + /** + * Wheter a starting offset been applied to this Query + * + * @return bool + */ + public function hasOffset() + { + return $this->limit_offset > 0; + } + + /** + * Get the query limit + * + * @return int|null + */ + public function getLimit() + { + return $this->limit_count; + } + + /** + * Get the query starting offset + * + * @return int|null + */ + public function getOffset() + { + return $this->limit_offset; + } + + /** + * Get the columns that have been asked for with this query + * + * @return array + */ + public function listColumns() + { + return $this->columns; + } + + /** + * Get the filters that have been applied to this query + * + * @return array + */ + public function listFilters() + { + return $this->filters; + } + + /** + * Extend this function for things that should happen at construction time + */ + protected function init() + { + } + + /** + * Extend this function for things that should happen before query execution + */ + protected function finish() + { + } + + /** + * Total result size regardless of limit and offset + * + * @return int + */ + public function count() + { + return $this->ds->count($this); + } + + /** + * Fetch result as an array of objects + * + * @return array + */ + public function fetchAll() + { + return $this->ds->fetchAll($this); + } + + /** + * Fetch first result row + * + * @return object + */ + public function fetchRow() + { + return $this->ds->fetchRow($this); + } + + /** + * Fetch first result column + * + * @return array + */ + public function fetchColumn() + { + return $this->ds->fetchColumn($this); + } + + /** + * Fetch first column value from first result row + * + * @return mixed + */ + public function fetchOne() + { + return $this->ds->fetchOne($this); + } + + /** + * Fetch result as a key/value pair array + * + * @return array + */ + public function fetchPairs() + { + return $this->ds->fetchPairs($this); + } + + /** + * Return a pagination adapter for this query + * + * @return \Zend_Paginator + */ + public function paginate($limit = null, $page = null) + { + $this->finish(); + if ($page === null && $limit === null) { + $request = \Zend_Controller_Front::getInstance()->getRequest(); + + if ($page === null) { + $page = $request->getParam('page', 0); + } + + if ($limit === null) { + $limit = $request->getParam('limit', 20); + } + } + + $paginator = new \Zend_Paginator( + new \Icinga\Web\Paginator\Adapter\QueryAdapter($this) + ); + + $paginator->setItemCountPerPage($limit); + $paginator->setCurrentPageNumber($page); + + return $paginator; + } + + /** + * Destructor. Remove $ds, just to be on the safe side + */ + public function __destruct() + { + unset($this->ds); + } +} diff --git a/library/Icinga/Data/ArrayDatasource.php b/library/Icinga/Data/ArrayDatasource.php new file mode 100644 index 000000000..fa447e5aa --- /dev/null +++ b/library/Icinga/Data/ArrayDatasource.php @@ -0,0 +1,100 @@ +data = (array) $array; + } + + /** + * Instantiate an ArrayQuery object + * + * @return ArrayQuery + */ + public function select() + { + return new ArrayQuery($this); + } + + public function fetchColumn(ArrayQuery $query) + { + $result = array(); + foreach ($this->getResult($query) as $row) { + $arr = (array) $row; + $result[] = array_shift($arr); + } + return $result; + } + + public function fetchAll(ArrayQuery $query) + { + $result = $this->getResult($query); + return $result; + } + + public function count(ArrayQuery $query) + { + $this->createResult($query); + return $query->getCount(); + } + + protected function createResult(ArrayQuery $query) + { + if ($query->hasResult()) { + return $this; + } + $result = array(); + $filters = $query->listFilters(); + $columns = $query->listColumns(); + foreach ($this->data as & $row) { + + // Skip rows that do not match applied filters + foreach ($filters as $f) { + if ($row->{$f[0]} !== $f[1]) { + continue 2; + } + } + + // Get only desired columns if asked so + if (empty($columns)) { + $result[] = $row; + } else { + $c_row = (object) array(); + foreach ($columns as $key) { + if (isset($row->$key)) { + $c_row->$key = $row->$key; + } else { + $c_row->$key = null; + } + } + $result[] = $c_row; + } + } + + // Sort the result + if ($query->hasOrder()) { + usort($result, array($query, 'compare')); + } + + $query->setResult($result); + return $this; + } + + protected function getResult(ArrayQuery $query) + { + if (! $query->hasResult()) { + $this->createResult($query); + } + return $query->getLimitedResult(); + } +} diff --git a/library/Icinga/Data/ArrayQuery.php b/library/Icinga/Data/ArrayQuery.php new file mode 100644 index 000000000..988aa0505 --- /dev/null +++ b/library/Icinga/Data/ArrayQuery.php @@ -0,0 +1,85 @@ +count; + } + + public function hasResult() + { + return $this->result !== null; + } + + public function getFullResult() + { + return $this->result; + } + + public function getLimitedResult() + { + if ($this->hasLimit()) { + if ($this->hasOffset()) { + $offset = $this->getOffset(); + } else { + $offset = 0; + } + return array_slice($this->result, $offset, $this->getLimit()); + } else { + return $this->result; + } + } + + public function setResult($result) + { + $this->result = $result; + $this->count = count($result); + return $this; + } + + /** + * ArrayDatasource will apply this function to sort the array + * + * @param mixed $a Left side comparsion value + * @param mixed $b Right side comparsion value + * @param int $col_num Current position in order_columns array + * + * @return int + */ + public function compare(& $a, & $b, $col_num = 0) + { + if (! array_key_exists($col_num, $this->order_columns)) { + return 0; + } + $col = $this->order_columns[$col_num][0]; + $dir = $this->order_columns[$col_num][1]; + + //$res = strnatcmp(strtolower($a->$col), strtolower($b->$col)); + $res = strcmp(strtolower($a->$col), strtolower($b->$col)); + if ($res === 0) { + if (array_key_exists(++$col_num, $this->order_columns)) { + return $this->compare($a, $b, $col_num); + } else { + return 0; + } + } + if ($dir === self::SORT_ASC) { + return $res; + } else { + return $res * -1; + } + } +} diff --git a/library/Icinga/Data/DatasourceInterface.php b/library/Icinga/Data/DatasourceInterface.php new file mode 100644 index 000000000..7061e1696 --- /dev/null +++ b/library/Icinga/Data/DatasourceInterface.php @@ -0,0 +1,14 @@ +config = $config; + $this->connect(); + $this->init(); + } + + public function select() + { + return new Query($this); + } + + public function getDbType() + { + return $this->dbtype; + } + + public function getDb() + { + return $this->db; + } + + protected function init() + { + } + + protected function connect() + { + $this->dbtype = $this->config->get('dbtype', 'mysql'); + + $options = array( + \Zend_Db::AUTO_QUOTE_IDENTIFIERS => false, + \Zend_Db::CASE_FOLDING => \Zend_Db::CASE_LOWER + ); + + $drv_options = array( + \PDO::ATTR_TIMEOUT => 2, + // TODO: Check whether LC is useful. Zend_Db does fetchNum for Oci: + \PDO::ATTR_CASE => \PDO::CASE_LOWER + // TODO: ATTR_ERRMODE => ERRMODE_EXCEPTION vs ERRMODE_SILENT + ); + + switch ($this->dbtype) { + case 'mysql': + $adapter = 'Pdo_Mysql'; + $drv_options[\PDO::MYSQL_ATTR_INIT_COMMAND] = + "SET SESSION SQL_MODE='STRICT_ALL_TABLES,NO_ZERO_IN_DATE," + . "NO_ZERO_DATE,NO_ENGINE_SUBSTITUTION';"; + // Not using ONLY_FULL_GROUP_BY as of performance impact + // TODO: NO_ZERO_IN_DATE as been added with 5.1.11. Is it + // ignored by other versions? + $port = $this->config->get('port', 3306); + break; + case 'pgsql': + $adapter = 'Pdo_Pgsql'; + $port = $this->config->get('port', 5432); + break; + case 'oracle': + $adapter = 'Pdo_Oci'; + // $adapter = 'Oracle'; + $port = $this->config->get('port', 1521); +// $drv_options[\PDO::ATTR_STRINGIFY_FETCHES] = true; + + if ($adapter === 'Oracle') { + // Unused right now + putenv('ORACLE_SID=XE'); + putenv('ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe'); + putenv('PATH=$PATH:$ORACLE_HOME/bin'); + putenv('ORACLE_BASE=/u01/app/oracle'); + putenv('NLS_LANG=AMERICAN_AMERICA.UTF8'); + + } + + break; + default: + throw new \Exception(sprintf( + 'Backend "%s" is not supported', $type + )); + } + $attributes = array( + 'host' => $this->config->host, + 'port' => $port, + 'username' => $this->config->user, + 'password' => $this->config->pass, + 'dbname' => $this->config->db, + 'options' => $options, + 'driver_options' => $drv_options + ); + if ($this->dbtype === 'oracle') { + $attributes['persistent'] = true; + } + $this->db = \Zend_Db::factory($adapter, $attributes); + if ($adapter === 'Oracle') { + $this->db->setLobAsString(false); + } + + // TODO: Zend_Db::FETCH_ASSOC for Oracle? + $this->db->setFetchMode(\Zend_Db::FETCH_OBJ); + + } +} diff --git a/library/Icinga/Data/Db/Query.php b/library/Icinga/Data/Db/Query.php new file mode 100644 index 000000000..48221639e --- /dev/null +++ b/library/Icinga/Data/Db/Query.php @@ -0,0 +1,130 @@ +db = $this->ds->getConnection()->getDb(); + $this->baseQuery = $this->db->select(); + } + + protected function getSelectQuery() + { + if ($this->selectQuery === null) { + $this->createQueryObjects(); + } + + if ($this->hasLimit()) { + $this->selectQuery->limit($this->getLimit(), $this->getOffset()); + } + return $this->selectQuery; + } + + protected function getCountQuery() + { + if ($this->countQuery === null) { + $this->createQueryObjects(); + } + return $this->countQuery; + } + + protected function createQueryObjects() + { + $this->beforeCreatingCountQuery(); + $this->countQuery = clone($this->baseQuery); + if ($this->countColumns === null) { + $this->countColumns = array('cnt' => 'COUNT(*)'); + } + $this->countQuery->columns($this->countColumns); + + $this->beforeCreatingSelectQuery(); + $this->selectQuery = clone($this->baseQuery); + $this->selectQuery->columns($this->columns); + if ($this->hasOrder()) { + foreach ($this->order_columns as $col) { + $this->selectQuery->order( + $col[0] + . ' ' + . ( $col[1] === self::SORT_DESC ? 'DESC' : 'ASC') + ); + } + } + } + + protected function beforeCreatingCountQuery() + { + } + + protected function beforeCreatingSelectQuery() + { + } + + public function count() + { + return $this->db->fetchOne($this->getCountQuery()); + } + + public function fetchAll() + { + return $this->db->fetchAll($this->getSelectQuery()); + } + + public function fetchRow() + { + return $this->db->fetchRow($this->getSelectQuery()); + } + + public function fetchOne() + { + return $this->db->fetchOne($this->getSelectQuery()); + } + + public function fetchPairs() + { + return $this->db->fetchPairs($this->getSelectQuery()); + } + + public function dump() + { + return "QUERY\n=====\n" + . $this->getSelectQuery() + . "\n\nCOUNT\n=====\n" + . $this->getCountQuery() + . "\n\n"; + } + + public function __toString() + { + return (string) $this->getSelectQuery(); + } +} diff --git a/library/Icinga/Data/Filter.php b/library/Icinga/Data/Filter.php new file mode 100644 index 000000000..c070c139b --- /dev/null +++ b/library/Icinga/Data/Filter.php @@ -0,0 +1,54 @@ + + * @copyright 2013 Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Filter extends ArrayIterator +{ + + public function without($keys) + { + $filter = new Filter(); + $params = $this->toParams(); + if (! is_array($keys)) { + $keys = array($keys); + } + foreach ($keys as $key) { + if (array_key_exists($key, $params)) { + unset($params[$key]); + } + } + foreach ($params as $k => $v) { + $filter[] = array($k, $v); + } + return $filter; + } + + /** + * Get filtere as key-value array + * + * @return array + */ + public function toParams() + { + $params = array(); + + foreach ($this as $filter) { + $params[$filter[0]] = $filter[1]; + } + + return $params; + } +} diff --git a/library/Icinga/Objects/Host.php b/library/Icinga/Objects/Host.php new file mode 100755 index 000000000..2907659b5 --- /dev/null +++ b/library/Icinga/Objects/Host.php @@ -0,0 +1,8 @@ +props = $this->defaults; + if (! empty($props)) { + $this->setProperties($props); + } + } + + public function setProperties($props) + { + foreach ($props as $key => $val) { + $this->props[$key] = $val; + } + } + + protected function set($key, $val) + { + $this->props[$key] = $val; + return $this; + } + + public function __set($key, $val) + { + $this->set($key, $val); + } + + public function __get($key) + { + if (array_key_exists($key, $this->props)) { + return $this->props[$key]; + } + return null; + } + + protected function setLoadedFromBackend($loaded = true) + { + $this->fromBackend = $loaded; + return $this; + } + + public static function fromBackend($row) + { + $class = get_called_class(); + $object = new $class($row); + $object->setLoadedFromBackend(); + return $object; + } +} diff --git a/library/Icinga/Objects/Service.php b/library/Icinga/Objects/Service.php new file mode 100755 index 000000000..0bf73b543 --- /dev/null +++ b/library/Icinga/Objects/Service.php @@ -0,0 +1,7 @@ +SetCreator('IcingaWeb'); + $this->SetAuthor('IcingaWeb Team'); + $this->SetTitle('IcingaWeb Sample PDF - Title'); + $this->SetSubject('IcingaWeb Sample PDF - Subject'); + $this->SetKeywords('IcingaWeb, Monitoring'); + + // set default header data + // $pdf->SetHeaderData('tcpdf_logo.jpg', 30, 'Header title', + // 'Header string', array(0,64,255), array(0,64,128)); + // $pdf->setFooterData($tc=array(0,64,0), $lc=array(0,64,128)); + + $this->setHeaderFont(array('helvetica', '', 10)); + $this->setFooterFont(array('helvetica', '', 8)); + $this->SetDefaultMonospacedFont('courier'); + + $this->SetMargins(15, 27, 15); // left, top, right + $this->SetHeaderMargin(5); + $this->SetFooterMargin(10); + + $this->SetAutoPageBreak(true, 25); // margin bottom + $this->setImageScale(1.75); + + $lang = array( + 'a_meta_charset' => 'UTF-8', + 'a_meta_dir' => 'ltr', + 'a_meta_language' => 'de', + 'w_page' => 'Seite', + ); + $this->setLanguageArray($lang); + + + $this->setFontSubsetting(true); + $this->SetFont('dejavusans', '', 16, '', true); + } +} diff --git a/library/Icinga/Protocol/Ldap/LdapUtils.php b/library/Icinga/Protocol/Ldap/LdapUtils.php index 5f28cae79..8232ebf67 100644 --- a/library/Icinga/Protocol/Ldap/LdapUtils.php +++ b/library/Icinga/Protocol/Ldap/LdapUtils.php @@ -27,6 +27,7 @@ class LdapUtils public static function explodeDN($dn, $with_type = true) { $res = ldap_explode_dn($dn, $with_type ? 0 : 1); + foreach ($res as $k => $v) { $res[$k] = preg_replace( '/\\\([0-9a-f]{2})/ei', diff --git a/library/Icinga/Protocol/Livestatus/Connection.php b/library/Icinga/Protocol/Livestatus/Connection.php new file mode 100755 index 000000000..1954ad983 --- /dev/null +++ b/library/Icinga/Protocol/Livestatus/Connection.php @@ -0,0 +1,249 @@ +available_tables); + } + + public function __construct($socket = '/var/lib/icinga/rw/live') + { + $this->assertPhpExtensionLoaded('sockets'); + if ($socket[0] === '/') { + if (! is_writable($socket)) { + throw new \Exception(sprintf( + 'Cannot write to livestatus socket "%s"', + $socket + )); + } + $this->socket_type = self::TYPE_UNIX; + $this->socket_path = $socket; + } else { + if (! preg_match('~^tcp://([^:]+):(\d+)~', $socket, $m)) { + throw new \Exception(sprintf( + 'Invalid TCP socket syntax: "%s"', + $socket + )); + } + // TODO: Better syntax checks + $this->socket_host = $m[1]; + $this->socket_port = (int) $m[2]; + $this->socket_type = self::TYPE_TCP; + } + } + + public function select() + { + $select = new Query($this); + return $select; + } + + public function count(Query $query) + { + $count = clone($query); + $count->count(); + \Icinga\Benchmark::measure('Sending Livestatus Count Query'); + $data = $this->_fetch((string) $count); + \Icinga\Benchmark::measure('Got Livestatus count result'); + return $data[0][0]; + } + + public function fetchAll(Query $query) + { + \Icinga\Benchmark::measure('Sending Livestatus Query'); + $data = $this->_fetch((string) $query); + \Icinga\Benchmark::measure('Got Livestatus Data'); + if ($query->hasColumns()) { + $headers = $query->getColumnAliases(); + } else { + $headers = array_shift($data); + } + $result = array(); + foreach ($data as $row) { + $result_row = & $result[]; + $result_row = (object) array(); + foreach ($row as $key => $val) { + $result_row->{$headers[$key]} = $val; + } + } + if ($query->hasOrder()) { + usort($result, array($query, 'compare')); + } + if ($query->hasLimit()) { + $result = array_slice( + $result, + $query->getOffset(), + $query->getLimit() + ); + } + \Icinga\Benchmark::measure('Data sorted, limits applied'); + + return $result; + } + + protected function _fetch($raw_query) + { + $conn = $this->getConnection(); + $this->writeToSocket($raw_query); + $header = $this->readFromSocket(16); + $status = (int) substr($header, 0, 3); + $length = (int) trim(substr($header, 4)); + $body = $this->readFromSocket($length); + if ($status !== 200) { + throw new \Exception(sprintf( + 'Problem while reading %d bytes from livestatus: %s', + $length, + $body + )); + } + $result = json_decode($body); + if ($result === null) { + throw new \Exception('Got invalid response body from livestatus'); + } + + return $result; + } + + protected function readFromSocket($length) + { + $offset = 0; + $buffer = ''; + + while($offset < $length) { + $data = socket_read($this->connection, $length - $offset); + if ($data === false) { + throw new \Exception(sprintf( + 'Failed to read from livestatus socket: %s', + socket_strerror(socket_last_error($this->connection)) + )); + } + $size = strlen($data); + $offset += $size; + $buffer .= $data; + + if ($size === 0) { + break; + } + } + if ($offset !== $length) { + throw new \Exception(sprintf( + 'Got only %d instead of %d bytes from livestatus socket', + $offset, $length + )); + } + + return $buffer; + } + + protected function writeToSocket($data) + { + $res = socket_write($this->connection, $data); + if ($res === false) { + throw new \Exception('Writing to livestatus socket failed'); + } + return true; + } + + protected function assertPhpExtensionLoaded($name) + { + if (! extension_loaded($name)) { + throw new \Exception(sprintf( + 'The extension "%s" is not loaded', + $name + )); + } + } + + protected function getConnection() + { + if ($this->connection === null) { + if ($this->socket_type === self::TYPE_TCP) { + $this->establishTcpConnection(); + } else { + $this->establishSocketConnection(); + } + } + return $this->connection; + } + + protected function establishTcpConnection() + { + // TODO: find a bedder place for this + if (! defined('TCP_NODELAY')) { + define('TCP_NODELAY', 1); + } + + $this->connection = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if (! @socket_connect($this->connection, $this->socket_host, $this->socket_port)) { + throw new \Exception(sprintf( + 'Cannot connect to livestatus TCP socket "%s:%d": %s', + $this->socket_host, + $this->socket_port, + socket_strerror(socket_last_error($this->connection)) + )); + } + socket_set_option($this->connection, SOL_TCP, TCP_NODELAY, 1); + } + + protected function establishSocketConnection() + { + $this->connection = socket_create(AF_UNIX, SOCK_STREAM, 0); + if (! socket_connect($this->connection, $this->socket_path)) { + throw new \Exception(sprintf( + 'Cannot connect to livestatus local socket "%s"', + $this->socket_path + )); + } + } + + public function disconnect() + { + if ($this->connection) { + socket_close($this->connection); + } + } + + public function __destruct() + { + $this->disconnect(); + } +} + diff --git a/library/Icinga/Protocol/Livestatus/Query.php b/library/Icinga/Protocol/Livestatus/Query.php new file mode 100755 index 000000000..e137d5429 --- /dev/null +++ b/library/Icinga/Protocol/Livestatus/Query.php @@ -0,0 +1,210 @@ +connection = $connection; + } + + public function getAdapter() + { + return $this->connection; + } + + public function compare(& $a, & $b, $col_num = 0) + { + if (! array_key_exists($col_num, $this->order_columns)) { + return 0; + } + $col = $this->order_columns[$col_num][0]; + $dir = $this->order_columns[$col_num][1]; + + //$res = strnatcmp(strtolower($a->$col), strtolower($b->$col)); + $res = strcmp(strtolower($a->$col), strtolower($b->$col)); + if ($res === 0) { + if (array_key_exists(++$col_num, $this->order_columns)) { + return $this->compare($a, $b, $col_num); + } else { + return 0; + } + } + if ($dir === self::SORT_ASC) { + return $res; + } else { + return $res * -1; + } + } + + public function hasOrder() + { + return ! empty($this->order_columns); + } + + public function where($key, $val = null) + { + $this->filters[$key] = $val; + return $this; + } + + public function order($col) + { + if (($pos = strpos($col, ' ')) !== false) { + $dir = strtoupper(substr($col, $pos + 1)); + if ($dir === 'DESC') { + $dir = self::SORT_DESC; + } else { + $dir = self::SORT_ASC; + } + $col = substr($col, 0, $pos); + } else { + $col = $col; + } + $this->order_columns[] = array($col, $dir); + return $this; + } + + // Nur wenn keine stats, sonst im RAM!! + // Offset gibt es nicht, muss simuliert werden + public function limit($count = null, $offset = null) + { + if (! preg_match('~^\d+~', $count . $offset)) { + throw new Exception(sprintf( + 'Got invalid limit: %s, %s', + $count, + $offset + )); + } + $this->limit_count = (int) $count; + $this->limit_offset = (int) $offset; + return $this; + } + + public function hasLimit() + { + return $this->limit_count !== null; + } + + public function hasOffset() + { + return $this->limit_offset > 0; + } + + public function getLimit() + { + return $this->limit_count; + } + + public function getOffset() + { + return $this->limit_offset; + } + + public function from($table, $columns = null) + { + if (! $this->connection->hasTable($table)) { + throw new Exception(sprintf( + 'This livestatus connection does not provide "%s"', + $table + )); + } + $this->table = $table; + if (is_array($columns)) { + // TODO: check for valid names? + $this->columns = $columns; + } + return $this; + } + + public function hasColumns() + { + return $this->columns !== null; + } + + public function getColumns() + { + return $this->columns; + } + + public function getColumnAliases() + { + $aliases = array(); + foreach ($this->getColumns() as $key => $val) { + if (is_int($key)) { + $aliases[] = $val; + } else { + $aliases[] = $key; + } + } + return $aliases; + } + + public function count() + { + $this->count = true; + return $this; + } + + public function __toString() + { + if ($this->table === null) { + throw new Exception('Table is required'); + } + $default_headers = array( + 'OutputFormat: json', + 'ResponseHeader: fixed16', + 'KeepAlive: on' + ); + $parts = array( + sprintf('GET %s', $this->table) + ); + if ($this->count === false && $this->columns !== null) { + $parts[] = 'Columns: ' . implode(' ', $this->columns); + } + foreach ($this->filters as $key => $val) { + if ($key === 'search') { + $parts[] = 'Filter: host_name ~~ ' . $val; + $parts[] = 'Filter: description ~~ ' . $val; + $parts[] = 'Or: 2'; + continue; + } + if ($val === null) { + $parts[] = 'Filter: ' . $key; + } elseif (strpos($key, '?') === false) { + $parts[] = sprintf('Filter: %s = %s', $key, $val); + } else { + $parts[] = sprintf('Filter: %s', str_replace('?', $val, $key)); + } + } + if ($this->count === true) { + $parts[] = 'Stats: state >= 0'; + } + if (! $this->count && $this->hasLimit() && ! $this->hasOrder()) { + $parts[] = 'Limit: ' . ($this->limit_count + $this->limit_offset); + } + $lql = implode("\n", $parts) + . "\n" + . implode("\n", $default_headers) + . "\n\n"; + return $lql; + } + + public function __destruct() + { + unset($this->connection); + } +} + diff --git a/library/Icinga/Protocol/Nrpe/Connection.php b/library/Icinga/Protocol/Nrpe/Connection.php new file mode 100644 index 000000000..0cda7e34d --- /dev/null +++ b/library/Icinga/Protocol/Nrpe/Connection.php @@ -0,0 +1,106 @@ +host = $host; + $this->port = $port; + } + + public function useSsl($use_ssl = true) + { + $this->use_ssl = $use_ssl; + return $this; + } + + public function sendCommand($command, $args = null) + { + if (! empty($args)) { + $command .= '!' . implode('!', $args); + } + + $packet = Packet::createQuery($command); + return $this->send($packet); + } + + public function getLastReturnCode() + { + return $this->lastReturnCode; + } + + public function send(Packet $packet) + { + $conn = $this->connection(); + $bytes = $packet->getBinary(); + fputs($conn, $bytes, strlen($bytes)); + // TODO: Check result checksum! + $result = fread($conn, 8192); + if ($result === false) { + throw new \Exception('CHECK_NRPE: Error receiving data from daemon.'); + } elseif (strlen($result) === 0) { + throw new \Exception( + 'CHECK_NRPE: Received 0 bytes from daemon.' + . ' Check the remote server logs for error messages' + ); + } + // TODO: CHECK_NRPE: Receive underflow - only %d bytes received (%d expected) + $code = unpack('n', substr($result, 8, 2)); + $this->lastReturnCode = $code[1]; + $this->disconnect(); + return rtrim(substr($result, 10, -2)); + } + + protected function connect() + { + $ctx = stream_context_create(); + if ($this->use_ssl) { + // TODO: fail if not ok: + $res = stream_context_set_option($ctx, 'ssl', 'ciphers', 'ADH'); + $uri = sprintf('ssl://%s:%d', $this->host, $this->port); + } else { + $uri = sprintf('tcp://%s:%d', $this->host, $this->port); + } + $this->connection = stream_socket_client( + $uri, + $errno, + $errstr, + 60, + STREAM_CLIENT_CONNECT, + $ctx + ); + if (! $this->connection) { + throw new \Exception(sprintf('NRPE Connection failed: ' . $errstr)); + } + } + + protected function connection() + { + if ($this->connection === null) { + $this->connect(); + } + return $this->connection; + } + + protected function disconnect() + { + if ($this->connection !== null) { + fclose($this->connection); + $this->connection = null; + } + return $this; + } + + public function __destruct() + { + $this->disconnect(); + } +} diff --git a/library/Icinga/Protocol/Nrpe/Packet.php b/library/Icinga/Protocol/Nrpe/Packet.php new file mode 100644 index 000000000..65cde2f04 --- /dev/null +++ b/library/Icinga/Protocol/Nrpe/Packet.php @@ -0,0 +1,68 @@ +type = $type; + $this->body = $body; + $this->regenerateRandomBytes(); + } + + // TODO: renew "from time to time" to allow long-running daemons + protected function regenerateRandomBytes() + { + self::$randomBytes = ''; + for ($i = 0; $i < 4096; $i++) { + self::$randomBytes .= pack('N', mt_rand()); + } + } + + public static function createQuery($body) + { + $packet = new Packet(self::QUERY, $body); + return $packet; + } + + protected function getFillString($length) + { + $max = strlen(self::$randomBytes) - $length; + return substr(self::$randomBytes, rand(0, $max), $length); + } + + // TODO: WTF is SR? And 2324? + public function getBinary() + { + $version = pack('n', $this->version); + $type = pack('n', $this->type); + $dummycrc = "\x00\x00\x00\x00"; + $result = "\x00\x00"; + $result = pack('n', 2324); + $body = $this->body + . "\x00" + . $this->getFillString(1023 - strlen($this->body)) + . 'SR'; + + $crc = pack( + 'N', + crc32($version . $type . $dummycrc . $result . $body) + ); + $bytes = $version . $type . $crc . $result . $body; + return $bytes; + } + + public function __toString() + { + return $this->body; + } +} diff --git a/library/Icinga/Util/Format.php b/library/Icinga/Util/Format.php new file mode 100644 index 000000000..27662bb61 --- /dev/null +++ b/library/Icinga/Util/Format.php @@ -0,0 +1,71 @@ + 0 && $result < 2) { + $pow--; + $result = $value / pow($base, $pow); + } + return sprintf( + '%s%0.2f %s', + $sign, + $result, + $units[$pow] + ); + } +} diff --git a/library/Icinga/Web/Cookie.php b/library/Icinga/Web/Cookie.php new file mode 100644 index 000000000..7969197f6 --- /dev/null +++ b/library/Icinga/Web/Cookie.php @@ -0,0 +1,8 @@ +_prefill = $options['prefill']; + unset($options['prefill']); + } + */ + $this->request = Front::getInstance()->getRequest(); + // $this->handleRequest(); + foreach ($this->elements() as $key => $values) { + $this->addElement($values[0], $key, $values[1]); // do it better! + } + + // Should be replaced with button check: + $this->addElement('hidden', '__submitted'); + $this->setDefaults(array('__submitted' => 'true')); + + parent::__construct($options); + if ($this->getAttrib('action') === null) { + $this->setAction($this->request->getRequestUri()); + } + if ($this->getAttrib('method') === null) { + $this->setMethod('post'); + } + if ($this->hasBeenSubmitted()) { + $this->handleRequest(); + } + } + + public function redirectNow($url) + { + ZfActionHelper::getStaticHelper('redirector') + ->gotoUrlAndExit($url); + } + + public function handleRequest() + { + if ($this->isValid($this->request->getPost())) { + $this->onSuccess(); + } else { + $this->onFailure(); + } + } + + public function onSuccess() + { + } + + public function onFailure() + { + } + + public function hasBeenSubmitted() + { + return $this->request->getPost('__submitted', 'false') === 'true'; + } + + public function elements() + { + return array(); + } +} diff --git a/library/Icinga/Web/Hook/Grapher.php b/library/Icinga/Web/Hook/Grapher.php new file mode 100644 index 000000000..6161f5d80 --- /dev/null +++ b/library/Icinga/Web/Hook/Grapher.php @@ -0,0 +1,90 @@ + + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Grapher +{ + /** + * Whether this grapher provides preview images + * + * @var bool + */ + protected $hasPreviews = false; + + /** + * Constructor must live without arguments right now + * + * Therefore the constructor is final, we might change our opinion about + * this one far day + */ + final public function __construct() + { + $this->init(); + } + + /** + * Whether this grapher provides preview images + * + * @return bool + */ + public function hasPreviews() + { + return $this->hasPreviews; + } + + /** + * Overwrite this function if you want to do some initialization stuff + * + * @return void + */ + protected function init() + { + } + + /** + * Whether a graph for the given host[, service [, plot]] exists + * + * @return bool + */ + public function hasGraph($host, $service = null, $plot = null) + { + return false; + } + + /** + * Get a preview image for the given host[, service [, plot]] exists + * + * WARNING: We are not sure yet whether this will remain as is + * + * @return string + */ + public function getPreviewImage($host, $service = null, $plot = null) + { + throw new Exception('This backend has no preview images'); + } + + /** + * Get URL pointing to the grapher + * + * WARNING: We are not sure yet whether this will remain as is + * + * @return string + */ + public function getGraphUrl($host, $service = null, $plot = null) + { + throw new Exception('This backend has no images'); + } +} diff --git a/library/Icinga/Web/Hook/Toptray.php b/library/Icinga/Web/Hook/Toptray.php new file mode 100755 index 000000000..1685049b4 --- /dev/null +++ b/library/Icinga/Web/Hook/Toptray.php @@ -0,0 +1,30 @@ +align = $align; + } + + final public function getWidgetDOM() + { + try { + return ''; + } catch (\Exception $e) { + Logger::error("Could not create tray widget : %s",$e->getMessage()); + return ''; + } + + + } + + abstract protected function buildDOM(); +} diff --git a/library/Icinga/Web/ModuleActionController.php b/library/Icinga/Web/ModuleActionController.php new file mode 100644 index 000000000..3c6b47e4a --- /dev/null +++ b/library/Icinga/Web/ModuleActionController.php @@ -0,0 +1,87 @@ + + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class ModuleActionController extends ActionController +{ + protected $module; + protected $module_dir; + + /** + * Gives you this modules base directory + * + * @return string + */ + public function getModuleDir() + { + if ($this->module_dir === null) { + $this->module_dir = $this->getModule()->getBaseDir(); + } + return $this->module_dir; + } + + public function getModule() + { + if ($this->module === null) { + $this->module = Icinga::app()->getModule( + $this->module_name + ); + } + return $this->module; + } + + /** + * Translates the given string with the modules translation catalog + * + * @param string $string The string that should be translated + * + * @return string + */ + public function translate($string) + { + return mt($this->module_name, $string); + } + + /** + * This is where the module configuration is going to be loaded + * + * @return void + */ + protected function loadConfig() + { + $this->config = Config::getInstance()->{$this->module_name}; + } + + /** + * Once dispatched we are going to place each modules output in a div + * container having the icinga-module and the icinga-$module-name classes + * + * @return void + */ + public function postDispatch() + { + parent::postDispatch(); + $this->_helper->layout()->moduleStart = + '
' + . "\n" + ; + $this->_helper->layout()->moduleEnd = "
\n"; + } +} diff --git a/library/Icinga/Web/Notification.php b/library/Icinga/Web/Notification.php new file mode 100644 index 000000000..c14c32ada --- /dev/null +++ b/library/Icinga/Web/Notification.php @@ -0,0 +1,113 @@ +addMessage($msg, 'info'); + } + + public static function success($msg) + { + self::getInstance()->addMessage($msg, 'success'); + } + + public static function warning($msg) + { + self::getInstance()->addMessage($msg, 'warning'); + } + + public static function error($msg) + { + self::getInstance()->addMessage($msg, 'error'); + } + + protected function addMessage($message, $type = 'info') + { + if (! in_array( + $type, + array( + 'info', + 'error', + 'warning', + 'success' + ) + )) { + throw new ProgrammingError( + sprintf( + '"%s" is not a valid notification type', + $type + ) + ); + } + + if ($this->is_cli) { + $msg = sprintf('[%s] %s', $type, $message); + switch ($type) { + case 'info': + case 'success': + Log::info($msg); + break; + case 'warning': + Log::warn($msg); + break; + case 'error': + Log::error($msg); + break; + } + return; + } + + $mo = (object) array( + 'type' => $type, + 'message' => $message, + ); + + // Get, change, set - just to be on the safe side: + $msgs = $this->session->messages; + $msgs[] = $mo; + $this->session->messages = $msgs; + } + + public function hasMessages() + { + return ! empty($this->session->messages); + } + + public function getMessages() + { + $msgs = $this->session->messages; + $this->session->messages = array(); + return $msgs; + } + + final private function __construct() + { + $this->session = new SessionNamespace('IcingaNotification'); + if (! is_array($this->session->messages)) { + $this->session->messages = array(); + } + + if (Platform::isCli()) { + $this->is_cli = true; + } + } + + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new Notification(); + } + return self::$instance; + } +} diff --git a/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php new file mode 100755 index 000000000..14ea80012 --- /dev/null +++ b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php @@ -0,0 +1,62 @@ +query = $query; + } + + /** + * Returns an array of items for a page. + * + * @param integer $offset Page offset + * @param integer $itemCountPerPage Number of items per page + * @return array + */ + public function getItems($offset, $itemCountPerPage) + { + return $this->query->limit($itemCountPerPage, $offset)->fetchAll(); + } + + /** + * Returns the total number of items in the query result. + * + * @return integer + */ + public function count() + { + if ($this->count === null) { + $this->count = $this->query->count(); + } + return $this->count; + } +} + diff --git a/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php b/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php new file mode 100755 index 000000000..ff7b7d13c --- /dev/null +++ b/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php @@ -0,0 +1,63 @@ +getPageRange(); + } + + $pageNumber = $paginator->getCurrentPageNumber(); + $pageCount = count($paginator); + $range = array(); + + + + if ($pageCount < 15) { + for ($i = 1; $i < 15; $i++) { + if ($i > $pageCount) break; + $range[$i] = $i; + } + } else { + foreach (array(1, 2) as $i) { + $range[$i] = $i; + } + if ($pageNumber > 8) { + $range[] = '...'; + $start = 5; + if ($pageCount - $pageNumber < 8) { + $start = 9 - ($pageCount - $pageNumber); + } + for ($i = $pageNumber - $start; $i < $pageNumber + (10 - $start); $i++) { + if ($i > $pageCount) break; + $range[$i] = $i; + } + } else { + for ($i = 3; $i <= 10; $i++) { + $range[$i] = $i; + } + } + if ($pageNumber < ($pageCount - 7)) { + $range[] = '...'; + foreach (array($pageCount - 1, $pageCount) as $i) { + $range[$i] = $i; + } + } + } + if (empty($range)) $range[] = 1; + return $range; + } +} + + diff --git a/library/Icinga/Web/Session.php b/library/Icinga/Web/Session.php new file mode 100755 index 000000000..224866619 --- /dev/null +++ b/library/Icinga/Web/Session.php @@ -0,0 +1,156 @@ + + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Session +{ + /** + * Session is a Singleton stored in $instance + * + * @var Session + */ + protected static $instance; + + protected $defaultOptions = array( + 'use_trans_sid' => false, + 'use_cookies' => true, + 'use_only_cooies' => true, + 'cookie_httponly' => true, + 'use_only_cookies' => true, + 'hash_function' => true, + 'hash_bits_per_character' => 5, + ); + + /** + * The ZF session namespace + * + * @var \Zend_Session_Namespace + */ + protected $session; + + protected $started = false; + protected $closed = true; + + /** + * Constructor is protected to enforce singleton usage + */ + protected function __construct() + { + Zend_Session::start(); + $this->session = new Zend_Session_Namespace('Icinga'); + } +/* +// Not yet + public function start() + { + if ($this->started) { + return $this; + } + if ($this->closed) { + ini_set('session.cache_limiter', null); + ini_set('session.use_only_cookies', false); + ini_set('session.use_cookies', false); + ini_set('session.use_trans_sid', false); + } + $this->applyOptions(); + session_start(); + return $this; + } + + protected function applyOptions() + { + foreach ($this->defaultOptions as $key => $val) { + ini_set('session.' . $key => $val); + } + return $this; + } +*/ + public static function setOptions($options) + { + return Zend_Session::setOptions($options); + } + + /** + * Once authenticated we store the given user(name) to our session + * + * @param Auth\User $user The user object + * // TODO: Useless + * @return self + */ + public function setAuthenticatedUser(User $user) + { + $this->session->userInfo = (string) $user; + $this->session->realname = (string) $user; // TODO: getRealName() + return $this; + } + + /** + * Get the user object for the authenticated user + * + * // TODO: This has not been done yet. Useless? + * + * @return User $user The user object + */ + public function getUser() + { + throw new ProgrammingError('Not implemented yet'); + } + + /** + * Whether this session has an authenticated user + * + * // TODO: remove + * @return bool + */ + public function isAuthenticated() + { + return isset($this->session->username); + } + + /** + * Forget everything we know about the authenticated user + * + * // TODO: Remove + * @return self + */ + public function discardAuthentication() + { + unset($this->session->username); + unset($this->session->realname); + return $this; + } + + /** + * Get a Singleton instance + * + * TODO: This doesn't work so yet, it gives you a Zend_Session_Namespace + * instance. Facade has to be completed before we can fix this. + * + * @return Icinga\Web\Session -> not yet + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new Session(); + } + return self::$instance->session; + } +} diff --git a/library/Icinga/Web/Widget.php b/library/Icinga/Web/Widget.php new file mode 100644 index 000000000..bc160dcdb --- /dev/null +++ b/library/Icinga/Web/Widget.php @@ -0,0 +1,53 @@ + + * $tabs = Widget::create('tabs'); + * + * + * @copyright Copyright (c) 2013 Icinga-Web Team + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Widget +{ + + /** + * Create a new widget + * + * @param string $name Widget name + * @param array $options Widget constructor options + * + * @return Icinga\Web\Widget\AbstractWidget + */ + public static function create($name, $options = array()) + { + $class = 'Icinga\\Web\\Widget\\' . ucfirst($name); + + if (! class_exists($class)) { + throw new ProgrammingError( + sprintf( + 'There is no such widget: %s', + $name + ) + ); + } + + $widget = new $class($options); + return $widget; + } +} diff --git a/library/Icinga/Web/Widget/AbstractWidget.php b/library/Icinga/Web/Widget/AbstractWidget.php new file mode 100644 index 000000000..1698c3dc6 --- /dev/null +++ b/library/Icinga/Web/Widget/AbstractWidget.php @@ -0,0 +1,162 @@ + + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +abstract class AbstractWidget +{ + /** + * If you are going to access the current view with the view() function, + * it's instance is stored here for performance reasons. + * + * @var Zend_View_Abstract + */ + protected static $view; + + /** + * Fill $properties with default values for all your valid widget properties + * + * @var array + */ + protected $properties = array(); + + /** + * You MUST extend this function. This is where all your HTML voodoo happens + * + * @return string + */ + abstract public function renderAsHtml(); + + /** + * You are not allowed to override the constructor, but you can put + * initialization stuff in your init() function + * + * @return void + */ + protected function init() + { + } + + /** + * We are not allowing you to override the constructor unless someone + * presents a very good reason for doing so + * + * @param array $properties An optional properties array + */ + final public function __construct($properties = array()) + { + foreach ($properties as $key => $val) { + $this->$key = $val; + } + $this->init(); + } + + /** + * Getter for widget properties + * + * @param string $key The option you're interested in + * + * @throws ProgrammingError for unknown property name + * + * @return mixed + */ + public function __get($key) + { + if (array_key_exists($key, $this->properties)) { + return $this->properties[$key]; + } + + throw new ProgrammingError( + sprintf( + 'Trying to get invalid "%s" property for %s', + $key, + get_class($this) + ) + ); + } + + /** + * Setter for widget properties + * + * @param string $key The option you want to set + * @param string $val The new value going to be assigned to this option + * + * @throws ProgrammingError for unknown property name + * + * @return mixed + */ + public function __set($key, $val) + { + if (array_key_exists($key, $this->properties)) { + $this->properties[$key] = $val; + return; + } + + throw new ProgrammingError( + sprintf( + 'Trying to set invalid "%s" property in %s. Allowed are: %s', + $key, + get_class($this), + empty($this->properties) + ? 'none' + : implode(', ', array_keys($this->properties)) + ) + ); + } + + /** + * Access the current view + * + * Will instantiate a new one if none exists + * // TODO: App->getView + * + * @return Zend_View_Abstract + */ + protected function view() + { + if (self::$view === null) { + + $renderer = ZfActionHelper::getStaticHelper( + 'viewRenderer' + ); + + if (null === $renderer->view) { + $renderer->initView(); + } + + self::$view = $renderer->view; + } + + return self::$view; + } + + /** + * Cast this widget to a string. Will call your renderAsHtml() function + * + * @return string + */ + public function __toString() + { + return $this->renderAsHtml(); + } +} diff --git a/library/Icinga/Web/Widget/Form.php b/library/Icinga/Web/Widget/Form.php new file mode 100644 index 000000000..01802379e --- /dev/null +++ b/library/Icinga/Web/Widget/Form.php @@ -0,0 +1,43 @@ + + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Form extends AbstractWidget +{ + protected $form; + protected $properties = array( + 'name' => null + ); + + public function __call($func, $args) + { + return call_user_func_array(array($this->form, $func), $args); + } + + protected function init() + { + // Load form by name given in props? + $class = 'Icinga\\Web\\Form\\' . ucfirst($this->name) . 'Form'; + $file = ICINGA_APPDIR + . '/forms/authentication/' + . ucfirst($this->name) + . 'Form.php'; + require_once($file); + $this->form = new $class; + } + + public function renderAsHtml() + { + return (string) $this->form; + } +} diff --git a/library/Icinga/Web/Widget/Tab.php b/library/Icinga/Web/Widget/Tab.php new file mode 100644 index 000000000..a8ea47f68 --- /dev/null +++ b/library/Icinga/Web/Widget/Tab.php @@ -0,0 +1,119 @@ + + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Tab extends AbstractWidget +{ + /** + * Whether this tab is currently active + * + * @var bool + */ + protected $active = false; + + /** + * Default values for widget properties + * + * @var array + */ + protected $properties = array( + 'name' => null, + 'title' => '', + 'url' => null, + 'urlParams' => array(), + 'icon' => null, + ); + + /** + * Health check at initialization time + * + * @throws Icinga\Exception\ProgrammingError if tab name is missing + * + * @return void + */ + protected function init() + { + if ($this->name === null) { + throw new ProgrammingError( + 'Cannot create a nameless tab' + ); + } + } + + /** + * Set this tab active (default) or inactive + * + * This is usually done through the tabs container widget, therefore it + * is not a good idea to directly call this function + * + * @param bool $active Whether the tab should be active + * + * @return self + */ + public function setActive($active = true) + { + $this->active = (bool) $active; + return $this; + } + + /** + * Whether this tab is currently active + * + * @return bool + */ + public function isActive() + { + return $this->active; + } + + /** + * This is where the list item HTML is created + * + * @return string + */ + public function renderAsHtml() + { + $view = $this->view(); + $class = $this->isActive() ? ' class="active"' : ''; + $caption = $this->title; + if ($this->icon !== null) { + $caption = $view->img($this->icon, array( + 'width' => 16, + 'height' => 16 + )) . ' ' . $caption; + } + if ($this->url !== null) { + $tab = $view->qlink( + $caption, + $this->url, + $this->urlParams, + array('quote' => false) + ); + } else { + $tab = $caption; + } + return "
  • $tab
  • \n"; + } +} diff --git a/library/Icinga/Web/Widget/Tabs.php b/library/Icinga/Web/Widget/Tabs.php new file mode 100644 index 000000000..98c5534f7 --- /dev/null +++ b/library/Icinga/Web/Widget/Tabs.php @@ -0,0 +1,195 @@ + + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Tabs extends AbstractWidget +{ + /** + * This is where single tabs added to this container will be stored + * + * @var array + */ + protected $tabs = array(); + + /** + * The name of the currently activated tab + * + * @var string + */ + protected $active; + + /** + * Class name(s) going to be assigned to the <ul> element + * + * @var string + */ + protected $tab_class = 'nav-tabs'; + + /** + * Activate the tab with the given name + * + * If another tab is currently active it will be deactivated + * + * @param string $name Name of the tab going to be activated + * + * @throws ProgrammingError if given tab name doesn't exist + * + * @return self + */ + public function activate($name) + { + if ($this->has($name)) { + if ($this->active !== null) { + $this->tabs[$this->active]->setActive(false); + } + $this->get($name)->setActive(); + $this->active = $name; + return $this; + } + + throw new ProgrammingError( + sprintf( + "Cannot activate a tab that doesn't exist: %s. Available: %s", + $name, + empty($this->tabs) + ? 'none' + : implode(', ', array_keys($this->tabs)) + ) + ); + } + + public function getActiveName() + { + return $this->active; + } + + /** + * Set the CSS class name(s) for the <ul> element + * + * @param string $name CSS class name(s) + * + * @return self + */ + public function setClass($name) + { + $this->tab_class = $name; + return $this; + } + + /** + * Whether the given tab name exists + * + * @param string $name Tab name + * + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->tabs); + } + + /** + * Whether the given tab name exists + * + * @param string $name The tab you're interested in + * + * @throws ProgrammingError if given tab name doesn't exist + * + * @return Tab + */ + public function get($name) + { + if (! $this->has($name)) { + throw new ProgrammingError( + sprintf( + 'There is no such tab: %s', + $name + ) + ); + } + return $this->tabs[$name]; + } + + /** + * Add a new tab + * + * A unique tab name is required, the Tab itself can either be an array + * with tab properties or an instance of an existing Tab + * + * @param string $name The new tab name + * @param array|Tab The tab itself of it's properties + * + * @throws ProgrammingError if tab name already exists + * + * @return self + */ + public function add($name, $tab) + { + if ($this->has($name)) { + throw new ProgrammingError( + sprintf( + 'Cannot add a tab named "%s" twice"', + $name + ) + ); + } + return $this->set($name, $tab); + } + + /** + * Set a tab + * + * A unique tab name is required, will be replaced in case it already + * exists. The tab can either be an array with tab properties or an instance + * of an existing Tab + * + * @param string $name The new tab name + * @param array|Tab The tab itself of it's properties + * + * @return self + */ + public function set($name, $tab) + { + if ($tab instanceof Tab) { + $this->tabs[$name] = $tab; + } else { + $this->tabs[$name] = new Tab($tab + array('name' => $name)); + } + return $this; + } + + /** + * This is where the tabs are going to be rendered + * + * @return string + */ + public function renderAsHtml() + { + $view = $this->view(); + + if (empty($this->tabs)) { + return ''; + } + $html = '\n"; + return $html; + } +} From 0098053187f69c29798aca7ec7340316f3eee234 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 7 Jun 2013 13:20:34 +0200 Subject: [PATCH 10/10] Remove empty files from the Icinga library refs #4257 --- library/Icinga/Authentication/Backend/Db.php | 0 library/Icinga/Authentication/Backend/LegacyWeb1x.php | 0 library/Icinga/Authentication/Group.php | 0 library/Icinga/Authentication/GroupBackend.php | 0 library/Icinga/Authentication/GroupMembership.php | 0 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 library/Icinga/Authentication/Backend/Db.php delete mode 100644 library/Icinga/Authentication/Backend/LegacyWeb1x.php delete mode 100644 library/Icinga/Authentication/Group.php delete mode 100644 library/Icinga/Authentication/GroupBackend.php delete mode 100644 library/Icinga/Authentication/GroupMembership.php diff --git a/library/Icinga/Authentication/Backend/Db.php b/library/Icinga/Authentication/Backend/Db.php deleted file mode 100644 index e69de29bb..000000000 diff --git a/library/Icinga/Authentication/Backend/LegacyWeb1x.php b/library/Icinga/Authentication/Backend/LegacyWeb1x.php deleted file mode 100644 index e69de29bb..000000000 diff --git a/library/Icinga/Authentication/Group.php b/library/Icinga/Authentication/Group.php deleted file mode 100644 index e69de29bb..000000000 diff --git a/library/Icinga/Authentication/GroupBackend.php b/library/Icinga/Authentication/GroupBackend.php deleted file mode 100644 index e69de29bb..000000000 diff --git a/library/Icinga/Authentication/GroupMembership.php b/library/Icinga/Authentication/GroupMembership.php deleted file mode 100644 index e69de29bb..000000000