summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
commitf66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch)
treefbff2135e7013f196b891bbde54618eb050e4aaf
parentInitial commit. (diff)
downloadicingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz
icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/ISSUE_TEMPLATE.md24
-rw-r--r--.github/workflows/L10n-update.yml20
-rw-r--r--.gitignore6
-rw-r--r--LICENSE339
-rw-r--r--README.md61
-rw-r--r--application/clicommands/BasketCommand.php127
-rw-r--r--application/clicommands/BenchmarkCommand.php152
-rw-r--r--application/clicommands/CommandCommand.php15
-rw-r--r--application/clicommands/CommandsCommand.php14
-rw-r--r--application/clicommands/ConfigCommand.php178
-rw-r--r--application/clicommands/CoreCommand.php16
-rw-r--r--application/clicommands/DaemonCommand.php26
-rw-r--r--application/clicommands/DependencyCommand.php15
-rw-r--r--application/clicommands/EndpointCommand.php19
-rw-r--r--application/clicommands/ExportCommand.php180
-rw-r--r--application/clicommands/HealthCommand.php80
-rw-r--r--application/clicommands/HostCommand.php15
-rw-r--r--application/clicommands/HostgroupCommand.php15
-rw-r--r--application/clicommands/HostgroupsCommand.php14
-rw-r--r--application/clicommands/HostsCommand.php14
-rw-r--r--application/clicommands/HousekeepingCommand.php74
-rw-r--r--application/clicommands/ImportCommand.php62
-rw-r--r--application/clicommands/ImportsourceCommand.php168
-rw-r--r--application/clicommands/JobsCommand.php74
-rw-r--r--application/clicommands/KickstartCommand.php88
-rw-r--r--application/clicommands/MigrationCommand.php66
-rw-r--r--application/clicommands/NotificationCommand.php15
-rw-r--r--application/clicommands/ServiceCommand.php92
-rw-r--r--application/clicommands/ServicegroupCommand.php15
-rw-r--r--application/clicommands/ServicesetCommand.php14
-rw-r--r--application/clicommands/ServicesetsCommand.php15
-rw-r--r--application/clicommands/SyncruleCommand.php195
-rw-r--r--application/clicommands/TimeperiodCommand.php15
-rw-r--r--application/clicommands/UserCommand.php15
-rw-r--r--application/clicommands/UsergroupCommand.php15
-rw-r--r--application/clicommands/ZoneCommand.php15
-rw-r--r--application/controllers/ApiuserController.php9
-rw-r--r--application/controllers/ApiusersController.php9
-rw-r--r--application/controllers/BasketController.php416
-rw-r--r--application/controllers/BasketsController.php53
-rw-r--r--application/controllers/BranchController.php138
-rw-r--r--application/controllers/CommandController.php126
-rw-r--r--application/controllers/CommandsController.php20
-rw-r--r--application/controllers/CommandtemplateController.php16
-rw-r--r--application/controllers/ConfigController.php539
-rw-r--r--application/controllers/CustomvarController.php17
-rw-r--r--application/controllers/DaemonController.php64
-rw-r--r--application/controllers/DashboardController.php78
-rw-r--r--application/controllers/DataController.php406
-rw-r--r--application/controllers/DatafieldController.php40
-rw-r--r--application/controllers/DatafieldcategoryController.php46
-rw-r--r--application/controllers/DependenciesController.php15
-rw-r--r--application/controllers/DependencyController.php63
-rw-r--r--application/controllers/DependencytemplateController.php16
-rw-r--r--application/controllers/DeploymentController.php28
-rw-r--r--application/controllers/EndpointController.php9
-rw-r--r--application/controllers/EndpointsController.php9
-rw-r--r--application/controllers/HealthController.php31
-rw-r--r--application/controllers/HostController.php637
-rw-r--r--application/controllers/HostgroupController.php9
-rw-r--r--application/controllers/HostgroupsController.php9
-rw-r--r--application/controllers/HostsController.php138
-rw-r--r--application/controllers/HosttemplateController.php16
-rw-r--r--application/controllers/ImportrunController.php24
-rw-r--r--application/controllers/ImportsourceController.php375
-rw-r--r--application/controllers/ImportsourcesController.php57
-rw-r--r--application/controllers/IndexController.php79
-rw-r--r--application/controllers/InspectController.php200
-rw-r--r--application/controllers/JobController.php117
-rw-r--r--application/controllers/JobsController.php20
-rw-r--r--application/controllers/KickstartController.php30
-rw-r--r--application/controllers/NotificationController.php85
-rw-r--r--application/controllers/NotificationsController.php31
-rw-r--r--application/controllers/NotificationtemplateController.php16
-rw-r--r--application/controllers/PhperrorController.php43
-rw-r--r--application/controllers/ScheduledDowntimeController.php45
-rw-r--r--application/controllers/ScheduledDowntimesController.php47
-rw-r--r--application/controllers/SchemaController.php113
-rw-r--r--application/controllers/SelfServiceController.php435
-rw-r--r--application/controllers/ServiceController.php311
-rw-r--r--application/controllers/ServiceapplyrulesController.php39
-rw-r--r--application/controllers/ServicegroupController.php9
-rw-r--r--application/controllers/ServicegroupsController.php9
-rw-r--r--application/controllers/ServicesController.php42
-rw-r--r--application/controllers/ServicesetController.php141
-rw-r--r--application/controllers/ServicetemplateController.php16
-rw-r--r--application/controllers/SettingsController.php48
-rw-r--r--application/controllers/SuggestController.php415
-rw-r--r--application/controllers/SyncruleController.php696
-rw-r--r--application/controllers/SyncrulesController.php45
-rw-r--r--application/controllers/TemplatechoiceController.php41
-rw-r--r--application/controllers/TemplatechoicesController.php39
-rw-r--r--application/controllers/TimeperiodController.php33
-rw-r--r--application/controllers/TimeperiodsController.php9
-rw-r--r--application/controllers/TimeperiodtemplateController.php16
-rw-r--r--application/controllers/UserController.php18
-rw-r--r--application/controllers/UsergroupController.php9
-rw-r--r--application/controllers/UsergroupsController.php9
-rw-r--r--application/controllers/UsersController.php13
-rw-r--r--application/controllers/UsertemplateController.php16
-rw-r--r--application/controllers/ZoneController.php9
-rw-r--r--application/controllers/ZonesController.php9
-rw-r--r--application/forms/AddToBasketForm.php129
-rw-r--r--application/forms/ApplyMigrationsForm.php54
-rw-r--r--application/forms/BasketCreateSnapshotForm.php37
-rw-r--r--application/forms/BasketForm.php147
-rw-r--r--application/forms/BasketUploadForm.php147
-rw-r--r--application/forms/CustomvarForm.php26
-rw-r--r--application/forms/DeployConfigForm.php121
-rw-r--r--application/forms/DeployFormsBug7530.php126
-rw-r--r--application/forms/DeploymentLinkForm.php170
-rw-r--r--application/forms/DirectorDatafieldCategoryForm.php36
-rw-r--r--application/forms/DirectorDatafieldForm.php301
-rw-r--r--application/forms/DirectorDatalistEntryForm.php80
-rw-r--r--application/forms/DirectorDatalistForm.php45
-rw-r--r--application/forms/DirectorJobForm.php141
-rw-r--r--application/forms/IcingaAddServiceForm.php183
-rw-r--r--application/forms/IcingaAddServiceSetForm.php123
-rw-r--r--application/forms/IcingaApiUserForm.php25
-rw-r--r--application/forms/IcingaCloneObjectForm.php259
-rw-r--r--application/forms/IcingaCommandArgumentForm.php190
-rw-r--r--application/forms/IcingaCommandForm.php137
-rw-r--r--application/forms/IcingaDeleteObjectForm.php41
-rw-r--r--application/forms/IcingaDependencyForm.php309
-rw-r--r--application/forms/IcingaEndpointForm.php61
-rw-r--r--application/forms/IcingaForgetApiKeyForm.php34
-rw-r--r--application/forms/IcingaGenerateApiKeyForm.php42
-rw-r--r--application/forms/IcingaHostForm.php390
-rw-r--r--application/forms/IcingaHostGroupForm.php40
-rw-r--r--application/forms/IcingaHostSelfServiceForm.php156
-rw-r--r--application/forms/IcingaHostVarForm.php36
-rw-r--r--application/forms/IcingaImportObjectForm.php54
-rw-r--r--application/forms/IcingaMultiEditForm.php324
-rw-r--r--application/forms/IcingaNotificationForm.php298
-rw-r--r--application/forms/IcingaObjectFieldForm.php219
-rw-r--r--application/forms/IcingaScheduledDowntimeForm.php133
-rw-r--r--application/forms/IcingaScheduledDowntimeRangeForm.php110
-rw-r--r--application/forms/IcingaServiceDictionaryMemberForm.php54
-rw-r--r--application/forms/IcingaServiceForm.php806
-rw-r--r--application/forms/IcingaServiceGroupForm.php40
-rw-r--r--application/forms/IcingaServiceSetForm.php135
-rw-r--r--application/forms/IcingaServiceVarForm.php36
-rw-r--r--application/forms/IcingaTemplateChoiceForm.php140
-rw-r--r--application/forms/IcingaTimePeriodForm.php82
-rw-r--r--application/forms/IcingaTimePeriodRangeForm.php105
-rw-r--r--application/forms/IcingaUserForm.php214
-rw-r--r--application/forms/IcingaUserGroupForm.php47
-rw-r--r--application/forms/IcingaZoneForm.php43
-rw-r--r--application/forms/ImportCheckForm.php50
-rw-r--r--application/forms/ImportRowModifierForm.php182
-rw-r--r--application/forms/ImportRunForm.php50
-rw-r--r--application/forms/ImportSourceForm.php163
-rw-r--r--application/forms/KickstartForm.php482
-rw-r--r--application/forms/RemoveLinkForm.php59
-rw-r--r--application/forms/RestoreBasketForm.php77
-rw-r--r--application/forms/RestoreObjectForm.php92
-rw-r--r--application/forms/SelfServiceSettingsForm.php306
-rw-r--r--application/forms/SettingsForm.php238
-rw-r--r--application/forms/SyncCheckForm.php69
-rw-r--r--application/forms/SyncPropertyForm.php444
-rw-r--r--application/forms/SyncRuleForm.php112
-rw-r--r--application/forms/SyncRunForm.php67
-rw-r--r--application/locale/de_DE/LC_MESSAGES/director.mobin0 -> 170714 bytes
-rw-r--r--application/locale/de_DE/LC_MESSAGES/director.po8230
-rw-r--r--application/locale/it_IT/LC_MESSAGES/director.mobin0 -> 148601 bytes
-rw-r--r--application/locale/it_IT/LC_MESSAGES/director.po7431
-rw-r--r--application/locale/ja_JP/LC_MESSAGES/director.mobin0 -> 154051 bytes
-rw-r--r--application/locale/ja_JP/LC_MESSAGES/director.po6186
-rw-r--r--application/locale/translateMe.php12
-rw-r--r--application/views/helpers/FormDataFilter.php564
-rw-r--r--application/views/helpers/FormIplExtensibleSet.php23
-rw-r--r--application/views/helpers/FormSimpleNote.php15
-rw-r--r--application/views/helpers/FormStoredPassword.php60
-rw-r--r--application/views/helpers/RenderPlainObject.php14
-rw-r--r--application/views/scripts/phperror/dependencies.phtml9
-rw-r--r--application/views/scripts/phperror/error.phtml8
-rw-r--r--application/views/scripts/settings/index.phtml7
-rw-r--r--application/views/scripts/suggest/index.phtml3
-rw-r--r--configuration.php181
-rwxr-xr-xcontrib/docker-test.sh51
-rw-r--r--contrib/linux-agent-installer/Icinga2Agent.bash318
-rw-r--r--contrib/systemd/icinga-director.service21
-rw-r--r--contrib/windows-agent-installer/Icinga2Agent.psm13402
-rw-r--r--doc/01-Introduction.md51
-rw-r--r--doc/02-Installation.md75
-rw-r--r--doc/02-Installation.md.d/From-Source.md83
-rw-r--r--doc/03-Automation.md134
-rw-r--r--doc/04-Getting-started.md60
-rw-r--r--doc/05-Upgrading.md230
-rw-r--r--doc/10-How-it-works.md112
-rw-r--r--doc/12-Handling-custom-variables.md13
-rw-r--r--doc/14-Fields-example-interfaces-array.md31
-rw-r--r--doc/15-Service-apply-for-example.md44
-rw-r--r--doc/16-Fields-example-SNMP.md104
-rw-r--r--doc/24-Working-with-agents.md80
-rw-r--r--doc/30-Configuration-Baskets.md92
-rw-r--r--doc/60-CLI.md719
-rw-r--r--doc/70-Import-and-Sync.md88
-rw-r--r--doc/70-REST-API.md684
-rw-r--r--doc/74-Self-Service-API.md49
-rw-r--r--doc/75-Background-Daemon.md65
-rw-r--r--doc/79-Jobs.md40
-rw-r--r--doc/80-FAQ.md75
-rw-r--r--doc/82-Changelog.md1202
-rw-r--r--doc/91-Want-more.md17
-rw-r--r--doc/93-Testing.md304
-rw-r--r--doc/screenshot/director/08_import-and-sync/081_director_import_source.pngbin0 -> 77206 bytes
-rw-r--r--doc/screenshot/director/08_import-and-sync/082_director_import_modifier_lowercase.pngbin0 -> 27649 bytes
-rw-r--r--doc/screenshot/director/08_import-and-sync/083_director_import_modifier_sid.pngbin0 -> 31388 bytes
-rw-r--r--doc/screenshot/director/08_import-and-sync/084_director_import_modifier_regex.pngbin0 -> 40729 bytes
-rw-r--r--doc/screenshot/director/08_import-and-sync/085_director_import_preview.pngbin0 -> 32010 bytes
-rw-r--r--doc/screenshot/director/08_import-and-sync/086_director_sync_rule_ad_hosts.pngbin0 -> 17516 bytes
-rw-r--r--doc/screenshot/director/08_import-and-sync/087_director_sync_properties_ad_host.pngbin0 -> 49400 bytes
-rw-r--r--doc/screenshot/director/14_fields-for-interfaces/141_define_datafields.pngbin0 -> 36931 bytes
-rw-r--r--doc/screenshot/director/14_fields-for-interfaces/142_add_datafield.pngbin0 -> 22196 bytes
-rw-r--r--doc/screenshot/director/14_fields-for-interfaces/143_add_host_template.pngbin0 -> 41883 bytes
-rw-r--r--doc/screenshot/director/14_fields-for-interfaces/144_add_template_field.pngbin0 -> 38304 bytes
-rw-r--r--doc/screenshot/director/14_fields-for-interfaces/145_create_host.pngbin0 -> 45254 bytes
-rw-r--r--doc/screenshot/director/14_fields-for-interfaces/146_config_preview.pngbin0 -> 28049 bytes
-rw-r--r--doc/screenshot/director/15_apply-for-services/151_monitored_services.pngbin0 -> 58184 bytes
-rw-r--r--doc/screenshot/director/15_apply-for-services/152_add_service_template.pngbin0 -> 33134 bytes
-rw-r--r--doc/screenshot/director/15_apply-for-services/153_add_service_template_field.pngbin0 -> 53038 bytes
-rw-r--r--doc/screenshot/director/15_apply-for-services/154_create_apply_rule.pngbin0 -> 13927 bytes
-rw-r--r--doc/screenshot/director/15_apply-for-services/155_configure_apply_for.pngbin0 -> 35789 bytes
-rw-r--r--doc/screenshot/director/15_apply-for-services/156_config_preview.pngbin0 -> 34882 bytes
-rw-r--r--doc/screenshot/director/16_fields_snmp/161_snmp_versions_create_list.pngbin0 -> 27497 bytes
-rw-r--r--doc/screenshot/director/16_fields_snmp/162_snmp_versions_fill_list.pngbin0 -> 21504 bytes
-rw-r--r--doc/screenshot/director/16_fields_snmp/163_snmp_version_create_field.pngbin0 -> 44404 bytes
-rw-r--r--doc/screenshot/director/16_fields_snmp/164_snmp_fields_on_template.pngbin0 -> 88704 bytes
-rw-r--r--doc/screenshot/director/16_fields_snmp/165_host_snmp_choose.pngbin0 -> 50798 bytes
-rw-r--r--doc/screenshot/director/16_fields_snmp/166_host_snmp_v2c.pngbin0 -> 29789 bytes
-rw-r--r--doc/screenshot/director/16_fields_snmp/167_host_snmp_v3.pngbin0 -> 31781 bytes
-rw-r--r--doc/screenshot/director/16_fields_snmp/168_assign_snmp_check.pngbin0 -> 52955 bytes
-rw-r--r--doc/screenshot/director/24-agents/2401_agent_template.pngbin0 -> 46752 bytes
-rw-r--r--doc/screenshot/director/24-agents/2402_create_agent_based_host.pngbin0 -> 41833 bytes
-rw-r--r--doc/screenshot/director/24-agents/2403_show_agent_instructions_1.pngbin0 -> 85405 bytes
-rw-r--r--doc/screenshot/director/24-agents/2404_show_agent_instructions_2.pngbin0 -> 59309 bytes
-rw-r--r--doc/screenshot/director/24-agents/2405_agent_preview.pngbin0 -> 37837 bytes
-rw-r--r--doc/screenshot/director/24-agents/2406_agent_based_service.pngbin0 -> 41596 bytes
-rw-r--r--doc/screenshot/director/24-agents/2407_create_agent_based_load_check.pngbin0 -> 47195 bytes
-rw-r--r--doc/screenshot/director/24-agents/2409_agent_based_service_rendered_for_host.pngbin0 -> 22576 bytes
-rw-r--r--doc/screenshot/director/24-agents/2410_agent_based_service_rendered_for_host_template.pngbin0 -> 22440 bytes
-rw-r--r--doc/screenshot/director/24-agents/2411_assign_agent_based_service.pngbin0 -> 48232 bytes
-rw-r--r--doc/screenshot/director/74_self-service-api/7401-director_self-service-dashboard.pngbin0 -> 55190 bytes
-rw-r--r--doc/screenshot/director/74_self-service-api/7402-director_self-service-choose-source.pngbin0 -> 39570 bytes
-rw-r--r--doc/screenshot/director/74_self-service-api/7403-director_self-service-settings.pngbin0 -> 63224 bytes
-rw-r--r--doc/screenshot/director/93_testing/931_director_testing_duration.pngbin0 -> 58659 bytes
-rw-r--r--doc/screenshot/director/93_testing/932_director_testing_output_testdox.pngbin0 -> 148209 bytes
-rw-r--r--doc/screenshot/director/93_testing/933_director_testing_history.pngbin0 -> 102245 bytes
-rw-r--r--doc/screenshot/director/readme/director_main_screen.pngbin0 -> 290202 bytes
-rw-r--r--library/Director/Acl.php90
-rw-r--r--library/Director/Application/Dependency.php113
-rw-r--r--library/Director/Application/DependencyChecker.php73
-rw-r--r--library/Director/Application/MemoryLimit.php53
-rw-r--r--library/Director/CheckPlugin/Check.php59
-rw-r--r--library/Director/CheckPlugin/CheckResult.php31
-rw-r--r--library/Director/CheckPlugin/CheckResults.php150
-rw-r--r--library/Director/CheckPlugin/PluginState.php114
-rw-r--r--library/Director/CheckPlugin/Range.php101
-rw-r--r--library/Director/CheckPlugin/Threshold.php47
-rw-r--r--library/Director/Cli/Command.php115
-rw-r--r--library/Director/Cli/ObjectCommand.php517
-rw-r--r--library/Director/Cli/ObjectsCommand.php115
-rw-r--r--library/Director/Cli/PluginOutputBeautifier.php75
-rw-r--r--library/Director/ConfigDiff.php47
-rw-r--r--library/Director/Core/CoreApi.php940
-rw-r--r--library/Director/Core/DeploymentApiInterface.php75
-rw-r--r--library/Director/Core/Json.php34
-rw-r--r--library/Director/Core/LegacyDeploymentApi.php466
-rw-r--r--library/Director/Core/RestApiClient.php276
-rw-r--r--library/Director/Core/RestApiResponse.php149
-rw-r--r--library/Director/CoreBeta/ApiStream.php57
-rw-r--r--library/Director/CoreBeta/Stream.php17
-rw-r--r--library/Director/CoreBeta/StreamContext.php89
-rw-r--r--library/Director/CoreBeta/StreamContextSslOptions.php52
-rw-r--r--library/Director/CustomVariable/CustomVariable.php286
-rw-r--r--library/Director/CustomVariable/CustomVariableArray.php100
-rw-r--r--library/Director/CustomVariable/CustomVariableBoolean.php53
-rw-r--r--library/Director/CustomVariable/CustomVariableDictionary.php130
-rw-r--r--library/Director/CustomVariable/CustomVariableNull.php52
-rw-r--r--library/Director/CustomVariable/CustomVariableNumber.php73
-rw-r--r--library/Director/CustomVariable/CustomVariableString.php59
-rw-r--r--library/Director/CustomVariable/CustomVariables.php488
-rw-r--r--library/Director/Daemon/BackgroundDaemon.php235
-rw-r--r--library/Director/Daemon/DaemonDb.php365
-rw-r--r--library/Director/Daemon/DaemonProcessDetails.php122
-rw-r--r--library/Director/Daemon/DaemonProcessState.php85
-rw-r--r--library/Director/Daemon/DaemonUtil.php16
-rw-r--r--library/Director/Daemon/DbBasedComponent.php19
-rw-r--r--library/Director/Daemon/DeploymentChecker.php51
-rw-r--r--library/Director/Daemon/JobRunner.php234
-rw-r--r--library/Director/Daemon/JsonRpcLogWriter.php37
-rw-r--r--library/Director/Daemon/LogProxy.php76
-rw-r--r--library/Director/Daemon/Logger.php24
-rw-r--r--library/Director/Daemon/ProcessList.php125
-rw-r--r--library/Director/Daemon/RunningDaemonInfo.php154
-rw-r--r--library/Director/Daemon/SystemdLogWriter.php27
-rw-r--r--library/Director/Dashboard/AlertsDashboard.php19
-rw-r--r--library/Director/Dashboard/AutomationDashboard.php17
-rw-r--r--library/Director/Dashboard/BranchesDashboard.php36
-rw-r--r--library/Director/Dashboard/CommandsDashboard.php35
-rw-r--r--library/Director/Dashboard/Dashboard.php305
-rw-r--r--library/Director/Dashboard/Dashlet/ActivityLogDashlet.php35
-rw-r--r--library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php25
-rw-r--r--library/Director/Dashboard/Dashlet/BasketDashlet.php30
-rw-r--r--library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/ChoicesDashlet.php41
-rw-r--r--library/Director/Dashboard/Dashlet/CommandObjectDashlet.php25
-rw-r--r--library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php26
-rw-r--r--library/Director/Dashboard/Dashlet/CustomvarDashlet.php30
-rw-r--r--library/Director/Dashboard/Dashlet/Dashlet.php239
-rw-r--r--library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php30
-rw-r--r--library/Director/Dashboard/Dashlet/DatafieldDashlet.php30
-rw-r--r--library/Director/Dashboard/Dashlet/DatalistDashlet.php30
-rw-r--r--library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php26
-rw-r--r--library/Director/Dashboard/Dashlet/DeploymentDashlet.php114
-rw-r--r--library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php63
-rw-r--r--library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php26
-rw-r--r--library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php21
-rw-r--r--library/Director/Dashboard/Dashlet/HostChoicesDashlet.php7
-rw-r--r--library/Director/Dashboard/Dashlet/HostGroupsDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/HostObjectDashlet.php25
-rw-r--r--library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/HostsDashlet.php32
-rw-r--r--library/Director/Dashboard/Dashlet/ImportSourceDashlet.php65
-rw-r--r--library/Director/Dashboard/Dashlet/InfrastructureDashlet.php30
-rw-r--r--library/Director/Dashboard/Dashlet/JobDashlet.php65
-rw-r--r--library/Director/Dashboard/Dashlet/KickstartDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php37
-rw-r--r--library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php21
-rw-r--r--library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/NotificationsDashlet.php33
-rw-r--r--library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php25
-rw-r--r--library/Director/Dashboard/Dashlet/SelfServiceDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/ServiceChoicesDashlet.php7
-rw-r--r--library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php34
-rw-r--r--library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/SettingsDashlet.php30
-rw-r--r--library/Director/Dashboard/Dashlet/SingleServicesDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/SyncDashlet.php65
-rw-r--r--library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php28
-rw-r--r--library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php25
-rw-r--r--library/Director/Dashboard/Dashlet/UserGroupsDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/UserObjectDashlet.php23
-rw-r--r--library/Director/Dashboard/Dashlet/UserTemplateDashlet.php31
-rw-r--r--library/Director/Dashboard/Dashlet/UsersDashlet.php25
-rw-r--r--library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php25
-rw-r--r--library/Director/Dashboard/DataDashboard.php18
-rw-r--r--library/Director/Dashboard/DeploymentDashboard.php17
-rw-r--r--library/Director/Dashboard/DirectorDashboard.php17
-rw-r--r--library/Director/Dashboard/HostsDashboard.php35
-rw-r--r--library/Director/Dashboard/InfrastructureDashboard.php60
-rw-r--r--library/Director/Dashboard/NotificationsDashboard.php44
-rw-r--r--library/Director/Dashboard/ObjectsDashboard.php17
-rw-r--r--library/Director/Dashboard/ServicesDashboard.php39
-rw-r--r--library/Director/Dashboard/TimeperiodsDashboard.php33
-rw-r--r--library/Director/Dashboard/UsersDashboard.php36
-rw-r--r--library/Director/Data/AssignFilterHelper.php160
-rw-r--r--library/Director/Data/DataArrayHelper.php48
-rw-r--r--library/Director/Data/Db/DbConnection.php51
-rw-r--r--library/Director/Data/Db/DbDataFormatter.php26
-rw-r--r--library/Director/Data/Db/DbObject.php1487
-rw-r--r--library/Director/Data/Db/DbObjectStore.php169
-rw-r--r--library/Director/Data/Db/DbObjectTypeRegistry.php75
-rw-r--r--library/Director/Data/Db/DbObjectWithSettings.php168
-rw-r--r--library/Director/Data/Db/IcingaObjectFilterRenderer.php133
-rw-r--r--library/Director/Data/Db/IcingaObjectQuery.php255
-rw-r--r--library/Director/Data/Db/ServiceSetQueryBuilder.php158
-rw-r--r--library/Director/Data/Exporter.php303
-rw-r--r--library/Director/Data/FieldReferenceLoader.php51
-rw-r--r--library/Director/Data/HostServiceLoader.php170
-rw-r--r--library/Director/Data/ImportExportDeniedProperties.php52
-rw-r--r--library/Director/Data/InvalidDataException.php26
-rw-r--r--library/Director/Data/Json.php69
-rw-r--r--library/Director/Data/PropertiesFilter.php25
-rw-r--r--library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php20
-rw-r--r--library/Director/Data/PropertiesFilter/CustomVariablesFilter.php13
-rw-r--r--library/Director/Data/PropertyMangler.php60
-rw-r--r--library/Director/Data/RecursiveUtf8Validator.php59
-rw-r--r--library/Director/Data/Serializable.php10
-rw-r--r--library/Director/Data/SerializableValue.php90
-rw-r--r--library/Director/Data/ValueFilter.php10
-rw-r--r--library/Director/Data/ValueFilter/FilterBoolean.php19
-rw-r--r--library/Director/Data/ValueFilter/FilterInt.php21
-rw-r--r--library/Director/DataType/DataTypeArray.php14
-rw-r--r--library/Director/DataType/DataTypeBoolean.php30
-rw-r--r--library/Director/DataType/DataTypeDatalist.php159
-rw-r--r--library/Director/DataType/DataTypeDictionary.php107
-rw-r--r--library/Director/DataType/DataTypeDirectorObject.php87
-rw-r--r--library/Director/DataType/DataTypeNumber.php19
-rw-r--r--library/Director/DataType/DataTypeSqlQuery.php96
-rw-r--r--library/Director/DataType/DataTypeString.php35
-rw-r--r--library/Director/DataType/DataTypeTime.php16
-rw-r--r--library/Director/Db.php755
-rw-r--r--library/Director/Db/AppliedServiceSetLoader.php58
-rw-r--r--library/Director/Db/Branch/Branch.php216
-rw-r--r--library/Director/Db/Branch/BranchActivity.php390
-rw-r--r--library/Director/Db/Branch/BranchMerger.php157
-rw-r--r--library/Director/Db/Branch/BranchModificationInspection.php93
-rw-r--r--library/Director/Db/Branch/BranchSettings.php121
-rw-r--r--library/Director/Db/Branch/BranchStore.php240
-rw-r--r--library/Director/Db/Branch/BranchSupport.php91
-rw-r--r--library/Director/Db/Branch/BranchedObject.php404
-rw-r--r--library/Director/Db/Branch/MergeError.php37
-rw-r--r--library/Director/Db/Branch/MergeErrorDeleteMissingObject.php15
-rw-r--r--library/Director/Db/Branch/MergeErrorModificationForMissingObject.php15
-rw-r--r--library/Director/Db/Branch/MergeErrorRecreateOnMerge.php15
-rw-r--r--library/Director/Db/Branch/PlainObjectPropertyDiff.php50
-rw-r--r--library/Director/Db/Branch/UuidLookup.php141
-rw-r--r--library/Director/Db/Cache/CustomVariableCache.php84
-rw-r--r--library/Director/Db/Cache/GroupMembershipCache.php104
-rw-r--r--library/Director/Db/Cache/PrefetchCache.php166
-rw-r--r--library/Director/Db/DbSelectParenthesis.php24
-rw-r--r--library/Director/Db/DbUtil.php96
-rw-r--r--library/Director/Db/HostMembershipHousekeeping.php8
-rw-r--r--library/Director/Db/Housekeeping.php249
-rw-r--r--library/Director/Db/IcingaObjectFilterHelper.php133
-rw-r--r--library/Director/Db/MembershipHousekeeping.php135
-rw-r--r--library/Director/Db/Migration.php70
-rw-r--r--library/Director/Db/Migrations.php239
-rw-r--r--library/Director/Deployment/ConditionalConfigRenderer.php64
-rw-r--r--library/Director/Deployment/ConditionalDeployment.php190
-rw-r--r--library/Director/Deployment/DeploymentGracePeriod.php61
-rw-r--r--library/Director/Deployment/DeploymentInfo.php59
-rw-r--r--library/Director/Deployment/DeploymentStatus.php164
-rw-r--r--library/Director/DirectorObject/Automation/Basket.php232
-rw-r--r--library/Director/DirectorObject/Automation/BasketContent.php24
-rw-r--r--library/Director/DirectorObject/Automation/BasketSnapshot.php531
-rw-r--r--library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php226
-rw-r--r--library/Director/DirectorObject/Automation/CompareBasketObject.php146
-rw-r--r--library/Director/DirectorObject/Automation/ExportInterface.php20
-rw-r--r--library/Director/DirectorObject/Automation/ImportExport.php149
-rw-r--r--library/Director/DirectorObject/Lookup/AppliedServiceInfo.php109
-rw-r--r--library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php127
-rw-r--r--library/Director/DirectorObject/Lookup/InheritedServiceInfo.php94
-rw-r--r--library/Director/DirectorObject/Lookup/ServiceFinder.php79
-rw-r--r--library/Director/DirectorObject/Lookup/ServiceInfo.php46
-rw-r--r--library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php121
-rw-r--r--library/Director/DirectorObject/Lookup/SingleServiceInfo.php83
-rw-r--r--library/Director/DirectorObject/ObjectPurgeHelper.php149
-rw-r--r--library/Director/Exception/DuplicateKeyException.php9
-rw-r--r--library/Director/Exception/JsonEncodeException.php7
-rw-r--r--library/Director/Exception/JsonException.php55
-rw-r--r--library/Director/Exception/NestingError.php9
-rw-r--r--library/Director/Field/FieldSpec.php206
-rw-r--r--library/Director/Health.php285
-rw-r--r--library/Director/Hook/BranchSupportHook.php39
-rw-r--r--library/Director/Hook/DataTypeHook.php59
-rw-r--r--library/Director/Hook/DeploymentHook.php65
-rw-r--r--library/Director/Hook/HostFieldHook.php19
-rw-r--r--library/Director/Hook/IcingaObjectFormHook.php22
-rw-r--r--library/Director/Hook/ImportSourceHook.php136
-rw-r--r--library/Director/Hook/JobHook.php86
-rw-r--r--library/Director/Hook/PropertyModifierHook.php258
-rw-r--r--library/Director/Hook/ServiceFieldHook.php19
-rw-r--r--library/Director/Hook/ShipConfigFilesHook.php11
-rw-r--r--library/Director/IcingaConfig/AgentWizard.php337
-rw-r--r--library/Director/IcingaConfig/AssignRenderer.php268
-rw-r--r--library/Director/IcingaConfig/ExtensibleSet.php574
-rw-r--r--library/Director/IcingaConfig/IcingaConfig.php781
-rw-r--r--library/Director/IcingaConfig/IcingaConfigFile.php168
-rw-r--r--library/Director/IcingaConfig/IcingaConfigHelper.php430
-rw-r--r--library/Director/IcingaConfig/IcingaConfigRendered.php34
-rw-r--r--library/Director/IcingaConfig/IcingaConfigRenderer.php10
-rw-r--r--library/Director/IcingaConfig/IcingaLegacyConfigHelper.php110
-rw-r--r--library/Director/IcingaConfig/StateFilterSet.php31
-rw-r--r--library/Director/IcingaConfig/TypeFilterSet.php39
-rw-r--r--library/Director/Import/Import.php481
-rw-r--r--library/Director/Import/ImportSourceCoreApi.php92
-rw-r--r--library/Director/Import/ImportSourceDirectorObject.php120
-rw-r--r--library/Director/Import/ImportSourceLdap.php90
-rw-r--r--library/Director/Import/ImportSourceRestApi.php380
-rw-r--r--library/Director/Import/ImportSourceSql.php70
-rw-r--r--library/Director/Import/PurgeStrategy/ImportRunBasedPurgeStrategy.php90
-rw-r--r--library/Director/Import/PurgeStrategy/PurgeNothingPurgeStrategy.php11
-rw-r--r--library/Director/Import/PurgeStrategy/PurgeStrategy.php31
-rw-r--r--library/Director/Import/Sync.php942
-rw-r--r--library/Director/Import/SyncUtils.php153
-rw-r--r--library/Director/Job/ConfigJob.php75
-rw-r--r--library/Director/Job/HousekeepingJob.php39
-rw-r--r--library/Director/Job/ImportJob.php122
-rw-r--r--library/Director/Job/SyncJob.php128
-rw-r--r--library/Director/KickstartHelper.php555
-rw-r--r--library/Director/Monitoring.php149
-rw-r--r--library/Director/Objects/DirectorActivityLog.php232
-rw-r--r--library/Director/Objects/DirectorDatafield.php344
-rw-r--r--library/Director/Objects/DirectorDatafieldCategory.php64
-rw-r--r--library/Director/Objects/DirectorDatalist.php225
-rw-r--r--library/Director/Objects/DirectorDatalistEntry.php112
-rw-r--r--library/Director/Objects/DirectorDeploymentLog.php199
-rw-r--r--library/Director/Objects/DirectorJob.php314
-rw-r--r--library/Director/Objects/DynamicApplyMatches.php14
-rw-r--r--library/Director/Objects/Extension/Arguments.php61
-rw-r--r--library/Director/Objects/Extension/FlappingSupport.php54
-rw-r--r--library/Director/Objects/Extension/PriorityColumn.php40
-rw-r--r--library/Director/Objects/GroupMembershipResolver.php689
-rw-r--r--library/Director/Objects/HostApplyMatches.php8
-rw-r--r--library/Director/Objects/HostGroupMembershipResolver.php8
-rw-r--r--library/Director/Objects/IcingaApiUser.php31
-rw-r--r--library/Director/Objects/IcingaArguments.php442
-rw-r--r--library/Director/Objects/IcingaCommand.php365
-rw-r--r--library/Director/Objects/IcingaCommandArgument.php263
-rw-r--r--library/Director/Objects/IcingaCommandField.php17
-rw-r--r--library/Director/Objects/IcingaDependency.php631
-rw-r--r--library/Director/Objects/IcingaEndpoint.php157
-rw-r--r--library/Director/Objects/IcingaFlatVar.php61
-rw-r--r--library/Director/Objects/IcingaHost.php668
-rw-r--r--library/Director/Objects/IcingaHostField.php17
-rw-r--r--library/Director/Objects/IcingaHostGroup.php42
-rw-r--r--library/Director/Objects/IcingaHostGroupAssignment.php20
-rw-r--r--library/Director/Objects/IcingaHostVar.php29
-rw-r--r--library/Director/Objects/IcingaNotification.php254
-rw-r--r--library/Director/Objects/IcingaNotificationField.php17
-rw-r--r--library/Director/Objects/IcingaObject.php3258
-rw-r--r--library/Director/Objects/IcingaObjectField.php26
-rw-r--r--library/Director/Objects/IcingaObjectGroup.php76
-rw-r--r--library/Director/Objects/IcingaObjectGroups.php408
-rw-r--r--library/Director/Objects/IcingaObjectImports.php439
-rw-r--r--library/Director/Objects/IcingaObjectLegacyAssignments.php79
-rw-r--r--library/Director/Objects/IcingaObjectMultiRelations.php454
-rw-r--r--library/Director/Objects/IcingaRanges.php321
-rw-r--r--library/Director/Objects/IcingaRelatedObject.php211
-rw-r--r--library/Director/Objects/IcingaScheduledDowntime.php135
-rw-r--r--library/Director/Objects/IcingaScheduledDowntimeRange.php88
-rw-r--r--library/Director/Objects/IcingaScheduledDowntimeRanges.php18
-rw-r--r--library/Director/Objects/IcingaService.php828
-rw-r--r--library/Director/Objects/IcingaServiceAssignment.php20
-rw-r--r--library/Director/Objects/IcingaServiceField.php17
-rw-r--r--library/Director/Objects/IcingaServiceGroup.php42
-rw-r--r--library/Director/Objects/IcingaServiceSet.php591
-rw-r--r--library/Director/Objects/IcingaServiceSetAssignment.php20
-rw-r--r--library/Director/Objects/IcingaServiceVar.php29
-rw-r--r--library/Director/Objects/IcingaTemplateChoice.php321
-rw-r--r--library/Director/Objects/IcingaTemplateChoiceHost.php14
-rw-r--r--library/Director/Objects/IcingaTemplateChoiceService.php14
-rw-r--r--library/Director/Objects/IcingaTemplateResolver.php479
-rw-r--r--library/Director/Objects/IcingaTimePeriod.php190
-rw-r--r--library/Director/Objects/IcingaTimePeriodRange.php88
-rw-r--r--library/Director/Objects/IcingaTimePeriodRanges.php35
-rw-r--r--library/Director/Objects/IcingaUser.php92
-rw-r--r--library/Director/Objects/IcingaUserField.php17
-rw-r--r--library/Director/Objects/IcingaUserGroup.php29
-rw-r--r--library/Director/Objects/IcingaVar.php72
-rw-r--r--library/Director/Objects/IcingaZone.php110
-rw-r--r--library/Director/Objects/ImportExportHelper.php68
-rw-r--r--library/Director/Objects/ImportRowModifier.php91
-rw-r--r--library/Director/Objects/ImportRun.php159
-rw-r--r--library/Director/Objects/ImportSource.php537
-rw-r--r--library/Director/Objects/InstantiatedViaHook.php14
-rw-r--r--library/Director/Objects/ObjectApplyMatches.php239
-rw-r--r--library/Director/Objects/ObjectWithArguments.php18
-rw-r--r--library/Director/Objects/ServiceGroupMembershipResolver.php8
-rw-r--r--library/Director/Objects/SyncProperty.php48
-rw-r--r--library/Director/Objects/SyncRule.php553
-rw-r--r--library/Director/Objects/SyncRun.php46
-rw-r--r--library/Director/PlainObjectRenderer.php130
-rw-r--r--library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php172
-rw-r--r--library/Director/PropertyModifier/PropertyModifierArrayFilter.php152
-rw-r--r--library/Director/PropertyModifier/PropertyModifierArrayToRow.php68
-rw-r--r--library/Director/PropertyModifier/PropertyModifierArrayUnique.php37
-rw-r--r--library/Director/PropertyModifier/PropertyModifierBitmask.php39
-rw-r--r--library/Director/PropertyModifier/PropertyModifierCombine.php40
-rw-r--r--library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php94
-rw-r--r--library/Director/PropertyModifier/PropertyModifierDnsRecords.php112
-rw-r--r--library/Director/PropertyModifier/PropertyModifierExtractFromDN.php99
-rw-r--r--library/Director/PropertyModifier/PropertyModifierFromAdSid.php36
-rw-r--r--library/Director/PropertyModifier/PropertyModifierFromLatin1.php23
-rw-r--r--library/Director/PropertyModifier/PropertyModifierGetHostByAddr.php53
-rw-r--r--library/Director/PropertyModifier/PropertyModifierGetHostByName.php54
-rw-r--r--library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php129
-rw-r--r--library/Director/PropertyModifier/PropertyModifierJoin.php34
-rw-r--r--library/Director/PropertyModifier/PropertyModifierJsonDecode.php68
-rw-r--r--library/Director/PropertyModifier/PropertyModifierLConfCustomVar.php48
-rw-r--r--library/Director/PropertyModifier/PropertyModifierListToObject.php95
-rw-r--r--library/Director/PropertyModifier/PropertyModifierLowercase.php17
-rw-r--r--library/Director/PropertyModifier/PropertyModifierMakeBoolean.php90
-rw-r--r--library/Director/PropertyModifier/PropertyModifierMap.php97
-rw-r--r--library/Director/PropertyModifier/PropertyModifierNegateBoolean.php26
-rw-r--r--library/Director/PropertyModifier/PropertyModifierParseURL.php81
-rw-r--r--library/Director/PropertyModifier/PropertyModifierRegexReplace.php45
-rw-r--r--library/Director/PropertyModifier/PropertyModifierRegexSplit.php51
-rw-r--r--library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php147
-rw-r--r--library/Director/PropertyModifier/PropertyModifierRenameColumn.php34
-rw-r--r--library/Director/PropertyModifier/PropertyModifierReplace.php36
-rw-r--r--library/Director/PropertyModifier/PropertyModifierReplaceNull.php33
-rw-r--r--library/Director/PropertyModifier/PropertyModifierSimpleGroupBy.php68
-rw-r--r--library/Director/PropertyModifier/PropertyModifierSkipDuplicates.php26
-rw-r--r--library/Director/PropertyModifier/PropertyModifierSplit.php51
-rw-r--r--library/Director/PropertyModifier/PropertyModifierStripDomain.php38
-rw-r--r--library/Director/PropertyModifier/PropertyModifierSubstring.php54
-rw-r--r--library/Director/PropertyModifier/PropertyModifierToInt.php31
-rw-r--r--library/Director/PropertyModifier/PropertyModifierTrim.php54
-rw-r--r--library/Director/PropertyModifier/PropertyModifierURLEncode.php19
-rw-r--r--library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php44
-rw-r--r--library/Director/PropertyModifier/PropertyModifierUppercase.php17
-rw-r--r--library/Director/PropertyModifier/PropertyModifierUuidBinToHex.php19
-rw-r--r--library/Director/PropertyModifier/PropertyModifierXlsNumericIp.php26
-rw-r--r--library/Director/ProvidedHook/CubeLinks.php65
-rw-r--r--library/Director/ProvidedHook/IcingaDbCubeLinks.php66
-rw-r--r--library/Director/ProvidedHook/Monitoring/HostActions.php73
-rw-r--r--library/Director/ProvidedHook/Monitoring/ServiceActions.php87
-rw-r--r--library/Director/Repository/IcingaTemplateRepository.php122
-rw-r--r--library/Director/Repository/RepositoryByObjectHelper.php99
-rw-r--r--library/Director/Resolver/CommandUsage.php104
-rw-r--r--library/Director/Resolver/HostServiceBlacklist.php91
-rw-r--r--library/Director/Resolver/IcingaHostObjectResolver.php44
-rw-r--r--library/Director/Resolver/IcingaObjectResolver.php558
-rw-r--r--library/Director/Resolver/OverriddenVarsResolver.php74
-rw-r--r--library/Director/Resolver/OverrideHelper.php38
-rw-r--r--library/Director/Resolver/TemplateTree.php491
-rw-r--r--library/Director/RestApi/IcingaObjectHandler.php196
-rw-r--r--library/Director/RestApi/IcingaObjectsHandler.php144
-rw-r--r--library/Director/RestApi/RequestHandler.php86
-rw-r--r--library/Director/RestApi/RestApiClient.php311
-rw-r--r--library/Director/RestApi/RestApiParams.php29
-rw-r--r--library/Director/Restriction/FilterByNameRestriction.php64
-rw-r--r--library/Director/Restriction/HostgroupRestriction.php171
-rw-r--r--library/Director/Restriction/MatchingFilter.php40
-rw-r--r--library/Director/Restriction/ObjectRestriction.php84
-rw-r--r--library/Director/Settings.php219
-rw-r--r--library/Director/StartupLogRenderer.php126
-rw-r--r--library/Director/Test/BaseTestCase.php127
-rw-r--r--library/Director/Test/Bootstrap.php28
-rw-r--r--library/Director/Test/IcingaObjectTestCase.php92
-rw-r--r--library/Director/Test/ImportSourceDummy.php52
-rw-r--r--library/Director/Test/SyncTest.php105
-rw-r--r--library/Director/Test/TestProcess.php116
-rw-r--r--library/Director/Test/TestSuite.php68
-rw-r--r--library/Director/Test/TestSuiteLint.php56
-rw-r--r--library/Director/Test/TestSuiteStyle.php66
-rw-r--r--library/Director/Test/TestSuiteUnit.php26
-rw-r--r--library/Director/Test/Web/Form/TestDirectorObjectForm.php19
-rw-r--r--library/Director/TranslationDummy.php20
-rw-r--r--library/Director/Util.php175
-rw-r--r--library/Director/Web/ActionBar/AutomationObjectActionBar.php65
-rw-r--r--library/Director/Web/ActionBar/ChoicesActionBar.php27
-rw-r--r--library/Director/Web/ActionBar/DirectorBaseActionBar.php67
-rw-r--r--library/Director/Web/ActionBar/ObjectsActionBar.php27
-rw-r--r--library/Director/Web/ActionBar/TemplateActionBar.php42
-rw-r--r--library/Director/Web/Controller/ActionController.php253
-rw-r--r--library/Director/Web/Controller/BranchHelper.php76
-rw-r--r--library/Director/Web/Controller/Extension/CoreApi.php46
-rw-r--r--library/Director/Web/Controller/Extension/DirectorDb.php160
-rw-r--r--library/Director/Web/Controller/Extension/ObjectRestrictions.php48
-rw-r--r--library/Director/Web/Controller/Extension/RestApi.php114
-rw-r--r--library/Director/Web/Controller/Extension/SingleObjectApiHandler.php236
-rw-r--r--library/Director/Web/Controller/ObjectController.php733
-rw-r--r--library/Director/Web/Controller/ObjectsController.php548
-rw-r--r--library/Director/Web/Controller/TemplateController.php243
-rw-r--r--library/Director/Web/Form/ClickHereForm.php31
-rw-r--r--library/Director/Web/Form/CloneImportSourceForm.php72
-rw-r--r--library/Director/Web/Form/CloneSyncRuleForm.php76
-rw-r--r--library/Director/Web/Form/CsrfToken.php53
-rw-r--r--library/Director/Web/Form/DbSelectorForm.php85
-rw-r--r--library/Director/Web/Form/Decorator/ViewHelperRaw.php14
-rw-r--r--library/Director/Web/Form/DirectorForm.php58
-rw-r--r--library/Director/Web/Form/DirectorObjectForm.php1734
-rw-r--r--library/Director/Web/Form/Element/Boolean.php90
-rw-r--r--library/Director/Web/Form/Element/DataFilter.php361
-rw-r--r--library/Director/Web/Form/Element/ExtensibleSet.php89
-rw-r--r--library/Director/Web/Form/Element/FormElement.php9
-rw-r--r--library/Director/Web/Form/Element/InstanceSummary.php51
-rw-r--r--library/Director/Web/Form/Element/OptionalYesNo.php22
-rw-r--r--library/Director/Web/Form/Element/SimpleNote.php34
-rw-r--r--library/Director/Web/Form/Element/StoredPassword.php62
-rw-r--r--library/Director/Web/Form/Element/Text.php16
-rw-r--r--library/Director/Web/Form/Element/YesNo.php14
-rw-r--r--library/Director/Web/Form/Filter/QueryColumnsFromSql.php48
-rw-r--r--library/Director/Web/Form/FormLoader.php43
-rw-r--r--library/Director/Web/Form/IcingaObjectFieldLoader.php628
-rw-r--r--library/Director/Web/Form/IconHelper.php89
-rw-r--r--library/Director/Web/Form/IplElement/ExtensibleSetElement.php570
-rw-r--r--library/Director/Web/Form/QuickBaseForm.php177
-rw-r--r--library/Director/Web/Form/QuickForm.php641
-rw-r--r--library/Director/Web/Form/QuickSubForm.php36
-rw-r--r--library/Director/Web/Form/Validate/IsDataListEntry.php55
-rw-r--r--library/Director/Web/Form/Validate/NamePattern.php38
-rw-r--r--library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php196
-rw-r--r--library/Director/Web/ObjectPreview.php182
-rw-r--r--library/Director/Web/SelfService.php311
-rw-r--r--library/Director/Web/Table/ActivityLogTable.php294
-rw-r--r--library/Director/Web/Table/ApplyRulesTable.php240
-rw-r--r--library/Director/Web/Table/BasketSnapshotTable.php125
-rw-r--r--library/Director/Web/Table/BasketTable.php50
-rw-r--r--library/Director/Web/Table/BranchActivityTable.php116
-rw-r--r--library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php78
-rw-r--r--library/Director/Web/Table/ChoicesTable.php65
-rw-r--r--library/Director/Web/Table/ConfigFileDiffTable.php140
-rw-r--r--library/Director/Web/Table/CoreApiFieldsTable.php106
-rw-r--r--library/Director/Web/Table/CoreApiObjectsTable.php60
-rw-r--r--library/Director/Web/Table/CoreApiPrototypesTable.php43
-rw-r--r--library/Director/Web/Table/CustomvarTable.php102
-rw-r--r--library/Director/Web/Table/CustomvarVariantsTable.php125
-rw-r--r--library/Director/Web/Table/DatafieldCategoryTable.php64
-rw-r--r--library/Director/Web/Table/DatafieldTable.php118
-rw-r--r--library/Director/Web/Table/DatalistEntryTable.php73
-rw-r--r--library/Director/Web/Table/DatalistTable.php41
-rw-r--r--library/Director/Web/Table/DbHelper.php67
-rw-r--r--library/Director/Web/Table/Dependency/DependencyInfoTable.php101
-rw-r--r--library/Director/Web/Table/Dependency/Html.php74
-rw-r--r--library/Director/Web/Table/DependencyTemplateUsageTable.php22
-rw-r--r--library/Director/Web/Table/DeploymentLogTable.php90
-rw-r--r--library/Director/Web/Table/FilterableByUsage.php10
-rw-r--r--library/Director/Web/Table/GeneratedConfigFileTable.php120
-rw-r--r--library/Director/Web/Table/GroupMemberTable.php201
-rw-r--r--library/Director/Web/Table/HostTemplateUsageTable.php22
-rw-r--r--library/Director/Web/Table/IcingaAppliedServiceTable.php49
-rw-r--r--library/Director/Web/Table/IcingaCommandArgumentTable.php89
-rw-r--r--library/Director/Web/Table/IcingaHostAppliedForServiceTable.php117
-rw-r--r--library/Director/Web/Table/IcingaHostAppliedServicesTable.php207
-rw-r--r--library/Director/Web/Table/IcingaHostsMatchingFilterTable.php71
-rw-r--r--library/Director/Web/Table/IcingaObjectDatafieldTable.php87
-rw-r--r--library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php67
-rw-r--r--library/Director/Web/Table/IcingaServiceSetHostTable.php64
-rw-r--r--library/Director/Web/Table/IcingaServiceSetServiceTable.php259
-rw-r--r--library/Director/Web/Table/IcingaTimePeriodRangeTable.php61
-rw-r--r--library/Director/Web/Table/ImportedrowsTable.php103
-rw-r--r--library/Director/Web/Table/ImportrunTable.php90
-rw-r--r--library/Director/Web/Table/ImportsourceHookTable.php107
-rw-r--r--library/Director/Web/Table/ImportsourceTable.php63
-rw-r--r--library/Director/Web/Table/JobTable.php82
-rw-r--r--library/Director/Web/Table/NotificationTemplateUsageTable.php22
-rw-r--r--library/Director/Web/Table/ObjectSetTable.php211
-rw-r--r--library/Director/Web/Table/ObjectsTable.php315
-rw-r--r--library/Director/Web/Table/ObjectsTableApiUser.php13
-rw-r--r--library/Director/Web/Table/ObjectsTableCommand.php67
-rw-r--r--library/Director/Web/Table/ObjectsTableEndpoint.php86
-rw-r--r--library/Director/Web/Table/ObjectsTableHost.php40
-rw-r--r--library/Director/Web/Table/ObjectsTableHostTemplateChoice.php27
-rw-r--r--library/Director/Web/Table/ObjectsTableService.php219
-rw-r--r--library/Director/Web/Table/ObjectsTableZone.php13
-rw-r--r--library/Director/Web/Table/PropertymodifierTable.php145
-rw-r--r--library/Director/Web/Table/QuickTable.php547
-rw-r--r--library/Director/Web/Table/ReadOnlyFormAvpTable.php113
-rw-r--r--library/Director/Web/Table/ServiceTemplateUsageTable.php27
-rw-r--r--library/Director/Web/Table/SyncRunTable.php90
-rw-r--r--library/Director/Web/Table/SyncpropertyTable.php97
-rw-r--r--library/Director/Web/Table/SyncruleTable.php67
-rw-r--r--library/Director/Web/Table/TableLoader.php34
-rw-r--r--library/Director/Web/Table/TableWithBranchSupport.php69
-rw-r--r--library/Director/Web/Table/TemplateUsageTable.php157
-rw-r--r--library/Director/Web/Table/TemplatesTable.php156
-rw-r--r--library/Director/Web/Tabs/DataTabs.php34
-rw-r--r--library/Director/Web/Tabs/ImportTabs.php30
-rw-r--r--library/Director/Web/Tabs/ImportsourceTabs.php58
-rw-r--r--library/Director/Web/Tabs/InfraTabs.php49
-rw-r--r--library/Director/Web/Tabs/MainTabs.php85
-rw-r--r--library/Director/Web/Tabs/ObjectTabs.php160
-rw-r--r--library/Director/Web/Tabs/ObjectsTabs.php85
-rw-r--r--library/Director/Web/Tabs/SyncRuleTabs.php54
-rw-r--r--library/Director/Web/Tree/InspectTreeRenderer.php97
-rw-r--r--library/Director/Web/Tree/TemplateTreeRenderer.php91
-rw-r--r--library/Director/Web/Widget/AbstractList.php40
-rw-r--r--library/Director/Web/Widget/ActivityLogInfo.php634
-rw-r--r--library/Director/Web/Widget/AdditionalTableActions.php158
-rw-r--r--library/Director/Web/Widget/BackgroundDaemonDetails.php131
-rw-r--r--library/Director/Web/Widget/BranchedObjectHint.php69
-rw-r--r--library/Director/Web/Widget/BranchedObjectsHint.php27
-rw-r--r--library/Director/Web/Widget/Daemon/BackgroundDaemonState.php57
-rw-r--r--library/Director/Web/Widget/DeployedConfigInfoHeader.php101
-rw-r--r--library/Director/Web/Widget/DeploymentInfo.php169
-rw-r--r--library/Director/Web/Widget/Documentation.php97
-rw-r--r--library/Director/Web/Widget/HealthCheckPluginOutput.php94
-rw-r--r--library/Director/Web/Widget/IcingaConfigDiff.php58
-rw-r--r--library/Director/Web/Widget/IcingaObjectInspection.php254
-rw-r--r--library/Director/Web/Widget/ImportSourceDetails.php83
-rw-r--r--library/Director/Web/Widget/InspectPackages.php174
-rw-r--r--library/Director/Web/Widget/JobDetails.php69
-rw-r--r--library/Director/Web/Widget/ListItem.php26
-rw-r--r--library/Director/Web/Widget/NotInBranchedHint.php23
-rw-r--r--library/Director/Web/Widget/OrderedList.php8
-rw-r--r--library/Director/Web/Widget/ShowConfigFile.php106
-rw-r--r--library/Director/Web/Widget/SyncRunDetails.php129
-rw-r--r--library/Director/Web/Widget/UnorderedList.php8
-rw-r--r--library/Director/Web/Window.php13
-rw-r--r--module.info6
-rw-r--r--phpcs.xml21
-rw-r--r--phpunit.xml17
-rw-r--r--public/css/module.less1835
-rw-r--r--public/img/globe.pngbin0 -> 882 bytes
-rw-r--r--public/img/leaf.gifbin0 -> 945 bytes
-rw-r--r--public/img/script.pngbin0 -> 471 bytes
-rw-r--r--public/img/server.pngbin0 -> 423 bytes
-rw-r--r--public/img/service.pngbin0 -> 601 bytes
-rw-r--r--public/img/tree.pngbin0 -> 524 bytes
-rw-r--r--public/js/module.js840
-rw-r--r--register-hooks.php146
-rw-r--r--run-missingdeps.php23
-rw-r--r--run-php5.3.php26
-rw-r--r--run.php18
-rw-r--r--schema/mysql-legacy-changes/upgrade_1.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_10.sql14
-rw-r--r--schema/mysql-legacy-changes/upgrade_11.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_12.sql17
-rw-r--r--schema/mysql-legacy-changes/upgrade_13.sql18
-rw-r--r--schema/mysql-legacy-changes/upgrade_14.sql18
-rw-r--r--schema/mysql-legacy-changes/upgrade_15.sql17
-rw-r--r--schema/mysql-legacy-changes/upgrade_16.sql17
-rw-r--r--schema/mysql-legacy-changes/upgrade_17.sql17
-rw-r--r--schema/mysql-legacy-changes/upgrade_18.sql17
-rw-r--r--schema/mysql-legacy-changes/upgrade_19.sql17
-rw-r--r--schema/mysql-legacy-changes/upgrade_2.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_20.sql17
-rw-r--r--schema/mysql-legacy-changes/upgrade_21.sql15
-rw-r--r--schema/mysql-legacy-changes/upgrade_22.sql43
-rw-r--r--schema/mysql-legacy-changes/upgrade_23.sql51
-rw-r--r--schema/mysql-legacy-changes/upgrade_24.sql91
-rw-r--r--schema/mysql-legacy-changes/upgrade_25.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_26.sql4
-rw-r--r--schema/mysql-legacy-changes/upgrade_27.sql58
-rw-r--r--schema/mysql-legacy-changes/upgrade_28.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_29.sql16
-rw-r--r--schema/mysql-legacy-changes/upgrade_3.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_30.sql8
-rw-r--r--schema/mysql-legacy-changes/upgrade_31.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_32.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_33.sql11
-rw-r--r--schema/mysql-legacy-changes/upgrade_34.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_35.sql6
-rw-r--r--schema/mysql-legacy-changes/upgrade_36.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_37.sql17
-rw-r--r--schema/mysql-legacy-changes/upgrade_38.sql4
-rw-r--r--schema/mysql-legacy-changes/upgrade_39.sql5
-rw-r--r--schema/mysql-legacy-changes/upgrade_4.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_40.sql9
-rw-r--r--schema/mysql-legacy-changes/upgrade_41.sql19
-rw-r--r--schema/mysql-legacy-changes/upgrade_42.sql7
-rw-r--r--schema/mysql-legacy-changes/upgrade_43.sql13
-rw-r--r--schema/mysql-legacy-changes/upgrade_44.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_45.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_46.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_47.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_48.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_49.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_5.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_50.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_51.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_52.sql5
-rw-r--r--schema/mysql-legacy-changes/upgrade_53.sql9
-rw-r--r--schema/mysql-legacy-changes/upgrade_54.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_55.sql8
-rw-r--r--schema/mysql-legacy-changes/upgrade_56.sql13
-rw-r--r--schema/mysql-legacy-changes/upgrade_57.sql20
-rw-r--r--schema/mysql-legacy-changes/upgrade_58.sql5
-rw-r--r--schema/mysql-legacy-changes/upgrade_59.sql3
-rw-r--r--schema/mysql-legacy-changes/upgrade_6.sql5
-rw-r--r--schema/mysql-legacy-changes/upgrade_60.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_61.sql2
-rw-r--r--schema/mysql-legacy-changes/upgrade_62.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_7.sql1
-rw-r--r--schema/mysql-legacy-changes/upgrade_8.sql36
-rw-r--r--schema/mysql-legacy-changes/upgrade_9.sql1
-rw-r--r--schema/mysql-migrations/upgrade_100.sql6
-rw-r--r--schema/mysql-migrations/upgrade_101.sql7
-rw-r--r--schema/mysql-migrations/upgrade_102.sql13
-rw-r--r--schema/mysql-migrations/upgrade_103.sql12
-rw-r--r--schema/mysql-migrations/upgrade_104.sql19
-rw-r--r--schema/mysql-migrations/upgrade_105.sql6
-rw-r--r--schema/mysql-migrations/upgrade_107.sql9
-rw-r--r--schema/mysql-migrations/upgrade_108.sql18
-rw-r--r--schema/mysql-migrations/upgrade_109.sql16
-rw-r--r--schema/mysql-migrations/upgrade_110.sql104
-rw-r--r--schema/mysql-migrations/upgrade_112.sql6
-rw-r--r--schema/mysql-migrations/upgrade_114.sql55
-rw-r--r--schema/mysql-migrations/upgrade_115.sql23
-rw-r--r--schema/mysql-migrations/upgrade_116.sql18
-rw-r--r--schema/mysql-migrations/upgrade_117.sql20
-rw-r--r--schema/mysql-migrations/upgrade_119.sql6
-rw-r--r--schema/mysql-migrations/upgrade_120.sql184
-rw-r--r--schema/mysql-migrations/upgrade_121.sql8
-rw-r--r--schema/mysql-migrations/upgrade_122.sql12
-rw-r--r--schema/mysql-migrations/upgrade_123.sql30
-rw-r--r--schema/mysql-migrations/upgrade_124.sql3
-rw-r--r--schema/mysql-migrations/upgrade_125.sql18
-rw-r--r--schema/mysql-migrations/upgrade_126.sql217
-rw-r--r--schema/mysql-migrations/upgrade_127.sql152
-rw-r--r--schema/mysql-migrations/upgrade_128.sql6
-rw-r--r--schema/mysql-migrations/upgrade_129.sql6
-rw-r--r--schema/mysql-migrations/upgrade_130.sql6
-rw-r--r--schema/mysql-migrations/upgrade_131.sql19
-rw-r--r--schema/mysql-migrations/upgrade_132.sql21
-rw-r--r--schema/mysql-migrations/upgrade_133.sql21
-rw-r--r--schema/mysql-migrations/upgrade_134.sql19
-rw-r--r--schema/mysql-migrations/upgrade_135.sql9
-rw-r--r--schema/mysql-migrations/upgrade_136.sql6
-rw-r--r--schema/mysql-migrations/upgrade_137.sql9
-rw-r--r--schema/mysql-migrations/upgrade_138.sql6
-rw-r--r--schema/mysql-migrations/upgrade_139.sql7
-rw-r--r--schema/mysql-migrations/upgrade_140.sql5
-rw-r--r--schema/mysql-migrations/upgrade_141.sql7
-rw-r--r--schema/mysql-migrations/upgrade_143.sql21
-rw-r--r--schema/mysql-migrations/upgrade_144.sql91
-rw-r--r--schema/mysql-migrations/upgrade_145.sql7
-rw-r--r--schema/mysql-migrations/upgrade_146.sql14
-rw-r--r--schema/mysql-migrations/upgrade_147.sql20
-rw-r--r--schema/mysql-migrations/upgrade_148.sql10
-rw-r--r--schema/mysql-migrations/upgrade_149.sql11
-rw-r--r--schema/mysql-migrations/upgrade_150.sql17
-rw-r--r--schema/mysql-migrations/upgrade_151.sql38
-rw-r--r--schema/mysql-migrations/upgrade_152.sql9
-rw-r--r--schema/mysql-migrations/upgrade_153.sql42
-rw-r--r--schema/mysql-migrations/upgrade_154.sql12
-rw-r--r--schema/mysql-migrations/upgrade_155.sql19
-rw-r--r--schema/mysql-migrations/upgrade_156.sql7
-rw-r--r--schema/mysql-migrations/upgrade_157.sql6
-rw-r--r--schema/mysql-migrations/upgrade_159.sql6
-rw-r--r--schema/mysql-migrations/upgrade_160.sql6
-rw-r--r--schema/mysql-migrations/upgrade_161.sql58
-rw-r--r--schema/mysql-migrations/upgrade_162.sql6
-rw-r--r--schema/mysql-migrations/upgrade_163.sql38
-rw-r--r--schema/mysql-migrations/upgrade_164.sql8
-rw-r--r--schema/mysql-migrations/upgrade_165.sql6
-rw-r--r--schema/mysql-migrations/upgrade_166.sql21
-rw-r--r--schema/mysql-migrations/upgrade_167.sql25
-rw-r--r--schema/mysql-migrations/upgrade_168.sql21
-rw-r--r--schema/mysql-migrations/upgrade_170.sql7
-rw-r--r--schema/mysql-migrations/upgrade_171.sql3
-rw-r--r--schema/mysql-migrations/upgrade_172.sql11
-rw-r--r--schema/mysql-migrations/upgrade_173.sql6
-rw-r--r--schema/mysql-migrations/upgrade_174.sql241
-rw-r--r--schema/mysql-migrations/upgrade_175.sql484
-rw-r--r--schema/mysql-migrations/upgrade_176.sql6
-rw-r--r--schema/mysql-migrations/upgrade_177.sql20
-rw-r--r--schema/mysql-migrations/upgrade_178.sql20
-rw-r--r--schema/mysql-migrations/upgrade_179.sql5
-rw-r--r--schema/mysql-migrations/upgrade_180.sql26
-rw-r--r--schema/mysql-migrations/upgrade_182.sql12
-rw-r--r--schema/mysql-migrations/upgrade_63.sql12
-rw-r--r--schema/mysql-migrations/upgrade_64.sql10
-rw-r--r--schema/mysql-migrations/upgrade_65.sql37
-rw-r--r--schema/mysql-migrations/upgrade_66.sql37
-rw-r--r--schema/mysql-migrations/upgrade_67.sql23
-rw-r--r--schema/mysql-migrations/upgrade_68.sql6
-rw-r--r--schema/mysql-migrations/upgrade_69.sql9
-rw-r--r--schema/mysql-migrations/upgrade_70.sql13
-rw-r--r--schema/mysql-migrations/upgrade_71.sql44
-rw-r--r--schema/mysql-migrations/upgrade_72.sql14
-rw-r--r--schema/mysql-migrations/upgrade_73.sql50
-rw-r--r--schema/mysql-migrations/upgrade_74.sql14
-rw-r--r--schema/mysql-migrations/upgrade_75.sql50
-rw-r--r--schema/mysql-migrations/upgrade_76.sql28
-rw-r--r--schema/mysql-migrations/upgrade_77.sql78
-rw-r--r--schema/mysql-migrations/upgrade_78.sql20
-rw-r--r--schema/mysql-migrations/upgrade_82.sql17
-rw-r--r--schema/mysql-migrations/upgrade_84.sql5
-rw-r--r--schema/mysql-migrations/upgrade_85.sql15
-rw-r--r--schema/mysql-migrations/upgrade_86.sql35
-rw-r--r--schema/mysql-migrations/upgrade_87.sql6
-rw-r--r--schema/mysql-migrations/upgrade_89.sql6
-rw-r--r--schema/mysql-migrations/upgrade_90.sql5
-rw-r--r--schema/mysql-migrations/upgrade_91.sql5
-rw-r--r--schema/mysql-migrations/upgrade_92.sql27
-rw-r--r--schema/mysql-migrations/upgrade_93.sql22
-rw-r--r--schema/mysql-migrations/upgrade_94.sql29
-rw-r--r--schema/mysql-migrations/upgrade_95.sql22
-rw-r--r--schema/mysql-migrations/upgrade_96.sql5
-rw-r--r--schema/mysql-migrations/upgrade_97.sql11
-rw-r--r--schema/mysql.sql2442
-rw-r--r--schema/pgsql-legacy-changes/upgrade-10.sql20
-rw-r--r--schema/pgsql-legacy-changes/upgrade-11.sql20
-rw-r--r--schema/pgsql-legacy-changes/upgrade-2.sql2
-rw-r--r--schema/pgsql-legacy-changes/upgrade-3.sql21
-rw-r--r--schema/pgsql-legacy-changes/upgrade-4.sql20
-rw-r--r--schema/pgsql-legacy-changes/upgrade-5.sql20
-rw-r--r--schema/pgsql-legacy-changes/upgrade-6.sql20
-rw-r--r--schema/pgsql-legacy-changes/upgrade-7.sql20
-rw-r--r--schema/pgsql-legacy-changes/upgrade-8.sql20
-rw-r--r--schema/pgsql-legacy-changes/upgrade-9.sql20
-rw-r--r--schema/pgsql-legacy-changes/upgrade_1.sql13
-rw-r--r--schema/pgsql-legacy-changes/upgrade_21.sql17
-rw-r--r--schema/pgsql-legacy-changes/upgrade_22.sql55
-rw-r--r--schema/pgsql-legacy-changes/upgrade_23.sql60
-rw-r--r--schema/pgsql-legacy-changes/upgrade_34.sql189
-rw-r--r--schema/pgsql-legacy-changes/upgrade_35.sql2
-rw-r--r--schema/pgsql-migrations/upgrade_100.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_101.sql9
-rw-r--r--schema/pgsql-migrations/upgrade_102.sql13
-rw-r--r--schema/pgsql-migrations/upgrade_103.sql11
-rw-r--r--schema/pgsql-migrations/upgrade_104.sql25
-rw-r--r--schema/pgsql-migrations/upgrade_105.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_106.sql9
-rw-r--r--schema/pgsql-migrations/upgrade_107.sql9
-rw-r--r--schema/pgsql-migrations/upgrade_109.sql16
-rw-r--r--schema/pgsql-migrations/upgrade_110.sql104
-rw-r--r--schema/pgsql-migrations/upgrade_111.sql11
-rw-r--r--schema/pgsql-migrations/upgrade_113.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_114.sql63
-rw-r--r--schema/pgsql-migrations/upgrade_115.sql28
-rw-r--r--schema/pgsql-migrations/upgrade_116.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_117.sql26
-rw-r--r--schema/pgsql-migrations/upgrade_119.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_120.sql201
-rw-r--r--schema/pgsql-migrations/upgrade_121.sql8
-rw-r--r--schema/pgsql-migrations/upgrade_122.sql12
-rw-r--r--schema/pgsql-migrations/upgrade_123.sql34
-rw-r--r--schema/pgsql-migrations/upgrade_124.sql21
-rw-r--r--schema/pgsql-migrations/upgrade_125.sql18
-rw-r--r--schema/pgsql-migrations/upgrade_127.sql197
-rw-r--r--schema/pgsql-migrations/upgrade_128.sql5
-rw-r--r--schema/pgsql-migrations/upgrade_131.sql22
-rw-r--r--schema/pgsql-migrations/upgrade_132.sql25
-rw-r--r--schema/pgsql-migrations/upgrade_133.sql25
-rw-r--r--schema/pgsql-migrations/upgrade_134.sql19
-rw-r--r--schema/pgsql-migrations/upgrade_135.sql9
-rw-r--r--schema/pgsql-migrations/upgrade_136.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_137.sql9
-rw-r--r--schema/pgsql-migrations/upgrade_138.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_139.sql9
-rw-r--r--schema/pgsql-migrations/upgrade_140.sql5
-rw-r--r--schema/pgsql-migrations/upgrade_141.sql7
-rw-r--r--schema/pgsql-migrations/upgrade_142.sql13
-rw-r--r--schema/pgsql-migrations/upgrade_143.sql27
-rw-r--r--schema/pgsql-migrations/upgrade_144.sql99
-rw-r--r--schema/pgsql-migrations/upgrade_146.sql14
-rw-r--r--schema/pgsql-migrations/upgrade_147.sql23
-rw-r--r--schema/pgsql-migrations/upgrade_148.sql10
-rw-r--r--schema/pgsql-migrations/upgrade_149.sql14
-rw-r--r--schema/pgsql-migrations/upgrade_150.sql17
-rw-r--r--schema/pgsql-migrations/upgrade_151.sql38
-rw-r--r--schema/pgsql-migrations/upgrade_152.sql7
-rw-r--r--schema/pgsql-migrations/upgrade_153.sql45
-rw-r--r--schema/pgsql-migrations/upgrade_154.sql12
-rw-r--r--schema/pgsql-migrations/upgrade_155.sql22
-rw-r--r--schema/pgsql-migrations/upgrade_156.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_157.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_158.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_160.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_161.sql70
-rw-r--r--schema/pgsql-migrations/upgrade_162.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_164.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_165.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_166.sql7
-rw-r--r--schema/pgsql-migrations/upgrade_167.sql24
-rw-r--r--schema/pgsql-migrations/upgrade_168.sql25
-rw-r--r--schema/pgsql-migrations/upgrade_169.sql8
-rw-r--r--schema/pgsql-migrations/upgrade_170.sql5
-rw-r--r--schema/pgsql-migrations/upgrade_171.sql3
-rw-r--r--schema/pgsql-migrations/upgrade_172.sql13
-rw-r--r--schema/pgsql-migrations/upgrade_173.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_174.sql61
-rw-r--r--schema/pgsql-migrations/upgrade_175.sql512
-rw-r--r--schema/pgsql-migrations/upgrade_176.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_177.sql8
-rw-r--r--schema/pgsql-migrations/upgrade_178.sql23
-rw-r--r--schema/pgsql-migrations/upgrade_179.sql5
-rw-r--r--schema/pgsql-migrations/upgrade_180.sql32
-rw-r--r--schema/pgsql-migrations/upgrade_181.sql19
-rw-r--r--schema/pgsql-migrations/upgrade_182.sql14
-rw-r--r--schema/pgsql-migrations/upgrade_77.sql72
-rw-r--r--schema/pgsql-migrations/upgrade_78.sql25
-rw-r--r--schema/pgsql-migrations/upgrade_79.sql11
-rw-r--r--schema/pgsql-migrations/upgrade_80.sql11
-rw-r--r--schema/pgsql-migrations/upgrade_81.sql5
-rw-r--r--schema/pgsql-migrations/upgrade_82.sql12
-rw-r--r--schema/pgsql-migrations/upgrade_83.sql7
-rw-r--r--schema/pgsql-migrations/upgrade_84.sql5
-rw-r--r--schema/pgsql-migrations/upgrade_85.sql15
-rw-r--r--schema/pgsql-migrations/upgrade_86.sql35
-rw-r--r--schema/pgsql-migrations/upgrade_88.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_89.sql5
-rw-r--r--schema/pgsql-migrations/upgrade_90.sql6
-rw-r--r--schema/pgsql-migrations/upgrade_91.sql5
-rw-r--r--schema/pgsql-migrations/upgrade_92.sql27
-rw-r--r--schema/pgsql-migrations/upgrade_93.sql24
-rw-r--r--schema/pgsql-migrations/upgrade_94.sql34
-rw-r--r--schema/pgsql-migrations/upgrade_95.sql20
-rw-r--r--schema/pgsql-migrations/upgrade_96.sql7
-rw-r--r--schema/pgsql-migrations/upgrade_97.sql11
-rw-r--r--schema/pgsql-migrations/upgrade_98.sql7
-rw-r--r--schema/pgsql.sql2781
-rw-r--r--test/bootstrap.php16
-rw-r--r--test/config/authentication.ini0
-rw-r--r--test/config/config.ini0
-rw-r--r--test/config/resources.ini13
-rw-r--r--test/php/library/Director/Application/DependencyTest.php72
-rw-r--r--test/php/library/Director/Application/FiltersWorkAsExpectedTest.php15
-rw-r--r--test/php/library/Director/Application/MemoryLimitTest.php67
-rw-r--r--test/php/library/Director/CustomVariable/CustomVariablesTest.php79
-rw-r--r--test/php/library/Director/Data/AssignFilterHelperTest.php86
-rw-r--r--test/php/library/Director/Data/RecursiveUtf8ValidatorTest.php45
-rw-r--r--test/php/library/Director/IcingaConfig/AssignRendererTest.php126
-rw-r--r--test/php/library/Director/IcingaConfig/ExtensibleSetTest.php162
-rw-r--r--test/php/library/Director/IcingaConfig/IcingaConfigHelperTest.php130
-rw-r--r--test/php/library/Director/IcingaConfig/StateFilterTest.php171
-rw-r--r--test/php/library/Director/IcingaConfig/rendered/dict1.out6
-rw-r--r--test/php/library/Director/Import/HostSyncTest.php250
-rw-r--r--test/php/library/Director/Import/ImportSourceRestApiTest.php29
-rw-r--r--test/php/library/Director/Import/SyncUtilsTest.php108
-rw-r--r--test/php/library/Director/Objects/HostApplyMatchesTest.php93
-rw-r--r--test/php/library/Director/Objects/HostGroupMembershipResolverTest.php353
-rw-r--r--test/php/library/Director/Objects/IcingaCommandTest.php216
-rw-r--r--test/php/library/Director/Objects/IcingaHostTest.php771
-rw-r--r--test/php/library/Director/Objects/IcingaNotificationTest.php248
-rw-r--r--test/php/library/Director/Objects/IcingaServiceSetTest.php183
-rw-r--r--test/php/library/Director/Objects/IcingaServiceTest.php293
-rw-r--r--test/php/library/Director/Objects/IcingaTemplateResolverTest.php158
-rw-r--r--test/php/library/Director/Objects/IcingaTimePeriodTest.php184
-rw-r--r--test/php/library/Director/Objects/rendered/command1.out4
-rw-r--r--test/php/library/Director/Objects/rendered/command2.out4
-rw-r--r--test/php/library/Director/Objects/rendered/command3.out4
-rw-r--r--test/php/library/Director/Objects/rendered/command4.out4
-rw-r--r--test/php/library/Director/Objects/rendered/command5.out4
-rw-r--r--test/php/library/Director/Objects/rendered/command6.out4
-rw-r--r--test/php/library/Director/Objects/rendered/command7.out9
-rw-r--r--test/php/library/Director/Objects/rendered/host1.out12
-rw-r--r--test/php/library/Director/Objects/rendered/host2.out17
-rw-r--r--test/php/library/Director/Objects/rendered/host3.out14
-rw-r--r--test/php/library/Director/Objects/rendered/notification1.out4
-rw-r--r--test/php/library/Director/Objects/rendered/service1.out14
-rw-r--r--test/php/library/Director/Objects/rendered/service2.out16
-rw-r--r--test/php/library/Director/Objects/rendered/service3.out15
-rw-r--r--test/php/library/Director/Objects/rendered/service4.out13
-rw-r--r--test/php/library/Director/Objects/rendered/service5.out14
-rw-r--r--test/php/library/Director/Objects/rendered/service6.out15
-rw-r--r--test/php/library/Director/Objects/rendered/service7.out14
-rw-r--r--test/php/library/Director/PropertyModifier/PropertyModifierArrayElementByPositionTest.php143
-rw-r--r--test/php/library/Director/PropertyModifier/PropertyModifierArrayFilterTest.php120
-rw-r--r--test/php/library/Director/PropertyModifier/PropertyModifierCombineTest.php51
-rw-r--r--test/php/library/Director/PropertyModifier/PropertyModifierListToObjectTest.php104
-rw-r--r--test/php/library/Director/PropertyModifier/PropertyModifierParseURLTest.php147
-rw-r--r--test/php/library/Director/Resolver/TemplateTreeTest.php259
-rw-r--r--test/php/library/Director/Restriction/MatchingFilterTest.php56
-rw-r--r--test/phpunit-compat.php10
-rwxr-xr-xtest/setup_vendor.sh71
-rwxr-xr-xtest/travis-prepare.sh25
1129 files changed, 137634 insertions, 0 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..e9bddce
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,24 @@
+## Expected Behavior
+<!--- If you're describing a bug, tell us what should happen -->
+<!--- If you're suggesting a change/improvement, tell us how it should work -->
+
+## Current Behavior
+<!--- If describing a bug, tell us what happens instead of the expected behavior -->
+<!--- If suggesting a change/improvement, explain the difference from current behavior -->
+
+## Possible Solution
+<!--- Not obligatory, but suggest a fix/reason for the bug, -->
+<!--- or ideas how to implement: the addition or change -->
+
+## Steps to Reproduce (for bugs)
+<!--- Provide a link to a live example, or an unambiguous set of steps to -->
+<!--- reproduce this bug. Include configuration, logs, etc. to reproduce, if relevant -->
+
+## Your Environment
+<!--- Include as many relevant details about the environment you experienced the problem in -->
+* Director version (System - About):
+* Icinga Web 2 version and modules (System - About):
+* Icinga 2 version (`icinga2 --version`):
+* Operating System and version:
+* Webserver, PHP versions:
+
diff --git a/.github/workflows/L10n-update.yml b/.github/workflows/L10n-update.yml
new file mode 100644
index 0000000..9dce59a
--- /dev/null
+++ b/.github/workflows/L10n-update.yml
@@ -0,0 +1,20 @@
+name: L10n Update
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ trigger-update:
+ name: L10n Update Trigger
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Repository dispatch
+ uses: peter-evans/repository-dispatch@v1
+ with:
+ token: ${{ secrets.ICINGABOT_TOKEN }}
+ repository: Icinga/L10n
+ event-type: update
+ client-payload: '{"origin": "${{ github.repository }}", "commit": "${{ github.sha }}"}'
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3511072
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+## Editors
+/.idea/
+.*.sw[op]
+
+## PHP vendor artifacts
+/vendor/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c6af13c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,61 @@
+Icinga Director
+===============
+
+Icinga Director has been designed to make Icinga 2 configuration handling easy.
+It tries to target two main audiences:
+
+* Users with the desire to completely automate their datacenter
+* Sysops willing to grant their "point & click" users a lot of flexibility
+
+What makes Icinga Director so special is the fact that it tries to target both
+of them at once.
+
+![Icinga Director](doc/screenshot/director/readme/director_main_screen.png)
+
+Read more about Icinga Director in our [Introduction](doc/01-Introduction.md) section.
+Afterwards, you should be ready for [getting started](doc/04-Getting-started.md).
+
+Documentation
+-------------
+
+Please have a look at our [Installation instructions](doc/02-Installation.md)
+and our hints for how to apply [Upgrades](doc/05-Upgrading.md). We love automation
+and in case you also do so, the [Automation chapter](doc/03-Automation.md) could
+be worth a read. When upgrading, you should also have a look at our [Changelog](doc/82-Changelog.md).
+
+You could be interested in understanding how the [Director works](doc/10-How-it-works.md)
+internally. [Working with agents](doc/24-Working-with-agents.md) is a topic that
+affects many Icinga administrators. Other interesting entry points might be
+[Import and Synchronization](doc/70-Import-and-Sync.md), our [CLI interface](doc/60-CLI.md),
+the [REST API](doc/70-REST-API.md) and last but not least our [FAQ](doc/80-FAQ.md).
+
+A complete list of all our documentation can be found in the [doc](doc/) directory.
+
+Contributing
+------------
+
+Icinga Director is an Open Source project and lives from your contributions. No
+matter whether these are feature requests, issues, translations, documentation
+or code.
+
+* Please check whether a related issue already exists on our [Issue Tracker](https://github.com/Icinga/icingaweb2-module-director/issues)
+* Make sure your code conforms to the [PSR-2: Coding Style Guide](http://www.php-fig.org/psr/psr-2/)
+* [Unit-Tests](doc/93-Testing.md) would be great
+* Send a [Pull Request](https://github.com/Icinga/icingaweb2-module-director/pulls)
+
+Addons
+------
+
+The following are to be considered community-supported modules, as they are not
+supported by the Icinga Team. At least not yet. But please give them a try if
+they fit your needs. They are being used in productive environments:
+
+* [AWS - Amazon Web Services](https://github.com/Icinga/icingaweb2-module-aws):
+ provides an Import Source for Autoscaling Groups on AWS
+* [File-Shipper](https://github.com/Icinga/icingaweb2-module-fileshipper):
+ allows Director to ship additional config files with manual config with its
+ deployments
+* [PuppetDB](https://github.com/Icinga/icingaweb2-module-puppetdb): provides
+ an Import Source dealing with your PuppetDB
+* [vSphere](https://github.com/Icinga/icingaweb2-module-vsphere): VMware vSphere
+ Import Source for Virtual Machines and Host Systems
diff --git a/application/clicommands/BasketCommand.php b/application/clicommands/BasketCommand.php
new file mode 100644
index 0000000..dd2434f
--- /dev/null
+++ b/application/clicommands/BasketCommand.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
+use Icinga\Module\Director\DirectorObject\ObjectPurgeHelper;
+
+/**
+ * Export Director Config Objects
+ */
+class BasketCommand extends Command
+{
+ /**
+ * List configured Baskets
+ *
+ * USAGE
+ *
+ * icingacli director basket list
+ *
+ * OPTIONS
+ */
+ public function listAction()
+ {
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()
+ ->from('director_basket', 'basket_name')
+ ->order('basket_name');
+ foreach ($db->fetchCol($query) as $name) {
+ echo "$name\n";
+ }
+ }
+
+ /**
+ * JSON-dump for objects related to the given Basket
+ *
+ * USAGE
+ *
+ * icingacli director basket dump --name <basket>
+ *
+ * OPTIONS
+ */
+ public function dumpAction()
+ {
+ $basket = $this->requireBasket();
+ $snapshot = BasketSnapshot::createForBasket($basket, $this->db());
+ echo $snapshot->getJsonDump() . "\n";
+ }
+
+ /**
+ * Take a snapshot for the given Basket
+ *
+ * USAGE
+ *
+ * icingacli director basket snapshot --name <basket>
+ *
+ * OPTIONS
+ */
+ public function snapshotAction()
+ {
+ $basket = $this->requireBasket();
+ $snapshot = BasketSnapshot::createForBasket($basket, $this->db());
+ $snapshot->store();
+ $hexSum = bin2hex($snapshot->get('content_checksum'));
+ printf(
+ "Snapshot '%s' taken for Basket '%s' at %s\n",
+ substr($hexSum, 0, 7),
+ $basket->get('basket_name'),
+ DateFormatter::formatDateTime($snapshot->get('ts_create') / 1000)
+ );
+ }
+
+ /**
+ * Restore a Basket from JSON dump provided on STDIN
+ *
+ * USAGE
+ *
+ * icingacli director basket restore < basket-dump.json
+ *
+ * OPTIONS
+ * --purge <ObjectType>[,<ObjectType] Purge objects of the
+ * Given types. WARNING: this removes ALL objects that are
+ * not shipped with the given basket
+ * --force Purge refuses to purge Objects in case there are
+ * no Objects of a given ObjectType in the provided basket
+ * unless forced to do so
+ */
+ public function restoreAction()
+ {
+ if ($purge = $this->params->get('purge')) {
+ $purge = explode(',', $purge);
+ ObjectPurgeHelper::assertObjectTypesAreEligibleForPurge($purge);
+ }
+ $json = file_get_contents('php://stdin');
+ BasketSnapshot::restoreJson($json, $this->db());
+ if ($purge) {
+ $this->purgeObjectTypes(Json::decode($json), $purge, $this->params->get('force'));
+ }
+ echo "Objects from Basket Snapshot have been restored\n";
+ }
+
+ protected function purgeObjectTypes($objects, array $types, $force = false)
+ {
+ $helper = new ObjectPurgeHelper($this->db());
+ if ($force) {
+ $helper->force();
+ }
+ foreach ($types as $type) {
+ list($className, $typeFilter) = BasketSnapshot::getClassAndObjectTypeForType($type);
+ $helper->purge(
+ isset($objects->$type) ? (array) $objects->$type : [],
+ $className,
+ $typeFilter
+ );
+ }
+ }
+
+ /**
+ */
+ protected function requireBasket()
+ {
+ return Basket::load($this->params->getRequired('name'), $this->db());
+ }
+}
diff --git a/application/clicommands/BenchmarkCommand.php b/application/clicommands/BenchmarkCommand.php
new file mode 100644
index 0000000..6ccd8c8
--- /dev/null
+++ b/application/clicommands/BenchmarkCommand.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\CustomVariable\CustomVariable;
+use Icinga\Module\Director\Data\Db\IcingaObjectFilterRenderer;
+use Icinga\Module\Director\Data\Db\IcingaObjectQuery;
+use Icinga\Module\Director\Objects\HostGroupMembershipResolver;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaHostVar;
+use Icinga\Module\Director\Objects\IcingaVar;
+
+class BenchmarkCommand extends Command
+{
+ public function testflatfilterAction()
+ {
+ $q = new IcingaObjectQuery('host', $this->db());
+ $filter = Filter::fromQueryString(
+ // 'host.vars.snmp_community="*ub*"&(host.vars.location="London"|host.vars.location="Berlin")'
+ // 'host.vars.snmp_community="*ub*"&(host.vars.location="FRA DC"|host.vars.location="NBG DC")'
+ 'host.vars.priority="*igh"&(host.vars.location="FRA DC"|host.vars.location="NBG DC")'
+ );
+ IcingaObjectFilterRenderer::apply($filter, $q);
+ echo $q->getSql() . "\n";
+
+ print_r($q->listNames());
+ }
+
+ public function rerendervarsAction()
+ {
+ $conn = $this->db();
+ $db = $conn->getDbAdapter();
+ $db->beginTransaction();
+ $query = $db->select()->from(
+ array('v' => 'icinga_var'),
+ array(
+ 'v.varname',
+ 'v.varvalue',
+ 'v.checksum',
+ 'v.rendered_checksum',
+ 'v.rendered',
+ 'format' => "('json')",
+ )
+ );
+ Benchmark::measure('Ready to fetch all vars');
+ $rows = $db->fetchAll($query);
+ Benchmark::measure('Got vars, storing flat');
+ foreach ($rows as $row) {
+ $var = CustomVariable::fromDbRow($row);
+ $rendered = $var->render();
+ $checksum = sha1($rendered, true);
+ if ($checksum === $row->rendered_checksum) {
+ continue;
+ }
+
+ $where = $db->quoteInto('checksum = ?', $row->checksum);
+ $db->update(
+ 'icinga_var',
+ array(
+ 'rendered' => $rendered,
+ 'rendered_checksum' => $checksum
+ ),
+ $where
+ );
+ }
+
+ $db->commit();
+ }
+
+ public function flattenvarsAction()
+ {
+ $conn = $this->db();
+ $db = $conn->getDbAdapter();
+ $db->beginTransaction();
+ $query = $db->select()->from(['v' => 'icinga_host_var'], [
+ 'v.host_id',
+ 'v.varname',
+ 'v.varvalue',
+ 'v.format',
+ 'v.checksum'
+ ]);
+ Benchmark::measure('Ready to fetch all vars');
+ $rows = $db->fetchAll($query);
+ Benchmark::measure('Got vars, storing flat');
+
+ foreach ($rows as $row) {
+ $var = CustomVariable::fromDbRow($row);
+ $checksum = $var->checksum();
+ if (! IcingaVar::exists($checksum, $conn)) {
+ IcingaVar::generateForCustomVar($var, $conn);
+ }
+
+ if ($row->checksum === null) {
+ $where = $db->quoteInto('host_id = ?', $row->host_id)
+ . $db->quoteInto(' AND varname = ?', $row->varname);
+ $db->update('icinga_host_var', ['checksum' => $checksum], $where);
+ }
+ }
+
+ $db->commit();
+ }
+
+ public function resolvehostgroupsAction()
+ {
+ $resolver = new HostGroupMembershipResolver($this->db());
+ $resolver->refreshDb();
+ }
+
+ public function filterAction()
+ {
+ $flat = [];
+
+ /** @var FilterChain|FilterExpression $filter */
+ $filter = Filter::fromQueryString(
+ // 'object_name=*ic*2*&object_type=object'
+ 'vars.bpconfig=*'
+ );
+ Benchmark::measure('ready');
+ $objs = IcingaHost::loadAll($this->db());
+ Benchmark::measure('db done');
+
+ foreach ($objs as $host) {
+ $flat[$host->get('id')] = (object) [];
+ foreach ($host->getProperties() as $k => $v) {
+ $flat[$host->get('id')]->$k = $v;
+ }
+ }
+ Benchmark::measure('objects ready');
+
+ $vars = IcingaHostVar::loadAll($this->db());
+ Benchmark::measure('vars loaded');
+ foreach ($vars as $var) {
+ if (! array_key_exists($var->get('host_id'), $flat)) {
+ // Templates?
+ continue;
+ }
+ $flat[$var->get('host_id')]->{'vars.' . $var->get('varname')} = $var->get('varvalue');
+ }
+ Benchmark::measure('vars done');
+
+ foreach ($flat as $host) {
+ if ($filter->matches($host)) {
+ echo $host->object_name . "\n";
+ }
+ }
+ }
+}
diff --git a/application/clicommands/CommandCommand.php b/application/clicommands/CommandCommand.php
new file mode 100644
index 0000000..5c96442
--- /dev/null
+++ b/application/clicommands/CommandCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Commands
+ *
+ * Use this command to show, create, modify or delete Icinga Command
+ * objects
+ */
+class CommandCommand extends ObjectCommand
+{
+}
diff --git a/application/clicommands/CommandsCommand.php b/application/clicommands/CommandsCommand.php
new file mode 100644
index 0000000..9a74337
--- /dev/null
+++ b/application/clicommands/CommandsCommand.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectsCommand;
+
+/**
+ * List Icinga Commands
+ *
+ * Use this command to list Icinga Command objects
+ */
+class CommandsCommand extends ObjectsCommand
+{
+}
diff --git a/application/clicommands/ConfigCommand.php b/application/clicommands/ConfigCommand.php
new file mode 100644
index 0000000..e313aa4
--- /dev/null
+++ b/application/clicommands/ConfigCommand.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Application\Benchmark;
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\Deployment\ConditionalDeployment;
+use Icinga\Module\Director\Deployment\DeploymentGracePeriod;
+use Icinga\Module\Director\Deployment\DeploymentStatus;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Import\SyncUtils;
+
+/**
+ * Generate, show and deploy Icinga 2 configuration
+ */
+class ConfigCommand extends Command
+{
+ /**
+ * Re-render the current configuration
+ */
+ public function renderAction()
+ {
+ $profile = $this->params->shift('profile');
+ if ($profile) {
+ $this->enableDbProfiler();
+ }
+
+ $config = new IcingaConfig($this->db());
+ Benchmark::measure('Rendering config');
+ if ($config->hasBeenModified()) {
+ Benchmark::measure('Config rendered, storing to db');
+ $config->store();
+ Benchmark::measure('All done');
+ $checksum = $config->getHexChecksum();
+ printf(
+ "New config with checksum %s has been generated\n",
+ $checksum
+ );
+ } else {
+ $checksum = $config->getHexChecksum();
+ printf(
+ "Config with checksum %s already exists\n",
+ $checksum
+ );
+ }
+
+ if ($profile) {
+ $this->dumpDbProfile();
+ }
+ }
+
+ protected function dumpDbProfile()
+ {
+ $profiler = $this->getDbProfiler();
+
+ $totalTime = $profiler->getTotalElapsedSecs();
+ $queryCount = $profiler->getTotalNumQueries();
+ $longestTime = 0;
+ $longestQuery = null;
+
+ /** @var \Zend_Db_Profiler_Query $query */
+ foreach ($profiler->getQueryProfiles() as $query) {
+ echo $query->getQuery() . "\n";
+ if ($query->getElapsedSecs() > $longestTime) {
+ $longestTime = $query->getElapsedSecs();
+ $longestQuery = $query->getQuery();
+ }
+ }
+
+ echo 'Executed ' . $queryCount . ' queries in ' . $totalTime . ' seconds' . "\n";
+ echo 'Average query length: ' . $totalTime / $queryCount . ' seconds' . "\n";
+ echo 'Queries per second: ' . $queryCount / $totalTime . "\n";
+ echo 'Longest query length: ' . $longestTime . "\n";
+ echo "Longest query: \n" . $longestQuery . "\n";
+ }
+
+ protected function getDbProfiler()
+ {
+ return $this->db()->getDbAdapter()->getProfiler();
+ }
+
+ protected function enableDbProfiler()
+ {
+ return $this->getDbProfiler()->setEnabled(true);
+ }
+
+ /**
+ * Deploy the current configuration
+ *
+ * USAGE
+ *
+ * icingacli director config deploy [--checksum <checksum>] [--force] [--wait <seconds>]
+ * [--grace-period <seconds>]
+ *
+ * OPTIONS
+ *
+ * --checksum <checksum> Optionally deploy a specific configuration
+ * --force Force a deployment, even when the configuration
+ * hasn't changed
+ * --wait <seconds> Optionally wait until Icinga completed it's
+ * restart
+ * --grace-period <seconds> Do not deploy if a deployment took place
+ * less than <seconds> ago
+ */
+ public function deployAction()
+ {
+ $db = $this->db();
+
+ $checksum = $this->params->get('checksum');
+ if ($checksum) {
+ $config = IcingaConfig::load(hex2bin($checksum), $db);
+ } else {
+ $config = IcingaConfig::generate($db);
+ $checksum = $config->getHexChecksum();
+ }
+
+ $deployer = new ConditionalDeployment($db, $this->api());
+ $deployer->force((bool) $this->params->get('force'));
+ if ($graceTime = $this->params->get('grace-period')) {
+ $deployer->setGracePeriod(new DeploymentGracePeriod((int) $graceTime, $db));
+ if ($this->params->get('force')) {
+ fwrite(STDERR, "WARNING: force overrides Grace period\n");
+ }
+ }
+ $deployer->refresh();
+
+ if ($deployment = $deployer->deploy($config)) {
+ if ($deployer->hasBeenForced()) {
+ echo $deployer->getNoDeploymentReason() . ", deploying anyway\n";
+ }
+ printf("Config '%s' has been deployed\n", $checksum);
+ } else {
+ echo $deployer->getNoDeploymentReason() . "\n";
+ return;
+ }
+
+ if ($timeout = $this->getWaitTime()) {
+ $deployed = $deployer->waitForStartupAfterDeploy($deployment, $timeout);
+ if ($deployed !== true) {
+ $this->fail("Waiting for Icinga restart failed '%s': %s\n", $checksum, $deployed);
+ }
+ }
+ }
+
+ /**
+ * Checks the deployments status
+ */
+ public function deploymentstatusAction()
+ {
+ $db = $this->db();
+ $api = $this->api();
+ $status = new DeploymentStatus($db, $api);
+ $result = $status->getDeploymentStatus($this->params->get('configs'), $this->params->get('activities'));
+ if ($key = $this->params->get('key')) {
+ $result = SyncUtils::getSpecificValue($result, $key);
+ }
+
+ if (is_string($result)) {
+ echo "$result\n";
+ } else {
+ echo Json::encode($result, JSON_PRETTY_PRINT) . "\n";
+ }
+ }
+
+ protected function getWaitTime()
+ {
+ if ($timeout = $this->params->get('wait')) {
+ if (!ctype_digit($timeout)) {
+ $this->fail("--wait must be the number of seconds to wait'");
+ }
+
+ return (int) $timeout;
+ }
+
+ return null;
+ }
+}
diff --git a/application/clicommands/CoreCommand.php b/application/clicommands/CoreCommand.php
new file mode 100644
index 0000000..4927aa5
--- /dev/null
+++ b/application/clicommands/CoreCommand.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\PlainObjectRenderer;
+
+class CoreCommand extends Command
+{
+ public function constantsAction()
+ {
+ foreach ($this->api()->getConstants() as $name => $value) {
+ printf("const %s = %s\n", $name, PlainObjectRenderer::render($value));
+ }
+ }
+}
diff --git a/application/clicommands/DaemonCommand.php b/application/clicommands/DaemonCommand.php
new file mode 100644
index 0000000..e89e1da
--- /dev/null
+++ b/application/clicommands/DaemonCommand.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\Daemon\BackgroundDaemon;
+
+class DaemonCommand extends Command
+{
+ /**
+ * Run the main Director daemon
+ *
+ * USAGE
+ *
+ * icingacli director daemon run [--db-resource <name>]
+ */
+ public function runAction()
+ {
+ $this->app->getModuleManager()->loadEnabledModules();
+ $daemon = new BackgroundDaemon();
+ if ($dbResource = $this->params->get('db-resource')) {
+ $daemon->setDbResourceName($dbResource);
+ }
+ $daemon->run();
+ }
+}
diff --git a/application/clicommands/DependencyCommand.php b/application/clicommands/DependencyCommand.php
new file mode 100644
index 0000000..ff5cbdc
--- /dev/null
+++ b/application/clicommands/DependencyCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Dependencies
+ *
+ * Use this command to show, create, modify or delete Icinga Dependency
+ * objects
+ */
+class DependencyCommand extends ObjectCommand
+{
+}
diff --git a/application/clicommands/EndpointCommand.php b/application/clicommands/EndpointCommand.php
new file mode 100644
index 0000000..f61f4fc
--- /dev/null
+++ b/application/clicommands/EndpointCommand.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Endpoints
+ *
+ * Use this command to show, create, modify or delete Icinga Endpoint
+ * objects
+ */
+class EndpointCommand extends ObjectCommand
+{
+ public function statusAction()
+ {
+ print_r($this->api()->getStatus());
+ }
+}
diff --git a/application/clicommands/ExportCommand.php b/application/clicommands/ExportCommand.php
new file mode 100644
index 0000000..2b2119d
--- /dev/null
+++ b/application/clicommands/ExportCommand.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\DirectorObject\Automation\ImportExport;
+
+/**
+ * Export Director Config Objects
+ */
+class ExportCommand extends Command
+{
+ /**
+ * Export all ImportSource definitions
+ *
+ * USAGE
+ *
+ * icingacli director export importsources [options]
+ *
+ * OPTIONS
+ *
+ * --no-pretty JSON is pretty-printed per default
+ * Use this flag to enforce unformatted JSON
+ */
+ public function importsourcesAction()
+ {
+ $export = new ImportExport($this->db());
+ echo $this->renderJson(
+ $export->serializeAllImportSources(),
+ !$this->params->shift('no-pretty')
+ );
+ }
+
+ /**
+ * Export all SyncRule definitions
+ *
+ * USAGE
+ *
+ * icingacli director export syncrules [options]
+ *
+ * OPTIONS
+ *
+ * --no-pretty JSON is pretty-printed per default
+ * Use this flag to enforce unformatted JSON
+ */
+ public function syncrulesAction()
+ {
+ $export = new ImportExport($this->db());
+ echo $this->renderJson(
+ $export->serializeAllSyncRules(),
+ !$this->params->shift('no-pretty')
+ );
+ }
+
+ /**
+ * Export all Job definitions
+ *
+ * USAGE
+ *
+ * icingacli director export jobs [options]
+ *
+ * OPTIONS
+ *
+ * --no-pretty JSON is pretty-printed per default
+ * Use this flag to enforce unformatted JSON
+ */
+ public function jobsAction()
+ {
+ $export = new ImportExport($this->db());
+ echo $this->renderJson(
+ $export->serializeAllJobs(),
+ !$this->params->shift('no-pretty')
+ );
+ }
+
+ /**
+ * Export all DataField definitions
+ *
+ * USAGE
+ *
+ * icingacli director export datafields [options]
+ *
+ * OPTIONS
+ *
+ * --no-pretty JSON is pretty-printed per default
+ * Use this flag to enforce unformatted JSON
+ */
+ public function datafieldsAction()
+ {
+ $export = new ImportExport($this->db());
+ echo $this->renderJson(
+ $export->serializeAllDataFields(),
+ !$this->params->shift('no-pretty')
+ );
+ }
+
+ /**
+ * Export all DataList definitions
+ *
+ * USAGE
+ *
+ * icingacli director export datalists [options]
+ *
+ * OPTIONS
+ *
+ * --no-pretty JSON is pretty-printed per default
+ * Use this flag to enforce unformatted JSON
+ */
+ public function datalistsAction()
+ {
+ $export = new ImportExport($this->db());
+ echo $this->renderJson(
+ $export->serializeAllDataLists(),
+ !$this->params->shift('no-pretty')
+ );
+ }
+
+ // /**
+ // * Export all IcingaHostGroup definitions
+ // *
+ // * USAGE
+ // *
+ // * icingacli director export hostgroup [options]
+ // *
+ // * OPTIONS
+ // *
+ // * --no-pretty JSON is pretty-printed per default
+ // * Use this flag to enforce unformatted JSON
+ // */
+ // public function hostgroupAction()
+ // {
+ // $export = new ImportExport($this->db());
+ // echo $this->renderJson(
+ // $export->serializeAllHostGroups(),
+ // !$this->params->shift('no-pretty')
+ // );
+ // }
+ //
+ // /**
+ // * Export all IcingaServiceGroup definitions
+ // *
+ // * USAGE
+ // *
+ // * icingacli director export servicegroup [options]
+ // *
+ // * OPTIONS
+ // *
+ // * --no-pretty JSON is pretty-printed per default
+ // * Use this flag to enforce unformatted JSON
+ // */
+ // public function servicegroupAction()
+ // {
+ // $export = new ImportExport($this->db());
+ // echo $this->renderJson(
+ // $export->serializeAllServiceGroups(),
+ // !$this->params->shift('no-pretty')
+ // );
+ // }
+
+ /**
+ * Export all IcingaTemplateChoiceHost definitions
+ *
+ * USAGE
+ *
+ * icingacli director export hosttemplatechoices [options]
+ *
+ * OPTIONS
+ *
+ * --no-pretty JSON is pretty-printed per default
+ * Use this flag to enforce unformatted JSON
+ */
+ public function hosttemplatechoicesAction()
+ {
+ $export = new ImportExport($this->db());
+ echo $this->renderJson(
+ $export->serializeAllHostTemplateChoices(),
+ !$this->params->shift('no-pretty')
+ );
+ }
+}
diff --git a/application/clicommands/HealthCommand.php b/application/clicommands/HealthCommand.php
new file mode 100644
index 0000000..1635c50
--- /dev/null
+++ b/application/clicommands/HealthCommand.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\CheckPlugin\PluginState;
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\Health;
+use Icinga\Module\Director\Cli\PluginOutputBeautifier;
+
+/**
+ * Check Icinga Director Health
+ *
+ * Use this command as a CheckPlugin to monitor your Icinga Director health
+ */
+class HealthCommand extends Command
+{
+ /**
+ * Run health checks
+ *
+ * Use this command to run all or a specific set of Health Checks.
+ *
+ * USAGE
+ *
+ * icingacli director health check [options]
+ *
+ * OPTIONS
+ *
+ * --check <name> Run only a specific set of checks
+ * valid names: config, sync, import, jobs, deployment
+ * --db <name> Use a specific Icinga Web DB resource
+ * --watch <seconds> Refresh every <second>. For interactive use only
+ */
+ public function checkAction()
+ {
+ $health = new Health();
+ if ($name = $this->params->get('db')) {
+ $health->setDbResourceName($name);
+ }
+
+ if ($name = $this->params->get('check')) {
+ $check = $health->getCheck($name);
+ echo PluginOutputBeautifier::beautify($check->getOutput(), $this->screen);
+
+ exit($check->getState()->getNumeric());
+ } else {
+ $state = new PluginState('OK');
+ $checks = $health->getAllChecks();
+
+ $output = [];
+ foreach ($checks as $check) {
+ $state->raise($check->getState());
+ $output[] = $check->getOutput();
+ }
+
+ if ($state->getNumeric() === 0) {
+ echo "Icinga Director: everything is fine\n\n";
+ } else {
+ echo "Icinga Director: there are problems\n\n";
+ }
+
+ $out = PluginOutputBeautifier::beautify(implode("\n", $output), $this->screen);
+ echo $out;
+
+ if (! $this->isBeingWatched()) {
+ exit($state->getNumeric());
+ }
+ }
+ }
+
+ /**
+ * Cli should provide this information, as it shifts the parameter
+ *
+ * @return bool
+ */
+ protected function isBeingWatched()
+ {
+ global $argv;
+ return in_array('--watch', $argv);
+ }
+}
diff --git a/application/clicommands/HostCommand.php b/application/clicommands/HostCommand.php
new file mode 100644
index 0000000..21ec5eb
--- /dev/null
+++ b/application/clicommands/HostCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Hosts
+ *
+ * Use this command to show, create, modify or delete Icinga Host
+ * objects
+ */
+class HostCommand extends ObjectCommand
+{
+}
diff --git a/application/clicommands/HostgroupCommand.php b/application/clicommands/HostgroupCommand.php
new file mode 100644
index 0000000..88b17d9
--- /dev/null
+++ b/application/clicommands/HostgroupCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Hostgroups
+ *
+ * Use this command to show, create, modify or delete Icinga Hostgroups
+ * objects
+ */
+class HostGroupCommand extends ObjectCommand
+{
+}
diff --git a/application/clicommands/HostgroupsCommand.php b/application/clicommands/HostgroupsCommand.php
new file mode 100644
index 0000000..1007a05
--- /dev/null
+++ b/application/clicommands/HostgroupsCommand.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectsCommand;
+
+/**
+ * Manage Icinga Hostgroups
+ *
+ * Use this command to list Icinga Hostgroup objects
+ */
+class HostgroupsCommand extends ObjectsCommand
+{
+}
diff --git a/application/clicommands/HostsCommand.php b/application/clicommands/HostsCommand.php
new file mode 100644
index 0000000..3008284
--- /dev/null
+++ b/application/clicommands/HostsCommand.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectsCommand;
+
+/**
+ * Manage Icinga Hosts
+ *
+ * Use this command to list Icinga Host objects
+ */
+class HostsCommand extends ObjectsCommand
+{
+}
diff --git a/application/clicommands/HousekeepingCommand.php b/application/clicommands/HousekeepingCommand.php
new file mode 100644
index 0000000..974e28d
--- /dev/null
+++ b/application/clicommands/HousekeepingCommand.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Exception\MissingParameterException;
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\Db\Housekeeping;
+use Icinga\Module\Director\Db\MembershipHousekeeping;
+
+class HousekeepingCommand extends Command
+{
+ protected $housekeeping;
+
+ public function tasksAction()
+ {
+ if ($pending = $this->params->shift('pending')) {
+ $tasks = $this->housekeeping()->getPendingTaskSummary();
+ } else {
+ $tasks = $this->housekeeping()->getTaskSummary();
+ }
+
+ $len = array_reduce(
+ $tasks,
+ function ($max, $task) {
+ return max(
+ $max,
+ strlen($task->title) + strlen($task->name) + 3
+ );
+ }
+ );
+
+ if (count($tasks)) {
+ print "\n";
+ printf(" %-" . $len . "s | %s\n", 'Housekeeping task (name)', 'Count');
+ printf("-%-" . $len . "s-|-------\n", str_repeat('-', $len));
+ }
+
+ foreach ($tasks as $task) {
+ printf(
+ " %-" . $len . "s | %5d\n",
+ sprintf('%s (%s)', $task->title, $task->name),
+ $task->count
+ );
+ }
+
+ if (count($tasks)) {
+ print "\n";
+ }
+ }
+
+ public function runAction()
+ {
+ if (!$job = $this->params->shift()) {
+ throw new MissingParameterException(
+ 'Job is required, say ALL to run all pending jobs'
+ );
+ }
+
+ if ($job === 'ALL') {
+ $this->housekeeping()->runAllTasks();
+ } else {
+ $this->housekeeping()->runTask($job);
+ }
+ }
+
+ protected function housekeeping()
+ {
+ if ($this->housekeeping === null) {
+ $this->housekeeping = new Housekeeping($this->db());
+ }
+
+ return $this->housekeeping;
+ }
+}
diff --git a/application/clicommands/ImportCommand.php b/application/clicommands/ImportCommand.php
new file mode 100644
index 0000000..3edfff2
--- /dev/null
+++ b/application/clicommands/ImportCommand.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\DirectorObject\Automation\ImportExport;
+use Icinga\Module\Director\Objects\ImportSource;
+
+/**
+ * Export Director Config Objects
+ */
+class ImportCommand extends Command
+{
+ /**
+ * Import ImportSource definitions
+ *
+ * USAGE
+ *
+ * icingacli director import importsources < importsources.json
+ *
+ * OPTIONS
+ */
+ public function importsourcesAction()
+ {
+ $json = file_get_contents('php://stdin');
+ $import = new ImportExport($this->db());
+ $count = $import->unserializeImportSources(json_decode($json));
+ echo "$count Import Sources have been imported\n";
+ }
+
+ // /**
+ // * Import an ImportSource definition
+ // *
+ // * USAGE
+ // *
+ // * icingacli director import importsource < importsource.json
+ // *
+ // * OPTIONS
+ // */
+ // public function importsourcection()
+ // {
+ // $json = file_get_contents('php://stdin');
+ // $object = ImportSource::import(json_decode($json), $this->db());
+ // $object->store();
+ // printf("Import Source '%s' has been imported\n", $object->getObjectName());
+ // }
+
+ /**
+ * Import SyncRule definitions
+ *
+ * USAGE
+ *
+ * icingacli director import syncrules < syncrules.json
+ */
+ public function syncrulesAction()
+ {
+ $json = file_get_contents('php://stdin');
+ $import = new ImportExport($this->db());
+ $count = $import->unserializeSyncRules(json_decode($json));
+ echo "$count Sync Rules have been imported\n";
+ }
+}
diff --git a/application/clicommands/ImportsourceCommand.php b/application/clicommands/ImportsourceCommand.php
new file mode 100644
index 0000000..477fdf5
--- /dev/null
+++ b/application/clicommands/ImportsourceCommand.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Application\Benchmark;
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\ImportSource;
+
+/**
+ * Deal with Director Import Sources
+ *
+ * Use this command to check or trigger your defined Import Sources
+ */
+class ImportsourceCommand extends Command
+{
+ /**
+ * List defined Import Sources
+ *
+ * This shows a table with your defined Import Sources, their IDs and
+ * current state. As triggering Imports requires an ID, this is where
+ * you can look up the desired ID.
+ *
+ * USAGE
+ *
+ * icingacli director importsource list
+ */
+ public function listAction()
+ {
+ $sources = ImportSource::loadAll($this->db());
+ if (empty($sources)) {
+ echo "No Import Source has been defined\n";
+
+ return;
+ }
+
+ printf("%4s | %s\n", 'ID', 'Import Source name');
+ printf("-----+%s\n", str_repeat('-', 64));
+
+ foreach ($sources as $source) {
+ $state = $source->get('import_state');
+ printf("%4d | %s\n", $source->get('id'), $source->get('source_name'));
+ printf(" | -> %s%s\n", $state, $state === 'failing' ? ': ' . $source->get('last_error_message') : '');
+ }
+ }
+
+ /**
+ * Check a given Import Source for changes
+ *
+ * This command fetches data from the given Import Source and compares it
+ * to the most recently imported data.
+ *
+ * USAGE
+ *
+ * icingacli director importsource check --id <id>
+ *
+ * OPTIONS
+ *
+ * --id <id> An Import Source ID. Use the list command to figure out
+ * --benchmark Show timing and memory usage details
+ */
+ public function checkAction()
+ {
+ $source = $this->getImportSource();
+ $source->checkForChanges();
+ $this->showImportStateDetails($source);
+ }
+
+ /**
+ * Fetch current data from a given Import Source
+ *
+ * This command fetches data from the given Import Source and outputs
+ * them as plain JSON
+ *
+ * USAGE
+ *
+ * icingacli director importsource fetch --id <id>
+ *
+ * OPTIONS
+ *
+ * --id <id> An Import Source ID. Use the list command to figure out
+ * --benchmark Show timing and memory usage details
+ */
+ public function fetchAction()
+ {
+ $source = $this->getImportSource();
+ $source->checkForChanges();
+ $hook = ImportSourceHook::forImportSource($source);
+ Benchmark::measure('Ready to fetch data');
+ $data = $hook->fetchData();
+ $source->applyModifiers($data);
+ Benchmark::measure(sprintf('Got %d rows, ready to dump JSON', count($data)));
+ echo Json::encode($data, JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * Trigger an Import Run for a given Import Source
+ *
+ * This command fetches data from the given Import Source and stores it to
+ * the Director DB, so that the next related Sync Rule run can work with
+ * fresh data. In case data didn't change, nothing is going to be stored.
+ *
+ * USAGE
+ *
+ * icingacli director importsource run --id <id>
+ *
+ * OPTIONS
+ *
+ * --id <id> An Import Source ID. Use the list command to figure out
+ * --benchmark Show timing and memory usage details
+ */
+ public function runAction()
+ {
+ $source = $this->getImportSource();
+
+ if ($source->runImport()) {
+ print "New data has been imported\n";
+ $this->showImportStateDetails($source);
+ } else {
+ print "Nothing has been changed, imported data is still up to date\n";
+ }
+ }
+
+ /**
+ * @return ImportSource
+ */
+ protected function getImportSource()
+ {
+ return ImportSource::loadWithAutoIncId(
+ (int) $this->params->getRequired('id'),
+ $this->db()
+ );
+ }
+
+ /**
+ * @param ImportSource $source
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function showImportStateDetails(ImportSource $source)
+ {
+ echo $this->getImportStateDescription($source) . "\n";
+ }
+
+ /**
+ * @param ImportSource $source
+ * @return string
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function getImportStateDescription(ImportSource $source)
+ {
+ switch ($source->get('import_state')) {
+ case 'unknown':
+ return "It's currently unknown whether we are in sync with this"
+ . ' Import Source. You should either check for changes or'
+ . ' trigger a new Import Run.';
+ case 'in-sync':
+ return 'This Import Source is in sync';
+ case 'pending-changes':
+ return 'There are pending changes for this Import Source. You'
+ . ' should trigger a new Import Run.';
+ case 'failing':
+ return 'This Import Source failed: ' . $source->get('last_error_message');
+ default:
+ return 'This Import Source has an invalid state: ' . $source->get('import_state');
+ }
+ }
+}
diff --git a/application/clicommands/JobsCommand.php b/application/clicommands/JobsCommand.php
new file mode 100644
index 0000000..1c6297f
--- /dev/null
+++ b/application/clicommands/JobsCommand.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Exception;
+use gipfl\Cli\Process;
+use gipfl\Protocol\JsonRpc\Connection;
+use gipfl\Protocol\NetString\StreamWrapper;
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\Daemon\JsonRpcLogWriter as JsonRpcLogWriterAlias;
+use Icinga\Module\Director\Daemon\Logger;
+use Icinga\Module\Director\Objects\DirectorJob;
+use React\EventLoop\Factory as Loop;
+use React\EventLoop\LoopInterface;
+use React\Stream\ReadableResourceStream;
+use React\Stream\WritableResourceStream;
+
+class JobsCommand extends Command
+{
+ public function runAction()
+ {
+ $this->app->getModuleManager()->loadEnabledModules();
+ $loop = Loop::create();
+ if ($this->params->get('rpc')) {
+ $this->enableRpc($loop);
+ }
+ if ($this->params->get('rpc') && $jobId = $this->params->get('id')) {
+ $exitCode = 1;
+ $jobId = (int) $jobId;
+ $loop->futureTick(function () use ($jobId, $loop, &$exitCode) {
+ Process::setTitle('icinga::director::job');
+ try {
+ $this->raiseLimits();
+ $job = DirectorJob::loadWithAutoIncId($jobId, $this->db());
+ Process::setTitle('icinga::director::job (' . $job->get('job_name') . ')');
+ if ($job->run()) {
+ $exitCode = 0;
+ } else {
+ $exitCode = 1;
+ }
+ } catch (Exception $e) {
+ Logger::error($e->getMessage());
+ $exitCode = 1;
+ }
+ $loop->futureTick(function () use ($loop) {
+ $loop->stop();
+ });
+ });
+ } else {
+ Logger::error('This command is no longer available. Please check our Upgrading documentation');
+ $exitCode = 1;
+ }
+
+ $loop->run();
+ exit($exitCode);
+ }
+
+ protected function enableRpc(LoopInterface $loop)
+ {
+ // stream_set_blocking(STDIN, 0);
+ // stream_set_blocking(STDOUT, 0);
+ // print_r(stream_get_meta_data(STDIN));
+ // stream_set_write_buffer(STDOUT, 0);
+ // ini_set('implicit_flush', 1);
+ $netString = new StreamWrapper(
+ new ReadableResourceStream(STDIN, $loop),
+ new WritableResourceStream(STDOUT, $loop)
+ );
+ $jsonRpc = new Connection();
+ $jsonRpc->handle($netString);
+
+ Logger::replaceRunningInstance(new JsonRpcLogWriterAlias($jsonRpc));
+ }
+}
diff --git a/application/clicommands/KickstartCommand.php b/application/clicommands/KickstartCommand.php
new file mode 100644
index 0000000..80aa183
--- /dev/null
+++ b/application/clicommands/KickstartCommand.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\KickstartHelper;
+
+/**
+ * Kickstart a Director installation
+ *
+ * Once you prepared your DB resource this command retrieves information about
+ * unapplied database migration and helps applying them.
+ */
+class KickstartCommand extends Command
+{
+ /**
+ * Check whether a kickstart run is required
+ *
+ * This is the case when there is a kickstart.ini in your Directors config
+ * directory and no ApiUser in your Director DB.
+ *
+ * This is mostly for automation, so one could create a Puppet manifest
+ * as follows:
+ *
+ * exec { 'Icinga Director Kickstart':
+ * path => '/usr/local/bin:/usr/bin:/bin',
+ * command => 'icingacli director kickstart run',
+ * onlyif => 'icingacli director kickstart required',
+ * require => Exec['Icinga Director DB migration'],
+ * }
+ *
+ * Exit code 0 means that a kickstart run is required, code 2 that it is
+ * not.
+ */
+ public function requiredAction()
+ {
+ if ($this->kickstart()->isConfigured()) {
+ if ($this->kickstart()->isRequired()) {
+ if ($this->isVerbose) {
+ echo "Kickstart has been configured and should be triggered\n";
+ }
+
+ exit(0);
+ } else {
+ echo "Kickstart configured, execution is not required\n";
+ exit(1);
+ }
+ } else {
+ echo "Kickstart has not been configured\n";
+ exit(2);
+ }
+ }
+
+ /**
+ * Trigger the kickstart helper
+ *
+ * This will connect to the endpoint configured in your kickstart.ini,
+ * store the given API user and import existing objects like zones,
+ * endpoints and commands.
+ *
+ * /etc/icingaweb2/modules/director/kickstart.ini could look as follows:
+ *
+ * [config]
+ * endpoint = "master-node.example.com"
+ *
+ * ; Host can be an IP address or a hostname. Equals to endpoint name
+ * ; if not set:
+ * host = "127.0.0.1"
+ *
+ * ; Port is 5665 if none given
+ * port = 5665
+ *
+ * username = "director"
+ * password = "***"
+ *
+ */
+ public function runAction()
+ {
+ $this->raiseLimits();
+ $this->kickstart()->loadConfigFromFile()->run();
+ exit(0);
+ }
+
+ protected function kickstart()
+ {
+ return new KickstartHelper($this->db());
+ }
+}
diff --git a/application/clicommands/MigrationCommand.php b/application/clicommands/MigrationCommand.php
new file mode 100644
index 0000000..6a4d002
--- /dev/null
+++ b/application/clicommands/MigrationCommand.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\Db\Migrations;
+
+/**
+ * Handle DB migrations
+ *
+ * This command retrieves information about unapplied database migration and
+ * helps applying them.
+ */
+class MigrationCommand extends Command
+{
+ /**
+ * Check whether there are pending migrations
+ *
+ * This is mostly for automation, so one could create a Puppet manifest
+ * as follows:
+ *
+ * exec { 'Icinga Director DB migration':
+ * command => 'icingacli director migration run',
+ * onlyif => 'icingacli director migration pending',
+ * }
+ *
+ * Exit code 0 means that there are pending migrations, code 1 that there
+ * are no such. Use --verbose for human readable output
+ */
+ public function pendingAction()
+ {
+ if ($count = $this->migrations()->countPendingMigrations()) {
+ if ($this->isVerbose) {
+ if ($count === 1) {
+ echo "There is 1 pending migration\n";
+ } else {
+ printf("There are %d pending migrations\n", $count);
+ }
+ }
+
+ exit(0);
+ } else {
+ if ($this->isVerbose) {
+ echo "There are no pending migrations\n";
+ }
+
+ exit(1);
+ }
+ }
+
+ /**
+ * Run any pending migrations
+ *
+ * All pending migrations will be silently applied
+ */
+ public function runAction()
+ {
+ $this->migrations()->applyPendingMigrations();
+ exit(0);
+ }
+
+ protected function migrations()
+ {
+ return new Migrations($this->db());
+ }
+}
diff --git a/application/clicommands/NotificationCommand.php b/application/clicommands/NotificationCommand.php
new file mode 100644
index 0000000..bb5402a
--- /dev/null
+++ b/application/clicommands/NotificationCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Notifications
+ *
+ * Use this command to show, create, modify or delete Icinga Notification
+ * objects
+ */
+class NotificationCommand extends ObjectCommand
+{
+}
diff --git a/application/clicommands/ServiceCommand.php b/application/clicommands/ServiceCommand.php
new file mode 100644
index 0000000..1bd21e7
--- /dev/null
+++ b/application/clicommands/ServiceCommand.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Cli\Params;
+use Icinga\Module\Director\Cli\ObjectCommand;
+use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Resolver\OverrideHelper;
+use InvalidArgumentException;
+
+/**
+ * Manage Icinga Services
+ *
+ * Use this command to show, create, modify or delete Icinga Service
+ * objects
+ */
+class ServiceCommand extends ObjectCommand
+{
+ public function setAction()
+ {
+ if (($host = $this->params->get('host')) && $this->params->shift('allow-overrides')) {
+ if ($this->setServiceProperties($host)) {
+ return;
+ }
+ }
+
+ parent::setAction();
+ }
+
+ protected function setServiceProperties($hostname)
+ {
+ $serviceName = $this->getName();
+ $host = IcingaHost::load($hostname, $this->db());
+ $service = ServiceFinder::find($host, $serviceName);
+ if ($service->requiresOverrides()) {
+ self::checkForOverrideSafety($this->params);
+ $properties = $this->remainingParams();
+ unset($properties['host']);
+ OverrideHelper::applyOverriddenVars($host, $serviceName, $properties);
+ $this->persistChanges($host, 'Host', $hostname . " (Overrides for $serviceName)", 'modified');
+ return true;
+ }
+
+ return false;
+ }
+
+ protected static function checkForOverrideSafety(Params $params)
+ {
+ if ($params->shift('replace')) {
+ throw new InvalidArgumentException('--replace is not available for Variable Overrides');
+ }
+ $appends = self::stripPrefixedProperties($params, 'append-');
+ $remove = self::stripPrefixedProperties($params, 'remove-');
+ OverrideHelper::assertVarsForOverrides($appends);
+ OverrideHelper::assertVarsForOverrides($remove);
+ if (!empty($appends)) {
+ throw new InvalidArgumentException('--append- is not available for Variable Overrides');
+ }
+ if (!empty($remove)) {
+ throw new InvalidArgumentException('--remove- is not available for Variable Overrides');
+ }
+ // Alternative, untested:
+ // $this->appendToArrayProperties($object, $appends);
+ // $this->removeProperties($object, $remove);
+ }
+
+ protected function load($name)
+ {
+ return parent::load($this->makeServiceKey($name));
+ }
+
+ protected function exists($name)
+ {
+ return parent::exists($this->makeServiceKey($name));
+ }
+
+ protected function makeServiceKey($name)
+ {
+ if ($host = $this->params->get('host')) {
+ return [
+ 'object_name' => $name,
+ 'host_id' => IcingaHost::load($host, $this->db())->get('id'),
+ ];
+ } else {
+ return [
+ 'object_name' => $name,
+ 'object_type' => 'template',
+ ];
+ }
+ }
+}
diff --git a/application/clicommands/ServicegroupCommand.php b/application/clicommands/ServicegroupCommand.php
new file mode 100644
index 0000000..1c732d4
--- /dev/null
+++ b/application/clicommands/ServicegroupCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Servicegroups
+ *
+ * Use this command to show, create, modify or delete Icinga Servicegroups
+ * objects
+ */
+class ServiceGroupCommand extends ObjectCommand
+{
+}
diff --git a/application/clicommands/ServicesetCommand.php b/application/clicommands/ServicesetCommand.php
new file mode 100644
index 0000000..648a42c
--- /dev/null
+++ b/application/clicommands/ServicesetCommand.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+/**
+ * Manage Icinga Service Sets
+ *
+ * Use this command to show, create, modify or delete Icinga Service
+ * objects
+ */
+class ServicesetCommand extends ServiceCommand
+{
+ protected $type = 'ServiceSet';
+}
diff --git a/application/clicommands/ServicesetsCommand.php b/application/clicommands/ServicesetsCommand.php
new file mode 100644
index 0000000..54669d5
--- /dev/null
+++ b/application/clicommands/ServicesetsCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectsCommand;
+
+/**
+ * Manage Icinga Service Sets
+ *
+ * Use this command to list Icinga Service Set objects
+ */
+class ServicesetsCommand extends ObjectsCommand
+{
+ protected $type = 'ServiceSet';
+}
diff --git a/application/clicommands/SyncruleCommand.php b/application/clicommands/SyncruleCommand.php
new file mode 100644
index 0000000..37a3f0e
--- /dev/null
+++ b/application/clicommands/SyncruleCommand.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\Command;
+use Icinga\Module\Director\Objects\DirectorActivityLog;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\SyncRule;
+use RuntimeException;
+
+/**
+ * Deal with Director Sync Rules
+ *
+ * Use this command to check or trigger your defined Sync Rules
+ */
+class SyncruleCommand extends Command
+{
+ /**
+ * List defined Sync Rules
+ *
+ * This shows a table with your defined Sync Rules, their IDs and
+ * current state. As triggering a Sync requires an ID, this is where
+ * you can look up the desired ID.
+ *
+ * USAGE
+ *
+ * icingacli director syncrule list
+ */
+ public function listAction()
+ {
+ $rules = SyncRule::loadAll($this->db());
+ if (empty($rules)) {
+ echo "No Sync Rule has been defined\n";
+
+ return;
+ }
+
+ printf("%4s | %s\n", 'ID', 'Sync Rule name');
+ printf("-----+%s\n", str_repeat('-', 64));
+
+ foreach ($rules as $rule) {
+ $state = $rule->get('sync_state');
+ printf("%4d | %s\n", $rule->get('id'), $rule->get('rule_name'));
+ printf(" | -> %s%s\n", $state, $state === 'failing' ? ': ' . $rule->get('last_error_message') : '');
+ }
+ }
+
+ /**
+ * Check a given Sync Rule for changes
+ *
+ * This command runs a complete Sync in memory but doesn't persist eventual changes.
+ *
+ * USAGE
+ *
+ * icingacli director syncrule check --id <id>
+ *
+ * OPTIONS
+ *
+ * --id <id> A Sync Rule ID. Use the list command to figure out
+ * --benchmark Show timing and memory usage details
+ */
+ public function checkAction()
+ {
+ $rule = $this->getSyncRule();
+ $hasChanges = $rule->checkForChanges();
+ $this->showSyncStateDetails($rule);
+ if ($hasChanges) {
+ $mods = $this->getExpectedModificationCounts($rule);
+ printf(
+ "Expected modifications: %dx create, %dx modify, %dx delete\n",
+ $mods->modify,
+ $mods->create,
+ $mods->delete
+ );
+ }
+
+ exit($this->getSyncStateExitCode($rule));
+ }
+
+ protected function getExpectedModificationCounts(SyncRule $rule)
+ {
+ $modifications = $rule->getExpectedModifications();
+
+ $create = 0;
+ $modify = 0;
+ $delete = 0;
+
+ /** @var IcingaObject $object */
+ foreach ($modifications as $object) {
+ if ($object->hasBeenLoadedFromDb()) {
+ if ($object->shouldBeRemoved()) {
+ $delete++;
+ } else {
+ $modify++;
+ }
+ } else {
+ $create++;
+ }
+ }
+
+ return (object) [
+ DirectorActivityLog::ACTION_CREATE => $create,
+ DirectorActivityLog::ACTION_MODIFY => $modify,
+ DirectorActivityLog::ACTION_DELETE => $delete,
+ ];
+ }
+
+ /**
+ * Trigger a Sync Run for a given Sync Rule
+ *
+ * This command builds new objects according your Sync Rule, compares them
+ * with existing ones and persists eventual changes.
+ *
+ * USAGE
+ *
+ * icingacli director syncrule run --id <id>
+ *
+ * OPTIONS
+ *
+ * --id <id> A Sync Rule ID. Use the list command to figure out
+ * --benchmark Show timing and memory usage details
+ */
+ public function runAction()
+ {
+ $rule = $this->getSyncRule();
+
+ if ($rule->applyChanges()) {
+ print "New data has been imported\n";
+ $this->showSyncStateDetails($rule);
+ } else {
+ print "Nothing has been changed, imported data is still up to date\n";
+ }
+ }
+
+ /**
+ * @return SyncRule
+ */
+ protected function getSyncRule()
+ {
+ return SyncRule::loadWithAutoIncId(
+ (int) $this->params->getRequired('id'),
+ $this->db()
+ );
+ }
+
+ /**
+ * @param SyncRule $rule
+ */
+ protected function showSyncStateDetails(SyncRule $rule)
+ {
+ echo $this->getSyncStateDescription($rule) . "\n";
+ }
+
+ /**
+ * @param SyncRule $rule
+ * @return string
+ */
+ protected function getSyncStateDescription(SyncRule $rule)
+ {
+ switch ($rule->get('sync_state')) {
+ case 'unknown':
+ return "It's currently unknown whether we are in sync with this rule."
+ . ' You should either check for changes or trigger a new Sync Run.';
+ case 'in-sync':
+ return 'This Sync Rule is in sync';
+ case 'pending-changes':
+ return 'There are pending changes for this Sync Rule. You should'
+ . ' trigger a new Sync Run.';
+ case 'failing':
+ return 'This Sync Rule failed: '. $rule->get('last_error_message');
+ default:
+ throw new RuntimeException('Invalid sync state: ' . $rule->get('sync_state'));
+ }
+ }
+
+ /**
+ * @param SyncRule $rule
+ * @return string
+ */
+ protected function getSyncStateExitCode(SyncRule $rule)
+ {
+ switch ($rule->get('sync_state')) {
+ case 'unknown':
+ return 3;
+ case 'in-sync':
+ return 0;
+ case 'pending-changes':
+ return 1;
+ case 'failing':
+ return 2;
+ default:
+ throw new RuntimeException('Invalid sync state: ' . $rule->get('sync_state'));
+ }
+ }
+}
diff --git a/application/clicommands/TimeperiodCommand.php b/application/clicommands/TimeperiodCommand.php
new file mode 100644
index 0000000..352289a
--- /dev/null
+++ b/application/clicommands/TimeperiodCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Timeperiods
+ *
+ * Use this command to show, create, modify or delete Icinga Timeperiod
+ * objects
+ */
+class TimePeriodCommand extends ObjectCommand
+{
+}
diff --git a/application/clicommands/UserCommand.php b/application/clicommands/UserCommand.php
new file mode 100644
index 0000000..9c4c9d4
--- /dev/null
+++ b/application/clicommands/UserCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Users
+ *
+ * Use this command to show, create, modify or delete Icinga User
+ * objects
+ */
+class UserCommand extends ObjectCommand
+{
+}
diff --git a/application/clicommands/UsergroupCommand.php b/application/clicommands/UsergroupCommand.php
new file mode 100644
index 0000000..04ba7c3
--- /dev/null
+++ b/application/clicommands/UsergroupCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Usergroups
+ *
+ * Use this command to show, create, modify or delete Icinga Usergroup
+ * objects
+ */
+class UsergroupCommand extends ObjectCommand
+{
+}
diff --git a/application/clicommands/ZoneCommand.php b/application/clicommands/ZoneCommand.php
new file mode 100644
index 0000000..a5c45f9
--- /dev/null
+++ b/application/clicommands/ZoneCommand.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Clicommands;
+
+use Icinga\Module\Director\Cli\ObjectCommand;
+
+/**
+ * Manage Icinga Zones
+ *
+ * Use this command to show, create, modify or delete Icinga Zone
+ * objects
+ */
+class ZoneCommand extends ObjectCommand
+{
+}
diff --git a/application/controllers/ApiuserController.php b/application/controllers/ApiuserController.php
new file mode 100644
index 0000000..36438ae
--- /dev/null
+++ b/application/controllers/ApiuserController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectController;
+
+class ApiuserController extends ObjectController
+{
+}
diff --git a/application/controllers/ApiusersController.php b/application/controllers/ApiusersController.php
new file mode 100644
index 0000000..5597521
--- /dev/null
+++ b/application/controllers/ApiusersController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class ApiusersController extends ObjectsController
+{
+}
diff --git a/application/controllers/BasketController.php b/application/controllers/BasketController.php
new file mode 100644
index 0000000..8733d16
--- /dev/null
+++ b/application/controllers/BasketController.php
@@ -0,0 +1,416 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Exception;
+use gipfl\Diff\HtmlRenderer\InlineDiff;
+use gipfl\Diff\PhpDiff;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Web\Table\NameValueTable;
+use gipfl\Web\Widget\Hint;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\Data\Exporter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
+use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshotFieldResolver;
+use Icinga\Module\Director\DirectorObject\Automation\CompareBasketObject;
+use Icinga\Module\Director\Forms\AddToBasketForm;
+use Icinga\Module\Director\Forms\BasketCreateSnapshotForm;
+use Icinga\Module\Director\Forms\BasketForm;
+use Icinga\Module\Director\Forms\BasketUploadForm;
+use Icinga\Module\Director\Forms\RestoreBasketForm;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use ipl\Html\Html;
+use Icinga\Module\Director\Web\Table\BasketSnapshotTable;
+
+class BasketController extends ActionController
+{
+ protected $isApified = true;
+
+ protected function basketTabs()
+ {
+ $name = $this->params->get('name');
+ return $this->tabs()->add('show', [
+ 'label' => $this->translate('Basket'),
+ 'url' => 'director/basket',
+ 'urlParams' => ['name' => $name]
+ ])->add('snapshots', [
+ 'label' => $this->translate('Snapshots'),
+ 'url' => 'director/basket/snapshots',
+ 'urlParams' => ['name' => $name]
+ ]);
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Exception\MissingParameterException
+ */
+ public function indexAction()
+ {
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Back'),
+ 'director/baskets',
+ null,
+ ['class' => 'icon-left-big']
+ )
+ );
+ $basket = $this->requireBasket();
+ $this->basketTabs()->activate('show');
+ $this->addTitle($basket->get('basket_name'));
+ if ($basket->isEmpty()) {
+ $this->content()->add(Hint::info($this->translate('This basket is empty')));
+ }
+ $this->content()->add(
+ (new BasketForm())->setObject($basket)->handleRequest()
+ );
+ }
+
+ /**
+ * @throws \Icinga\Exception\MissingParameterException
+ */
+ public function addAction()
+ {
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Baskets'),
+ 'director/baskets',
+ null,
+ ['class' => 'icon-tag']
+ )
+ );
+ $this->addSingleTab($this->translate('Add to Basket'));
+ $this->addTitle($this->translate('Add chosen objects to a Configuration Basket'));
+ $form = new AddToBasketForm();
+ $form->setDb($this->db())
+ ->setType($this->params->getRequired('type'))
+ ->setNames($this->url()->getParams()->getValues('names'))
+ ->handleRequest();
+ $this->content()->add($form);
+ }
+
+ public function createAction()
+ {
+ $this->actions()->add(
+ Link::create(
+ $this->translate('back'),
+ 'director/baskets',
+ null,
+ ['class' => 'icon-left-big']
+ )
+ );
+ $this->addSingleTab($this->translate('Create Basket'));
+ $this->addTitle($this->translate('Create a new Configuration Basket'));
+ $form = (new BasketForm())
+ ->setDb($this->db())
+ ->handleRequest();
+ $this->content()->add($form);
+ }
+
+ public function uploadAction()
+ {
+ $this->actions()->add(
+ Link::create(
+ $this->translate('back'),
+ 'director/baskets',
+ null,
+ ['class' => 'icon-left-big']
+ )
+ );
+ $this->addSingleTab($this->translate('Upload a Basket'));
+ $this->addTitle($this->translate('Upload a Configuration Basket'));
+ $form = (new BasketUploadForm())
+ ->setDb($this->db())
+ ->handleRequest();
+ $this->content()->add($form);
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function snapshotsAction()
+ {
+ $name = $this->params->get('name');
+ if ($name === null || $name === '') {
+ $basket = null;
+ } else {
+ $basket = Basket::load($name, $this->db());
+ }
+ if ($basket === null) {
+ $this->addTitle($this->translate('Basket Snapshots'));
+ $this->addSingleTab($this->translate('Snapshots'));
+ } else {
+ $this->addTitle(sprintf(
+ $this->translate('%s: Snapshots'),
+ $basket->get('basket_name')
+ ));
+ $this->basketTabs()->activate('snapshots');
+ }
+ if ($basket !== null) {
+ $this->content()->add(
+ (new BasketCreateSnapshotForm())
+ ->setBasket($basket)
+ ->handleRequest()
+ );
+ }
+ $table = new BasketSnapshotTable($this->db());
+ if ($basket !== null) {
+ $table->setBasket($basket);
+ }
+
+ $table->renderTo($this);
+ }
+
+ /**
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function snapshotAction()
+ {
+ $basket = $this->requireBasket();
+ $snapshot = BasketSnapshot::load([
+ 'basket_uuid' => $basket->get('uuid'),
+ 'ts_create' => $this->params->getRequired('ts'),
+ ], $this->db());
+ $snapSum = bin2hex($snapshot->get('content_checksum'));
+
+ if ($this->params->get('action') === 'download') {
+ $this->getResponse()->setHeader('Content-Type', 'application/json', true);
+ $this->getResponse()->setHeader('Content-Disposition', sprintf(
+ 'attachment; filename=Director-Basket_%s_%s.json',
+ str_replace([' ', '"'], ['_', '_'], iconv(
+ 'UTF-8',
+ 'ISO-8859-1//IGNORE',
+ $basket->get('basket_name')
+ )),
+ substr($snapSum, 0, 7)
+ ));
+ echo $snapshot->getJsonDump();
+ return;
+ }
+
+ $this->addTitle(
+ $this->translate('%s: %s (Snapshot)'),
+ $basket->get('basket_name'),
+ substr($snapSum, 0, 7)
+ );
+
+ $this->actions()->add([
+ Link::create(
+ $this->translate('Show Basket'),
+ 'director/basket',
+ ['name' => $basket->get('basket_name')],
+ ['data-base-target' => '_next']
+ ),
+ Link::create(
+ $this->translate('Restore'),
+ $this->url()->with('action', 'restore'),
+ null,
+ ['class' => 'icon-rewind']
+ ),
+ Link::create(
+ $this->translate('Download'),
+ $this->url()
+ ->with([
+ 'action' => 'download',
+ 'dbResourceName' => $this->getDbResourceName()
+ ]),
+ null,
+ [
+ 'class' => 'icon-download',
+ 'target' => '_blank'
+ ]
+ ),
+ ]);
+
+ $properties = new NameValueTable();
+ $properties->addNameValuePairs([
+ $this->translate('Created') => DateFormatter::formatDateTime($snapshot->get('ts_create') / 1000),
+ $this->translate('Content Checksum') => bin2hex($snapshot->get('content_checksum')),
+ ]);
+ $this->content()->add($properties);
+
+ if ($this->params->get('action') === 'restore') {
+ $form = new RestoreBasketForm();
+ $form
+ ->setSnapshot($snapshot)
+ ->handleRequest();
+ $this->content()->add($form);
+ $targetDbName = $form->getValue('target_db');
+ $connection = $form->getDb();
+ } else {
+ $targetDbName = null;
+ $connection = $this->db();
+ }
+
+ $json = $snapshot->getJsonDump();
+ $this->addSingleTab($this->translate('Snapshot'));
+ $all = Json::decode($json);
+ $exporter = new Exporter($this->db());
+ $fieldResolver = new BasketSnapshotFieldResolver($all, $connection);
+ foreach ($all as $type => $objects) {
+ if ($type === 'Datafield') {
+ // TODO: we should now be able to show all fields and link
+ // to a "diff" for the ones that should be created
+ // $this->content()->add(Html::tag('h2', sprintf('+%d Datafield(s)', count($objects))));
+ continue;
+ }
+ $table = new NameValueTable();
+ $table->addAttributes([
+ 'class' => ['table-basket-changes', 'table-row-selectable'],
+ 'data-base-target' => '_next',
+ ]);
+ foreach ($objects as $key => $object) {
+ $linkParams = [
+ 'name' => $basket->get('basket_name'),
+ 'checksum' => $this->params->get('checksum'),
+ 'ts' => $this->params->get('ts'),
+ 'type' => $type,
+ 'key' => $key,
+ ];
+ if ($targetDbName !== null) {
+ $linkParams['target_db'] = $targetDbName;
+ }
+ try {
+ $current = BasketSnapshot::instanceByIdentifier($type, $key, $connection);
+ if ($current === null) {
+ $table->addNameValueRow(
+ $key,
+ Link::create(
+ Html::tag('strong', ['style' => 'color: green'], $this->translate('new')),
+ 'director/basket/snapshotobject',
+ $linkParams
+ )
+ );
+ continue;
+ }
+ $currentExport = $exporter->export($current);
+ $fieldResolver->tweakTargetIds($currentExport);
+
+ // Ignore originalId
+ if (isset($currentExport->originalId)) {
+ unset($currentExport->originalId);
+ }
+ if (isset($object->originalId)) {
+ unset($object->originalId);
+ }
+ $hasChanged = ! CompareBasketObject::equals($currentExport, $object);
+ $table->addNameValueRow(
+ $key,
+ $hasChanged
+ ? Link::create(
+ Html::tag('strong', ['style' => 'color: orange'], $this->translate('modified')),
+ 'director/basket/snapshotobject',
+ $linkParams
+ )
+ : Html::tag('span', ['style' => 'color: green'], $this->translate('unchanged'))
+ );
+ } catch (Exception $e) {
+ $table->addNameValueRow(
+ $key,
+ Html::tag('a', sprintf(
+ '%s (%s:%d)',
+ $e->getMessage(),
+ basename($e->getFile()),
+ $e->getLine()
+ ))
+ );
+ }
+ }
+ $this->content()->add(Html::tag('h2', $type));
+ $this->content()->add($table);
+ }
+ $this->content()->add(Html::tag('div', ['style' => 'height: 5em']));
+ }
+
+ /**
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function snapshotobjectAction()
+ {
+ $basket = $this->requireBasket();
+ $snapshot = BasketSnapshot::load([
+ 'basket_uuid' => $basket->get('uuid'),
+ 'ts_create' => $this->params->getRequired('ts'),
+ ], $this->db());
+ $snapshotUrl = $this->url()->without('type')->without('key')->setPath('director/basket/snapshot');
+ $type = $this->params->get('type');
+ $key = $this->params->get('key');
+
+ $this->addTitle($this->translate('Single Object Diff'));
+ $this->content()->add(Hint::info(Html::sprintf(
+ $this->translate('Comparing %s "%s" from Snapshot "%s" to current config'),
+ $type,
+ $key,
+ Link::create(
+ substr(bin2hex($snapshot->get('content_checksum')), 0, 7),
+ $snapshotUrl,
+ null,
+ ['data-base-target' => '_next']
+ )
+ )));
+ $this->actions()->add([
+ Link::create(
+ $this->translate('back'),
+ $snapshotUrl,
+ null,
+ ['class' => 'icon-left-big']
+ ),
+ /*
+ Link::create(
+ $this->translate('Restore'),
+ $this->url()->with('action', 'restore'),
+ null,
+ ['class' => 'icon-rewind']
+ )
+ */
+ ]);
+ $exporter = new Exporter($this->db());
+ $json = $snapshot->getJsonDump();
+ $this->addSingleTab($this->translate('Snapshot'));
+ $objects = Json::decode($json);
+ $targetDbName = $this->params->get('target_db');
+ if ($targetDbName === null) {
+ $connection = $this->db();
+ } else {
+ $connection = Db::fromResourceName($targetDbName);
+ }
+ $fieldResolver = new BasketSnapshotFieldResolver($objects, $connection);
+ $objectFromBasket = $objects->$type->$key;
+ unset($objectFromBasket->originalId);
+ CompareBasketObject::normalize($objectFromBasket);
+ $objectFromBasket = Json::encode($objectFromBasket, JSON_PRETTY_PRINT);
+ $current = BasketSnapshot::instanceByIdentifier($type, $key, $connection);
+ if ($current === null) {
+ $current = '';
+ } else {
+ $exported = $exporter->export($current);
+ $fieldResolver->tweakTargetIds($exported);
+ unset($exported->originalId);
+ CompareBasketObject::normalize($exported);
+ $current = Json::encode($exported, JSON_PRETTY_PRINT);
+ }
+
+ if ($current === $objectFromBasket) {
+ $this->content()->add([
+ Hint::ok('Basket equals current object'),
+ Html::tag('pre', $current)
+ ]);
+ } else {
+ $this->content()->add(new InlineDiff(new PhpDiff($current, $objectFromBasket)));
+ }
+ }
+
+ /**
+ * @return Basket
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function requireBasket()
+ {
+ return Basket::load($this->params->getRequired('name'), $this->db());
+ }
+}
diff --git a/application/controllers/BasketsController.php b/application/controllers/BasketsController.php
new file mode 100644
index 0000000..6b50b62
--- /dev/null
+++ b/application/controllers/BasketsController.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Table\BasketTable;
+
+class BasketsController extends ActionController
+{
+ protected $isApified = false;
+
+ public function indexAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $this->addSingleTab($this->translate('Baskets'));
+ $this->actions()->add([
+ Link::create(
+ $this->translate('Create'),
+ 'director/basket/create',
+ null,
+ ['class' => 'icon-plus']
+ ),
+ Link::create(
+ $this->translate('Upload'),
+ 'director/basket/upload',
+ null,
+ ['class' => 'icon-upload']
+ ),
+ ]);
+ $this->addTitle($this->translate('Configuration Baskets'));
+ $this->content()->add(Html::tag('p', $this->translate(
+ 'A Configuration Basket references specific Configuration'
+ . ' Objects or all objects of a specific type. It has been'
+ . ' designed to share Templates, Import/Sync strategies and'
+ . ' other base Configuration Objects. It is not a tool to'
+ . ' operate with single Hosts or Services.'
+ )));
+ $this->content()->add(Html::tag('p', $this->translate(
+ 'You can create Basket snapshots at any time, this will persist'
+ . ' a serialized representation of all involved objects at that'
+ . ' moment in time. Snapshots can be exported, imported, shared'
+ . ' and restored - to the very same or another Director instance.'
+ )));
+ $table = (new BasketTable($this->db()))
+ ->setAttribute('data-base-target', '_self');
+ // TODO: temporarily disabled, this was a thing in dipl
+ if (/*$table->hasSearch() || */count($table)) {
+ $table->renderTo($this);
+ }
+ }
+}
diff --git a/application/controllers/BranchController.php b/application/controllers/BranchController.php
new file mode 100644
index 0000000..3b36e83
--- /dev/null
+++ b/application/controllers/BranchController.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\Diff\HtmlRenderer\SideBySideDiff;
+use gipfl\Diff\PhpDiff;
+use gipfl\IcingaWeb2\Widget\NameValueTable;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db\Branch\BranchActivity;
+use Icinga\Module\Director\Db\Branch\BranchStore;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\SyncRule;
+use Icinga\Module\Director\PlainObjectRenderer;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Controller\BranchHelper;
+use Icinga\Module\Director\Web\Widget\IcingaConfigDiff;
+use ipl\Html\Html;
+
+class BranchController extends ActionController
+{
+ use BranchHelper;
+
+ public function init()
+ {
+ parent::init();
+ IcingaObject::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch()));
+ SyncRule::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch()));
+ }
+
+ protected function checkDirectorPermissions()
+ {
+ }
+
+ public function activityAction()
+ {
+ $this->assertPermission('director/showconfig');
+ $ts = $this->params->getRequired('ts');
+ $activity = BranchActivity::load($ts, $this->db());
+ $store = new BranchStore($this->db());
+ $branch = $store->fetchBranchByUuid($activity->getBranchUuid());
+ if ($branch->isSyncPreview()) {
+ $this->addSingleTab($this->translate('Sync Preview'));
+ $this->addTitle($this->translate('Expected Modification'));
+ } else {
+ $this->addSingleTab($this->translate('Activity'));
+ $this->addTitle($this->translate('Branch Activity'));
+ }
+
+ $this->content()->add($this->prepareActivityInfo($activity));
+ $this->showActivity($activity);
+ }
+
+ protected function prepareActivityInfo(BranchActivity $activity)
+ {
+ $table = new NameValueTable();
+ $table->addNameValuePairs([
+ $this->translate('Author') => $activity->getAuthor(),
+ $this->translate('Date') => date('Y-m-d H:i:s', $activity->getTimestamp()),
+ $this->translate('Action') => $activity->getAction()
+ . ' ' . preg_replace('/^icinga_/', '', $activity->getObjectTable())
+ . ' ' . $activity->getObjectName(),
+ // $this->translate('Actions') => ['Undo form'],
+ ]);
+ return $table;
+ }
+
+ protected function leftFromActivity(BranchActivity $activity)
+ {
+ if ($activity->isActionCreate()) {
+ return null;
+ }
+ $object = DbObjectTypeRegistry::newObject($activity->getObjectTable(), [], $this->db());
+ $properties = $this->objectTypeFirst($activity->getFormerProperties()->jsonSerialize());
+ foreach ($properties as $key => $value) {
+ $object->set($key, $value);
+ }
+
+ return $object;
+ }
+
+ protected function rightFromActivity(BranchActivity $activity)
+ {
+ if ($activity->isActionDelete()) {
+ return null;
+ }
+ $object = DbObjectTypeRegistry::newObject($activity->getObjectTable(), [], $this->db());
+ if (! $activity->isActionCreate()) {
+ foreach ($activity->getFormerProperties()->jsonSerialize() as $key => $value) {
+ $object->set($key, $value);
+ }
+ }
+ $properties = $this->objectTypeFirst($activity->getModifiedProperties()->jsonSerialize());
+ foreach ($properties as $key => $value) {
+ $object->set($key, $value);
+ }
+
+ return $object;
+ }
+
+ protected function objectTypeFirst($properties)
+ {
+ $properties = (array) $properties;
+ if (isset($properties['object_type'])) {
+ $type = $properties['object_type'];
+ unset($properties['object_type']);
+ $properties = ['object_type' => $type] + $properties;
+ }
+
+ return $properties;
+ }
+
+ protected function showActivity(BranchActivity $activity)
+ {
+ $left = $this->leftFromActivity($activity);
+ $right = $this->rightFromActivity($activity);
+ if ($left instanceof IcingaObject || $right instanceof IcingaObject) {
+ $this->content()->add(new IcingaConfigDiff(
+ $left ? $left->toSingleIcingaConfig() : $this->createEmptyConfig(),
+ $right ? $right->toSingleIcingaConfig() : $this->createEmptyConfig()
+ ));
+ } else {
+ $this->content()->add([
+ Html::tag('h3', $this->translate('Modification')),
+ new SideBySideDiff(new PhpDiff(
+ PlainObjectRenderer::render($left->getProperties()),
+ PlainObjectRenderer::render($right->getProperties())
+ ))
+ ]);
+ }
+ }
+
+ protected function createEmptyConfig()
+ {
+ return new IcingaConfig($this->db());
+ }
+}
diff --git a/application/controllers/CommandController.php b/application/controllers/CommandController.php
new file mode 100644
index 0000000..de0ba54
--- /dev/null
+++ b/application/controllers/CommandController.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Module\Director\Objects\IcingaCommandArgument;
+use Icinga\Module\Director\Web\Table\BranchedIcingaCommandArgumentTable;
+use ipl\Html\Html;
+use Icinga\Module\Director\Forms\IcingaCommandArgumentForm;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Resolver\CommandUsage;
+use Icinga\Module\Director\Web\Controller\ObjectController;
+use Icinga\Module\Director\Web\Table\IcingaCommandArgumentTable;
+
+class CommandController extends ObjectController
+{
+ /**
+ * @throws \Icinga\Exception\AuthenticationException
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function init()
+ {
+ parent::init();
+ $o = $this->object;
+ if ($o && ! $o->isExternal()) {
+ if ($this->getBranch()->isBranch()) {
+ $urlParams = ['uuid' => $o->getUniqueId()->toString()];
+ } else {
+ $urlParams = ['name' => $o->getObjectName()];
+ }
+ $this->tabs()->add('arguments', [
+ 'url' => 'director/command/arguments',
+ 'urlParams' => $urlParams,
+ 'label' => 'Arguments'
+ ]);
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function indexAction()
+ {
+ if (! $this->getRequest()->isApiRequest()) {
+ $this->showUsage();
+ }
+ parent::indexAction();
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function renderAction()
+ {
+ if ($this->object->isExternal()) {
+ $this->showUsage();
+ }
+
+ parent::renderAction();
+ }
+
+ /**
+ * @throws \Zend_Db_Select_Exception
+ */
+ protected function showUsage()
+ {
+ /** @var IcingaCommand $command */
+ $command = $this->object;
+ if ($command->isInUse()) {
+ $usage = new CommandUsage($command);
+ $this->content()->add(Hint::info(Html::sprintf(
+ $this->translate('This Command is currently being used by %s'),
+ Html::tag('span', null, $usage->getLinks())->setSeparator(', ')
+ ))->addAttributes([
+ 'data-base-target' => '_next'
+ ]));
+ } else {
+ $this->content()->add(Hint::warning($this->translate('This Command is currently not in use')));
+ }
+ }
+
+ public function argumentsAction()
+ {
+ $p = $this->params;
+ /** @var IcingaCommand $o */
+ $o = $this->object;
+ $this->tabs()->activate('arguments');
+ $this->addTitle($this->translate('Command arguments: %s'), $o->getObjectName());
+ $form = (new IcingaCommandArgumentForm)
+ ->setBranch($this->getBranch())
+ ->setCommandObject($o);
+ if ($argument = $p->shift('argument')) {
+ $this->addBackLink('director/command/arguments', [
+ 'name' => $p->get('name')
+ ]);
+ if ($this->branch->isBranch()) {
+ $arguments = $o->arguments();
+ $argument = $arguments->get($argument);
+ // IcingaCommandArgument::create((array) $arguments->get($argument)->toFullPlainObject());
+ // $argument->setBeingLoadedFromDb();
+ } else {
+ $argument = IcingaCommandArgument::load([
+ 'command_id' => $o->get('id'),
+ 'argument_name' => $argument
+ ], $this->db());
+ }
+ $form->setObject($argument);
+ }
+ $form->handleRequest();
+ $this->content()->add([$form]);
+ if ($this->branch->isBranch()) {
+ (new BranchedIcingaCommandArgumentTable($o, $this->getBranch()))->renderTo($this);
+ } else {
+ (new IcingaCommandArgumentTable($o, $this->getBranch()))->renderTo($this);
+ }
+ }
+
+ protected function hasBasketSupport()
+ {
+ return true;
+ }
+}
diff --git a/application/controllers/CommandsController.php b/application/controllers/CommandsController.php
new file mode 100644
index 0000000..246028f
--- /dev/null
+++ b/application/controllers/CommandsController.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class CommandsController extends ObjectsController
+{
+ public function indexAction()
+ {
+ parent::indexAction();
+ $validTypes = ['object', 'external_object'];
+ $type = $this->params->get('type', 'object');
+ if (! in_array($type, $validTypes)) {
+ $type = 'object';
+ }
+
+ $this->table->setType($type);
+ }
+}
diff --git a/application/controllers/CommandtemplateController.php b/application/controllers/CommandtemplateController.php
new file mode 100644
index 0000000..ca5f827
--- /dev/null
+++ b/application/controllers/CommandtemplateController.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Web\Controller\TemplateController;
+
+class CommandtemplateController extends TemplateController
+{
+ protected function requireTemplate()
+ {
+ return IcingaCommand::load([
+ 'object_name' => $this->params->get('name')
+ ], $this->db());
+ }
+}
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
new file mode 100644
index 0000000..3f8a105
--- /dev/null
+++ b/application/controllers/ConfigController.php
@@ -0,0 +1,539 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\Diff\HtmlRenderer\SideBySideDiff;
+use gipfl\Diff\PhpDiff;
+use gipfl\Web\Widget\Hint;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Deployment\DeploymentStatus;
+use Icinga\Module\Director\Forms\DeployConfigForm;
+use Icinga\Module\Director\Forms\SettingsForm;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+use Icinga\Module\Director\Settings;
+use Icinga\Module\Director\Web\Controller\BranchHelper;
+use Icinga\Module\Director\Web\Table\ActivityLogTable;
+use Icinga\Module\Director\Web\Table\BranchActivityTable;
+use Icinga\Module\Director\Web\Table\ConfigFileDiffTable;
+use Icinga\Module\Director\Web\Table\DeploymentLogTable;
+use Icinga\Module\Director\Web\Table\GeneratedConfigFileTable;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Tabs\InfraTabs;
+use Icinga\Module\Director\Web\Widget\ActivityLogInfo;
+use Icinga\Module\Director\Web\Widget\DeployedConfigInfoHeader;
+use Icinga\Module\Director\Web\Widget\ShowConfigFile;
+use Icinga\Web\Notification;
+use Exception;
+use RuntimeException;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Url;
+
+class ConfigController extends ActionController
+{
+ use BranchHelper;
+
+ protected $isApified = true;
+
+ protected function checkDirectorPermissions()
+ {
+ }
+
+ /**
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function deploymentsAction()
+ {
+ if ($this->sendNotFoundForRestApi()) {
+ return;
+ }
+ $this->assertPermission('director/deploy');
+ $this->addTitle($this->translate('Deployments'));
+ try {
+ if (DirectorDeploymentLog::hasUncollected($this->db())) {
+ $this->setAutorefreshInterval(2);
+ } else {
+ $this->setAutorefreshInterval(20);
+ }
+ } catch (Exception $e) {
+ $this->content()->prepend(Hint::warning($e->getMessage()));
+ // No problem, Icinga might be reloading
+ }
+
+ if (! $this->getBranch()->isBranch()) {
+ // TODO: a form!
+ $this->actions()->add(Link::create(
+ $this->translate('Render config'),
+ 'director/config/store',
+ null,
+ ['class' => 'icon-wrench']
+ ));
+ }
+
+ $this->tabs(new InfraTabs($this->Auth()))->activate('deploymentlog');
+ $table = new DeploymentLogTable($this->db());
+ try {
+ // Move elsewhere
+ $table->setActiveStageName(
+ $this->api()->getActiveStageName()
+ );
+ } catch (Exception $e) {
+ // Don't care
+ }
+
+ $table->renderTo($this);
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function deployAction()
+ {
+ $request = $this->getRequest();
+ if (! $request->isApiRequest()) {
+ throw new NotFoundError('Not found');
+ }
+
+ if (! $request->isPost()) {
+ throw new RuntimeException(sprintf(
+ 'Unsupported method: %s',
+ $request->getMethod()
+ ));
+ }
+ $this->assertPermission('director/deploy');
+
+ // TODO: require POST
+ $checksum = $this->params->get('checksum');
+ if ($checksum) {
+ $config = IcingaConfig::load(hex2bin($checksum), $this->db());
+ } else {
+ $config = IcingaConfig::generate($this->db());
+ $checksum = $config->getHexChecksum();
+ }
+
+ try {
+ $this->api()->wipeInactiveStages($this->db());
+ } catch (Exception $e) {
+ $this->deploymentFailed($checksum, $e->getMessage());
+ }
+
+ if ($this->api()->dumpConfig($config, $this->db())) {
+ $this->deploymentSucceeded($checksum);
+ } else {
+ $this->deploymentFailed($checksum);
+ }
+ }
+
+ public function deploymentStatusAction()
+ {
+ if ($this->sendNotFoundUnlessRestApi()) {
+ return;
+ }
+ $db = $this->db();
+ $api = $this->api();
+ $status = new DeploymentStatus($db, $api);
+ $result = $status->getDeploymentStatus($this->params->get('configs'), $this->params->get('activities'));
+
+ $this->sendJson($this->getResponse(), (object) $result);
+ }
+
+ /**
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function activitiesAction()
+ {
+ if ($this->sendNotFoundForRestApi()) {
+ return;
+ }
+ $this->assertPermission('director/audit');
+ $this->showOptionalBranchActivity();
+ $this->setAutorefreshInterval(10);
+ $this->tabs(new InfraTabs($this->Auth()))->activate('activitylog');
+ $this->addTitle($this->translate('Activity Log'));
+ $lastDeployedId = $this->db()->getLastDeploymentActivityLogId();
+ $table = new ActivityLogTable($this->db());
+ $table->setLastDeployedId($lastDeployedId);
+ if ($idRangeEx = $this->url()->getParam('idRangeEx')) {
+ $table->applyFilter(Filter::fromQueryString($idRangeEx));
+ }
+ $filter = Filter::fromQueryString(
+ $this->url()->without(['page', 'limit', 'q', 'idRangeEx'])->getQueryString()
+ );
+ $table->applyFilter($filter);
+ if ($this->url()->hasParam('author')) {
+ $this->actions()->add(Link::create(
+ $this->translate('All changes'),
+ $this->url()
+ ->without(['author', 'page']),
+ null,
+ ['class' => 'icon-users', 'data-base-target' => '_self']
+ ));
+ } else {
+ $this->actions()->add(Link::create(
+ $this->translate('My changes'),
+ $this->url()
+ ->with('author', $this->Auth()->getUser()->getUsername())
+ ->without('page'),
+ null,
+ ['class' => 'icon-user', 'data-base-target' => '_self']
+ ));
+ }
+ if ($this->hasPermission('director/deploy') && ! $this->getBranch()->isBranch()) {
+ if ($this->db()->hasDeploymentEndpoint()) {
+ $this->actions()->add(DeployConfigForm::load()
+ ->setDb($this->db())
+ ->setApi($this->api())
+ ->handleRequest());
+ }
+ }
+
+ $table->renderTo($this);
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ public function activityAction()
+ {
+ if ($this->sendNotFoundForRestApi()) {
+ return;
+ }
+ $this->assertPermission('director/showconfig');
+ $p = $this->params;
+ $info = new ActivityLogInfo(
+ $this->db(),
+ $p->get('type'),
+ $p->get('name')
+ );
+
+ $info->setChecksum($p->get('checksum'))
+ ->setId($p->get('id'));
+
+ $this->tabs($info->getTabs($this->url()));
+ $info->showTab($this->params->get('show'));
+
+ $this->addTitle($info->getTitle());
+ $this->controls()->prepend($info->getPagination($this->url()));
+ $this->content()->add($info);
+ }
+
+ /**
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function settingsAction()
+ {
+ if ($this->sendNotFoundForRestApi()) {
+ return;
+ }
+ $this->assertPermission('director/admin');
+
+ $this->addSingleTab($this->translate('Settings'))
+ ->addTitle($this->translate('Global Director Settings'));
+ $this->content()->add(
+ SettingsForm::load()
+ ->setSettings(new Settings($this->db()))
+ ->handleRequest()
+ );
+ }
+
+ /**
+ * Show all files for a given config
+ *
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function filesAction()
+ {
+ if ($this->sendNotFoundForRestApi()) {
+ return;
+ }
+ $this->assertPermission('director/showconfig');
+ $config = IcingaConfig::load(
+ hex2bin($this->params->getRequired('checksum')),
+ $this->db()
+ );
+ $deploymentId = $this->params->get('deployment_id');
+
+ $tabs = $this->tabs();
+ if ($deploymentId) {
+ $tabs->add('deployment', [
+ 'label' => $this->translate('Deployment'),
+ 'url' => 'director/deployment',
+ 'urlParams' => ['id' => $deploymentId]
+ ]);
+ }
+
+ $tabs->add('config', [
+ 'label' => $this->translate('Config'),
+ 'url' => $this->url(),
+ ])->activate('config');
+
+ $this->addTitle($this->translate('Generated config'));
+ $this->content()->add(new DeployedConfigInfoHeader(
+ $config,
+ $this->db(),
+ $this->api(),
+ $this->getBranch(),
+ $deploymentId
+ ));
+
+ GeneratedConfigFileTable::load($config, $this->db())
+ ->setActiveFilename($this->params->get('active_file'))
+ ->setDeploymentId($deploymentId)
+ ->renderTo($this);
+ }
+
+ /**
+ * Show a single file
+ *
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function fileAction()
+ {
+ if ($this->sendNotFoundForRestApi()) {
+ return;
+ }
+ $this->assertPermission('director/showconfig');
+ $filename = $this->params->getRequired('file_path');
+ $this->configTabs()->add('file', array(
+ 'label' => $this->translate('Rendered file'),
+ 'url' => $this->url(),
+ ))->activate('file');
+
+ $params = $this->getConfigTabParams();
+ if ('deployment' === $this->params->get('backTo')) {
+ $this->addBackLink('director/deployment', ['id' => $params['deployment_id']]);
+ } else {
+ $params['active_file'] = $filename;
+ $this->addBackLink('director/config/files', $params);
+ }
+
+ $config = IcingaConfig::load(hex2bin($this->params->get('config_checksum')), $this->db());
+ $this->addTitle($this->translate('Config file "%s"'), $filename);
+ $this->content()->add(new ShowConfigFile(
+ $config->getFile($filename),
+ $this->params->get('highlight'),
+ $this->params->get('highlightSeverity')
+ ));
+ }
+
+ /**
+ * TODO: Check if this can be removed
+ *
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function storeAction()
+ {
+ $this->assertPermission('director/deploy');
+ try {
+ $config = IcingaConfig::generate($this->db());
+ } catch (Exception $e) {
+ Notification::error($e->getMessage());
+ $this->redirectNow('director/config/deployments');
+ }
+ $this->redirectNow(
+ Url::fromPath(
+ 'director/config/files',
+ array('checksum' => $config->getHexChecksum())
+ )
+ );
+ }
+
+ /**
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function diffAction()
+ {
+ if ($this->sendNotFoundForRestApi()) {
+ return;
+ }
+ $this->assertPermission('director/showconfig');
+
+ $db = $this->db();
+ $this->addTitle($this->translate('Config diff'));
+ $this->addSingleTab($this->translate('Config diff'));
+
+ $leftSum = $this->params->get('left');
+ $rightSum = $this->params->get('right');
+
+ $configs = $db->enumDeployedConfigs();
+ foreach (array($leftSum, $rightSum) as $sum) {
+ if (! array_key_exists($sum, $configs)) {
+ $configs[$sum] = substr($sum, 0, 7);
+ }
+ }
+
+ $baseUrl = $this->url()->without(['left', 'right']);
+ $this->content()->add(Html::tag('form', ['action' => (string) $baseUrl, 'method' => 'GET'], [
+ new HtmlString($this->view->formSelect(
+ 'left',
+ $leftSum,
+ ['class' => 'autosubmit', 'style' => 'width: 37%'],
+ [null => $this->translate('- please choose -')] + $configs
+ )),
+ Link::create(
+ Icon::create('flapping'),
+ $baseUrl,
+ ['left' => $rightSum, 'right' => $leftSum]
+ ),
+ new HtmlString($this->view->formSelect(
+ 'right',
+ $rightSum,
+ ['class' => 'autosubmit', 'style' => 'width: 37%'],
+ [null => $this->translate('- please choose -')] + $configs
+ )),
+ ]));
+
+ if ($rightSum === null || $leftSum === null || ! strlen($rightSum) || ! strlen($leftSum)) {
+ return;
+ }
+ ConfigFileDiffTable::load($leftSum, $rightSum, $this->db())->renderTo($this);
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws \Icinga\Exception\MissingParameterException
+ */
+ public function filediffAction()
+ {
+ if ($this->sendNotFoundForRestApi()) {
+ return;
+ }
+ $this->assertPermission('director/showconfig');
+
+ $p = $this->params;
+ $db = $this->db();
+ $leftSum = $p->getRequired('left');
+ $rightSum = $p->getRequired('right');
+ $filename = $p->getRequired('file_path');
+
+ $left = IcingaConfig::load(hex2bin($leftSum), $db);
+ $right = IcingaConfig::load(hex2bin($rightSum), $db);
+
+ $this
+ ->addTitle($this->translate('Config file "%s"'), $filename)
+ ->addSingleTab($this->translate('Diff'))
+ ->content()->add(new SideBySideDiff(new PhpDiff(
+ $left->getFile($filename),
+ $right->getFile($filename)
+ )));
+ }
+
+ protected function showOptionalBranchActivity()
+ {
+ if ($this->url()->hasParam('idRangeEx')) {
+ return;
+ }
+ $branch = $this->getBranch();
+ if ($branch->isBranch() && (int) $this->params->get('page', '1') === 1) {
+ $table = new BranchActivityTable($branch->getUuid(), $this->db());
+ if (count($table) > 0) {
+ $this->content()->add(Hint::info(Html::sprintf($this->translate(
+ 'The following modifications are visible in this %s only...'
+ ), Branch::requireHook()->linkToBranch(
+ $branch,
+ $this->Auth(),
+ $this->translate('configuration branch')
+ ))));
+ $this->content()->add($table);
+ $this->content()->add(Html::tag('br'));
+ $this->content()->add(Hint::ok($this->translate(
+ '...and the modifications below are already in the main branch:'
+ )));
+ $this->content()->add(Html::tag('br'));
+ }
+ }
+ }
+
+ /**
+ * @param $checksum
+ */
+ protected function deploymentSucceeded($checksum)
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ $this->sendJson($this->getResponse(), (object) array('checksum' => $checksum));
+ return;
+ } else {
+ $url = Url::fromPath('director/config/deployments');
+ Notification::success(
+ $this->translate('Config has been submitted, validation is going on')
+ );
+ $this->redirectNow($url);
+ }
+ }
+
+ /**
+ * @param $checksum
+ * @param null $error
+ */
+ protected function deploymentFailed($checksum, $error = null)
+ {
+ $extra = $error ? ': ' . $error: '';
+
+ if ($this->getRequest()->isApiRequest()) {
+ $this->sendJsonError($this->getResponse(), 'Config deployment failed' . $extra);
+ return;
+ } else {
+ $url = Url::fromPath('director/config/files', array('checksum' => $checksum));
+ Notification::error(
+ $this->translate('Config deployment failed') . $extra
+ );
+ $this->redirectNow($url);
+ }
+ }
+
+ /**
+ * @return \gipfl\IcingaWeb2\Widget\Tabs
+ */
+ protected function configTabs()
+ {
+ $tabs = $this->tabs();
+
+ if ($this->hasPermission('director/deploy')
+ && $deploymentId = $this->params->get('deployment_id')
+ ) {
+ $tabs->add('deployment', [
+ 'label' => $this->translate('Deployment'),
+ 'url' => 'director/deployment',
+ 'urlParams' => ['id' => $deploymentId]
+ ]);
+ }
+
+ if ($this->hasPermission('director/showconfig')) {
+ $tabs->add('config', [
+ 'label' => $this->translate('Config'),
+ 'url' => 'director/config/files',
+ 'urlParams' => $this->getConfigTabParams()
+ ]);
+ }
+
+ return $tabs;
+ }
+
+ protected function getConfigTabParams()
+ {
+ $params = [
+ 'checksum' => $this->params->get(
+ 'config_checksum',
+ $this->params->get('checksum')
+ )
+ ];
+
+ if ($deploymentId = $this->params->get('deployment_id')) {
+ $params['deployment_id'] = $deploymentId;
+ }
+
+ return $params;
+ }
+}
diff --git a/application/controllers/CustomvarController.php b/application/controllers/CustomvarController.php
new file mode 100644
index 0000000..f0d4574
--- /dev/null
+++ b/application/controllers/CustomvarController.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Table\CustomvarVariantsTable;
+
+class CustomvarController extends ActionController
+{
+ public function variantsAction()
+ {
+ $varName = $this->params->getRequired('name');
+ $this->addSingleTab($this->translate('Custom Variable'))
+ ->addTitle($this->translate('Custom Variable variants: %s'), $varName);
+ CustomvarVariantsTable::create($this->db(), $varName)->renderTo($this);
+ }
+}
diff --git a/application/controllers/DaemonController.php b/application/controllers/DaemonController.php
new file mode 100644
index 0000000..ab0038f
--- /dev/null
+++ b/application/controllers/DaemonController.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Application\Icinga;
+use Icinga\Module\Director\Daemon\RunningDaemonInfo;
+use Icinga\Module\Director\Web\Tabs\MainTabs;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Widget\BackgroundDaemonDetails;
+use Icinga\Module\Director\Web\Widget\Documentation;
+use ipl\Html\Html;
+
+class DaemonController extends ActionController
+{
+ public function indexAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $this->tabs(new MainTabs($this->Auth(), $this->getDbResourceName()))->activate('daemon');
+ $this->setTitle($this->translate('Director Background Daemon'));
+ // Avoiding layout issues:
+ $this->content()->add(Html::tag('h1', $this->translate('Director Background Daemon')));
+ // TODO: move dashboard titles into controls. Or figure out whether 2.7 "broke" this
+
+ $error = null;
+ try {
+ $db = $this->db()->getDbAdapter();
+ $daemons = $db->fetchAll(
+ $db->select()->from('director_daemon_info')->order('fqdn')->order('username')->order('pid')
+ );
+ } catch (\Exception $e) {
+ $daemons = [];
+ $error = $e->getMessage();
+ }
+
+ if (empty($daemons)) {
+ $documentation = new Documentation(Icinga::app(), $this->Auth());
+ $message = Html::sprintf($this->translate(
+ 'The Icinga Director Background Daemon is not running.'
+ . ' Please check our %s in case you need step by step instructions'
+ . ' showing you how to fix this.'
+ ), $documentation->getModuleLink(
+ $this->translate('documentation'),
+ 'director',
+ '75-Background-Daemon',
+ $this->translate('Icinga Director Background Daemon')
+ ));
+ $this->content()->add(Hint::error([
+ $message,
+ ($error ? [Html::tag('br'), Html::tag('strong', $error)] : null),
+ ]));
+ return;
+ }
+
+ try {
+ foreach ($daemons as $daemon) {
+ $info = new RunningDaemonInfo($daemon);
+ $this->content()->add([new BackgroundDaemonDetails($info, $daemon) /*, $logWindow*/]);
+ }
+ } catch (\Exception $e) {
+ $this->content()->add(Hint::error($e->getMessage()));
+ }
+ }
+}
diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php
new file mode 100644
index 0000000..95c1cd0
--- /dev/null
+++ b/application/controllers/DashboardController.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Tabs\MainTabs;
+use Icinga\Module\Director\Dashboard\Dashboard;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Form\DbSelectorForm;
+
+class DashboardController extends ActionController
+{
+ protected function checkDirectorPermissions()
+ {
+ // No special permissions required, override parent method
+ }
+
+ protected function addDbSelection()
+ {
+ if ($this->isMultiDbSetup()) {
+ $form = new DbSelectorForm(
+ $this->getResponse(),
+ $this->Window(),
+ $this->listAllowedDbResourceNames()
+ );
+ $this->content()->add($form);
+ $form->handleRequest($this->getServerRequest());
+ }
+ }
+
+ public function indexAction()
+ {
+ if ($this->getRequest()->isGet()) {
+ $this->setAutorefreshInterval(10);
+ }
+
+ $mainDashboards = [
+ 'Objects',
+ 'Alerts',
+ 'Branches',
+ 'Automation',
+ 'Deployment',
+ 'Director',
+ 'Data',
+ ];
+ $this->setTitle($this->translate('Icinga Director - Main Dashboard'));
+ $names = $this->params->getValues('name', $mainDashboards);
+ if (! $this->params->has('name')) {
+ $this->addDbSelection();
+ }
+ if (count($names) === 1) {
+ $name = $names[0];
+ $dashboard = Dashboard::loadByName($name, $this->db());
+ $this->tabs($dashboard->getTabs())->activate($name);
+ } else {
+ $this->tabs(new MainTabs($this->Auth(), $this->getDbResourceName()))->activate('main');
+ }
+
+ $cntDashboards = 0;
+ foreach ($names as $name) {
+ if ($name instanceof Dashboard) {
+ $dashboard = $name;
+ } else {
+ $dashboard = Dashboard::loadByName($name, $this->db());
+ }
+ if ($dashboard->isAvailable()) {
+ $cntDashboards++;
+ $this->content()->add($dashboard);
+ }
+ }
+
+ if ($cntDashboards === 0) {
+ $msg = $this->translate(
+ 'No dashboard available, you might have not enough permissions'
+ );
+ $this->content()->add($msg);
+ }
+ }
+}
diff --git a/application/controllers/DataController.php b/application/controllers/DataController.php
new file mode 100644
index 0000000..ae4bbcf
--- /dev/null
+++ b/application/controllers/DataController.php
@@ -0,0 +1,406 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Forms\DirectorDatalistEntryForm;
+use Icinga\Module\Director\Forms\DirectorDatalistForm;
+use Icinga\Module\Director\Forms\IcingaServiceDictionaryMemberForm;
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\PlainObjectRenderer;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader;
+use Icinga\Module\Director\Web\Table\CustomvarTable;
+use Icinga\Module\Director\Web\Table\DatafieldCategoryTable;
+use Icinga\Module\Director\Web\Table\DatafieldTable;
+use Icinga\Module\Director\Web\Table\DatalistEntryTable;
+use Icinga\Module\Director\Web\Table\DatalistTable;
+use Icinga\Module\Director\Web\Tabs\DataTabs;
+use gipfl\IcingaWeb2\Link;
+use InvalidArgumentException;
+use ipl\Html\Html;
+use ipl\Html\Table;
+
+class DataController extends ActionController
+{
+ public function listsAction()
+ {
+ $this->addTitle($this->translate('Data lists'));
+ $this->actions()->add(
+ Link::create($this->translate('Add'), 'director/data/list', null, [
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next'
+ ])
+ );
+
+ $this->tabs(new DataTabs())->activate('datalist');
+ (new DatalistTable($this->db()))->renderTo($this);
+ }
+
+ /**
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function listAction()
+ {
+ $form = DirectorDatalistForm::load()
+ ->setSuccessUrl('director/data/lists')
+ ->setDb($this->db());
+
+ if ($name = $this->params->get('name')) {
+ $list = $this->requireList('name');
+ $form->setObject($list);
+ $this->addListActions($list);
+ $this->addTitle(
+ $this->translate('Data List: %s'),
+ $list->get('list_name')
+ )->addListTabs($name, 'list');
+ } else {
+ $this
+ ->addTitle($this->translate('Add a new Data List'))
+ ->addSingleTab($this->translate('Data List'));
+ }
+
+ $this->content()->add($form->handleRequest());
+ }
+
+ public function fieldsAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $this->tabs(new DataTabs())->activate('datafield');
+ $this->addTitle($this->translate('Data Fields'));
+ $this->actions()->add(Link::create(
+ $this->translate('Add'),
+ 'director/datafield/add',
+ null,
+ [
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next',
+ ]
+ ));
+
+ (new DatafieldTable($this->db()))->renderTo($this);
+ }
+
+ public function fieldcategoriesAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $this->tabs(new DataTabs())->activate('datafieldcategory');
+ $this->addTitle($this->translate('Data Field Categories'));
+ $this->actions()->add(Link::create(
+ $this->translate('Add'),
+ 'director/datafieldcategory/add',
+ null,
+ [
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next',
+ ]
+ ));
+
+ (new DatafieldCategoryTable($this->db()))->renderTo($this);
+ }
+
+ public function varsAction()
+ {
+ $this->tabs(new DataTabs())->activate('customvars');
+ $this->addTitle($this->translate('Custom Vars - Overview'));
+ (new CustomvarTable($this->db()))->renderTo($this);
+ }
+
+ /**
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function listentryAction()
+ {
+ $entryName = $this->params->get('entry_name');
+ $list = $this->requireList('list');
+ $this->addListActions($list);
+ $listId = $list->get('id');
+ $listName = $list->get('list_name');
+ $title = $title = $this->translate('List Entries') . ': ' . $listName;
+ $this->addTitle($title);
+
+ $form = DirectorDatalistEntryForm::load()
+ ->setSuccessUrl('director/data/listentry', ['list' => $listName])
+ ->setList($list);
+
+ if (null !== $entryName) {
+ $form->loadObject([
+ 'list_id' => $listId,
+ 'entry_name' => $entryName
+ ]);
+ $this->actions()->add(Link::create(
+ $this->translate('back'),
+ 'director/data/listentry',
+ ['list' => $listName],
+ ['class' => 'icon-left-big']
+ ));
+ }
+ $form->handleRequest();
+
+ $this->addListTabs($listName, 'entries');
+
+ $table = new DatalistEntryTable($this->db());
+ $table->getAttributes()->set('data-base-target', '_self');
+ $table->setList($list);
+ $this->content()->add([$form, $table]);
+ }
+
+ public function dictionaryAction()
+ {
+ $connection = $this->db();
+ $this->addSingleTab('Nested Dictionary');
+ $varName = $this->params->get('varname');
+ $instance = $this->url()->getParam('instance');
+ $action = $this->url()->getParam('action');
+ $object = $this->requireObject();
+
+ if ($instance || $action) {
+ $this->actions()->add(
+ Link::create($this->translate('Back'), $this->url()->without(['action', 'instance']), null, [
+ 'class' => 'icon-edit'
+ ])
+ );
+ } else {
+ $this->actions()->add(
+ Link::create($this->translate('Add'), $this->url(), [
+ 'action' => 'add'
+ ], [
+ 'class' => 'icon-edit'
+ ])
+ );
+ }
+ $subjects = $this->prepareSubjectsLabel($object, $varName);
+ $fieldLoader = new IcingaObjectFieldLoader($object);
+ $instances = $this->getCurrentInstances($object, $varName);
+
+ if (empty($instances)) {
+ $this->content()->add(Hint::info(sprintf(
+ $this->translate('No %s have been created yet'),
+ $subjects
+ )));
+ } else {
+ $this->content()->add($this->prepareInstancesTable($instances));
+ }
+
+ $field = $this->getFieldByName($fieldLoader, $varName);
+ $template = $object::load([
+ 'object_name' => $field->getSetting('template_name')
+ ], $connection);
+
+ $form = new IcingaServiceDictionaryMemberForm();
+ $form->setDb($connection);
+ if ($instance) {
+ $instanceObject = $object::create([
+ 'imports' => [$template],
+ 'object_name' => $instance,
+ 'vars' => $instances[$instance]
+ ], $connection);
+ $form->setObject($instanceObject);
+ } elseif ($action === 'add') {
+ $form->presetImports([$template->getObjectName()]);
+ } else {
+ return;
+ }
+ if ($instance) {
+ if (! isset($instances[$instance])) {
+ throw new NotFoundError("There is no such instance: $instance");
+ }
+ $subTitle = sprintf($this->translate('Modify instance: %s'), $instance);
+ } else {
+ $subTitle = $this->translate('Add a new instance');
+ }
+
+ $this->content()->add(Html::tag('h2', ['style' => 'margin-top: 2em'], $subTitle));
+ $form->handleRequest($this->getRequest());
+ $this->content()->add($form);
+ if ($form->succeeded()) {
+ $virtualObject = $form->getObject();
+ $name = $virtualObject->getObjectName();
+ $params = $form->getObject()->getVars();
+ $instances[$name] = $params;
+ if ($name !== $instance) { // Has been renamed
+ unset($instances[$instance]);
+ }
+ ksort($instances);
+ $object->set("vars.$varName", (object)$instances);
+ $object->store();
+ $this->redirectNow($this->url()->without(['instance', 'action']));
+ } elseif ($form->shouldBeDeleted()) {
+ unset($instances[$instance]);
+ if (empty($instances)) {
+ $object->set("vars.$varName", null)->store();
+ } else {
+ $object->set("vars.$varName", (object)$instances)->store();
+ }
+ $this->redirectNow($this->url()->without(['instance', 'action']));
+ }
+ }
+
+ protected function requireObject()
+ {
+ $connection = $this->db();
+ $hostName = $this->params->getRequired('host');
+ $serviceName = $this->params->get('service');
+ if ($serviceName) {
+ $host = IcingaHost::load($hostName, $connection);
+ $object = IcingaService::load([
+ 'host_id' => $host->get('id'),
+ 'object_name' => $serviceName,
+ ], $connection);
+ } else {
+ $object = IcingaHost::load($hostName, $connection);
+ }
+
+ if (! $object->isObject()) {
+ throw new InvalidArgumentException(sprintf(
+ 'Only single objects allowed, %s is a %s',
+ $object->getObjectName(),
+ $object->get('object_type')
+ ));
+ }
+ return $object;
+ }
+
+ protected function shorten($string, $maxLen)
+ {
+ if (strlen($string) <= $maxLen) {
+ return $string;
+ }
+
+ return substr($string, 0, $maxLen) . '...';
+ }
+
+ protected function getFieldByName(IcingaObjectFieldLoader $loader, $name)
+ {
+ foreach ($loader->getFields() as $field) {
+ if ($field->get('varname') === $name) {
+ return $field;
+ }
+ }
+
+ throw new InvalidArgumentException("Found no configured field for '$name'");
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @param $varName
+ * @return array
+ */
+ protected function getCurrentInstances(IcingaObject $object, $varName)
+ {
+ $currentVars = $object->getVars();
+ if (isset($currentVars->$varName)) {
+ $currentValue = $currentVars->$varName;
+ } else {
+ $currentValue = (object)[];
+ }
+ if (is_object($currentValue)) {
+ $currentValue = (array)$currentValue;
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ '"%s" is not a valid Dictionary',
+ json_encode($currentValue)
+ ));
+ }
+ return $currentValue;
+ }
+
+ /**
+ * @param array $currentValue
+ * @param $subjects
+ * @return Hint|Table
+ */
+ protected function prepareInstancesTable(array $currentValue)
+ {
+ $table = new Table();
+ $table->addAttributes([
+ 'class' => 'common-table table-row-selectable'
+ ]);
+ $table->getHeader()->add(
+ Table::row([
+ $this->translate('Key / Instance'),
+ $this->translate('Properties')
+ ], ['style' => 'text-align: left'], 'th')
+ );
+ foreach ($currentValue as $key => $item) {
+ $table->add(Table::row([
+ Link::create($key, $this->url()->with('instance', $key)),
+ str_replace("\n", ' ', $this->shorten(PlainObjectRenderer::render($item), 512))
+ ]));
+ }
+
+ return $table;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @param $varName
+ * @return string
+ */
+ protected function prepareSubjectsLabel(IcingaObject $object, $varName)
+ {
+ if ($object instanceof IcingaService) {
+ $hostName = $object->get('host');
+ $subjects = $object->getObjectName() . " ($varName)";
+ } else {
+ $hostName = $object->getObjectName();
+ $subjects = sprintf(
+ $this->translate('%s instances'),
+ $varName
+ );
+ }
+ $this->addTitle(sprintf(
+ $this->translate('%s on %s'),
+ $subjects,
+ $hostName
+ ));
+ return $subjects;
+ }
+
+ protected function addListActions(DirectorDatalist $list)
+ {
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Add to Basket'),
+ 'director/basket/add',
+ [
+ 'type' => 'DataList',
+ 'names' => $list->getUniqueIdentifier()
+ ],
+ ['class' => 'icon-tag']
+ )
+ );
+ }
+
+ /**
+ * @param $paramName
+ * @return DirectorDatalist
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function requireList($paramName)
+ {
+ return DirectorDatalist::load($this->params->getRequired($paramName), $this->db());
+ }
+
+ protected function addListTabs($name, $activate)
+ {
+ $this->tabs()->add('list', [
+ 'url' => 'director/data/list',
+ 'urlParams' => ['name' => $name],
+ 'label' => $this->translate('Edit list'),
+ ])->add('entries', [
+ 'url' => 'director/data/listentry',
+ 'urlParams' => ['list' => $name],
+ 'label' => $this->translate('List entries'),
+ ])->activate($activate);
+
+ return $this;
+ }
+}
diff --git a/application/controllers/DatafieldController.php b/application/controllers/DatafieldController.php
new file mode 100644
index 0000000..afad317
--- /dev/null
+++ b/application/controllers/DatafieldController.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Forms\DirectorDatafieldForm;
+use Icinga\Module\Director\Web\Controller\ActionController;
+
+class DatafieldController extends ActionController
+{
+ public function addAction()
+ {
+ $this->indexAction();
+ }
+
+ public function editAction()
+ {
+ $this->indexAction();
+ }
+
+ public function indexAction()
+ {
+ $form = DirectorDatafieldForm::load()
+ ->setDb($this->db());
+
+ if ($id = $this->params->get('id')) {
+ $form->loadObject((int) $id);
+ $this->addTitle(
+ $this->translate('Modify %s'),
+ $form->getObject()->varname
+ );
+ $this->addSingleTab($this->translate('Edit a Field'));
+ } else {
+ $this->addTitle($this->translate('Add a new Data Field'));
+ $this->addSingleTab($this->translate('New Field'));
+ }
+
+ $form->handleRequest();
+ $this->content()->add($form);
+ }
+}
diff --git a/application/controllers/DatafieldcategoryController.php b/application/controllers/DatafieldcategoryController.php
new file mode 100644
index 0000000..32c76ef
--- /dev/null
+++ b/application/controllers/DatafieldcategoryController.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Forms\DirectorDatafieldCategoryForm;
+use Icinga\Module\Director\Web\Controller\ActionController;
+
+class DatafieldcategoryController extends ActionController
+{
+ public function addAction()
+ {
+ $this->indexAction();
+ }
+
+ public function editAction()
+ {
+ $this->indexAction();
+ }
+
+ public function indexAction()
+ {
+ $edit = false;
+
+ if ($name = $this->params->get('name')) {
+ $edit = true;
+ }
+
+ $form = DirectorDatafieldCategoryForm::load()
+ ->setDb($this->db());
+
+ if ($edit) {
+ $form->loadObject($name);
+ $this->addTitle(
+ $this->translate('Modify %s'),
+ $form->getObject()->category_name
+ );
+ $this->addSingleTab($this->translate('Edit a Category'));
+ } else {
+ $this->addTitle($this->translate('Add a new Data Field Category'));
+ $this->addSingleTab($this->translate('New Category'));
+ }
+
+ $form->handleRequest();
+ $this->content()->add($form);
+ }
+}
diff --git a/application/controllers/DependenciesController.php b/application/controllers/DependenciesController.php
new file mode 100644
index 0000000..276dd63
--- /dev/null
+++ b/application/controllers/DependenciesController.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class DependenciesController extends ObjectsController
+{
+ protected function addObjectsTabs()
+ {
+ $res = parent::addObjectsTabs();
+ $this->tabs()->remove('index');
+ return $res;
+ }
+}
diff --git a/application/controllers/DependencyController.php b/application/controllers/DependencyController.php
new file mode 100644
index 0000000..9d21cd5
--- /dev/null
+++ b/application/controllers/DependencyController.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Forms\IcingaDependencyForm;
+use Icinga\Module\Director\Web\Controller\ObjectController;
+use Icinga\Module\Director\Objects\IcingaDependency;
+
+class DependencyController extends ObjectController
+{
+ protected $apply;
+
+ /**
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function init()
+ {
+ parent::init();
+
+ if ($apply = $this->params->get('apply')) {
+ $this->apply = IcingaDependency::load(
+ array('object_name' => $apply, 'object_type' => 'template'),
+ $this->db()
+ );
+ }
+ }
+
+ /**
+ * @return \Icinga\Module\Director\Objects\IcingaObject
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\InvalidPropertyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function loadObject()
+ {
+ if ($this->object === null) {
+ if ($name = $this->params->get('name')) {
+ $params = array('object_name' => $name);
+ $db = $this->db();
+
+ $this->object = IcingaDependency::load($params, $db);
+ } else {
+ parent::loadObject();
+ }
+ }
+
+ return $this->object;
+ }
+
+ /**
+ * Hint: this is never being called. Why?
+ *
+ * @param $form
+ */
+ protected function beforeHandlingAddRequest($form)
+ {
+ /** @var IcingaDependencyForm $form */
+ if ($this->apply) {
+ $form->createApplyRuleFor($this->apply);
+ }
+ }
+}
diff --git a/application/controllers/DependencytemplateController.php b/application/controllers/DependencytemplateController.php
new file mode 100644
index 0000000..e2bc49d
--- /dev/null
+++ b/application/controllers/DependencytemplateController.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Objects\IcingaDependency;
+use Icinga\Module\Director\Web\Controller\TemplateController;
+
+class DependencytemplateController extends TemplateController
+{
+ protected function requireTemplate()
+ {
+ return IcingaDependency::load([
+ 'object_name' => $this->params->get('name')
+ ], $this->db());
+ }
+}
diff --git a/application/controllers/DeploymentController.php b/application/controllers/DeploymentController.php
new file mode 100644
index 0000000..2d35f3c
--- /dev/null
+++ b/application/controllers/DeploymentController.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+use Icinga\Module\Director\Web\Widget\DeploymentInfo;
+
+class DeploymentController extends ActionController
+{
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/deploy');
+ }
+
+ public function indexAction()
+ {
+ $info = new DeploymentInfo(DirectorDeploymentLog::load(
+ $this->params->get('id'),
+ $this->db()
+ ));
+ $this->addTitle($this->translate('Deployment details'));
+ $this->tabs(
+ $info->getTabs($this->getAuth(), $this->getRequest())
+ )->activate('deployment');
+ $this->content()->add($info);
+ }
+}
diff --git a/application/controllers/EndpointController.php b/application/controllers/EndpointController.php
new file mode 100644
index 0000000..e8a4fb0
--- /dev/null
+++ b/application/controllers/EndpointController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectController;
+
+class EndpointController extends ObjectController
+{
+}
diff --git a/application/controllers/EndpointsController.php b/application/controllers/EndpointsController.php
new file mode 100644
index 0000000..40501a4
--- /dev/null
+++ b/application/controllers/EndpointsController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class EndpointsController extends ObjectsController
+{
+}
diff --git a/application/controllers/HealthController.php b/application/controllers/HealthController.php
new file mode 100644
index 0000000..4fac4d2
--- /dev/null
+++ b/application/controllers/HealthController.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Tabs\MainTabs;
+use ipl\Html\Html;
+use Icinga\Module\Director\Web\Widget\HealthCheckPluginOutput;
+use Icinga\Module\Director\Health;
+use Icinga\Module\Director\Web\Controller\ActionController;
+
+class HealthController extends ActionController
+{
+ public function indexAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $this->tabs(new MainTabs($this->Auth(), $this->getDbResourceName()))->activate('health');
+ $this->setTitle($this->translate('Director Health'));
+ $health = new Health();
+ $health->setDbResourceName($this->getDbResourceName());
+ $output = new HealthCheckPluginOutput($health);
+ $this->content()->add($output);
+ $this->content()->add([
+ Html::tag('h1', ['class' => 'icon-pin'], $this->translate('Hint: Check Plugin')),
+ Html::tag('p', $this->translate(
+ 'Did you know that you can run this entire Health Check'
+ . ' (or just some sections) as an Icinga Check on a regular'
+ . ' base?'
+ ))
+ ]);
+ }
+}
diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php
new file mode 100644
index 0000000..e107d22
--- /dev/null
+++ b/application/controllers/HostController.php
@@ -0,0 +1,637 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Module\Director\Monitoring;
+use Icinga\Module\Director\Web\Table\ObjectsTableService;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Url;
+use gipfl\IcingaWeb2\Widget\Tabs;
+use Exception;
+use Icinga\Module\Director\CustomVariable\CustomVariableDictionary;
+use Icinga\Module\Director\Db\AppliedServiceSetLoader;
+use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder;
+use Icinga\Module\Director\Forms\IcingaAddServiceForm;
+use Icinga\Module\Director\Forms\IcingaServiceForm;
+use Icinga\Module\Director\Forms\IcingaServiceSetForm;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Restriction\HostgroupRestriction;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use Icinga\Module\Director\Web\Controller\ObjectController;
+use Icinga\Module\Director\Web\SelfService;
+use Icinga\Module\Director\Web\Table\IcingaHostAppliedForServiceTable;
+use Icinga\Module\Director\Web\Table\IcingaHostAppliedServicesTable;
+use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable;
+
+class HostController extends ObjectController
+{
+ protected function checkDirectorPermissions()
+ {
+ if ($this->isServiceAction() && (new Monitoring())->authCanEditService(
+ $this->Auth(),
+ $this->getParam('name'),
+ $this->getParam('service')
+ )) {
+ return;
+ }
+
+ if ($this->isServicesReadOnlyAction()) {
+ $this->assertPermission('director/monitoring/services-ro');
+ return;
+ }
+
+ if ($this->hasPermission('director/hosts')) { // faster
+ return;
+ }
+
+ if ($this->canModifyHostViaMonitoringPermissions($this->getParam('name'))) {
+ return;
+ }
+
+ $this->assertPermission('director/hosts'); // complain about default hosts permission
+ }
+
+ protected function isServicesReadOnlyAction()
+ {
+ return in_array($this->getRequest()->getActionName(), [
+ 'servicesro',
+ 'findservice',
+ 'invalidservice',
+ ]);
+ }
+
+ protected function isServiceAction()
+ {
+ return in_array($this->getRequest()->getActionName(), [
+ 'servicesro',
+ 'findservice',
+ 'invalidservice',
+ 'servicesetservice',
+ 'appliedservice',
+ 'inheritedservice',
+ ]);
+ }
+
+ protected function canModifyHostViaMonitoringPermissions($hostname)
+ {
+ if ($this->hasPermission('director/monitoring/hosts')) {
+ $monitoring = new Monitoring();
+ return $monitoring->authCanEditHost($this->Auth(), $hostname);
+ }
+
+ return false;
+ }
+
+ /**
+ * @return HostgroupRestriction
+ */
+ protected function getHostgroupRestriction()
+ {
+ return new HostgroupRestriction($this->db(), $this->Auth());
+ }
+
+ public function editAction()
+ {
+ parent::editAction();
+ $this->addOptionalMonitoringLink();
+ }
+
+ public function serviceAction()
+ {
+ $host = $this->getHostObject();
+ $this->addServicesHeader();
+ $this->addTitle($this->translate('Add Service to %s'), $host->getObjectName());
+ $this->content()->add(
+ IcingaAddServiceForm::load()
+ ->setBranch($this->getBranch())
+ ->setHost($host)
+ ->setDb($this->db())
+ ->handleRequest()
+ );
+ }
+
+ public function servicesetAction()
+ {
+ $host = $this->getHostObject();
+ $this->addServicesHeader();
+ $this->addTitle($this->translate('Add Service Set to %s'), $host->getObjectName());
+
+ $this->content()->add(
+ IcingaServiceSetForm::load()
+ ->setBranch($this->getBranch())
+ ->setHost($host)
+ ->setDb($this->db())
+ ->handleRequest()
+ );
+ }
+
+ protected function addServicesHeader()
+ {
+ $host = $this->getHostObject();
+ $hostname = $host->getObjectName();
+ $this->tabs()->activate('services');
+
+ $this->actions()->add(Link::create(
+ $this->translate('Add service'),
+ 'director/host/service',
+ ['name' => $hostname],
+ ['class' => 'icon-plus']
+ ))->add(Link::create(
+ $this->translate('Add service set'),
+ 'director/host/serviceset',
+ ['name' => $hostname],
+ ['class' => 'icon-plus']
+ ));
+ }
+
+ public function findserviceAction()
+ {
+ $host = $this->getHostObject();
+ $this->redirectNow(
+ (new ServiceFinder($host, $this->getAuth()))
+ ->getRedirectionUrl($this->params->get('service'))
+ );
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function invalidserviceAction()
+ {
+ if (! $this->showInfoForNonDirectorService()) {
+ $this->content()->add(Hint::error(sprintf(
+ $this->translate('No such service: %s'),
+ $this->params->get('service')
+ )));
+ }
+
+ $this->servicesAction();
+ }
+
+ protected function showInfoForNonDirectorService()
+ {
+ try {
+ $api = $this->getApiIfAvailable();
+ if ($api) {
+ $name = $this->params->get('name') . '!' . $this->params->get('service');
+ $info = $api->getObject($name, 'Services');
+ if (isset($info->attrs->source_location)) {
+ $source = $info->attrs->source_location;
+ $this->content()->add(Hint::info(Html::sprintf(
+ 'The configuration for this object has not been rendered by'
+ . ' Icinga Director. You can find it on line %s in %s.',
+ Html::tag('strong', null, $source->first_line),
+ Html::tag('strong', null, $source->path)
+ )));
+ }
+ }
+
+ return true;
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function servicesAction()
+ {
+ $this->addServicesHeader();
+ $host = $this->getHostObject();
+ $this->addTitle($this->translate('Services: %s'), $host->getObjectName());
+ $branch = $this->getBranch();
+ $hostHasBeenCreatedInBranch = $branch->isBranch() && $host->get('id');
+ $content = $this->content();
+ $table = (new ObjectsTableService($this->db()))
+ ->setAuth($this->Auth())
+ ->setHost($host)
+ ->setBranch($branch)
+ ->setTitle($this->translate('Individual Service objects'))
+ ->removeQueryLimit();
+
+ if (count($table)) {
+ $content->add($table);
+ }
+
+ /** @var IcingaHost[] $parents */
+ $parents = IcingaTemplateRepository::instanceByObject($this->object)
+ ->getTemplatesFor($this->object, true);
+ foreach ($parents as $parent) {
+ $table = (new ObjectsTableService($this->db()))
+ ->setAuth($this->Auth())
+ ->setBranch($branch)
+ ->setHost($parent)
+ ->setInheritedBy($host)
+ ->removeQueryLimit();
+
+ if (count($table)) {
+ $content->add(
+ $table->setTitle(sprintf(
+ $this->translate('Inherited from %s'),
+ $parent->getObjectName()
+ ))
+ );
+ }
+ }
+
+ if (! $hostHasBeenCreatedInBranch) {
+ $this->addHostServiceSetTables($host);
+ }
+ foreach ($parents as $parent) {
+ $this->addHostServiceSetTables($parent, $host);
+ }
+
+ $appliedSets = AppliedServiceSetLoader::fetchForHost($host);
+ foreach ($appliedSets as $set) {
+ $title = sprintf($this->translate('%s (Applied Service set)'), $set->getObjectName());
+
+ $content->add(
+ IcingaServiceSetServiceTable::load($set)
+ // ->setHost($host)
+ ->setBranch($branch)
+ ->setAffectedHost($host)
+ ->setTitle($title)
+ ->removeQueryLimit()
+ );
+ }
+
+ $table = IcingaHostAppliedServicesTable::load($host)
+ ->setTitle($this->translate('Applied services'));
+
+ if (count($table)) {
+ $content->add($table);
+ }
+ }
+
+ /**
+ * Hint: this duplicates quite some logic from servicesAction. We might want
+ * to clean this up, but as soon as we store fully resolved Services this
+ * will be obsolete anyways
+ *
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ * @throws \Icinga\Exception\MissingParameterException
+ */
+ public function servicesroAction()
+ {
+ $this->assertPermission('director/monitoring/services-ro');
+ $host = $this->getHostObject();
+ $service = $this->params->getRequired('service');
+ $db = $this->db();
+ $branch = $this->getBranch();
+ $this->controls()->setTabs(new Tabs());
+ $this->addSingleTab($this->translate('Configuration (read-only)'));
+ $this->addTitle($this->translate('Services on %s'), $host->getObjectName());
+ $content = $this->content();
+
+ $table = (new ObjectsTableService($db))
+ ->setAuth($this->Auth())
+ ->setHost($host)
+ ->setBranch($branch)
+ ->setReadonly()
+ ->highlightService($service)
+ ->setTitle($this->translate('Individual Service objects'));
+
+ if (count($table)) {
+ $content->add($table);
+ }
+
+ /** @var IcingaHost[] $parents */
+ $parents = IcingaTemplateRepository::instanceByObject($this->object)
+ ->getTemplatesFor($this->object, true);
+ foreach ($parents as $parent) {
+ $table = (new ObjectsTableService($db))
+ ->setReadonly()
+ ->setBranch($branch)
+ ->setHost($parent)
+ ->highlightService($service)
+ ->setInheritedBy($host);
+ if (count($table)) {
+ $content->add(
+ $table->setTitle(sprintf(
+ 'Inherited from %s',
+ $parent->getObjectName()
+ ))
+ );
+ }
+ }
+
+ $this->addHostServiceSetTables($host);
+ foreach ($parents as $parent) {
+ $this->addHostServiceSetTables($parent, $host, $service);
+ }
+
+ $appliedSets = AppliedServiceSetLoader::fetchForHost($host);
+ foreach ($appliedSets as $set) {
+ $title = sprintf($this->translate('%s (Applied Service set)'), $set->getObjectName());
+
+ $content->add(
+ IcingaServiceSetServiceTable::load($set)
+ // ->setHost($host)
+ ->setBranch($branch)
+ ->setAffectedHost($host)
+ ->setReadonly()
+ ->highlightService($service)
+ ->setTitle($title)
+ );
+ }
+
+ $table = IcingaHostAppliedServicesTable::load($host)
+ ->setReadonly()
+ ->highlightService($service)
+ ->setTitle($this->translate('Applied services'));
+
+ if (count($table)) {
+ $content->add($table);
+ }
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @param IcingaHost|null $affectedHost
+ */
+ protected function addHostServiceSetTables(IcingaHost $host, IcingaHost $affectedHost = null, $roService = null)
+ {
+ $db = $this->db();
+ if ($affectedHost === null) {
+ $affectedHost = $host;
+ }
+ if ($host->get('id') === null) {
+ return;
+ }
+
+ $query = $db->getDbAdapter()->select()
+ ->from(
+ array('ss' => 'icinga_service_set'),
+ 'ss.*'
+ )->join(
+ array('hsi' => 'icinga_service_set_inheritance'),
+ 'hsi.parent_service_set_id = ss.id',
+ array()
+ )->join(
+ array('hs' => 'icinga_service_set'),
+ 'hs.id = hsi.service_set_id',
+ array()
+ )->where('hs.host_id = ?', $host->get('id'));
+
+ $sets = IcingaServiceSet::loadAll($db, $query, 'object_name');
+ /** @var IcingaServiceSet $set*/
+ foreach ($sets as $name => $set) {
+ $title = sprintf($this->translate('%s (Service set)'), $name);
+ $table = IcingaServiceSetServiceTable::load($set)
+ ->setHost($host)
+ ->setBranch($this->getBranch())
+ ->setAffectedHost($affectedHost)
+ ->setTitle($title);
+ if ($roService) {
+ $table->setReadonly()->highlightService($roService);
+ }
+ $this->content()->add($table);
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function appliedserviceAction()
+ {
+ $db = $this->db();
+ $host = $this->getHostObject();
+ $serviceId = $this->params->get('service_id');
+ $parent = IcingaService::loadWithAutoIncId($serviceId, $db);
+ $serviceName = $parent->getObjectName();
+
+ $service = IcingaService::create([
+ 'imports' => $parent,
+ 'object_type' => 'apply',
+ 'object_name' => $serviceName,
+ 'host_id' => $host->get('id'),
+ 'vars' => $host->getOverriddenServiceVars($serviceName),
+ ], $db);
+
+ $this->addTitle(
+ $this->translate('Applied service: %s'),
+ $serviceName
+ );
+
+ $this->content()->add(
+ IcingaServiceForm::load()
+ ->setDb($db)
+ ->setBranch($this->getBranch())
+ ->setHost($host)
+ ->setApplyGenerated($parent)
+ ->setObject($service)
+ ->handleRequest()
+ );
+
+ $this->commonForServices();
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function inheritedserviceAction()
+ {
+ $db = $this->db();
+ $host = $this->getHostObject();
+ $serviceName = $this->params->get('service');
+ $from = IcingaHost::load($this->params->get('inheritedFrom'), $this->db());
+
+ $parent = IcingaService::load([
+ 'object_name' => $serviceName,
+ 'host_id' => $from->get('id')
+ ], $this->db());
+
+ // TODO: we want to eventually show the host template name, doesn't work
+ // as template resolution would break.
+ // $parent->object_name = $from->object_name;
+
+ $service = IcingaService::create([
+ 'object_type' => 'apply',
+ 'object_name' => $serviceName,
+ 'host_id' => $host->get('id'),
+ 'imports' => [$parent],
+ 'vars' => $host->getOverriddenServiceVars($serviceName),
+ ], $db);
+
+ $this->addTitle($this->translate('Inherited service: %s'), $serviceName);
+
+ $form = IcingaServiceForm::load()
+ ->setDb($db)
+ ->setBranch($this->getBranch())
+ ->setHost($host)
+ ->setInheritedFrom($from->getObjectName())
+ ->setObject($service)
+ ->handleRequest();
+ $this->content()->add($form);
+ $this->commonForServices();
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function removesetAction()
+ {
+ // TODO: clean this up, use POST
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()->from(
+ array('ss' => 'icinga_service_set'),
+ array('id' => 'ss.id')
+ )->join(
+ array('si' => 'icinga_service_set_inheritance'),
+ 'si.service_set_id = ss.id',
+ array()
+ )->where(
+ 'si.parent_service_set_id = ?',
+ $this->params->get('setId')
+ )->where('ss.host_id = ?', $this->object->get('id'));
+
+ IcingaServiceSet::loadWithAutoIncId($db->fetchOne($query), $this->db())->delete();
+ $this->redirectNow(
+ Url::fromPath('director/host/services', array(
+ 'name' => $this->object->getObjectName()
+ ))
+ );
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function servicesetserviceAction()
+ {
+ $db = $this->db();
+ $host = $this->getHostObject();
+ $serviceName = $this->params->get('service');
+ $setParams = [
+ 'object_name' => $this->params->get('set'),
+ 'host_id' => $host->get('id')
+ ];
+ $setTemplate = IcingaServiceSet::load($this->params->get('set'), $db);
+ if (IcingaServiceSet::exists($setParams, $db)) {
+ $set = IcingaServiceSet::load($setParams, $db);
+ } else {
+ $set = $setTemplate;
+ }
+
+ $service = IcingaService::load([
+ 'object_name' => $serviceName,
+ 'service_set_id' => $setTemplate->get('id')
+ ], $this->db());
+ $service = IcingaService::create([
+ 'id' => $service->get('id'),
+ 'object_type' => 'apply',
+ 'object_name' => $serviceName,
+ 'host_id' => $host->get('id'),
+ 'imports' => $service->listImportNames(),
+ 'vars' => $host->getOverriddenServiceVars($serviceName),
+ ], $db);
+
+ // $set->copyVarsToService($service);
+ $this->addTitle(
+ $this->translate('%s on %s (from set: %s)'),
+ $serviceName,
+ $host->getObjectName(),
+ $set->getObjectName()
+ );
+
+ $form = IcingaServiceForm::load()
+ ->setDb($db)
+ ->setBranch($this->getBranch())
+ ->setHost($host)
+ ->setServiceSet($set)
+ ->setObject($service)
+ ->handleRequest();
+ $this->tabs()->activate('services');
+ $this->content()->add($form);
+ $this->commonForServices();
+ }
+
+ protected function commonForServices()
+ {
+ $host = $this->object;
+ $this->actions()->add(Link::create(
+ $this->translate('back'),
+ 'director/host/services',
+ ['name' => $host->getObjectName()],
+ ['class' => 'icon-left-big']
+ ));
+ $this->tabs()->activate('services');
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function agentAction()
+ {
+ $selfService = new SelfService($this->getHostObject(), $this->api());
+ if ($os = $this->params->get('download')) {
+ $selfService->handleLegacyAgentDownloads($os);
+ return;
+ }
+
+ $selfService->renderTo($this);
+ $this->tabs()->activate('agent');
+ }
+
+ protected function addOptionalMonitoringLink()
+ {
+ $host = $this->object;
+ try {
+ $mon = $this->monitoring();
+ if ($host->isObject()
+ && $mon->isAvailable()
+ && $mon->hasHost($host->getObjectName())
+ ) {
+ $this->actions()->add(Link::create(
+ $this->translate('Show'),
+ 'monitoring/host/show',
+ ['host' => $host->getObjectName()],
+ [
+ 'class' => 'icon-globe critical',
+ 'data-base-target' => '_next'
+ ]
+ ));
+
+ // Intentionally placed here, show it only for deployed Hosts
+ $this->addOptionalInspectLink();
+ }
+ } catch (Exception $e) {
+ // Silently ignore errors in the monitoring module
+ }
+ }
+
+ protected function addOptionalInspectLink()
+ {
+ if (! $this->hasPermission('director/inspect')) {
+ return;
+ }
+
+ $this->actions()->add(Link::create(
+ $this->translate('Inspect'),
+ 'director/inspect/object',
+ [
+ 'type' => 'host',
+ 'plural' => 'hosts',
+ 'name' => $this->object->getObjectName()
+ ],
+ [
+ 'class' => 'icon-zoom-in',
+ 'data-base-target' => '_next'
+ ]
+ ));
+ }
+
+ /**
+ * @return IcingaHost
+ */
+ protected function getHostObject()
+ {
+ assert($this->object instanceof IcingaHost);
+ return $this->object;
+ }
+}
diff --git a/application/controllers/HostgroupController.php b/application/controllers/HostgroupController.php
new file mode 100644
index 0000000..aa4cc51
--- /dev/null
+++ b/application/controllers/HostgroupController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectController;
+
+class HostgroupController extends ObjectController
+{
+}
diff --git a/application/controllers/HostgroupsController.php b/application/controllers/HostgroupsController.php
new file mode 100644
index 0000000..2b4b417
--- /dev/null
+++ b/application/controllers/HostgroupsController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class HostgroupsController extends ObjectsController
+{
+}
diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php
new file mode 100644
index 0000000..0332072
--- /dev/null
+++ b/application/controllers/HostsController.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Forms\IcingaAddServiceForm;
+use Icinga\Module\Director\Forms\IcingaAddServiceSetForm;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+use gipfl\IcingaWeb2\Link;
+
+class HostsController extends ObjectsController
+{
+ protected $multiEdit = array(
+ 'imports',
+ 'groups',
+ 'disabled'
+ );
+
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/hosts');
+ }
+
+ public function editAction()
+ {
+ $url = clone($this->getRequest()->getUrl());
+ $url->setPath('director/hosts/addservice');
+
+ $urlSet = clone($url);
+ $urlSet->setPath('director/hosts/addserviceset');
+
+ parent::editAction();
+
+ $this->actions()->add(Link::create(
+ $this->translate('Add Service'),
+ $url,
+ null,
+ ['class' => 'icon-plus']
+ ))->add(Link::create(
+ $this->translate('Add Service Set'),
+ $urlSet,
+ null,
+ ['class' => 'icon-plus']
+ ));
+ }
+
+ public function edittemplatesAction()
+ {
+ parent::editAction();
+
+ $objects = $this->loadMultiObjectsFromParams();
+ $names = [];
+ /** @var ExportInterface $object */
+ foreach ($objects as $object) {
+ $names[] = $object->getUniqueIdentifier();
+ }
+
+ $url = Url::fromPath('director/basket/add', [
+ 'type' => 'HostTemplate',
+ ]);
+
+ $url->getParams()->addValues('names', $names);
+
+ $this->actions()->add(Link::create(
+ $this->translate('Add to Basket'),
+ $url,
+ null,
+ ['class' => 'icon-tag']
+ ));
+ }
+
+ public function addserviceAction()
+ {
+ $this->addSingleTab($this->translate('Add Service'));
+ $filter = Filter::fromQueryString($this->params->toString());
+
+ $objects = array();
+ $db = $this->db();
+ /** @var $filter FilterChain */
+ foreach ($filter->filters() as $sub) {
+ /** @var $sub FilterChain */
+ foreach ($sub->filters() as $ex) {
+ /** @var $ex FilterChain|FilterExpression */
+ if ($ex->isExpression() && $ex->getColumn() === 'name') {
+ $name = $ex->getExpression();
+ $objects[$name] = IcingaHost::load($name, $db);
+ }
+ }
+ }
+ $this->addTitle(
+ $this->translate('Add service to %d hosts'),
+ count($objects)
+ );
+
+ $this->content()->add(
+ IcingaAddServiceForm::load()
+ ->setHosts($objects)
+ ->setDb($this->db())
+ ->handleRequest()
+ );
+ }
+
+ public function addservicesetAction()
+ {
+ $this->addSingleTab($this->translate('Add Service Set'));
+ $filter = Filter::fromQueryString($this->params->toString());
+
+ $objects = array();
+ $db = $this->db();
+ /** @var $filter FilterChain */
+ foreach ($filter->filters() as $sub) {
+ /** @var $sub FilterChain */
+ foreach ($sub->filters() as $ex) {
+ /** @var $ex FilterChain|FilterExpression */
+ if ($ex->isExpression() && $ex->getColumn() === 'name') {
+ $name = $ex->getExpression();
+ $objects[$name] = IcingaHost::load($name, $db);
+ }
+ }
+ }
+ $this->addTitle(
+ $this->translate('Add Service Set to %d hosts'),
+ count($objects)
+ );
+
+ $this->content()->add(
+ IcingaAddServiceSetForm::load()
+ ->setHosts($objects)
+ ->setDb($this->db())
+ ->handleRequest()
+ );
+ }
+}
diff --git a/application/controllers/HosttemplateController.php b/application/controllers/HosttemplateController.php
new file mode 100644
index 0000000..a5bfc2b
--- /dev/null
+++ b/application/controllers/HosttemplateController.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Controller\TemplateController;
+
+class HosttemplateController extends TemplateController
+{
+ protected function requireTemplate()
+ {
+ return IcingaHost::load([
+ 'object_name' => $this->params->get('name')
+ ], $this->db());
+ }
+}
diff --git a/application/controllers/ImportrunController.php b/application/controllers/ImportrunController.php
new file mode 100644
index 0000000..d0e34e5
--- /dev/null
+++ b/application/controllers/ImportrunController.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Objects\ImportRun;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Table\ImportedrowsTable;
+
+class ImportrunController extends ActionController
+{
+ public function indexAction()
+ {
+ $importRun = ImportRun::load($this->params->getRequired('id'), $this->db());
+ $this->addTitle($this->translate('Import run'));
+ $this->addSingleTab($this->translate('Import run'));
+
+ $table = ImportedrowsTable::load($importRun);
+ if ($chosen = $this->params->get('chosenColumns')) {
+ $table->setColumns(preg_split('/,/', $chosen, -1, PREG_SPLIT_NO_EMPTY));
+ }
+
+ $table->renderTo($this);
+ }
+}
diff --git a/application/controllers/ImportsourceController.php b/application/controllers/ImportsourceController.php
new file mode 100644
index 0000000..cbddb9e
--- /dev/null
+++ b/application/controllers/ImportsourceController.php
@@ -0,0 +1,375 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Exception;
+use gipfl\Web\Widget\Hint;
+use Icinga\Module\Director\Data\Exporter;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Forms\ImportRowModifierForm;
+use Icinga\Module\Director\Forms\ImportSourceForm;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Web\ActionBar\AutomationObjectActionBar;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Controller\BranchHelper;
+use Icinga\Module\Director\Web\Form\CloneImportSourceForm;
+use Icinga\Module\Director\Web\Table\ImportrunTable;
+use Icinga\Module\Director\Web\Table\ImportsourceHookTable;
+use Icinga\Module\Director\Web\Table\PropertymodifierTable;
+use Icinga\Module\Director\Web\Tabs\ImportsourceTabs;
+use Icinga\Module\Director\Web\Widget\ImportSourceDetails;
+use InvalidArgumentException;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Error;
+use ipl\Html\Html;
+
+class ImportsourceController extends ActionController
+{
+ use BranchHelper;
+
+ /** @var ImportSource|null */
+ private $importSource;
+
+ private $id;
+
+ /**
+ * @throws \Icinga\Exception\AuthenticationException
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function init()
+ {
+ parent::init();
+ $id = $this->params->get('source_id', $this->params->get('id'));
+ if ($id !== null && is_numeric($id)) {
+ $this->id = (int) $id;
+ }
+
+ $tabs = $this->tabs(new ImportsourceTabs($this->id));
+ $action = $this->getRequest()->getActionName();
+ if ($tabs->has($action)) {
+ $tabs->activate($action);
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function addMainActions()
+ {
+ $this->actions(new AutomationObjectActionBar(
+ $this->getRequest()
+ ));
+ $source = $this->getImportSource();
+
+ $this->actions()->add(Link::create(
+ $this->translate('Add to Basket'),
+ 'director/basket/add',
+ [
+ 'type' => 'ImportSource',
+ 'names' => $source->getUniqueIdentifier()
+ ],
+ [
+ 'class' => 'icon-tag',
+ 'data-base-target' => '_next'
+ ]
+ ));
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function indexAction()
+ {
+ $this->addMainActions();
+ $source = $this->getImportSource();
+ if ($this->params->get('format') === 'json') {
+ $this->sendJson($this->getResponse(), (new Exporter($this->db()))->export($source));
+ return;
+ }
+ $this->addTitle(
+ $this->translate('Import source: %s'),
+ $source->get('source_name')
+ )->setAutorefreshInterval(10);
+ $branch = $this->getBranch();
+ if ($this->getBranch()->isBranch()) {
+ $this->content()->add(Hint::info(Html::sprintf($this->translate(
+ 'Please note that importing data will take place in your main Branch.'
+ . ' Modifications to Import Sources are not allowed while being in a Configuration Branch.'
+ . ' To get the full functionality, please deactivate %s'
+ ), Branch::requireHook()->linkToBranch($branch, $this->getAuth(), $branch->getName()))));
+ }
+ $this->content()->add(new ImportSourceDetails($source));
+ }
+
+ public function addAction()
+ {
+ $this->addTitle($this->translate('Add import source'));
+ if ($this->showNotInBranch($this->translate('Creating Import Sources'))) {
+ return;
+ }
+
+ $this->content()->add(
+ ImportSourceForm::load()->setDb($this->db())
+ ->setSuccessUrl('director/importsources')
+ ->handleRequest()
+ );
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function editAction()
+ {
+ $this->addMainActions();
+ $this->activateTabWithPostfix($this->translate('Modify'));
+ if ($this->showNotInBranch($this->translate('Modifying Import Sources'))) {
+ return;
+ }
+ $form = ImportSourceForm::load()
+ ->setObject($this->getImportSource())
+ ->setListUrl('director/importsources')
+ ->handleRequest();
+ $this->addTitle(
+ $this->translate('Import source: %s'),
+ $form->getObject()->get('source_name')
+ )->setAutorefreshInterval(10);
+
+ $this->content()->add($form);
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function cloneAction()
+ {
+ $this->addMainActions();
+ $this->activateTabWithPostfix($this->translate('Clone'));
+ if ($this->showNotInBranch($this->translate('Cloning Import Sources'))) {
+ return;
+ }
+ $source = $this->getImportSource();
+ $this->addTitle('Clone: %s', $source->get('source_name'));
+ $form = new CloneImportSourceForm($source);
+ $this->content()->add($form);
+ $form->on(CloneImportSourceForm::ON_SUCCESS, function (CloneImportSourceForm $form) {
+ $this->getResponse()->redirectAndExit($form->getSuccessUrl());
+ });
+ $form->handleRequest($this->getServerRequest());
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function previewAction()
+ {
+ $source = $this->getImportSource();
+
+ $this->addTitle(
+ $this->translate('Import source preview: %s'),
+ $source->get('source_name')
+ );
+ $fetchUrl = clone($this->url());
+
+ $this->actions()->add(Link::create(
+ $this->translate('Download JSON'),
+ $fetchUrl->setPath('director/importsource/fetch'),
+ null,
+ [
+ 'target' => '_blank',
+ 'class' => 'icon-download',
+ ]
+ ))->add(Link::create('[..]', '#', null, [
+ 'onclick' => 'javascript:$("table.raw-data-table").toggleClass("collapsed");'
+ ]));
+ try {
+ (new ImportsourceHookTable())->setImportSource($source)->renderTo($this);
+ } catch (Exception $e) {
+ $this->content()->add(Error::show($e));
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function fetchAction()
+ {
+ $response = $this->getResponse();
+ try {
+ $source = $this->getImportSource();
+ $source->checkForChanges();
+ $hook = ImportSourceHook::forImportSource($source);
+ $data = $hook->fetchData();
+ $source->applyModifiers($data);
+
+ $filename = sprintf(
+ "director-importsource-%d_%s.json",
+ $this->getParam('id'),
+ date('YmdHis')
+ );
+ $response->setHeader('Content-Type', 'application/json', true);
+ $response->setHeader('Content-disposition', "attachment; filename=$filename", true);
+ $response->sendHeaders();
+ $this->sendJson($this->getResponse(), $data);
+ } catch (Exception $e) {
+ $this->sendJsonError($response, $e->getMessage());
+ }
+ // TODO: this is not clean
+ if (\ob_get_level()) {
+ \ob_end_flush();
+ }
+ exit;
+ }
+
+ /**
+ * @return ImportSource
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function requireImportSourceAndAddModifierTable()
+ {
+ $source = $this->getImportSource();
+ $table = PropertymodifierTable::load($source, $this->url());
+ if ($this->getBranch()->isBranch()) {
+ $table->setReadOnly();
+ } else {
+ $table->handleSortPriorityActions($this->getRequest(), $this->getResponse());
+ }
+ $table->renderTo($this);
+
+ return $source;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function modifierAction()
+ {
+ $source = $this->requireImportSourceAndAddModifierTable();
+ $this->addTitle($this->translate('Property modifiers: %s'), $source->get('source_name'));
+ $this->addAddLink(
+ $this->translate('Add property modifier'),
+ 'director/importsource/addmodifier',
+ ['source_id' => $source->get('id')],
+ '_self'
+ );
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function historyAction()
+ {
+ $source = $this->getImportSource();
+ $this->addTitle($this->translate('Import run history: %s'), $source->get('source_name'));
+
+ // TODO: temporarily disabled, find a better place for stats:
+ // $this->view->stats = $this->db()->fetchImportStatistics();
+ ImportrunTable::load($source)->renderTo($this);
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function addmodifierAction()
+ {
+ $source = $this->requireImportSourceAndAddModifierTable();
+ $this->addTitle(
+ $this->translate('%s: add Property Modifier'),
+ $source->get('source_name')
+ )->addBackToModifiersLink($source);
+ $this->tabs()->activate('modifier');
+
+ if ($this->showNotInBranch($this->translate('Modifying Import Sources'))) {
+ return;
+ }
+
+ $this->content()->prepend(
+ ImportRowModifierForm::load()->setDb($this->db())
+ ->setSource($source)
+ ->setSuccessUrl(
+ 'director/importsource/modifier',
+ ['source_id' => $source->get('id')]
+ )->handleRequest()
+ );
+ }
+
+ /**
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function editmodifierAction()
+ {
+ // We need to load the table AFTER adding the title, otherwise search
+ // will not be placed next to the title
+ $source = $this->getImportSource();
+
+ $this->addTitle(
+ $this->translate('%s: Property Modifier'),
+ $source->get('source_name')
+ )->addBackToModifiersLink($source);
+ $source = $this->requireImportSourceAndAddModifierTable();
+ $this->tabs()->activate('modifier');
+ if ($this->showNotInBranch($this->translate('Modifying Import Sources'))) {
+ return;
+ }
+
+ $listUrl = 'director/importsource/modifier?source_id='
+ . (int) $source->get('id');
+ $this->content()->prepend(
+ ImportRowModifierForm::load()->setDb($this->db())
+ ->loadObject((int) $this->params->getRequired('id'))
+ ->setListUrl($listUrl)
+ ->setSource($source)
+ ->handleRequest()
+ );
+ }
+
+ /**
+ * @return ImportSource
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getImportSource()
+ {
+ if ($this->importSource === null) {
+ if ($this->id === null) {
+ throw new InvalidArgumentException('Got no ImportSource id');
+ }
+ $this->importSource = ImportSource::loadWithAutoIncId(
+ $this->id,
+ $this->db()
+ );
+ }
+
+ return $this->importSource;
+ }
+
+ protected function activateTabWithPostfix($title)
+ {
+ /** @var ImportsourceTabs $tabs */
+ $tabs = $this->tabs();
+ $tabs->activateMainWithPostfix($title);
+
+ return $this;
+ }
+
+ /**
+ * @param ImportSource $source
+ * @return $this
+ */
+ protected function addBackToModifiersLink(ImportSource $source)
+ {
+ $this->actions()->add(
+ Link::create(
+ $this->translate('back'),
+ 'director/importsource/modifier',
+ ['source_id' => $source->get('id')],
+ ['class' => 'icon-left-big']
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/controllers/ImportsourcesController.php b/application/controllers/ImportsourcesController.php
new file mode 100644
index 0000000..4287292
--- /dev/null
+++ b/application/controllers/ImportsourcesController.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\DirectorObject\Automation\ImportExport;
+use Icinga\Module\Director\Web\Table\ImportsourceTable;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Tabs\ImportTabs;
+
+class ImportsourcesController extends ActionController
+{
+ protected $isApified = true;
+
+ public function indexAction()
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ switch (strtolower($this->getRequest()->getMethod())) {
+ case 'get':
+ $this->sendExport();
+ break;
+ case 'post':
+ $this->acceptImport($this->getRequest()->getRawBody());
+ break;
+ // TODO: put / replace all?
+ default:
+ $this->sendUnsupportedMethod();
+ }
+
+ return;
+ }
+
+ $this->addTitle($this->translate('Import source'))
+ ->setAutoRefreshInterval(10)
+ ->addAddLink(
+ $this->translate('Add a new Import Source'),
+ 'director/importsource/add'
+ )->tabs(new ImportTabs())->activate('importsource');
+
+ (new ImportsourceTable($this->db()))->renderTo($this);
+ }
+
+ /**
+ * @param $raw
+ */
+ protected function acceptImport($raw)
+ {
+ (new ImportExport($this->db()))->unserializeImportSources(json_decode($raw));
+ }
+
+ protected function sendExport()
+ {
+ $this->sendJson(
+ $this->getResponse(),
+ (new ImportExport($this->db()))->serializeAllImportSources()
+ );
+ }
+}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
new file mode 100644
index 0000000..3f6c62e
--- /dev/null
+++ b/application/controllers/IndexController.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Exception;
+use gipfl\Web\Widget\Hint;
+use Icinga\Module\Director\Db\Migrations;
+use Icinga\Module\Director\Forms\ApplyMigrationsForm;
+use Icinga\Module\Director\Forms\KickstartForm;
+use ipl\Html\Html;
+
+class IndexController extends DashboardController
+{
+ protected $hasDeploymentEndpoint;
+
+ public function indexAction()
+ {
+ if ($this->Config()->get('db', 'resource')) {
+ $migrations = new Migrations($this->db());
+
+ if ($migrations->hasSchema()) {
+ if (!$this->hasDeploymentEndpoint()) {
+ $this->showKickstartForm();
+ }
+ }
+
+ if ($migrations->hasPendingMigrations()) {
+ $this->content()->prepend(
+ ApplyMigrationsForm::load()
+ ->setMigrations($migrations)
+ ->handleRequest()
+ );
+ } elseif ($migrations->hasBeenDowngraded()) {
+ $this->content()->add(Hint::warning(sprintf($this->translate(
+ 'Your DB schema (migration #%d) is newer than your code base.'
+ . ' Downgrading Icinga Director is not supported and might'
+ . ' lead to unexpected problems.'
+ ), $migrations->getLastMigrationNumber())));
+ }
+
+ if ($migrations->hasSchema()) {
+ parent::indexAction();
+ } else {
+ $this->addTitle(sprintf(
+ $this->translate('Icinga Director Setup: %s'),
+ $this->translate('Create Schema')
+ ));
+ $this->addSingleTab('Setup');
+ }
+ } else {
+ $this->addTitle(sprintf(
+ $this->translate('Icinga Director Setup: %s'),
+ $this->translate('Choose DB Resource')
+ ));
+ $this->addSingleTab('Setup');
+ $this->showKickstartForm();
+ }
+ }
+
+ protected function showKickstartForm()
+ {
+ $form = KickstartForm::load();
+ if ($name = $this->getPreferredDbResourceName()) {
+ $form->setDbResourceName($name);
+ }
+ $this->content()->prepend($form->handleRequest());
+ }
+
+ protected function hasDeploymentEndpoint()
+ {
+ try {
+ $this->hasDeploymentEndpoint = $this->db()->hasDeploymentEndpoint();
+ } catch (Exception $e) {
+ return false;
+ }
+
+ return $this->hasDeploymentEndpoint;
+ }
+}
diff --git a/application/controllers/InspectController.php b/application/controllers/InspectController.php
new file mode 100644
index 0000000..d631652
--- /dev/null
+++ b/application/controllers/InspectController.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\IcingaWeb2\Link;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\PlainObjectRenderer;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Table\CoreApiFieldsTable;
+use Icinga\Module\Director\Web\Table\CoreApiObjectsTable;
+use Icinga\Module\Director\Web\Table\CoreApiPrototypesTable;
+use Icinga\Module\Director\Web\Tabs\ObjectTabs;
+use Icinga\Module\Director\Web\Tree\InspectTreeRenderer;
+use Icinga\Module\Director\Web\Widget\IcingaObjectInspection;
+use Icinga\Module\Director\Web\Widget\InspectPackages;
+use ipl\Html\Html;
+
+class InspectController extends ActionController
+{
+ private $endpoint;
+
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/inspect');
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function typesAction()
+ {
+ $object = $this->endpoint();
+ $name = $object->getObjectName();
+ $this->tabs(
+ new ObjectTabs('endpoint', $this->Auth(), $object)
+ )->activate('inspect');
+
+ $this->addTitle($this->translate('Icinga 2 - Objects: %s'), $name);
+
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Status'),
+ 'director/inspect/status',
+ ['endpoint' => $name],
+ [
+ 'class' => 'icon-eye',
+ 'data-base-target' => '_next'
+ ]
+ )
+ );
+ $this->content()->add(
+ new InspectTreeRenderer($object)
+ );
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function typeAction()
+ {
+ $api = $this->endpoint()->api();
+ $typeName = $this->params->get('type');
+ $this->addSingleTab($this->translate('Inspect - object list'));
+ $this->addTitle(
+ $this->translate('Object type "%s"'),
+ $typeName
+ );
+ $c = $this->content();
+ $type = $api->getType($typeName);
+ if ($type->abstract) {
+ $c->add($this->translate('This is an abstract object type.'));
+ }
+
+ if (! $type->abstract) {
+ $objects = $api->listObjects($typeName, $type->plural_name);
+ $c->add(Html::tag('p', null, sprintf($this->translate('%d objects found'), count($objects))));
+ $c->add(new CoreApiObjectsTable($objects, $this->endpoint(), $type));
+ }
+
+ if (count((array) $type->fields)) {
+ $c->add([
+ Html::tag('h2', null, $this->translate('Type attributes')),
+ new CoreApiFieldsTable($type->fields, $this->url())
+ ]);
+ }
+
+ if (count($type->prototype_keys)) {
+ $c->add([
+ Html::tag('h2', null, $this->translate('Prototypes (methods)')),
+ new CoreApiPrototypesTable($type->prototype_keys, $type->name)
+ ]);
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function objectAction()
+ {
+ $name = $this->params->get('name');
+ $pType = $this->params->get('plural');
+ $this->addSingleTab($this->translate('Object Inspection'));
+ $this->addTitle('%s "%s"', $pType, $name);
+ $this->showEndpointInformation($this->endpoint());
+ $this->content()->add(
+ new IcingaObjectInspection(
+ $this->endpoint()->api()->getObject($name, $pType),
+ $this->db()
+ )
+ );
+ }
+
+ /**
+ * @param IcingaEndpoint $endpoint
+ */
+ protected function showEndpointInformation(IcingaEndpoint $endpoint)
+ {
+ $this->content()->add(
+ Html::tag('p', null, Html::sprintf(
+ 'Inspected via %s (%s)',
+ $this->linkToEndpoint($endpoint),
+ $endpoint->getDescriptiveUrl()
+ ))
+ );
+ }
+
+ /**
+ * @param IcingaEndpoint $endpoint
+ * @return Link
+ */
+ protected function linkToEndpoint(IcingaEndpoint $endpoint)
+ {
+ return Link::create($endpoint->getObjectName(), 'director/endpoint', [
+ 'name' => $endpoint->getObjectName()
+ ]);
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function statusAction()
+ {
+ $this->addSingleTab($this->translate('Status'));
+ $this->addTitle($this->translate('Icinga 2 API - Status'));
+ $this->content()->add(Html::tag(
+ 'pre',
+ null,
+ PlainObjectRenderer::render($this->endpoint()->api()->getStatus())
+ ));
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function packagesAction()
+ {
+ $db = $this->db();
+ $endpointName = $this->params->get('endpoint');
+ $package = $this->params->get('package');
+ $stage = $this->params->get('stage');
+ $file = $this->params->get('file');
+ if ($endpointName === null) {
+ $endpoint = null;
+ } else {
+ $endpoint = IcingaEndpoint::load($endpointName, $db);
+ }
+ if ($endpoint === null) {
+ $this->addSingleTab($this->translate('Inspect Packages'));
+ } elseif ($file !== null) {
+ $this->addSingleTab($this->translate('Inspect File Content'));
+ } else {
+ $this->tabs(
+ new ObjectTabs('endpoint', $this->Auth(), $endpoint)
+ )->activate('packages');
+ }
+ $widget = new InspectPackages($this->db(), 'director/inspect/packages');
+ $this->addTitle($widget->getTitle($endpoint, $package, $stage, $file));
+ if ($file === null) {
+ $this->actions()->add($widget->getBreadCrumb($endpoint, $package, $stage));
+ }
+ $this->content()->add($widget->getContent($endpoint, $package, $stage, $file));
+ }
+
+ /**
+ * @return IcingaEndpoint
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function endpoint()
+ {
+ if ($this->endpoint === null) {
+ if ($name = $this->params->get('endpoint')) {
+ $this->endpoint = IcingaEndpoint::load($name, $this->db());
+ } else {
+ $this->endpoint = $this->db()->getDeploymentEndpoint();
+ }
+ }
+
+ return $this->endpoint;
+ }
+}
diff --git a/application/controllers/JobController.php b/application/controllers/JobController.php
new file mode 100644
index 0000000..278c96b
--- /dev/null
+++ b/application/controllers/JobController.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\IcingaWeb2\Link;
+use Icinga\Module\Director\Forms\DirectorJobForm;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Web\Controller\BranchHelper;
+use Icinga\Module\Director\Web\Widget\JobDetails;
+
+class JobController extends ActionController
+{
+ use BranchHelper;
+
+ /**
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function indexAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $job = $this->requireJob();
+ $this
+ ->addJobTabs($job, 'show')
+ ->addTitle($this->translate('Job: %s'), $job->get('job_name'))
+ ->addToBasketLink()
+ ->content()->add(new JobDetails($job));
+ }
+
+ public function addAction()
+ {
+ $this
+ ->addSingleTab($this->translate('New Job'))
+ ->addTitle($this->translate('Add a new Job'));
+ if ($this->showNotInBranch($this->translate('Creating Jobs'))) {
+ return;
+ }
+
+ $this->content()->add(
+ DirectorJobForm::load()
+ ->setSuccessUrl('director/job')
+ ->setDb($this->db())
+ ->handleRequest()
+ );
+ }
+
+ /**
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function editAction()
+ {
+ $job = $this->requireJob();
+ $this
+ ->addJobTabs($job, 'edit')
+ ->addTitle($this->translate('Job: %s'), $job->get('job_name'))
+ ->addToBasketLink();
+ if ($this->showNotInBranch($this->translate('Modifying Jobs'))) {
+ return;
+ }
+
+ $form = DirectorJobForm::load()
+ ->setListUrl('director/jobs')
+ ->setObject($job)
+ ->handleRequest();
+ $this->content()->add($form);
+ }
+
+ /**
+ * @return DirectorJob
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Exception\MissingParameterException
+ */
+ protected function requireJob()
+ {
+ return DirectorJob::loadWithAutoIncId((int) $this->params->getRequired('id'), $this->db());
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function addToBasketLink()
+ {
+ $job = $this->requireJob();
+ $this->actions()->add(Link::create(
+ $this->translate('Add to Basket'),
+ 'director/basket/add',
+ [
+ 'type' => 'DirectorJob',
+ 'names' => $job->getUniqueIdentifier()
+ ],
+ ['class' => 'icon-tag']
+ ));
+
+ return $this;
+ }
+
+ protected function addJobTabs(DirectorJob $job, $active)
+ {
+ $id = $job->get('id');
+
+ $this->tabs()->add('show', [
+ 'url' => 'director/job',
+ 'urlParams' => ['id' => $id],
+ 'label' => $this->translate('Job'),
+ ])->add('edit', [
+ 'url' => 'director/job/edit',
+ 'urlParams' => ['id' => $id],
+ 'label' => $this->translate('Config'),
+ ])->activate($active);
+
+ return $this;
+ }
+}
diff --git a/application/controllers/JobsController.php b/application/controllers/JobsController.php
new file mode 100644
index 0000000..11e86ed
--- /dev/null
+++ b/application/controllers/JobsController.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Table\JobTable;
+use Icinga\Module\Director\Web\Tabs\ImportTabs;
+
+class JobsController extends ActionController
+{
+ public function indexAction()
+ {
+ $this->addTitle($this->translate('Jobs'))
+ ->setAutoRefreshInterval(10)
+ ->addAddLink($this->translate('Add a new Job'), 'director/job/add')
+ ->tabs(new ImportTabs())->activate('jobs');
+
+ (new JobTable($this->db()))->renderTo($this);
+ }
+}
diff --git a/application/controllers/KickstartController.php b/application/controllers/KickstartController.php
new file mode 100644
index 0000000..99cde1b
--- /dev/null
+++ b/application/controllers/KickstartController.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Exception;
+use Icinga\Module\Director\Forms\KickstartForm;
+use Icinga\Module\Director\Web\Controller\BranchHelper;
+
+class KickstartController extends DashboardController
+{
+ use BranchHelper;
+
+ public function indexAction()
+ {
+ $this->addSingleTab($this->translate('Kickstart'))
+ ->addTitle($this->translate('Director Kickstart Wizard'));
+ if ($this->showNotInBranch($this->translate('Kickstart'))) {
+ return;
+ }
+ $form = KickstartForm::load();
+ try {
+ $form->setEndpoint($this->db()->getDeploymentEndpoint());
+ } catch (Exception $e) {
+ // Silently ignore DB errors
+ }
+
+ $form->handleRequest();
+ $this->content()->add($form);
+ }
+}
diff --git a/application/controllers/NotificationController.php b/application/controllers/NotificationController.php
new file mode 100644
index 0000000..97fa0f4
--- /dev/null
+++ b/application/controllers/NotificationController.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectController;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaNotification;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class NotificationController extends ObjectController
+{
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/notifications');
+ }
+
+ // TODO: KILL IT
+ public function init()
+ {
+ parent::init();
+ // TODO: Check if this is still needed, remove it otherwise
+ /** @var \Icinga\Web\Widget\Tab $tab */
+ if ($this->object && $this->object->object_type === 'apply') {
+ if ($host = $this->params->get('host')) {
+ foreach ($this->getTabs()->getTabs() as $tab) {
+ $tab->getUrl()->setParam('host', $host);
+ }
+ }
+
+ if ($service = $this->params->get('service')) {
+ foreach ($this->getTabs()->getTabs() as $tab) {
+ $tab->getUrl()->setParam('service', $service);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param DirectorObjectForm $form
+ */
+ protected function onObjectFormLoaded(DirectorObjectForm $form)
+ {
+ if (! $this->object) {
+ return;
+ }
+
+ if ($this->object->isTemplate()) {
+ $form->setListUrl('director/notifications/templates');
+ } else {
+ $form->setListUrl('director/notifications/applyrules');
+ }
+ }
+
+ protected function hasBasketSupport()
+ {
+ return $this->object->isTemplate() || $this->object->isApplyRule();
+ }
+
+ protected function loadObject()
+ {
+ if ($this->object === null) {
+ if ($name = $this->params->get('name')) {
+ $params = array('object_name' => $name);
+ $db = $this->db();
+
+ if ($hostname = $this->params->get('host')) {
+ $this->view->host = IcingaHost::load($hostname, $db);
+ $params['host_id'] = $this->view->host->id;
+ }
+
+ if ($service = $this->params->get('service')) {
+ $this->view->service = IcingaService::load($service, $db);
+ $params['service_id'] = $this->view->service->id;
+ }
+
+ $this->object = IcingaNotification::load($params, $db);
+ } else {
+ parent::loadObject();
+ }
+ }
+
+ return $this->object;
+ }
+}
diff --git a/application/controllers/NotificationsController.php b/application/controllers/NotificationsController.php
new file mode 100644
index 0000000..2ddb360
--- /dev/null
+++ b/application/controllers/NotificationsController.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class NotificationsController extends ObjectsController
+{
+ protected function addObjectsTabs()
+ {
+ $res = parent::addObjectsTabs();
+ $this->tabs()->remove('index');
+ return $res;
+ }
+
+ public function indexAction()
+ {
+ throw new NotFoundError('Not found');
+ }
+
+ protected function assertApplyRulePermission()
+ {
+ return $this->assertPermission('director/notifications');
+ }
+
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/notifications');
+ }
+}
diff --git a/application/controllers/NotificationtemplateController.php b/application/controllers/NotificationtemplateController.php
new file mode 100644
index 0000000..0b8602c
--- /dev/null
+++ b/application/controllers/NotificationtemplateController.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Objects\IcingaNotification;
+use Icinga\Module\Director\Web\Controller\TemplateController;
+
+class NotificationtemplateController extends TemplateController
+{
+ protected function requireTemplate()
+ {
+ return IcingaNotification::load([
+ 'object_name' => $this->params->get('name')
+ ], $this->db());
+ }
+}
diff --git a/application/controllers/PhperrorController.php b/application/controllers/PhperrorController.php
new file mode 100644
index 0000000..40a32c1
--- /dev/null
+++ b/application/controllers/PhperrorController.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Director\Application\DependencyChecker;
+use Icinga\Module\Director\Web\Table\Dependency\DependencyInfoTable;
+use Icinga\Web\Controller;
+
+class PhperrorController extends Controller
+{
+ public function errorAction()
+ {
+ $this->getTabs()->add('error', array(
+ 'label' => $this->translate('Error'),
+ 'url' => $this->getRequest()->getUrl()
+ ))->activate('error');
+ $msg = $this->translate(
+ "PHP version 5.4.x is required for Director >= 1.4.0, you're running %s."
+ . ' Please either upgrade PHP or downgrade Icinga Director'
+ );
+ $this->view->title = $this->translate('Unsatisfied dependencies');
+ $this->view->message = sprintf($msg, PHP_VERSION);
+ }
+
+ public function dependenciesAction()
+ {
+ $checker = new DependencyChecker(Icinga::app());
+ if ($checker->satisfiesDependencies($this->Module())) {
+ $this->redirectNow('director');
+ }
+ $this->setAutorefreshInterval(15);
+ $this->getTabs()->add('error', [
+ 'label' => $this->translate('Error'),
+ 'url' => $this->getRequest()->getUrl()
+ ])->activate('error');
+ $this->view->title = $this->translate('Unsatisfied dependencies');
+ $this->view->table = (new DependencyInfoTable($checker, $this->Module()))->render();
+ $this->view->message = $this->translate(
+ "Icinga Director depends on the following modules, please install/upgrade as required"
+ );
+ }
+}
diff --git a/application/controllers/ScheduledDowntimeController.php b/application/controllers/ScheduledDowntimeController.php
new file mode 100644
index 0000000..e681a70
--- /dev/null
+++ b/application/controllers/ScheduledDowntimeController.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Forms\IcingaScheduledDowntimeRangeForm;
+use Icinga\Module\Director\Objects\IcingaScheduledDowntime;
+use Icinga\Module\Director\Web\Controller\ObjectController;
+use Icinga\Module\Director\Web\Table\IcingaScheduledDowntimeRangeTable;
+
+class ScheduledDowntimeController extends ObjectController
+{
+ protected $objectBaseUrl = 'director/scheduled-downtime';
+
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/scheduled-downtimes');
+ }
+
+ public function rangesAction()
+ {
+ /** @var IcingaScheduledDowntime $object */
+ $object = $this->object;
+ $this->tabs()->activate('ranges');
+ $this->addTitle($this->translate('Time period ranges'));
+ $form = IcingaScheduledDowntimeRangeForm::load()
+ ->setScheduledDowntime($object);
+
+ if (null !== ($name = $this->params->get('range'))) {
+ $this->addBackLink($this->url()->without('range'));
+ $form->loadObject([
+ 'scheduled_downtime_id' => $object->get('id'),
+ 'range_key' => $name,
+ 'range_type' => $this->params->get('range_type')
+ ]);
+ }
+
+ $this->content()->add($form->handleRequest());
+ IcingaScheduledDowntimeRangeTable::load($object)->renderTo($this);
+ }
+
+ public function getType()
+ {
+ return 'scheduledDowntime';
+ }
+}
diff --git a/application/controllers/ScheduledDowntimesController.php b/application/controllers/ScheduledDowntimesController.php
new file mode 100644
index 0000000..b6d314c
--- /dev/null
+++ b/application/controllers/ScheduledDowntimesController.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class ScheduledDowntimesController extends ObjectsController
+{
+ protected function addObjectsTabs()
+ {
+ $res = parent::addObjectsTabs();
+ $this->tabs()->remove('index');
+ $this->tabs()->remove('templates');
+ return $res;
+ }
+
+ protected function getTable()
+ {
+ return parent::getTable()
+ ->setBaseObjectUrl('director/scheduled-downtime');
+ }
+
+ protected function getApplyRulesTable()
+ {
+ return parent::getApplyRulesTable()->createLinksWithNames();
+ }
+
+ public function getType()
+ {
+ return 'scheduledDowntime';
+ }
+
+ public function getBaseObjectUrl()
+ {
+ return 'scheduled-downtime';
+ }
+
+ protected function assertApplyRulePermission()
+ {
+ return $this->assertPermission('director/scheduled-downtimes');
+ }
+
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/scheduled-downtimes');
+ }
+}
diff --git a/application/controllers/SchemaController.php b/application/controllers/SchemaController.php
new file mode 100644
index 0000000..b0ca24e
--- /dev/null
+++ b/application/controllers/SchemaController.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ActionController;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+
+class SchemaController extends ActionController
+{
+ protected $schemas;
+
+ public function init()
+ {
+ $this->schemas = [
+ 'mysql' => $this->translate('MySQL schema'),
+ 'pgsql' => $this->translate('PostgreSQL schema'),
+ ];
+ }
+
+ /**
+ * @throws \Icinga\Exception\IcingaException
+ */
+ public function mysqlAction()
+ {
+ $this->serveSchema('mysql');
+ }
+
+ /**
+ * @throws \Icinga\Exception\IcingaException
+ */
+ public function pgsqlAction()
+ {
+ $this->serveSchema('pgsql');
+ }
+
+ /**
+ * @param $type
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function serveSchema($type)
+ {
+ $schema = $this->loadSchema($type);
+
+ if ($this->params->get('format') === 'sql') {
+ header('Content-type: application/octet-stream');
+ header('Content-Disposition: attachment; filename=' . $type . '.sql');
+ echo $schema;
+ exit;
+ // TODO: Shutdown
+ }
+
+ $this
+ ->addSchemaTabs($type)
+ ->addTitle($this->schemas[$type])
+ ->addDownloadAction()
+ ->content()->add(Html::tag('pre', null, $schema));
+ }
+
+ protected function loadSchema($type)
+ {
+ return file_get_contents(
+ sprintf(
+ '%s/schema/%s.sql',
+ $this->Module()->getBasedir(),
+ $type
+ )
+ );
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ protected function addDownloadAction()
+ {
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Download'),
+ $this->url()->with('format', 'sql'),
+ null,
+ [
+ 'target' => '_blank',
+ 'class' => 'icon-download',
+ ]
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @param $active
+ * @return $this
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ protected function addSchemaTabs($active)
+ {
+ $tabs = $this->tabs();
+ foreach ($this->schemas as $type => $title) {
+ $tabs->add($type, [
+ 'url' => 'director/schema/' . $type,
+ 'label' => $title,
+ ]);
+ }
+
+ $tabs->activate($active);
+
+ return $this;
+ }
+}
diff --git a/application/controllers/SelfServiceController.php b/application/controllers/SelfServiceController.php
new file mode 100644
index 0000000..0b3b642
--- /dev/null
+++ b/application/controllers/SelfServiceController.php
@@ -0,0 +1,435 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Exception;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Forms\IcingaHostSelfServiceForm;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaZone;
+use Icinga\Module\Director\Settings;
+use Icinga\Module\Director\Util;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use ipl\Html\Html;
+
+class SelfServiceController extends ActionController
+{
+ /** @var bool */
+ protected $isApified = true;
+
+ /** @var bool */
+ protected $requiresAuthentication = false;
+
+ /** @var Settings */
+ protected $settings;
+
+ protected function assertApiPermission()
+ {
+ // no permission required, we'll check the API key
+ }
+
+ protected function checkDirectorPermissions()
+ {
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws ProgrammingError
+ * @throws \Zend_Controller_Request_Exception
+ */
+ public function apiVersionAction()
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ $this->sendPowerShellResponse('1.4.0');
+ } else {
+ throw new NotFoundError('Not found');
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Zend_Controller_Exception
+ */
+ public function registerHostAction()
+ {
+ $request = $this->getRequest();
+ $form = IcingaHostSelfServiceForm::create($this->db());
+ $form->setApiRequest($request->isApiRequest());
+ try {
+ if ($key = $this->params->get('key')) {
+ $form->loadTemplateWithApiKey($key);
+ }
+ } catch (Exception $e) {
+ $this->sendPowerShellError($e->getMessage(), 404);
+ return;
+ }
+ if ($name = $this->params->get('name')) {
+ $form->setHostName($name);
+ }
+
+ if ($request->isApiRequest()) {
+ $data = json_decode($request->getRawBody());
+ $request->setPost((array) $data);
+ $form->handleRequest();
+ if ($newKey = $form->getHostApiKey()) {
+ $this->sendPowerShellResponse($newKey);
+ } else {
+ $error = implode('; ', $form->getErrorMessages());
+ if ($error === '') {
+ if ($form->isMissingRequiredFields()) {
+ $fields = $form->listMissingRequiredFields();
+ if (count($fields) === 1) {
+ $this->sendPowerShellError(
+ sprintf("%s is required", $fields[0]),
+ 400
+ );
+ } else {
+ $this->sendPowerShellError(
+ sprintf("Missing parameters: %s", implode(', ', $fields)),
+ 400
+ );
+ }
+ return;
+ } else {
+ $this->sendPowerShellError('An unknown error ocurred', 500);
+ }
+ } else {
+ $this->sendPowerShellError($error, 400);
+ }
+ }
+ return;
+ }
+
+ $form->handleRequest();
+ $this->addSingleTab($this->translate('Self Service'))
+ ->addTitle($this->translate('Self Service - Host Registration'))
+ ->content()->add(Html::tag('p', null, $this->translate(
+ 'In case an Icinga Admin provided you with a self service API'
+ . ' token, this is where you can register new hosts'
+ )))
+ ->add($form);
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Zend_Controller_Request_Exception
+ * @throws \Zend_Controller_Response_Exception
+ */
+ public function ticketAction()
+ {
+ if (!$this->getRequest()->isApiRequest()) {
+ throw new NotFoundError('Not found');
+ }
+
+ try {
+ $key = $this->params->getRequired('key');
+ $host = IcingaHost::loadWithApiKey($key, $this->db());
+ if ($host->isTemplate()) {
+ throw new NotFoundError('Got invalid API key "%s"', $key);
+ }
+ $name = $host->getObjectName();
+
+ if ($host->getResolvedProperty('has_agent') !== 'y') {
+ throw new NotFoundError('The host "%s" is not an agent', $name);
+ }
+
+ $this->sendPowerShellResponse($this->api()->getTicket($name));
+ } catch (Exception $e) {
+ if ($e instanceof NotFoundError) {
+ $this->sendPowerShellError($e->getMessage(), 404);
+ } else {
+ $this->sendPowerShellError($e->getMessage(), 500);
+ }
+ }
+ }
+
+ /**
+ * @param $response
+ * @throws ProgrammingError
+ * @throws \Zend_Controller_Request_Exception
+ */
+ protected function sendPowerShellResponse($response)
+ {
+ if ($this->getRequest()->getHeader('X-Director-Accept') === 'text/plain') {
+ if (is_array($response)) {
+ echo $this->makePlainTextPowerShellArray($response);
+ } else {
+ echo $response;
+ }
+ } else {
+ $this->sendJson($this->getResponse(), $response);
+ }
+ }
+
+ /**
+ * @param $error
+ * @param $code
+ * @throws \Zend_Controller_Request_Exception
+ * @throws \Zend_Controller_Response_Exception
+ */
+ protected function sendPowerShellError($error, $code)
+ {
+ if ($this->getRequest()->getHeader('X-Director-Accept') === 'text/plain') {
+ $this->getResponse()->setHttpResponseCode($code);
+ echo "ERROR: $error";
+ } else {
+ $this->sendJsonError($this->getResponse(), $error, $code);
+ }
+ }
+
+ /**
+ * @param $value
+ * @return string
+ * @throws ProgrammingError
+ */
+ protected function makePowerShellBoolean($value)
+ {
+ if ($value === 'y' || $value === true) {
+ return 'true';
+ } elseif ($value === 'n' || $value === false) {
+ return 'false';
+ } else {
+ throw new ProgrammingError(
+ 'Expected boolean value, got %s',
+ var_export($value, 1)
+ );
+ }
+ }
+
+ /**
+ * @param array $params
+ * @return string
+ * @throws ProgrammingError
+ */
+ protected function makePlainTextPowerShellArray(array $params)
+ {
+ $plain = '';
+
+ foreach ($params as $key => $value) {
+ if (is_bool($value)) {
+ $value = $this->makePowerShellBoolean($value);
+ } elseif (is_array($value)) {
+ $value = implode('!', $value);
+ }
+ $plain .= "$key: $value\r\n";
+ }
+
+ return $plain;
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Zend_Controller_Request_Exception
+ * @throws \Zend_Controller_Response_Exception
+ */
+ public function powershellParametersAction()
+ {
+ if (!$this->getRequest()->isApiRequest()) {
+ throw new NotFoundError('Not found');
+ }
+
+ try {
+ $this->shipPowershellParams();
+ } catch (Exception $e) {
+ if ($e instanceof NotFoundError) {
+ $this->sendPowerShellError($e->getMessage(), 404);
+ } else {
+ $this->sendPowerShellError($e->getMessage(), 500);
+ }
+ }
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws ProgrammingError
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Zend_Controller_Request_Exception
+ * @throws \Zend_Controller_Response_Exception
+ */
+ protected function shipPowershellParams()
+ {
+ $db = $this->db();
+ $key = $this->params->getRequired('key');
+ $host = IcingaHost::loadWithApiKey($key, $db);
+
+ $settings = $this->getSettings();
+ $transform = $settings->get('self-service/transform_hostname');
+ $params = [
+ 'fetch_agent_name' => $settings->get('self-service/agent_name') === 'hostname',
+ 'fetch_agent_fqdn' => $settings->get('self-service/agent_name') === 'fqdn',
+ 'transform_hostname' => $transform,
+ 'flush_api_directory' => $settings->get('self-service/flush_api_dir') === 'y',
+ // ConvertEndpointIPConfig:
+ 'resolve_parent_host' => $settings->get('self-service/resolve_parent_host'),
+ // InstallFrameworkService:
+ 'install_framework_service' => '0',
+ // ServiceDirectory => framework_service_directory
+ // FrameworkServiceUrl => framework_service_url
+ // InstallFrameworkPlugins:
+ 'install_framework_plugins' => '0',
+ // PluginsUrl => framework_plugins_url
+ ];
+ $username = $settings->get('self-service/icinga_service_user');
+ if ($username !== null && strlen($username) > 0) {
+ $params['icinga_service_user'] = $username;
+ }
+
+ if ($transform === '2') {
+ $transformMethod = '.upperCase';
+ } elseif ($transform === '1') {
+ $transformMethod = '.lowerCase';
+ } else {
+ $transformMethod = '';
+ }
+
+ $hostObject = (object) [
+ 'address' => '&ipaddress&',
+ ];
+
+ switch ($settings->get('self-service/agent_name')) {
+ case 'hostname':
+ $hostObject->display_name = "&fqdn$transformMethod&";
+ break;
+ case 'fqdn':
+ $hostObject->display_name = "&hostname$transformMethod&";
+ break;
+ }
+ $params['director_host_object'] = json_encode($hostObject);
+
+ if ($settings->get('self-service/download_type')) {
+ $params['download_url'] = $settings->get('self-service/download_url');
+ $params['agent_version'] = $settings->get('self-service/agent_version');
+ $params['allow_updates'] = $settings->get('self-service/allow_updates') === 'y';
+ $params['agent_listen_port'] = $host->getAgentListenPort();
+ if ($hashes = $settings->get('self-service/installer_hashes')) {
+ $params['installer_hashes'] = $hashes;
+ }
+
+ if ($settings->get('self-service/install_nsclient') === 'y') {
+ $params['install_nsclient'] = true;
+ $this->addBooleanSettingsToParams($settings, [
+ 'nsclient_add_defaults',
+ 'nsclient_firewall',
+ 'nsclient_service',
+ ], $params);
+
+
+ $this->addStringSettingsToParams($settings, [
+ 'nsclient_directory',
+ 'nsclient_installer_path'
+ ], $params);
+ }
+ }
+
+ $this->addHostToParams($host, $params);
+
+ if ($this->getRequest()->getHeader('X-Director-Accept') === 'text/plain') {
+ echo $this->makePlainTextPowerShellArray($params);
+ } else {
+ $this->sendJson($this->getResponse(), $params);
+ }
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @param array $params
+ * @throws NotFoundError
+ * @throws ProgrammingError
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Zend_Controller_Request_Exception
+ * @throws \Zend_Controller_Response_Exception
+ */
+ protected function addHostToParams(IcingaHost $host, array &$params)
+ {
+ if (! $host->isObject()) {
+ return;
+ }
+
+ $db = $this->db();
+ $settings = $this->getSettings();
+ $name = $host->getObjectName();
+ if ($host->getSingleResolvedProperty('has_agent') !== 'y') {
+ $this->sendPowerShellError(sprintf(
+ '%s is not configured for Icinga Agent usage',
+ $name
+ ), 403);
+ return;
+ }
+
+ $zoneName = $host->getRenderingZone();
+ if ($zoneName === IcingaHost::RESOLVE_ERROR) {
+ $this->sendPowerShellError(sprintf(
+ 'Could not resolve target Zone for %s',
+ $name
+ ), 404);
+ return;
+ }
+
+ $masterConnectsToAgent = $host->getSingleResolvedProperty(
+ 'master_should_connect'
+ ) === 'y';
+ $params['agent_add_firewall_rule'] = $masterConnectsToAgent;
+
+ $params['global_zones'] = $settings->get('self-service/global_zones');
+
+ $zone = IcingaZone::load($zoneName, $db);
+ $endpointNames = $zone->listEndpoints();
+ if (! $masterConnectsToAgent) {
+ $endpointsConfig = [];
+ foreach ($endpointNames as $endpointName) {
+ $endpoint = IcingaEndpoint::load($endpointName, $db);
+ $endpointsConfig[] = sprintf(
+ '%s;%s',
+ $endpoint->getSingleResolvedProperty('host'),
+ $endpoint->getResolvedPort()
+ );
+ }
+
+ $params['endpoints_config'] = $endpointsConfig;
+ }
+ $master = $db->getDeploymentEndpoint();
+ $params['parent_zone'] = $zoneName;
+ $params['ca_server'] = $master->getObjectName();
+ $params['parent_endpoints'] = $endpointNames;
+ $params['accept_config'] = $host->getSingleResolvedProperty('accept_config')=== 'y';
+ }
+
+ protected function addStringSettingsToParams(Settings $settings, array $keys, array &$params)
+ {
+ foreach ($keys as $key) {
+ $value = $settings->get("self-service/$key");
+ if (strlen($value)) {
+ $params[$key] = $value;
+ }
+ }
+ }
+
+ protected function addBooleanSettingsToParams(Settings $settings, array $keys, array &$params)
+ {
+ foreach ($keys as $key) {
+ $value = $settings->get("self-service/$key");
+ if ($value !== null) {
+ $params[$key] = $value === 'y';
+ }
+ }
+ }
+
+ /**
+ * @return Settings
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ protected function getSettings()
+ {
+ if ($this->settings === null) {
+ $this->settings = new Settings($this->db());
+ }
+
+ return $this->settings;
+ }
+}
diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php
new file mode 100644
index 0000000..3cd54d6
--- /dev/null
+++ b/application/controllers/ServiceController.php
@@ -0,0 +1,311 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Exception;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db\Branch\UuidLookup;
+use Icinga\Module\Director\Forms\IcingaServiceForm;
+use Icinga\Module\Director\Monitoring;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Controller\ObjectController;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Table\IcingaAppliedServiceTable;
+use Icinga\Web\Widget\Tab;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class ServiceController extends ObjectController
+{
+ /** @var IcingaHost */
+ protected $host;
+
+ protected $set;
+
+ protected $apply;
+
+ protected function checkDirectorPermissions()
+ {
+ if ($this->hasPermission('director/monitoring/services')) {
+ $monitoring = new Monitoring();
+ if ($monitoring->authCanEditService($this->Auth(), $this->getParam('host'), $this->getParam('name'))) {
+ return;
+ }
+ }
+ $this->assertPermission('director/hosts');
+ }
+
+ public function init()
+ {
+ // This happens in parent::init() too, but is required to take place before the next two lines
+ $this->enableStaticObjectLoader($this->getTableName());
+
+ // Hint: having Host and Set loaded first is important for UUID lookups with legacy URLs
+ $this->host = $this->getOptionalRelatedObjectFromParams('host', 'host');
+ $this->set = $this->getOptionalRelatedObjectFromParams('service_set', 'set');
+ parent::init();
+ if ($this->object) {
+ if ($this->host === null) {
+ $this->host = $this->loadOptionalRelatedObject($this->object, 'host');
+ }
+ if ($this->set === null) {
+ $this->set = $this->loadOptionalRelatedObject($this->object, 'service_set');
+ }
+ }
+ $this->addOptionalHostTabs();
+ $this->addOptionalSetTabs();
+ }
+
+ protected function getOptionalRelatedObjectFromParams($type, $parameter)
+ {
+ if ($id = $this->params->get("${parameter}_id")) {
+ $key = (int) $id;
+ } else {
+ $key = $this->params->get($parameter);
+ }
+ if ($key !== null) {
+ $table = DbObjectTypeRegistry::tableNameByType($type);
+ $key = UuidLookup::findUuidForKey($key, $table, $this->db(), $this->getBranch());
+ return $this->loadSpecificObject($table, $key);
+ }
+
+ return null;
+ }
+
+ protected function loadOptionalRelatedObject(IcingaObject $object, $relation)
+ {
+ $key = $object->getUnresolvedRelated($relation);
+ if ($key === null) {
+ if ($key = $object->get("${relation}_id")) {
+ $key = (int) $key;
+ } else {
+ $key = $object->get($relation);
+ // We reach this when accessing Service Template Fields
+ }
+ }
+
+ if ($key === null) {
+ return null;
+ }
+
+ $table = DbObjectTypeRegistry::tableNameByType($relation);
+ $uuid = UuidLookup::findUuidForKey($key, $table, $this->db(), $this->getBranch());
+ return $this->loadSpecificObject($table, $uuid);
+ }
+
+ protected function addParamToTabs($name, $value)
+ {
+ foreach ($this->tabs()->getTabs() as $tab) {
+ /** @var Tab $tab */
+ $tab->getUrl()->setParam($name, $value);
+ }
+
+ return $this;
+ }
+
+ public function addAction()
+ {
+ parent::addAction();
+ if ($this->host) {
+ // TODO: use setTitle. And figure out, where we use this old route.
+ $this->view->title = $this->host->object_name . ': ' . $this->view->title;
+ } elseif ($this->set) {
+ $this->view->title = sprintf(
+ $this->translate('Add a service to "%s"'),
+ $this->set->object_name
+ );
+ } elseif ($this->apply) {
+ $this->view->title = sprintf(
+ $this->translate('Apply "%s"'),
+ $this->apply->object_name
+ );
+ }
+ }
+
+ protected function onObjectFormLoaded(DirectorObjectForm $form)
+ {
+ if ($this->set) {
+ /** @var IcingaServiceForm$form */
+ $form->setServiceSet($this->set);
+ }
+ if ($this->object === null && $this->apply) {
+ $form->createApplyRuleFor($this->apply);
+ }
+ }
+
+ public function editAction()
+ {
+ $this->tabs()->activate('modify');
+
+ /** @var IcingaService $object */
+ $object = $this->object;
+ $this->addTitle($object->getObjectName());
+ if ($object->isTemplate() && $this->showNotInBranch($this->translate('Modifying Templates'))) {
+ return;
+ }
+
+ $form = IcingaServiceForm::load()->setDb($this->db());
+ $form->setBranch($this->getBranch());
+
+ if ($this->host) {
+ $this->actions()->add(Link::create(
+ $this->translate('back'),
+ 'director/host/services',
+ ['uuid' => $this->host->getUniqueId()->toString()],
+ ['class' => 'icon-left-big']
+ ));
+ $form->setHost($this->host);
+ }
+
+ if ($this->set) {
+ $form->setServiceSet($this->set);
+ }
+ if ($this->host && $object->usesVarOverrides()) {
+ $fake = IcingaService::create(array(
+ 'object_type' => 'object',
+ 'host_id' => $object->get('host_id'),
+ 'imports' => $object,
+ 'object_name' => $object->object_name,
+ 'use_var_overrides' => 'y',
+ 'vars' => $this->host->getOverriddenServiceVars($object->object_name),
+ ), $this->db());
+
+ $form->setObject($fake);
+ } else {
+ $form->setObject($object);
+ }
+
+ $form->handleRequest();
+ $this->addActionClone();
+
+ if ($this->host) {
+ $this->view->subtitle = sprintf(
+ $this->translate('(on %s)'),
+ $this->host->object_name
+ );
+ }
+
+ try {
+ if ($object->isTemplate()
+ && $object->getResolvedProperty('check_command_id')
+ ) {
+ $this->view->actionLinks .= ' ' . $this->view->qlink(
+ 'Create apply-rule',
+ 'director/service/add',
+ array('apply' => $object->object_name),
+ array('class' => 'icon-plus')
+ );
+ }
+ } catch (Exception $e) {
+ // ignore the error, show no apply link
+ }
+
+ $this->content()->add($form);
+ }
+
+ public function assignAction()
+ {
+ // TODO: figure out whether and where we link to this
+ /** @var IcingaService $service */
+ $service = $this->object;
+ $this->actions()->add(new Link(
+ $this->translate('back'),
+ $this->getRequest()->getUrl()->without('rule_id'),
+ null,
+ array('class' => 'icon-left-big')
+ ));
+
+ $this->tabs()->activate('applied');
+ $this->addTitle(
+ $this->translate('Apply: %s'),
+ $service->getObjectName()
+ );
+ $table = (new IcingaAppliedServiceTable($this->db()))
+ ->setService($service);
+ $table->getAttributes()->set('data-base-target', '_self');
+
+ $this->content()->add($table);
+ }
+
+ protected function getLegacyKey()
+ {
+ if ($key = $this->params->get('id')) {
+ $key = (int) $key;
+ } else {
+ $key = $this->params->get('name');
+ }
+
+ if ($key === null) {
+ throw new \InvalidArgumentException('uuid, name or id required');
+ }
+
+ return $key;
+ }
+
+ protected function loadObject()
+ {
+ if ($this->params->has('uuid')) {
+ parent::loadObject();
+ return;
+ }
+
+ $key = $this->getLegacyKey();
+ // Hint: not passing 'object' as type, we still have name-based links in previews and similar
+ $uuid = UuidLookup::findServiceUuid($this->db(), $this->getBranch(), null, $key, $this->host, $this->set);
+ if ($uuid === null) {
+ if (! $this->params->get('allowOverrides')) {
+ throw new NotFoundError('Not found');
+ }
+ } else {
+ $this->params->set('uuid', $uuid->toString());
+ parent::loadObject();
+ }
+ }
+
+ protected function addOptionalHostTabs()
+ {
+ if ($this->host === null) {
+ return;
+ }
+ $hostname = $this->host->getObjectName();
+ $tabs = new Tabs();
+ $urlParams = ['uuid' => $this->host->getUniqueId()->toString()];
+ $tabs->add('host', [
+ 'url' => 'director/host',
+ 'urlParams' => $urlParams,
+ 'label' => $this->translate('Host'),
+ ])->add('services', [
+ 'url' => 'director/host/services',
+ 'urlParams' => $urlParams,
+ 'label' => $this->translate('Services'),
+ ]);
+
+ $this->addParamToTabs('host', $hostname);
+ $this->controls()->prependTabs($tabs);
+ }
+
+ protected function addOptionalSetTabs()
+ {
+ if ($this->set === null) {
+ return;
+ }
+ $setName = $this->set->getObjectName();
+ $tabs = new Tabs();
+ $tabs->add('set', [
+ 'url' => 'director/serviceset',
+ 'urlParams' => ['name' => $setName],
+ 'label' => $this->translate('ServiceSet'),
+ ])->add('services', [
+ 'url' => 'director/serviceset/services',
+ 'urlParams' => ['name' => $setName],
+ 'label' => $this->translate('Services'),
+ ]);
+
+ $this->addParamToTabs('serviceset', $setName);
+ $this->controls()->prependTabs($tabs);
+ }
+}
diff --git a/application/controllers/ServiceapplyrulesController.php b/application/controllers/ServiceapplyrulesController.php
new file mode 100644
index 0000000..c3a7f2b
--- /dev/null
+++ b/application/controllers/ServiceapplyrulesController.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\RestApi\IcingaObjectsHandler;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Table\ApplyRulesTable;
+
+class ServiceapplyrulesController extends ActionController
+{
+ protected $isApified = true;
+
+ public function indexAction()
+ {
+ $request = $this->getRequest();
+ if (! $request->isApiRequest()) {
+ throw new NotFoundError('Not found');
+ }
+
+ $table = ApplyRulesTable::create('service', $this->db());
+/*
+ $query = $this->db()->getDbAdapter()
+ ->select()
+ ->from('icinga_service')
+ ->where('object_type = ?', 'apply');
+ $rules = IcingaService::loadAll($this->db(), $query);
+*/
+
+ $handler = (new IcingaObjectsHandler(
+ $request,
+ $this->getResponse(),
+ $this->db()
+ ))->setTable($table);
+
+ $handler->dispatch();
+ }
+}
diff --git a/application/controllers/ServicegroupController.php b/application/controllers/ServicegroupController.php
new file mode 100644
index 0000000..b2fc50e
--- /dev/null
+++ b/application/controllers/ServicegroupController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectController;
+
+class ServicegroupController extends ObjectController
+{
+}
diff --git a/application/controllers/ServicegroupsController.php b/application/controllers/ServicegroupsController.php
new file mode 100644
index 0000000..d35e638
--- /dev/null
+++ b/application/controllers/ServicegroupsController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class ServicegroupsController extends ObjectsController
+{
+}
diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php
new file mode 100644
index 0000000..8d178c2
--- /dev/null
+++ b/application/controllers/ServicesController.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class ServicesController extends ObjectsController
+{
+ protected $multiEdit = array(
+ 'imports',
+ 'groups',
+ 'disabled'
+ );
+
+ public function edittemplatesAction()
+ {
+ parent::editAction();
+
+ $objects = $this->loadMultiObjectsFromParams();
+ $names = [];
+ /** @var ExportInterface $object */
+ foreach ($objects as $object) {
+ $names[] = $object->getUniqueIdentifier();
+ }
+
+ $url = Url::fromPath('director/basket/add', [
+ 'type' => 'ServiceTemplate',
+ ]);
+
+ $url->getParams()->addValues('names', $names);
+
+ $this->actions()->add(Link::create(
+ $this->translate('Add to Basket'),
+ $url,
+ null,
+ ['class' => 'icon-tag']
+ ));
+ }
+}
diff --git a/application/controllers/ServicesetController.php b/application/controllers/ServicesetController.php
new file mode 100644
index 0000000..684d2fc
--- /dev/null
+++ b/application/controllers/ServicesetController.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Forms\IcingaServiceSetForm;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Web\Controller\ObjectController;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Table\IcingaHostsMatchingFilterTable;
+use Icinga\Module\Director\Web\Table\IcingaServiceSetHostTable;
+use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable;
+use gipfl\IcingaWeb2\Link;
+
+class ServicesetController extends ObjectController
+{
+ /** @var IcingaHost */
+ protected $host;
+
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/servicesets');
+ }
+
+ public function init()
+ {
+ if (null !== ($host = $this->params->get('host'))) {
+ $this->host = IcingaHost::load($host, $this->db());
+ }
+
+ parent::init();
+ if ($this->object) {
+ $this->addServiceSetTabs();
+ }
+ }
+
+ protected function onObjectFormLoaded(DirectorObjectForm $form)
+ {
+ if ($this->host) {
+ /** @var IcingaServiceSetForm $form */
+ $form->setHost($this->host);
+ }
+ }
+
+ public function addAction()
+ {
+ parent::addAction();
+ if ($this->host) {
+ $this->addTitle(
+ $this->translate('Add a service set to "%s"'),
+ $this->host->getObjectName()
+ );
+ }
+ }
+
+ public function servicesAction()
+ {
+ /** @var IcingaServiceSet $set */
+ $set = $this->object;
+ $name = $set->getObjectName();
+ $this->tabs()->activate('services');
+ $this->addTitle(
+ $this->translate('Services in this set: %s'),
+ $name
+ );
+ $this->actions()->add(Link::create(
+ $this->translate('Add service'),
+ 'director/service/add',
+ ['set' => $name],
+ ['class' => 'icon-plus']
+ ));
+
+ IcingaServiceSetServiceTable::load($set)
+ ->setBranch($this->getBranch())
+ ->renderTo($this);
+ }
+
+ public function hostsAction()
+ {
+ /** @var IcingaServiceSet $set */
+ $set = $this->object;
+ $this->tabs()->activate('hosts');
+ $this->addTitle(
+ $this->translate('Hosts using this set: %s'),
+ $set->getObjectName()
+ );
+
+ $table = IcingaServiceSetHostTable::load($set);
+ if ($table->count()) {
+ $table->renderTo($this);
+ }
+ $filter = $set->get('assign_filter');
+ if ($filter !== null && \strlen($filter) > 0) {
+ $this->content()->add(
+ IcingaHostsMatchingFilterTable::load(Filter::fromQueryString($filter), $this->db())
+ );
+ }
+ }
+
+ protected function addServiceSetTabs()
+ {
+ $hexUuid = $this->object->getUniqueId()->toString();
+ $tabs = $this->tabs();
+ $tabs->add('services', [
+ 'url' => 'director/serviceset/services',
+ 'urlParams' => ['uuid' => $hexUuid],
+ 'label' => 'Services'
+ ]);
+ if ($this->branch->isBranch()) {
+ return $this;
+ }
+ $tabs->add('hosts', [
+ 'url' => 'director/serviceset/hosts',
+ 'urlParams' => ['uuid' => $hexUuid],
+ 'label' => 'Hosts'
+ ]);
+
+ return $this;
+ }
+
+ protected function loadObject()
+ {
+ if ($this->object === null) {
+ if (null !== ($name = $this->params->get('name'))) {
+ $params = ['object_name' => $name];
+ $db = $this->db();
+
+ if ($this->host) {
+ $params['host_id'] = $this->host->get('id');
+ }
+
+ $this->object = IcingaServiceSet::load($params, $db);
+ } else {
+ parent::loadObject();
+ }
+ }
+
+ return $this->object;
+ }
+}
diff --git a/application/controllers/ServicetemplateController.php b/application/controllers/ServicetemplateController.php
new file mode 100644
index 0000000..25d0742
--- /dev/null
+++ b/application/controllers/ServicetemplateController.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Web\Controller\TemplateController;
+
+class ServicetemplateController extends TemplateController
+{
+ protected function requireTemplate()
+ {
+ return IcingaService::load([
+ 'object_name' => $this->params->get('name')
+ ], $this->db());
+ }
+}
diff --git a/application/controllers/SettingsController.php b/application/controllers/SettingsController.php
new file mode 100644
index 0000000..c4709e6
--- /dev/null
+++ b/application/controllers/SettingsController.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Forms\KickstartForm;
+use Icinga\Module\Director\Forms\SelfServiceSettingsForm;
+use Icinga\Module\Director\Settings;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use ipl\Html\Html;
+
+class SettingsController extends ActionController
+{
+ /**
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ */
+ public function indexAction()
+ {
+ // Hint: this is for the module configuration tab, legacy code
+ $this->view->tabs = $this->Module()
+ ->getConfigTabs()
+ ->activate('config');
+
+ $this->view->form = KickstartForm::load()
+ ->setModuleConfig($this->Config())
+ ->handleRequest();
+ }
+
+ /**
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\IcingaException
+ */
+ public function selfServiceAction()
+ {
+ $form = SelfServiceSettingsForm::create($this->db(), new Settings($this->db()));
+ $form->handleRequest();
+
+ $hint = $this->translate(
+ 'The Icinga Director Self Service API allows your Hosts to register'
+ . ' themselves. This allows them to get their Icinga Agent configured,'
+ . ' installed and upgraded in an automated way.'
+ );
+
+ $this->addSingleTab($this->translate('Self Service'))
+ ->addTitle($this->translate('Self Service API - Global Settings'))
+ ->content()->add(Html::tag('p', null, $hint))
+ ->add($form);
+ }
+}
diff --git a/application/controllers/SuggestController.php b/application/controllers/SuggestController.php
new file mode 100644
index 0000000..659c48c
--- /dev/null
+++ b/application/controllers/SuggestController.php
@@ -0,0 +1,415 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Restriction\HostgroupRestriction;
+use ipl\Html\Html;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Objects\HostApplyMatches;
+
+class SuggestController extends ActionController
+{
+ protected function checkDirectorPermissions()
+ {
+ }
+
+ public function indexAction()
+ {
+ // TODO: Using some temporarily hardcoded methods, should use DataViews later on
+ $context = $this->getRequest()->getPost('context');
+ $key = null;
+
+ if (strpos($context, '!') !== false) {
+ list($context, $key) = preg_split('~!~', $context, 2);
+ }
+
+ $func = 'suggest' . ucfirst($context);
+ if (method_exists($this, $func)) {
+ if (! empty($key)) {
+ $all = $this->$func($key);
+ } else {
+ $all = $this->$func();
+ }
+ } else {
+ $all = array();
+ }
+ // TODO: also get cursor position and eventually add an asterisk in the middle
+ // tODO: filter also when fetching, eventually limit somehow
+ $search = $this->getRequest()->getPost('value');
+ $begins = array();
+ $matches = array();
+ $begin = Filter::expression('value', '=', $search . '*');
+ $middle = Filter::expression('value', '=', '*' . $search . '*')->setCaseSensitive(false);
+ $prefixes = array();
+ foreach ($all as $str) {
+ if (false !== ($pos = strrpos($str, '.'))) {
+ $prefix = substr($str, 0, $pos) . '.';
+ $prefixes[$prefix] = $prefix;
+ }
+ if (strlen($search)) {
+ $row = (object) array('value' => $str);
+ if ($begin->matches($row)) {
+ $begins[] = $this->highlight($str, $search);
+ } elseif ($middle->matches($row)) {
+ $matches[] = $this->highlight($str, $search);
+ }
+ } else {
+ $matches[] = Html::escape($str);
+ }
+ }
+
+ $containing = array_slice(array_merge($begins, $matches), 0, 100);
+ $suggestions = $containing;
+
+ if ($func === 'suggestHostFilterColumns' || $func === 'suggestHostaddresses') {
+ ksort($prefixes);
+
+ if (count($suggestions) < 5) {
+ $suggestions = array_merge($suggestions, array_keys($prefixes));
+ }
+ }
+ $this->view->suggestions = $suggestions;
+ }
+
+ /**
+ * One more dummy helper for tests
+ *
+ * TODO: Should not remain here
+ *
+ * @return array
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Security\SecurityException
+ */
+ protected function suggestLocations()
+ {
+ $this->assertPermission('director/hosts');
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()
+ ->distinct()
+ ->from('icinga_host_var', 'varvalue')
+ ->where('varname = ?', 'location')
+ ->order('varvalue');
+ return $db->fetchCol($query);
+ }
+
+ protected function suggestHostnames($type = 'object')
+ {
+ $this->assertPermission('director/hosts');
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()
+ ->from('icinga_host', 'object_name')
+ ->order('object_name');
+
+ if ($type !== null) {
+ $query->where('object_type = ?', $type);
+ }
+ $restriction = new HostgroupRestriction($this->db(), $this->Auth());
+ $restriction->filterHostsQuery($query);
+
+ return $db->fetchCol($query);
+ }
+
+ protected function suggestHostsAndTemplates()
+ {
+ return $this->suggestHostnames(null);
+ }
+
+ protected function suggestServicenames()
+ {
+ $r=array();
+ $this->assertPermission('director/services');
+ $db = $this->db()->getDbAdapter();
+ $for_host = $this->getRequest()->getPost('for_host');
+ if (!empty($for_host)) {
+ $tmp_host = IcingaHost::load($for_host, $this->db());
+ }
+
+ $query = $db->select()->distinct()
+ ->from('icinga_service', 'object_name')
+ ->order('object_name')
+ ->where("object_type IN ('object','apply')");
+ if (!empty($tmp_host)) {
+ $query->where('host_id = ?', $tmp_host->id);
+ }
+ $r = array_merge($r, $db->fetchCol($query));
+ if (!empty($tmp_host)) {
+ $resolver = $tmp_host->templateResolver();
+ foreach ($resolver->fetchResolvedParents() as $template_obj) {
+ $query = $db->select()->distinct()
+ ->from('icinga_service', 'object_name')
+ ->order('object_name')
+ ->where("object_type IN ('object','apply')")
+ ->where('host_id = ?', $template_obj->id);
+ $r = array_merge($r, $db->fetchCol($query));
+ }
+
+ $matcher = HostApplyMatches::prepare($tmp_host);
+ foreach ($this->getAllApplyRules() as $rule) {
+ if ($matcher->matchesFilter($rule->filter)) { //TODO
+ $r[]=$rule->name;
+ }
+ }
+ }
+ natcasesort($r);
+ return $r;
+ }
+
+ protected function suggestHosttemplates()
+ {
+ $this->assertPermission('director/hosts');
+ return $this->fetchTemplateNames('icinga_host', 'template_choice_id IS NULL');
+ }
+
+ protected function suggestServicetemplates()
+ {
+ $this->assertPermission('director/services');
+ return $this->fetchTemplateNames('icinga_service', 'template_choice_id IS NULL');
+ }
+
+ protected function suggestNotificationtemplates()
+ {
+ $this->assertPermission('director/notifications');
+ return $this->fetchTemplateNames('icinga_notification');
+ }
+
+ protected function suggestCommandtemplates()
+ {
+ $this->assertPermission('director/commands');
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()
+ ->from('icinga_command', 'object_name')
+ ->order('object_name');
+ return $db->fetchCol($query);
+ }
+
+ protected function suggestUsertemplates()
+ {
+ $this->assertPermission('director/users');
+ return $this->fetchTemplateNames('icinga_user');
+ }
+
+ /**
+ * @return array
+ * @throws \Icinga\Security\SecurityException
+ * @codingStandardsIgnoreStart
+ */
+ protected function suggestScheduled_downtimetemplates()
+ {
+ // @codingStandardsIgnoreEnd
+ $this->assertPermission('director/scheduled-downtimes');
+ return $this->fetchTemplateNames('icinga_scheduled_downtime');
+ }
+
+ protected function suggestCheckcommandnames()
+ {
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()
+ ->from('icinga_command', 'object_name')
+ ->where('object_type != ?', 'template')
+ ->order('object_name');
+
+ return $db->fetchCol($query);
+ }
+
+ protected function fetchTemplateNames($table, $where = null)
+ {
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()
+ ->from($table, 'object_name')
+ ->where('object_type = ?', 'template')
+ ->order('object_name');
+
+ if ($where !== null) {
+ $query->where('template_choice_id IS NULL');
+ }
+
+ return $db->fetchCol($query);
+ }
+
+ protected function suggestHostgroupnames()
+ {
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()->from('icinga_hostgroup', 'object_name')->order('object_name');
+ return $db->fetchCol($query);
+ }
+
+ protected function suggestHostaddresses()
+ {
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()->from('icinga_host', 'address')->order('address');
+ return $db->fetchCol($query);
+ }
+
+ protected function suggestHostFilterColumns()
+ {
+ return $this->getFilterColumns('host.', [
+ $this->translate('Host properties'),
+ $this->translate('Custom variables')
+ ]);
+ }
+
+ protected function suggestServiceFilterColumns()
+ {
+ return $this->getFilterColumns('service.', [
+ $this->translate('Service properties'),
+ $this->translate('Host properties'),
+ $this->translate('Host Custom variables'),
+ $this->translate('Custom variables')
+ ]);
+ }
+
+ protected function suggestDataListValuesForListId($id)
+ {
+ $db = $this->db()->getDbAdapter();
+ $select = $db->select()
+ ->from('director_datalist_entry', ['entry_name', 'entry_value'])
+ ->where('list_id = ?', $id)
+ ->order('entry_value ASC');
+
+ $result = $db->fetchPairs($select);
+ if ($result) {
+ return $result;
+ } else {
+ return [];
+ }
+ }
+
+ protected function suggestDataListValues($field = null)
+ {
+ if ($field === null) {
+ // field is required!
+ return [];
+ }
+
+ $datalistType = 'Icinga\\Module\\Director\\DataType\\DataTypeDatalist';
+ $db = $this->db()->getDbAdapter();
+
+ $query = $db->select()
+ ->from(['f' =>'director_datafield'], [])
+ ->join(
+ ['sid' => 'director_datafield_setting'],
+ 'sid.datafield_id = f.id AND sid.setting_name = \'datalist_id\'',
+ []
+ )
+ ->join(
+ ['l' => 'director_datalist'],
+ 'l.id = sid.setting_value',
+ []
+ )
+ ->join(
+ ['e' => 'director_datalist_entry'],
+ 'e.list_id = l.id',
+ ['entry_name', 'entry_value']
+ )
+ ->where('datatype = ?', $datalistType)
+ ->where('varname = ?', $field)
+ ->order('entry_value');
+
+
+ // TODO: respect allowed_roles
+ /* this implementation from DataTypeDatalist is broken
+ $roles = array_map('json_encode', Acl::instance()->listRoleNames());
+
+ if (empty($roles)) {
+ $query->where('allowed_roles IS NULL');
+ } else {
+ $query->where('(allowed_roles IS NULL OR allowed_roles IN (?))', $roles);
+ }
+ */
+
+ $data = [];
+ foreach ($db->fetchPairs($query) as $key => $label) {
+ // TODO: find a better solution here
+ // $data[] = sprintf("%s [%s]", $label, $key);
+ $data[] = $key;
+ }
+ return $data;
+ }
+
+ protected function getFilterColumns($prefix, $keys)
+ {
+ if ($prefix === 'host.') {
+ $all = IcingaHost::enumProperties($this->db(), $prefix);
+ } else {
+ $all = IcingaService::enumProperties($this->db(), $prefix);
+ }
+ $res = [];
+ foreach ($keys as $key) {
+ if (array_key_exists($key, $all)) {
+ $res = array_merge($res, array_keys($all[$key]));
+ }
+ }
+
+ natsort($res);
+ return $res;
+ }
+
+ protected function suggestDependencytemplates()
+ {
+ $this->assertPermission('director/hosts');
+ return $this->fetchTemplateNames('icinga_dependency');
+ }
+
+ protected function highlight($val, $search)
+ {
+ $search = ($search);
+ $val = Html::escape($val);
+ return preg_replace(
+ '/(' . preg_quote($search, '/') . ')/i',
+ '<strong>\1</strong>',
+ $val
+ );
+ }
+
+ protected function getAllApplyRules()
+ {
+ $allApplyRules=$this->fetchAllApplyRules();
+ foreach ($allApplyRules as $rule) {
+ $rule->filter = Filter::fromQueryString($rule->assign_filter);
+ }
+
+ return $allApplyRules;
+ }
+
+ protected function fetchAllApplyRules()
+ {
+ $db = $this->db()->getDbAdapter();
+ $query = $db->select()->from(
+ array('s' => 'icinga_service'),
+ array(
+ 'id' => 's.id',
+ 'name' => 's.object_name',
+ 'assign_filter' => 's.assign_filter',
+ )
+ )->where('object_type = ? AND assign_filter IS NOT NULL', 'apply');
+
+ return $db->fetchAll($query);
+ }
+
+ protected function suggestImportsourceproperties($sourceId = null)
+ {
+ if ($sourceId === null) {
+ return [];
+ }
+
+ try {
+ $importSource = ImportSource::loadWithAutoIncId($sourceId, $this->db());
+ $source = ImportSourceHook::loadByName($importSource->get('source_name'), $this->db());
+
+ $columns = array_merge(
+ $source->listColumns(),
+ $importSource->listProperties()
+ );
+
+ return array_combine($columns, $columns);
+ } catch (NotFoundError $e) {
+ return [];
+ }
+ }
+}
diff --git a/application/controllers/SyncruleController.php b/application/controllers/SyncruleController.php
new file mode 100644
index 0000000..928cf2c
--- /dev/null
+++ b/application/controllers/SyncruleController.php
@@ -0,0 +1,696 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\Web\Widget\Hint;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchStore;
+use Icinga\Module\Director\Db\Branch\BranchSupport;
+use Icinga\Module\Director\Web\Controller\BranchHelper;
+use Icinga\Module\Director\Web\Form\ClickHereForm;
+use Icinga\Module\Director\Web\Table\BranchActivityTable;
+use Icinga\Module\Director\Web\Widget\IcingaConfigDiff;
+use Icinga\Module\Director\Web\Widget\UnorderedList;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Forms\SyncCheckForm;
+use Icinga\Module\Director\Forms\SyncPropertyForm;
+use Icinga\Module\Director\Forms\SyncRuleForm;
+use Icinga\Module\Director\Forms\SyncRunForm;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Import\Sync;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Web\ActionBar\AutomationObjectActionBar;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Objects\SyncRule;
+use Icinga\Module\Director\Objects\SyncRun;
+use Icinga\Module\Director\Web\Form\CloneSyncRuleForm;
+use Icinga\Module\Director\Web\Table\SyncpropertyTable;
+use Icinga\Module\Director\Web\Table\SyncRunTable;
+use Icinga\Module\Director\Web\Tabs\SyncRuleTabs;
+use Icinga\Module\Director\Web\Widget\SyncRunDetails;
+use Icinga\Web\Notification;
+use ipl\Html\Form;
+use ipl\Html\Html;
+
+class SyncruleController extends ActionController
+{
+ use BranchHelper;
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function indexAction()
+ {
+ $this->setAutoRefreshInterval(10);
+ $rule = $this->requireSyncRule();
+ $this->tabs(new SyncRuleTabs($rule))->activate('show');
+ $ruleName = $rule->get('rule_name');
+ $this->addTitle($this->translate('Sync rule: %s'), $ruleName);
+
+ $checkForm = SyncCheckForm::load()->setSyncRule($rule)->handleRequest();
+ $store = new DbObjectStore($this->db(), $this->getBranch());
+ $runForm = new SyncRunForm($rule, $store);
+ $runForm->on(SyncRunForm::ON_SUCCESS, function (SyncRunForm $form) {
+ $message = $form->getSuccessMessage();
+ if ($message === null) {
+ Notification::error($this->translate('Synchronization failed'));
+ } else {
+ Notification::success($message);
+ }
+ $this->redirectNow($this->url());
+ });
+ $runForm->handleRequest($this->getServerRequest());
+
+ if ($lastRunId = $rule->getLastSyncRunId()) {
+ $run = SyncRun::load($lastRunId, $this->db());
+ } else {
+ $run = null;
+ }
+
+ $c = $this->content();
+ $c->add(Html::tag('p', null, $rule->get('description')));
+ if (! $rule->hasSyncProperties()) {
+ $this->addPropertyHint($rule);
+ return;
+ }
+ $this->addMainActions();
+ if (! $run) {
+ $c->add(Hint::warning($this->translate('This Sync Rule has never been run before.')));
+ }
+
+ switch ($rule->get('sync_state')) {
+ case 'unknown':
+ $c->add(Html::tag('p', null, $this->translate(
+ "It's currently unknown whether we are in sync with this rule."
+ . ' You should either check for changes or trigger a new Sync Run.'
+ )));
+ break;
+ case 'in-sync':
+ $c->add(Html::tag('p', null, sprintf(
+ $this->translate('This Sync Rule was last found to by in Sync at %s.'),
+ $rule->get('last_attempt')
+ )));
+ /*
+ TODO: check whether...
+ - there have been imports since then, differing from former ones
+ - there have been activities since then
+ */
+ break;
+ case 'pending-changes':
+ $c->add(Hint::warning($this->translate(
+ 'There are pending changes for this Sync Rule. You should trigger a new'
+ . ' Sync Run.'
+ )));
+ break;
+ case 'failing':
+ $c->add(Hint::error(sprintf(
+ $this->translate(
+ 'This Sync Rule failed when last checked at %s: %s'
+ ),
+ $rule->get('last_attempt'),
+ $rule->get('last_error_message')
+ )));
+ break;
+ }
+
+ $c->add($checkForm);
+ if ($this->hasBranch()) {
+ $objectType = $rule->get('object_type');
+ $table = DbObjectTypeRegistry::tableNameByType($objectType);
+ if (! BranchSupport::existsForTableName($table)) {
+ $this->showNotInBranch(sprintf($this->translate("Synchronizing '%s'"), $objectType));
+ return;
+ }
+ }
+
+ $c->add($runForm);
+
+ if ($run) {
+ $c->add(Html::tag('h3', null, $this->translate('Last sync run details')));
+ $c->add(new SyncRunDetails($run));
+ if ($run->get('rule_name') !== $ruleName) {
+ $c->add(Html::tag('p', null, sprintf(
+ $this->translate("It has been renamed since then, its former name was %s"),
+ $run->get('rule_name')
+ )));
+ }
+ }
+ }
+
+ /**
+ * @param SyncRule $rule
+ */
+ protected function addPropertyHint(SyncRule $rule)
+ {
+ $this->content()->add(Hint::warning(Html::sprintf(
+ $this->translate('You must define some %s before you can run this Sync Rule'),
+ new Link(
+ $this->translate('Sync Properties'),
+ 'director/syncrule/property',
+ ['rule_id' => $rule->get('id')]
+ )
+ )));
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function addAction()
+ {
+ $this->editAction();
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Exception
+ */
+ public function previewAction()
+ {
+ $rule = $this->requireSyncRule();
+ $branchSupport = BranchSupport::existsForSyncRule($rule);
+ $branchStore = new BranchStore($this->db());
+ $owner = $this->getAuth()->getUser()->getUsername();
+ if ($branchSupport) {
+ if ($this->getBranch()->isBranch()) {
+ $tmpBranchName = sprintf(
+ '%s/%s-%s',
+ Branch::PREFIX_SYNC_PREVIEW,
+ $this->getBranch()->getUuid()->toString(),
+ $rule->get('id')
+ );
+ // We could keep changes for preview on branch too
+ $branchStore->deleteByName($tmpBranchName);
+ $tmpBranch = $branchStore->cloneBranchForSync($this->getBranch(), $tmpBranchName, $owner);
+ $after = 1600000000; // a date in 2020, minus 10000000
+ } else {
+ $tmpBranchName = Branch::PREFIX_SYNC_PREVIEW . '/' . $rule->get('id');
+ $tmpBranch = $branchStore->fetchOrCreateByName($tmpBranchName, $owner);
+ $after = null;
+ }
+ $store = new DbObjectStore($this->db(), $tmpBranch);
+ } else {
+ $tmpBranch = $store = null;
+ }
+
+ $this->tabs(new SyncRuleTabs($rule))->activate('preview');
+ $this->addTitle($this->translate('Sync Preview'));
+ $sync = new Sync($rule, $store);
+ $keepBranchPreview = false;
+ if ($tmpBranch) {
+ if ($lastTime = $branchStore->getLastActivityTime($tmpBranch, $after)) {
+ if ((time() - $lastTime) > 100) {
+ $branchStore->wipeBranch($tmpBranch, $after);
+ } else {
+ $here = (new ClickHereForm())->handleRequest($this->getServerRequest());
+ if ($here->hasBeenClicked()) {
+ $branchStore->wipeBranch($tmpBranch, $after);
+ $this->redirectNow($this->url());
+ } else {
+ $keepBranchPreview = true;
+ }
+ $this->content()->add(Hint::info(Html::sprintf(
+ $this->translate('This preview has been generated %s, please click %s to regenerate it'),
+ DateFormatter::timeAgo($lastTime),
+ $here
+ )));
+ }
+ }
+ }
+ if (!$keepBranchPreview) {
+ $modifications = $sync->getExpectedModifications();
+ }
+
+ if ($tmpBranch) {
+ try {
+ if (!$keepBranchPreview) {
+ $sync->apply();
+ }
+ } catch (\Exception $e) {
+ $this->content()->add(Hint::error($e->getMessage()));
+ return;
+ }
+
+ $changes = new BranchActivityTable($tmpBranch->getUuid(), $this->db());
+ $changes->disableObjectLink();
+ if (count($changes) === 0) {
+ $this->showInSync();
+ }
+ $changes->renderTo($this);
+ } else {
+ if (empty($modifications)) {
+ $this->showInSync();
+ return;
+ }
+ $this->showExpectedModificationSummary($modifications);
+ }
+ }
+
+ protected function showInSync()
+ {
+ $this->content()->add(Hint::ok($this->translate(
+ 'This Sync Rule is in sync and would currently not apply any changes'
+ )));
+ }
+
+ protected function showExpectedModificationSummary($modifications)
+ {
+ $create = [];
+ $modify = [];
+ $delete = [];
+ $modifiedProperties = [];
+ /** @var IcingaObject $object */
+ foreach ($modifications as $object) {
+ if ($object->hasBeenLoadedFromDb()) {
+ if ($object->shouldBeRemoved()) {
+ $delete[] = $object;
+ } else {
+ $modify[] = $object;
+ foreach ($object->getModifiedProperties() as $property => $value) {
+ if (isset($modifiedProperties[$property])) {
+ $modifiedProperties[$property]++;
+ } else {
+ $modifiedProperties[$property] = 1;
+ }
+ }
+ if (! $object instanceof IcingaObject) {
+ continue;
+ }
+ if ($object->supportsGroups()) {
+ if ($object->hasModifiedGroups()) {
+ if (isset($modifiedProperties['groups'])) {
+ $modifiedProperties['groups']++;
+ } else {
+ $modifiedProperties['groups'] = 1;
+ }
+ }
+ }
+
+ if ($object->supportsImports()) {
+ if ($object->imports()->hasBeenModified()) {
+ if (isset($modifiedProperties['imports'])) {
+ $modifiedProperties['imports']++;
+ } else {
+ $modifiedProperties['imports'] = 1;
+ }
+ }
+ }
+ if ($object->supportsCustomVars()) {
+ if ($object->vars()->hasBeenModified()) {
+ foreach ($object->vars() as $var) {
+ if ($var->isNew()) {
+ $varName = 'add vars.' . $var->getKey();
+ } elseif ($var->hasBeenDeleted()) {
+ $varName = 'remove vars.' . $var->getKey();
+ } elseif ($var->hasBeenModified()) {
+ $varName = 'vars.' . $var->getKey();
+ } else {
+ continue;
+ }
+ if (isset($modifiedProperties[$varName])) {
+ $modifiedProperties[$varName]++;
+ } else {
+ $modifiedProperties[$varName] = 1;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ $create[] = $object;
+ }
+ }
+
+ $content = $this->content();
+ if (! empty($delete)) {
+ $content->add([
+ Html::tag('h2', ['class' => 'icon-cancel action-delete'], sprintf(
+ $this->translate('%d object(s) will be deleted'),
+ count($delete)
+ )),
+ $this->objectList($delete)
+ ]);
+ }
+ if (! empty($modify)) {
+ $content->add([
+ Html::tag('h2', ['class' => 'icon-wrench action-modify'], sprintf(
+ $this->translate('%d object(s) will be modified'),
+ count($modify)
+ )),
+ $this->listModifiedProperties($modifiedProperties),
+ $this->objectList($modify),
+ ]);
+ }
+ if (! empty($create)) {
+ $content->add([
+ Html::tag('h2', ['class' => 'icon-plus action-create'], sprintf(
+ $this->translate('%d object(s) will be created'),
+ count($create)
+ )),
+ $this->objectList($create)
+ ]);
+ }
+ }
+
+ /**
+ * @param IcingaObject[] $objects
+ * @return \ipl\Html\HtmlElement
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function objectList($objects)
+ {
+ return Html::tag('p', $this->firstNames($objects));
+ }
+
+ /**
+ * Lots of duplicated code, this whole diff logic should be mouved to a
+ * dedicated class
+ *
+ * @param IcingaObject[] $objects
+ * @param int $max
+ * @return string
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function firstNames($objects, $max = 50)
+ {
+ $names = [];
+ $list = new UnorderedList();
+ $list->addAttributes([
+ 'style' => 'list-style-type: none; marign: 0; padding: 0',
+ ]);
+ $total = count($objects);
+ $i = 0;
+ PrefetchCache::forget();
+ IcingaHost::clearAllPrefetchCaches(); // why??
+ IcingaService::clearAllPrefetchCaches();
+ foreach ($objects as $object) {
+ $i++;
+ $name = $this->getObjectNameString($object);
+ if ($object->hasBeenLoadedFromDb()) {
+ if ($object instanceof IcingaHost) {
+ $names[$name] = Link::create(
+ $name,
+ 'director/host',
+ ['name' => $name],
+ ['data-base-target' => '_next']
+ );
+ $oldObject = IcingaHost::load($object->getObjectName(), $this->db());
+ $cfgNew = new IcingaConfig($this->db());
+ $cfgOld = new IcingaConfig($this->db());
+ $oldObject->renderToConfig($cfgOld);
+ $object->renderToConfig($cfgNew);
+ foreach (IcingaConfigDiff::getDiffs($cfgOld, $cfgNew) as $file => $diff) {
+ $names[$name . '___PRETITLE___' . $file] = Html::tag('h3', $file);
+ $names[$name . '___PREVIEW___' . $file] = $diff;
+ }
+ } elseif ($object instanceof IcingaService && $object->isObject()) {
+ $host = $object->getRelated('host');
+
+ $names[$name] = Link::create(
+ $name,
+ 'director/service/edit',
+ [
+ 'name' => $object->getObjectName(),
+ 'host' => $host->getObjectName()
+ ],
+ ['data-base-target' => '_next']
+ );
+ $oldObject = IcingaService::load([
+ 'host_id' => $host->get('id'),
+ 'object_name' => $object->getObjectName()
+ ], $this->db());
+
+ $cfgNew = new IcingaConfig($this->db());
+ $cfgOld = new IcingaConfig($this->db());
+ $oldObject->renderToConfig($cfgOld);
+ $object->renderToConfig($cfgNew);
+ foreach (IcingaConfigDiff::getDiffs($cfgOld, $cfgNew) as $file => $diff) {
+ $names[$name . '___PRETITLE___' . $file] = Html::tag('h3', $file);
+ $names[$name . '___PREVIEW___' . $file] = $diff;
+ }
+ } else {
+ $names[$name] = $name;
+ }
+ } else {
+ $names[$name] = $name;
+ }
+ if ($i === $max) {
+ break;
+ }
+ }
+ ksort($names);
+
+ foreach ($names as $name) {
+ $list->addItem($name);
+ }
+
+ if ($total > $max) {
+ $list->add(sprintf(
+ $this->translate('...and %d more'),
+ $total - $max
+ ));
+ }
+
+ return $list;
+ }
+
+ protected function listModifiedProperties($properties)
+ {
+ $list = new UnorderedList();
+ foreach ($properties as $property => $cnt) {
+ $list->addItem("${cnt}x $property");
+ }
+
+ return $list;
+ }
+
+ protected function getObjectNameString($object)
+ {
+ if ($object instanceof IcingaService) {
+ if ($object->isObject()) {
+ return $object->getRelated('host')->getObjectName()
+ . ': ' . $object->getObjectName();
+ } else {
+ return $object->getObjectName();
+ }
+ } elseif ($object instanceof IcingaHost) {
+ return $object->getObjectName();
+ } elseif ($object instanceof ExportInterface) {
+ return $object->getUniqueIdentifier();
+ } elseif ($object instanceof IcingaObject) {
+ return $object->getObjectName();
+ } else {
+ /** @var \Icinga\Module\Director\Data\Db\DbObject $object */
+ return json_encode($object->getKeyParams());
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function editAction()
+ {
+ $form = SyncRuleForm::load()
+ ->setListUrl('director/syncrules')
+ ->setDb($this->db());
+
+ if ($id = $this->params->get('id')) {
+ $form->loadObject((int) $id);
+ /** @var SyncRule $rule */
+ $rule = $form->getObject();
+ $this->tabs(new SyncRuleTabs($rule))->activate('edit');
+ $this->addTitle(sprintf(
+ $this->translate('Sync rule: %s'),
+ $rule->get('rule_name')
+ ));
+ $this->addMainActions();
+
+ if (! $rule->hasSyncProperties()) {
+ $this->addPropertyHint($rule);
+ }
+ if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) {
+ return;
+ }
+
+ } else {
+ $this->addTitle($this->translate('Add sync rule'));
+ $this->tabs(new SyncRuleTabs())->activate('add');
+ if ($this->showNotInBranch($this->translate('Creating Sync Rules'))) {
+ return;
+ }
+ }
+
+ $form->handleRequest();
+ $this->content()->add($form);
+ }
+
+ /**
+ * @throws \Icinga\Exception\MissingParameterException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function cloneAction()
+ {
+ $id = $this->params->getRequired('id');
+ $rule = SyncRule::loadWithAutoIncId((int) $id, $this->db());
+ $this->tabs()->add('show', [
+ 'url' => 'director/syncrule',
+ 'urlParams' => ['id' => $id],
+ 'label' => $this->translate('Sync rule'),
+ ])->add('clone', [
+ 'url' => 'director/syncrule/clone',
+ 'urlParams' => ['id' => $id],
+ 'label' => $this->translate('Clone'),
+ ])->activate('clone');
+ $this->addTitle('Clone: %s', $rule->get('rule_name'));
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Modify'),
+ 'director/syncrule/edit',
+ ['id' => $rule->get('id')],
+ ['class' => 'icon-paste']
+ )
+ );
+ if ($this->showNotInBranch($this->translate('Cloning Sync Rules'))) {
+ return;
+ }
+
+ $form = new CloneSyncRuleForm($rule);
+ $this->content()->add($form);
+ $form->on(Form::ON_SUCCESS, function (CloneSyncRuleForm $form) {
+ $this->getResponse()->redirectAndExit($form->getSuccessUrl());
+ });
+ $form->handleRequest($this->getServerRequest());
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function propertyAction()
+ {
+ $rule = $this->requireSyncRule('rule_id');
+ $this->tabs(new SyncRuleTabs($rule))->activate('property');
+
+ $this->actions()->add(Link::create(
+ $this->translate('Add sync property rule'),
+ 'director/syncrule/addproperty',
+ ['rule_id' => $rule->get('id')],
+ ['class' => 'icon-plus']
+ ));
+ $this->addTitle($this->translate('Sync properties') . ': ' . $rule->get('rule_name'));
+
+ SyncpropertyTable::create($rule)
+ ->handleSortPriorityActions($this->getRequest(), $this->getResponse())
+ ->renderTo($this);
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function editpropertyAction()
+ {
+ $this->addpropertyAction();
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function addpropertyAction()
+ {
+ $db = $this->db();
+ $rule = $this->requireSyncRule('rule_id');
+ $ruleId = (int) $rule->get('id');
+
+ $form = SyncPropertyForm::load()->setDb($db);
+ $this->tabs(new SyncRuleTabs($rule))->activate('property');
+ $this->actions()->add(new Link(
+ $this->translate('back'),
+ 'director/syncrule/property',
+ ['rule_id' => $ruleId],
+ ['class' => 'icon-left-big']
+ ));
+
+ if ($id = $this->params->get('id')) {
+ $form->loadObject((int) $id);
+ $this->addTitle(
+ $this->translate('Sync "%s": %s'),
+ $form->getObject()->get('destination_field'),
+ $rule->get('rule_name')
+ );
+ if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) {
+ return;
+ }
+ } else {
+ $this->addTitle(
+ $this->translate('Add sync property: %s'),
+ $rule->get('rule_name')
+ );
+ if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) {
+ return;
+ }
+ }
+ $form->setRule($rule);
+ $form->setSuccessUrl('director/syncrule/property', ['rule_id' => $ruleId]);
+ $this->content()->add($form->handleRequest());
+ SyncpropertyTable::create($rule)
+ ->handleSortPriorityActions($this->getRequest(), $this->getResponse())
+ ->renderTo($this);
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function historyAction()
+ {
+ $this->setAutoRefreshInterval(30);
+ $rule = $this->requireSyncRule();
+ $this->tabs(new SyncRuleTabs($rule))->activate('history');
+ $this->addTitle($this->translate('Sync history') . ': ' . $rule->get('rule_name'));
+
+ if ($runId = $this->params->get('run_id')) {
+ $run = SyncRun::load($runId, $this->db());
+ $this->content()->add(new SyncRunDetails($run));
+ }
+ (new SyncRunTable($rule))->renderTo($this);
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function addMainActions()
+ {
+ $this->actions(new AutomationObjectActionBar(
+ $this->getRequest()
+ ));
+ $source = $this->requireSyncRule();
+
+ $this->actions()->add(Link::create(
+ $this->translate('Add to Basket'),
+ 'director/basket/add',
+ [
+ 'type' => 'SyncRule',
+ 'names' => $source->getUniqueIdentifier()
+ ],
+ [
+ 'class' => 'icon-tag',
+ 'data-base-target' => '_next',
+ ]
+ ));
+ }
+
+ /**
+ * @param string $key
+ * @return SyncRule
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function requireSyncRule($key = 'id')
+ {
+ $id = $this->params->get($key);
+ return SyncRule::loadWithAutoIncId($id, $this->db());
+ }
+}
diff --git a/application/controllers/SyncrulesController.php b/application/controllers/SyncrulesController.php
new file mode 100644
index 0000000..1829ebe
--- /dev/null
+++ b/application/controllers/SyncrulesController.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\DirectorObject\Automation\ImportExport;
+use Icinga\Module\Director\Web\Table\SyncruleTable;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Tabs\ImportTabs;
+
+class SyncrulesController extends ActionController
+{
+ protected $isApified = true;
+
+ /**
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ */
+ public function indexAction()
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ $this->sendExport();
+ return;
+ }
+
+ $this->addTitle($this->translate('Sync rule'))
+ ->setAutoRefreshInterval(10)
+ ->addAddLink(
+ $this->translate('Add a new Sync Rule'),
+ 'director/syncrule/add'
+ )->tabs(new ImportTabs())->activate('syncrule');
+
+ (new SyncruleTable($this->db()))->renderTo($this);
+ }
+
+ /**
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ protected function sendExport()
+ {
+ $this->sendJson(
+ $this->getResponse(),
+ (new ImportExport($this->db()))->serializeAllSyncRules()
+ );
+ }
+}
diff --git a/application/controllers/TemplatechoiceController.php b/application/controllers/TemplatechoiceController.php
new file mode 100644
index 0000000..faf3dfe
--- /dev/null
+++ b/application/controllers/TemplatechoiceController.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Forms\IcingaTemplateChoiceForm;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Controller\BranchHelper;
+
+class TemplatechoiceController extends ActionController
+{
+ use BranchHelper;
+
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/admin');
+ }
+
+ public function hostAction()
+ {
+ $this->prepare('host', $this->translate('Host template choice'));
+ }
+
+ public function serviceAction()
+ {
+ $this->prepare('service', $this->translate('Service template choice'));
+ }
+
+ protected function prepare($type, $title)
+ {
+ $this->addSingleTab('Choice')
+ ->addTitle($title);
+ $form = IcingaTemplateChoiceForm::create($type, $this->db())
+ ->optionallyLoad($this->params->get('name'))
+ ->setListUrl("director/templatechoices/$type")
+ ->handleRequest();
+ if ($this->showNotInBranch($this->translate('Modifying Template Choices'))) {
+ return;
+ }
+ $this->content()->add($form);
+ }
+}
diff --git a/application/controllers/TemplatechoicesController.php b/application/controllers/TemplatechoicesController.php
new file mode 100644
index 0000000..753591a
--- /dev/null
+++ b/application/controllers/TemplatechoicesController.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\ActionBar\ChoicesActionBar;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Table\ChoicesTable;
+use Icinga\Module\Director\Web\Tabs\ObjectsTabs;
+
+class TemplatechoicesController extends ActionController
+{
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/admin');
+ }
+
+ public function hostAction()
+ {
+ $this->prepare('host', $this->translate('Host template choices'));
+ }
+
+ public function serviceAction()
+ {
+ $this->prepare('service', $this->translate('Service template choices'));
+ }
+
+ public function notificationAction()
+ {
+ $this->prepare('notification', $this->translate('Notification template choices'));
+ }
+
+ protected function prepare($type, $title)
+ {
+ $this->tabs(new ObjectsTabs($type, $this->Auth(), $type))->activate('choices');
+ $this->setAutorefreshInterval(10)->addTitle($title);
+ $this->actions(new ChoicesActionBar($type, $this->url()));
+ ChoicesTable::create($type, $this->db())->renderTo($this);
+ }
+}
diff --git a/application/controllers/TimeperiodController.php b/application/controllers/TimeperiodController.php
new file mode 100644
index 0000000..82c7749
--- /dev/null
+++ b/application/controllers/TimeperiodController.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Forms\IcingaTimePeriodRangeForm;
+use Icinga\Module\Director\Objects\IcingaTimePeriod;
+use Icinga\Module\Director\Web\Controller\ObjectController;
+use Icinga\Module\Director\Web\Table\IcingaTimePeriodRangeTable;
+
+class TimeperiodController extends ObjectController
+{
+ public function rangesAction()
+ {
+ /** @var IcingaTimePeriod $object */
+ $object = $this->object;
+ $this->tabs()->activate('ranges');
+ $this->addTitle($this->translate('Time period ranges'));
+ $form = IcingaTimePeriodRangeForm::load()
+ ->setTimePeriod($object);
+
+ if (null !== ($name = $this->params->get('range'))) {
+ $this->addBackLink($this->url()->without('range'));
+ $form->loadObject([
+ 'timeperiod_id' => $object->get('id'),
+ 'range_key' => $name,
+ 'range_type' => $this->params->get('range_type')
+ ]);
+ }
+
+ $this->content()->add($form->handleRequest());
+ IcingaTimePeriodRangeTable::load($object)->renderTo($this);
+ }
+}
diff --git a/application/controllers/TimeperiodsController.php b/application/controllers/TimeperiodsController.php
new file mode 100644
index 0000000..e5adb19
--- /dev/null
+++ b/application/controllers/TimeperiodsController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class TimeperiodsController extends ObjectsController
+{
+}
diff --git a/application/controllers/TimeperiodtemplateController.php b/application/controllers/TimeperiodtemplateController.php
new file mode 100644
index 0000000..a7b26a8
--- /dev/null
+++ b/application/controllers/TimeperiodtemplateController.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Objects\IcingaTimePeriod;
+use Icinga\Module\Director\Web\Controller\TemplateController;
+
+class TimeperiodtemplateController extends TemplateController
+{
+ protected function requireTemplate()
+ {
+ return IcingaTimePeriod::load([
+ 'object_name' => $this->params->get('name')
+ ], $this->db());
+ }
+}
diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
new file mode 100644
index 0000000..b021be9
--- /dev/null
+++ b/application/controllers/UserController.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectController;
+
+class UserController extends ObjectController
+{
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/users');
+ }
+
+ protected function hasBasketSupport()
+ {
+ return true;
+ }
+}
diff --git a/application/controllers/UsergroupController.php b/application/controllers/UsergroupController.php
new file mode 100644
index 0000000..e58fd7e
--- /dev/null
+++ b/application/controllers/UsergroupController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectController;
+
+class UsergroupController extends ObjectController
+{
+}
diff --git a/application/controllers/UsergroupsController.php b/application/controllers/UsergroupsController.php
new file mode 100644
index 0000000..057890f
--- /dev/null
+++ b/application/controllers/UsergroupsController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class UsergroupsController extends ObjectsController
+{
+}
diff --git a/application/controllers/UsersController.php b/application/controllers/UsersController.php
new file mode 100644
index 0000000..ee6d93d
--- /dev/null
+++ b/application/controllers/UsersController.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class UsersController extends ObjectsController
+{
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/users');
+ }
+}
diff --git a/application/controllers/UsertemplateController.php b/application/controllers/UsertemplateController.php
new file mode 100644
index 0000000..41fce86
--- /dev/null
+++ b/application/controllers/UsertemplateController.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Objects\IcingaUser;
+use Icinga\Module\Director\Web\Controller\TemplateController;
+
+class UsertemplateController extends TemplateController
+{
+ protected function requireTemplate()
+ {
+ return IcingaUser::load([
+ 'object_name' => $this->params->get('name')
+ ], $this->db());
+ }
+}
diff --git a/application/controllers/ZoneController.php b/application/controllers/ZoneController.php
new file mode 100644
index 0000000..a4125bb
--- /dev/null
+++ b/application/controllers/ZoneController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectController;
+
+class ZoneController extends ObjectController
+{
+}
diff --git a/application/controllers/ZonesController.php b/application/controllers/ZonesController.php
new file mode 100644
index 0000000..2dcaf58
--- /dev/null
+++ b/application/controllers/ZonesController.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Controllers;
+
+use Icinga\Module\Director\Web\Controller\ObjectsController;
+
+class ZonesController extends ObjectsController
+{
+}
diff --git a/application/forms/AddToBasketForm.php b/application/forms/AddToBasketForm.php
new file mode 100644
index 0000000..44b5357
--- /dev/null
+++ b/application/forms/AddToBasketForm.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Web\Widget\Hint;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use gipfl\IcingaWeb2\Link;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class AddToBasketForm extends DirectorForm
+{
+ /** @var Basket */
+ private $basket;
+
+ private $type = '(has not been set)';
+
+ private $names = [];
+
+ /**
+ * @throws \Zend_Form_Exception
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function setup()
+ {
+ $db = $this->getDb()->getDbAdapter();
+ $enum = $db->fetchPairs($db->select()->from('director_basket', [
+ 'a' => 'basket_name',
+ 'b' => 'basket_name',
+ ])->order('basket_name'));
+
+ $names = [];
+ $basket = null;
+ if ($this->hasBeenSent()) {
+ $basketName = $this->getSentValue('basket');
+ if ($basketName) {
+ $basket = Basket::load($basketName, $this->getDb());
+ }
+ }
+ $count = 0;
+ $type = $this->type;
+ foreach ($this->names as $name) {
+ if (! empty($names)) {
+ $names[] = ', ';
+ }
+ if ($basket && $basket->hasObject($type, $name)) {
+ $names[] = Html::tag('span', [
+ 'style' => 'text-decoration: line-through'
+ ], $name);
+ } else {
+ $count++;
+ $names[] = $name;
+ }
+ }
+ $this->addHtmlHint((new HtmlDocument())->add([
+ 'The following objects will be added: ',
+ $names
+ ]));
+ $this->addElement('select', 'basket', [
+ 'label' => $this->translate('Basket'),
+ 'multiOptions' => $this->optionalEnum($enum),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ ]);
+
+ if ($count > 0) {
+ $this->setSubmitLabel(sprintf(
+ $this->translate('Add %s objects'),
+ $count
+ ));
+ } else {
+ $this->setSubmitLabel($this->translate('Add'));
+ $this->addSubmitButtonIfSet();
+ $this->getElement($this->submitButtonName)->setAttrib('disabled', true);
+ }
+ }
+
+ public function setType($type)
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function setNames($names)
+ {
+ $this->names = $names;
+
+ return $this;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ $type = $this->type;
+ $basket = Basket::load($this->getValue('basket'), $this->getDb());
+ $basketName = $basket->get('basket_name');
+
+ if (empty($this->names)) {
+ $this->getElement('basket')->addErrorMessage($this->translate(
+ 'No object has been chosen'
+ ));
+ }
+ if ($basket->supportsCustomSelectionFor($type)) {
+ $basket->addObjects($type, $this->names);
+ $basket->store();
+ $this->setSuccessMessage(sprintf($this->translate(
+ 'Configuration objects have been added to the chosen basket "%s"'
+ ), $basketName));
+ return parent::onSuccess();
+ } else {
+ $this->addHtmlHint(Hint::error(Html::sprintf($this->translate(
+ 'Please check your Basket configuration, %s does not support'
+ . ' single "%s" configuration objects'
+ ), Link::create(
+ $basketName,
+ 'director/basket',
+ ['name' => $basketName],
+ ['data-base-target' => '_next']
+ ), $type)));
+
+ return false;
+ }
+ }
+}
diff --git a/application/forms/ApplyMigrationsForm.php b/application/forms/ApplyMigrationsForm.php
new file mode 100644
index 0000000..4f1e62b
--- /dev/null
+++ b/application/forms/ApplyMigrationsForm.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Module\Director\Db\Migrations;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class ApplyMigrationsForm extends DirectorForm
+{
+ /** @var Migrations */
+ protected $migrations;
+
+ public function setup()
+ {
+ if ($this->migrations->hasSchema()) {
+ $count = $this->migrations->countPendingMigrations();
+ if ($count === 1) {
+ $this->setSubmitLabel(
+ $this->translate('Apply a pending schema migration')
+ );
+ } else {
+ $this->setSubmitLabel(
+ sprintf(
+ $this->translate('Apply %d pending schema migrations'),
+ $count
+ )
+ );
+ }
+ } else {
+ $this->setSubmitLabel($this->translate('Create schema'));
+ }
+ }
+
+ public function onSuccess()
+ {
+ try {
+ $this->setSuccessMessage($this->translate(
+ 'Pending database schema migrations have successfully been applied'
+ ));
+
+ $this->migrations->applyPendingMigrations();
+ parent::onSuccess();
+ } catch (Exception $e) {
+ $this->addError($e->getMessage());
+ }
+ }
+
+ public function setMigrations(Migrations $migrations)
+ {
+ $this->migrations = $migrations;
+ return $this;
+ }
+}
diff --git a/application/forms/BasketCreateSnapshotForm.php b/application/forms/BasketCreateSnapshotForm.php
new file mode 100644
index 0000000..165c7ac
--- /dev/null
+++ b/application/forms/BasketCreateSnapshotForm.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class BasketCreateSnapshotForm extends DirectorForm
+{
+ /** @var Basket */
+ private $basket;
+
+ public function setBasket(Basket $basket)
+ {
+ $this->basket = $basket;
+
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->setSubmitLabel($this->translate('Create Snapshot'));
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ /** @var \Icinga\Module\Director\Db $connection */
+ $connection = $this->basket->getConnection();
+ $snapshot = BasketSnapshot::createForBasket($this->basket, $connection);
+ $snapshot->store();
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/BasketForm.php b/application/forms/BasketForm.php
new file mode 100644
index 0000000..8ff6cca
--- /dev/null
+++ b/application/forms/BasketForm.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Zend_Form_SubForm as ZfSubForm;
+
+class BasketForm extends DirectorObjectForm
+{
+ protected $listUrl = 'director/baskets';
+
+ protected function getAvailableTypes()
+ {
+ return [
+ 'Command' => $this->translate('Command Definitions'),
+ 'ExternalCommand' => $this->translate('External Command Definitions'),
+ 'CommandTemplate' => $this->translate('Command Template'),
+ 'HostGroup' => $this->translate('Host Group'),
+ 'IcingaTemplateChoiceHost' => $this->translate('Host Template Choice'),
+ 'HostTemplate' => $this->translate('Host Templates'),
+ 'ServiceGroup' => $this->translate('Service Groups'),
+ 'IcingaTemplateChoiceService' => $this->translate('Service Template Choice'),
+ 'ServiceTemplate' => $this->translate('Service Templates'),
+ 'ServiceSet' => $this->translate('Service Sets'),
+ 'UserGroup' => $this->translate('User Groups'),
+ 'UserTemplate' => $this->translate('User Templates'),
+ 'User' => $this->translate('Users'),
+ 'NotificationTemplate' => $this->translate('Notification Templates'),
+ 'Notification' => $this->translate('Notifications'),
+ 'TimePeriod' => $this->translate('Time Periods'),
+ 'Dependency' => $this->translate('Dependencies'),
+ 'DataList' => $this->translate('Data Lists'),
+ 'ImportSource' => $this->translate('Import Sources'),
+ 'SyncRule' => $this->translate('Sync Rules'),
+ 'DirectorJob' => $this->translate('Job Definitions'),
+ 'Basket' => $this->translate('Basket Definitions'),
+ ];
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addElement('text', 'basket_name', [
+ 'label' => $this->translate('Basket Name'),
+ 'required' => true,
+ ]);
+
+ $types = $this->getAvailableTypes();
+
+ $options = [
+ 'IGNORE' => $this->translate('Ignore'),
+ 'ALL' => $this->translate('All of them'),
+ '[]' => $this->translate('Custom Selection'),
+ ];
+
+ $this->addHtmlHint($this->translate(
+ 'What should we place into this Basket every time we create'
+ . ' new snapshot?'
+ ));
+
+ $sub = new ZfSubForm();
+ $sub->setDecorators([
+ ['HtmlTag', ['tag' => 'dl']],
+ 'FormElements'
+ ]);
+
+ foreach ($types as $name => $label) {
+ $sub->addElement('select', $name, [
+ 'label' => $label,
+ 'multiOptions' => $options,
+ ]);
+ }
+
+ $this->addSubForm($sub, 'objects');
+ $this->addDeleteButton();
+
+ $this->addHtmlHint($this->translate(
+ 'Choose "All" to always add all of them,'
+ . ' "Ignore" to not care about a specific Type at all and'
+ . ' opt for "Custom Selection" in case you want to choose'
+ . ' just some specific Objects.'
+ ));
+ }
+
+ protected function setDefaultsFromObject(DbObject $object)
+ {
+ parent::setDefaultsFromObject($object);
+ /** @var Basket $object */
+ $values = [];
+ foreach ($this->getAvailableTypes() as $type => $label) {
+ $values[$type] = 'IGNORE';
+ }
+ foreach ($object->getChosenObjects() as $type => $selection) {
+ if ($selection === true) {
+ $values[$type] = 'ALL';
+ } elseif (is_array($selection)) {
+ $values[$type] = '[]';
+ }
+ }
+
+ $this->populate([
+ 'objects' => $values
+ ]);
+ }
+
+ protected function onRequest()
+ {
+ parent::onRequest(); // TODO: Change the autogenerated stub
+ }
+
+ protected function getObjectClassname()
+ {
+ return Basket::class;
+ }
+
+ public function onSuccess()
+ {
+ /** @var Basket $basket */
+ $basket = $this->object();
+
+ if ($basket->isEmpty()) {
+ $this->addError($this->translate("It's not allowed to store an empty basket"));
+
+ return;
+ }
+ if (! $basket->hasBeenLoadedFromDb()) {
+ $basket->set('owner_type', 'user');
+ $basket->set('owner_value', $this->getAuth()->getUser()->getUsername());
+ }
+
+ parent::onSuccess();
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ /** @var Basket $basket */
+ $basket = $this->object();
+ $this->setSuccessUrl(
+ 'director/basket',
+ ['name' => $basket->get('basket_name')]
+ );
+ }
+}
diff --git a/application/forms/BasketUploadForm.php b/application/forms/BasketUploadForm.php
new file mode 100644
index 0000000..a88dc06
--- /dev/null
+++ b/application/forms/BasketUploadForm.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Web\Notification;
+
+class BasketUploadForm extends DirectorObjectForm
+{
+ protected $listUrl = 'director/baskets';
+
+ protected $failed;
+
+ protected $upload;
+
+ protected $rawUpload;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addElement('text', 'basket_name', [
+ 'label' => $this->translate('Basket Name'),
+ 'required' => true,
+ ]);
+ $this->setAttrib('enctype', 'multipart/form-data');
+
+ $this->addElement('file', 'uploaded_file', [
+ 'label' => $this->translate('Choose file'),
+ 'destination' => $this->getTempDir(),
+ 'valueDisabled' => true,
+ 'isArray' => false,
+ 'multiple' => false,
+ 'ignore' => true,
+ ]);
+
+ $this->setSubmitLabel($this->translate('Upload'));
+ }
+
+ protected function getTempDir()
+ {
+ return sys_get_temp_dir();
+ }
+
+ protected function getObjectClassname()
+ {
+ return '\\Icinga\\Module\\Director\\DirectorObject\\Automation\\Basket';
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ /** @var Basket $basket */
+ $basket = $this->object();
+ $this->setSuccessUrl(
+ 'director/basket',
+ ['name' => $basket->get('basket_name')]
+ );
+ }
+
+ /**
+ * @return bool
+ * @throws IcingaException
+ */
+ protected function processUploadedSource()
+ {
+ if (! array_key_exists('uploaded_file', $_FILES)) {
+ throw new IcingaException('Got no file');
+ }
+
+ if (! isset($_FILES['uploaded_file']['tmp_name'])
+ || ! is_uploaded_file($_FILES['uploaded_file']['tmp_name'])
+ ) {
+ $this->addError('Got no uploaded file');
+ $this->failed = true;
+
+ return false;
+ }
+ $tmpFile = $_FILES['uploaded_file']['tmp_name'];
+ $originalFilename = $_FILES['uploaded_file']['name'];
+
+ $source = file_get_contents($tmpFile);
+ unlink($tmpFile);
+ try {
+ $json = Json::decode($source);
+ $this->rawUpload = $source;
+ $this->upload = $json;
+ } catch (Exception $e) {
+ $this->addError($originalFilename . ' failed: ' . $e->getMessage());
+ Notification::error($originalFilename . ' failed: ' . $e->getMessage());
+ $this->failed = true;
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public function onRequest()
+ {
+ if ($this->hasBeenSent()) {
+ try {
+ $this->processUploadedSource();
+ } catch (Exception $e) {
+ $this->addError($e->getMessage());
+ return;
+ }
+ }
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ /** @var Basket $basket */
+ $basket = $this->object();
+
+ foreach ($this->upload as $type => $content) {
+ if ($type !== 'Datafield') {
+ $basket->addObjects($type, array_keys((array) $content));
+ }
+ }
+ if ($basket->isEmpty()) {
+ $this->addError($this->translate("It's not allowed to store an empty basket"));
+
+ return;
+ }
+
+ $basket->set('owner_type', 'user');
+ $basket->set('owner_value', $this->getAuth()->getUser()->getUsername());
+ $basket->store($this->db);
+
+ BasketSnapshot::forBasketFromJson(
+ $basket,
+ $this->rawUpload
+ )->store($this->db);
+ $this->setObjectSuccessUrl();
+ $this->beforeSuccessfulRedirect();
+ $this->redirectOnSuccess($this->translate('Basket has been uploaded'));
+ }
+}
diff --git a/application/forms/CustomvarForm.php b/application/forms/CustomvarForm.php
new file mode 100644
index 0000000..759464c
--- /dev/null
+++ b/application/forms/CustomvarForm.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class CustomvarForm extends QuickForm
+{
+ protected $submitLabel = false;
+
+ public function setup()
+ {
+ $this->removeCsrfToken();
+ $this->removeElement(self::ID);
+ $this->addElement('text', 'varname', array(
+ 'label' => $this->translate('Variable name'),
+ 'required' => true,
+ ));
+
+ $this->addElement('text', 'varvalue', array(
+ 'label' => $this->translate('Value'),
+ ));
+
+ // $this->addHidden('format', 'string'); // expression, json?
+ }
+}
diff --git a/application/forms/DeployConfigForm.php b/application/forms/DeployConfigForm.php
new file mode 100644
index 0000000..0b817fa
--- /dev/null
+++ b/application/forms/DeployConfigForm.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Core\DeploymentApiInterface;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class DeployConfigForm extends DirectorForm
+{
+ use DeployFormsBug7530;
+
+ /** @var DeploymentApiInterface */
+ private $api;
+
+ /** @var string */
+ private $checksum;
+
+ /** @var int */
+ private $deploymentId;
+
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ }
+
+ public function setup()
+ {
+ $activities = $this->db->countActivitiesSinceLastDeployedConfig();
+ if ($this->deploymentId) {
+ $label = $this->translate('Re-deploy now');
+ } elseif ($activities === 0) {
+ $label = $this->translate('There are no pending changes. Deploy anyway');
+ } else {
+ $label = sprintf(
+ $this->translate('Deploy %d pending changes'),
+ $activities
+ );
+ }
+
+ if ($this->deploymentId) {
+ $deployIcon = 'reply-all';
+ } else {
+ $deployIcon = 'forward';
+ }
+
+ $this->addHtml(
+ $this->getView()->icon(
+ $deployIcon,
+ $label,
+ array('class' => 'link-color')
+ ) . '<nobr>'
+ );
+
+ $el = $this->createElement('submit', 'btn_deploy', array(
+ 'label' => $label,
+ 'escape' => false,
+ 'decorators' => array('ViewHelper'),
+ 'class' => 'link-button ' . $deployIcon,
+ ));
+
+ $this->addHtml('</nobr>');
+ $this->submitButtonName = $el->getName();
+ $this->setSubmitLabel($label);
+ $this->addElement($el);
+ }
+
+ public function onSuccess()
+ {
+ if ($this->skipBecauseOfBug7530()) {
+ return;
+ }
+
+ $db = $this->db;
+ $msg = $this->translate('Config has been submitted, validation is going on');
+ $this->setSuccessMessage($msg);
+
+ $isApiRequest = $this->getRequest()->isApiRequest();
+ if ($this->checksum) {
+ $config = IcingaConfig::load(hex2bin($this->checksum), $db);
+ } else {
+ $config = IcingaConfig::generate($db);
+ }
+
+ $this->api->wipeInactiveStages($db);
+
+ if ($this->api->dumpConfig($config, $db)) {
+ if ($isApiRequest) {
+ die('Api not ready');
+ // return $this->sendJson((object) array('checksum' => $checksum));
+ } else {
+ $this->setSuccessUrl('director/config/deployments');
+ $this->setSuccessMessage(
+ $this->translate('Config has been submitted, validation is going on')
+ );
+ }
+ parent::onSuccess();
+ } else {
+ throw new IcingaException($this->translate('Config deployment failed'));
+ }
+ }
+
+ public function setChecksum($checksum)
+ {
+ $this->checksum = $checksum;
+ return $this;
+ }
+
+ public function setDeploymentId($id)
+ {
+ $this->deploymentId = $id;
+ return $this;
+ }
+
+ public function setApi(DeploymentApiInterface $api)
+ {
+ $this->api = $api;
+ return $this;
+ }
+}
diff --git a/application/forms/DeployFormsBug7530.php b/application/forms/DeployFormsBug7530.php
new file mode 100644
index 0000000..4d456ae
--- /dev/null
+++ b/application/forms/DeployFormsBug7530.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Module\Director\Core\CoreApi;
+use ipl\Html\Html;
+
+trait DeployFormsBug7530
+{
+ public function hasBeenSubmitted()
+ {
+ if (parent::hasBeenSubmitted()) {
+ return true;
+ } else {
+ return \strlen($this->getSentValue('confirm_7530')) > 0;
+ }
+ }
+
+ protected function shouldWarnAboutBug7530()
+ {
+ /** @var \Icinga\Module\Director\Db $db */
+ $db = $this->getDb();
+
+ return $db->settings()->get('ignore_bug7530') !== 'y'
+ && $this->getSentValue('confirm_7530') !== 'i_know'
+ && $this->configMightTriggerBug7530()
+ & $this->coreHasBug7530();
+ }
+
+ protected function configMightTriggerBug7530()
+ {
+ /** @var \Icinga\Module\Director\Db $connection */
+ $connection = $this->getDb();
+ $db = $connection->getDbAdapter();
+
+ $zoneIds = $db->fetchCol(
+ $db->select()
+ ->from('icinga_zone', 'id')
+ ->where('object_type = ?', 'object')
+ );
+ if (empty($zoneIds)) {
+ return false;
+ }
+
+ $objectTypes = [
+ 'icinga_host',
+ 'icinga_service',
+ 'icinga_notification',
+ 'icinga_command',
+ ];
+
+ foreach ($objectTypes as $objectType) {
+ if ((int) $db->fetchOne(
+ $db->select()
+ ->from($objectType, 'COUNT(*)')
+ ->where('zone_id IN (?)', $zoneIds)
+ ) > 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function coreHasBug7530()
+ {
+ // TODO: Cache this
+ if ($this->api instanceof CoreApi) {
+ $version = $this->api->getVersion();
+ if ($version === null) {
+ throw new \RuntimeException($this->translate('Unable to detect your Icinga 2 Core version'));
+ } elseif (\version_compare($version, '2.11.0', '>=')
+ && \version_compare($version, '2.12.0', '<')
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function skipBecauseOfBug7530()
+ {
+ $bug7530 = $this->getSentValue('confirm_7530');
+ if ($bug7530 === 'whaaat') {
+ $this->setSuccessMessage($this->translate('Config has not been deployed'));
+ parent::onSuccess();
+ } elseif ($bug7530 === 'hell_yes') {
+ $this->db->settings()->set('ignore_bug7530', 'y');
+ }
+ if ($this->shouldWarnAboutBug7530()) {
+ $this->addHtml(Hint::warning(Html::sprintf($this->translate(
+ "Warning: you're running Icinga v2.11.0 and our configuration looks"
+ . " like you could face issue %s. We're already working on a solution."
+ . " The GitHub Issue and our %s contain related details."
+ ), Html::tag('a', [
+ 'href' => 'https://github.com/Icinga/icinga2/issues/7530',
+ 'target' => '_blank',
+ 'title' => sprintf(
+ $this->translate('Show Issue %s on GitHub'),
+ '7530'
+ ),
+ 'class' => 'icon-github-circled',
+ ], '#7530'), Html::tag('a', [
+ 'href' => 'https://icinga.com/docs/icinga2/latest/doc/16-upgrading-icinga-2/'
+ . '#config-sync-zones-in-zones',
+ 'target' => '_blank',
+ 'title' => $this->translate('Upgrading Icinga 2 - Confic Sync: Zones in Zones'),
+ 'class' => 'icon-info-circled',
+ ], $this->translate('Upgrading documentation')))));
+ $this->addElement('select', 'confirm_7530', [
+ 'multiOptions' => $this->optionalEnum([
+ 'i_know' => $this->translate("I know what I'm doing, deploy anyway"),
+ 'hell_yes' => $this->translate("I know, please don't bother me again"),
+ 'whaaat' => $this->translate("Thanks, I'll verify this and come back later"),
+ ]),
+ 'class' => 'autosubmit',
+ 'decorators' => ['ViewHelper'],
+ ]);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/application/forms/DeploymentLinkForm.php b/application/forms/DeploymentLinkForm.php
new file mode 100644
index 0000000..f42a627
--- /dev/null
+++ b/application/forms/DeploymentLinkForm.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Authentication\Auth;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Core\DeploymentApiInterface;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Deployment\DeploymentInfo;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+use gipfl\IcingaWeb2\Icon;
+use Zend_View_Interface;
+
+class DeploymentLinkForm extends DirectorForm
+{
+ use DeployFormsBug7530;
+
+ /** @var DeploymentInfo */
+ protected $info;
+
+ /** @var Auth */
+ protected $auth;
+
+ /** @var DeploymentApiInterface */
+ protected $api;
+
+ /** @var Db */
+ protected $db;
+
+ /**
+ * @param DeploymentInfo $info
+ * @param Auth $auth
+ * @return static
+ */
+ public static function create(Db $db, DeploymentInfo $info, Auth $auth, DeploymentApiInterface $api)
+ {
+ $self = static::load();
+ $self->setAuth($auth);
+ $self->db = $db;
+ $self->info = $info;
+ $self->api = $api;
+ return $self;
+ }
+
+ public function setAuth(Auth $auth)
+ {
+ $this->auth = $auth;
+ return $this;
+ }
+
+ public function setup()
+ {
+ if (! $this->canDeploy()) {
+ return;
+ }
+
+ $onObject = $this->info->getSingleObjectChanges();
+ $total = $this->info->getTotalChanges();
+
+ if ($onObject === 0) {
+ if ($total === 1) {
+ $msg = $this->translate('There is a single pending change');
+ } else {
+ $msg = sprintf(
+ $this->translate('There are %d pending changes'),
+ $total
+ );
+ }
+ } elseif ($total === 1) {
+ $msg = $this->translate('There has been a single change to this object, nothing else has been modified');
+ } elseif ($total === $onObject) {
+ $msg = sprintf(
+ $this->translate('There have been %d changes to this object, nothing else has been modified'),
+ $onObject
+ );
+ } else {
+ $msg = sprintf(
+ $this->translate('There are %d pending changes, %d of them applied to this object'),
+ $total,
+ $onObject
+ );
+ }
+
+ $this->setAttrib('class', 'gipfl-inline-form');
+ $this->addHtml(Icon::create('wrench'));
+ try {
+ // As this is shown for single objects, ignore errors caused by an
+ // unreachable core
+ $target = $this->shouldWarnAboutBug7530() ? '_self' : '_next';
+ } catch (\Exception $e) {
+ $target = '_next';
+ }
+ $this->addSubmitButton($this->translate('Deploy'), [
+ 'class' => 'link-button icon-wrench',
+ 'title' => $msg,
+ 'data-base-target' => $target,
+ ]);
+ }
+
+ protected function canDeploy()
+ {
+ return $this->auth->hasPermission('director/deploy');
+ }
+
+ public function render(Zend_View_Interface $view = null)
+ {
+ if (! $this->canDeploy()) {
+ return '';
+ }
+
+ return parent::render($view);
+ }
+
+ public function onSuccess()
+ {
+ try {
+ if ($this->skipBecauseOfBug7530()) {
+ return;
+ }
+ } catch (\Exception $e) {
+ // continue
+ }
+ $this->deploy();
+ }
+
+ public function deploy()
+ {
+ $this->setSuccessUrl('director/config/deployments');
+ $config = IcingaConfig::generate($this->db);
+ $checksum = $config->getHexChecksum();
+
+ try {
+ $this->api->wipeInactiveStages($this->db);
+ } catch (\Exception $e) {
+ $this->notifyError($e->getMessage());
+ }
+
+ if ($this->api->dumpConfig($config, $this->db)) {
+ $this->deploymentSucceeded($checksum);
+ } else {
+ $this->deploymentFailed($checksum);
+ }
+ }
+
+ protected function deploymentSucceeded($checksum)
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ throw new IcingaException('Not yet');
+ // $this->sendJson($this->getResponse(), (object) array('checksum' => $checksum));
+ } else {
+ $msg = $this->translate('Config has been submitted, validation is going on');
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ protected function deploymentFailed($checksum, $error = null)
+ {
+ $extra = $error ? ': ' . $error: '';
+
+ if ($this->getRequest()->isApiRequest()) {
+ throw new IcingaException('Not yet');
+ // $this->sendJsonError($this->getResponse(), 'Config deployment failed' . $extra);
+ } else {
+ $msg = $this->translate('Config deployment failed') . $extra;
+ $this->notifyError($msg);
+ $this->redirectAndExit('director/config/deployments');
+ }
+ }
+}
diff --git a/application/forms/DirectorDatafieldCategoryForm.php b/application/forms/DirectorDatafieldCategoryForm.php
new file mode 100644
index 0000000..fe5efc9
--- /dev/null
+++ b/application/forms/DirectorDatafieldCategoryForm.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class DirectorDatafieldCategoryForm extends DirectorObjectForm
+{
+ protected $objectName = 'Data field category';
+
+ protected $listUrl = 'director/data/fieldcategories';
+
+ public function setup()
+ {
+ $this->addHtmlHint(
+ $this->translate(
+ 'Data field categories allow to structure Data Fields. Fields with'
+ . ' a category will be shown grouped by category.'
+ )
+ );
+
+ $this->addElement('text', 'category_name', [
+ 'label' => $this->translate('Category name'),
+ 'description' => $this->translate(
+ 'The unique name of the category used for grouping your custom Data Fields.'
+ ),
+ 'required' => true,
+ ]);
+
+ $this->addElement('text', 'description', [
+ 'label' => $this->translate('Description'),
+ ]);
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/DirectorDatafieldForm.php b/application/forms/DirectorDatafieldForm.php
new file mode 100644
index 0000000..a306bd7
--- /dev/null
+++ b/application/forms/DirectorDatafieldForm.php
@@ -0,0 +1,301 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Director\CustomVariable\CustomVariables;
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Application\Hook;
+use Exception;
+
+class DirectorDatafieldForm extends DirectorObjectForm
+{
+ protected $objectName = 'Data field';
+
+ protected $listUrl = 'director/data/fields';
+
+ protected function onRequest()
+ {
+ if ($this->hasBeenSent()) {
+ if ($this->shouldBeDeleted()) {
+ $varname = $this->getSentValue('varname');
+ if ($cnt = CustomVariables::countAll($varname, $this->getDb())) {
+ $this->askForVariableDeletion($varname, $cnt);
+ }
+ } elseif ($this->shouldBeRenamed()) {
+ $varname = $this->object()->getOriginalProperty('varname');
+ if ($cnt = CustomVariables::countAll($varname, $this->getDb())) {
+ $this->askForVariableRename(
+ $varname,
+ $this->getSentValue('varname'),
+ $cnt
+ );
+ }
+ }
+ }
+
+ parent::onRequest();
+ }
+
+ protected function askForVariableDeletion($varname, $cnt)
+ {
+ $msg = $this->translate(
+ 'Leaving custom variables in place while removing the related field is'
+ . ' perfectly legal and might be a desired operation. This way you can'
+ . ' no longer modify related custom variables in the Director GUI, but'
+ . ' the variables themselves will stay there and continue to be deployed.'
+ . ' When you re-add a field for the same variable later on, everything'
+ . ' will continue to work as before'
+ );
+
+ $this->addBoolean('wipe_vars', array(
+ 'label' => $this->translate('Wipe related vars'),
+ 'description' => sprintf($msg, $this->getSentValue('varname')),
+ 'required' => true,
+ ));
+
+ if ($wipe = $this->getSentValue('wipe_vars')) {
+ if ($wipe === 'y') {
+ CustomVariables::deleteAll($varname, $this->getDb());
+ }
+ } else {
+ $this->abortDeletion();
+ $this->addError(
+ sprintf(
+ $this->translate('Also wipe all "%s" custom variables from %d objects?'),
+ $varname,
+ $cnt
+ )
+ );
+ $this->getElement('wipe_vars')->addError(
+ sprintf(
+ $this->translate(
+ 'There are %d objects with a related property. Should I also'
+ . ' remove the "%s" property from them?'
+ ),
+ $cnt,
+ $varname
+ )
+ );
+ }
+ }
+
+ protected function askForVariableRename($oldname, $newname, $cnt)
+ {
+ $msg = $this->translate(
+ 'Leaving custom variables in place while renaming the related field is'
+ . ' perfectly legal and might be a desired operation. This way you can'
+ . ' no longer modify related custom variables in the Director GUI, but'
+ . ' the variables themselves will stay there and continue to be deployed.'
+ . ' When you re-add a field for the same variable later on, everything'
+ . ' will continue to work as before'
+ );
+
+ $this->addBoolean('rename_vars', array(
+ 'label' => $this->translate('Rename related vars'),
+ 'description' => sprintf($msg, $this->getSentValue('varname')),
+ 'required' => true,
+ ));
+
+ if ($wipe = $this->getSentValue('rename_vars')) {
+ if ($wipe === 'y') {
+ CustomVariables::renameAll($oldname, $newname, $this->getDb());
+ }
+ } else {
+ $this->abortDeletion();
+ $this->addError(
+ sprintf(
+ $this->translate('Also rename all "%s" custom variables to "%s" on %d objects?'),
+ $oldname,
+ $newname,
+ $cnt
+ )
+ );
+ $this->getElement('rename_vars')->addError(
+ sprintf(
+ $this->translate(
+ 'There are %d objects with a related property. Should I also'
+ . ' rename the "%s" property to "%s" on them?'
+ ),
+ $cnt,
+ $oldname,
+ $newname
+ )
+ );
+ }
+ }
+
+ public function setup()
+ {
+ $this->addHtmlHint(
+ $this->translate(
+ 'Data fields allow you to customize input controls for Icinga custom'
+ . ' variables. Once you defined them here, you can provide them through'
+ . ' your defined templates. This gives you a granular control over what'
+ . ' properties your users should be allowed to configure in which way.'
+ )
+ );
+
+ $this->addElement('text', 'varname', array(
+ 'label' => $this->translate('Field name'),
+ 'description' => $this->translate(
+ 'The unique name of the field. This will be the name of the custom'
+ . ' variable in the rendered Icinga configuration.'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('text', 'caption', array(
+ 'label' => $this->translate('Caption'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'The caption which should be displayed to your users when this field'
+ . ' is shown'
+ )
+ ));
+
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'An extended description for this field. Will be shown as soon as a'
+ . ' user puts the focus on this field'
+ ),
+ 'rows' => '3',
+ ));
+
+ $this->addElement('select', 'category_id', [
+ 'label' => $this->translate('Data Field Category'),
+ 'multiOptions' => $this->optionalEnum($this->enumCategpories()),
+ ]);
+
+ $error = false;
+ try {
+ $types = $this->enumDataTypes();
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ $types = $this->optionalEnum(array());
+ }
+
+ $this->addElement('select', 'datatype', array(
+ 'label' => $this->translate('Data type'),
+ 'description' => $this->translate('Field type'),
+ 'required' => true,
+ 'multiOptions' => $types,
+ 'class' => 'autosubmit',
+ ));
+ if ($error) {
+ $this->getElement('datatype')->addError($error);
+ }
+
+ $object = $this->object();
+ try {
+ if ($class = $this->getSentValue('datatype')) {
+ if ($class && array_key_exists($class, $types)) {
+ $this->addSettings($class);
+ }
+ } elseif ($class = $object->get('datatype')) {
+ $this->addSettings($class);
+ }
+
+ // TODO: next line looks like obsolete duplicate code to me
+ $this->addSettings();
+ } catch (Exception $e) {
+ $this->getElement('datatype')->addError($e->getMessage());
+ }
+
+ foreach ($object->getSettings() as $key => $val) {
+ if ($el = $this->getElement($key)) {
+ $el->setValue($val);
+ }
+ }
+
+ $this->setButtons();
+ }
+
+ public function shouldBeRenamed()
+ {
+ $object = $this->object();
+ return $object->hasBeenLoadedFromDb()
+ && $object->getOriginalProperty('varname') !== $this->getSentValue('varname');
+ }
+
+ protected function addSettings($class = null)
+ {
+ if ($class === null) {
+ $class = $this->getValue('datatype');
+ }
+
+ if ($class !== null) {
+ if (! class_exists($class)) {
+ throw new ConfigurationError(
+ 'The hooked class "%s" for this data field does no longer exist',
+ $class
+ );
+ }
+
+ $class::addSettingsFormFields($this);
+ }
+ }
+
+ protected function clearOutdatedSettings()
+ {
+ $names = array();
+ $object = $this->object();
+ $global = array('varname', 'description', 'caption', 'datatype');
+
+ /** @var \Zend_Form_Element $el */
+ foreach ($this->getElements() as $el) {
+ if ($el->getIgnore()) {
+ continue;
+ }
+
+ $name = $el->getName();
+ if (in_array($name, $global)) {
+ continue;
+ }
+
+ $names[$name] = $name;
+ }
+
+
+ foreach ($object->getSettings() as $setting => $value) {
+ if (! array_key_exists($setting, $names)) {
+ unset($object->$setting);
+ }
+ }
+ }
+
+ public function onSuccess()
+ {
+ $this->clearOutdatedSettings();
+
+ if ($class = $this->getValue('datatype')) {
+ if (array_key_exists($class, $this->enumDataTypes())) {
+ $this->addHidden('format', $class::getFormat());
+ }
+ }
+
+ parent::onSuccess();
+ }
+
+ protected function enumDataTypes()
+ {
+ $hooks = Hook::all('Director\\DataType');
+ $enum = array(null => '- please choose -');
+ /** @var DataTypeHook $hook */
+ foreach ($hooks as $hook) {
+ $enum[get_class($hook)] = $hook->getName();
+ }
+
+ return $enum;
+ }
+
+ protected function enumCategpories()
+ {
+ $db = $this->getDb()->getDbAdapter();
+ return $db->fetchPairs(
+ $db->select()->from('director_datafield_category', ['id', 'category_name'])
+ );
+ }
+}
diff --git a/application/forms/DirectorDatalistEntryForm.php b/application/forms/DirectorDatalistEntryForm.php
new file mode 100644
index 0000000..c6e309f
--- /dev/null
+++ b/application/forms/DirectorDatalistEntryForm.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Application\Config;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class DirectorDatalistEntryForm extends DirectorObjectForm
+{
+ /** @var DirectorDatalist */
+ protected $datalist;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addElement('text', 'entry_name', [
+ 'label' => $this->translate('Key'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Will be stored as a custom variable value when this entry'
+ . ' is chosen from the list'
+ )
+ ]);
+
+ $this->addElement('text', 'entry_value', [
+ 'label' => $this->translate('Label'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'This will be the visible caption for this entry'
+ )
+ ]);
+
+ $rolesConfig = Config::app('roles', true);
+ $roles = [];
+ foreach ($rolesConfig as $name => $role) {
+ $roles[$name] = $name;
+ }
+
+ $this->addElement('extensibleSet', 'allowed_roles', [
+ 'label' => $this->translate('Allowed roles'),
+ 'required' => false,
+ 'multiOptions' => $roles,
+ 'description' => $this->translate(
+ 'Allow to use this entry only to users with one of these Icinga Web 2 roles'
+ )
+ ]);
+
+ $this->addHidden('list_id', $this->datalist->get('id'));
+ $this->addHidden('format', 'string');
+ if (!$this->isNew()) {
+ $this->addHidden('entry_name', $this->object->get('entry_name'));
+ }
+
+ $this->addSimpleDisplayGroup(['entry_name', 'entry_value', 'allowed_roles'], 'entry', [
+ 'legend' => $this->isNew()
+ ? $this->translate('Add data list entry')
+ : $this->translate('Modify data list entry')
+ ]);
+
+ $this->setButtons();
+ }
+
+ /**
+ * @param DirectorDatalist $list
+ * @return $this
+ */
+ public function setList(DirectorDatalist $list)
+ {
+ $this->datalist = $list;
+ /** @var Db $db */
+ $db = $list->getConnection();
+ $this->setDb($db);
+
+ return $this;
+ }
+}
diff --git a/application/forms/DirectorDatalistForm.php b/application/forms/DirectorDatalistForm.php
new file mode 100644
index 0000000..91c0ea7
--- /dev/null
+++ b/application/forms/DirectorDatalistForm.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Authentication\Auth;
+
+class DirectorDatalistForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addElement('text', 'list_name', array(
+ 'label' => $this->translate('List name'),
+ 'description' => $this->translate(
+ 'Data lists are mainly used as data providers for custom variables'
+ . ' presented as dropdown boxes boxes. You can manually manage'
+ . ' their entries here in place, but you could also create dedicated'
+ . ' sync rules after creating a new empty list. This would allow you'
+ . ' to keep your available choices in sync with external data providers'
+ ),
+ 'required' => true,
+ ));
+ $this->addSimpleDisplayGroup(array('list_name'), 'list', array(
+ 'legend' => $this->translate('Data list')
+ ));
+
+ $this->setButtons();
+ }
+
+ public function onSuccess()
+ {
+ $this->object()->set('owner', self::username());
+ parent::onSuccess();
+ }
+
+ protected static function username()
+ {
+ $auth = Auth::getInstance();
+ if ($auth->isAuthenticated()) {
+ return $auth->getUser()->getUsername();
+ } else {
+ return '<unknown>';
+ }
+ }
+}
diff --git a/application/forms/DirectorJobForm.php b/application/forms/DirectorJobForm.php
new file mode 100644
index 0000000..7ca998c
--- /dev/null
+++ b/application/forms/DirectorJobForm.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Hook\JobHook;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Web\Hook;
+
+class DirectorJobForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $jobTypes = $this->enumJobTypes();
+
+ $this->addElement('select', 'job_class', array(
+ 'label' => $this->translate('Job Type'),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($jobTypes),
+ 'description' => $this->translate(
+ 'These are different available job types'
+ ),
+ 'class' => 'autosubmit'
+ ));
+
+ if (! $jobClass = $this->getJobClass()) {
+ return;
+ }
+
+ if ($desc = $jobClass::getDescription($this)) {
+ $this->addHtmlHint($desc);
+ }
+
+ $this->addBoolean(
+ 'disabled',
+ array(
+ 'label' => $this->translate('Disabled'),
+ 'description' => $this->translate(
+ 'This allows to temporarily disable this job'
+ )
+ ),
+ 'n'
+ );
+
+ $this->addElement('text', 'run_interval', array(
+ 'label' => $this->translate('Run interval'),
+ 'description' => $this->translate(
+ 'Execution interval for this job, in seconds'
+ ),
+ 'value' => $jobClass::getSuggestedRunInterval($this)
+ ));
+
+ $periods = $this->db->enumTimeperiods();
+
+ if (!empty($periods)) {
+ $this->addElement(
+ 'select',
+ 'timeperiod_id',
+ array(
+ 'label' => $this->translate('Time period'),
+ 'description' => $this->translate(
+ 'The name of a time period within this job should be active.'
+ . ' Supports only simple time periods (weekday and multiple'
+ . ' time definitions)'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+ }
+
+ $this->addElement('text', 'job_name', array(
+ 'label' => $this->translate('Job name'),
+ 'description' => $this->translate(
+ 'A short name identifying this job. Use something meaningful,'
+ . ' like "Import Puppet Hosts"'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addSettings();
+ $this->setButtons();
+ }
+
+ public function getSentOrObjectSetting($name, $default = null)
+ {
+ if ($this->hasObject()) {
+ $value = $this->getSentValue($name);
+ if ($value === null) {
+ /** @var DbObjectWithSettings $object */
+ $object = $this->getObject();
+ return $object->getSetting($name, $default);
+ } else {
+ return $value;
+ }
+ } else {
+ return $this->getSentValue($name, $default);
+ }
+ }
+
+ protected function getJobClass($class = null)
+ {
+ if ($class === null) {
+ $class = $this->getSentOrObjectValue('job_class');
+ }
+
+ if (array_key_exists($class, $this->enumJobTypes())) {
+ return $class;
+ }
+
+ return null;
+ }
+
+ protected function addSettings($class = null)
+ {
+ if (! $class = $this->getJobClass($class)) {
+ return;
+ }
+
+ $class::addSettingsFormFields($this);
+ foreach ($this->object()->getSettings() as $key => $val) {
+ if ($el = $this->getElement($key)) {
+ $el->setValue($val);
+ }
+ }
+ }
+
+ protected function enumJobTypes()
+ {
+ /** @var JobHook[] $hooks */
+ $hooks = Hook::all('Director\\Job');
+
+ $enum = array();
+
+ foreach ($hooks as $hook) {
+ $enum[get_class($hook)] = $hook->getName();
+ }
+ asort($enum);
+
+ return $enum;
+ }
+}
diff --git a/application/forms/IcingaAddServiceForm.php b/application/forms/IcingaAddServiceForm.php
new file mode 100644
index 0000000..df2302e
--- /dev/null
+++ b/application/forms/IcingaAddServiceForm.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\IcingaWeb2\Link;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+
+class IcingaAddServiceForm extends DirectorObjectForm
+{
+ /** @var IcingaHost[] */
+ private $hosts;
+
+ /** @var IcingaHost */
+ private $host;
+
+ /** @var IcingaService */
+ protected $object;
+
+ protected $objectName = 'service';
+
+ public function setup()
+ {
+ if ($this->object === null) {
+ $this->object = IcingaService::create(
+ ['object_type' => 'object'],
+ $this->db
+ );
+ }
+
+ $this->addSingleImportElement();
+
+ if (! ($imports = $this->getSentOrObjectValue('imports'))) {
+ $this->setSubmitLabel($this->translate('Next'));
+ $this->groupMainProperties();
+ return;
+ }
+
+ $this->removeElement('imports');
+ $this->addHidden('imports', $imports);
+ $this->setElementValue('imports', $imports);
+ $this->addNameElement();
+ $name = $this->getSentOrObjectValue('object_name');
+ if (empty($name)) {
+ $this->setElementValue('object_name', $imports);
+ }
+ $this->groupMainProperties()
+ ->setButtons();
+ }
+
+ protected function groupMainProperties($importsFirst = false)
+ {
+ $elements = [
+ 'object_type',
+ 'imports',
+ 'object_name',
+ ];
+
+ $this->addDisplayGroup($elements, 'object_definition', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'Fieldset',
+ ],
+ 'order' => self::GROUP_ORDER_OBJECT_DEFINITION,
+ 'legend' => $this->translate('Main properties')
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @param bool $required
+ * @return $this
+ */
+ protected function addSingleImportElement($required = null)
+ {
+ $enum = $this->enumServiceTemplates();
+ if (empty($enum)) {
+ if ($required) {
+ if ($this->hasBeenSent()) {
+ $this->addError($this->translate('No service has been chosen'));
+ } else {
+ if ($this->hasPermission('director/admin')) {
+ $html = sprintf(
+ $this->translate('Please define a %s first'),
+ Link::create(
+ $this->translate('Service Template'),
+ 'director/service/add',
+ ['type' => 'template']
+ )
+ );
+ } else {
+ $html = $this->translate('No Service Templates have been provided yet');
+ }
+ $this->addHtml('<p class="warning">' . $html . '</p>');
+ }
+ }
+
+ return $this;
+ }
+ $this->addElement('text', 'imports', [
+ 'label' => $this->translate('Service'),
+ 'description' => $this->translate('Choose a service template'),
+ 'required' => true,
+ 'data-suggestion-context' => 'servicetemplates',
+ 'class' => 'autosubmit director-suggest'
+ ]);
+
+ return $this;
+ }
+
+ protected function enumServiceTemplates()
+ {
+ $tpl = $this->getDb()->enumIcingaTemplates('service');
+ return array_combine($tpl, $tpl);
+ }
+
+ /**
+ * @param IcingaHost[] $hosts
+ * @return $this
+ */
+ public function setHosts(array $hosts)
+ {
+ $this->hosts = $hosts;
+ return $this;
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @return $this
+ */
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ protected function addNameElement()
+ {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Name'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Name for the Icinga service you are going to create'
+ )
+ ]);
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ if ($this->host !== null) {
+ if ($id = $this->host->get('id')) {
+ $this->object->set('host_id', $id);
+ } else {
+ $this->object->set('host', $this->host->getObjectName());
+ }
+ parent::onSuccess();
+ return;
+ }
+
+ $plain = $this->object->toPlainObject();
+ $db = $this->object->getConnection();
+
+ // TODO: Test this:
+ foreach ($this->hosts as $host) {
+ $service = IcingaService::fromPlainObject($plain, $db)
+ ->set('host_id', $host->get('id'));
+ $this->getDbObjectStore()->store($service);
+ }
+
+ $msg = sprintf(
+ $this->translate('The service "%s" has been added to %d hosts'),
+ $this->object->getObjectName(),
+ count($this->hosts)
+ );
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaAddServiceSetForm.php b/application/forms/IcingaAddServiceSetForm.php
new file mode 100644
index 0000000..b889110
--- /dev/null
+++ b/application/forms/IcingaAddServiceSetForm.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaAddServiceSetForm extends DirectorObjectForm
+{
+ /** @var IcingaHost[] */
+ private $hosts;
+
+ /** @var IcingaHost */
+ private $host;
+
+ /** @var IcingaServiceSet */
+ protected $object;
+
+ protected $objectName = 'service_set';
+
+ protected $listUrl = 'director/services/sets';
+
+ public function setup()
+ {
+ if ($this->object === null) {
+ $this->object = IcingaServiceSet::create(
+ ['object_type' => 'object'],
+ $this->db
+ );
+ }
+
+ $object = $this->object();
+ if ($this->hasBeenSent()) {
+ $object->set('object_name', $this->getSentValue('imports'));
+ $object->set('imports', $object->getObjectName());
+ }
+
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->addSingleImportsElement();
+ }
+
+ if (count($object->get('imports'))) {
+ $description = $object->getResolvedProperty('description');
+ if ($description) {
+ $this->addHtmlHint($description);
+ }
+ }
+
+ $this->addHidden('object_type', 'object');
+ $this->setButtons();
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ if ($this->host) {
+ $this->setSuccessUrl(
+ 'director/host/services',
+ array('name' => $this->host->getObjectName())
+ );
+ } else {
+ parent::setObjectSuccessUrl();
+ }
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+ /**
+ * @param IcingaHost[] $hosts
+ * @return $this
+ */
+ public function setHosts(array $hosts)
+ {
+ $this->hosts = $hosts;
+ return $this;
+ }
+
+ protected function addSingleImportsElement()
+ {
+ $enum = $this->enumAllowedTemplates();
+
+ $this->addElement('select', 'imports', array(
+ 'label' => $this->translate('Service set'),
+ 'description' => $this->translate(
+ 'The service Set that should be assigned'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionallyAddFromEnum($enum),
+ 'class' => 'autosubmit'
+ ));
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ if ($this->host !== null) {
+ $this->object->set('host_id', $this->host->get('id'));
+ parent::onSuccess();
+ return;
+ }
+
+ $plain = $this->object->toPlainObject();
+ $db = $this->object->getConnection();
+
+ foreach ($this->hosts as $host) {
+ IcingaServiceSet::fromPlainObject($plain, $db)
+ ->set('host_id', $host->get('id'))
+ ->store();
+ }
+
+ $msg = sprintf(
+ $this->translate('The Service Set "%s" has been added to %d hosts'),
+ $this->object->getObjectName(),
+ count($this->hosts)
+ );
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaApiUserForm.php b/application/forms/IcingaApiUserForm.php
new file mode 100644
index 0000000..eda0857
--- /dev/null
+++ b/application/forms/IcingaApiUserForm.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaApiUserForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addHidden('object_type', 'external_object');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Name'),
+ 'required' => true,
+ ));
+
+ $this->addElement('password', 'password', array(
+ 'label' => $this->translate('Password'),
+ 'required' => true,
+ ));
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/IcingaCloneObjectForm.php b/application/forms/IcingaCloneObjectForm.php
new file mode 100644
index 0000000..6ee99ba
--- /dev/null
+++ b/application/forms/IcingaCloneObjectForm.php
@@ -0,0 +1,259 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Acl;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class IcingaCloneObjectForm extends DirectorForm
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ protected $baseObjectUrl;
+
+ /** @var Branch */
+ protected $branch;
+
+ public function setup()
+ {
+ $isBranch = $this->branch && $this->branch->isBranch();
+ $branchOnly = $this->object->get('id') === null;
+ if ($isBranch && $this->object instanceof IcingaObject && $this->object->isTemplate()) {
+ $this->addHtml(Hint::error($this->translate(
+ 'Templates cannot be cloned in Configuration Branches'
+ )));
+ $this->submitLabel = false;
+ return;
+ }
+ $name = $this->object->getObjectName();
+ $this->addElement('text', 'new_object_name', array(
+ 'label' => $this->translate('New name'),
+ 'required' => true,
+ 'value' => $name,
+ ));
+
+ if (!$branchOnly && Acl::instance()->hasPermission('director/admin')) {
+ $this->addElement('select', 'clone_type', array(
+ 'label' => 'Clone type',
+ 'required' => true,
+ 'multiOptions' => array(
+ 'equal' => $this->translate('Clone the object as is, preserving imports'),
+ 'flat' => $this->translate('Flatten all inherited properties, strip imports'),
+ )
+ ));
+ }
+
+ if (!$branchOnly && ($this->object instanceof IcingaHost
+ || $this->object instanceof IcingaServiceSet)
+ ) {
+ $this->addBoolean('clone_services', [
+ 'label' => $this->translate('Clone Services'),
+ 'description' => $this->translate(
+ 'Also clone single Services defined for this Host'
+ )
+ ], 'y');
+ }
+
+ if (!$branchOnly && $this->object instanceof IcingaHost) {
+ $this->addBoolean('clone_service_sets', [
+ 'label' => $this->translate('Clone Service Sets'),
+ 'description' => $this->translate(
+ 'Also clone single Service Sets defined for this Host'
+ )
+ ], 'y');
+ }
+
+ if ($this->object instanceof IcingaService) {
+ if ($this->object->get('service_set_id') !== null) {
+ $this->addElement('select', 'target_service_set', [
+ 'label' => $this->translate('Target Service Set'),
+ 'description' => $this->translate(
+ 'Clone this service to the very same or to another Service Set'
+ ),
+ 'multiOptions' => $this->enumServiceSets(),
+ 'value' => $this->object->get('service_set_id')
+ ]);
+ } elseif ($this->object->get('host_id') !== null) {
+ $this->addElement('text', 'target_host', [
+ 'label' => $this->translate('Target Host'),
+ 'description' => $this->translate(
+ 'Clone this service to the very same or to another Host'
+ ),
+ 'value' => $this->object->get('host'),
+ 'class' => "autosubmit director-suggest",
+ 'data-suggestion-context' => 'HostsAndTemplates',
+ ]);
+ }
+ }
+
+ if ($this->object->isTemplate() && $this->object->supportsFields()) {
+ $this->addBoolean('clone_fields', [
+ 'label' => $this->translate('Clone Template Fields'),
+ 'description' => $this->translate(
+ 'Also clone fields provided by this Template'
+ )
+ ], 'y');
+ }
+
+ $this->submitLabel = sprintf(
+ $this->translate('Clone "%s"'),
+ $name
+ );
+ }
+
+ public function setBranch(Branch $branch)
+ {
+ $this->branch = $branch;
+
+ return $this;
+ }
+
+ public function setObjectBaseUrl($url)
+ {
+ $this->baseObjectUrl = $url;
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object;
+ $table = $object->getTableName();
+ $type = $object->getShortTableName();
+ $connection = $object->getConnection();
+ $db = $connection->getDbAdapter();
+ $newName = $this->getValue('new_object_name');
+ $resolve = Acl::instance()->hasPermission('director/admin')
+ && $this->getValue('clone_type') === 'flat';
+
+ $msg = sprintf(
+ 'The %s "%s" has been cloned from "%s"',
+ $type,
+ $newName,
+ $object->getObjectName()
+ );
+
+ if ($object->isTemplate() && $this->branch && $this->branch->isBranch()) {
+ throw new IcingaException('Cloning templates is not available for Branches');
+ }
+
+ if ($object->isTemplate() && $object->getObjectName() === $newName) {
+ throw new IcingaException(
+ $this->translate('Name needs to be changed when cloning a Template')
+ );
+ }
+
+ $new = $object::fromPlainObject(
+ $object->toPlainObject($resolve),
+ $connection
+ )->set('object_name', $newName);
+
+ if ($new->isExternal()) {
+ $new->set('object_type', 'object');
+ }
+
+ if ($set = $this->getValue('target_service_set')) {
+ $new->set(
+ 'service_set_id',
+ IcingaServiceSet::loadWithAutoIncId((int) $set, $connection)->get('id')
+ );
+ } elseif ($host = $this->getValue('target_host')) {
+ $new->set('host', $host);
+ }
+
+ $services = [];
+ $sets = [];
+ if ($object instanceof IcingaHost) {
+ $new->set('api_key', null);
+ if ($this->getValue('clone_services') === 'y') {
+ $services = $object->fetchServices();
+ }
+ if ($this->getValue('clone_service_sets') === 'y') {
+ $sets = $object->fetchServiceSets();
+ }
+ } elseif ($object instanceof IcingaServiceSet) {
+ if ($this->getValue('clone_services') === 'y') {
+ $services = $object->fetchServices();
+ }
+ }
+ if ($this->getValue('clone_fields') === 'y') {
+ $fields = $db->fetchAll(
+ $db->select()
+ ->from($table . '_field')
+ ->where("${type}_id = ?", $object->get('id'))
+ );
+ } else {
+ $fields = [];
+ }
+
+ $store = new DbObjectStore($connection, $this->branch);
+ if ($store->store($new)) {
+ $newId = $new->get('id');
+ foreach ($services as $service) {
+ $clone = IcingaService::fromPlainObject(
+ $service->toPlainObject(),
+ $connection
+ );
+
+ if ($new instanceof IcingaHost) {
+ $clone->set('host_id', $newId);
+ } elseif ($new instanceof IcingaServiceSet) {
+ $clone->set('service_set_id', $newId);
+ }
+ $store->store($clone);
+ }
+
+ foreach ($sets as $set) {
+ $newSet = IcingaServiceSet::fromPlainObject(
+ $set->toPlainObject(),
+ $connection
+ )->set('host_id', $newId);
+ $store->store($newSet);
+ }
+
+ foreach ($fields as $row) {
+ $row->{"${type}_id"} = $newId;
+ $db->insert($table . '_field', (array) $row);
+ }
+
+ if ($new instanceof IcingaServiceSet) {
+ $this->setSuccessUrl(
+ 'director/serviceset',
+ $new->getUrlParams()
+ );
+ } else {
+ $this->setSuccessUrl(
+ $this->baseObjectUrl ?: 'director/' . strtolower($type),
+ $new->getUrlParams()
+ );
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ protected function enumServiceSets()
+ {
+ $db = $this->object->getConnection()->getDbAdapter();
+ return $db->fetchPairs(
+ $db->select()
+ ->from('icinga_service_set', ['id', 'object_name'])
+ ->order('object_name')
+ );
+ }
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaCommandArgumentForm.php b/application/forms/IcingaCommandArgumentForm.php
new file mode 100644
index 0000000..5dbef41
--- /dev/null
+++ b/application/forms/IcingaCommandArgumentForm.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaCommandArgumentForm extends DirectorObjectForm
+{
+ /** @var IcingaCommand */
+ protected $commandObject;
+
+ public function setCommandObject(IcingaCommand $object)
+ {
+ $this->commandObject = $object;
+ $this->setDb($object->getConnection());
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->addHidden('command_id', $this->commandObject->get('id'));
+
+ $this->addElement('text', 'argument_name', array(
+ 'label' => $this->translate('Argument name'),
+ 'filters' => array('StringTrim'),
+ 'description' => $this->translate('e.g. -H or --hostname, empty means "skip_key"')
+ ));
+
+ $this->addElement('text', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate('Description of the argument')
+ ));
+
+ $this->addElement('select', 'argument_format', array(
+ 'label' => $this->translate('Value type'),
+ 'multiOptions' => array(
+ 'string' => $this->translate('String'),
+ 'expression' => $this->translate('Icinga DSL')
+ ),
+ 'description' => $this->translate(
+ 'Whether the argument value is a string (allowing macros like $host$)'
+ . ' or an Icinga DSL lambda function (will be enclosed with {{ ... }}'
+ ),
+ 'class' => 'autosubmit',
+ ));
+
+ if ($this->getSentOrObjectValue('argument_format') === 'expression') {
+ $this->addElement('textarea', 'argument_value', array(
+ 'label' => $this->translate('Value'),
+ 'description' => $this->translate(
+ 'An Icinga DSL expression, e.g.: var cmd = macro("$cmd$");'
+ . ' return typeof(command) == String ...'
+ ),
+ 'rows' => 3
+ ));
+ } else {
+ $this->addElement('text', 'argument_value', array(
+ 'label' => $this->translate('Value'),
+ 'description' => $this->translate(
+ 'e.g. 5%, $host.name$, $lower$%:$upper$%'
+ )
+ ));
+ }
+
+ $this->addElement('text', 'sort_order', array(
+ 'label' => $this->translate('Position'),
+ 'description' => $this->translate(
+ 'Leave empty for non-positional arguments. Can be a positive or'
+ . ' negative number and influences argument ordering'
+ )
+ ));
+
+ $this->addElement('select', 'set_if_format', array(
+ 'label' => $this->translate('Condition format'),
+ 'multiOptions' => array(
+ 'string' => $this->translate('String'),
+ 'expression' => $this->translate('Icinga DSL')
+ ),
+ 'description' => $this->translate(
+ 'Whether the set_if parameter is a string (allowing macros like $host$)'
+ . ' or an Icinga DSL lambda function (will be enclosed with {{ ... }}'
+ ),
+ 'class' => 'autosubmit',
+ ));
+
+ if ($this->getSentOrObjectValue('set_if_format') === 'expression') {
+ $this->addElement('textarea', 'set_if', array(
+ 'label' => $this->translate('Condition (set_if)'),
+ 'description' => $this->translate(
+ 'An Icinga DSL expression that returns a boolean value, e.g.: var cmd = bool(macro("$cmd$"));'
+ . ' return cmd ...'
+ ),
+ 'rows' => 3
+ ));
+ } else {
+ $this->addElement('text', 'set_if', array(
+ 'label' => $this->translate('Condition (set_if)'),
+ 'description' => $this->translate(
+ 'Only set this parameter if the argument value resolves to a'
+ . ' numeric value. String values are not supported'
+ )
+ ));
+ }
+
+ $this->addBoolean('repeat_key', array(
+ 'label' => $this->translate('Repeat key'),
+ 'description' => $this->translate(
+ 'Whether this parameter should be repeated when multiple values'
+ . ' (read: array) are given'
+ )
+ ));
+
+ $this->addBoolean('skip_key', array(
+ 'label' => $this->translate('Skip key'),
+ 'description' => $this->translate(
+ 'Whether the parameter name should not be passed to the command.'
+ . ' Per default, the parameter name (e.g. -H) will be appended,'
+ . ' so no need to explicitly set this to "No".'
+ )
+ ));
+
+ $this->addBoolean('required', array(
+ 'label' => $this->translate('Required'),
+ 'required' => false,
+ 'description' => $this->translate('Whether this argument should be required')
+ ));
+
+ $this->setButtons();
+ }
+
+ protected function deleteObject($object)
+ {
+ $cmd = $this->commandObject;
+
+ $msg = sprintf(
+ $this->translate('%s argument "%s" has been removed'),
+ $this->translate($this->getObjectShortClassName()),
+ $object->argument_name
+ );
+
+ // TODO: remove argument_id, once verified that it is no longer in use
+ $url = $this->getSuccessUrl()->without('argument_id')->without('argument');
+
+ $cmd->arguments()->remove($object->argument_name);
+ if ($this->branch->isBranch()) {
+ $this->getDbObjectStore()->store($cmd);
+ $this->setSuccessUrl($url);
+ } else {
+ if ($cmd->store()) {
+ $this->setSuccessUrl($url);
+ }
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object();
+ $cmd = $this->commandObject;
+ if ($object->get('argument_name') === null) {
+ $object->set('skip_key', true);
+ $object->set('argument_name', $cmd->getNextSkippableKeyName());
+ }
+
+ if ($object->hasBeenModified()) {
+ $cmd->arguments()->set(
+ $object->get('argument_name'),
+ $object
+ );
+ $msg = sprintf(
+ $this->translate('The argument %s has successfully been stored'),
+ $object->get('argument_name')
+ );
+ $this->getDbObjectStore()->store($cmd);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+ $this->setSuccessUrl('director/command/arguments', [
+ 'argument' => $object->get('argument_name'),
+ 'name' => $cmd->getObjectName()
+ ]);
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaCommandForm.php b/application/forms/IcingaCommandForm.php
new file mode 100644
index 0000000..ba1386b
--- /dev/null
+++ b/application/forms/IcingaCommandForm.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaCommandForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ return;
+ }
+
+ $this->addElement('select', 'methods_execute', array(
+ 'label' => $this->translate('Command type'),
+ 'multiOptions' => array(
+ null => '- please choose -',
+ $this->translate('Plugin commands') => array(
+ 'PluginCheck' => 'Plugin Check Command',
+ 'PluginNotification' => 'Notification Plugin Command',
+ 'PluginEvent' => 'Event Plugin Command',
+ ),
+ $this->translate('Internal commands') => array(
+ 'IcingaCheck' => 'Icinga Check Command',
+ 'ClusterCheck' => 'Icinga Cluster Check Command',
+ 'ClusterZoneCheck' => 'Icinga Cluster Zone Check Command',
+ 'IdoCheck' => 'Ido Check Command',
+ 'RandomCheck' => 'Random Check Command',
+ )
+ ),
+ 'required' => ! $this->isTemplate(),
+ 'description' => $this->translate(
+ 'Plugin Check commands are what you need when running checks agains'
+ . ' your infrastructure. Notification commands will be used when it'
+ . ' comes to notify your users. Event commands allow you to trigger'
+ . ' specific actions when problems occur. Some people use them for'
+ . ' auto-healing mechanisms, like restarting services or rebooting'
+ . ' systems at specific thresholds'
+ ),
+ 'class' => 'autosubmit'
+ ));
+
+ $nameLabel = $this->isTemplate()
+ ? $this->translate('Name')
+ : $this->translate('Command name');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $nameLabel,
+ 'required' => true,
+ 'description' => $this->translate('Identifier for the Icinga command you are going to create')
+ ));
+
+ $this->addImportsElement(false);
+
+ $this->addElement('text', 'command', array(
+ 'label' => $this->translate('Command'),
+ 'required' => ! $this->isTemplate(),
+ 'description' => $this->translate(
+ 'The command Icinga should run. Absolute paths are accepted as provided,'
+ . ' relative paths are prefixed with "PluginDir + ", similar Constant prefixes are allowed.'
+ . ' Spaces will lead to separation of command path and standalone arguments. Please note that'
+ . ' this means that we do not support spaces in plugin names and paths right now.'
+ )
+ ));
+
+ $this->addElement('text', 'timeout', array(
+ 'label' => $this->translate('Timeout'),
+ 'description' => $this->translate(
+ 'Optional command timeout. Allowed values are seconds or durations postfixed with a'
+ . ' specific unit (e.g. 1m or also 3m 30s).'
+ )
+ ));
+
+ $descIsString = [
+ $this->translate('Render the command as a plain string instead of an array.'),
+ $this->translate('If enabled you can not define arguments.'),
+ $this->translate('Disabled by default, and should only be used in rare cases.'),
+ $this->translate('WARNING, this can allow shell script injection via custom variables used in command.'),
+ ];
+
+ $this->addBoolean(
+ 'is_string',
+ array(
+ 'label' => $this->translate('Render as string'),
+ 'description' => join(' ', $descIsString),
+ )
+ );
+
+ $this->addDisabledElement();
+ $this->addZoneSection();
+ $this->setButtons();
+ }
+
+ protected function addZoneSection()
+ {
+ $this->addZoneElement(true);
+
+ $elements = array(
+ 'zone_id',
+ );
+ $this->addDisplayGroup($elements, 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ));
+
+ return $this;
+ }
+
+ protected function enumAllowedTemplates()
+ {
+ $object = $this->object();
+ $tpl = $this->db->enum($object->getTableName());
+ if (empty($tpl)) {
+ return array();
+ }
+
+ $id = $object->get('id');
+
+ if (array_key_exists($id, $tpl)) {
+ unset($tpl[$id]);
+ }
+
+ if (empty($tpl)) {
+ return array();
+ }
+
+ $tpl = array_combine($tpl, $tpl);
+ return $tpl;
+ }
+}
diff --git a/application/forms/IcingaDeleteObjectForm.php b/application/forms/IcingaDeleteObjectForm.php
new file mode 100644
index 0000000..409bdc3
--- /dev/null
+++ b/application/forms/IcingaDeleteObjectForm.php
@@ -0,0 +1,41 @@
+<?php
+
+// TODO: Check whether this can be removed
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class IcingaDeleteObjectForm extends QuickForm
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ public function setup()
+ {
+ $this->submitLabel = sprintf(
+ $this->translate('YES, please delete "%s"'),
+ $this->object->getObjectName()
+ );
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object;
+ $msg = sprintf(
+ 'The %s "%s" has been deleted',
+ $object->getShortTableName(),
+ $object->getObjectName()
+ );
+
+ if ($object->delete()) {
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaDependencyForm.php b/application/forms/IcingaDependencyForm.php
new file mode 100644
index 0000000..ab30844
--- /dev/null
+++ b/application/forms/IcingaDependencyForm.php
@@ -0,0 +1,309 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Objects\IcingaDependency;
+
+class IcingaDependencyForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->setupDependencyElements();
+ }
+
+ /***
+ * @throws \Zend_Form_Exception
+ */
+ protected function setupDependencyElements()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ $this->addNameElement()
+ ->addDisabledElement()
+ ->addImportsElement()
+ ->addObjectsElement()
+ ->addBooleanElements()
+ ->addPeriodElement()
+ ->addAssignmentElements()
+ ->addEventFilterElements(['states'])
+ ->groupMainProperties()
+ ->addZoneSection()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addZoneSection()
+ {
+ $this->addZoneElement(true);
+
+ $elements = array(
+ 'zone_id',
+ );
+ $this->addDisplayGroup($elements, 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addNameElement()
+ {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Name'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga dependency you are going to create')
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addAssignmentElements()
+ {
+ if (!$this->object || !$this->object->isApplyRule()) {
+ return $this;
+ }
+
+ $this->addElement('select', 'apply_to', [
+ 'label' => $this->translate('Apply to'),
+ 'description' => $this->translate(
+ 'Whether this dependency should affect hosts or services'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $this->optionalEnum([
+ 'host' => $this->translate('Hosts'),
+ 'service' => $this->translate('Services'),
+ ])
+ ]);
+
+ $applyTo = $this->getSentOrObjectValue('apply_to');
+
+ if (! $applyTo) {
+ return $this;
+ }
+
+ $suggestionContext = ucfirst($applyTo) . 'FilterColumns';
+ $this->addAssignFilter([
+ 'suggestionContext' => $suggestionContext,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addPeriodElement()
+ {
+ $periods = $this->db->enumTimeperiods();
+ if (empty($periods)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'select',
+ 'period_id',
+ array(
+ 'label' => $this->translate('Time period'),
+ 'description' => $this->translate(
+ 'The name of a time period which determines when this'
+ . ' notification should be triggered. Not set by default.'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addBooleanElements()
+ {
+ $this->addBoolean('disable_checks', [
+ 'label' => $this->translate('Disable Checks'),
+ 'description' => $this->translate(
+ 'Whether to disable checks when this dependency fails.'
+ . ' Defaults to false.'
+ )
+ ], null);
+
+ $this->addBoolean('disable_notifications', [
+ 'label' => $this->translate('Disable Notificiations'),
+ 'description' => $this->translate(
+ 'Whether to disable notifications when this dependency fails.'
+ . ' Defaults to true.'
+ )
+ ], null);
+
+ $this->addBoolean('ignore_soft_states', [
+ 'label' => $this->translate('Ignore Soft States'),
+ 'description' => $this->translate(
+ 'Whether to ignore soft states for the reachability calculation.'
+ . ' Defaults to true.'
+ )
+ ], null);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addObjectsElement()
+ {
+ $dependency = $this->getObject();
+ $parentHost = $dependency->get('parent_host');
+ if ($parentHost === null) {
+ $parentHostVar = $dependency->get('parent_host_var');
+ if (\strlen($parentHostVar) > 0) {
+ $parentHost = '$' . $dependency->get('parent_host_var') . '$';
+ }
+ }
+ $this->addElement('text', 'parent_host', [
+ 'label' => $this->translate('Parent Host'),
+ 'description' => $this->translate(
+ 'The parent host. You might want to refer Host Custom Variables'
+ . ' via $host.vars.varname$'
+ ),
+ 'class' => "autosubmit director-suggest",
+ 'data-suggestion-context' => 'hostnames',
+ 'order' => 10,
+ 'required' => $this->isObject(),
+ 'value' => $parentHost
+ ]);
+ $sentParent = $this->getSentOrObjectValue('parent_host');
+
+ if (!empty($sentParent) || $dependency->isApplyRule()) {
+ $parentService = $dependency->get('parent_service');
+ $this->addElement('text', 'parent_service', [
+ 'label' => $this->translate('Parent Service'),
+ 'description' => $this->translate(
+ 'Optional. The parent service. If omitted this dependency'
+ . ' object is treated as host dependency.'
+ ),
+ 'class' => "autosubmit director-suggest",
+ 'data-suggestion-context' => 'servicenames',
+ 'data-suggestion-for-host' => $sentParent,
+ 'order' => 20,
+ 'value' => $parentService
+ ]);
+ }
+
+ // If configuring Object, allow selection of child host and/or service,
+ // otherwise apply rules will determine child object.
+ if ($dependency->isObject()) {
+ $this->addElement('text', 'child_host', [
+ 'label' => $this->translate('Child Host'),
+ 'description' => $this->translate('The child host.'),
+ 'value' => $dependency->get('child_host'),
+ 'order' => 30,
+ 'class' => 'autosubmit director-suggest',
+ 'required' => $this->isObject(),
+ 'data-suggestion-context' => 'hostnames',
+ ]);
+
+ $sentChild = $this->getSentOrObjectValue('child_host');
+
+ if (!empty($sentChild)) {
+ $this->addElement('text', 'child_service', [
+ 'label' => $this->translate('Child Service'),
+ 'description' => $this->translate(
+ 'Optional. The child service. If omitted this dependency'
+ . ' object is treated as host dependency.'
+ ),
+ 'class' => 'autosubmit director-suggest',
+ 'order' => 40,
+ 'value' => $this->getObject()->get('child_service'),
+ 'data-suggestion-context' => 'servicenames',
+ 'data-suggestion-for-host' => $sentChild,
+ ]);
+ }
+ }
+
+ $elements = ['parent_host', 'child_host', 'parent_service', 'child_service'];
+ $this->addDisplayGroup($elements, 'related_objects', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'Fieldset',
+ ],
+ 'order' => self::GROUP_ORDER_RELATED_OBJECTS,
+ 'legend' => $this->translate('Related Objects')
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Hint: this is unused. Why?
+ *
+ * @param IcingaDependency $dependency
+ * @return $this
+ */
+ public function createApplyRuleFor(IcingaDependency $dependency)
+ {
+ $object = $this->object();
+ $object->setImports($dependency->getObjectName());
+ $object->set('object_type', 'apply');
+ $object->set('object_name', $dependency->getObjectName());
+
+ return $this;
+ }
+
+ protected function handleProperties(DbObject $object, &$values)
+ {
+ if ($this->hasBeenSent()) {
+ if (isset($values['parent_host'])
+ && $this->isCustomVar($values['parent_host'])
+ ) {
+ $values['parent_host_var'] = \trim($values['parent_host'], '$');
+ $values['parent_host'] = '';
+ }
+ }
+
+ parent::handleProperties($object, $values);
+ }
+
+ protected function isCustomVar($string)
+ {
+ return \preg_match('/^\$(?:host)\.vars\..+\$$/', $string);
+ // Eventually: return \preg_match('/^\$(?:host|service)\.vars\..+\$$/', $string);
+ }
+}
diff --git a/application/forms/IcingaEndpointForm.php b/application/forms/IcingaEndpointForm.php
new file mode 100644
index 0000000..1c08cb4
--- /dev/null
+++ b/application/forms/IcingaEndpointForm.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaEndpointForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ return;
+ }
+
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Endpoint template name'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga endpoint template you are going to create')
+ ));
+ } else {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Endpoint'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga endpoint you are going to create')
+ ));
+ }
+
+ $this->addElement('text', 'host', array(
+ 'label' => $this->translate('Endpoint address'),
+ 'description' => $this->translate('IP address / hostname of remote node')
+ ));
+
+ $this->addElement('text', 'port', array(
+ 'label' => $this->translate('Port'),
+ 'description' => $this->translate('The port of the endpoint.'),
+ ));
+
+ $this->addElement('text', 'log_duration', array(
+ 'label' => $this->translate('Log Duration'),
+ 'description' => $this->translate('The log duration time.')
+ ));
+
+ $this->addElement('select', 'apiuser_id', array(
+ 'label' => $this->translate('API user'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumApiUsers())
+ ));
+
+ $this->addZoneElement();
+
+ if ($this->object->hasBeenLoadedFromDb()) {
+ $imports = $this->object->get('imports');
+ if ($imports !== null && count($imports) > 0) {
+ $this->addImportsElement(false);
+ }
+ }
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/IcingaForgetApiKeyForm.php b/application/forms/IcingaForgetApiKeyForm.php
new file mode 100644
index 0000000..d1f475c
--- /dev/null
+++ b/application/forms/IcingaForgetApiKeyForm.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class IcingaForgetApiKeyForm extends DirectorForm
+{
+ /** @var IcingaHost */
+ protected $host;
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->addStandaloneSubmitButton(sprintf(
+ $this->translate('Drop Self Service API key'),
+ $this->host->getObjectName()
+ ));
+ }
+
+ public function onSuccess()
+ {
+ $this->host->set('api_key', null)->store();
+ $this->redirectOnSuccess(sprintf($this->translate(
+ 'The Self Service API key for %s has been dropped'
+ ), $this->host->getObjectName()));
+ }
+}
diff --git a/application/forms/IcingaGenerateApiKeyForm.php b/application/forms/IcingaGenerateApiKeyForm.php
new file mode 100644
index 0000000..18980f0
--- /dev/null
+++ b/application/forms/IcingaGenerateApiKeyForm.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class IcingaGenerateApiKeyForm extends DirectorForm
+{
+ /** @var IcingaHost */
+ protected $host;
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ public function setup()
+ {
+ if ($this->host->getProperty('api_key')) {
+ $label = $this->translate('Regenerate Self Service API key');
+ } else {
+ $label = $this->translate('Generate Self Service API key');
+ }
+
+ $this->addStandaloneSubmitButton(sprintf(
+ $label,
+ $this->host->getObjectName()
+ ));
+ }
+
+ public function onSuccess()
+ {
+ $host = $this->host;
+ $host->generateApiKey();
+ $host->store();
+ $this->redirectOnSuccess(sprintf($this->translate(
+ 'A new Self Service API key for %s has been generated'
+ ), $host->getObjectName()));
+ }
+}
diff --git a/application/forms/IcingaHostForm.php b/application/forms/IcingaHostForm.php
new file mode 100644
index 0000000..ec71471
--- /dev/null
+++ b/application/forms/IcingaHostForm.php
@@ -0,0 +1,390 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\AuthenticationException;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use Icinga\Module\Director\Restriction\HostgroupRestriction;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+
+class IcingaHostForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ $simpleImports = $this->isNew() && ! $this->isTemplate();
+ if ($simpleImports) {
+ if (!$this->addSingleImportElement(true)) {
+ $this->setSubmitLabel(false);
+ return;
+ }
+
+ if (! ($imports = $this->getSentOrObjectValue('imports'))) {
+ $this->setSubmitLabel($this->translate('Next'));
+ $this->groupMainProperties();
+ return;
+ }
+ }
+
+ $nameLabel = $this->isTemplate()
+ ? $this->translate('Name')
+ : $this->translate('Hostname');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $nameLabel,
+ 'required' => true,
+ 'spellcheck' => 'false',
+ 'description' => $this->translate(
+ 'Icinga object name for this host. This is usually a fully qualified host name'
+ . ' but it could basically be any kind of string. To make things easier for your'
+ . ' users we strongly suggest to use meaningful names for templates. E.g. "generic-host"'
+ . ' is ugly, "Standard Linux Server" is easier to understand'
+ )
+ ));
+
+ if (! $simpleImports) {
+ $this->addImportsElement();
+ }
+
+ $this->addChoices('host')
+ ->addDisplayNameElement()
+ ->addAddressElements()
+ ->addGroupsElement()
+ ->addDisabledElement()
+ ->groupMainProperties($simpleImports)
+ ->addCheckCommandElements()
+ ->addCheckExecutionElements()
+ ->addExtraInfoElements()
+ ->addClusteringElements()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addClusteringElements()
+ {
+ $this->addZoneElement();
+ $this->addBoolean('has_agent', [
+ 'label' => $this->translate('Icinga2 Agent'),
+ 'description' => $this->translate(
+ 'Whether this host has the Icinga 2 Agent installed'
+ ),
+ 'class' => 'autosubmit',
+ ]);
+
+ if ($this->getSentOrResolvedObjectValue('has_agent') === 'y') {
+ $this->addBoolean('master_should_connect', [
+ 'label' => $this->translate('Establish connection'),
+ 'description' => $this->translate(
+ 'Whether the parent (master) node should actively try to connect to this agent'
+ ),
+ 'required' => true
+ ]);
+ $this->addBoolean('accept_config', [
+ 'label' => $this->translate('Accepts config'),
+ 'description' => $this->translate('Whether the agent is configured to accept config'),
+ 'required' => true
+ ]);
+
+ $this->addHidden('command_endpoint_id', null);
+ $this->setSentValue('command_endpoint_id', null);
+
+ $settings = $this->object->getConnection()->settings();
+ if ($settings->get('feature_custom_endpoint') === 'y' && ! $this->isTemplate()) {
+ $this->addElement('text', 'custom_endpoint_name', [
+ 'label' => $this->translate('Custom Endpoint Name'),
+ 'description' => $this->translate(
+ 'Use a different name for the generated endpoint object than the host name'
+ . ' and add a custom variable to allow services setting the correct command endpoint.'
+ ),
+ ]);
+ }
+ } else {
+ if ($this->isTemplate()) {
+ $this->addElement('select', 'command_endpoint_id', [
+ 'label' => $this->translate('Command endpoint'),
+ 'description' => $this->translate(
+ 'Setting a command endpoint allows you to force host checks'
+ . ' to be executed by a specific endpoint. Please carefully'
+ . ' study the related Icinga documentation before using this'
+ . ' feature'
+ ),
+ 'multiOptions' => $this->optionalEnum($this->enumEndpoints())
+ ]);
+ }
+
+ foreach (['master_should_connect', 'accept_config'] as $key) {
+ $this->addHidden($key, null);
+ $this->setSentValue($key, null);
+ }
+ }
+
+ $elements = [
+ 'zone_id',
+ 'has_agent',
+ 'master_should_connect',
+ 'accept_config',
+ 'command_endpoint_id',
+ 'custom_endpoint_name',
+ 'api_key',
+ ];
+ $this->addDisplayGroup($elements, 'clustering', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'Fieldset',
+ ],
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Icinga Agent and zone settings')
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @param bool $required
+ * @return bool
+ */
+ protected function addSingleImportElement($required = null)
+ {
+ $enum = $this->enumHostTemplates();
+ if (empty($enum)) {
+ if ($required) {
+ if ($this->hasBeenSent()) {
+ $this->addError($this->translate('No Host template has been chosen'));
+ } else {
+ if ($this->hasPermission('director/admin')) {
+ $html = sprintf(
+ $this->translate('Please define a %s first'),
+ Link::create(
+ $this->translate('Host Template'),
+ 'director/host/add',
+ ['type' => 'template']
+ )
+ );
+ } else {
+ $html = $this->translate('No Host Template has been provided yet');
+ }
+
+ $this->addHtml('<p class="warning">' . $html . '</p>');
+ }
+ }
+
+ return false;
+ }
+
+ $this->addElement('select', 'imports', [
+ 'label' => $this->translate('Host Template'),
+ 'description' => $this->translate(
+ 'Choose a Host Template'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($enum),
+ 'class' => 'autosubmit'
+ ]);
+
+ return true;
+ }
+
+ protected function enumHostTemplates()
+ {
+ $tpl = IcingaTemplateRepository::instanceByType('host', $this->getDb())
+ ->listAllowedTemplateNames();
+ return array_combine($tpl, $tpl);
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addGroupsElement()
+ {
+ if ($this->hasHostGroupRestriction()
+ && ! $this->getAuth()->hasPermission('director/groups-for-restricted-hosts')
+ ) {
+ return $this;
+ }
+
+ $this->addElement('extensibleSet', 'groups', array(
+ 'label' => $this->translate('Groups'),
+ 'suggest' => 'hostgroupnames',
+ 'description' => $this->translate(
+ 'Hostgroups that should be directly assigned to this node. Hostgroups can be useful'
+ . ' for various reasons. You might assign service checks based on assigned hostgroup.'
+ . ' They are also often used as an instrument to enforce restricted views in Icinga Web 2.'
+ . ' Hostgroups can be directly assigned to single hosts or to host templates. You might'
+ . ' also want to consider assigning hostgroups using apply rules'
+ )
+ ));
+
+ $applied = $this->getAppliedGroups();
+ if (! empty($applied)) {
+ $this->addElement('simpleNote', 'applied_groups', [
+ 'label' => $this->translate('Applied groups'),
+ 'value' => $this->createHostgroupLinks($applied),
+ 'ignore' => true,
+ ]);
+ }
+
+ $inherited = $this->getInheritedGroups();
+ if (! empty($inherited)) {
+ /** @var BaseHtmlElement $links */
+ $links = $this->createHostgroupLinks($inherited);
+ if (count($this->object()->getGroups())) {
+ $links->addAttributes(['class' => 'strike-links']);
+ /** @var BaseHtmlElement $link */
+ foreach ($links->getContent() as $link) {
+ if ($link instanceof BaseHtmlElement) {
+ $link->addAttributes([
+ 'title' => $this->translate(
+ 'Group has been inherited, but will be overridden'
+ . ' by locally assigned group(s)'
+ )
+ ]);
+ }
+ }
+ }
+ $this->addElement('simpleNote', 'inherited_groups', [
+ 'label' => $this->translate('Inherited groups'),
+ 'value' => $links,
+ 'ignore' => true,
+ ]);
+ }
+
+ return $this;
+ }
+
+ protected function strikeGroupLinks(BaseHtmlElement $links)
+ {
+ /** @var BaseHtmlElement $link */
+ foreach ($links->getContent() as $link) {
+ $link->getAttributes()->add('style', 'text-decoration: strike');
+ }
+ $links->add('aha');
+ }
+
+ protected function getInheritedGroups()
+ {
+ if ($this->hasObject()) {
+ return $this->object->listInheritedGroupNames();
+ } else {
+ return [];
+ }
+ }
+
+ protected function createHostgroupLinks($groups)
+ {
+ $links = [];
+ foreach ($groups as $name) {
+ if (! empty($links)) {
+ $links[] = ', ';
+ }
+ $links[] = Link::create(
+ $name,
+ 'director/hostgroup',
+ ['name' => $name],
+ ['data-base-target' => '_next']
+ );
+ }
+
+ return Html::tag('span', [
+ 'style' => 'line-height: 2.5em; padding-left: 0.5em'
+ ], $links);
+ }
+
+ protected function getAppliedGroups()
+ {
+ if ($this->isNew()) {
+ return [];
+ }
+
+ return $this->object()->getAppliedGroups();
+ }
+
+ protected function hasHostGroupRestriction()
+ {
+ return $this->getAuth()->getRestrictions('director/filter/hostgroups');
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addAddressElements()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('text', 'address', array(
+ 'label' => $this->translate('Host address'),
+ 'description' => $this->translate(
+ 'Host address. Usually an IPv4 address, but may be any kind of address'
+ . ' your check plugin is able to deal with'
+ )
+ ));
+
+ $this->addElement('text', 'address6', array(
+ 'label' => $this->translate('IPv6 address'),
+ 'description' => $this->translate('Usually your hosts main IPv6 address')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addDisplayNameElement()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('text', 'display_name', array(
+ 'label' => $this->translate('Display name'),
+ 'spellcheck' => 'false',
+ 'description' => $this->translate(
+ 'Alternative name for this host. Might be a host alias or and kind'
+ . ' of string helping your users to identify this host'
+ )
+ ));
+
+ return $this;
+ }
+
+ protected function enumEndpoints()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from('icinga_endpoint', [
+ 'id',
+ 'object_name'
+ ])->where(
+ 'object_type IN (?)',
+ ['object', 'external_object']
+ )->order('object_name');
+
+ return $db->fetchPairs($select);
+ }
+
+ public function onSuccess()
+ {
+ if ($this->hasHostGroupRestriction()) {
+ $restriction = new HostgroupRestriction($this->getDb(), $this->getAuth());
+ if (! $restriction->allowsHost($this->object())) {
+ throw new AuthenticationException($this->translate(
+ 'Unable to store a host with the given properties because of insufficient permissions'
+ ));
+ }
+ }
+
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/IcingaHostGroupForm.php b/application/forms/IcingaHostGroupForm.php
new file mode 100644
index 0000000..be48318
--- /dev/null
+++ b/application/forms/IcingaHostGroupForm.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaHostGroupForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Hostgroup'),
+ 'required' => true,
+ 'description' => $this->translate('Icinga object name for this host group')
+ ]);
+
+ $this->addGroupDisplayNameElement()
+ ->addAssignmentElements()
+ ->setButtons();
+ }
+
+ protected function addAssignmentElements()
+ {
+ $this->addAssignFilter([
+ 'suggestionContext' => 'HostFilterColumns',
+ 'required' => false,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaHostSelfServiceForm.php b/application/forms/IcingaHostSelfServiceForm.php
new file mode 100644
index 0000000..1e05b96
--- /dev/null
+++ b/application/forms/IcingaHostSelfServiceForm.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+use Icinga\Security\SecurityException;
+
+class IcingaHostSelfServiceForm extends DirectorForm
+{
+ /** @var string */
+ private $hostApiKey;
+
+ /** @var IcingaHost */
+ private $template;
+
+ private $hostName;
+
+ public function setup()
+ {
+ if ($this->hostName === null) {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Host name'),
+ 'required' => true,
+ 'value' => $this->hostName,
+ ));
+ }
+ $this->addElement('text', 'display_name', array(
+ 'label' => $this->translate('Alias'),
+ ));
+
+ $this->addElement('text', 'address', array(
+ 'label' => $this->translate('Host address'),
+ 'description' => $this->translate(
+ 'Host address. Usually an IPv4 address, but may be any kind of address'
+ . ' your check plugin is able to deal with'
+ )
+ ));
+
+ $this->addElement('text', 'address6', array(
+ 'label' => $this->translate('IPv6 address'),
+ 'description' => $this->translate('Usually your hosts main IPv6 address')
+ ));
+
+ if ($this->template === null) {
+ $this->addElement('text', 'key', array(
+ 'label' => $this->translate('API Key'),
+ 'ignore' => true,
+ 'required' => true,
+ ));
+ }
+
+ $this->submitLabel = sprintf(
+ $this->translate('Register')
+ );
+ }
+
+ public function setHostName($name)
+ {
+ $this->hostName = $name;
+ $this->removeElement('object_name');
+ return $this;
+ }
+
+ public function loadTemplateWithApiKey($key)
+ {
+ $this->template = IcingaHost::loadWithApiKey($key, $this->getDb());
+ if (! $this->template->isTemplate()) {
+ throw new NotFoundError('Got invalid API key "%s"', $key);
+ }
+
+ if ($this->template->getResolvedProperty('has_agent') !== 'y') {
+ throw new NotFoundError(
+ 'Got valid API key "%s", but template is not for Agents',
+ $key
+ );
+ }
+
+ $this->removeElement('key');
+
+ return $this->template;
+ }
+
+ public function listMissingRequiredFields()
+ {
+ $result = [];
+ foreach ($this->getElements() as $element) {
+ if (in_array('isEmpty', $element->getErrors())) {
+ $result[] = $element->getName();
+ }
+ }
+
+ return $result;
+ }
+
+ public function isMissingRequiredFields()
+ {
+ return count($this->listMissingRequiredFields()) > 0;
+ }
+
+ public function onSuccess()
+ {
+ $db = $this->getDb();
+ if ($this->template === null) {
+ $this->loadTemplateWithApiKey($this->getValue('key'));
+ }
+ $name = $this->hostName ?: $this->getValue('object_name');
+ if (IcingaHost::exists($name, $db)) {
+ $host = IcingaHost::load($name, $db);
+ if ($host->isTemplate()) {
+ throw new SecurityException(
+ 'You are not allowed to create "%s"',
+ $name
+ );
+ }
+
+ if (null !== $host->getProperty('api_key')) {
+ throw new SecurityException(
+ 'The host "%s" has already been registered',
+ $name
+ );
+ }
+
+ $propertyNames = ['display_name', 'address', 'address6'];
+ foreach ($propertyNames as $property) {
+ if (\strlen($value = $this->getValue($property)) > 0) {
+ $host->set($property, $value);
+ }
+ }
+ } else {
+ $host = IcingaHost::create(array_filter($this->getValues(), 'strlen'), $db);
+ $host->set('object_name', $name);
+ $host->set('object_type', 'object');
+ $host->set('imports', [$this->template]);
+ }
+
+ $key = $host->generateApiKey();
+ $host->store($db);
+ $this->hostApiKey = $key;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getHostApiKey()
+ {
+ return $this->hostApiKey;
+ }
+
+ public static function create(Db $db)
+ {
+ return static::load()->setDb($db);
+ }
+}
diff --git a/application/forms/IcingaHostVarForm.php b/application/forms/IcingaHostVarForm.php
new file mode 100644
index 0000000..cb15bcb
--- /dev/null
+++ b/application/forms/IcingaHostVarForm.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+/**
+ * @deprecated
+ */
+class IcingaHostVarForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addElement('select', 'host_id', array(
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate('The name of the host'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumHosts()),
+ 'required' => true
+ ));
+
+ $this->addElement('text', 'varname', array(
+ 'label' => $this->translate('Name'),
+ 'description' => $this->translate('host var name')
+ ));
+
+ $this->addElement('textarea', 'varvalue', array(
+ 'label' => $this->translate('Value'),
+ 'description' => $this->translate('host var value')
+ ));
+
+ $this->addElement('text', 'format', array(
+ 'label' => $this->translate('Format'),
+ 'description' => $this->translate('value format')
+ ));
+ }
+}
diff --git a/application/forms/IcingaImportObjectForm.php b/application/forms/IcingaImportObjectForm.php
new file mode 100644
index 0000000..3942f74
--- /dev/null
+++ b/application/forms/IcingaImportObjectForm.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class IcingaImportObjectForm extends QuickForm
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ public function setup()
+ {
+ $this->addNote($this->translate(
+ "Importing an object means that its type will change from"
+ . ' "external" to "object". That way it will make part of the'
+ . ' next deployment. So in case you imported this object from'
+ . ' your Icinga node make sure to remove it from your local'
+ . ' configuration before issueing the next deployment. In case'
+ . ' of a conflict nothing bad will happen, just your config'
+ . " won't deploy."
+ ));
+
+ $this->submitLabel = sprintf(
+ $this->translate('Import external "%s"'),
+ $this->object->object_name
+ );
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object;
+ if ($object->set('object_type', 'object')->store()) {
+ $this->redirectOnSuccess(sprintf(
+ $this->translate('%s "%s" has been imported"'),
+ $object->getShortTableName(),
+ $object->getObjectName()
+ ));
+ } else {
+ $this->addError(sprintf(
+ $this->translate('Failed to import %s "%s"'),
+ $object->getShortTableName(),
+ $object->getObjectName()
+ ));
+ }
+ }
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaMultiEditForm.php b/application/forms/IcingaMultiEditForm.php
new file mode 100644
index 0000000..4149a70
--- /dev/null
+++ b/application/forms/IcingaMultiEditForm.php
@@ -0,0 +1,324 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Hook\IcingaObjectFormHook;
+use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Zend_Form_Element as ZfElement;
+
+class IcingaMultiEditForm extends DirectorObjectForm
+{
+ /** @var DbObject[] */
+ private $objects;
+
+ private $elementGroupMap;
+
+ /** @var QuickForm */
+ private $relatedForm;
+
+ private $propertiesToPick;
+
+ public function setObjects($objects)
+ {
+ $this->objects = $objects;
+ $this->object = current($this->objects);
+ $this->db = $this->object()->getConnection();
+ return $this;
+ }
+
+ public function isMultiObjectForm()
+ {
+ return true;
+ }
+
+ public function pickElementsFrom(QuickForm $form, $properties)
+ {
+ $this->relatedForm = $form;
+ $this->propertiesToPick = $properties;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $object = $this->object;
+
+ $loader = new IcingaObjectFieldLoader($object);
+ $loader->prepareElements($this);
+ $loader->addFieldsToForm($this);
+
+ if ($form = $this->relatedForm) {
+ if ($form instanceof DirectorObjectForm) {
+ $form->setDb($object->getConnection())
+ ->setObject($object);
+ }
+
+ $form->prepareElements();
+ } else {
+ $this->propertiesToPick = array();
+ }
+
+ foreach ($this->propertiesToPick as $property) {
+ if ($el = $form->getElement($property)) {
+ $this->makeVariants($el);
+ }
+ }
+
+ /** @var \Zend_Form_Element $el */
+ foreach ($this->getElements() as $el) {
+ $name = $el->getName();
+ if (substr($name, 0, 4) === 'var_') {
+ $this->makeVariants($el);
+ }
+ }
+
+ $this->setButtons();
+ }
+
+ public function onSuccess()
+ {
+ foreach ($this->getValues() as $key => $value) {
+ $this->setSubmittedMultiValue($key, $value);
+ }
+
+ $modified = $this->storeModifiedObjects();
+ if ($modified === 0) {
+ $msg = $this->translate('No object has been modified');
+ } elseif ($modified === 1) {
+ $msg = $this->translate('One object has been modified');
+ } else {
+ $msg = sprintf(
+ $this->translate('%d objects have been modified'),
+ $modified
+ );
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+
+ /**
+ * No default objects behaviour
+ */
+ protected function onRequest()
+ {
+ IcingaObjectFormHook::callOnSetup($this);
+ if ($this->hasBeenSent()) {
+ $this->handlePost();
+ }
+ }
+
+ protected function handlePost()
+ {
+ $this->callOnRequestCallables();
+ if ($this->shouldBeDeleted()) {
+ $this->deleteObjects();
+ }
+ }
+
+ protected function setSubmittedMultiValue($key, $value)
+ {
+ $parts = preg_split('/_/', $key);
+ $objectsSum = array_pop($parts);
+ $valueSum = array_pop($parts);
+ $property = implode('_', $parts);
+
+ if ($value === '') {
+ $value = null;
+ }
+
+ foreach ($this->getVariants($property) as $json => $objects) {
+ if ($valueSum !== sha1($json)) {
+ continue;
+ }
+
+ if ($objectsSum !== sha1(json_encode($objects))) {
+ continue;
+ }
+
+ if (substr($property, 0, 4) === 'var_') {
+ $property = 'vars.' . substr($property, 4);
+ }
+
+ foreach ($this->getObjects($objects) as $object) {
+ $object->$property = $value;
+ }
+ }
+ }
+
+ protected function storeModifiedObjects()
+ {
+ $modified = 0;
+ $store = $this->getDbObjectStore();
+ foreach ($this->objects as $object) {
+ if ($object->hasBeenModified()) {
+ $modified++;
+ $store->store($object);
+ }
+ }
+
+ return $modified;
+ }
+
+ protected function getDisplayGroupForElement(ZfElement $element)
+ {
+ if ($this->elementGroupMap === null) {
+ $this->resolveDisplayGroups();
+ }
+
+ $name = $element->getName();
+ if (array_key_exists($name, $this->elementGroupMap)) {
+ $groupName = $this->elementGroupMap[$name];
+
+ if ($group = $this->getDisplayGroup($groupName)) {
+ return $group;
+ } elseif ($this->relatedForm) {
+ return $this->stealDisplayGroup($groupName, $this->relatedForm);
+ }
+ }
+
+ return null;
+ }
+
+ protected function stealDisplayGroup($name, QuickForm $form)
+ {
+ if ($group = $form->getDisplayGroup($name)) {
+ $group = clone($group);
+ $group->setElements(array());
+ $this->_displayGroups[$name] = $group;
+ $this->_order[$name] = $group->getOrder();
+ $this->_orderUpdated = true;
+
+ return $group;
+ }
+
+ return null;
+ }
+
+ protected function resolveDisplayGroups()
+ {
+ $this->elementGroupMap = array();
+ if ($form = $this->relatedForm) {
+ $this->extractFormDisplayGroups($form);
+ }
+
+ $this->extractFormDisplayGroups($this);
+ }
+
+ protected function extractFormDisplayGroups(QuickForm $form)
+ {
+ /** @var \Zend_Form_DisplayGroup $group */
+ foreach ($form->getDisplayGroups() as $group) {
+ $groupName = $group->getName();
+ foreach ($group->getElements() as $name => $e) {
+ $this->elementGroupMap[$name] = $groupName;
+ }
+ }
+ }
+
+ protected function makeVariants(ZfElement $element)
+ {
+ $key = $element->getName();
+ $this->removeElement($key);
+ $label = $element->getLabel();
+ $group = $this->getDisplayGroupForElement($element);
+ $description = $element->getDescription();
+
+ foreach ($this->getVariants($key) as $json => $objects) {
+ $value = json_decode($json);
+ $checksum = sha1($json) . '_' . sha1(json_encode($objects));
+
+ $v = clone($element);
+ $v->setName($key . '_' . $checksum);
+ $v->setDescription($description . ' ' . $this->descriptionForObjects($objects));
+ $v->setLabel($label . $this->labelCount($objects));
+ $v->setValue($value);
+ if ($group) {
+ $group->addElement($v);
+ }
+ $this->addElement($v);
+ }
+ }
+
+ protected function getVariants($key)
+ {
+ $variants = array();
+ if (substr($key, 0, 4) === 'var_') {
+ $key = 'vars.' . substr($key, 4);
+ }
+
+ foreach ($this->objects as $name => $object) {
+ $value = json_encode($object->$key);
+ if (! array_key_exists($value, $variants)) {
+ $variants[$value] = array();
+ }
+
+ $variants[$value][] = $name;
+ }
+
+ foreach ($variants as & $objects) {
+ natsort($objects);
+ }
+
+ return $variants;
+ }
+
+ protected function descriptionForObjects($list)
+ {
+ return sprintf(
+ $this->translate('Changing this value affects %d object(s): %s'),
+ count($list),
+ implode(', ', $list)
+ );
+ }
+
+ protected function labelCount($list)
+ {
+ return ' (' . count($list) . ')';
+ }
+
+ protected function db()
+ {
+ if ($this->db === null) {
+ $this->db = $this->object()->getConnection();
+ }
+
+ return $this->db;
+ }
+
+ public function getObjects($names = null)
+ {
+ if ($names === null) {
+ return $this->objects;
+ }
+
+ $res = array();
+
+ foreach ($names as $name) {
+ $res[$name] = $this->objects[$name];
+ }
+
+ return $res;
+ }
+
+ protected function deleteObjects()
+ {
+ $msg = sprintf(
+ '%d objects of type "%s" have been removed',
+ count($this->objects),
+ $this->translate($this->object->getShortTableName())
+ );
+
+ $store = $this->getDbObjectStore();
+ foreach ($this->objects as $object) {
+ $store->delete($object);
+ }
+
+ if ($this->listUrl) {
+ $this->setSuccessUrl($this->listUrl);
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaNotificationForm.php b/application/forms/IcingaNotificationForm.php
new file mode 100644
index 0000000..0fca6b8
--- /dev/null
+++ b/application/forms/IcingaNotificationForm.php
@@ -0,0 +1,298 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaNotificationForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Notification Template'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga notification template you are going to create')
+ ));
+ } else {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Notification'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga notification you are going to create')
+ ));
+
+ $this->eventuallyAddNameRestriction(
+ 'director/notification/apply/filter-by-name'
+ );
+ }
+
+ $this->addDisabledElement()
+ ->addImportsElement()
+ ->addUsersElement()
+ ->addUsergroupsElement()
+ ->addIntervalElement()
+ ->addPeriodElement()
+ ->addTimesElements()
+ ->addAssignmentElements()
+ ->addDisabledElement()
+ ->addCommandElements()
+ ->addEventFilterElements()
+ ->addZoneElements()
+ ->groupMainProperties()
+ ->setButtons();
+ }
+
+ protected function addZoneElements()
+ {
+ if (! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addZoneElement();
+ $this->addDisplayGroup(array('zone_id'), 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function addAssignmentElements()
+ {
+ if (!$this->object || !$this->object->isApplyRule()) {
+ return $this;
+ }
+
+ $this->addElement('select', 'apply_to', array(
+ 'label' => $this->translate('Apply to'),
+ 'description' => $this->translate(
+ 'Whether this notification should affect hosts or services'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $this->optionalEnum(
+ array(
+ 'host' => $this->translate('Hosts'),
+ 'service' => $this->translate('Services'),
+ )
+ )
+ ));
+
+ $applyTo = $this->getSentOrObjectValue('apply_to');
+
+ if (! $applyTo) {
+ return $this;
+ }
+
+ $suggestionContext = ucfirst($applyTo) . 'FilterColumns';
+ $this->addAssignFilter([
+ 'required' => true,
+ 'suggestionContext' => $suggestionContext,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addUsersElement()
+ {
+ $users = $this->enumUsers();
+ if (empty($users)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'extensibleSet',
+ 'users',
+ array(
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Users that should be notified by this notifications'
+ ),
+ 'multiOptions' => $this->optionalEnum($users)
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addUsergroupsElement()
+ {
+ $groups = $this->enumUsergroups();
+ if (empty($groups)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'extensibleSet',
+ 'user_groups',
+ array(
+ 'label' => $this->translate('User groups'),
+ 'description' => $this->translate(
+ 'User groups that should be notified by this notifications'
+ ),
+ 'multiOptions' => $this->optionalEnum($groups)
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function addIntervalElement()
+ {
+ $this->addElement(
+ 'text',
+ 'notification_interval',
+ array(
+ 'label' => $this->translate('Notification interval'),
+ 'description' => $this->translate(
+ 'The notification interval (in seconds). This interval is'
+ . ' used for active notifications. Defaults to 30 minutes.'
+ . ' If set to 0, re-notifications are disabled.'
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function addTimesElements()
+ {
+ $this->addElement(
+ 'text',
+ 'times_begin',
+ array(
+ 'label' => $this->translate('First notification delay'),
+ 'description' => $this->translate(
+ 'Delay unless the first notification should be sent'
+ ) . '. ' . $this->getTimeValueInfo()
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'times_end',
+ array(
+ 'label' => $this->translate('Last notification'),
+ 'description' => $this->translate(
+ 'When the last notification should be sent'
+ ) . '. ' . $this->getTimeValueInfo()
+ )
+ );
+
+ return $this;
+ }
+
+ protected function getTimeValueInfo()
+ {
+ return $this->translate(
+ 'Unit is seconds unless a suffix is given. Supported suffixes include'
+ . ' ms (milliseconds), s (seconds), m (minutes), h (hours) and d (days).'
+ );
+ }
+
+ /**
+ * @return self
+ */
+ protected function addPeriodElement()
+ {
+ $periods = $this->db->enumTimeperiods();
+ if (empty($periods)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'select',
+ 'period_id',
+ array(
+ 'label' => $this->translate('Time period'),
+ 'description' => $this->translate(
+ 'The name of a time period which determines when this'
+ . ' notification should be triggered. Not set by default.'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function addCommandElements()
+ {
+ if (! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('select', 'command_id', array(
+ 'label' => $this->translate('Notification command'),
+ 'description' => $this->translate('Check command definition'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumNotificationCommands()),
+ 'class' => 'autosubmit',
+ ));
+
+ return $this;
+ }
+
+ protected function enumUsers()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from(
+ 'icinga_user',
+ array(
+ 'name' => 'object_name',
+ 'display' => 'COALESCE(display_name, object_name)'
+ )
+ )->where('object_type = ?', 'object')->order('display');
+
+ return $db->fetchPairs($select);
+ }
+
+ protected function enumUsergroups()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from(
+ 'icinga_usergroup',
+ array(
+ 'name' => 'object_name',
+ 'display' => 'COALESCE(display_name, object_name)'
+ )
+ )->where('object_type = ?', 'object')->order('display');
+
+ return $db->fetchPairs($select);
+ }
+}
diff --git a/application/forms/IcingaObjectFieldForm.php b/application/forms/IcingaObjectFieldForm.php
new file mode 100644
index 0000000..537c95e
--- /dev/null
+++ b/application/forms/IcingaObjectFieldForm.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader;
+
+class IcingaObjectFieldForm extends DirectorObjectForm
+{
+ /** @var IcingaObject Please note that $object would conflict with logic in parent class */
+ protected $icingaObject;
+
+ public function setIcingaObject($object)
+ {
+ $this->icingaObject = $object;
+ $this->className = get_class($object) . 'Field';
+ return $this;
+ }
+
+ public function setup()
+ {
+ $object = $this->icingaObject;
+ $type = $object->getShortTableName();
+ $this->addHidden($type . '_id', $object->get('id'));
+
+ $this->addHtmlHint(
+ 'Custom data fields allow you to easily fill custom variables with'
+ . " meaningful data. It's perfectly legal to override inherited fields."
+ . ' You may for example want to allow "network devices" specifying any'
+ . ' string for vars.snmp_community, but restrict "customer routers" to'
+ . ' a specific set, shown as a dropdown.'
+ );
+
+ // TODO: remove assigned ones!
+ $existingFields = $this->db->enumDatafields();
+ $blacklistedVars = array();
+ $suggestedFields = array();
+
+ foreach ($existingFields as $id => $field) {
+ if (preg_match('/ \(([^\)]+)\)$/', $field, $m)) {
+ $blacklistedVars['$' . $m[1] . '$'] = $id;
+ }
+ }
+
+ // TODO: think about imported existing vars without fields
+ // TODO: extract vars from command line (-> dummy)
+ // TODO: do not suggest chosen ones
+ $argumentVars = array();
+ $argumentVarDescriptions = array();
+ if ($object instanceof IcingaCommand) {
+ $command = $object;
+ } elseif ($object->hasProperty('check_command_id')) {
+ $command = $object->getResolvedRelated('check_command');
+ } else {
+ $command = null;
+ }
+
+ if ($command) {
+ foreach ($command->arguments() as $arg) {
+ if ($arg->argument_format === 'string') {
+ $val = $arg->argument_value;
+ // TODO: create var::extractMacros or so
+
+ if (preg_match_all('/(\$[a-z0-9_]+\$)/i', $val, $m, PREG_PATTERN_ORDER)) {
+ foreach ($m[1] as $val) {
+ if (array_key_exists($val, $blacklistedVars)) {
+ $id = $blacklistedVars[$val];
+
+ // Hint: if not set it might already have been
+ // removed in this loop
+ if (array_key_exists($id, $existingFields)) {
+ $suggestedFields[$id] = $existingFields[$id];
+ unset($existingFields[$id]);
+ }
+ } else {
+ $argumentVars[$val] = $val;
+ $argumentVarDescriptions[$val] = $arg->description;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Prepare combined fields array
+ $fields = array();
+ if (! empty($suggestedFields)) {
+ asort($existingFields);
+ $fields[$this->translate('Suggested fields')] = $suggestedFields;
+ }
+
+ if (! empty($argumentVars)) {
+ ksort($argumentVars);
+ $fields[$this->translate('Argument macros')] = $argumentVars;
+ }
+
+ if (! empty($existingFields)) {
+ $fields[$this->translate('Other available fields')] = $existingFields;
+ }
+
+ $this->addElement('select', 'datafield_id', array(
+ 'label' => 'Field',
+ 'required' => true,
+ 'description' => 'Field to assign',
+ 'class' => 'autosubmit',
+ 'multiOptions' => $this->optionalEnum($fields)
+ ));
+
+ if (empty($fields)) {
+ // TODO: show message depending on permissions
+ $msg = $this->translate(
+ 'There are no data fields available. Please ask an administrator to create such'
+ );
+
+ $this->getElement('datafield_id')->addError($msg);
+ }
+
+ if (($id = $this->getSentValue('datafield_id')) && ! ctype_digit($id)) {
+ $this->addElement('text', 'caption', array(
+ 'label' => $this->translate('Caption'),
+ 'required' => true,
+ 'ignore' => true,
+ 'value' => trim($id, '$'),
+ 'description' => $this->translate('The caption which should be displayed')
+ ));
+
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate('A description about the field'),
+ 'ignore' => true,
+ 'value' => array_key_exists($id, $argumentVarDescriptions) ? $argumentVarDescriptions[$id] : null,
+ 'rows' => '3',
+ ));
+ }
+
+ $this->addElement('select', 'is_required', array(
+ 'label' => $this->translate('Mandatory'),
+ 'description' => $this->translate('Whether this field should be mandatory'),
+ 'required' => true,
+ 'multiOptions' => array(
+ 'n' => $this->translate('Optional'),
+ 'y' => $this->translate('Mandatory'),
+ )
+ ));
+
+ $filterFields = array();
+ $prefix = null;
+ if ($object instanceof IcingaHost) {
+ $prefix = 'host.vars.';
+ } elseif ($object instanceof IcingaService) {
+ $prefix = 'service.vars.';
+ }
+
+ if ($prefix) {
+ $loader = new IcingaObjectFieldLoader($object);
+ $fields = $loader->getFields();
+
+ foreach ($fields as $varName => $field) {
+ $filterFields[$prefix . $field->varname] = $field->caption;
+ }
+
+ $this->addFilterElement('var_filter', array(
+ 'description' => $this->translate(
+ 'You might want to show this field only when certain conditions are met.'
+ . ' Otherwise it will not be available and values eventually set before'
+ . ' will be cleared once stored'
+ ),
+ 'columns' => $filterFields,
+ ));
+
+ $this->addDisplayGroup(array($this->getElement('var_filter')), 'field_filter', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => 30,
+ 'legend' => $this->translate('Show based on filter')
+ ));
+ }
+
+ $this->setButtons();
+ }
+
+ protected function onRequest()
+ {
+ parent::onRequest();
+ if ($this->getSentValue('delete') === $this->translate('Delete')) {
+ $this->object()->delete();
+ $this->setSuccessUrl($this->getSuccessUrl()->without('field_id'));
+ $this->redirectOnSuccess($this->translate('Field has been removed'));
+ }
+ }
+
+ public function onSuccess()
+ {
+ $fieldId = $this->getValue('datafield_id');
+
+ if (! ctype_digit($fieldId)) {
+ $field = DirectorDatafield::create(array(
+ 'varname' => trim($fieldId, '$'),
+ 'caption' => $this->getValue('caption'),
+ 'description' => $this->getValue('description'),
+ 'datatype' => 'Icinga\Module\Director\DataType\DataTypeString',
+ ));
+ $field->store($this->getDb());
+ $this->setElementValue('datafield_id', $field->get('id'));
+ $this->object()->set('datafield_id', $field->get('id'));
+ }
+
+ $this->object()->set('var_filter', $this->getValue('var_filter'));
+ return parent::onSuccess();
+ }
+}
diff --git a/application/forms/IcingaScheduledDowntimeForm.php b/application/forms/IcingaScheduledDowntimeForm.php
new file mode 100644
index 0000000..b126d59
--- /dev/null
+++ b/application/forms/IcingaScheduledDowntimeForm.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaScheduledDowntimeForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Template name'),
+ 'required' => true,
+ ]);
+ } else {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Downtime name'),
+ 'required' => true,
+ ]);
+ }
+
+ if ($this->object()->isApplyRule()) {
+ $this->eventuallyAddNameRestriction('director/scheduled-downtime/apply/filter-by-name');
+ }
+ $this->addImportsElement();
+ $this->addElement('text', 'author', [
+ 'label' => $this->translate('Author'),
+ 'description' => $this->translate(
+ 'This name will show up as the author for ever related downtime'
+ . ' comment'
+ ),
+ 'required' => ! $this->isTemplate()
+ ]);
+ $this->addElement('textarea', 'comment', [
+ 'label' => $this->translate('Comment'),
+ 'description' => $this->translate(
+ 'Every related downtime will show this comment'
+ ),
+ 'required' => ! $this->isTemplate(),
+ 'rows' => 4,
+ ]);
+ $this->addBoolean('fixed', [
+ 'label' => $this->translate('Fixed'),
+ 'description' => $this->translate(
+ 'Whether this downtime is fixed or flexible. If unsure please'
+ . ' check the related documentation:'
+ . ' https://icinga.com/docs/icinga2/latest/doc/08-advanced-topics/#downtimes'
+ ),
+ 'required' => ! $this->isTemplate(),
+ ]);
+ $this->addElement('text', 'duration', [
+ 'label' => $this->translate('Duration'),
+ 'description' => $this->translate(
+ 'How long the downtime lasts. Only has an effect for flexible'
+ . ' (non-fixed) downtimes. Time in seconds, supported suffixes'
+ . ' include ms (milliseconds), s (seconds), m (minutes),'
+ . ' h (hours) and d (days). To express "90 minutes" you might'
+ . ' want to write 1h 30m'
+ )
+ ]);
+ $this->addDisabledElement();
+ $this->addAssignmentElements();
+ $this->setButtons();
+ }
+
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addAssignmentElements()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('select', 'apply_to', [
+ 'label' => $this->translate('Apply to'),
+ 'description' => $this->translate(
+ 'Whether this dependency should affect hosts or services'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $this->optionalEnum([
+ 'host' => $this->translate('Hosts'),
+ 'service' => $this->translate('Services'),
+ ])
+ ]);
+
+ $applyTo = $this->getSentOrObjectValue('apply_to');
+
+ if (! $applyTo) {
+ return $this;
+ }
+
+ if ($applyTo === 'host') {
+ $this->addBoolean('with_services', [
+ 'label' => $this->translate('With Services'),
+ 'description' => $this->translate(
+ 'Whether Downtimes should also explicitly be scheduled for'
+ . ' all Services belonging to affected Hosts'
+ )
+ ]);
+ }
+
+ $suggestionContext = ucfirst($applyTo) . 'FilterColumns';
+ $this->addAssignFilter([
+ 'suggestionContext' => $suggestionContext,
+ 'required' => true,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ $this->setSuccessUrl(
+ 'director/scheduled-downtime',
+ $this->object()->getUrlParams()
+ );
+ }
+}
diff --git a/application/forms/IcingaScheduledDowntimeRangeForm.php b/application/forms/IcingaScheduledDowntimeRangeForm.php
new file mode 100644
index 0000000..b5f95d0
--- /dev/null
+++ b/application/forms/IcingaScheduledDowntimeRangeForm.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaScheduledDowntime;
+use Icinga\Module\Director\Objects\IcingaScheduledDowntimeRange;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaScheduledDowntimeRangeForm extends DirectorObjectForm
+{
+ /** @var IcingaScheduledDowntime */
+ private $downtime;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('scheduled_downtime_id', $this->downtime->get('id'));
+ $this->addElement('text', 'range_key', [
+ 'label' => $this->translate('Day(s)'),
+ 'description' => $this->translate(
+ 'Might be monday, tuesday or 2016-01-28 - have a look at the documentation for more examples'
+ ),
+ ]);
+
+ $this->addElement('text', 'range_value', [
+ 'label' => $this->translate('Timeperiods'),
+ 'description' => $this->translate(
+ 'One or more time periods, e.g. 00:00-24:00 or 00:00-09:00,17:00-24:00'
+ ),
+ ]);
+
+ $this->setButtons();
+ }
+
+ public function setScheduledDowntime(IcingaScheduledDowntime $downtime)
+ {
+ $this->downtime = $downtime;
+ $this->setDb($downtime->getConnection());
+ return $this;
+ }
+
+ /**
+ * @param IcingaScheduledDowntimeRange $object
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function deleteObject($object)
+ {
+ $key = $object->get('range_key');
+ $downtime = $this->downtime;
+ $downtime->ranges()->remove($key);
+ $downtime->store();
+ $msg = sprintf(
+ $this->translate('Time range "%s" has been removed from %s'),
+ $key,
+ $downtime->getObjectName()
+ );
+
+ $url = $this->getSuccessUrl()->without(
+ ['range', 'range_type']
+ );
+
+ $this->setSuccessUrl($url);
+ $this->redirectOnSuccess($msg);
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ $object = $this->object();
+ if ($object->hasBeenModified()) {
+ $this->downtime->ranges()->setRange(
+ $this->getValue('range_key'),
+ $this->getValue('range_value')
+ );
+ }
+
+ if ($this->downtime->hasBeenModified()) {
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->setHttpResponseCode(201);
+ }
+
+ $msg = sprintf(
+ $object->hasBeenLoadedFromDb()
+ ? $this->translate('The %s has successfully been stored')
+ : $this->translate('A new %s has successfully been created'),
+ $this->translate($this->getObjectShortClassName())
+ );
+
+ $this->downtime->store($this->db);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+ if ($object instanceof IcingaObject) {
+ $this->setSuccessUrl(
+ 'director/' . strtolower($this->getObjectShortClassName()),
+ $object->getUrlParams()
+ );
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaServiceDictionaryMemberForm.php b/application/forms/IcingaServiceDictionaryMemberForm.php
new file mode 100644
index 0000000..90b8f94
--- /dev/null
+++ b/application/forms/IcingaServiceDictionaryMemberForm.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Objects\IcingaService;
+
+class IcingaServiceDictionaryMemberForm extends DirectorObjectForm
+{
+ /** @var IcingaService */
+ protected $object;
+
+ private $succeeded;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Name'),
+ 'required' => !$this->object()->isApplyRule(),
+ 'description' => $this->translate(
+ 'Name for the instance you are going to create'
+ )
+ ]);
+ $this->groupMainProperties()->setButtons();
+ }
+
+ protected function isNew()
+ {
+ return $this->object === null;
+ }
+
+ protected function deleteObject($object)
+ {
+ }
+
+ protected function getObjectClassname()
+ {
+ return IcingaService::class;
+ }
+
+ public function succeeded()
+ {
+ return $this->succeeded;
+ }
+
+ public function onSuccess()
+ {
+ $this->succeeded = true;
+ }
+}
diff --git a/application/forms/IcingaServiceForm.php b/application/forms/IcingaServiceForm.php
new file mode 100644
index 0000000..f22f9e6
--- /dev/null
+++ b/application/forms/IcingaServiceForm.php
@@ -0,0 +1,806 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Data\PropertiesFilter\ArrayCustomVariablesFilter;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Web\Table\ObjectsTableHost;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use RuntimeException;
+
+class IcingaServiceForm extends DirectorObjectForm
+{
+ /** @var IcingaHost */
+ private $host;
+
+ /** @var IcingaServiceSet */
+ private $set;
+
+ private $apply;
+
+ /** @var IcingaService */
+ protected $object;
+
+ /** @var IcingaService */
+ private $applyGenerated;
+
+ private $inheritedFrom;
+
+ /** @var bool|null */
+ private $blacklisted;
+
+ public function setApplyGenerated(IcingaService $applyGenerated)
+ {
+ $this->applyGenerated = $applyGenerated;
+
+ return $this;
+ }
+
+ public function setInheritedFrom($hostname)
+ {
+ $this->inheritedFrom = $hostname;
+
+ return $this;
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws ProgrammingError
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ if (!$this->isNew() || $this->providesOverrides()) {
+ $this->tryToFetchHost();
+ }
+
+ if ($this->providesOverrides()) {
+ return;
+ }
+
+ if ($this->host && $this->set) {
+ // Probably never reached, as providesOverrides includes this
+ $this->setupOnHostForSet();
+
+ return;
+ }
+
+ if ($this->set !== null) {
+ $this->setupSetRelatedElements();
+ } elseif ($this->host === null) {
+ $this->setupServiceElements();
+ } else {
+ $this->setupHostRelatedElements();
+ }
+ }
+
+ protected function tryToFetchHost()
+ {
+ try {
+ if ($this->host === null) {
+ $this->host = $this->object->getResolvedRelated('host');
+ }
+ } catch (NestingError $nestingError) {
+ // ignore for the form to load
+ }
+ }
+
+ public function providesOverrides()
+ {
+ return $this->applyGenerated
+ || $this->inheritedFrom
+ || ($this->host && $this->set)
+ || ($this->object && $this->object->usesVarOverrides());
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws ProgrammingError
+ * @throws \Zend_Form_Exception
+ */
+ protected function addFields()
+ {
+ if ($this->providesOverrides() && $this->hasBeenBlacklisted()) {
+ $this->onAddedFields();
+
+ return;
+ } else {
+ parent::addFields();
+ }
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws ProgrammingError
+ * @throws \Zend_Form_Exception
+ */
+ protected function onAddedFields()
+ {
+ if (! $this->providesOverrides()) {
+ return;
+ }
+ $hasDeleteButton = false;
+ $isBranch = $this->branch && $this->branch->isBranch();
+
+ if ($this->hasBeenBlacklisted()) {
+ $this->addHtml(
+ Hint::warning($this->translate('This Service has been deactivated on this host')),
+ ['name' => 'HINT_blacklisted']
+ );
+ $group = null;
+ if (! $isBranch) {
+ $this->addDeleteButton($this->translate('Reactivate'));
+ $hasDeleteButton = true;
+ }
+ $this->setSubmitLabel(false);
+ } else {
+ $this->addOverrideHint();
+ $group = $this->getDisplayGroup('custom_fields');
+ if (! $group) {
+ foreach ($this->getDisplayGroups() as $groupName => $eventualGroup) {
+ if (preg_match('/^custom_fields:/', $groupName)) {
+ $group = $eventualGroup;
+ break;
+ }
+ }
+ }
+ if ($group) {
+ $elements = $group->getElements();
+ $group->setElements([$this->getElement('inheritance_hint')]);
+ $group->addElements($elements);
+ $this->setSubmitLabel($this->translate('Override vars'));
+ } else {
+ $this->addElementsToGroup(
+ ['inheritance_hint'],
+ 'custom_fields',
+ 20,
+ $this->translate('Hints regarding this service')
+ );
+
+ $this->setSubmitLabel(false);
+ }
+
+ if (! $isBranch) {
+ $this->addDeleteButton($this->translate('Deactivate'));
+ $hasDeleteButton = true;
+ }
+ }
+
+ if (! $this->hasSubmitButton() && $hasDeleteButton) {
+ $this->addDisplayGroup([$this->deleteButtonName], 'buttons', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'DtDdWrapper',
+ ],
+ 'order' => self::GROUP_ORDER_BUTTONS,
+ ]);
+ }
+ }
+
+ /**
+ * @return IcingaHost|null
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * @param IcingaService $service
+ * @return IcingaService
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getFirstParent(IcingaService $service)
+ {
+ /** @var IcingaService[] $objects */
+ $objects = $service->imports()->getObjects();
+ if (empty($objects)) {
+ throw new RuntimeException('Something went wrong, got no parent');
+ }
+ reset($objects);
+
+ return current($objects);
+ }
+
+ /**
+ * @return bool
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function hasBeenBlacklisted()
+ {
+ if (! $this->providesOverrides() || $this->object === null) {
+ return false;
+ }
+
+ if ($this->blacklisted === null) {
+ $host = $this->host;
+ // Safety check, branches
+ $hostId = $host->get('id');
+ $service = $this->getServiceToBeBlacklisted();
+ $serviceId = $service->get('id');
+ if (! $hostId || ! $serviceId) {
+ return false;
+ }
+ $db = $this->db->getDbAdapter();
+ if ($this->providesOverrides()) {
+ $this->blacklisted = 1 === (int)$db->fetchOne(
+ $db->select()->from('icinga_host_service_blacklist', 'COUNT(*)')
+ ->where('host_id = ?', $hostId)
+ ->where('service_id = ?', $serviceId)
+ );
+ } else {
+ $this->blacklisted = false;
+ }
+ }
+
+ return $this->blacklisted;
+ }
+
+ /**
+ * @param $object
+ * @throws IcingaException
+ * @throws ProgrammingError
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function deleteObject($object)
+ {
+ /** @var IcingaService $object */
+ if ($this->providesOverrides()) {
+ if ($this->hasBeenBlacklisted()) {
+ $this->removeFromBlacklist();
+ } else {
+ $this->blacklist();
+ }
+ } else {
+ parent::deleteObject($object);
+ }
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function blacklist()
+ {
+ $host = $this->host;
+ $service = $this->getServiceToBeBlacklisted();
+
+ $db = $this->db->getDbAdapter();
+ $host->unsetOverriddenServiceVars($this->object->getObjectName())->store();
+
+ if ($db->insert('icinga_host_service_blacklist', [
+ 'host_id' => $host->get('id'),
+ 'service_id' => $service->get('id')
+ ])) {
+ $msg = sprintf(
+ $this->translate('%s has been deactivated on %s'),
+ $service->getObjectName(),
+ $host->getObjectName()
+ );
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ /**
+ * @return IcingaService
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getServiceToBeBlacklisted()
+ {
+ if ($this->set) {
+ return $this->object;
+ } else {
+ return $this->getFirstParent($this->object);
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function removeFromBlacklist()
+ {
+ $host = $this->host;
+ $service = $this->getServiceToBeBlacklisted();
+
+ $db = $this->db->getDbAdapter();
+ $where = implode(' AND ', [
+ $db->quoteInto('host_id = ?', $host->get('id')),
+ $db->quoteInto('service_id = ?', $service->get('id')),
+ ]);
+ if ($db->delete('icinga_host_service_blacklist', $where)) {
+ $msg = sprintf(
+ $this->translate('%s is no longer deactivated on %s'),
+ $service->getObjectName(),
+ $host->getObjectName()
+ );
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ /**
+ * @param IcingaService $service
+ * @return $this
+ */
+ public function createApplyRuleFor(IcingaService $service)
+ {
+ $this->apply = $service;
+ $object = $this->object();
+ $object->set('imports', $service->getObjectName());
+ $object->set('object_type', 'apply');
+ $object->set('object_name', $service->getObjectName());
+
+ return $this;
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function setupServiceElements()
+ {
+ if ($this->object) {
+ $objectType = $this->object->get('object_type');
+ } elseif ($this->preferredObjectType) {
+ $objectType = $this->preferredObjectType;
+ } else {
+ $objectType = 'template';
+ }
+ $this->addHidden('object_type', $objectType);
+ $forceCommandElements = $this->hasPermission('director/admin');
+
+ $this->addNameElement()
+ ->addHostObjectElement()
+ ->addImportsElement()
+ ->addChoices('service')
+ ->addGroupsElement()
+ ->addDisabledElement()
+ ->addApplyForElement()
+ ->groupMainProperties()
+ ->addAssignmentElements()
+ ->addCheckCommandElements($forceCommandElements)
+ ->addCheckExecutionElements()
+ ->addExtraInfoElements()
+ ->addAgentAndZoneElements()
+ ->setButtons();
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws ProgrammingError
+ */
+ protected function addOverrideHint()
+ {
+ if ($this->object && $this->object->usesVarOverrides()) {
+ $hint = $this->translate(
+ 'This service has been generated in an automated way, but still'
+ . ' allows you to override the following properties in a safe way.'
+ );
+ } elseif ($apply = $this->applyGenerated) {
+ $hint = Html::sprintf(
+ $this->translate(
+ 'This service has been generated using the %s apply rule, assigned where %s'
+ ),
+ Link::create(
+ $apply->getObjectName(),
+ 'director/service',
+ ['id' => $apply->get('id')],
+ ['data-base-target' => '_next']
+ ),
+ (string) Filter::fromQueryString($apply->assign_filter)
+ );
+ } elseif ($this->host && $this->set) {
+ $hint = Html::sprintf(
+ $this->translate(
+ 'This service belongs to the %s Service Set. Still, you might want'
+ . ' to override the following properties for this host only.'
+ ),
+ Link::create(
+ $this->set->getObjectName(),
+ 'director/serviceset',
+ ['id' => $this->set->get('id')],
+ ['data-base-target' => '_next']
+ )
+ );
+ } elseif ($this->inheritedFrom) {
+ $msg = $this->translate(
+ 'This service has been inherited from %s. Still, you might want'
+ . ' to change the following properties for this host only.'
+ );
+
+ $name = $this->inheritedFrom;
+ $link = Link::create(
+ $name,
+ 'director/service',
+ [
+ 'host' => $name,
+ 'name' => $this->object->getObjectName(),
+ ],
+ ['data-base-target' => '_next']
+ );
+
+ $hint = Html::sprintf($msg, $link);
+ } else {
+ throw new ProgrammingError('Got no override hint for your situation');
+ }
+
+ $this->setSubmitLabel($this->translate('Override vars'));
+
+ $this->addHtmlHint($hint, ['name' => 'inheritance_hint']);
+ }
+
+ protected function setupOnHostForSet()
+ {
+ $msg = $this->translate(
+ 'This service belongs to the service set "%s". Still, you might want'
+ . ' to change the following properties for this host only.'
+ );
+
+ $name = $this->set->getObjectName();
+ $link = Link::create(
+ $name,
+ 'director/serviceset',
+ ['name' => $name],
+ ['data-base-target' => '_next']
+ );
+
+ $this->addHtmlHint(
+ Html::sprintf($msg, $link),
+ ['name' => 'inheritance_hint']
+ );
+
+ $this->addElementsToGroup(
+ ['inheritance_hint'],
+ 'custom_fields',
+ 50,
+ $this->translate('Custom properties')
+ );
+
+ $this->setSubmitLabel($this->translate('Override vars'));
+ }
+
+ protected function addAssignmentElements()
+ {
+ $this->addAssignFilter([
+ 'suggestionContext' => 'HostFilterColumns',
+ 'required' => true,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function setupHostRelatedElements()
+ {
+ $this->addHidden('host', $this->host->getObjectName());
+ $this->addHidden('object_type', 'object');
+ $this->addImportsElement();
+ $imports = $this->getSentOrObjectValue('imports');
+
+ if ($this->hasBeenSent()) {
+ $imports = $this->getElement('imports')->setValue($imports)->getValue();
+ }
+
+ if ($this->isNew() && empty($imports)) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ $this->addNameElement()
+ ->addChoices('service')
+ ->addDisabledElement()
+ ->addGroupsElement()
+ ->groupMainProperties()
+ ->addCheckCommandElements()
+ ->addExtraInfoElements()
+ ->setButtons();
+
+ $this->setDefaultNameFromTemplate($imports);
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @return $this
+ */
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function setupSetRelatedElements()
+ {
+ $this->addHidden('service_set', $this->set->getObjectName());
+ $this->addHidden('object_type', 'apply');
+ $this->addImportsElement();
+ $this->setButtons();
+ $imports = $this->getSentOrObjectValue('imports');
+
+ if ($this->hasBeenSent()) {
+ $imports = $this->getElement('imports')->setValue($imports)->getValue();
+ }
+
+ if ($this->isNew() && empty($imports)) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ $this->addNameElement()
+ ->addDisabledElement()
+ ->addGroupsElement()
+ ->groupMainProperties();
+
+ if ($this->hasPermission('director/admin')) {
+ $this->addCheckCommandElements(true)
+ ->addCheckExecutionElements(true)
+ ->addExtraInfoElements();
+ }
+
+ $this->setDefaultNameFromTemplate($imports);
+ }
+
+ public function setServiceSet(IcingaServiceSet $set)
+ {
+ $this->set = $set;
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addNameElement()
+ {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Name'),
+ 'required' => !$this->object()->isApplyRule(),
+ 'description' => $this->translate(
+ 'Name for the Icinga service you are going to create'
+ )
+ ));
+
+ if ($this->object()->isApplyRule()) {
+ $this->eventuallyAddNameRestriction('director/service/apply/filter-by-name');
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addHostObjectElement()
+ {
+ if ($this->isObject()) {
+ $this->addElement('select', 'host', [
+ 'label' => $this->translate('Host'),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($this->enumHostsAndTemplates()),
+ 'description' => $this->translate(
+ 'Choose the host this single service should be assigned to'
+ )
+ ]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addApplyForElement()
+ {
+ if ($this->object->isApplyRule()) {
+ $hostProperties = IcingaHost::enumProperties(
+ $this->object->getConnection(),
+ 'host.',
+ new ArrayCustomVariablesFilter()
+ );
+
+ $this->addElement('select', 'apply_for', array(
+ 'label' => $this->translate('Apply For'),
+ 'class' => 'assign-property autosubmit',
+ 'multiOptions' => $this->optionalEnum($hostProperties, $this->translate('None')),
+ 'description' => $this->translate(
+ 'Evaluates the apply for rule for ' .
+ 'all objects with the custom attribute specified. ' .
+ 'E.g selecting "host.vars.custom_attr" will generate "for (config in ' .
+ 'host.vars.array_var)" where "config" will be accessible through "$config$". ' .
+ 'NOTE: only custom variables of type "Array" are eligible.'
+ )
+ ));
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addGroupsElement()
+ {
+ $groups = $this->enumServicegroups();
+
+ if (! empty($groups)) {
+ $this->addElement('extensibleSet', 'groups', array(
+ 'label' => $this->translate('Groups'),
+ 'multiOptions' => $this->optionallyAddFromEnum($groups),
+ 'positional' => false,
+ 'description' => $this->translate(
+ 'Service groups that should be directly assigned to this service.'
+ . ' Servicegroups can be useful for various reasons. They are'
+ . ' helpful to provided service-type specific view in Icinga Web 2,'
+ . ' either for custom dashboards or as an instrument to enforce'
+ . ' restrictions. Service groups can be directly assigned to'
+ . ' single services or to service templates.'
+ )
+ ));
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addAgentAndZoneElements()
+ {
+ if (!$this->isTemplate()) {
+ return $this;
+ }
+
+ $this->optionalBoolean(
+ 'use_agent',
+ $this->translate('Run on agent'),
+ $this->translate(
+ 'Whether the check commmand for this service should be executed'
+ . ' on the Icinga agent'
+ )
+ );
+ $this->addZoneElement();
+
+ $elements = array(
+ 'use_agent',
+ 'zone_id',
+ );
+ $this->addDisplayGroup($elements, 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Icinga Agent and zone settings')
+ ));
+
+ return $this;
+ }
+
+ protected function enumHostsAndTemplates()
+ {
+ if ($this->branch && $this->branch->isBranch()) {
+ return $this->enumHosts();
+ }
+
+ return [
+ $this->translate('Templates') => $this->enumHostTemplates(),
+ $this->translate('Hosts') => $this->enumHosts(),
+ ];
+ }
+
+ protected function enumHostTemplates()
+ {
+ $names = array_values($this->db->enumHostTemplates());
+ return array_combine($names, $names);
+ }
+
+ protected function enumHosts()
+ {
+ $db = $this->db->getDbAdapter();
+ $table = new ObjectsTableHost($this->db);
+ $table->setAuth($this->getAuth());
+ if ($this->branch && $this->branch->isBranch()) {
+ $table->setBranchUuid($this->branch->getUuid());
+ }
+ $result = [];
+ foreach ($db->fetchAll($table->getQuery()->reset(\Zend_Db_Select::LIMIT_COUNT)) as $row) {
+ $result[$row->object_name] = $row->object_name;
+ }
+
+ return $result;
+ }
+
+ protected function enumServicegroups()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from(
+ 'icinga_servicegroup',
+ array(
+ 'name' => 'object_name',
+ 'display' => 'COALESCE(display_name, object_name)'
+ )
+ )->where('object_type = ?', 'object')->order('display');
+
+ return $db->fetchPairs($select);
+ }
+
+ protected function succeedForOverrides()
+ {
+ $vars = array();
+ foreach ($this->object->vars() as $key => $var) {
+ $vars[$key] = $var->getValue();
+ }
+
+ $host = $this->host;
+ $serviceName = $this->object->getObjectName();
+
+ $this->host->overrideServiceVars($serviceName, (object) $vars);
+
+ if ($host->hasBeenModified()) {
+ $msg = sprintf(
+ empty($vars)
+ ? $this->translate('All overrides have been removed from "%s"')
+ : $this->translate('The given properties have been stored for "%s"'),
+ $this->translate($host->getObjectName())
+ );
+
+ $this->getDbObjectStore()->store($host);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+
+ public function onSuccess()
+ {
+ if ($this->providesOverrides()) {
+ $this->succeedForOverrides();
+ return;
+ }
+
+ parent::onSuccess();
+ }
+
+ /**
+ * @param array $imports
+ */
+ protected function setDefaultNameFromTemplate($imports)
+ {
+ if ($this->hasBeenSent()) {
+ $name = $this->getSentOrObjectValue('object_name');
+ if ($name === null || !strlen($name)) {
+ $this->setElementValue('object_name', end($imports));
+ $this->object->set('object_name', end($imports));
+ }
+ }
+ }
+}
diff --git a/application/forms/IcingaServiceGroupForm.php b/application/forms/IcingaServiceGroupForm.php
new file mode 100644
index 0000000..db23cbb
--- /dev/null
+++ b/application/forms/IcingaServiceGroupForm.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaServiceGroupForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Servicegroup'),
+ 'required' => true,
+ 'description' => $this->translate('Icinga object name for this service group')
+ ]);
+
+ $this->addGroupDisplayNameElement()
+ ->addAssignmentElements()
+ ->setButtons();
+ }
+
+ protected function addAssignmentElements()
+ {
+ $this->addAssignFilter([
+ 'suggestionContext' => 'ServiceFilterColumns',
+ 'required' => false,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaServiceSetForm.php b/application/forms/IcingaServiceSetForm.php
new file mode 100644
index 0000000..21508d5
--- /dev/null
+++ b/application/forms/IcingaServiceSetForm.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaServiceSetForm extends DirectorObjectForm
+{
+ protected $host;
+
+ protected $listUrl = 'director/services/sets';
+
+ public function setup()
+ {
+ if ($this->host === null) {
+ $this->setupTemplate();
+ } else {
+ $this->setupHost();
+ }
+
+ $this->setButtons();
+ }
+
+ protected function setupTemplate()
+ {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Service set name'),
+ 'description' => $this->translate(
+ 'A short name identifying this set of services'
+ ),
+ 'required' => true,
+ ])
+ ->eventuallyAddNameRestriction('director/service_set/filter-by-name')
+ ->addHidden('object_type', 'template')
+ ->addDescriptionElement()
+ ->addAssignmentElements();
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ if ($this->host) {
+ $this->setSuccessUrl(
+ 'director/host/services',
+ array('name' => $this->host->getObjectName())
+ );
+ } else {
+ parent::setObjectSuccessUrl();
+ }
+ }
+
+ protected function setupHost()
+ {
+ $object = $this->object();
+ if ($this->hasBeenSent()) {
+ $object->set('object_name', $this->getSentValue('imports'));
+ $object->set('imports', $object->object_name);
+ }
+
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->addSingleImportsElement();
+ }
+
+ if (count($object->get('imports'))) {
+ $description = $object->getResolvedProperty('description');
+ if ($description) {
+ $this->addHtmlHint($description);
+ }
+ }
+
+ $this->addHidden('object_type', 'object');
+ $this->addHidden('host', $this->host->getObjectName());
+ $this->groupMainProperties();
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ protected function addSingleImportsElement()
+ {
+ $enum = $this->enumAllowedTemplates();
+
+ $this->addElement('select', 'imports', array(
+ 'label' => $this->translate('Service set'),
+ 'description' => $this->translate(
+ 'The service set that should be assigned to this host'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionallyAddFromEnum($enum),
+ 'class' => 'autosubmit'
+ ));
+
+ return $this;
+ }
+
+ protected function addDescriptionElement()
+ {
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'A meaningful description explaining your users what to expect'
+ . ' when assigning this set of services'
+ ),
+ 'rows' => '3',
+ 'required' => ! $this->isTemplate(),
+ ));
+
+ return $this;
+ }
+
+ protected function addAssignmentElements()
+ {
+ if (! $this->hasPermission('director/service_set/apply')) {
+ return $this;
+ }
+
+ $this->addAssignFilter([
+ 'suggestionContext' => 'HostFilterColumns',
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. You'
+ . ' might also want to skip this, define it later and/or just'
+ . ' add this set of services to single hosts. The "contains"'
+ . ' operator is valid for arrays only. Please use wildcards and'
+ . ' the = (equals) operator when searching for partial string'
+ . ' matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaServiceVarForm.php b/application/forms/IcingaServiceVarForm.php
new file mode 100644
index 0000000..e7ac4a0
--- /dev/null
+++ b/application/forms/IcingaServiceVarForm.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+/**
+ * @deprecated
+ */
+class IcingaServiceVarForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addElement('select', 'service_id', array(
+ 'label' => $this->translate('Service'),
+ 'description' => $this->translate('The name of the service'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumServices()),
+ 'required' => true
+ ));
+
+ $this->addElement('text', 'varname', array(
+ 'label' => $this->translate('Name'),
+ 'description' => $this->translate('service var name')
+ ));
+
+ $this->addElement('textarea', 'varvalue', array(
+ 'label' => $this->translate('Value'),
+ 'description' => $this->translate('service var value')
+ ));
+
+ $this->addElement('text', 'format', array(
+ 'label' => $this->translate('Format'),
+ 'description' => $this->translate('value format')
+ ));
+ }
+}
diff --git a/application/forms/IcingaTemplateChoiceForm.php b/application/forms/IcingaTemplateChoiceForm.php
new file mode 100644
index 0000000..31fe610
--- /dev/null
+++ b/application/forms/IcingaTemplateChoiceForm.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaTemplateChoice;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaTemplateChoiceForm extends DirectorObjectForm
+{
+ private $choiceType;
+
+ public static function create($type, Db $db)
+ {
+ return static::load()->setDb($db)->setChoiceType($type);
+ }
+
+ public function optionallyLoad($name)
+ {
+ if ($name !== null) {
+ /** @var IcingaTemplateChoice $class - cheating IDE */
+ $class = $this->getObjectClassName();
+ $this->setObject($class::load($name, $this->getDb()));
+ }
+
+ return $this;
+ }
+
+ protected function getObjectClassname()
+ {
+ if ($this->className === null) {
+ return 'Icinga\\Module\\Director\\Objects\\IcingaTemplateChoice'
+ . ucfirst($this->choiceType);
+ }
+
+ return $this->className;
+ }
+
+ public function setChoiceType($type)
+ {
+ $this->choiceType = $type;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Choice name'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'This will be shown as a label for the given choice'
+ )
+ ));
+
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'rows' => 4,
+ 'description' => $this->translate(
+ 'A detailled description explaining what this choice is all about'
+ )
+ ));
+
+ $this->addElement('extensibleSet', 'members', array(
+ 'label' => $this->translate('Available choices'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Your users will be allowed to choose among those templates'
+ ),
+ 'multiOptions' => $this->fetchUnboundTemplates()
+ ));
+
+ $this->addElement('text', 'min_required', array(
+ 'label' => $this->translate('Minimum required'),
+ 'description' => $this->translate(
+ 'Choosing this many options will be mandatory for this Choice.'
+ . ' Setting this to zero will leave this Choice optional, setting'
+ . ' it to one results in a "required" Choice. You can use higher'
+ . ' numbers to enforce multiple options, this Choice will then turn'
+ . ' into a multi-selection element.'
+ ),
+ 'value' => 0,
+ ));
+
+ $this->addElement('text', 'max_allowed', array(
+ 'label' => $this->translate('Allowed maximum'),
+ 'description' => $this->translate(
+ 'It will not be allowed to choose more than this many options.'
+ . ' Setting it to one (1) will result in a drop-down box, a'
+ . ' higher number will turn this into a multi-selection element.'
+ ),
+ 'value' => 1,
+ ));
+
+ $this->addElement('select', 'required_template', [
+ 'label' => $this->translate('Associated Template'),
+ 'description' => $this->translate(
+ 'Choose Choice Associated Template'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->fetchUnboundTemplates(),
+ ]);
+
+ $this->setButtons();
+ }
+
+ protected function fetchUnboundTemplates()
+ {
+ /** @var IcingaTemplateChoice $object */
+ $object = $this->object();
+ $db = $this->getDb()->getDbAdapter();
+ $table = $object->getObjectTableName();
+ $query = $db->select()->from(
+ ['o' => $table],
+ [
+ 'k' => 'o.object_name',
+ 'v' => 'o.object_name',
+ ]
+ )->where("o.object_type = 'template'");
+ if ($object->hasBeenLoadedFromDb()) {
+ $query->where(
+ 'o.template_choice_id IS NULL OR o.template_choice_id = ?',
+ $object->get('id')
+ );
+ } else {
+ $query->where('o.template_choice_id IS NULL');
+ }
+
+ return $db->fetchPairs($query);
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ /** @var IcingaTemplateChoice $object */
+ $object = $this->object();
+ $this->setSuccessUrl(
+ 'director/templatechoice/' . $object->getObjectshortTableName(),
+ $object->getUrlParams()
+ );
+ }
+}
diff --git a/application/forms/IcingaTimePeriodForm.php b/application/forms/IcingaTimePeriodForm.php
new file mode 100644
index 0000000..8afcdf3
--- /dev/null
+++ b/application/forms/IcingaTimePeriodForm.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaTimePeriodForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Name'),
+ 'required' => true,
+ ]);
+
+ $this->addElement('text', 'display_name', [
+ 'label' => $this->translate('Display Name'),
+ ]);
+
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'update_method', [
+ 'label' => $this->translate('Update Method'),
+ 'value' => 'LegacyTimePeriod',
+ ]);
+ } else {
+ // TODO: I'd like to skip this for objects inheriting from a template
+ // with a defined update_method. However, unfortunately it's too
+ // early for $this->object()->getResolvedProperty('update_method').
+ // Should be fixed.
+ $this->addHidden('update_method', 'LegacyTimePeriod');
+ }
+
+ $this->addIncludeExclude()
+ ->addImportsElement()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addIncludeExclude()
+ {
+ $periods = [];
+ foreach ($this->db->enumTimeperiods() as $id => $period) {
+ if ($this->object === null || $this->object->get('object_name') !== $period) {
+ $periods[$period] = $period;
+ }
+ }
+
+ if (empty($periods)) {
+ return $this;
+ }
+
+ $this->addElement('extensibleSet', 'includes', [
+ 'label' => $this->translate('Include period'),
+ 'multiOptions' => $this->optionalEnum($periods),
+ 'description' => $this->translate(
+ 'Include other time periods into this.'
+ ),
+ ]);
+
+ $this->addElement('extensibleSet', 'excludes', [
+ 'label' => $this->translate('Exclude period'),
+ 'multiOptions' => $this->optionalEnum($periods),
+ 'description' => $this->translate(
+ 'Exclude other time periods from this.'
+ ),
+ ]);
+
+ $this->optionalBoolean(
+ 'prefer_includes',
+ $this->translate('Prefer includes'),
+ $this->translate('Whether to prefer timeperiods includes or excludes. Default to true.')
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaTimePeriodRangeForm.php b/application/forms/IcingaTimePeriodRangeForm.php
new file mode 100644
index 0000000..977684e
--- /dev/null
+++ b/application/forms/IcingaTimePeriodRangeForm.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaTimePeriod;
+use Icinga\Module\Director\Objects\IcingaTimePeriodRange;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaTimePeriodRangeForm extends DirectorObjectForm
+{
+ /**
+ * @var IcingaTimePeriod
+ */
+ private $period;
+
+ public function setup()
+ {
+ $this->addHidden('timeperiod_id', $this->period->get('id'));
+ $this->addElement('text', 'range_key', array(
+ 'label' => $this->translate('Day(s)'),
+ 'description' => $this->translate(
+ 'Might be monday, tuesday or 2016-01-28 - have a look at the documentation for more examples'
+ ),
+ ));
+
+ $this->addElement('text', 'range_value', array(
+ 'label' => $this->translate('Timerperiods'),
+ 'description' => $this->translate(
+ 'One or more time periods, e.g. 00:00-24:00 or 00:00-09:00,17:00-24:00'
+ ),
+ ));
+
+ $this->setButtons();
+ }
+
+ public function setTimePeriod(IcingaTimePeriod $period)
+ {
+ $this->period = $period;
+ $this->setDb($period->getConnection());
+ return $this;
+ }
+
+ /**
+ * @param IcingaTimePeriodRange $object
+ */
+ protected function deleteObject($object)
+ {
+ $key = $object->get('range_key');
+ $period = $this->period;
+ $period->ranges()->remove($key);
+ $period->store();
+ $msg = sprintf(
+ 'Time period range "%s" has been removed from %s',
+ $key,
+ $period->getObjectName()
+ );
+
+ $url = $this->getSuccessUrl()->without(
+ ['range', 'range_type']
+ );
+
+ $this->setSuccessUrl($url);
+ $this->redirectOnSuccess($msg);
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object();
+ if ($object->hasBeenModified()) {
+ $this->period->ranges()->setRange(
+ $this->getValue('range_key'),
+ $this->getValue('range_value')
+ );
+ }
+
+ if ($this->period->hasBeenModified()) {
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->setHttpResponseCode(201);
+ }
+
+ $msg = sprintf(
+ $object->hasBeenLoadedFromDb()
+ ? $this->translate('The %s has successfully been stored')
+ : $this->translate('A new %s has successfully been created'),
+ $this->translate($this->getObjectShortClassName())
+ );
+
+ $this->period->store($this->db);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+ if ($object instanceof IcingaObject) {
+ $this->setSuccessUrl(
+ 'director/' . strtolower($this->getObjectShortClassName()),
+ $object->getUrlParams()
+ );
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaUserForm.php b/application/forms/IcingaUserForm.php
new file mode 100644
index 0000000..bff2252
--- /dev/null
+++ b/application/forms/IcingaUserForm.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaUserForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('User template name'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga user template you are going to create')
+ ));
+ } else {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Username'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga user object you are going to create')
+ ));
+ }
+
+ if (! $this->isTemplate()) {
+ $this->addElement('text', 'email', array(
+ 'label' => $this->translate('Email'),
+ 'description' => $this->translate('The Email address of the user.')
+ ));
+
+ $this->addElement('text', 'pager', array(
+ 'label' => $this->translate('Pager'),
+ 'description' => $this->translate('The pager address of the user.')
+ ));
+ }
+
+ $this->addGroupsElement()
+ ->addImportsElement()
+ ->addDisplayNameElement()
+ ->addEnableNotificationsElement()
+ ->addDisabledElement()
+ ->addZoneElements()
+ ->addPeriodElement()
+ ->addEventFilterElements()
+ ->groupMainProperties()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addZoneElements()
+ {
+ if (! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addZoneElement();
+ $this->addDisplayGroup(array('zone_id'), 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addEnableNotificationsElement()
+ {
+ $this->optionalBoolean(
+ 'enable_notifications',
+ $this->translate('Send notifications'),
+ $this->translate('Whether to send notifications for this user')
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addGroupsElement()
+ {
+ $groups = $this->enumUsergroups();
+
+ if (empty($groups)) {
+ return $this;
+ }
+
+ $this->addElement('extensibleSet', 'groups', array(
+ 'label' => $this->translate('Groups'),
+ 'multiOptions' => $this->optionallyAddFromEnum($groups),
+ 'positional' => false,
+ 'description' => $this->translate(
+ 'User groups that should be directly assigned to this user. Groups can be useful'
+ . ' for various reasons. You might prefer to send notifications to groups instead of'
+ . ' single users'
+ )
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addDisplayNameElement()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('text', 'display_name', array(
+ 'label' => $this->translate('Display name'),
+ 'description' => $this->translate(
+ 'Alternative name for this user. In case your object name is a'
+ . ' username, this could be the full name of the corresponding person'
+ )
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addPeriodElement()
+ {
+ $periods = $this->db->enumTimeperiods();
+ if (empty($periods)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'select',
+ 'period_id',
+ array(
+ 'label' => $this->translate('Time period'),
+ 'description' => $this->translate(
+ 'The name of a time period which determines when notifications'
+ . ' to this User should be triggered. Not set by default.'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function groupObjectDefinition()
+ {
+ $elements = array(
+ 'object_type',
+ 'object_name',
+ 'display_name',
+ 'imports',
+ 'groups',
+ 'email',
+ 'pager',
+ 'period_id',
+ 'enable_notifications',
+ 'disabled',
+ );
+ $this->addDisplayGroup($elements, 'object_definition', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_OBJECT_DEFINITION,
+ 'legend' => $this->translate('User properties')
+ ));
+ }
+
+ /**
+ * @return array
+ */
+ protected function enumUsergroups()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from(
+ 'icinga_usergroup',
+ array(
+ 'name' => 'object_name',
+ 'display' => 'COALESCE(display_name, object_name)'
+ )
+ )->where('object_type = ?', 'object')->order('display');
+
+ return $db->fetchPairs($select);
+ }
+}
diff --git a/application/forms/IcingaUserGroupForm.php b/application/forms/IcingaUserGroupForm.php
new file mode 100644
index 0000000..d9706b4
--- /dev/null
+++ b/application/forms/IcingaUserGroupForm.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaUserGroupForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Usergroup'),
+ 'required' => true,
+ 'description' => $this->translate('Icinga object name for this user group')
+ ));
+
+ $this->addGroupDisplayNameElement()
+ ->addZoneElements()
+ ->groupMainProperties()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addZoneElements()
+ {
+ $this->addZoneElement(true);
+ $this->addDisplayGroup(['zone_id'], 'clustering', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'Fieldset',
+ ],
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ]);
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaZoneForm.php b/application/forms/IcingaZoneForm.php
new file mode 100644
index 0000000..bf27cae
--- /dev/null
+++ b/application/forms/IcingaZoneForm.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaZoneForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Zone name'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Name for the Icinga zone you are going to create'
+ )
+ ));
+
+ $this->addElement('select', 'is_global', array(
+ 'label' => $this->translate('Global zone'),
+ 'description' => $this->translate(
+ 'Whether this zone should be available everywhere. Please note that'
+ . ' it rarely leads to the desired result when you try to distribute'
+ . ' global zones in distrubuted environments'
+ ),
+ 'multiOptions' => array(
+ 'n' => $this->translate('No'),
+ 'y' => $this->translate('Yes'),
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('select', 'parent_id', array(
+ 'label' => $this->translate('Parent Zone'),
+ 'description' => $this->translate('Chose an (optional) parent zone'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumZones()),
+ ));
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/ImportCheckForm.php b/application/forms/ImportCheckForm.php
new file mode 100644
index 0000000..31c9781
--- /dev/null
+++ b/application/forms/ImportCheckForm.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class ImportCheckForm extends DirectorForm
+{
+ /** @var ImportSource */
+ protected $source;
+
+ public function setImportSource(ImportSource $source)
+ {
+ $this->source = $source;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->submitLabel = false;
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('Check for changes'),
+ 'decorators' => ['ViewHelper']
+ ]);
+ }
+
+ public function onSuccess()
+ {
+ $source = $this->source;
+ if ($source->checkForChanges()) {
+ $this->setSuccessMessage(
+ $this->translate('This Import Source provides modified data')
+ );
+ } else {
+ $this->setSuccessMessage(
+ $this->translate(
+ 'Nothing to do, data provided by this Import Source'
+ . " didn't change since the last import run"
+ )
+ );
+ }
+
+ if ($source->get('import_state') === 'failing') {
+ $this->addError($this->translate('Checking this Import Source failed'));
+ } else {
+ parent::onSuccess();
+ }
+ }
+}
diff --git a/application/forms/ImportRowModifierForm.php b/application/forms/ImportRowModifierForm.php
new file mode 100644
index 0000000..9e53bd9
--- /dev/null
+++ b/application/forms/ImportRowModifierForm.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use RuntimeException;
+
+class ImportRowModifierForm extends DirectorObjectForm
+{
+ /** @var ImportSource */
+ protected $source;
+
+ /** @var ImportSourceHook */
+ protected $importSource;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('source_id', $this->source->id);
+
+ $this->addElement('text', 'property_name', array(
+ 'label' => $this->translate('Property'),
+ 'description' => $this->translate(
+ 'Please start typing for a list of suggestions. Dots allow you to access nested'
+ . ' properties: column.some.key. Such nested properties cannot be modified in-place,'
+ . ' but you can store the modified value to a new "target property"'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit director-suggest',
+ 'data-suggestion-context' => 'importsourceproperties!' . $this->source->id,
+ ));
+
+ $this->addElement('text', 'target_property', [
+ 'label' => $this->translate('Target property'),
+ 'description' => $this->translate(
+ 'You might want to write the modified value to another (new) property.'
+ . ' This property name can be defined here, the original property would'
+ . ' remain unmodified. Please leave this blank in case you just want to'
+ . ' modify the value of a specific property'
+ ),
+ ]);
+
+ $this->addElement('textarea', 'description', [
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'An extended description for this Import Row Modifier. This should explain'
+ . " it's purpose and why it has been put in place at all."
+ ),
+ 'rows' => '3',
+ ]);
+
+ $error = false;
+ try {
+ $mods = $this->enumModifiers();
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ $mods = $this->optionalEnum([]);
+ }
+
+ $this->addElement('select', 'provider_class', [
+ 'label' => $this->translate('Modifier'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'A property modifier allows you to modify a specific property at import time'
+ ),
+ 'multiOptions' => $this->optionalEnum($mods),
+ 'class' => 'autosubmit',
+ ]);
+ if ($error) {
+ $this->getElement('provider_class')->addError($error);
+ }
+
+ try {
+ if ($class = $this->getSentValue('provider_class')) {
+ if ($class && array_key_exists($class, $mods)) {
+ $this->addSettings($class);
+ }
+ } elseif ($class = $this->object()->get('provider_class')) {
+ $this->addSettings($class);
+ }
+
+ // TODO: next line looks like obsolete duplicate code to me
+ $this->addSettings();
+ } catch (Exception $e) {
+ $this->getElement('provider_class')->addError($e->getMessage());
+ }
+
+ foreach ($this->object()->getSettings() as $key => $val) {
+ if ($el = $this->getElement($key)) {
+ $el->setValue($val);
+ }
+ }
+
+ $this->setButtons();
+ }
+
+ public function getSetting($name, $default = null)
+ {
+ if ($this->hasBeenSent()) {
+ $value = $this->getSentValue($name);
+ if ($value !== null) {
+ return $value;
+ }
+ }
+ if ($this->isNew()) {
+ $value = $this->getElement($name)->getValue();
+ if ($value === null) {
+ return $default;
+ }
+
+ return $value;
+ }
+
+ return $this->object()->getSetting($name, $default);
+ }
+
+ /**
+ * @return ImportSourceHook
+ * @throws ConfigurationError
+ */
+ protected function getImportSource()
+ {
+ if ($this->importSource === null) {
+ $this->importSource = ImportSourceHook::loadByName(
+ $this->source->get('source_name'),
+ $this->db
+ );
+ }
+
+ return $this->importSource;
+ }
+
+ protected function enumModifiers()
+ {
+ /** @var PropertyModifierHook[] $hooks */
+ $hooks = Hook::all('Director\\PropertyModifier');
+ $enum = [];
+ foreach ($hooks as $hook) {
+ $enum[get_class($hook)] = $hook->getName();
+ }
+
+ asort($enum);
+
+ return $enum;
+ }
+
+ /**
+ * @param null $class
+ */
+ protected function addSettings($class = null)
+ {
+ if ($class === null) {
+ $class = $this->getValue('provider_class');
+ }
+
+ if ($class !== null) {
+ if (! class_exists($class)) {
+ throw new RuntimeException(sprintf(
+ 'The hooked class "%s" for this property modifier does no longer exist',
+ $class
+ ));
+ }
+
+ $class::addSettingsFormFields($this);
+ }
+ }
+
+ public function setSource(ImportSource $source)
+ {
+ $this->source = $source;
+
+ return $this;
+ }
+}
diff --git a/application/forms/ImportRunForm.php b/application/forms/ImportRunForm.php
new file mode 100644
index 0000000..9f6494f
--- /dev/null
+++ b/application/forms/ImportRunForm.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class ImportRunForm extends DirectorForm
+{
+ /** @var ImportSource */
+ protected $source;
+
+ public function setImportSource(ImportSource $source)
+ {
+ $this->source = $source;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->submitLabel = false;
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('Trigger Import Run'),
+ 'decorators' => ['ViewHelper']
+ ]);
+ }
+
+ public function onSuccess()
+ {
+ $source = $this->source;
+ if ($source->runImport()) {
+ $this->setSuccessMessage(
+ $this->translate('Imported new data from this Import Source')
+ );
+ } else {
+ $this->setSuccessMessage(
+ $this->translate(
+ 'Nothing to do, data provided by this Import Source'
+ . " didn't change since the last import run"
+ )
+ );
+ }
+
+ if ($source->get('import_state') === 'failing') {
+ $this->addError($this->translate('Triggering this Import Source failed'));
+ } else {
+ parent::onSuccess();
+ }
+ }
+}
diff --git a/application/forms/ImportSourceForm.php b/application/forms/ImportSourceForm.php
new file mode 100644
index 0000000..b547a32
--- /dev/null
+++ b/application/forms/ImportSourceForm.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Web\Hook;
+
+class ImportSourceForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addElement('text', 'source_name', array(
+ 'label' => $this->translate('Import source name'),
+ 'description' => $this->translate(
+ 'A short name identifying this import source. Use something meaningful,'
+ . ' like "Hosts from Puppet", "Users from Active Directory" or similar'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'An extended description for this Import Source. This should explain'
+ . " what kind of data you're going to import from this source."
+ ),
+ 'rows' => '3',
+ ));
+
+ $this->addElement('select', 'provider_class', array(
+ 'label' => $this->translate('Source Type'),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($this->enumSourceTypes()),
+ 'description' => $this->translate(
+ 'These are different data providers fetching data from various sources.'
+ . ' You didn\'t find what you\'re looking for? Import sources are implemented'
+ . ' as a hook in Director, so you might find (or write your own) Icinga Web 2'
+ . ' module fetching data from wherever you want'
+ ),
+ 'class' => 'autosubmit'
+ ));
+
+ $this->addSettings();
+ $this->setButtons();
+ }
+
+ public function getSentOrObjectSetting($name, $default = null)
+ {
+ if ($this->hasObject()) {
+ $value = $this->getSentValue($name);
+ if ($value === null) {
+ /** @var ImportSource $object */
+ $object = $this->getObject();
+
+ return $object->getSetting($name, $default);
+ } else {
+ return $value;
+ }
+ } else {
+ return $this->getSentValue($name, $default);
+ }
+ }
+
+ public function hasChangedSetting($name)
+ {
+ if ($this->hasBeenSent() && $this->hasObject()) {
+ /** @var ImportSource $object */
+ $object = $this->getObject();
+ return $object->getStoredSetting($name)
+ !== $this->getSentValue($name);
+ } else {
+ return false;
+ }
+ }
+
+ protected function addSettings()
+ {
+ if (! ($class = $this->getProviderClass())) {
+ return;
+ }
+
+ $defaultKeyCol = $this->getDefaultKeyColumnName();
+
+ $this->addElement('text', 'key_column', array(
+ 'label' => $this->translate('Key column name'),
+ 'description' => $this->translate(
+ 'This must be a column containing unique values like hostnames. Unless otherwise'
+ . ' specified this will then be used as the object_name for the syncronized'
+ . ' Icinga object. Especially when getting started with director please make'
+ . ' sure to strictly follow this rule. Duplicate values for this column on different'
+ . ' rows will trigger a failure, your import run will not succeed. Please pay attention'
+ . ' when synching services, as "purge" will only work correctly with a key_column'
+ . ' corresponding to host!name. Check the "Combine" property modifier in case your'
+ . ' data source cannot provide such a field'
+ ),
+ 'placeholder' => $defaultKeyCol,
+ 'required' => $defaultKeyCol === null,
+ ));
+
+ if (array_key_exists($class, $this->enumSourceTypes())) {
+ $class::addSettingsFormFields($this);
+ foreach ($this->object()->getSettings() as $key => $val) {
+ if ($el = $this->getElement($key)) {
+ $el->setValue($val);
+ }
+ }
+ }
+ }
+
+ protected function getDefaultKeyColumnName()
+ {
+ if (! ($class = $this->getProviderClass())) {
+ return null;
+ }
+
+ if (! class_exists($class)) {
+ return null;
+ }
+
+ return $class::getDefaultKeyColumnName();
+ }
+
+ protected function getProviderClass()
+ {
+ if ($this->hasBeenSent()) {
+ $class = $this->getRequest()->getPost('provider_class');
+ } else {
+ if (! ($class = $this->object()->get('provider_class'))) {
+ return null;
+ }
+ }
+
+ return $class;
+ }
+
+ public function onSuccess()
+ {
+ if (! $this->getValue('key_column')) {
+ if ($default = $this->getDefaultKeyColumnName()) {
+ $this->setElementValue('key_column', $default);
+ $this->object()->set('key_column', $default);
+ }
+ }
+
+ parent::onSuccess();
+ }
+
+ protected function enumSourceTypes()
+ {
+ /** @var ImportSourceHook[] $hooks */
+ $hooks = Hook::all('Director\\ImportSource');
+
+ $enum = array();
+ foreach ($hooks as $hook) {
+ $enum[get_class($hook)] = $hook->getName();
+ }
+ asort($enum);
+
+ return $enum;
+ }
+}
diff --git a/application/forms/KickstartForm.php b/application/forms/KickstartForm.php
new file mode 100644
index 0000000..0079cfb
--- /dev/null
+++ b/application/forms/KickstartForm.php
@@ -0,0 +1,482 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Data\ResourceFactory;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Migrations;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\KickstartHelper;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+
+class KickstartForm extends DirectorForm
+{
+ private $config;
+
+ private $storeConfigLabel;
+
+ private $createDbLabel;
+
+ private $migrateDbLabel;
+
+ /** @var IcingaEndpoint */
+ private $endpoint;
+
+ private $dbResourceName;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->storeConfigLabel = $this->translate('Store configuration');
+ $this->createDbLabel = $this->translate('Create database schema');
+ $this->migrateDbLabel = $this->translate('Apply schema migrations');
+
+ if ($this->dbResourceName === null) {
+ $this->addResourceConfigElements();
+ $this->addResourceDisplayGroup();
+
+ if (!$this->config()->get('db', 'resource')
+ || ($this->config()->get('db', 'resource') !== $this->getResourceName())) {
+ return;
+ }
+ }
+
+ if (!$this->hasBeenSent() && !$this->tryDbConnection()) {
+ return;
+ }
+
+ if (!$this->migrations()->hasSchema()) {
+ $this->addHtmlHint($this->translate(
+ 'No database schema has been created yet'
+ ), array('name' => 'HINT_schema'));
+
+ $this->addResourceDisplayGroup();
+ $this->setSubmitLabel($this->createDbLabel);
+ return;
+ }
+
+ if ($this->migrations()->hasPendingMigrations()) {
+ $this->addHtmlHint($this->translate(
+ 'There are pending database migrations'
+ ), array('name' => 'HINT_schema'));
+
+ $this->addResourceDisplayGroup();
+ $this->setSubmitLabel($this->migrateDbLabel);
+ return;
+ }
+
+ if (! $this->endpoint && $this->getDb()->hasDeploymentEndpoint()) {
+ $hint = Html::sprintf(
+ $this->translate('Your database looks good, you are ready to %s'),
+ Link::create(
+ $this->translate('start working with the Icinga Director'),
+ 'director',
+ null,
+ ['data-base-target' => '_main']
+ )
+ );
+
+ $this->addHtmlHint($hint, ['name' => 'HINT_ready']);
+ $this->getDisplayGroup('config')->addElements(
+ array($this->getElement('HINT_ready'))
+ );
+
+ return;
+ }
+
+ $this->addResourceDisplayGroup();
+
+ if ($this->getDb()->hasDeploymentEndpoint()) {
+ $this->addHtmlHint(
+ $this->translate(
+ 'Your configuration looks good. Still, you might want to re-run'
+ . ' this kickstart wizard to (re-)import modified or new manually'
+ . ' defined Command definitions or to get fresh new ITL commands'
+ . ' after an Icinga 2 Core upgrade.'
+ ),
+ array('name' => 'HINT_kickstart')
+ // http://docs.icinga.com/icinga2/latest/doc/module/icinga2/chapter/
+ // ... object-types#objecttype-apilistener
+ );
+ } else {
+ $this->addHtmlHint(
+ $this->translate(
+ 'Your installation of Icinga Director has not yet been prepared for'
+ . ' deployments. This kickstart wizard will assist you with setting'
+ . ' up the connection to your Icinga 2 server.'
+ ),
+ array('name' => 'HINT_kickstart')
+ // http://docs.icinga.com/icinga2/latest/doc/module/icinga2/chapter/
+ // ... object-types#objecttype-apilistener
+ );
+ }
+
+ $this->addElement('text', 'endpoint', array(
+ 'label' => $this->translate('Endpoint Name'),
+ 'description' => $this->translate(
+ 'This is the name of the Endpoint object (and certificate name) you'
+ . ' created for your ApiListener object. In case you are unsure what'
+ . ' this means please make sure to read the documentation first'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('text', 'host', array(
+ 'label' => $this->translate('Icinga Host'),
+ 'description' => $this->translate(
+ 'IP address / hostname of your Icinga node. Please note that this'
+ . ' information will only be used for the very first connection to'
+ . ' your Icinga instance. The Director then relies on a correctly'
+ . ' configured Endpoint object. Correctly configures means that either'
+ . ' it\'s name is resolvable or that it\'s host property contains'
+ . ' either an IP address or a resolvable host name. Your Director must'
+ . ' be able to reach this endpoint'
+ ),
+ 'required' => false,
+ ));
+
+ $this->addElement('text', 'port', array(
+ 'label' => $this->translate('Port'),
+ 'value' => '5665',
+ 'description' => $this->translate(
+ 'The port you are going to use. The default port 5665 will be used'
+ . ' if none is set'
+ ),
+ 'required' => false,
+ ));
+
+ $this->addElement('text', 'username', array(
+ 'label' => $this->translate('API user'),
+ 'description' => $this->translate(
+ 'Your Icinga 2 API username'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('password', 'password', array(
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate(
+ 'The corresponding password'
+ ),
+ 'required' => true,
+ ));
+
+ if ($ep = $this->endpoint) {
+ $user = $ep->getApiUser();
+ $this->setDefaults(array(
+ 'endpoint' => $ep->get('object_name'),
+ 'host' => $ep->get('host'),
+ 'port' => $ep->get('port'),
+ 'username' => $user->get('object_name'),
+ 'password' => $user->get('password'),
+ ));
+
+ if (! empty($user->password)) {
+ $this->getElement('password')->setAttrib(
+ 'placeholder',
+ '(use stored password)'
+ )->setRequired(false);
+ }
+ }
+
+ $this->addKickstartDisplayGroup();
+ $this->setSubmitLabel($this->translate('Run import'));
+ }
+
+ /**
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ protected function onSetup()
+ {
+ if ($this->hasBeenSubmitted()) {
+ // Do not hinder the form from being stored
+ return;
+ }
+
+ $this->tryDbConnection();
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function addResourceConfigElements()
+ {
+ $config = $this->config();
+ $resources = $this->enumResources();
+
+ if (!$this->getResourceName()) {
+ $this->addHtmlHint($this->translate(
+ 'No database resource has been configured yet. Please choose a'
+ . ' resource to complete your config'
+ ), array('name' => 'HINT_no_resource'));
+ }
+
+ $this->addElement('select', 'resource', array(
+ 'required' => true,
+ 'label' => $this->translate('DB Resource'),
+ 'multiOptions' => $this->optionalEnum($resources),
+ 'class' => 'autosubmit',
+ 'value' => $config->get('db', 'resource')
+ ));
+
+ if (empty($resources)) {
+ $this->getElement('resource')->addError(
+ $this->translate('This has to be a MySQL or PostgreSQL database')
+ );
+
+ $this->addHtmlHint(Html::sprintf(
+ $this->translate('Please click %s to create new DB resources'),
+ Link::create(
+ $this->translate('here'),
+ 'config/resource',
+ null,
+ ['data-base-target' => '_main']
+ )
+ ));
+ }
+
+ $this->setSubmitLabel($this->storeConfigLabel);
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function addResourceDisplayGroup()
+ {
+ if ($this->dbResourceName !== null) {
+ return;
+ }
+
+ $elements = array(
+ 'HINT_no_resource',
+ 'resource',
+ 'HINT_ready',
+ 'HINT_schema',
+ 'HINT_db_perms',
+ 'HINT_config_store'
+ );
+
+ $this->addDisplayGroup($elements, 'config', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => 40,
+ 'legend' => $this->translate('Database backend')
+ ));
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function addKickstartDisplayGroup()
+ {
+ $elements = array(
+ 'HINT_kickstart', 'endpoint', 'host', 'port', 'username', 'password'
+ );
+
+ $this->addDisplayGroup($elements, 'wizard', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => 60,
+ 'legend' => $this->translate('Kickstart Wizard')
+ ));
+ }
+
+ /**
+ * @return bool
+ * @throws \Zend_Form_Exception
+ */
+ protected function storeResourceConfig()
+ {
+ $config = $this->config();
+ $value = $this->getValue('resource');
+
+ $config->setSection('db', array('resource' => $value));
+
+ try {
+ $config->saveIni();
+ $this->setSuccessMessage($this->translate('Configuration has been stored'));
+
+ return true;
+ } catch (Exception $e) {
+ $this->getElement('resource')->addError(
+ sprintf(
+ $this->translate(
+ 'Unable to store the configuration to "%s". Please check'
+ . ' file permissions or manually store the content shown below'
+ ),
+ $config->getConfigFile()
+ )
+ );
+ $this->addHtmlHint(
+ Html::tag('pre', null, (string) $config),
+ array('name' => 'HINT_config_store')
+ );
+
+ $this->getDisplayGroup('config')->addElements(
+ array($this->getElement('HINT_config_store'))
+ );
+ $this->removeElement('HINT_ready');
+
+ return false;
+ }
+ }
+
+ public function setEndpoint(IcingaEndpoint $endpoint)
+ {
+ $this->endpoint = $endpoint;
+ return $this;
+ }
+
+ /**
+ * @throws \Icinga\Exception\ProgrammingError
+ * @throws \Zend_Form_Exception
+ */
+ public function onSuccess()
+ {
+ if ($this->getSubmitLabel() === $this->storeConfigLabel) {
+ if ($this->storeResourceConfig()) {
+ parent::onSuccess();
+ } else {
+ return;
+ }
+ }
+
+ if ($this->getSubmitLabel() === $this->createDbLabel
+ || $this->getSubmitLabel() === $this->migrateDbLabel) {
+ $this->migrations()->applyPendingMigrations();
+ parent::onSuccess();
+ }
+
+ $values = $this->getValues();
+ if ($this->endpoint && empty($values['password'])) {
+ $values['password'] = $this->endpoint->getApiUser()->password;
+ }
+
+ $kickstart = new KickstartHelper($this->getDb());
+ unset($values['resource']);
+ $kickstart->setConfig($values)->run();
+
+ parent::onSuccess();
+ }
+
+ public function setDbResourceName($name)
+ {
+ $this->dbResourceName = $name;
+
+ return $this;
+ }
+
+ protected function getResourceName()
+ {
+ if ($this->dbResourceName !== null) {
+ return $this->dbResourceName;
+ }
+
+ if ($this->hasBeenSent()) {
+ $resource = $this->getSentValue('resource');
+ $resources = $this->enumResources();
+ if (in_array($resource, $resources)) {
+ return $resource;
+ } else {
+ return null;
+ }
+ } else {
+ return $this->config()->get('db', 'resource');
+ }
+ }
+
+ public function getDb()
+ {
+ return Db::fromResourceName($this->getResourceName());
+ }
+
+ protected function getResource()
+ {
+ return ResourceFactory::create($this->getResourceName());
+ }
+
+ /**
+ * @return Migrations
+ */
+ protected function migrations()
+ {
+ return new Migrations($this->getDb());
+ }
+
+ public function setModuleConfig(Config $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ protected function config()
+ {
+ if ($this->config === null) {
+ $this->config = Config::module('director');
+ }
+
+ return $this->config;
+ }
+
+ protected function enumResources()
+ {
+ $resources = array();
+ $allowed = array('mysql', 'pgsql');
+
+ foreach (ResourceFactory::getResourceConfigs() as $name => $resource) {
+ if ($resource->get('type') === 'db' && in_array($resource->get('db'), $allowed)) {
+ $resources[$name] = $name;
+ }
+ }
+
+ return $resources;
+ }
+
+ protected function tryDbConnection()
+ {
+ if ($resourceName = $this->getResourceName()) {
+ $resourceConfig = ResourceFactory::getResourceConfig($resourceName);
+ if (!isset($resourceConfig->charset)
+ || !in_array($resourceConfig->charset, array('utf8', 'utf8mb4', 'UTF8', 'UTF-8'))
+ ) {
+ if ($resource = $this->getElement('resource')) {
+ $resource->addError('Please change the encoding for the director database to utf8');
+ } else {
+ $this->addError('Please change the encoding for the director database to utf8');
+ }
+ }
+
+ $resource = $this->getResource();
+ $db = $resource->getDbAdapter();
+
+ try {
+ $db->fetchOne('SELECT 1');
+ return true;
+ } catch (Exception $e) {
+ $this->getElement('resource')
+ ->addError('Could not connect to database: ' . $e->getMessage());
+
+ $hint = $this->translate(
+ 'Please make sure that your database exists and your user has'
+ . ' been granted enough permissions'
+ );
+
+ $this->addHtmlHint($hint, array('name' => 'HINT_db_perms'));
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/application/forms/RemoveLinkForm.php b/application/forms/RemoveLinkForm.php
new file mode 100644
index 0000000..6f0c7cc
--- /dev/null
+++ b/application/forms/RemoveLinkForm.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\IcingaWeb2\Icon;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class RemoveLinkForm extends DirectorForm
+{
+ private $label;
+
+ private $title;
+
+ private $onSuccessAction;
+
+ public function __construct($label, $title, $action, $params = [])
+ {
+ // Required to detect the right instance
+ $this->formName = 'RemoveSet' . sha1(json_encode($params));
+ parent::__construct([
+ 'style' => 'float: right',
+ 'data-base-target' => '_self'
+ ]);
+ $this->label = $label;
+ $this->title = $title;
+ foreach ($params as $name => $value) {
+ $this->addHidden($name, $value);
+ }
+ $this->setAction($action);
+ }
+
+ public function runOnSuccess($action)
+ {
+ $this->onSuccessAction = $action;
+
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->setAttrib('class', 'inline');
+ $this->addHtml(Icon::create('cancel'));
+ $this->addSubmitButton($this->label, [
+ 'class' => 'link-button',
+ 'title' => $this->title,
+ ]);
+ }
+
+ public function onSuccess()
+ {
+ if ($this->onSuccessAction !== null) {
+ $func = $this->onSuccessAction;
+ $func();
+ $this->redirectOnSuccess(
+ $this->translate('Service Set has been removed')
+ );
+ }
+ }
+}
diff --git a/application/forms/RestoreBasketForm.php b/application/forms/RestoreBasketForm.php
new file mode 100644
index 0000000..90d5b38
--- /dev/null
+++ b/application/forms/RestoreBasketForm.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
+use Icinga\Module\Director\Web\Controller\Extension\DirectorDb;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class RestoreBasketForm extends QuickForm
+{
+ use DirectorDb;
+
+ /** @var BasketSnapshot */
+ private $snapshot;
+
+ public function setSnapshot(BasketSnapshot $snapshot)
+ {
+ $this->snapshot = $snapshot;
+
+ return $this;
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ * @return Auth
+ */
+ protected function Auth()
+ {
+ return Auth::getInstance();
+ }
+
+ /**
+ * @return Config
+ */
+ protected function Config()
+ {
+ // @codingStandardsIgnoreEnd
+ return Config::module('director');
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $allowedDbs = $this->listAllowedDbResourceNames();
+ $this->addElement('select', 'target_db', [
+ 'label' => $this->translate('Target DB'),
+ 'description' => $this->translate('Restore to this target Director DB'),
+ 'multiOptions' => $allowedDbs,
+ 'value' => $this->getRequest()->getParam('target_db', $this->getFirstDbResourceName()),
+ 'class' => 'autosubmit',
+ ]);
+
+ $this->setSubmitLabel($this->translate('Restore'));
+ }
+
+ public function getDb()
+ {
+ return Db::fromResourceName($this->getValue('target_db'));
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function onSuccess()
+ {
+ $this->snapshot->restoreTo($this->getDb());
+ $this->setSuccessUrl($this->getSuccessUrl()->with('target_db', $this->getValue('target_db')));
+ $this->setSuccessMessage(sprintf('Restored to %s', $this->getValue('target_db')));
+
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/RestoreObjectForm.php b/application/forms/RestoreObjectForm.php
new file mode 100644
index 0000000..e665d65
--- /dev/null
+++ b/application/forms/RestoreObjectForm.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class RestoreObjectForm extends DirectorForm
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ public function setup()
+ {
+ $this->addSubmitButton($this->translate('Restore former object'));
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object;
+ $name = $object->getObjectName();
+ $db = $this->db;
+
+ $keyParams = $object->getKeyParams();
+
+ if ($object->supportsApplyRules() && $object->get('object_type') === 'apply') {
+ // TODO: not all apply should be considered unique by name + object_type
+ $query = $db->getDbAdapter()
+ ->select()
+ ->from($object->getTableName())
+ ->where('object_type = ?', 'apply')
+ ->where('object_name = ?', $name);
+
+ $rules = $object::loadAll($db, $query);
+
+ if (empty($rules)) {
+ $existing = null;
+ } elseif (count($rules) === 1) {
+ $existing = current($rules);
+ } else {
+ // TODO: offer drop down?
+ throw new NotImplementedError(
+ "Found multiple apply rule matching name '%s', can not restore!",
+ $name
+ );
+ }
+ } else {
+ try {
+ $existing = $object::load($keyParams, $db);
+ } catch (NotFoundError $e) {
+ $existing = null;
+ }
+ }
+
+ if ($existing !== null) {
+ $typeExisting = $existing->get('object_type');
+ $typeObject = $object->get('object_type');
+ if ($typeExisting !== $typeObject) {
+ // Not sure when that may occur
+ throw new NotImplementedError(
+ 'Found existing object has a mismatching object_type: %s != %s',
+ $typeExisting,
+ $typeObject
+ );
+ }
+
+ $existing->replaceWith($object);
+
+ if ($existing->hasBeenModified()) {
+ $msg = $this->translate('Object has been restored');
+ $existing->store();
+ } else {
+ $msg = $this->translate(
+ 'Nothing to do, restore would not modify the current object'
+ );
+ }
+ } else {
+ $msg = $this->translate('Object has been re-created');
+ $object->store($db);
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+}
diff --git a/application/forms/SelfServiceSettingsForm.php b/application/forms/SelfServiceSettingsForm.php
new file mode 100644
index 0000000..4471bcf
--- /dev/null
+++ b/application/forms/SelfServiceSettingsForm.php
@@ -0,0 +1,306 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Settings;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class SelfServiceSettingsForm extends DirectorForm
+{
+ /** @var Settings */
+ protected $settings;
+
+ public function setup()
+ {
+ $settings = $this->settings;
+ $this->addElement('select', 'agent_name', [
+ 'label' => $this->translate('Host Name'),
+ 'description' => $this->translate(
+ 'What to use as your Icinga 2 Agent\'s Host Name'
+ ),
+ 'multiOptions' => [
+ 'fqdn' => $this->translate('Fully qualified domain name (FQDN)'),
+ 'hostname' => $this->translate('Host name (local part, without domain)'),
+ ],
+ 'value' => $settings->getStoredOrDefaultValue('self-service/agent_name')
+ ]);
+
+ $this->addElement('select', 'transform_hostname', [
+ 'label' => $this->translate('Transform Host Name'),
+ 'description' => $this->translate(
+ 'Whether to adjust your host name'
+ ),
+ 'multiOptions' => [
+ '0' => $this->translate('Do not transform at all'),
+ '1' => $this->translate('Transform to lowercase'),
+ '2' => $this->translate('Transform to uppercase'),
+ ],
+ 'value' => $settings->getStoredOrDefaultValue('self-service/transform_hostname')
+ ]);
+ $this->addElement('select', 'resolve_parent_host', [
+ 'label' => $this->translate('Transform Parent Host to IP'),
+ 'description' => $this->translate(
+ 'This is only important in case your master/satellite nodes do not'
+ . ' have IP addresses as their "host" property. The Agent can be'
+ . ' told to issue related DNS lookups on it\' own'
+ ),
+ 'multiOptions' => [
+ '0' => $this->translate("Don't care, my host settings are fine"),
+ '1' => $this->translate('My Agents should use DNS to look up Endpoint names'),
+ ],
+ 'value' => $settings->getStoredOrDefaultValue('self-service/resolve_parent_host')
+ ]);
+
+ $this->addElement('extensibleSet', 'global_zones', [
+ 'label' => $this->translate('Global Zones'),
+ 'description' => $this->translate(
+ 'To ensure downloaded packages are build by the Icinga Team'
+ . ' and not compromised by third parties, you will be able'
+ . ' to provide an array of SHA1 hashes here. In case you have'
+ . ' defined any hashses, the module will not continue with'
+ . ' updating / installing the Agent in case the SHA1 hash of'
+ . ' the downloaded MSI package is not matching one of the'
+ . ' provided hashes of this setting'
+ ),
+ 'multiOptions' => $this->enumGlobalZones(),
+ 'value' => $settings->getStoredOrDefaultValue('self-service/global_zones'),
+ ]);
+
+ $this->addElement('select', 'download_type', [
+ 'label' => $this->translate('Installation Source'),
+ 'description' => $this->translate(
+ 'You might want to let the generated Powershell script install'
+ . ' the Icinga 2 Agent in an automated way. If so, please choose'
+ . ' where your Windows nodes should fetch the Agent installer'
+ ),
+ 'multiOptions' => [
+ null => $this->translate('- no automatic installation -'),
+ // TODO: not yet
+ // 'director' => $this->translate('Download via the Icinga Director'),
+ 'icinga' => $this->translate('Download from packages.icinga.com'),
+ 'url' => $this->translate('Download from a custom url'),
+ 'file' => $this->translate('Use a local file or network share'),
+ ],
+ 'value' => $settings->getStoredOrDefaultValue('self-service/download_type'),
+ 'class' => 'autosubmit'
+ ]);
+
+ $downloadType = $this->getSentValue(
+ 'download_type',
+ $settings->getStoredOrDefaultValue('self-service/download_type')
+ );
+
+ if ($downloadType) {
+ $this->addInstallSettings($downloadType, $settings);
+ }
+
+ $this->addEventuallyConfiguredBoolean('flush_api_dir', [
+ 'label' => $this->translate('Flush API directory'),
+ 'description' => $this->translate(
+ 'In case the Icinga Agent will accept configuration from the parent'
+ . ' Icinga 2 system, it will possibly write data to /var/lib/icinga2/api/*.'
+ . ' By setting this parameter to true, all content inside the api directory'
+ . ' will be flushed before an eventual restart of the Icinga 2 Agent'
+ ),
+ 'required' => true,
+ ]);
+ }
+
+ protected function addInstallSettings($downloadType, Settings $settings)
+ {
+ $this->addElement('text', 'download_url', [
+ 'label' => $this->translate('Source Path'),
+ 'description' => $this->translate(
+ 'Define a download Url or local directory from which the a specific'
+ . ' Icinga 2 Agent MSI Installer package should be fetched. Please'
+ . ' ensure to only define the base download Url or Directory. The'
+ . ' Module will generate the MSI file name based on your operating'
+ . ' system architecture and the version to install. The Icinga 2 MSI'
+ . ' Installer name is internally build as follows:'
+ . ' Icinga2-v[InstallAgentVersion]-[OSArchitecture].msi (full example:'
+ . ' Icinga2-v2.6.3-x86_64.msi)'
+ ),
+ 'value' => $settings->getStoredOrDefaultValue('self-service/download_url'),
+ ]);
+
+ // TODO: offer to check for available versions
+ if ($downloadType === 'icinga') {
+ $el = $this->getElement('download_url');
+ $el->setAttrib('disabled', 'disabled');
+ $value = 'https://packages.icinga.com/windows/';
+ $el->setValue($value);
+ $this->setSentValue('download_url', $value);
+ }
+ if ($downloadType === 'director') {
+ $el = $this->getElement('download_url');
+ $el->setAttrib('disabled', 'disabled');
+
+ $r = $this->getRequest();
+ $scheme = $r->getServer('HTTP_X_FORWARDED_PROTO', $r->getScheme());
+
+ $value = sprintf(
+ '%s://%s%s/director/download/windows/',
+ $scheme,
+ $r->getHttpHost(),
+ $this->getRequest()->getBaseUrl()
+ );
+ $el->setValue($value);
+ $this->setSentValue('download_url', $value);
+ }
+
+ $this->addElement('text', 'agent_version', [
+ 'label' => $this->translate('Agent Version'),
+ 'description' => $this->translate(
+ 'In case the Icinga 2 Agent should be automatically installed,'
+ . ' this has to be a string value like: 2.6.3'
+ ),
+ 'value' => $settings->getStoredOrDefaultValue('self-service/agent_version'),
+ 'required' => true,
+ ]);
+
+ $hashes = $settings->getStoredOrDefaultValue('self-service/installer_hashes');
+ $this->addElement('extensibleSet', 'installer_hashes', [
+ 'label' => $this->translate('Installer Hashes'),
+ 'description' => $this->translate(
+ 'To ensure downloaded packages are build by the Icinga Team'
+ . ' and not compromised by third parties, you will be able'
+ . ' to provide an array of SHA1 hashes here. In case you have'
+ . ' defined any hashses, the module will not continue with'
+ . ' updating / installing the Agent in case the SHA1 hash of'
+ . ' the downloaded MSI package is not matching one of the'
+ . ' provided hashes of this setting'
+ ),
+ 'value' => $hashes,
+ ]);
+
+ $this->addElement('text', 'icinga_service_user', [
+ 'label' => $this->translate('Service User'),
+ 'description' => $this->translate(
+ 'The user that should run the Icinga 2 service on Windows.'
+ ),
+ 'value' => $settings->getStoredOrDefaultValue('self-service/icinga_service_user'),
+ ]);
+
+ $this->addEventuallyConfiguredBoolean('allow_updates', [
+ 'label' => $this->translate('Allow Updates'),
+ 'description' => $this->translate(
+ 'In case the Icinga 2 Agent is already installed on the system,'
+ . ' this parameter will allow you to configure if you wish to'
+ . ' upgrade / downgrade to a specified version with the as well.'
+ ),
+ 'required' => true,
+ ]);
+
+ $this->addNscpSettings();
+ }
+
+ protected function addNscpSettings()
+ {
+ $this->addEventuallyConfiguredBoolean('install_nsclient', [
+ 'label' => $this->translate('Install NSClient++'),
+ 'description' => $this->translate(
+ 'Also install NSClient++. It can be used through the Icinga Agent'
+ . ' and comes with a bunch of additional Check Plugins'
+ ),
+ 'required' => true,
+ ]);
+ /*
+ * TODO: eventually add those:
+ if ($settings->get('self-service/install_nsclient') === 'y') {
+ $params['install_nsclient'] = true;
+ $this->addBooleanSettingsToParams($settings, [
+ 'nsclient_add_defaults',
+ 'nsclient_firewall',
+ 'nsclient_service',
+ ], $params);
+
+
+ $this->addStringSettingsToParams($settings, [
+ 'nsclient_directory',
+ 'nsclient_installer_path'
+ ], $params);
+ }
+ */
+ }
+
+ public static function create(Db $db, Settings $settings)
+ {
+ return static::load()->setDb($db)->setSettings($settings);
+ }
+
+ protected function addEventuallyConfiguredBoolean($name, $params)
+ {
+ $key = "self-service/$name";
+ $value = $this->settings->getStoredValue($key);
+ $params['value'] = $value;
+ $params['multiOptions'] = $this->eventuallyConfiguredEnum($name, [
+ 'y' => $this->translate('Yes'),
+ 'n' => $this->translate('No'),
+ ]);
+
+ return $this->addElement('select', $name, $params);
+ }
+
+ protected function eventuallyConfiguredEnum($name, $enum)
+ {
+ $key = "self-service/$name";
+ $default = $this->settings->getDefaultValue($key);
+ if ($default === null) {
+ return [
+ null => $this->translate('- please choose -')
+ ] + $enum;
+ } else {
+ return [
+ null => sprintf($this->translate('%s (default)'), $enum[$default])
+ ] + $enum;
+ }
+ }
+
+ protected function setSentValue($key, $value)
+ {
+ $this->getRequest()->setPost($key, $value);
+ return $this;
+ }
+
+ protected function enumGlobalZones()
+ {
+ $db = $this->getDb()->getDbAdapter();
+ $zones = $db->fetchCol(
+ $db->select()->from('icinga_zone', 'object_name')
+ ->where('disabled = ?', 'n')
+ ->where('is_global = ?', 'y')
+ ->order('object_name')
+ );
+
+ return array_combine($zones, $zones);
+ }
+
+ public function setSettings(Settings $settings)
+ {
+ $this->settings = $settings;
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ try {
+ foreach ($this->getValues() as $key => $value) {
+ if ($value === '') {
+ $value = null;
+ }
+
+ $this->settings->set("self-service/$key", $value);
+ }
+
+ $this->setSuccessMessage($this->translate(
+ 'Self Service Settings have been stored'
+ ));
+
+ parent::onSuccess();
+ } catch (Exception $e) {
+ $this->addException($e);
+ }
+ }
+}
diff --git a/application/forms/SettingsForm.php b/application/forms/SettingsForm.php
new file mode 100644
index 0000000..f6ba654
--- /dev/null
+++ b/application/forms/SettingsForm.php
@@ -0,0 +1,238 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Module\Director\Settings;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class SettingsForm extends DirectorForm
+{
+ /** @var Settings */
+ protected $settings;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $settings = $this->settings;
+
+ $this->addHtmlHint(
+ $this->translate(
+ 'Please only change those settings in case you are really sure'
+ . ' that you are required to do so. Usually the defaults chosen'
+ . ' by the Icinga Director should make a good fit for your'
+ . ' environment.'
+ )
+ );
+ $globalZones = $this->eventuallyConfiguredEnum('default_global_zone', $this->enumGlobalZones());
+
+ $this->addElement('select', 'default_global_zone', array(
+ 'label' => $this->translate('Default global zone'),
+ 'multiOptions' => $globalZones,
+ 'description' => $this->translate(
+ 'Icinga Director decides to deploy objects like CheckCommands'
+ . ' to a global zone. This defaults to "director-global" but'
+ . ' might be adjusted to a custom Zone name'
+ ),
+ 'value' => $settings->getStoredValue('default_global_zone')
+ ));
+
+ $this->addElement('text', 'icinga_package_name', array(
+ 'label' => $this->translate('Icinga Package Name'),
+ 'description' => $this->translate(
+ 'The Icinga Package name Director uses to deploy it\'s configuration.'
+ . ' This defaults to "director" and should not be changed unless'
+ . ' you really know what you\'re doing'
+ ),
+ 'placeholder' => $settings->get('icinga_package_name'),
+ 'value' => $settings->getStoredValue('icinga_package_name')
+ ));
+
+ $this->addElement('select', 'disable_all_jobs', array(
+ 'label' => $this->translate('Disable all Jobs'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'disable_all_jobs',
+ array(
+ 'n' => $this->translate('No'),
+ 'y' => $this->translate('Yes'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'Whether all configured Jobs should be disabled'
+ ),
+ 'value' => $settings->getStoredValue('disable_all_jobs')
+ ));
+
+ $this->addElement('select', 'enable_audit_log', array(
+ 'label' => $this->translate('Enable audit log'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'enable_audit_log',
+ array(
+ 'n' => $this->translate('No'),
+ 'y' => $this->translate('Yes'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'All changes are tracked in the Director database. In addition'
+ . ' you might also want to send an audit log through the Icinga'
+ . " Web 2 logging mechanism. That way all changes would be"
+ . ' written to either Syslog or the configured log file. When'
+ . ' enabling this please make sure that you configured Icinga'
+ . ' Web 2 to log at least at "informational" level.'
+ ),
+ 'value' => $settings->getStoredValue('enable_audit_log')
+ ));
+
+ if ($settings->getStoredValue('ignore_bug7530')) {
+ // Show this only for those who touched this setting
+ $this->addElement('select', 'ignore_bug7530', array(
+ 'label' => $this->translate('Ignore Bug #7530'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'ignore_bug7530',
+ array(
+ 'n' => $this->translate('No'),
+ 'y' => $this->translate('Yes'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'Icinga v2.11.0 breaks some configurations, the Director will'
+ . ' warn you before every deployment in case your config is'
+ . ' affected. This setting allows to hide this warning.'
+ ),
+ 'value' => $settings->getStoredValue('ignore_bug7530')
+ ));
+ }
+
+ $this->addBoolean('feature_custom_endpoint', [
+ 'label' => $this->translate('Feature: Custom Endpoint Name'),
+ 'description' => $this->translate(
+ 'Enabled the feature for custom endpoint names,'
+ . ' where you can choose a different name for the generated endpoint object.'
+ . ' This uses some Icinga config snippets and a special custom variable.'
+ . ' Please do NOT enable this, unless you really need divergent endpoint names!'
+ ),
+ 'value' => $settings->getStoredValue('feature_custom_endpoint')
+ ]);
+
+
+ $this->addElement('select', 'config_format', array(
+ 'label' => $this->translate('Configuration format'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'config_format',
+ array(
+ 'v2' => $this->translate('Icinga v2.x'),
+ 'v1' => $this->translate('Icinga v1.x'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'Default configuration format. Please note that v1.x is for'
+ . ' special transitional projects only and completely'
+ . ' unsupported. There are no plans to make Director a first-'
+ . 'class configuration backends for Icinga 1.x'
+ ),
+ 'class' => 'autosubmit',
+ 'value' => $settings->getStoredValue('config_format')
+ ));
+
+ $this->setSubmitLabel($this->translate('Store'));
+
+ if ($this->hasBeenSent()) {
+ if ($this->getSentValue('config_format') !== 'v1') {
+ return;
+ }
+ } elseif ($settings->getStoredValue('config_format') !== 'v1') {
+ return;
+ }
+
+ $this->addElement('select', 'deployment_mode_v1', array(
+ 'label' => $this->translate('Deployment mode'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'deployment_mode_v1',
+ array(
+ 'active-passive' => $this->translate('Active-Passive'),
+ 'masterless' => $this->translate('Master-less'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'Deployment mode for Icinga 1 configuration'
+ ),
+ 'value' => $settings->getStoredValue('deployment_mode_v1')
+ ));
+
+ $this->addElement('text', 'deployment_path_v1', array(
+ 'label' => $this->translate('Deployment Path'),
+ 'description' => $this->translate(
+ 'Local directory to deploy Icinga 1.x configuration.'
+ . ' Must be writable by icingaweb2.'
+ . ' (e.g. /etc/icinga/director)'
+ ),
+ 'value' => $settings->getStoredValue('deployment_path_v1')
+ ));
+
+ $this->addElement('text', 'activation_script_v1', array(
+ 'label' => $this->translate('Activation Tool'),
+ 'description' => $this->translate(
+ 'Script or tool to call when activating a new configuration stage.'
+ . ' (e.g. sudo /usr/local/bin/icinga-director-activate)'
+ . ' (name of the stage will be the argument for the script)'
+ ),
+ 'value' => $settings->getStoredValue('activation_script_v1')
+ ));
+ }
+
+ protected function eventuallyConfiguredEnum($name, $enum)
+ {
+ if (array_key_exists($name, $enum)) {
+ $default = sprintf(
+ $this->translate('%s (default)'),
+ $enum[$this->settings->getDefaultValue($name)]
+ );
+ } else {
+ $default = $this->translate('- please choose -');
+ }
+
+ return [null => $default] + $enum;
+ }
+
+ public function setSettings(Settings $settings)
+ {
+ $this->settings = $settings;
+ return $this;
+ }
+
+ protected function enumGlobalZones()
+ {
+ $db = $this->settings->getDb();
+ $zones = $db->fetchCol(
+ $db->select()->from('icinga_zone', 'object_name')
+ ->where('disabled = ?', 'n')
+ ->where('is_global = ?', 'y')
+ ->order('object_name')
+ );
+
+ return array_combine($zones, $zones);
+ }
+
+ public function onSuccess()
+ {
+ try {
+ foreach ($this->getValues() as $key => $value) {
+ if ($value === '') {
+ $value = null;
+ }
+
+ $this->settings->set($key, $value);
+ }
+
+ $this->setSuccessMessage($this->translate(
+ 'Settings have been stored'
+ ));
+
+ parent::onSuccess();
+ } catch (Exception $e) {
+ $this->addError($e->getMessage());
+ }
+ }
+}
diff --git a/application/forms/SyncCheckForm.php b/application/forms/SyncCheckForm.php
new file mode 100644
index 0000000..8fb3bd0
--- /dev/null
+++ b/application/forms/SyncCheckForm.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\DirectorActivityLog;
+use Icinga\Module\Director\Objects\SyncRule;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class SyncCheckForm extends DirectorForm
+{
+ /** @var SyncRule */
+ protected $rule;
+
+ public function setSyncRule(SyncRule $rule)
+ {
+ $this->rule = $rule;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->submitLabel = false;
+ $this->addElement('submit', 'submit', array(
+ 'label' => $this->translate('Check for changes'),
+ 'decorators' => array('ViewHelper')
+ ));
+ }
+
+ public function onSuccess()
+ {
+ if ($this->rule->checkForChanges()) {
+ $this->notifySuccess(
+ $this->translate(('This Sync Rule would apply new changes'))
+ );
+ $sum = [
+ DirectorActivityLog::ACTION_CREATE => 0,
+ DirectorActivityLog::ACTION_MODIFY => 0,
+ DirectorActivityLog::ACTION_DELETE => 0
+ ];
+
+ // TODO: Preview them? Like "hosta, hostb and 4 more would be...
+ foreach ($this->rule->getExpectedModifications() as $object) {
+ if ($object->shouldBeRemoved()) {
+ $sum[DirectorActivityLog::ACTION_DELETE]++;
+ } elseif (! $object->hasBeenLoadedFromDb()) {
+ $sum[DirectorActivityLog::ACTION_CREATE]++;
+ } elseif ($object->hasBeenModified()) {
+ $sum[DirectorActivityLog::ACTION_MODIFY]++;
+ }
+ }
+
+ /**
+ if ($sum['modify'] === 1) {
+ $html .= $this->translate('One object would be modified'
+ } elseif ($sum['modify'] > 1) {
+ }
+ */
+ $html = '<pre>' . print_r($sum, 1) . '</pre>';
+
+ $this->addHtml($html);
+ } elseif ($this->rule->get('sync_state') === 'in-sync') {
+ $this->notifySuccess(
+ $this->translate('Nothing would change, this rule is still in sync')
+ );
+ } else {
+ $this->addError($this->translate('Checking this sync rule failed'));
+ }
+ }
+}
diff --git a/application/forms/SyncPropertyForm.php b/application/forms/SyncPropertyForm.php
new file mode 100644
index 0000000..720237e
--- /dev/null
+++ b/application/forms/SyncPropertyForm.php
@@ -0,0 +1,444 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\SyncProperty;
+use Icinga\Module\Director\Objects\SyncRule;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class SyncPropertyForm extends DirectorObjectForm
+{
+ /**
+ * @var SyncRule
+ */
+ private $rule;
+
+ /** @var ImportSource */
+ private $importSource;
+
+ /** @var ImportSourceHook */
+ private $importSourceHook;
+
+ private $dummyObject;
+
+ const EXPRESSION = '__EXPRESSION__';
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('rule_id', $this->rule->get('id'));
+
+ $this->addElement('select', 'source_id', array(
+ 'label' => $this->translate('Source Name'),
+ 'multiOptions' => $this->enumImportSource(),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ ));
+ if (! $this->hasObject() && ! $this->getSentValue('source_id')) {
+ return;
+ }
+
+ $this->addElement('select', 'destination_field', array(
+ 'label' => $this->translate('Destination Field'),
+ 'multiOptions' => $this->optionalEnum($this->listDestinationFields()),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ ));
+
+ if ($this->getSentValue('destination_field')) {
+ $destination = $this->getSentValue('destination_field');
+ } elseif ($this->hasObject()) {
+ $destination = $this->getObject()->destination_field;
+ } else {
+ return;
+ }
+
+ $isCustomvar = substr($destination, 0, 5) === 'vars.';
+
+ if ($isCustomvar) {
+ $varname = substr($destination, 5);
+ $this->addElement('text', 'customvar', array(
+ 'label' => $this->translate('Custom variable'),
+ 'required' => true,
+ 'ignore' => true,
+ ));
+
+ if ($varname !== '*') {
+ $this->setElementValue('destination_field', 'vars.*');
+ $this->setElementValue('customvar', $varname);
+ if ($this->hasObject()) {
+ $this->getObject()->destination_field = 'vars.*';
+ }
+ }
+ }
+
+ $this->addSourceColumnElement($destination);
+
+ $this->addElement('YesNo', 'use_filter', array(
+ 'label' => $this->translate('Set based on filter'),
+ 'ignore' => true,
+ 'class' => 'autosubmit',
+ 'required' => true,
+ ));
+
+ if ($this->hasBeenSent()) {
+ $useFilter = $this->getSentValue('use_filter');
+ if ($useFilter === null) {
+ $this->setElementValue('use_filter', $useFilter = 'n');
+ }
+ } else {
+ $expression = $this->getObject()->filter_expression;
+ $useFilter = ($expression === null || strlen($expression) === 0) ? 'n' : 'y';
+ $this->setElementValue('use_filter', $useFilter);
+ }
+
+ if ($useFilter === 'y') {
+ $this->addElement('text', 'filter_expression', array(
+ 'label' => $this->translate('Filter Expression'),
+ 'description' => $this->translate(
+ 'This allows to filter for specific parts within the given source expression.'
+ . ' You are allowed to refer all imported columns. Examples: host=www* would'
+ . ' set this property only for rows imported with a host property starting'
+ . ' with "www". Complex example: host=www*&!(address=127.*|address6=::1)'
+ ),
+ 'required' => true,
+ // TODO: validate filter
+ ));
+ }
+
+ if ($isCustomvar || $destination === 'vars') {
+ $this->addElement('select', 'merge_policy', array(
+ 'label' => $this->translate('Merge Policy'),
+ 'description' => $this->translate(
+ 'Whether you want to merge or replace the destination field.'
+ . ' Makes no difference for strings'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum(array(
+ 'merge' => 'merge',
+ 'override' => 'replace'
+ ))
+ ));
+ } else {
+ $this->addHidden('merge_policy', 'override');
+ }
+
+ $this->setButtons();
+ }
+
+ protected function hasSubOption($options, $key)
+ {
+ foreach ($options as $mainKey => $sub) {
+ if (! is_array($sub)) {
+ // null -> please choose - or similar
+ continue;
+ }
+
+ if (array_key_exists($key, $sub)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $destination
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addSourceColumnElement($destination)
+ {
+ $error = false;
+
+ $srcTitle = $this->translate('Source columns');
+ try {
+ $columns[$srcTitle] = $this->listSourceColumns();
+ natsort($columns[$srcTitle]);
+ } catch (Exception $e) {
+ $srcTitle .= sprintf(' (%s)', $this->translate('failed to fetch'));
+ $columns[$srcTitle] = array();
+ $error = sprintf(
+ $this->translate('Unable to fetch data: %s'),
+ $e->getMessage()
+ );
+ }
+
+ if ($destination === 'import') {
+ $this->addIcingaTempateColumns($columns);
+ } elseif ($destination === 'list_id') {
+ $this->addDatalistsColumns($columns);
+ }
+
+ $xpTitle = $this->translate('Expert mode');
+ $columns[$xpTitle][self::EXPRESSION] = $this->translate('Custom expression');
+
+ $this->addElement('select', 'source_column', array(
+ 'label' => $this->translate('Source Column'),
+ 'multiOptions' => $this->optionalEnum($columns),
+ 'required' => true,
+ 'ignore' => true,
+ 'class' => 'autosubmit',
+ ));
+
+ if ($error) {
+ $this->getElement('source_column')->addError($error);
+ }
+
+ $showExpression = false;
+
+ if ($this->hasBeenSent()) {
+ $sentValue = $this->getSentValue('source_column');
+ if ($sentValue === self::EXPRESSION) {
+ $showExpression = true;
+ }
+ } elseif ($this->hasObject()) {
+ $objectValue = $this->getObject()->source_expression;
+ if ($this->hasSubOption($columns, $objectValue)) {
+ $this->setElementValue('source_column', $objectValue);
+ } else {
+ $this->setElementValue('source_column', self::EXPRESSION);
+ $showExpression = true;
+ }
+ }
+
+ if ($showExpression) {
+ $this->addElement('text', 'source_expression', array(
+ 'label' => $this->translate('Source Expression'),
+ 'description' => $this->translate(
+ 'A custom string. Might contain source columns, please use placeholders'
+ . ' of the form ${columnName} in such case. Structured data sources'
+ . ' can be referenced as ${columnName.sub.key}'
+ ),
+ 'required' => true,
+ ));
+ }
+
+
+ return $this;
+ }
+
+ protected function addIcingaTempateColumns(&$columns)
+ {
+ $funcTemplates = 'enum' . ucfirst($this->rule->get('object_type')) . 'Templates';
+ if (method_exists($this->db, $funcTemplates)) {
+ $templates = $this->db->$funcTemplates();
+ if (! empty($templates)) {
+ $templates = array_combine($templates, $templates);
+ }
+
+ $title = $this->translate('Existing templates');
+ $columns[$title] = $templates;
+ natsort($columns[$title]);
+ }
+ }
+
+ protected function addDatalistsColumns(&$columns)
+ {
+ // Clear other columns, we don't allow them right now
+ $columns = [];
+ $db = $this->db->getDbAdapter();
+ $enum = $db->fetchPairs(
+ $db->select()->from('director_datalist', ['id', 'list_name'])->order('list_name')
+ );
+
+ $columns[$this->translate('Existing Data Lists')] = $enum;
+ }
+
+ protected function enumImportSource()
+ {
+ $sources = $this->db->enumImportSource();
+ $usedIds = $this->rule->listInvolvedSourceIds();
+ if (empty($usedIds)) {
+ return $this->optionalEnum($sources);
+ }
+ $usedSources = array();
+ foreach ($usedIds as $id) {
+ $usedSources[$id] = $sources[$id];
+ unset($sources[$id]);
+ }
+
+ if (empty($sources)) {
+ return $this->optionalEnum($usedSources);
+ }
+
+ return $this->optionalEnum(
+ array(
+ $this->translate('Used sources') => $usedSources,
+ $this->translate('Other sources') => $sources
+ )
+ );
+ }
+
+ /**
+ * @return array
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function listSourceColumns()
+ {
+ $columns = array();
+ $source = $this->getImportSource();
+ $hook = $this->getImportSourceHook();
+ foreach ($hook->listColumns() as $col) {
+ $columns['${' . $col . '}'] = $col;
+ }
+
+ foreach ($source->listModifierTargetProperties() as $property) {
+ $columns['${' . $property . '}'] = $property;
+ }
+
+ return $columns;
+ }
+
+ protected function listDestinationFields()
+ {
+ $props = [];
+ $special = [];
+ $dummy = $this->dummyObject();
+
+ if ($dummy instanceof IcingaObject) {
+ if ($dummy->supportsCustomVars()) {
+ $special['vars.*'] = $this->translate('Custom variable (vars.)');
+ $special['vars'] = $this->translate('All custom variables (vars)');
+ }
+ if ($dummy->supportsImports()) {
+ $special['import'] = $this->translate('Inheritance (import)');
+ }
+ if ($dummy->supportsArguments()) {
+ $special['arguments'] = $this->translate('Arguments');
+ }
+ if ($dummy->supportsGroups()) {
+ $special['groups'] = $this->translate('Group membership');
+ }
+ if ($dummy->supportsRanges()) {
+ $special['ranges'] = $this->translate('Time ranges');
+ }
+ }
+
+ foreach ($dummy->listProperties() as $prop) {
+ if ($dummy instanceof IcingaObject && $prop === 'id') {
+ continue;
+ }
+
+ // TODO: allow those fields, but munge them (store ids)
+ //if (preg_match('~_id$~', $prop)) continue;
+ if (substr($prop, -3) === '_id') {
+ $short = substr($prop, 0, -3);
+ if ($dummy instanceof IcingaObject) {
+ if ($dummy->hasRelation($short)) {
+ $prop = $short;
+ } else {
+ continue;
+ }
+ }
+ }
+
+ $props[$prop] = $prop;
+ }
+
+ if ($dummy instanceof IcingaObject) {
+ foreach ($dummy->listMultiRelations() as $prop) {
+ $props[$prop] = sprintf('%s (%s)', $prop, $this->translate('a list'));
+ }
+ }
+
+ ksort($props);
+
+ $result = [];
+ if (! empty($special)) {
+ $result[$this->translate('Special properties')] = $special;
+ }
+ if (! empty($props)) {
+ $result[$this->translate('Object properties')] = $props;
+ }
+
+ return $result;
+ }
+
+ /**
+ * @return ImportSource
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getImportSource()
+ {
+ if ($this->importSource === null) {
+ if ($this->hasObject()) {
+ $id = (int) $this->object->get('source_id');
+ } else {
+ $id = (int) $this->getSentValue('source_id');
+ }
+ $this->importSource = ImportSource::loadWithAutoIncId($id, $this->db);
+ }
+
+ return $this->importSource;
+ }
+
+ /**
+ * @return ImportSourceHook
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getImportSourceHook()
+ {
+ if ($this->importSourceHook === null) {
+ $this->importSourceHook = ImportSourceHook::loadByName(
+ $this->getImportSource()->get('source_name'),
+ $this->db
+ );
+ }
+
+ return $this->importSourceHook;
+ }
+
+ public function onSuccess()
+ {
+ /** @var SyncProperty $object */
+ $object = $this->getObject();
+ $object->set('rule_id', $this->rule->get('id')); // ?!
+
+ if ($this->getValue('use_filter') === 'n') {
+ $object->set('filter_expression', null);
+ }
+
+ $sourceColumn = $this->getValue('source_column');
+ $this->removeElement('source_column');
+
+ if ($sourceColumn !== self::EXPRESSION) {
+ $object->set('source_expression', $sourceColumn);
+ }
+
+ $destination = $this->getValue('destination_field');
+ if ($destination === 'vars.*') {
+ $destination = $this->getValue('customvar');
+ $object->set('destination_field', 'vars.' . $destination);
+ }
+
+ return parent::onSuccess();
+ }
+
+ protected function dummyObject()
+ {
+ if ($this->dummyObject === null) {
+ $this->dummyObject = IcingaObject::createByType(
+ $this->rule->get('object_type'),
+ array(),
+ $this->db
+ );
+ }
+
+ return $this->dummyObject;
+ }
+
+ public function setRule(SyncRule $rule)
+ {
+ $this->rule = $rule;
+ return $this;
+ }
+}
diff --git a/application/forms/SyncRuleForm.php b/application/forms/SyncRuleForm.php
new file mode 100644
index 0000000..d88e493
--- /dev/null
+++ b/application/forms/SyncRuleForm.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class SyncRuleForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $availableTypes = [
+ 'host' => $this->translate('Host'),
+ 'hostgroup' => $this->translate('Host Group'),
+ 'service' => $this->translate('Service'),
+ 'servicegroup' => $this->translate('Service Group'),
+ 'serviceSet' => $this->translate('Service Set'),
+ 'user' => $this->translate('User'),
+ 'usergroup' => $this->translate('User Group'),
+ 'datalistEntry' => $this->translate('Data List Entry'),
+ 'command' => $this->translate('Command'),
+ 'timePeriod' => $this->translate('Time Period'),
+ 'notification' => $this->translate('Notification'),
+ 'scheduledDowntime' => $this->translate('Scheduled Downtime'),
+ 'dependency' => $this->translate('Dependency'),
+ 'endpoint' => $this->translate('Endpoint'),
+ 'zone' => $this->translate('Zone'),
+ ];
+
+ $this->addElement('text', 'rule_name', [
+ 'label' => $this->translate('Rule name'),
+ 'description' => $this->translate('Please provide a rule name'),
+ 'required' => true,
+ ]);
+
+ $this->addElement('textarea', 'description', [
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'An extended description for this Sync Rule. This should explain'
+ . ' what this Rule is going to accomplish.'
+ ),
+ 'rows' => '3',
+ ]);
+
+ $this->addElement('select', 'object_type', [
+ 'label' => $this->translate('Object Type'),
+ 'description' => $this->translate('Choose an object type'),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($availableTypes)
+ ]);
+
+ $this->addElement('select', 'update_policy', [
+ 'label' => $this->translate('Update Policy'),
+ 'description' => $this->translate(
+ 'Define what should happen when an object with a matching key'
+ . " already exists. You could merge its properties (import source"
+ . ' wins), replace it completely with the imported object or ignore'
+ . ' it (helpful for one-time imports). "Update only" means that this'
+ . ' Rule would never create (or delete) full Objects.'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum([
+ 'merge' => $this->translate('Merge'),
+ 'override' => $this->translate('Replace'),
+ 'ignore' => $this->translate('Ignore'),
+ 'update-only' => $this->translate('Update only'),
+ ])
+ ]);
+
+ $this->addBoolean('purge_existing', [
+ 'label' => $this->translate('Purge'),
+ 'description' => $this->translate(
+ 'Whether to purge existing objects. This means that objects of'
+ . ' the same type will be removed from Director in case they no'
+ . ' longer exist at your import source.'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ ]);
+
+ if ($this->getSentOrObjectValue('purge_existing') === 'y') {
+ $this->addElement('select', 'purge_action', [
+ 'label' => $this->translate('Purge Action'),
+ 'description' => $this->translate(
+ 'Whether to delete or to disable objects subject to purge'
+ ),
+ 'multiOptions' => $this->optionalEnum([
+ 'delete' => $this->translate('Delete'),
+ 'disable' => $this->translate('Disable'),
+ ]),
+ 'required' => true,
+ ]);
+ }
+
+ $this->addElement('text', 'filter_expression', [
+ 'label' => $this->translate('Filter Expression'),
+ 'description' => sprintf(
+ $this->translate(
+ 'Sync only part of your imported objects with this rule. Icinga Web 2'
+ . ' filter syntax is allowed, so this could look as follows: %s'
+ ),
+ '(host=a|host=b)&!ip=127.*'
+ ) . ' ' . $this->translate(
+ 'Be careful: this is usually NOT what you want, as it makes Sync "blind"'
+ . ' for objects matching this filter. This means that "Purge" will not'
+ . ' work as expected. The "Black/Whitelist" Import Property Modifier'
+ . ' is probably what you\'re looking for.'
+ ),
+ ]);
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/SyncRunForm.php b/application/forms/SyncRunForm.php
new file mode 100644
index 0000000..0bc5fda
--- /dev/null
+++ b/application/forms/SyncRunForm.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\Web\Form;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Import\Sync;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class SyncRunForm extends Form
+{
+ use TranslationHelper;
+
+ protected $defaultDecoratorClass = null;
+
+ /** @var ?string */
+ protected $successMessage = null;
+
+ /** @var SyncRule */
+ protected $rule;
+
+ /** @var DbObjectStore */
+ protected $store;
+
+ public function __construct(SyncRule $rule, DbObjectStore $store)
+ {
+ $this->rule = $rule;
+ $this->store = $store;
+ }
+
+ public function assemble()
+ {
+ if ($this->store->getBranch()->isBranch()) {
+ $label = sprintf($this->translate('Sync to Branch: %s'), $this->store->getBranch()->getName());
+ } else {
+ $label = $this->translate('Trigger this Sync');
+ }
+ $this->addElement('submit', 'submit', [
+ 'label' => $label,
+ ]);
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSuccessMessage()
+ {
+ return $this->successMessage;
+ }
+
+ public function onSuccess()
+ {
+ $sync = new Sync($this->rule, $this->store);
+ if ($sync->hasModifications()) {
+ if ($sync->apply()) {
+ // and changed
+ $this->successMessage = $this->translate(('Source has successfully been synchronized'));
+ } else {
+ $this->successMessage = $this->translate('Nothing changed, rule is in sync');
+ }
+ } else {
+ // Used to be $rule->get('sync_state') === 'in-sync', $changed = $rule->applyChanges();
+ $this->successMessage = $this->translate('Nothing to do, rule is in sync');
+ }
+ }
+}
diff --git a/application/locale/de_DE/LC_MESSAGES/director.mo b/application/locale/de_DE/LC_MESSAGES/director.mo
new file mode 100644
index 0000000..3cf12af
--- /dev/null
+++ b/application/locale/de_DE/LC_MESSAGES/director.mo
Binary files differ
diff --git a/application/locale/de_DE/LC_MESSAGES/director.po b/application/locale/de_DE/LC_MESSAGES/director.po
new file mode 100644
index 0000000..ec2fb2a
--- /dev/null
+++ b/application/locale/de_DE/LC_MESSAGES/director.po
@@ -0,0 +1,8230 @@
+# Director - Config tool for Icinga 2.
+# Copyright (C) 2022 TEAM NAME
+# This file is distributed under the same license as Director Module.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Director Module (master)\n"
+"Report-Msgid-Bugs-To: https://github.com/Icinga/icingaweb2-module-director/issues\n"
+"POT-Creation-Date: 2022-09-21 07:17+0000\n"
+"PO-Revision-Date: 2022-09-21 09:21+0200\n"
+"Last-Translator: Thomas Gelf <thomas@gelf.net>\n"
+"Language-Team: \n"
+"Language: de_DE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-Basepath: .\n"
+"X-Generator: Poedit 3.0.1\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:615
+#, php-format
+msgid " (inherited from \"%s\")"
+msgstr " (geerbt von \"%s\")"
+
+#: library/Director/Web/Table/TemplatesTable.php:64
+msgid " - not in use -"
+msgstr " - unbenutzt -"
+
+#: library/Director/Web/Table/DatafieldCategoryTable.php:48
+#: library/Director/Web/Table/DatafieldTable.php:52
+msgid "# Used"
+msgstr "# Benutzt"
+
+#: library/Director/Web/Table/DatafieldTable.php:53
+msgid "# Vars"
+msgstr "# Vars"
+
+#: library/Director/Web/Table/CustomvarTable.php:29
+#, php-format
+msgid "%d / %d"
+msgstr "%d / %d"
+
+#: library/Director/Resolver/CommandUsage.php:51
+#, php-format
+msgid "%d Host Template(s)"
+msgstr "%d Host-Vorlagen"
+
+#: library/Director/Resolver/CommandUsage.php:50
+#, php-format
+msgid "%d Host(s)"
+msgstr "%d Host(s)"
+
+#: library/Director/Resolver/CommandUsage.php:61
+#, php-format
+msgid "%d Notification Apply Rule(s)"
+msgstr "%d Benachrichtigungs-Apply-Regeln"
+
+#: library/Director/Resolver/CommandUsage.php:60
+#, php-format
+msgid "%d Notification Template(s)"
+msgstr "%d Benachrichtigungsvorlagen"
+
+#: library/Director/Resolver/CommandUsage.php:59
+#, php-format
+msgid "%d Notification(s)"
+msgstr "%d Benachrichtigung(en)"
+
+#: library/Director/Resolver/CommandUsage.php:56
+#, php-format
+msgid "%d Service Apply Rule(s)"
+msgstr "%d Service Apply-Regel(n)"
+
+#: library/Director/Resolver/CommandUsage.php:55
+#, php-format
+msgid "%d Service Template(s)"
+msgstr "%d Service-Vorlage(n)"
+
+#: library/Director/Resolver/CommandUsage.php:54
+#, php-format
+msgid "%d Service(s)"
+msgstr "%d Service(s)"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:170
+#, php-format
+msgid "%d apply rules have been defined"
+msgstr "%d Apply-Regeln wurden definiert"
+
+#: library/Director/Db/Branch/BranchModificationInspection.php:68
+#: library/Director/Web/Table/SyncRunTable.php:49
+#: library/Director/Web/Widget/SyncRunDetails.php:77
+#, php-format
+msgid "%d created"
+msgstr "%d erstellt"
+
+#: library/Director/Db/Branch/BranchModificationInspection.php:71
+#: library/Director/Web/Table/SyncRunTable.php:61
+#: library/Director/Web/Widget/SyncRunDetails.php:89
+#, php-format
+msgid "%d deleted"
+msgstr "%d gelöscht"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:110
+#, php-format
+msgid "%d files"
+msgstr "%d Dateien"
+
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:93
+#, php-format
+msgid "%d files rendered in %0.2fs"
+msgstr "%d Dateien in %0.2fs erstellt"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:212
+#, php-format
+msgid "%d have been externally defined and will not be deployed"
+msgstr "%d wurden extern definiert und werden nicht ausgerollt"
+
+#: library/Director/Db/Branch/BranchModificationInspection.php:74
+#: library/Director/Web/Table/SyncRunTable.php:55
+#: library/Director/Web/Widget/SyncRunDetails.php:83
+#, php-format
+msgid "%d modified"
+msgstr "%d geändert"
+
+#: application/controllers/SyncruleController.php:353
+#, php-format
+msgid "%d object(s) will be created"
+msgstr "%d Objekt(e) werden erstellt"
+
+#: application/controllers/SyncruleController.php:334
+#, php-format
+msgid "%d object(s) will be deleted"
+msgstr "%d Objekt(e) werden gelöscht"
+
+#: application/controllers/SyncruleController.php:343
+#, php-format
+msgid "%d object(s) will be modified"
+msgstr "%d Objekt(e) werden verändert"
+
+#: application/controllers/InspectController.php:76
+#, php-format
+msgid "%d objects found"
+msgstr "%d Objekte gefunden"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:195
+#, php-format
+msgid "%d objects have been defined"
+msgstr "%d Objekte wurden definiert"
+
+#: application/forms/IcingaMultiEditForm.php:93
+#, php-format
+msgid "%d objects have been modified"
+msgstr "%d Objekte wurden verändert"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:119
+#, php-format
+msgid "%d objects, %d templates, %d apply rules"
+msgstr "%d Objekte, %d Vorlagen, %d Apply-Regeln"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:204
+#, php-format
+msgid "%d of them are templates"
+msgstr "%d davon sind Vorlagen"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:152
+#, php-format
+msgid "%d templates have been defined"
+msgstr "%d Vorlagen wurden definiert"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:581
+#, php-format
+msgid "%s \"%s\" has been created"
+msgstr "%s \"%s\" wurde erstellt"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:584
+#, php-format
+msgid "%s \"%s\" has been deleted"
+msgstr "%s \"%s\" wurde gelöscht"
+
+#: application/forms/IcingaImportObjectForm.php:36
+#, php-format
+msgid "%s \"%s\" has been imported\""
+msgstr "%s \"%s\" wurde importiert\""
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:587
+#, php-format
+msgid "%s \"%s\" has been modified"
+msgstr "%s \"%s\" wurde geändert"
+
+#: library/Director/Web/Table/IcingaHostAppliedServicesTable.php:107
+#, php-format
+msgid "%s %s(%s)"
+msgstr "%s %s(%s)"
+
+#: library/Director/Web/Table/ObjectSetTable.php:61
+#, php-format
+msgid "%s (%d members)"
+msgstr "%s (%d Mitglieder)"
+
+#: application/controllers/HostController.php:248
+#: application/controllers/HostController.php:328
+#, php-format
+msgid "%s (Applied Service set)"
+msgstr "%s (zugewiesenes Service-Set)"
+
+#: application/controllers/HostController.php:384
+#, php-format
+msgid "%s (Service set)"
+msgstr "%s (Service-Set)"
+
+#: application/forms/SettingsForm.php:189
+#: application/forms/SelfServiceSettingsForm.php:256
+#, php-format
+msgid "%s (default)"
+msgstr "%s (default)"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:312
+#, php-format
+msgid "%s (inherited from %s)"
+msgstr "%s (geerbt von %s)"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:307
+#, php-format
+msgid "%s (inherited)"
+msgstr "%s (geerbt)"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:325
+#, php-format
+msgid "%s (not an Array!)"
+msgstr "%s (kein Array)"
+
+#: library/Director/Web/Controller/TemplateController.php:72
+#, php-format
+msgid "%s Templates"
+msgstr "%s Vorlagen"
+
+#: application/forms/IcingaCommandArgumentForm.php:137
+#, php-format
+msgid "%s argument \"%s\" has been removed"
+msgstr "Das %s-Argument \"%s\" wurde entfernt"
+
+#: library/Director/Web/Controller/TemplateController.php:37
+#, php-format
+msgid "%s based on %s"
+msgstr "%s basierend auf %s"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:114
+#, php-format
+msgid "%s config changes are available in your configuration branch"
+msgstr ""
+"%s Konfigurationsänderungen sind in diesem Konfigurationszweig verfügbar"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:129
+#, php-format
+msgid "%s config changes happend since the last deployed configuration"
+msgstr ""
+"%s Konfigurationsänderungen seit der letzten ausgerollten Konfiguration"
+
+#: application/forms/IcingaServiceForm.php:286
+#, php-format
+msgid "%s has been deactivated on %s"
+msgstr "%s wurde auf %s deaktiviert"
+
+#: application/controllers/DataController.php:354
+#, php-format
+msgid "%s instances"
+msgstr "%s Instanzen"
+
+#: application/forms/IcingaServiceForm.php:322
+#, php-format
+msgid "%s is no longer deactivated on %s"
+msgstr "%s ist auf %s nicht mehr deaktiviert"
+
+#: library/Director/Web/Widget/NotInBranchedHint.php:18
+#, php-format
+msgid "%s is not available while being in a Configuration Branch: %s"
+msgstr "%s ist beim Arbeiten in einem Konfigurationszweig nicht verfügbar: %s"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:49
+#, php-format
+msgid "%s objects have been modified"
+msgstr "%s Objekte wurden verändert"
+
+#: application/controllers/DataController.php:359
+#, php-format
+msgid "%s on %s"
+msgstr "%s auf %s"
+
+#: application/controllers/HostController.php:542
+#, php-format
+msgid "%s on %s (from set: %s)"
+msgstr "%s auf %s (aus dem Set \"%s\")"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:227
+#, php-format
+msgid "%s related group objects have been created"
+msgstr "%s verwandte Gruppenobjekte wurden erstellt"
+
+#: library/Director/Web/Controller/TemplateController.php:75
+#, php-format
+msgid "%s templates based on %s"
+msgstr "%s Vorlagen basierend auf %s"
+
+#: library/Director/Web/Widget/HealthCheckPluginOutput.php:46
+#, php-format
+msgid "%s: %d"
+msgstr "%s: %d"
+
+#: application/controllers/BasketController.php:195
+#, php-format
+msgid "%s: %s (Snapshot)"
+msgstr "%s: %s (Snapshot)"
+
+#: application/controllers/ImportsourceController.php:310
+#, php-format
+msgid "%s: Property Modifier"
+msgstr "%s: Eigenschaftsmodifikatoren"
+
+#: application/controllers/BasketController.php:146
+#, php-format
+msgid "%s: Snapshots"
+msgstr "%s: Snapshots"
+
+#: application/controllers/ImportsourceController.php:280
+#, php-format
+msgid "%s: add Property Modifier"
+msgstr "%s: Eigenschaftsmodifikator hinzufügen"
+
+#: library/Director/Db/Housekeeping.php:54
+msgid "(Host) group resolve cache"
+msgstr "Gruppenmitgliedschaftscache (Hosts)"
+
+#: application/controllers/ServiceController.php:187
+#, php-format
+msgid "(on %s)"
+msgstr "(auf %s)"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:243
+msgid "- add more -"
+msgstr "- mehr hinzuzufügen -"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1090
+msgid "- click to add more -"
+msgstr "- Hier klicken um mehr hinzuzufügen -"
+
+#: application/forms/SelfServiceSettingsForm.php:79
+msgid "- no automatic installation -"
+msgstr "- keine automatische Installation -"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:45
+#: library/Director/DataType/DataTypeSqlQuery.php:37
+#: library/Director/Job/ImportJob.php:118
+#: library/Director/Job/SyncJob.php:124
+#: library/Director/Web/Form/QuickBaseForm.php:119
+#: application/views/helpers/FormDataFilter.php:465
+#: application/forms/SettingsForm.php:193
+#: application/forms/SelfServiceSettingsForm.php:252
+#: application/controllers/ConfigController.php:383
+#: application/controllers/ConfigController.php:394
+msgid "- please choose -"
+msgstr "- bitte wählen -"
+
+#: application/controllers/SyncruleController.php:455
+#, php-format
+msgid "...and %d more"
+msgstr "...und %d weitere"
+
+#: library/Director/Web/Controller/ObjectController.php:730
+#: application/controllers/ConfigController.php:452
+msgid "...and the modifications below are already in the main branch:"
+msgstr "...und folgende Änderungen befinden sich bereits im Hauptzweig:"
+
+#: application/controllers/BasketsController.php:34
+msgid ""
+"A Configuration Basket references specific Configuration Objects or all "
+"objects of a specific type. It has been designed to share Templates, Import/"
+"Sync strategies and other base Configuration Objects. It is not a tool to "
+"operate with single Hosts or Services."
+msgstr ""
+"Ein Konfiguratinsbasket referenziert bestimmte Konfigurationsobjekte oder "
+"alle Objekte eines bestimmten Typs. Er wurde entworfen um Vorlagen, Import/"
+"Synchronisationsstrategien und andere grundlegende Konfigurationsobjekte zu "
+"teilen. Dies ist kein Werkzeug welches sich um einzelne Hosts oder Services "
+"kümmert."
+
+#: library/Director/Import/ImportSourceLdap.php:64
+msgid ""
+"A custom LDAP filter to use in addition to the object class. This allows for "
+"a lot of flexibility but requires LDAP filter skills. Simple filters might "
+"look as follows: operatingsystem=*server*"
+msgstr ""
+"Ein benutzerdefinierter LDAP Filter zur gemeinsamen Verwendung mit der "
+"Objektklasse. Dies gewährt ein großes Maß an Flexibilität, benötigt jedoch "
+"Kenntnisse über LDAP Filter. Einfache Filter können wie folgt aussehen: "
+"operatingsystem=*server*"
+
+#: application/forms/SyncPropertyForm.php:215
+msgid ""
+"A custom string. Might contain source columns, please use placeholders of "
+"the form ${columnName} in such case. Structured data sources can be "
+"referenced as ${columnName.sub.key}"
+msgstr ""
+"Benutzerdefinierte Zeichenkette. Falls Quellspalten enthalten sind, müssen "
+"Platzhalter in der Form ${columnName} genutzt werden. Strukturierte Daten "
+"können mittels ${columnName.sub.key} addressiert werden"
+
+#: application/forms/IcingaObjectFieldForm.php:134
+msgid "A description about the field"
+msgstr "Eine Beschreibung des Feldes"
+
+#: application/forms/IcingaTemplateChoiceForm.php:59
+msgid "A detailled description explaining what this choice is all about"
+msgstr "Eine detaillierte Beschreibung zum Sinn und Zweck dieser Auswahl"
+
+#: application/forms/IcingaServiceSetForm.php:104
+msgid ""
+"A meaningful description explaining your users what to expect when assigning "
+"this set of services"
+msgstr ""
+"Eine aussagekräftige Beschreibung, die den Benutzern erklärt, was geschieht, "
+"wenn dieses Service-Set zugewiesen wird"
+
+# Geschlecht kann hier leider nicht bestimmt werden -ThW
+#: library/Director/Web/Form/DirectorObjectForm.php:662
+#: application/forms/IcingaTimePeriodRangeForm.php:85
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:90
+#, php-format
+msgid "A new %s has successfully been created"
+msgstr "Ein neuer %s wurde erfolgreich erstellt"
+
+#: application/forms/IcingaGenerateApiKeyForm.php:39
+#, php-format
+msgid "A new Self Service API key for %s has been generated"
+msgstr "Ein neuer Selbstbedienungs-API-Schlüssel für %s wurde erstellt"
+
+#: application/forms/ImportRowModifierForm.php:72
+msgid ""
+"A property modifier allows you to modify a specific property at import time"
+msgstr ""
+"Ein Eigenschaftsmodifikator erlaubt das Verändern bestimmter Eigenschaften "
+"während des Imports"
+
+#: application/forms/ImportSourceForm.php:17
+msgid ""
+"A short name identifying this import source. Use something meaningful, like "
+"\"Hosts from Puppet\", \"Users from Active Directory\" or similar"
+msgstr ""
+"Eine kurzer Name um diese Importquelle zu bezeichnen. Sprechende "
+"Bezeichnungen wie \"Hosts aus Puppet\" oder \"Benutzer aus LDAP\" bieten "
+"sich an"
+
+#: application/forms/DirectorJobForm.php:74
+msgid ""
+"A short name identifying this job. Use something meaningful, like \"Import "
+"Puppet Hosts\""
+msgstr ""
+"Eine kurzer Name um diesen Auftrag zu bezeichnen. Sprechende Bezeichnungen "
+"wie \"Puppet Hosts importieren\" bieten sich an"
+
+#: application/forms/IcingaServiceSetForm.php:30
+msgid "A short name identifying this set of services"
+msgstr "Eine kurzer Name um dieses Service-Set zu bezeichnen"
+
+#: library/Director/Web/SelfService.php:234
+#, php-format
+msgid ""
+"A ticket for this agent could not have been requested from your deployment "
+"endpoint: %s"
+msgstr ""
+"Für diesen Agenten konnte kein Ticket vom Deployment-Endpoint angefordert "
+"werden: %s"
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:95
+#, php-format
+msgid ""
+"A total of %d config changes happened since your last deployed config has "
+"been rendered"
+msgstr ""
+"Insgesamt wurde die Konfiguration %d mal seit dem letzten Ausrollen geändert"
+
+#: application/forms/IcingaHostSelfServiceForm.php:49
+msgid "API Key"
+msgstr "API-Schlüssel"
+
+#: library/Director/Db/Branch/BranchModificationInspection.php:34
+msgid "API Users"
+msgstr "API Benutzer"
+
+#: application/forms/IcingaEndpointForm.php:46
+#: application/forms/KickstartForm.php:155
+msgid "API user"
+msgstr "API Benutzer"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1434
+msgid "Accept passive checks"
+msgstr "Passive Checkergebnisse akzeptieren"
+
+#: application/forms/IcingaHostForm.php:93
+msgid "Accepts config"
+msgstr "Akzeptiert Konfiguration"
+
+# Aktuell wird es so in anderen Modulen übersetzt. -ThW
+#: library/Director/IcingaConfig/TypeFilterSet.php:28
+msgid "Acknowledgement"
+msgstr "Bestätigung"
+
+#: library/Director/Web/Table/ConfigFileDiffTable.php:81
+#: library/Director/Web/Widget/ActivityLogInfo.php:514
+#: library/Director/Web/Widget/ActivityLogInfo.php:525
+#: application/controllers/BranchController.php:62
+msgid "Action"
+msgstr "Aktion"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1547
+msgid "Action URL"
+msgstr "Aktions-URL"
+
+#: library/Director/Web/Table/QuickTable.php:281
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:72
+#: library/Director/Web/Widget/ActivityLogInfo.php:555
+msgid "Actions"
+msgstr "Aktionen"
+
+#: application/forms/SettingsForm.php:175
+msgid "Activation Tool"
+msgstr "Aktivierungswerkzeug"
+
+#: application/forms/SettingsForm.php:154
+msgid "Active-Passive"
+msgstr "Aktiv-Passiv"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:30
+#: application/controllers/BranchController.php:48
+msgid "Activity"
+msgstr "Aktivität"
+
+#: library/Director/Dashboard/Dashlet/ActivityLogDashlet.php:11
+#: library/Director/Web/Tabs/InfraTabs.php:29
+#: application/controllers/ConfigController.php:160
+msgid "Activity Log"
+msgstr "Aktivitätslog"
+
+#: library/Director/Web/Controller/ObjectController.php:304
+#, php-format
+msgid "Activity Log: %s"
+msgstr "Aktivitätslog: %s"
+
+#: configuration.php:173
+msgid "Activity log"
+msgstr "Aktivitätslog"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:19
+#: library/Director/Web/ActionBar/ChoicesActionBar.php:16
+#: library/Director/Web/ActionBar/ObjectsActionBar.php:16
+#: library/Director/Web/Form/DirectorObjectForm.php:504
+#: library/Director/Web/Controller/ActionController.php:166
+#: library/Director/Web/Controller/ObjectsController.php:315
+#: library/Director/Web/Controller/ObjectsController.php:356
+#: application/forms/AddToBasketForm.php:73
+#: application/controllers/DataController.php:34
+#: application/controllers/DataController.php:77
+#: application/controllers/DataController.php:95
+#: application/controllers/DataController.php:171
+msgid "Add"
+msgstr "Hinzufügen"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:51
+#: library/Director/Web/Controller/ObjectController.php:104
+#, php-format
+msgid "Add %s"
+msgstr "%s hinzufügen"
+
+#: application/forms/AddToBasketForm.php:69
+#, php-format
+msgid "Add %s objects"
+msgstr "%s Objekte hinzufügen"
+
+#: library/Director/Web/Controller/ObjectController.php:446
+#, php-format
+msgid "Add %s: %s"
+msgstr "%s hinzufügen: %s"
+
+#: application/controllers/HostsController.php:40
+#: application/controllers/HostsController.php:79
+msgid "Add Service"
+msgstr "Service hinzufügen"
+
+#: application/controllers/HostsController.php:45
+#: application/controllers/HostsController.php:110
+msgid "Add Service Set"
+msgstr "Service-Set hinzufügen"
+
+#: application/controllers/HostsController.php:127
+#, php-format
+msgid "Add Service Set to %d hosts"
+msgstr "Füge Service-Set zu %d Hosts hinzu"
+
+#: application/controllers/HostController.php:121
+#, php-format
+msgid "Add Service Set to %s"
+msgstr "Service-Set zu %s hinzufügen"
+
+#: application/controllers/HostController.php:107
+#, php-format
+msgid "Add Service to %s"
+msgstr "Service zu %s hinzufügen"
+
+#: application/controllers/DatafieldController.php:33
+msgid "Add a new Data Field"
+msgstr "Einen neues Datenfeld hinzufügen"
+
+#: application/controllers/DatafieldcategoryController.php:39
+msgid "Add a new Data Field Category"
+msgstr "Eine neue Datenfeldkategorie hinzufügen"
+
+#: application/controllers/DataController.php:64
+msgid "Add a new Data List"
+msgstr "Datenliste hinzufügen"
+
+#: application/controllers/ImportsourcesController.php:35
+msgid "Add a new Import Source"
+msgstr "Importquelle hinzufügen"
+
+#: application/controllers/JobsController.php:15
+#: application/controllers/JobController.php:35
+msgid "Add a new Job"
+msgstr "Job hinzufügen"
+
+#: application/controllers/SyncrulesController.php:28
+msgid "Add a new Sync Rule"
+msgstr "Synchronisationsregel hinzufügen"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:514
+msgid "Add a new entry"
+msgstr "Einen neuen Eintrag hinzufügen"
+
+#: application/controllers/DataController.php:216
+msgid "Add a new instance"
+msgstr "Neue Instanz hinzufügen"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:271
+msgid "Add a new one..."
+msgstr "Einen Neuen hinzufügen..."
+
+#: application/controllers/ServicesetController.php:51
+#, php-format
+msgid "Add a service set to \"%s\""
+msgstr "Füge ein Service-Set zu \"%s\" hinzu"
+
+#: application/controllers/ServiceController.php:118
+#, php-format
+msgid "Add a service to \"%s\""
+msgstr "Füge einen Service zu \"%s\" hinzu"
+
+#: application/views/helpers/FormDataFilter.php:516
+msgid "Add another filter"
+msgstr "Weiteren Filter hinzufügen"
+
+#: application/controllers/BasketController.php:85
+msgid "Add chosen objects to a Configuration Basket"
+msgstr "Gewählte Objekte zu einem Konfigurationsbasket hinzufügen"
+
+#: application/forms/DirectorDatalistEntryForm.php:60
+msgid "Add data list entry"
+msgstr "Datenlisteneintrag hinzufügen"
+
+#: application/controllers/ImportsourceController.php:108
+msgid "Add import source"
+msgstr "Importquelle hinzufügen"
+
+#: library/Director/Web/Controller/ObjectController.php:452
+#, php-format
+msgid "Add new Icinga %s"
+msgstr "Neuen Icinga %s hinzufügen"
+
+#: library/Director/Web/Controller/ObjectController.php:435
+#, php-format
+msgid "Add new Icinga %s template"
+msgstr "Neue Icinga %s Vorlage hinzufügen"
+
+#: application/controllers/ImportsourceController.php:253
+msgid "Add property modifier"
+msgstr "Eigenschaftsmodifikator hinzufügen"
+
+#: application/controllers/HostController.php:139
+#: application/controllers/ServicesetController.php:68
+msgid "Add service"
+msgstr "Service hinzufügen"
+
+#: application/controllers/HostController.php:144
+msgid "Add service set"
+msgstr "Service-Set hinzufügen"
+
+#: application/controllers/HostsController.php:96
+#, php-format
+msgid "Add service to %d hosts"
+msgstr "Füge einen Service zu %d Hosts hinzu"
+
+#: application/controllers/SyncruleController.php:580
+msgid "Add sync property rule"
+msgstr "Regel für Synchronisationseigenschaft hinzufügen"
+
+#: application/controllers/SyncruleController.php:630
+#, php-format
+msgid "Add sync property: %s"
+msgstr "Synchronisationseigenschaft hinzufügen: %s"
+
+#: application/controllers/SyncruleController.php:522
+msgid "Add sync rule"
+msgstr "Synchronisationsregel hinzufügen"
+
+#: library/Director/Web/Controller/ObjectController.php:417
+#: library/Director/Web/Controller/TemplateController.php:129
+#: application/controllers/HostsController.php:70
+#: application/controllers/ServicesController.php:36
+#: application/controllers/BasketController.php:84
+#: application/controllers/ImportsourceController.php:67
+#: application/controllers/JobController.php:89
+#: application/controllers/SyncruleController.php:673
+#: application/controllers/DataController.php:370
+msgid "Add to Basket"
+msgstr "Zu Basket hinzufügen"
+
+#: configuration.php:61
+msgid ""
+"Additional (monitoring module) object filter to further restrict write access"
+msgstr ""
+"Zusätzliche (Monitoring-Modul-spezifische) Objekt-Filter, um den "
+"Schreibzugriff weiter einzuschränken"
+
+#: library/Director/Import/ImportSourceRestApi.php:151
+msgid "Additional headers for the HTTP request."
+msgstr "Zusätzliche HTTP-Header für diesen Request"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1533
+msgid "Additional notes for this object"
+msgstr "Weitere Notizen zu diesem Objekt"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1586
+msgid "Additional properties"
+msgstr "Weitere Eigenschaften"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:145
+msgid "Agent"
+msgstr "Agent"
+
+#: application/forms/SelfServiceSettingsForm.php:154
+msgid "Agent Version"
+msgstr "Agenten-Version"
+
+#: library/Director/PropertyModifier/PropertyModifierSimpleGroupBy.php:60
+msgid "Aggregation Columns"
+msgstr "Aggregationsspalten"
+
+#: application/forms/IcingaHostSelfServiceForm.php:31
+msgid "Alias"
+msgstr "Alias"
+
+#: library/Director/Import/ImportSourceDirectorObject.php:80
+msgid "All Object Types"
+msgstr "Alle Objekttypen"
+
+#: application/controllers/ConfigController.php:173
+msgid "All changes"
+msgstr "Alle Änderungen"
+
+#: application/forms/SettingsForm.php:78
+msgid ""
+"All changes are tracked in the Director database. In addition you might also "
+"want to send an audit log through the Icinga Web 2 logging mechanism. That "
+"way all changes would be written to either Syslog or the configured log "
+"file. When enabling this please make sure that you configured Icinga Web 2 "
+"to log at least at \"informational\" level."
+msgstr ""
+"Alle Änderungen werden in der Director-Datenbank nachvollziehbar "
+"gespeichert. Zusätzlich kann ein Revisionslog durch den Logging-Mechanismus "
+"von Icinga Web 2 geschickt werden. Auf diese Weise werden alle Änderungen "
+"entweder an Syslog oder die konfigurierte Logdatei geschickt. Dafür muss "
+"Icinga Web 2 mindestens mit dem \"Info\" Level loggen."
+
+#: application/forms/SyncPropertyForm.php:309
+msgid "All custom variables (vars)"
+msgstr "Alle benutzerdefinierten Variablen (vars)"
+
+#: application/forms/BasketForm.php:56
+msgid "All of them"
+msgstr "All diese"
+
+#: application/forms/IcingaServiceForm.php:766
+#, php-format
+msgid "All overrides have been removed from \"%s\""
+msgstr "Alle überschriebenen Eigenschaften wurden von \"%s\" entfernt"
+
+#: library/Director/Web/Controller/ObjectsController.php:307
+#, php-format
+msgid "All your %s Apply Rules"
+msgstr "Alle %s Apply Regeln"
+
+#: library/Director/Web/Controller/ObjectsController.php:252
+#, php-format
+msgid "All your %s Templates"
+msgstr "Alle %s Templates"
+
+#: application/forms/SelfServiceSettingsForm.php:187
+msgid "Allow Updates"
+msgstr "Updates erlauben"
+
+#: library/Director/DataType/DataTypeDatalist.php:153
+msgid "Allow for values not on the list"
+msgstr "Erlaube Werte die nicht au f der Liste stehen"
+
+#: configuration.php:39
+msgid "Allow readonly users to see where a Service came from"
+msgstr "Erlaube Nur-Lesen-Benutzern zu sehen woher ein Service kommt"
+
+#: configuration.php:10
+msgid "Allow to access the director API"
+msgstr "Zugriff auf die Director API erlauben"
+
+#: configuration.php:11
+msgid "Allow to access the full audit log"
+msgstr "Zugriff auf das volle Audit-Log erlauben"
+
+#: configuration.php:21
+msgid "Allow to configure hosts"
+msgstr "Erlauben, Hosts zu konfigurieren"
+
+#: configuration.php:26 configuration.php:29
+msgid "Allow to configure notifications (unrestricted)"
+msgstr "Erlaube (unbeschränkt) Benachrichtigungen zu konfigurieren"
+
+#: configuration.php:23
+msgid "Allow to configure service sets"
+msgstr "Erlauben, Service-Sets zu konfigurieren"
+
+#: configuration.php:22
+msgid "Allow to configure services"
+msgstr "Erlauben, Services zu konfigurieren"
+
+#: configuration.php:25
+msgid "Allow to configure users"
+msgstr "Erlauben, Benutzer zu konfigurieren"
+
+#: configuration.php:24
+msgid "Allow to define Service Set Apply Rules"
+msgstr "Erlaube Definieren von Service-Set Apply-Regeln"
+
+#: configuration.php:20
+msgid "Allow to deploy configuration"
+msgstr "Das Ausrollen von Konfigurationen erlauben"
+
+#: configuration.php:34
+msgid ""
+"Allow to inspect objects through the Icinga 2 API (could contain sensitive "
+"information)"
+msgstr ""
+"Untersuchen von Objekten über die API von Icinga 2 erlauben (könnte sensible "
+"Daten enthalten)"
+
+#: configuration.php:14
+msgid "Allow to show configuration (could contain sensitive information)"
+msgstr "Anzeigen der Konfiguration erlauben (könnte sensible Daten enthalten)"
+
+#: configuration.php:18
+msgid "Allow to show the full executed SQL queries in some places"
+msgstr ""
+"Erlaube das Anzeigen der vollständigen ausgeführten SQL-Statements an "
+"einigen Stellen"
+
+#: application/forms/DirectorDatalistEntryForm.php:48
+msgid ""
+"Allow to use this entry only to users with one of these Icinga Web 2 roles"
+msgstr ""
+"Erlaube es ausschließlich Benutzern mit einer dieser Icinga Web 2 Rollen, "
+"diesen Eintrag zu nutzen"
+
+#: configuration.php:49
+msgid "Allow unrestricted access to Icinga Director"
+msgstr "Unbeschränkten Zugriff auf die Director API erlauben"
+
+#: configuration.php:43
+msgid ""
+"Allow users to modify Hosts they are allowed to see in the monitoring module"
+msgstr ""
+"Erlaube Benutzern, Hosts zu bearbeiten, welche sie im Monitoring-Modul sehen "
+"dürfen"
+
+#: configuration.php:47
+msgid ""
+"Allow users to modify Service they are allowed to see in the monitoring "
+"module"
+msgstr ""
+"Erlaube Benutzern, Services zu bearbeiten, welche sie im Monitoring-Modul "
+"sehen dürfen"
+
+#: configuration.php:67
+msgid "Allow users with Hostgroup restrictions to access the Groups field"
+msgstr ""
+"Mache Benutzern mit Hostgruppenbeschränkung das Feld \"Gruppen\" zugänglich"
+
+#: application/forms/IcingaTemplateChoiceForm.php:85
+msgid "Allowed maximum"
+msgstr "Erlaubtes Maximum"
+
+#: application/forms/DirectorDatalistEntryForm.php:44
+msgid "Allowed roles"
+msgstr "Erlaubte Rollen"
+
+#: application/forms/IcingaCloneObjectForm.php:102
+msgid "Also clone fields provided by this Template"
+msgstr "Klone auch Felder welche durch diese Vorlage bereitgestellt werden"
+
+#: application/forms/IcingaCloneObjectForm.php:70
+msgid "Also clone single Service Sets defined for this Host"
+msgstr "Klone auch Service-Sets welche für diesen Host definiert wurden"
+
+#: application/forms/IcingaCloneObjectForm.php:61
+msgid "Also clone single Services defined for this Host"
+msgstr "Klone auch Einzelservices welche für diesen Host definiert wurden"
+
+#: library/Director/Import/ImportSourceRestApi.php:211
+msgid "Also deeper keys can be specific by a dot-notation:"
+msgstr ""
+"Auch tieferliegende Schlüssel können via Punkt-Notation erreicht werden:"
+
+#: application/forms/SelfServiceSettingsForm.php:204
+msgid ""
+"Also install NSClient++. It can be used through the Icinga Agent and comes "
+"with a bunch of additional Check Plugins"
+msgstr ""
+"Installiere auch NSClient++. Dieser kann über den installierten Icinga Agent "
+"genutzt werden und bringt eine Reihe zusätzlicher Check-Plugins"
+
+#: application/forms/DirectorDatafieldForm.php:109
+#, php-format
+msgid "Also rename all \"%s\" custom variables to \"%s\" on %d objects?"
+msgstr ""
+"Auch alle benutzerdefinierten \"%s\" Variablen nach \"%s\" auf %d Objekten "
+"umbenennen?"
+
+#: application/forms/DirectorDatafieldForm.php:66
+#, php-format
+msgid "Also wipe all \"%s\" custom variables from %d objects?"
+msgstr ""
+"Außerdem alle \"%s\" benutzerdefinierten Variablen von %d Objekten entfernen?"
+
+#: application/forms/IcingaHostForm.php:355
+msgid ""
+"Alternative name for this host. Might be a host alias or and kind of string "
+"helping your users to identify this host"
+msgstr ""
+"Alternativer Name für diesen Host. Kann ein Alias oder jede Zeichenkette "
+"sein, die Benutzern hilft, diesen Host zu identifizieren"
+
+#: application/forms/IcingaUserForm.php:135
+msgid ""
+"Alternative name for this user. In case your object name is a username, this "
+"could be the full name of the corresponding person"
+msgstr ""
+"Alternativer Name für diesen Benutzer. Falls der Name des Objekts ein "
+"Benutzername ist, könnte hier der vollständige Name der betreffenden Person "
+"hinterlegt werden"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1567
+msgid "Alternative text to be shown in case above icon is missing"
+msgstr "Alternativer Text der angezeigt werden soll, falls obiges Icon fehlt"
+
+#: application/forms/IcingaCommandArgumentForm.php:91
+msgid ""
+"An Icinga DSL expression that returns a boolean value, e.g.: var cmd = "
+"bool(macro(\"$cmd$\")); return cmd ..."
+msgstr ""
+"Ein Icinga DSL Ausdruck welcher einen booleschen Wert liefert, z.B.: var cmd "
+"= bool(macro(\"$cmd$\")); return cmd ..."
+
+#: application/forms/IcingaCommandArgumentForm.php:52
+msgid ""
+"An Icinga DSL expression, e.g.: var cmd = macro(\"$cmd$\"); return "
+"typeof(command) == String ..."
+msgstr ""
+"Ein Icinga DSL Ausdruck, z.B.: var cmd = macro(\"$cmd$\"); return "
+"typeof(command) == String ..."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1549
+msgid ""
+"An URL leading to additional actions for this object. Often used with Icinga "
+"Classic, rarely with Icinga Web 2 as it provides far better possibilities to "
+"integrate addons"
+msgstr ""
+"Eine URL zu weiteren Aktionen für dieses Objekt. Wird oft mit Icinga "
+"Classic, jedoch selten mit Icinga Web 2 genutzt, da dieses viel bessere "
+"Möglichkeiten zur Integration von Addons bietet"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1542
+msgid "An URL pointing to additional notes for this object"
+msgstr "Eine URL zu Notizen für dieses Objekt"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1558
+msgid ""
+"An URL pointing to an icon for this object. Try \"tux.png\" for icons "
+"relative to public/img/icons or \"cloud\" (no extension) for items from the "
+"Icinga icon font"
+msgstr ""
+"Eine URL zu einem Icon für dieses Objekt. \"tux.png\" für Icons relativ zu "
+"public/img/icons oder \"cloud\" (ohne Erweiterung) für Objekte aus dem "
+"Icinga Icon-Fundus"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1314
+msgid ""
+"An alternative display name for this group. If you wonder how this could be "
+"helpful just leave it blank"
+msgstr ""
+"Ein alternativer Anzeigename für diese Gruppe. Kann leer gelassen werden"
+
+#: application/forms/ImportRowModifierForm.php:54
+msgid ""
+"An extended description for this Import Row Modifier. This should explain "
+"it's purpose and why it has been put in place at all."
+msgstr ""
+"Eine erweiterte Beschreibung für diesen Eigenschaftsmodifikator. Diese soll "
+"seine Aufgabe sowie seinen Sinn und Zweck beschreiben."
+
+#: application/forms/ImportSourceForm.php:26
+msgid ""
+"An extended description for this Import Source. This should explain what "
+"kind of data you're going to import from this source."
+msgstr ""
+"Eine erweiterte Beschreibung für diese Import-Quelle. Diese sollte "
+"erläutern, welche Art von Daten von dieser Quelle bereitgestellt werden."
+
+#: application/forms/SyncRuleForm.php:38
+msgid ""
+"An extended description for this Sync Rule. This should explain what this "
+"Rule is going to accomplish."
+msgstr ""
+"Eine erweiterte Beschreibung für diese Synchronisationsregel. Sollte "
+"erläutern was diese Regel bezwecken will."
+
+#: application/forms/DirectorDatafieldForm.php:161
+msgid ""
+"An extended description for this field. Will be shown as soon as a user puts "
+"the focus on this field"
+msgstr ""
+"Eine ausführliche Beschreibung dieses Felds. Wird angezeigt, sobald ein "
+"Benutzer den Fokus auf dieses Feld legt"
+
+#: library/Director/Import/ImportSourceLdap.php:58
+msgid ""
+"An object class to search for. Might be \"user\", \"group\", \"computer\" or "
+"similar"
+msgstr ""
+"Die Objektklasse, nach der gesucht werden soll. z.B. \"user\", \"group\", "
+"\"computer\" oder Ähnliches"
+
+#: library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php:35
+msgid ""
+"Another Import Source. We're going to look up the row with the key matching "
+"the value in the chosen column"
+msgstr ""
+"Eine andere Import-Quelle. Wir suchen dort eine Zeile mit einem Schlüssel, "
+"der dem Wert in der gewählten Spalte entspricht"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:20
+msgid "Any first (leftmost) component"
+msgstr "Beliebige erste (linkeste) Komponente"
+
+#: library/Director/Web/SelfService.php:70
+#: library/Director/Web/SelfService.php:111
+msgid "Api Key:"
+msgstr "API-Schlüssel:"
+
+#: library/Director/Web/Controller/TemplateController.php:54
+#, php-format
+msgid "Applied %s"
+msgstr "Angewendete(r) %s"
+
+#: application/forms/IcingaHostForm.php:231
+msgid "Applied groups"
+msgstr "Angewendete Gruppen"
+
+#: application/controllers/HostController.php:417
+#, php-format
+msgid "Applied service: %s"
+msgstr "Zugewiesener Service: %s"
+
+#: application/controllers/HostController.php:261
+#: application/controllers/HostController.php:345
+msgid "Applied services"
+msgstr "Zugewiesene Services"
+
+#: library/Director/Web/Tabs/ObjectsTabs.php:49
+msgid "Apply"
+msgstr "Anwenden"
+
+#: application/controllers/ServiceController.php:123
+#, php-format
+msgid "Apply \"%s\""
+msgstr "Apply \"%s\""
+
+#: application/forms/ApplyMigrationsForm.php:25
+#, php-format
+msgid "Apply %d pending schema migrations"
+msgstr "%d Schema-Migrations-Scripte anwenden"
+
+#: application/forms/IcingaServiceForm.php:623
+msgid "Apply For"
+msgstr "Anwenden auf"
+
+#: library/Director/Web/Controller/TemplateController.php:169
+msgid "Apply Rule"
+msgstr "Apply-Regel"
+
+#: library/Director/Web/Table/ApplyRulesTable.php:165
+msgid "Apply Rule rendering preview"
+msgstr "Rendering-Vorschau für diese Apply-Regel"
+
+#: library/Director/Import/ImportSourceDirectorObject.php:84
+#: library/Director/Web/Table/DependencyTemplateUsageTable.php:11
+#: library/Director/Web/Table/NotificationTemplateUsageTable.php:11
+#: library/Director/Web/Table/ServiceTemplateUsageTable.php:12
+msgid "Apply Rules"
+msgstr "Apply-Regeln"
+
+#: application/forms/ApplyMigrationsForm.php:20
+msgid "Apply a pending schema migration"
+msgstr "Ein ausstehende Schema-Migration durchführen"
+
+#: library/Director/Job/SyncJob.php:92
+msgid "Apply changes"
+msgstr "Änderungen anwenden"
+
+#: library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php:19
+msgid "Apply notifications with specific properties according to given rules."
+msgstr ""
+"Benachrichtigungen mit bestimmten Eigenschaften anhand von gegebenen Regeln "
+"anwenden"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1123
+msgid "Apply rule"
+msgstr "Apply Regel"
+
+#: library/Director/Web/Table/ApplyRulesTable.php:172
+msgid "Apply rule history"
+msgstr "Apply-Regel Historie"
+
+#: application/forms/KickstartForm.php:38
+msgid "Apply schema migrations"
+msgstr "Schema-Migrations-Scripte anwenden"
+
+#: application/forms/IcingaNotificationForm.php:81
+#: application/forms/IcingaDependencyForm.php:93
+#: application/forms/IcingaScheduledDowntimeForm.php:82
+msgid "Apply to"
+msgstr "Anwenden auf"
+
+#: application/controllers/ServiceController.php:224
+#, php-format
+msgid "Apply: %s"
+msgstr "Apply: %s"
+
+#: library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php:46
+#: library/Director/Web/Table/IcingaCommandArgumentTable.php:49
+msgid "Argument"
+msgstr "Argument"
+
+#: application/forms/IcingaObjectFieldForm.php:99
+msgid "Argument macros"
+msgstr "Argument Makros"
+
+#: application/forms/IcingaCommandArgumentForm.php:25
+msgid "Argument name"
+msgstr "Argumentname"
+
+#: application/forms/SyncPropertyForm.php:315
+msgid "Arguments"
+msgstr "Argumente"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:80
+#: library/Director/DataType/DataTypeSqlQuery.php:77
+#: library/Director/DataType/DataTypeDatalist.php:133
+msgid "Array"
+msgstr "Array"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1619
+msgid "Assign where"
+msgstr "Zuweisen wo"
+
+#: application/forms/IcingaTemplateChoiceForm.php:95
+msgid "Associated Template"
+msgstr "Zugehörige Vorlage"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:506
+#: application/forms/IcingaScheduledDowntimeForm.php:31
+#: application/controllers/BranchController.php:60
+msgid "Author"
+msgstr "Autor"
+
+#: library/Director/DataType/DataTypeDatalist.php:151
+msgid "Autocomplete"
+msgstr "Automatische Vervollständigung"
+
+#: library/Director/Dashboard/AutomationDashboard.php:15
+msgid "Automate all tasks"
+msgstr "Automatisiere alle Aufgaben"
+
+#: configuration.php:169
+msgid "Automation"
+msgstr "Automatisierung"
+
+#: application/forms/IcingaTemplateChoiceForm.php:64
+msgid "Available choices"
+msgstr "Verfügbare Auswahlmöglichkeiten"
+
+#: library/Director/Web/Controller/TemplateController.php:95
+#: application/controllers/BasketController.php:54
+#: application/controllers/DataController.php:165
+msgid "Back"
+msgstr "Zurück"
+
+#: library/Director/Web/Table/BasketTable.php:31
+#: application/forms/AddToBasketForm.php:61
+#: application/controllers/BasketController.php:36
+msgid "Basket"
+msgstr "Basket"
+
+#: application/forms/BasketForm.php:38
+msgid "Basket Definitions"
+msgstr "Basket-Definitionen"
+
+#: application/forms/BasketUploadForm.php:29
+#: application/forms/BasketForm.php:48
+msgid "Basket Name"
+msgstr "Basket-Name"
+
+#: application/controllers/BasketController.php:142
+msgid "Basket Snapshots"
+msgstr "Basket-Snapshots"
+
+#: application/forms/BasketUploadForm.php:145
+msgid "Basket has been uploaded"
+msgstr "Basket wurde hochgeladen"
+
+#: application/controllers/BasketsController.php:17
+#: application/controllers/BasketController.php:78
+msgid "Baskets"
+msgstr "Baskets"
+
+#: application/forms/SyncRuleForm.php:103
+msgid ""
+"Be careful: this is usually NOT what you want, as it makes Sync \"blind\" "
+"for objects matching this filter. This means that \"Purge\" will not work as "
+"expected. The \"Black/Whitelist\" Import Property Modifier is probably what "
+"you're looking for."
+msgstr ""
+"Vorsicht: das ist für gewöhnlich NICHT das was man möchte, denn es macht "
+"Sync \"blind\" für Objekte die diesem Filter entsprechen. Das bedeutet meist "
+"dass das \"Bereinigen\" nicht wie erwartet funktioniert. Der \"Black/"
+"Whitelist\"-Eigenschaftsmodifikator ist für gewöhnlich was man stattdessen "
+"nutzen möchte."
+
+#: library/Director/PropertyModifier/PropertyModifierTrim.php:20
+msgid "Beginning and Ending"
+msgstr "Anfang und Ende"
+
+#: library/Director/PropertyModifier/PropertyModifierTrim.php:21
+msgid "Beginning only"
+msgstr "Nur den Anfang"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:79
+msgid "Binary"
+msgstr "Binary"
+
+#: application/controllers/BranchController.php:49
+msgid "Branch Activity"
+msgstr "Aktivität in diesem Zweig"
+
+#: library/Director/DataType/DataTypeDictionary.php:36
+msgid "Can be managed once this object has been created"
+msgstr "Kann verwaltet werden, sobald dieses Objekt erstellt wurde"
+
+#: library/Director/Db/Branch/MergeErrorModificationForMissingObject.php:10
+#, php-format
+msgid "Cannot apply modification for %s %s, object does not exist"
+msgstr ""
+"Änderung für %s %s kann nicht angewandt werden, das Objekt existiert nicht"
+
+#: library/Director/Db/Branch/MergeErrorDeleteMissingObject.php:10
+#, php-format
+msgid "Cannot delete %s %s, it does not exist"
+msgstr "%s %s kann nicht gelöscht werden, es existiert nicht"
+
+#: library/Director/Db/Branch/MergeErrorRecreateOnMerge.php:10
+#, php-format
+msgid "Cannot recreate %s %s"
+msgstr "%s %s kann nicht erneut erstellt werden"
+
+#: application/forms/DirectorDatafieldForm.php:150
+#: application/forms/IcingaObjectFieldForm.php:125
+msgid "Caption"
+msgstr "Beschriftung"
+
+#: library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php:17
+msgid "Categories bring structure to your Data Fields"
+msgstr "Kategorien bringen Struktur in konfigurierte Datenfelder"
+
+#: library/Director/Web/Table/DatafieldTable.php:51
+msgid "Category"
+msgstr "Kategorie"
+
+#: library/Director/Web/Table/DatafieldCategoryTable.php:47
+msgid "Category Name"
+msgstr "Kategoriename"
+
+#: application/forms/DirectorDatafieldCategoryForm.php:23
+msgid "Category name"
+msgstr "Kategoriename"
+
+#: application/forms/IcingaMultiEditForm.php:270
+#, php-format
+msgid "Changing this value affects %d object(s): %s"
+msgstr "Diesen Wert zu ändern beeinflusst %d Objekt(e): %s"
+
+#: library/Director/PropertyModifier/PropertyModifierTrim.php:28
+msgid "Character Mask"
+msgstr "Zeichenmaske"
+
+#: library/Director/Import/ImportSourceCoreApi.php:57
+msgid "Check Commands"
+msgstr "Check-Kommando"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1335
+msgid "Check command"
+msgstr "Check-Kommando"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1336
+#: application/forms/IcingaNotificationForm.php:263
+msgid "Check command definition"
+msgstr "Check-Kommandodefinition"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1404
+msgid ""
+"Check command timeout in seconds. Overrides the CheckCommand's timeout "
+"attribute"
+msgstr ""
+"Timeout für das Check-Kommando in Sekunden. Überschreibt die Timeout-"
+"Konfiguration des Kommandos"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:336
+msgid "Check execution"
+msgstr "Check-Ausführung"
+
+#: application/forms/ImportCheckForm.php:23
+#: application/forms/SyncCheckForm.php:24
+msgid "Check for changes"
+msgstr "Auf Änderungen prüfen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1371
+msgid "Check interval"
+msgstr "Check-Intervall"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1416
+msgid "Check period"
+msgstr "Checkzeitraum"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1402
+msgid "Check timeout"
+msgstr "Check-Timeout"
+
+#: application/forms/ImportCheckForm.php:45
+msgid "Checking this Import Source failed"
+msgstr "Überprüfen dieser Importquelle fehlgeschlagen"
+
+#: application/forms/SyncCheckForm.php:66
+msgid "Checking this sync rule failed"
+msgstr "Überprüfen dieser Synchronisationsregel fehlgeschlagen"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:550
+msgid "Checksum"
+msgstr "Prüfsumme"
+
+#: application/forms/IcingaDependencyForm.php:233
+msgid "Child Host"
+msgstr "Kind-Host"
+
+#: application/forms/IcingaDependencyForm.php:246
+msgid "Child Service"
+msgstr "Kind-Service"
+
+#: application/forms/IcingaTemplateChoiceForm.php:48
+msgid "Choice name"
+msgstr "Auswahlname"
+
+#: library/Director/Dashboard/Dashlet/ChoicesDashlet.php:11
+#: library/Director/Web/Tabs/ObjectsTabs.php:74
+msgid "Choices"
+msgstr "Auswahl"
+
+#: application/forms/BasketForm.php:82
+msgid ""
+"Choose \"All\" to always add all of them, \"Ignore\" to not care about a "
+"specific Type at all and opt for \"Custom Selection\" in case you want to "
+"choose just some specific Objects."
+msgstr ""
+"Wählen Sie \"Alle\" um immer alle davon hinzuzufügen, \"Ignorieren\" um sich "
+"nicht um einen bestimmten Typ zu kümmern und entscheiden Sie sich für "
+"\"Benutzerdefinierte Auswahl\" wenn nur bestimmte Objekte manuell gewählt "
+"werden sollen."
+
+#: application/forms/IcingaTemplateChoiceForm.php:97
+msgid "Choose Choice Associated Template"
+msgstr "Wähle eine der Auswahl zugehörige Vorlage"
+
+#: application/controllers/IndexController.php:53
+msgid "Choose DB Resource"
+msgstr "Datenbankressource wählen"
+
+#: application/forms/IcingaHostForm.php:188
+msgid "Choose a Host Template"
+msgstr "Wähle eine Service-Vorlage"
+
+#: application/forms/IcingaAddServiceForm.php:105
+msgid "Choose a service template"
+msgstr "Wähle eine Service-Vorlage"
+
+#: application/forms/SyncRuleForm.php:46
+msgid "Choose an object type"
+msgstr "Einen Objekttyp auswählen"
+
+#: application/forms/BasketUploadForm.php:35
+msgid "Choose file"
+msgstr "Datei wählen"
+
+#: application/forms/IcingaServiceForm.php:601
+msgid "Choose the host this single service should be assigned to"
+msgstr "Host wählen, dem dieser einzelne Service zugewiesen werden soll"
+
+#: application/forms/IcingaTemplateChoiceForm.php:75
+msgid ""
+"Choosing this many options will be mandatory for this Choice. Setting this "
+"to zero will leave this Choice optional, setting it to one results in a "
+"\"required\" Choice. You can use higher numbers to enforce multiple options, "
+"this Choice will then turn into a multi-selection element."
+msgstr ""
+"Diese Anzahl an Optionen müssen für diese Auswahl zwingend gewählt werden. "
+"Setzt man diese Einstellung auf Null, wird die Auswahl optional, setzt man "
+"sie auf Eins wird die Auswahl zwingend. Setzt man eine höhere Anzahl kann "
+"man mehrere Optionen erzwingen, die Auswahl wandelt sich dann in ein "
+"Mehrfach-Auswahlfeld."
+
+#: application/forms/IcingaZoneForm.php:37
+msgid "Chose an (optional) parent zone"
+msgstr "Eine (optionale) Elternzone auswählen"
+
+#: library/Director/Web/Widget/Documentation.php:38
+#, php-format
+msgid "Click to read our documentation: %s"
+msgstr "Anklicken um unsere Dokumentation zu lesen: %s"
+
+#: library/Director/Web/ActionBar/AutomationObjectActionBar.php:44
+#: library/Director/Web/Form/CloneImportSourceForm.php:35
+#: library/Director/Web/Form/CloneSyncRuleForm.php:35
+#: library/Director/Web/Controller/ObjectController.php:381
+#: application/controllers/ImportsourceController.php:148
+#: application/controllers/SyncruleController.php:548
+msgid "Clone"
+msgstr "Klonen"
+
+#: application/forms/IcingaCloneObjectForm.php:108
+#, php-format
+msgid "Clone \"%s\""
+msgstr "Klone \"%s\""
+
+#: application/forms/IcingaCloneObjectForm.php:68
+msgid "Clone Service Sets"
+msgstr "Klone Service-Set"
+
+#: application/forms/IcingaCloneObjectForm.php:59
+msgid "Clone Services"
+msgstr "Klone Services"
+
+#: application/forms/IcingaCloneObjectForm.php:100
+msgid "Clone Template Fields"
+msgstr "Vorlagen-Felder klonen"
+
+#: application/forms/IcingaCloneObjectForm.php:49
+msgid "Clone the object as is, preserving imports"
+msgstr "Objekt vollständig unter Erhalt der Vererbung klonen"
+
+#: application/forms/IcingaCloneObjectForm.php:89
+msgid "Clone this service to the very same or to another Host"
+msgstr "Klone diesen Service zum selben oder einem anderen Host"
+
+#: application/forms/IcingaCloneObjectForm.php:80
+msgid "Clone this service to the very same or to another Service Set"
+msgstr "Klone diesen Service"
+
+#: library/Director/Web/Controller/ObjectController.php:214
+#, php-format
+msgid "Clone: %s"
+msgstr "Klone %s"
+
+#: library/Director/Web/Controller/ObjectController.php:221
+msgid "Cloning Apply Rules"
+msgstr "Apply-Regeln klonen"
+
+#: application/controllers/ImportsourceController.php:149
+msgid "Cloning Import Sources"
+msgstr "Importquellen klonen"
+
+#: application/controllers/SyncruleController.php:559
+msgid "Cloning Sync Rules"
+msgstr "Synchronisationsregeln klonen"
+
+#: library/Director/Web/Controller/ObjectController.php:217
+msgid "Cloning Templates"
+msgstr "Vorlagen Klonen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1162
+msgid "Cluster Zone"
+msgstr "Cluster Zone"
+
+#: library/Director/Dashboard/Dashlet/ChoicesDashlet.php:17
+msgid ""
+"Combine multiple templates into meaningful Choices, making life easier for "
+"your users"
+msgstr ""
+"Kombiniere mehrere Vorlagen zu sprechenden Wahlmöglichkeiten, um die "
+"Benutzung zu vereinfachen"
+
+#: library/Director/PropertyModifier/PropertyModifierSimpleGroupBy.php:62
+msgid ""
+"Comma-separated list of columns that should be aggregated (transformed into "
+"an Array). For all other columns only the first value will be kept."
+msgstr ""
+"Kommagetrennte Liste von Spalten, welche aggregiert (in ein Array "
+"verwandelt) werden sollen. Für alle anderen Spalten wird lediglich der erste "
+"Wert beibehalten."
+
+#: library/Director/TranslationDummy.php:16
+#: application/forms/IcingaCommandForm.php:58
+#: application/forms/SyncRuleForm.php:20
+msgid "Command"
+msgstr "Kommando"
+
+#: application/forms/BasketForm.php:17
+msgid "Command Definitions"
+msgstr "Kommandodefinitionen"
+
+#: application/forms/BasketForm.php:19
+msgid "Command Template"
+msgstr "Kommando-Vorlage"
+
+#: library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php:19
+msgid "Command Templates"
+msgstr "Kommando-Vorlagen"
+
+#: application/controllers/CommandController.php:92
+#, php-format
+msgid "Command arguments: %s"
+msgstr "Kommandoargumente: %s"
+
+#: application/forms/IcingaHostForm.php:114
+msgid "Command endpoint"
+msgstr "Kommandoendpunkt"
+
+#: application/forms/IcingaCommandForm.php:47
+msgid "Command name"
+msgstr "Kommandoname"
+
+#: application/forms/IcingaCommandForm.php:17
+msgid "Command type"
+msgstr "Kommandotyp"
+
+#: configuration.php:161
+#: library/Director/Dashboard/Dashlet/CommandObjectDashlet.php:13
+#: library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php:19
+#: library/Director/Db/Branch/BranchModificationInspection.php:37
+#: library/Director/Web/Table/CustomvarVariantsTable.php:57
+msgid "Commands"
+msgstr "Kommandos"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:39
+msgid "Comment"
+msgstr "Kommentar"
+
+#: application/controllers/BasketController.php:345
+#, php-format
+msgid "Comparing %s \"%s\" from Snapshot \"%s\" to current config"
+msgstr ""
+"Vergleiche %s \"%s\" aus dem Snapshot \"%s\" mit der aktuellen Konfiguration"
+
+#: application/forms/IcingaCommandArgumentForm.php:89
+#: application/forms/IcingaCommandArgumentForm.php:98
+msgid "Condition (set_if)"
+msgstr "Bedingung (set_if)"
+
+#: application/forms/IcingaCommandArgumentForm.php:75
+msgid "Condition format"
+msgstr "Bedingungsformat"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:85
+#: library/Director/Web/Widget/DeploymentInfo.php:60
+#: application/controllers/JobController.php:112
+#: application/controllers/ConfigController.php:277
+#: application/controllers/ConfigController.php:515
+msgid "Config"
+msgstr "Konfiguration"
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:18
+msgid "Config Deployment"
+msgstr "Ausrollen der Konfiguration"
+
+#: application/forms/DeployConfigForm.php:100
+#: application/forms/DeploymentLinkForm.php:165
+#: application/controllers/ConfigController.php:490
+msgid "Config deployment failed"
+msgstr "Ausrollen der Konfiguration fehlgeschlagen"
+
+#: application/controllers/ConfigController.php:364
+#: application/controllers/ConfigController.php:365
+msgid "Config diff"
+msgstr "Konfigurationsunterschied"
+
+#: application/controllers/ConfigController.php:323
+#: application/controllers/ConfigController.php:425
+#, php-format
+msgid "Config file \"%s\""
+msgstr "Konfigurationsdatei \"%s\""
+
+#: application/forms/DeployConfigForm.php:76
+#: application/forms/DeployConfigForm.php:95
+#: application/forms/DeploymentLinkForm.php:152
+#: application/controllers/ConfigController.php:470
+msgid "Config has been submitted, validation is going on"
+msgstr "Konfiguration wurde übergeben, Überprüfung ist im Gange"
+
+#: application/forms/DeployFormsBug7530.php:87
+msgid "Config has not been deployed"
+msgstr "Konfiguration wurde nicht ausgerollt"
+
+#: library/Director/Web/ObjectPreview.php:41
+#, php-format
+msgid "Config preview: %s"
+msgstr "Konfigurationsvorschau: %s"
+
+#: library/Director/ProvidedHook/Monitoring/ServiceActions.php:65
+#: library/Director/Web/Widget/DeploymentInfo.php:82
+msgid "Configuration"
+msgstr "Konfiguration"
+
+#: application/controllers/HostController.php:285
+msgid "Configuration (read-only)"
+msgstr "Konfiguration (nur lesen)"
+
+#: library/Director/Dashboard/Dashlet/BasketDashlet.php:11
+#: application/controllers/BasketsController.php:32
+msgid "Configuration Baskets"
+msgstr "Konfigurationsbaskets"
+
+#: application/forms/SettingsForm.php:121
+msgid "Configuration format"
+msgstr "Konfigurationsformat"
+
+#: application/forms/KickstartForm.php:309
+msgid "Configuration has been stored"
+msgstr "Konfiguration wurde gespeichert"
+
+#: application/forms/AddToBasketForm.php:112
+#, php-format
+msgid "Configuration objects have been added to the chosen basket \"%s\""
+msgstr "Konfigurationsobjekte wurden zum gewählten Basket \"%s\" hinzugefügt"
+
+#: library/Director/Web/SelfService.php:192
+msgid "Configure this Agent via Self Service API"
+msgstr "Konfiguriere diesen Agent über die Selbstbedienungs-API"
+
+#: application/controllers/BasketController.php:231
+msgid "Content Checksum"
+msgstr "Prüfsumme (Inhalt)"
+
+#: application/controllers/BasketsController.php:20
+msgid "Create"
+msgstr "Erstellen"
+
+#: application/controllers/BasketController.php:104
+msgid "Create Basket"
+msgstr "Basket erstellen"
+
+#: application/controllers/IndexController.php:46
+msgid "Create Schema"
+msgstr "Schema erstellen"
+
+#: application/forms/BasketCreateSnapshotForm.php:23
+msgid "Create Snapshot"
+msgstr "Snapshot erstellen"
+
+#: library/Director/Web/Controller/ObjectsController.php:320
+#, php-format
+msgid "Create a new %s Apply Rule"
+msgstr "Erstelle eine neue %s Apply-Regel"
+
+#: library/Director/Web/Controller/ObjectsController.php:361
+#, php-format
+msgid "Create a new %s Set"
+msgstr "Erstelle ein neues %s-Set"
+
+#: library/Director/Web/Controller/TemplateController.php:157
+#, php-format
+msgid "Create a new %s inheriting from this one"
+msgstr "Neuen und von diesem erbenden %s erstellen"
+
+#: library/Director/Web/Controller/TemplateController.php:147
+#: library/Director/Web/Controller/TemplateController.php:167
+#, php-format
+msgid "Create a new %s inheriting from this template"
+msgstr "Einen neuen von dieser Vorlage erbenden %s erstellen"
+
+#: application/controllers/BasketController.php:105
+msgid "Create a new Configuration Basket"
+msgstr "Erstelle einen neuen Konfigurationsbasket"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:23
+#: library/Director/Web/Controller/ObjectController.php:147
+msgid "Create a new Template"
+msgstr "Neue Vorlage erstellen"
+
+#: library/Director/Web/ActionBar/ObjectsActionBar.php:20
+msgid "Create a new object"
+msgstr "Neues Objekt erstellen"
+
+#: library/Director/Web/ActionBar/ChoicesActionBar.php:20
+msgid "Create a new template choice"
+msgstr "Neue Template-Auswahl erstellen"
+
+#: application/forms/KickstartForm.php:37
+msgid "Create database schema"
+msgstr "Datenbankschema erstellen"
+
+#: application/forms/ApplyMigrationsForm.php:31
+msgid "Create schema"
+msgstr "Datenbankschema erstellen"
+
+#: application/controllers/BasketController.php:230
+msgid "Created"
+msgstr "Erstellt"
+
+#: application/controllers/ImportsourceController.php:109
+msgid "Creating Import Sources"
+msgstr "Importquellen erstellen"
+
+#: application/controllers/JobController.php:36
+msgid "Creating Jobs"
+msgstr "Aufträge erstellen"
+
+#: application/controllers/SyncruleController.php:524
+msgid "Creating Sync Rules"
+msgstr "Synchronisationsregeln erstellen"
+
+#: library/Director/Web/Controller/ObjectController.php:146
+msgid "Creating Templates"
+msgstr "Vorlagen erstellen"
+
+#: library/Director/IcingaConfig/StateFilterSet.php:26
+msgid "Critical"
+msgstr "Kritisch"
+
+#: library/Director/Web/Controller/TemplateController.php:186
+msgid "Current Template Usage"
+msgstr "Aktuelle Nutzung dieser Vorlage"
+
+#: application/forms/IcingaHostForm.php:104
+msgid "Custom Endpoint Name"
+msgstr "Individueller Endpunktname"
+
+#: application/forms/BasketForm.php:57
+msgid "Custom Selection"
+msgstr "Benutzerdefinierte Auswahl"
+
+#: application/controllers/CustomvarController.php:13
+msgid "Custom Variable"
+msgstr "Benutzerdefinierte Variable"
+
+#: application/controllers/CustomvarController.php:14
+#, php-format
+msgid "Custom Variable variants: %s"
+msgstr "Varianten der benutzerdefinierten Variable: %s"
+
+#: library/Director/Web/Tabs/DataTabs.php:30
+msgid "Custom Variables"
+msgstr "Benutzerdefinierte Variablen"
+
+#: application/controllers/DataController.php:110
+msgid "Custom Vars - Overview"
+msgstr "Benutzerdefinierte Variablen - Übersicht"
+
+#: application/forms/SyncPropertyForm.php:180
+msgid "Custom expression"
+msgstr "Benutzerdefinierter Ausdruck"
+
+#: library/Director/Web/Controller/ObjectController.php:250
+#, php-format
+msgid "Custom fields: %s"
+msgstr "Benutzerdefinierte Felder: \"%s\""
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:25
+msgid "Custom notification"
+msgstr "Benutzerdefinierte Benachrichtigung"
+
+#: library/Director/Web/Form/IcingaObjectFieldLoader.php:266
+#: application/forms/IcingaServiceForm.php:464
+msgid "Custom properties"
+msgstr "Benutzerdefinierte Eigenschaften"
+
+#: application/forms/SyncPropertyForm.php:67
+msgid "Custom variable"
+msgstr "Benutzerdefinierte Variable"
+
+#: application/forms/SyncPropertyForm.php:308
+msgid "Custom variable (vars.)"
+msgstr "Benutzerdefinierte Variable (vars.)"
+
+#: library/Director/Objects/IcingaService.php:756
+#: library/Director/Objects/IcingaHost.php:164
+#: application/controllers/SuggestController.php:253
+#: application/controllers/SuggestController.php:263
+msgid "Custom variables"
+msgstr "Benutzerdefinierte Variablen"
+
+#: library/Director/Dashboard/Dashlet/CustomvarDashlet.php:11
+msgid "CustomVar Overview"
+msgstr "Benutzerdefinierte Variable - Übersicht"
+
+#: library/Director/Import/ImportSourceSql.php:41
+msgid "DB Query"
+msgstr "Datenbank-Abfrage"
+
+#: application/forms/KickstartForm.php:222
+msgid "DB Resource"
+msgstr "Datenbankressource"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:15
+msgid "DN component"
+msgstr "DN Komponente"
+
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:25
+msgid "DNS record type"
+msgstr "DNS-Record-Typ"
+
+#: library/Director/Web/Tabs/MainTabs.php:34
+msgid "Daemon"
+msgstr "Dienst"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:40
+#, php-format
+msgid "Daemon has been stopped %s, was running with PID %s as %s@%s"
+msgstr "Dienst wurde %s gestoppt, lief zuvor mit PID %s als %s@%s"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:62
+#, php-format
+msgid "Daemon is running with PID %s as %s@%s, last refresh happened %s"
+msgstr ""
+"Dienst läuft mit PID %s als %s@%s, die letzte Aktualisierung fand %s statt"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:51
+#, php-format
+msgid ""
+"Daemon keep-alive is outdated, was last seen running with PID %s as %s@%s %s"
+msgstr ""
+"Daemon keep-alive is outdated, was last seen running with PID %s as %s@%s %s"
+
+#: library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php:11
+#: application/controllers/DataController.php:93
+msgid "Data Field Categories"
+msgstr "Datenfeldkategorien"
+
+#: application/forms/DirectorDatafieldForm.php:168
+msgid "Data Field Category"
+msgstr "Datenfeldkategorie"
+
+#: application/controllers/DataController.php:75
+msgid "Data Fields"
+msgstr "Datenfelder"
+
+#: application/controllers/DataController.php:65
+msgid "Data List"
+msgstr "Datenliste"
+
+#: application/forms/SyncRuleForm.php:19
+msgid "Data List Entry"
+msgstr "Datenlisteneintrag"
+
+#: application/controllers/DataController.php:59
+#, php-format
+msgid "Data List: %s"
+msgstr "Datenliste: %s"
+
+#: application/forms/BasketForm.php:34
+msgid "Data Lists"
+msgstr "Datenlisten"
+
+#: library/Director/Web/Tabs/DataTabs.php:24
+msgid "Data field categories"
+msgstr "Datenfeldkategorien"
+
+#: application/forms/DirectorDatafieldCategoryForm.php:17
+msgid ""
+"Data field categories allow to structure Data Fields. Fields with a category "
+"will be shown grouped by category."
+msgstr ""
+"Datenfeldkategorien erlauben es, Datenfelder zu strukturieren. Felder mit "
+"einer Kategorie werden nach Kategorie gruppiert angezeigt."
+
+#: library/Director/Web/Tabs/DataTabs.php:21
+msgid "Data fields"
+msgstr "Datenfelder"
+
+#: application/forms/DirectorDatafieldForm.php:133
+msgid ""
+"Data fields allow you to customize input controls for Icinga custom "
+"variables. Once you defined them here, you can provide them through your "
+"defined templates. This gives you a granular control over what properties "
+"your users should be allowed to configure in which way."
+msgstr ""
+"Datenfelder erlauben es, Eingabefelder für benutzerdefinierte Variablen zu "
+"personalisieren. Sobald diese hier definiert wurden, können sie über "
+"definierte Templates zur Verfügung gestellt werden. Das erlaubt eine "
+"granuläre Kontrolle darüber, welche Eigenschaften in welche Weise "
+"konfigurierbar sein sollen."
+
+#: library/Director/Dashboard/Dashlet/DatafieldDashlet.php:17
+msgid "Data fields make sure that configuration fits your rules"
+msgstr ""
+"Datenfelder sorgen dafür, dass die Konfiguration in vorgegebene Regeln passt"
+
+#: application/forms/DirectorDatalistForm.php:24
+msgid "Data list"
+msgstr "Datenliste"
+
+#: library/Director/Web/Tabs/DataTabs.php:27
+#: application/controllers/DataController.php:32
+msgid "Data lists"
+msgstr "Datenlisten"
+
+#: application/forms/DirectorDatalistForm.php:15
+msgid ""
+"Data lists are mainly used as data providers for custom variables presented "
+"as dropdown boxes boxes. You can manually manage their entries here in "
+"place, but you could also create dedicated sync rules after creating a new "
+"empty list. This would allow you to keep your available choices in sync with "
+"external data providers"
+msgstr ""
+"Datenlisten werden hauptsächlich als Datenquelle für benutzerdefinierte "
+"Variablen benutzt, die in Dropdown-Auswahlfeldern präsentiert werden. Ihre "
+"Einträge können hier manuell, oder über eine dedizierte "
+"Synchronisationsregel nach dem Anlegen einer leeren Liste verwaltet werden. "
+"Letzteres erlaubt das Synchronisieren der verfügbaren Auswahlmöglichkeiten "
+"mit externen Datenquellen"
+
+#: application/forms/DirectorDatafieldForm.php:181
+msgid "Data type"
+msgstr "Datentyp"
+
+#: application/forms/KickstartForm.php:272
+msgid "Database backend"
+msgstr "Datenbankbackend"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:507
+#: application/controllers/BranchController.php:61
+msgid "Date"
+msgstr "Datum"
+
+#: library/Director/Web/Table/IcingaTimePeriodRangeTable.php:45
+#: library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php:51
+#: application/forms/IcingaTimePeriodRangeForm.php:21
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:22
+msgid "Day(s)"
+msgstr "Tag(e)"
+
+#: application/forms/IcingaServiceForm.php:173
+msgid "Deactivate"
+msgstr "Deaktivieren"
+
+#: application/forms/SettingsForm.php:130
+msgid ""
+"Default configuration format. Please note that v1.x is for special "
+"transitional projects only and completely unsupported. There are no plans to "
+"make Director a first-class configuration backends for Icinga 1.x"
+msgstr ""
+"Standardkonfigurationsformat. Bitte beachten, dass v1.x nur für ganz "
+"spezielle Migrationsprojekte implementiert wurde und in keiner Weise "
+"unterstützt wird. Es gibt keine Pläne aus dem Director ein "
+"Konfigurationswerkzeug für Icinga 1.x zu machen"
+
+#: application/forms/SettingsForm.php:32
+msgid "Default global zone"
+msgstr "Globale Standard-Zone"
+
+#: library/Director/Dashboard/CommandsDashboard.php:23
+msgid ""
+"Define Check-, Notification- or Event-Commands. Command definitions are the "
+"glue between your Host- and Service-Checks and the Check plugins on your "
+"Monitoring (or monitored) systems"
+msgstr ""
+"Definiere Check-, Benachrichtigungs- oder Event-Kommandos. "
+"Kommandodefinitionen sind das Verbindungsstück zwischen Host- und Service-"
+"Checks und den Check-Plugins auf dem Monitoring- (oder dem überwachten) "
+"System"
+
+#: library/Director/Dashboard/Dashlet/DatafieldDashlet.php:11
+msgid "Define Data Fields"
+msgstr "Datenfelder definieren"
+
+#: library/Director/Dashboard/Dashlet/HostGroupsDashlet.php:17
+msgid ""
+"Define Host Groups to give your configuration more structure. They are "
+"useful for Dashboards, Notifications or Restrictions"
+msgstr ""
+"Definieren Host-Gruppen um der Konfiguration mehr Struktur zu geben. "
+"Nützlich sind diese auch für Dashboards, Benachrichtigungen oder "
+"Berechtigungen"
+
+#: application/forms/SelfServiceSettingsForm.php:116
+msgid ""
+"Define a download Url or local directory from which the a specific Icinga 2 "
+"Agent MSI Installer package should be fetched. Please ensure to only define "
+"the base download Url or Directory. The Module will generate the MSI file "
+"name based on your operating system architecture and the version to install. "
+"The Icinga 2 MSI Installer name is internally build as follows: Icinga2-"
+"v[InstallAgentVersion]-[OSArchitecture].msi (full example: Icinga2-v2.6.3-"
+"x86_64.msi)"
+msgstr ""
+"Definieren einen Download-URL oder ein lokales Verzeichnis von welchem ein "
+"bestimmtes MSI-Installer-Paket für den Icinga-2-Agenten geladen werden soll. "
+"Bitte sicherstellen, dass nur Basis-URL oder -Verzeichnis angegeben werden. "
+"Das Powershell-Modul erstellt den MSI-Dateinamen auf Basis der jeweiligen "
+"Betriebssystemarchitektur und zu installierenden Version. Intern wird der "
+"MSI-Installer-Name wie folgt erstellt: Icinga2-v[InstallAgentVersion]-"
+"[OSArchitecture].msi (volles Beispiel: Icinga2-v2.6.3-x86_64.msi)"
+
+#: library/Director/Dashboard/Dashlet/ImportSourceDashlet.php:29
+msgid "Define and manage imports from various data sources"
+msgstr "Definiert und verwaltet Importe von diversen Datenquellen"
+
+#: library/Director/Dashboard/TimeperiodsDashboard.php:14
+msgid "Define custom Time Periods"
+msgstr "Benutzerdefinierte Zeiträume festlegen"
+
+#: library/Director/Dashboard/Dashlet/SyncDashlet.php:29
+msgid "Define how imported data should be synchronized with Icinga"
+msgstr ""
+"Definieren, wie importierte Daten mit Icinga synchronisiert werden sollen"
+
+#: application/forms/SyncRuleForm.php:54
+msgid ""
+"Define what should happen when an object with a matching key already exists. "
+"You could merge its properties (import source wins), replace it completely "
+"with the imported object or ignore it (helpful for one-time imports). "
+"\"Update only\" means that this Rule would never create (or delete) full "
+"Objects."
+msgstr ""
+"Angeben, was geschehen soll wenn ein Objekt mit gleichem Schlüssel bereits "
+"existiert. Die Eigenschaften können zusammengeführt (Importquelle gewinnt), "
+"durch das importierte Objekt ersetzt oder ignoriert (hilfreich für einmalige "
+"Importe) werden."
+
+#: library/Director/Dashboard/ObjectsDashboard.php:15
+msgid "Define whatever you want to be monitored"
+msgstr "Angeben, was überwacht werden soll"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1393
+msgid "Defines after how many check attempts a new hard state is reached"
+msgstr ""
+"Legt fest, nach wie vielen Versuchen ein neuer Hard State erreicht wird"
+
+#: library/Director/Dashboard/Dashlet/UserGroupsDashlet.php:17
+msgid ""
+"Defining Notifications for User Groups instead of single Users gives more "
+"flexibility"
+msgstr ""
+"Für Benutzergruppen anstelle einzelner Benutzer definierte "
+"Benachrichtigungen geben mehr Flexibilität"
+
+#: library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php:17
+msgid ""
+"Defining Service Groups get more structure. Great for Dashboards. "
+"Notifications and Permissions might be based on groups."
+msgstr ""
+"Definiere Service-Gruppen für mehr Struktur. Nützlich für Dashboards. "
+"Benachrichtigungen und Berechtigungen können auf Gruppen basieren."
+
+#: application/forms/IcingaNotificationForm.php:199
+msgid "Delay unless the first notification should be sent"
+msgstr "Verzögerung bis die erste Benachrichtigung verschickt werden soll"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:925
+#: application/forms/IcingaObjectFieldForm.php:193
+#: application/forms/SyncRuleForm.php:87
+msgid "Delete"
+msgstr "Löschen"
+
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:13
+msgid "Delimiter"
+msgstr "Trenner"
+
+#: library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php:13
+#: library/Director/Db/Branch/BranchModificationInspection.php:47
+#: application/forms/BasketForm.php:33
+msgid "Dependencies"
+msgstr "Abhängigkeiten"
+
+#: application/forms/SyncRuleForm.php:24
+msgid "Dependency"
+msgstr "Abhängigkeit"
+
+#: application/forms/DeploymentLinkForm.php:94
+msgid "Deploy"
+msgstr "Ausrollen"
+
+#: application/forms/DeployConfigForm.php:37
+#, php-format
+msgid "Deploy %d pending changes"
+msgstr "%d ausstehende Änderungen ausrollen"
+
+#: library/Director/Dashboard/DeploymentDashboard.php:15
+msgid "Deploy configuration to your Icinga nodes"
+msgstr "Konfiguration auf Icinga Knoten ausrollen"
+
+#: library/Director/Job/ConfigJob.php:45
+msgid "Deploy modified config"
+msgstr "Veränderte Konfiguration ausrollen"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:54
+#: application/controllers/ConfigController.php:270
+#: application/controllers/ConfigController.php:507
+msgid "Deployment"
+msgstr "Deployment"
+
+#: application/forms/SettingsForm.php:165
+msgid "Deployment Path"
+msgstr "Deployment-Pfad"
+
+#: application/controllers/DeploymentController.php:22
+msgid "Deployment details"
+msgstr "Deployment-Details"
+
+#: application/forms/SettingsForm.php:150
+msgid "Deployment mode"
+msgstr "Ausrollmodus"
+
+#: application/forms/SettingsForm.php:159
+msgid "Deployment mode for Icinga 1 configuration"
+msgstr "Ausrollmodus für Icinga 1 Konfiguration"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:77
+msgid "Deployment time"
+msgstr "Deployment-Zeit"
+
+#: configuration.php:178
+#: library/Director/Web/Tabs/InfraTabs.php:36
+#: application/controllers/ConfigController.php:57
+msgid "Deployments"
+msgstr "Deployments"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:87
+msgid "Deprecated"
+msgstr "Abgekündigt"
+
+#: application/forms/IcingaCommandArgumentForm.php:31
+#: application/forms/ImportSourceForm.php:24
+#: application/forms/DirectorDatafieldCategoryForm.php:31
+#: application/forms/DirectorDatafieldForm.php:159
+#: application/forms/IcingaServiceSetForm.php:102
+#: application/forms/IcingaObjectFieldForm.php:133
+#: application/forms/SyncRuleForm.php:36
+#: application/forms/ImportRowModifierForm.php:52
+#: application/forms/IcingaTemplateChoiceForm.php:56
+msgid "Description"
+msgstr "Beschreibung"
+
+#: application/forms/IcingaCommandArgumentForm.php:32
+msgid "Description of the argument"
+msgstr "Beschreibung des Arguments"
+
+#: library/Director/Web/Table/SyncpropertyTable.php:63
+msgid "Destination"
+msgstr "Ziel"
+
+#: application/forms/SyncPropertyForm.php:48
+msgid "Destination Field"
+msgstr "Zielfeld"
+
+#: application/controllers/HealthController.php:25
+msgid ""
+"Did you know that you can run this entire Health Check (or just some "
+"sections) as an Icinga Check on a regular base?"
+msgstr ""
+"Wussten Sie, dass sich dieser Health-Check (oder auch nur Abschnitte daraus) "
+"als regelmäßiger Icinga-Check ausführen lässt?"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:367
+#: application/controllers/ConfigController.php:426
+msgid "Diff"
+msgstr "Diff"
+
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:84
+msgid "Diff with other config"
+msgstr "Mit anderer Konfiguration vergleichen"
+
+#: library/Director/Web/Table/TemplateUsageTable.php:55
+msgid "Direct"
+msgstr "Direkt"
+
+#: application/controllers/DaemonController.php:20
+#: application/controllers/DaemonController.php:22
+msgid "Director Background Daemon"
+msgstr "Director Hintergrunddienst"
+
+#: application/controllers/HealthController.php:17
+msgid "Director Health"
+msgstr "Director-Health"
+
+#: application/controllers/KickstartController.php:16
+msgid "Director Kickstart Wizard"
+msgstr "Director Kickstart-Assistent"
+
+#: library/Director/Import/ImportSourceDirectorObject.php:69
+msgid "Director Object"
+msgstr "Director-Objekt"
+
+#: library/Director/Dashboard/Dashlet/SettingsDashlet.php:11
+msgid "Director Settings"
+msgstr "Director-Einstellungen"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:81
+msgid "Director database schema has not been created yet"
+msgstr "Datenbankschema für Director wurde noch nicht erstellt"
+
+#: application/forms/SyncRuleForm.php:88
+msgid "Disable"
+msgstr "Deaktivien"
+
+#: application/forms/IcingaDependencyForm.php:159
+msgid "Disable Checks"
+msgstr "Checks deaktivieren"
+
+#: application/forms/IcingaDependencyForm.php:167
+msgid "Disable Notificiations"
+msgstr "Benachrichtigungen deaktivieren"
+
+#: application/forms/SettingsForm.php:54
+msgid "Disable all Jobs"
+msgstr "Alle Aufträge deaktivieren"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1296
+#: application/forms/DirectorJobForm.php:37
+msgid "Disabled"
+msgstr "Deaktiviert"
+
+#: application/forms/IcingaCommandForm.php:79
+msgid "Disabled by default, and should only be used in rare cases."
+msgstr ""
+"Standardmäßig deaktiviert, sollte nur in seltenen Fällen benutzt werden."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1297
+msgid "Disabled objects will not be deployed"
+msgstr "Deaktivierte Objekte werden nicht ausgerollt"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1312
+#: application/forms/IcingaTimePeriodForm.php:20
+msgid "Display Name"
+msgstr "Anzeigename"
+
+#: application/forms/IcingaUserForm.php:133
+#: application/forms/IcingaHostForm.php:352
+msgid "Display name"
+msgstr "Anzeigename"
+
+#: library/Director/Web/Table/CustomvarTable.php:42
+msgid "Distinct Commands"
+msgstr "Unterschiedliche Kommandos"
+
+#: library/Director/Dashboard/DataDashboard.php:16
+msgid "Do more with custom data"
+msgstr "Mehr mit benutzerdefinierten Daten anfangen"
+
+#: application/forms/SelfServiceSettingsForm.php:36
+msgid "Do not transform at all"
+msgstr "Überhaupt nicht umwandeln"
+
+#: library/Director/Web/SelfService.php:177
+msgid "Documentation"
+msgstr "Dokumentation"
+
+#: application/forms/SelfServiceSettingsForm.php:50
+msgid "Don't care, my host settings are fine"
+msgstr "Keine Sorge, meine Host-Einstellungen sind in Ordnung"
+
+#: library/Director/IcingaConfig/StateFilterSet.php:21
+msgid "Down"
+msgstr "Down"
+
+#: library/Director/Web/SelfService.php:249
+#: library/Director/Web/SelfService.php:262
+#: application/controllers/SchemaController.php:80
+#: application/controllers/BasketController.php:214
+msgid "Download"
+msgstr "Download"
+
+#: application/controllers/ImportsourceController.php:176
+msgid "Download JSON"
+msgstr "JSON herunterladen"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:59
+msgid "Download as JSON"
+msgstr "Als JSON herunterladen"
+
+#: application/forms/SelfServiceSettingsForm.php:83
+msgid "Download from a custom url"
+msgstr "Von einer benutzerdefinierten URL laden"
+
+#: application/forms/SelfServiceSettingsForm.php:82
+msgid "Download from packages.icinga.com"
+msgstr "Von packages.icinga.com herunterladen"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:30
+msgid "Downtime ends"
+msgstr "Downtime endet"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:21
+msgid "Downtime name"
+msgstr "Downtime-Name"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:31
+msgid "Downtime removed"
+msgstr "Downtime entfernt"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:29
+msgid "Downtime starts"
+msgstr "Downtime startet"
+
+#: application/forms/IcingaForgetApiKeyForm.php:22
+msgid "Drop Self Service API key"
+msgstr "Selbstbedienungs-API-Schlüssel verwerfen"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayToRow.php:23
+#: library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php:30
+msgid "Drop the current row"
+msgstr "Die aktuelle Zeile verwerfen"
+
+#: library/Director/DataType/DataTypeDatalist.php:150
+msgid "Dropdown (list values only)"
+msgstr "Aufklappmenü (nur Listeneinträge)"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:83
+#: library/Director/Web/Widget/SyncRunDetails.php:29
+#: application/forms/IcingaScheduledDowntimeForm.php:56
+msgid "Duration"
+msgstr "Dauer"
+
+#: library/Director/PropertyModifier/PropertyModifierListToObject.php:18
+msgid ""
+"Each Array in the list must contain this property. It's value will be used "
+"as the key/object property name for the row."
+msgstr ""
+"Jedes Array in der Liste enthält diese Eigenschaft. Dessen Wert wird als "
+"Schlüssel/Objektbezeichner für die Zeile benutzt."
+
+#: application/controllers/DatafieldcategoryController.php:37
+msgid "Edit a Category"
+msgstr "Kategorie bearbeiten"
+
+#: application/controllers/DatafieldController.php:31
+msgid "Edit a Field"
+msgstr "Ein Feld bearbeiten"
+
+#: application/controllers/DataController.php:397
+msgid "Edit list"
+msgstr "Liste bearbeiten"
+
+#: library/Director/DataType/DataTypeDatalist.php:144
+msgid "Element behavior"
+msgstr "Verhalten des Elements"
+
+#: application/forms/IcingaUserForm.php:36
+msgid "Email"
+msgstr "E-Mail"
+
+#: application/forms/SettingsForm.php:69
+msgid "Enable audit log"
+msgstr "Revisionslog aktivieren"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1446
+msgid "Enable event handler"
+msgstr "Eventhandler aktivieren"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1458
+msgid "Enable flap detection"
+msgstr "Flap-Erkennung aktivieren"
+
+#: application/forms/SettingsForm.php:111
+msgid ""
+"Enabled the feature for custom endpoint names, where you can choose a "
+"different name for the generated endpoint object. This uses some Icinga "
+"config snippets and a special custom variable. Please do NOT enable this, "
+"unless you really need divergent endpoint names!"
+msgstr ""
+"Aktiviere das Feature für freie Endpunktnamen. Hiermit lässt sich ein "
+"anderer Name für das generierte Endpunktobjekt wählen. Hierzu werden "
+"spezielle Icinga-Konfigurationsschnipsel und eine spezielle "
+"benutzerdefinierte Variable eingesetzt. Bitte NICHT aktivieren, wenn "
+"abweichende Endpunktnamen nicht wirklich erforderlich sind!"
+
+#: library/Director/PropertyModifier/PropertyModifierTrim.php:22
+msgid "Ending only"
+msgstr "Nur das Ende"
+
+#: library/Director/Import/ImportSourceDirectorObject.php:72
+#: library/Director/Web/Table/ObjectsTableEndpoint.php:19
+#: application/forms/IcingaEndpointForm.php:24
+#: application/forms/SyncRuleForm.php:25
+msgid "Endpoint"
+msgstr "Endpunkt"
+
+#: application/forms/KickstartForm.php:121
+msgid "Endpoint Name"
+msgstr "Name des Endpunkts"
+
+#: application/forms/IcingaEndpointForm.php:31
+msgid "Endpoint address"
+msgstr "Adresse des Endpunkts"
+
+#: library/Director/Web/Widget/InspectPackages.php:46
+msgid "Endpoint in your Root Zone"
+msgstr "Endpunkte in Rootzone"
+
+#: application/forms/IcingaEndpointForm.php:18
+msgid "Endpoint template name"
+msgstr "Name der Endpunktsvorlage"
+
+#: library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php:17
+#: library/Director/Db/Branch/BranchModificationInspection.php:35
+#: library/Director/Import/ImportSourceCoreApi.php:59
+msgid "Endpoints"
+msgstr "Endpunkte"
+
+#: application/controllers/PhperrorController.php:15
+#: application/controllers/PhperrorController.php:34
+msgid "Error"
+msgstr "Fehler"
+
+#: application/forms/IcingaHostForm.php:86
+msgid "Establish connection"
+msgstr "Verbindung herstellen"
+
+#: application/forms/IcingaServiceForm.php:627
+msgid ""
+"Evaluates the apply for rule for all objects with the custom attribute "
+"specified. E.g selecting \"host.vars.custom_attr\" will generate \"for "
+"(config in host.vars.array_var)\" where \"config\" will be accessible "
+"through \"$config$\". NOTE: only custom variables of type \"Array\" are "
+"eligible."
+msgstr ""
+"Berechnet die Apply-Regel für alle Objekte für welche diese "
+"benutzerdefinierte Eigenschaft spezifiziert wurde. Wählt man z.B. \"host."
+"vars.custom_attr\", wird \"for (config in host.vars.array_var)\" generiert. "
+"Dabei ist \"config\" dann als \"$config$\" zugänglich. HINWEIS: nur "
+"benutzerdefinierte Eigenschaften vom Typ \"Array\" sind wählbar."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1350
+msgid "Event command"
+msgstr "Event-Kommando"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1351
+msgid "Event command definition"
+msgstr "Event-Kommandodefinition"
+
+#: library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php:23
+msgid ""
+"Every Dictionary entry has a key, its value will be provided in this column"
+msgstr ""
+
+#: application/forms/IcingaScheduledDowntimeForm.php:41
+msgid "Every related downtime will show this comment"
+msgstr "Jede zugehörige Downtime wird diesen Kommentar anzeigen"
+
+#: application/forms/IcingaTimePeriodForm.php:70
+msgid "Exclude other time periods from this."
+msgstr "Andere Zeiträume von diesem ausschließen."
+
+#: application/forms/IcingaTimePeriodForm.php:67
+msgid "Exclude period"
+msgstr "Zeitraum ausschließen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1428
+msgid "Execute active checks"
+msgstr "Aktive Checks ausführen"
+
+#: application/forms/DirectorJobForm.php:48
+msgid "Execution interval for this job, in seconds"
+msgstr "Ausführungsintervall dieses Auftrags, in Sekunden"
+
+#: application/forms/SyncPropertyForm.php:251
+msgid "Existing Data Lists"
+msgstr "Vorhandene Datenlisten"
+
+#: application/forms/SyncPropertyForm.php:236
+msgid "Existing templates"
+msgstr "Vorhandene Vorlagen"
+
+#: application/controllers/BranchController.php:46
+msgid "Expected Modification"
+msgstr "Erwartete Änderungen"
+
+#: application/forms/SyncPropertyForm.php:179
+msgid "Expert mode"
+msgstr "Expertenmodus"
+
+#: library/Director/DataType/DataTypeDatalist.php:154
+msgid "Extend the list with new values"
+msgstr "Erweitere die Liste mit neuen Werten"
+
+#: library/Director/Web/Tabs/ObjectsTabs.php:35
+msgid "External"
+msgstr "Extern"
+
+#: application/forms/BasketForm.php:18
+msgid "External Command Definitions"
+msgstr "Externe Kommandodefinitionen"
+
+#: library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php:19
+msgid "External Commands"
+msgstr "Externe Kommandos"
+
+#: library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php:12
+msgid ""
+"External Commands have been defined in your local Icinga 2 Configuration."
+msgstr ""
+"Externe Kommandos wurden in der lokalen Icinga-2-Konfiguration definiert."
+
+#: library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php:19
+msgid "External Notification Commands"
+msgstr "Externe Benachrichtigungskommandos"
+
+#: library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php:12
+#: library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php:12
+msgid ""
+"External Notification Commands have been defined in your local Icinga 2 "
+"Configuration."
+msgstr ""
+"Externe Benachrichtigungskommandos wurden in der lokalen Icinga-2-"
+"Konfiguration definiert."
+
+#: library/Director/Import/ImportSourceDirectorObject.php:83
+msgid "External Objects"
+msgstr "Externe Objekte"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:75
+msgid "FQDN"
+msgstr "FQDN"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:141
+msgid "Failed"
+msgstr "Fehlgeschlagen"
+
+#: application/forms/IcingaImportObjectForm.php:42
+#, php-format
+msgid "Failed to import %s \"%s\""
+msgstr "Import von %s fehlgeschlagen \"%s\""
+
+#: application/forms/SettingsForm.php:109
+msgid "Feature: Custom Endpoint Name"
+msgstr "Feature: Individueller Endpunktname"
+
+#: application/forms/IcingaObjectFieldForm.php:196
+msgid "Field has been removed"
+msgstr "Feld wurde entfernt"
+
+#: library/Director/Web/Table/IcingaObjectDatafieldTable.php:50
+#: library/Director/Web/Table/DatafieldTable.php:50
+#: application/forms/DirectorDatafieldForm.php:141
+msgid "Field name"
+msgstr "Feldname"
+
+#: application/forms/DirectorDatafieldForm.php:182
+msgid "Field type"
+msgstr "Feldtyp"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:103
+msgid "Fields"
+msgstr "Felder"
+
+#: library/Director/Web/Table/GeneratedConfigFileTable.php:84
+#: library/Director/Web/Table/ConfigFileDiffTable.php:82
+msgid "File"
+msgstr "Datei"
+
+#: library/Director/Web/Widget/InspectPackages.php:54
+#, php-format
+msgid "File Content: %s"
+msgstr "Dateiinhalt: %s"
+
+#: library/Director/Web/Widget/InspectPackages.php:52
+#, php-format
+msgid "Files in Stage: %s"
+msgstr "Dateien im Stage: %s"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:44
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:54
+msgid "Filter"
+msgstr "Filter"
+
+#: application/forms/SyncPropertyForm.php:103
+#: application/forms/SyncRuleForm.php:95
+msgid "Filter Expression"
+msgstr "Filterausdruck"
+
+#: configuration.php:80
+msgid "Filter available notification apply rules"
+msgstr "Verfügbare Benachrichtigungs-Apply-Regeln filtern"
+
+#: configuration.php:87
+msgid "Filter available scheduled downtime rules"
+msgstr "Verfügbare Geplante-Downtime-Regeln filtern"
+
+#: configuration.php:73
+msgid "Filter available service apply rules"
+msgstr "Verfügbare Service-Apply-Regeln filtern"
+
+#: configuration.php:94
+msgid ""
+"Filter available service set templates. Use asterisks (*) as wildcards, like "
+"in DB* or *net*"
+msgstr ""
+"Verfügbare Service-Set-Vorlagen filtern. Benutze Sternchen (*) als "
+"Platzhalter, wie in DB* oder *net*"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:29
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:27
+msgid "Filter method"
+msgstr "Filter-Methode"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:33
+msgid "First Element"
+msgstr "Erstes Element"
+
+#: application/forms/IcingaNotificationForm.php:197
+msgid "First notification delay"
+msgstr "Verzögerung der ersten Benachrichtigung"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:47
+msgid "Fixed"
+msgstr "Fix"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:33
+msgid "Flapping"
+msgstr "Flapping"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:35
+msgid "Flapping ends"
+msgstr "Flapping endet"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1479
+msgid ""
+"Flapping lower bound in percent for a service to be considered not flapping"
+msgstr ""
+"Untere Flapping-Grenze in Prozent um einen Service nicht als Flapping "
+"einzuordnen"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:34
+msgid "Flapping starts"
+msgstr "Flapping beginnt"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1466
+msgid "Flapping threshold (high)"
+msgstr "Flapping Schwellewert (hoch)"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1477
+msgid "Flapping threshold (low)"
+msgstr "Flapping Schwellwert (niedrig)"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1468
+msgid "Flapping upper bound in percent for a service to be considered flapping"
+msgstr ""
+"Obere Flapping-Grenze in Prozent um einen Service als Flapping einzuordnen"
+
+#: application/forms/IcingaCloneObjectForm.php:50
+msgid "Flatten all inherited properties, strip imports"
+msgstr "Alle vererbten Eigenschaften flach machen, Importierte entfernen"
+
+#: application/forms/SelfServiceSettingsForm.php:100
+msgid "Flush API directory"
+msgstr "API-Verzeichnis leeren"
+
+#: library/Director/Web/SelfService.php:245
+msgid "For manual configuration"
+msgstr "Manuelle Konfiguration"
+
+#: library/Director/Job/ConfigJob.php:31
+msgid "Force rendering"
+msgstr "Erstellen erzwingen"
+
+#: library/Director/Objects/DirectorDatafield.php:237
+#, php-format
+msgid "Form element could not be created, %s is missing"
+msgstr "Formularelement konnte nicht erstellt werden, %s ist nicht vorhanden"
+
+#: library/Director/Web/Form/QuickForm.php:524
+#: library/Director/Web/Form/QuickForm.php:551
+msgid "Form has successfully been sent"
+msgstr "Formular wurde erfolgreich abgeschickt"
+
+#: application/forms/IcingaServiceVarForm.php:32
+#: application/forms/IcingaHostVarForm.php:32
+msgid "Format"
+msgstr "Format"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:393
+msgid "Former object"
+msgstr "Vorheriges Objekt"
+
+#: application/forms/SelfServiceSettingsForm.php:24
+msgid "Fully qualified domain name (FQDN)"
+msgstr "Voll qualifizierter Domain-Name (FQDN)"
+
+#: application/forms/IcingaGenerateApiKeyForm.php:24
+msgid "Generate Self Service API key"
+msgstr "Selbstservice-API-Schlüssel generieren"
+
+#: library/Director/Web/SelfService.php:114
+msgid "Generate a new key"
+msgstr "Neuen Schlüssel generieren"
+
+#: library/Director/Web/SelfService.php:132
+msgid ""
+"Generated Host keys will continue to work, but you'll no longer be able to "
+"register new Hosts with this key"
+msgstr ""
+"Generierte Host-Schlüssel werden weiterhin funktionieren, aber es wird nicht "
+"mehr möglich sein neue Hosts mit diesem Schlüssel zu registrieren"
+
+#: application/controllers/ConfigController.php:281
+msgid "Generated config"
+msgstr "Erzeugte Konfiguration"
+
+#: library/Director/Dashboard/AlertsDashboard.php:17
+msgid "Get alerts when something goes wrong"
+msgstr "Werde alarmiert, wenn etwas schief läuft"
+
+#: library/Director/Dashboard/Dashlet/CustomvarDashlet.php:17
+msgid "Get an overview of used CustomVars and their variants"
+msgstr "Eine Übersicht über benutzerdefinierte Variablen und deren Varianten"
+
+#: library/Director/PropertyModifier/PropertyModifierGetHostByAddr.php:27
+msgid "Get host by address (Reverse DNS lookup)"
+msgstr "Host über Adresse ermitteln (Reverse DNS Lookup)"
+
+#: library/Director/PropertyModifier/PropertyModifierGetHostByName.php:27
+msgid "Get host by name (DNS lookup)"
+msgstr "Adresse für Hostname ermitteln (DNS Lookup)"
+
+#: application/controllers/ConfigController.php:241
+msgid "Global Director Settings"
+msgstr "Globale Director-Einstellungen"
+
+#: library/Director/Web/SelfService.php:101
+msgid "Global Self Service Setting"
+msgstr "Globale Selbstbedienungs-Einstellungen"
+
+#: application/forms/SelfServiceSettingsForm.php:57
+msgid "Global Zones"
+msgstr "Globale Zonen"
+
+#: application/forms/IcingaZoneForm.php:22
+msgid "Global zone"
+msgstr "Globale Zone"
+
+#: library/Director/PropertyModifier/PropertyModifierJoin.php:13
+msgid "Glue"
+msgstr "Kleben"
+
+#: library/Director/Web/ActionBar/DirectorBaseActionBar.php:40
+#, php-format
+msgid "Go back to \"%s\" Dashboard"
+msgstr "Zurück zum \"%s\" Dashboard"
+
+#: library/Director/Job/ConfigJob.php:57
+msgid "Grace period"
+msgstr "Gnadenfrist"
+
+#: library/Director/Web/Table/GroupMemberTable.php:72
+msgid "Group"
+msgstr "Gruppe"
+
+#: library/Director/PropertyModifier/PropertyModifierSimpleGroupBy.php:14
+msgid "Group by a column, aggregate others"
+msgstr "Gruppiere nach einer Spalte, aggregiere andere"
+
+#: application/forms/IcingaHostForm.php:248
+msgid ""
+"Group has been inherited, but will be overridden by locally assigned group(s)"
+msgstr ""
+"Die Gruppe wurde geerbt, wird aber von lokal gesetzten Gruppen überschrieben"
+
+#: application/forms/SyncPropertyForm.php:318
+msgid "Group membership"
+msgstr "Gruppenmitgliedschaft"
+
+#: library/Director/Web/Controller/ObjectController.php:329
+#, php-format
+msgid "Group membership: %s"
+msgstr "Gruppenmitgliedschaft: %s"
+
+#: library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php:17
+msgid ""
+"Grouping your Services into Sets allow you to quickly assign services often "
+"used together in a single operation all at once"
+msgstr ""
+"Das Bündel von Services in Sets erlaubt es, Services welche häufig gemeinsam "
+"benutzt werden in einem Einzelschritt zuzuweisen"
+
+#: library/Director/Web/Tabs/ObjectsTabs.php:65
+#: application/forms/IcingaUserForm.php:109
+#: application/forms/IcingaHostForm.php:217
+#: application/forms/IcingaServiceForm.php:649
+msgid "Groups"
+msgstr "Gruppen"
+
+#: library/Director/Import/ImportSourceRestApi.php:134
+msgid "HTTP (this is plaintext!)"
+msgstr "HTTP (das ist Klartext)"
+
+#: library/Director/Import/ImportSourceRestApi.php:149
+msgid "HTTP Header"
+msgstr "HTTP-Header"
+
+#: library/Director/Import/ImportSourceRestApi.php:250
+msgid "HTTP proxy"
+msgstr "HTTP-Proxy"
+
+#: library/Director/Import/ImportSourceRestApi.php:133
+msgid "HTTPS (strongly recommended)"
+msgstr "HTTPS (dringend empfohlen)"
+
+#: library/Director/Web/Tabs/MainTabs.php:31
+msgid "Health"
+msgstr "Health"
+
+#: library/Director/Dashboard/Dashlet/SingleServicesDashlet.php:17
+msgid "Here you can find all single services directly attached to single hosts"
+msgstr ""
+"Hier finden sich alle Einzel-Services welche einzelnen Hosts zugewiesen "
+"wurden"
+
+#: library/Director/DataType/DataTypeString.php:27
+#: library/Director/Web/Table/CoreApiFieldsTable.php:89
+msgid "Hidden"
+msgstr "Versteckt"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:70
+msgid "Hide SQL"
+msgstr "SQL verbergeb"
+
+#: application/controllers/HealthController.php:23
+msgid "Hint: Check Plugin"
+msgstr "Hinweis: Check-Plugin"
+
+#: application/forms/IcingaServiceForm.php:166
+msgid "Hints regarding this service"
+msgstr "Hinweise zu diesem Service"
+
+#: library/Director/Web/Tabs/ImportsourceTabs.php:45
+#: library/Director/Web/Tabs/SyncRuleTabs.php:43
+#: library/Director/Web/Tabs/ObjectTabs.php:95
+msgid "History"
+msgstr "Historie"
+
+#: library/Director/TranslationDummy.php:13
+#: library/Director/Import/ImportSourceDirectorObject.php:71
+#: library/Director/Web/Table/ObjectsTableService.php:106
+#: library/Director/Web/Table/ObjectsTableEndpoint.php:20
+#: application/forms/IcingaHostVarForm.php:15
+#: application/forms/IcingaServiceForm.php:597
+#: application/forms/SyncRuleForm.php:12
+#: application/controllers/ServiceController.php:280
+msgid "Host"
+msgstr "Host"
+
+#: library/Director/Objects/IcingaService.php:769
+#: application/controllers/SuggestController.php:262
+msgid "Host Custom variables"
+msgstr "Benutzerdefinierte Host-Variablen"
+
+#: application/forms/SyncRuleForm.php:13
+#: application/forms/BasketForm.php:20
+msgid "Host Group"
+msgstr "Hostgruppe"
+
+#: library/Director/Dashboard/Dashlet/HostGroupsDashlet.php:11
+msgid "Host Groups"
+msgstr "Hostgruppen"
+
+#: application/forms/SelfServiceSettingsForm.php:19
+msgid "Host Name"
+msgstr "Hostname"
+
+#: application/forms/IcingaHostForm.php:169
+#: application/forms/IcingaHostForm.php:186
+msgid "Host Template"
+msgstr "Host-Vorlage"
+
+#: application/forms/BasketForm.php:21
+msgid "Host Template Choice"
+msgstr "Auswahlmöglichkeit für Hostvorlagen"
+
+#: library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php:11
+#: application/forms/BasketForm.php:22
+msgid "Host Templates"
+msgstr "Host-Vorlagen"
+
+#: application/forms/IcingaHostForm.php:327
+#: application/forms/IcingaHostSelfServiceForm.php:35
+msgid "Host address"
+msgstr "Hostadresse"
+
+#: application/forms/IcingaHostForm.php:329
+#: application/forms/IcingaHostSelfServiceForm.php:37
+msgid ""
+"Host address. Usually an IPv4 address, but may be any kind of address your "
+"check plugin is able to deal with"
+msgstr ""
+"Hostadresse. Üblicherweise eine IPv4 Adresse, kann jedoch jede Art von "
+"Adresse sein, mit der das Plugin umgehen kann"
+
+#: configuration.php:99
+msgid "Host configs"
+msgstr "Host-Konfigurationen"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:57
+msgid "Host groups"
+msgstr "Hostgruppen"
+
+#: application/forms/IcingaHostSelfServiceForm.php:25
+msgid "Host name"
+msgstr "Hostname"
+
+#: application/forms/SelfServiceSettingsForm.php:25
+msgid "Host name (local part, without domain)"
+msgstr "Hostname (lokaler Teil, ohne Domain)"
+
+#: library/Director/Dashboard/Dashlet/HostObjectDashlet.php:13
+msgid "Host objects"
+msgstr "Hostobjekte"
+
+#: library/Director/Objects/IcingaService.php:768
+#: library/Director/Objects/IcingaHost.php:163
+#: application/controllers/SuggestController.php:252
+#: application/controllers/SuggestController.php:261
+msgid "Host properties"
+msgstr "Hosteigenschaften"
+
+#: application/controllers/TemplatechoiceController.php:20
+msgid "Host template choice"
+msgstr "Auswahlmöglichkeit für Hostvorlagen"
+
+#: application/controllers/TemplatechoicesController.php:19
+msgid "Host template choices"
+msgstr "Auswahlmöglichkeiten für Hostvorlagen"
+
+#: application/forms/IcingaHostGroupForm.php:14
+msgid "Hostgroup"
+msgstr "Hostgruppe"
+
+#: library/Director/Db/Branch/BranchModificationInspection.php:39
+#: library/Director/Import/ImportSourceCoreApi.php:61
+msgid "Hostgroups"
+msgstr "Hostgruppen"
+
+#: application/forms/IcingaHostForm.php:220
+msgid ""
+"Hostgroups that should be directly assigned to this node. Hostgroups can be "
+"useful for various reasons. You might assign service checks based on "
+"assigned hostgroup. They are also often used as an instrument to enforce "
+"restricted views in Icinga Web 2. Hostgroups can be directly assigned to "
+"single hosts or to host templates. You might also want to consider assigning "
+"hostgroups using apply rules"
+msgstr ""
+"Hostgruppen, die diesem Knoten direkt zugeordnet werden sollen. Hostgruppen "
+"für verschiedene Zwecke verwendet werden. Services können anhand von "
+"Hostgruppen zugeordnet werden. Außerdem werden sie oft zum Umsetzen von "
+"eingeschränkten Ansichten in Icinga Web 2 verwendet. Hostgruppen können "
+"direkt einzelnen Hosts oder Hostvorlagen zugeordnet werden. Auch über Apply "
+"Regeln können Hostgruppen zugewiesen werden"
+
+#: library/Director/Web/Table/IcingaServiceSetHostTable.php:38
+#: library/Director/Web/Table/IcingaHostsMatchingFilterTable.php:48
+#: application/forms/IcingaHostForm.php:39
+msgid "Hostname"
+msgstr "Hostname"
+
+#: library/Director/Import/ImportSourceRestApi.php:262
+msgid "Hostname, IP or <host>:<port>"
+msgstr "Hostname, IP oder <host>:<port>"
+
+#: configuration.php:153
+#: library/Director/Dashboard/Dashlet/HostsDashlet.php:11
+#: library/Director/IcingaConfig/StateFilterSet.php:19
+#: library/Director/DataType/DataTypeDirectorObject.php:56
+#: library/Director/DataType/DataTypeDictionary.php:59
+#: library/Director/Db/Branch/BranchModificationInspection.php:38
+#: library/Director/Import/ImportSourceCoreApi.php:60
+#: library/Director/Web/Table/CustomvarVariantsTable.php:58
+#: library/Director/Web/Table/CustomvarTable.php:43
+#: application/forms/IcingaNotificationForm.php:89
+#: application/forms/IcingaServiceForm.php:711
+#: application/forms/IcingaDependencyForm.php:100
+#: application/forms/IcingaScheduledDowntimeForm.php:89
+msgid "Hosts"
+msgstr "Hosts"
+
+#: application/controllers/ServicesetController.php:85
+#, php-format
+msgid "Hosts using this set: %s"
+msgstr "Hosts welche dieses Set benutzen: %s"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:58
+msgid ""
+"How long the downtime lasts. Only has an effect for flexible (non-fixed) "
+"downtimes. Time in seconds, supported suffixes include ms (milliseconds), s "
+"(seconds), m (minutes), h (hours) and d (days). To express \"90 minutes\" "
+"you might want to write 1h 30m"
+msgstr ""
+"Wie lang die Downtime dauert. Beeinflusst nur flexible (keine fixen) "
+"Downtimes. Zeit in Sekunden, unterstützt auch die Suffixes ms "
+"(Millisekunden), s (Sekunden), m (Minuten), h (Stunden) and d (Tage). Um "
+"\"90 Minuten\" auszudrücken könnte man 1h 30m schreiben"
+
+#: application/forms/DeployFormsBug7530.php:114
+msgid "I know what I'm doing, deploy anyway"
+msgstr "Ich weiß was ich tue, bitte dennoch ausrollen"
+
+#: application/forms/DeployFormsBug7530.php:115
+msgid "I know, please don't bother me again"
+msgstr "Ich weiß, und möchte damit nicht mehr belästigt werden"
+
+#: application/forms/IcingaEndpointForm.php:32
+msgid "IP address / hostname of remote node"
+msgstr "IP Adresse / Hostname des entfernten Knoten"
+
+#: application/forms/KickstartForm.php:133
+msgid ""
+"IP address / hostname of your Icinga node. Please note that this information "
+"will only be used for the very first connection to your Icinga instance. The "
+"Director then relies on a correctly configured Endpoint object. Correctly "
+"configures means that either it's name is resolvable or that it's host "
+"property contains either an IP address or a resolvable host name. Your "
+"Director must be able to reach this endpoint"
+msgstr ""
+"IP Adresse / Hostname des Icinga Knotens. Diese Information wird nur für den "
+"ersten Verbindungsaufbau zur Icinga Instanz verwendet. Danach verlässt sich "
+"der Director auf ein richtig konfiguriertes Endpunkt-Objekt. \"Richtig "
+"konfiguriert\" bedeutet dabei, dass entweder der Name auflösbar ist oder die "
+"host-Eigenschaft entweder eine IP Adresse oder einen auflösbaren Hostname "
+"enthält. Der Director muss diesen Endpunkt erreichen können"
+
+#: application/forms/IcingaHostForm.php:335
+#: application/forms/IcingaHostSelfServiceForm.php:43
+msgid "IPv6 address"
+msgstr "IPv6 Adresse"
+
+#: library/Director/Web/Controller/ObjectsController.php:350
+#, php-format
+msgid "Icinga %s Sets"
+msgstr "Icinga %s-Sets"
+
+#: application/controllers/InspectController.php:38
+#, php-format
+msgid "Icinga 2 - Objects: %s"
+msgstr "Icinga-2-Objekte: %s"
+
+#: application/controllers/InspectController.php:144
+msgid "Icinga 2 API - Status"
+msgstr "Icinga 2 API - Status"
+
+#: library/Director/Web/SelfService.php:216
+msgid "Icinga 2 Client documentation"
+msgstr "Icinga 2 Client-Dokumentation"
+
+#: library/Director/Web/SelfService.php:158
+#: library/Director/Web/SelfService.php:164
+msgid "Icinga 2 Powershell Module"
+msgstr "Icinga 2 Powershell Modul"
+
+#: application/forms/IcingaHostForm.php:147
+#: application/forms/IcingaServiceForm.php:697
+msgid "Icinga Agent and zone settings"
+msgstr "Icinga Agenten- und Zoneneinstellungen"
+
+#: library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php:13
+msgid "Icinga Api users"
+msgstr "Icinga API Benutzer"
+
+#: application/forms/IcingaCommandArgumentForm.php:39
+#: application/forms/IcingaCommandArgumentForm.php:78
+msgid "Icinga DSL"
+msgstr "Icinga-DSL"
+
+#: configuration.php:118
+msgid "Icinga Director"
+msgstr "Icinga-Director"
+
+#: application/controllers/DashboardController.php:45
+msgid "Icinga Director - Main Dashboard"
+msgstr "Icinga Director - Übersichtsdashboard"
+
+#: application/controllers/DaemonController.php:46
+msgid "Icinga Director Background Daemon"
+msgstr "Icinga Director Hintergrunddienst"
+
+#: library/Director/Dashboard/DirectorDashboard.php:15
+msgid "Icinga Director Configuration"
+msgstr "Icinga Director Konfiguration"
+
+#: application/controllers/IndexController.php:45
+#: application/controllers/IndexController.php:52
+#, php-format
+msgid "Icinga Director Setup: %s"
+msgstr "Icinga Director Setup: %s"
+
+#: application/forms/SettingsForm.php:35
+msgid ""
+"Icinga Director decides to deploy objects like CheckCommands to a global "
+"zone. This defaults to \"director-global\" but might be adjusted to a custom "
+"Zone name"
+msgstr ""
+"Icinga Director bevorzugt es, Objekte wie CheckCommands in eine globale Zone "
+"auszubringen. Der Standard dafür ist \"director-global\", es kann aber auch "
+"eine benutzerdefinierte Zone verwendet werden"
+
+#: application/controllers/PhperrorController.php:40
+msgid ""
+"Icinga Director depends on the following modules, please install/upgrade as "
+"required"
+msgstr ""
+"Der Icinga Director benötigt folgende Module, bitte wie angegeben "
+"installieren und/oder aktualisieren"
+
+#: library/Director/Dashboard/Dashlet/SelfServiceDashlet.php:17
+msgid ""
+"Icinga Director offers a Self Service API, allowing new Icinga nodes to "
+"register themselves"
+msgstr ""
+"Der Icinga Director stellt eine Selbstbedienungs-API bereit, über welche "
+"sich neue Icinga-Knoten selbst registrieren können"
+
+#: application/forms/KickstartForm.php:131
+msgid "Icinga Host"
+msgstr "Icinga-Host"
+
+#: library/Director/Dashboard/Dashlet/InfrastructureDashlet.php:11
+msgid "Icinga Infrastructure"
+msgstr "Icinga-Infrastruktur"
+
+#: application/forms/SettingsForm.php:43
+msgid "Icinga Package Name"
+msgstr "Icinga Package Name"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1164
+msgid ""
+"Icinga cluster zone. Allows to manually override Directors decisions of "
+"where to deploy your config to. You should consider not doing so unless you "
+"gained deep understanding of how an Icinga Cluster stack works"
+msgstr ""
+"Icinga-Cluster-Zone. Erlaubt ein manuelles Überschreiben der Entscheidung, "
+"wohin der Director die Konfiguration ausrollt. Sollte nicht ohne "
+"ausreichendes Wissen um den Icinga-Cluster-Stack verändert werden"
+
+#: library/Director/Web/SelfService.php:145
+#: library/Director/Web/SelfService.php:151
+msgid "Icinga for Windows"
+msgstr "Icinga für Windows"
+
+#: application/forms/IcingaHostGroupForm.php:16
+msgid "Icinga object name for this host group"
+msgstr "Icinga-Objektname für diese Hostgruppe"
+
+#: application/forms/IcingaHostForm.php:46
+msgid ""
+"Icinga object name for this host. This is usually a fully qualified host "
+"name but it could basically be any kind of string. To make things easier for "
+"your users we strongly suggest to use meaningful names for templates. E.g. "
+"\"generic-host\" is ugly, \"Standard Linux Server\" is easier to understand"
+msgstr ""
+"Icinga-Objektname für diesen Host. Dies ist üblicherweise ein FQDN, kann "
+"jedoch jede beliebige Zeichenkette sein. Der Einfachheit halber sollten "
+"sprechende Namen für Vorlagen verwendet werden. z.B. ist \"Standard Linux "
+"Server\" leichter verständlich als \"generic-host\""
+
+#: application/forms/IcingaServiceGroupForm.php:16
+msgid "Icinga object name for this service group"
+msgstr "Icinga-Objektname für diese Servicegruppe"
+
+#: application/forms/IcingaUserGroupForm.php:19
+msgid "Icinga object name for this user group"
+msgstr "Icinga-Objektname für diese Benutzergruppe"
+
+#: application/forms/SettingsForm.php:126
+msgid "Icinga v1.x"
+msgstr "Icinga v1.x"
+
+#: application/forms/SettingsForm.php:100
+msgid ""
+"Icinga v2.11.0 breaks some configurations, the Director will warn you before "
+"every deployment in case your config is affected. This setting allows to "
+"hide this warning."
+msgstr ""
+"Mit Icinga v2.11.0 schlagen bestimmte Konfigurationen fehl, der Director "
+"warnt vor jedem Deployment falls die auszurollende Konfiguration betroffen "
+"ist. Diese Einstellung erlaubt es diese Warnung zu verbergen."
+
+#: application/forms/SettingsForm.php:125
+msgid "Icinga v2.x"
+msgstr "Icinga v2.x"
+
+#: application/forms/IcingaHostForm.php:77
+msgid "Icinga2 Agent"
+msgstr "Icinga-2-Agent"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1556
+msgid "Icon image"
+msgstr "Icon Bild"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1565
+msgid "Icon image alt"
+msgstr "Icon Bild alternativ"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:81
+msgid "Id"
+msgstr "ID"
+
+#: application/forms/IcingaCommandForm.php:52
+msgid "Identifier for the Icinga command you are going to create"
+msgstr "Bezeichner für das Icinga-Kommando, das erstellt werden soll"
+
+#: application/forms/IcingaCommandForm.php:78
+msgid "If enabled you can not define arguments."
+msgstr "Wenn aktiviert können keine Argumente definiert werden."
+
+#: application/forms/SyncRuleForm.php:64
+#: application/forms/BasketForm.php:55
+msgid "Ignore"
+msgstr "Ignorieren"
+
+#: application/forms/SettingsForm.php:91
+msgid "Ignore Bug #7530"
+msgstr "Bug #7530 ignorieren"
+
+#: application/forms/IcingaDependencyForm.php:175
+msgid "Ignore Soft States"
+msgstr "Soft-States ignorieren"
+
+#: library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php:33
+msgid "Import Source"
+msgstr "Importquelle"
+
+#: application/forms/BasketForm.php:35
+msgid "Import Sources"
+msgstr "Importquellen"
+
+#: library/Director/Dashboard/Dashlet/ImportSourceDashlet.php:14
+msgid "Import data sources"
+msgstr "Import-Datenquellen"
+
+#: application/forms/IcingaImportObjectForm.php:26
+#, php-format
+msgid "Import external \"%s\""
+msgstr "Externe \"%s\" importieren"
+
+#: application/controllers/ImportrunController.php:14
+#: application/controllers/ImportrunController.php:15
+msgid "Import run"
+msgstr "Importlauf"
+
+#: application/controllers/ImportsourceController.php:266
+#, php-format
+msgid "Import run history: %s"
+msgstr "Importlaufhistorie: %s"
+
+#: library/Director/Job/ImportJob.php:80
+#: library/Director/Web/Tabs/ImportsourceTabs.php:37
+#: library/Director/Web/Tabs/ImportTabs.php:20
+#: application/controllers/ImportsourcesController.php:32
+msgid "Import source"
+msgstr "Importquelle"
+
+#: application/forms/ImportSourceForm.php:15
+msgid "Import source name"
+msgstr "Name der Importquelle"
+
+#: application/controllers/ImportsourceController.php:170
+#, php-format
+msgid "Import source preview: %s"
+msgstr "Vorschau der Importquelle: %s"
+
+#: application/controllers/ImportsourceController.php:92
+#: application/controllers/ImportsourceController.php:135
+#, php-format
+msgid "Import source: %s"
+msgstr "Importquelle: %s"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1270
+msgid ""
+"Importable templates, add as many as you want. Please note that order "
+"matters when importing properties from multiple templates: last one wins"
+msgstr ""
+"Importierbare Vorlagen. Es können beliebig viele erstellt werden, wobei die "
+"Reihenfolge wichtig ist, wenn Eigenschaften von mehreren Vorlagen geerbt "
+"werden: Der letzte Eintrag gewinnt"
+
+#: application/forms/ImportRunForm.php:33
+msgid "Imported new data from this Import Source"
+msgstr "Neue Daten von dieser Import-Datenquelle importiert"
+
+#: library/Director/Web/Table/ImportrunTable.php:32
+msgid "Imported rows"
+msgstr "Importierte Reihen"
+
+#: application/forms/IcingaImportObjectForm.php:16
+msgid ""
+"Importing an object means that its type will change from \"external\" to "
+"\"object\". That way it will make part of the next deployment. So in case "
+"you imported this object from your Icinga node make sure to remove it from "
+"your local configuration before issueing the next deployment. In case of a "
+"conflict nothing bad will happen, just your config won't deploy."
+msgstr ""
+"Ein Objekt zu importieren bedeutet, dass es von einem \"externen Objekt\" zu "
+"einem \"Objekt\" verändert wird. Damit wird es mit der nächsten "
+"Konfiguration ausgerollt. Falls dieses Objekt aus dem Icinga Knoten "
+"importiert wurde, muss es aus vor dem nächsten Ausrollen aus der lokalen "
+"Konfiguration entfernt werden. Sollte ein Konflikt auftreten, geschieht "
+"nichts weiter, als dass die Konfiguration nicht ausgerollt werden kann."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1268
+msgid "Imports"
+msgstr "Importe"
+
+#: application/controllers/SelfServiceController.php:110
+msgid ""
+"In case an Icinga Admin provided you with a self service API token, this is "
+"where you can register new hosts"
+msgstr ""
+"Wenn ein Icinga-Administrator einen Selbstbedienungs-API-"
+"Schlüsselbereitstellt, können hier neue Hosts registriert werden"
+
+#: application/forms/SelfServiceSettingsForm.php:189
+msgid ""
+"In case the Icinga 2 Agent is already installed on the system, this "
+"parameter will allow you to configure if you wish to upgrade / downgrade to "
+"a specified version with the as well."
+msgstr ""
+"Wenn der Icinga-2-Agent bereits auf dem System installiert ist, erlaubt es "
+"dieser Parameter festzulegen, ob auch Up- und Downgrades zu der spezifierten "
+"Icinga 2 Version gewünscht sind."
+
+#: application/forms/SelfServiceSettingsForm.php:156
+msgid ""
+"In case the Icinga 2 Agent should be automatically installed, this has to be "
+"a string value like: 2.6.3"
+msgstr ""
+"Falls der Icinga-2-Agent automatisch installiert werden soll, muss dies in "
+"String der Form 2.6.3 sein"
+
+#: application/forms/SelfServiceSettingsForm.php:102
+msgid ""
+"In case the Icinga Agent will accept configuration from the parent Icinga 2 "
+"system, it will possibly write data to /var/lib/icinga2/api/*. By setting "
+"this parameter to true, all content inside the api directory will be flushed "
+"before an eventual restart of the Icinga 2 Agent"
+msgstr ""
+"Wenn der Icinga-Agent Konfiguration von seinem übergeordneten Icinga-2-"
+"System akzeptiert, wird er für gewöhnlich Daten nach /var/lib/icinga2/api/* "
+"schreiben. Wird dieser Schalter auf Ja gesetzt, wird jeglicher Inhalt dieses "
+"Verzeichnisses vor einem eventuellen Restart des Icinga-2-Agenten geleert"
+
+#: library/Director/Web/SelfService.php:147
+#, php-format
+msgid "In case you're using %s, please run this Script:"
+msgstr "Falls %s benutzt wird, bitte dieses Script ausführen:"
+
+#: library/Director/Web/SelfService.php:160
+#, php-format
+msgid "In case you're using the legacy %s, please run:"
+msgstr "Falls das vorherige %s benutzt wird, bitte dieses Script ausführen:"
+
+#: library/Director/Import/ImportSourceRestApi.php:246
+msgid ""
+"In case your API is only reachable through a proxy, please choose it's "
+"protocol right here"
+msgstr ""
+"Wenn die API nur über einen Proxy zugänglich ist, bitte hier dessen "
+"Protokoll auswählen"
+
+#: library/Director/Import/ImportSourceRestApi.php:270
+msgid "In case your proxy requires authentication, please configure this here"
+msgstr ""
+"Falls der Proxy eine Authentifizierung verlangt, kann diese hier "
+"konfiguriert werden"
+
+#: application/forms/IcingaTimePeriodForm.php:62
+msgid "Include other time periods into this."
+msgstr "Andere Zeiträume in diesen einbinden."
+
+#: application/forms/IcingaTimePeriodForm.php:59
+msgid "Include period"
+msgstr "Zeitraum einbinden"
+
+#: library/Director/Web/Table/TemplateUsageTable.php:56
+msgid "Indirect"
+msgstr "Indirekt"
+
+#: application/controllers/HostController.php:214
+#: application/controllers/HostController.php:295
+msgid "Individual Service objects"
+msgstr "Individuelle Service-Objekte"
+
+#: library/Director/Web/Tabs/InfraTabs.php:43
+msgid "Infrastructure"
+msgstr "Infrastruktur"
+
+#: application/forms/SyncPropertyForm.php:312
+msgid "Inheritance (import)"
+msgstr "Vererbung (Import)"
+
+#: library/Director/Web/SelfService.php:195
+msgid "Inherited Template Api Key:"
+msgstr "Vom Template geerbter API-Schlüssel:"
+
+#: application/controllers/HostController.php:232
+#, php-format
+msgid "Inherited from %s"
+msgstr "Geerbt von %s"
+
+#: application/forms/IcingaHostForm.php:256
+msgid "Inherited groups"
+msgstr "Geerbte Gruppen"
+
+#: application/controllers/HostController.php:461
+#, php-format
+msgid "Inherited service: %s"
+msgstr "Geerbter Service: %s"
+
+#: library/Director/ProvidedHook/Monitoring/HostActions.php:36
+#: library/Director/ProvidedHook/Monitoring/ServiceActions.php:43
+#: library/Director/Web/Tabs/ObjectTabs.php:132
+#: application/controllers/HostController.php:621
+msgid "Inspect"
+msgstr "Inspizieren"
+
+#: application/controllers/InspectController.php:63
+msgid "Inspect - object list"
+msgstr "Inspizieren - Objektliste"
+
+#: application/controllers/InspectController.php:170
+msgid "Inspect File Content"
+msgstr "Dateiinhalt inspizieren"
+
+#: application/controllers/InspectController.php:168
+msgid "Inspect Packages"
+msgstr "Pakete Inspizieren"
+
+#: application/forms/SelfServiceSettingsForm.php:202
+msgid "Install NSClient++"
+msgstr "NSClient++ installieren"
+
+#: application/forms/SelfServiceSettingsForm.php:72
+msgid "Installation Source"
+msgstr "Installationsquelle"
+
+#: library/Director/Web/Table/Dependency/DependencyInfoTable.php:39
+msgid "Installed"
+msgstr "Installiert"
+
+#: application/forms/SelfServiceSettingsForm.php:165
+msgid "Installer Hashes"
+msgstr "Prüfsummen für den Installer"
+
+#: application/forms/IcingaCommandForm.php:25
+msgid "Internal commands"
+msgstr "Interne Kommandos"
+
+#: application/controllers/SyncruleController.php:140
+#, php-format
+msgid "It has been renamed since then, its former name was %s"
+msgstr "Es wurde seither umbenannt. Sein bisheriger Name war %s"
+
+#: library/Director/Web/SelfService.php:72
+msgid ""
+"It is not a good idea to do so as long as your Agent still has a valid Self "
+"Service API key!"
+msgstr ""
+"Dies ist keine gute Idee solange der Agent noch über einen gültigen "
+"Selbstbedienungs-API-Schlüssel verfügt!"
+
+#: application/forms/IcingaTemplateChoiceForm.php:87
+msgid ""
+"It will not be allowed to choose more than this many options. Setting it to "
+"one (1) will result in a drop-down box, a higher number will turn this into "
+"a multi-selection element."
+msgstr ""
+"Es wird nicht erlaubt, mehr Optionen zu wählen als durch diese Einstellung "
+"vorgegeben wird. Setzt man sie auf Eins (1) erhält man ein Dropdown-Feld, "
+"eine höhere Nummer verwandelt diese in ein Mehrfach-Auswahlfeld."
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:35
+msgid ""
+"It's currently unknown whether we are in sync with this Import Source. You "
+"should either check for changes or trigger a new Import Run."
+msgstr ""
+"Aktuell ist unbekannt ob die Konfiguration mit dieser Importquelle synchron "
+"ist. Es sollte auf Änderungen geprüft oder ein neuer Importlauf angestoßen "
+"werden."
+
+#: application/controllers/SyncruleController.php:91
+msgid ""
+"It's currently unknown whether we are in sync with this rule. You should "
+"either check for changes or trigger a new Sync Run."
+msgstr ""
+"Aktuell ist unbekannt, ob die Konfiguration mit dieser Regel synchron ist. "
+"Es sollte auf Änderungen geprüft oder ein neuer Importlauf angestoßen werden."
+
+#: application/forms/BasketUploadForm.php:130
+#: application/forms/BasketForm.php:126
+msgid "It's not allowed to store an empty basket"
+msgstr "Das Speichern eines leeren Baskets ist nicht erlaubt"
+
+#: application/controllers/JobController.php:108
+msgid "Job"
+msgstr "Auftrag"
+
+#: application/forms/BasketForm.php:37
+msgid "Job Definitions"
+msgstr "Job-Definitionen"
+
+#: application/forms/DirectorJobForm.php:17
+msgid "Job Type"
+msgstr "Auftragstyp"
+
+#: library/Director/Web/Table/JobTable.php:60
+#: application/forms/DirectorJobForm.php:72
+msgid "Job name"
+msgstr "Auftragsname"
+
+#: application/controllers/JobController.php:26
+#: application/controllers/JobController.php:57
+#, php-format
+msgid "Job: %s"
+msgstr "Auftrag: %s"
+
+#: library/Director/Dashboard/Dashlet/JobDashlet.php:14
+#: library/Director/Web/Tabs/ImportTabs.php:26
+#: application/controllers/JobsController.php:13
+msgid "Jobs"
+msgstr "Aufträge"
+
+#: library/Director/Web/Table/BranchActivityTable.php:70
+#: library/Director/Web/Table/ActivityLogTable.php:220
+msgid "Jump to this object"
+msgstr "Zu diesem Objekt springen"
+
+#: library/Director/Web/SelfService.php:267
+msgid "Just download and run this script on your Linux Client Machine:"
+msgstr ""
+"Einfach dieses Skript herunterladen und auf dem Linux Client-Rechner "
+"ausführen:"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:57
+msgid "Keep matching elements"
+msgstr "Übereinstimmende Elemente behalten"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:73
+msgid "Keep only matching rows"
+msgstr "Behalte nur passende Zeilen"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:31
+msgid "Keep the DN as is"
+msgstr "DN unverändert behalten"
+
+#: library/Director/PropertyModifier/PropertyModifierJsonDecode.php:26
+msgid "Keep the JSON string as is"
+msgstr "JSON-String beibehalten"
+
+#: library/Director/PropertyModifier/PropertyModifierListToObject.php:27
+msgid "Keep the first row with that key"
+msgstr "Behalte die erste Zeile mit diesem Schlüssel"
+
+#: library/Director/PropertyModifier/PropertyModifierListToObject.php:28
+msgid "Keep the last row with that key"
+msgstr "Behalte die letzte Zeile mit diesem Schlüssel"
+
+#: library/Director/PropertyModifier/PropertyModifierGetHostByAddr.php:18
+#: library/Director/PropertyModifier/PropertyModifierGetHostByName.php:18
+msgid "Keep the property (hostname) as is"
+msgstr "Die Eigenschaft (hostname) behalten, wie sie ist"
+
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:40
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:35
+msgid "Keep the property as is"
+msgstr "Die Eigenschaft beibehalten"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayToRow.php:25
+#: library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php:32
+msgid "Keep the row, set the column value to null"
+msgstr "Behalte die Zeile so wie sie ist, setze den Spaltenwert auf null"
+
+#: library/Director/Web/Table/DatalistEntryTable.php:54
+#: application/forms/DirectorDatalistEntryForm.php:21
+msgid "Key"
+msgstr "Schlüssel"
+
+#: application/controllers/DataController.php:327
+msgid "Key / Instance"
+msgstr "Schlüssel / Instanz"
+
+#: library/Director/PropertyModifier/PropertyModifierListToObject.php:15
+msgid "Key Property"
+msgstr "Schlüsseleigenschaft"
+
+#: library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php:21
+msgid "Key Property Name"
+msgstr "Schlüsseleigenschaft"
+
+#: application/forms/ImportSourceForm.php:87
+msgid "Key column name"
+msgstr "Schlüsselspaltenname"
+
+#: application/controllers/KickstartController.php:15
+#: application/controllers/KickstartController.php:17
+msgid "Kickstart"
+msgstr "Kickstart"
+
+#: library/Director/Dashboard/Dashlet/KickstartDashlet.php:11
+#: application/forms/KickstartForm.php:292
+msgid "Kickstart Wizard"
+msgstr "Kickstart Assistent"
+
+#: library/Director/Import/ImportSourceLdap.php:50
+msgid "LDAP Search Base"
+msgstr "LDAP Suchbasis"
+
+#: library/Director/Web/Table/DatalistEntryTable.php:55
+#: library/Director/Web/Table/IcingaObjectDatafieldTable.php:49
+#: library/Director/Web/Table/DatafieldTable.php:49
+#: application/forms/DirectorDatalistEntryForm.php:30
+msgid "Label"
+msgstr "Bezeichnung"
+
+#: library/Director/Web/Widget/IcingaObjectInspection.php:58
+msgid "Last Check Result"
+msgstr "Letztes Check-Ergebnis"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:34
+msgid "Last Element"
+msgstr "Letztes Element"
+
+#: application/forms/IcingaNotificationForm.php:208
+msgid "Last notification"
+msgstr "Letzte Benachrichtigung"
+
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:77
+msgid "Last related activity"
+msgstr "Zuletzt verwendete Konfiguration"
+
+#: application/controllers/SyncruleController.php:136
+msgid "Last sync run details"
+msgstr "Details des letzten Imports"
+
+#: application/forms/IcingaCommandArgumentForm.php:69
+msgid ""
+"Leave empty for non-positional arguments. Can be a positive or negative "
+"number and influences argument ordering"
+msgstr ""
+"Leer lassen für Argumente, deren Position egal ist. Kann eine positive oder "
+"negative Zahl sein und beeinflusst die Reihenfolge der Argumente"
+
+#: application/forms/DirectorDatafieldForm.php:44
+msgid ""
+"Leaving custom variables in place while removing the related field is "
+"perfectly legal and might be a desired operation. This way you can no longer "
+"modify related custom variables in the Director GUI, but the variables "
+"themselves will stay there and continue to be deployed. When you re-add a "
+"field for the same variable later on, everything will continue to work as "
+"before"
+msgstr ""
+"Benutzerdefinierte Eigenschaften beizubehalten während das zugehörige Feld "
+"entfernt wird ist durchaus legal und kann eine gewünschte Operation sein. "
+"Auf diesem Wege lassen sich zugehörige Felder dann nicht mehr in der "
+"Director-Oberfläche bearbeiten, aber die jeweiligen Werte bleiben "
+"konfiguriert und werden weiterhin ausgerollt. Wenn man ein Feld mit "
+"demselben Namen zu einem späteren Zeitpunkt wieder hinzufügt, wird alles "
+"wieder funktionieren wie zuvor"
+
+#: application/forms/DirectorDatafieldForm.php:87
+msgid ""
+"Leaving custom variables in place while renaming the related field is "
+"perfectly legal and might be a desired operation. This way you can no longer "
+"modify related custom variables in the Director GUI, but the variables "
+"themselves will stay there and continue to be deployed. When you re-add a "
+"field for the same variable later on, everything will continue to work as "
+"before"
+msgstr ""
+"Benutzerdefinierte Eigenschaften beizubehalten während das zugehörige Feld "
+"umbenannt wird ist durchaus legal und kann eine gewünschte Operation sein. "
+"Auf diesem Wege lassen sich zugehörige Felder dann nicht mehr in der "
+"Director-Oberfläche bearbeiten, aber die jeweiligen Werte bleiben "
+"konfiguriert und werden weiterhin ausgerollt. Wenn man ein Feld mit "
+"demselben Namen zu einem späteren Zeitpunkt wieder hinzufügt, wird alles "
+"wieder funktionieren wie zuvor"
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:35
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:45
+msgid "Let the import fail"
+msgstr "Import fehlschlagen lassen"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:55
+msgid "Let the whole Import Run fail"
+msgstr "Den ganzen Importlauf fehlschlagen lassen"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:32
+#: library/Director/PropertyModifier/PropertyModifierGetHostByAddr.php:19
+#: library/Director/PropertyModifier/PropertyModifierListToObject.php:26
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:41
+#: library/Director/PropertyModifier/PropertyModifierJsonDecode.php:27
+#: library/Director/PropertyModifier/PropertyModifierArrayToRow.php:24
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:36
+#: library/Director/PropertyModifier/PropertyModifierGetHostByName.php:19
+#: library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php:31
+msgid "Let the whole import run fail"
+msgstr "Den ganzen Importlauf fehlschlagen lassen"
+
+#: configuration.php:54
+msgid "Limit access to the given comma-separated list of hostgroups"
+msgstr ""
+"Den Zugriff auf diese Kommagetrennte Liste von Host-Gruppen beschränken"
+
+#: library/Director/Web/SelfService.php:260
+msgid "Linux commandline"
+msgstr "Linux Kommandozeile"
+
+#: application/controllers/DataController.php:125
+msgid "List Entries"
+msgstr "Listeneinträge"
+
+#: application/controllers/DataController.php:401
+msgid "List entries"
+msgstr "Listeneinträge"
+
+#: library/Director/Web/Table/DatalistTable.php:31
+#: application/forms/DirectorDatalistForm.php:13
+msgid "List name"
+msgstr "Listenname"
+
+#: library/Director/Import/ImportSourceRestApi.php:213
+msgid "Literal dots in a key name can be written in the escape notation:"
+msgstr "Punkte im Schlüsselbezeichner können bei Bedarf verschont werden:"
+
+#: application/forms/SettingsForm.php:167
+msgid ""
+"Local directory to deploy Icinga 1.x configuration. Must be writable by "
+"icingaweb2. (e.g. /etc/icinga/director)"
+msgstr ""
+"Lokales Verzeichnis in welches die Icinga 1.x Konfiguration ausgerollt "
+"werden soll. Muss von icingaweb2 beschreibbar sein (z.B. /etc/icinga/"
+"director)"
+
+#: application/forms/IcingaEndpointForm.php:41
+msgid "Log Duration"
+msgstr "Behaltefrist des Logs"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:576
+#: application/forms/IcingaAddServiceForm.php:67
+msgid "Main properties"
+msgstr "Haupteigenschaften"
+
+#: library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php:12
+msgid ""
+"Manage definitions for your Commands that should be executed as Check "
+"Plugins, Notifications or based on Events"
+msgstr ""
+"Definitionen für Kommandos verwalten, welche als Check-Plugins "
+"Benachrichtigungen oder Event-basiert ausgeführt werden sollen"
+
+#: library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php:17
+msgid ""
+"Manage your Host Templates. Use Fields to make it easy for your users to get "
+"them customized."
+msgstr ""
+"Host-Vorlagen verwalten. Verwende Felder um Benutzern deren Anpassung zu "
+"erleichtern."
+
+#: library/Director/Dashboard/Dashlet/InfrastructureDashlet.php:17
+msgid ""
+"Manage your Icinga 2 infrastructure: Masters, Zones, Satellites and more"
+msgstr "Icinga 2 Infrastruktur verwalten: Master, Zonen, Satelliten und mehr"
+
+#: library/Director/Dashboard/CommandsDashboard.php:17
+msgid "Manage your Icinga Commands"
+msgstr "Icinga Kommandos verwalten"
+
+#: library/Director/Dashboard/HostsDashboard.php:16
+msgid "Manage your Icinga Hosts"
+msgstr "Icinga Hosts verwalten"
+
+#: library/Director/Dashboard/InfrastructureDashboard.php:24
+msgid "Manage your Icinga Infrastructure"
+msgstr "Icinga 2 Infrastruktur verwalten"
+
+#: library/Director/Dashboard/ServicesDashboard.php:18
+msgid "Manage your Icinga Service Checks"
+msgstr "Icinga Service Checks verwalten"
+
+#: library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php:17
+msgid ""
+"Manage your Service Templates. Use Fields to make it easy for your users to "
+"get them customized."
+msgstr ""
+"Service-Vorlagen verwalten. Verwende Felder um Benutzern deren Anpassung zu "
+"erleichtern."
+
+#: library/Director/Web/Controller/ObjectController.php:254
+msgid "Managing Fields"
+msgstr "Datenfelder verwalten"
+
+#: library/Director/Web/Table/IcingaObjectDatafieldTable.php:51
+#: application/forms/IcingaObjectFieldForm.php:142
+#: application/forms/IcingaObjectFieldForm.php:147
+msgid "Mandatory"
+msgstr "Pflicht"
+
+#: application/forms/SettingsForm.php:155
+msgid "Master-less"
+msgstr "Masterlos"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:35
+msgid "Match NULL value columns"
+msgstr "Triff auf Spalten NULL-Werten zu"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:34
+msgid "Match boolean FALSE"
+msgstr "Triff auf boolesches FALSE zu"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:33
+msgid "Match boolean TRUE"
+msgstr "Triff auf boolesches TRUE zu"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1391
+msgid "Max check attempts"
+msgstr "Maximale Checkwiederholungen"
+
+#: library/Director/Web/Table/GroupMemberTable.php:73
+#: library/Director/Web/Table/GroupMemberTable.php:78
+msgid "Member"
+msgstr "Mitglied"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:114
+msgid "Members"
+msgstr "Mitglieder"
+
+#: library/Director/Web/Controller/ObjectController.php:615
+#: application/forms/SyncRuleForm.php:62
+msgid "Merge"
+msgstr "Zusammenführen"
+
+#: application/forms/SyncPropertyForm.php:117
+msgid "Merge Policy"
+msgstr "Zusammenführungsrichtlinie"
+
+#: application/forms/IcingaTimePeriodRangeForm.php:23
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:24
+msgid ""
+"Might be monday, tuesday or 2016-01-28 - have a look at the documentation "
+"for more examples"
+msgstr ""
+"Mögliche Werte sind z.B. monday, tuesday oder 2016-01-28 - Mehr Beispiele "
+"finden sich in der Dokumentation"
+
+#: application/forms/IcingaTemplateChoiceForm.php:73
+msgid "Minimum required"
+msgstr "Erforderliches Minimum"
+
+#: application/controllers/BranchController.php:112
+msgid "Modification"
+msgstr "Änderung"
+
+#: application/forms/ImportRowModifierForm.php:69
+msgid "Modifier"
+msgstr "Modifikator"
+
+#: library/Director/Web/Tabs/ImportsourceTabs.php:41
+msgid "Modifiers"
+msgstr "Modifikatoren"
+
+#: library/Director/ProvidedHook/Monitoring/HostActions.php:55
+#: library/Director/ProvidedHook/Monitoring/ServiceActions.php:56
+#: library/Director/ProvidedHook/Monitoring/ServiceActions.php:62
+#: library/Director/Web/ActionBar/AutomationObjectActionBar.php:38
+#: library/Director/Web/Tabs/SyncRuleTabs.php:37
+#: library/Director/Web/Controller/TemplateController.php:121
+#: application/controllers/ImportsourceController.php:126
+#: application/controllers/SyncruleController.php:553
+msgid "Modify"
+msgstr "Ändere"
+
+#: library/Director/ProvidedHook/CubeLinks.php:52
+#: library/Director/ProvidedHook/IcingaDbCubeLinks.php:53
+#, php-format
+msgid "Modify %d hosts"
+msgstr "%d Hosts verändern"
+
+#: library/Director/Web/Controller/ObjectsController.php:215
+#, php-format
+msgid "Modify %d objects"
+msgstr "%d Objekte bearbeiten"
+
+#: application/controllers/DatafieldController.php:28
+#: application/controllers/DatafieldcategoryController.php:34
+#, php-format
+msgid "Modify %s"
+msgstr "Ändere %s"
+
+#: library/Director/ProvidedHook/CubeLinks.php:35
+#: library/Director/ProvidedHook/IcingaDbCubeLinks.php:30
+msgid "Modify a host"
+msgstr "Ändere einen Host"
+
+#: application/forms/DirectorDatalistEntryForm.php:61
+msgid "Modify data list entry"
+msgstr "Datenlisteneintrag verändern"
+
+#: application/controllers/DataController.php:214
+#, php-format
+msgid "Modify instance: %s"
+msgstr "Instanz ändern: %s"
+
+#: library/Director/Web/Table/ApplyRulesTable.php:158
+msgid "Modify this Apply Rule"
+msgstr "Diese Apply-Regeln ändern"
+
+#: library/Director/Web/Controller/ObjectController.php:180
+msgid "Modifying Apply Rules"
+msgstr "Apply-Regeln ändern"
+
+#: application/controllers/ImportsourceController.php:127
+#: application/controllers/ImportsourceController.php:285
+#: application/controllers/ImportsourceController.php:315
+msgid "Modifying Import Sources"
+msgstr "Importquellen ändern"
+
+#: application/controllers/JobController.php:59
+msgid "Modifying Jobs"
+msgstr "Aufträge ändern"
+
+#: application/controllers/SyncruleController.php:517
+#: application/controllers/SyncruleController.php:625
+#: application/controllers/SyncruleController.php:633
+msgid "Modifying Sync Rules"
+msgstr "Synchronisationsregeln ändern"
+
+#: application/controllers/TemplatechoiceController.php:36
+msgid "Modifying Template Choices"
+msgstr "Auswahlmöglichkeit für Vorlagen abändern"
+
+#: library/Director/Web/Controller/ObjectController.php:176
+#: application/controllers/ServiceController.php:147
+msgid "Modifying Templates"
+msgstr "Vorlagen ändern"
+
+#: library/Director/Web/Table/Dependency/DependencyInfoTable.php:37
+msgid "Module name"
+msgstr "Modulname"
+
+#: library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php:15
+msgid "Monitored Services"
+msgstr "Überwachte Services"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:544
+msgid "Move down"
+msgstr "Nach unten bewegen"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:534
+msgid "Move up"
+msgstr "Nach oben bewegen"
+
+#: library/Director/Web/Controller/ObjectsController.php:213
+msgid "Multiple objects"
+msgstr "Mehrere Objekte"
+
+#: application/forms/SelfServiceSettingsForm.php:51
+msgid "My Agents should use DNS to look up Endpoint names"
+msgstr "Meine Agents sollen DNS benutzen, um Endpoint-Namen nachzuschlagen"
+
+#: application/controllers/ConfigController.php:181
+msgid "My changes"
+msgstr "Meine Änderungen"
+
+#: application/controllers/SchemaController.php:16
+msgid "MySQL schema"
+msgstr "MySQL Schema"
+
+#: library/Director/Web/Table/ChoicesTable.php:41
+#: library/Director/Web/Table/CoreApiObjectsTable.php:57
+#: library/Director/Web/Table/CoreApiPrototypesTable.php:40
+#: library/Director/Web/Table/CoreApiFieldsTable.php:79
+#: library/Director/Web/Table/ObjectSetTable.php:45
+#: application/forms/IcingaServiceVarForm.php:22
+#: application/forms/IcingaHostForm.php:38
+#: application/forms/IcingaHostVarForm.php:22
+#: application/forms/IcingaTimePeriodForm.php:15
+#: application/forms/IcingaCommandForm.php:46
+#: application/forms/IcingaServiceForm.php:575
+#: application/forms/IcingaDependencyForm.php:74
+#: application/forms/IcingaApiUserForm.php:14
+#: application/forms/IcingaAddServiceForm.php:143
+#: application/forms/IcingaServiceDictionaryMemberForm.php:22
+msgid "Name"
+msgstr "Name"
+
+#: application/forms/IcingaDependencyForm.php:76
+msgid "Name for the Icinga dependency you are going to create"
+msgstr "Name der Icinga-Abhängigkeit, die erstellt werden soll"
+
+#: application/forms/IcingaEndpointForm.php:20
+msgid "Name for the Icinga endpoint template you are going to create"
+msgstr "Name der Icinga-Endpunkt-Vorlage, die erstellt werden soll"
+
+#: application/forms/IcingaEndpointForm.php:26
+msgid "Name for the Icinga endpoint you are going to create"
+msgstr "Name des Icinga-Endpunkt, der erstellt werden soll"
+
+#: application/forms/IcingaNotificationForm.php:21
+msgid "Name for the Icinga notification template you are going to create"
+msgstr "Name der Icinga-Benachrichtigungs-Vorlage, die erstellt werden soll"
+
+#: application/forms/IcingaNotificationForm.php:27
+msgid "Name for the Icinga notification you are going to create"
+msgstr "Name der Icinga-Benachrichtigung, die erstellt werden soll"
+
+#: application/forms/IcingaServiceForm.php:578
+#: application/forms/IcingaAddServiceForm.php:146
+msgid "Name for the Icinga service you are going to create"
+msgstr "Name des Icinga-Service, der erstellt werden soll"
+
+#: application/forms/IcingaUserForm.php:30
+msgid "Name for the Icinga user object you are going to create"
+msgstr "Name für das Icinga-Benutzerobjekt, das Sie erstellen möchten"
+
+#: application/forms/IcingaUserForm.php:24
+msgid "Name for the Icinga user template you are going to create"
+msgstr "Name für die Icinga-Benutzervorlage, die Sie erstellen möchten"
+
+#: application/forms/IcingaZoneForm.php:17
+msgid "Name for the Icinga zone you are going to create"
+msgstr "Name der Icinga-Zone, die erstellt werden soll"
+
+#: application/forms/IcingaServiceDictionaryMemberForm.php:25
+msgid "Name for the instance you are going to create"
+msgstr "Name der Instanz, die erstellt werden soll"
+
+#: application/forms/IcingaCloneObjectForm.php:151
+msgid "Name needs to be changed when cloning a Template"
+msgstr "Beim Klonen einer Vorlage muss der Name geändert werden"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:90
+msgid "Nav"
+msgstr "Nav"
+
+#: application/controllers/DatafieldcategoryController.php:40
+msgid "New Category"
+msgstr "Neue Kategorie"
+
+#: application/controllers/DatafieldController.php:34
+msgid "New Field"
+msgstr "Neues Feld"
+
+#: application/controllers/JobController.php:34
+msgid "New Job"
+msgstr "Neuer Job"
+
+#: library/Director/Web/Tabs/ImportsourceTabs.php:54
+msgid "New import source"
+msgstr "Neue Importquelle"
+
+#: library/Director/Web/Form/CloneImportSourceForm.php:31
+#: library/Director/Web/Form/CloneSyncRuleForm.php:31
+#: application/forms/IcingaCloneObjectForm.php:39
+msgid "New name"
+msgstr "Neuer Name"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:379
+msgid "New object"
+msgstr "Neues Objekt"
+
+#: application/forms/IcingaHostForm.php:31
+#: application/forms/IcingaAddServiceForm.php:35
+msgid "Next"
+msgstr "Weiter"
+
+#: library/Director/Job/ImportJob.php:102
+#: library/Director/Job/ConfigJob.php:40
+#: library/Director/Job/ConfigJob.php:52
+#: library/Director/Job/SyncJob.php:102
+#: library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:26
+#: application/forms/IcingaZoneForm.php:29
+#: application/forms/SettingsForm.php:58
+#: application/forms/SettingsForm.php:73
+#: application/forms/SettingsForm.php:95
+#: application/forms/SelfServiceSettingsForm.php:240
+msgid "No"
+msgstr "Nein"
+
+#: application/controllers/DataController.php:184
+#, php-format
+msgid "No %s have been created yet"
+msgstr "Bisher wurden keine %s erstellt"
+
+#: library/Director/Util.php:167
+#, php-format
+msgid "No %s resource available"
+msgstr "Keine %s Ressource verfügbar"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:101
+msgid "No API user configured, you might run the kickstart helper"
+msgstr ""
+"Kein API Benutzer konfiguriert. Der kickstart helper sollte ausgeführt werden"
+
+#: application/forms/IcingaHostForm.php:175
+msgid "No Host Template has been provided yet"
+msgstr "Es wurde noch keine passende Host-Vorlage bereitgestellt"
+
+#: application/forms/IcingaHostForm.php:163
+msgid "No Host template has been chosen"
+msgstr "Keine Host-Vorlage wurde ausgewählt"
+
+#: application/forms/IcingaAddServiceForm.php:95
+msgid "No Service Templates have been provided yet"
+msgstr "Es wurde noch keine passende Vorlage bereitgestellt"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:670
+#: application/forms/IcingaCommandArgumentForm.php:181
+#: application/forms/IcingaTimePeriodRangeForm.php:94
+#: application/forms/IcingaServiceForm.php:777
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:99
+msgid "No action taken, object has not been modified"
+msgstr "Keine Aktion durchgeführt, das Objekt wurde nicht verändert"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:162
+msgid "No apply rule has been defined yet"
+msgstr "Bisher wurde kein Apply-Regel definiert"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:43
+msgid "No changes have been made"
+msgstr "Keine Änderungen wurden gemacht"
+
+#: application/controllers/DashboardController.php:73
+msgid "No dashboard available, you might have not enough permissions"
+msgstr ""
+"Kein Dashboard verfügbar, eventuell wurden nicht genügend Zugriffsrechte "
+"gewährt"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:70
+msgid "No database has been configured for Icinga Director"
+msgstr "Keine Datenbank für den Icinga-Director wurde konfiguriert"
+
+#: application/forms/KickstartForm.php:215
+msgid ""
+"No database resource has been configured yet. Please choose a resource to "
+"complete your config"
+msgstr ""
+"Es wurde bisher keine Datenbankressource konfiguriert. Bitte eine Ressource "
+"auswählen, um die Konfiguration zu vervollständigen"
+
+#: application/forms/KickstartForm.php:56
+msgid "No database schema has been created yet"
+msgstr "Kein Datenbankschema wurde bisher erstellt"
+
+#: application/forms/AddToBasketForm.php:105
+msgid "No object has been chosen"
+msgstr "Keine Objekt wurde ausgewählt"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:180
+msgid "No object has been defined yet"
+msgstr "Kein Objekt wurde bisher definiert"
+
+#: application/forms/IcingaMultiEditForm.php:88
+msgid "No object has been modified"
+msgstr "Kein Objekt wurde verändert"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1245
+msgid "No related template has been provided yet"
+msgstr "Es wurde noch keine passende Vorlage bereitgestellt"
+
+#: application/forms/IcingaAddServiceForm.php:83
+msgid "No service has been chosen"
+msgstr "Keine Service wurde ausgewählt"
+
+#: application/controllers/HostController.php:167
+#, php-format
+msgid "No such service: %s"
+msgstr "Kein solcher Service: %s"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1240
+msgid "No template has been chosen"
+msgstr "Keine Vorlage wurde ausgewählt"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:144
+msgid "No template has been defined yet"
+msgstr "Es wurde noch keine Vorlage definiert"
+
+#: application/forms/IcingaServiceForm.php:625
+msgid "None"
+msgstr "Keine"
+
+#: library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php:57
+msgid "None could be used for deployments right now"
+msgstr "Keines kann momentan zum Ausrollen verwendet werden"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1531
+msgid "Notes"
+msgstr "Notizen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1540
+msgid "Notes URL"
+msgstr "Notizen-URL"
+
+#: application/forms/SyncRunForm.php:60
+msgid "Nothing changed, rule is in sync"
+msgstr "Keine Änderung, Regel ist synchron"
+
+#: application/forms/ImportCheckForm.php:38
+#: application/forms/ImportRunForm.php:38
+msgid ""
+"Nothing to do, data provided by this Import Source didn't change since the "
+"last import run"
+msgstr ""
+"Keine Aktion nötig. Die Daten von dieser Importquelle haben sich seit dem "
+"letzten Lauf nicht geändert"
+
+#: application/forms/RestoreObjectForm.php:76
+msgid "Nothing to do, restore would not modify the current object"
+msgstr ""
+"Nichts zu tun, eine Wiederherstellung würde das aktuelle Objekt nicht ändern"
+
+#: application/forms/SyncRunForm.php:64
+msgid "Nothing to do, rule is in sync"
+msgstr "Nichts zu tun, Regel ist synchron"
+
+#: application/forms/SyncCheckForm.php:63
+msgid "Nothing would change, this rule is still in sync"
+msgstr "Es würde sich nichts ändern, diese Regel ist noch synchron"
+
+#: library/Director/TranslationDummy.php:18
+#: application/forms/IcingaNotificationForm.php:25
+#: application/forms/SyncRuleForm.php:22
+msgid "Notification"
+msgstr "Benachrichtigung"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:58
+msgid "Notification Apply Rules"
+msgstr "Benachrichtigungs-Apply-Regeln"
+
+#: library/Director/Web/Controller/TemplateController.php:57
+#, php-format
+msgid "Notification Apply Rules based on %s"
+msgstr "Benachrichtigungs-Apply-Regeln basierend auf %s"
+
+#: library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php:19
+#: library/Director/Import/ImportSourceCoreApi.php:58
+msgid "Notification Commands"
+msgstr "Benachrichtigungskommandos"
+
+#: library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php:12
+msgid ""
+"Notification Commands allow you to trigger any action you want when a "
+"notification takes place"
+msgstr ""
+"Benachrichtigungs-Kommandos erlauben das Ausführen beliebiger Aktionen wenn "
+"eine Benachrichtigung stattfinden soll"
+
+#: application/forms/IcingaNotificationForm.php:19
+msgid "Notification Template"
+msgstr "Benachrichtigungsvorlage"
+
+#: application/forms/BasketForm.php:30
+msgid "Notification Templates"
+msgstr "Benachrichtigungsvorlagen"
+
+#: application/forms/IcingaNotificationForm.php:262
+msgid "Notification command"
+msgstr "Benachrichtigungskommando"
+
+#: application/forms/IcingaNotificationForm.php:176
+msgid "Notification interval"
+msgstr "Benachrichtigungsintervall"
+
+#: application/controllers/TemplatechoicesController.php:29
+msgid "Notification template choices"
+msgstr "Auswahlmöglichkeiten für Benachrichtigungsvorlagen"
+
+#: library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php:13
+msgid "Notification templates"
+msgstr "Benachrichtigungsvorlagen"
+
+#: configuration.php:165
+#: library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php:13
+#: library/Director/Dashboard/Dashlet/NotificationsDashlet.php:13
+#: library/Director/Db/Branch/BranchModificationInspection.php:46
+#: library/Director/Web/Table/CustomvarVariantsTable.php:61
+#: library/Director/Web/Table/CustomvarTable.php:46
+#: application/forms/BasketForm.php:31
+msgid "Notifications"
+msgstr "Benachrichtigungen"
+
+#: library/Director/Dashboard/NotificationsDashboard.php:20
+msgid ""
+"Notifications are sent when a host or service reaches a non-ok hard state or "
+"recovers from such. One might also want to send them for special events like "
+"when a Downtime starts, a problem gets acknowledged and much more. You can "
+"send specific notifications only within specific time periods, you can delay "
+"them and of course re-notify at specific intervals.\n"
+"\n"
+" Combine those possibilities in case you need to define escalation levels, "
+"like notifying operators first and your management later on in case the "
+"problem remains unhandled for a certain time.\n"
+"\n"
+" You might send E-Mail or SMS, make phone calls or page on various "
+"channels. You could also delegate notifications to external service "
+"providers. The possibilities are endless, as you are allowed to define as "
+"many custom notification commands as you want"
+msgstr ""
+"Benachrichtigungen werden gesendet, wenn ein Host oder Services einen harten "
+"Nicht-OK-Zustand erreicht oder sich von einem solchen erholt. Es könnte "
+"zudem wünschenswert sein, Benachrichtigungen auch für spezielle Events wie "
+"den Beginn einer Downtime, das Bestätigen eines Problems und mehr zu "
+"versenden. Benachrichtigungen lassen sich auf Wunsch nur innerhalb "
+"bestimmter Zeitfenster versenden. Zudem können sie künstlich verzögert und / "
+"oder in bestimmten Intervallen neu versandt werden.\n"
+"\n"
+"Wer Eskalationsebenen umsetzen möchte kombiniert diese Möglichkeiten. Man "
+"könnte z.B. erst das Operating benachrichtigen, und erst wenn das Problem "
+"für längere Zeit unbehandelt bleibt das Management mit einbeziehen.\n"
+"\n"
+"Man könnte E-Mails oder SMS versenden, Telefonanrufe durchführen und sich "
+"auf unterschiedlichsten Kanälen anpiepen lassen. Benachrichtigungen lassen "
+"sich natürlich auch an externe Serviceanbieter auslagern. Die Möglichkeiten "
+"sind endlos, nachdem man beliebig viele eigene Kommandos anbinden kann"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:43
+msgid "Numeric position or key name"
+msgstr "Numerische Position oder Schlüsselbezeichner"
+
+#: library/Director/IcingaConfig/StateFilterSet.php:24
+msgid "OK"
+msgstr "Ok"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:67
+#: library/Director/Web/Form/DirectorObjectForm.php:1114
+#: library/Director/Web/Form/DirectorObjectForm.php:1119
+#: library/Director/Web/Controller/TemplateController.php:149
+msgid "Object"
+msgstr "Objekt"
+
+#: application/controllers/InspectController.php:102
+msgid "Object Inspection"
+msgstr "Objekt-Inspektion"
+
+#: library/Director/Import/ImportSourceDirectorObject.php:78
+#: application/forms/SyncRuleForm.php:45
+msgid "Object Type"
+msgstr "Objekttyp"
+
+#: library/Director/Import/ImportSourceLdap.php:56
+msgid "Object class"
+msgstr "Objektklasse"
+
+#: library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php:18
+msgid "Object dependency relationships."
+msgstr "Objekt-Abhängigkeiten."
+
+#: application/forms/RestoreObjectForm.php:80
+msgid "Object has been re-created"
+msgstr "Objekt wurde wiederhergestellt"
+
+#: application/forms/RestoreObjectForm.php:72
+msgid "Object has been restored"
+msgstr "Objekt wurde wiederhergestellt"
+
+#: application/forms/SyncPropertyForm.php:359
+msgid "Object properties"
+msgstr "Objekteigenschaften"
+
+#: library/Director/Web/Table/SyncruleTable.php:46
+#: library/Director/Web/Form/DirectorObjectForm.php:1127
+msgid "Object type"
+msgstr "Objekttyp"
+
+#: application/controllers/InspectController.php:65
+#, php-format
+msgid "Object type \"%s\""
+msgstr "Objekttyp \"%s\""
+
+#: library/Director/Web/Table/GeneratedConfigFileTable.php:85
+msgid "Object/Tpl/Apply"
+msgstr "Objekt/Vorlage/Apply"
+
+#: library/Director/Import/ImportSourceDirectorObject.php:81
+#: library/Director/Web/Table/HostTemplateUsageTable.php:11
+#: library/Director/Web/Table/TemplateUsageTable.php:24
+#: library/Director/Web/Table/ServiceTemplateUsageTable.php:11
+msgid "Objects"
+msgstr "Objekte"
+
+#: library/Director/Import/ImportSourceRestApi.php:209
+msgid ""
+"Often the expected result is provided in a property like \"objects\". Please "
+"specify this if required."
+msgstr ""
+"Häufig wird das zu erwartende Ergebnis in einer Eigenschaft wie \"objects\" "
+"zurückgeliefert. Bitte angeben falls erforderlich."
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:27
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:34
+msgid "On failure"
+msgstr "Bei Fehler"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:166
+msgid "One apply rule has been defined"
+msgstr "Eine Apply-Regel wurde definiert"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:188
+msgid "One external object has been defined, it will not be deployed"
+msgstr "Ein externes Objekt wurde erstellt, es wird nicht ausgerollt"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:191
+msgid "One object has been defined"
+msgstr "Ein Objekt wurde definiert"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:46
+#: application/forms/IcingaMultiEditForm.php:90
+msgid "One object has been modified"
+msgstr "Ein Objekt wurde verändert"
+
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:16
+msgid "One or more characters that should be used to split this string"
+msgstr ""
+"Ein oder mehrere Zeichen, die zum Trennen der Zeichenkette genutzt werden "
+"sollen"
+
+#: library/Director/PropertyModifier/PropertyModifierJoin.php:16
+msgid ""
+"One or more characters that will be used to glue an input array to a string. "
+"Can be left empty"
+msgstr ""
+"Ein oder mehrere Zeichen, die verwendet werden, um einen Eingabearray an "
+"eine Zeichenkette zu heften. Kann leer bleiben"
+
+#: application/forms/IcingaTimePeriodRangeForm.php:30
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:31
+msgid "One or more time periods, e.g. 00:00-24:00 or 00:00-09:00,17:00-24:00"
+msgstr ""
+"Einer oder mehrere Zeiträume, z.B. 00:00-24:00 oder 00:00-09:00,17:00-24:00"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:148
+#: library/Director/Dashboard/Dashlet/Dashlet.php:185
+msgid "One template has been defined"
+msgstr "Eine Vorlage wurde definiert"
+
+#: application/forms/IcingaCommandArgumentForm.php:100
+msgid ""
+"Only set this parameter if the argument value resolves to a numeric value. "
+"String values are not supported"
+msgstr ""
+"Nur setzen, wenn der Wert des Arguments numereisch ist. Zeichenketten als "
+"Wert werden nicht unterstützt"
+
+#: application/forms/IcingaObjectFieldForm.php:146
+msgid "Optional"
+msgstr "Optional"
+
+#: application/forms/IcingaCommandForm.php:71
+msgid ""
+"Optional command timeout. Allowed values are seconds or durations postfixed "
+"with a specific unit (e.g. 1m or also 3m 30s)."
+msgstr ""
+"Optionaler Kommando-Timeout. Erlaubt sind Werte in Sekunden oder Werte mit "
+"nachgestellter Zeiteinheit. z.B. 1m oder auch 3m 30s."
+
+#: application/forms/IcingaDependencyForm.php:248
+msgid ""
+"Optional. The child service. If omitted this dependency object is treated as "
+"host dependency."
+msgstr ""
+"Optional. Der Kind-Service. Falls leer wird dieses Objekt als Host-"
+"Abhängigkeit behandelt."
+
+#: application/forms/IcingaDependencyForm.php:218
+msgid ""
+"Optional. The parent service. If omitted this dependency object is treated "
+"as host dependency."
+msgstr ""
+"Optional. Der Eltern-Service. Falls leer wird dieses Objekt als Host-"
+"Abhängigkeit behandelt."
+
+#: application/forms/IcingaObjectFieldForm.php:103
+msgid "Other available fields"
+msgstr "Andere verfügbare Felder"
+
+#: application/forms/SyncPropertyForm.php:274
+msgid "Other sources"
+msgstr "Andere Quellen"
+
+#: application/forms/IcingaServiceForm.php:160
+#: application/forms/IcingaServiceForm.php:435
+#: application/forms/IcingaServiceForm.php:467
+msgid "Override vars"
+msgstr "Variablen überschreiben"
+
+#: library/Director/Web/ActionBar/AutomationObjectActionBar.php:32
+#: library/Director/Web/Tabs/MainTabs.php:26
+msgid "Overview"
+msgstr "Überblick"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:81
+msgid "PHP Binary"
+msgstr "PHP Binary"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:84
+msgid "PHP Integer"
+msgstr "PHP Integer"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:83
+msgid "PHP Version"
+msgstr "PHP-Version"
+
+#: application/controllers/PhperrorController.php:19
+#, php-format
+msgid ""
+"PHP version 5.4.x is required for Director >= 1.4.0, you're running %s. "
+"Please either upgrade PHP or downgrade Icinga Director"
+msgstr ""
+"PHP Version 5.4.x ist für den Director >= 1.4.0 erforderlich, hier läuft %s. "
+"Bitte entweder PHP upgraden oder den Icinga Director downgraden"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:73
+msgid "PID"
+msgstr "PID"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:137
+msgid "Packages"
+msgstr "Pakete"
+
+#: library/Director/Web/Widget/InspectPackages.php:48
+#, php-format
+msgid "Packages on Endpoint: %s"
+msgstr "Pakete auf Endpunkt: %s"
+
+#: application/forms/IcingaUserForm.php:41
+msgid "Pager"
+msgstr "Pager"
+
+#: application/forms/IcingaDependencyForm.php:200
+msgid "Parent Host"
+msgstr "Eltern-Host"
+
+#: application/forms/IcingaDependencyForm.php:216
+msgid "Parent Service"
+msgstr "Eltern-Service"
+
+#: application/forms/IcingaZoneForm.php:36
+msgid "Parent Zone"
+msgstr "Übergeordnete Zone"
+
+#: library/Director/Import/ImportSourceRestApi.php:233
+#: application/forms/IcingaApiUserForm.php:19
+#: application/forms/KickstartForm.php:163
+msgid "Password"
+msgstr "Passwort"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:13
+#: library/Director/PropertyModifier/PropertyModifierCombine.php:14
+msgid "Pattern"
+msgstr "Muster"
+
+#: application/forms/ApplyMigrationsForm.php:39
+msgid "Pending database schema migrations have successfully been applied"
+msgstr ""
+"Ausstehende Datenbankschemamigrationsskripte wurden erfolgreich angewandt"
+
+#: library/Director/Util.php:169
+msgid "Please ask an administrator to grant you access to resources"
+msgstr "Zugriff auf Ressourcen kann durch Administrator gewährt werden"
+
+#: application/forms/AddToBasketForm.php:117
+#, php-format
+msgid ""
+"Please check your Basket configuration, %s does not support single \"%s\" "
+"configuration objects"
+msgstr ""
+"Bitte Basketkonfiguration überprüfen, %s unterstützt keine einzelnen "
+"Konfigurationsobjekte vom Typ \"%s\""
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:19
+msgid "Please choose a data list that can be used for map lookups"
+msgstr "Eine Datenliste auswählen, die für Map lookups verwendet wird"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:69
+#: library/Director/DataType/DataTypeDictionary.php:66
+msgid "Please choose a specific Icinga object type"
+msgstr "Bitte einen bestimmten Icinga-Objekttyp auswählen"
+
+#: library/Director/Job/ImportJob.php:82
+msgid ""
+"Please choose your import source that should be executed. You could create "
+"different schedules for different sources or also opt for running all of "
+"them at once."
+msgstr ""
+"Eine Importquelle die ausgeführt werden soll auswählen. Es können "
+"verschiedene Zeitpläne für verschiedene Quellen erstellt oder alle "
+"gleichzeitig ausgeführt werden."
+
+#: library/Director/Job/SyncJob.php:82
+msgid ""
+"Please choose your synchronization rule that should be executed. You could "
+"create different schedules for different rules or also opt for running all "
+"of them at once."
+msgstr ""
+"Eine Synchronisationsregel die ausgeführt werden soll auswählen. Es können "
+"verschiedene Zeitpläne für verschiedene Regeln erstellt oder alle "
+"gleichzeitig ausgeführt werden."
+
+#: library/Director/Import/ImportSourceSql.php:55
+msgid "Please click \"Store\" once again to determine query columns"
+msgstr ""
+"Bitte klicke \"Speicher\" erneut um die von der Abfrage bereitgestellten "
+"Spalten zu ermitteln"
+
+#: application/forms/KickstartForm.php:234
+#, php-format
+msgid "Please click %s to create new DB resources"
+msgstr "%s klicken, um neue Datenbankressourcen zu erstellen"
+
+#: library/Director/Util.php:159
+#, php-format
+msgid "Please click %s to create new resources"
+msgstr "%s klicken, um neue Ressourcen zu erstellen"
+
+#: application/forms/IcingaHostForm.php:167
+#: application/forms/IcingaAddServiceForm.php:87
+#, php-format
+msgid "Please define a %s first"
+msgstr "Bitte zuerst eine %s definieren"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1243
+msgid "Please define a related template first"
+msgstr "Bitte zuerst eine entsprechende Vorlage definieren"
+
+#: application/forms/KickstartForm.php:472
+msgid ""
+"Please make sure that your database exists and your user has been granted "
+"enough permissions"
+msgstr ""
+"Überprüfen, ob die Datenbank existiert und der Benutzer ausreichende "
+"Berechtigungen hat"
+
+#: application/controllers/ImportsourceController.php:98
+#, php-format
+msgid ""
+"Please note that importing data will take place in your main Branch. "
+"Modifications to Import Sources are not allowed while being in a "
+"Configuration Branch. To get the full functionality, please deactivate %s"
+msgstr ""
+"Bitte beachten, dass das Importieren von Daten im Hauptzweig stattfindet. "
+"Änderungen an Importquellen sind nicht erlaubt, während man sich in einem "
+"anderen Konfigurationszweig befindet. Für die volle Funktionalität bitte %s "
+"deaktivieren"
+
+#: application/forms/SettingsForm.php:23
+msgid ""
+"Please only change those settings in case you are really sure that you are "
+"required to do so. Usually the defaults chosen by the Icinga Director should "
+"make a good fit for your environment."
+msgstr ""
+"Diese Einstellungen sollten nur dann geändert werden, wenn es als unbedingt "
+"nötig erachtet wird. Für gewöhnlich sollten die vom Icinga Director "
+"gewählten Standardwerte für Umgebungen jeglicher Art passend sein."
+
+#: application/forms/SyncRuleForm.php:31
+msgid "Please provide a rule name"
+msgstr "Bitte einen Regelnamen angeben"
+
+#: library/Director/PropertyModifier/PropertyModifierSubstring.php:17
+#: library/Director/PropertyModifier/PropertyModifierSubstring.php:27
+#, php-format
+msgid "Please see %s for detailled instructions of how start and end work"
+msgstr ""
+"Siehe %s für eine detaillierte Anleitung, wie Beginn und Ende funktionieren"
+
+#: application/forms/ImportRowModifierForm.php:32
+msgid ""
+"Please start typing for a list of suggestions. Dots allow you to access "
+"nested properties: column.some.key. Such nested properties cannot be "
+"modified in-place, but you can store the modified value to a new \"target "
+"property\""
+msgstr ""
+"Für eine Liste von Vorschlägen bitte mit dem Tippen beginnen. Punkte "
+"erlauben es, verschachtelte Eigenschaften anzusprechen: column.some.key. "
+"Solche verschachtelten EIgenschaften können nicht an Ort und Stelle "
+"verändert werden, dafür lässt sich der modifizierte Wert aber in eine neue "
+"\"Zieleigenschaft\" schreiben"
+
+#: application/forms/IcingaCommandForm.php:35
+msgid ""
+"Plugin Check commands are what you need when running checks agains your "
+"infrastructure. Notification commands will be used when it comes to notify "
+"your users. Event commands allow you to trigger specific actions when "
+"problems occur. Some people use them for auto-healing mechanisms, like "
+"restarting services or rebooting systems at specific thresholds"
+msgstr ""
+"Plugin-Check-Kommandos werden benötigt, um Infrastruktur zu prüfen. Mit "
+"Benachrichtigungskommandos werden Benutzer benachrichtigt. Event-Kommandos "
+"führen bestimmte Aktionen aus, wenn Probleme auftreten. Sie werden manchmal "
+"für Selbstheilungsmechanismen verwendet, wie das Neustarten von Services "
+"oder das Neustarten von Systemen beim Überschreiten bestimmter Schwellwerte"
+
+#: application/forms/IcingaCommandForm.php:20
+msgid "Plugin commands"
+msgstr "Plugin-Kommandos"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:50
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:65
+msgid "Policy"
+msgstr "Richtlinie"
+
+#: application/forms/IcingaEndpointForm.php:36
+#: application/forms/KickstartForm.php:145
+msgid "Port"
+msgstr "Port"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:41
+#: application/forms/IcingaCommandArgumentForm.php:67
+msgid "Position"
+msgstr "Position"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:30
+msgid "Position Type"
+msgstr "Positionstyp"
+
+#: application/controllers/SchemaController.php:17
+msgid "PostgreSQL schema"
+msgstr "PostgreSQL Schema"
+
+#: application/forms/IcingaTimePeriodForm.php:76
+msgid "Prefer includes"
+msgstr "Includes bevorzugen"
+
+#: library/Director/Dashboard/BranchesDashboard.php:24
+msgid "Prepare your configuration in a safe Environment"
+msgstr "Bereite Konfiguration in einer geschützten Umgebung vor"
+
+#: library/Director/Dashboard/Dashlet/BasketDashlet.php:17
+msgid "Preserve specific configuration objects in a specific state"
+msgstr ""
+"Bestimmte Konfigurationsobjekte in einem bestimmten Zustand aufbewahren"
+
+#: library/Director/Web/Tabs/ImportsourceTabs.php:49
+#: library/Director/Web/Tabs/SyncRuleTabs.php:33
+#: library/Director/Web/Tabs/ObjectTabs.php:86
+msgid "Preview"
+msgstr "Vorschau"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:23
+msgid "Problem"
+msgstr "Problem"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:27
+msgid "Problem handling"
+msgstr "Problembehandlung"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:94
+msgid "Process List"
+msgstr "Prozessliste"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1452
+msgid "Process performance data"
+msgstr "Performancedaten verarbeiten"
+
+#: library/Director/Import/ImportSourceLdap.php:70
+#: library/Director/Web/Tabs/SyncRuleTabs.php:39
+#: application/controllers/DataController.php:328
+msgid "Properties"
+msgstr "Eigenschaften"
+
+#: library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php:61
+#: library/Director/Web/Table/PropertymodifierTable.php:129
+#: library/Director/Web/Table/PropertymodifierTable.php:132
+#: application/forms/ImportRowModifierForm.php:30
+msgid "Property"
+msgstr "Eigenschaft"
+
+#: application/controllers/ImportsourceController.php:251
+#, php-format
+msgid "Property modifiers: %s"
+msgstr "Eigenschaftsmodifikatoren: %s"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:88
+msgid "Protected"
+msgstr "Geschützt"
+
+#: library/Director/Import/ImportSourceRestApi.php:128
+msgid "Protocol"
+msgstr "Protokoll"
+
+#: application/controllers/InspectController.php:89
+msgid "Prototypes (methods)"
+msgstr "Prototypen (Methoden)"
+
+#: library/Director/Dashboard/Dashlet/DatalistDashlet.php:11
+msgid "Provide Data Lists"
+msgstr "Datenlisten bereitstellen"
+
+#: library/Director/Dashboard/Dashlet/DatalistDashlet.php:17
+msgid "Provide data lists to make life easier for your users"
+msgstr "Datenlisten erleichtern Anwendern die Arbeit"
+
+#: library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php:18
+msgid "Provide templates for your TimePeriod objects."
+msgstr "Vorlagen für Benachrichtigungen bereitstellen."
+
+#: library/Director/Dashboard/Dashlet/UserTemplateDashlet.php:18
+msgid "Provide templates for your User objects."
+msgstr "Vorlagen für Benutzer-Objekte bereitstellen."
+
+#: library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php:18
+msgid "Provide templates for your notifications."
+msgstr "Vorlagen für Benachrichtigungen bereitstellen."
+
+#: library/Director/Import/ImportSourceRestApi.php:244
+msgid "Proxy"
+msgstr "Proxy"
+
+#: library/Director/Import/ImportSourceRestApi.php:260
+msgid "Proxy Address"
+msgstr "Proxyadresse"
+
+#: library/Director/Import/ImportSourceRestApi.php:278
+msgid "Proxy Password"
+msgstr "Proxykennwort"
+
+#: library/Director/Import/ImportSourceRestApi.php:268
+msgid "Proxy Username"
+msgstr "Proxybenutzer"
+
+#: application/forms/SyncRuleForm.php:70
+msgid "Purge"
+msgstr "Bereinigen"
+
+#: application/forms/SyncRuleForm.php:82
+msgid "Purge Action"
+msgstr "Aktion beim Bereinigen"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:122
+msgid "Ranges"
+msgstr "Bereiche"
+
+#: application/forms/DeployConfigForm.php:32
+msgid "Re-deploy now"
+msgstr "Jetzt erneut ausrollen"
+
+#: application/forms/IcingaServiceForm.php:141
+msgid "Reactivate"
+msgstr "Reaktivieren"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:24
+msgid "Recovery"
+msgstr "Erholung"
+
+#: application/forms/IcingaGenerateApiKeyForm.php:22
+msgid "Regenerate Self Service API key"
+msgstr "Selbstbedienungs-API-Schlüssel neu generieren"
+
+#: application/forms/IcingaHostSelfServiceForm.php:56
+msgid "Register"
+msgstr "Registrieren"
+
+#: library/Director/Web/SelfService.php:62
+msgid "Registered Agent"
+msgstr "Registrierter Agent"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:34
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:32
+msgid "Regular Expression"
+msgstr "Regulärer Ausdruck"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:16
+msgid "Regular expression pattern to split the string (e.g. /\\s+/ or /[,;]/)"
+msgstr ""
+"Regulärer Ausdruck anhand dessen eine String aufgeteilt werden soll (z.B. /"
+"\\s+/ oder /[,;]/)"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:58
+msgid "Reject matching elements"
+msgstr "Übereinstimmende Elemente ablehnen"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:17
+msgid "Reject or keep rows based on property value"
+msgstr "Die ganze Zeile abhängig vom Eigenschaftswert abweisen"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:72
+msgid "Reject the whole row"
+msgstr "Verwerfe die ganze Zeile"
+
+#: application/forms/IcingaDependencyForm.php:268
+msgid "Related Objects"
+msgstr "Verwandte Objekte"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:532
+msgid "Remark"
+msgstr "Anmerkung"
+
+#: library/Director/Web/Table/IcingaServiceSetServiceTable.php:225
+msgid "Remove"
+msgstr "Entfernen"
+
+#: library/Director/Web/Table/IcingaServiceSetServiceTable.php:227
+#, php-format
+msgid "Remove \"%s\" from this host"
+msgstr "\"%s\" von diesem Host entfernen"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:524
+msgid "Remove this entry"
+msgstr "Diesen Eintrag entfernen"
+
+#: application/views/helpers/FormDataFilter.php:507
+msgid "Remove this part of your filter"
+msgstr "Diesen Teil des Filters entfernen"
+
+#: application/forms/DirectorDatafieldForm.php:96
+msgid "Rename related vars"
+msgstr "Zugehörige Variablen umbenennen"
+
+#: application/forms/IcingaCommandForm.php:86
+msgid "Render as string"
+msgstr "Als String rendern"
+
+#: application/controllers/ConfigController.php:72
+msgid "Render config"
+msgstr "Konfiguration erstellen"
+
+#: application/forms/IcingaCommandForm.php:77
+msgid "Render the command as a plain string instead of an array."
+msgstr "Rendere das Kommando als reinen String und nicht als Array."
+
+#: application/controllers/ConfigController.php:310
+msgid "Rendered file"
+msgstr "Erzeugte Datei"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:97
+#, php-format
+msgid "Rendered in %0.2fs, deployed in %0.2fs"
+msgstr "Erstellt in %0.2fs, ausgerollt in %0.2fs"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:539
+#: library/Director/Web/Widget/ActivityLogInfo.php:544
+msgid "Rendering"
+msgstr "Erstellen"
+
+#: application/forms/IcingaCommandArgumentForm.php:107
+msgid "Repeat key"
+msgstr "Schlüssel wiederholen"
+
+#: application/forms/SyncRuleForm.php:63
+msgid "Replace"
+msgstr "Ersetzen"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:86
+#: library/Director/Web/Table/Dependency/DependencyInfoTable.php:38
+#: application/forms/IcingaCommandArgumentForm.php:124
+msgid "Required"
+msgstr "Benötigt"
+
+#: library/Director/Import/ImportSourceDirectorObject.php:90
+msgid "Resolved"
+msgstr "Aufgelöst"
+
+#: application/forms/RestoreBasketForm.php:58
+#: application/controllers/BasketController.php:208
+msgid "Restore"
+msgstr "Wiederherstellen"
+
+#: application/forms/RestoreObjectForm.php:17
+msgid "Restore former object"
+msgstr "Vorheriges Objekt wiederherstellen"
+
+#: application/forms/RestoreBasketForm.php:52
+msgid "Restore to this target Director DB"
+msgstr "In dieser Director-Ziel-DB wiederherstellen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1380
+msgid "Retry interval"
+msgstr "Wiederholungsintervall"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1382
+msgid ""
+"Retry interval, will be applied after a state change unless the next hard "
+"state is reached"
+msgstr ""
+"Wiederholungsintervall, wird nach einem Statuswechsel verwendet, bis der "
+"nächste Hard Status erreicht wurde"
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:34
+msgid "Return lookup key unmodified"
+msgstr "Suchschlüssel unverändert zurück geben"
+
+#: library/Director/Web/Widget/InspectPackages.php:69
+msgid "Root Zone"
+msgstr "Rootzone"
+
+#: library/Director/Web/Table/SyncruleTable.php:45
+#: application/forms/SyncRuleForm.php:30
+msgid "Rule name"
+msgstr "Regelname"
+
+#: library/Director/Job/ImportJob.php:119
+msgid "Run all imports at once"
+msgstr "Alle Importe gleichzeitig ausführen"
+
+#: library/Director/Job/SyncJob.php:125
+msgid "Run all rules at once"
+msgstr "Alle Regeln gleichzeitig ausführen"
+
+#: library/Director/Job/ImportJob.php:92
+#: application/forms/KickstartForm.php:189
+msgid "Run import"
+msgstr "Import ausführen"
+
+#: application/forms/DirectorJobForm.php:46
+msgid "Run interval"
+msgstr "Laufintervall"
+
+#: application/forms/IcingaServiceForm.php:678
+msgid "Run on agent"
+msgstr "Auf Agent ausführen"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:76
+msgid "Running with systemd"
+msgstr "Läuft mit systemd"
+
+#: library/Director/Import/ImportSourceRestApi.php:251
+msgid "SOCKS5 proxy"
+msgstr "SOCKS5 Proxy"
+
+#: library/Director/Dashboard/Dashlet/JobDashlet.php:29
+msgid ""
+"Schedule and automate Import, Syncronization, Config Deployment, "
+"Housekeeping and more"
+msgstr ""
+"Import, Synchronisation, Ausrollen der Konfiguration, Bereinigung und mehr "
+"planen und automatisieren"
+
+#: library/Director/Dashboard/NotificationsDashboard.php:14
+#: library/Director/Dashboard/UsersDashboard.php:15
+msgid "Schedule your notifications"
+msgstr "Benachrichtigungen planen"
+
+#: library/Director/Dashboard/Dashlet/NotificationsDashlet.php:19
+msgid ""
+"Schedule your notifications. Define who should be notified, when, and for "
+"which kind of problem"
+msgstr ""
+"Benachrichtigungen planen. Wer soll wann benachrichtigt werden, wofür, und "
+"für welche Art von Problemen"
+
+#: application/forms/SyncRuleForm.php:23
+msgid "Scheduled Downtime"
+msgstr "Geplante Downtime"
+
+#: library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php:13
+#: library/Director/Db/Branch/BranchModificationInspection.php:48
+msgid "Scheduled Downtimes"
+msgstr "Geplante Downtimes"
+
+#: application/forms/SettingsForm.php:177
+msgid ""
+"Script or tool to call when activating a new configuration stage. (e.g. "
+"sudo /usr/local/bin/icinga-director-activate) (name of the stage will be the "
+"argument for the script)"
+msgstr ""
+"Skript oder Tools welches beim Aktivieren einer neuen Konfigurationsphase "
+"ausgeführt werden soll. (z.B. sudo /usr/local/bin/icinga-director-activate) "
+"(der Name der Konfiguration wird dem Skript als Argument mitgegeben)"
+
+#: application/controllers/SettingsController.php:43
+#: application/controllers/SelfServiceController.php:107
+msgid "Self Service"
+msgstr "Selbstbedienung"
+
+#: application/controllers/SelfServiceController.php:108
+msgid "Self Service - Host Registration"
+msgstr "Selbstbedienung - Host-Registrierung"
+
+#: library/Director/Dashboard/Dashlet/SelfServiceDashlet.php:11
+#: library/Director/Web/SelfService.php:180
+msgid "Self Service API"
+msgstr "Selbstbedienungs-API"
+
+#: application/controllers/SettingsController.php:44
+msgid "Self Service API - Global Settings"
+msgstr "Selbstbedienungs-API - Globale Einstellungen"
+
+#: application/forms/SelfServiceSettingsForm.php:298
+msgid "Self Service Settings have been stored"
+msgstr "Selbstbedienungseinstellungen wurden gespeichert"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1440
+#: application/forms/IcingaUserForm.php:89
+msgid "Send notifications"
+msgstr "Benachrichtigungen senden"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:78
+msgid "Sent to"
+msgstr "Senden an"
+
+#: library/Director/TranslationDummy.php:14
+#: application/forms/IcingaServiceVarForm.php:15
+#: application/forms/IcingaAddServiceForm.php:104
+#: application/forms/SyncRuleForm.php:14
+msgid "Service"
+msgstr "Service"
+
+#: library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php:11
+msgid "Service Apply Rules"
+msgstr "Service Apply-Regeln"
+
+#: application/forms/SyncRuleForm.php:15
+msgid "Service Group"
+msgstr "Servicegruppe"
+
+#: library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php:11
+#: application/forms/BasketForm.php:23
+msgid "Service Groups"
+msgstr "Servicegruppen"
+
+#: library/Director/Web/Table/ObjectsTableService.php:107
+msgid "Service Name"
+msgstr "Servicename"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:61
+#: application/forms/SyncRuleForm.php:16
+msgid "Service Set"
+msgstr "Service-Set"
+
+#: application/forms/RemoveLinkForm.php:55
+msgid "Service Set has been removed"
+msgstr "Service-Set wurde entfernt"
+
+#: library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php:11
+#: library/Director/Web/Table/CustomvarVariantsTable.php:60
+#: library/Director/Web/Table/CustomvarTable.php:45
+#: application/forms/BasketForm.php:26
+msgid "Service Sets"
+msgstr "Service-Sets"
+
+#: application/forms/IcingaAddServiceForm.php:89
+msgid "Service Template"
+msgstr "Service-Vorlage"
+
+#: application/forms/BasketForm.php:24
+msgid "Service Template Choice"
+msgstr "Auswahlmöglichkeit für Service-Vorlagen"
+
+#: library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php:11
+#: application/forms/BasketForm.php:25
+msgid "Service Templates"
+msgstr "Service-Vorlagen"
+
+#: application/forms/SelfServiceSettingsForm.php:179
+msgid "Service User"
+msgstr "Dienst-Benutzeraccount"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:60
+msgid "Service groups"
+msgstr "Servicegruppen"
+
+#: application/forms/IcingaServiceForm.php:653
+msgid ""
+"Service groups that should be directly assigned to this service. "
+"Servicegroups can be useful for various reasons. They are helpful to "
+"provided service-type specific view in Icinga Web 2, either for custom "
+"dashboards or as an instrument to enforce restrictions. Service groups can "
+"be directly assigned to single services or to service templates."
+msgstr ""
+"Servicegruppen, die diesem Service direkt zugeordnet werden sollen. "
+"Servicegruppen können für diverse Anwendungen praktisch sein, wie auf "
+"Service-Typ eingeschränkte Ansichten in Icinga Web 2, für benutzerdefinierte "
+"Dashboards oder zum Umsetzen von Einschränkungen. Servicegruppen können "
+"direkt einem einzelnen Service zugeordnet werden oder Service-Vorlagen."
+
+#: library/Director/Web/Table/IcingaHostAppliedForServiceTable.php:102
+msgid "Service name"
+msgstr "Servicename"
+
+#: library/Director/Objects/IcingaService.php:755
+#: application/controllers/SuggestController.php:260
+msgid "Service properties"
+msgstr "Service-Eigenschaften"
+
+#: application/forms/IcingaServiceSetForm.php:87
+#: application/forms/IcingaAddServiceSetForm.php:86
+msgid "Service set"
+msgstr "Service-Set"
+
+#: application/forms/IcingaServiceSetForm.php:28
+msgid "Service set name"
+msgstr "Service-Set Name"
+
+#: application/controllers/TemplatechoiceController.php:25
+msgid "Service template choice"
+msgstr "Auswahlmöglichkeite für Service-Vorlagen"
+
+#: application/controllers/TemplatechoicesController.php:24
+msgid "Service template choices"
+msgstr "Auswahlmöglichkeiten für Service-Vorlagen"
+
+#: application/controllers/ServiceController.php:301
+msgid "ServiceSet"
+msgstr "Service-Set"
+
+#: application/forms/IcingaServiceGroupForm.php:14
+msgid "Servicegroup"
+msgstr "Servicegruppe"
+
+#: library/Director/Db/Branch/BranchModificationInspection.php:41
+msgid "Servicegroups"
+msgstr "Servicegruppen"
+
+#: library/Director/Web/Table/IcingaAppliedServiceTable.php:32
+#: library/Director/Web/Table/ObjectsTableService.php:103
+#: library/Director/Web/Table/IcingaServiceSetServiceTable.php:168
+msgid "Servicename"
+msgstr "Servicename"
+
+#: configuration.php:157
+#: library/Director/IcingaConfig/StateFilterSet.php:23
+#: library/Director/DataType/DataTypeDirectorObject.php:59
+#: library/Director/DataType/DataTypeDictionary.php:60
+#: library/Director/Db/Branch/BranchModificationInspection.php:40
+#: library/Director/Web/Table/CustomvarVariantsTable.php:59
+#: library/Director/Web/Table/CustomvarTable.php:44
+#: library/Director/Web/Tabs/ObjectTabs.php:75
+#: application/forms/IcingaNotificationForm.php:90
+#: application/forms/IcingaDependencyForm.php:101
+#: application/forms/IcingaScheduledDowntimeForm.php:90
+#: application/controllers/ServiceController.php:284
+#: application/controllers/ServiceController.php:305
+msgid "Services"
+msgstr "Services"
+
+#: application/controllers/ServicesetController.php:64
+#, php-format
+msgid "Services in this set: %s"
+msgstr "Services in diesem Set: %s"
+
+#: application/controllers/HostController.php:286
+#, php-format
+msgid "Services on %s"
+msgstr "Services auf %s"
+
+#: application/controllers/HostController.php:206
+#, php-format
+msgid "Services: %s"
+msgstr "Services: %s"
+
+#: library/Director/Db/Branch/BranchModificationInspection.php:42
+msgid "Servicesets"
+msgstr "Service-Sets"
+
+#: application/forms/SyncPropertyForm.php:84
+msgid "Set based on filter"
+msgstr "Basierend auf Filter setzen"
+
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:44
+msgid "Set false"
+msgstr "Auf \"false\" setzen"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:30
+#: library/Director/PropertyModifier/PropertyModifierGetHostByAddr.php:17
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:39
+#: library/Director/PropertyModifier/PropertyModifierJsonDecode.php:25
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:34
+#: library/Director/PropertyModifier/PropertyModifierGetHostByName.php:17
+msgid "Set no value (null)"
+msgstr "Keinen Wert setzen (null)"
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:33
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:42
+msgid "Set null"
+msgstr "Auf \"null\" setzen"
+
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:43
+msgid "Set true"
+msgstr "Auf \"true\" setzen"
+
+#: library/Director/Web/Tabs/ObjectsTabs.php:81
+msgid "Sets"
+msgstr "Sets"
+
+#: application/forms/IcingaHostForm.php:116
+msgid ""
+"Setting a command endpoint allows you to force host checks to be executed by "
+"a specific endpoint. Please carefully study the related Icinga documentation "
+"before using this feature"
+msgstr ""
+"Das Setzen eines Kommandoenpunkts erlaubt das Ausführen von Host-Checks auf "
+"einem bestimmten Endpoint. Bitte nicht verwenden, ohne die entsprechende "
+"Icinga-Dokumentation gelesen zu haben"
+
+#: library/Director/Web/SelfService.php:97
+#: application/controllers/ConfigController.php:240
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: application/forms/SettingsForm.php:230
+msgid "Settings have been stored"
+msgstr "Einstellungen wurden gespeichert"
+
+#: library/Director/Web/SelfService.php:90
+msgid "Share this Template for Self Service API"
+msgstr "Für die Selbstbedienungs-API freigeben"
+
+#: library/Director/Web/SelfService.php:88
+msgid "Shared for Self Service API"
+msgstr "Für die Selbstbedienungs-API freigegeben"
+
+#: library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:21
+msgid "Should all the other characters be lowercased first?"
+msgstr ""
+"Sollen alle anderen Zeichen zuvor in Kleinbuchstaben umgewandelt werden?"
+
+#: application/controllers/HostController.php:597
+msgid "Show"
+msgstr "Zeigen"
+
+#: application/controllers/BasketController.php:202
+msgid "Show Basket"
+msgstr "Basket anzeigen"
+
+#: application/forms/DeployFormsBug7530.php:101
+#, php-format
+msgid "Show Issue %s on GitHub"
+msgstr "Issue %s auf GitHub anzeigen"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:75
+msgid "Show SQL"
+msgstr "SQL anzeigen"
+
+#: library/Director/Web/Table/ApplyRulesTable.php:151
+msgid "Show affected Objects"
+msgstr "Betroffene Objekte zeigen"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:97
+msgid "Show all actions"
+msgstr "Alle Aktionen anzeigen"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:501
+msgid "Show available options"
+msgstr "Verfügbare Optionen anzeigen"
+
+#: application/forms/IcingaObjectFieldForm.php:183
+msgid "Show based on filter"
+msgstr "Basierend auf Filter zeigen"
+
+#: library/Director/Web/Table/BranchActivityTable.php:86
+#: library/Director/Web/Table/ActivityLogTable.php:230
+msgid "Show details related to this change"
+msgstr "Details zu dieser Änderung zeigen"
+
+#: library/Director/Web/ObjectPreview.php:52
+msgid "Show normal"
+msgstr "Normal anzeigen"
+
+#: library/Director/Web/ObjectPreview.php:61
+msgid "Show resolved"
+msgstr "Aufgelöst anzeigen"
+
+#: library/Director/Web/Widget/BranchedObjectsHint.php:23
+#, php-format
+msgid ""
+"Showing a branched view, with potential changes being visible only in this %s"
+msgstr ""
+"Zeige eine abgezweigte Ansicht, mit potentiell nur in diesem %s sichtbaren "
+"Änderungen"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:33
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:31
+msgid "Simple match with wildcards (*)"
+msgstr "Einfache Suche mit Platzhaltern (*)"
+
+#: application/controllers/BasketController.php:343
+msgid "Single Object Diff"
+msgstr "Einfacher Objekt-Diff"
+
+#: library/Director/Dashboard/Dashlet/SingleServicesDashlet.php:11
+msgid "Single Services"
+msgstr "Einzelne Services"
+
+#: library/Director/Web/Table/GeneratedConfigFileTable.php:86
+msgid "Size"
+msgstr "Größe"
+
+#: application/forms/IcingaCommandArgumentForm.php:115
+msgid "Skip key"
+msgstr "Schlüssel überspringen"
+
+#: library/Director/PropertyModifier/PropertyModifierSkipDuplicates.php:13
+msgid "Skip row if this value appears more than once"
+msgstr "Zeile überspringen wenn dieser Wert mehr als einmal vorkommt"
+
+#: application/controllers/BasketController.php:249
+#: application/controllers/BasketController.php:373
+msgid "Snapshot"
+msgstr "Snapshot"
+
+#: library/Director/Web/Table/BasketTable.php:32
+#: application/controllers/BasketController.php:40
+#: application/controllers/BasketController.php:143
+msgid "Snapshots"
+msgstr "Snapshots"
+
+#: library/Director/Import/ImportSourceRestApi.php:194
+msgid "Something like https://api.example.com/rest/v2/objects"
+msgstr "So etwas wie https://api.example.com/rest/v2/objects"
+
+#: application/forms/SyncPropertyForm.php:183
+msgid "Source Column"
+msgstr "Quellspalte"
+
+#: application/forms/SyncPropertyForm.php:213
+msgid "Source Expression"
+msgstr "Quellausdruck"
+
+#: application/forms/SyncPropertyForm.php:38
+msgid "Source Name"
+msgstr "Quellenname"
+
+#: application/forms/SelfServiceSettingsForm.php:114
+msgid "Source Path"
+msgstr "Quellpfad"
+
+#: application/forms/ImportSourceForm.php:33
+msgid "Source Type"
+msgstr "Quellentyp"
+
+#: application/forms/SyncPropertyForm.php:160
+msgid "Source columns"
+msgstr "Quellspalten"
+
+#: library/Director/Web/Table/SyncpropertyTable.php:62
+msgid "Source field"
+msgstr "Quellenfeld"
+
+#: library/Director/Web/Table/ImportrunTable.php:30
+#: library/Director/Web/Table/ImportsourceTable.php:18
+#: library/Director/Web/Table/SyncpropertyTable.php:61
+msgid "Source name"
+msgstr "Quellenname"
+
+#: application/forms/SyncPropertyForm.php:356
+msgid "Special properties"
+msgstr "Spezielle Eigenschaften"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:36
+msgid "Specific Element (by key name)"
+msgstr "Spezifisches Element (nach Schlüsselname)"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:35
+msgid "Specific Element (by position)"
+msgstr "Spezifisches Element (nach Position)"
+
+#: library/Director/Import/ImportSourceRestApi.php:152
+msgid ""
+"Specify headers in text format \"Header: Value\", each header on a new line."
+msgstr ""
+"Header im Textformat angeben \"Header: Wert\", jeden Header auf einer Zeile."
+
+#: library/Director/PropertyModifier/PropertyModifierTrim.php:30
+msgid ""
+"Specify the characters that trim should remove.Default is: \" "
+"\\t\\n\\r\\0\\x0B\""
+msgstr ""
+"Zeichen welche von Trim entfernt werden sollen. Standard: \" "
+"\\t\\n\\r\\0\\x0B\""
+
+#: library/Director/Web/Widget/DeploymentInfo.php:87
+msgid "Stage name"
+msgstr "Phasenname"
+
+#: library/Director/Web/Widget/InspectPackages.php:50
+#, php-format
+msgid "Stages in Package: %s"
+msgstr "Phasen in Paket: %s"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:28
+msgid "Start time"
+msgstr "Startzeit"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:88
+msgid "Startup"
+msgstr "Start"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:162
+msgid "Startup Log"
+msgstr "Start-Log"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:72
+msgid "Startup Time"
+msgstr "Startzeit"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:84
+msgid "State"
+msgstr "Zustand"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1688
+msgid "State and transition type filters"
+msgstr "Status- und Übergangstypen-Filter"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:22
+msgid "State changes"
+msgstr "Zustandsänderungen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1663
+msgid "States"
+msgstr "Zustände"
+
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:91
+msgid "Statistics"
+msgstr "Statistiken"
+
+#: application/controllers/InspectController.php:42
+#: application/controllers/InspectController.php:143
+msgid "Status"
+msgstr "Zustand"
+
+#: library/Director/Web/SelfService.php:127
+msgid "Stop sharing this Template"
+msgstr "Diese Vorlage nicht mehr bereitstellen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:508
+#: application/forms/SettingsForm.php:139
+msgid "Store"
+msgstr "Speichern"
+
+#: application/forms/KickstartForm.php:36
+msgid "Store configuration"
+msgstr "Konfiguration speichern"
+
+#: library/Director/DataType/DataTypeDatalist.php:152
+msgid "Strict, list values only"
+msgstr "Strikt, nur Listeneinträge"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:79
+#: library/Director/DataType/DataTypeSqlQuery.php:76
+#: library/Director/DataType/DataTypeDatalist.php:132
+#: application/forms/IcingaCommandArgumentForm.php:38
+#: application/forms/IcingaCommandArgumentForm.php:77
+msgid "String"
+msgstr "Zeichenkette"
+
+#: application/views/helpers/FormDataFilter.php:534
+msgid "Strip this operator, preserve child nodes"
+msgstr "Diesen Operator entfernen, Kind-Knoten beibehalten"
+
+#: library/Director/Web/Form/QuickForm.php:221
+msgid "Submit"
+msgstr "Absenden"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:139
+msgid "Succeeded"
+msgstr "Gelungen"
+
+#: application/forms/IcingaObjectFieldForm.php:94
+msgid "Suggested fields"
+msgstr "Vorgeschlagene Felder"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:37
+msgid "Switch to Table view"
+msgstr "Zur Tabellenansicht wechseln"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:36
+msgid "Switch to Tree view"
+msgstr "Zur Baumansicht wechseln"
+
+#: application/controllers/SyncruleController.php:621
+#, php-format
+msgid "Sync \"%s\": %s"
+msgstr "\"%s\": \"%s\" synchronisieren"
+
+#: application/controllers/BranchController.php:45
+#: application/controllers/SyncruleController.php:203
+msgid "Sync Preview"
+msgstr "Synchronisationsvorschau"
+
+#: application/controllers/SyncruleController.php:155
+msgid "Sync Properties"
+msgstr "Synchronisationseigenschaften"
+
+#: application/forms/BasketForm.php:36
+msgid "Sync Rules"
+msgstr "Synchronisationsregeln"
+
+#: application/controllers/SyncruleController.php:653
+msgid "Sync history"
+msgstr "Synchronisationshistorie"
+
+#: application/forms/SyncRuleForm.php:98
+#, php-format
+msgid ""
+"Sync only part of your imported objects with this rule. Icinga Web 2 filter "
+"syntax is allowed, so this could look as follows: %s"
+msgstr ""
+"Nur einen Teil der importierten Objekte mit dieser Regel synchronisieren. "
+"Die Icinga Web 2 Filter Syntax kann verwendet werden. z.B: %s"
+
+#: application/controllers/SyncruleController.php:585
+msgid "Sync properties"
+msgstr "Synchronisationseigenschaften"
+
+#: library/Director/Web/Tabs/SyncRuleTabs.php:29
+#: library/Director/Web/Tabs/SyncRuleTabs.php:50
+#: library/Director/Web/Tabs/ImportTabs.php:23
+#: application/controllers/SyncrulesController.php:25
+#: application/controllers/SyncruleController.php:544
+msgid "Sync rule"
+msgstr "Synchronisationsregel"
+
+#: application/controllers/SyncruleController.php:55
+#: application/controllers/SyncruleController.php:509
+#, php-format
+msgid "Sync rule: %s"
+msgstr "Synchronisationsregel: %s"
+
+#: application/forms/SyncRunForm.php:35
+#, php-format
+msgid "Sync to Branch: %s"
+msgstr ""
+"In Branch s\n"
+"ynchronisieren: %s"
+
+#: application/controllers/SyncruleController.php:63
+msgid "Synchronization failed"
+msgstr "Synchronisation fehlgeschlagen"
+
+#: library/Director/Job/SyncJob.php:80
+msgid "Synchronization rule"
+msgstr "Synchronisationsregel"
+
+#: library/Director/Dashboard/Dashlet/SyncDashlet.php:14
+msgid "Synchronize"
+msgstr "Synchronisieren"
+
+#: application/controllers/SyncruleController.php:128
+#, php-format
+msgid "Synchronizing '%s'"
+msgstr "'%s' synchronisieren"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:30
+msgid "Table"
+msgstr "Tabelle"
+
+#: application/forms/RestoreBasketForm.php:51
+msgid "Target DB"
+msgstr "Ziel-DB"
+
+#: application/forms/IcingaCloneObjectForm.php:87
+msgid "Target Host"
+msgstr "Zielhost"
+
+#: application/forms/IcingaCloneObjectForm.php:78
+msgid "Target Service Set"
+msgstr "Ziel-Service-Set"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:77
+#: library/Director/DataType/DataTypeSqlQuery.php:74
+#: library/Director/DataType/DataTypeDatalist.php:130
+msgid "Target data type"
+msgstr "Zieldatentyp"
+
+#: application/forms/ImportRowModifierForm.php:42
+msgid "Target property"
+msgstr "Zieleigenschaft"
+
+#: library/Director/DataType/DataTypeDictionary.php:86
+#: library/Director/Web/Form/DirectorObjectForm.php:1111
+#: library/Director/Web/Form/DirectorObjectForm.php:1115
+#: library/Director/Web/Controller/TemplateController.php:159
+msgid "Template"
+msgstr "Vorlage"
+
+#: library/Director/DataType/DataTypeDictionary.php:64
+msgid "Template (Object) Type"
+msgstr "Vorlagentyp (Objekt)"
+
+#: library/Director/Web/Table/TemplatesTable.php:52
+msgid "Template Name"
+msgstr "Vorlagenname"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:16
+msgid "Template name"
+msgstr "Vorlagenname"
+
+#: library/Director/Web/Controller/ObjectController.php:348
+#: library/Director/Web/Controller/TemplateController.php:116
+#, php-format
+msgid "Template: %s"
+msgstr "Vorlage: %s"
+
+#: library/Director/Import/ImportSourceDirectorObject.php:82
+#: library/Director/Web/Tree/TemplateTreeRenderer.php:43
+#: library/Director/Web/Table/DependencyTemplateUsageTable.php:10
+#: library/Director/Web/Table/NotificationTemplateUsageTable.php:10
+#: library/Director/Web/Table/HostTemplateUsageTable.php:10
+#: library/Director/Web/Table/TemplateUsageTable.php:23
+#: library/Director/Web/Table/ServiceTemplateUsageTable.php:10
+#: library/Director/Web/Tabs/ObjectsTabs.php:58
+#: application/forms/IcingaServiceForm.php:710
+msgid "Templates"
+msgstr "Vorlagen"
+
+#: application/forms/IcingaCloneObjectForm.php:32
+msgid "Templates cannot be cloned in Configuration Branches"
+msgstr "Vorlagen können in Konfigurationszweigen nicht geklont werden"
+
+#: application/forms/DeployFormsBug7530.php:116
+msgid "Thanks, I'll verify this and come back later"
+msgstr "Danke, ich überprüfe das und komme wieder"
+
+#: library/Director/Job/ImportJob.php:67
+msgid "The \"Import\" job allows to run import actions at regular intervals"
+msgstr ""
+"Der \"Import\" Auftrag erlaubt das Ausführen von Importen in regelmäßigen "
+"Abständen"
+
+#: library/Director/Job/SyncJob.php:65
+msgid "The \"Sync\" job allows to run sync actions at regular intervals"
+msgstr ""
+"Der \"Sync\" Auftrag erlaubt das Ausführen von Synchronisationen in "
+"regelmäßigen Abständen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:661
+#: application/forms/IcingaTimePeriodRangeForm.php:84
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:89
+#, php-format
+msgid "The %s has successfully been stored"
+msgstr "%s wurde erfolgreich gespeichert"
+
+#: library/Director/Job/ConfigJob.php:71
+msgid ""
+"The Config job allows you to generate and eventually deploy your Icinga 2 "
+"configuration"
+msgstr ""
+"Der Auftrag \"Konfiguration\" erlaubt das Erstellen und eventuelle Ausrollen "
+"der Icinga 2 Konfiguration"
+
+#: application/forms/IcingaUserForm.php:37
+msgid "The Email address of the user."
+msgstr "Die E-Mail-Adresse des Benutzers."
+
+#: library/Director/Job/HousekeepingJob.php:21
+msgid ""
+"The Housekeeping job provides various task that keep your Director database "
+"fast and clean"
+msgstr ""
+"Der \"Bereinigen\" Auftrag bietet verschiedene Aktionen, die die Director-"
+"Datenbank schnell und sauber halten"
+
+#: application/controllers/DaemonController.php:39
+#, php-format
+msgid ""
+"The Icinga Director Background Daemon is not running. Please check our %s in "
+"case you need step by step instructions showing you how to fix this."
+msgstr ""
+"Der Icinga Director Hintergrunddienst läuft nicht. Um eine Schritt-für-"
+"Schritt-Anleitung hierzu anzuzeigen bitte unsere %s zu Rate ziehen."
+
+#: application/controllers/SettingsController.php:38
+msgid ""
+"The Icinga Director Self Service API allows your Hosts to register "
+"themselves. This allows them to get their Icinga Agent configured, installed "
+"and upgraded in an automated way."
+msgstr ""
+"Die Icinga Director Selbstbedienungs-API erlaubt es Hosts sich selbst zu "
+"registrieren. Auf diese Weise wird deren Icinga-Agent konfiguriert, "
+"installiert und automatisch aktualisiert."
+
+#: application/forms/SettingsForm.php:45
+msgid ""
+"The Icinga Package name Director uses to deploy it's configuration. This "
+"defaults to \"director\" and should not be changed unless you really know "
+"what you're doing"
+msgstr ""
+"Der Icinga Package-Name welchen der Director zum Ausrollen seiner "
+"Konfiguration benutzt. Für gewöhnlich ist das \"director\" und sollte nur "
+"geändert werden, wenn die Auswirkung dieser Anpassung bewusst ist"
+
+#: library/Director/Import/ImportSourceLdap.php:72
+msgid ""
+"The LDAP properties that should be fetched. This is required to be a comma-"
+"separated list like: \"cn, dnshostname, operatingsystem, sAMAccountName\""
+msgstr ""
+"Die LDAP Eigenschaften, die geholt werden sollen. Muss eine Komma-separierte "
+"Liste sein, wie \"cn, dnshostname, operatingsystem, sAMAccountName\""
+
+#: application/forms/IcingaForgetApiKeyForm.php:31
+#, php-format
+msgid "The Self Service API key for %s has been dropped"
+msgstr "Der Selbstbedienungs-API-Schlüssel für %s wurde verworfen"
+
+#: application/forms/IcingaAddServiceSetForm.php:116
+#, php-format
+msgid "The Service Set \"%s\" has been added to %d hosts"
+msgstr "Das Service-Set \"%s\" wurde zu %d Hosts hinzugefügt"
+
+#: application/forms/IcingaCommandArgumentForm.php:173
+#, php-format
+msgid "The argument %s has successfully been stored"
+msgstr "Das Argument %s wurde erfolgreich gespeichert"
+
+#: application/forms/IcingaObjectFieldForm.php:129
+msgid "The caption which should be displayed"
+msgstr "Die Beschriftung, die angezeigt werden soll"
+
+#: application/forms/DirectorDatafieldForm.php:153
+msgid ""
+"The caption which should be displayed to your users when this field is shown"
+msgstr "Die Beschriftung welche Benutzern zu diesem Feld angezeigt werden soll"
+
+#: application/forms/IcingaDependencyForm.php:234
+msgid "The child host."
+msgstr "Der Kind-Host."
+
+#: application/forms/IcingaCommandForm.php:61
+msgid ""
+"The command Icinga should run. Absolute paths are accepted as provided, "
+"relative paths are prefixed with \"PluginDir + \", similar Constant prefixes "
+"are allowed. Spaces will lead to separation of command path and standalone "
+"arguments. Please note that this means that we do not support spaces in "
+"plugin names and paths right now."
+msgstr ""
+"Das Kommando, das Icinga ausführen soll. Absolute Pfade werden wie angegeben "
+"übernommen, relativen Pfaden wird ein \"PluginDir +\" vorangestellt, wobei "
+"ähnliche Konstanten als Prefix erlaubt sind. Leerzeichen können zur Teilung "
+"des Pfads zum Kommandos und eigenständigen Argumenten verwendet werden. Das "
+"bedeutet, das aktuell keine Leerzeichen in Plugin-Namen und Pfaden "
+"unterstützt werden."
+
+#: application/forms/KickstartForm.php:165
+msgid "The corresponding password"
+msgstr "Das entsprechende Passwort"
+
+#: library/Director/PropertyModifier/PropertyModifierStripDomain.php:14
+msgid "The domain name you want to be stripped"
+msgstr "Der Domänenname, der entfernt werden soll"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:18
+msgid "The first (leftmost) CN"
+msgstr "Der erste (linkeste) CN"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:19
+msgid "The first (leftmost) OU"
+msgstr "Die erste (linkeste) OU"
+
+#: library/Director/Web/Controller/ObjectController.php:718
+#: application/controllers/ConfigController.php:443
+#, php-format
+msgid "The following modifications are visible in this %s only..."
+msgstr "Die folgenden Änderungen sind nur in diesem %s sichtbar..."
+
+#: application/forms/IcingaServiceForm.php:767
+#, php-format
+msgid "The given properties have been stored for \"%s\""
+msgstr "Die übergebenen Eigenschaften wurden für \"%s\" gespeichert"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1666
+msgid "The host/service states you want to get notifications for"
+msgstr ""
+"Die Host/Service Status, für die Benachrichtigungen versandt werden sollen"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:21
+msgid "The last (rightmost) OU"
+msgstr "Die letzte (rechteste) OU"
+
+#: library/Director/Web/Widget/JobDetails.php:60
+#, php-format
+msgid "The last attempt failed %s: %s"
+msgstr "Der letzte Versuch schlug %s fehl: %s"
+
+#: library/Director/Web/Widget/JobDetails.php:55
+#, php-format
+msgid "The last attempt succeeded %s"
+msgstr "Der letzte Versuch war %s erfolgreich"
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:83
+msgid "The last deployment did not succeed"
+msgstr "Das letzte Ausrollen war nicht erfolgreich"
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:85
+msgid "The last deployment is currently pending"
+msgstr "Das Ausrollen der Konfiguration ist im Gange"
+
+#: application/forms/IcingaEndpointForm.php:42
+msgid "The log duration time."
+msgstr "Die Dauer der Aufzeichnungen."
+
+#: application/forms/IcingaUserForm.php:160
+msgid ""
+"The name of a time period which determines when notifications to this User "
+"should be triggered. Not set by default."
+msgstr ""
+"Der Name des Zeitraumes, der angibt, wann Benachrichtigungen für diesen "
+"Benutzer ausgelöst werden sollen. Kein Default-Wert."
+
+#: application/forms/IcingaNotificationForm.php:242
+#: application/forms/IcingaDependencyForm.php:143
+msgid ""
+"The name of a time period which determines when this notification should be "
+"triggered. Not set by default."
+msgstr ""
+"Der Name des Zeitraumes, der angibt, wann diese Benachrichtigung ausgelöst "
+"werden soll. Kein Default-Wert."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1418
+msgid ""
+"The name of a time period which determines when this object should be "
+"monitored. Not limited by default."
+msgstr ""
+"Der Name des Zeitraumes, der angibt, wann dieses Objekt überwacht wird. "
+"Standardmäßig nicht eingeschränkt."
+
+#: application/forms/DirectorJobForm.php:62
+msgid ""
+"The name of a time period within this job should be active. Supports only "
+"simple time periods (weekday and multiple time definitions)"
+msgstr ""
+"Der Name des Zeitraums innerhalb derer dieser Auftrag aktiv sein soll. "
+"Erlaubt nur einfache Zeiträume (Wochentag und mehrere Zeitangaben)"
+
+#: application/forms/IcingaHostVarForm.php:16
+msgid "The name of the host"
+msgstr "Der Name des Hosts"
+
+#: application/forms/IcingaServiceVarForm.php:16
+msgid "The name of the service"
+msgstr "Der Name des Service"
+
+#: application/forms/IcingaNotificationForm.php:178
+msgid ""
+"The notification interval (in seconds). This interval is used for active "
+"notifications. Defaults to 30 minutes. If set to 0, re-notifications are "
+"disabled."
+msgstr ""
+"Der Benachrichtigungsintervall (in Sekunden). Dieser Intervall wird für "
+"aktive Benachrichtigungen verwendet. Default-Wert ist 30 Minuten. Wird er "
+"auf 0 gesetzt, sind Benachrichtungswiederholungen deaktiviert."
+
+#: library/Director/PropertyModifier/PropertyModifierBitmask.php:16
+msgid ""
+"The numeric bitmask you want to apply. In case you have a hexadecimal or "
+"binary mask please transform it to a decimal number first. The result of "
+"this modifier is a boolean value, telling whether the given mask applies to "
+"the numeric value in your source column"
+msgstr ""
+"Die numerische Bitmaske welche angewandt werden soll. Hexadezimale und "
+"binäre Masken müssen zuvor in eine dezimale Zahl umgewandelt werden. Das "
+"Ergebnis ist ein boolescher Wert welcher angibt, ob die Maske auf den "
+"numerischen Wert in der Quellspalte zutrifft"
+
+#: application/forms/IcingaUserForm.php:42
+msgid "The pager address of the user."
+msgstr "Die Pageradresse des Benutzers."
+
+#: application/forms/IcingaDependencyForm.php:202
+msgid ""
+"The parent host. You might want to refer Host Custom Variables via $host."
+"vars.varname$"
+msgstr ""
+"Der Elternhost. Benutzerdefinierte Hosteigenschaften lassen sich via $host."
+"vars.varname$ referenzieren"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexReplace.php:15
+msgid ""
+"The pattern you want to search for. This can be a regular expression like /"
+"^www\\d+\\./"
+msgstr ""
+"Das Muster, nach dem gesucht werden soll. Kann ein regulärer Ausdruck wie /"
+"^www\\d+\\./ sein"
+
+#: application/forms/IcingaEndpointForm.php:37
+msgid "The port of the endpoint."
+msgstr "Der Port des Endpunkts."
+
+#: application/forms/KickstartForm.php:148
+msgid ""
+"The port you are going to use. The default port 5665 will be used if none is "
+"set"
+msgstr ""
+"Der Port, der genutzt werden soll. Wird kein Wert gesetzt, gilt der Default-"
+"Wert 5665"
+
+#: library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php:64
+msgid "The property to get from the row we found in the chosen Import Source"
+msgstr ""
+"Die Eigenschaft welche wir aus der Zeile die wir in der gewählten "
+"Importquelle gefunden haben extrahieren wollen"
+
+#: application/forms/IcingaAddServiceForm.php:176
+#, php-format
+msgid "The service \"%s\" has been added to %d hosts"
+msgstr "Der Service \"%s\" wurde zu %d Hosts hinzugefügt"
+
+#: application/forms/IcingaAddServiceSetForm.php:88
+msgid "The service Set that should be assigned"
+msgstr "Das Service-Set welches zugewiesen werden soll"
+
+#: application/forms/IcingaServiceSetForm.php:89
+msgid "The service set that should be assigned to this host"
+msgstr "Das Service-Set welches diesem Host zugewiesen werden soll"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1676
+msgid "The state transition types you want to get notifications for"
+msgstr ""
+"Die Arten von Statusänderungen, für die Benachrichtigungen verschickt werden "
+"sollen"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexReplace.php:23
+msgid "The string that should be used as a preplacement"
+msgstr "Die Zeichenkette, die als Ersatz genutzt werden soll"
+
+#: library/Director/PropertyModifier/PropertyModifierReplace.php:14
+msgid "The string you want to search for"
+msgstr "Die Zeichenkette, nach der gesucht werden soll"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:56
+msgid ""
+"The string/pattern you want to search for, use regular expression like /"
+"^www\\d+\\./"
+msgstr ""
+"Das Muster, nach dem gesucht werden soll. Kann ein regulärer Ausdruck wie /"
+"^www\\d+\\./ sein"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:46
+msgid ""
+"The string/pattern you want to search for, use wildcard matches like www.* "
+"or *linux*"
+msgstr ""
+"Das Muster, nach dem gesucht werden soll, Platzhalter wie www.* oder *linux* "
+"sind möglich"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:41
+msgid ""
+"The string/pattern you want to search for. Depends on the chosen method, use "
+"www.* or *linux* for wildcard matches and expression like /^www\\d+\\./ in "
+"case you opted for a regular expression"
+msgstr ""
+"Der String/das Muster nach dem gesucht werden soll. Abhängig von der "
+"gewählten Methode, benutze www.* oder *linux* für Platzhalter-basierte "
+"Suchen und Ausdrücken wie /^www\\d+\\./ falls Reguläre Ausdrücke bevorzugt "
+"wurden"
+
+#: application/forms/DirectorDatafieldCategoryForm.php:25
+msgid ""
+"The unique name of the category used for grouping your custom Data Fields."
+msgstr ""
+"Der eindeutige Kategoriebezeichner welcher zum Gruppieren der "
+"benutzerdefinierten Felder benutzt werden soll."
+
+#: application/forms/DirectorDatafieldForm.php:143
+msgid ""
+"The unique name of the field. This will be the name of the custom variable "
+"in the rendered Icinga configuration."
+msgstr ""
+"Der eindeutige Bezeichner dieses Feldes. Dieser wird als Name der "
+"benutzerdefinierten Eigenschaft in der gerenderten Icinga-Konfiguration "
+"benutzt."
+
+#: application/forms/SelfServiceSettingsForm.php:181
+msgid "The user that should run the Icinga 2 service on Windows."
+msgstr ""
+"Der Benutzeraccount unter welchem der Icinga-2-Dienst unter Windows laufen "
+"soll."
+
+#: application/forms/DirectorDatafieldForm.php:74
+#, php-format
+msgid ""
+"There are %d objects with a related property. Should I also remove the "
+"\"%s\" property from them?"
+msgstr ""
+"Es gibt %d Objekte mit einer entsprechenden Eigenschaft. Soll der Wert für "
+"\"%s\" von all diesen entfernt werden?"
+
+#: application/forms/DirectorDatafieldForm.php:118
+#, php-format
+msgid ""
+"There are %d objects with a related property. Should I also rename the "
+"\"%s\" property to \"%s\" on them?"
+msgstr ""
+"Es gibt %d Objekte mit einer entsprechenden Eigenschaft. Soll auf all diesen "
+"die Variable \"%s\" nach \"%s\" umbenannt werden?"
+
+#: application/forms/DeploymentLinkForm.php:66
+#, php-format
+msgid "There are %d pending changes"
+msgstr "Es gibt %d noch nicht ausgerollte Änderungen"
+
+#: application/forms/DeploymentLinkForm.php:79
+#, php-format
+msgid "There are %d pending changes, %d of them applied to this object"
+msgstr ""
+"Es sind %d Änderungen ausständig, wovon %d auf dieses Objekt angewendet "
+"werden"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:90
+#, php-format
+msgid "There are %d pending database migrations"
+msgstr "Es sind %d Datenbankmigrationen ausständig"
+
+#: application/forms/IcingaObjectFieldForm.php:117
+msgid ""
+"There are no data fields available. Please ask an administrator to create "
+"such"
+msgstr ""
+"Es sind keine Datenfelder verfügbar. Ein Administrator kann welche anlegen"
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:91
+msgid "There are no pending changes"
+msgstr "Es gibt keine anstehenden Änderungen"
+
+#: application/forms/DeployConfigForm.php:34
+msgid "There are no pending changes. Deploy anyway"
+msgstr "Es gibt keine anstehenden Änderungen. Dennoch ausrollen"
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:52
+msgid ""
+"There are pending changes for this Import Source. You should trigger a new "
+"Import Run."
+msgstr ""
+"Es gibt ausstehende Änderungen für diese Importquelle. Ein neuer Importlauf "
+"sollte angestoßen werden."
+
+#: application/controllers/SyncruleController.php:108
+msgid ""
+"There are pending changes for this Sync Rule. You should trigger a new Sync "
+"Run."
+msgstr ""
+"Es gibt ausstehende Änderungen für diese Synchronisationsregel. Ein neuer "
+"Synchronisationslauf sollte angestoßen werden."
+
+#: application/forms/KickstartForm.php:66
+msgid "There are pending database migrations"
+msgstr "Es sind Datenbankmigrationen ausständig"
+
+#: application/forms/DeploymentLinkForm.php:71
+msgid ""
+"There has been a single change to this object, nothing else has been modified"
+msgstr ""
+"An diesem Objekt wurde eine einzelne Änderung vorgenommen. Nichts Anderes "
+"wurde verändert"
+
+#: application/forms/DeploymentLinkForm.php:74
+#, php-format
+msgid ""
+"There have been %d changes to this object, nothing else has been modified"
+msgstr ""
+"An diesem Objekt wurden %d Änderungen vorgenommen, nichts anderes wurde "
+"verändert"
+
+#: application/forms/DeploymentLinkForm.php:63
+msgid "There is a single pending change"
+msgstr "Es gibt eine einzelne ausstehende Änderung"
+
+#: application/forms/DirectorJobForm.php:21
+msgid "These are different available job types"
+msgstr "Verschiedene verfügbare Auftragstypen"
+
+#: application/forms/ImportSourceForm.php:37
+msgid ""
+"These are different data providers fetching data from various sources. You "
+"didn't find what you're looking for? Import sources are implemented as a "
+"hook in Director, so you might find (or write your own) Icinga Web 2 module "
+"fetching data from wherever you want"
+msgstr ""
+"Dies sind verschiedene Datenanbieter, die Daten von diversen Quellen holen. "
+"Falls der gesuchte Anbieter nicht dabei ist, kann ein weiteres (evtl. selbst "
+"geschriebenes) Icinga Web 2 Modul diese Funktionalität nachrüsten, da "
+"Importquellen als Hook in Director realisiert werden"
+
+#: application/controllers/CommandController.php:76
+#, php-format
+msgid "This Command is currently being used by %s"
+msgstr "Dieses Kommando wird gegenwärtig von %s benutzt"
+
+#: application/controllers/CommandController.php:82
+msgid "This Command is currently not in use"
+msgstr "Dieses Kommando wird gegenwärtig nicht benutzt"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:951
+#, php-format
+msgid "This Command is still in use by %d other objects"
+msgstr "Dieses Kommando wird von %d anderen Objekten benutzt"
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:59
+#, php-format
+msgid "This Import Source failed when last checked at %s: %s"
+msgstr "Diese Importquelle schlug bei der letzten Prüfung um %s fehl: %s"
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:67
+#, php-format
+msgid "This Import Source has an invalid state: %s"
+msgstr "Diese Importquelle hat einen ungültigen Zustand: %s"
+
+#: application/forms/ImportCheckForm.php:33
+msgid "This Import Source provides modified data"
+msgstr "Diese Importquelle stellt veränderte Daten zur Verfügung"
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:42
+#, php-format
+msgid "This Import Source was last found to be in sync at %s."
+msgstr "Diese Importquelle war zuletzt um %s syncron."
+
+#: application/forms/IcingaServiceForm.php:136
+msgid "This Service has been deactivated on this host"
+msgstr "Dieser Service wurde auf diesem Host deaktiviert"
+
+#: application/controllers/SyncruleController.php:115
+#, php-format
+msgid "This Sync Rule failed when last checked at %s: %s"
+msgstr ""
+"Diese Synchronisationsregel schlug bei der letzten Prüfung um %s fehl: %s"
+
+#: application/controllers/SyncruleController.php:85
+msgid "This Sync Rule has never been run before."
+msgstr "Diese Synchronisationsregel wurde noch nie ausgeführt."
+
+#: application/controllers/SyncruleController.php:258
+msgid "This Sync Rule is in sync and would currently not apply any changes"
+msgstr ""
+"Diese Sync-Regel ist synchron und würde aktuell keine Änderungen verursachen"
+
+#: application/controllers/SyncruleController.php:97
+#, php-format
+msgid "This Sync Rule was last found to by in Sync at %s."
+msgstr "Diese Synchronisationsregel war zuletzt synchron um %s."
+
+#: application/forms/SyncPropertyForm.php:105
+msgid ""
+"This allows to filter for specific parts within the given source expression. "
+"You are allowed to refer all imported columns. Examples: host=www* would set "
+"this property only for rows imported with a host property starting with "
+"\"www\". Complex example: host=www*&!(address=127.*|address6=::1)"
+msgstr ""
+"Erlaubt das Filtern nach bestimmten Teilen innerhalb des angegebenen "
+"Quellausdrucks. Alle importierten Spalten können angegeben werden. "
+"Beispiele: host=www* würde diese Eigenschaft nur für importierte Zeilen "
+"setzen, bei denen die Host Eigenschaft mit \"www\" beginnt. Komplexes "
+"Beispiel: host=www*&!(address=127.*|address6=::1)"
+
+#: library/Director/DataType/DataTypeDatalist.php:147
+msgid "This allows to show either a drop-down list or an auto-completion"
+msgstr ""
+"Dies erlaubt es, entweder eine Ausklappmenü oder ein Feld mit automatischer "
+"Vervollständigung anzuzeigen"
+
+#: application/forms/DirectorJobForm.php:39
+msgid "This allows to temporarily disable this job"
+msgstr "Erlaubt das vorübergehende Deaktivieren dieses Auftrags"
+
+#: application/forms/IcingaHostGroupForm.php:30
+#: application/forms/IcingaNotificationForm.php:106
+#: application/forms/IcingaServiceGroupForm.php:30
+#: application/forms/IcingaServiceForm.php:476
+#: application/forms/IcingaDependencyForm.php:115
+#: application/forms/IcingaScheduledDowntimeForm.php:115
+msgid ""
+"This allows you to configure an assignment filter. Please feel free to "
+"combine as many nested operators as you want. The \"contains\" operator is "
+"valid for arrays only. Please use wildcards and the = (equals) operator when "
+"searching for partial string matches, like in *.example.com"
+msgstr ""
+"Hiermit lässt sich ein Zuweisungsfilter (assign) definieren. Dabei lassen "
+"sich beliebig viele Operatoren beliebig tief verschachtelt benutzen. Der "
+"Operator \"contains\" (\"enthält\") ist nur für Arrays zulässig. Um "
+"Teilstrings zu vergleichen bitte Jokerzeichen (wildcards) benutzen, wie in *."
+"example.com"
+
+#: application/forms/IcingaServiceSetForm.php:123
+msgid ""
+"This allows you to configure an assignment filter. Please feel free to "
+"combine as many nested operators as you want. You might also want to skip "
+"this, define it later and/or just add this set of services to single hosts. "
+"The \"contains\" operator is valid for arrays only. Please use wildcards and "
+"the = (equals) operator when searching for partial string matches, like in *."
+"example.com"
+msgstr ""
+"Hiermit lässt sich ein Zuweisungsfilter (assign) definieren. Dabei lassen "
+"sich beliebig viele Operatoren beliebig tief verschachtelt benutzen. Dieser "
+"Schritt lässt sich auch überspringen und eventuell später umsetzen. "
+"Alternativ (oder zusätzlich) kann dieses Service-Set einzelne Hosts direkt "
+"zugewiesen werden. Der Operator \"contains\" (\"enthält\") ist nur für "
+"Arrays zulässig. Um Teilstrings zu vergleichen bitte Jokerzeichen "
+"(wildcards) benutzen, wie in *.example.com"
+
+#: library/Director/Job/ConfigJob.php:47
+msgid "This allows you to immediately deploy a modified configuration"
+msgstr "Dies erlaubt das sofortige Ausrollen einer veränderten Konfiguration"
+
+#: library/Director/ProvidedHook/CubeLinks.php:37
+#: library/Director/ProvidedHook/IcingaDbCubeLinks.php:32
+#, php-format
+msgid "This allows you to modify properties for \"%s\""
+msgstr "Erlaubt das Ändern der Eigenschaften von \"%s\""
+
+#: library/Director/ProvidedHook/CubeLinks.php:54
+#: library/Director/ProvidedHook/IcingaDbCubeLinks.php:55
+msgid "This allows you to modify properties for all chosen hosts at once"
+msgstr ""
+"Hiermit lassen sich Eigenschaften für alle gewählten Hosts auf einmal ändern"
+
+#: application/controllers/BasketController.php:64
+msgid "This basket is empty"
+msgstr "Dieser Basket ist leer"
+
+#: application/forms/KickstartForm.php:230
+msgid "This has to be a MySQL or PostgreSQL database"
+msgstr "Muss eine MySQL oder PostgeSQL Datenbank sein"
+
+#: library/Director/Web/SelfService.php:65
+msgid ""
+"This host has been registered via the Icinga Director Self Service API. In "
+"case you re-installed the host or somehow lost it's secret key, you might "
+"want to dismiss the current key. This would allow you to register the same "
+"host again."
+msgstr ""
+"Dieser Host wurde über die Icinga Director Selbstbedienungs-API registriert. "
+"Falls der Host neu installiert wurde oder der Schlüssel anderweitig verloren "
+"ging, kann es erwünscht sein, den aktuellen Schlüssel zu verwerfen. Das "
+"würde es erlauben, denselben Host neu zu registrieren."
+
+#: application/controllers/InspectController.php:71
+msgid "This is an abstract object type."
+msgstr "Das ist ein abstrakter Objekttyp."
+
+#: application/forms/SelfServiceSettingsForm.php:45
+msgid ""
+"This is only important in case your master/satellite nodes do not have IP "
+"addresses as their \"host\" property. The Agent can be told to issue related "
+"DNS lookups on it' own"
+msgstr ""
+"Das ist nur wichtig wenn Master- oder Satellitenknoten keine IP als \"host\"-"
+"Eigenschaft gesetzt haben. Dem Agent kann angewiesen werden, eigenständig "
+"entsprechende DNS-Lookups eigenständig vorzunehmen"
+
+#: library/Director/Web/Controller/TemplateController.php:179
+#, php-format
+msgid "This is the \"%s\" %s Template. Based on this, you might want to:"
+msgstr "Das ist die \"%s\" %s Vorlage. Basierend auf diese, kann man:"
+
+#: application/forms/KickstartForm.php:123
+msgid ""
+"This is the name of the Endpoint object (and certificate name) you created "
+"for your ApiListener object. In case you are unsure what this means please "
+"make sure to read the documentation first"
+msgstr ""
+"Der Name (und Zertifikatsname) des Endpunkt-Objekts, der für das ApiListener-"
+"Objekt erstellt wurde. Bei Unklarheit, was damit gemeint ist, sollte die "
+"Dokumentation erneut zu Rate gezogen werden"
+
+#: library/Director/Dashboard/Dashlet/HostsDashlet.php:17
+msgid ""
+"This is where you add all your servers, containers, network or sensor "
+"devices - and much more. Every subject worth to be monitored"
+msgstr ""
+"Hier werden alle Server, Container, Netzwerk- oder Sensor-Geräte und vieles "
+"mehr hinzugefügt. Jede Komponente die es wert ist, überwacht zu werden"
+
+#: library/Director/Dashboard/HostsDashboard.php:22
+msgid ""
+"This is where you manage your Icinga 2 Host Checks. Host templates are your "
+"main building blocks. You can bundle them to \"choices\", allowing (or "
+"forcing) your users to choose among a given set of preconfigured templates."
+msgstr ""
+"Hier werden Icinga 2 Host-Checks verwaltet. Host-Vorlagen stellen die "
+"Hauptbausteine dar. Diese können in \"Auswahlmöglichkeiten\" gebündelt "
+"werden, um es eigenen Benutzern zu erlauben, aus einem definierten Set von "
+"Vorlagen zu wählen - oder dies gar zu erzwingen."
+
+#: library/Director/Dashboard/ServicesDashboard.php:24
+msgid ""
+"This is where you manage your Icinga 2 Service Checks. Service Templates are "
+"your base building blocks, Service Sets allow you to assign multiple "
+"Services at once. Apply Rules make it possible to assign Services based on "
+"Host properties. And the list of all single Service Objects gives you the "
+"possibility to still modify (or delete) many of them at once."
+msgstr ""
+"Hier werden Icinga 2 Service-Checks verwaltet. Service-Vorlagen stellen die "
+"Hauptbausteine dar, Service-Sets erlauben es mehrere Services gemeinsam "
+"zuzuweisen. Apply-Regeln ermöglichen es, Services basierend auf Host-"
+"Eigenschaften zuzuweisen. Und die Liste aller Einzel-Services erlaubt es, "
+"einzelne oder mehrere zugleich zu bearbeiten oder zu löschen."
+
+#: library/Director/Dashboard/UsersDashboard.php:21
+msgid ""
+"This is where you manage your Icinga 2 User (Contact) objects. Try to keep "
+"your User objects simply by movin complexity to your templates. Bundle your "
+"users in groups and build Notifications based on them. Running MS Active "
+"Directory or another central User inventory? Stay away from fiddling with "
+"manual config, try to automate all the things with Imports and related Sync "
+"Rules!"
+msgstr ""
+"Hier werden Icingga 2 Benutzer-Objekte (Kontakte) verwaltet. Es wird "
+"empfohlen Benutzerobjekte einfach zu halten, und Komplexität in die "
+"entsprechenden Vorlagen zu verschieben. Das Bündeln von Benutzern in Gruppen "
+"erlaubt auf selbige basierende Benachrichtigungen. Ist MS Active Directory "
+"oder ein anderes zentrales Benutzerverzeichnis vorhanden? Dann gilt es sich "
+"am Besten überhaupt nicht mit manueller Konfiguration abzumühen. Über die "
+"Import-Funktion und entsprechende Synchronisationsregeln lässt sich nahezu "
+"alles automatisieren!"
+
+#: library/Director/Dashboard/InfrastructureDashboard.php:39
+msgid ""
+"This is where you manage your Icinga 2 infrastructure. When adding a new "
+"Icinga Master or Satellite please re-run the Kickstart Helper once."
+msgstr ""
+"Hier wird die Icinga 2 Infrastruktur verwaltet. Den Kickstart-Helper beim "
+"Hinzufügen von neuen Icinga Mastern oder Satelliten bitte erneut ausführen."
+
+#: library/Director/Web/Table/ObjectsTableEndpoint.php:47
+msgid "This is your Config master and will receive our Deployments"
+msgstr ""
+"Dies ist der Konfigurations-Master zu welchem sämtliche Konfiguration "
+"ausgerollt wird"
+
+#: library/Director/Web/Widget/JobDetails.php:66
+msgid "This job has not been executed yet"
+msgstr "Dieser Auftrag wurde bisher nicht ausgeführt"
+
+#: library/Director/Web/Widget/JobDetails.php:36
+#, php-format
+msgid "This job runs every %ds and is currently pending"
+msgstr "Dieser Auftrag läuft alle %d und ist aktuell ausständig"
+
+#: library/Director/Web/Widget/JobDetails.php:40
+#, php-format
+msgid "This job runs every %ds."
+msgstr "Dieser Auftrag läuft alle %d."
+
+#: library/Director/Web/Widget/JobDetails.php:27
+#, php-format
+msgid ""
+"This job would run every %ds. It has been disabled and will therefore not be "
+"executed as scheduled"
+msgstr ""
+"Dieser Auftrag würde alle %d ausgeführt. Er ist deaktiviert und wird deshalb "
+"nicht wie geplant ausgeführt"
+
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:33
+msgid ""
+"This modifier transforms 0/\"0\"/false/\"false\"/\"n\"/\"no\" to false and "
+"1, \"1\", true, \"true\", \"y\" and \"yes\" to true, both in a case "
+"insensitive way. What should happen if the given value does not match any of "
+"those? You could return a null value, or default to false or true. You might "
+"also consider interrupting the whole import process as of invalid source data"
+msgstr ""
+"Dieser Modifikator verwandelt 0/\"0\"/false/\"false\"/\"n\"/\"no\" nach "
+"false und , \"1\", true, \"true\", \"y\" and \"yes\" nach true, jeweils "
+"unabhängig von Groß-/Kleinschreibung. Was soll passieren falls keiner dieser "
+"Fälle zutrifft? In diesem Fall lässt sich ein Null-Wert zurückgeben oder "
+"aber true oder false als Fallback nutzen. Alternativ kann man auch den "
+"komplette Importvorgang aufgrund ungültiger Daten fehlschlagen lassen"
+
+#: application/forms/ImportSourceForm.php:89
+msgid ""
+"This must be a column containing unique values like hostnames. Unless "
+"otherwise specified this will then be used as the object_name for the "
+"syncronized Icinga object. Especially when getting started with director "
+"please make sure to strictly follow this rule. Duplicate values for this "
+"column on different rows will trigger a failure, your import run will not "
+"succeed. Please pay attention when synching services, as \"purge\" will only "
+"work correctly with a key_column corresponding to host!name. Check the "
+"\"Combine\" property modifier in case your data source cannot provide such a "
+"field"
+msgstr ""
+"Muss eine Spalte sein, die eindeutige Werte wie Hostnamen enthält. Wenn "
+"nicht anders angegeben, wird dieser Wert dann als object_name für die "
+"synchronisierten Icinga Objekte verwendet. Insbesondere Benutzer mit wenig "
+"Erfahrung mit dem Director sollten sich unbedingt an diese Regel halten. "
+"Doppelte Werte in dieser Spalte erzeugen einen Fehler und der gesamte "
+"Importlauf schlägt fehl. Bitte beim Synchronisieren von Services Acht geben, "
+"\"bereinigen\" wird nur mit einer der host!name Syntax entsprechenden "
+"Schlüsselspalte funktionieren. Nutze den Eigenschaftsmodifikator "
+"\"Kombinieren\", falls die gegebenen Datenquelle keine solche Spalte "
+"bereitstellen kann"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:33
+msgid "This name will show up as the author for ever related downtime comment"
+msgstr ""
+"Der Name wird als Autor für sämtliche zugehörigen Downtime-Kommentare "
+"aufscheinen"
+
+#: library/Director/Web/Widget/BranchedObjectHint.php:57
+#, php-format
+msgid "This object has been created in %s"
+msgstr "Dieses Objekt wurde in %s erstellt"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:545
+msgid "This object has been disabled"
+msgstr "Dieses Objekt wurde deaktiviert"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:540
+msgid "This object has been enabled"
+msgstr "Dieses Objekt wurde aktiviert"
+
+#: library/Director/Web/Widget/BranchedObjectHint.php:62
+#, php-format
+msgid "This object has modifications visible only in %s"
+msgstr "Dieses Objekt hat Änderungen, welche nur in %s sichtbar sind"
+
+#: library/Director/Web/Widget/BranchedObjectHint.php:34
+#, php-format
+msgid ""
+"This object will be created in %s. It will not be part of any deployment "
+"unless being merged"
+msgstr ""
+"Das Objekt wird in %s erstellt. Es sind nicht Teil eines eventuellen "
+"Deployments, solange die Änderungen nicht zusammengeführt werden"
+
+#: library/Director/Web/ObjectPreview.php:75
+msgid "This object will not be deployed as it has been disabled"
+msgstr "Das Objekt wird nicht ausgerollt, da es deaktiviert wurde"
+
+#: library/Director/PropertyModifier/PropertyModifierCombine.php:17
+msgid ""
+"This pattern will be evaluated, and variables like ${some_column} will be "
+"filled accordingly. A typical use-case is generating unique service "
+"identifiers via ${host}!${service} in case your data source doesn't allow "
+"you to ship such. The chosen \"property\" has no effect here and will be "
+"ignored."
+msgstr ""
+"Dieses Muster wird ausgewertet, und Variablen wie ${eine_spalte} werden "
+"entsprechend befüllt. Ein typischer Anwendungsfall ist das Erstellen von "
+"eindeutigen Service-Bezeichnern via ${host}!${service} falls die Datenquelle "
+"einen solchen nicht bereitstellt. Die gewählte \"Eigenschaft\" hat hier "
+"keine Auswirkung und wird ignoriert."
+
+#: application/controllers/SyncruleController.php:219
+#, php-format
+msgid "This preview has been generated %s, please click %s to regenerate it"
+msgstr ""
+"Dieser Vorschau wurde %s generiert, zwecks Aktualisierung bitte %s klicken"
+
+#: library/Director/Web/SelfService.php:256
+msgid ""
+"This requires the Icinga Agent to be installed. It generates and signs it's "
+"certificate and it also generates a minimal icinga2.conf to get your agent "
+"connected to it's parents"
+msgstr ""
+"Hierfür muss der Icinga Agent installiert sein. Es erstellt und signiert "
+"sein Zertifikat und erstellt eine minimale icinga2.conf mit welcher der "
+"Agent zu den ihm übergeordneten Systemen verbunden wird"
+
+#: application/forms/IcingaServiceForm.php:403
+#, php-format
+msgid ""
+"This service belongs to the %s Service Set. Still, you might want to "
+"override the following properties for this host only."
+msgstr ""
+"Dieser Service gehört zum Service Set %s. Dennoch können die folgenden "
+"Eigenschaften nur für diesen speziellen Host geändert werden."
+
+#: application/forms/IcingaServiceForm.php:443
+#, php-format
+msgid ""
+"This service belongs to the service set \"%s\". Still, you might want to "
+"change the following properties for this host only."
+msgstr ""
+"Dieser Service gehört zum Set \"%s\". Dennoch können die folgenden "
+"Eigenschaften nur für diesen speziellen Host geändert werden."
+
+#: application/forms/IcingaServiceForm.php:384
+msgid ""
+"This service has been generated in an automated way, but still allows you to "
+"override the following properties in a safe way."
+msgstr ""
+"Dieser Service wurde durch einen Automatismus erstellt, erlaubt es aber "
+"dennoch die folgenden Eigenschaften auf sichere Weise zu überschreiben."
+
+#: application/forms/IcingaServiceForm.php:390
+#, php-format
+msgid ""
+"This service has been generated using the %s apply rule, assigned where %s"
+msgstr ""
+"Dieser Service wurde durch die Apply-Regel %s erstellt, und wird zugewiesen "
+"wo %s"
+
+#: application/forms/IcingaServiceForm.php:415
+#, php-format
+msgid ""
+"This service has been inherited from %s. Still, you might want to change the "
+"following properties for this host only."
+msgstr ""
+"Dieser Service wurde von %s geerbt. Dennoch können die folgenden "
+"Eigenschaften nur für diesen speziellen Host geändert werden."
+
+#: library/Director/Web/Table/IcingaServiceSetServiceTable.php:216
+#, php-format
+msgid "This set has been inherited from %s"
+msgstr "Dieses Set wurde von %s geerbt"
+
+#: library/Director/Dashboard/Dashlet/KickstartDashlet.php:17
+msgid ""
+"This synchronizes Icinga Director to your Icinga 2 infrastructure. A new run "
+"should be triggered on infrastructure changes"
+msgstr ""
+"Synchronisiert den Icinga Director mit der Icinga 2 Infrastruktur. Bei "
+"Änderungen an derselben sollte ein neuer Lauf angestoßen werden"
+
+#: library/Director/Web/Table/TemplateUsageTable.php:103
+msgid "This template is not in use"
+msgstr "Diese Vorlage ist nicht in Verwendung"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:941
+#, php-format
+msgid "This template is still in use by %d other objects"
+msgstr "Diese Vorlage ist noch in Verwendung durch %d andere Objekte"
+
+#: application/forms/IcingaTemplateChoiceForm.php:51
+msgid "This will be shown as a label for the given choice"
+msgstr "Dies wird als Bezeichner für diese Auswahlmöglichkeit angezeigt werden"
+
+#: application/forms/DirectorDatalistEntryForm.php:33
+msgid "This will be the visible caption for this entry"
+msgstr "Die sichtbare Bezeichnung für diesen Eintrag"
+
+#: library/Director/Web/SelfService.php:116
+msgid "This will invalidate the former key"
+msgstr "Das macht den vorherigen Schlüssel ungültig"
+
+#: library/Director/Web/SelfService.php:246
+msgid "Ticket"
+msgstr "Ticket"
+
+#: application/forms/SyncRuleForm.php:21
+msgid "Time Period"
+msgstr "Zeitraum"
+
+#: application/forms/BasketForm.php:32
+msgid "Time Periods"
+msgstr "Zeiträume"
+
+#: application/forms/IcingaUserForm.php:158
+#: application/forms/IcingaNotificationForm.php:240
+#: application/forms/DirectorJobForm.php:60
+#: application/forms/IcingaDependencyForm.php:141
+msgid "Time period"
+msgstr "Zeitraum"
+
+#: application/controllers/TimeperiodController.php:17
+#: application/controllers/ScheduledDowntimeController.php:24
+msgid "Time period ranges"
+msgstr "Zeiträume"
+
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:56
+#, php-format
+msgid "Time range \"%s\" has been removed from %s"
+msgstr "Der Zeitraum \"%s\" wurden von \"%s\" entfernt"
+
+#: application/forms/SyncPropertyForm.php:321
+msgid "Time ranges"
+msgstr "Zeiträume"
+
+#: application/forms/IcingaCommandForm.php:69
+msgid "Timeout"
+msgstr "Timeout"
+
+#: library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php:13
+msgid "Timeperiod Templates"
+msgstr "Zeitraumvorlage"
+
+#: library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php:16
+#: library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php:13
+#: library/Director/Db/Branch/BranchModificationInspection.php:45
+#: library/Director/Web/Table/IcingaTimePeriodRangeTable.php:46
+#: library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php:52
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:29
+msgid "Timeperiods"
+msgstr "Zeiträume"
+
+#: application/forms/IcingaTimePeriodRangeForm.php:28
+msgid "Timerperiods"
+msgstr "Zeiträume"
+
+#: library/Director/Web/Table/ImportrunTable.php:31
+msgid "Timestamp"
+msgstr "Zeitstempel"
+
+#: library/Director/DataType/DataTypeDictionary.php:28
+msgid "To be managed on objects only"
+msgstr "Kann nur auf Objekten verwaltet werden"
+
+#: application/forms/SelfServiceSettingsForm.php:59
+#: application/forms/SelfServiceSettingsForm.php:167
+msgid ""
+"To ensure downloaded packages are build by the Icinga Team and not "
+"compromised by third parties, you will be able to provide an array of SHA1 "
+"hashes here. In case you have defined any hashses, the module will not "
+"continue with updating / installing the Agent in case the SHA1 hash of the "
+"downloaded MSI package is not matching one of the provided hashes of this "
+"setting"
+msgstr ""
+"Um sicherzustellen, dass die heruntergeladenen Pakete vom Icinga-Team "
+"stammen und nicht von Dritten kompromittiert wurden, kann hier eine Liste "
+"von SHA1 Prüfsummen bereitgestellt werden. Falls hier ein oder mehrere "
+"solche Hash-Werte bereitgestellt werden, und das heruntergeladenen MSI-Paket "
+"keinem davon entspricht, wird das Powershell-Modul sich weigern mit Update "
+"oder Installation des Agenten fortzufahren"
+
+#: library/Director/Web/SelfService.php:221
+msgid "Top Down"
+msgstr "Top Down"
+
+#: library/Director/Web/Table/TemplateUsageTable.php:57
+msgid "Total"
+msgstr "Gesamt"
+
+#: application/forms/SelfServiceSettingsForm.php:31
+msgid "Transform Host Name"
+msgstr "Hostname transformieren"
+
+#: application/forms/SelfServiceSettingsForm.php:43
+msgid "Transform Parent Host to IP"
+msgstr "Elternhost in IP umwandeln"
+
+#: application/forms/SelfServiceSettingsForm.php:37
+msgid "Transform to lowercase"
+msgstr "In Kleinbuchstaben umwandeln"
+
+#: application/forms/SelfServiceSettingsForm.php:38
+msgid "Transform to uppercase"
+msgstr "In Großbuchstaben umwandeln"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1673
+msgid "Transition types"
+msgstr "Änderungsstypen"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:30
+msgid "Tree"
+msgstr "Baum"
+
+#: application/forms/ImportRunForm.php:23
+msgid "Trigger Import Run"
+msgstr "Importlauf anstoßen"
+
+#: application/forms/SyncRunForm.php:37
+msgid "Trigger this Sync"
+msgstr "Diese Synchronisation anstoßen"
+
+#: application/forms/ImportRunForm.php:45
+msgid "Triggering this Import Source failed"
+msgstr "Anstoßen dieser Importquelle schlug fehl"
+
+#: library/Director/PropertyModifier/PropertyModifierTrim.php:16
+msgid "Trim Method"
+msgstr "Trim-Methode"
+
+#: library/Director/Dashboard/Dashlet/SettingsDashlet.php:17
+msgid "Tweak some global Director settings"
+msgstr "Einige globale Director-Einstellungen anpassen"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:80
+#: library/Director/Web/Table/ObjectsTableEndpoint.php:22
+msgid "Type"
+msgstr "Typ"
+
+#: application/controllers/InspectController.php:82
+msgid "Type attributes"
+msgstr "Typ-Attribute"
+
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:27
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:28
+msgid "URL component"
+msgstr "URL- Komponente"
+
+#: library/Director/PropertyModifier/PropertyModifierUuidBinToHex.php:12
+msgid "UUID: from binary to hex"
+msgstr "UUID: binär in hexadezimal umwandeln"
+
+#: application/forms/DeployFormsBug7530.php:72
+msgid "Unable to detect your Icinga 2 Core version"
+msgstr "Die Icinga 2 Core-Version konnte nicht ermittelt werden"
+
+#: library/Director/DataType/DataTypeSqlQuery.php:27
+#: application/forms/SyncPropertyForm.php:168
+#, php-format
+msgid "Unable to fetch data: %s"
+msgstr "Daten konnten nicht geholt werden: %s"
+
+#: application/forms/IcingaHostForm.php:383
+msgid ""
+"Unable to store a host with the given properties because of insufficient "
+"permissions"
+msgstr ""
+"Ein Host konnte aufgrund unzureichender Berechtigungen nicht mit den "
+"gegebenen Eigenschaften abgespeichert werden"
+
+#: application/forms/KickstartForm.php:316
+#, php-format
+msgid ""
+"Unable to store the configuration to \"%s\". Please check file permissions "
+"or manually store the content shown below"
+msgstr ""
+"Die Konfiguration kann nicht nach \"%s\" gespeichert werden. Bitte "
+"Dateisystemberechtigungen prüfen oder den unten angegebenen Inhalt manuell "
+"speichern"
+
+#: library/Director/Db/Housekeeping.php:49
+msgid "Undeployed configurations"
+msgstr "Nicht ausgerollte Konfigurationen"
+
+#: application/forms/IcingaNotificationForm.php:221
+msgid ""
+"Unit is seconds unless a suffix is given. Supported suffixes include ms "
+"(milliseconds), s (seconds), m (minutes), h (hours) and d (days)."
+msgstr ""
+"Wie lang die Downtime dauert. Beeinflusst nur flexible (keine fixen) "
+"Downtimes. Zeit in Sekunden, unterstützt auch die Suffixes ms "
+"(Millisekunden), s (Sekunden), m (Minuten), h (Stunden) and d (Tage). Um "
+"\"90 Minuten\" auszudrücken könnte man 1h 30m schreiben."
+
+#: library/Director/IcingaConfig/StateFilterSet.php:27
+msgid "Unknown"
+msgstr "Unbekannt"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:136
+msgid "Unknown, failed to collect related information"
+msgstr "Unbekannt, entsprechende Information konnte nicht gesammelt werden"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:134
+msgid "Unknown, still waiting for config check outcome"
+msgstr "Unbekannt, warte auf Ergebnis der Konfigurationsprüfung"
+
+#: library/Director/Db/Housekeeping.php:53
+msgid "Unlinked imported properties"
+msgstr "Nicht-verknüpfte importierte Eigenschaften"
+
+#: library/Director/Db/Housekeeping.php:51
+msgid "Unlinked imported row sets"
+msgstr "Nicht-verknüpfte importierte Zeilensets"
+
+#: library/Director/Db/Housekeeping.php:52
+msgid "Unlinked imported rows"
+msgstr "Nicht-verknüpfte importierte Zeilen"
+
+#: application/controllers/PhperrorController.php:22
+#: application/controllers/PhperrorController.php:37
+msgid "Unsatisfied dependencies"
+msgstr "Unerfüllte Abhängigkeiten"
+
+#: library/Director/Db/Housekeeping.php:50
+msgid "Unused rendered files"
+msgstr "Nicht verwendete, generierte Dateien"
+
+#: library/Director/IcingaConfig/StateFilterSet.php:20
+msgid "Up"
+msgstr "Up"
+
+#: application/forms/IcingaTimePeriodForm.php:25
+msgid "Update Method"
+msgstr "Aktualisierungsmethode"
+
+#: application/forms/SyncRuleForm.php:52
+msgid "Update Policy"
+msgstr "Aktualisierungsrichtlinie"
+
+#: application/forms/SyncRuleForm.php:65
+msgid "Update only"
+msgstr "Nur aktualisieren"
+
+#: application/forms/DeployFormsBug7530.php:109
+msgid "Upgrading Icinga 2 - Confic Sync: Zones in Zones"
+msgstr "Upgrading Icinga 2 - Confic Sync: Zones in Zones"
+
+#: application/forms/DeployFormsBug7530.php:111
+msgid "Upgrading documentation"
+msgstr "Upgrading-Dokumentation"
+
+#: application/forms/BasketUploadForm.php:43
+#: application/controllers/BasketsController.php:26
+msgid "Upload"
+msgstr "Hochladen"
+
+#: application/controllers/BasketController.php:122
+msgid "Upload a Basket"
+msgstr "Basket hochladen"
+
+#: application/controllers/BasketController.php:123
+msgid "Upload a Configuration Basket"
+msgstr "Konfigurationsbasket hochladen"
+
+#: library/Director/Web/Controller/ObjectController.php:367
+msgid "Usage"
+msgstr "Benutzung"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:101
+#, php-format
+msgid "Usage (%s)"
+msgstr "Benutzung (%s)"
+
+#: application/forms/IcingaHostForm.php:106
+msgid ""
+"Use a different name for the generated endpoint object than the host name "
+"and add a custom variable to allow services setting the correct command "
+"endpoint."
+msgstr ""
+"Benutze einen anderen Bezeichner (als den Hostnamen) für das generierte "
+"Endpunktobjekt und füge eine benutzerdefinierte Variable hinzu, welche es "
+"Services erlaubt, den korrekten Commandendpunkt zu setzen."
+
+#: application/forms/SelfServiceSettingsForm.php:84
+msgid "Use a local file or network share"
+msgstr "Benutze eine lokales Verzeichnis oder eine Netzwerkfreigabe"
+
+#: library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:18
+msgid "Use lowercase first"
+msgstr "Erst in Kleinbuchstaben umwandeln"
+
+#: application/forms/SyncPropertyForm.php:273
+msgid "Used sources"
+msgstr "Verwendete Quellen"
+
+#: library/Director/TranslationDummy.php:17
+#: application/forms/SyncRuleForm.php:17
+msgid "User"
+msgstr "Benutzer"
+
+#: application/forms/SyncRuleForm.php:18
+msgid "User Group"
+msgstr "Benutzergruppe"
+
+#: library/Director/Dashboard/Dashlet/UserGroupsDashlet.php:11
+#: application/forms/BasketForm.php:27
+msgid "User Groups"
+msgstr "Benutzergruppen"
+
+#: library/Director/Dashboard/Dashlet/UserTemplateDashlet.php:13
+#: application/forms/BasketForm.php:28
+msgid "User Templates"
+msgstr "Benutzervorlagen"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:63
+#: application/forms/IcingaNotificationForm.php:156
+msgid "User groups"
+msgstr "Benutzergruppen"
+
+#: application/forms/IcingaUserForm.php:113
+msgid ""
+"User groups that should be directly assigned to this user. Groups can be "
+"useful for various reasons. You might prefer to send notifications to groups "
+"instead of single users"
+msgstr ""
+"Benutzergruppen, die direkt diesem Benutzer zugeordnet werden sollen. "
+"Gruppen können für verschiedene Aufgaben verwendet werden. Eventuell ist es "
+"besser, Benachrichtigungen an Gruppen statt an einzelne Benutzer zu schicken"
+
+#: application/forms/IcingaNotificationForm.php:158
+msgid "User groups that should be notified by this notifications"
+msgstr ""
+"Benutzergruppen, die durch diese Benachrichtigungen verständigt werden sollen"
+
+#: application/forms/IcingaUserForm.php:194
+msgid "User properties"
+msgstr "Benutzereigenschaften"
+
+#: application/forms/IcingaUserForm.php:22
+msgid "User template name"
+msgstr "Benutzervorlagenname"
+
+#: application/forms/IcingaUserGroupForm.php:17
+msgid "Usergroup"
+msgstr "Benutzergruppe"
+
+#: library/Director/Db/Branch/BranchModificationInspection.php:44
+#: library/Director/Import/ImportSourceCoreApi.php:63
+msgid "Usergroups"
+msgstr "Benutzergruppen"
+
+#: library/Director/Import/ImportSourceRestApi.php:226
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:74
+#: application/forms/IcingaUserForm.php:28
+msgid "Username"
+msgstr "Benutzername"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:62
+#: library/Director/Db/Branch/BranchModificationInspection.php:43
+#: library/Director/Import/ImportSourceCoreApi.php:62
+#: library/Director/Web/Table/CustomvarVariantsTable.php:62
+#: library/Director/Web/Table/CustomvarTable.php:47
+#: application/forms/IcingaNotificationForm.php:131
+#: application/forms/BasketForm.php:29
+msgid "Users"
+msgstr "Benutzer"
+
+#: library/Director/Dashboard/Dashlet/UserObjectDashlet.php:16
+#: library/Director/Dashboard/Dashlet/UsersDashlet.php:13
+msgid "Users / Contacts"
+msgstr "Benutzer / Kontakte"
+
+#: application/forms/IcingaNotificationForm.php:133
+msgid "Users that should be notified by this notifications"
+msgstr "Benutzer, die durch diese Benachrichtigungen verständigt werden sollen"
+
+#: library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php:17
+msgid ""
+"Using Apply Rules a Service can be applied to multiple hosts at once, based "
+"on filters dealing with any combination of their properties"
+msgstr ""
+"Mittels Apply-Regeln kann ein Service mehreren Hosts auf einmal zugewiesen "
+"werden, und zwar basierend auf Filtern welche einzelne oder mehrere "
+"Eigenschaften derselben kombinieren"
+
+#: application/forms/IcingaHostForm.php:336
+#: application/forms/IcingaHostSelfServiceForm.php:44
+msgid "Usually your hosts main IPv6 address"
+msgstr "Üblicherweise die Haupt-IPv6-Adresse des Hosts"
+
+#: library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php:47
+#: library/Director/Web/Table/IcingaCommandArgumentTable.php:50
+#: application/forms/IcingaServiceVarForm.php:27
+#: application/forms/IcingaCommandArgumentForm.php:50
+#: application/forms/IcingaCommandArgumentForm.php:59
+#: application/forms/CustomvarForm.php:21
+#: application/forms/IcingaHostVarForm.php:27
+msgid "Value"
+msgstr "Wert"
+
+#: application/forms/IcingaCommandArgumentForm.php:36
+msgid "Value type"
+msgstr "Werttyp"
+
+#: library/Director/Web/Table/CustomvarVariantsTable.php:56
+msgid "Variable Value"
+msgstr "Variablenwert"
+
+#: library/Director/Web/Table/CustomvarTable.php:41
+#: application/forms/CustomvarForm.php:16
+msgid "Variable name"
+msgstr "Variablenname"
+
+#: library/Director/Import/ImportSourceRestApi.php:176
+msgid "Verify Host"
+msgstr "Host überprüfen"
+
+#: library/Director/Import/ImportSourceRestApi.php:169
+msgid "Verify Peer"
+msgstr "Peer überprüfen"
+
+#: library/Director/DataType/DataTypeString.php:24
+msgid "Visibility"
+msgstr "Sichtbarkeit"
+
+#: library/Director/DataType/DataTypeString.php:26
+msgid "Visible"
+msgstr "Sichtbar"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1486
+msgid "Volatile"
+msgstr "Sprunghaft (Volatile)"
+
+#: application/forms/IcingaCommandForm.php:80
+msgid ""
+"WARNING, this can allow shell script injection via custom variables used in "
+"command."
+msgstr ""
+"VORSICHT, hiermit schafft man die Möglichkeit über benutzerdefinierte "
+"Variablen Shell-Code in Kommandos zu injizieren."
+
+#: library/Director/Dashboard/InfrastructureDashboard.php:50
+#, php-format
+msgid "Want to connect to your Icinga Agents? Have a look at our %s!"
+msgstr ""
+"Sollen Icinga Agents verbunden werden? Riskieren einen Blick in unsere %s!"
+
+#: library/Director/Dashboard/TimeperiodsDashboard.php:20
+msgid ""
+"Want to define to execute specific checks only withing specific time "
+"periods? Get mobile notifications only out of office hours, but mail "
+"notifications all around the clock? Time Periods allow you to tackle those "
+"and similar requirements."
+msgstr ""
+"Sollen bestimmte Checks nur innerhalb bestimmter Zeiträume ausgeführt "
+"werden? Mobile Benachrichtigungen nur außerhalb der Bürozeiten, aber "
+"Alarmierung via E-Mail rund um die Uhr? Zeiträume erlauben es, diese und "
+"ähnliche Anforderungen zu erfüllen."
+
+#: library/Director/IcingaConfig/StateFilterSet.php:25
+msgid "Warning"
+msgstr "Warnung"
+
+#: application/forms/DeployFormsBug7530.php:94
+#, php-format
+msgid ""
+"Warning: you're running Icinga v2.11.0 and our configuration looks like you "
+"could face issue %s. We're already working on a solution. The GitHub Issue "
+"and our %s contain related details."
+msgstr ""
+"Vorsicht: hier läuft Icinga v2.11.0 und die erstellte Konfiguration sieht "
+"aus als würde sie von Issue %s betroffen sein. Wir arbeiten bereits an einer "
+"Lösung. Das GitHub-Issue und unsere %s enthalten weiterführende "
+"Informationen."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1129
+msgid ""
+"What kind of object this should be. Templates allow full access to any "
+"property, they are your building blocks for \"real\" objects. External "
+"objects should usually not be manually created or modified. They allow you "
+"to work with objects locally defined on your Icinga nodes, while not "
+"rendering and deploying them with the Director. Apply rules allow to assign "
+"services, notifications and groups to other objects."
+msgstr ""
+"Welche Art von Objekt dies werden soll. Vorlagen erlauben vollen Zugriff auf "
+"alle Eigenschaften - sie sind die Bauklötze für \"echte\" Objekte. Externe "
+"Objekte sollten üblicherweise nicht erstellt oder verändert werden. Sie "
+"erlauben es, mit Objekten zu arbeiten, die lokal auf den Icinga Knoten "
+"definiert wurden, ohne sie über den Director auszubringen. Apply regeln "
+"erlauben das Zuweisen von Services, Benachrichtigungen und Gruppen an andere "
+"Objekte."
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:28
+msgid ""
+"What should happen if the lookup key does not exist in the data list? You "
+"could return a null value, keep the unmodified imported value or interrupt "
+"the import process"
+msgstr ""
+"Was soll geschehen, wenn der Suchschlüssel sich nicht in der Datenliste "
+"befindet? Es kann ein null Wert zurückgegeben werden, der importierte Wert "
+"unverändert übernommen oder der Importvorgang unterbrochen werden"
+
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:24
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:24
+msgid "What should happen when the given string is empty?"
+msgstr "Was soll passieren wenn der übergebene String leer ist?"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:66
+msgid "What should happen when the result array is empty?"
+msgstr "Was soll passieren wenn das resultierende Array leer ist?"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:51
+msgid "What should happen when the specified element is not available?"
+msgstr "Was soll passieren wenn das spezifizierte Element nicht verfügbar ist?"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:53
+msgid "What should happen with matching elements?"
+msgstr "Was soll mit passenden Elementen passieren?"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:68
+msgid ""
+"What should happen with the row, when this property matches the given "
+"expression?"
+msgstr ""
+"Was soll mit der Zeile passieren, wenn der gegebene Ausdruck auf diese "
+"Eigenschaft zutrifft?"
+
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:32
+msgid "What should we do if the DNS lookup fails?"
+msgstr "Was soll geschehen, wenn die (DNS) Auflösung fehlschlägt?"
+
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:36
+msgid ""
+"What should we do if the URL could not get parsed or component not found?"
+msgstr ""
+"Was soll mit der URL geschehen, wenn sie nicht geparsed werden kann oder "
+"aber Komponenten davon nicht gefunden werden?"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:28
+msgid "What should we do if the desired part does not exist?"
+msgstr "Was soll geschehen, wenn der gewünschte Teil nicht existiert?"
+
+#: library/Director/PropertyModifier/PropertyModifierGetHostByAddr.php:15
+#: library/Director/PropertyModifier/PropertyModifierGetHostByName.php:15
+msgid "What should we do if the host (DNS) lookup fails?"
+msgstr "Was soll geschehen, wenn die (DNS) Auflösung fehlschlägt?"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayToRow.php:21
+#: library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php:28
+msgid "What should we do in case the given value is empty?"
+msgstr "Was soll passieren wenn der übergebene Wert leer ist?"
+
+#: library/Director/PropertyModifier/PropertyModifierJsonDecode.php:22
+msgid "What should we do in case we are unable to decode the given string?"
+msgstr ""
+"Was soll geschehen, wenn sich der übergebene String nicht dekodieren lässt?"
+
+#: library/Director/PropertyModifier/PropertyModifierListToObject.php:24
+msgid "What should we do, if the same key occurs twice?"
+msgstr "Was soll geschehen, wenn derselbe Schlüssel zweimal auftaucht?"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:16
+msgid "What should we extract from the DN?"
+msgstr "Was soll aus dem DN extrahiert werden?"
+
+#: application/forms/BasketForm.php:61
+msgid ""
+"What should we place into this Basket every time we create new snapshot?"
+msgstr "Was sollen wir beim Erstellen eines Snapshots in diesen Basket geben?"
+
+#: application/forms/SelfServiceSettingsForm.php:21
+msgid "What to use as your Icinga 2 Agent's Host Name"
+msgstr "Was soll als Hostname für den Icinga-2-Agent genutzt werden?"
+
+#: library/Director/Job/ConfigJob.php:59
+msgid ""
+"When deploying configuration, wait at least this amount of seconds unless "
+"the next deployment should take place"
+msgstr ""
+"Beim Ausrollen einer Konfiguration mindestens diese Anzahl an Sekunden "
+"warten, bevor das nächste Ausrollen durchgeführt wird"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:63
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:21
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:21
+#: library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php:27
+msgid "When empty"
+msgstr "Falls leer"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:48
+msgid "When not available"
+msgstr "Wenn nicht verfügbar"
+
+#: application/forms/IcingaNotificationForm.php:210
+msgid "When the last notification should be sent"
+msgstr "Wann die letzte Nachricht versenderwerden"
+
+#: library/Director/Dashboard/InfrastructureDashboard.php:44
+msgid ""
+"When you feel the desire to manually create Zone or Endpoint objects please "
+"rethink this twice. Doing so is mostly the wrong way, might lead to a dead "
+"end, requiring quite some effort to clean up the whole mess afterwards."
+msgstr ""
+"Das Bedürfnis, neue Zonen- oder Endpunkt-Objekte manuell zu erstellen bitte "
+"zweimal überdenken. Das ist meistens der falsche Weg, kann in Sackgassen "
+"führen und damit eine Menge Arbeit beim manuellen Aufräumen verursachen."
+
+#: library/Director/PropertyModifier/PropertyModifierTrim.php:17
+msgid "Where to trim the string(s)"
+msgstr "Wo String(s) gestutzt werden soll"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:104
+msgid ""
+"Whether Downtimes should also explicitly be scheduled for all Services "
+"belonging to affected Hosts"
+msgstr ""
+"Ob Downtimes zudem explizit auch für alle zu den betroffenen Hosts gehörigen "
+"Services eingeplant werden sollen"
+
+#: application/forms/SettingsForm.php:63
+msgid "Whether all configured Jobs should be disabled"
+msgstr "Ob alle konfigurierten Jobs deaktiviert werden sollen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1459
+msgid "Whether flap detection is enabled on this object"
+msgstr "Ob die Flapping-Erkennung für dieses Objekt aktiviert werden soll"
+
+#: library/Director/Job/ConfigJob.php:33
+msgid ""
+"Whether rendering should be forced. If not enforced, this job re-renders the "
+"configuration only when there have been activities since the last rendered "
+"config"
+msgstr ""
+"Ob das Erstellen erzwungen werden soll. Wird es nicht erzwungen, wird die "
+"Konfiguration nur erstellt, falls seit dem letzten Erstellen Aktivitäten "
+"durchgeführt wurden"
+
+#: application/forms/IcingaHostForm.php:94
+msgid "Whether the agent is configured to accept config"
+msgstr ""
+"Ja, falls der Agent so konfiguriert ist, dass er Konfiguration akzeptiert"
+
+#: application/forms/IcingaCommandArgumentForm.php:42
+msgid ""
+"Whether the argument value is a string (allowing macros like $host$) or an "
+"Icinga DSL lambda function (will be enclosed with {{ ... }}"
+msgstr ""
+"Ist der Wert des Arguments eine Zeichenkette (erlaubt Makros wie $host$) "
+"oder eine Icinga-DSL-Lambda-Funktion? (wird in {{ ... }} eingeschlossen"
+
+#: application/forms/IcingaServiceForm.php:680
+msgid ""
+"Whether the check commmand for this service should be executed on the Icinga "
+"agent"
+msgstr ""
+"Ob das Kommando für diesen Service auf dem Icinga-Agent ausgeführt werden "
+"soll"
+
+#: application/forms/IcingaCommandArgumentForm.php:117
+msgid ""
+"Whether the parameter name should not be passed to the command. Per default, "
+"the parameter name (e.g. -H) will be appended, so no need to explicitly set "
+"this to \"No\"."
+msgstr ""
+"Ob der Parameter-Name an das Kommando übergeben werden soll. Normalerweise "
+"wird dieser (z.B. -H) hinzugefügt, es ist also nicht erforderlich dies "
+"explizit auf \"Nein\" zu setzen."
+
+#: application/forms/IcingaHostForm.php:88
+msgid ""
+"Whether the parent (master) node should actively try to connect to this agent"
+msgstr ""
+"Soll der (Master) Knoten aktiv versuchen, sich zu diesem Agenten zu "
+"verbinden?"
+
+#: application/forms/IcingaCommandArgumentForm.php:81
+msgid ""
+"Whether the set_if parameter is a string (allowing macros like $host$) or an "
+"Icinga DSL lambda function (will be enclosed with {{ ... }}"
+msgstr ""
+"Ist der set_if Parameter eine Zeichenkette (erlaubt Makros wie $host$) oder "
+"eine Icinga-DSL-Lambda-Funktion? (wird in {{ ... }} eingeschlossen"
+
+#: application/forms/IcingaCommandArgumentForm.php:126
+msgid "Whether this argument should be required"
+msgstr "Soll dieses Argument erforderlich sein?"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1487
+msgid "Whether this check is volatile."
+msgstr "Ist dieser Check sprunghaft (volatile)?"
+
+#: application/forms/IcingaDependencyForm.php:95
+#: application/forms/IcingaScheduledDowntimeForm.php:84
+msgid "Whether this dependency should affect hosts or services"
+msgstr "Soll diese Abhängigkeit Hosts oder Services betreffen"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:49
+msgid ""
+"Whether this downtime is fixed or flexible. If unsure please check the "
+"related documentation: https://icinga.com/docs/icinga2/latest/doc/08-"
+"advanced-topics/#downtimes"
+msgstr ""
+"Ob diese Downtime fix oder flexibel ist. Im Zweifel bitte die entsprechende "
+"Dokumentation zu Rate ziehen: https://icinga.com/docs/icinga2/latest/doc/08-"
+"advanced-topics/#downtimes"
+
+#: application/forms/IcingaObjectFieldForm.php:143
+msgid "Whether this field should be mandatory"
+msgstr "Soll dieses Feld Pflicht sein?"
+
+#: application/forms/IcingaHostForm.php:79
+msgid "Whether this host has the Icinga 2 Agent installed"
+msgstr "Hat dieser Host den Icinga-2-Agent installiert?"
+
+#: application/forms/IcingaNotificationForm.php:83
+msgid "Whether this notification should affect hosts or services"
+msgstr "Soll diese Benachrichtigung Hosts oder Services betreffen"
+
+#: application/forms/IcingaCommandArgumentForm.php:109
+msgid ""
+"Whether this parameter should be repeated when multiple values (read: array) "
+"are given"
+msgstr ""
+"Soll dieser Parameter wiederholt werden, wenn mehrere Werte (in einem Array) "
+"angegeben werden"
+
+#: library/Director/DataType/DataTypeDatalist.php:136
+msgid ""
+"Whether this should be a String or an Array in the generated Icinga "
+"configuration. In case you opt for Array, Director users will be able to "
+"select multiple elements from the list"
+msgstr ""
+"Ob dies ein String oder ein Array in der generierten Icinga-Konfiguration "
+"sein soll. Wird Array gewählt, können Director-Benutzer mehrere Elemente aus "
+"einer Liste wählen"
+
+#: application/forms/IcingaZoneForm.php:24
+msgid ""
+"Whether this zone should be available everywhere. Please note that it rarely "
+"leads to the desired result when you try to distribute global zones in "
+"distrubuted environments"
+msgstr ""
+"Soll diese Zone überall verfügbar sein? Beachte, dass es selten zielführend "
+"ist, globale Zonen in verteilten Umgebungen zu verteilen"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1435
+msgid "Whether to accept passive check results for this object"
+msgstr "Sollen passive Checkergebnisse für dieses Objekt akzeptiert werden?"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1429
+msgid "Whether to actively check this object"
+msgstr "Soll dieses Objekt aktiv geprüft werden?"
+
+#: application/forms/SelfServiceSettingsForm.php:33
+msgid "Whether to adjust your host name"
+msgstr "Ob der Hostname angepasst werden soll"
+
+#: application/forms/SyncRuleForm.php:84
+msgid "Whether to delete or to disable objects subject to purge"
+msgstr "Ob zu bereinigende Objekte deaktiviert oder gelöscht werden sollen"
+
+#: application/forms/IcingaDependencyForm.php:161
+msgid ""
+"Whether to disable checks when this dependency fails. Defaults to false."
+msgstr ""
+"Ob Checks deaktiviert werden sollen, wenn diese Abhängigkeit fehlschlägt. "
+"Standardmäßig ist dies nicht der Fall."
+
+#: application/forms/IcingaDependencyForm.php:169
+msgid ""
+"Whether to disable notifications when this dependency fails. Defaults to "
+"true."
+msgstr ""
+"Sollen Benachrichtigungen deaktiviert werden, wenn diese Abhängigkeit "
+"fehlschlägt. Standard-Einstellung ist Ja."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1447
+msgid "Whether to enable event handlers this object"
+msgstr "Sollen Event-Handler für dieses Objekt aktiviert werden?"
+
+#: application/forms/IcingaDependencyForm.php:177
+msgid ""
+"Whether to ignore soft states for the reachability calculation. Defaults to "
+"true."
+msgstr ""
+"Ob Soft-States für die Berechnung der Erreichbarkeit berücksichtigt werden "
+"sollen. Dies ist standardmäßig der Fall."
+
+#: application/forms/IcingaTimePeriodForm.php:77
+msgid "Whether to prefer timeperiods includes or excludes. Default to true."
+msgstr ""
+"Ob das Einbinden oder Ausschließen von Zeiträumen bevorzugt werden soll. "
+"Standard-Einstellung ist Ja."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1453
+msgid "Whether to process performance data provided by this object"
+msgstr "Sollen Performancedaten von diesem Objekt verarbeitet werden?"
+
+#: application/forms/SyncRuleForm.php:72
+msgid ""
+"Whether to purge existing objects. This means that objects of the same type "
+"will be removed from Director in case they no longer exist at your import "
+"source."
+msgstr ""
+"Sollen existierende Objekte entfernt werden? Das bedeutet, dass Objekte "
+"desselben Typs aus dem Director entfernt werden, falls sie in der "
+"Importquelle nicht mehr vorhanden sein sollten."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1441
+msgid "Whether to send notifications for this object"
+msgstr "Sollen Benachrichtigungen für dieses Objekt verschickt werden?"
+
+#: application/forms/IcingaUserForm.php:90
+msgid "Whether to send notifications for this user"
+msgstr "Sollen Benachrichtigungen für diesen Benutzer verschickt werden?"
+
+#: library/Director/Import/ImportSourceRestApi.php:130
+msgid "Whether to use encryption when talking to the REST API"
+msgstr ""
+"Ob für die Verbindung zu dieser REST-API Verschlüsselung benutzt werden soll"
+
+#: library/Director/Import/ImportSourceRestApi.php:171
+msgid ""
+"Whether we should check that our peer's certificate has been signed by a "
+"trusted CA. This is strongly recommended."
+msgstr ""
+"Ob wir prüfen sollen, dass das Zertifikat unseres Gegenübers von einer "
+"vertrauenswürdigen Zertifizierungsstelle signiert wurde. Das wird "
+"strengstens empfohlen."
+
+#: library/Director/Import/ImportSourceRestApi.php:178
+msgid "Whether we should check that the certificate matches theconfigured host"
+msgstr ""
+"Ob geprüft werden soll, dass die Zertifikate zum konfigurierten Host passen"
+
+#: application/forms/SyncPropertyForm.php:119
+msgid ""
+"Whether you want to merge or replace the destination field. Makes no "
+"difference for strings"
+msgstr ""
+"Soll das Zielfeld zusammengeführt oder ersetzt werden? Macht keinen "
+"Unterschied für Zeichenketten"
+
+#: application/forms/DirectorDatalistEntryForm.php:24
+msgid ""
+"Will be stored as a custom variable value when this entry is chosen from the "
+"list"
+msgstr ""
+"Wird als benutzerdefinierte Variable gespeichert, wenn dieser Eintrag aus "
+"der Liste gewählt wird"
+
+#: library/Director/Import/ImportSourceRestApi.php:228
+msgid "Will be used to authenticate against your REST API"
+msgstr "Wird für die Authentifizierung gegen die REST-API benutzt"
+
+#: library/Director/Web/SelfService.php:247
+msgid "Windows Kickstart Script"
+msgstr "Kickstart-Skript für Windows"
+
+#: application/forms/DirectorDatafieldForm.php:53
+msgid "Wipe related vars"
+msgstr "Zugehörige Eigenschaften leeren"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:102
+msgid "With Services"
+msgstr "Mit Services"
+
+#: library/Director/Dashboard/Dashlet/ActivityLogDashlet.php:17
+msgid "Wondering about what changed why? Track your changes!"
+msgstr "Hier kann nachvollzogen werden, was weshalb verändert wurde."
+
+#: library/Director/Dashboard/InfrastructureDashboard.php:35
+msgid "Working with Agents and Config Zones"
+msgstr "Mit Agenten und Konfigurationszonen arbeiten"
+
+#: application/views/helpers/FormDataFilter.php:525
+msgid "Wrap this expression into an operator"
+msgstr "Diesen Ausdruck in einen Operator packen"
+
+#: application/forms/IcingaDeleteObjectForm.php:17
+#, php-format
+msgid "YES, please delete \"%s\""
+msgstr "JA, bitte \"%s\" löschen"
+
+#: library/Director/Job/ImportJob.php:101
+#: library/Director/Job/ConfigJob.php:39
+#: library/Director/Job/ConfigJob.php:51
+#: library/Director/Job/SyncJob.php:101
+#: library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:25
+#: application/forms/IcingaZoneForm.php:30
+#: application/forms/SettingsForm.php:59
+#: application/forms/SettingsForm.php:74
+#: application/forms/SettingsForm.php:96
+#: application/forms/SelfServiceSettingsForm.php:239
+msgid "Yes"
+msgstr "Ja"
+
+#: application/controllers/BasketsController.php:41
+msgid ""
+"You can create Basket snapshots at any time, this will persist a serialized "
+"representation of all involved objects at that moment in time. Snapshots can "
+"be exported, imported, shared and restored - to the very same or another "
+"Director instance."
+msgstr ""
+"Basket-Snapshots können jederzeit erstellt werden, damit wird eine "
+"serialisierte Darstellung aller involvierten Objekte zum gegebenen Zeitpunkt "
+"persistiert. Snapshots können exportiert, importiert, geteilt und "
+"wiederhergestellt werden - zur selben oder einer anderen Director-Instanz."
+
+#: library/Director/Web/SelfService.php:129
+msgid ""
+"You can stop sharing a Template at any time. This will immediately "
+"invalidate the former key."
+msgstr ""
+"Die Freigabe einer Vorlage kann jederzeit widerrufen werden. Das macht den "
+"vorherigen Schlüssel sofort ungültig."
+
+#: library/Director/Job/ImportJob.php:94
+#: library/Director/Job/SyncJob.php:94
+msgid ""
+"You could immediately apply eventual changes or just learn about them. In "
+"case you do not want them to be applied immediately, defining a job still "
+"makes sense. You will be made aware of available changes in your Director "
+"GUI."
+msgstr ""
+"Änderungen können sofort angewandt oder nur angezeigt werden. Falls sie "
+"nicht sofort angewandt werden, ist es immer noch sinnvoll, einen Auftrag zu "
+"erstellen. Verfügbare Änderungen werden in der Director-GUI angezeigt."
+
+#: application/forms/SelfServiceSettingsForm.php:74
+msgid ""
+"You might want to let the generated Powershell script install the Icinga 2 "
+"Agent in an automated way. If so, please choose where your Windows nodes "
+"should fetch the Agent installer"
+msgstr ""
+"Es kann erwünscht sein, dass das generierte Powershell-Script den Icinga-2-"
+"Agenten automatisch installiert. Falls das gewünscht ist gilt es hier zu "
+"wühlen, von wo sich die Windows-Knoten den Agenten-Installer besorgen sollen"
+
+#: application/forms/IcingaObjectFieldForm.php:169
+msgid ""
+"You might want to show this field only when certain conditions are met. "
+"Otherwise it will not be available and values eventually set before will be "
+"cleared once stored"
+msgstr ""
+"Es könnte erwünscht sein, dieses Feld nur unter bestimmten Bedingungen "
+"anzuzeigen. Treffen diese nicht zu wird es nicht angezeigt, eventuell "
+"zugehörige Eigenschaften werden beim Speichern dann auch wieder entfernt."
+
+#: application/forms/ImportRowModifierForm.php:44
+msgid ""
+"You might want to write the modified value to another (new) property. This "
+"property name can be defined here, the original property would remain "
+"unmodified. Please leave this blank in case you just want to modify the "
+"value of a specific property"
+msgstr ""
+"Es kann erwünscht sein, den geänderten Wert in eine andere (neue) "
+"Eigenschaft zu schreiben. Dieser Eigenschaftsname kann hier definiert "
+"werden, die ursprüngliche Eigenschaft bleibt dann unverändert. Falls "
+"lediglich der ursprüngliche Wert an Ort und Stelle geändert werden soll, "
+"dieses Feld bitte leer lassen"
+
+#: application/controllers/SyncruleController.php:153
+#, php-format
+msgid "You must define some %s before you can run this Sync Rule"
+msgstr ""
+"Bevor dieses Synchronisationsregel ausgeführt werden kann, müssen %s "
+"definiert werden"
+
+#: library/Director/Dashboard/BranchesDashboard.php:19
+#, php-format
+msgid "You're currently working in a Configuration Branch: %s"
+msgstr "Gegenwärtig wird in einem Konfigurationszweig gearbeitet: %s"
+
+#: application/controllers/IndexController.php:35
+#, php-format
+msgid ""
+"Your DB schema (migration #%d) is newer than your code base. Downgrading "
+"Icinga Director is not supported and might lead to unexpected problems."
+msgstr ""
+"Das Datenbankschema (Migration #%d) ist neuer als der installierte "
+"Quellcode. Ein Downgrade des Icinga Directors wird nicht unterstützt und "
+"kann zu unerwarteten Problemen führen."
+
+#: application/forms/KickstartForm.php:157
+msgid "Your Icinga 2 API username"
+msgstr "Der Icinga-2-API Benutzername"
+
+#: library/Director/Import/ImportSourceLdap.php:52
+msgid ""
+"Your LDAP search base. Often something like OU=Users,OU=HQ,DC=your,"
+"DC=company,DC=tld"
+msgstr ""
+"Die LDAP Suchbasis. Meist etwas wie OU=Users,OU=HQ,DC=your,DC=company,DC=tld"
+
+#: library/Director/Web/Widget/BranchedObjectHint.php:42
+#, php-format
+msgid ""
+"Your changes will be stored in %s. The'll not be part of any deployment "
+"unless being merged"
+msgstr ""
+"Änderungen werden in %s gespeichert. Sie sind nicht Teil eines eventuellen "
+"Deployments, solange sie nicht zusammengeführt werden"
+
+#: application/forms/KickstartForm.php:98
+msgid ""
+"Your configuration looks good. Still, you might want to re-run this "
+"kickstart wizard to (re-)import modified or new manually defined Command "
+"definitions or to get fresh new ITL commands after an Icinga 2 Core upgrade."
+msgstr ""
+"Die Konfiguration sieht gut aus. Dennoch kann dieser Kickstart-Assistent "
+"jederzeit neu ausgeführt werden, um geänderte oder neu erstellte "
+"Kommandodefinitionen oder neue ITL-Kommandos nach einem Icinga-2-Core-"
+"Upgrade (neu) zu importieren."
+
+#: application/forms/KickstartForm.php:76
+#, php-format
+msgid "Your database looks good, you are ready to %s"
+msgstr ""
+"Datenbank sieht gut aus. Icinga Director sollte jetzt fertig für %s sein."
+
+#: application/forms/KickstartForm.php:110
+msgid ""
+"Your installation of Icinga Director has not yet been prepared for "
+"deployments. This kickstart wizard will assist you with setting up the "
+"connection to your Icinga 2 server."
+msgstr ""
+"Diese Icinga-Director-Installation wurde noch nicht zum Ausrollen von "
+"Konfiguration vorbereitet. Dieser Kickstart-Assistent hilft bei der "
+"Einrichtung der Verbindung zum Icinga 2 Server."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1372
+msgid "Your regular check interval"
+msgstr "Der übliche Check-Intervall"
+
+#: library/Director/PropertyModifier/PropertyModifierReplace.php:20
+#: library/Director/PropertyModifier/PropertyModifierReplaceNull.php:20
+msgid "Your replacement string"
+msgstr "Die Ersatzzeichenkette"
+
+#: application/forms/IcingaTemplateChoiceForm.php:67
+msgid "Your users will be allowed to choose among those templates"
+msgstr "Benutzern wird erlaubt, zwischen diesen Vorlagen zu wählen"
+
+#: library/Director/TranslationDummy.php:15
+#: library/Director/Import/ImportSourceDirectorObject.php:73
+#: library/Director/Web/Table/ObjectsTableEndpoint.php:21
+#: application/forms/SyncRuleForm.php:26
+msgid "Zone"
+msgstr "Zone"
+
+#: application/forms/IcingaZoneForm.php:14
+msgid "Zone name"
+msgstr "Zonenname"
+
+#: application/forms/IcingaUserForm.php:76
+#: application/forms/IcingaNotificationForm.php:65
+#: application/forms/IcingaCommandForm.php:110
+#: application/forms/IcingaUserGroupForm.php:42
+#: application/forms/IcingaDependencyForm.php:61
+msgid "Zone settings"
+msgstr "Zoneneinstellungen"
+
+#: library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php:13
+#: library/Director/Db/Branch/BranchModificationInspection.php:36
+#: library/Director/Import/ImportSourceCoreApi.php:64
+msgid "Zones"
+msgstr "Zonen"
+
+#: application/forms/SyncPropertyForm.php:348
+msgid "a list"
+msgstr "eine Liste"
+
+#: library/Director/Web/Widget/InspectPackages.php:122
+msgid "active"
+msgstr "aktiv"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:87
+msgid "all"
+msgstr "alle"
+
+#: library/Director/Web/ActionBar/DirectorBaseActionBar.php:35
+#: library/Director/Web/Controller/ObjectController.php:278
+#: library/Director/Web/Controller/ObjectController.php:638
+#: library/Director/Web/Controller/ActionController.php:182
+#: application/controllers/ServiceController.php:156
+#: application/controllers/ServiceController.php:216
+#: application/controllers/HostController.php:564
+#: application/controllers/BasketController.php:98
+#: application/controllers/BasketController.php:116
+#: application/controllers/BasketController.php:357
+#: application/controllers/ImportsourceController.php:366
+#: application/controllers/SyncruleController.php:612
+#: application/controllers/DataController.php:138
+msgid "back"
+msgstr "Zurück"
+
+#: library/Director/Web/Widget/BranchedObjectsHint.php:24
+#: library/Director/Web/Controller/ObjectController.php:722
+#: application/controllers/ConfigController.php:447
+msgid "configuration branch"
+msgstr "Konfigurationszweig"
+
+#: application/locale/translateMe.php:11
+msgid "critical"
+msgstr "Kritisch"
+
+#: library/Director/Web/Table/Dependency/DependencyInfoTable.php:57
+msgid "disabled"
+msgstr "deaktiviert"
+
+#: library/Director/Dashboard/InfrastructureDashboard.php:32
+#: application/controllers/DaemonController.php:43
+msgid "documentation"
+msgstr "Dokumentation"
+
+#: application/locale/translateMe.php:6
+msgid "down"
+msgstr "Down"
+
+#: application/forms/IcingaCommandArgumentForm.php:27
+msgid "e.g. -H or --hostname, empty means \"skip_key\""
+msgstr "z.B. -H oder --hostname, leer bedeutet \"skip_key\""
+
+#: application/forms/IcingaCommandArgumentForm.php:61
+msgid "e.g. 5%, $host.name$, $lower$%:$upper$%"
+msgstr "z.B. 5%, $host.name$, $lower$%:$upper$%"
+
+#: application/forms/SyncPropertyForm.php:165
+msgid "failed to fetch"
+msgstr "Abholen fehlgeschlagen"
+
+#: library/Director/Util.php:161
+#: library/Director/Web/Form/ClickHereForm.php:17
+#: application/forms/KickstartForm.php:236
+msgid "here"
+msgstr "hier"
+
+#: application/forms/IcingaHostVarForm.php:23
+msgid "host var name"
+msgstr "host var name"
+
+#: application/forms/IcingaHostVarForm.php:28
+msgid "host var value"
+msgstr "host var value"
+
+#: library/Director/Web/Table/Dependency/DependencyInfoTable.php:60
+msgid "missing"
+msgstr "fehlend"
+
+#: application/controllers/BasketController.php:304
+msgid "modified"
+msgstr "geändert"
+
+#: library/Director/Web/Table/Dependency/DependencyInfoTable.php:65
+msgid "more"
+msgstr "mehr"
+
+#: application/controllers/BasketController.php:282
+msgid "new"
+msgstr "neu"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:78
+msgid "no"
+msgstr "nein"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:222
+msgid "no related group exists"
+msgstr "keine verwandte Gruppe existiert"
+
+#: application/locale/translateMe.php:9
+msgid "ok"
+msgstr "ok"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:316
+msgid "on host"
+msgstr "auf dem Host"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:298
+msgid "on service set"
+msgstr "am Service-Set"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:224
+msgid "one related group exists"
+msgstr "eine verwandte Gruppe existiert"
+
+#: application/locale/translateMe.php:8
+msgid "pending"
+msgstr "ausstehend"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:71
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:29
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:56
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:29
+msgid "return NULL"
+msgstr "NULL zurückgeben"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:70
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:28
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:28
+msgid "return an empty array"
+msgstr "ein leeres Array zurückliefern"
+
+#: application/forms/IcingaServiceVarForm.php:23
+msgid "service var name"
+msgstr "service var name"
+
+#: application/forms/IcingaServiceVarForm.php:28
+msgid "service var value"
+msgstr "service var value"
+
+#: application/forms/KickstartForm.php:78
+msgid "start working with the Icinga Director"
+msgstr "die Arbeit mit dem Icinga Director zu beginnen"
+
+#: library/Director/Web/Widget/BranchedObjectHint.php:27
+msgid "this configuration branch"
+msgstr "diesem Konfigurationszweig"
+
+#: application/controllers/BasketController.php:308
+msgid "unchanged"
+msgstr "unverändert"
+
+#: application/locale/translateMe.php:12
+msgid "unknown"
+msgstr "Unbekannt"
+
+#: application/locale/translateMe.php:7
+msgid "unreachable"
+msgstr "unreachable"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:89
+msgid "unsupported"
+msgstr "nicht unterstützt"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:89
+msgid "unused"
+msgstr "unbenutzt"
+
+#: application/locale/translateMe.php:5
+msgid "up"
+msgstr "up"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:88
+msgid "used"
+msgstr "benutzt"
+
+#: application/forms/IcingaServiceVarForm.php:33
+#: application/forms/IcingaHostVarForm.php:33
+msgid "value format"
+msgstr "Wertformat"
+
+#: library/Director/Web/Table/GroupMemberTable.php:74
+#: library/Director/Web/Table/GroupMemberTable.php:79
+msgid "via"
+msgstr "via"
+
+#: application/locale/translateMe.php:10
+msgid "warning"
+msgstr "warning"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:77
+msgid "yes"
+msgstr "ja"
+
+#~ msgid "Create a new Service Set"
+#~ msgstr "Ein neues Service-Set erstellen"
+
+#, php-format
+#~ msgid ""
+#~ "This allows you to modify properties for \"%s\" (deployed from director)"
+#~ msgstr ""
+#~ "Dies erlaubt das Ändern von Eigenschaften für \"%s\" (ausgerollt vom "
+#~ "Director)"
+
+#~ msgid ""
+#~ "This allows you to modify properties for all chosen hosts (deployed from "
+#~ "director) at once"
+#~ msgstr ""
+#~ "Hiermit lassen sich Eigenschaften für alle gewählten (und vom Director "
+#~ "ausgerollten) Hosts auf einmal ändern"
+
+#, fuzzy
+#~ msgid "Please choose a specific Icinga object type. All "
+#~ msgstr "Bitte einen bestimmten Icinga-Objekttyp auswählen"
+
+#~ msgid "- inherited -"
+#~ msgstr "- geerbt -"
+
+#~ msgid "Blacklist"
+#~ msgstr "Blacklist"
+
+#~ msgid "Generated from host vars"
+#~ msgstr "Aus Host-Variablen generiert"
+
+#~ msgid "%s (where %s)"
+#~ msgstr "%s (wo %s)"
+
+#~ msgid "Add Service: %s"
+#~ msgstr "Service hinzufügen: %s"
+
+#~ msgid "The parent host."
+#~ msgstr "Der Eltern-Host."
+
+#~ msgid ""
+#~ "This allows you to configure an assignment filter. Please feel free to "
+#~ "combine as many nested operators as you want"
+#~ msgstr ""
+#~ "Hiermit lässt sich ein Zuweisungsfilter (assign) definieren. Dabei lassen "
+#~ "sich beliebig viele Operatoren beliebig tief verschachtelt benutzen"
+
+#~ msgid "This must be an import source column (property)"
+#~ msgstr "Muss eine Spalte der Importquelle sein (Eigenschaft)"
+
+#~ msgid "Name for the Icinga timeperiod you are going to create"
+#~ msgstr "Name des Icinga-Zeitraums, der erstellt werden soll"
+
+#~ msgid "Name for the Icinga timperiod template you are going to create"
+#~ msgstr "Name der Icinga-Zeitraum-Vorlage, die erstellt werden soll"
+
+#~ msgid "Please define a Service Template first"
+#~ msgstr "Bitte zuerst eine entsprechende Service-Vorlage definieren"
+
+#~ msgid "Serviceset"
+#~ msgstr "Service-Set"
+
+#~ msgid "Timeperiod"
+#~ msgstr "Zeitraum"
+
+#~ msgid "Timeperiod object"
+#~ msgstr "Zeitraumobjekt"
+
+#~ msgid "Timeperiod template"
+#~ msgstr "Zeitraumvorlage"
+
+#~ msgid "Timeperiod template name"
+#~ msgstr "Zeitraumvorlagenname"
+
+#~ msgid "Whether this should be a template"
+#~ msgstr "Soll dies eine Vorlage sein?"
+
+#~ msgid "the display name"
+#~ msgstr "Der Anzeigename"
+
+#~ msgid "the update method"
+#~ msgstr "die Updatemethode"
+
+#~ msgid "Change priority"
+#~ msgstr "Priorität ändern"
+
+#~ msgid "Count Query"
+#~ msgstr "Count-Abfage"
+
+#~ msgid "Move down (lower priority)"
+#~ msgstr "Nach unten schieben (Priorität verringern)"
+
+#~ msgid "Move up (raise priority)"
+#~ msgstr "Nach oben schieben (Priorität erhöhen)"
+
+#~ msgid "Next page"
+#~ msgstr "Nächste Seite"
+
+#~ msgid "Pagination"
+#~ msgstr "Seitennavigation"
+
+#~ msgid "Previous page"
+#~ msgstr "Vorhergehende Seite"
+
+#~ msgid "Prio"
+#~ msgstr "Prio"
+
+#~ msgid "SQL Query"
+#~ msgstr "SQL-Abfrage"
+
+#~ msgid "Search is simple! Try to combine multiple words"
+#~ msgstr "Suchen ist einfach! Versuche, mehrere Wörter zu kombinieren"
+
+#~ msgid "Search..."
+#~ msgstr "Suchen..."
+
+#~ msgid "Show rows %u to %u of %u"
+#~ msgstr "Zeige die Zeilen %u bis %u von %u"
+
+#~ msgid "This feature is still experimental"
+#~ msgstr "Dieses Feature ist noch experimentell"
+
+#~ msgid "Filter available service sets"
+#~ msgstr "Verfügbare Service-Sets filtern"
+
+#~ msgid "Set Members"
+#~ msgstr "Set-Mitglieder"
+
+#~ msgid "s"
+#~ msgstr "s"
+
+#~ msgid "Activity log entry"
+#~ msgstr "Aktivitätslogeintrag"
+
+#~ msgid "Add a job"
+#~ msgstr "Auftrag hinzufügen"
+
+#~ msgid "Add job"
+#~ msgstr "Auftrag hinzufügen"
+
+#~ msgid "Add list"
+#~ msgstr "Liste hinzufügen"
+
+#~ msgid "Apply Icinga %s"
+#~ msgstr "Icinga %s anwenden"
+
+#~ msgid "Clone Icinga %s"
+#~ msgstr "Klone Icinga %s"
+
+#~ msgid "Configs"
+#~ msgstr "Konfigurationen"
+
+#~ msgid "Create immediately"
+#~ msgstr "Sofort erstellen"
+
+#~ msgid "Deploy to master"
+#~ msgstr "Auf den Master ausbringen"
+
+#~ msgid "Edit import source"
+#~ msgstr "Importquelle bearbeiten"
+
+#~ msgid "Edit sync property rule"
+#~ msgstr "Synchronisationseigenschaftsregel bearbeiten"
+
+#~ msgid "Entries"
+#~ msgstr "Einträge"
+
+#~ msgid "Icinga "
+#~ msgstr "Icinga "
+
+#~ msgid "Icinga %s"
+#~ msgstr "Icinga %s"
+
+#~ msgid "Job %s"
+#~ msgstr "Auftrag %s"
+
+#~ msgid "No object found"
+#~ msgstr "Kein Objekt gefunden"
+
+#~ msgid "Object name"
+#~ msgstr "Objektname"
+
+#~ msgid "Show unfiltered"
+#~ msgstr "Ungefiltert zeigen"
+
+#~ msgid "Sync run details"
+#~ msgstr "Synchronisationslaufdetails"
+
+#~ msgid "Template tree"
+#~ msgstr "Vorlagenbaum"
+
+#~ msgid ""
+#~ "This is an external object. It has been imported from Icinga 2 through "
+#~ "the Core API and cannot be managed with the Icinga Director. It is "
+#~ "however perfectly valid to create objects using this or referring to this "
+#~ "object. You might also want to define related Fields to make work based "
+#~ "on this object more enjoyable."
+#~ msgstr ""
+#~ "Dies ist ein externes Objekt. Es wurde von Icinga 2 durch die Core API "
+#~ "importiert und kann nicht mit Icinga Director verwaltet werden. Objekte, "
+#~ "die sich auf dieses Objekt beziehen, können jedoch mit dem Director "
+#~ "erstellt werden. Außerdem können darauf bezugnehmende Felder erstellt "
+#~ "werden, um die Arbeit zu erleichtern."
+
+#~ msgid "Time"
+#~ msgstr "Zeit"
+
+#~ msgid "Allow to see template details"
+#~ msgstr "Erlauben, Details von Vorlagen zu sehen"
+
+#~ msgid "Allow to use only host templates matching this filter"
+#~ msgstr ""
+#~ "Nur die Verwendung von Hostvorlagen, die diesem Filter entsprechen, "
+#~ "erlauben"
+
+#~ msgid "Allow to use only these db resources (comma separated list)"
+#~ msgstr ""
+#~ "Nur die Verwendung dieser DB Ressourcen erlauben (kommaseparierte Liste)"
+
+#~ msgid "Command-specific custom vars"
+#~ msgstr "Kommandospezifische benutzerdefinierte Variablen"
+
+#~ msgid "Config history"
+#~ msgstr "Konfigurationshistorie"
+
+#~ msgid ""
+#~ "Data fields allow you to customize input controls your custom variables."
+#~ msgstr ""
+#~ "Datenfelder erlauben das Anpassen der Eingabekontrollen von "
+#~ "benutzerdefinierten Variablen."
+
+#~ msgid "Expression"
+#~ msgstr "Ausdruck"
+
+#~ msgid "Operator"
+#~ msgstr "Operator"
+
+#~ msgid "Owner"
+#~ msgstr "Besitzer"
+
+#~ msgid "Service configs"
+#~ msgstr "Servicekonfigurationen"
+
+#~ msgid "The unique name of the field"
+#~ msgstr "Der eindeutige Name des Felds"
+
+#~ msgid "There are pending database schema migrations"
+#~ msgstr "Es sind Datenbankmigrationen ausständig"
+
+#~ msgid "by host group property"
+#~ msgstr "Nach Hostgruppeneigenschaft"
+
+#~ msgid "check command"
+#~ msgstr "Check-Kommando"
+
+#~ msgid "to a host group"
+#~ msgstr "zu einer Hostgruppe"
+
+#~ msgid "%s template \"%s\": custom fields"
+#~ msgstr "%s Vorlage \"%s\": benutzerdefinierte Felder"
+
+#~ msgid "Add entry"
+#~ msgstr "Eintrag hinzufügen"
+
+#~ msgid "Deployments / History"
+#~ msgstr "Deployments / Historie"
+
+#~ msgid "Edit sync rule"
+#~ msgstr "Synchronisationsregel bearbeiten"
+
+#~ msgid "Filter string"
+#~ msgstr "Filterzeichenkette"
+
+#~ msgid "Import / Sync"
+#~ msgstr "Import / Synchronisierung"
+
+#~ msgid "Import runs"
+#~ msgstr "Importläufe"
+
+#~ msgid "Run"
+#~ msgstr "Ausführen"
+
+#~ msgid "Unable to store the configuration to \"%s\""
+#~ msgstr "Konfiguration kann nicht nach \"%s\" gespeichert werden"
+
+#~ msgid "Purge existing values."
+#~ msgstr "Existierende Werte bereinigen"
+
+#~ msgid "Whether the field should be merged, replaced or ignored"
+#~ msgstr "Soll das Feld zusammengeführt, ersetzt oder ignoriert werden?"
+
+#~ msgid "Alert your users"
+#~ msgstr "Benutzer alarmieren"
+
+#~ msgid "Manage deployments, access audit log and history"
+#~ msgstr ""
+#~ "Deployments verwalten, auf die Revisionsaufzeichnungen und die Historie "
+#~ "zugreifen"
+
+#~ msgid "Name for the Icinga zone (templat) you are going to create"
+#~ msgstr "Name der Icinga-Zone (Vorlage), die erstellt werden soll"
+
+#~ msgid "Zone (template) name"
+#~ msgstr "Zonen(vorlagen)name"
+
+#~ msgid "click here"
+#~ msgstr "Hier klicken"
+
+#~ msgid "database schema"
+#~ msgstr "Datenbankschema"
+
+#~ msgid "e.g. "
+#~ msgstr "z.B."
+
+#~ msgid "start using"
+#~ msgstr "start mit"
diff --git a/application/locale/it_IT/LC_MESSAGES/director.mo b/application/locale/it_IT/LC_MESSAGES/director.mo
new file mode 100644
index 0000000..66e4bb7
--- /dev/null
+++ b/application/locale/it_IT/LC_MESSAGES/director.mo
Binary files differ
diff --git a/application/locale/it_IT/LC_MESSAGES/director.po b/application/locale/it_IT/LC_MESSAGES/director.po
new file mode 100644
index 0000000..c4f6d2d
--- /dev/null
+++ b/application/locale/it_IT/LC_MESSAGES/director.po
@@ -0,0 +1,7431 @@
+# Icinga Web 2 - Head for multiple monitoring backends.
+# Copyright (C) 2020 Icinga Development Team
+# This file is distributed under the same license as Director Module.
+# Thomas Widhalm <widhalmt@widhalm.or.at>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Director Module (master)\n"
+"Report-Msgid-Bugs-To: dev@icinga.com\n"
+"POT-Creation-Date: 2020-02-11 10:27+0000\n"
+"PO-Revision-Date: 2019-09-25 14:01+0200\n"
+"Language: it_IT\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-Basepath: .\n"
+"Language-Team: \n"
+"X-Generator: Poedit 2.0.6\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:613
+#, php-format
+msgid " (inherited from \"%s\")"
+msgstr " (ereditato da \"%s\")"
+
+#: library/Director/Web/Table/TemplatesTable.php:63
+msgid " - not in use -"
+msgstr " - non utilizzato -"
+
+#: library/Director/Web/Table/DatafieldTable.php:49
+msgid "# Used"
+msgstr "# in uso"
+
+#: library/Director/Web/Table/DatafieldTable.php:50
+msgid "# Vars"
+msgstr "# Vars"
+
+#: library/Director/Web/Table/CustomvarTable.php:29
+#, php-format
+msgid "%d / %d"
+msgstr "%d / %d"
+
+#: library/Director/Resolver/CommandUsage.php:51
+#, php-format
+msgid "%d Host Template(s)"
+msgstr "%d Host-Template"
+
+#: library/Director/Resolver/CommandUsage.php:50
+#, php-format
+msgid "%d Host(s)"
+msgstr "%d Host(s)"
+
+#: library/Director/Resolver/CommandUsage.php:61
+#, php-format
+msgid "%d Notification Apply Rule(s)"
+msgstr "%d Regole-Apply-Notifica"
+
+#: library/Director/Resolver/CommandUsage.php:60
+#, php-format
+msgid "%d Notification Template(s)"
+msgstr "%d template di notifica"
+
+#: library/Director/Resolver/CommandUsage.php:59
+#, php-format
+msgid "%d Notification(s)"
+msgstr "%d Notifiche"
+
+#: library/Director/Resolver/CommandUsage.php:56
+#, php-format
+msgid "%d Service Apply Rule(s)"
+msgstr "%d Regole Service Apply"
+
+#: library/Director/Resolver/CommandUsage.php:55
+#, php-format
+msgid "%d Service Template(s)"
+msgstr "%d Service-Template(n)"
+
+
+#: library/Director/Resolver/CommandUsage.php:54
+#, php-format
+msgid "%d Service(s)"
+msgstr "%d Service(s)"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:231
+#, php-format
+msgid "%d apply rules have been defined"
+msgstr "%d Sono definite regole di Apply"
+
+#: library/Director/Web/Table/SyncRunTable.php:46
+#, php-format
+msgid "%d created"
+msgstr "%d creato"
+
+#: library/Director/Web/Table/SyncRunTable.php:58
+#, php-format
+msgid "%d deleted"
+msgstr "%d cancellato"
+
+
+#: library/Director/Web/Widget/DeploymentInfo.php:110
+#, php-format
+msgid "%d files"
+msgstr "%d files"
+
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:83
+#, php-format
+msgid "%d files rendered in %0.2fs"
+msgstr "%d file creati in %0.2fs"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:273
+#, php-format
+msgid "%d have been externally defined and will not be deployed"
+msgstr "%d sono stati creati all'esterno e non vengono distribuiti"
+
+#: library/Director/Web/Table/SyncRunTable.php:52
+#, php-format
+msgid "%d modified"
+msgstr "%d modificato"
+
+#: application/controllers/SyncruleController.php:273
+#, php-format
+msgid "%d object(s) will be created"
+msgstr "creati %d oggetti"
+
+#: application/controllers/SyncruleController.php:254
+#, php-format
+msgid "%d object(s) will be deleted"
+msgstr "%d oggetti verranno cancellati"
+
+#: application/controllers/SyncruleController.php:263
+#, php-format
+msgid "%d object(s) will be modified"
+msgstr "%d oggetti verranno modificati"
+
+#: application/controllers/InspectController.php:78
+#, php-format
+msgid "%d objects found"
+msgstr "%d oggetti trovati"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:256
+#, php-format
+msgid "%d objects have been defined"
+msgstr "%d oggetti sono stati definiti"
+
+#: application/forms/IcingaMultiEditForm.php:93
+#, php-format
+msgid "%d objects have been modified"
+msgstr "%d oggetti sono stati modificati"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:119
+#, php-format
+msgid "%d objects, %d templates, %d apply rules"
+msgstr "%d oggetti, %d template, %d regole-Apply"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:265
+#, php-format
+msgid "%d of them are templates"
+msgstr "%d di questi sono template"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:213
+#, php-format
+msgid "%d templates have been defined"
+msgstr "%d template sono stati definiti"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:604
+#, php-format
+msgid "%s \"%s\" has been created"
+msgstr "%s \"%s\" é stato creato"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:607
+#, php-format
+msgid "%s \"%s\" has been deleted"
+msgstr "%s \"%s\" é stato cancellato"
+
+#: application/forms/IcingaImportObjectForm.php:36
+#, php-format
+msgid "%s \"%s\" has been imported\""
+msgstr "%s \"%s\" é stato importato\""
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:610
+#, php-format
+msgid "%s \"%s\" has been modified"
+msgstr "%s \"%s\" é stato modificato"
+
+#: library/Director/Web/Table/IcingaHostAppliedServicesTable.php:107
+#, php-format
+msgid "%s %s(%s)"
+msgstr "%s %s(%s)"
+
+#: library/Director/Web/Table/ObjectSetTable.php:57
+#, php-format
+msgid "%s (%d members)"
+msgstr "%s (%d membri)"
+
+#: application/controllers/HostController.php:178
+#: application/controllers/HostController.php:262
+#, php-format
+msgid "%s (Applied Service set)"
+msgstr "%s (Service-Set associati)"
+
+#: application/controllers/HostController.php:312
+#, php-format
+msgid "%s (Service set)"
+msgstr "%s (Service-Set)"
+
+#: application/forms/SelfServiceSettingsForm.php:243
+#: application/forms/SettingsForm.php:177
+#, php-format
+msgid "%s (default)"
+msgstr "%s (default)"
+
+#: library/Director/Web/Controller/TemplateController.php:71
+#, php-format
+msgid "%s Templates"
+msgstr "%s Template"
+
+#: library/Director/Web/Controller/TemplateController.php:36
+#, php-format
+msgid "%s based on %s"
+msgstr "%s basato su %s"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:112
+#, php-format
+msgid "%s config changes happend since the last deployed configuration"
+msgstr ""
+"%s cambimenti di configurazione dopo l'ultima distribuzione di configurazioni"
+
+#: application/forms/IcingaServiceForm.php:256
+#, php-format
+msgid "%s has been blacklisted on %s"
+msgstr "%s é stato messo in blacklist %s"
+
+#: application/forms/IcingaServiceForm.php:292
+#, php-format
+msgid "%s is no longer blacklisted on %s"
+msgstr "%s non é più nella lista di blacklist %s"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:48
+#, php-format
+msgid "%s objects have been modified"
+msgstr "%s oggetti sono stati cambiati"
+
+#: application/controllers/HostController.php:460
+#, php-format
+msgid "%s on %s (from set: %s)"
+msgstr "%s auf %s (da questo set \"%s\")"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:288
+#, php-format
+msgid "%s related group objects have been created"
+msgstr "sono stati creati %s oggetti di gruppo associati"
+
+#: library/Director/Web/Controller/TemplateController.php:74
+#, php-format
+msgid "%s templates based on %s"
+msgstr "%s template basati su %s"
+
+#: library/Director/Web/Widget/HealthCheckPluginOutput.php:46
+#, php-format
+msgid "%s: %d"
+msgstr "%s: %d"
+
+#: application/controllers/BasketController.php:193
+#, php-format
+msgid "%s: %s (Snapshot)"
+msgstr "%s: %s (Snapshot)"
+
+#: application/controllers/ImportsourceController.php:224
+#, php-format
+msgid "%s: Property Modifier"
+msgstr "%s: modificatore di proprietá"
+
+#: application/controllers/BasketController.php:144
+#, php-format
+msgid "%s: Snapshots"
+msgstr "%s: Snapshots"
+
+#: application/controllers/ImportsourceController.php:198
+#, php-format
+msgid "%s: add Property Modifier"
+msgstr "%s: aggiunta di modificatore di proprietá"
+
+#: library/Director/Db/Housekeeping.php:54
+msgid "(Host) group resolve cache"
+msgstr "cache di risoluzione gruppo (Hosts)"
+
+#: application/controllers/ServiceController.php:169
+#, php-format
+msgid "(on %s)"
+msgstr "(su %s)"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:234
+msgid "- add more -"
+msgstr "- aggiungere altro -"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1065
+msgid "- click to add more -"
+msgstr "- premi per aggiungere altro -"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:622
+msgid "- inherited -"
+msgstr "- eereditato -"
+
+#: application/forms/SelfServiceSettingsForm.php:66
+msgid "- no automatic installation -"
+msgstr "- nessuna installazione automatica -"
+
+#: application/views/helpers/FormDataFilter.php:465
+#: application/controllers/ConfigController.php:363
+#: application/controllers/ConfigController.php:374
+#: application/forms/SelfServiceSettingsForm.php:239
+#: application/forms/SettingsForm.php:181
+#: library/Director/Web/Form/QuickBaseForm.php:120
+#: library/Director/DataType/DataTypeDirectorObject.php:43
+#: library/Director/DataType/DataTypeSqlQuery.php:37
+#: library/Director/Job/ImportJob.php:118
+#: library/Director/Job/SyncJob.php:124
+msgid "- please choose -"
+msgstr "- prego scegliere -"
+
+#: application/controllers/SyncruleController.php:375
+#, php-format
+msgid "...and %d more"
+msgstr "...e %d altro"
+
+#: application/controllers/BasketsController.php:34
+msgid ""
+"A Configuration Basket references specific Configuration Objects or all "
+"objects of a specific type. It has been designed to share Templates, Import/"
+"Sync strategies and other base Configuration Objects. It is not a tool to "
+"operate with single Hosts or Services."
+msgstr ""
+"Un basket di configurazione si riferisce ad elementi di configurazione o tutti "
+"gli oggetti di un particolare tipo. É stato sviluppato per condividere Template, "
+"strategie di Import/Sync e altri elementi base di configurazione. "
+"Questo non é uno strumento per configurare singoli Host o Services"
+
+#: library/Director/Import/ImportSourceLdap.php:61
+msgid ""
+"A custom LDAP filter to use in addition to the object class. This allows for "
+"a lot of flexibility but requires LDAP filter skills. Simple filters might "
+"look as follows: operatingsystem=*server*"
+msgstr ""
+"Un filtro LDAP aggiuntivo da usare insieme alla classe dell'oggetto. "
+"Questo permette una grande flessibilitá ma necessita di conoscenze "
+"profonde sui filtri LDAP. Filtri semplici possono essere di questo tipo: "
+"operatingsystem=*server*"
+
+#: application/forms/SyncPropertyForm.php:214
+msgid ""
+"A custom string. Might contain source columns, please use placeholders of "
+"the form ${columnName} in such case. Structured data sources can be "
+"referenced as ${columnName.sub.key}"
+msgstr ""
+"Una stringa personalizzata. Se sono presenti colonne sorgenti, devono "
+"venir usati segnaposti nella forma di ${columnName}. È possibile fare "
+"riferimento a dati strutturati tramite ${columnName.sub.key"
+
+
+#: application/forms/IcingaObjectFieldForm.php:134
+msgid "A description about the field"
+msgstr "Una descrizione del campo"
+
+#: application/forms/IcingaTemplateChoiceForm.php:59
+msgid "A detailled description explaining what this choice is all about"
+msgstr "Una descrizione dettagliata sull'obiettivo di questa selezione"
+
+#: application/forms/IcingaServiceSetForm.php:102
+msgid ""
+"A meaningful description explaining your users what to expect when assigning "
+"this set of services"
+msgstr ""
+"Una descrizione significativa che spiega agli utenti cosa aspettarsi "
+"dall'assegnazione di questo service set"
+
+
+# Geschlecht kann hier leider nicht bestimmt werden -ThW
+#: application/forms/IcingaTimePeriodRangeForm.php:85
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:90
+#: library/Director/Web/Form/DirectorObjectForm.php:652
+#, php-format
+msgid "A new %s has successfully been created"
+msgstr "Un nuovo %s é stato creato con successo"
+
+#: application/forms/IcingaGenerateApiKeyForm.php:39
+#, php-format
+msgid "A new Self Service API key for %s has been generated"
+msgstr "Un nuovo Self Service API per %s é stato creato con successo"
+
+#: application/forms/ImportRowModifierForm.php:72
+msgid ""
+"A property modifier allows you to modify a specific property at import time"
+msgstr ""
+"Un modificatore di proprietà consente di modificare una proprietà specifica "
+"al momento dell'importazione"
+
+
+#: application/forms/ImportSourceForm.php:17
+msgid ""
+"A short name identifying this import source. Use something meaningful, like "
+"\"Hosts from Puppet\", \"Users from Active Directory\" or similar"
+msgstr ""
+"Un breve nome per definire questa sorgente di importazione. "
+"Usa qualcosa di significativo come \"Hosts da Puppet\" o \"utenti da LDAP\" "
+
+
+#: application/forms/DirectorJobForm.php:74
+msgid ""
+"A short name identifying this job. Use something meaningful, like \"Import "
+"Puppet Hosts\""
+msgstr ""
+"Un breve nome per descrivere questa operazione. "
+"Usa qualcosa di significativo come \"Importazione di Host da Puppet\" "
+
+#: application/forms/IcingaServiceSetForm.php:30
+msgid "A short name identifying this set of services"
+msgstr "Un breve nome per questo Service-Set"
+
+#: library/Director/Web/SelfService.php:211
+#, php-format
+msgid ""
+"A ticket for this agent could not have been requested from your deployment "
+"endpoint: %s"
+msgstr ""
+"Non è stato possibile richiedere un ticket per questo agent da questo endpoint: %s "
+
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:95
+#, php-format
+msgid ""
+"A total of %d config changes happened since your last deployed config has "
+"been rendered"
+msgstr ""
+"Nel complesso, la configurazione è stata modificata %d volte dall'ultima distribuzione"
+
+#: application/forms/IcingaHostSelfServiceForm.php:49
+msgid "API Key"
+msgstr "API Key"
+
+#: application/forms/IcingaEndpointForm.php:46
+#: application/forms/KickstartForm.php:151
+msgid "API user"
+msgstr "utente API"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1393
+msgid "Accept passive checks"
+msgstr "Accetta check passivi"
+
+#: application/forms/IcingaHostForm.php:93
+msgid "Accepts config"
+msgstr "Accetta configurazioni"
+
+# Aktuell wird es so in anderen Modulen übersetzt. -ThW
+#: library/Director/IcingaConfig/TypeFilterSet.php:28
+msgid "Acknowledgement"
+msgstr "Conferma"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:544
+#: library/Director/Web/Widget/ActivityLogInfo.php:555
+#: library/Director/Web/Table/ConfigFileDiffTable.php:81
+msgid "Action"
+msgstr "Azione"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1506
+msgid "Action URL"
+msgstr "Action-URL"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:578
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:62
+#: library/Director/Web/Table/QuickTable.php:280
+msgid "Actions"
+msgstr "Azione"
+
+#: application/forms/SettingsForm.php:163
+msgid "Activation Tool"
+msgstr "Strumento di attivazione"
+
+#: application/forms/SettingsForm.php:142
+msgid "Active-Passive"
+msgstr "Attivo-passivo"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:27
+msgid "Activity"
+msgstr "Attivitá"
+
+#: application/controllers/ConfigController.php:141
+#: library/Director/Dashboard/Dashlet/ActivityLogDashlet.php:11
+#: library/Director/Web/Tabs/InfraTabs.php:29
+msgid "Activity Log"
+msgstr "Log di attivitá"
+
+#: library/Director/Web/Controller/ObjectController.php:240
+#, php-format
+msgid "Activity Log: %s"
+msgstr "Log di attivitá: %s"
+
+#: configuration.php:138
+msgid "Activity log"
+msgstr "Log di attivitá"
+
+#: application/controllers/DataController.php:22
+#: application/controllers/DataController.php:65
+#: application/forms/AddToBasketForm.php:72
+#: library/Director/Web/Form/DirectorObjectForm.php:503
+#: library/Director/Web/Controller/ActionController.php:144
+#: library/Director/Web/Controller/ObjectsController.php:295
+#: library/Director/Web/Controller/ObjectsController.php:336
+#: library/Director/Web/ActionBar/ChoicesActionBar.php:16
+#: library/Director/Web/ActionBar/ObjectsActionBar.php:16
+#: library/Director/Web/ActionBar/TemplateActionBar.php:19
+msgid "Add"
+msgstr "Aggiungere"
+
+#: library/Director/Web/Controller/ObjectController.php:78
+#: library/Director/Web/Tabs/ObjectTabs.php:51
+#, php-format
+msgid "Add %s"
+msgstr "%s aggiunto"
+
+#: application/forms/AddToBasketForm.php:68
+#, php-format
+msgid "Add %s objects"
+msgstr "%s oggetti aggiunti"
+
+#: library/Director/Web/Controller/ObjectController.php:381
+#, php-format
+msgid "Add %s: %s"
+msgstr "Aggiungere %s: %s"
+
+#: application/controllers/HostsController.php:40
+#: application/controllers/HostsController.php:79
+msgid "Add Service"
+msgstr "Aggiungere Service"
+
+#: application/controllers/HostsController.php:45
+#: application/controllers/HostsController.php:110
+msgid "Add Service Set"
+msgstr "Aggiungere Service-Set"
+
+#: application/controllers/HostsController.php:127
+#, php-format
+msgid "Add Service Set to %d hosts"
+msgstr "Aggiungi Service-Set a %d Hosts"
+
+#: application/controllers/HostController.php:74
+#, php-format
+msgid "Add Service Set to %s"
+msgstr "Aggiungi Service-Set a %s"
+
+#: application/controllers/HostController.php:61
+#, php-format
+msgid "Add Service to %s"
+msgstr "Aggiungi Service a %s"
+
+#: application/controllers/DatafieldController.php:39
+msgid "Add a new Data Field"
+msgstr "Aggiungi un nuovo campo dati"
+
+#: application/controllers/DataController.php:52
+msgid "Add a new Data List"
+msgstr "Aggiungi un nuovo elenco di dati"
+
+#: application/controllers/ImportsourcesController.php:49
+msgid "Add a new Import Source"
+msgstr "Aggiungi una nuova sorgente di importazione"
+
+#: application/controllers/JobsController.php:15
+#: application/controllers/JobController.php:32
+msgid "Add a new Job"
+msgstr "Aggiungi un nuovo Job"
+
+#: application/controllers/SyncrulesController.php:28
+msgid "Add a new Sync Rule"
+msgstr "Aggiungi una nuova regola di sincronizzazione"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:470
+msgid "Add a new entry"
+msgstr "Aggiungi un nuova voce"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:256
+msgid "Add a new one..."
+msgstr "Aggiungi un nuovo..."
+
+#: application/controllers/ServicesetController.php:49
+#, php-format
+msgid "Add a service set to \"%s\""
+msgstr "Aggiungi un Service-Set a \"%s\" "
+
+#: application/controllers/ServiceController.php:105
+#, php-format
+msgid "Add a service to \"%s\""
+msgstr "Aggiungi un Service a \"%s\" "
+
+#: application/views/helpers/FormDataFilter.php:516
+msgid "Add another filter"
+msgstr "Aggiungi un altro filtro"
+
+#: application/controllers/BasketController.php:83
+msgid "Add chosen objects to a Configuration Basket"
+msgstr "Aggiungi oggetti selezionati al basket di configurazione"
+
+#: application/forms/DirectorDatalistEntryForm.php:60
+msgid "Add data list entry"
+msgstr "Aggiungi la voce dell'elenco dati"
+
+#: application/controllers/ImportsourceController.php:90
+msgid "Add import source"
+msgstr "Aggiungi sorgente di importazione"
+
+#: library/Director/Web/Controller/ObjectController.php:387
+#, php-format
+msgid "Add new Icinga %s"
+msgstr "Aggiungi un nuovo Icinga %s"
+
+#: library/Director/Web/Controller/ObjectController.php:370
+#, php-format
+msgid "Add new Icinga %s template"
+msgstr "Aggiungi nuovo Icinga al template %s"
+
+#: application/controllers/ImportsourceController.php:171
+msgid "Add property modifier"
+msgstr "Aggiungi modificatore di proprietá"
+
+#: application/controllers/HostController.php:90
+#: application/controllers/ServicesetController.php:66
+msgid "Add service"
+msgstr "Aggiungi Service"
+
+#: application/controllers/HostController.php:95
+msgid "Add service set"
+msgstr "Aggiungi Service-Set"
+
+#: application/controllers/HostsController.php:96
+#, php-format
+msgid "Add service to %d hosts"
+msgstr "Aggiungi un Service a %d Hosts"
+
+#: application/controllers/SyncruleController.php:524
+msgid "Add sync property rule"
+msgstr "Aggiungi regola di sincronizzazione"
+
+#: application/controllers/SyncruleController.php:563
+#, php-format
+msgid "Add sync property: %s"
+msgstr "Aggiungi proprietá di sincronizzazione: %s"
+
+#: application/controllers/SyncruleController.php:475
+msgid "Add sync rule"
+msgstr "Aggiungere regola di sincronizzazione"
+
+#: application/controllers/HostsController.php:70
+#: application/controllers/BasketController.php:82
+#: application/controllers/ServicesController.php:36
+#: application/controllers/JobController.php:79
+#: application/controllers/DataController.php:128
+#: application/controllers/ImportsourceController.php:57
+#: application/controllers/SyncruleController.php:612
+#: library/Director/Web/Controller/ObjectController.php:352
+#: library/Director/Web/Controller/TemplateController.php:128
+msgid "Add to Basket"
+msgstr "Aggiungere a Basket"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1492
+msgid "Additional notes for this object"
+msgstr "Note aggiuntive per questo oggetto"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1545
+msgid "Additional properties"
+msgstr "Proprietá aggiuntive"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:140
+msgid "Agent"
+msgstr "Agent"
+
+#: application/forms/SelfServiceSettingsForm.php:141
+msgid "Agent Version"
+msgstr "Versione Agent"
+
+#: application/forms/IcingaHostSelfServiceForm.php:31
+msgid "Alias"
+msgstr "Alias"
+
+#: application/controllers/ConfigController.php:154
+msgid "All changes"
+msgstr "Tutte le modifiche"
+
+#: application/forms/SettingsForm.php:78
+msgid ""
+"All changes are tracked in the Director database. In addition you might also "
+"want to send an audit log through the Icinga Web 2 logging mechanism. That "
+"way all changes would be written to either Syslog or the configured log "
+"file. When enabling this please make sure that you configured Icinga Web 2 "
+"to log at least at \"informational\" level."
+msgstr ""
+"Tutte le modifiche vengono loggate nel database del Director. Inoltre, è possibile "
+"inviare un log tramite il meccanismo di logging di Icinga Web 2. "
+"In questo modo tutte le modifiche vengono scritte nel Syslog o il log file configurato. "
+"Per ottenere questo risultato Icinga 2 deve girare almeno con il livello di Log \"Info\"."
+
+
+#: application/forms/SyncPropertyForm.php:308
+msgid "All custom variables (vars)"
+msgstr "Tutte le variabili personalizzate (vars)"
+
+#: application/forms/BasketForm.php:52
+msgid "All of them"
+msgstr "Tutte queste"
+
+#: application/forms/IcingaServiceForm.php:730
+#, php-format
+msgid "All overrides have been removed from \"%s\""
+msgstr "Tutte le sovrascritture sono state rimosse da \"%s\""
+
+#: library/Director/Web/Controller/ObjectsController.php:287
+#, php-format
+msgid "All your %s Apply Rules"
+msgstr "Tutte le regole Apply %s"
+
+#: library/Director/Web/Controller/ObjectsController.php:232
+#, php-format
+msgid "All your %s Templates"
+msgstr "Tutti i Template %s"
+
+#: application/forms/SelfServiceSettingsForm.php:174
+msgid "Allow Updates"
+msgstr "Abilitare le modifiche"
+
+#: library/Director/DataType/DataTypeDatalist.php:146
+msgid "Allow for values not on the list"
+msgstr "Consenti valori non presenti nell'elenco"
+
+#: configuration.php:31
+msgid "Allow readonly users to see where a Service came from"
+msgstr "Consenti agli utenti di sola lettura di vedere da dove proviene un servizio"
+
+#: configuration.php:6
+msgid "Allow to access the director API"
+msgstr "Consentire l'accesso all'API del Director"
+
+#: configuration.php:7
+msgid "Allow to access the full audit log"
+msgstr "Consentire di accedere all'audit log completo"
+
+#: configuration.php:17
+msgid "Allow to configure hosts"
+msgstr "Consenti la configurazione di Host"
+
+#: configuration.php:22
+msgid "Allow to configure notifications"
+msgstr "Consenti la configurazione di notifiche"
+
+#: configuration.php:19
+msgid "Allow to configure service sets"
+msgstr "Consenti la configurazione di Service-Sets"
+
+#: configuration.php:18
+msgid "Allow to configure services"
+msgstr "Consenti la configurazione di Services"
+
+#: configuration.php:21
+msgid "Allow to configure users"
+msgstr "Consenti la configurazione di utenti"
+
+#: configuration.php:20
+msgid "Allow to define Service Set Apply Rules"
+msgstr "Consenti la definizione di regole Apply Service-Set"
+
+#: configuration.php:16
+msgid "Allow to deploy configuration"
+msgstr "Consentire la distribuzione delle configurazioni"
+
+#: configuration.php:26
+msgid ""
+"Allow to inspect objects through the Icinga 2 API (could contain sensitive "
+"information)"
+msgstr ""
+"Consentire di ispezionare oggetti tramite l'API Icinga 2 (potrebbe contenere elementi "
+"sensibili)"
+
+
+#: configuration.php:10
+msgid "Allow to show configuration (could contain sensitive information)"
+msgstr "Consenti di mostrare la configurazione (potrebbe contenere informazioni riservate)"
+
+#: configuration.php:14
+msgid "Allow to show the full executed SQL queries in some places"
+msgstr ""
+"Consentire di mostrare le query SQL eseguite completamente in "
+"alcuni punti"
+
+
+#: application/forms/DirectorDatalistEntryForm.php:48
+msgid ""
+"Allow to use this entry only to users with one of these Icinga Web 2 roles"
+msgstr ""
+"Consentire di utilizzare questa voce solo agli utenti con uno di questi "
+"ruoli Icinga Web 2"
+
+
+#: configuration.php:33
+msgid "Allow unrestricted access to Icinga Director"
+msgstr "Consentire l'accesso senza restrizioni a Icinga Director"
+
+#: application/forms/IcingaTemplateChoiceForm.php:85
+msgid "Allowed maximum"
+msgstr "Massimo consentito"
+
+#: application/forms/DirectorDatalistEntryForm.php:44
+msgid "Allowed roles"
+msgstr "Ruoli consentiti"
+
+#: application/forms/IcingaCloneObjectForm.php:85
+msgid "Also clone fields provided by this Template"
+msgstr "Clona anche i campi forniti da questo Template"
+
+#: application/forms/IcingaCloneObjectForm.php:53
+msgid "Also clone single Service Sets defined for this Host"
+msgstr "Clonare anche singoli Service Sets definiti per questo Host"
+
+#: application/forms/IcingaCloneObjectForm.php:44
+msgid "Also clone single Services defined for this Host"
+msgstr "Clonare anche singoli Services definiti per questo Host"
+
+#: application/forms/SelfServiceSettingsForm.php:191
+msgid ""
+"Also install NSClient++. It can be used through the Icinga Agent and comes "
+"with a bunch of additional Check Plugins"
+msgstr ""
+"Installa anche NSClient++. Può essere utilizzato tramite l'agente Icinga e arriva "
+"con parecchi Check-Plugins aggiuntivi"
+
+#: application/forms/DirectorDatafieldForm.php:109
+#, php-format
+msgid "Also rename all \"%s\" custom variables to \"%s\" on %d objects?"
+msgstr ""
+"Rinominare anche variabili personalizzate da \"%s\" a \"%s\" su %d questi "
+"oggetti?"
+
+#: application/forms/DirectorDatafieldForm.php:66
+#, php-format
+msgid "Also wipe all \"%s\" custom variables from %d objects?"
+msgstr ""
+"Pulisci anche tutti le variabili personalizzate \"%s\" dagli %d oggetti?"
+
+#: application/forms/IcingaHostForm.php:336
+msgid ""
+"Alternative name for this host. Might be a host alias or and kind of string "
+"helping your users to identify this host"
+msgstr ""
+"Nome alternativo per questo Host. Può essere un Alias o qualsiasi stringa, "
+"aiuta gli utenti nell'identificare questo Host"
+
+#: application/forms/IcingaUserForm.php:135
+msgid ""
+"Alternative name for this user. In case your object name is a username, this "
+"could be the full name of the corresponding person"
+msgstr ""
+"Nome alternativo per questo utente. Se il nome dell'oggetto fosse uno username, "
+"potrebbe essere utile usare il nome completo della persona."
+
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1526
+msgid "Alternative text to be shown in case above icon is missing"
+msgstr "Testo alternativo da mostrare nel caso in cui l'icona manchi"
+
+#: application/forms/IcingaCommandArgumentForm.php:91
+msgid ""
+"An Icinga DSL expression that returns a boolean value, e.g.: var cmd = "
+"bool(macro(\"$cmd$\")); return cmd ..."
+msgstr ""
+"Un'espressione Icinga DSL che restituisce un valore booleano, e.g.: var cmd "
+"= bool(macro(\"$cmd$\")); return cmd ..."
+
+#: application/forms/IcingaCommandArgumentForm.php:52
+msgid ""
+"An Icinga DSL expression, e.g.: var cmd = macro(\"$cmd$\"); return "
+"typeof(command) == String ..."
+msgstr ""
+"Un'espressione Icinga DSL, e.g.: var cmd = macro(\"$cmd$\"); return "
+"typeof(command) == String ..."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1508
+msgid ""
+"An URL leading to additional actions for this object. Often used with Icinga "
+"Classic, rarely with Icinga Web 2 as it provides far better possibilities to "
+"integrate addons"
+msgstr ""
+"Un URL che porta ad azioni aggiuntive per questo oggetto. Viene usato spesso con Icinga "
+"Classic, meno usato con Icinga Web 2, in quanto quest'ultimo offre soluzioni di Addons migliori"
+
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1501
+msgid "An URL pointing to additional notes for this object"
+msgstr "Un URL che punta a note aggiuntive per questo oggetto"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1517
+msgid ""
+"An URL pointing to an icon for this object. Try \"tux.png\" for icons "
+"relative to public/img/icons or \"cloud\" (no extension) for items from the "
+"Icinga icon font"
+msgstr ""
+"Un URL che punta ad un'icona per questo oggetto. Prova \"tux.png\" per icone relatice al path "
+"public/img/icons o \"cloud\" (senza estensione) per oggetti dall'Icinga icon font."
+
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1273
+msgid ""
+"An alternative display name for this group. If you wonder how this could be "
+"helpful just leave it blank"
+msgstr ""
+"Un nome visualizzato alternativo per questo gruppo. Può essere lasciato vuoto."
+
+#: application/forms/ImportRowModifierForm.php:54
+msgid ""
+"An extended description for this Import Row Modifier. This should explain "
+"it's purpose and why it has been put in place at all."
+msgstr ""
+"Una descrizione estesa per questo modificatore di riga di importazione. "
+"Questo dovrebbe indicare il suo scopo e perché è stato messo in atto."
+
+#: application/forms/ImportSourceForm.php:26
+msgid ""
+"An extended description for this Import Source. This should explain what "
+"kind of data you're going to import from this source."
+msgstr ""
+"Una descrizione estesa per questa sorgente di importazione. "
+"Questo dovrebbe indicare che tipo di dati verranno importati da questa sorgente"
+
+#: application/forms/SyncRuleForm.php:38
+msgid ""
+"An extended description for this Sync Rule. This should explain what this "
+"Rule is going to accomplish."
+msgstr ""
+"Una descrizione estesa per questa regola di sincronizzazione. "
+"Questo dovrebbe indicare ciò che questa regola sta per realizzare."
+
+#: application/forms/DirectorDatafieldForm.php:161
+msgid ""
+"An extended description for this field. Will be shown as soon as a user puts "
+"the focus on this field"
+msgstr ""
+"Una descrizione estesa per questo campo. "
+"Verrà mostrato non appena un utente si sposta su questo campo"
+
+#: library/Director/Import/ImportSourceLdap.php:55
+msgid ""
+"An object class to search for. Might be \"user\", \"group\", \"computer\" or "
+"similar"
+msgstr ""
+"Una classe di oggetti da cercare. e.g. \"user\", \"group\", "
+"\"computer\" o simili"
+
+#: library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php:35
+msgid ""
+"Another Import Source. We're going to look up the row with the key matching "
+"the value in the chosen column"
+msgstr ""
+"Un'altra sorgente di importazione. Verrà individuata la riga la cui chiave "
+"corrisponde al valore nella colonna scelta"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:20
+msgid "Any first (leftmost) component"
+msgstr "Qualsiasi primo componente (più a sinistra)"
+
+#: library/Director/Web/SelfService.php:115
+msgid "Api Key:"
+msgstr "API Key:"
+
+#: library/Director/Web/Controller/TemplateController.php:53
+#, php-format
+msgid "Applied %s"
+msgstr "Applicato %s"
+
+#: application/forms/IcingaHostForm.php:217
+msgid "Applied groups"
+msgstr "Applicato a gruppi"
+
+#: application/controllers/HostController.php:344
+#, php-format
+msgid "Applied service: %s"
+msgstr "Applicato service: %s"
+
+#: application/controllers/HostController.php:189
+#: application/controllers/HostController.php:277
+msgid "Applied services"
+msgstr "Applicato services"
+
+#: library/Director/Web/Tabs/ObjectsTabs.php:45
+msgid "Apply"
+msgstr "Applica"
+
+#: application/controllers/ServiceController.php:110
+#, php-format
+msgid "Apply \"%s\""
+msgstr "Applica \"%s\""
+
+#: application/forms/ApplyMigrationsForm.php:25
+#, php-format
+msgid "Apply %d pending schema migrations"
+msgstr "Applica %d migrazione di schema in sospeso"
+
+#: application/forms/IcingaServiceForm.php:609
+msgid "Apply For"
+msgstr "Applica a"
+
+#: library/Director/Web/Controller/TemplateController.php:168
+msgid "Apply Rule"
+msgstr "Regola Apply"
+
+#: library/Director/Web/Table/ApplyRulesTable.php:161
+msgid "Apply Rule rendering preview"
+msgstr "Anteprima di come risulta la regola applicata"
+
+#: library/Director/Web/Table/DependencyTemplateUsageTable.php:11
+#: library/Director/Web/Table/NotificationTemplateUsageTable.php:11
+#: library/Director/Web/Table/ServiceTemplateUsageTable.php:12
+msgid "Apply Rules"
+msgstr "Regole Apply"
+
+#: application/forms/ApplyMigrationsForm.php:20
+msgid "Apply a pending schema migration"
+msgstr "Applicare una migrazione dello schema in sospeso"
+
+#: library/Director/Job/SyncJob.php:92
+msgid "Apply changes"
+msgstr "Applica le modifiche"
+
+#: library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php:19
+msgid "Apply notifications with specific properties according to given rules. "
+msgstr ""
+"Applica notifiche con proprietà specifiche in base a determinate regole."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1098
+msgid "Apply rule"
+msgstr "Applica regole"
+
+#: library/Director/Web/Table/ApplyRulesTable.php:168
+msgid "Apply rule history"
+msgstr "Storico delle regole applicate"
+
+#: application/forms/KickstartForm.php:38
+msgid "Apply schema migrations"
+msgstr "Applica schema di migrazione"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:78
+#: application/forms/IcingaDependencyForm.php:93
+#: application/forms/IcingaNotificationForm.php:81
+msgid "Apply to"
+msgstr "Applica a"
+
+#: application/controllers/ServiceController.php:206
+#, php-format
+msgid "Apply: %s"
+msgstr "Applica: %s"
+
+#: library/Director/Web/Table/IcingaCommandArgumentTable.php:45
+msgid "Argument"
+msgstr "Argomento"
+
+#: application/forms/IcingaObjectFieldForm.php:99
+msgid "Argument macros"
+msgstr "Argomento macro"
+
+#: application/forms/IcingaCommandArgumentForm.php:25
+msgid "Argument name"
+msgstr "Nome argomento"
+
+#: application/forms/SyncPropertyForm.php:314
+msgid "Arguments"
+msgstr "Argomenti"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:77
+#: library/Director/DataType/DataTypeSqlQuery.php:77
+#: library/Director/DataType/DataTypeDatalist.php:131
+msgid "Array"
+msgstr "Array"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1578
+msgid "Assign where"
+msgstr "Assegna dove"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:27
+#: library/Director/Web/Widget/ActivityLogInfo.php:536
+msgid "Author"
+msgstr "Autore"
+
+#: library/Director/DataType/DataTypeDatalist.php:144
+msgid "Autocomplete"
+msgstr "Autocompletamento"
+
+#: library/Director/Dashboard/AutomationDashboard.php:15
+msgid "Automate all tasks"
+msgstr "Automatizza tutte le operazioni"
+
+#: configuration.php:134
+msgid "Automation"
+msgstr "Automazione"
+
+#: application/forms/IcingaTemplateChoiceForm.php:64
+msgid "Available choices"
+msgstr "Scelte disponibili"
+
+#: application/controllers/BasketController.php:50
+#: library/Director/Web/Controller/TemplateController.php:94
+msgid "Back"
+msgstr "Indietro"
+
+#: application/controllers/BasketController.php:32
+#: application/forms/AddToBasketForm.php:60
+#: library/Director/Web/Table/BasketTable.php:31
+msgid "Basket"
+msgstr "Basket"
+
+#: application/forms/BasketForm.php:34
+msgid "Basket Definitions"
+msgstr "Definizioni Basket"
+
+#: application/forms/BasketUploadForm.php:29
+#: application/forms/BasketForm.php:44
+msgid "Basket Name"
+msgstr "Nome Basket"
+
+#: application/controllers/BasketController.php:140
+msgid "Basket Snapshots"
+msgstr "Basket Snapshots"
+
+#: application/forms/BasketUploadForm.php:145
+msgid "Basket has been uploaded"
+msgstr "Il Basket é stato caricato"
+
+#: application/controllers/BasketController.php:76
+#: application/controllers/BasketsController.php:17
+msgid "Baskets"
+msgstr "Baskets"
+
+#: application/forms/SyncRuleForm.php:87
+msgid ""
+"Be careful: this is usually NOT what you want, as it makes Sync \"blind\" "
+"for objects matching this filter. This means that \"Purge\" will not work as "
+"expected. The \"Black/Whitelist\" Import Property Modifier is probably what "
+"you're looking for."
+msgstr ""
+"Fai attenzione: di solito NON è quello che vuoi, poiché fa la "
+"Sync \"blind\" per tutti gli oggetti corrispondenti a questo filtro. Ciò significa che "
+" \"Purge\" non funzionerà come ci si aspetta. Ti consigliamo di usare invece il \"Black/Whitelist\" "
+"Import Modificatore proprietà."
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:72
+msgid "Binary"
+msgstr "Binary"
+
+#: application/forms/IcingaServiceForm.php:160
+msgid "Blacklist"
+msgstr "Blacklist"
+
+#: application/forms/IcingaObjectFieldForm.php:125
+#: application/forms/DirectorDatafieldForm.php:150
+msgid "Caption"
+msgstr "Didascalia"
+
+#: application/forms/IcingaMultiEditForm.php:269
+#, php-format
+msgid "Changing this value affects %d object(s): %s"
+msgstr "La modifica di questo valore influisce %d oggett(o,i): %s"
+
+#: library/Director/Import/ImportSourceCoreApi.php:57
+msgid "Check Commands"
+msgstr "Check Commands"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1294
+msgid "Check command"
+msgstr "Check command"
+
+#: application/forms/IcingaNotificationForm.php:255
+#: library/Director/Web/Form/DirectorObjectForm.php:1295
+msgid "Check command definition"
+msgstr "Definizione Check command"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1363
+msgid ""
+"Check command timeout in seconds. Overrides the CheckCommand's timeout "
+"attribute"
+msgstr ""
+"Check command timeout in secondi. Sovrascrive l'attributo timeout del commando"
+
+
+#: library/Director/Web/Form/DirectorObjectForm.php:323
+msgid "Check execution"
+msgstr "Check execution"
+
+#: application/forms/SyncCheckForm.php:23
+#: application/forms/ImportCheckForm.php:23
+msgid "Check for changes"
+msgstr "Verifica le modifiche"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1330
+msgid "Check interval"
+msgstr "Check interval"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1375
+msgid "Check period"
+msgstr "Check period"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1361
+msgid "Check timeout"
+msgstr "Check timeout"
+
+#: application/forms/ImportCheckForm.php:45
+msgid "Checking this Import Source failed"
+msgstr "Verifica di questa sorgente di importazione non riuscita"
+
+#: application/forms/SyncCheckForm.php:61
+msgid "Checking this sync rule failed"
+msgstr "Verifica della regola di sincronizzazione non riuscita"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:573
+msgid "Checksum"
+msgstr "Checksum"
+
+#: application/forms/IcingaDependencyForm.php:233
+msgid "Child Host"
+msgstr "Host-figlio"
+
+#: application/forms/IcingaDependencyForm.php:246
+msgid "Child Service"
+msgstr "Service-figlio"
+
+#: application/forms/IcingaTemplateChoiceForm.php:48
+msgid "Choice name"
+msgstr "Nome scelto"
+
+#: library/Director/Dashboard/Dashlet/ChoicesDashlet.php:11
+#: library/Director/Web/Tabs/ObjectsTabs.php:70
+msgid "Choices"
+msgstr "Selezione"
+
+#: application/forms/BasketForm.php:78
+msgid ""
+"Choose \"All\" to always add all of them, \"Ignore\" to not care about a "
+"specific Type at all and opt for \"Custom Selection\" in case you want to "
+"choose just some specific Objects."
+msgstr ""
+"Scegli \"Tutti\" per aggiungere sempre tutti, \"Ignora\" per non includere uno "
+"specifico Tipo e scegli \"Selezione personalizzata\" nel caso in cui si vogliano "
+"includere solo alcuni oggetti specifici."
+
+
+#: application/forms/IcingaHostForm.php:176
+msgid "Choose a Host Template"
+msgstr "Scegli un Host template"
+
+#: application/forms/IcingaAddServiceForm.php:107
+msgid "Choose a service template"
+msgstr "Scegli un Service template"
+
+#: application/forms/SyncRuleForm.php:46
+msgid "Choose an object type"
+msgstr "Scegli un tipo di oggetto"
+
+#: application/forms/BasketUploadForm.php:35
+msgid "Choose file"
+msgstr "Scegli un file"
+
+#: application/forms/IcingaServiceForm.php:587
+msgid "Choose the host this single service should be assigned to"
+msgstr "Scegli l'Host a cui assegnare questo singolo servizio"
+
+#: application/forms/IcingaTemplateChoiceForm.php:75
+msgid ""
+"Choosing this many options will be mandatory for this Choice. Setting this "
+"to zero will leave this Choice optional, setting it to one results in a "
+"\"required\" Choice. You can use higher numbers to enforce multiple options, "
+"this Choice will then turn into a multi-selection element."
+msgstr ""
+"La scelta di molte opzioni sarà obbligatoria per questa scelta. Impostando questo "
+"su zero, questa scelta sarà facoltativa, impostandola su uno diventerà una scelta "
+"\"obbligatorio\". È possibile utilizzare numeri più alti per applicare più opzioni, "
+"questa scelta si trasformerà quindi in un elemento a selezione multipla."
+
+
+#: application/forms/IcingaZoneForm.php:37
+msgid "Chose an (optional) parent zone"
+msgstr "Scegli una zona padre (facoltativa)"
+
+#: library/Director/Web/Widget/Documentation.php:31
+#, php-format
+msgid "Click to read our documentation: %s"
+msgstr "Clicca per leggere la documentazione: %s"
+
+#: application/controllers/ImportsourceController.php:123
+#: application/controllers/SyncruleController.php:498
+#: library/Director/Web/Form/CloneImportSourceForm.php:34
+#: library/Director/Web/Form/CloneSyncRuleForm.php:34
+#: library/Director/Web/Controller/ObjectController.php:316
+#: library/Director/Web/ActionBar/AutomationObjectActionBar.php:44
+msgid "Clone"
+msgstr "Clona"
+
+#: application/forms/IcingaCloneObjectForm.php:91
+#, php-format
+msgid "Clone \"%s\""
+msgstr "Clona \"%s\""
+
+#: application/forms/IcingaCloneObjectForm.php:51
+msgid "Clone Service Sets"
+msgstr "Clona Service-Set"
+
+#: application/forms/IcingaCloneObjectForm.php:42
+msgid "Clone Services"
+msgstr "Clona Services"
+
+#: application/forms/IcingaCloneObjectForm.php:83
+msgid "Clone Template Fields"
+msgstr "Clona campi template"
+
+#: application/forms/IcingaCloneObjectForm.php:32
+msgid "Clone the object as is, preserving imports"
+msgstr "Clona l'oggetto così com'è, preservando le importazioni"
+
+#: application/forms/IcingaCloneObjectForm.php:72
+msgid "Clone this service to the very same or to another Host"
+msgstr "Clonare questo Service sullo stesso o su un altro Host"
+
+#: application/forms/IcingaCloneObjectForm.php:63
+msgid "Clone this service to the very same or to another Service Set"
+msgstr "Clonare questo servizio sullo stesso o su un altro Service Set"
+
+#: library/Director/Web/Controller/ObjectController.php:174
+#, php-format
+msgid "Clone: %s"
+msgstr "Clona %s"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1137
+msgid "Cluster Zone"
+msgstr "Cluster Zone"
+
+#: library/Director/Dashboard/Dashlet/ChoicesDashlet.php:17
+msgid ""
+"Combine multiple templates into meaningful Choices, making life easier for "
+"your users"
+msgstr ""
+"Combina più template in scelte significative, semplificando la vita "
+"ai tuoi utenti"
+
+#: application/forms/IcingaCommandForm.php:59
+#: application/forms/SyncRuleForm.php:20
+#: library/Director/TranslationDummy.php:16
+msgid "Command"
+msgstr "Command"
+
+#: application/forms/BasketForm.php:17
+msgid "Command Definitions"
+msgstr "Definizione Command"
+
+#: application/forms/BasketForm.php:19
+msgid "Command Template"
+msgstr "Command Template"
+
+#: library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php:19
+msgid "Command Templates"
+msgstr "Command Templates"
+
+#: application/controllers/CommandController.php:89
+#, php-format
+msgid "Command arguments: %s"
+msgstr "Command argomenti: %s"
+
+#: application/forms/IcingaHostForm.php:103
+msgid "Command endpoint"
+msgstr "Command endpoint"
+
+#: application/forms/IcingaCommandForm.php:48
+msgid "Command name"
+msgstr "Nome command"
+
+#: application/forms/IcingaCommandForm.php:17
+msgid "Command type"
+msgstr "Tipo command"
+
+#: configuration.php:126
+#: library/Director/Dashboard/Dashlet/CommandObjectDashlet.php:13
+#: library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php:19
+#: library/Director/Web/Table/CustomvarVariantsTable.php:57
+msgid "Commands"
+msgstr "Commands"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:35
+msgid "Comment"
+msgstr "Commento"
+
+#: application/controllers/BasketController.php:336
+#, php-format
+msgid "Comparing %s \"%s\" from Snapshot \"%s\" to current config"
+msgstr ""
+"Comparazione %s \"%s\" da Snapshot \"%s\" con la configurazione attuale"
+
+#: application/forms/IcingaCommandArgumentForm.php:89
+#: application/forms/IcingaCommandArgumentForm.php:98
+msgid "Condition (set_if)"
+msgstr "Condizione (set_if)"
+
+#: application/forms/IcingaCommandArgumentForm.php:75
+msgid "Condition format"
+msgstr "Formato condizione"
+
+#: application/controllers/JobController.php:102
+#: application/controllers/ConfigController.php:258
+#: application/controllers/ConfigController.php:469
+#: library/Director/Web/Widget/DeploymentInfo.php:60
+#: library/Director/Web/Table/CoreApiFieldsTable.php:80
+msgid "Config"
+msgstr "Configurazione"
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:18
+msgid "Config Deployment"
+msgstr "Distribuzione configurazione"
+
+#: application/controllers/ConfigController.php:444
+#: application/forms/DeploymentLinkForm.php:155
+#: application/forms/DeployConfigForm.php:100
+msgid "Config deployment failed"
+msgstr "Distribuzione configurazione fallita"
+
+#: application/controllers/ConfigController.php:344
+#: application/controllers/ConfigController.php:345
+msgid "Config diff"
+msgstr "Differenze configurazione"
+
+#: application/controllers/ConfigController.php:303
+#: application/controllers/ConfigController.php:405
+#, php-format
+msgid "Config file \"%s\""
+msgstr "File di configurazione \"%s\""
+
+#: application/controllers/ConfigController.php:424
+#: application/forms/DeploymentLinkForm.php:142
+#: application/forms/DeployConfigForm.php:76
+#: application/forms/DeployConfigForm.php:95
+msgid "Config has been submitted, validation is going on"
+msgstr "La configurazione è stata inviata, la convalida è in corso"
+
+#: application/forms/DeployFormsBug7530.php:83
+msgid "Config has not been deployed"
+msgstr "La configurazione non è stata distribuita"
+
+#: library/Director/Web/ObjectPreview.php:40
+#, php-format
+msgid "Config preview: %s"
+msgstr "Anteprima della configurazione: %s"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:82
+#: library/Director/ProvidedHook/Monitoring/ServiceActions.php:54
+msgid "Configuration"
+msgstr "Configurazione"
+
+#: application/controllers/HostController.php:212
+msgid "Configuration (read-only)"
+msgstr "Configurazione (solo lettura)"
+
+#: application/controllers/BasketsController.php:32
+#: library/Director/Dashboard/Dashlet/BasketDashlet.php:11
+msgid "Configuration Baskets"
+msgstr "Configurazione Baskets"
+
+#: application/forms/SettingsForm.php:109
+msgid "Configuration format"
+msgstr "Formato configurazione"
+
+#: application/forms/KickstartForm.php:332
+msgid "Configuration has been stored"
+msgstr "La Configurazione é stata salvata"
+
+#: application/forms/AddToBasketForm.php:111
+#, php-format
+msgid "Configuration objects have been added to the chosen basket \"%s\""
+msgstr "Gli oggetti di configurazione sono stati aggiunti al Basket \"%s\" selezionato"
+
+#: library/Director/Web/SelfService.php:152
+msgid "Configure this Agent via Self Service API"
+msgstr "Configurare questo agente tramite l'API self-service"
+
+#: application/controllers/BasketController.php:229
+msgid "Content Checksum"
+msgstr "Checksum contenuto"
+
+#: application/controllers/BasketsController.php:20
+msgid "Create"
+msgstr "Creato"
+
+#: application/controllers/BasketController.php:102
+msgid "Create Basket"
+msgstr "Creare Basket"
+
+#: application/forms/BasketCreateSnapshotForm.php:23
+msgid "Create Snapshot"
+msgstr "Creare Snapshot"
+
+#: library/Director/Web/Controller/ObjectsController.php:300
+#, php-format
+msgid "Create a new %s Apply Rule"
+msgstr "Crea una nuova regola %s Apply"
+
+#: library/Director/Web/Controller/ObjectsController.php:341
+#, php-format
+msgid "Create a new %s Set"
+msgstr "Crea un nuovo %s Set"
+
+#: library/Director/Web/Controller/TemplateController.php:156
+#, php-format
+msgid "Create a new %s inheriting from this one"
+msgstr "Crea un nuovo %s ereditando da questo"
+
+#: library/Director/Web/Controller/TemplateController.php:146
+#: library/Director/Web/Controller/TemplateController.php:166
+#, php-format
+msgid "Create a new %s inheriting from this template"
+msgstr "Crea un nuovo %s ereditando da questo template"
+
+#: application/controllers/BasketController.php:103
+msgid "Create a new Configuration Basket"
+msgstr "Crea un nuovo Basket di configurazione"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:23
+msgid "Create a new Template"
+msgstr "Crea un nuovo template"
+
+#: library/Director/Web/ActionBar/ObjectsActionBar.php:20
+msgid "Create a new object"
+msgstr "Crea un nuovo oggetto"
+
+#: library/Director/Web/ActionBar/ChoicesActionBar.php:20
+msgid "Create a new template choice"
+msgstr "Crea un nuovo template di selezione"
+
+#: application/forms/KickstartForm.php:37
+msgid "Create database schema"
+msgstr "Crea lo schema di database"
+
+#: application/forms/ApplyMigrationsForm.php:31
+msgid "Create schema"
+msgstr "Crea lo schema"
+
+#: application/controllers/BasketController.php:228
+msgid "Created"
+msgstr "Creato"
+
+#: library/Director/IcingaConfig/StateFilterSet.php:26
+msgid "Critical"
+msgstr "Critico"
+
+#: library/Director/Web/Controller/TemplateController.php:185
+msgid "Current Template Usage"
+msgstr "Utilizzo template corrente"
+
+#: application/forms/BasketForm.php:53
+msgid "Custom Selection"
+msgstr "Selezione personalizzata"
+
+#: application/controllers/CustomvarController.php:13
+msgid "Custom Variable"
+msgstr "Variable personalizzata"
+
+#: application/controllers/CustomvarController.php:14
+#, php-format
+msgid "Custom Variable variants: %s"
+msgstr "Varianti di varibili personalizzate: %s"
+
+#: library/Director/Web/Tabs/DataTabs.php:27
+msgid "Custom Variables"
+msgstr "Variabili personalizzate"
+
+#: application/controllers/DataController.php:80
+msgid "Custom Vars - Overview"
+msgstr "Vars personalizzati - Panoramica"
+
+#: application/forms/SyncPropertyForm.php:179
+msgid "Custom expression"
+msgstr "Espressione personalizzata"
+
+#: library/Director/Web/Controller/ObjectController.php:190
+#, php-format
+msgid "Custom fields: %s"
+msgstr "Campi personalizzati: \"%s\""
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:25
+msgid "Custom notification"
+msgstr "Notifica personalizzata"
+
+#: application/forms/IcingaServiceForm.php:438
+#: library/Director/Web/Form/IcingaObjectFieldLoader.php:227
+msgid "Custom properties"
+msgstr "Proprietà personalizzate"
+
+#: application/forms/SyncPropertyForm.php:67
+msgid "Custom variable"
+msgstr "Variabile personalizzata"
+
+#: application/forms/SyncPropertyForm.php:307
+msgid "Custom variable (vars.)"
+msgstr "Variabile personalizzata (vars.)"
+
+#: application/controllers/SuggestController.php:250
+#: application/controllers/SuggestController.php:260
+#: library/Director/Objects/IcingaHost.php:157
+#: library/Director/Objects/IcingaService.php:727
+msgid "Custom variables"
+msgstr "Variabili personalizzate"
+
+#: library/Director/Dashboard/Dashlet/CustomvarDashlet.php:11
+msgid "CustomVar Overview"
+msgstr "Panoramica di variabili personalizzate"
+
+#: library/Director/Import/ImportSourceSql.php:40
+msgid "DB Query"
+msgstr "Query DB"
+
+#: application/forms/KickstartForm.php:245
+msgid "DB Resource"
+msgstr "Risorsa DB"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:15
+msgid "DN component"
+msgstr "DN componente"
+
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:25
+msgid "DNS record type"
+msgstr "DNS record type"
+
+#: library/Director/Web/Tabs/MainTabs.php:34
+msgid "Daemon"
+msgstr "Servizio"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:41
+#, php-format
+msgid "Daemon has been stopped %s, was running with PID %s as %s@%s"
+msgstr "Il servizio %s è stato arrestato, girava prima con PID %s e %s@%s"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:90
+#, php-format
+msgid "Daemon is running with PID %s as %s@%s, last refresh happened %s"
+msgstr ""
+"Il servizio è in esecuzione con PID %s %s@%s, l'ultimo aggiornamento è avvenuto in %s "
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:54
+#, php-format
+msgid ""
+"Daemon keep-alive is outdated, was last seen running with PID %s as %s@%s %s"
+msgstr ""
+"Servizio keep-alive è obsoleto, è stato visto per l'ultima volta in esecuzione con PID %s %s@%s %s"
+
+#: application/controllers/DataController.php:63
+msgid "Data Fields"
+msgstr "Campi dati"
+
+
+#: application/controllers/DataController.php:53
+msgid "Data List"
+msgstr "Elenco dati"
+
+#: application/forms/SyncRuleForm.php:19
+msgid "Data List Entry"
+msgstr "Voce dell'elenco dati"
+
+#: application/controllers/DataController.php:47
+#, php-format
+msgid "Data List: %s"
+msgstr "Elenco dati: %s"
+
+#: application/forms/BasketForm.php:30
+msgid "Data Lists"
+msgstr "Elenchi di dati"
+
+#: library/Director/Web/Tabs/DataTabs.php:21
+msgid "Data fields"
+msgstr "Campi dati"
+
+#: application/forms/DirectorDatafieldForm.php:133
+msgid ""
+"Data fields allow you to customize input controls for Icinga custom "
+"variables. Once you defined them here, you can provide them through your "
+"defined templates. This gives you a granular control over what properties "
+"your users should be allowed to configure in which way."
+msgstr ""
+"I campi dati consentono di personalizzare i controlli di input per le "
+"custom variables di Icinga. Dopo averli definiti qui, è possibile utilizzarli nelle "
+"definitioni dei template. Questo ti dà un controllo granulare su quali proprietà "
+"i tuoi utenti dovrebbero essere autorizzati a configurare e in che modo."
+
+#: library/Director/Dashboard/Dashlet/DatafieldDashlet.php:17
+msgid "Data fields make sure that configuration fits your rules"
+msgstr ""
+"I campi dati assicurano che la configurazione soddisfi le tue regole."
+
+#: application/forms/DirectorDatalistForm.php:24
+msgid "Data list"
+msgstr "Elenco campi"
+
+#: application/controllers/DataController.php:20
+#: library/Director/Web/Tabs/DataTabs.php:24
+msgid "Data lists"
+msgstr "Elenchi di dati"
+
+#: application/forms/DirectorDatalistForm.php:15
+msgid ""
+"Data lists are mainly used as data providers for custom variables presented "
+"as dropdown boxes boxes. You can manually manage their entries here in "
+"place, but you could also create dedicated sync rules after creating a new "
+"empty list. This would allow you to keep your available choices in sync with "
+"external data providers"
+msgstr ""
+"Gli elenchi di dati vengono utilizzati principalmente come sorgente di dati per "
+"le variabili personalizzate presentate come caselle. Puoi gestire manualmente "
+"le loro voci direttamente qui, ma puoi anche creare regole di sincronizzazione "
+"dedicate, dopo aver creato un nuovo elenco. Ciò ti consente di mantenere "
+"sincronizzate le opzioni disponibili con sorgenti di dati esterni"
+
+#: application/forms/DirectorDatafieldForm.php:176
+msgid "Data type"
+msgstr "Tipo di dati"
+
+#: application/forms/KickstartForm.php:295
+msgid "Database backend"
+msgstr "Database backend"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:537
+msgid "Date"
+msgstr "Data"
+
+#: application/forms/IcingaTimePeriodRangeForm.php:21
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:22
+#: library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php:51
+#: library/Director/Web/Table/IcingaTimePeriodRangeTable.php:45
+msgid "Day(s)"
+msgstr "Giorno(i)"
+
+#: application/forms/SettingsForm.php:118
+msgid ""
+"Default configuration format. Please note that v1.x is for special "
+"transitional projects only and completely unsupported. There are no plans to "
+"make Director a first-class configuration backends for Icinga 1.x"
+msgstr ""
+"Si noti che v1.x è solo per progetti speciali di transizione e completamente "
+"non supportato. Non ci sono piani per rendere Director un backend di configurazione "
+"per Icinga 1.x."
+
+#: application/forms/SettingsForm.php:32
+msgid "Default global zone"
+msgstr "Zona globale predefinita"
+
+#: library/Director/Dashboard/CommandsDashboard.php:23
+msgid ""
+"Define Check-, Notification- or Event-Commands. Command definitions are the "
+"glue between your Host- and Service-Checks and the Check plugins on your "
+"Monitoring (or monitored) systems"
+msgstr ""
+"Definisci i comandi Check, Notification o Event. Le definizioni dei comandi "
+"sono i link tra i tuoi controlli Host e Service e i plugin Check sui tuoi sistemi "
+"di monitoraggio (o monitorati)"
+
+#: library/Director/Dashboard/Dashlet/DatafieldDashlet.php:11
+msgid "Define Data Fields"
+msgstr "Definisci campi dati"
+
+#: library/Director/Dashboard/Dashlet/HostGroupsDashlet.php:17
+msgid ""
+"Define Host Groups to give your configuration more structure. They are "
+"useful for Dashboards, Notifications or Restrictions"
+msgstr ""
+"Definisci i gruppi host per dare più struttura alla tua configurazione. "
+"Sono utili per dashboard, notifiche o restrizioni"
+
+#: application/forms/SelfServiceSettingsForm.php:103
+msgid ""
+"Define a download Url or local directory from which the a specific Icinga 2 "
+"Agent MSI Installer package should be fetched. Please ensure to only define "
+"the base download Url or Directory. The Module will generate the MSI file "
+"name based on your operating system architecture and the version to install. "
+"The Icinga 2 MSI Installer name is internally build as follows: Icinga2-"
+"v[InstallAgentVersion]-[OSArchitecture].msi (full example: Icinga2-v2.6.3-"
+"x86_64.msi)"
+msgstr ""
+"Definire un URL di download o una directory locale da cui prelevare uno specifico "
+"pacchetto di installazione MSI Icinga 2 Agent. Assicurati di definire solo l'URL o "
+"la directory di download di base. Il modulo genererà il nome del file MSI in base "
+"all'architettura del sistema operativo e alla versione da installare. "
+"Il nome del programma di installazione MSI Icinga 2 è costruito internamente come "
+"segue: Icinga2-v [InstallAgentVersion] - [OSArchitecture] .msi "
+"(esempio completo: Icinga2-v2.6.3-x86_64.msi)"
+
+#: library/Director/Dashboard/Dashlet/ImportSourceDashlet.php:29
+msgid "Define and manage imports from various data sources"
+msgstr "Definire e gestire le importazioni da varie sorgenti dati"
+
+#: library/Director/Dashboard/TimeperiodsDashboard.php:14
+msgid "Define custom Time Periods"
+msgstr "Definire periodi di tempo personalizzati"
+
+#: library/Director/Dashboard/Dashlet/SyncDashlet.php:29
+msgid "Define how imported data should be synchronized with Icinga"
+msgstr ""
+"Definire come sincronizzare i dati importati con Icinga"
+
+#: application/forms/SyncRuleForm.php:54
+msgid ""
+"Define what should happen when an object with a matching key already exists. "
+"You could merge its properties (import source wins), replace it completely "
+"with the imported object or ignore it (helpful for one-time imports)"
+msgstr ""
+"Definire cosa dovrebbe accadere quando esiste già un oggetto con una chiave "
+"corrispondente. È possibile unire le sue proprietà (importazione sorgente vince), "
+"sostituirlo completamente con l'oggetto importato o ignorarlo "
+"(utile per le importazioni singole)"
+
+#: library/Director/Dashboard/ObjectsDashboard.php:15
+msgid "Define whatever you want to be monitored"
+msgstr "Definisci qualunque cosa tu voglia monitorare"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1352
+msgid "Defines after how many check attempts a new hard state is reached"
+msgstr ""
+"Definisce dopo quanti tentativi di controllo viene raggiunto un nuovo Hard State"
+
+#: library/Director/Dashboard/Dashlet/UserGroupsDashlet.php:17
+msgid ""
+"Defining Notifications for User Groups instead of single Users gives more "
+"flexibility"
+msgstr ""
+"La definizione di notifiche per gruppi di utenti anziché singoli utenti offre "
+"maggiore flessibilità"
+
+#: library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php:17
+msgid ""
+"Defining Service Groups get more structure. Great for Dashboards. "
+"Notifications and Permissions might be based on groups."
+msgstr ""
+"La definizione di gruppi di servizi ottiene una maggiore struttura. "
+"Ottimo per i dashboard. Le notifiche e le autorizzazioni potrebbero "
+"essere basate sui gruppi."
+
+#: application/forms/IcingaNotificationForm.php:199
+msgid "Delay unless the first notification should be sent"
+msgstr "Ritarda prima di spedire la prima notifica"
+
+#: application/forms/IcingaObjectFieldForm.php:193
+#: library/Director/Web/Form/DirectorObjectForm.php:902
+msgid "Delete"
+msgstr "Elimina"
+
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:13
+msgid "Delimiter"
+msgstr "Delimitatore"
+
+#: application/forms/BasketForm.php:29
+#: library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php:13
+msgid "Dependencies"
+msgstr "Dipendenze"
+
+#: application/forms/SyncRuleForm.php:24
+msgid "Dependency"
+msgstr "Dipendenza"
+
+#: application/forms/DeploymentLinkForm.php:88
+msgid "Deploy"
+msgstr "Distribuire"
+
+#: application/forms/DeployConfigForm.php:37
+#, php-format
+msgid "Deploy %d pending changes"
+msgstr "Distribuisci %d modifiche in sospeso"
+
+#: library/Director/Dashboard/DeploymentDashboard.php:15
+msgid "Deploy configuration to your Icinga nodes"
+msgstr "Distribuire la configurazione sui nodi Icinga"
+
+#: library/Director/Job/ConfigJob.php:195
+msgid "Deploy modified config"
+msgstr "Distribuire la configurazione modificata"
+
+#: application/controllers/ConfigController.php:251
+#: application/controllers/ConfigController.php:461
+#: library/Director/Web/Widget/DeploymentInfo.php:54
+msgid "Deployment"
+msgstr "Distribuzione"
+
+#: application/forms/SettingsForm.php:153
+msgid "Deployment Path"
+msgstr "Percorso di distribuzione"
+
+#: application/controllers/DeploymentController.php:22
+msgid "Deployment details"
+msgstr "Dettagli di distribuzione"
+
+#: application/forms/SettingsForm.php:138
+msgid "Deployment mode"
+msgstr "Modalità di distribuzione"
+
+#: application/forms/SettingsForm.php:147
+msgid "Deployment mode for Icinga 1 configuration"
+msgstr "Modalità di distribuzione per la configurazione di Icinga 1"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:77
+msgid "Deployment time"
+msgstr "Tempo di distribuzione"
+
+#: configuration.php:143
+#: application/controllers/ConfigController.php:50
+#: library/Director/Web/Tabs/InfraTabs.php:36
+msgid "Deployments"
+msgstr "Distribuzioni"
+
+#: application/forms/IcingaCommandArgumentForm.php:31
+#: application/forms/SyncRuleForm.php:36
+#: application/forms/ImportSourceForm.php:24
+#: application/forms/IcingaTemplateChoiceForm.php:56
+#: application/forms/IcingaObjectFieldForm.php:133
+#: application/forms/IcingaServiceSetForm.php:100
+#: application/forms/ImportRowModifierForm.php:52
+#: application/forms/DirectorDatafieldForm.php:159
+msgid "Description"
+msgstr "Descrizione"
+
+#: application/forms/IcingaCommandArgumentForm.php:32
+msgid "Description of the argument"
+msgstr "Descrizione dell'argomento"
+
+#: library/Director/Web/Table/SyncpropertyTable.php:63
+msgid "Destination"
+msgstr "Destinazione"
+
+#: application/forms/SyncPropertyForm.php:48
+msgid "Destination Field"
+msgstr "Campo di destinazione"
+
+#: application/controllers/HealthController.php:25
+msgid ""
+"Did you know that you can run this entire Health Check (or just some "
+"sections) as an Icinga Check on a regular base?"
+msgstr ""
+"Sapevi che puoi eseguire questo Health Check (o solo alcune sezioni) "
+"come Icinga Check su base regolare?"
+
+#: application/controllers/ConfigController.php:406
+#: library/Director/Web/Widget/ActivityLogInfo.php:368
+msgid "Diff"
+msgstr "Diff"
+
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:74
+msgid "Diff with other config"
+msgstr "Differenza con altre configurazioni"
+
+#: library/Director/Web/Table/TemplateUsageTable.php:55
+msgid "Direct"
+msgstr "Diretto"
+
+#: application/controllers/DaemonController.php:19
+#: application/controllers/DaemonController.php:21
+msgid "Director Background Daemon"
+msgstr "Director Background Daemon"
+
+#: application/controllers/HealthController.php:17
+msgid "Director Health"
+msgstr "Director Health"
+
+#: application/controllers/KickstartController.php:13
+msgid "Director Kickstart Wizard"
+msgstr "Director Kickstart Wizard"
+
+#: library/Director/Dashboard/Dashlet/SettingsDashlet.php:11
+msgid "Director Settings"
+msgstr "Impostazioni Director"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:79
+msgid "Director database schema has not been created yet"
+msgstr "Lo schema del database Director non è stato ancora creato"
+
+#: application/forms/IcingaDependencyForm.php:159
+msgid "Disable Checks"
+msgstr "Disattivare i Checks"
+
+#: application/forms/IcingaDependencyForm.php:167
+msgid "Disable Notificiations"
+msgstr "Disattivare le notifiche"
+
+#: application/forms/SettingsForm.php:54
+msgid "Disable all Jobs"
+msgstr "Disattiva tutti i Jobs"
+
+#: application/forms/DirectorJobForm.php:37
+#: library/Director/Web/Form/DirectorObjectForm.php:1255
+msgid "Disabled"
+msgstr "Disabilitato"
+
+#: application/forms/IcingaCommandForm.php:80
+msgid "Disabled by default, and should only be used in rare cases."
+msgstr ""
+"Disabilitato per impostazione predefinita e deve essere utilizzato solo in rari casi."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1256
+msgid "Disabled objects will not be deployed"
+msgstr "Gli oggetti disabilitati non verranno distribuiti"
+
+#: application/forms/IcingaTimePeriodForm.php:20
+#: library/Director/Web/Form/DirectorObjectForm.php:1271
+msgid "Display Name"
+msgstr "Nome da visualizzare"
+
+#: application/forms/IcingaUserForm.php:133
+#: application/forms/IcingaHostForm.php:333
+msgid "Display name"
+msgstr "Nome da visualizzare"
+
+#: library/Director/Web/Table/CustomvarTable.php:42
+msgid "Distinct Commands"
+msgstr "Comandi distinti"
+
+#: library/Director/Dashboard/DataDashboard.php:15
+msgid "Do more with custom data"
+msgstr "Fai di più con i dati personalizzati"
+
+#: application/forms/SelfServiceSettingsForm.php:36
+msgid "Do not transform at all"
+msgstr "Non trasformare affatto"
+
+#: library/Director/Web/SelfService.php:104
+#: library/Director/Web/SelfService.php:157
+msgid "Documentation"
+msgstr "Documentazione"
+
+#: library/Director/IcingaConfig/StateFilterSet.php:21
+msgid "Down"
+msgstr "Giù"
+
+#: application/controllers/BasketController.php:212
+#: application/controllers/SchemaController.php:80
+#: library/Director/Web/SelfService.php:227
+#: library/Director/Web/SelfService.php:240
+msgid "Download"
+msgstr "Scarica"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:59
+msgid "Download as JSON"
+msgstr "Scarica come JSON"
+
+#: application/forms/SelfServiceSettingsForm.php:70
+msgid "Download from a custom url"
+msgstr "Scarica da un URL personalizzato"
+
+#: application/forms/SelfServiceSettingsForm.php:69
+msgid "Download from packages.icinga.com"
+msgstr "Scarica da packages.icinga.com"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:30
+msgid "Downtime ends"
+msgstr "Fine Downtime"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:21
+msgid "Downtime name"
+msgstr "Nome Downtime"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:31
+msgid "Downtime removed"
+msgstr "Downtime rimosso"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:29
+msgid "Downtime starts"
+msgstr "Downtime partito"
+
+#: application/forms/IcingaForgetApiKeyForm.php:22
+msgid "Drop Self Service API key"
+msgstr "Elimina Self Service API key"
+
+#: library/Director/DataType/DataTypeDatalist.php:143
+msgid "Dropdown (list values only)"
+msgstr "Menu a tendina (solo valori elenco)"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:52
+#: library/Director/Web/Widget/DeploymentInfo.php:83
+#: library/Director/Web/Widget/SyncRunDetails.php:26
+msgid "Duration"
+msgstr "Durata"
+
+#: application/controllers/DatafieldController.php:37
+msgid "Edit a Field"
+msgstr "Modifica un campo"
+
+#: application/controllers/DataController.php:155
+msgid "Edit list"
+msgstr "Modifica elenco"
+
+#: library/Director/DataType/DataTypeDatalist.php:137
+msgid "Element behavior"
+msgstr "Comportamento dell'elemento"
+
+#: application/forms/IcingaUserForm.php:36
+msgid "Email"
+msgstr "E-Mail"
+
+#: application/forms/SettingsForm.php:69
+msgid "Enable audit log"
+msgstr "Abilita audit log"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1405
+msgid "Enable event handler"
+msgstr "Abilita event handler"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1417
+msgid "Enable flap detection"
+msgstr "Abilita flap detection"
+
+#: application/forms/SyncRuleForm.php:25
+#: application/forms/IcingaEndpointForm.php:24
+#: library/Director/Web/Table/ObjectsTableEndpoint.php:19
+msgid "Endpoint"
+msgstr "Endpoint"
+
+#: application/forms/KickstartForm.php:117
+msgid "Endpoint Name"
+msgstr "Nome endpoint"
+
+#: application/forms/IcingaEndpointForm.php:31
+msgid "Endpoint address"
+msgstr "Indirizzo endpoint"
+
+#: application/forms/IcingaEndpointForm.php:18
+msgid "Endpoint template name"
+msgstr "Nome template endpoint"
+
+#: library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php:17
+#: library/Director/Import/ImportSourceCoreApi.php:59
+msgid "Endpoints"
+msgstr "Endpoints"
+
+#: application/controllers/PhperrorController.php:14
+#: application/controllers/PhperrorController.php:53
+msgid "Error"
+msgstr "Errore"
+
+#: application/forms/IcingaHostForm.php:86
+msgid "Establish connection"
+msgstr "Stabilire una connessione"
+
+#: application/forms/IcingaServiceForm.php:613
+msgid ""
+"Evaluates the apply for rule for all objects with the custom attribute "
+"specified. E.g selecting \"host.vars.custom_attr\" will generate \"for "
+"(config in host.vars.array_var)\" where \"config\" will be accessible "
+"through \"$config$\". NOTE: only custom variables of type \"Array\" are "
+"eligible."
+msgstr ""
+"Calcola la regola Apply per tutti gli oggetti con l'attributo personalizzato specificato. "
+"Ad esempio, selezionando \"host.vars.custom_attr\" genererà \"for (config in host.vars.array_var)\" "
+"dove \"config\" é accessibile tramite \"$config$\". NOTA: sono ammesse solo le variabili "
+"personalizzate di tipo \"Array\". "
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1309
+msgid "Event command"
+msgstr "Event command"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1310
+msgid "Event command definition"
+msgstr "Definizione event command"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:37
+msgid "Every related downtime will show this comment"
+msgstr "Ogni downtime correlato mostrerà questo commento"
+
+#: application/forms/IcingaTimePeriodForm.php:70
+msgid "Exclude other time periods from this."
+msgstr "Escludere altri periodi di tempo da questo."
+
+#: application/forms/IcingaTimePeriodForm.php:67
+msgid "Exclude period"
+msgstr "Escludi periodo"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1387
+msgid "Execute active checks"
+msgstr "Esegui checks attivi"
+
+#: application/forms/DirectorJobForm.php:48
+msgid "Execution interval for this job, in seconds"
+msgstr "Intervallo di esecuzione per questo Job, in secondi"
+
+#: application/forms/SyncPropertyForm.php:250
+msgid "Existing Data Lists"
+msgstr "Elenchi di dati esistenti"
+
+#: application/forms/SyncPropertyForm.php:235
+msgid "Existing templates"
+msgstr "Elenchi di template esistenti"
+
+#: application/forms/SyncPropertyForm.php:178
+msgid "Expert mode"
+msgstr "Expert mode"
+
+#: library/Director/DataType/DataTypeDatalist.php:147
+msgid "Extend the list with new values"
+msgstr "Estendi l'elenco con nuovi valori"
+
+#: library/Director/Web/Tabs/ObjectsTabs.php:34
+msgid "External"
+msgstr "Esterno"
+
+#: application/forms/BasketForm.php:18
+msgid "External Command Definitions"
+msgstr "Definizione comandi esterni"
+
+#: library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php:19
+msgid "External Commands"
+msgstr "Comandi esterni"
+
+#: library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php:12
+msgid ""
+"External Commands have been defined in your local Icinga 2 Configuration."
+msgstr ""
+"I comandi esterni sono stati definiti nella configurazione Icinga 2 locale."
+
+#: library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php:19
+msgid "External Notification Commands"
+msgstr "Comandi di notifica esterni"
+
+#: library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php:12
+#: library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php:12
+msgid ""
+"External Notification Commands have been defined in your local Icinga 2 "
+"Configuration. "
+msgstr ""
+"I comandi di notifica esterni sono stati definiti nella configurazione Icinga2 locale."
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:68
+msgid "FQDN"
+msgstr "FQDN"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:141
+msgid "Failed"
+msgstr "Fallito"
+
+#: application/forms/IcingaImportObjectForm.php:42
+#, php-format
+msgid "Failed to import %s \"%s\""
+msgstr "Importazione di %s \"%s\" fallita"
+
+#: application/forms/IcingaObjectFieldForm.php:196
+msgid "Field has been removed"
+msgstr "Il campo è stato rimosso"
+
+#: application/forms/DirectorDatafieldForm.php:141
+#: library/Director/Web/Table/DatafieldTable.php:48
+#: library/Director/Web/Table/IcingaObjectDatafieldTable.php:50
+msgid "Field name"
+msgstr "Nome del campo"
+
+#: application/forms/DirectorDatafieldForm.php:177
+msgid "Field type"
+msgstr "Tipo di campo"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:103
+msgid "Fields"
+msgstr "Campi"
+
+#: library/Director/Web/Table/ConfigFileDiffTable.php:82
+#: library/Director/Web/Table/GeneratedConfigFileTable.php:84
+msgid "File"
+msgstr "File"
+
+#: application/forms/SyncRuleForm.php:79
+#: application/forms/SyncPropertyForm.php:102
+msgid "Filter Expression"
+msgstr "Espressione filtro"
+
+#: configuration.php:52
+msgid "Filter available notification apply rules"
+msgstr "Filtra le notifiche disponibili per applicare le regole"
+
+#: configuration.php:45
+msgid "Filter available service apply rules"
+msgstr "Filtra i servizi disponibili per applicare le regole"
+
+#: configuration.php:59
+msgid ""
+"Filter available service set templates. Use asterisks (*) as wildcards, like "
+"in DB* or *net*"
+msgstr ""
+"Filtra i template di set di servizi disponibili. Usa gli asterischi (*) come "
+"caratteri wildcards, come in DB* o *net*"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:29
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:27
+msgid "Filter method"
+msgstr "Metodo di filtro"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:33
+msgid "First Element"
+msgstr "Primo elemento"
+
+#: application/forms/IcingaNotificationForm.php:197
+msgid "First notification delay"
+msgstr "Primo ritardo di notifica"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:43
+msgid "Fixed"
+msgstr "Fissato"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:33
+msgid "Flapping"
+msgstr "Flapping"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:35
+msgid "Flapping ends"
+msgstr "Flapping terminato"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1438
+msgid ""
+"Flapping lower bound in percent for a service to be considered not flapping"
+msgstr ""
+"Flapping limite inferiore in percentuale per un servizio da considerare non flapping"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:34
+msgid "Flapping starts"
+msgstr "Flapping iniziato"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1425
+msgid "Flapping threshold (high)"
+msgstr "Flapping soglia (alta)"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1436
+msgid "Flapping threshold (low)"
+msgstr "Flapping soglia (bassa)"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1427
+msgid "Flapping upper bound in percent for a service to be considered flapping"
+msgstr ""
+"Flapping limite superiore in percentuale per un servizio da considerare flapping"
+
+#: application/forms/IcingaCloneObjectForm.php:33
+msgid "Flatten all inherited properties, strip imports"
+msgstr "Appiattire tutte le proprietà ereditate, rimuovere le importazioni"
+
+#: application/forms/SelfServiceSettingsForm.php:87
+msgid "Flush API directory"
+msgstr "Svuota directory API"
+
+#: library/Director/Web/SelfService.php:223
+msgid "For manual configuration"
+msgstr "Per la configurazione manuale"
+
+#: library/Director/Job/ConfigJob.php:181
+msgid "Force rendering"
+msgstr "Forza il rendering"
+
+#: library/Director/Objects/DirectorDatafield.php:160
+#, php-format
+msgid "Form element could not be created, %s is missing"
+msgstr "Impossibile creare l'elemento del modulo,%s mancante"
+
+#: library/Director/Web/Form/QuickForm.php:518
+#: library/Director/Web/Form/QuickForm.php:545
+msgid "Form has successfully been sent"
+msgstr "Il modulo è stato inviato con successo"
+
+#: application/forms/IcingaHostVarForm.php:32
+#: application/forms/IcingaServiceVarForm.php:32
+msgid "Format"
+msgstr "Formato"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:388
+msgid "Former object"
+msgstr "Oggetto precedente"
+
+#: application/forms/SelfServiceSettingsForm.php:24
+msgid "Fully qualified domain name (FQDN)"
+msgstr "Nome di dominio completo (FQDN)"
+
+#: application/forms/IcingaGenerateApiKeyForm.php:24
+msgid "Generate Self Service API key"
+msgstr "Genera Self Service API key"
+
+#: library/Director/Web/SelfService.php:121
+msgid "Generate a new key"
+msgstr "Genera una nuova chiave"
+
+#: application/controllers/ConfigController.php:262
+msgid "Generated config"
+msgstr "Config generata"
+
+#: application/controllers/HostController.php:149
+#: application/controllers/HostController.php:230
+msgid "Generated from host vars"
+msgstr "Generato da host vars"
+
+#: library/Director/Dashboard/AlertsDashboard.php:17
+msgid "Get alerts when something goes wrong"
+msgstr "Ricevi avvisi quando qualcosa va storto"
+
+#: library/Director/Dashboard/Dashlet/CustomvarDashlet.php:17
+msgid "Get an overview of used CustomVars and their variants"
+msgstr "Ottieni una panoramica dei CustomVar usati e delle loro varianti"
+
+#: application/controllers/ConfigController.php:222
+msgid "Global Director Settings"
+msgstr "Impostazioni Director globale"
+
+#: library/Director/Web/SelfService.php:97
+msgid "Global Self Service Setting"
+msgstr "Impostazione Self Service globale"
+
+#: application/forms/SelfServiceSettingsForm.php:44
+msgid "Global Zones"
+msgstr "Zone globali"
+
+#: application/forms/IcingaZoneForm.php:22
+msgid "Global zone"
+msgstr "Zona globale"
+
+#: library/Director/PropertyModifier/PropertyModifierJoin.php:13
+msgid "Glue"
+msgstr "Link"
+
+#: library/Director/Web/ActionBar/DirectorBaseActionBar.php:40
+#, php-format
+msgid "Go back to \"%s\" Dashboard"
+msgstr "Torna a \"%s\" Dashboard"
+
+#: library/Director/Job/ConfigJob.php:207
+msgid "Grace period"
+msgstr "Periodo di grazia"
+
+#: library/Director/Web/Table/GroupMemberTable.php:59
+msgid "Group"
+msgstr "Gruppo"
+
+#: application/forms/IcingaHostForm.php:233
+msgid ""
+"Group has been inherited, but will be overridden by locally assigned group(s)"
+msgstr ""
+"Il gruppo è stato ereditato, ma verrà sostituito dai gruppi assegnati localmente"
+
+#: application/forms/SyncPropertyForm.php:317
+msgid "Group membership"
+msgstr "Membro del gruppo"
+
+#: library/Director/Web/Controller/ObjectController.php:264
+#, php-format
+msgid "Group membership: %s"
+msgstr "Membro del gruppo: %s"
+
+#: library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php:17
+msgid ""
+"Grouping your Services into Sets allow you to quickly assign services often "
+"used together in a single operation all at once"
+msgstr ""
+"Raggruppare i tuoi servizi in set ti consente di assegnare rapidamente i servizi "
+"spesso utilizzati contemporaneamente con un'unica operazione "
+
+
+#: application/forms/IcingaUserForm.php:109
+#: application/forms/IcingaHostForm.php:203
+#: application/forms/IcingaServiceForm.php:635
+#: library/Director/Web/Tabs/ObjectsTabs.php:61
+msgid "Groups"
+msgstr "Gruppi"
+
+#: library/Director/Import/ImportSourceRestApi.php:78
+msgid "HTTP (this is plaintext!)"
+msgstr "HTTP (questo è in chiaro)"
+
+#: library/Director/Import/ImportSourceRestApi.php:173
+msgid "HTTP proxy"
+msgstr "HTTP proxy"
+
+#: library/Director/Import/ImportSourceRestApi.php:77
+msgid "HTTPS (strongly recommended)"
+msgstr "HTTPS (fortemente raccomandato)"
+
+#: library/Director/Web/Tabs/MainTabs.php:31
+msgid "Health"
+msgstr "Health"
+
+#: library/Director/Dashboard/Dashlet/SingleServicesDashlet.php:17
+msgid "Here you can find all single services directly attached to single hosts"
+msgstr ""
+"Qui puoi trovare tutti i singoli servizi direttamente associati ai singoli host "
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:83
+#: library/Director/DataType/DataTypeString.php:27
+msgid "Hidden"
+msgstr "Nascosto"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:70
+msgid "Hide SQL"
+msgstr "Nascondi SQL"
+
+#: application/controllers/HealthController.php:23
+msgid "Hint: Check Plugin"
+msgstr "Suggerimento: Check-Plugin"
+
+#: application/forms/IcingaServiceForm.php:154
+msgid "Hints regarding this service"
+msgstr "Suggerimenti riguardanti questo servizio"
+
+#: library/Director/Web/Tabs/ImportsourceTabs.php:45
+#: library/Director/Web/Tabs/ObjectTabs.php:95
+#: library/Director/Web/Tabs/SyncRuleTabs.php:43
+msgid "History"
+msgstr "History"
+
+#: application/controllers/ServiceController.php:53
+#: application/forms/SyncRuleForm.php:12
+#: application/forms/IcingaHostVarForm.php:15
+#: application/forms/IcingaServiceForm.php:583
+#: library/Director/TranslationDummy.php:13
+#: library/Director/Web/Table/ObjectsTableEndpoint.php:20
+msgid "Host"
+msgstr "Host"
+
+#: application/controllers/SuggestController.php:259
+#: library/Director/Objects/IcingaService.php:740
+msgid "Host Custom variables"
+msgstr "Host variabili personalizzate"
+
+#: application/forms/SyncRuleForm.php:13
+#: application/forms/BasketForm.php:20
+msgid "Host Group"
+msgstr "Gruppo Host"
+
+#: library/Director/Dashboard/Dashlet/HostGroupsDashlet.php:11
+msgid "Host Groups"
+msgstr "Gruppi di Host"
+
+#: application/forms/SelfServiceSettingsForm.php:19
+msgid "Host Name"
+msgstr "Nome Host"
+
+#: application/forms/IcingaHostForm.php:157
+#: application/forms/IcingaHostForm.php:174
+msgid "Host Template"
+msgstr "Host Template"
+
+#: application/forms/BasketForm.php:21
+msgid "Host Template Choice"
+msgstr "Scelta del template Host"
+
+#: application/forms/BasketForm.php:22
+#: library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php:11
+msgid "Host Templates"
+msgstr "Host Templates"
+
+#: application/forms/IcingaHostSelfServiceForm.php:35
+#: application/forms/IcingaHostForm.php:308
+msgid "Host address"
+msgstr "Indirizzo Host"
+
+#: application/forms/IcingaHostSelfServiceForm.php:37
+#: application/forms/IcingaHostForm.php:310
+msgid ""
+"Host address. Usually an IPv4 address, but may be any kind of address your "
+"check plugin is able to deal with"
+msgstr ""
+"Indirizzo Host. Di solito un indirizzo IPv4, ma può essere qualsiasi tipo di "
+"indirizzo con cui il plug-in riesce a lavorare"
+
+#: configuration.php:64
+msgid "Host configs"
+msgstr "Configurazioni Host"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:55
+msgid "Host groups"
+msgstr "Gruppi Host"
+
+#: application/forms/IcingaHostSelfServiceForm.php:25
+msgid "Host name"
+msgstr "Nome Host"
+
+#: application/forms/SelfServiceSettingsForm.php:25
+msgid "Host name (local part, without domain)"
+msgstr "Nome Host (parte locale, senza dominio)"
+
+#: library/Director/Dashboard/Dashlet/HostObjectDashlet.php:13
+msgid "Host objects"
+msgstr "Oggetti Hosts"
+
+#: application/controllers/SuggestController.php:249
+#: application/controllers/SuggestController.php:258
+#: library/Director/Objects/IcingaHost.php:156
+#: library/Director/Objects/IcingaService.php:739
+msgid "Host properties"
+msgstr "Proprietà dell'Host"
+
+#: application/controllers/TemplatechoiceController.php:17
+msgid "Host template choice"
+msgstr "Scelta del template Host"
+
+#: application/controllers/TemplatechoicesController.php:19
+msgid "Host template choices"
+msgstr "Scelta del template Host"
+
+#: application/forms/IcingaHostGroupForm.php:14
+msgid "Hostgroup"
+msgstr "Gruppo Host"
+
+#: library/Director/Import/ImportSourceCoreApi.php:61
+msgid "Hostgroups"
+msgstr "Gruppi di Host"
+
+#: application/forms/IcingaHostForm.php:206
+msgid ""
+"Hostgroups that should be directly assigned to this node. Hostgroups can be "
+"useful for various reasons. You might assign service checks based on "
+"assigned hostgroup. They are also often used as an instrument to enforce "
+"restricted views in Icinga Web 2. Hostgroups can be directly assigned to "
+"single hosts or to host templates. You might also want to consider assigning "
+"hostgroups using apply rules"
+msgstr ""
+"Hostgroup che devono essere assegnati direttamente a questo nodo. Gli hostgroup "
+"possono essere utili per vari motivi. È possibile assegnare service checks basati "
+"su hostgroup. Inoltre, vengono spesso utilizzati come strumento per imporre "
+"visualizzazioni limitate in Icinga Web 2. Gli hostgroup possono essere direttamente assegnati "
+"a singoli host o a template host. Potresti anche prendere in considerazione l'assegnazione "
+"di hostgroup utilizzando le regole apply"
+
+#: application/forms/IcingaHostForm.php:39
+#: library/Director/Web/Table/IcingaServiceSetHostTable.php:38
+msgid "Hostname"
+msgstr "Nome Host"
+
+#: library/Director/Import/ImportSourceRestApi.php:185
+msgid "Hostname, IP or <host>:<port>"
+msgstr "Nome Host, IP o <host>:<port>"
+
+#: configuration.php:118
+#: application/forms/IcingaScheduledDowntimeForm.php:85
+#: application/forms/IcingaDependencyForm.php:100
+#: application/forms/IcingaNotificationForm.php:89
+#: application/forms/IcingaServiceForm.php:693
+#: library/Director/Dashboard/Dashlet/HostsDashlet.php:11
+#: library/Director/Web/Table/CustomvarVariantsTable.php:58
+#: library/Director/Web/Table/CustomvarTable.php:43
+#: library/Director/Import/ImportSourceCoreApi.php:60
+#: library/Director/DataType/DataTypeDirectorObject.php:54
+#: library/Director/IcingaConfig/StateFilterSet.php:19
+msgid "Hosts"
+msgstr "Hosts"
+
+#: application/controllers/ServicesetController.php:81
+#, php-format
+msgid "Hosts using this set: %s"
+msgstr "Hosts che usano questo Set: %s"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:54
+msgid ""
+"How long the downtime lasts. Only has an effect for flexible (non-fixed) "
+"downtimes. Time in seconds, supported suffixes include ms (milliseconds), s "
+"(seconds), m (minutes), h (hours) and d (days). To express \"90 minutes\" "
+"you might want to write 1h 30m"
+msgstr ""
+"Quanto dura il tempo di downtime. Ha effetto solo per flexible downtime (non fissi)."
+"Tempo in secondi, i suffissi supportati includono ms (millisecondi), s (secondi), "
+"m (minuti), h (ore) e d (giorni ). Per esprimere \"90 minuti\" potresti voler scrivere 1h 30m"
+
+#: application/forms/DeployFormsBug7530.php:110
+msgid "I know what I'm doing, deploy anyway"
+msgstr "So cosa sto facendo, distribuire comunque"
+
+#: application/forms/DeployFormsBug7530.php:111
+msgid "I know, please don't bother me again"
+msgstr "Lo so, per favore non disturbarmi di nuovo"
+
+#: application/forms/IcingaEndpointForm.php:32
+msgid "IP address / hostname of remote node"
+msgstr "Indirizzo IP / nome Host del nodo remoto"
+
+#: application/forms/KickstartForm.php:129
+msgid ""
+"IP address / hostname of your Icinga node. Please note that this information "
+"will only be used for the very first connection to your Icinga instance. The "
+"Director then relies on a correctly configured Endpoint object. Correctly "
+"configures means that either it's name is resolvable or that it's host "
+"property contains either an IP address or a resolvable host name. Your "
+"Director must be able to reach this endpoint"
+msgstr ""
+"Indirizzo IP / nome host del nodo Icinga. Tenere presente che queste informazioni "
+"verranno utilizzate solo per la prima connessione all'istanza Icinga. Il Director "
+"si basa quindi su un oggetto Endpoint configurato correttamente. Configurazione "
+"corretta significa che il nome è risolvibile o che la sua proprietà Host contiene "
+"un indirizzo IP o un nome Host risolvibile. Il tuo Director deve essere in grado "
+"di raggiungere questo endpoint"
+
+#: application/forms/IcingaHostSelfServiceForm.php:43
+#: application/forms/IcingaHostForm.php:316
+msgid "IPv6 address"
+msgstr "Indirizzo IPv6"
+
+#: library/Director/Web/Controller/ObjectsController.php:330
+#, php-format
+msgid "Icinga %s Sets"
+msgstr "Icinga %s Sets"
+
+#: application/controllers/InspectController.php:39
+#, php-format
+msgid "Icinga 2 - Objects: %s"
+msgstr "Oggetti Icinga 2: %s"
+
+#: application/controllers/InspectController.php:152
+msgid "Icinga 2 API - Status"
+msgstr "Icinga 2 API - Stato"
+
+#: library/Director/Web/SelfService.php:192
+msgid "Icinga 2 Client documentation"
+msgstr "Icinga 2 documentazione client"
+
+#: application/forms/IcingaHostForm.php:135
+#: application/forms/IcingaServiceForm.php:683
+msgid "Icinga Agent and zone settings"
+msgstr "Icinga Agent e proprietá della zona"
+
+#: library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php:13
+msgid "Icinga Api users"
+msgstr "Utene Icinga API"
+
+#: application/forms/IcingaCommandArgumentForm.php:39
+#: application/forms/IcingaCommandArgumentForm.php:78
+msgid "Icinga DSL"
+msgstr "Icinga DSL"
+
+#: configuration.php:83
+msgid "Icinga Director"
+msgstr "Icinga Director"
+
+#: application/controllers/DashboardController.php:39
+msgid "Icinga Director - Main Dashboard"
+msgstr "Icinga Director - Dashboard principale"
+
+#: application/controllers/DaemonController.php:45
+msgid "Icinga Director Background Daemon"
+msgstr "Icinga Director Background Daemon"
+
+#: library/Director/Dashboard/DirectorDashboard.php:15
+msgid "Icinga Director Configuration"
+msgstr "Configurzione Icinga Director"
+
+#: application/forms/SettingsForm.php:35
+msgid ""
+"Icinga Director decides to deploy objects like CheckCommands to a global "
+"zone. This defaults to \"director-global\" but might be adjusted to a custom "
+"Zone name"
+msgstr ""
+"Icinga Director decide di distribuire oggetti come CheckCommands in una zona globale. "
+"L'impostazione predefinita è \"director-global\" ma potrebbe essere adattata a un nome "
+"di zona personalizzato"
+
+#: application/controllers/PhperrorController.php:57
+msgid ""
+"Icinga Director depends on the following modules, please install/upgrade as "
+"required"
+msgstr ""
+"Icinga Director dipende dai seguenti moduli, si prega di installare / aggiornare"
+"come richiesto"
+
+#: library/Director/Dashboard/Dashlet/SelfServiceDashlet.php:17
+msgid ""
+"Icinga Director offers a Self Service API, allowing new Icinga nodes to "
+"register themselves"
+msgstr ""
+"Icinga Director offre un'API Self Service che consente ai nuovi nodi Icinga "
+"di registrarsi"
+
+#: application/forms/KickstartForm.php:127
+msgid "Icinga Host"
+msgstr "Icinga Host"
+
+#: library/Director/Dashboard/Dashlet/InfrastructureDashlet.php:11
+msgid "Icinga Infrastructure"
+msgstr "Icinga Infrastruttura"
+
+#: application/forms/SettingsForm.php:43
+msgid "Icinga Package Name"
+msgstr "Icinga Package Name"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1139
+msgid ""
+"Icinga cluster zone. Allows to manually override Directors decisions of "
+"where to deploy your config to. You should consider not doing so unless you "
+"gained deep understanding of how an Icinga Cluster stack works"
+msgstr ""
+"Zona cluster Icinga. Consente di sovrascrivere manualmente le decisioni del "
+"Director su dove distribuire la configurazione. Si dovrebbe considerare di non "
+"farlo a meno che non si sia acquisita una profonda conoscenza di come funziona "
+"lo stack Cluster Icinga"
+
+#: application/forms/IcingaHostGroupForm.php:16
+msgid "Icinga object name for this host group"
+msgstr "Nome oggetto Icinga per questo gruppo Host"
+
+#: application/forms/IcingaHostForm.php:46
+msgid ""
+"Icinga object name for this host. This is usually a fully qualified host "
+"name but it could basically be any kind of string. To make things easier for "
+"your users we strongly suggest to use meaningful names for templates. E.g. "
+"\"generic-host\" is ugly, \"Standard Linux Server\" is easier to understand"
+msgstr ""
+"Nome dell'oggetto Icinga per questo Host. Di norma è un fully qualified host "
+"completo ma potrebbe essere praticamente qualsiasi tipo di stringa. Per rendere "
+"le cose più comprensibili per i tuoi utenti ti consigliamo vivamente di usare nomi "
+"significativi per i template. Ad esempio \"generic-host\" è brutto, "
+"\"Standard Linux Server\" è più facile da capire"
+
+#: application/forms/IcingaServiceGroupForm.php:16
+msgid "Icinga object name for this service group"
+msgstr "Nome oggetto Icinga per questo gruppo di servizi"
+
+#: application/forms/IcingaUserGroupForm.php:19
+msgid "Icinga object name for this user group"
+msgstr "Nome oggetto Icinga per questo gruppo di utenti"
+
+#: application/forms/SettingsForm.php:114
+msgid "Icinga v1.x"
+msgstr "Icinga v1.x"
+
+#: application/forms/SettingsForm.php:100
+msgid ""
+"Icinga v2.11.0 breaks some configurations, the Director will warn you before "
+"every deployment in case your config is affected. This setting allows to "
+"hide this warning."
+msgstr ""
+"Icinga v2.11.0 rompe alcune configurazioni, il Director ti avviserà prima di ogni "
+"distribuzione nel caso in cui la tua configurazione ne sia colpita. Questa "
+"impostazione consente di soprimere questo avviso."
+
+#: application/forms/SettingsForm.php:113
+msgid "Icinga v2.x"
+msgstr "Icinga v2.x"
+
+#: application/forms/IcingaHostForm.php:77
+msgid "Icinga2 Agent"
+msgstr "Icinga2 Agent"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1515
+msgid "Icon image"
+msgstr "Immagine Icon"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1524
+msgid "Icon image alt"
+msgstr "Immagine Icon alternativa"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:76
+msgid "Id"
+msgstr "Id"
+
+#: application/forms/IcingaCommandForm.php:53
+msgid "Identifier for the Icinga command you are going to create"
+msgstr "Identificatore del comando Icinga che stai per creare"
+
+#: application/forms/IcingaCommandForm.php:79
+msgid "If enabled you can not define arguments."
+msgstr "Se abilitato non è possibile definire argomenti."
+
+#: application/forms/SyncRuleForm.php:63
+#: application/forms/BasketForm.php:51
+msgid "Ignore"
+msgstr "Ignorare"
+
+#: application/forms/SettingsForm.php:91
+msgid "Ignore Bug #7530"
+msgstr "Ignora Bug #7530"
+
+#: application/forms/IcingaDependencyForm.php:175
+msgid "Ignore Soft States"
+msgstr "Ignora Soft States"
+
+#: library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php:33
+msgid "Import Source"
+msgstr "Sorgente di importazione"
+
+#: application/forms/BasketForm.php:31
+msgid "Import Sources"
+msgstr "Sorgente di importazione"
+
+#: library/Director/Dashboard/Dashlet/ImportSourceDashlet.php:14
+msgid "Import data sources"
+msgstr "Sorgente Import per i dati"
+
+#: application/forms/IcingaImportObjectForm.php:26
+#, php-format
+msgid "Import external \"%s\""
+msgstr "Sorgente import esterno \"%s\""
+
+#: application/controllers/ImportrunController.php:14
+#: application/controllers/ImportrunController.php:15
+msgid "Import run"
+msgstr "Esegui importazione"
+
+#: application/controllers/ImportsourceController.php:184
+#, php-format
+msgid "Import run history: %s"
+msgstr "Cronologia esecuzioni importanzioni: %s"
+
+#: application/controllers/ImportsourcesController.php:46
+#: library/Director/Web/Tabs/ImportTabs.php:20
+#: library/Director/Web/Tabs/ImportsourceTabs.php:37
+#: library/Director/Job/ImportJob.php:80
+msgid "Import source"
+msgstr "Sorgente importazione"
+
+#: application/forms/ImportSourceForm.php:15
+msgid "Import source name"
+msgstr "Nome della sorgente di importazione"
+
+#: application/controllers/ImportsourceController.php:139
+#, php-format
+msgid "Import source preview: %s"
+msgstr "Anteprima della sorgente di importazione: %s"
+
+#: application/controllers/ImportsourceController.php:82
+#: application/controllers/ImportsourceController.php:110
+#, php-format
+msgid "Import source: %s"
+msgstr "Sorgente di importazione: %s"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1229
+msgid ""
+"Importable templates, add as many as you want. Please note that order "
+"matters when importing properties from multiple templates: last one wins"
+msgstr ""
+"Template importabili, aggiungine quanti ne desideri. Tieni presente che "
+"l'ordine é importante quando importi proprietà da più template: l'ultimo vince"
+
+#: application/forms/ImportRunForm.php:33
+msgid "Imported new data from this Import Source"
+msgstr "Nuovi dati importati da questa sorgente di importazione"
+
+#: library/Director/Web/Table/ImportrunTable.php:32
+msgid "Imported rows"
+msgstr "Righe importate"
+
+#: application/forms/IcingaImportObjectForm.php:16
+msgid ""
+"Importing an object means that its type will change from \"external\" to "
+"\"object\". That way it will make part of the next deployment. So in case "
+"you imported this object from your Icinga node make sure to remove it from "
+"your local configuration before issueing the next deployment. In case of a "
+"conflict nothing bad will happen, just your config won't deploy."
+msgstr ""
+"L'importazione di un oggetto significa che il suo tipo cambierà da \"esterno\" a "
+"\"oggetto\". In questo modo farà parte della distribuzione successiva. Quindi, "
+"nel caso in cui tu abbia importato questo oggetto dal tuo nodo Icinga assicurati "
+"di rimuoverlo dalla configurazione locale prima di eseguire la distribuzione "
+"successiva. In caso di conflitto non accadrà nulla di grave, solo la configurazione "
+"non verrà distribuita."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1227
+msgid "Imports"
+msgstr "Imports"
+
+#: application/controllers/SelfServiceController.php:104
+msgid ""
+"In case an Icinga Admin provided you with a self service API token, this is "
+"where you can register new hosts"
+msgstr ""
+"Nel caso in cui un amministratore Icinga ti fornisse un token API self-service, "
+"è qui che puoi registrare nuovi host"
+
+#: application/forms/SelfServiceSettingsForm.php:176
+msgid ""
+"In case the Icinga 2 Agent is already installed on the system, this "
+"parameter will allow you to configure if you wish to upgrade / downgrade to "
+"a specified version with the as well."
+msgstr ""
+"Nel caso in cui Icinga 2 Agent sia già installato sul sistema, questo parametro "
+"ti consentirà di configurare se desideri aggiornare / eseguire il downgrade anche "
+"a una versione specificata."
+
+#: application/forms/SelfServiceSettingsForm.php:143
+msgid ""
+"In case the Icinga 2 Agent should be automatically installed, this has to be "
+"a string value like: 2.6.3"
+msgstr ""
+"Nel caso in cui Icinga 2 Agent venga installato automaticamente, questo deve "
+"essere un valore stringa come: 2.6.3"
+
+#: application/forms/SelfServiceSettingsForm.php:89
+msgid ""
+"In case the Icinga Agent will accept configuration from the parent Icinga 2 "
+"system, it will possibly write data to /var/lib/icinga2/api/*. By setting "
+"this parameter to true, all content inside the api directory will be flushed "
+"before an eventual restart of the Icinga 2 Agent"
+msgstr ""
+"Nel caso in cui Icinga Agent accetti la configurazione dal sistema Icinga 2 padre,"
+"probabilmente scriverà i dati su /var/lib/icinga2/api/*. Impostando questo "
+"parametro su true, tutto il contenuto all'interno della directory api verrà pulita"
+"prima di un eventuale riavvio dell'agente Icinga 2"
+
+#: library/Director/Import/ImportSourceRestApi.php:169
+msgid ""
+"In case your API is only reachable through a proxy, please choose it's "
+"protocol right here"
+msgstr ""
+"Nel caso in cui la tua API sia raggiungibile solo attraverso un proxy, "
+"scegli il suo protocollo proprio qui"
+
+#: library/Director/Import/ImportSourceRestApi.php:193
+msgid "In case your proxy requires authentication, please configure this here"
+msgstr ""
+"Nel caso in cui il proxy richieda l'autenticazione, puoi configurarlo qui"
+
+#: application/forms/IcingaTimePeriodForm.php:62
+msgid "Include other time periods into this."
+msgstr "Includere altri periodi di tempo in questo."
+
+#: application/forms/IcingaTimePeriodForm.php:59
+msgid "Include period"
+msgstr "Includi periodo"
+
+#: library/Director/Web/Table/TemplateUsageTable.php:56
+msgid "Indirect"
+msgstr "Indiretto"
+
+#: application/controllers/HostController.php:140
+#: application/controllers/HostController.php:218
+msgid "Individual Service objects"
+msgstr "Oggetti Service individuali"
+
+#: library/Director/Web/Tabs/InfraTabs.php:43
+msgid "Infrastructure"
+msgstr "Infrastruttura"
+
+#: application/forms/SyncPropertyForm.php:311
+msgid "Inheritance (import)"
+msgstr "Ereditá (Import)"
+
+#: application/forms/IcingaHostForm.php:240
+msgid "Inherited groups"
+msgstr "Gruppi ereditati"
+
+#: application/controllers/HostController.php:387
+#, php-format
+msgid "Inherited service: %s"
+msgstr "Service eriditati: %s"
+
+#: application/controllers/HostController.php:538
+#: library/Director/Web/Tabs/ObjectTabs.php:132
+msgid "Inspect"
+msgstr "Ispezionare"
+
+#: application/controllers/InspectController.php:65
+msgid "Inspect - object list"
+msgstr "Ispeziona - elenco oggetti"
+
+#: application/forms/SelfServiceSettingsForm.php:189
+msgid "Install NSClient++"
+msgstr "Install NSClient++"
+
+#: application/forms/SelfServiceSettingsForm.php:59
+msgid "Installation Source"
+msgstr "Fonte di installazione"
+
+#: application/views/scripts/phperror/dependencies.phtml:18
+msgid "Installed"
+msgstr "Installato"
+
+#: application/forms/SelfServiceSettingsForm.php:152
+msgid "Installer Hashes"
+msgstr "Hash di installazione"
+
+#: application/forms/IcingaCommandForm.php:25
+msgid "Internal commands"
+msgstr "Comandi interni"
+
+#: application/controllers/SyncruleController.php:106
+#, php-format
+msgid "It has been renamed since then, its former name was %s"
+msgstr "Da allora è stato rinominato, il suo nome precedente era %s"
+
+#: library/Director/Web/SelfService.php:68
+msgid ""
+"It is not a good idea to do so as long as your Agent still has a valid Self "
+"Service API key!"
+msgstr ""
+"Non è una buona idea farlo finché il tuo agente ha ancora una chiave "
+"API self-service valida!"
+
+#: application/forms/IcingaTemplateChoiceForm.php:87
+msgid ""
+"It will not be allowed to choose more than this many options. Setting it to "
+"one (1) will result in a drop-down box, a higher number will turn this into "
+"a multi-selection element."
+msgstr ""
+"Non sarà permesso scegliere più opzioni di queste preimpostate. Impostandolo "
+"su uno (1) si otterrà una casella Dropdown, un numero più alto lo trasformerà "
+"in un elemento a selezione multipla."
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:37
+msgid ""
+"It's currently unknown whether we are in sync with this Import Source. You "
+"should either check for changes or trigger a new Import Run."
+msgstr ""
+"Al momento non è noto se siamo sincronizzati con questa sorgente di importazione. "
+"È necessario verificare la presenza di modifiche o eseguire una nuova importazione."
+
+#: application/controllers/SyncruleController.php:66
+msgid ""
+"It's currently unknown whether we are in sync with this rule. You should "
+"either check for changes or trigger a new Sync Run."
+msgstr ""
+"Al momento non è noto se siamo sincronizzati con questa regola. È necessario "
+"verificare la presenza di modifiche o eseguire una nuova sincronizzazione."
+
+#: application/forms/BasketUploadForm.php:130
+#: application/forms/BasketForm.php:122
+msgid "It's not allowed to store an empty basket"
+msgstr "Non è consentito conservare un Basket vuoto"
+
+#: application/controllers/JobController.php:98
+msgid "Job"
+msgstr "Job"
+
+#: application/forms/BasketForm.php:33
+msgid "Job Definitions"
+msgstr "Definizione Job"
+
+#: application/forms/DirectorJobForm.php:17
+msgid "Job Type"
+msgstr "Tipologia Job"
+
+#: application/forms/DirectorJobForm.php:72
+#: library/Director/Web/Table/JobTable.php:60
+msgid "Job name"
+msgstr "Nome Job"
+
+#: application/controllers/JobController.php:23
+#: application/controllers/JobController.php:55
+#, php-format
+msgid "Job: %s"
+msgstr "Job: %s"
+
+#: application/controllers/JobsController.php:13
+#: library/Director/Dashboard/Dashlet/JobDashlet.php:14
+#: library/Director/Web/Tabs/ImportTabs.php:26
+msgid "Jobs"
+msgstr "Jobs"
+
+#: library/Director/Web/Table/ActivityLogTable.php:81
+msgid "Jump to this object"
+msgstr "Vai a questo oggetto"
+
+#: library/Director/Web/SelfService.php:245
+msgid "Just download and run this script on your Linux Client Machine:"
+msgstr ""
+"Scarica ed esegui questo script sul tuo computer client Linux:"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:57
+msgid "Keep matching elements"
+msgstr "Mantieni gli elementi corrispondenti"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:60
+msgid "Keep only matching rows (Whitelist)"
+msgstr "Mantieni solo le righe corrispondenti (Whitelist)"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:31
+msgid "Keep the DN as is"
+msgstr "Mantieni DN"
+
+#: library/Director/PropertyModifier/PropertyModifierJsonDecode.php:26
+msgid "Keep the JSON string as is"
+msgstr "Mantieni la stringa JSON così com'è"
+
+#: library/Director/PropertyModifier/PropertyModifierGetHostByName.php:18
+msgid "Keep the property (hostname) as is"
+msgstr "Mantieni la proprietà (nome Host) così com'è"
+
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:35
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:40
+msgid "Keep the property as is"
+msgstr "Mantieni la proprietà così com'è"
+
+#: application/forms/DirectorDatalistEntryForm.php:21
+#: library/Director/Web/Table/DatalistEntryTable.php:54
+msgid "Key"
+msgstr "Chiave"
+
+#: application/forms/ImportSourceForm.php:87
+msgid "Key column name"
+msgstr "Nome della colonna chiave"
+
+#: application/controllers/KickstartController.php:12
+#: application/controllers/IndexController.php:42
+msgid "Kickstart"
+msgstr "Kickstart"
+
+#: application/forms/KickstartForm.php:315
+#: library/Director/Dashboard/Dashlet/KickstartDashlet.php:11
+msgid "Kickstart Wizard"
+msgstr "Assistente Kickstart"
+
+#: library/Director/Import/ImportSourceLdap.php:47
+msgid "LDAP Search Base"
+msgstr "LDAP Search Base"
+
+#: application/forms/DirectorDatalistEntryForm.php:30
+#: library/Director/Web/Table/DatalistEntryTable.php:55
+#: library/Director/Web/Table/DatafieldTable.php:47
+#: library/Director/Web/Table/IcingaObjectDatafieldTable.php:49
+msgid "Label"
+msgstr "Etichetta"
+
+#: library/Director/Web/Widget/IcingaObjectInspection.php:58
+msgid "Last Check Result"
+msgstr "Risultato ultimo check"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:34
+msgid "Last Element"
+msgstr "Ultimo elemento"
+
+#: application/forms/IcingaNotificationForm.php:208
+msgid "Last notification"
+msgstr "Ultima notifica"
+
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:67
+msgid "Last related activity"
+msgstr "Ultima attività correlata"
+
+#: application/controllers/SyncruleController.php:102
+msgid "Last sync run details"
+msgstr "Dettagli dell'ultima sincronizzazione"
+
+#: application/forms/IcingaCommandArgumentForm.php:69
+msgid ""
+"Leave empty for non-positional arguments. Can be a positive or negative "
+"number and influences argument ordering"
+msgstr ""
+"Lascia vuoto per argomenti non posizionali. Può essere un numero positivo "
+"o negativo e influenza l'ordine degli argomenti"
+
+#: application/forms/DirectorDatafieldForm.php:44
+msgid ""
+"Leaving custom variables in place while removing the related field is "
+"perfectly legal and might be a desired operation. This way you can no longer "
+"modify related custom variables in the Director GUI, but the variables "
+"themselves will stay there and continue to be deployed. When you re-add a "
+"field for the same variable later on, everything will continue to work as "
+"before"
+msgstr ""
+"Lasciare le variabili personalizzate mentre si rimuove il campo correlato è "
+"un'operazione perfettamente valida e potrebbe essere quella desiderata. "
+"In questo modo non è più possibile modificare le variabili personalizzate correlate "
+"nella GUI del Director, ma le variabili stesse rimarranno lì e continueranno a "
+"essere distribuite. Quando riaggiungi nuovamente un campo per la stessa "
+"variabile, tutto continuerà a funzionare come prima"
+
+#: application/forms/DirectorDatafieldForm.php:87
+msgid ""
+"Leaving custom variables in place while renaming the related field is "
+"perfectly legal and might be a desired operation. This way you can no longer "
+"modify related custom variables in the Director GUI, but the variables "
+"themselves will stay there and continue to be deployed. When you re-add a "
+"field for the same variable later on, everything will continue to work as "
+"before"
+msgstr ""
+"Lasciare le variabili personalizzate in atto durante la ridenominazione del campo "
+"correlato è un'operazione perfettamente valida e potrebbe essere quella desiderata. "
+"In questo modo non è più possibile modificare le variabili personalizzate correlate "
+"nella GUI del Director, ma le variabili stesse rimarranno lì e continueranno a essere "
+"distribuite. Quando riaggiungi nuovamente un campo per la stessa variabile, tutto "
+"continuerà a funzionare come prima"
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:35
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:45
+msgid "Let the import fail"
+msgstr "Lascia che l'importazione fallisca"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:54
+msgid "Let the whole Import Run fail"
+msgstr "Lascia che l'intera esecuzione dell'importazione fallisca"
+
+#: library/Director/PropertyModifier/PropertyModifierJsonDecode.php:27
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:36
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:32
+#: library/Director/PropertyModifier/PropertyModifierGetHostByName.php:19
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:41
+msgid "Let the whole import run fail"
+msgstr "Lascia che l'intera esecuzione dell'importazione fallisca"
+
+#: configuration.php:38
+msgid "Limit access to the given comma-separated list of hostgroups"
+msgstr ""
+"Limitare l'accesso all'elenco di hostgroups separato da virgole"
+
+#: library/Director/Web/SelfService.php:238
+msgid "Linux commandline"
+msgstr "Riga di comando di Linux"
+
+#: application/controllers/DataController.php:95
+msgid "List Entries"
+msgstr "Elenca voci"
+
+#: application/controllers/DataController.php:159
+msgid "List entries"
+msgstr "Elenca voci"
+
+#: application/forms/DirectorDatalistForm.php:13
+#: library/Director/Web/Table/DatalistTable.php:31
+msgid "List name"
+msgstr "Nome lista"
+
+#: application/forms/SettingsForm.php:155
+msgid ""
+"Local directory to deploy Icinga 1.x configuration. Must be writable by "
+"icingaweb2. (e.g. /etc/icinga/director)"
+msgstr ""
+"Directory locale per distribuire la configurazione di Icinga 1.x. "
+"Deve essere scrivibile da icingaweb2. (ad es. /etc/icinga/director)"
+
+#: application/forms/IcingaEndpointForm.php:41
+msgid "Log Duration"
+msgstr "Durata del Log"
+
+#: application/forms/IcingaAddServiceForm.php:67
+#: library/Director/Web/Form/DirectorObjectForm.php:574
+msgid "Main properties"
+msgstr "Proprietà principali"
+
+
+#: library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php:12
+msgid ""
+"Manage definitions for your Commands that should be executed as Check "
+"Plugins, Notifications or based on Events"
+msgstr ""
+"Gestisci le definizioni per i tuoi comandi che devono essere "
+"eseguiti come check plugin, notifiche o basati su eventi"
+
+#: library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php:17
+#, fuzzy
+msgid ""
+"Manage your Host Templates. Use Fields to make it easy for your users to get "
+"them customized."
+msgstr ""
+"Gestisci i tuoi Host template. Usa i campi per aggevolarne la "
+"personalizzazione ai tuoi utenti"
+
+#: library/Director/Dashboard/Dashlet/InfrastructureDashlet.php:17
+#, fuzzy
+msgid ""
+"Manage your Icinga 2 infrastructure: Masters, Zones, Satellites and more"
+msgstr ""
+"Gestisci la tua infrastruttura di Icinga2: Master, Zone, Satelliti e altro"
+
+#: library/Director/Dashboard/CommandsDashboard.php:17
+#, fuzzy
+msgid "Manage your Icinga Commands"
+msgstr "Gestisci i tuoi Comandi Icinga"
+
+#: library/Director/Dashboard/HostsDashboard.php:16
+#, fuzzy
+msgid "Manage your Icinga Hosts"
+msgstr "Gestisci i tuoi Icinga Hosts"
+
+#: library/Director/Dashboard/InfrastructureDashboard.php:18
+#, fuzzy
+msgid "Manage your Icinga Infrastructure"
+msgstr "Gestisci Infrastuttura Icinga"
+
+#: library/Director/Dashboard/ServicesDashboard.php:18
+#, fuzzy
+msgid "Manage your Icinga Service Checks"
+msgstr "Gestisci i tuoi Icinga2 Service Checks"
+
+#: library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php:17
+#, fuzzy
+msgid ""
+"Manage your Service Templates. Use Fields to make it easy for your users to "
+"get them customized."
+msgstr ""
+"Gestisci i tuoi Templates per i servizi. Usa i Campi per aggevolarne la "
+"personalizzazione ai tuoi utenti"
+
+#: application/forms/IcingaObjectFieldForm.php:142
+#: application/forms/IcingaObjectFieldForm.php:147
+#: library/Director/Web/Table/IcingaObjectDatafieldTable.php:51
+#, fuzzy
+msgid "Mandatory"
+msgstr "Obbligatorio"
+
+#: application/forms/SettingsForm.php:143
+#, fuzzy
+msgid "Master-less"
+msgstr "Senza Master"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:35
+#, fuzzy
+msgid "Match NULL value columns"
+msgstr "Match colonne valore NULL"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:34
+#, fuzzy
+msgid "Match boolean FALSE"
+msgstr "Match boolean FALSE"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:33
+#, fuzzy
+msgid "Match boolean TRUE"
+msgstr "Match boolean TRUE"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1350
+#, fuzzy
+msgid "Max check attempts"
+msgstr "Massimo numero di ripetizioni dei controlli"
+
+#: library/Director/Web/Table/GroupMemberTable.php:60
+#: library/Director/Web/Table/GroupMemberTable.php:65
+#, fuzzy
+msgid "Member"
+msgstr "Membro"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:114
+#, fuzzy
+msgid "Members"
+msgstr "Membri"
+
+#: application/forms/SyncRuleForm.php:61
+#, fuzzy
+msgid "Merge"
+msgstr "Unisci"
+
+#: application/forms/SyncPropertyForm.php:116
+#, fuzzy
+msgid "Merge Policy"
+msgstr "Regole su Unisci"
+
+#: application/forms/IcingaTimePeriodRangeForm.php:23
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:24
+#, fuzzy
+msgid ""
+"Might be monday, tuesday or 2016-01-28 - have a look at the documentation for "
+"more examples"
+msgstr ""
+"Per esempio lunedí, martedí, 2020-01-28 - consulta la documentazione per "
+"ulteriori esempi"
+
+#: application/forms/IcingaTemplateChoiceForm.php:73
+#, fuzzy
+msgid "Minimum required"
+msgstr "Minimo richiesto"
+
+#: application/forms/ImportRowModifierForm.php:69
+#, fuzzy
+msgid "Modifier"
+msgstr "Modificatore"
+
+#: library/Director/Web/Tabs/ImportsourceTabs.php:41
+#, fuzzy
+msgid "Modifiers"
+msgstr "Modificatori"
+
+#: application/controllers/ImportsourceController.php:104
+#: application/controllers/SyncruleController.php:503
+#: library/Director/Web/Controller/TemplateController.php:120
+#: library/Director/Web/ActionBar/AutomationObjectActionBar.php:38
+#: library/Director/Web/Tabs/SyncRuleTabs.php:37
+#: library/Director/ProvidedHook/Monitoring/ServiceActions.php:52
+#, fuzzy
+msgid "Modify"
+msgstr "Modifica"
+
+#: library/Director/ProvidedHook/CubeLinks.php:52
+#, fuzzy, php-format
+msgid "Modify %d hosts"
+msgstr "Modifica %d Hosts"
+
+#: library/Director/Web/Controller/ObjectsController.php:195
+#, fuzzy, php-format
+msgid "Modify %d objects"
+msgstr "Modifica %d Oggetti "
+
+#: application/controllers/DatafieldController.php:34
+#, fuzzy, php-format
+msgid "Modify %s"
+msgstr "Modifica %s"
+
+#: library/Director/ProvidedHook/CubeLinks.php:35
+#, fuzzy
+msgid "Modify a host"
+msgstr "Modifica un Host"
+
+#: application/forms/DirectorDatalistEntryForm.php:61
+#, fuzzy
+msgid "Modify data list entry"
+msgstr "Modifica l'inserimento dell'elenco dei dati"
+
+#: library/Director/Web/Table/ApplyRulesTable.php:154
+#, fuzzy
+msgid "Modify this Apply Rule"
+msgstr "Modifica questa Apply Rule"
+
+#: application/views/scripts/phperror/dependencies.phtml:16
+#, fuzzy
+msgid "Module name"
+msgstr "Nome Modulo"
+
+#: library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php:15
+#, fuzzy
+msgid "Monitored Services"
+msgstr "Servizi monitorati"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:500
+#, fuzzy
+msgid "Move down"
+msgstr "Sposta in basso"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:490
+#, fuzzy
+msgid "Move up"
+msgstr "Sposta in alto"
+
+#: library/Director/Web/Controller/ObjectsController.php:193
+#, fuzzy
+msgid "Multiple objects"
+msgstr "Oggetti multipli"
+
+#: application/controllers/ConfigController.php:162
+#, fuzzy
+msgid "My changes"
+msgstr "Miei cambiamenti"
+
+#: application/controllers/SchemaController.php:16
+#, fuzzy
+msgid "MySQL schema"
+msgstr "MySQL Schema"
+
+#: application/forms/IcingaAddServiceForm.php:146
+#: application/forms/IcingaCommandForm.php:47
+#: application/forms/IcingaApiUserForm.php:14
+#: application/forms/IcingaHostVarForm.php:22
+#: application/forms/IcingaServiceVarForm.php:22
+#: application/forms/IcingaTimePeriodForm.php:15
+#: application/forms/IcingaDependencyForm.php:74
+#: application/forms/IcingaHostForm.php:38
+#: application/forms/IcingaServiceForm.php:561
+#: library/Director/Web/Table/CoreApiObjectsTable.php:57
+#: library/Director/Web/Table/CoreApiPrototypesTable.php:40
+#: library/Director/Web/Table/ChoicesTable.php:41
+#: library/Director/Web/Table/CoreApiFieldsTable.php:74
+#, fuzzy
+msgid "Name"
+msgstr "Nome"
+
+#: application/forms/IcingaDependencyForm.php:76
+#, fuzzy
+msgid "Name for the Icinga dependency you are going to create"
+msgstr "Nome della dipenza Icinga che stai creando"
+
+#: application/forms/IcingaEndpointForm.php:20
+#, fuzzy
+msgid "Name for the Icinga endpoint template you are going to create"
+msgstr "Nome del template endpoint Icinga che stai creando"
+
+#: application/forms/IcingaEndpointForm.php:26
+#, fuzzy
+msgid "Name for the Icinga endpoint you are going to create"
+msgstr "Nome del endpoint Icinga che stai creando"
+
+#: application/forms/IcingaNotificationForm.php:21
+#, fuzzy
+msgid "Name for the Icinga notification template you are going to create"
+msgstr "Nome del template notifica Icinga che stai creando"
+
+#: application/forms/IcingaNotificationForm.php:27
+#, fuzzy
+msgid "Name for the Icinga notification you are going to create"
+msgstr "Nome della notifica Incinga che stai creando"
+
+#: application/forms/IcingaAddServiceForm.php:149
+#: application/forms/IcingaServiceForm.php:564
+#, fuzzy
+msgid "Name for the Icinga service you are going to create"
+msgstr "Nome del servizio Icinga che stai creando"
+
+#: application/forms/IcingaUserForm.php:30
+#, fuzzy
+msgid "Name for the Icinga user object you are going to create"
+msgstr "Nome dell'oggetto User che stai creando"
+
+#: application/forms/IcingaUserForm.php:24
+#, fuzzy
+msgid "Name for the Icinga user template you are going to create"
+msgstr "Nome del template User che stai creando"
+
+#: application/forms/IcingaZoneForm.php:17
+#, fuzzy
+msgid "Name for the Icinga zone you are going to create"
+msgstr "Nome della Zona Icinga che stai creando"
+
+#: application/forms/IcingaCloneObjectForm.php:116
+#, fuzzy
+msgid "Name needs to be changed when cloning a Template"
+msgstr "Il nome deve essere cambiato quando cloni un template"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:84
+#, fuzzy
+msgid "Nav"
+msgstr "Nav"
+
+#: application/controllers/DatafieldController.php:40
+#, fuzzy
+msgid "New Field"
+msgstr "Nuovo campo"
+
+#: application/controllers/JobController.php:31
+#, fuzzy
+msgid "New Job"
+msgstr "Nuovo lavoro"
+
+#: library/Director/Web/Tabs/ImportsourceTabs.php:54
+#, fuzzy
+msgid "New import source"
+msgstr "Nuova sorgente di importazione"
+
+#: application/forms/IcingaCloneObjectForm.php:22
+#: library/Director/Web/Form/CloneImportSourceForm.php:30
+#: library/Director/Web/Form/CloneSyncRuleForm.php:30
+#, fuzzy
+msgid "New name"
+msgstr "Nuovo nome"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:377
+#, fuzzy
+msgid "New object"
+msgstr "Nuovo oggetto"
+
+#: application/forms/IcingaAddServiceForm.php:35
+#: application/forms/IcingaHostForm.php:31
+#, fuzzy
+msgid "Next"
+msgstr "Avanti"
+
+#: application/forms/IcingaZoneForm.php:29
+#: application/forms/SelfServiceSettingsForm.php:227
+#: application/forms/SettingsForm.php:58 application/forms/SettingsForm.php:73
+#: application/forms/SettingsForm.php:95
+#: library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:26
+#: library/Director/Job/ImportJob.php:102 library/Director/Job/SyncJob.php:102
+#: library/Director/Job/ConfigJob.php:190
+#: library/Director/Job/ConfigJob.php:202
+#, fuzzy
+msgid "No"
+msgstr "No"
+
+#: library/Director/Util.php:183
+#, fuzzy, php-format
+msgid "No %s resource available"
+msgstr "Nessuna %s risorsa avviabile"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:99
+#, fuzzy
+msgid "No API user configured, you might run the kickstart helper"
+msgstr "Nessun utente API configurato, si potrebbe eseguire l'helper kickstart"
+
+#: application/forms/IcingaHostForm.php:163
+#, fuzzy
+msgid "No Host Template has been provided yet"
+msgstr "Non è stato ancora fornito alcun Host template"
+
+#: application/forms/IcingaHostForm.php:151
+#, fuzzy
+msgid "No Host template has been chosen"
+msgstr "Non è stato selezionato alcun Host template"
+
+#: application/forms/IcingaAddServiceForm.php:95
+#, fuzzy
+msgid "No Service Templates have been provided yet"
+msgstr "Non sono stati ancora forniti template di servizio"
+
+#: application/forms/IcingaCommandArgumentForm.php:175
+#: application/forms/IcingaServiceForm.php:741
+#: application/forms/IcingaTimePeriodRangeForm.php:94
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:99
+#: library/Director/Web/Form/DirectorObjectForm.php:660
+#, fuzzy
+msgid "No action taken, object has not been modified"
+msgstr "Nessun azione effettuata, l'oggetto non é stato modificato"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:223
+#, fuzzy
+msgid "No apply rule has been defined yet"
+msgstr "Non é stata ancora definita nessuna apply rule"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:42
+#, fuzzy
+msgid "No changes have been made"
+msgstr "Non sono state apportate modifiche"
+
+#: application/controllers/DashboardController.php:67
+#, fuzzy
+msgid "No dashboard available, you might have not enough permissions"
+msgstr "Nessuna dashboard disponibile, potreste non avere i permessi necessari"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:68
+#, fuzzy
+msgid "No database has been configured for Icinga Director"
+msgstr "Nessun database é stato configurato per Icinga Director"
+
+#: application/forms/KickstartForm.php:238
+#, fuzzy
+msgid ""
+"No database resource has been configured yet. Please choose a resource to "
+"complete your config"
+msgstr ""
+"Non è stata ancora configurata alcuna risorsa di database. Scegliere una "
+"risorsa per completare la configurazione"
+
+#: application/forms/KickstartForm.php:52
+#, fuzzy
+msgid "No database schema has been created yet"
+msgstr "Non é stato creato ancora nessun schema database"
+
+#: application/forms/AddToBasketForm.php:104
+#, fuzzy
+msgid "No object has been chosen"
+msgstr "Nessun oggetto é stato selezionato"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:241
+#, fuzzy
+msgid "No object has been defined yet"
+msgstr "Nessun oggetto é stato ancora definito"
+
+#: application/forms/IcingaMultiEditForm.php:88
+#, fuzzy
+msgid "No object has been modified"
+msgstr "Nessun oggetto e' stato modificato"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1204
+msgid "No related template has been provided yet"
+msgstr "Nessun template correlato è stato ancora fornito"
+
+#: application/forms/IcingaAddServiceForm.php:83
+msgid "No service has been chosen"
+msgstr "Nessun servizio é stato selezionato"
+
+#: application/controllers/HostController.php:121
+#, php-format
+msgid "No such service: %s"
+msgstr "Nessun servizio del genere: %s"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1199
+msgid "No template has been chosen"
+msgstr "Nessun template é stato selezionato"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:205
+msgid "No template has been defined yet"
+msgstr "Nessun template é stato ancora definito"
+
+#: application/forms/IcingaServiceForm.php:611
+msgid "None"
+msgstr "Nessuno"
+
+#: library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php:57
+msgid "None could be used for deployments right now"
+msgstr ""
+"Nessuno potrebbe essere utilizzato per le distribuzioni in questo momento"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1490
+msgid "Notes"
+msgstr "Note"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1499
+msgid "Notes URL"
+msgstr "Note URL"
+
+#: application/forms/SyncRunForm.php:39
+msgid "Nothing changed, rule is in sync"
+msgstr "Nessuna modifca, regole in sincronizzazione"
+
+#: application/forms/ImportCheckForm.php:38
+#: application/forms/ImportRunForm.php:38
+msgid ""
+"Nothing to do, data provided by this Import Source didn't change since the "
+"last import run"
+msgstr ""
+"Nulla da fare, i dati forniti da questa sorgente di importazione non sono "
+"cambiati dall'ultima esecuzione dell'importazione"
+
+#: application/forms/RestoreObjectForm.php:76
+msgid "Nothing to do, restore would not modify the current object"
+msgstr "Niente da fare, il ripristino non modificherebbe l'oggetto corrente"
+
+#: application/forms/SyncCheckForm.php:58
+msgid "Nothing would change, this rule is still in sync"
+msgstr "Nulla cambierebbe, questa regola è ancora in sincronizzazione"
+
+#: application/forms/SyncRuleForm.php:22
+#: application/forms/IcingaNotificationForm.php:25
+#: library/Director/TranslationDummy.php:18
+msgid "Notification"
+msgstr "Notifica"
+
+#: library/Director/Web/Controller/TemplateController.php:56
+#, php-format
+msgid "Notification Apply Rules based on %s"
+msgstr "Regole di apply rule per notifiche basate su %s"
+
+#: library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php:19
+#: library/Director/Import/ImportSourceCoreApi.php:58
+msgid "Notification Commands"
+msgstr "Comandi di notifica"
+
+#: library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php:12
+msgid ""
+"Notification Commands allow you to trigger any action you want when a "
+"notification takes place"
+msgstr ""
+"I comandi di notifica consentono di attivare qualsiasi azione che si "
+"desidera quando avviene una notifica"
+
+#: application/forms/IcingaNotificationForm.php:19
+msgid "Notification Template"
+msgstr "Notifica template"
+
+#: application/forms/IcingaNotificationForm.php:254
+msgid "Notification command"
+msgstr "Comandi di notifica"
+
+#: application/forms/IcingaNotificationForm.php:176
+msgid "Notification interval"
+msgstr "Interfallo di notifica"
+
+#: application/controllers/TemplatechoicesController.php:29
+msgid "Notification template choices"
+msgstr "Scelta notifica template"
+
+#: library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php:13
+msgid "Notification templates"
+msgstr "Notifica templates"
+
+#: configuration.php:130 application/forms/BasketForm.php:27
+#: library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php:13
+#: library/Director/Dashboard/Dashlet/NotificationsDashlet.php:13
+#: library/Director/Web/Table/CustomvarVariantsTable.php:61
+#: library/Director/Web/Table/CustomvarTable.php:46
+msgid "Notifications"
+msgstr "Notifiche"
+
+#: library/Director/Dashboard/NotificationsDashboard.php:20
+msgid ""
+"Notifications are sent when a host or service reaches a non-ok hard state or "
+"recovers from such. One might also want to send them for special events like "
+"when a Downtime starts, a problem gets acknowledged and much more. You can "
+"send specific notifications only within specific time periods, you can delay "
+"them and of course re-notify at specific intervals.\n"
+"\n"
+" Combine those possibilities in case you need to define escalation levels, "
+"like notifying operators first and your management later on in case the "
+"problem remains unhandled for a certain time.\n"
+"\n"
+" You might send E-Mail or SMS, make phone calls or page on various "
+"channels. You could also delegate notifications to external service "
+"providers. The possibilities are endless, as you are allowed to define as "
+"many custom notification commands as you want"
+msgstr ""
+"Le notifiche vengono inviate quando un host o un servizio raggiunge uno "
+"stato non-ok hard state o si riprende da tale stato. Si potrebbe anche "
+"volerle inviare per eventi speciali come quando inizia un periodo di "
+"inattività, quando un problema viene riconosciuto e molto altro ancora. È "
+"possibile inviare notifiche specifiche solo entro periodi di tempo "
+"specifici, è possibile ritardarle e naturalmente ri-notificare a intervalli "
+"di tempo specifici.\n"
+"\n"
+" Combinate queste possibilità nel caso in cui sia necessario definire "
+"livelli di escalation, come la notifica prima agli operatori e la vostra "
+"gestione in seguito nel caso in cui il problema rimanga non gestito per un "
+"certo periodo di tempo.\n"
+"\n"
+" Potreste inviare e-mail o SMS, fare telefonate o pagine su vari canali. "
+"Potreste anche delegare le notifiche a fornitori di servizi esterni. Le "
+"possibilità sono infinite, poiché è possibile definire tutti i comandi di "
+"notifica personalizzati che si desidera"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:42
+msgid "Numeric position"
+msgstr "Posizione numerica"
+
+#: library/Director/IcingaConfig/StateFilterSet.php:24
+msgid "OK"
+msgstr "Ok"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1089
+#: library/Director/Web/Form/DirectorObjectForm.php:1094
+#: library/Director/Web/Controller/TemplateController.php:148
+#: library/Director/DataType/DataTypeDirectorObject.php:64
+msgid "Object"
+msgstr "Oggetto"
+
+#: application/controllers/InspectController.php:106
+msgid "Object Inspection"
+msgstr "Ispezione dell'oggetto"
+
+#: application/forms/SyncRuleForm.php:45
+msgid "Object Type"
+msgstr "Tipo Oggetto"
+
+#: library/Director/Import/ImportSourceLdap.php:53
+msgid "Object class"
+msgstr "Classe dell'oggetto"
+
+#: library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php:18
+#, fuzzy
+msgid "Object dependency relationships."
+msgstr "Relazioni di dipendenza da oggetti"
+
+#: application/forms/RestoreObjectForm.php:80
+msgid "Object has been re-created"
+msgstr "L'oggetto é stato ri-creato"
+
+#: application/forms/RestoreObjectForm.php:72
+msgid "Object has been restored"
+msgstr "L'oggetto é stato ripristinato"
+
+#: application/forms/SyncPropertyForm.php:358
+msgid "Object properties"
+msgstr "Proprietá dell'oggetto"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1102
+#: library/Director/Web/Table/SyncruleTable.php:46
+msgid "Object type"
+msgstr "Tipo Oggetto"
+
+#: application/controllers/InspectController.php:67
+#, php-format
+msgid "Object type \"%s\""
+msgstr "Tipo Oggetto \"%s\""
+
+#: library/Director/Web/Table/GeneratedConfigFileTable.php:85
+msgid "Object/Tpl/Apply"
+msgstr "Oggetto/Tpl/Applica"
+
+#: library/Director/Web/Table/HostTemplateUsageTable.php:11
+#: library/Director/Web/Table/ServiceTemplateUsageTable.php:11
+#: library/Director/Web/Table/TemplateUsageTable.php:24
+#, fuzzy
+msgid "Objects"
+msgstr "Oggetti"
+
+#: library/Director/Import/ImportSourceRestApi.php:136
+msgid ""
+"Often the expected result is provided in a property like \"objects\". Please "
+"specify this if required"
+msgstr ""
+"Spesso il risultato atteso è fornito in una proprietà come \"oggetti\". Si "
+"prega di specificare questo se richiesto"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:27
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:34
+msgid "On failure"
+msgstr "In caso di fallimento"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:227
+msgid "One apply rule has been defined"
+msgstr "Una apply rule é stata definita"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:249
+msgid "One external object has been defined, it will not be deployed"
+msgstr "Un oggetto esterno é stato definite, non verrá implementato"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:252
+msgid "One object has been defined"
+msgstr "Un oggetto è stato definito"
+
+#: application/forms/IcingaMultiEditForm.php:90
+#: library/Director/Web/Widget/SyncRunDetails.php:45
+msgid "One object has been modified"
+msgstr "Un oggetto é stato modificato"
+
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:16
+msgid "One or more characters that should be used to split this string"
+msgstr "Uno o più caratteri da utilizzare per separare questa stringa"
+
+#: library/Director/PropertyModifier/PropertyModifierJoin.php:16
+msgid ""
+"One or more characters that will be used to glue an input array to a string. "
+"Can be left empty"
+msgstr ""
+"Uno o più caratteri che saranno usati per incollare un input array ad una "
+"stringa. Può essere lasciato vuoto"
+
+#: application/forms/IcingaTimePeriodRangeForm.php:30
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:31
+msgid "One or more time periods, e.g. 00:00-24:00 or 00:00-09:00,17:00-24:00"
+msgstr ""
+"Uno o più periodi di tempo, ad es. 00:00-24:00 o 00:00-09:00,17:00-24:00"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:209
+#: library/Director/Dashboard/Dashlet/Dashlet.php:246
+msgid "One template has been defined"
+msgstr "Un template é stato definito"
+
+#: application/forms/IcingaCommandArgumentForm.php:100
+msgid ""
+"Only set this parameter if the argument value resolves to a numeric value. "
+"String values are not supported"
+msgstr ""
+"Impostare questo parametro solo se il valore dell'argomento si risolve su un "
+"valore numerico. I valori delle stringhe non sono supportati"
+
+#: application/forms/IcingaObjectFieldForm.php:146
+msgid "Optional"
+msgstr "Opzionale"
+
+#: application/forms/IcingaCommandForm.php:72
+msgid ""
+"Optional command timeout. Allowed values are seconds or durations postfixed "
+"with a specific unit (e.g. 1m or also 3m 30s)."
+msgstr ""
+"Timeout comando opzionale. I valori ammessi sono i secondi o le durate "
+"fissate con una specifica unità (ad esempio 1m o anche 3m 30s)"
+
+#: application/forms/IcingaDependencyForm.php:248
+#, fuzzy
+msgid ""
+"Optional. The child service. If omitted this dependency object is treated as "
+"host dependency."
+msgstr ""
+"Opzionale. Il servizio figlio. Se omesso, questo oggetto di dipendenza viene "
+"trattato come dipendenza dell'host"
+
+#: application/forms/IcingaDependencyForm.php:218
+#, fuzzy
+msgid ""
+"Optional. The parent service. If omitted this dependency object is treated "
+"as host dependency."
+msgstr ""
+"Opzionale. Il servizio parente. Se omesso questo oggetto di dipendenza viene "
+"trattato come dipendenza dell'host"
+
+#: application/forms/IcingaObjectFieldForm.php:103
+msgid "Other available fields"
+msgstr "Altri campi disponibili"
+
+#: application/forms/SyncPropertyForm.php:273
+msgid "Other sources"
+msgstr "Altre risorse"
+
+#: application/forms/IcingaServiceForm.php:148
+#: application/forms/IcingaServiceForm.php:405
+#: application/forms/IcingaServiceForm.php:441
+msgid "Override vars"
+msgstr "Sovrascrivi variabili"
+
+#: library/Director/Web/ActionBar/AutomationObjectActionBar.php:32
+#: library/Director/Web/Tabs/MainTabs.php:26
+msgid "Overview"
+msgstr "Panoramica"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:74
+msgid "PHP Binary"
+msgstr "PHP Binario"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:77
+msgid "PHP Integer"
+msgstr "PHP Intero"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:76
+msgid "PHP Version"
+msgstr "PHP-Versione"
+
+#: application/controllers/PhperrorController.php:18
+#, php-format
+msgid ""
+"PHP version 5.4.x is required for Director >= 1.4.0, you're running %s. "
+"Please either upgrade PHP or downgrade Icinga Director"
+msgstr ""
+"La versione 5.4.x di PHP è richiesta per Director >= 1.4.0, in corso %s. Si "
+"prega di aggiornare PHP o di eseguire il downgrade di Icinga Director"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:66
+msgid "PID"
+msgstr "PID"
+
+#: application/forms/IcingaUserForm.php:41
+msgid "Pager"
+msgstr "Paginazione"
+
+#: application/forms/IcingaDependencyForm.php:200
+msgid "Parent Host"
+msgstr "Host-Parente"
+
+#: application/forms/IcingaDependencyForm.php:216
+msgid "Parent Service"
+msgstr "Sevizio-Parente"
+
+#: application/forms/IcingaZoneForm.php:36
+msgid "Parent Zone"
+msgstr "Zona-Parente"
+
+#: application/forms/IcingaApiUserForm.php:19
+#: application/forms/KickstartForm.php:159
+#: library/Director/Import/ImportSourceRestApi.php:156
+msgid "Password"
+msgstr "Password"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:13
+#: library/Director/PropertyModifier/PropertyModifierCombine.php:14
+msgid "Pattern"
+msgstr "Modello"
+
+#: application/forms/ApplyMigrationsForm.php:39
+msgid "Pending database schema migrations have successfully been applied"
+msgstr ""
+"Sono state applicate con successo le migrazioni degli schemi di database in "
+"sospeso"
+
+#: library/Director/Util.php:185
+msgid "Please ask an administrator to grant you access to resources"
+msgstr ""
+"Si prega di contattare l'amministratore per richiedere l'accesso alle risorse"
+
+#: application/forms/AddToBasketForm.php:118
+#, php-format
+msgid ""
+"Please check your Basket configuration, %s does not support single \"%s\" "
+"configuration objects"
+msgstr ""
+"Si prega di controllare la configurazione del vostro Basket, %s non supporta "
+"singoli oggetti di configurazione \"%s"
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:19
+msgid "Please choose a data list that can be used for map lookups"
+msgstr ""
+"Si prega di scegliere una lista dati che puo' essere utilizzata per la map "
+"lookups"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:66
+msgid "Please choose a specific Icinga object type"
+msgstr "Scegliere un tipo di Icinga-Oggetto specifico"
+
+#: library/Director/Job/ImportJob.php:82
+#, fuzzy
+msgid ""
+"Please choose your import source that should be executed. You could create "
+"different schedules for different sources or also opt for running all of "
+"them at once."
+msgstr ""
+"Scegliete la sorgente di importazione da eseguire. Potete creare diverse "
+"programmazioni per diverse fonti o anche scegliere di eseguirli tutti in una "
+"volta"
+
+#: library/Director/Job/SyncJob.php:82
+#, fuzzy
+msgid ""
+"Please choose your synchronization rule that should be executed. You could "
+"create different schedules for different rules or also opt for running all "
+"of them at once."
+msgstr ""
+"Si prega di scegliere la regola di sincronizzazione da eseguire. Potete "
+"creare diverse programmazioni per regole diverse o anche scegliere di "
+"eseguirle tutte in una volta"
+
+#: library/Director/Import/ImportSourceSql.php:57
+msgid "Please click \"Store\" once again to determine query columns"
+msgstr ""
+"Si prega di cliccare nuovamente \"Memorizza\" per determinare la colonna di "
+"interrogazione"
+
+#: application/forms/KickstartForm.php:257
+#, php-format
+msgid "Please click %s to create new DB resources"
+msgstr "Si prega di premere %s per creare nuove risorse DB"
+
+#: library/Director/Util.php:175
+#, php-format
+msgid "Please click %s to create new resources"
+msgstr "Si prega di premere %s per creare nuove risorse"
+
+#: application/forms/IcingaAddServiceForm.php:87
+#: application/forms/IcingaHostForm.php:155
+#, php-format
+msgid "Please define a %s first"
+msgstr "Si prega di definire prima un %s"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1202
+msgid "Please define a related template first"
+msgstr "Si prega di definire prima un template correlato"
+
+#: application/forms/KickstartForm.php:219
+msgid ""
+"Please make sure that your database exists and your user has been granted "
+"enough permissions"
+msgstr ""
+"Si prega di assicurarsi che il database esista e che il tuo utente abbia "
+"ottenuto sufficienti autorizzazioni"
+
+#: application/forms/SettingsForm.php:23
+#, fuzzy
+msgid ""
+"Please only change those settings in case you are really sure that you are "
+"required to do so. Usually the defaults chosen by the Icinga Director should "
+"make a good fit for your environment."
+msgstr ""
+"Si prega di modificare queste impostazioni solo nel caso in cui siate "
+"veramente sicuri che sia necessario farlo. Di solito le impostazioni "
+"predefinite scelte dal Director di Icinga dovrebbero essere adatte al "
+"vostro ambiente"
+
+#: application/forms/SyncRuleForm.php:31
+#, fuzzy
+msgid "Please provide a rule name"
+msgstr "Si prega di fornire il nome della regola"
+
+#: library/Director/PropertyModifier/PropertyModifierSubstring.php:17
+#: library/Director/PropertyModifier/PropertyModifierSubstring.php:27
+#, php-format
+msgid "Please see %s for detailled instructions of how start and end work"
+msgstr ""
+"Per istruzioni dettagliate su come iniziare e terminare il lavoro, vedere le "
+"%s"
+
+#: application/forms/ImportRowModifierForm.php:32
+msgid ""
+"Please start typing for a list of suggestions. Dots allow you to access "
+"nested properties: column.some.key. Such nested properties cannot be "
+"modified in-place, but you can store the modified value to a new \"target "
+"property\""
+msgstr ""
+"Si prega di iniziare a digitare per ottenere dei suggerimenti. I punti "
+"consentono di accedere alle proprietà annidate: column.some.key. Tali "
+"proprietà annidate non possono essere modificate in loco, ma è possibile "
+"memorizzare il valore modificato in una nuova \"proprietà di destinazione\"."
+
+#: application/forms/IcingaCommandForm.php:36
+msgid ""
+"Plugin Check commands are what you need when running checks agains your "
+"infrastructure. Notification commands will be used when it comes to notify "
+"your users. Event commands allow you to trigger specific actions when "
+"problems occur. Some people use them for auto-healing mechanisms, like "
+"restarting services or rebooting systems at specific thresholds"
+msgstr ""
+"I comandi Plugin Check sono ciò di cui avete bisogno quando eseguite i "
+"controlli sulla vostra infrastruttura. I comandi di notifica saranno "
+"utilizzati quando si tratta di notificare i vostri utenti. I comandi degli "
+"eventi vi permettono di attivare azioni specifiche quando si verificano "
+"problemi. Alcuni li usano per meccanismi di auto-guarigione, come il riavvio "
+"di servizi o il riavvio di sistemi a soglie specifiche"
+
+#: application/forms/IcingaCommandForm.php:20
+msgid "Plugin commands"
+msgstr "Comandi Plugin"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:50
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:52
+msgid "Policy"
+msgstr "Policy"
+
+#: application/forms/IcingaEndpointForm.php:36
+#: application/forms/KickstartForm.php:141
+msgid "Port"
+msgstr "Porta"
+
+#: application/forms/IcingaCommandArgumentForm.php:67
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:40
+msgid "Position"
+msgstr "Posizione"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:30
+msgid "Position Type"
+msgstr "Tipo posizione"
+
+#: application/controllers/SchemaController.php:17
+msgid "PostgreSQL schema"
+msgstr "PostgreSQL Schema"
+
+#: application/forms/IcingaTimePeriodForm.php:76
+msgid "Prefer includes"
+msgstr "Preferisce include"
+
+#: library/Director/Dashboard/Dashlet/BasketDashlet.php:17
+msgid "Preserve specific configuration objects in a specific state"
+msgstr ""
+"Conservare gli oggetti di configurazione specifici in uno stato specifico"
+
+#: library/Director/Web/Tabs/ImportsourceTabs.php:49
+#: library/Director/Web/Tabs/ObjectTabs.php:86
+#: library/Director/Web/Tabs/SyncRuleTabs.php:33
+msgid "Preview"
+msgstr "Anteprima"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:23
+msgid "Problem"
+msgstr "Problema"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:27
+msgid "Problem handling"
+msgstr "Gestione problema"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:99
+msgid "Process List"
+msgstr "Elenco dei processi"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1411
+msgid "Process performance data"
+msgstr "Dati sulle prestazioni del processo"
+
+#: library/Director/Web/Tabs/SyncRuleTabs.php:39
+#: library/Director/Import/ImportSourceLdap.php:67
+#, fuzzy
+msgid "Properties"
+msgstr "Proprietá "
+
+#: application/forms/ImportRowModifierForm.php:30
+#: library/Director/Web/Table/PropertymodifierTable.php:113
+#: library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php:61
+#, fuzzy
+msgid "Property"
+msgstr "Proprietá "
+
+#: application/controllers/ImportsourceController.php:169
+#, php-format
+msgid "Property modifiers: %s"
+msgstr "Modificatori di proprietà: %s"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:82
+msgid "Protected"
+msgstr "Protetto"
+
+#: library/Director/Import/ImportSourceRestApi.php:72
+msgid "Protocol"
+msgstr "Protocollo"
+
+#: application/controllers/InspectController.php:91
+msgid "Prototypes (methods)"
+msgstr "Prototipi (metodi)"
+
+#: library/Director/Dashboard/Dashlet/DatalistDashlet.php:11
+msgid "Provide Data Lists"
+msgstr "Fornire elenchi dati"
+
+#: library/Director/Dashboard/Dashlet/DatalistDashlet.php:17
+msgid "Provide data lists to make life easier for your users"
+msgstr "Fornire liste di dati per facilitare il lavoro ai vostri utenti"
+
+#: library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php:18
+#, fuzzy
+msgid "Provide templates for your TimePeriod objects."
+msgstr "Fornire templates per gli oggetti TimePeriod"
+
+#: library/Director/Dashboard/Dashlet/UserTemplateDashlet.php:18
+#, fuzzy
+msgid "Provide templates for your User objects."
+msgstr "Fornire templates per i vostri oggetti User"
+
+#: library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php:18
+#, fuzzy
+msgid "Provide templates for your notifications."
+msgstr "Fornire templates per le vostre notifiche"
+
+#: library/Director/Import/ImportSourceRestApi.php:167
+msgid "Proxy"
+msgstr "Proxy"
+
+#: library/Director/Import/ImportSourceRestApi.php:183
+msgid "Proxy Address"
+msgstr "Indirizzo Proxy"
+
+#: library/Director/Import/ImportSourceRestApi.php:201
+msgid "Proxy Password"
+msgstr "Password Proxy"
+
+#: library/Director/Import/ImportSourceRestApi.php:191
+msgid "Proxy Username"
+msgstr "Username Proxy"
+
+#: application/forms/SyncRuleForm.php:68
+msgid "Purge"
+msgstr "Ripulire"
+
+#: library/Director/Web/Tabs/ObjectTabs.php:122
+msgid "Ranges"
+msgstr "Ranges"
+
+#: application/forms/DeployConfigForm.php:32
+msgid "Re-deploy now"
+msgstr "Ri-distribuisci adesso"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:24
+msgid "Recovery"
+msgstr "Recupera"
+
+#: application/forms/IcingaGenerateApiKeyForm.php:22
+msgid "Regenerate Self Service API key"
+msgstr "Rigenerare la chiave API del Self Service"
+
+#: application/forms/IcingaHostSelfServiceForm.php:56
+msgid "Register"
+msgstr "Registra"
+
+#: library/Director/Web/SelfService.php:59
+msgid "Registered Agent"
+msgstr "Registra Agente"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:34
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:32
+msgid "Regular Expression"
+msgstr "Espressione Regolare"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:16
+msgid "Regular expression pattern to split the string (e.g. /\\s+/ or /[,;]/)"
+msgstr ""
+"Schema di espressione regolare per dividere la stringa (ad esempio /\\s+/ o /"
+"[,;]/)"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:58
+msgid "Reject matching elements"
+msgstr "Rifiuta gli elementi corrispondenti"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:59
+msgid "Reject the whole row (Blacklist)"
+msgstr "Rifiuta l'intera fila (Blacklist)"
+
+#: application/forms/IcingaDependencyForm.php:268
+msgid "Related Objects"
+msgstr "Oggetti correlati"
+
+#: library/Director/Web/Table/IcingaServiceSetServiceTable.php:191
+msgid "Remove"
+msgstr "Rimuovi"
+
+#: library/Director/Web/Table/IcingaServiceSetServiceTable.php:193
+#, php-format
+msgid "Remove \"%s\" from this host"
+msgstr "Rimuovere \"%s\" da questo host"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:480
+msgid "Remove this entry"
+msgstr "Rimuovere questa voce"
+
+#: application/views/helpers/FormDataFilter.php:507
+msgid "Remove this part of your filter"
+msgstr "Rimuovere questa parte del filtro"
+
+#: application/forms/DirectorDatafieldForm.php:96
+msgid "Rename related vars"
+msgstr "Rinomina le variabili correlati"
+
+#: application/forms/IcingaCommandForm.php:87
+msgid "Render as string"
+msgstr "Renderizza come stringa"
+
+#: application/controllers/ConfigController.php:67
+msgid "Render config"
+msgstr "Creare la configurazione"
+
+#: application/forms/IcingaCommandForm.php:78
+#, fuzzy
+msgid "Render the command as a plain string instead of an array."
+msgstr "Renderizza il comando come una stringa semplice invece di un array."
+
+#: application/controllers/ConfigController.php:290
+msgid "Rendered file"
+msgstr "File renderizzato"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:97
+#, php-format
+msgid "Rendered in %0.2fs, deployed in %0.2fs"
+msgstr "Rendered in %0.2fs, distribuito in %0.2fs"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:562
+#: library/Director/Web/Widget/ActivityLogInfo.php:567
+msgid "Rendering"
+msgstr "Rendering"
+
+#: application/forms/IcingaCommandArgumentForm.php:107
+msgid "Repeat key"
+msgstr "Ripeti chiave"
+
+#: application/forms/SyncRuleForm.php:62
+msgid "Replace"
+msgstr "Sostituisci"
+
+#: application/views/scripts/phperror/dependencies.phtml:17
+#: application/forms/IcingaCommandArgumentForm.php:124
+#: library/Director/Web/Table/CoreApiFieldsTable.php:81
+msgid "Required"
+msgstr "Richiesto"
+
+#: application/controllers/BasketController.php:206
+#: application/forms/RestoreBasketForm.php:58
+#: application/forms/IcingaServiceForm.php:139
+msgid "Restore"
+msgstr "Ripristina"
+
+#: application/forms/RestoreObjectForm.php:17
+msgid "Restore former object"
+msgstr "Ripristinare l'oggetto precedente"
+
+#: application/forms/RestoreBasketForm.php:52
+msgid "Restore to this target Director DB"
+msgstr "Ripristinare a questo Director DB"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1339
+msgid "Retry interval"
+msgstr "Intervallo di ripetizione"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1341
+msgid ""
+"Retry interval, will be applied after a state change unless the next hard "
+"state is reached"
+msgstr ""
+"Intervallo di ripetizione, sarà applicato dopo un cambio di stato, a meno "
+"che non venga raggiunto il prossimo stato difficile"
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:34
+msgid "Return lookup key unmodified"
+msgstr "La chiave di ricerca per la restituzione rimane invariata"
+
+#: application/forms/SyncRuleForm.php:30
+#: library/Director/Web/Table/SyncruleTable.php:45
+msgid "Rule name"
+msgstr "Nome regola"
+
+#: library/Director/Job/ImportJob.php:119
+msgid "Run all imports at once"
+msgstr "Eseguire tutte le importazioni in una sola volta"
+
+#: library/Director/Job/SyncJob.php:125
+#, fuzzy
+msgid "Run all rules at once"
+msgstr "Eseguire tutte le regole in una volta"
+
+#: application/forms/KickstartForm.php:185
+#: library/Director/Job/ImportJob.php:92
+msgid "Run import"
+msgstr "Esegui importazione"
+
+#: application/forms/DirectorJobForm.php:46
+msgid "Run interval"
+msgstr "Intervallo di esecuzione"
+
+#: application/forms/IcingaServiceForm.php:664
+msgid "Run on agent"
+msgstr "Esegui su agent"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:69
+msgid "Running with systemd"
+msgstr "Funzionamento con systemd"
+
+#: library/Director/Import/ImportSourceRestApi.php:174
+msgid "SOCKS5 proxy"
+msgstr "SOCKS5 Proxy"
+
+#: library/Director/Dashboard/Dashlet/JobDashlet.php:29
+msgid ""
+"Schedule and automate Import, Syncronization, Config Deployment, "
+"Housekeeping and more"
+msgstr ""
+"Programmare e automatizzare l'importazione, la sincronizzazione, la "
+"configurazione, l'implementazione, la pulizia e altro ancora"
+
+#: library/Director/Dashboard/NotificationsDashboard.php:14
+#: library/Director/Dashboard/UsersDashboard.php:15
+msgid "Schedule your notifications"
+msgstr "Programmare le notifiche"
+
+#: library/Director/Dashboard/Dashlet/NotificationsDashlet.php:19
+msgid ""
+"Schedule your notifications. Define who should be notified, when, and for "
+"which kind of problem"
+msgstr ""
+"Programmate le vostre notifiche. Definire chi deve essere notificato, quando "
+"e per quale tipo di problema"
+
+#: application/forms/SyncRuleForm.php:23
+msgid "Scheduled Downtime"
+msgstr "Downtime programmato"
+
+#: library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php:13
+msgid "Scheduled Downtimes"
+msgstr "Downtime programmati"
+
+#: application/forms/SettingsForm.php:165
+msgid ""
+"Script or tool to call when activating a new configuration stage. (e.g. "
+"sudo /usr/local/bin/icinga-director-activate) (name of the stage will be the "
+"argument for the script)"
+msgstr ""
+"Script o tool da chiamare quando si attiva una nuova fase di configurazione. "
+"(ad esempio sudo /usr/local/bin/icinga-director-activate) (il nome dello "
+"stage sarà l'argomento per lo script)"
+
+#: application/controllers/SettingsController.php:43
+#: application/controllers/SelfServiceController.php:101
+msgid "Self Service"
+msgstr "Self Service"
+
+#: application/controllers/SelfServiceController.php:102
+msgid "Self Service - Host Registration"
+msgstr "Self Service - Registrazione Host"
+
+#: library/Director/Dashboard/Dashlet/SelfServiceDashlet.php:11
+msgid "Self Service API"
+msgstr "Self Service API"
+
+#: application/controllers/SettingsController.php:44
+msgid "Self Service API - Global Settings"
+msgstr "Self Service API - Impostazioni Globali"
+
+#: application/forms/SelfServiceSettingsForm.php:285
+msgid "Self Service Settings have been stored"
+msgstr "Le impostazioni del Self Service sono state memorizzate"
+
+#: application/forms/IcingaUserForm.php:89
+#: library/Director/Web/Form/DirectorObjectForm.php:1399
+msgid "Send notifications"
+msgstr "Invia notifiche"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:78
+msgid "Sent to"
+msgstr "Invia a"
+
+#: application/forms/IcingaAddServiceForm.php:105
+#: application/forms/SyncRuleForm.php:14
+#: application/forms/IcingaServiceVarForm.php:15
+#: library/Director/TranslationDummy.php:14
+msgid "Service"
+msgstr "Servizio"
+
+#: library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php:11
+msgid "Service Apply Rules"
+msgstr "Apply rules del servizio"
+
+#: application/forms/SyncRuleForm.php:15
+msgid "Service Group"
+msgstr "Gruppo di servizio"
+
+#: application/forms/BasketForm.php:23
+#: library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php:11
+msgid "Service Groups"
+msgstr "Gruppi di servizio"
+
+#: application/forms/SyncRuleForm.php:16
+#: library/Director/DataType/DataTypeDirectorObject.php:58
+msgid "Service Set"
+msgstr "Set di servizi"
+
+#: application/forms/RemoveLinkForm.php:55
+msgid "Service Set has been removed"
+msgstr "Il set di servizi è stato rimosso"
+
+#: application/forms/BasketForm.php:26
+#: library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php:11
+#: library/Director/Web/Table/CustomvarVariantsTable.php:60
+#: library/Director/Web/Table/CustomvarTable.php:45
+msgid "Service Sets"
+msgstr "Set di servizi"
+
+#: application/forms/IcingaAddServiceForm.php:89
+msgid "Service Template"
+msgstr "Template di servizi"
+
+#: application/forms/BasketForm.php:24
+msgid "Service Template Choice"
+msgstr "Scelta del template di servizio"
+
+#: application/forms/BasketForm.php:25
+#: library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php:11
+msgid "Service Templates"
+msgstr "Templates di servizio"
+
+#: application/forms/SelfServiceSettingsForm.php:166
+msgid "Service User"
+msgstr "Utente del servizio"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:57
+msgid "Service groups"
+msgstr "Gruppi di servizio"
+
+#: application/forms/IcingaServiceForm.php:639
+msgid ""
+"Service groups that should be directly assigned to this service. "
+"Servicegroups can be useful for various reasons. They are helpful to "
+"provided service-type specific view in Icinga Web 2, either for custom "
+"dashboards or as an instrument to enforce restrictions. Service groups can "
+"be directly assigned to single services or to service templates."
+msgstr ""
+"Gruppi di servizi che dovrebbero essere assegnati direttamente a questo "
+"servizio. I gruppi di servizio possono essere utili per vari motivi. Sono "
+"utili per fornire una vista specifica del tipo di servizio in Icinga Web 2, "
+"sia dashboard personalizzate che come strumento per far rispettare le "
+"restrizioni. I gruppi di servizi possono essere assegnati direttamente a "
+"singoli servizi o a template di servizio"
+
+#: library/Director/Web/Table/IcingaHostAppliedForServiceTable.php:102
+msgid "Service name"
+msgstr "Nome del servizio"
+
+#: application/controllers/SuggestController.php:257
+#: library/Director/Objects/IcingaService.php:726
+msgid "Service properties"
+msgstr "Proprietà del servizio"
+
+#: application/forms/IcingaAddServiceSetForm.php:86
+#: application/forms/IcingaServiceSetForm.php:85
+msgid "Service set"
+msgstr "Set di servizi"
+
+#: application/forms/IcingaServiceSetForm.php:28
+msgid "Service set name"
+msgstr "Nome del set di servizi"
+
+#: application/controllers/TemplatechoiceController.php:22
+msgid "Service template choice"
+msgstr "Scelta del template di servizio"
+
+#: application/controllers/TemplatechoicesController.php:24
+msgid "Service template choices"
+msgstr "Scelta del template di servizio"
+
+#: application/controllers/ServiceController.php:76
+msgid "ServiceSet"
+msgstr "SetServizi"
+
+#: application/forms/IcingaServiceGroupForm.php:14
+msgid "Servicegroup"
+msgstr "Gruppo di servizio"
+
+#: library/Director/Web/Table/IcingaHostServiceTable.php:140
+#: library/Director/Web/Table/IcingaServiceSetServiceTable.php:154
+#: library/Director/Web/Table/IcingaAppliedServiceTable.php:32
+msgid "Servicename"
+msgstr "Nome del servizio"
+
+#: configuration.php:122 application/controllers/ServiceController.php:57
+#: application/controllers/ServiceController.php:80
+#: application/forms/IcingaScheduledDowntimeForm.php:86
+#: application/forms/IcingaDependencyForm.php:101
+#: application/forms/IcingaNotificationForm.php:90
+#: library/Director/Web/Table/CustomvarVariantsTable.php:59
+#: library/Director/Web/Table/CustomvarTable.php:44
+#: library/Director/Web/Tabs/ObjectTabs.php:75
+#: library/Director/DataType/DataTypeDirectorObject.php:56
+#: library/Director/IcingaConfig/StateFilterSet.php:23
+msgid "Services"
+msgstr "Servizi"
+
+#: application/controllers/ServicesetController.php:62
+#, php-format
+msgid "Services in this set: %s"
+msgstr "Servizi in questo set: %s"
+
+#: application/controllers/HostController.php:213
+#, php-format
+msgid "Services on %s"
+msgstr "Servizi su %s"
+
+#: application/controllers/HostController.php:137
+#, php-format
+msgid "Services: %s"
+msgstr "Servizi: %s"
+
+#: application/forms/SyncPropertyForm.php:84
+msgid "Set based on filter"
+msgstr "Impostazione basata sul filtro"
+
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:44
+msgid "Set false"
+msgstr "Imposta falso"
+
+#: library/Director/PropertyModifier/PropertyModifierJsonDecode.php:25
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:34
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:30
+#: library/Director/PropertyModifier/PropertyModifierGetHostByName.php:17
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:39
+msgid "Set no value (null)"
+msgstr "Imposta nessun valore (null)"
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:33
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:42
+msgid "Set null"
+msgstr "Imposta null"
+
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:43
+msgid "Set true"
+msgstr "Importa vero"
+
+#: library/Director/Web/Tabs/ObjectsTabs.php:77
+msgid "Sets"
+msgstr "Set"
+
+#: application/forms/IcingaHostForm.php:105
+msgid ""
+"Setting a command endpoint allows you to force host checks to be executed by "
+"a specific endpoint. Please carefully study the related Icinga documentation "
+"before using this feature"
+msgstr ""
+"L'impostazione di un endpoint di comando consente di forzare l'esecuzione "
+"dei controlli dell'host da parte di uno specifico endpoint. Si prega di "
+"studiare attentamente la relativa documentazione di Icinga prima di "
+"utilizzare questa funzione"
+
+#: application/controllers/ConfigController.php:221
+#: library/Director/Web/SelfService.php:93
+#, fuzzy
+msgid "Settings"
+msgstr "Impostazioni"
+
+#: application/forms/SettingsForm.php:218
+msgid "Settings have been stored"
+msgstr "Le impostazioni sono state memorizzate"
+
+#: library/Director/Web/SelfService.php:86
+msgid "Share this Template for Self Service API"
+msgstr "Condividi questo template di API per il Self Service"
+
+#: library/Director/Web/SelfService.php:84
+msgid "Shared for Self Service API"
+msgstr "API condivisa per il Self Service"
+
+#: library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:21
+msgid "Should all the other characters be lowercased first?"
+msgstr "Tutti gli altri caratteri devono essere in lettere minuscole?"
+
+#: application/controllers/HostController.php:514
+msgid "Show"
+msgstr "Mostra"
+
+#: application/controllers/BasketController.php:200
+msgid "Show Basket"
+msgstr "Mostra Basket"
+
+#: application/forms/DeployFormsBug7530.php:97
+#, php-format
+msgid "Show Issue %s on GitHub"
+msgstr "Mostra Issue %s su GitHub"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:75
+msgid "Show SQL"
+msgstr "Mostra SQL"
+
+#: library/Director/Web/Table/ApplyRulesTable.php:147
+msgid "Show affected Objects"
+msgstr "Mostra gli oggetti interessati"
+
+#: library/Director/Web/Form/IplElement/ExtensibleSetElement.php:457
+msgid "Show available options"
+msgstr "Mostra le opzioni disponibili"
+
+#: application/forms/IcingaObjectFieldForm.php:183
+msgid "Show based on filter"
+msgstr "Mostra in base al filtro"
+
+#: library/Director/Web/Table/ActivityLogTable.php:91
+msgid "Show details related to this change"
+msgstr "Mostra i dettagli relativi a questo cambiamento"
+
+#: library/Director/Web/ObjectPreview.php:51
+msgid "Show normal"
+msgstr "Mostra normale"
+
+#: library/Director/Web/ObjectPreview.php:60
+msgid "Show resolved"
+msgstr "Mostra risolto"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:33
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:31
+msgid "Simple match with wildcards (*)"
+msgstr "Ricerca semplice con caratteri jolly (*)"
+
+#: application/controllers/BasketController.php:332
+msgid "Single Object Diff"
+msgstr "Diffusione a singolo oggetto"
+
+#: library/Director/Dashboard/Dashlet/SingleServicesDashlet.php:11
+msgid "Single Services"
+msgstr "Servizi singoli"
+
+#: library/Director/Web/Table/GeneratedConfigFileTable.php:86
+msgid "Size"
+msgstr "Dimensione"
+
+#: application/forms/IcingaCommandArgumentForm.php:115
+msgid "Skip key"
+msgstr "Tasto Salta"
+
+#: application/controllers/BasketController.php:247
+#: application/controllers/BasketController.php:364
+msgid "Snapshot"
+msgstr "Snapshot"
+
+#: application/controllers/BasketController.php:36
+#: application/controllers/BasketController.php:141
+#: library/Director/Web/Table/BasketTable.php:32
+msgid "Snapshots"
+msgstr "Snapshots"
+
+#: library/Director/Import/ImportSourceRestApi.php:121
+msgid "Something like https://api.example.com/rest/v2/objects"
+msgstr "Qualcosa come https://api.example.com/rest/v2/objects"
+
+#: application/forms/SyncPropertyForm.php:182
+msgid "Source Column"
+msgstr "Colonna Sorgente"
+
+#: application/forms/SyncPropertyForm.php:212
+msgid "Source Expression"
+msgstr "Espressione Sorgente"
+
+#: application/forms/SyncPropertyForm.php:38
+msgid "Source Name"
+msgstr "Nome sorgente"
+
+#: application/forms/SelfServiceSettingsForm.php:101
+msgid "Source Path"
+msgstr "Percorso Sorgente"
+
+#: application/forms/ImportSourceForm.php:33
+msgid "Source Type"
+msgstr "Tipo Sorgente"
+
+#: application/forms/SyncPropertyForm.php:159
+msgid "Source columns"
+msgstr "Colonne Sorgenti"
+
+#: library/Director/Web/Table/SyncpropertyTable.php:62
+msgid "Source field"
+msgstr "Campo Sorgente"
+
+#: library/Director/Web/Table/ImportrunTable.php:30
+#: library/Director/Web/Table/SyncpropertyTable.php:61
+#: library/Director/Web/Table/ImportsourceTable.php:18
+msgid "Source name"
+msgstr "Nome Sorgente"
+
+#: application/forms/SyncPropertyForm.php:355
+msgid "Special properties"
+msgstr "Proprietá speciali"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:35
+msgid "Specific Element (by position)"
+msgstr "Elementi Specifici (per posizione)"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:87
+msgid "Stage name"
+msgstr "Nome Stage"
+
+#: library/Director/Web/Widget/SyncRunDetails.php:25
+msgid "Start time"
+msgstr "Ora di inizio"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:88
+msgid "Startup"
+msgstr "Avvio"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:162
+msgid "Startup Log"
+msgstr "Avvio Log"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:65
+msgid "Startup Time"
+msgstr "Inizio Tempo"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:79
+msgid "State"
+msgstr "Stato"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1647
+msgid "State and transition type filters"
+msgstr "Filtri di stato e di transizione"
+
+#: library/Director/IcingaConfig/TypeFilterSet.php:22
+msgid "State changes"
+msgstr "Cambiamenti di stato"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1622
+msgid "States"
+msgstr "Stati"
+
+#: library/Director/Web/Widget/DeployedConfigInfoHeader.php:81
+msgid "Statistics"
+msgstr "Statistiche"
+
+#: application/controllers/InspectController.php:43
+#: application/controllers/InspectController.php:151
+msgid "Status"
+msgstr "Stati"
+
+#: library/Director/Web/SelfService.php:134
+msgid "Stop sharing this Template"
+msgstr "Sospendi la condivisione del template"
+
+#: application/forms/SettingsForm.php:127
+#: library/Director/Web/Form/DirectorObjectForm.php:507
+msgid "Store"
+msgstr "Salva"
+
+#: application/forms/KickstartForm.php:36
+msgid "Store configuration"
+msgstr "Salva configurazione"
+
+#: library/Director/DataType/DataTypeDatalist.php:145
+msgid "Strict, list values only"
+msgstr "Severo, solo valori di lista"
+
+#: application/forms/IcingaCommandArgumentForm.php:38
+#: application/forms/IcingaCommandArgumentForm.php:77
+#: library/Director/DataType/DataTypeDirectorObject.php:76
+#: library/Director/DataType/DataTypeSqlQuery.php:76
+#: library/Director/DataType/DataTypeDatalist.php:130
+msgid "String"
+msgstr "Stringa"
+
+#: application/views/helpers/FormDataFilter.php:534
+msgid "Strip this operator, preserve child nodes"
+msgstr "Rimuovere questo operatore, preservare i nodi figlio"
+
+#: library/Director/Web/Form/QuickForm.php:215
+msgid "Submit"
+msgstr "Invia a"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:139
+msgid "Succeeded"
+msgstr "Successo"
+
+#: application/forms/IcingaObjectFieldForm.php:94
+msgid "Suggested fields"
+msgstr "Campi suggeriti"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:37
+msgid "Switch to Table view"
+msgstr "Passa alla vista Tabella"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:36
+msgid "Switch to Tree view"
+msgstr "Passa alla vista ad albero"
+
+#: application/controllers/SyncruleController.php:557
+#, php-format
+msgid "Sync \"%s\": %s"
+msgstr "Sincronizza \"%s\": \"%s\" "
+
+#: application/controllers/SyncruleController.php:121
+msgid "Sync Properties"
+msgstr "Proprietà di sincronizzazione"
+
+#: application/forms/BasketForm.php:32
+msgid "Sync Rules"
+msgstr "Regole di sincronizzazione"
+
+#: application/controllers/SyncruleController.php:592
+msgid "Sync history"
+msgstr "Storia della sincronizzazione"
+
+#: application/forms/SyncRuleForm.php:82
+#, php-format
+msgid ""
+"Sync only part of your imported objects with this rule. Icinga Web 2 filter "
+"syntax is allowed, so this could look as follows: %s"
+msgstr ""
+"Sincronizzare solo una parte degli oggetti importati con questa regola. La "
+"sintassi del filtro Icinga Web 2 è consentita, quindi potrebbe apparire come "
+"segue: %s"
+
+#: application/controllers/SyncruleController.php:529
+msgid "Sync properties"
+msgstr "Proprietà di sincronizzazione"
+
+#: application/controllers/SyncrulesController.php:25
+#: application/controllers/SyncruleController.php:494
+#: library/Director/Web/Tabs/ImportTabs.php:23
+#: library/Director/Web/Tabs/SyncRuleTabs.php:29
+#: library/Director/Web/Tabs/SyncRuleTabs.php:50
+msgid "Sync rule"
+msgstr "Regola di sincronizzazione"
+
+#: application/controllers/SyncruleController.php:41
+#: application/controllers/SyncruleController.php:466
+#, php-format
+msgid "Sync rule: %s"
+msgstr "Regola di sincronizzazione: %s"
+
+#: application/forms/SyncRunForm.php:42
+msgid "Synchronization failed"
+msgstr "Sincronizzazione fallita"
+
+#: library/Director/Job/SyncJob.php:80
+msgid "Synchronization rule"
+msgstr "Regola di sincronizzazione"
+
+#: library/Director/Dashboard/Dashlet/SyncDashlet.php:14
+msgid "Synchronize"
+msgstr "Sincronizzare"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:30
+msgid "Table"
+msgstr "Tabella"
+
+#: application/forms/RestoreBasketForm.php:51
+msgid "Target DB"
+msgstr "Destinatario DB"
+
+#: application/forms/IcingaCloneObjectForm.php:70
+msgid "Target Host"
+msgstr "Destinatario Host"
+
+#: application/forms/IcingaCloneObjectForm.php:61
+msgid "Target Service Set"
+msgstr "Target Set Servizio"
+
+#: library/Director/DataType/DataTypeDirectorObject.php:74
+#: library/Director/DataType/DataTypeSqlQuery.php:74
+#: library/Director/DataType/DataTypeDatalist.php:128
+msgid "Target data type"
+msgstr "Tipo di dati di destinazione"
+
+#: application/forms/ImportRowModifierForm.php:42
+msgid "Target property"
+msgstr "Proprietà obiettivo"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1086
+#: library/Director/Web/Form/DirectorObjectForm.php:1090
+#: library/Director/Web/Controller/TemplateController.php:158
+msgid "Template"
+msgstr "Template"
+
+#: library/Director/Web/Table/TemplatesTable.php:51
+msgid "Template Name"
+msgstr "Nome template"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:16
+msgid "Template name"
+msgstr "Nome template"
+
+#: library/Director/Web/Controller/ObjectController.php:283
+#: library/Director/Web/Controller/TemplateController.php:115
+#, php-format
+msgid "Template: %s"
+msgstr "Tempalte: %s"
+
+#: application/forms/IcingaServiceForm.php:692
+#: library/Director/Web/Tree/TemplateTreeRenderer.php:43
+#: library/Director/Web/Table/DependencyTemplateUsageTable.php:10
+#: library/Director/Web/Table/HostTemplateUsageTable.php:10
+#: library/Director/Web/Table/NotificationTemplateUsageTable.php:10
+#: library/Director/Web/Table/ServiceTemplateUsageTable.php:10
+#: library/Director/Web/Table/TemplateUsageTable.php:23
+#: library/Director/Web/Tabs/ObjectsTabs.php:54
+#, fuzzy
+msgid "Templates"
+msgstr "Templates"
+
+#: application/forms/DeployFormsBug7530.php:112
+msgid "Thanks, I'll verify this and come back later"
+msgstr "Grazie, lo verificherò e tornerò più tardi"
+
+#: library/Director/Job/ImportJob.php:67
+msgid "The \"Import\" job allows to run import actions at regular intervals"
+msgstr ""
+"Il lavoro \"Importa\" consente di eseguire azioni di importazione a "
+"intervalli regolari"
+
+#: library/Director/Job/SyncJob.php:65
+msgid "The \"Sync\" job allows to run sync actions at regular intervals"
+msgstr ""
+"Il lavoro \"Sync\" permette di eseguire azioni di sincronizzazione a "
+"intervalli regolari"
+
+#: application/forms/IcingaTimePeriodRangeForm.php:84
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:89
+#: library/Director/Web/Form/DirectorObjectForm.php:651
+#, php-format
+msgid "The %s has successfully been stored"
+msgstr "Il %s è stato memorizzato con successo"
+
+#: library/Director/Job/ConfigJob.php:221
+msgid ""
+"The Config job allows you to generate and eventually deploy your Icinga 2 "
+"configuration"
+msgstr ""
+"Il lavoro \"Configurazione\" permette di creare ed eventualmente di lanciare "
+"la configurazione di Icinga 2"
+
+#: application/forms/IcingaUserForm.php:37
+msgid "The Email address of the user."
+msgstr "L'indirizzo e-mail dell'utente."
+
+#: library/Director/Job/HousekeepingJob.php:21
+msgid ""
+"The Housekeeping job provides various task that keep your Director database "
+"fast and clean"
+msgstr ""
+"Il lavoro di pulizia offre diverse azioni che mantengono il database del "
+"Director veloce e pulito"
+
+#: application/controllers/DaemonController.php:38
+#, php-format
+msgid ""
+"The Icinga Director Background Daemon is not running. Please check our %s in "
+"case you need step by step instructions showing you how to fix this."
+msgstr ""
+"Il demone dell'Icinga Director non è in funzione. Si prega di "
+"controllare le nostre %s nel caso in cui abbiate bisogno di istruzioni passo "
+"dopo passo che vi mostrino come risolvere il problema."
+
+#: application/controllers/SettingsController.php:38
+msgid ""
+"The Icinga Director Self Service API allows your Hosts to register "
+"themselves. This allows them to get their Icinga Agent configured, installed "
+"and upgraded in an automated way."
+msgstr ""
+"L'API Self Service Director di Icinga consente ai vostri Ospiti di "
+"registrarsi. Questo permette loro di configurare, installare e aggiornare il "
+"loro agente Icinga in modo automatico."
+
+#: application/forms/SettingsForm.php:45
+msgid ""
+"The Icinga Package name Director uses to deploy it's configuration. This "
+"defaults to \"director\" and should not be changed unless you really know "
+"what you're doing"
+msgstr ""
+"Il nome del pacchetto Icinga che Director utilizza per distribuire la sua "
+"configurazione. Questo valore predefinito è \"director\" e non dovrebbe "
+"essere cambiato a meno che non si sappia davvero cosa si sta facendo"
+
+#: library/Director/Import/ImportSourceLdap.php:69
+msgid ""
+"The LDAP properties that should be fetched. This is required to be a comma-"
+"separated list like: \"cn, dnshostname, operatingsystem, sAMAccountName\""
+msgstr ""
+"Le proprietà LDAP che dovrebbero essere recuperate. Questo deve essere un "
+"elenco separato da virgole come: \"cn, dnshostname, sistema operativo, "
+"sAMAccountName\""
+
+#: application/forms/IcingaForgetApiKeyForm.php:31
+#, php-format
+msgid "The Self Service API key for %s has been dropped"
+msgstr "La chiave API del Self Service per %s è stata abbandonata"
+
+#: application/forms/IcingaAddServiceSetForm.php:116
+#, php-format
+msgid "The Service Set \"%s\" has been added to %d hosts"
+msgstr "Il set di servizi \"%s\" è stato aggiunto a %d host"
+
+#: application/forms/IcingaCommandArgumentForm.php:167
+#, php-format
+msgid "The argument %s has successfully been stored"
+msgstr "L'argomento %s è stato memorizzato con successo"
+
+#: application/forms/IcingaObjectFieldForm.php:129
+msgid "The caption which should be displayed"
+msgstr "L'etichetta da visualizzare"
+
+#: application/forms/DirectorDatafieldForm.php:153
+msgid ""
+"The caption which should be displayed to your users when this field is shown"
+msgstr ""
+"La didascalia che dovrebbe essere visualizzata agli utenti quando viene "
+"mostrato questo campo"
+
+#: application/forms/IcingaDependencyForm.php:234
+msgid "The child host."
+msgstr "L'Host figlio."
+
+#: application/forms/IcingaCommandForm.php:62
+msgid ""
+"The command Icinga should run. Absolute paths are accepted as provided, "
+"relative paths are prefixed with \"PluginDir + \", similar Constant prefixes "
+"are allowed. Spaces will lead to separation of command path and standalone "
+"arguments. Please note that this means that we do not support spaces in "
+"plugin names and paths right now."
+msgstr ""
+"Il comando che Icinga dovrebbe eseguire. I percorsi assoluti saranno presi "
+"come specificato, i percorsi relativi saranno preceduti dal prefisso "
+"\"PluginDir +\", con costanti simili consentite come prefisso. Gli spazi "
+"possono essere usati per dividere il percorso del comando e gli argomenti a "
+"sé stanti. Ciò significa che attualmente non sono supportati spazi nei nomi "
+"e nei percorsi dei plugin."
+
+#: application/forms/KickstartForm.php:161
+msgid "The corresponding password"
+msgstr "La password corrispondente"
+
+#: library/Director/PropertyModifier/PropertyModifierStripDomain.php:14
+msgid "The domain name you want to be stripped"
+msgstr "Il nome a dominio da rimuovere"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:18
+msgid "The first (leftmost) CN"
+msgstr "La prima NC (più a sinistra)"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:19
+msgid "The first (leftmost) OU"
+msgstr "Il primo (più a sinistra) OU"
+
+#: application/forms/IcingaServiceForm.php:731
+#, php-format
+msgid "The given properties have been stored for \"%s\""
+msgstr "Le proprietà trasferite sono state salvate per \"%s\""
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1625
+msgid "The host/service states you want to get notifications for"
+msgstr "Gli stati host/servizio per i quali devono essere inviate le notifiche"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:21
+msgid "The last (rightmost) OU"
+msgstr "L'ultimo (più a destra) OU"
+
+#: library/Director/Web/Widget/JobDetails.php:59
+#, php-format
+msgid "The last attempt failed %s: %s"
+msgstr "L'ultimo tentativo è fallito %s: %s"
+
+#: library/Director/Web/Widget/JobDetails.php:54
+#, php-format
+msgid "The last attempt succeeded %s"
+msgstr "L'ultimo tentativo è riuscito %s"
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:83
+msgid "The last deployment did not succeed"
+msgstr "L'ultimo rollout non ha avuto successo"
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:85
+msgid "The last deployment is currently pending"
+msgstr "Il rollout della configurazione è in corso"
+
+#: application/forms/IcingaEndpointForm.php:42
+msgid "The log duration time."
+msgstr "Il tempo di durata del log."
+
+#: application/forms/IcingaUserForm.php:160
+msgid ""
+"The name of a time period which determines when notifications to this User "
+"should be triggered. Not set by default."
+msgstr ""
+"Il nome del periodo di tempo che specifica quando le notifiche devono essere "
+"attivate per questo utente. Nessun valore predefinito."
+
+#: application/forms/IcingaDependencyForm.php:143
+#: application/forms/IcingaNotificationForm.php:234
+msgid ""
+"The name of a time period which determines when this notification should be "
+"triggered. Not set by default."
+msgstr ""
+"Il nome del periodo di tempo che specifica quando questa notifica deve "
+"essere attivata. Nessun valore predefinito."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1377
+msgid ""
+"The name of a time period which determines when this object should be "
+"monitored. Not limited by default."
+msgstr ""
+"Il nome del periodo di tempo che specifica quando questo oggetto viene "
+"monitorato. Non limitato per impostazione predefinita."
+
+#: application/forms/DirectorJobForm.php:62
+msgid ""
+"The name of a time period within this job should be active. Supports only "
+"simple time periods (weekday and multiple time definitions)"
+msgstr ""
+"Il nome del periodo di tempo entro il quale questo lavoro dovrebbe essere "
+"attivo. Permette solo periodi di tempo semplici (giorno della settimana e "
+"registrazioni multiple)"
+
+#: application/forms/IcingaHostVarForm.php:16
+msgid "The name of the host"
+msgstr "Il nome degli hosts"
+
+#: application/forms/IcingaServiceVarForm.php:16
+msgid "The name of the service"
+msgstr "Il nome del servizio"
+
+#: application/forms/IcingaNotificationForm.php:178
+msgid ""
+"The notification interval (in seconds). This interval is used for active "
+"notifications. Defaults to 30 minutes. If set to 0, re-notifications are "
+"disabled."
+msgstr ""
+"L'intervallo di notifica (in secondi). Questo intervallo viene utilizzato "
+"per le notifiche attive. Il valore predefinito è 30 minuti. Se è impostato a "
+"0, le ripetizioni di notifica sono disattivate."
+
+#: library/Director/PropertyModifier/PropertyModifierBitmask.php:16
+msgid ""
+"The numeric bitmask you want to apply. In case you have a hexadecimal or "
+"binary mask please transform it to a decimal number first. The result of "
+"this modifier is a boolean value, telling whether the given mask applies to "
+"the numeric value in your source column"
+msgstr ""
+"La bitmaschera numerica da applicare. Le maschere esadecimali e binarie "
+"devono prima essere convertite in un numero decimale. Il risultato è un "
+"valore booleano che indica se la maschera si applica al valore numerico "
+"nella colonna sorgente"
+
+#: application/forms/IcingaUserForm.php:42
+msgid "The pager address of the user."
+msgstr "L'indirizzo del cercapersone dell'utente."
+
+#: application/forms/IcingaDependencyForm.php:202
+msgid ""
+"The parent host. You might want to refer Host Custom Variables via $host."
+"vars.varname$"
+msgstr ""
+"L'host genitore. Proprietà host personalizzate possono essere referenziate "
+"tramite $host.vars.varname$"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexReplace.php:15
+#, fuzzy
+msgid ""
+"The pattern you want to search for. This can be a regular expression like /"
+"^www\\d+\\./"
+msgstr ""
+"Lo schema che si vuole cercare. Può essere un'espressione regolare come /"
+"^wwwwd++./"
+
+#: application/forms/IcingaEndpointForm.php:37
+#, fuzzy
+msgid "The port of the endpoint."
+msgstr "La Porta di un endpoint."
+
+#: application/forms/KickstartForm.php:144
+msgid ""
+"The port you are going to use. The default port 5665 will be used if none is "
+"set"
+msgstr ""
+" La porta da utilizzare. Se non è impostato alcun valore, si applica il "
+"valore predefinito 5665"
+
+#: library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php:64
+msgid "The property to get from the row we found in the chosen Import Source"
+msgstr ""
+"La proprietà che vogliamo estrarre dalla linea che abbiamo trovato nella "
+"fonte di importazione selezionata"
+
+#: application/forms/IcingaAddServiceForm.php:174
+#, php-format
+msgid "The service \"%s\" has been added to %d hosts"
+msgstr "Il servizio \"%s\" è stato aggiunto a %d Hosts"
+
+#: application/forms/IcingaAddServiceSetForm.php:88
+msgid "The service Set that should be assigned"
+msgstr "Il set di servizi da assegnare"
+
+#: application/forms/IcingaServiceSetForm.php:87
+msgid "The service set that should be assigned to this host"
+msgstr "Il set di servizi da assegnare a questo host"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1635
+msgid "The state transition types you want to get notifications for"
+msgstr ""
+"I tipi di cambiamenti di stato per i quali devono essere inviate le notifiche"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexReplace.php:23
+msgid "The string that should be used as a preplacement"
+msgstr "La stringa di caratteri da utilizzare in sostituzione"
+
+#: library/Director/PropertyModifier/PropertyModifierReplace.php:14
+msgid "The string you want to search for"
+msgstr "La stringa di caratteri da cercare"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:41
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:43
+msgid ""
+"The string/pattern you want to search for. Depends on the chosen method, use "
+"www.* or *linux* for wildcard matches and expression like /^www\\d+\\./ in "
+"case you opted for a regular expression"
+msgstr ""
+"La stringa / pattern da cercare. A seconda del metodo selezionato, utilizzare "
+"www.* o *linux* per ricerche basate su caratteri jolly ed espressioni come /"
+"^www\\d+\\/ se si preferiscono le espressioni regolari"
+
+#: application/forms/DirectorDatafieldForm.php:143
+msgid ""
+"The unique name of the field. This will be the name of the custom variable "
+"in the rendered Icinga configuration."
+msgstr ""
+"L'identificatore unico di questo campo. Questo è usato come nome della "
+"proprietà personalizzata nella configurazione resa da Icinga."
+
+#: application/forms/SelfServiceSettingsForm.php:168
+msgid "The user that should run the Icinga 2 service on Windows."
+msgstr ""
+"L'account utente sotto il quale il servizio Icinga 2 funzionerà su Windows."
+
+#: application/forms/DirectorDatafieldForm.php:74
+#, php-format
+msgid ""
+"There are %d objects with a related property. Should I also remove the \"%s"
+"\" property from them?"
+msgstr ""
+"Ci sono oggetti %d con una proprietà corrispondente. Il valore di \"%s\" "
+"deve essere rimosso da tutti?"
+
+#: application/forms/DirectorDatafieldForm.php:118
+#, php-format
+msgid ""
+"There are %d objects with a related property. Should I also rename the \"%s"
+"\" property to \"%s\" on them?"
+msgstr ""
+"Ci sono oggetti %d con una proprietà corrispondente. La variabile \"%s\" "
+"deve essere rinominata in \"%s\" su tutti?"
+
+#: application/forms/DeploymentLinkForm.php:66
+#, php-format
+msgid "There are %d pending changes"
+msgstr "Ci sono variazioni %d non ancora attuate"
+
+#: application/forms/DeploymentLinkForm.php:79
+#, php-format
+msgid "There are %d pending changes, %d of them applied to this object"
+msgstr ""
+"Ci sono %d modifiche pendenti, di cui %d sono applicate a questo oggetto"
+
+#: library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:88
+#, php-format
+msgid "There are %d pending database migrations"
+msgstr "Ci sono %d migrazioni di database in sospeso"
+
+#: application/forms/IcingaObjectFieldForm.php:117
+msgid ""
+"There are no data fields available. Please ask an administrator to create "
+"such"
+msgstr "Non sono disponibili campi dati. Un amministratore può crearne alcuni"
+
+#: library/Director/Dashboard/Dashlet/DeploymentDashlet.php:91
+msgid "There are no pending changes"
+msgstr "Non ci sono cambiamenti in sospeso"
+
+#: application/forms/DeployConfigForm.php:34
+msgid "There are no pending changes. Deploy anyway"
+msgstr "Non ci sono cambiamenti in sospeso. Distrubuisci in ogni caso"
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:55
+msgid ""
+"There are pending changes for this Import Source. You should trigger a new "
+"Import Run."
+msgstr ""
+"Ci sono modifiche in sospeso per questa fonte di importazione. Dovrebbe "
+"essere attivata una nuova operazione di importazione."
+
+#: application/controllers/SyncruleController.php:83
+msgid ""
+"There are pending changes for this Sync Rule. You should trigger a new Sync "
+"Run."
+msgstr ""
+"Ci sono modifiche in sospeso per questa regola di sincronizzazione. Dovrebbe "
+"essere attivata una nuova corsa di sincronizzazione."
+
+#: application/forms/KickstartForm.php:62
+msgid "There are pending database migrations"
+msgstr "Le migrazioni del database sono in corso"
+
+#: application/forms/DeploymentLinkForm.php:71
+#, fuzzy
+msgid ""
+"There has been a single change to this object, nothing else has been modified"
+msgstr ""
+"É stata applicata una singola modifica a questo oggetto, nessun'altra "
+"modifica e' stata effettuata"
+
+#: application/forms/DeploymentLinkForm.php:74
+#, php-format
+msgid ""
+"There have been %d changes to this object, nothing else has been modified"
+msgstr ""
+"Ci sono state %d modifiche a questo oggetto, non è stato modificato "
+"nient'altro"
+
+#: application/forms/DeploymentLinkForm.php:63
+msgid "There is a single pending change"
+msgstr "C'è un unico cambiamento in sospeso"
+
+#: application/forms/DirectorJobForm.php:21
+msgid "These are different available job types"
+msgstr "Questi sono diversi tipi di lavoro disponibili"
+
+#: application/forms/ImportSourceForm.php:37
+msgid ""
+"These are different data providers fetching data from various sources. You "
+"didn't find what you're looking for? Import sources are implemented as a "
+"hook in Director, so you might find (or write your own) Icinga Web 2 module "
+"fetching data from wherever you want"
+msgstr ""
+"Si tratta di diversi fornitori di dati che raccolgono dati da varie fonti. "
+"Non avete trovato quello che cercate? Le fonti di importazione sono "
+"implementate come un gancio in Director, quindi potresti trovare (o scrivere "
+"il tuo) modulo Icinga Web 2 che recupera dati da qualsiasi luogo tu voglia"
+
+#: application/controllers/CommandController.php:71
+#, php-format
+msgid "This Command is currently being used by %s"
+msgstr "Questo comando è attualmente utilizzato da %s"
+
+#: application/controllers/CommandController.php:78
+msgid "This Command is currently not in use"
+msgstr "Questo comando non è attualmente utilizzato"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:928
+#, php-format
+msgid "This Command is still in use by %d other objects"
+msgstr "Questo comando è usato da %d altri oggetti"
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:62
+#, php-format
+msgid "This Import Source failed when last checked at %s: %s"
+msgstr ""
+"Questa fonte di importazione non è riuscita in %s durante l'ultimo "
+"controllo: %s"
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:70
+#, php-format
+msgid "This Import Source has an invalid state: %s"
+msgstr "Questa fonte di importazione ha uno stato non valido: %s"
+
+#: application/forms/ImportCheckForm.php:33
+msgid "This Import Source provides modified data"
+msgstr "Questa fonte di importazione fornisce dati modificati"
+
+#: library/Director/Web/Widget/ImportSourceDetails.php:45
+#, php-format
+msgid "This Import Source was last found to be in sync at %s."
+msgstr ""
+"Questa fonte di importazione è stata trovata per l'ultima volta in "
+"sincronizzazione il %s."
+
+#: application/forms/IcingaServiceForm.php:134
+msgid "This Service has been blacklisted on this host"
+msgstr "Questo servizio è stato messo nella lista nera di questo host"
+
+#: application/controllers/SyncruleController.php:90
+#, php-format
+msgid "This Sync Rule failed when last checked at %s: %s"
+msgstr ""
+"Questa regola di sincronizzazione non è riuscita il %s durante l'ultimo "
+"controllo: %s"
+
+#: application/controllers/SyncruleController.php:60
+msgid "This Sync Rule has never been run before."
+msgstr "Questa regola di sincronizzazione non è mai stata eseguita prima."
+
+#: application/controllers/SyncruleController.php:178
+msgid "This Sync Rule is in sync and would currently not apply any changes"
+msgstr ""
+"Questa regola di sincronizzazione è sincronizzata e non causerebbe "
+"attualmente alcun cambiamento"
+
+#: application/controllers/SyncruleController.php:72
+#, php-format
+msgid "This Sync Rule was last found to by in Sync at %s."
+msgstr ""
+"Questa regola di sincronizzazione è stata sincronizzata l'ultima volta il %s."
+
+#: application/forms/SyncPropertyForm.php:104
+msgid ""
+"This allows to filter for specific parts within the given source expression. "
+"You are allowed to refer all imported columns. Examples: host=www* would set "
+"this property only for rows imported with a host property starting with \"www"
+"\". Complex example: host=www*&!(address=127.*|address6=::1)"
+msgstr ""
+"Permette il filtraggio di parti specifiche all'interno dell'espressione "
+"della sorgente specificata. Tutte le colonne importate possono essere "
+"specificate. Esempi: host=wwww* imposterebbe questa proprietà solo per le "
+"linee importate dove la proprietà host inizia con \"www\". Esempio "
+"complesso: host=wwwww*&!(indirizzo=127.*|indirizzo6=::1)"
+
+#: library/Director/DataType/DataTypeDatalist.php:140
+msgid "This allows to show either a drop-down list or an auto-completion"
+msgstr ""
+"Questo permette di visualizzare un menu a tendina o un campo con "
+"completamento automatico"
+
+#: application/forms/DirectorJobForm.php:39
+msgid "This allows to temporarily disable this job"
+msgstr "Consente di disattivare temporaneamente questo lavoro"
+
+#: application/forms/IcingaHostGroupForm.php:30
+#: application/forms/IcingaScheduledDowntimeForm.php:111
+#: application/forms/IcingaDependencyForm.php:115
+#: application/forms/IcingaNotificationForm.php:106
+#: application/forms/IcingaServiceForm.php:450
+#: application/forms/IcingaServiceGroupForm.php:30
+msgid ""
+"This allows you to configure an assignment filter. Please feel free to "
+"combine as many nested operators as you want. The \"contains\" operator is "
+"valid for arrays only. Please use wildcards and the = (equals) operator when "
+"searching for partial string matches, like in *.example.com"
+msgstr ""
+"Questo permette di definire un filtro di assegnazione (assign). Un numero "
+"qualsiasi di operatori può essere annidato a qualsiasi profondità. "
+"L'operatore \"contiene\" è consentito solo per gli array. Per confrontare le "
+"sottostringhe si prega di utilizzare i caratteri jolly, come in *.example.com"
+
+#: application/forms/IcingaServiceSetForm.php:121
+msgid ""
+"This allows you to configure an assignment filter. Please feel free to "
+"combine as many nested operators as you want. You might also want to skip "
+"this, define it later and/or just add this set of services to single hosts. "
+"The \"contains\" operator is valid for arrays only. Please use wildcards and "
+"the = (equals) operator when searching for partial string matches, like in *."
+"example.com"
+msgstr ""
+"Questo permette di definire un filtro di assegnazione (assign). Un numero "
+"qualsiasi di operatori può essere annidato a qualsiasi profondità. Questo "
+"passo può anche essere saltato ed eventualmente implementato in un secondo "
+"momento. In alternativa (o in aggiunta) questo set di servizi può essere "
+"assegnato direttamente ai singoli host. L'operatore \"contiene\" è "
+"consentito solo per gli array. Per confrontare le sottostringhe si prega di "
+"utilizzare i caratteri jolly, come in *.example.com"
+
+#: library/Director/Job/ConfigJob.php:197
+msgid "This allows you to immediately deploy a modified configuration"
+msgstr "Questo permette il rollout immediato di una configurazione modificata"
+
+#: library/Director/ProvidedHook/CubeLinks.php:37
+#, php-format
+msgid "This allows you to modify properties for \"%s\""
+msgstr "Permette di modificare le proprietà di \"%s\""
+
+#: library/Director/ProvidedHook/CubeLinks.php:54
+msgid "This allows you to modify properties for all chosen hosts at once"
+msgstr ""
+"Questo consente di modificare le proprietà per tutti gli host selezionati in "
+"una sola volta"
+
+#: application/controllers/BasketController.php:62
+msgid "This basket is empty"
+msgstr "Questo basket è vuoto"
+
+#: application/forms/KickstartForm.php:253
+msgid "This has to be a MySQL or PostgreSQL database"
+msgstr "Questo deve essere un database MySQL o PostgreSQL"
+
+#: library/Director/Web/SelfService.php:62
+msgid ""
+"This host has been registered via the Icinga Director Self Service API. In "
+"case you re-installed the host or somehow lost it's secret key, you might "
+"want to dismiss the current key. This would allow you to register the same "
+"host again."
+msgstr ""
+"Questo host è stato registrato utilizzando l'API self-service di Icinga "
+"Director. Se l'host è stato reinstallato o la chiave è stata smarrita, è "
+"possibile che si desideri eliminare la chiave corrente. Questo permetterebbe "
+"di ri-registrare lo stesso ospite."
+
+#: application/controllers/InspectController.php:73
+#, fuzzy
+msgid "This is an abstract object type."
+msgstr "Questo é un oggetto ti tipo astratto."
+
+#: library/Director/Web/Controller/TemplateController.php:178
+#, php-format
+msgid "This is the \"%s\" %s Template. Based on this, you might want to:"
+msgstr "Questo è il template \"%s\" %s. Sulla base di questo, è possibile:"
+
+#: application/forms/KickstartForm.php:119
+msgid ""
+"This is the name of the Endpoint object (and certificate name) you created "
+"for your ApiListener object. In case you are unsure what this means please "
+"make sure to read the documentation first"
+msgstr ""
+"Questo è il nome dell'oggetto Endpoint (e il nome del certificato) che avete "
+"creato per il vostro oggetto ApiListener. Nel caso non siate sicuri di cosa "
+"questo significhi, assicuratevi di leggere prima la documentazione"
+
+#: library/Director/Dashboard/Dashlet/HostsDashlet.php:17
+msgid ""
+"This is where you add all your servers, containers, network or sensor "
+"devices - and much more. Every subject worth to be monitored"
+msgstr ""
+"Qui si aggiungono tutti i vostri server, contenitori, dispositivi di rete o "
+"sensori - e molto altro ancora. Ogni soggetto che vale la pena di essere "
+"monitorato"
+
+#: library/Director/Dashboard/HostsDashboard.php:22
+msgid ""
+"This is where you manage your Icinga 2 Host Checks. Host templates are your "
+"main building blocks. You can bundle them to \"choices\", allowing (or "
+"forcing) your users to choose among a given set of preconfigured templates."
+msgstr ""
+"Qui è dove si gestiscono i controlli degli host di Icinga 2. I template di "
+"host sono i vostri principali elementi costitutivi. Puoi raggrupparli in "
+"\"scelte\", permettendo (o obbligando) i tuoi utenti a scegliere tra un dato "
+"set di template preconfigurati."
+
+#: library/Director/Dashboard/ServicesDashboard.php:24
+msgid ""
+"This is where you manage your Icinga 2 Service Checks. Service Templates are "
+"your base building blocks, Service Sets allow you to assign multiple "
+"Services at once. Apply Rules make it possible to assign Services based on "
+"Host properties. And the list of all single Service Objects gives you the "
+"possibility to still modify (or delete) many of them at once."
+msgstr ""
+"Qui è dove si gestiscono i controlli di servizio di Icinga 2. I Service "
+"Template sono i vostri blocchi di base, i Service Set vi permettono di "
+"assegnare più Servizi contemporaneamente. Le Regole di applicazione "
+"consentono di assegnare i Servizi in base alle proprietà Host. E l'elenco di "
+"tutti i singoli Oggetti di Servizio ti dà la possibilità di modificare (o "
+"cancellare) molti di essi contemporaneamente."
+
+#: library/Director/Dashboard/UsersDashboard.php:21
+msgid ""
+"This is where you manage your Icinga 2 User (Contact) objects. Try to keep "
+"your User objects simply by movin complexity to your templates. Bundle your "
+"users in groups and build Notifications based on them. Running MS Active "
+"Directory or another central User inventory? Stay away from fiddling with "
+"manual config, try to automate all the things with Imports and related Sync "
+"Rules!"
+msgstr ""
+"Qui si gestiscono gli oggetti di Icinga 2 User (Contact). Cercate di "
+"mantenere i vostri oggetti User semplici spostando la complessità nei "
+"templates. Raggruppate gli utenti in gruppi e configurate le "
+"Notifiche in base ad essi. Cercate di utilizzare MS Active Directory o un altro "
+"database utente centralizzato? Consigliamo di evitare le "
+"configurazioni manuali, e di utilizzare regole di importazione e sincronizzazione create "
+"per automatizzare la procedura."
+
+#: library/Director/Dashboard/InfrastructureDashboard.php:24
+msgid ""
+"This is where you manage your Icinga 2 infrastructure. When adding a new "
+"Icinga Master or Satellite please re-run the Kickstart Helper once.\n"
+"\n"
+"When you feel the desire to manually create Zone or Endpoint objects please "
+"rethink this twice. Doing so is mostly the wrong way, might lead to a dead "
+"end, requiring quite some effort to clean up the whole mess afterwards."
+msgstr ""
+"Qui è dove si gestisce l'infrastruttura di Icinga 2. Quando si aggiunge un "
+"nuovo Icinga Master o Satellite, si prega di eseguire nuovamente il "
+"Kickstart Helper.\n"
+"\n"
+"Creare Zone o Endpoint manualmente, il più delle volte si rivela la scelta sbagliata "
+"portando ad un vicolo cieco che costringe l'utente a ripulire il sistema "
+"dalle modifiche apportate."
+
+#: library/Director/Web/Table/ObjectsTableEndpoint.php:45
+msgid "This is your Config master and will receive our Deployments"
+msgstr "Questo è il vostro Config master e riceverà le nostre distribuzioni"
+
+#: library/Director/Web/Widget/JobDetails.php:67
+msgid "This job has not been executed yet"
+msgstr "Questo lavoro non è stato ancora eseguito"
+
+#: library/Director/Web/Widget/JobDetails.php:35
+#, php-format
+msgid "This job runs every %ds and is currently pending"
+msgstr "Questo lavoro viene eseguito ogni %ds ed è attualmente in sospeso"
+
+#: library/Director/Web/Widget/JobDetails.php:39
+#, php-format
+msgid "This job runs every %ds."
+msgstr "Questo lavoro viene eseguito ogni %ds."
+
+#: library/Director/Web/Widget/JobDetails.php:26
+#, php-format
+msgid ""
+"This job would run every %ds. It has been disabled and will therefore not be "
+"executed as scheduled"
+msgstr ""
+"Questo lavoro verrebbe eseguito ogni %ds. È stato disattivato e quindi non "
+"verrà eseguito come previsto"
+
+#: library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:33
+msgid ""
+"This modifier transforms 0/\"0\"/false/\"false\"/\"n\"/\"no\" to false and "
+"1, \"1\", true, \"true\", \"y\" and \"yes\" to true, both in a case "
+"insensitive way. What should happen if the given value does not match any of "
+"those? You could return a null value, or default to false or true. You might "
+"also consider interrupting the whole import process as of invalid source data"
+msgstr ""
+"Questo modificatore trasforma 0/\"0\"0\"/falso/\"falso\"/\"n\"/\"no\" in "
+"falso e 1, \"1\", vero, \"vero\", \"y\" e \"sì\" in vero, in entramnbi i casi "
+"senza distinzione tra maiuscole e minuscole. Cosa dovrebbe succedere se il valore dato non "
+"corrispondesse a nessuno di questi? Si potrebbe restituire un valore nullo, o "
+"fissare il valore di default a vero o a falso. Si potrebbe anche considerare di interrompere "
+"l'intero processo di importazione, in quanto i dati della sorgente non sarebbero da considerarsi validi"
+
+#: application/forms/ImportSourceForm.php:89
+msgid ""
+"This must be a column containing unique values like hostnames. Unless "
+"otherwise specified this will then be used as the object_name for the "
+"syncronized Icinga object. Especially when getting started with director "
+"please make sure to strictly follow this rule. Duplicate values for this "
+"column on different rows will trigger a failure, your import run will not "
+"succeed. Please pay attention when synching services, as \"purge\" will only "
+"work correctly with a key_column corresponding to host!name. Check the "
+"\"Combine\" property modifier in case your data source cannot provide such a "
+"field"
+msgstr ""
+"Questa colonna deve contenere valori unici, come gli hostname. "
+"Se non diversamente specificato, questo sarà usato come nome oggetto per "
+"la sincronizzazione dell'oggetto Icinga. Soprattutto quando si inizia a lavorare con "
+"il Director, assicurarsi di seguire rigorosamente questa regola. Creare dei valori duplicati "
+"in righe diverse di questa colonna porterà ad un errore che nel processo "
+"di importazione. Si prega di prestare attenzione quando si "
+"sincronizzano i servizi, poiché \"purge\" funzionerà correttamente solo con "
+"una chiave colonna corrispondente al nome host!name. Controllate il "
+"modificatore di proprietà \"Combine\" nel caso in cui la vostra fonte di "
+"dati non sia in grado di fornire tale campo"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:29
+msgid "This name will show up as the author for ever related downtime comment"
+msgstr ""
+"Questo nome si presenterà come l'autore per il commento sempre relativo ai "
+"tempi di inattività"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:568
+msgid "This object has been disabled"
+msgstr "Questo oggetto è stato disabilitato"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:563
+msgid "This object has been enabled"
+msgstr "Questo oggetto è stato abilitato"
+
+#: library/Director/Web/ObjectPreview.php:76
+msgid "This object will not be deployed as it has been disabled"
+msgstr "Questo oggetto non verrà distribuito in quanto è stato disabilitato"
+
+#: library/Director/PropertyModifier/PropertyModifierCombine.php:17
+msgid ""
+"This pattern will be evaluated, and variables like ${some_column} will be "
+"filled accordingly. A typical use-case is generating unique service "
+"identifiers via ${host}!${service} in case your data source doesn't allow "
+"you to ship such. The chosen \"property\" has no effect here and will be "
+"ignored."
+msgstr ""
+"Questo pattern sarà esaminato e le variabili come ${qualche_colonna} saranno "
+"sostituite di conseguenza. Un tipico caso d'uso è la generazione di "
+"identificatori di servizio unici tramite ${host}!${servizio} nel caso in cui "
+"la vostra fonte di dati non vi permetta di spedirli. La \"proprietà\" scelta "
+"non ha alcun effetto in questo caso e sarà ignorata."
+
+#: library/Director/Web/SelfService.php:234
+msgid ""
+"This requires the Icinga Agent to be installed. It generates and signs it's "
+"certificate and it also generates a minimal icinga2.conf to get your agent "
+"connected to it's parents"
+msgstr ""
+"Questo richiede che l'agente Icinga sia installato. Esso genera e firma il "
+"suo certificato e crea una configurazione minimale icinga2.conf per far sì che "
+"l'agente sia collegato al suo corrispondente elemento padre."
+
+#: application/forms/IcingaServiceForm.php:373
+#, php-format
+msgid ""
+"This service belongs to the %s Service Set. Still, you might want to "
+"override the following properties for this host only."
+msgstr ""
+"Questo servizio appartiene al set di servizi %s. Tuttavia, potreste voler "
+"sovrascrivere le seguenti proprietà solo per questo host."
+
+#: application/forms/IcingaServiceForm.php:417
+#, php-format
+msgid ""
+"This service belongs to the service set \"%s\". Still, you might want to "
+"change the following properties for this host only."
+msgstr ""
+"Questo servizio appartiene al set \"%s\". Tuttavia, le seguenti proprietà "
+"possono essere modificate solo per questo specifico host."
+
+#: application/forms/IcingaServiceForm.php:354
+msgid ""
+"This service has been generated in an automated way, but still allows you to "
+"override the following properties in a safe way."
+msgstr ""
+"Questo servizio è stato creato da un automatismo, ma permette comunque di "
+"sovrascrivere le seguenti proprietà in modo sicuro."
+
+#: application/forms/IcingaServiceForm.php:360
+#, php-format
+msgid ""
+"This service has been generated using the %s apply rule, assigned where %s"
+msgstr ""
+"Questo servizio è stato creato dalla apply rule %s, e viene "
+"assegnato dove %s"
+
+#: application/forms/IcingaServiceForm.php:385
+#, php-format
+msgid ""
+"This service has been inherited from %s. Still, you might want to change the "
+"following properties for this host only."
+msgstr ""
+"Questo servizio è stato ereditato da %s. Tuttavia, potreste voler cambiare "
+"le seguenti proprietà solo per questo host."
+
+#: library/Director/Web/Table/IcingaServiceSetServiceTable.php:184
+#, php-format
+msgid "This set has been inherited from %s"
+msgstr "Questo set è stato ereditato da %s"
+
+#: library/Director/Dashboard/Dashlet/KickstartDashlet.php:17
+msgid ""
+"This synchronizes Icinga Director to your Icinga 2 infrastructure. A new run "
+"should be triggered on infrastructure changes"
+msgstr ""
+"Questo sincronizza il Director Icinga con la vostra infrastruttura di Icinga "
+"2. In caso di modifiche all'infrastruttura, eseguire una nuova sincronizzazione "
+
+
+#: library/Director/Web/Table/TemplateUsageTable.php:103
+msgid "This template is not in use"
+msgstr "Questo template non è in uso"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:918
+#, php-format
+msgid "This template is still in use by %d other objects"
+msgstr "Questo template è ancora in uso da %d altri oggetti"
+
+#: application/forms/IcingaTemplateChoiceForm.php:51
+msgid "This will be shown as a label for the given choice"
+msgstr "Questo sarà mostrato come etichetta per la scelta data"
+
+#: application/forms/DirectorDatalistEntryForm.php:33
+msgid "This will be the visible caption for this entry"
+msgstr "Questa sarà la didascalia visibile per questa voce"
+
+#: library/Director/Web/SelfService.php:123
+#, fuzzy
+msgid "This will invalidate the former key"
+msgstr "Questo invaliderà la chiave precedente"
+
+#: library/Director/Web/SelfService.php:224
+msgid "Ticket"
+msgstr "Ticket"
+
+#: application/forms/SyncRuleForm.php:21
+msgid "Time Period"
+msgstr "Periodo di tempo"
+
+#: application/forms/BasketForm.php:28
+msgid "Time Periods"
+msgstr "Periodi di tempo"
+
+#: application/forms/IcingaUserForm.php:158
+#: application/forms/DirectorJobForm.php:60
+#: application/forms/IcingaDependencyForm.php:141
+#: application/forms/IcingaNotificationForm.php:232
+msgid "Time period"
+msgstr "Periodo temporale"
+
+#: application/controllers/TimeperiodController.php:17
+#: application/controllers/ScheduledDowntimeController.php:17
+msgid "Time period ranges"
+msgstr "Fasce nel periodo temporale"
+
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:56
+#, php-format
+msgid "Time range \"%s\" has been removed from %s"
+msgstr "L'intervallo di tempo \"%s\" è stato eliminato da %s"
+
+#: application/forms/SyncPropertyForm.php:320
+msgid "Time ranges"
+msgstr "Intervalli temporali"
+
+#: application/forms/IcingaCommandForm.php:70
+msgid "Timeout"
+msgstr "Timeout"
+
+#: library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php:13
+msgid "Timeperiod Templates"
+msgstr "Templates per periodo temporale"
+
+#: application/forms/IcingaScheduledDowntimeRangeForm.php:29
+#: library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php:16
+#: library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php:13
+#: library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php:52
+#: library/Director/Web/Table/IcingaTimePeriodRangeTable.php:46
+msgid "Timeperiods"
+msgstr "Periodi di tempo"
+
+#: application/forms/IcingaTimePeriodRangeForm.php:28
+msgid "Timerperiods"
+msgstr "Periodi di tempo"
+
+#: library/Director/Web/Table/ImportrunTable.php:31
+msgid "Timestamp"
+msgstr "Data e ora"
+
+#: application/forms/SelfServiceSettingsForm.php:46
+#: application/forms/SelfServiceSettingsForm.php:154
+msgid ""
+"To ensure downloaded packages are build by the Icinga Team and not "
+"compromised by third parties, you will be able to provide an array of SHA1 "
+"hashes here. In case you have defined any hashses, the module will not "
+"continue with updating / installing the Agent in case the SHA1 hash of the "
+"downloaded MSI package is not matching one of the provided hashes of this "
+"setting"
+msgstr ""
+"Per garantire che i pacchetti scaricati siano compilati dal Team di Icinga e "
+"non siano compromessi da terze parti, si potrà fornire un array di hash SHA1 "
+"qui. Nel caso abbiate definito degli hash, il modulo non continuerà con "
+"l'aggiornamento / installazione dell'agente nel caso in cui l'hash SHA1 del "
+"pacchetto MSI scaricato non corrisponda ad uno degli hash forniti di questa "
+"impostazione"
+
+#: library/Director/Web/SelfService.php:197
+msgid "Top Down"
+msgstr "Top Down"
+
+#: library/Director/Web/Table/TemplateUsageTable.php:57
+msgid "Total"
+msgstr "Totale"
+
+#: application/forms/SelfServiceSettingsForm.php:31
+msgid "Transform Host Name"
+msgstr "Trasformare Nome Host"
+
+#: application/forms/SelfServiceSettingsForm.php:37
+msgid "Transform to lowercase"
+msgstr "Trasformare in minuscola"
+
+#: application/forms/SelfServiceSettingsForm.php:38
+msgid "Transform to uppercase"
+msgstr "Trasformare in maiuscola"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1632
+msgid "Transition types"
+msgstr "Tipi di transizione"
+
+#: library/Director/Web/ActionBar/TemplateActionBar.php:30
+msgid "Tree"
+msgstr "Albero"
+
+#: application/forms/ImportRunForm.php:23
+msgid "Trigger Import Run"
+msgstr "Attivare l'importazione"
+
+#: application/forms/SyncRunForm.php:23
+msgid "Trigger this Sync"
+msgstr "Attivare questa sincronizzazione"
+
+#: application/forms/ImportRunForm.php:45
+msgid "Triggering this Import Source failed"
+msgstr "L'avvio di questa fonte di importazione non è riuscito"
+
+#: library/Director/Dashboard/Dashlet/SettingsDashlet.php:17
+msgid "Tweak some global Director settings"
+msgstr "Modificare alcune impostazioni globali del Director"
+
+#: library/Director/Web/Table/CoreApiFieldsTable.php:75
+msgid "Type"
+msgstr "Tipo"
+
+#: application/controllers/InspectController.php:84
+msgid "Type attributes"
+msgstr "Tipo attributi"
+
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:27
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:28
+msgid "URL component"
+msgstr "Componente URL"
+
+#: application/forms/DeployFormsBug7530.php:70
+msgid "Unable to detect your Icinga 2 Core version"
+msgstr "Impossibile rilevare la versione Core di Icinga 2"
+
+#: application/forms/SyncPropertyForm.php:167
+#: library/Director/DataType/DataTypeSqlQuery.php:27
+#, php-format
+msgid "Unable to fetch data: %s"
+msgstr "Impossibile recuperare i dati: %s"
+
+#: application/forms/IcingaHostForm.php:364
+msgid ""
+"Unable to store a host with the given properties because of insufficient "
+"permissions"
+msgstr ""
+"Impossibile memorizzare un host con le proprietà date a causa di "
+"autorizzazioni insufficienti"
+
+#: application/forms/KickstartForm.php:339
+#, php-format
+msgid ""
+"Unable to store the configuration to \"%s\". Please check file permissions "
+"or manually store the content shown below"
+msgstr ""
+"Impossibile memorizzare la configurazione in \"%s\". Si prega di controllare "
+"i permessi dei file o di memorizzare manualmente il contenuto mostrato qui "
+"sotto"
+
+#: library/Director/Db/Housekeeping.php:49
+msgid "Undeployed configurations"
+msgstr "Configurazioni non distribuite"
+
+#: library/Director/IcingaConfig/StateFilterSet.php:27
+msgid "Unknown"
+msgstr "Sconosciuto"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:136
+msgid "Unknown, failed to collect related information"
+msgstr "Sconosciuto, non ha raccolto informazioni correlate"
+
+#: library/Director/Web/Widget/DeploymentInfo.php:134
+msgid "Unknown, still waiting for config check outcome"
+msgstr ""
+"Sconosciuto, ancora in attesa dell'esito del controllo della configurazione"
+
+#: library/Director/Db/Housekeeping.php:53
+msgid "Unlinked imported properties"
+msgstr "Proprietà importate non collegate"
+
+#: library/Director/Db/Housekeeping.php:51
+msgid "Unlinked imported row sets"
+msgstr "Set di file importate non collegate"
+
+#: library/Director/Db/Housekeeping.php:52
+msgid "Unlinked imported rows"
+msgstr "Righe importate non collegate"
+
+#: application/controllers/PhperrorController.php:21
+#: application/controllers/PhperrorController.php:59
+msgid "Unsatisfied dependencies"
+msgstr "Le dipendenze non sono soddisfatte"
+
+#: library/Director/Db/Housekeeping.php:50
+msgid "Unused rendered files"
+msgstr "Alcuni file generati non sono in uso"
+
+#: library/Director/IcingaConfig/StateFilterSet.php:20
+msgid "Up"
+msgstr "Su"
+
+#: application/forms/IcingaTimePeriodForm.php:25
+msgid "Update Method"
+msgstr "Metodo di aggiornamento"
+
+#: application/forms/SyncRuleForm.php:52
+msgid "Update Policy"
+msgstr "Politica di aggiornamento"
+
+#: application/forms/DeployFormsBug7530.php:105
+msgid "Upgrading Icinga 2 - Confic Sync: Zones in Zones"
+msgstr "Aggiornamento di Icinga 2 - Confic Sync: Zone in zone"
+
+#: application/forms/DeployFormsBug7530.php:107
+msgid "Upgrading documentation"
+msgstr "Aggiornamento della documentazione"
+
+#: application/controllers/BasketsController.php:26
+#: application/forms/BasketUploadForm.php:43
+msgid "Upload"
+msgstr "Carica"
+
+#: application/controllers/BasketController.php:120
+msgid "Upload a Basket"
+msgstr "Carica un Basket"
+
+#: application/controllers/BasketController.php:121
+msgid "Upload a Configuration Basket"
+msgstr "Caricare una configurazione Basket "
+
+#: library/Director/Web/Controller/ObjectController.php:302
+msgid "Usage"
+msgstr "Utilizzo"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:101
+#, php-format
+msgid "Usage (%s)"
+msgstr "Utilizzo (%s)"
+
+#: application/forms/SelfServiceSettingsForm.php:71
+msgid "Use a local file or network share"
+msgstr "Utilizzare una directory locale o una condivisione di rete"
+
+#: library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:18
+msgid "Use lowercase first"
+msgstr "Convertire solo in minuscole"
+
+#: application/forms/SyncPropertyForm.php:272
+msgid "Used sources"
+msgstr "Fonti utilizzate"
+
+#: application/forms/SyncRuleForm.php:17
+#: library/Director/TranslationDummy.php:17
+msgid "User"
+msgstr "Utente"
+
+#: application/forms/SyncRuleForm.php:18
+msgid "User Group"
+msgstr "Gruppo utenti"
+
+#: library/Director/Dashboard/Dashlet/UserGroupsDashlet.php:11
+msgid "User Groups"
+msgstr "Gruppi utenti"
+
+#: library/Director/Dashboard/Dashlet/UserTemplateDashlet.php:13
+msgid "User Templates"
+msgstr "Templates utente"
+
+#: application/forms/IcingaNotificationForm.php:156
+#: library/Director/DataType/DataTypeDirectorObject.php:60
+msgid "User groups"
+msgstr "Gruppi utenti"
+
+#: application/forms/IcingaUserForm.php:113
+msgid ""
+"User groups that should be directly assigned to this user. Groups can be "
+"useful for various reasons. You might prefer to send notifications to groups "
+"instead of single users"
+msgstr ""
+"Gruppi di utenti che dovrebbero essere assegnati direttamente a questo "
+"utente. I gruppi possono essere utilizzati per diversi scopi. Potrebbe "
+"essere utile inviare le notifiche a gruppi invece che a singoli utenti"
+
+#: application/forms/IcingaNotificationForm.php:158
+msgid "User groups that should be notified by this notifications"
+msgstr "Gruppi di utenti che devono essere notificati"
+
+#: application/forms/IcingaUserForm.php:194
+msgid "User properties"
+msgstr "Proprietà dell'utente"
+
+#: application/forms/IcingaUserForm.php:22
+msgid "User template name"
+msgstr "Nome del template utente"
+
+#: application/forms/IcingaUserGroupForm.php:17
+msgid "Usergroup"
+msgstr "Gruppo utente"
+
+#: library/Director/Import/ImportSourceCoreApi.php:63
+msgid "Usergroups"
+msgstr "Gruppi utente"
+
+#: application/forms/IcingaUserForm.php:28
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:67
+#: library/Director/Import/ImportSourceRestApi.php:149
+msgid "Username"
+msgstr "Nome utente"
+
+#: application/forms/IcingaNotificationForm.php:131
+#: library/Director/Web/Table/CustomvarVariantsTable.php:62
+#: library/Director/Web/Table/CustomvarTable.php:47
+#: library/Director/Import/ImportSourceCoreApi.php:62
+#: library/Director/DataType/DataTypeDirectorObject.php:59
+msgid "Users"
+msgstr "Utenti"
+
+#: library/Director/Dashboard/Dashlet/UserObjectDashlet.php:16
+#: library/Director/Dashboard/Dashlet/UsersDashlet.php:13
+msgid "Users / Contacts"
+msgstr "Utenti / Contatti"
+
+#: application/forms/IcingaNotificationForm.php:133
+msgid "Users that should be notified by this notifications"
+msgstr "Utenti che devono essere notificati"
+
+#: library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php:17
+msgid ""
+"Using Apply Rules a Service can be applied to multiple hosts at once, based "
+"on filters dealing with any combination of their properties"
+msgstr ""
+"Utilizzando le Regole di applicazione, un servizio può essere applicato a più "
+"host contemporaneamente sulla base di filtri che trattano qualsiasi "
+"combinazione delle loro proprietà"
+
+#: application/forms/IcingaHostSelfServiceForm.php:44
+#: application/forms/IcingaHostForm.php:317
+msgid "Usually your hosts main IPv6 address"
+msgstr "L'indirizzo IPv6 principale del vostro host"
+
+#: application/forms/IcingaCommandArgumentForm.php:50
+#: application/forms/IcingaCommandArgumentForm.php:59
+#: application/forms/CustomvarForm.php:21
+#: application/forms/IcingaHostVarForm.php:27
+#: application/forms/IcingaServiceVarForm.php:27
+#: library/Director/Web/Table/IcingaCommandArgumentTable.php:46
+msgid "Value"
+msgstr "Valore"
+
+#: application/forms/IcingaCommandArgumentForm.php:36
+msgid "Value type"
+msgstr "Tipo valore"
+
+#: library/Director/Web/Table/CustomvarVariantsTable.php:56
+msgid "Variable Value"
+msgstr "Valore Variabile"
+
+#: application/forms/CustomvarForm.php:16
+#: library/Director/Web/Table/CustomvarTable.php:41
+msgid "Variable name"
+msgstr "Nome Variabile"
+
+#: library/Director/Import/ImportSourceRestApi.php:103
+msgid "Verify Host"
+msgstr "Verifica Host"
+
+#: library/Director/Import/ImportSourceRestApi.php:96
+msgid "Verify Peer"
+msgstr "Verifica Peer"
+
+#: library/Director/DataType/DataTypeString.php:24
+msgid "Visibility"
+msgstr "Visibilità"
+
+#: library/Director/DataType/DataTypeString.php:26
+msgid "Visible"
+msgstr "Visibile"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1445
+msgid "Volatile"
+msgstr "Volatile"
+
+#: application/forms/IcingaCommandForm.php:81
+msgid ""
+"WARNING, this can allow shell script injection via custom variables used in "
+"command."
+msgstr ""
+"ATTENZIONE, questo può consentire l'iniezione di script via shell tramite "
+"variabili personalizzate utilizzate nel commando."
+
+#: library/Director/Dashboard/TimeperiodsDashboard.php:20
+msgid ""
+"Want to define to execute specific checks only withing specific time "
+"periods? Get mobile notifications only out of office hours, but mail "
+"notifications all around the clock? Time Periods allow you to tackle those "
+"and similar requirements."
+msgstr ""
+"Volete impostare l'esecuzione di controlli specifici solo in sepcifici periodi di tempo? "
+"Ricevere notifiche sul cellulare solo fuori dall'orario d'ufficio, ma "
+"notifiche via e-mail 24 ore su 24? I Periodi di temporali consentono di "
+"soddisfare questi e requisiti simili."
+
+#: library/Director/IcingaConfig/StateFilterSet.php:25
+msgid "Warning"
+msgstr "Attenzione"
+
+#: application/forms/DeployFormsBug7530.php:90
+#, php-format
+msgid ""
+"Warning: you're running Icinga v2.11.0 and our configuration looks like you "
+"could face issue %s. We're already working on a solution. The GitHub Issue "
+"and our %s contain related details."
+msgstr ""
+"Attenzione: state utilizzando Icinga v2.11.0 ed è possibile che riscontriate "
+"il problema %s. Stiamo già lavorando ad una soluzione. Per maggiori dettagli"
+"fate riferimento a GitHub e al nostro %s."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1104
+msgid ""
+"What kind of object this should be. Templates allow full access to any "
+"property, they are your building blocks for \"real\" objects. External "
+"objects should usually not be manually created or modified. They allow you "
+"to work with objects locally defined on your Icinga nodes, while not "
+"rendering and deploying them with the Director. Apply rules allow to assign "
+"services, notifications and groups to other objects."
+msgstr ""
+"Che tipo di oggetto dovrebbe essere. I templates consentono l'accesso completo "
+"a qualsiasi proprietà, sono i vostri elementi costitutivi per gli oggetti "
+"\"reali\". Gli oggetti esterni di solito non dovrebbero essere creati o "
+"modificati manualmente. Permettono di lavorare con oggetti definiti "
+"localmente sui nodi di Icinga, senza renderli e distribuirli con il "
+"Direttore. Le regole di applicazione permettono di assegnare servizi, "
+"notifiche e gruppi ad altri oggetti."
+
+#: library/Director/PropertyModifier/PropertyModifierMap.php:28
+msgid ""
+"What should happen if the lookup key does not exist in the data list? You "
+"could return a null value, keep the unmodified imported value or interrupt "
+"the import process"
+msgstr ""
+"Cosa potrebbe succedere se la chiave di ricerca non esistesse nella lista "
+"data? Si potrebbe restituire un valore nullo, mantenere il valore importato "
+"non modificato o interrompere il processo di importazione"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:24
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:24
+msgid "What should happen when the given string is empty?"
+msgstr "Cosa dovrebbe accadere quando la stringa data fosse vuota?"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:66
+msgid "What should happen when the result array is empty?"
+msgstr "Cosa dovrebbe succedere se l'array risultante fosse vuoto?"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:50
+msgid "What should happen when the specified element is not available?"
+msgstr "Cosa deve succedere se l'articolo specificato non fosse disponibile?"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:53
+msgid "What should happen with matching elements?"
+msgstr "Cosa dovrebbe accadere con gli elementi selezionati?"
+
+#: library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:55
+msgid ""
+"What should happen with the row, when this property matches the given "
+"expression?"
+msgstr ""
+"Cosa dovrebbe succedere alla riga se l'espressione data corrispondesse a questa "
+"proprietà?"
+
+#: library/Director/PropertyModifier/PropertyModifierDnsRecords.php:32
+msgid "What should we do if the DNS lookup fails?"
+msgstr "Cosa dovrebbe succedere se la risoluzione (DNS) fallisse?"
+
+#: library/Director/PropertyModifier/PropertyModifierParseURL.php:36
+msgid ""
+"What should we do if the URL could not get parsed or component not found?"
+msgstr ""
+"Cosa si deve fare se l'URL non potesse essere analizzato o se non fosse "
+"possibile trovarne i componenti?"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:28
+msgid "What should we do if the desired part does not exist?"
+msgstr "Cosa dovrebbe succedere se la parte desiderata non esistesse?"
+
+#: library/Director/PropertyModifier/PropertyModifierGetHostByName.php:15
+msgid "What should we do if the host (DNS) lookup fails?"
+msgstr "Cosa dobbiamo fare se la ricerca dell'host (DNS) non riuscisse?"
+
+#: library/Director/PropertyModifier/PropertyModifierJsonDecode.php:22
+msgid "What should we do in case we are unable to decode the given string?"
+msgstr ""
+"Cosa dovrebbe succedere se la stringa passata non potesse essere decodificata?"
+
+#: library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:16
+msgid "What should we extract from the DN?"
+msgstr "Cosa dobbiamo estrarre dal DN?"
+
+#: application/forms/BasketForm.php:57
+msgid ""
+"What should we place into this Basket every time we create new snapshot?"
+msgstr ""
+"Cosa dobbiamo mettere in questo Basket ogni volta che creiamo un nuovo "
+"snapshot?"
+
+#: application/forms/SelfServiceSettingsForm.php:21
+#, fuzzy
+msgid "What to use as your Icinga 2 Agent's Host Name"
+msgstr "Hostname da usare per l'Icinga2-Agent?"
+
+#: library/Director/Job/ConfigJob.php:209
+msgid ""
+"When deploying configuration, wait at least this amount of seconds unless "
+"the next deployment should take place"
+msgstr ""
+"Durante la distribuzione della configurazione, attendere almeno qualche secondo "
+"a meno che non si debba procedere con la successiva distribuzione "
+
+
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:21
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:21
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:63
+#, fuzzy
+msgid "When empty"
+msgstr "Quando é vuoto"
+
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:47
+msgid "When not available"
+msgstr "Quando non disponibile"
+
+#: application/forms/IcingaNotificationForm.php:210
+#, fuzzy
+msgid "When the last notification should be sent"
+msgstr "Quando deve essere inviata l'ultima notifica?"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:100
+msgid ""
+"Whether Downtimes should also explicitly be scheduled for all Services "
+"belonging to affected Hosts"
+msgstr ""
+"Se i tempi di inattività devono essere esplicitamente programmati anche per "
+"tutti i servizi appartenenti agli host interessati"
+
+#: application/forms/SettingsForm.php:63
+msgid "Whether all configured Jobs should be disabled"
+msgstr "Se tutti i lavori configurati devono essere disattivati"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1418
+msgid "Whether flap detection is enabled on this object"
+msgstr "Se il riconoscimento dei flap è abilitato su questo oggetto"
+
+#: library/Director/Job/ConfigJob.php:183
+msgid ""
+"Whether rendering should be forced. If not enforced, this job re-renders the "
+"configuration only when there have been activities since the last rendered "
+"config"
+msgstr ""
+"Se l'attivazione debba essere forzata. Se non è forzata, la configurazione "
+"viene attivata solo se le attività sono state eseguite dopo l'ultima attivazione"
+
+#: application/forms/IcingaHostForm.php:94
+#, fuzzy
+msgid "Whether the agent is configured to accept config"
+msgstr "Abilitare l'agente per ricevere la configurazione?"
+
+#: application/forms/IcingaCommandArgumentForm.php:42
+msgid ""
+"Whether the argument value is a string (allowing macros like $host$) or an "
+"Icinga DSL lambda function (will be enclosed with {{ ... }}"
+msgstr ""
+"Il valore dell'argomento è una stringa (permette macro come $host$) o una "
+"funzione lambda di Icinga DSL? (è scritto in {{ ... }}}"
+
+#: application/forms/IcingaServiceForm.php:666
+#, fuzzy
+msgid ""
+"Whether the check commmand for this service should be executed on the Icinga "
+"agent"
+msgstr "Il comando per questo servizio deve essere eseguito sull'Icinga-Agent?"
+
+#: application/forms/IcingaCommandArgumentForm.php:117
+msgid ""
+"Whether the parameter name should not be passed to the command. Per default, "
+"the parameter name (e.g. -H) will be appended, so no need to explicitly set "
+"this to \"No\"."
+msgstr ""
+"Se il nome del parametro deve essere passato al comando. Non è necessario "
+"impostare questo parametro su \"No\", in quanto per default il nome del parametro "
+"viene aggiunto (ad es. -H)."
+
+#: application/forms/IcingaHostForm.php:88
+#, fuzzy
+msgid ""
+"Whether the parent (master) node should actively try to connect to this agent"
+msgstr ""
+"Il Nodo (Master) deve cercare attivamente di connettersi a questo agente?"
+
+#: application/forms/IcingaCommandArgumentForm.php:81
+msgid ""
+"Whether the set_if parameter is a string (allowing macros like $host$) or an "
+"Icinga DSL lambda function (will be enclosed with {{ ... }}"
+msgstr ""
+"Il parametro set_if è una stringa (permette macro come $host$) o una "
+"funzione lambda di Icinga DSL? (è scritto in {{ ... }}}"
+
+#: application/forms/IcingaCommandArgumentForm.php:126
+#, fuzzy
+msgid "Whether this argument should be required"
+msgstr "É necessario questo argomento?"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1446
+#, fuzzy
+msgid "Whether this check is volatile."
+msgstr "Questo controllo è irregolare (volatile)?"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:80
+#: application/forms/IcingaDependencyForm.php:95
+msgid "Whether this dependency should affect hosts or services"
+msgstr "Se questa dipendenza deve interessare gli host o i servizi"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:45
+msgid ""
+"Whether this downtime is fixed or flexible. If unsure please check the "
+"related documentation: https://icinga.com/docs/icinga2/latest/doc/08-"
+"advanced-topics/#downtimes"
+msgstr ""
+"Sia che si tratti di tempi di fermo macchina fissi o flessibili. In caso di "
+"dubbi si prega di consultare la relativa documentazione: https://icinga.com/"
+"docs/icinga2/latest/doc/08-advanced-topics/#downtimes"
+
+#: application/forms/IcingaObjectFieldForm.php:143
+#, fuzzy
+msgid "Whether this field should be mandatory"
+msgstr "Questo Campo deve essere obbligatorio?"
+
+#: application/forms/IcingaHostForm.php:79
+#, fuzzy
+msgid "Whether this host has the Icinga 2 Agent installed"
+msgstr "Questo Host ha installato Icinga2-Agent?"
+
+#: application/forms/IcingaNotificationForm.php:83
+msgid "Whether this notification should affect hosts or services"
+msgstr "Se questa notifica deve riguardare gli host o i servizi"
+
+#: application/forms/IcingaCommandArgumentForm.php:109
+msgid ""
+"Whether this parameter should be repeated when multiple values (read: array) "
+"are given"
+msgstr ""
+"Se questo parametro deve essere ripetuto se vengono specificati più valori "
+"(in un array)"
+
+#: application/forms/IcingaZoneForm.php:24
+msgid ""
+"Whether this zone should be available everywhere. Please note that it rarely "
+"leads to the desired result when you try to distribute global zones in "
+"distrubuted environments"
+msgstr ""
+"Questa zona deve essere disponibile ovunque? Si noti che questa configurazione "
+"raramente porta il risultato desiderato quando si cerca di "
+"distribuire zone globali in ambienti distribuiti"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1394
+#, fuzzy
+msgid "Whether to accept passive check results for this object"
+msgstr ""
+"I risultati del controllo passivo devono essere accettati per questo Oggetto?"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1388
+#, fuzzy
+msgid "Whether to actively check this object"
+msgstr "Questo Oggetto deve essere controllato attivamente?"
+
+#: application/forms/SelfServiceSettingsForm.php:33
+msgid "Whether to adjust your host name"
+msgstr "Se modificare il nome dell'host"
+
+#: application/forms/IcingaDependencyForm.php:161
+msgid ""
+"Whether to disable checks when this dependency fails. Defaults to false."
+msgstr ""
+"Se disabilitare i controlli quando questa dipendenza fallisce. Le "
+"impostazioni predefinite sono false."
+
+#: application/forms/IcingaDependencyForm.php:169
+msgid ""
+"Whether to disable notifications when this dependency fails. Defaults to "
+"true."
+msgstr ""
+"Se disabilitare le notifiche quando questa dipendenza fallisce. Default é sí."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1406
+#, fuzzy
+msgid "Whether to enable event handlers this object"
+msgstr "Attivare Event-handler per questo Oggetto?"
+
+#: application/forms/IcingaDependencyForm.php:177
+msgid ""
+"Whether to ignore soft states for the reachability calculation. Defaults to "
+"true."
+msgstr ""
+"Se gli stati soft debbano essere considerati per il calcolo "
+"dell'accessibilità. Default é sí."
+
+#: application/forms/IcingaTimePeriodForm.php:77
+msgid "Whether to prefer timeperiods includes or excludes. Default to true."
+msgstr "Se preferire i periodi di tempo include o esclude. Default é sí."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1412
+#, fuzzy
+msgid "Whether to process performance data provided by this object"
+msgstr "I dati sulle prestazioni di questo Oggetto devono essere elaborati?"
+
+#: application/forms/SyncRuleForm.php:70
+msgid ""
+"Whether to purge existing objects. This means that objects of the same type "
+"will be removed from Director in case they no longer exist at your import "
+"source."
+msgstr ""
+"Se eliminare gli oggetti esistenti. Ciò significa che gli oggetti dello "
+"stesso tipo saranno rimossi dal Director nel caso in cui non esistano più "
+"sulla fonte di importazione."
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1400
+#, fuzzy
+msgid "Whether to send notifications for this object"
+msgstr "Le Notifiche devono essere inviate per questo Oggetto?"
+
+#: application/forms/IcingaUserForm.php:90
+#, fuzzy
+msgid "Whether to send notifications for this user"
+msgstr "Le Notifiche devono essere inviate per questo Utente?"
+
+#: library/Director/Import/ImportSourceRestApi.php:74
+msgid "Whether to use encryption when talking to the REST API"
+msgstr "Se utilizzare la crittografia quando si utilizzano le REST API"
+
+#: library/Director/Import/ImportSourceRestApi.php:98
+msgid ""
+"Whether we should check that our peer's certificate has been signed by a "
+"trusted CA. This is strongly recommended."
+msgstr ""
+"Se dobbiamo controllare che il certificato del nostro peer sia stato firmato "
+"da una CA di fiducia. Scelta raccomandato."
+
+#: library/Director/Import/ImportSourceRestApi.php:105
+msgid "Whether we should check that the certificate matches theconfigured host"
+msgstr ""
+"Se dobbiamo controllare che il certificato corrisponda all'host configurato"
+
+#: application/forms/SyncPropertyForm.php:118
+msgid ""
+"Whether you want to merge or replace the destination field. Makes no "
+"difference for strings"
+msgstr ""
+"Il campo di destinazione deve essere unito o sostituito? Non fa differenza "
+"per le stringhe di caratteri"
+
+#: application/forms/DirectorDatalistEntryForm.php:24
+msgid ""
+"Will be stored as a custom variable value when this entry is chosen from the "
+"list"
+msgstr ""
+"Sarà memorizzato come valore di variabile personalizzata quando questa voce "
+"viene scelta dalla lista"
+
+#: library/Director/Import/ImportSourceRestApi.php:151
+msgid "Will be used for SOAP authentication against your vCenter"
+msgstr "Verrà utilizzato per l'autenticazione SOAP contro il vCenter"
+
+#: library/Director/Web/SelfService.php:225
+msgid "Windows Kickstart Script"
+msgstr "Windows Kickstart Script"
+
+#: application/forms/DirectorDatafieldForm.php:53
+msgid "Wipe related vars"
+msgstr "Pulire le variabili correlate"
+
+#: application/forms/IcingaScheduledDowntimeForm.php:98
+msgid "With Services"
+msgstr "Con servizi"
+
+#: library/Director/Dashboard/Dashlet/ActivityLogDashlet.php:17
+msgid "Wondering about what changed why? Track your changes!"
+msgstr ""
+"Qui potete vedere cosa è stato cambiato. Tieni traccia dei tuoi cambiamenti!"
+
+#: application/views/helpers/FormDataFilter.php:525
+msgid "Wrap this expression into an operator"
+msgstr "Inserire questa espressione in un operatore"
+
+#: application/forms/IcingaDeleteObjectForm.php:17
+#, php-format
+msgid "YES, please delete \"%s\""
+msgstr "SÌ, cancellare \"%s\"."
+
+#: application/forms/IcingaZoneForm.php:30
+#: application/forms/SelfServiceSettingsForm.php:226
+#: application/forms/SettingsForm.php:59 application/forms/SettingsForm.php:74
+#: application/forms/SettingsForm.php:96
+#: library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:25
+#: library/Director/Job/ImportJob.php:101 library/Director/Job/SyncJob.php:101
+#: library/Director/Job/ConfigJob.php:189
+#: library/Director/Job/ConfigJob.php:201
+msgid "Yes"
+msgstr "Sí "
+
+#: application/controllers/BasketsController.php:41
+msgid ""
+"You can create Basket snapshots at any time, this will persist a serialized "
+"representation of all involved objects at that moment in time. Snapshots can "
+"be exported, imported, shared and restored - to the very same or another "
+"Director instance."
+msgstr ""
+"È possibile creare Basket snapshot in qualsiasi momento, in questo modo "
+"persisterà una rappresentazione serializzata di tutti gli oggetti coinvolti "
+"in quel momento nel tempo. Gli snapshot possono essere esportati, importati, "
+"condivisi e ripristinati - nella stessa o in un'altra istanza del Director."
+
+#: library/Director/Web/SelfService.php:136
+msgid ""
+"You can stop sharing a Template at any time. This will immediately "
+"invalidate the former key."
+msgstr ""
+"La condivisione di un Template puo' essere sospesa in qualsiasi momento. Questo "
+"invaliderà immediatamente la precedente chiave."
+
+#: library/Director/Job/ImportJob.php:94 library/Director/Job/SyncJob.php:94
+msgid ""
+"You could immediately apply eventual changes or just learn about them. In "
+"case you do not want them to be applied immediately, defining a job still "
+"makes sense. You will be made aware of available changes in your Director "
+"GUI."
+msgstr ""
+"Eventuali modifiche potrebbero venire immediatamente applicate "
+"o l'utente potrebbe essere avvisato dei cambiamenti. "
+"Definire un processo ha senso anche nel caso in cui non si desideri "
+"applicare immediatamente le modifiche. L'utente sarà "
+"informato dei cambiamenti effettuati nella GUI del Director."
+
+#: application/forms/SelfServiceSettingsForm.php:61
+msgid ""
+"You might want to let the generated Powershell script install the Icinga 2 "
+"Agent in an automated way. If so, please choose where your Windows nodes "
+"should fetch the Agent installer"
+msgstr ""
+"Si consiglia di lasciare che lo script Powershell generato installi l'agente "
+"Icinga 2 in modo automatico. Se è così, scegliete dove i vostri nodi di "
+"Windows devono andare a prendere il programma di installazione dell'Agent"
+
+#: application/forms/IcingaObjectFieldForm.php:169
+msgid ""
+"You might want to show this field only when certain conditions are met. "
+"Otherwise it will not be available and values eventually set before will be "
+"cleared once stored"
+msgstr ""
+"Si consiglia di mostrare questo campo solo quando sono soddisfatte "
+"determinate condizioni. Se queste condizioni non sono soddisfatte, il campo "
+"non verrà visualizzato e le proprietà associate verranno rimosse quando il "
+"campo verrà salvato"
+
+#: application/forms/ImportRowModifierForm.php:44
+msgid ""
+"You might want to write the modified value to another (new) property. This "
+"property name can be defined here, the original property would remain "
+"unmodified. Please leave this blank in case you just want to modify the "
+"value of a specific property"
+msgstr ""
+"Potreste voler scrivere il valore modificato su un'altra (nuova) proprietà. "
+"Il nome di questa proprietà può essere definito qui, la proprietà originale "
+"rimane invariata. Si prega di lasciare il campo vuoto nel caso in cui si "
+"voglia modificare il valore di una specifica proprietà"
+
+#: application/controllers/SyncruleController.php:119
+#, php-format
+msgid "You must define some %s before you can run this Sync Rule"
+msgstr ""
+"È necessario definire alcuni %s prima di poter eseguire questa regola di "
+"sincronizzazione"
+
+#: application/forms/KickstartForm.php:153
+msgid "Your Icinga 2 API username"
+msgstr "Icinga 2 API username"
+
+#: library/Director/Import/ImportSourceLdap.php:49
+msgid ""
+"Your LDAP search base. Often something like OU=Users,OU=HQ,DC=your,"
+"DC=company,DC=tld"
+msgstr ""
+"La vostra base di ricerca LDAP. Spesso qualcosa come OU=Utenti,OU=HQ,DC=tuo,"
+"DC=azienda,DC=dettaglio"
+
+#: application/forms/KickstartForm.php:94
+msgid ""
+"Your configuration looks good. Still, you might want to re-run this "
+"kickstart wizard to (re-)import modified or new manually defined Command "
+"definitions or to get fresh new ITL commands after an Icinga 2 Core upgrade."
+msgstr ""
+"La vostra configurazione sembra buona. Tuttavia, si potrebbe voler eseguire "
+"nuovamente questa procedura guidata di kickstart per (re)importare le "
+"definizioni dei comandi modificate o nuove definite manualmente o per "
+"ottenere nuovi comandi ITL dopo un aggiornamento di Icinga 2 Core."
+
+#: application/forms/KickstartForm.php:72
+#, fuzzy, php-format
+msgid "Your database looks good, you are ready to %s"
+msgstr ""
+"Il database sembra buono. Il Director di Icinga dovrebbe essere pronto per "
+"%s "
+
+#: application/forms/KickstartForm.php:106
+#, fuzzy
+msgid ""
+"Your installation of Icinga Director has not yet been prepared for "
+"deployments. This kickstart wizard will assist you with setting up the "
+"connection to your Icinga 2 server."
+msgstr ""
+"Questa installazione di Icinga Director non è stata ancora preparata per il "
+"rollout della configurazione. Questo wizard di kickstart vi aiuterà a "
+"configurare la connessione al server di Icinga 2"
+
+#: library/Director/Web/Form/DirectorObjectForm.php:1331
+msgid "Your regular check interval"
+msgstr "Il vostro regolare intervallo di controllo"
+
+#: library/Director/PropertyModifier/PropertyModifierReplace.php:20
+msgid "Your replacement string"
+msgstr "La stringa da sostituire"
+
+#: application/forms/IcingaTemplateChoiceForm.php:67
+msgid "Your users will be allowed to choose among those templates"
+msgstr "I vostri utenti potranno scegliere tra questi templates"
+
+#: application/forms/SyncRuleForm.php:26
+#: library/Director/TranslationDummy.php:15
+#: library/Director/Web/Table/ObjectsTableEndpoint.php:21
+#, fuzzy
+msgid "Zone"
+msgstr "Zone"
+
+#: application/forms/IcingaZoneForm.php:14
+msgid "Zone name"
+msgstr "Nome della zona"
+
+#: application/forms/IcingaCommandForm.php:111
+#: application/forms/IcingaUserForm.php:76
+#: application/forms/IcingaDependencyForm.php:61
+#: application/forms/IcingaNotificationForm.php:65
+#: application/forms/IcingaUserGroupForm.php:42
+msgid "Zone settings"
+msgstr "Impostazioni Zona"
+
+#: library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php:13
+#: library/Director/Import/ImportSourceCoreApi.php:64
+msgid "Zones"
+msgstr "Zone"
+
+#: application/forms/SyncPropertyForm.php:347
+msgid "a list"
+msgstr "un elenco"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:87
+msgid "all"
+msgstr "tutto"
+
+#: application/controllers/HostController.php:481
+#: application/controllers/BasketController.php:96
+#: application/controllers/BasketController.php:114
+#: application/controllers/BasketController.php:348
+#: application/controllers/ServiceController.php:137
+#: application/controllers/ServiceController.php:198
+#: application/controllers/DataController.php:108
+#: application/controllers/ImportsourceController.php:277
+#: application/controllers/SyncruleController.php:571
+#: library/Director/Web/Controller/ActionController.php:160
+#: library/Director/Web/Controller/ObjectController.php:214
+#: library/Director/Web/Controller/ObjectController.php:519
+#: library/Director/Web/ActionBar/DirectorBaseActionBar.php:35
+msgid "back"
+msgstr "indietro"
+
+#: application/locale/translateMe.php:11
+msgid "critical"
+msgstr "critico"
+
+#: application/views/scripts/phperror/dependencies.phtml:47
+msgid "disabled"
+msgstr "disattivato"
+
+#: application/controllers/DaemonController.php:42
+msgid "documentation"
+msgstr "documentazione"
+
+#: application/locale/translateMe.php:6
+msgid "down"
+msgstr "giù"
+
+#: application/forms/IcingaCommandArgumentForm.php:27
+msgid "e.g. -H or --hostname, empty means \"skip_key\""
+msgstr "ad es. -H o --hostname, vuoto significa \"skip_key\"."
+
+#: application/forms/IcingaCommandArgumentForm.php:61
+msgid "e.g. 5%, $host.name$, $lower$%:$upper$%"
+msgstr "ad es. 5%, $host.name$, $lower$%:$upper$%"
+
+#: application/forms/SyncPropertyForm.php:164
+msgid "failed to fetch"
+msgstr "Raccolta fallita"
+
+#: application/forms/KickstartForm.php:259 library/Director/Util.php:177
+msgid "here"
+msgstr "qui"
+
+#: application/forms/IcingaHostVarForm.php:23
+msgid "host var name"
+msgstr "host var name"
+
+#: application/forms/IcingaHostVarForm.php:28
+msgid "host var value"
+msgstr "host var value"
+
+#: application/views/scripts/phperror/dependencies.phtml:50
+msgid "missing"
+msgstr "mancante"
+
+#: application/controllers/BasketController.php:298
+msgid "modified"
+msgstr "modificato"
+
+#: application/views/scripts/phperror/dependencies.phtml:56
+msgid "more"
+msgstr "altro"
+
+#: application/controllers/BasketController.php:276
+msgid "new"
+msgstr "nuovo"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:71
+msgid "no"
+msgstr "no"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:283
+msgid "no related group exists"
+msgstr "non esiste un gruppo collegato"
+
+#: application/locale/translateMe.php:9
+msgid "ok"
+msgstr "ok"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:315
+msgid "on host"
+msgstr "su host"
+
+#: library/Director/Web/Widget/ActivityLogInfo.php:297
+msgid "on service set"
+msgstr "sul set di servizio"
+
+#: library/Director/Dashboard/Dashlet/Dashlet.php:285
+msgid "one related group exists"
+msgstr "esiste un gruppo collegato"
+
+#: application/locale/translateMe.php:8
+msgid "pending"
+msgstr "in attesa"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:29
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:29
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:71
+#: library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:55
+msgid "return NULL"
+msgstr "restituisce NULL"
+
+#: library/Director/PropertyModifier/PropertyModifierRegexSplit.php:28
+#: library/Director/PropertyModifier/PropertyModifierSplit.php:28
+#: library/Director/PropertyModifier/PropertyModifierArrayFilter.php:70
+msgid "return an empty array"
+msgstr "restituisce un array vuoto"
+
+#: application/forms/IcingaServiceVarForm.php:23
+msgid "service var name"
+msgstr "service var name"
+
+#: application/forms/IcingaServiceVarForm.php:28
+msgid "service var value"
+msgstr "service var valore"
+
+#: application/controllers/BasketController.php:302
+msgid "unchanged"
+msgstr "invariato"
+
+#: application/locale/translateMe.php:12
+msgid "unknown"
+msgstr "sconoscuto"
+
+#: application/locale/translateMe.php:7
+msgid "unreachable"
+msgstr "irraggiungibile"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:82
+msgid "unsupported"
+msgstr "non supportato"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:89
+msgid "unused"
+msgstr "inutilizzato"
+
+#: application/locale/translateMe.php:5
+msgid "up"
+msgstr "su"
+
+#: library/Director/Web/Widget/AdditionalTableActions.php:88
+msgid "used"
+msgstr "usato"
+
+#: application/forms/IcingaHostVarForm.php:33
+#: application/forms/IcingaServiceVarForm.php:33
+msgid "value format"
+msgstr "formato valore"
+
+#: library/Director/Web/Table/GroupMemberTable.php:61
+#: library/Director/Web/Table/GroupMemberTable.php:66
+msgid "via"
+msgstr "via"
+
+#: application/locale/translateMe.php:10
+msgid "warning"
+msgstr "Attenzione"
+
+#: library/Director/Web/Widget/BackgroundDaemonDetails.php:70
+msgid "yes"
+msgstr "sí"
+
+msgid "%s (where %s)"
+msgstr "%s (dove %s)"
+
+msgid "Add Service: %s"
+msgstr "Aggiungi servizio: %s"
+
+msgid "The parent host."
+msgstr "L'host parente."
+
+msgid ""
+"This allows you to configure an assignment filter. Please feel free to "
+"combine as many nested operators as you want"
+msgstr ""
+"Questo permette di configurare un filtro di assegnazione. Sentitevi liberi di "
+"combinare tutti gli operatori annidati che volete"
+
+msgid "This must be an import source column (property)"
+msgstr "Questa deve essere una colonna di fonte di importazione (proprietà)"
+
+msgid "Name for the Icinga timeperiod you are going to create"
+msgstr "Nome per il periodo di Icinga che stai per creare"
+
+msgid "Name for the Icinga timperiod template you are going to create"
+msgstr "Nome per il template del periodo di tempo di Icinga che si sta per creare"
+
+msgid "Please define a Service Template first"
+msgstr "Si prega di definire prima un template di servizio"
+
+msgid "Serviceset"
+msgstr "Service-Set"
+
+msgid "Timeperiod"
+msgstr "Periodo"
+
+msgid "Timeperiod object"
+msgstr "Periodo"
+
+msgid "Timeperiod template"
+msgstr "Template del periodo"
+
+msgid "Timeperiod template name"
+msgstr "Nome del template del periodo"
+
+msgid "Whether this should be a template"
+msgstr "È un template?"
+
+msgid "the display name"
+msgstr "Nome sul display"
+
+msgid "the update method"
+msgstr "metodo di aggiornamento"
+
+msgid "Change priority"
+msgstr "Cambia prioritá"
+
+msgid "Count Query"
+msgstr "Count-Query"
+
+msgid "Move down (lower priority)"
+msgstr "Muovi in basso (abbassa prioritá)"
+
+msgid "Move up (raise priority)"
+msgstr "Muovi in alto (aumenta prioritá)"
+
+msgid "Next page"
+msgstr "Prossima pagina"
+
+msgid "Pagination"
+msgstr "Navigazione della pagina"
+
+msgid "Previous page"
+msgstr "Pagina precedente"
+
+msgid "Prio"
+msgstr "Prio"
+
+msgid "SQL Query"
+msgstr "SQL-Query"
+
+msgid "Search is simple! Try to combine multiple words"
+msgstr "La ricerca è facile! Prova a combinare più parole"
+
+msgid "Search..."
+msgstr "Cerca..."
+
+msgid "Show rows %u to %u of %u"
+msgstr "Mostra righe %u da %u a %u"
+
+msgid "This feature is still experimental"
+msgstr "Questa feature è ancora sperimentale"
+
+msgid "Filter available service sets"
+msgstr "Filtro service sets disponibile"
+
+msgid "Set Members"
+msgstr "Imposta Membri"
+
+msgid "s"
+msgstr "s"
+
+msgid "Activity log entry"
+msgstr "Registrazione del registro delle attività"
+
+msgid "Add a job"
+msgstr "Aggiungi un lavoro"
+
+msgid "Add job"
+msgstr "Aggiungi lavoro"
+
+msgid "Add list"
+msgstr "Aggiungi elenco"
+
+msgid "Apply Icinga %s"
+msgstr "Applica Icinga %s"
+
+msgid "Clone Icinga %s"
+msgstr "Clona Icinga %s"
+
+msgid "Configs"
+msgstr "Configurazioni"
+
+msgid "Create immediately"
+msgstr "Crea subito"
+
+msgid "Deploy to master"
+msgstr "Distrubuisci al master"
+
+msgid "Edit import source"
+msgstr "Modifica fonte importazione"
+
+msgid "Edit sync property rule"
+msgstr "Modifica regola di sincronizzazione"
+
+msgid "Entries"
+msgstr "Voci"
+
+msgid "Icinga "
+msgstr "Icinga "
+
+msgid "Icinga %s"
+msgstr "Icinga %s"
+
+msgid "Job %s"
+msgstr "Lavoro %s"
+
+msgid "No object found"
+msgstr "Nessun oggetto trovato"
+
+msgid "Object name"
+msgstr "Nome oggetto"
+
+msgid "Show unfiltered"
+msgstr "Mostra non filtrato"
+
+msgid "Sync run details"
+msgstr "Dettagli di sincronizzazione"
+
+msgid "Template tree"
+msgstr "Albero dei template"
+
+msgid ""
+"This is an external object. It has been imported from Icinga 2 through "
+"the Core API and cannot be managed with the Icinga Director. It is "
+"however perfectly valid to create objects using this or referring to this "
+"object. You might also want to define related Fields to make work based "
+"on this object more enjoyable."
+msgstr ""
+"Questo è un oggetto esterno. È stato creato da Icinga 2 attraverso l'API di base "
+"e non può essere gestito con il Director di Icinga."
+"È comunque perfettamente valido creare oggetti utilizzando o facendo "
+"riferimento a questo oggetto. È anche possibile creare campi correlati "
+"per facilitare il lavoro."
+
+msgid "Time"
+msgstr "Tempo"
+
+msgid "Service sets"
+msgstr "Service-Sets"
+
+msgid "Allow to see template details"
+msgstr "Permetti di vedere i dettagli del template"
+
+msgid "Allow to use only host templates matching this filter"
+msgstr ""
+"Permettete di utilizzare solo template di host che corrispondono "
+"a questo filtro"
+
+msgid "Allow to use only these db resources (comma separated list)"
+msgstr ""
+"Permettete di utilizzare solo queste risorse db (elenco separato da virgole)"
+
+msgid "Command-specific custom vars"
+msgstr "Variabili specifiche del comando definite dall'utente"
+
+msgid "Config history"
+msgstr "Cronologia Configurazione"
+
+msgid ""
+"Data fields allow you to customize input controls your custom variables."
+msgstr ""
+"I campi dati consentono di personalizzare i controlli di "
+"input delle variabili personalizzate."
+
+msgid "Expression"
+msgstr "Espressione"
+
+msgid "Operator"
+msgstr "Operatore"
+
+msgid "Owner"
+msgstr "Proprietario"
+
+msgid "Service configs"
+msgstr "Configurazioni Servizio"
+
+msgid "The unique name of the field"
+msgstr "Il nome unico del campo"
+
+msgid "There are pending database schema migrations"
+msgstr "Le migrazioni del database sono in corso"
+
+msgid "by host group property"
+msgstr "per proprietà del gruppo host"
+
+msgid "check command"
+msgstr "Comando Check"
+
+msgid "to a host group"
+msgstr "ad un gruppo host"
+
+msgid "%s template \"%s\": custom fields"
+msgstr "%s template \"%s\": campo personalizzato"
+
+msgid "Add entry"
+msgstr "Aggiungi voce"
+
+msgid "Deployments / History"
+msgstr "Distrubuzioni / Cronologia"
+
+msgid "Edit entry"
+msgstr "Modifica voce"
+
+msgid "Edit sync rule"
+msgstr "Modifica regola di sincronizzazione"
+
+msgid "Filter string"
+msgstr "Filtro stringa"
+
+msgid "Import / Sync"
+msgstr "Importa / Sincronizza"
+
+msgid "Import runs"
+msgstr "Importa esecuzioni"
+
+msgid "Run"
+msgstr "Esegui"
+
+msgid "Unable to store the configuration to \"%s\""
+msgstr "Impossibile salvare la configuraazione a \"%s\""
+
+msgid "Purge existing values."
+msgstr "Eliminare i valori esistenti."
+
+msgid "Whether the field should be merged, replaced or ignored"
+msgstr "Il campo deve essere unito, sostituito o ignorato?"
+
+msgid "Alert your users"
+msgstr "Avvisa i tuoi utenti"
+
+msgid "Manage deployments, access audit log and history"
+msgstr ""
+"Gestisci distribuzioni, registro di audit degli accessi e la cronologia"
+
+msgid "Name for the Icinga zone (template) you are going to create"
+msgstr "Nome per la zona Icinga (template) che stai per creare"
+
+msgid "Zone (template) name"
+msgstr "Nome (template) Zona"
+
+msgid "click here"
+msgstr "clicca qui"
+
+msgid "database schema"
+msgstr "schema database"
+
+msgid "e.g. "
+msgstr "per es."
+
+msgid "start using"
+msgstr "inizia ad utilizzare"
diff --git a/application/locale/ja_JP/LC_MESSAGES/director.mo b/application/locale/ja_JP/LC_MESSAGES/director.mo
new file mode 100644
index 0000000..ed5b41e
--- /dev/null
+++ b/application/locale/ja_JP/LC_MESSAGES/director.mo
Binary files differ
diff --git a/application/locale/ja_JP/LC_MESSAGES/director.po b/application/locale/ja_JP/LC_MESSAGES/director.po
new file mode 100644
index 0000000..d1bec38
--- /dev/null
+++ b/application/locale/ja_JP/LC_MESSAGES/director.po
@@ -0,0 +1,6186 @@
+# Icinga Web 2 - Head for multiple monitoring backends.
+# Copyright (C) 2019 Icinga Development Team
+# This file is distributed under the same license as Director Module.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Director Module (1.6.2)\n"
+"Report-Msgid-Bugs-To: dev@icinga.com\n"
+"POT-Creation-Date: 2019-03-29 11:34+0900\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: ja_JP\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-Basepath: .\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:602
+#, php-format
+msgid " (inherited from \"%s\")"
+msgstr "(\"%s\" から継承)"
+
+#: ../../../../modules/director/library/Director/Web/Table/TemplatesTable.php:63
+msgid " - not in use -"
+msgstr "- 未使用 -"
+
+#: ../../../../modules/director/library/Director/Web/Table/DatafieldTable.php:49
+msgid "# Used"
+msgstr "# 使用済み"
+
+#: ../../../../modules/director/library/Director/Web/Table/DatafieldTable.php:50
+msgid "# Vars"
+msgstr "# 変数"
+
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarTable.php:29
+#, php-format
+msgid "%d / %d"
+msgstr "%d / %d"
+
+#: ../../../../modules/director/library/Director/Resolver/CommandUsage.php:51
+#, php-format
+msgid "%d Host Template(s)"
+msgstr "%d 個のホストテンプレート。"
+
+#: ../../../../modules/director/library/Director/Resolver/CommandUsage.php:50
+#, php-format
+msgid "%d Host(s)"
+msgstr "%d 個のホスト。"
+
+#: ../../../../modules/director/library/Director/Resolver/CommandUsage.php:61
+#, php-format
+msgid "%d Notification Apply Rule(s)"
+msgstr "%d 個の通知適用ルール。"
+
+#: ../../../../modules/director/library/Director/Resolver/CommandUsage.php:60
+#, php-format
+msgid "%d Notification Template(s)"
+msgstr "%d 個の通知テンプレート。"
+
+#: ../../../../modules/director/library/Director/Resolver/CommandUsage.php:59
+#, php-format
+msgid "%d Notification(s)"
+msgstr "%d 個の通知。"
+
+#: ../../../../modules/director/library/Director/Resolver/CommandUsage.php:56
+#, php-format
+msgid "%d Service Apply Rule(s)"
+msgstr "%d 個の適用ルール。"
+
+#: ../../../../modules/director/library/Director/Resolver/CommandUsage.php:55
+#, php-format
+msgid "%d Service Template(s)"
+msgstr "%d 個のテンプレート。"
+
+#: ../../../../modules/director/library/Director/Resolver/CommandUsage.php:54
+#, php-format
+msgid "%d Service(s)"
+msgstr "%d 個のサービス"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:231
+#, php-format
+msgid "%d apply rules have been defined"
+msgstr "%d個の適用ルールが定義されています"
+
+#: ../../../../modules/director/library/Director/Web/Table/SyncRunTable.php:46
+#, php-format
+msgid "%d created"
+msgstr "%d 個定義されています"
+
+#: ../../../../modules/director/library/Director/Web/Table/SyncRunTable.php:58
+#, php-format
+msgid "%d deleted"
+msgstr "%d 個削除されています。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:110
+#, php-format
+msgid "%d files"
+msgstr "%d 個のファイル"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeployedConfigInfoHeader.php:83
+#, php-format
+msgid "%d files rendered in %0.2fs"
+msgstr "%d 個のファイルが%0.2f 秒で生成されました。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:273
+#, php-format
+msgid "%d have been externally defined and will not be deployed"
+msgstr "%d 個は外部で定義されており、設定が反映されません。"
+
+#: ../../../../modules/director/library/Director/Web/Table/SyncRunTable.php:52
+#, php-format
+msgid "%d modified"
+msgstr "%d 個の変更。"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:78
+#, php-format
+msgid "%d objects found"
+msgstr "%d 個のオブジェクトが見つかりました。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:256
+#, php-format
+msgid "%d objects have been defined"
+msgstr "%d 個のオブジェクトが定義されています"
+
+#: ../../../../modules/director/application/forms/IcingaMultiEditForm.php:87
+#, php-format
+msgid "%d objects have been modified"
+msgstr "%d 個のオブジェクトが変更されています。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:119
+#, php-format
+msgid "%d objects, %d templates, %d apply rules"
+msgstr "%d 個のオブジェクト、%d 個のテンプレート、%d 個の適用ルール。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:265
+#, php-format
+msgid "%d of them are templates"
+msgstr "そのうちの%d 個はテンプレートです"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:213
+#, php-format
+msgid "%d templates have been defined"
+msgstr "%d 個のテンプレートが定義されています。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:450
+#, php-format
+msgid "%s \"%s\" has been created"
+msgstr "%s \"%s\"が作成されました。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:453
+#, php-format
+msgid "%s \"%s\" has been deleted"
+msgstr "%s \"%s\"が削除されました。"
+
+#: ../../../../modules/director/application/forms/IcingaImportObjectForm.php:36
+#, php-format
+msgid "%s \"%s\" has been imported\""
+msgstr "%s \"%s\"がインポートされました。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:456
+#, php-format
+msgid "%s \"%s\" has been modified"
+msgstr "%s \"%s\" が変更されました。"
+
+#: ../../../../modules/director/library/Director/Web/Table/ObjectSetTable.php:57
+#, php-format
+msgid "%s (%d members)"
+msgstr "%s (%d 人のメンバー)"
+
+#: ../../../../modules/director/application/controllers/HostController.php:178
+#: ../../../../modules/director/application/controllers/HostController.php:261
+#, php-format
+msgid "%s (Applied Service set)"
+msgstr "%s (適用されたサービスセット)"
+
+#: ../../../../modules/director/application/controllers/HostController.php:311
+#, php-format
+msgid "%s (Service set)"
+msgstr "%s (サービスセット)"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:243
+#: ../../../../modules/director/application/forms/SettingsForm.php:157
+#, php-format
+msgid "%s (default)"
+msgstr "%s (デフォルト)"
+
+#: ../../../../modules/director/library/Director/Web/Table/IcingaHostAppliedServicesTable.php:100
+#, php-format
+msgid "%s (where %s)"
+msgstr "%s (条件: %s)"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:69
+#, php-format
+msgid "%s Templates"
+msgstr "%s テンプレート"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:36
+#, php-format
+msgid "%s based on %s"
+msgstr "%s (ベース: %s)"
+
+#: ../../../../modules/director/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:112
+#, php-format
+msgid "%s config changes happend since the last deployed configuration"
+msgstr "最後に反映された設定以降の %s の設定変更"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:256
+#, php-format
+msgid "%s has been blacklisted on %s"
+msgstr "%s は %s のブラックリストに登録されました。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:292
+#, php-format
+msgid "%s is no longer blacklisted on %s"
+msgstr "%s は %s のブラックリストから除外されました。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/SyncRunDetails.php:48
+#, php-format
+msgid "%s objects have been modified"
+msgstr "%s オブジェクトが編集されました。"
+
+#: ../../../../modules/director/application/controllers/HostController.php:459
+#, php-format
+msgid "%s on %s (from set: %s)"
+msgstr "%s 上の%s(セット: %s から) "
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:288
+#, php-format
+msgid "%s related group objects have been created"
+msgstr "%s 個の関連オブジェクトが作成されました。"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:72
+#, php-format
+msgid "%s templates based on %s"
+msgstr "%s テンプレート(%s をベース)"
+
+#: ../../../../modules/director/library/Director/Web/Widget/HealthCheckPluginOutput.php:46
+#, php-format
+msgid "%s: %d"
+msgstr "%s: %d"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:193
+#, php-format
+msgid "%s: %s (Snapshot)"
+msgstr "%s: %s (スナップショット)"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:223
+#, php-format
+msgid "%s: Property Modifier"
+msgstr "%s: プロパティ変換ルール"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:144
+#, php-format
+msgid "%s: Snapshots"
+msgstr "%s: スナップショット"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:197
+#, php-format
+msgid "%s: add Property Modifier"
+msgstr "%s: プロパティ変換ルールを追加"
+
+#: ../../../../modules/director/library/Director/Db/Housekeeping.php:54
+msgid "(Host) group resolve cache"
+msgstr "(ホスト) グループがキャッシュを解決"
+
+#: ../../../../modules/director/application/controllers/ServiceController.php:169
+#, php-format
+msgid "(on %s)"
+msgstr "(%s について)"
+
+#: ../../../../modules/director/library/Director/Web/Form/IplElement/ExtensibleSetElement.php:234
+msgid "- add more -"
+msgstr "- さらに追加 -"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1032
+msgid "- click to add more -"
+msgstr "- クリックしてさらに追加 -"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:611
+msgid "- inherited -"
+msgstr "- 継承 -"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:66
+msgid "- no automatic installation -"
+msgstr "- 自動インストールなし -"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:360
+#: ../../../../modules/director/application/controllers/ConfigController.php:371
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:239
+#: ../../../../modules/director/application/forms/SettingsForm.php:161
+#: ../../../../modules/director/application/views/helpers/FormDataFilter.php:465
+#: ../../../../modules/director/library/Director/DataType/DataTypeDatalist.php:22
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:43
+#: ../../../../modules/director/library/Director/DataType/DataTypeSqlQuery.php:37
+#: ../../../../modules/director/library/Director/Job/ImportJob.php:118
+#: ../../../../modules/director/library/Director/Job/SyncJob.php:124
+#: ../../../../modules/director/library/Director/Web/Form/QuickBaseForm.php:119
+msgid "- please choose -"
+msgstr "- 選択してください -"
+
+#: ../../../../modules/director/application/controllers/BasketsController.php:34
+msgid ""
+"A Configuration Basket references specific Configuration Objects or all "
+"objects of a specific type. It has been designed to share Templates, Import/"
+"Sync strategies and other base Configuration Objects. It is not a tool to "
+"operate with single Hosts or Services."
+msgstr "構成バスケットは、特定の構成オブジェクトまたは特定のタイプの"
+"すべてのオブジェクトを参照します。 これはテンプレート、インポート/同期戦略、"
+"その他の基本設定オブジェクトを共有するように設計されています。 単一の"
+"ホストまたはサービスで動作するためのツールではありません。"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceLdap.php:61
+msgid ""
+"A custom LDAP filter to use in addition to the object class. This allows for "
+"a lot of flexibility but requires LDAP filter skills. Simple filters might "
+"look as follows: operatingsystem=*server*"
+msgstr "オブジェクトクラスに加えて使用するカスタムLDAPフィルタ。"
+"これにより、柔軟性が大幅に向上しますが、LDAPフィルタのスキルが必要です。"
+"単純なフィルタは次のようになります。operatingsystem=*server*"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:214
+msgid ""
+"A custom string. Might contain source columns, please use placeholders of "
+"the form ${columnName} in such case"
+msgstr "カスタム文字列 ソースカラムを含めることができます。その場合は "
+"${columnName} の形式のplaceholderを使用してください。"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:126
+msgid "A description about the field"
+msgstr "フィールドについての説明"
+
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:59
+msgid "A detailled description explaining what this choice is all about"
+msgstr "このチョイスのすべてについて説明する詳細な説明"
+
+#: ../../../../modules/director/application/forms/IcingaServiceSetForm.php:102
+msgid ""
+"A meaningful description explaining your users what to expect when assigning "
+"this set of services"
+msgstr "このサービスセットの説明を記述します"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodRangeForm.php:85
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:641
+#, php-format
+msgid "A new %s has successfully been created"
+msgstr "新しい %s が正常に作成されました"
+
+#: ../../../../modules/director/application/forms/IcingaGenerateApiKeyForm.php:39
+#, php-format
+msgid "A new Self Service API key for %s has been generated"
+msgstr "%s の新しいセルフサービスAPIキーが生成されました。"
+
+#: ../../../../modules/director/application/forms/ImportRowModifierForm.php:74
+msgid ""
+"A property modifier allows you to modify a specific property at import time"
+msgstr "プロパティ変換ルールを使用すると、インポート時に特定のプロパティを変更できます。"
+
+#: ../../../../modules/director/application/forms/ImportSourceForm.php:17
+msgid ""
+"A short name identifying this import source. Use something meaningful, like "
+"\"Hosts from Puppet\", \"Users from Active Directory\" or similar"
+msgstr "このインポート元を識別する名前。 わかりやすい意味のある名前を使用してください。"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:74
+msgid ""
+"A short name identifying this job. Use something meaningful, like \"Import "
+"Puppet Hosts\""
+msgstr "このジョブを識別する名前。 わかりやすい意味のある名前を使用してください。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceSetForm.php:30
+msgid "A short name identifying this set of services"
+msgstr "このサービスセットの名前"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:211
+#, php-format
+msgid ""
+"A ticket for this agent could not have been requested from your deployment "
+"endpoint: %s"
+msgstr "このエージェントのチケットは、デプロイメントエンドポイントから要求されていない可能性があります:%s"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DeploymentDashlet.php:95
+#, php-format
+msgid ""
+"A total of %d config changes happened since your last deployed config has "
+"been rendered"
+msgstr "最後の設定反映から合計 %d 回の設定変更が行われました。"
+
+#: ../../../../modules/director/application/forms/IcingaHostSelfServiceForm.php:49
+msgid "API Key"
+msgstr "APIキー"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:46
+#: ../../../../modules/director/application/forms/KickstartForm.php:151
+msgid "API user"
+msgstr "API ユーザ"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1360
+msgid "Accept passive checks"
+msgstr "パッシブ監視を許可する"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:93
+msgid "Accepts config"
+msgstr "設定の同期を受け入れる"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:28
+msgid "Acknowledgement"
+msgstr "Acknowledgement (検知)"
+
+#: ../../../../modules/director/library/Director/Web/Table/ConfigFileDiffTable.php:81
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:391
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:401
+msgid "Action"
+msgstr "アクション"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1473
+msgid "Action URL"
+msgstr "アクションURL"
+
+#: ../../../../modules/director/library/Director/Web/Table/QuickTable.php:280
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:424
+#: ../../../../modules/director/library/Director/Web/Widget/DeployedConfigInfoHeader.php:62
+msgid "Actions"
+msgstr "アクション"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:143
+msgid "Activation Tool"
+msgstr "アクティベーションツール"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:122
+msgid "Active-Passive"
+msgstr "アクティブ-パッシブ"
+
+#: ../../../../modules/director/library/Director/Web/Widget/SyncRunDetails.php:27
+msgid "Activity"
+msgstr "アクティビティ"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:141
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ActivityLogDashlet.php:11
+#: ../../../../modules/director/library/Director/Web/Tabs/InfraTabs.php:29
+msgid "Activity Log"
+msgstr "アクティビティログ"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:233
+#, php-format
+msgid "Activity Log: %s"
+msgstr "アクティビティログ: %s"
+
+#: ../../../../modules/director/configuration.php:138
+msgid "Activity log"
+msgstr "アクティビティログ"
+
+#: ../../../../modules/director/application/controllers/DataController.php:22
+#: ../../../../modules/director/application/controllers/DataController.php:64
+#: ../../../../modules/director/application/forms/AddToBasketForm.php:72
+#: ../../../../modules/director/library/Director/Web/ActionBar/ChoicesActionBar.php:16
+#: ../../../../modules/director/library/Director/Web/ActionBar/ObjectsActionBar.php:16
+#: ../../../../modules/director/library/Director/Web/ActionBar/TemplateActionBar.php:19
+#: ../../../../modules/director/library/Director/Web/Controller/ActionController.php:134
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectsController.php:247
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectsController.php:288
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:492
+msgid "Add"
+msgstr "追加"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:76
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectTabs.php:51
+#, php-format
+msgid "Add %s"
+msgstr "追加: %s"
+
+#: ../../../../modules/director/application/forms/AddToBasketForm.php:68
+#, php-format
+msgid "Add %s objects"
+msgstr "%s 個のオブジェクトを追加"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:366
+#, php-format
+msgid "Add %s: %s"
+msgstr "%sを追加: %s"
+
+#: ../../../../modules/director/application/controllers/HostsController.php:40
+#: ../../../../modules/director/application/controllers/HostsController.php:79
+msgid "Add Service"
+msgstr "サービスを追加"
+
+#: ../../../../modules/director/application/controllers/HostsController.php:45
+#: ../../../../modules/director/application/controllers/HostsController.php:110
+msgid "Add Service Set"
+msgstr "サービスセットを追加"
+
+#: ../../../../modules/director/application/controllers/HostsController.php:127
+#, php-format
+msgid "Add Service Set to %d hosts"
+msgstr "サービスセットを %d 個のホストに追加"
+
+#: ../../../../modules/director/application/controllers/HostController.php:74
+#, php-format
+msgid "Add Service Set: %s"
+msgstr "サービスセット: %s を追加"
+
+#: ../../../../modules/director/application/controllers/HostController.php:61
+#, php-format
+msgid "Add Service: %s"
+msgstr "サービス: %s を追加"
+
+#: ../../../../modules/director/application/controllers/DatafieldController.php:40
+msgid "Add a new Data Field"
+msgstr "新しいデータフィールドを追加"
+
+#: ../../../../modules/director/application/controllers/DataController.php:52
+msgid "Add a new Data List"
+msgstr "データリストを追加"
+
+#: ../../../../modules/director/application/controllers/ImportsourcesController.php:49
+msgid "Add a new Import Source"
+msgstr "新しいインポートソースを追加"
+
+#: ../../../../modules/director/application/controllers/JobController.php:31
+#: ../../../../modules/director/application/controllers/JobsController.php:15
+msgid "Add a new Job"
+msgstr "新しいジョブの追加"
+
+#: ../../../../modules/director/application/controllers/SyncrulesController.php:28
+msgid "Add a new Sync Rule"
+msgstr "新しい同期ルールを追加"
+
+#: ../../../../modules/director/library/Director/Web/Form/IplElement/ExtensibleSetElement.php:470
+msgid "Add a new entry"
+msgstr "新しいエントリを追加"
+
+#: ../../../../modules/director/library/Director/Web/Form/IplElement/ExtensibleSetElement.php:256
+msgid "Add a new one..."
+msgstr "新しいものを追加"
+
+#: ../../../../modules/director/application/controllers/ServicesetController.php:49
+#, php-format
+msgid "Add a service set to \"%s\""
+msgstr "\"%s\"にサービスセットを追加"
+
+#: ../../../../modules/director/application/controllers/ServiceController.php:105
+#, php-format
+msgid "Add a service to \"%s\""
+msgstr "\"%s\"にサービスを追加"
+
+#: ../../../../modules/director/application/views/helpers/FormDataFilter.php:516
+msgid "Add another filter"
+msgstr "別のフィルタを追加"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:83
+msgid "Add chosen objects to a Configuration Basket"
+msgstr "選択したオブジェクトを構成バスケットに追加する"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistEntryForm.php:60
+msgid "Add data list entry"
+msgstr "データリストエントリを追加"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:89
+msgid "Add import source"
+msgstr "インポートソースの追加"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:372
+#, php-format
+msgid "Add new Icinga %s"
+msgstr "新しいIcinga %s の追加"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:355
+#, php-format
+msgid "Add new Icinga %s template"
+msgstr "新しいIcinga %s テンプレートの追加"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:170
+msgid "Add property modifier"
+msgstr "プロパティ変換ルールを追加"
+
+#: ../../../../modules/director/application/controllers/HostController.php:90
+#: ../../../../modules/director/application/controllers/ServicesetController.php:66
+msgid "Add service"
+msgstr "サービスを追加"
+
+#: ../../../../modules/director/application/controllers/HostController.php:95
+msgid "Add service set"
+msgstr "サービスセットを追加"
+
+#: ../../../../modules/director/application/controllers/HostsController.php:96
+#, php-format
+msgid "Add service to %d hosts"
+msgstr "サービスを %d 個のホストに追加"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:210
+msgid "Add sync property rule"
+msgstr "同期プロパティルールを追加"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:249
+#, php-format
+msgid "Add sync property: %s"
+msgstr "同期プロパティを追加: %s"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:161
+msgid "Add sync rule"
+msgstr "同期ルールを追加"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:82
+#: ../../../../modules/director/application/controllers/DataController.php:124
+#: ../../../../modules/director/application/controllers/HostsController.php:70
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:55
+#: ../../../../modules/director/application/controllers/JobController.php:78
+#: ../../../../modules/director/application/controllers/ServicesController.php:36
+#: ../../../../modules/director/application/controllers/SyncruleController.php:295
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:337
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:126
+msgid "Add to Basket"
+msgstr "バスケットに追加"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1459
+msgid "Additional notes for this object"
+msgstr "このオブジェクトに関する注意事項"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1512
+msgid "Additional properties"
+msgstr "任意のプロパティ"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectTabs.php:140
+msgid "Agent"
+msgstr "エージェント"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:141
+msgid "Agent Version"
+msgstr "エージェントのバージョン"
+
+#: ../../../../modules/director/application/forms/IcingaHostSelfServiceForm.php:31
+msgid "Alias"
+msgstr "別名"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:151
+msgid "All changes"
+msgstr "すべての変更"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:78
+msgid ""
+"All changes are tracked in the Director database. In addition you might also "
+"want to send an audit log through the Icinga Web 2 logging mechanism. That "
+"way all changes would be written to either Syslog or the configured log "
+"file. When enabling this please make sure that you configured Icinga Web 2 "
+"to log at least at \"informational\" level."
+msgstr "すべての変更はDirectorデータベースで追跡されます。 さらに、"
+"Icinga Web 2のログ記録メカニズムを介して監査ログを送信することもできます。"
+"そうすれば、すべての変更はSyslogまたは設定されたログファイルに書き込まれます。"
+"これを有効にするときは、少なくとも \"informational\"レベルでログを"
+"記録するようにIcinga Web 2を設定したことを確認してください。"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:307
+msgid "All custom variables (vars)"
+msgstr "すべてのカスタム変数(vars)"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:50
+msgid "All of them"
+msgstr "それらすべて"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:725
+#, php-format
+msgid "All overrides have been removed from \"%s\""
+msgstr "すべてのオーバーライドは\"%s\"から削除されました"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectsController.php:240
+#, php-format
+msgid "All your %s Apply Rules"
+msgstr "すべての%s 適用ルール"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectsController.php:201
+#, php-format
+msgid "All your %s Templates"
+msgstr "すべての %s テンプレート"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:174
+msgid "Allow Updates"
+msgstr "アップデートを許可"
+
+#: ../../../../modules/director/configuration.php:31
+msgid "Allow readonly users to see where a Service came from"
+msgstr "読み取り専用ユーザーに、サービスの出所を確認することを許可"
+
+#: ../../../../modules/director/configuration.php:6
+msgid "Allow to access the director API"
+msgstr "director API へのアクセスを許可"
+
+#: ../../../../modules/director/configuration.php:7
+msgid "Allow to access the full audit log"
+msgstr "完全な audit ログへのアクセスを許可"
+
+#: ../../../../modules/director/configuration.php:17
+msgid "Allow to configure hosts"
+msgstr "ホストの設定を許可"
+
+#: ../../../../modules/director/configuration.php:22
+msgid "Allow to configure notifications"
+msgstr "通知の設定を許可"
+
+#: ../../../../modules/director/configuration.php:19
+msgid "Allow to configure service sets"
+msgstr "サービスセットの設定を許可"
+
+#: ../../../../modules/director/configuration.php:18
+msgid "Allow to configure services"
+msgstr "サービスの設定を許可"
+
+#: ../../../../modules/director/configuration.php:21
+msgid "Allow to configure users"
+msgstr "ユーザの設定を許可"
+
+#: ../../../../modules/director/configuration.php:20
+msgid "Allow to define Service Set Apply Rules"
+msgstr "サービス設定の適用ルールの定義を許可"
+
+#: ../../../../modules/director/configuration.php:16
+msgid "Allow to deploy configuration"
+msgstr "設定反映を許可"
+
+#: ../../../../modules/director/configuration.php:26
+msgid ""
+"Allow to inspect objects through the Icinga 2 API (could contain sensitive "
+"information)"
+msgstr "Icinga 2 APIを介してオブジェクトの検査を許可"
+"(機密情報を含む可能性があります)"
+
+#: ../../../../modules/director/configuration.php:10
+msgid "Allow to show configuration (could contain sensitive information)"
+msgstr "設定の表示を許可(機密情報を含む可能性があります)"
+
+#: ../../../../modules/director/configuration.php:14
+msgid "Allow to show the full executed SQL queries in some places"
+msgstr "いくつかの場所で完全に実行されたSQLクエリを表示することを許可します"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistEntryForm.php:48
+msgid ""
+"Allow to use this entry only to users with one of these Icinga Web 2 roles"
+msgstr "選択したロールのどれかを持つユーザーにのみ"
+"このエントリの使用を許可します"
+
+#: ../../../../modules/director/configuration.php:33
+msgid "Allow unrestricted access to Icinga Director"
+msgstr "Icinga Directorへの無制限アクセスを許可"
+
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:85
+msgid "Allowed maximum"
+msgstr "許可された上限"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistEntryForm.php:44
+msgid "Allowed roles"
+msgstr "許可されたロール"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:73
+msgid "Also clone fields provided by this Template"
+msgstr "このテンプレートに提供されているフィールドも複製します。"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:53
+msgid "Also clone single Service Sets defined for this Host"
+msgstr "このホストに定義されている単一のサービスセットも複製します。"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:44
+msgid "Also clone single Services defined for this Host"
+msgstr "このホストに定義されている単一のサービスも複製します。"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:191
+msgid ""
+"Also install NSClient++. It can be used through the Icinga Agent and comes "
+"with a bunch of additional Check Plugins"
+msgstr "NSClient++ もインストールします。 NSClient++ にはIcingaエージェントを介して使用することができ、監視プラグインが付属しています"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:107
+#, php-format
+msgid "Also rename all \"%s\" custom variables to \"%s\" on %d objects?"
+msgstr "すべての \"%s\" カスタム変数の名前を \"%s\" に変更しますか?(対象:%d個のオブジェクト)"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:64
+#, php-format
+msgid "Also wipe all \"%s\" custom variables from %d objects?"
+msgstr "すべての \"%s\" カスタム変数も消去しますか?(処理対象:%d個のオブジェクト)"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:336
+msgid ""
+"Alternative name for this host. Might be a host alias or and kind of string "
+"helping your users to identify this host"
+msgstr "このホストの別名。 ユーザーがこのホストを識別するのに役立つ"
+"ホストエイリアスまたはそのような文字列"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:135
+msgid ""
+"Alternative name for this user. In case your object name is a username, this "
+"could be the full name of the corresponding person"
+msgstr "このユーザーの別名。 オブジェクト名がユーザー名である場合、"
+"これは対応する人のフルネームを指定したりします。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1493
+msgid "Alternative text to be shown in case above icon is missing"
+msgstr "上記のアイコンがない場合に表示される代替テキスト"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:86
+msgid ""
+"An Icinga DSL expression that returns a boolean value, e.g.: var cmd = "
+"bool(macro(\"$cmd$\")); return cmd ..."
+msgstr "ブール値を返すIcinga DSL表記。"
+"例)var cmd = bool(macro(\"$cmd$\")); return cmd"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:47
+msgid ""
+"An Icinga DSL expression, e.g.: var cmd = macro(\"$cmd$\"); return "
+"typeof(command) == String ..."
+msgstr "Icinga DSL表記。"
+"例)var cmd = macro(\"$cmd$\"); return typeof(command)== String"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1475
+msgid ""
+"An URL leading to additional actions for this object. Often used with Icinga "
+"Classic, rarely with Icinga Web 2 as it provides far better possibilities to "
+"integrate addons"
+msgstr "このオブジェクトに対するアクションにつながるURL。"
+"Icinga Classicではよく使用されますが、Icinga Web 2では、アドオンを統合"
+"するためのはるかに優れた機能があるため、ほとんど使用されません。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1468
+msgid "An URL pointing to additional notes for this object"
+msgstr "このオブジェクトに関するURL"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1484
+msgid ""
+"An URL pointing to an icon for this object. Try \"tux.png\" for icons "
+"relative to public/img/icons or \"cloud\" (no extension) for items from the "
+"Icinga icon font"
+msgstr "このオブジェクトのアイコンを指すURL。 public/img/iconsに関連する"
+"アイコンには \"tux.png\"を、Icingaアイコンフォントのアイテムには\"cloud\""
+"(拡張子なし)を試してください。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1240
+msgid ""
+"An alternative display name for this group. If you wonder how this could be "
+"helpful just leave it blank"
+msgstr "このグループの別名で、表示に使われます。"
+"別名を設定する必要がなければ、空白のままでも問題ありません。"
+
+#: ../../../../modules/director/application/forms/ImportRowModifierForm.php:56
+msgid ""
+"An extended description for this Import Row Modifier. This should explain "
+"it's purpose and why it has been put in place at all."
+msgstr "このインポート行プロパティ変換ルールの詳細な説明。 目的と、なぜ必要なのかの説明を書きます。"
+
+#: ../../../../modules/director/application/forms/ImportSourceForm.php:26
+msgid ""
+"An extended description for this Import Source. This should explain what "
+"kind of data you're going to import from this source."
+msgstr "このインポートソースの詳細な説明。 このソースからどのような種類の"
+"データをインポートしようとしているのかを説明する必要があります。"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:35
+msgid ""
+"An extended description for this Sync Rule. This should explain what this "
+"Rule is going to accomplish."
+msgstr "この同期ルールの詳細な説明を記載します。"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:159
+msgid ""
+"An extended description for this field. Will be shown as soon as a user puts "
+"the focus on this field"
+msgstr "このフィールドの詳細な説明。 ユーザーがこのフィールドにフォーカスすると表示されます。"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceLdap.php:55
+msgid ""
+"An object class to search for. Might be \"user\", \"group\", \"computer\" or "
+"similar"
+msgstr "検索するオブジェクトクラス \"user\"、\"group\"、\"computer\"などの"
+"可能性があります"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:20
+msgid "Any first (leftmost) component"
+msgstr "最初の(一番左の)コンポーネントすべて"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:115
+msgid "Api Key:"
+msgstr "APIキー: "
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:52
+#, php-format
+msgid "Applied %s"
+msgstr "%s を適用しました"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:217
+msgid "Applied groups"
+msgstr "グループを適用しました"
+
+#: ../../../../modules/director/application/controllers/HostController.php:343
+#, php-format
+msgid "Applied service: %s"
+msgstr "サービスを適用しました: %s"
+
+#: ../../../../modules/director/application/controllers/HostController.php:189
+#: ../../../../modules/director/application/controllers/HostController.php:276
+msgid "Applied services"
+msgstr "サービスを適用しました"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectsTabs.php:44
+msgid "Apply"
+msgstr "適用"
+
+#: ../../../../modules/director/application/controllers/ServiceController.php:110
+#, php-format
+msgid "Apply \"%s\""
+msgstr "\"%s\"を適用"
+
+#: ../../../../modules/director/application/forms/ApplyMigrationsForm.php:25
+#, php-format
+msgid "Apply %d pending schema migrations"
+msgstr "%d 個の保留中のスキーマ移行を適用"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:604
+msgid "Apply For"
+msgstr "適用対象"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:166
+msgid "Apply Rule"
+msgstr "ルールを適用"
+
+#: ../../../../modules/director/library/Director/Web/Table/ApplyRulesTable.php:122
+msgid "Apply Rule rendering preview"
+msgstr "ルールを適用してプレビューを行う"
+
+#: ../../../../modules/director/library/Director/Web/Table/DependencyTemplateUsageTable.php:11
+#: ../../../../modules/director/library/Director/Web/Table/NotificationTemplateUsageTable.php:11
+#: ../../../../modules/director/library/Director/Web/Table/ServiceTemplateUsageTable.php:12
+msgid "Apply Rules"
+msgstr "ルールを適用"
+
+#: ../../../../modules/director/application/forms/ApplyMigrationsForm.php:20
+msgid "Apply a pending schema migration"
+msgstr "保留中のスキーマ移行を適用する"
+
+#: ../../../../modules/director/library/Director/Job/SyncJob.php:92
+msgid "Apply changes"
+msgstr "変更を適用する"
+
+# bug! cannot translate
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php:19
+msgid "Apply notifications with specific properties according to given rules."
+msgstr "指定されたルールに従って特定のプロパティで通知を適用します。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1065
+msgid "Apply rule"
+msgstr "ルールを適用する"
+
+#: ../../../../modules/director/library/Director/Web/Table/ApplyRulesTable.php:129
+msgid "Apply rule history"
+msgstr "ルールの適用履歴"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:38
+msgid "Apply schema migrations"
+msgstr "スキーマの移行を適用"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:92
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:81
+msgid "Apply to"
+msgstr "適用先"
+
+#: ../../../../modules/director/application/controllers/ServiceController.php:206
+#, php-format
+msgid "Apply: %s"
+msgstr "適用: %s"
+
+#: ../../../../modules/director/library/Director/Web/Table/IcingaCommandArgumentTable.php:45
+msgid "Argument"
+msgstr "引数"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:91
+msgid "Argument macros"
+msgstr "引数マクロ"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:25
+msgid "Argument name"
+msgstr "引数名"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:313
+msgid "Arguments"
+msgstr "引数"
+
+#: ../../../../modules/director/library/Director/DataType/DataTypeDatalist.php:66
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:77
+#: ../../../../modules/director/library/Director/DataType/DataTypeSqlQuery.php:77
+msgid "Array"
+msgstr "配列"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1545
+msgid "Assign where"
+msgstr "条件式の指定"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:383
+msgid "Author"
+msgstr "作成者"
+
+#: ../../../../modules/director/library/Director/Dashboard/AutomationDashboard.php:15
+msgid "Automate all tasks"
+msgstr "すべてのタスクの自動化"
+
+#: ../../../../modules/director/configuration.php:134
+msgid "Automation"
+msgstr "自動化"
+
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:64
+msgid "Available choices"
+msgstr "利用可能なチョイス"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:50
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:92
+msgid "Back"
+msgstr "戻る"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:32
+#: ../../../../modules/director/application/forms/AddToBasketForm.php:60
+#: ../../../../modules/director/library/Director/Web/Table/BasketTable.php:31
+msgid "Basket"
+msgstr "バスケット"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:32
+msgid "Basket Definitions"
+msgstr "バスケットの定義"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:42
+#: ../../../../modules/director/application/forms/BasketUploadForm.php:29
+msgid "Basket Name"
+msgstr "バスケット名"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:140
+msgid "Basket Snapshots"
+msgstr "バスケットのスナップショット"
+
+#: ../../../../modules/director/application/forms/BasketUploadForm.php:145
+msgid "Basket has been uploaded"
+msgstr "バスケットがアップロードされました"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:76
+#: ../../../../modules/director/application/controllers/BasketsController.php:17
+msgid "Baskets"
+msgstr "バスケット"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:160
+msgid "Blacklist"
+msgstr "ブラックリスト"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:148
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:117
+msgid "Caption"
+msgstr "キャプション"
+
+#: ../../../../modules/director/application/forms/IcingaMultiEditForm.php:261
+#, php-format
+msgid "Changing this value affects %d object(s): %s"
+msgstr "この値を変更すると、%d個のオブジェクトに影響します: %s"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceCoreApi.php:57
+msgid "Check Commands"
+msgstr "監視コマンド"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1261
+msgid "Check command"
+msgstr "監視コマンド"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:252
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1262
+msgid "Check command definition"
+msgstr "監視コマンドの定義"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1330
+msgid ""
+"Check command timeout in seconds. Overrides the CheckCommand's timeout "
+"attribute"
+msgstr "コマンドのタイムアウト時間を秒単位で指定します"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:322
+msgid "Check execution"
+msgstr "監視の実行"
+
+#: ../../../../modules/director/application/forms/ImportCheckForm.php:23
+#: ../../../../modules/director/application/forms/SyncCheckForm.php:23
+msgid "Check for changes"
+msgstr "変更の確認"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1297
+msgid "Check interval"
+msgstr "監視インターバル"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1342
+msgid "Check period"
+msgstr "監視スケジュール"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1328
+msgid "Check timeout"
+msgstr "監視タイムアウト"
+
+#: ../../../../modules/director/application/forms/ImportCheckForm.php:45
+msgid "Checking this Import Source failed"
+msgstr "このインポートソースの検査が失敗しました"
+
+#: ../../../../modules/director/application/forms/SyncCheckForm.php:61
+msgid "Checking this sync rule failed"
+msgstr "この同期ルールの検査に失敗しました"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:419
+msgid "Checksum"
+msgstr "チェックサム"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:231
+msgid "Child Host"
+msgstr "子ホスト"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:247
+msgid "Child Service"
+msgstr "子サービス"
+
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:48
+msgid "Choice name"
+msgstr "チョイス名"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ChoicesDashlet.php:11
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectsTabs.php:69
+msgid "Choices"
+msgstr "チョイス"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:76
+msgid ""
+"Choose \"All\" to always add all of them, \"Ignore\" to not care about a "
+"specific Type at all and opt for \"Custom Selection\" in case you want to "
+"choose just some specific Objects."
+msgstr "常にすべてを追加するには「すべて」を、特定のタイプをまったく気に"
+"しないようにするには「無視」を選択し、特定のオブジェクトだけを選択する場合は"
+"「カスタム選択」を選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:176
+msgid "Choose a Host Template"
+msgstr "ホストテンプレートを選択"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:106
+msgid "Choose a service template"
+msgstr "サービステンプレートを選択"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:43
+msgid "Choose an object type"
+msgstr "オブジェクトタイプを選択"
+
+#: ../../../../modules/director/application/forms/BasketUploadForm.php:35
+msgid "Choose file"
+msgstr "ファイルを選択"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:582
+msgid "Choose the host this single service should be assigned to"
+msgstr "この単一サービスが割り当てられるべきホストを選択"
+
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:75
+msgid ""
+"Choosing this many options will be mandatory for this Choice. Setting this "
+"to zero will leave this Choice optional, setting it to one results in a "
+"\"required\" Choice. You can use higher numbers to enforce multiple options, "
+"this Choice will then turn into a multi-selection element."
+msgstr "この設定はこのチョイスに必須です。 これをゼロに設定すると、"
+"このチョイスはオプションのままになり、1に設定すると「必須」のチョイスになります。"
+"複数のオプションを強制するためにより高い数を使うことができます、"
+"そしてこのチョイスはそれから複数選択要素に変わります。"
+
+#: ../../../../modules/director/application/forms/IcingaZoneForm.php:37
+msgid "Chose an (optional) parent zone"
+msgstr "(オプションの)親ゾーンを選択"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:122
+#: ../../../../modules/director/application/controllers/SyncruleController.php:184
+#: ../../../../modules/director/library/Director/Web/ActionBar/AutomationObjectActionBar.php:44
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:309
+#: ../../../../modules/director/library/Director/Web/Form/CloneImportSourceForm.php:34
+#: ../../../../modules/director/library/Director/Web/Form/CloneSyncRuleForm.php:34
+msgid "Clone"
+msgstr "複製"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:79
+#, php-format
+msgid "Clone \"%s\""
+msgstr "\"%s\"を複製"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:51
+msgid "Clone Service Sets"
+msgstr "サービスセットを複製"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:42
+msgid "Clone Services"
+msgstr "サービスを複製"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:71
+msgid "Clone Template Fields"
+msgstr "テンプレートフィールドを複製"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:32
+msgid "Clone the object as is, preserving imports"
+msgstr "インポートを保持しながら、オブジェクトをそのまま複製する"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:62
+msgid "Clone this service to the very same or to another Service Set"
+msgstr "このサービスをまったく同じまたは別のサービスセットに複製します。"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:167
+#, php-format
+msgid "Clone: %s"
+msgstr "複製: %s"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1104
+msgid "Cluster Zone"
+msgstr "クラスタゾーン"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ChoicesDashlet.php:17
+msgid ""
+"Combine multiple templates into meaningful Choices, making life easier for "
+"your users"
+msgstr "チョイスを利用することで、細分化されたテンプレートを、用途毎に"
+"まとめることができます。この機能を利用することで、ホストに複数のテンプレート"
+"を何度もインポートする手間を省くことができます。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:59
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:20
+#: ../../../../modules/director/library/Director/TranslationDummy.php:16
+msgid "Command"
+msgstr "コマンド"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:17
+msgid "Command Definitions"
+msgstr "コマンド定義"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php:19
+msgid "Command Templates"
+msgstr "コマンドテンプレート"
+
+#: ../../../../modules/director/application/controllers/CommandController.php:89
+#, php-format
+msgid "Command arguments: %s"
+msgstr "コマンド引数: %s"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:103
+msgid "Command endpoint"
+msgstr "コマンドエンドポイント"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:48
+msgid "Command name"
+msgstr "コマンド名"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:17
+msgid "Command type"
+msgstr "コマンドタイプ"
+
+#: ../../../../modules/director/configuration.php:126
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php:19
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/CommandObjectDashlet.php:13
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarVariantsTable.php:57
+msgid "Commands"
+msgstr "コマンド"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:336
+#, php-format
+msgid "Comparing %s \"%s\" from Snapshot \"%s\" to current config"
+msgstr "%s を\"%s\"と比較します。スナップショット\"%s\"から現在の設定との比較です。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:84
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:93
+msgid "Condition (set_if)"
+msgstr "条件(set_if)"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:70
+msgid "Condition format"
+msgstr "条件フォーマット"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:255
+#: ../../../../modules/director/application/controllers/ConfigController.php:466
+#: ../../../../modules/director/application/controllers/JobController.php:101
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiFieldsTable.php:78
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:60
+msgid "Config"
+msgstr "設定"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DeploymentDashlet.php:18
+msgid "Config Deployment"
+msgstr "設定反映"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:441
+#: ../../../../modules/director/application/forms/DeployConfigForm.php:99
+#: ../../../../modules/director/application/forms/DeploymentLinkForm.php:149
+msgid "Config deployment failed"
+msgstr "設定反映に失敗しました。"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:341
+#: ../../../../modules/director/application/controllers/ConfigController.php:342
+msgid "Config diff"
+msgstr "設定差分"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:300
+#: ../../../../modules/director/application/controllers/ConfigController.php:402
+#, php-format
+msgid "Config file \"%s\""
+msgstr "設定ファイル \"%s\""
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:421
+#: ../../../../modules/director/application/forms/DeployConfigForm.php:75
+#: ../../../../modules/director/application/forms/DeployConfigForm.php:94
+#: ../../../../modules/director/application/forms/DeploymentLinkForm.php:136
+msgid "Config has been submitted, validation is going on"
+msgstr "設定が送信され、検証が行われています。"
+
+#: ../../../../modules/director/library/Director/Web/ObjectPreview.php:40
+#, php-format
+msgid "Config preview: %s"
+msgstr "設定ファイルプレビュー: %s"
+
+#: ../../../../modules/director/library/Director/ProvidedHook/Monitoring/ServiceActions.php:54
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:82
+msgid "Configuration"
+msgstr "設定"
+
+#: ../../../../modules/director/application/controllers/HostController.php:212
+msgid "Configuration (read-only)"
+msgstr "設定(読み込み専用)"
+
+#: ../../../../modules/director/application/controllers/BasketsController.php:32
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/BasketDashlet.php:11
+msgid "Configuration Baskets"
+msgstr "構成バスケット"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:89
+msgid "Configuration format"
+msgstr "設定フォーマット"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:332
+msgid "Configuration has been stored"
+msgstr "設定が保存されました"
+
+#: ../../../../modules/director/application/forms/AddToBasketForm.php:111
+#, php-format
+msgid "Configuration objects have been added to the chosen basket \"%s\""
+msgstr "選択したバスケット \"%s\" に構成オブジェクトが追加されました。"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:152
+msgid "Configure this Agent via Self Service API"
+msgstr "このエージェントをセルフサービスAPIで設定。"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:229
+msgid "Content Checksum"
+msgstr "コンテンツチェックサム"
+
+#: ../../../../modules/director/application/controllers/BasketsController.php:20
+msgid "Create"
+msgstr "作成"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:102
+msgid "Create Basket"
+msgstr "バスケットを作成"
+
+#: ../../../../modules/director/application/forms/BasketCreateSnapshotForm.php:23
+msgid "Create Snapshot"
+msgstr "スナップショットを作成"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectsController.php:252
+#, php-format
+msgid "Create a new %s Apply Rule"
+msgstr "新しい %s 適用ルールを作成"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectsController.php:293
+#, php-format
+msgid "Create a new %s Set"
+msgstr "新しい %s セットを作成"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:154
+#, php-format
+msgid "Create a new %s inheriting from this one"
+msgstr "これを継承して新しい%s を作成する"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:144
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:164
+#, php-format
+msgid "Create a new %s inheriting from this template"
+msgstr "このテンプレートを継承して新しい %s を作成する"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:103
+msgid "Create a new Configuration Basket"
+msgstr "新しい構成バスケットを作成"
+
+#: ../../../../modules/director/library/Director/Web/ActionBar/TemplateActionBar.php:23
+msgid "Create a new Template"
+msgstr "新しいテンプレートを作成"
+
+#: ../../../../modules/director/library/Director/Web/ActionBar/ObjectsActionBar.php:20
+msgid "Create a new object"
+msgstr "新しいオブジェクトを作成"
+
+#: ../../../../modules/director/library/Director/Web/ActionBar/ChoicesActionBar.php:20
+msgid "Create a new template choice"
+msgstr "新しいテンプレートチョイスを作成"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:37
+msgid "Create database schema"
+msgstr "データベーススキーマを作成"
+
+#: ../../../../modules/director/application/forms/ApplyMigrationsForm.php:31
+msgid "Create schema"
+msgstr "スキーマの作成"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:228
+msgid "Created"
+msgstr "作成"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/StateFilterSet.php:26
+msgid "Critical"
+msgstr "Critical (危険)"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:183
+msgid "Current Template Usage"
+msgstr "現在のテンプレートの利用状況"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:51
+msgid "Custom Selection"
+msgstr "カスタム選択"
+
+#: ../../../../modules/director/application/controllers/CustomvarController.php:13
+msgid "Custom Variable"
+msgstr "カスタム変数"
+
+#: ../../../../modules/director/application/controllers/CustomvarController.php:14
+#, php-format
+msgid "Custom Variable variants: %s"
+msgstr "カスタム変数の亜種:%s"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/DataTabs.php:27
+msgid "Custom Variables"
+msgstr "カスタム変数"
+
+#: ../../../../modules/director/application/controllers/DataController.php:76
+msgid "Custom Vars - Overview"
+msgstr "カスタム変数 - 概要"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:179
+msgid "Custom expression"
+msgstr "カスタム表現"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:183
+#, php-format
+msgid "Custom fields: %s"
+msgstr "カスタムフィールド: %s"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:25
+msgid "Custom notification"
+msgstr "Custom (カスタム通知)"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:436
+#: ../../../../modules/director/library/Director/Web/Form/IcingaObjectFieldLoader.php:227
+msgid "Custom properties"
+msgstr "カスタムプロパティ"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:67
+msgid "Custom variable"
+msgstr "カスタム変数"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:306
+msgid "Custom variable (vars.)"
+msgstr "カスタム変数(vars)"
+
+#: ../../../../modules/director/application/controllers/SuggestController.php:231
+#: ../../../../modules/director/application/controllers/SuggestController.php:241
+#: ../../../../modules/director/library/Director/Objects/IcingaHost.php:157
+#: ../../../../modules/director/library/Director/Objects/IcingaService.php:710
+msgid "Custom variables"
+msgstr "カスタム変数"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/CustomvarDashlet.php:11
+msgid "CustomVar Overview"
+msgstr "カスタム変数概要"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceSql.php:40
+msgid "DB Query"
+msgstr "DB クエリ"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:245
+msgid "DB Resource"
+msgstr "DBリソース"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:15
+msgid "DN component"
+msgstr "DN コンポーネント"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierDnsRecords.php:25
+msgid "DNS record type"
+msgstr "DNSレコードタイプ"
+
+#: ../../../../modules/director/application/controllers/DataController.php:62
+msgid "Data Fields"
+msgstr "データフィールド"
+
+#: ../../../../modules/director/application/controllers/DataController.php:53
+msgid "Data List"
+msgstr "データリスト"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:19
+msgid "Data List Entry"
+msgstr "データリストエントリ"
+
+#: ../../../../modules/director/application/controllers/DataController.php:47
+#, php-format
+msgid "Data List: %s"
+msgstr "データリスト: %s"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:28
+msgid "Data Lists"
+msgstr "データリスト"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/DataTabs.php:21
+msgid "Data fields"
+msgstr "データフィールド"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:131
+msgid ""
+"Data fields allow you to customize input controls for Icinga custom "
+"variables. Once you defined them here, you can provide them through your "
+"defined templates. This gives you a granular control over what properties "
+"your users should be allowed to configure in which way."
+msgstr "データフィールドを使用すると、カスタム変数に入力する値を"
+"カスタマイズできます。"
+"具体的にはタイプを数値に限定したり、データのリストを選択させるような入力を"
+"ユーザに強制することができるようになります。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DatafieldDashlet.php:17
+msgid "Data fields make sure that configuration fits your rules"
+msgstr "データフィールドを使用すると、Icingaカスタム変数の入力制限をカスタマイズすることができます。これにより、設定がルールに従うことを確認できます"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistForm.php:24
+msgid "Data list"
+msgstr "データリスト"
+
+#: ../../../../modules/director/application/controllers/DataController.php:20
+#: ../../../../modules/director/library/Director/Web/Tabs/DataTabs.php:24
+msgid "Data lists"
+msgstr "データリスト"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistForm.php:15
+msgid ""
+"Data lists are mainly used as data providers for custom variables presented "
+"as dropdown boxes boxes. You can manually manage their entries here in "
+"place, but you could also create dedicated sync rules after creating a new "
+"empty list. This would allow you to keep your available choices in sync with "
+"external data providers"
+msgstr "データリストは、ドロップダウンボックスボックスとして表示される"
+"カスタム変数のデータプロバイダとして主に使用されます。 ここで手動で"
+"エントリを管理できますが、新しい空のリストを作成した後に専用の同期ルールを"
+"作成することもできます。 これにより、利用可能な選択肢を外部の"
+"データプロバイダと同期させることができます。"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:174
+msgid "Data type"
+msgstr "データタイプ"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:295
+msgid "Database backend"
+msgstr "データベースバックエンド"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:384
+msgid "Date"
+msgstr "日付"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodRangeForm.php:21
+#: ../../../../modules/director/library/Director/Web/Table/IcingaTimePeriodRangeTable.php:45
+msgid "Day(s)"
+msgstr "日付・曜日"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:98
+msgid ""
+"Default configuration format. Please note that v1.x is for special "
+"transitional projects only and completely unsupported. There are no plans to "
+"make Director a first-class configuration backends for Icinga 1.x"
+msgstr "デフォルトの設定フォーマット。 バージョン 1.x は特別な移行プロジェクト"
+"専用で、完全にはサポートされていません。 Icinga 1.xでは、Directorを"
+"もっとも推奨する設定バックエンドにする計画はありません。"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:32
+msgid "Default global zone"
+msgstr "デフォルトのグローバルゾーン"
+
+#: ../../../../modules/director/library/Director/Dashboard/CommandsDashboard.php:23
+msgid ""
+"Define Check-, Notification- or Event-Commands. Command definitions are the "
+"glue between your Host- and Service-Checks and the Check plugins on your "
+"Monitoring (or monitored) systems"
+msgstr "監視、通知、またはイベントコマンドを定義します。 コマンド定義は、"
+"Host-CheckとService-Check、およびMonitoring(または監視対象)システムのCheckプラグインを結びつけます。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DatafieldDashlet.php:11
+msgid "Define Data Fields"
+msgstr "データフィールドの定義"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/HostGroupsDashlet.php:17
+msgid ""
+"Define Host Groups to give your configuration more structure. They are "
+"useful for Dashboards, Notifications or Restrictions"
+msgstr "設定をより構造化するためにホストグループを定義します。ダッシュボード、"
+"通知、制限に活用できます。"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:103
+msgid ""
+"Define a download Url or local directory from which the a specific Icinga 2 "
+"Agent MSI Installer package should be fetched. Please ensure to only define "
+"the base download Url or Directory. The Module will generate the MSI file "
+"name based on your operating system architecture and the version to install. "
+"The Icinga 2 MSI Installer name is internally build as follows: Icinga2-"
+"v[InstallAgentVersion]-[OSArchitecture].msi (full example: Icinga2-v2.6.3-"
+"x86_64.msi)"
+msgstr "特定のIcinga 2 Agent MSIインストーラパッケージを取得するダウンロード"
+"URLまたはローカルディレクトリを定義します。 基本ダウンロードURLまたは"
+"ディレクトリのみを定義してください。 モジュールは、オペレーティングシステムの"
+"アーキテクチャとインストールするバージョンに基づいてMSIファイル名を生成"
+"します。 Icinga 2 MSIインストーラー名は、内部的に次のように構築されています。"
+"Icinga2-v [InstallAgentVersion] - [OSArchitecture] .msi"
+"(完全な例:Icinga2-v2.6.3-x86_64.msi)"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ImportSourceDashlet.php:29
+msgid "Define and manage imports from various data sources"
+msgstr "さまざまなデータソースからのインポートを定義および管理します"
+
+#: ../../../../modules/director/library/Director/Dashboard/TimeperiodsDashboard.php:14
+msgid "Define custom Time Periods"
+msgstr "カスタムスケジュールの定義"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/SyncDashlet.php:29
+msgid "Define how imported data should be synchronized with Icinga"
+msgstr "インポートしたデータをIcingaと同期させる方法を定義します"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:51
+msgid ""
+"Define what should happen when an object with a matching key already exists. "
+"You could merge its properties (import source wins), replace it completely "
+"with the imported object or ignore it (helpful for one-time imports)"
+msgstr "一致するキーを持つオブジェクトがすでに存在するときに何が起こるかを"
+"定義します。 プロパティはマージすることができ(インポート元が優先)、"
+"それをインポートされたオブジェクトと完全に置き換えるか、"
+"あるいは無視することができます(一回限りのインポートに役立ちます)"
+
+#: ../../../../modules/director/library/Director/Dashboard/ObjectsDashboard.php:17
+msgid "Define whatever you want to be monitored"
+msgstr "監視対象を定義"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1319
+msgid "Defines after how many check attempts a new hard state is reached"
+msgstr "異常(hard state)と判定するまでの監視の最大試行回数"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/UserGroupsDashlet.php:17
+msgid ""
+"Defining Notifications for User Groups instead of single Users gives more "
+"flexibility"
+msgstr "単一のユーザーではなくユーザーグループの通知を定義すると、"
+"柔軟性が向上します。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php:17
+msgid ""
+"Defining Service Groups get more structure. Great for Dashboards. "
+"Notifications and Permissions might be based on groups."
+msgstr "サービスグループを定義すると、より構造がわかります。 ダッシュボードに"
+"最適です。 通知と許可はグループに基づいている場合があります。"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:196
+msgid "Delay unless the first notification should be sent"
+msgstr "最初の通知時間"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:185
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:869
+msgid "Delete"
+msgstr "削除"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierSplit.php:13
+msgid "Delimiter"
+msgstr "分割子"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:27
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php:13
+msgid "Dependency"
+msgstr "依存関係オブジェクト"
+
+#: ../../../../modules/director/application/forms/DeploymentLinkForm.php:85
+msgid "Deploy"
+msgstr "設定反映"
+
+#: ../../../../modules/director/application/forms/DeployConfigForm.php:39
+#, php-format
+msgid "Deploy %d pending changes"
+msgstr "%d 個の保留された変更を反映"
+
+#: ../../../../modules/director/library/Director/Dashboard/DeploymentDashboard.php:15
+msgid "Deploy configuration to your Icinga nodes"
+msgstr "Icingaノードへの設定の反映"
+
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:195
+msgid "Deploy modified config"
+msgstr "変更された設定を反映"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:248
+#: ../../../../modules/director/application/controllers/ConfigController.php:458
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:54
+msgid "Deployment"
+msgstr "設定反映"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:133
+msgid "Deployment Path"
+msgstr "反映パス"
+#smori
+
+#: ../../../../modules/director/application/controllers/DeploymentController.php:22
+msgid "Deployment details"
+msgstr "設定反映結果の詳細"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:118
+msgid "Deployment mode"
+msgstr "設定反映モード"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:127
+msgid "Deployment mode for Icinga 1 configuration"
+msgstr "Icinga1の設定反映モード"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:77
+msgid "Deployment time"
+msgstr "設定反映時刻"
+
+#: ../../../../modules/director/configuration.php:143
+#: ../../../../modules/director/application/controllers/ConfigController.php:50
+#: ../../../../modules/director/library/Director/Web/Tabs/InfraTabs.php:36
+msgid "Deployments"
+msgstr "設定反映"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:157
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:125
+#: ../../../../modules/director/application/forms/IcingaServiceSetForm.php:100
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:56
+#: ../../../../modules/director/application/forms/ImportRowModifierForm.php:54
+#: ../../../../modules/director/application/forms/ImportSourceForm.php:24
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:33
+msgid "Description"
+msgstr "説明"
+
+#: ../../../../modules/director/library/Director/Web/Table/SyncpropertyTable.php:63
+msgid "Destination"
+msgstr "宛先"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:48
+msgid "Destination Field"
+msgstr "宛先フィールド"
+
+#: ../../../../modules/director/application/controllers/HealthController.php:31
+msgid ""
+"Did you know that you can run this entire Health Check (or just some "
+"sections) as an Icinga Check on a regular base?"
+msgstr "この設定状況の確認を定期的にIcingaの監視として実行できます"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:403
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:249
+msgid "Diff"
+msgstr "差分"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeployedConfigInfoHeader.php:74
+msgid "Diff with other config"
+msgstr "ほかの設定との差分"
+
+#: ../../../../modules/director/library/Director/Web/Table/TemplateUsageTable.php:55
+msgid "Direct"
+msgstr "直接"
+
+#: ../../../../modules/director/application/controllers/HealthController.php:23
+msgid "Director Health"
+msgstr "Director 正常性"
+
+#: ../../../../modules/director/application/controllers/KickstartController.php:13
+msgid "Director Kickstart Wizard"
+msgstr "Director キックスタートウィザード"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/SettingsDashlet.php:11
+msgid "Director Settings"
+msgstr "director設定"
+
+#: ../../../../modules/director/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:79
+msgid "Director database schema has not been created yet"
+msgstr "Director データベーススキーマがまだ作成されていません"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:156
+msgid "Disable Checks"
+msgstr "監視を無効化"
+# smori
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:164
+msgid "Disable Notificiations"
+msgstr "通知を無効化"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:54
+msgid "Disable all Jobs"
+msgstr "すべてのジョブを無効化"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:37
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1222
+msgid "Disabled"
+msgstr "無効"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1223
+msgid "Disabled objects will not be deployed"
+msgstr "無効化されたオブジェクトは設定の反映がされません。"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodForm.php:20
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1238
+msgid "Display Name"
+msgstr "表示名"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:333
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:133
+msgid "Display name"
+msgstr "表示名"
+
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarTable.php:42
+msgid "Distinct Commands"
+msgstr "個別のコマンド"
+
+#: ../../../../modules/director/library/Director/Dashboard/DataDashboard.php:15
+msgid "Do more with custom data"
+msgstr "カスタムデータで高度に設定"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:36
+msgid "Do not transform at all"
+msgstr "全く変換しない"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:104
+#: ../../../../modules/director/library/Director/Web/SelfService.php:157
+msgid "Documentation"
+msgstr "ドキュメント"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/StateFilterSet.php:21
+msgid "Down"
+msgstr "Down (停止)"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:212
+#: ../../../../modules/director/application/controllers/SchemaController.php:80
+#: ../../../../modules/director/library/Director/Web/SelfService.php:227
+#: ../../../../modules/director/library/Director/Web/SelfService.php:240
+msgid "Download"
+msgstr "ダウンロード"
+
+#: ../../../../modules/director/library/Director/Web/Widget/AdditionalTableActions.php:59
+msgid "Download as JSON"
+msgstr "JSON形式でダウンロード"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:70
+msgid "Download from a custom url"
+msgstr "カスタムURLからダウンロード"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:69
+msgid "Download from packages.icinga.com"
+msgstr "packages.icinga.com からダウンロード"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:30
+msgid "Downtime ends"
+msgstr "DowntimeEnd (ダウンタイムの終了)"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:31
+msgid "Downtime removed"
+msgstr "DowntimeRemoved (ダウンタイムの削除)"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:29
+msgid "Downtime starts"
+msgstr "DowntimeStart (ダウンタイムの開始)"
+
+#: ../../../../modules/director/application/forms/IcingaForgetApiKeyForm.php:22
+msgid "Drop Self Service API key"
+msgstr "セルフサービスAPIキーを削除"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:83
+#: ../../../../modules/director/library/Director/Web/Widget/SyncRunDetails.php:26
+msgid "Duration"
+msgstr "継続"
+
+#: ../../../../modules/director/application/controllers/DatafieldController.php:38
+msgid "Edit a Field"
+msgstr "フィールドを編集"
+
+#: ../../../../modules/director/application/controllers/DataController.php:151
+msgid "Edit list"
+msgstr "リストを編集"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:36
+msgid "Email"
+msgstr "Eメール"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:69
+msgid "Enable audit log"
+msgstr "audit ログを有効にする"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1372
+msgid "Enable event handler"
+msgstr "イベントハンドラを有効にする"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1384
+msgid "Enable flap detection"
+msgstr "フラップ検知を有効にする"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:24
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:22
+#: ../../../../modules/director/library/Director/Web/Table/ObjectsTableEndpoint.php:19
+msgid "Endpoint"
+msgstr "エンドポイント"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:117
+msgid "Endpoint Name"
+msgstr "エンドポイント名"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:31
+msgid "Endpoint address"
+msgstr "エンドポイントアドレス"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:18
+msgid "Endpoint template name"
+msgstr "エンドポイントテンプレート名"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php:17
+#: ../../../../modules/director/library/Director/Import/ImportSourceCoreApi.php:59
+msgid "Endpoints"
+msgstr "エンドポイント"
+
+#: ../../../../modules/director/application/controllers/PhperrorController.php:12
+msgid "Error"
+msgstr "エラー"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:86
+msgid "Establish connection"
+msgstr "親ノードからの接続"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:608
+msgid ""
+"Evaluates the apply for rule for all objects with the custom attribute "
+"specified. E.g selecting \"host.vars.custom_attr\" will generate \"for "
+"(config in host.vars.array_var)\" where \"config\" will be accessible "
+"through \"$config$\". NOTE: only custom variables of type \"Array\" are "
+"eligible."
+msgstr "カスタム属性が指定されているすべてのオブジェクトのルール適用を"
+"評価します。 例えば\"host.vars.custom_attr\"を選択すると "
+"\"for(config.host.vars.array_var)\"が生成されます。ここで "
+"\"config\"は \"$config$\"を通してアクセス可能です。 "
+"注:タイプ \"配列\"のカスタム変数のみが対象です。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1276
+msgid "Event command"
+msgstr "イベントコマンド"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1277
+msgid "Event command definition"
+msgstr "イベントコマンドの定義"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodForm.php:70
+msgid "Exclude other time periods from this."
+msgstr "指定したスケジュール設定の期間を除外します。"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodForm.php:67
+msgid "Exclude period"
+msgstr "除外スケジュールのインクルード"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1354
+msgid "Execute active checks"
+msgstr "アクティブ監視を実行する"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:48
+msgid "Execution interval for this job, in seconds"
+msgstr "このジョブの実行間隔。秒単位"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:249
+msgid "Existing Data Lists"
+msgstr "存在するデータリスト"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:234
+msgid "Existing templates"
+msgstr "存在するテンプレート"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:178
+msgid "Expert mode"
+msgstr "エキスパートモード"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectsTabs.php:33
+msgid "External"
+msgstr "外部"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php:19
+msgid "External Commands"
+msgstr "外部コマンド"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php:12
+msgid ""
+"External Commands have been defined in your local Icinga 2 Configuration."
+msgstr "外部コマンドはローカルのIcinga 2 設定にて定義されています。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php:19
+msgid "External Notification Commands"
+msgstr "外部通知コマンド"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php:12
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php:12
+msgid ""
+"External Notification Commands have been defined in your local Icinga 2 "
+"Configuration. "
+msgstr "外部通知コマンドはローカルのIcinga 2 設定にて定義されています。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:141
+msgid "Failed"
+msgstr "失敗"
+
+#: ../../../../modules/director/application/forms/IcingaImportObjectForm.php:42
+#, php-format
+msgid "Failed to import %s \"%s\""
+msgstr "%s の \"%s\"へのインポートが失敗しました。"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:188
+msgid "Field has been removed"
+msgstr "フィールドが削除されました"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:139
+#: ../../../../modules/director/library/Director/Web/Table/DatafieldTable.php:48
+#: ../../../../modules/director/library/Director/Web/Table/IcingaObjectDatafieldTable.php:50
+msgid "Field name"
+msgstr "フィールド名"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:175
+msgid "Field type"
+msgstr "フィールドタイプ"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectTabs.php:103
+msgid "Fields"
+msgstr "フィールド"
+
+#: ../../../../modules/director/library/Director/Web/Table/ConfigFileDiffTable.php:82
+#: ../../../../modules/director/library/Director/Web/Table/GeneratedConfigFileTable.php:84
+msgid "File"
+msgstr "ファイル"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:102
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:79
+msgid "Filter Expression"
+msgstr "フィルタ表現"
+
+#: ../../../../modules/director/configuration.php:52
+msgid "Filter available notification apply rules"
+msgstr "利用可能な通知適用ルールをフィルタリング"
+
+#: ../../../../modules/director/configuration.php:45
+msgid "Filter available service apply rules"
+msgstr "利用可能なサービス適用ルールをフィルタリング"
+
+#: ../../../../modules/director/configuration.php:59
+msgid ""
+"Filter available service set templates. Use asterisks (*) as wildcards, like "
+"in DB* or *net*"
+msgstr "利用可能なサービスセットテンプレートをフィルタリング。"
+"「DB*」、「*net*」のように、アスタリスク(*)をワイルドカードとして使う。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:29
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:27
+msgid "Filter method"
+msgstr "フィルタメソッド"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:33
+msgid "First Element"
+msgstr "第一要素"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:194
+msgid "First notification delay"
+msgstr "最初の通知の遅延"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:33
+msgid "Flapping"
+msgstr "フラッピング"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:35
+msgid "Flapping ends"
+msgstr "FlappingEnd (フラッピング終了)"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1405
+msgid ""
+"Flapping lower bound in percent for a service to be considered not flapping"
+msgstr "サービスをフラッピング状態とみなすための下限値(%単位)"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:34
+msgid "Flapping starts"
+msgstr "FlappingStart (フラッピング開始)"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1392
+msgid "Flapping threshold (high)"
+msgstr "フラッピング閾値(上限)"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1403
+msgid "Flapping threshold (low)"
+msgstr "フラッピング閾値(下限)"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1394
+msgid "Flapping upper bound in percent for a service to be considered flapping"
+msgstr "サービスをフラッピング状態とみなすための上限値(%単位)"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:33
+msgid "Flatten all inherited properties, strip imports"
+msgstr "継承されたすべてのプロパティを統合し、インポートを削除します"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:87
+msgid "Flush API directory"
+msgstr "APIディレクトリをフラッシュする"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:223
+msgid "For manual configuration"
+msgstr "手動設定用"
+
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:181
+msgid "Force rendering"
+msgstr "強制的に設定を生成する"
+
+#: ../../../../modules/director/library/Director/Objects/DirectorDatafield.php:160
+#, php-format
+msgid "Form element could not be created, %s is missing"
+msgstr "フォーム要素が作成できません。%s がありません"
+
+#: ../../../../modules/director/library/Director/Web/Form/QuickForm.php:456
+#: ../../../../modules/director/library/Director/Web/Form/QuickForm.php:481
+msgid "Form has successfully been sent"
+msgstr "フォームは正常に送信されました"
+
+#: ../../../../modules/director/application/forms/IcingaHostVarForm.php:32
+#: ../../../../modules/director/application/forms/IcingaServiceVarForm.php:32
+msgid "Format"
+msgstr "フォーマット"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:269
+msgid "Former object"
+msgstr "以前のオブジェクト"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:24
+msgid "Fully qualified domain name (FQDN)"
+msgstr "完全修飾ドメイン名(FQDN)"
+
+#: ../../../../modules/director/application/forms/IcingaGenerateApiKeyForm.php:24
+msgid "Generate Self Service API key"
+msgstr "セルフサービスAPIキーを生成"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:121
+msgid "Generate a new key"
+msgstr "新しい鍵を生成"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:259
+msgid "Generated config"
+msgstr "設定を生成しました"
+
+#: ../../../../modules/director/application/controllers/HostController.php:149
+#: ../../../../modules/director/application/controllers/HostController.php:229
+msgid "Generated from host vars"
+msgstr "ホスト変数から生成しました"
+
+#: ../../../../modules/director/library/Director/Dashboard/AlertsDashboard.php:15
+msgid "Get alerts when something goes wrong"
+msgstr "障害発生時のアラート通知"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/CustomvarDashlet.php:17
+msgid "Get an overview of used CustomVars and their variants"
+msgstr "使用されているカスタム変数とその状況の概要です"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:219
+msgid "Global Director Settings"
+msgstr "グローバルDirector設定"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:97
+msgid "Global Self Service Setting"
+msgstr "グローバルセルフサービス設定"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:44
+msgid "Global Zones"
+msgstr "グローバルゾーン"
+
+#: ../../../../modules/director/application/forms/IcingaZoneForm.php:22
+msgid "Global zone"
+msgstr "グローバルゾーン"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierJoin.php:13
+msgid "Glue"
+msgstr "接着剤"
+
+#: ../../../../modules/director/library/Director/Web/ActionBar/DirectorBaseActionBar.php:40
+#, php-format
+msgid "Go back to \"%s\" Dashboard"
+msgstr "\"%s\" ダッシュボードに戻る"
+
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:207
+msgid "Grace period"
+msgstr "猶予期間"
+
+#: ../../../../modules/director/library/Director/Web/Table/GroupMemberTable.php:59
+msgid "Group"
+msgstr "グループ"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:233
+msgid ""
+"Group has been inherited, but will be overridden by locally assigned group(s)"
+msgstr "グループは継承されましたが、ローカルに割り当てられたグループによって上書きされます"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:316
+msgid "Group membership"
+msgstr "グループのメンバーシップ"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:257
+#, php-format
+msgid "Group membership: %s"
+msgstr "グループのメンバーシップ: %s"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php:17
+msgid ""
+"Grouping your Services into Sets allow you to quickly assign services often "
+"used together in a single operation all at once"
+msgstr "サービスをセットにグループ化すると、一度に1つの操作で一緒に使用される"
+"ことが多いサービスを一度に素早く割り当てることができます。"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:203
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:630
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:109
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectsTabs.php:60
+msgid "Groups"
+msgstr "グループ"
+
+#: ../../../../modules/director/application/controllers/DashboardController.php:51
+#: ../../../../modules/director/application/controllers/HealthController.php:19
+msgid "Health"
+msgstr "director の設定状況"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/SingleServicesDashlet.php:17
+msgid "Here you can find all single services directly attached to single hosts"
+msgstr "単一のホストに直接接続されているすべての単一のサービスを"
+"見つけることができます。"
+
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiFieldsTable.php:81
+msgid "Hidden"
+msgstr "隠されています"
+
+#: ../../../../modules/director/library/Director/Web/Widget/AdditionalTableActions.php:70
+msgid "Hide SQL"
+msgstr "SQLを隠す"
+
+#: ../../../../modules/director/application/controllers/HealthController.php:29
+msgid "Hint: Check Plugin"
+msgstr "ヒント:監視プラグイン"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:154
+msgid "Hints regarding this service"
+msgstr "このサービスに関するヒント"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ImportsourceTabs.php:45
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectTabs.php:95
+#: ../../../../modules/director/library/Director/Web/Tabs/SyncRuleTabs.php:39
+msgid "History"
+msgstr "履歴"
+
+#: ../../../../modules/director/application/controllers/ServiceController.php:53
+#: ../../../../modules/director/application/forms/IcingaHostVarForm.php:15
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:578
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:12
+#: ../../../../modules/director/library/Director/TranslationDummy.php:13
+#: ../../../../modules/director/library/Director/Web/Table/ObjectsTableEndpoint.php:20
+msgid "Host"
+msgstr "ホスト"
+
+#: ../../../../modules/director/application/controllers/SuggestController.php:240
+#: ../../../../modules/director/library/Director/Objects/IcingaService.php:723
+msgid "Host Custom variables"
+msgstr "ホストカスタム変数"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:18
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:13
+msgid "Host Group"
+msgstr "ホストグループ"
+
+msgid "HostGroup"
+msgstr "ホストグループ"
+
+msgid "HostGroups"
+msgstr "ホストグループ"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/HostGroupsDashlet.php:11
+msgid "Host Groups"
+msgstr "ホストグループ"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:19
+msgid "Host Name"
+msgstr "ホスト名"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:157
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:174
+msgid "Host Template"
+msgstr "ホストテンプレート"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:19
+msgid "Host Template Choice"
+msgstr "ホストテンプレートチョイス"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:20
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php:11
+msgid "Host Templates"
+msgstr "ホストテンプレート"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:308
+#: ../../../../modules/director/application/forms/IcingaHostSelfServiceForm.php:35
+msgid "Host address"
+msgstr "ホストアドレス"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:310
+#: ../../../../modules/director/application/forms/IcingaHostSelfServiceForm.php:37
+msgid ""
+"Host address. Usually an IPv4 address, but may be any kind of address your "
+"check plugin is able to deal with"
+msgstr "ホストアドレス。 通常はIPv4アドレスですが、監視プラグインが扱う全てのアドレスを利用できます。"
+
+#: ../../../../modules/director/configuration.php:64
+msgid "Host configs"
+msgstr "ホスト設定"
+
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:55
+msgid "Host groups"
+msgstr "ホストグループ"
+
+#: ../../../../modules/director/application/forms/IcingaHostSelfServiceForm.php:25
+msgid "Host name"
+msgstr "ホスト名"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:25
+msgid "Host name (local part, without domain)"
+msgstr "ホスト名(ドメインなしのローカルパート)"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/HostObjectDashlet.php:13
+msgid "Host objects"
+msgstr "ホストオブジェクト"
+
+#: ../../../../modules/director/application/controllers/SuggestController.php:230
+#: ../../../../modules/director/application/controllers/SuggestController.php:239
+#: ../../../../modules/director/library/Director/Objects/IcingaHost.php:156
+#: ../../../../modules/director/library/Director/Objects/IcingaService.php:722
+msgid "Host properties"
+msgstr "ホストプロパティ"
+
+#: ../../../../modules/director/application/controllers/TemplatechoiceController.php:17
+msgid "Host template choice"
+msgstr "ホストテンプレートの選択"
+
+#: ../../../../modules/director/application/controllers/TemplatechoicesController.php:19
+msgid "Host template choices"
+msgstr "ホストテンプレートの選択"
+
+#: ../../../../modules/director/application/forms/IcingaHostGroupForm.php:14
+msgid "Hostgroup"
+msgstr "ホストグループ"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceCoreApi.php:61
+msgid "Hostgroups"
+msgstr "ホストグループ"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:206
+msgid ""
+"Hostgroups that should be directly assigned to this node. Hostgroups can be "
+"useful for various reasons. You might assign service checks based on "
+"assigned hostgroup. They are also often used as an instrument to enforce "
+"restricted views in Icinga Web 2. Hostgroups can be directly assigned to "
+"single hosts or to host templates. You might also want to consider assigning "
+"hostgroups using apply rules"
+msgstr "このノードに直接割り当てる必要があるホストグループ。 "
+"ホストグループはさまざまな理由で役に立ちます。 割り当てられた"
+"ホストグループに基づいてサービス監視を割り当てることができます。"
+" それらは、Icinga Web 2で制限付きビューを強制する手段としてもよく"
+"使用されます。ホストグループは、単一のホストまたはホストテンプレートに"
+"直接割り当てることができます。 適用ルールを使用してホストグループを"
+"割り当てることもできます。"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:39
+#: ../../../../modules/director/library/Director/Web/Table/IcingaServiceSetHostTable.php:38
+msgid "Hostname"
+msgstr "ホスト名"
+
+#: ../../../../modules/director/configuration.php:118
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:99
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:89
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:688
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/HostsDashlet.php:11
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:54
+#: ../../../../modules/director/library/Director/IcingaConfig/StateFilterSet.php:19
+#: ../../../../modules/director/library/Director/Import/ImportSourceCoreApi.php:60
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarTable.php:43
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarVariantsTable.php:58
+msgid "Hosts"
+msgstr "ホスト"
+
+#: ../../../../modules/director/application/controllers/ServicesetController.php:81
+#, php-format
+msgid "Hosts using this set: %s"
+msgstr "このセットを利用しているホスト: %s"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:32
+msgid "IP address / hostname of remote node"
+msgstr "リモートノードのIP アドレス / ホスト名"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:129
+msgid ""
+"IP address / hostname of your Icinga node. Please note that this information "
+"will only be used for the very first connection to your Icinga instance. The "
+"Director then relies on a correctly configured Endpoint object. Correctly "
+"configures means that either it's name is resolvable or that it's host "
+"property contains either an IP address or a resolvable host name. Your "
+"Director must be able to reach this endpoint"
+msgstr "IcingaノードのIPアドレス/ホスト名。 この情報は、"
+"Icingaインスタンスへの最初の接続にのみ使用されることに注意してください。"
+"その後、directorは正しく設定されたエンドポイントオブジェクトに依存します。"
+"正しく構成されているということは、その名前が解決可能であるか、または"
+"ホストプロパティにIPアドレスまたは解決可能なホスト名が含まれていることを"
+"意味します。 directorはこのエンドポイントに到達できなければなりません"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:316
+#: ../../../../modules/director/application/forms/IcingaHostSelfServiceForm.php:43
+msgid "IPv6 address"
+msgstr "IPv6 アドレス"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectsController.php:282
+#, php-format
+msgid "Icinga %s Sets"
+msgstr "Icinga %s セット"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:39
+#, php-format
+msgid "Icinga 2 - Objects: %s"
+msgstr "Icinga 2 API - オブジェクト: %s"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:152
+msgid "Icinga 2 API - Status"
+msgstr "Icinga 2 API - ステータス"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:192
+msgid "Icinga 2 Client documentation"
+msgstr "Icinga 2 Client のドキュメント"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:135
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:678
+msgid "Icinga Agent and zone settings"
+msgstr "Icingaエージェントとゾーンの設定"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php:13
+msgid "Icinga Api users"
+msgstr "Icinga APIユーザ"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:34
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:73
+msgid "Icinga DSL"
+msgstr "Icinga DSL"
+
+#: ../../../../modules/director/configuration.php:83
+msgid "Icinga Director"
+msgstr "Icinga director"
+
+#: ../../../../modules/director/application/controllers/DashboardController.php:35
+msgid "Icinga Director - Main Dashboard"
+msgstr "Icinga Director - メインダッシュボード"
+
+#: ../../../../modules/director/library/Director/Dashboard/DirectorDashboard.php:15
+msgid "Icinga Director Configuration"
+msgstr "Icinga directorの設定"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:35
+msgid ""
+"Icinga Director decides to deploy objects like CheckCommands to a global "
+"zone. This defaults to \"director-global\" but might be adjusted to a custom "
+"Zone name"
+msgstr "Icinga Directorは、監視コマンドなどのオブジェクト設定をグローバル"
+"ゾーンに反映するようにしました。 デフォルトでは「director-global」です。"
+"任意のゾーン名が指定されている場合は、そのゾーンに反映されます。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/SelfServiceDashlet.php:17
+msgid ""
+"Icinga Director offers a Self Service API, allowing new Icinga nodes to "
+"register themselves"
+msgstr "Icinga directorはセルフサービスAPIを提供し、新しいIcingaノードが"
+"自分自身を登録できるようにします。"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:127
+msgid "Icinga Host"
+msgstr "Icinga ホスト"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/InfrastructureDashlet.php:11
+msgid "Icinga Infrastructure"
+msgstr "Icinga インフラストラクチャ"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:43
+msgid "Icinga Package Name"
+msgstr "Icingaパッケージ名"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1106
+msgid ""
+"Icinga cluster zone. Allows to manually override Directors decisions of "
+"where to deploy your config to. You should consider not doing so unless you "
+"gained deep understanding of how an Icinga Cluster stack works"
+msgstr "設定を反映するクラスタゾーンを設定します。Icingaクラスタが"
+"どのように機能しているか理解していない場合は、設定しないでください。"
+
+#: ../../../../modules/director/application/forms/IcingaHostGroupForm.php:16
+msgid "Icinga object name for this host group"
+msgstr "このホストグループのIcingaオブジェクト名"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:46
+msgid ""
+"Icinga object name for this host. This is usually a fully qualified host "
+"name but it could basically be any kind of string. To make things easier for "
+"your users we strongly suggest to use meaningful names for templates. E.g. "
+"\"generic-host\" is ugly, \"Standard Linux Server\" is easier to understand"
+msgstr "このホストのIcingaオブジェクト名。これは通常完全修飾ホスト名ですが、"
+"基本的にはあらゆる種類の文字列にすることができます。 ユーザーの作業を"
+"容易にするために、テンプレートにはわかりやすい名前を使用することを"
+"強くお勧めします。 例えば、\"generic-host\"はわかりにくいですが、"
+"\"Standard Linux Server\"は理解しやすいです"
+
+#: ../../../../modules/director/application/forms/IcingaServiceGroupForm.php:16
+msgid "Icinga object name for this service group"
+msgstr "このサービスグループのIcingaオブジェクト名"
+
+#: ../../../../modules/director/application/forms/IcingaUserGroupForm.php:19
+msgid "Icinga object name for this user group"
+msgstr "このユーザグループのIcingaオブジェクト名"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:94
+msgid "Icinga v1.x"
+msgstr "Icinga v1.x"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:93
+msgid "Icinga v2.x"
+msgstr "Icinga v2.x"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:77
+msgid "Icinga2 Agent"
+msgstr "Icinga2 エージェント"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1482
+msgid "Icon image"
+msgstr "アイコン画像"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1491
+msgid "Icon image alt"
+msgstr "アイコン画像の代替テキスト"
+
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiFieldsTable.php:74
+msgid "Id"
+msgstr "ID"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:53
+msgid "Identifier for the Icinga command you are going to create"
+msgstr "作成しようとしているIcingaコマンドのID"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:49
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:60
+msgid "Ignore"
+msgstr "無視"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:172
+msgid "Ignore Soft States"
+msgstr "一時的な異常(soft state)を無視"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:29
+msgid "Import Sources"
+msgstr "インポートソース"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ImportSourceDashlet.php:14
+msgid "Import data sources"
+msgstr "データソースのインポート"
+
+#: ../../../../modules/director/application/forms/IcingaImportObjectForm.php:26
+#, php-format
+msgid "Import external \"%s\""
+msgstr "外部\"%s\"をインポート"
+
+#: ../../../../modules/director/application/controllers/ImportrunController.php:14
+#: ../../../../modules/director/application/controllers/ImportrunController.php:15
+msgid "Import run"
+msgstr "処理をインポート"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:183
+#, php-format
+msgid "Import run history: %s"
+msgstr "実行履歴をインポート: %s "
+
+#: ../../../../modules/director/application/controllers/ImportsourcesController.php:46
+#: ../../../../modules/director/library/Director/Job/ImportJob.php:80
+#: ../../../../modules/director/library/Director/Web/Tabs/ImportTabs.php:20
+#: ../../../../modules/director/library/Director/Web/Tabs/ImportsourceTabs.php:37
+msgid "Import source"
+msgstr "インポートソース"
+
+#: ../../../../modules/director/application/forms/ImportSourceForm.php:15
+msgid "Import source name"
+msgstr "インポートソース名"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:138
+#, php-format
+msgid "Import source preview: %s"
+msgstr "インポートソースのプレビュー: %s"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:81
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:109
+#, php-format
+msgid "Import source: %s"
+msgstr "インポートソース: %s"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1196
+msgid ""
+"Importable templates, add as many as you want. Please note that order "
+"matters when importing properties from multiple templates: last one wins"
+msgstr "必要なテンプレートを追加してください。複数のテンプレートを"
+"インポートする場合、最後にインポートされたテンプレートのプロパティが"
+"優先されます。"
+
+#: ../../../../modules/director/application/forms/ImportRunForm.php:33
+msgid "Imported new data from this Import Source"
+msgstr "このインポートソースからインポートされた新しいデータ"
+
+#: ../../../../modules/director/library/Director/Web/Table/ImportrunTable.php:32
+msgid "Imported rows"
+msgstr "インポートされた行"
+
+#: ../../../../modules/director/application/forms/IcingaImportObjectForm.php:16
+msgid ""
+"Importing an object means that its type will change from \"external\" to "
+"\"object\". That way it will make part of the next deployment. So in case "
+"you imported this object from your Icinga node make sure to remove it from "
+"your local configuration before issueing the next deployment. In case of a "
+"conflict nothing bad will happen, just your config won't deploy."
+msgstr "オブジェクトをインポートすると、そのタイプは「外部」から"
+"「オブジェクト」に変わります。 それは次の設定反映の対象になります。そのため、"
+"Icingaノードからこのオブジェクトをインポートした場合は、次の設定反映を行う前に"
+"必ずローカル設定から削除してください。設定の衝突が発生した場合、ほかへの"
+"悪影響はありませんが、衝突した設定が反映されません。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1194
+msgid "Imports"
+msgstr "インポート"
+
+#: ../../../../modules/director/application/controllers/SelfServiceController.php:104
+msgid ""
+"In case an Icinga Admin provided you with a self service API token, this is "
+"where you can register new hosts"
+msgstr "Icinga管理者からセルフサービスAPIトークンが提供された場合は、"
+"ここで新しいホストを登録できます。"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:176
+msgid ""
+"In case the Icinga 2 Agent is already installed on the system, this "
+"parameter will allow you to configure if you wish to upgrade / downgrade to "
+"a specified version with the as well."
+msgstr "Icinga 2 Agent がすでにシステムにインストールされている場合は、"
+"このパラメータを使用して指定したバージョンにアップグレードまたは"
+"ダウングレードするかも設定できます。"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:143
+msgid ""
+"In case the Icinga 2 Agent should be automatically installed, this has to be "
+"a string value like: 2.6.3"
+msgstr "Icinga 2 Agent を自動的にインストールする必要がある場合、そのバージョ"
+"ンを指定します。これは「2.6.3」のような文字列値にする必要があります"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:89
+msgid ""
+"In case the Icinga Agent will accept configuration from the parent Icinga 2 "
+"system, it will possibly write data to /var/lib/icinga2/api/*. By setting "
+"this parameter to true, all content inside the api directory will be flushed "
+"before an eventual restart of the Icinga 2 Agent"
+msgstr "Icinga Agentが親のIcinga 2システムから設定を受け入れる場合、おそらく"
+" /var/lib/icinga2/api/* にデータを書き込みます。 このパラメータを「はい」"
+"に設定すると、apiディレクトリ内のすべてのコンテンツは、最終的に"
+"Icinga 2 Agentを再起動する前にフラッシュされます。"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodForm.php:62
+msgid "Include other time periods into this."
+msgstr "選択した他のスケジュール設定を適用します"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodForm.php:59
+msgid "Include period"
+msgstr "スケジュールのインクルード"
+
+#: ../../../../modules/director/library/Director/Web/Table/TemplateUsageTable.php:56
+msgid "Indirect"
+msgstr "間接"
+
+#: ../../../../modules/director/application/controllers/HostController.php:140
+#: ../../../../modules/director/application/controllers/HostController.php:218
+msgid "Individual Service objects"
+msgstr "個々のサービスオブジェクト"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/InfraTabs.php:43
+msgid "Infrastructure"
+msgstr "インフラストラクチャ"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:310
+msgid "Inheritance (import)"
+msgstr "継承(インポート)"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:240
+msgid "Inherited groups"
+msgstr "継承されたグループ"
+
+#: ../../../../modules/director/application/controllers/HostController.php:386
+#, php-format
+msgid "Inherited service: %s"
+msgstr "継承されたサービス: %s"
+
+#: ../../../../modules/director/application/controllers/HostController.php:537
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectTabs.php:132
+msgid "Inspect"
+msgstr "検査"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:65
+msgid "Inspect - object list"
+msgstr "検査 - オブジェクトリスト"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:189
+msgid "Install NSClient++"
+msgstr "NSClient++ をインストール"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:59
+msgid "Installation Source"
+msgstr "インストールソース"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:152
+msgid "Installer Hashes"
+msgstr "インストーラのハッシュ"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:25
+msgid "Internal commands"
+msgstr "内部コマンド"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:97
+#, php-format
+msgid "It has been renamed since then, its former name was %s"
+msgstr "名前が変更されました(以前の名前: %s)"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:68
+msgid ""
+"It is not a good idea to do so as long as your Agent still has a valid Self "
+"Service API key!"
+msgstr "エージェントが有効なセルフサービスAPIキーをまだ持っている場合は、"
+"その操作はお勧めできません。"
+
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:87
+msgid ""
+"It will not be allowed to choose more than this many options. Setting it to "
+"one (1) will result in a drop-down box, a higher number will turn this into "
+"a multi-selection element."
+msgstr "これ以上の選択肢を選択することは許可されません。 これを1に設定するとドロップダウンボックスが表示され、数値が大きいほど複数選択要素になります。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ImportSourceDetails.php:40
+msgid ""
+"It's currently unknown whether we are in sync with this Import Source. You "
+"should either check for changes or trigger a new Import Run."
+msgstr "このインポートソースと同期しているかどうかは現在不明です。"
+"変更を確認するか、新しいインポート実行をトリガする必要があります。"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:57
+msgid ""
+"It's currently unknown whether we are in sync with this rule. You should "
+"either check for changes or trigger a new Sync Run."
+msgstr "このルールで同期しているかどうかは現在不明です。 変更を確認するか、"
+"新しい同期実行をトリガする必要があります。"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:120
+#: ../../../../modules/director/application/forms/BasketUploadForm.php:130
+msgid "It's not allowed to store an empty basket"
+msgstr "空のバスケットは保存できません"
+
+#: ../../../../modules/director/application/controllers/JobController.php:97
+msgid "Job"
+msgstr "ジョブ"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:31
+msgid "Job Definitions"
+msgstr "ジョブの定義"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:17
+msgid "Job Type"
+msgstr "ジョブタイプ"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:72
+#: ../../../../modules/director/library/Director/Web/Table/JobTable.php:60
+msgid "Job name"
+msgstr "ジョブ名"
+
+#: ../../../../modules/director/application/controllers/JobController.php:22
+#: ../../../../modules/director/application/controllers/JobController.php:54
+#, php-format
+msgid "Job: %s"
+msgstr "ジョブ: %s"
+
+#: ../../../../modules/director/application/controllers/JobsController.php:13
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/JobDashlet.php:14
+#: ../../../../modules/director/library/Director/Web/Tabs/ImportTabs.php:26
+msgid "Jobs"
+msgstr "ジョブ"
+
+#: ../../../../modules/director/library/Director/Web/Table/ActivityLogTable.php:81
+msgid "Jump to this object"
+msgstr "このオブジェクトに飛ぶ"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:245
+msgid "Just download and run this script on your Linux Client Machine:"
+msgstr "このスクリプトをLinuxクライアントマシンにダウンロードして実行するだけです。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:57
+msgid "Keep matching elements"
+msgstr "一致する要素を保持します"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:60
+msgid "Keep only matching rows (Whitelist)"
+msgstr "一致する行(ホワイトリスト)のみを保持します"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:31
+msgid "Keep the DN as is"
+msgstr "DNが次であるように保持します:"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierJsonDecode.php:26
+msgid "Keep the JSON string as is"
+msgstr "JSON文字列が次であるように保持します:"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierGetHostByName.php:18
+msgid "Keep the property (hostname) as is"
+msgstr "プロパティ(ホスト名)が次であるように保持します:"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierDnsRecords.php:35
+msgid "Keep the property as is"
+msgstr "プロパティが次であるように保持します:"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistEntryForm.php:21
+#: ../../../../modules/director/library/Director/Web/Table/DatalistEntryTable.php:54
+msgid "Key"
+msgstr "キー"
+
+#: ../../../../modules/director/application/forms/ImportSourceForm.php:87
+msgid "Key column name"
+msgstr "キーとなるカラム名"
+
+#: ../../../../modules/director/application/controllers/IndexController.php:42
+#: ../../../../modules/director/application/controllers/KickstartController.php:12
+msgid "Kickstart"
+msgstr "キックスタート"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:315
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/KickstartDashlet.php:11
+msgid "Kickstart Wizard"
+msgstr "キックスタートウィザード"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceLdap.php:47
+msgid "LDAP Search Base"
+msgstr "LDAP検索ベースDN"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistEntryForm.php:30
+#: ../../../../modules/director/library/Director/Web/Table/DatafieldTable.php:47
+#: ../../../../modules/director/library/Director/Web/Table/DatalistEntryTable.php:55
+#: ../../../../modules/director/library/Director/Web/Table/IcingaObjectDatafieldTable.php:49
+msgid "Label"
+msgstr "ラベル"
+
+#: ../../../../modules/director/library/Director/Web/Widget/IcingaObjectInspection.php:58
+msgid "Last Check Result"
+msgstr "最終監視結果"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:34
+msgid "Last Element"
+msgstr "最後の要素"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:205
+msgid "Last notification"
+msgstr "最終通知までの時間"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeployedConfigInfoHeader.php:67
+msgid "Last related activity"
+msgstr "最後の関連アクティビティ"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:93
+msgid "Last sync run details"
+msgstr "最終同期の処理の詳細"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:64
+msgid ""
+"Leave empty for non-positional arguments. Can be a positive or negative "
+"number and influences argument ordering"
+msgstr "位置に関する引数でない場合は空のままにしてください。"
+" 正数または負数にすることができ、引数の順序に影響を与えます"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:42
+msgid ""
+"Leaving custom variables in place while removing the related field is "
+"perfectly legal and might be a desired operation. This way you can no longer "
+"modify related custom variables in the Director GUI, but the variables "
+"themselves will stay there and continue to be deployed. When you re-add a "
+"field for the same variable later on, everything will continue to work as "
+"before"
+msgstr "関連フィールドを削除し、カスタム変数をそのままにしておくことも"
+"可能です。 このような状態では、Directorで"
+"関連するカスタム変数を変更することはできなくなりますが、変数自体はそのまま"
+"利用できます。 後で同じ変数のフィールドを追加し直しても、"
+"以前と同じように機能し続けます。"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:85
+msgid ""
+"Leaving custom variables in place while renaming the related field is "
+"perfectly legal and might be a desired operation. This way you can no longer "
+"modify related custom variables in the Director GUI, but the variables "
+"themselves will stay there and continue to be deployed. When you re-add a "
+"field for the same variable later on, everything will continue to work as "
+"before"
+msgstr "関連フィールドを削除し、カスタム変数をそのままにしておくことも"
+"可能です。 このような状態では、Directorで"
+"関連するカスタム変数を変更することはできなくなりますが、変数自体はそのまま"
+"利用できます。 後で同じ変数のフィールドを追加し直しても、"
+"以前と同じように機能し続けます。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:45
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMap.php:35
+msgid "Let the import fail"
+msgstr "インポートを失敗にする"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:54
+msgid "Let the whole Import Run fail"
+msgstr "インポート実行全体を失敗にする"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierDnsRecords.php:36
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:32
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierGetHostByName.php:19
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierJsonDecode.php:27
+msgid "Let the whole import run fail"
+msgstr "インポート実行全体を失敗にする"
+
+#: ../../../../modules/director/configuration.php:38
+msgid "Limit access to the given comma-separated list of hostgroups"
+msgstr "指定したホストグループのカンマ区切りリストへのアクセスを制限します"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:238
+msgid "Linux commandline"
+msgstr "Linux コマンドライン"
+
+#: ../../../../modules/director/application/controllers/DataController.php:91
+msgid "List Entries"
+msgstr "リストエントリ"
+
+#: ../../../../modules/director/application/controllers/DataController.php:155
+msgid "List entries"
+msgstr "リストエントリ"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistForm.php:13
+#: ../../../../modules/director/library/Director/Web/Table/DatalistTable.php:31
+msgid "List name"
+msgstr "リスト名"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:135
+msgid ""
+"Local directory to deploy Icinga 1.x configuration. Must be writable by "
+"icingaweb2. (e.g. /etc/icinga/director)"
+msgstr "Icinga 1.x構成を反映するためのローカルディレクトリ。 "
+"icingaweb2によって書き込み可能でなければなりません。"
+" (例:/etc/icinga/director)"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:41
+msgid "Log Duration"
+msgstr "ログ保存期間"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:66
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:563
+msgid "Main properties"
+msgstr "プロパティ"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php:12
+msgid ""
+"Manage definitions for your Commands that should be executed as Check "
+"Plugins, Notifications or based on Events"
+msgstr "監視、通知、またはイベントに基づいて実行されるコマンド"
+"の定義を管理します。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php:17
+msgid ""
+"Manage your Host Templates. Use Fields to make it easy for your users to get "
+"them customized."
+msgstr "ホストテンプレートを管理します。 ユーザーが簡単にカスタマイズできるようにするためには、フィールドを使用してください。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/InfrastructureDashlet.php:17
+msgid ""
+"Manage your Icinga 2 infrastructure: Masters, Zones, Satellites and more"
+msgstr "マスター/ゾーン/サテライトなど、Icinga 2 の基盤となる設定を管理します"
+
+#: ../../../../modules/director/library/Director/Dashboard/CommandsDashboard.php:17
+msgid "Manage your Icinga Commands"
+msgstr "Icinga コマンドを管理"
+
+#: ../../../../modules/director/library/Director/Dashboard/HostsDashboard.php:16
+msgid "Manage your Icinga Hosts"
+msgstr "Icinga のホストの管理"
+
+#: ../../../../modules/director/library/Director/Dashboard/InfrastructureDashboard.php:18
+msgid "Manage your Icinga Infrastructure"
+msgstr "Icinga インフラストラクチャの管理"
+
+#: ../../../../modules/director/library/Director/Dashboard/ServicesDashboard.php:18
+msgid "Manage your Icinga Service Checks"
+msgstr "Icinga サービス監視を管理"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php:17
+msgid ""
+"Manage your Service Templates. Use Fields to make it easy for your users to "
+"get them customized."
+msgstr "サービステンプレートを管理します。 ユーザーがフィールドを"
+"カスタマイズしやすくするには、フィールドを使用します。"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:134
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:139
+#: ../../../../modules/director/library/Director/Web/Table/IcingaObjectDatafieldTable.php:51
+msgid "Mandatory"
+msgstr "必須"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:123
+msgid "Master-less"
+msgstr "マスター無し"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:35
+msgid "Match NULL value columns"
+msgstr "NULL値のカラムと一致"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:34
+msgid "Match boolean FALSE"
+msgstr "真偽値FALSEに一致"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:33
+msgid "Match boolean TRUE"
+msgstr "真偽値TRUEに一致"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1317
+msgid "Max check attempts"
+msgstr "最大監視試行回数"
+
+#: ../../../../modules/director/library/Director/Web/Table/GroupMemberTable.php:60
+#: ../../../../modules/director/library/Director/Web/Table/GroupMemberTable.php:65
+msgid "Member"
+msgstr "メンバー"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectTabs.php:114
+msgid "Members"
+msgstr "メンバー"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:58
+msgid "Merge"
+msgstr "マージ"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:116
+msgid "Merge Policy"
+msgstr "マージポリシー"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodRangeForm.php:23
+msgid ""
+"Might be monday, tuesday or 2016-01-28 - have a look at the documentation for "
+"more examples"
+msgstr "monday, tuesday, 2016-01-28といった書式で指定します。"
+"より多くの例についてはドキュメントを見てください"
+
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:73
+msgid "Minimum required"
+msgstr "最低要件"
+
+#: ../../../../modules/director/application/forms/ImportRowModifierForm.php:71
+msgid "Modifier"
+msgstr "プロパティ変換ルール"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ImportsourceTabs.php:41
+msgid "Modifiers"
+msgstr "プロパティ変換ルール"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:103
+#: ../../../../modules/director/application/controllers/SyncruleController.php:189
+#: ../../../../modules/director/library/Director/ProvidedHook/Monitoring/ServiceActions.php:52
+#: ../../../../modules/director/library/Director/Web/ActionBar/AutomationObjectActionBar.php:38
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:118
+#: ../../../../modules/director/library/Director/Web/Tabs/SyncRuleTabs.php:33
+msgid "Modify"
+msgstr "編集"
+
+#: ../../../../modules/director/library/Director/ProvidedHook/CubeLinks.php:52
+#, php-format
+msgid "Modify %d hosts"
+msgstr "%d 個のホストを編集"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectsController.php:175
+#, php-format
+msgid "Modify %d objects"
+msgstr "%d 個のホストを編集"
+
+#: ../../../../modules/director/application/controllers/DatafieldController.php:35
+#, php-format
+msgid "Modify %s"
+msgstr "%s を編集"
+
+#: ../../../../modules/director/library/Director/ProvidedHook/CubeLinks.php:35
+msgid "Modify a host"
+msgstr "ホストを編集"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistEntryForm.php:61
+msgid "Modify data list entry"
+msgstr "データリストエントリの編集"
+
+#: ../../../../modules/director/library/Director/Web/Table/ApplyRulesTable.php:115
+msgid "Modify this Apply Rule"
+msgstr "この適用ルールを編集"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php:15
+msgid "Monitored Services"
+msgstr "監視対象サービス"
+
+#: ../../../../modules/director/library/Director/Web/Form/IplElement/ExtensibleSetElement.php:500
+msgid "Move down"
+msgstr "下へ移動"
+
+#: ../../../../modules/director/library/Director/Web/Form/IplElement/ExtensibleSetElement.php:490
+msgid "Move up"
+msgstr "上に移動"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectsController.php:173
+msgid "Multiple objects"
+msgstr "複数のオブジェクト"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:159
+msgid "My changes"
+msgstr "自身による変更"
+
+#: ../../../../modules/director/application/controllers/SchemaController.php:16
+msgid "MySQL schema"
+msgstr "MySQL スキーマ"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:145
+#: ../../../../modules/director/application/forms/IcingaApiUserForm.php:14
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:47
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:73
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:38
+#: ../../../../modules/director/application/forms/IcingaHostVarForm.php:22
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:556
+#: ../../../../modules/director/application/forms/IcingaServiceVarForm.php:22
+#: ../../../../modules/director/application/forms/IcingaTimePeriodForm.php:15
+#: ../../../../modules/director/library/Director/Web/Table/ChoicesTable.php:41
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiFieldsTable.php:72
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiObjectsTable.php:54
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiPrototypesTable.php:37
+msgid "Name"
+msgstr "名前"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:75
+msgid "Name for the Icinga dependency you are going to create"
+msgstr "Icinga依存関係オブジェクトの名前"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:20
+msgid "Name for the Icinga endpoint template you are going to create"
+msgstr "Icingaエンドポイントテンプレートの名前"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:26
+msgid "Name for the Icinga endpoint you are going to create"
+msgstr "Icingaエンドポイントの名前"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:21
+msgid "Name for the Icinga notification template you are going to create"
+msgstr "Icinga通知テンプレートの名前"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:27
+msgid "Name for the Icinga notification you are going to create"
+msgstr "Icinga通知の名前"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:148
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:559
+msgid "Name for the Icinga service you are going to create"
+msgstr "Icingaサービスの名前"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:30
+msgid "Name for the Icinga user object you are going to create"
+msgstr "作成しようとしているIcingaユーザオブジェクトの名前"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:24
+msgid "Name for the Icinga user template you are going to create"
+msgstr "作成しようとしているIcingaユーザテンプレートの名前"
+
+#: ../../../../modules/director/application/forms/IcingaZoneForm.php:17
+msgid "Name for the Icinga zone you are going to create"
+msgstr "作成しようとしているIcingaゾーンの名前"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:104
+msgid "Name needs to be changed when cloning a Template"
+msgstr "テンプレートを複製するときに名前を変更する必要があります"
+
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiFieldsTable.php:82
+msgid "Nav"
+msgstr "Nav"
+
+#: ../../../../modules/director/application/controllers/DatafieldController.php:41
+msgid "New Field"
+msgstr "新しいフィールド"
+
+#: ../../../../modules/director/application/controllers/JobController.php:30
+msgid "New Job"
+msgstr "新しいジョブ"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ImportsourceTabs.php:54
+msgid "New import source"
+msgstr "新しいインポートソース"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:22
+#: ../../../../modules/director/library/Director/Web/Form/CloneImportSourceForm.php:30
+#: ../../../../modules/director/library/Director/Web/Form/CloneSyncRuleForm.php:30
+msgid "New name"
+msgstr "新しい名前"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:258
+msgid "New object"
+msgstr "新しいオブジェクト"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:34
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:31
+msgid "Next"
+msgstr "次へ"
+
+#: ../../../../modules/director/application/forms/IcingaZoneForm.php:29
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:227
+#: ../../../../modules/director/application/forms/SettingsForm.php:58
+#: ../../../../modules/director/application/forms/SettingsForm.php:73
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:74
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:190
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:202
+#: ../../../../modules/director/library/Director/Job/ImportJob.php:102
+#: ../../../../modules/director/library/Director/Job/SyncJob.php:102
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:26
+msgid "No"
+msgstr "いいえ"
+
+#: ../../../../modules/director/library/Director/Util.php:183
+#, php-format
+msgid "No %s resource available"
+msgstr "利用可能な%s リソースはありません"
+
+#: ../../../../modules/director/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:99
+msgid "No API user configured, you might run the kickstart helper"
+msgstr "APIユーザーが設定されていません。キックスタートウィザードを"
+"実行する可能性があります。"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:163
+msgid "No Host Template has been provided yet"
+msgstr "ホストテンプレートがまだ提供されていません"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:151
+msgid "No Host template has been chosen"
+msgstr "ホストテンプレートが選択されていません"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:94
+msgid "No Service Templates have been provided yet"
+msgstr "サービステンプレートがまだ提供されていません"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:170
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:736
+#: ../../../../modules/director/application/forms/IcingaTimePeriodRangeForm.php:94
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:649
+msgid "No action taken, object has not been modified"
+msgstr "アクションは行われず、オブジェクトは変更されていません"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:223
+msgid "No apply rule has been defined yet"
+msgstr "適用ルールがまだ一つも定義されていません"
+
+#: ../../../../modules/director/library/Director/Web/Widget/SyncRunDetails.php:42
+msgid "No changes have been made"
+msgstr "何も変更されていません"
+
+#: ../../../../modules/director/application/controllers/DashboardController.php:78
+msgid "No dashboard available, you might have not enough permissions"
+msgstr "利用可能なダッシュボードがありません。十分な権限がない可能性があります"
+
+#: ../../../../modules/director/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:68
+msgid "No database has been configured for Icinga Director"
+msgstr "Icinga Director用にデータベースが構成されていません"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:238
+msgid ""
+"No database resource has been configured yet. Please choose a resource to "
+"complete your config"
+msgstr "データベースリソースはまだ設定されていません。 設定を完了する"
+"リソースを選択してください"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:52
+msgid "No database schema has been created yet"
+msgstr "データベーススキーマがまだ作成されていません"
+
+#: ../../../../modules/director/application/forms/AddToBasketForm.php:104
+msgid "No object has been chosen"
+msgstr "オブジェクトが選択されていません"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:241
+msgid "No object has been defined yet"
+msgstr "オブジェクトはまだ定義されていません"
+
+#: ../../../../modules/director/application/forms/IcingaMultiEditForm.php:82
+msgid "No object has been modified"
+msgstr "オブジェクトが変更されていません。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1171
+msgid "No related template has been provided yet"
+msgstr "関連するテンプレートが用意されていません。"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:82
+msgid "No service has been chosen"
+msgstr "サービスが選択されていません。"
+
+#: ../../../../modules/director/application/controllers/HostController.php:121
+#, php-format
+msgid "No such service: %s"
+msgstr "そのようなサービスはありません: %s"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1166
+msgid "No template has been chosen"
+msgstr "テンプレートが選択されていません。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:205
+msgid "No template has been defined yet"
+msgstr "テンプレートが定義されていません。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:606
+msgid "None"
+msgstr "なし"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php:57
+msgid "None could be used for deployments right now"
+msgstr "現在、反映できる設定はありません"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1457
+msgid "Notes"
+msgstr "メモ"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1466
+msgid "Notes URL"
+msgstr "関連URL"
+
+#: ../../../../modules/director/application/forms/SyncRunForm.php:39
+msgid "Nothing changed, rule is in sync"
+msgstr "変更はありません。ルールは同期されています。"
+
+#: ../../../../modules/director/application/forms/ImportCheckForm.php:38
+#: ../../../../modules/director/application/forms/ImportRunForm.php:38
+msgid ""
+"Nothing to do, data provided by this Import Source didn't change since the "
+"last import run"
+msgstr "何もしません。このインポートソースによって提供されたデータは"
+"最後のインポート実行以来、変更されていません"
+
+#: ../../../../modules/director/application/forms/RestoreObjectForm.php:76
+msgid "Nothing to do, restore would not modify the current object"
+msgstr "何もしません。復元しても現在のオブジェクトは変更されません"
+
+#: ../../../../modules/director/application/forms/SyncCheckForm.php:58
+msgid "Nothing would change, this rule is still in sync"
+msgstr "変更は起きません。このルールはまだ同期されています。"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:25
+#: ../../../../modules/director/library/Director/TranslationDummy.php:18
+msgid "Notification"
+msgstr "通知"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:55
+#, php-format
+msgid "Notification Apply Rules based on %s"
+msgstr "%s に基づく通知適用ルール"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php:19
+#: ../../../../modules/director/library/Director/Import/ImportSourceCoreApi.php:58
+msgid "Notification Commands"
+msgstr "通知コマンド"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php:12
+msgid ""
+"Notification Commands allow you to trigger any action you want when a "
+"notification takes place"
+msgstr "通知コマンドを使用すると、通知が発生したときに必要な操作を実行できます。"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:19
+msgid "Notification Template"
+msgstr "通知テンプレート"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:251
+msgid "Notification command"
+msgstr "通知コマンド"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:173
+msgid "Notification interval"
+msgstr "通知インターバル"
+
+#: ../../../../modules/director/application/controllers/TemplatechoicesController.php:29
+msgid "Notification template choices"
+msgstr "通知テンプレートチョイス"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php:13
+msgid "Notification templates"
+msgstr "通知テンプレート"
+
+#: ../../../../modules/director/configuration.php:130
+#: ../../../../modules/director/application/forms/BasketForm.php:25
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php:13
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/NotificationsDashlet.php:13
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarTable.php:46
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarVariantsTable.php:61
+msgid "Notifications"
+msgstr "通知"
+
+#: ../../../../modules/director/library/Director/Dashboard/NotificationsDashboard.php:20
+msgid ""
+"Notifications are sent when a host or service reaches a non-ok hard state or "
+"recovers from such. One might also want to send them for special events like "
+"when a Downtime starts, a problem gets acknowledged and much more. You can "
+"send specific notifications only within specific time periods, you can delay "
+"them and of course re-notify at specific intervals.\n"
+"\n"
+" Combine those possibilities in case you need to define escalation levels, "
+"like notifying operators first and your management later on in case the "
+"problem remains unhandled for a certain time.\n"
+"\n"
+" You might send E-Mail or SMS, make phone calls or page on various "
+"channels. You could also delegate notifications to external service "
+"providers. The possibilities are endless, as you are allowed to define as "
+"many custom notification commands as you want"
+msgstr "ホストまたはサービスが異常な状態になったときや、"
+"そこから復旧したときに通知が送信されます。 また、ダウンタイムの"
+"開始時、障害の発見時など、特別なイベントのために通知を送信することも"
+"できます。 特定の期間内にのみ特定の通知を送ることや、"
+"通知を遅らせること、特定の間隔で再通知することもできます。\n\n"
+"エスカレーションレベルを定義することもできます。"
+"たとえば、障害が一定時間解決されない場合に備えて、最初に"
+"オペレータに通知し、後で管理者に通知するという設定をすることもできます。\n\n"
+"なお通知は、メール以外の方法で行うこともできます。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:42
+msgid "Numeric position"
+msgstr "数字位置"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/StateFilterSet.php:24
+msgid "OK"
+msgstr "OK (正常)"
+
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:64
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:146
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1056
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1061
+msgid "Object"
+msgstr "オブジェクト"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:106
+msgid "Object Inspection"
+msgstr "オブジェクトの検査"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:42
+msgid "Object Type"
+msgstr "オブジェクトタイプ"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceLdap.php:53
+msgid "Object class"
+msgstr "オブジェクトクラス"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php:18
+msgid "Object dependency relationships."
+msgstr "依存関係オブジェクト"
+
+#: ../../../../modules/director/application/forms/RestoreObjectForm.php:80
+msgid "Object has been re-created"
+msgstr "オブジェクトが再作成されました"
+
+#: ../../../../modules/director/application/forms/RestoreObjectForm.php:72
+msgid "Object has been restored"
+msgstr "オブジェクトがリストアされました"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:357
+msgid "Object properties"
+msgstr "オブジェクトプロパティ"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1069
+#: ../../../../modules/director/library/Director/Web/Table/SyncruleTable.php:46
+msgid "Object type"
+msgstr "オブジェクトタイプ"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:67
+#, php-format
+msgid "Object type \"%s\""
+msgstr "オブジェクトタイプ \"%s\""
+
+#: ../../../../modules/director/library/Director/Web/Table/GeneratedConfigFileTable.php:85
+msgid "Object/Tpl/Apply"
+msgstr "オブジェクト/TPL/適用"
+
+#: ../../../../modules/director/library/Director/Web/Table/HostTemplateUsageTable.php:11
+#: ../../../../modules/director/library/Director/Web/Table/ServiceTemplateUsageTable.php:11
+#: ../../../../modules/director/library/Director/Web/Table/TemplateUsageTable.php:24
+msgid "Objects"
+msgstr "オブジェクト"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:27
+msgid "On failure"
+msgstr "失敗時"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:227
+msgid "One apply rule has been defined"
+msgstr "一つの適用ルールが定義されています。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:249
+msgid "One external object has been defined, it will not be deployed"
+msgstr "1つの外部オブジェクトが定義されています。設定の反映が行われません。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:252
+msgid "One object has been defined"
+msgstr "一つのオブジェクトが定義されています。"
+
+#: ../../../../modules/director/application/forms/IcingaMultiEditForm.php:84
+#: ../../../../modules/director/library/Director/Web/Widget/SyncRunDetails.php:45
+msgid "One object has been modified"
+msgstr "一つのオブジェクトが変更されています。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierSplit.php:16
+msgid "One or more characters that should be used to split this string"
+msgstr "この文字列を分割するために使用される1つ以上の文字"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierJoin.php:16
+msgid ""
+"One or more characters that will be used to glue an input array to a string. "
+"Can be left empty"
+msgstr "入力配列を文字列に結合するために使用される1つ以上の文字。 空のままにできます"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodRangeForm.php:30
+msgid "One or more time periods, e.g. 00:00-24:00 or 00:00-09:00,17:00-24:00"
+msgstr "一つまたは複数のスケジュール。(例)00:00-24:00 or 00:00-09:00,17:00-24:00"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:209
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:246
+msgid "One template has been defined"
+msgstr "1つのテンプレートが定義されています。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:95
+msgid ""
+"Only set this parameter if the argument value resolves to a numeric value. "
+"String values are not supported"
+msgstr "引数値が数値に解決される場合にのみ、このパラメータを設定してください。"
+"文字列値はサポートされていません"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:138
+msgid "Optional"
+msgstr "オプション"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:72
+msgid ""
+"Optional command timeout. Allowed values are seconds or durations postfixed "
+"with a specific unit (e.g. 1m or also 3m 30s)."
+msgstr "オプションのコマンドタイムアウト。書式は、"
+"秒数または期間のあとに特定の単位を付けたもの(たとえば、1分または3分30秒)。"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:249
+msgid ""
+"Optional. The child service. If omitted this dependency object is treated as "
+"host dependency."
+msgstr "オプションです。 子サービスです。省略した場合、"
+"この依存関係オブジェクトはホストの依存関係として扱われます。"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:213
+msgid ""
+"Optional. The parent service. If omitted this dependency object is treated "
+"as host dependency."
+msgstr "オプション。親サービス。省略した場合、この依存関係オブジェクトは"
+"ホストの依存関係として扱われます。"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:95
+msgid "Other available fields"
+msgstr "ほかの利用可能なフィールド"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:272
+msgid "Other sources"
+msgstr "ほかのソース"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:148
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:403
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:439
+msgid "Override vars"
+msgstr "変数のオーバーライド"
+
+#: ../../../../modules/director/application/controllers/DashboardController.php:46
+#: ../../../../modules/director/application/controllers/HealthController.php:16
+#: ../../../../modules/director/library/Director/Web/ActionBar/AutomationObjectActionBar.php:32
+msgid "Overview"
+msgstr "概要"
+
+#: ../../../../modules/director/application/controllers/PhperrorController.php:16
+#, php-format
+msgid ""
+"PHP version 5.4.x is required for Director >= 1.4.0, you're running %s. "
+"Please either upgrade PHP or downgrade Icinga Director"
+msgstr "PHPバージョン5.4.xはDirector> = 1.4.0に必要です。あなたは%sを"
+"実行しています。 PHPをアップグレードするか、"
+"Icinga Directorをダウングレードしてください。"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:41
+msgid "Pager"
+msgstr "ページャー"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:192
+msgid "Parent Host"
+msgstr "親ホスト"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:211
+msgid "Parent Service"
+msgstr "親サービス"
+
+#: ../../../../modules/director/application/forms/IcingaZoneForm.php:36
+msgid "Parent Zone"
+msgstr "親ゾーン"
+
+#: ../../../../modules/director/application/forms/IcingaApiUserForm.php:19
+#: ../../../../modules/director/application/forms/KickstartForm.php:159
+msgid "Password"
+msgstr "パスワード"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierCombine.php:14
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRegexSplit.php:13
+msgid "Pattern"
+msgstr "パターン"
+
+#: ../../../../modules/director/application/forms/ApplyMigrationsForm.php:39
+msgid "Pending database schema migrations have successfully been applied"
+msgstr "保留中のデータベーススキーマの移行は正常に適用されました"
+
+#: ../../../../modules/director/library/Director/Util.php:185
+msgid "Please ask an administrator to grant you access to resources"
+msgstr "リソースへのアクセスを許可するように管理者に依頼してください"
+
+#: ../../../../modules/director/application/forms/AddToBasketForm.php:118
+#, php-format
+msgid ""
+"Please check your Basket configuration, %s does not support single \"%s\" "
+"configuration objects"
+msgstr "バスケットの設定を確認してください。%sは単一の\"%s\"設定オブジェクト"
+"をサポートしていません"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMap.php:19
+msgid "Please choose a data list that can be used for map lookups"
+msgstr "地図検索に使用できるデータリストを選択してください"
+
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:66
+msgid "Please choose a specific Icinga object type"
+msgstr "特定のIcingaオブジェクトタイプを選択してください"
+
+#: ../../../../modules/director/library/Director/Job/ImportJob.php:82
+msgid ""
+"Please choose your import source that should be executed. You could create "
+"different schedules for different sources or also opt for running all of "
+"them at once."
+msgstr "実行する必要があるインポート元を選択してください。 ソースごとに"
+"異なるスケジュールを作成したり、それらすべてを一度に実行することもできます。"
+
+#: ../../../../modules/director/library/Director/Job/SyncJob.php:82
+msgid ""
+"Please choose your synchronization rule that should be executed. You could "
+"create different schedules for different rules or also opt for running all "
+"of them at once."
+msgstr "実行する同期ルールを選択してください。 異なるルールに対して異なる"
+"スケジュールを作成したり、それらすべてを一度に実行することもできます。"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceSql.php:57
+msgid "Please click \"Store\" once again to determine query columns"
+msgstr "クエリ欄を決定するには、もう一度「保存」をクリックしてください。"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:257
+#, php-format
+msgid "Please click %s to create new DB resources"
+msgstr "新しいDBリソースを作成するには、%sをクリックしてください"
+
+#: ../../../../modules/director/library/Director/Util.php:175
+#, php-format
+msgid "Please click %s to create new resources"
+msgstr "新しいリソースを作成するには、%sをクリックしてください"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:86
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:155
+#, php-format
+msgid "Please define a %s first"
+msgstr "%sを先に定義してください"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1169
+msgid "Please define a related template first"
+msgstr "関連するテンプレートを先に定義してください"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:219
+msgid ""
+"Please make sure that your database exists and your user has been granted "
+"enough permissions"
+msgstr "データベースが存在し、ユーザーに十分な権限が付与されていることを確認してください。"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:23
+msgid ""
+"Please only change those settings in case you are really sure that you are "
+"required to do so. Usually the defaults chosen by the Icinga Director should "
+"make a good fit for your environment."
+msgstr "本当にこれらの設定変更が必要だと確信している場合だけこれらの設定を変えてください。 通常、Icinga Directorによって選択されたデフォルトは、環境に適したものになるはずです。"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:28
+msgid "Please provide a rule name"
+msgstr "ルール名を入力してください"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierSubstring.php:17
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierSubstring.php:27
+#, php-format
+msgid "Please see %s for detailled instructions of how start and end work"
+msgstr "作業の開始と終了の詳細な手順については%sを参照してください。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:36
+msgid ""
+"Plugin Check commands are what you need when running checks agains your "
+"infrastructure. Notification commands will be used when it comes to notify "
+"your users. Event commands allow you to trigger specific actions when "
+"problems occur. Some people use them for auto-healing mechanisms, like "
+"restarting services or rebooting systems at specific thresholds"
+msgstr "プラグイン監視は、インフラストラクチャに対して再度"
+"監視を実行するときに必要なものです。通知コマンドは、ユーザーに通知を行う"
+"際に使われます。イベントコマンドは、障害が発生したときに特定のアクションを"
+"起こせます。 また、これらのコマンドを用いて、サービスを再起動したり、"
+"特定のしきい値でシステムを再起動するなど、自動修復メカニズムに利用することも"
+"できます。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:20
+msgid "Plugin commands"
+msgstr "プラグインコマンド"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:50
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:52
+msgid "Policy"
+msgstr "ポリシー"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:36
+#: ../../../../modules/director/application/forms/KickstartForm.php:141
+msgid "Port"
+msgstr "ポート"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:62
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:40
+msgid "Position"
+msgstr "位置"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:30
+msgid "Position Type"
+msgstr "位置タイプ"
+
+#: ../../../../modules/director/application/controllers/SchemaController.php:17
+msgid "PostgreSQL schema"
+msgstr "PostgreSQLスキーマ"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodForm.php:76
+msgid "Prefer includes"
+msgstr "インクルードの優先"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/BasketDashlet.php:17
+msgid "Preserve specific configuration objects in a specific state"
+msgstr "特定の構成オブジェクトを決まった状態に維持します"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ImportsourceTabs.php:49
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectTabs.php:86
+msgid "Preview"
+msgstr "プレビュー"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:23
+msgid "Problem"
+msgstr "Problem (障害)"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:27
+msgid "Problem handling"
+msgstr "障害対応"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1378
+msgid "Process performance data"
+msgstr "プロセスパフォーマンスデータ"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceLdap.php:67
+#: ../../../../modules/director/library/Director/Web/Tabs/SyncRuleTabs.php:35
+msgid "Properties"
+msgstr "プロパティ"
+
+#: ../../../../modules/director/application/forms/ImportRowModifierForm.php:30
+#: ../../../../modules/director/library/Director/Web/Table/PropertymodifierTable.php:113
+msgid "Property"
+msgstr "プロパティ"
+
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:168
+#, php-format
+msgid "Property modifiers: %s"
+msgstr "プロパティ変換ルール: %s"
+
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiFieldsTable.php:80
+msgid "Protected"
+msgstr "保護されています"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:91
+msgid "Prototypes (methods)"
+msgstr "プロトタイプ(方法)"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DatalistDashlet.php:11
+msgid "Provide Data Lists"
+msgstr "データリスト"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DatalistDashlet.php:17
+msgid "Provide data lists to make life easier for your users"
+msgstr "カスタム変数に利用する、データのリストを設定します。"
+"例えば、サービスタイプというデータリストに、「WEB」「DB」等の値を設定することができます。"
+"このリストを使うことで、変数の値を手で入力するのではなく、ユーザに選択させることができる"
+"ようになります。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php:18
+msgid "Provide templates for your TimePeriod objects."
+msgstr "スケジュールオブジェクト用のテンプレートを提供します。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/UserTemplateDashlet.php:18
+msgid "Provide templates for your User objects."
+msgstr "ユーザーオブジェクト用のテンプレートを提供します。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php:18
+msgid "Provide templates for your notifications."
+msgstr "通知用のテンプレートを提供します。"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:65
+msgid "Purge"
+msgstr "削除"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectTabs.php:122
+msgid "Ranges"
+msgstr "期間"
+
+#: ../../../../modules/director/application/forms/DeployConfigForm.php:34
+msgid "Re-deploy now"
+msgstr "再反映"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:24
+msgid "Recovery"
+msgstr "Recovery (復旧)"
+
+#: ../../../../modules/director/application/forms/IcingaGenerateApiKeyForm.php:22
+msgid "Regenerate Self Service API key"
+msgstr "セルフサービスAPIキーを再生成"
+
+#: ../../../../modules/director/application/forms/IcingaHostSelfServiceForm.php:56
+msgid "Register"
+msgstr "登録"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:59
+msgid "Registered Agent"
+msgstr "登録されたエージェント"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:34
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:32
+msgid "Regular Expression"
+msgstr "正規表現"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRegexSplit.php:16
+msgid "Regular expression pattern to split the string (e.g. /\\s+/ or /[,;]/)"
+msgstr "文字列を分割する正規表現パターン(例: /\\s+/ または /[,;]/)"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:58
+msgid "Reject matching elements"
+msgstr "マッチした要素を拒否"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:59
+msgid "Reject the whole row (Blacklist)"
+msgstr "行全体を拒否(ブラックリスト)"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:269
+msgid "Related Objects"
+msgstr "関連オブジェクト"
+
+#: ../../../../modules/director/library/Director/Web/Table/IcingaServiceSetServiceTable.php:203
+msgid "Remove"
+msgstr "削除"
+
+#: ../../../../modules/director/library/Director/Web/Table/IcingaServiceSetServiceTable.php:205
+#, php-format
+msgid "Remove \"%s\" from this host"
+msgstr "このホストから「%s」を削除"
+
+#: ../../../../modules/director/library/Director/Web/Form/IplElement/ExtensibleSetElement.php:480
+msgid "Remove this entry"
+msgstr "このエントリを削除"
+
+#: ../../../../modules/director/application/views/helpers/FormDataFilter.php:507
+msgid "Remove this part of your filter"
+msgstr "フィルタからこの部分を除去"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:94
+msgid "Rename related vars"
+msgstr "関連する変数の名前を変更"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:67
+msgid "Render config"
+msgstr "設定を生成"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:287
+msgid "Rendered file"
+msgstr "ファイルを生成"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:97
+#, php-format
+msgid "Rendered in %0.2fs, deployed in %0.2fs"
+msgstr "設定生成時間:%0.2f 秒, 設定反映時間: %0.2f 秒"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:408
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:413
+msgid "Rendering"
+msgstr "設定生成中"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:102
+msgid "Repeat key"
+msgstr "キーを反復"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:59
+msgid "Replace"
+msgstr "置換"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:119
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiFieldsTable.php:79
+msgid "Required"
+msgstr "要求"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:206
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:139
+#: ../../../../modules/director/application/forms/RestoreBasketForm.php:58
+msgid "Restore"
+msgstr "リストア"
+
+#: ../../../../modules/director/application/forms/RestoreObjectForm.php:17
+msgid "Restore former object"
+msgstr "以前のオブジェクトをリストア"
+
+#: ../../../../modules/director/application/forms/RestoreBasketForm.php:52
+msgid "Restore to this target Director DB"
+msgstr "対象のDirector DBをリストア"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1306
+msgid "Retry interval"
+msgstr "リトライ間隔"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1308
+msgid ""
+"Retry interval, will be applied after a state change unless the next hard "
+"state is reached"
+msgstr "最大監視試行回数に達するまでの試行間隔"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMap.php:34
+msgid "Return lookup key unmodified"
+msgstr "変更されていない参照キーを返す"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:27
+#: ../../../../modules/director/library/Director/Web/Table/SyncruleTable.php:45
+msgid "Rule name"
+msgstr "ルール名"
+
+#: ../../../../modules/director/library/Director/Job/ImportJob.php:119
+msgid "Run all imports at once"
+msgstr "すべてのインポートをまとめて実行"
+
+#: ../../../../modules/director/library/Director/Job/SyncJob.php:125
+msgid "Run all rules at once"
+msgstr "すべてのルールをまとめて実行"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:185
+#: ../../../../modules/director/library/Director/Job/ImportJob.php:92
+msgid "Run import"
+msgstr "インポートを実行"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:46
+msgid "Run interval"
+msgstr "実行インターバル"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:659
+msgid "Run on agent"
+msgstr "エージェント側で実行"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/JobDashlet.php:29
+msgid ""
+"Schedule and automate Import, Syncronization, Config Deployment, "
+"Housekeeping and more"
+msgstr "インポート、同期、設定反映、ハウスキーピングなどのスケジュールと自動化"
+
+#: ../../../../modules/director/library/Director/Dashboard/NotificationsDashboard.php:14
+#: ../../../../modules/director/library/Director/Dashboard/UsersDashboard.php:15
+msgid "Schedule your notifications"
+msgstr "通知をスケジュール"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/NotificationsDashlet.php:19
+msgid ""
+"Schedule your notifications. Define who should be notified, when, and for "
+"which kind of problem"
+msgstr "通知をスケジュールします。 誰にいつ、どんな種類の"
+"障害に対して通知するかを定義します。"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:145
+msgid ""
+"Script or tool to call when activating a new configuration stage. (e.g. "
+"sudo /usr/local/bin/icinga-director-activate) (name of the stage will be the "
+"argument for the script)"
+msgstr "新しい設定ステージをアクティブにするときに呼び出すスクリプトまたは"
+"ツール。 (例:sudo /usr/local/bin/icinga-director-activate)"
+"(ステージの名前がスクリプトの引数になります)"
+
+#: ../../../../modules/director/application/controllers/SelfServiceController.php:101
+#: ../../../../modules/director/application/controllers/SettingsController.php:43
+msgid "Self Service"
+msgstr "セルフサービス"
+
+#: ../../../../modules/director/application/controllers/SelfServiceController.php:102
+msgid "Self Service - Host Registration"
+msgstr "セルフサービス - ホストの登録"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/SelfServiceDashlet.php:11
+msgid "Self Service API"
+msgstr "セルフサービス API"
+
+#: ../../../../modules/director/application/controllers/SettingsController.php:44
+msgid "Self Service API - Global Settings"
+msgstr "セルフサービス API - グローバル設定"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:285
+msgid "Self Service Settings have been stored"
+msgstr "セルフサービス設定は保存されました。"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:89
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1366
+msgid "Send notifications"
+msgstr "通知を送信する"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:78
+msgid "Sent to"
+msgstr "へ送る"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:104
+#: ../../../../modules/director/application/forms/IcingaServiceVarForm.php:15
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:14
+#: ../../../../modules/director/library/Director/TranslationDummy.php:14
+msgid "Service"
+msgstr "サービス"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php:11
+msgid "Service Apply Rules"
+msgstr "サービス適用ルール"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:15
+msgid "Service Group"
+msgstr "サービスグループ"
+
+msgid "ServiceGroup"
+msgstr "サービスグループ"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:21
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php:11
+msgid "Service Groups"
+msgstr "サービスグループ"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:16
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:58
+msgid "Service Set"
+msgstr "サービスセット"
+
+#: ../../../../modules/director/application/forms/RemoveLinkForm.php:55
+msgid "Service Set has been removed"
+msgstr "サービスセットが削除されました"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:24
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php:11
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarTable.php:45
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarVariantsTable.php:60
+msgid "Service Sets"
+msgstr "サービスセット"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:88
+msgid "Service Template"
+msgstr "サービステンプレート"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:22
+msgid "Service Template Choice"
+msgstr "サービステンプレートチョイス"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:23
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php:11
+msgid "Service Templates"
+msgstr "サービステンプレート"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:166
+msgid "Service User"
+msgstr "サービスユーザ"
+
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:57
+msgid "Service groups"
+msgstr "サービスグループ"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:634
+msgid ""
+"Service groups that should be directly assigned to this service. "
+"Servicegroups can be useful for various reasons. They are helpful to "
+"provided service-type specific view in Icinga Web 2, either for custom "
+"dashboards or as an instrument to enforce restrictions. Service groups can "
+"be directly assigned to single services or to service templates."
+msgstr "このサービスに直接割り当てる必要があるサービスグループ。 "
+"サービスグループはさまざまな理由で役に立ちます。 カスタムダッシュボード"
+"または制限を適用する手段として、Icinga Web 2でサービスタイプ固有の"
+"ビューを提供するのに役立ちます。 サービスグループは、単一のサービスまたは"
+"サービステンプレートに直接割り当てることができます。"
+
+#: ../../../../modules/director/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php:102
+msgid "Service name"
+msgstr "サービス名"
+
+#: ../../../../modules/director/application/controllers/SuggestController.php:238
+#: ../../../../modules/director/library/Director/Objects/IcingaService.php:709
+msgid "Service properties"
+msgstr "サービスプロパティ"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceSetForm.php:86
+#: ../../../../modules/director/application/forms/IcingaServiceSetForm.php:85
+msgid "Service set"
+msgstr "サービスセット"
+
+#: ../../../../modules/director/application/forms/IcingaServiceSetForm.php:28
+msgid "Service set name"
+msgstr "サービスセット名"
+
+#: ../../../../modules/director/application/controllers/TemplatechoiceController.php:22
+msgid "Service template choice"
+msgstr "サービステンプレートチョイス"
+
+#: ../../../../modules/director/application/controllers/TemplatechoicesController.php:24
+msgid "Service template choices"
+msgstr "サービステンプレートチョイス"
+
+#: ../../../../modules/director/application/controllers/ServiceController.php:76
+msgid "ServiceSet"
+msgstr "サービスセット"
+
+#: ../../../../modules/director/application/forms/IcingaServiceGroupForm.php:14
+msgid "Servicegroup"
+msgstr "サービスグループ"
+
+#: ../../../../modules/director/library/Director/Web/Table/IcingaAppliedServiceTable.php:32
+#: ../../../../modules/director/library/Director/Web/Table/IcingaHostServiceTable.php:140
+#: ../../../../modules/director/library/Director/Web/Table/IcingaServiceSetServiceTable.php:170
+msgid "Servicename"
+msgstr "サービス名"
+
+#: ../../../../modules/director/configuration.php:122
+#: ../../../../modules/director/application/controllers/ServiceController.php:57
+#: ../../../../modules/director/application/controllers/ServiceController.php:80
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:100
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:90
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:56
+#: ../../../../modules/director/library/Director/IcingaConfig/StateFilterSet.php:23
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarTable.php:44
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarVariantsTable.php:59
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectTabs.php:75
+msgid "Services"
+msgstr "サービス"
+
+#: ../../../../modules/director/application/controllers/ServicesetController.php:62
+#, php-format
+msgid "Services in this set: %s"
+msgstr "このセット中のサービス: %s"
+
+#: ../../../../modules/director/application/controllers/HostController.php:213
+#, php-format
+msgid "Services on %s"
+msgstr "%sのサービス"
+
+#: ../../../../modules/director/application/controllers/HostController.php:137
+#, php-format
+msgid "Services: %s"
+msgstr "%sのサービス"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:84
+msgid "Set based on filter"
+msgstr "フィルタに基づいて設定"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:44
+msgid "Set false"
+msgstr "FALSEを設定する"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierDnsRecords.php:34
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:30
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierGetHostByName.php:17
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierJsonDecode.php:25
+msgid "Set no value (null)"
+msgstr "値が設定されていません(null)"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:42
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMap.php:33
+msgid "Set null"
+msgstr "NULLを設定"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:43
+msgid "Set true"
+msgstr "TRUEを設定"
+
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectsTabs.php:76
+msgid "Sets"
+msgstr "セット"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:105
+msgid ""
+"Setting a command endpoint allows you to force host checks to be executed by "
+"a specific endpoint. Please carefully study the related Icinga documentation "
+"before using this feature"
+msgstr "コマンドエンドポイントを設定すると、特定のエンドポイントで"
+"ホスト監視を強制的に実行できます。 この機能を使用する前に、関連する"
+"Icingaのドキュメントをよく調べてください。"
+
+#: ../../../../modules/director/application/controllers/ConfigController.php:218
+#: ../../../../modules/director/library/Director/Web/SelfService.php:93
+msgid "Settings"
+msgstr "設定"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:198
+msgid "Settings have been stored"
+msgstr "設定は保存されました"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:86
+msgid "Share this Template for Self Service API"
+msgstr "セルフサービスAPIにこのテンプレートを共有"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:84
+msgid "Shared for Self Service API"
+msgstr "セルフサービスAPIのために共有されています"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:21
+msgid "Should all the other characters be lowercased first?"
+msgstr "他のすべての文字を先頭小文字にしますか?"
+
+#: ../../../../modules/director/application/controllers/HostController.php:513
+msgid "Show"
+msgstr "表示"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:200
+msgid "Show Basket"
+msgstr "バスケットを表示"
+
+#: ../../../../modules/director/library/Director/Web/Widget/AdditionalTableActions.php:75
+msgid "Show SQL"
+msgstr "SQLを表示"
+
+#: ../../../../modules/director/library/Director/Web/Table/ApplyRulesTable.php:108
+msgid "Show affected Objects"
+msgstr "影響を受けるオブジェクトを表示"
+
+#: ../../../../modules/director/library/Director/Web/Form/IplElement/ExtensibleSetElement.php:457
+msgid "Show available options"
+msgstr "利用可能なオプションを表示"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:175
+msgid "Show based on filter"
+msgstr "フィルタに基づいて表示"
+
+#: ../../../../modules/director/library/Director/Web/Table/ActivityLogTable.php:91
+msgid "Show details related to this change"
+msgstr "この変更に関連する詳細を表示"
+
+#: ../../../../modules/director/library/Director/Web/ObjectPreview.php:51
+msgid "Show normal"
+msgstr "通常表示"
+
+#: ../../../../modules/director/library/Director/Web/ObjectPreview.php:60
+msgid "Show resolved"
+msgstr "インポート内容を展開して表示"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:33
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:31
+msgid "Simple match with wildcards (*)"
+msgstr "ワイルドカードとの単純一致(*)"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:332
+msgid "Single Object Diff"
+msgstr "単一オブジェクト差分"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/SingleServicesDashlet.php:11
+msgid "Single Services"
+msgstr "単一のサービス"
+
+#: ../../../../modules/director/library/Director/Web/Table/GeneratedConfigFileTable.php:86
+msgid "Size"
+msgstr "サイズ"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:110
+msgid "Skip key"
+msgstr "キーをスキップ"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:247
+#: ../../../../modules/director/application/controllers/BasketController.php:364
+msgid "Snapshot"
+msgstr "スナップショット"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:36
+#: ../../../../modules/director/application/controllers/BasketController.php:141
+#: ../../../../modules/director/library/Director/Web/Table/BasketTable.php:32
+msgid "Snapshots"
+msgstr "スナップショット"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:182
+msgid "Source Column"
+msgstr "ソースカラム"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:212
+msgid "Source Expression"
+msgstr "ソース表現"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:38
+msgid "Source Name"
+msgstr "ソース名"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:101
+msgid "Source Path"
+msgstr "ソースパス"
+
+#: ../../../../modules/director/application/forms/ImportSourceForm.php:33
+msgid "Source Type"
+msgstr "ソースタイプ"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:159
+msgid "Source columns"
+msgstr "ソースカラム"
+
+#: ../../../../modules/director/library/Director/Web/Table/SyncpropertyTable.php:62
+msgid "Source field"
+msgstr "ソースフィールド"
+
+#: ../../../../modules/director/library/Director/Web/Table/ImportrunTable.php:30
+#: ../../../../modules/director/library/Director/Web/Table/ImportsourceTable.php:18
+#: ../../../../modules/director/library/Director/Web/Table/SyncpropertyTable.php:61
+msgid "Source name"
+msgstr "ソース名"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:354
+msgid "Special properties"
+msgstr "特殊プロパティ"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:35
+msgid "Specific Element (by position)"
+msgstr "特定の要素(位置指定)"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:87
+msgid "Stage name"
+msgstr "ステージ名"
+
+#: ../../../../modules/director/library/Director/Web/Widget/SyncRunDetails.php:25
+msgid "Start time"
+msgstr "開始時間"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:88
+msgid "Startup"
+msgstr "スタートアップ"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:162
+msgid "Startup Log"
+msgstr "スタートアップログ"
+
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiFieldsTable.php:77
+msgid "State"
+msgstr "状態"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1614
+msgid "State and transition type filters"
+msgstr "状態フィルターと遷移タイプフィルター"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/TypeFilterSet.php:22
+msgid "State changes"
+msgstr "状態変化"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1589
+msgid "States"
+msgstr "状態"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeployedConfigInfoHeader.php:81
+msgid "Statistics"
+msgstr "統計"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:43
+#: ../../../../modules/director/application/controllers/InspectController.php:151
+msgid "Status"
+msgstr "ステータス"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:134
+msgid "Stop sharing this Template"
+msgstr "このテンプレートの共有をやめる"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:107
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:496
+msgid "Store"
+msgstr "保存"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:36
+msgid "Store configuration"
+msgstr "設定を保存"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:33
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:72
+#: ../../../../modules/director/library/Director/DataType/DataTypeDatalist.php:65
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:76
+#: ../../../../modules/director/library/Director/DataType/DataTypeSqlQuery.php:76
+msgid "String"
+msgstr "文字"
+
+#: ../../../../modules/director/application/views/helpers/FormDataFilter.php:534
+msgid "Strip this operator, preserve child nodes"
+msgstr "この演算子を取り除き、子ノードを保存します"
+
+#: ../../../../modules/director/library/Director/Web/Form/QuickForm.php:206
+msgid "Submit"
+msgstr "送信"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:139
+msgid "Succeeded"
+msgstr "成功しました"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:86
+msgid "Suggested fields"
+msgstr "サジェストされたフィールド"
+
+#: ../../../../modules/director/library/Director/Web/ActionBar/TemplateActionBar.php:37
+msgid "Switch to Table view"
+msgstr "テーブル表示に切り替え"
+
+#: ../../../../modules/director/library/Director/Web/ActionBar/TemplateActionBar.php:36
+msgid "Switch to Tree view"
+msgstr "ツリー表示に切り替え"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:243
+#, php-format
+msgid "Sync \"%s\": %s"
+msgstr "同期 \"%s\": %s"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:112
+msgid "Sync Properties"
+msgstr "同期プロパティ"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:30
+msgid "Sync Rules"
+msgstr "同期ルール"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:278
+msgid "Sync history"
+msgstr "同期履歴"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:82
+#, php-format
+msgid ""
+"Sync only part of your imported objects with this rule. Icinga Web 2 filter "
+"syntax is allowed, so this could look as follows: %s"
+msgstr "インポートしたオブジェクトの一部だけをこのルールと同期します。 "
+"IcingaWeb2フィルタの構文が利用できます。例: %s"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:215
+msgid "Sync properties"
+msgstr "同期プロパティ"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:180
+#: ../../../../modules/director/application/controllers/SyncrulesController.php:25
+#: ../../../../modules/director/library/Director/Web/Tabs/ImportTabs.php:23
+#: ../../../../modules/director/library/Director/Web/Tabs/SyncRuleTabs.php:29
+#: ../../../../modules/director/library/Director/Web/Tabs/SyncRuleTabs.php:46
+msgid "Sync rule"
+msgstr "同期ルール"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:32
+#: ../../../../modules/director/application/controllers/SyncruleController.php:152
+#, php-format
+msgid "Sync rule: %s"
+msgstr "同期ルール: %s"
+
+#: ../../../../modules/director/application/forms/SyncRunForm.php:42
+msgid "Synchronization failed"
+msgstr "同期が失敗しました。"
+
+#: ../../../../modules/director/library/Director/Job/SyncJob.php:80
+msgid "Synchronization rule"
+msgstr "同期ルール"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/SyncDashlet.php:14
+msgid "Synchronize"
+msgstr "同期"
+
+#: ../../../../modules/director/library/Director/Web/ActionBar/TemplateActionBar.php:30
+msgid "Table"
+msgstr "表"
+
+#: ../../../../modules/director/application/forms/RestoreBasketForm.php:51
+msgid "Target DB"
+msgstr "対象DB"
+
+#: ../../../../modules/director/application/forms/IcingaCloneObjectForm.php:60
+msgid "Target Service Set"
+msgstr "対象サービスセット"
+
+#: ../../../../modules/director/library/Director/DataType/DataTypeDatalist.php:63
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:74
+#: ../../../../modules/director/library/Director/DataType/DataTypeSqlQuery.php:74
+msgid "Target data type"
+msgstr "対象データタイプ"
+
+#: ../../../../modules/director/application/forms/ImportRowModifierForm.php:44
+msgid "Target property"
+msgstr "対象プロパティ"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:156
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1053
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1057
+msgid "Template"
+msgstr "テンプレート"
+
+#: ../../../../modules/director/library/Director/Web/Table/TemplatesTable.php:51
+msgid "Template Name"
+msgstr "テンプレート名"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:276
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:113
+#, php-format
+msgid "Template: %s"
+msgstr "テンプレート: %s"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:687
+#: ../../../../modules/director/library/Director/Web/Table/DependencyTemplateUsageTable.php:10
+#: ../../../../modules/director/library/Director/Web/Table/HostTemplateUsageTable.php:10
+#: ../../../../modules/director/library/Director/Web/Table/NotificationTemplateUsageTable.php:10
+#: ../../../../modules/director/library/Director/Web/Table/ServiceTemplateUsageTable.php:10
+#: ../../../../modules/director/library/Director/Web/Table/TemplateUsageTable.php:23
+#: ../../../../modules/director/library/Director/Web/Tabs/ObjectsTabs.php:53
+#: ../../../../modules/director/library/Director/Web/Tree/TemplateTreeRenderer.php:43
+msgid "Templates"
+msgstr "テンプレート"
+
+#: ../../../../modules/director/library/Director/Job/ImportJob.php:67
+msgid "The \"Import\" job allows to run import actions at regular intervals"
+msgstr "「インポート」ジョブは定期的にインポートアクションを実行することを"
+"可能にします"
+
+#: ../../../../modules/director/library/Director/Job/SyncJob.php:65
+msgid "The \"Sync\" job allows to run sync actions at regular intervals"
+msgstr "「同期」ジョブを使用すると、定期的に同期アクションを実行できます。"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodRangeForm.php:84
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:640
+#, php-format
+msgid "The %s has successfully been stored"
+msgstr "%s は正常に保存されました"
+
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:221
+msgid ""
+"The Config job allows you to generate and eventually deploy your Icinga 2 "
+"configuration"
+msgstr "構成ジョブを使用すると、Icinga 2構成を生成して設定の反映を"
+"することができます。"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:37
+msgid "The Email address of the user."
+msgstr "ユーザーのEメールアドレス。"
+
+#: ../../../../modules/director/library/Director/Job/HousekeepingJob.php:21
+msgid ""
+"The Housekeeping job provides various task that keep your Director database "
+"fast and clean"
+msgstr "ハウスキーピングジョブは、Directorデータベースを高速かつクリーンに"
+"保つためのさまざまなタスクを提供します。"
+
+#: ../../../../modules/director/application/controllers/SettingsController.php:38
+msgid ""
+"The Icinga Director Self Service API allows your Hosts to register "
+"themselves. This allows them to get their Icinga Agent configured, installed "
+"and upgraded in an automated way."
+msgstr "Icinga Director セルフサービス APIを使用すると、ホストは自分自身を"
+"登録できます。 これにより、Icinga Agentを自動化された方法で設定、"
+"インストール、アップグレードすることができます。"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:45
+msgid ""
+"The Icinga Package name Director uses to deploy it's configuration. This "
+"defaults to \"director\" and should not be changed unless you really know "
+"what you're doing"
+msgstr "Directorがその設定を反映するために使用するIcingaパッケージ名。"
+" これはデフォルトで \"director\"に設定されているので、"
+"分からなければ変更しないでください。"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceLdap.php:69
+msgid ""
+"The LDAP properties that should be fetched. This is required to be a comma-"
+"separated list like: \"cn, dnshostname, operatingsystem, sAMAccountName\""
+msgstr "取得する必要があるLDAPプロパティ。これは、\"cn, dnshostname, "
+"operatingsystem, sAMAccountName\"のようにコンマ区切りのリストにする"
+"必要があります。"
+
+#: ../../../../modules/director/application/forms/IcingaForgetApiKeyForm.php:31
+#, php-format
+msgid "The Self Service API key for %s has been dropped"
+msgstr "%sのセルフサービスAPIキーを削除しました。"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceSetForm.php:116
+#, php-format
+msgid "The Service Set \"%s\" has been added to %d hosts"
+msgstr "サービスセット\"%s\"を%d 個のホストに追加しました。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:162
+#, php-format
+msgid "The argument %s has successfully been stored"
+msgstr "引数%sは正常に保存されました"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:121
+msgid "The caption which should be displayed"
+msgstr "表示されるキャプション"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:151
+msgid ""
+"The caption which should be displayed to your users when this field is shown"
+msgstr "このフィールドが表示されたときにユーザーに表示されるキャプション"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:233
+msgid "The child host."
+msgstr "子ホスト"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:62
+msgid ""
+"The command Icinga should run. Absolute paths are accepted as provided, "
+"relative paths are prefixed with \"PluginDir + \", similar Constant prefixes "
+"are allowed. Spaces will lead to separation of command path and standalone "
+"arguments. Please note that this means that we do not support spaces in "
+"plugin names and paths right now."
+msgstr "Icingaが実行するコマンドです。 絶対パスは指定どおりに受け入れられ、"
+"相対パスの先頭には \"PluginDir +\"が付きます。同様の定数プレフィックスが"
+"許可されます。 スペースは、コマンドパスとスタンドアロン引数の分離に"
+"つながります。 これは、現時点ではプラグイン名とパスでスペースをサポート"
+"していないことを意味します。"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:161
+msgid "The corresponding password"
+msgstr "対応するパスワード"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierStripDomain.php:14
+msgid "The domain name you want to be stripped"
+msgstr "削除したいドメイン名"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:18
+msgid "The first (leftmost) CN"
+msgstr "最初(一番左)のCN"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:19
+msgid "The first (leftmost) OU"
+msgstr "最初(一番左)のOU"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:726
+#, php-format
+msgid "The given properties have been stored for \"%s\""
+msgstr "プロパティが\"%s\" に保存されました。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1592
+msgid "The host/service states you want to get notifications for"
+msgstr "通知を受け取るホスト/サービスの状態"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:21
+msgid "The last (rightmost) OU"
+msgstr "最後(一番右)のOU"
+
+#: ../../../../modules/director/library/Director/Web/Widget/JobDetails.php:51
+#, php-format
+msgid "The last attempt failed at %s: %s"
+msgstr "最後の試行は%sで失敗しました:%s"
+
+#: ../../../../modules/director/library/Director/Web/Widget/JobDetails.php:46
+#, php-format
+msgid "The last attempt succeeded at %s"
+msgstr "最後の試行は%sで成功しました"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DeploymentDashlet.php:83
+msgid "The last deployment did not succeed"
+msgstr "最後の設定反映は成功しませんでした"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DeploymentDashlet.php:85
+msgid "The last deployment is currently pending"
+msgstr "最後の設定反映は保留になっています。"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:42
+msgid "The log duration time."
+msgstr "接続が失われた場合の、監視ログの保存秒数"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:160
+msgid ""
+"The name of a time period which determines when notifications to this User "
+"should be triggered. Not set by default."
+msgstr "このユーザーへの通知をいつトリガーするかを決定する期間の名前。"
+"デフォルトでは設定されていません。"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:140
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:231
+msgid ""
+"The name of a time period which determines when this notification should be "
+"triggered. Not set by default."
+msgstr "この通知をいつトリガーするかを決定する期間の名前。"
+" デフォルトでは設定されていません。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1344
+msgid ""
+"The name of a time period which determines when this object should be "
+"monitored. Not limited by default."
+msgstr "監視を実行する期間の設定。デフォルトでは期間は無制限です。"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:62
+msgid ""
+"The name of a time period within this job should be active. Supports only "
+"simple time periods (weekday and multiple time definitions)"
+msgstr "有効なスケジュール設定を指定してください。"
+"曜日と複数の実行時間定義のみをサポートしています。"
+
+#: ../../../../modules/director/application/forms/IcingaHostVarForm.php:16
+msgid "The name of the host"
+msgstr "ホストの名称"
+
+#: ../../../../modules/director/application/forms/IcingaServiceVarForm.php:16
+msgid "The name of the service"
+msgstr "サービスの名称"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:175
+msgid ""
+"The notification interval (in seconds). This interval is used for active "
+"notifications. Defaults to 30 minutes. If set to 0, re-notifications are "
+"disabled."
+msgstr "通知間隔(秒)。この間隔はアクティブ通知に使用されます。 デフォルトは30分です。 0に設定すると、再通知は無効になります。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierBitmask.php:16
+msgid ""
+"The numeric bitmask you want to apply. In case you have a hexadecimal or "
+"binary mask please transform it to a decimal number first. The result of "
+"this modifier is a boolean value, telling whether the given mask applies to "
+"the numeric value in your source column"
+msgstr "適用したい数値ビットマスク。 16進数または2進数のマスクがある場合は、"
+"まずそれを10進数に変換してください。 このプロパティ変換ルールの結果はブール値で、"
+"与えられたマスクがソースカラムの数値に適用されるかどうかを示します。"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:42
+msgid "The pager address of the user."
+msgstr "ユーザの小型無線機のアドレス"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:194
+msgid "The parent host."
+msgstr "親ホスト"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRegexReplace.php:15
+msgid ""
+"The pattern you want to search for. This can be a regular expression like /"
+"^www\\d+\\./"
+msgstr "検索したいパターン。これは/^www\\d+\\./のような正規表現になります。"
+
+#: ../../../../modules/director/application/forms/IcingaEndpointForm.php:37
+msgid "The port of the endpoint."
+msgstr "エンドポイントのポート"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:144
+msgid ""
+"The port you are going to use. The default port 5665 will be used if none is "
+"set"
+msgstr "使用しようとしているポート。 何も設定されていない場合は、"
+"デフォルトのポート5665が使用されます。"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceForm.php:173
+#, php-format
+msgid "The service \"%s\" has been added to %d hosts"
+msgstr "サービス \"%s\" が %d 個のホストに追加されました"
+
+#: ../../../../modules/director/application/forms/IcingaAddServiceSetForm.php:88
+msgid "The service Set that should be assigned"
+msgstr "割り当てるべきサービスセット"
+
+#: ../../../../modules/director/application/forms/IcingaServiceSetForm.php:87
+msgid "The service set that should be assigned to this host"
+msgstr "このホストに割り当てるべきサービスセット"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1602
+msgid "The state transition types you want to get notifications for"
+msgstr "通知を受け取る状態遷移の種類"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRegexReplace.php:23
+msgid "The string that should be used as a preplacement"
+msgstr "置き換えに使用される文字列"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierReplace.php:14
+msgid "The string you want to search for"
+msgstr "検索したい文字列"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:41
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:43
+msgid ""
+"The string/pattern you want to search for. Depends on the chosen method, use "
+"www.* or *linux* for wildcard matches and expression like /^www\\d+\\./ in "
+"case you opted for a regular expression"
+msgstr "検索したい文字列/パターン。 選択した方法によって異なりますが、"
+"ワイルドカードの一致には www.* または *linux* を使用し、正規表現を"
+"選択した場合は/^www\\d+\\./のような表現を使用してください。"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:141
+msgid ""
+"The unique name of the field. This will be the name of the custom variable "
+"in the rendered Icinga configuration."
+msgstr "フィールドの名前。一意である必要があります。"
+"この名前はホストやサービス等のicingaの設定で定義するカスタム変数の"
+"名前になります。"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:168
+msgid "The user that should run the Icinga 2 service on Windows."
+msgstr "Windows上でIcinga 2サービスを実行する必要があるユーザー。"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:72
+#, php-format
+msgid ""
+"There are %d objects with a related property. Should I also remove the \"%s"
+"\" property from them?"
+msgstr "%d個のオブジェクトが関連プロパティを持っています。"
+"それらのオブジェクトからも\"%s\"プロパティを削除しますか? "
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:116
+#, php-format
+msgid ""
+"There are %d objects with a related property. Should I also rename the \"%s"
+"\" property to \"%s\" on them?"
+msgstr "%d個のオブジェクトが関連プロパティを持っています。 "
+"それらのオブジェクトの\"%s\"プロパティの名前も\"%s \"に変更しますか?"
+
+#: ../../../../modules/director/application/forms/DeploymentLinkForm.php:64
+#, php-format
+msgid "There are %d pending changes"
+msgstr "%d 個の保留された変更があります。"
+
+#: ../../../../modules/director/application/forms/DeploymentLinkForm.php:77
+#, php-format
+msgid "There are %d pending changes, %d of them applied to this object"
+msgstr "%d 個の保留された変更があります。それらのうち、%d 個が"
+"このオブジェクトに適用されます。"
+
+#: ../../../../modules/director/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php:88
+#, php-format
+msgid "There are %d pending database migrations"
+msgstr "%d 個の保留されたデータベース移行があります。"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:109
+msgid ""
+"There are no data fields available. Please ask an administrator to create "
+"such"
+msgstr "利用可能なデータフィールドがありません。"
+"作成するように管理者に依頼してください。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/DeploymentDashlet.php:91
+msgid "There are no pending changes"
+msgstr "保留中の変更はありません"
+
+#: ../../../../modules/director/application/forms/DeployConfigForm.php:36
+msgid "There are no pending changes. Deploy anyways"
+msgstr "設定を反映する(保留中の変更はありません。)"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ImportSourceDetails.php:58
+msgid ""
+"There are pending changes for this Import Source. You should trigger a new "
+"Import Run."
+msgstr "このインポートソースには保留中の変更があります。"
+"新しいインポート処理をトリガする必要があります。"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:74
+msgid ""
+"There are pending changes for this Sync Rule. You should trigger a new Sync "
+"Run."
+msgstr "この同期ルールには保留中の変更があります。 新しい同期処理をトリガする必要があります。"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:62
+msgid "There are pending database migrations"
+msgstr "保留中のデータベース移行があります。"
+
+#: ../../../../modules/director/application/forms/DeploymentLinkForm.php:69
+msgid ""
+"There has been a single change to this object, nothing else has been modified"
+msgstr "このオブジェクトには1つの変更があり、他の変更はありません。"
+
+#: ../../../../modules/director/application/forms/DeploymentLinkForm.php:72
+#, php-format
+msgid ""
+"There have been %d changes to this object, nothing else has been modified"
+msgstr "このオブジェクトには%d個の変更があり、他の変更はありません。"
+
+#: ../../../../modules/director/application/forms/DeploymentLinkForm.php:61
+msgid "There is a single pending change"
+msgstr "保留中の変更が1つあります。"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:21
+msgid "These are different available job types"
+msgstr "様々な利用可能なジョブタイプがあります。"
+
+#: ../../../../modules/director/application/forms/ImportSourceForm.php:37
+msgid ""
+"These are different data providers fetching data from various sources. You "
+"didn't find what you're looking for? Import sources are implemented as a "
+"hook in Director, so you might find (or write your own) Icinga Web 2 module "
+"fetching data from wherever you want"
+msgstr "さまざまなソースからデータを取得するさまざまなデータプロバイダ。"
+"なお、インポートソースはDirectorのフックとして"
+"実装されているので、Icinga Web 2モジュールが必要な場所からデータを取得"
+"している(または自分で作成した)可能性があります。"
+
+#: ../../../../modules/director/application/controllers/CommandController.php:71
+#, php-format
+msgid "This Command is currently being used by %s"
+msgstr "このコマンドは現在%s によって使用されています"
+
+#: ../../../../modules/director/application/controllers/CommandController.php:78
+msgid "This Command is currently not in use"
+msgstr "このコマンドは現在使用されていません"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:895
+#, php-format
+msgid "This Command is still in use by %d other objects"
+msgstr "このコマンドはまだほかの %d 個のオブジェクトに使用されています"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ImportSourceDetails.php:65
+#, php-format
+msgid "This Import Source failed when last checked at %s: %s"
+msgstr "このインポートソースは %s に行われた監視で失敗しました: %s "
+
+#: ../../../../modules/director/library/Director/Web/Widget/ImportSourceDetails.php:73
+#, php-format
+msgid "This Import Source has an invalid state: %s"
+msgstr "このインポートソースは無効な状態です: %s"
+
+#: ../../../../modules/director/application/forms/ImportCheckForm.php:33
+msgid "This Import Source provides modified data"
+msgstr "このインポートソースは変更されたデータを提供します"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ImportSourceDetails.php:48
+#, php-format
+msgid "This Import Source was last found to be in sync at %s."
+msgstr "このインポートソースは最後に%sに同期していることがわかりました"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:134
+msgid "This Service has been blacklisted on this host"
+msgstr "このサービスはこのホストでブラックリストに登録されています"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:81
+#, php-format
+msgid "This Sync Rule failed when last checked at %s: %s"
+msgstr "この同期ルールは、最後に %s に監視されたときに失敗しました: %s"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:51
+msgid "This Sync Rule has never been run before."
+msgstr "この同期ルールは一度も実行されたことがありません。"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:63
+#, php-format
+msgid "This Sync Rule was last found to by in Sync at %s."
+msgstr "この同期ルールは、%sの同期で最後に見つかったものです。"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:104
+msgid ""
+"This allows to filter for specific parts within the given source expression. "
+"You are allowed to refer all imported columns. Examples: host=www* would set "
+"this property only for rows imported with a host property starting with \"www"
+"\". Complex example: host=www*&!(address=127.*|address6=::1)"
+msgstr "指定された元の表現内の特定の部分をフィルタリングできます。 "
+"インポートされたすべてのカラムを参照することが許可されています。 "
+"例)「host=www*」 は、\"www\"で始まるホストプロパティでインポートされた行に"
+"対してのみこのプロパティを設定します。 "
+"複雑な例:host=www*&!(address=127.*|address6=::1)"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:39
+msgid "This allows to temporarily disable this job"
+msgstr "これにより、このジョブを一時的に無効にすることができます"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:115
+#: ../../../../modules/director/application/forms/IcingaHostGroupForm.php:30
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:106
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:448
+#: ../../../../modules/director/application/forms/IcingaServiceGroupForm.php:30
+msgid ""
+"This allows you to configure an assignment filter. Please feel free to "
+"combine as many nested operators as you want"
+msgstr "これにより、条件フィルタを設定できます。 入れ子にした演算子を"
+"自由に組み合わせることも可能です。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceSetForm.php:121
+msgid ""
+"This allows you to configure an assignment filter. Please feel free to "
+"combine as many nested operators as you want. You might also want to skip "
+"this, define it later and/or just add this set of services to single hosts"
+msgstr "これにより、条件フィルタを設定できます。 入れ子にした演算子を"
+"自由に組み合わせることも可能です。これをスキップして後で定義したり、"
+"このサービスセットを単一のホストに追加したりすることもできます。"
+
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:197
+msgid "This allows you to immediately deploy a modified configuration"
+msgstr "これにより、変更した設定をすぐに反映できます。"
+
+#: ../../../../modules/director/library/Director/ProvidedHook/CubeLinks.php:37
+#, php-format
+msgid "This allows you to modify properties for \"%s\""
+msgstr "これにより、\"%s\" のプロパティを編集できます"
+
+#: ../../../../modules/director/library/Director/ProvidedHook/CubeLinks.php:54
+msgid "This allows you to modify properties for all chosen hosts at once"
+msgstr "これにより、選択したすべてのホストのプロパティを一括で編集できます"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:62
+msgid "This basket is empty"
+msgstr "このバスケットは空です"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:253
+msgid "This has to be a MySQL or PostgreSQL database"
+msgstr "MySQLまたはPostgreSQLデータベースでなければなりません"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:62
+msgid ""
+"This host has been registered via the Icinga Director Self Service API. In "
+"case you re-installed the host or somehow lost it's secret key, you might "
+"want to dismiss the current key. This would allow you to register the same "
+"host again."
+msgstr "このホストはIcinga Director セルフサービス APIを介して登録されてい"
+"ます。ホストを再インストールした場合や、何らかの理由でその秘密鍵を"
+"失った場合、現在の鍵を消すことができます。"
+"これにより、同じホストを再度登録することができます。"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:73
+msgid "This is an abstract object type."
+msgstr "これは抽象オブジェクト型です。"
+
+#: ../../../../modules/director/library/Director/Web/Controller/TemplateController.php:176
+#, php-format
+msgid "This is the \"%s\" %s Template. Based on this, you might want to:"
+msgstr "これは%sの「%s」テンプレートです。テンプレートを利用して次のことができます。"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:119
+msgid ""
+"This is the name of the Endpoint object (and certificate name) you created "
+"for your ApiListener object. In case you are unsure what this means please "
+"make sure to read the documentation first"
+msgstr "これは、ApiListenerオブジェクト用に作成したエンドポイントオブジェクト"
+"の名前(および証明書の名前)です。 これが何を意味するのかよくわからない場合"
+"は、必ず最初にドキュメントを読むようにしてください。"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/HostsDashlet.php:17
+msgid ""
+"This is where you add all your servers, containers, network or sensor "
+"devices - and much more. Every subject worth to be monitored"
+msgstr "監視対象にするすべてのサーバー、コンテナー、ネットワーク、またはセンサーデバイスなどを追加できます"
+
+#: ../../../../modules/director/library/Director/Dashboard/HostsDashboard.php:22
+msgid ""
+"This is where you manage your Icinga 2 Host Checks. Host templates are your "
+"main building blocks. You can bundle them to \"choices\", allowing (or "
+"forcing) your users to choose among a given set of preconfigured templates."
+msgstr "Icinga 2ホスト監視を管理します。"
+"ホストテンプレートは主な構成要素です。 事前に設定した複数のテンプレートを組み合わせて「チョイス」を作成し、ユーザーがチョイスにに従って設定でできるようにする(または強制する)ことができます。"
+
+#: ../../../../modules/director/library/Director/Dashboard/ServicesDashboard.php:24
+msgid ""
+"This is where you manage your Icinga 2 Service Checks. Service Templates are "
+"your base building blocks, Service Sets allow you to assign multiple "
+"Services at once. Apply Rules make it possible to assign Services based on "
+"Host properties. And the list of all single Service Objects gives you the "
+"possibility to still modify (or delete) many of them at once."
+msgstr "Icinga 2 サービス監視設定を管理します。 サービステンプレートは、"
+"サービス監視設定の基本的な構成要素です。サービスセットを使用すると、一度に"
+"複数のサービス監視設定をまとめることができます。適用ルールを使用すると、"
+"ホスト設定を条件にして、サービス監視設定を動的に割り当てることができます。"
+" そして、すべての複数のサービスオブジェクを一度に変更(または削除)できます。"
+
+#: ../../../../modules/director/library/Director/Dashboard/UsersDashboard.php:21
+msgid ""
+"This is where you manage your Icinga 2 User (Contact) objects. Try to keep "
+"your User objects simply by movin complexity to your templates. Bundle your "
+"users in groups and build Notifications based on them. Running MS Active "
+"Directory or another central User inventory? Stay away from fiddling with "
+"manual config, try to automate all the things with Imports and related Sync "
+"Rules!"
+msgstr "Icinga 2 User(コンタクト)オブジェクトを管理します。 テンプレートに"
+"複雑さを加えるだけで、ユーザーオブジェクトを維持するようにしてください。 "
+"ユーザーをグループにまとめ、それらに基づいて通知を作成します。 MS Active "
+"DirectoryまたはLDAPなどの他の中央ユーザーインベントリを実行していますか?"
+"設定を手動でいじるのを避け、インポートや関連する同期ルールを使ってすべて"
+"のことを自動化してみてください。"
+
+#: ../../../../modules/director/library/Director/Dashboard/InfrastructureDashboard.php:24
+msgid ""
+"This is where you manage your Icinga 2 infrastructure. When adding a new "
+"Icinga Master or Satellite please re-run the Kickstart Helper once.\n"
+"\n"
+"When you feel the desire to manually create Zone or Endpoint objects please "
+"rethink this twice. Doing so is mostly the wrong way, might lead to a dead "
+"end, requiring quite some effort to clean up the whole mess afterwards."
+msgstr "Icinga 2インフラストラクチャを管理します。 新しいIcinga Masterまたは"
+"サテライトを追加するときは、キックスタートウィザードをもう一度実行してください。\n"
+"\n"
+"手動でZoneまたはEndpointオブジェクトを作成したいときは、よく考えて"
+"ください。 手動での作成は間違った方法であり、その後の混乱を招く恐れがあります。"
+
+#: ../../../../modules/director/library/Director/Web/Table/ObjectsTableEndpoint.php:45
+msgid "This is your Config master and will receive our Deployments"
+msgstr "これは設定マスタで、設定情報を受信します。"
+# smori
+
+#: ../../../../modules/director/library/Director/Web/Widget/JobDetails.php:57
+msgid "This job has not been executed yet"
+msgstr "このジョブは一度も実行されていません"
+
+#: ../../../../modules/director/library/Director/Web/Widget/JobDetails.php:33
+#, php-format
+msgid "This job runs every %ds and is currently pending"
+msgstr "このジョブは %d 秒ごとに実行され、現在は保留されています。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/JobDetails.php:37
+#, php-format
+msgid "This job runs every %ds."
+msgstr "このジョブは %d 秒ごとに実行されます"
+
+#: ../../../../modules/director/library/Director/Web/Widget/JobDetails.php:24
+#, php-format
+msgid ""
+"This job would run every %ds. It has been disabled and will therefore not be "
+"executed as scheduled"
+msgstr "このジョブは %d 秒ごとに実行されます。無効になっているため、スケジュールどおりに実行されません"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMakeBoolean.php:33
+msgid ""
+"This modifier transforms 0/\"0\"/false/\"false\"/\"n\"/\"no\" to false and "
+"1, \"1\", true, \"true\", \"y\" and \"yes\" to true, both in a case "
+"insensitive way. What should happen if the given value does not match any of "
+"those? You could return a null value, or default to false or true. You might "
+"also consider interrupting the whole import process as of invalid source data"
+msgstr "このプロパティ変換ルールは、0、\"0\"、false、\"false\"、\"n\"、\"no\"を false"
+" に、1、\"1\"、true、\"true\"、\"y\"、\"yes\"をtrueに変換します。 大文字と"
+"小文字を区別しません。 与えられた値がそれらのどれとも一致しない場合に、どう"
+"処理させるかを、このセレクトボックスで指定します。 null値を返すことも、"
+"デフォルトをfalseまたはtrueにすることも可能です。 また、無効なソースデータ"
+"としてインポートプロセス全体を中断することもできます。"
+
+#: ../../../../modules/director/application/forms/ImportSourceForm.php:89
+msgid ""
+"This must be a column containing unique values like hostnames. Unless "
+"otherwise specified this will then be used as the object_name for the "
+"syncronized Icinga object. Especially when getting started with director "
+"please make sure to strictly follow this rule. Duplicate values for this "
+"column on different rows will trigger a failure, your import run will not "
+"succeed. Please pay attention when synching services, as \"purge\" will only "
+"work correctly with a key_column corresponding to host!name. Check the "
+"\"Combine\" property modifier in case your data source cannot provide such a "
+"field"
+msgstr "これは、ホスト名などの一意の値を含む列である必要があります。"
+"特に指定のない限り、これは同期化されたIcingaオブジェクトのobject_nameとして"
+"使用されます。 特にdirectorを始めるときは、必ずこの規則に厳密に従って"
+"ください。この列の値が異なる行に重複していると失敗し、インポートは成功"
+"しません。 \"purge\"はhost!nameに対応するkey_columnでのみ正しく機能するので、"
+"サービスを同期するときは注意してください。 データソースがそのような"
+"フィールドを提供できない場合は、\"Combine\"プロパティ変換ルールを確認してください。"
+
+#: ../../../../modules/director/application/forms/ImportRowModifierForm.php:31
+msgid "This must be an import source column (property)"
+msgstr "これはインポートソースのカラム(プロパティ)である必要があります"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:414
+msgid "This object has been disabled"
+msgstr "このオブジェクトは無効化されています。"
+
+#: ../../../../modules/director/library/Director/Web/Widget/ActivityLogInfo.php:409
+msgid "This object has been enabled"
+msgstr "このオブジェクトは有効化されています。"
+
+#: ../../../../modules/director/library/Director/Web/ObjectPreview.php:76
+msgid "This object will not be deployed as it has been disabled"
+msgstr "このオブジェクトは無効化されているため設定が反映されません。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierCombine.php:17
+msgid ""
+"This pattern will be evaluated, and variables like ${some_column} will be "
+"filled accordingly. A typical use-case is generating unique service "
+"identifiers via ${host}!${service} in case your data source doesn't allow "
+"you to ship such. The chosen \"property\" has no effect here and will be "
+"ignored."
+msgstr "このパターンが評価され、それに応じて ${some_column} のような変数が"
+"埋められます。 典型的なユースケースは ${host}!${service}でユニークな"
+"サービス識別子を生成することです。 選択された「プロパティ」はここでは"
+"効果がなく、無視されます。"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:234
+msgid ""
+"This requires the Icinga Agent to be installed. It generates and signs it's "
+"certificate and it also generates a minimal icinga2.conf to get your agent "
+"connected to it's parents"
+msgstr "これには、Icinga Agentをインストールする必要があります。 証明書を"
+"生成して署名し、エージェントをその親に接続するための最小限のicinga2.confも"
+"生成します。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:371
+#, php-format
+msgid ""
+"This service belongs to the %s Service Set. Still, you might want to "
+"override the following properties for this host only."
+msgstr "このサービスは %s サービスセットに属しています。それでも、"
+"このホストに対してのみ次のプロパティを上書きすることをお勧めします。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:415
+#, php-format
+msgid ""
+"This service belongs to the service set \"%s\". Still, you might want to "
+"change the following properties for this host only."
+msgstr "このサービスはサービスセット \"%s\" に属しています。 それでも、"
+"このホストに対してのみ次のプロパティを変更することをお勧めします。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:352
+msgid ""
+"This service has been generated in an automated way, but still allows you to "
+"override the following properties in a safe way."
+msgstr "このサービスは自動化された方法で生成されていますが、それでも"
+"次のプロパティを安全な方法に上書きすることができます。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:358
+#, php-format
+msgid ""
+"This service has been generated using the %s apply rule, assigned where %s"
+msgstr "このサービスは%s適用ルールを使用して生成されました。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:383
+#, php-format
+msgid ""
+"This service has been inherited from %s. Still, you might want to change the "
+"following properties for this host only."
+msgstr "このサービスは%sから継承されました。 それでも、このホストに対してのみ"
+"次のプロパティを変更することをお勧めします。"
+
+#: ../../../../modules/director/library/Director/Web/Table/IcingaServiceSetServiceTable.php:196
+#, php-format
+msgid "This set has been inherited from %s"
+msgstr "このセットは%sから継承されました"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/KickstartDashlet.php:17
+msgid ""
+"This synchronizes Icinga Director to your Icinga 2 infrastructure. A new run "
+"should be triggered on infrastructure changes"
+msgstr "Icinga Director を Icinga 2 インフラストラクチャに同期します。"
+"インフラストラクチャの変更時に新しい実行がトリガされるはずです"
+
+#: ../../../../modules/director/library/Director/Web/Table/TemplateUsageTable.php:104
+msgid "This template is not in use"
+msgstr "このテンプレートは使用されていません"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:885
+#, php-format
+msgid "This template is still in use by %d other objects"
+msgstr "このテンプレートは %d 個のほかのオブジェクトに利用されています。"
+
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:51
+msgid "This will be shown as a label for the given choice"
+msgstr "与えられた選択肢に対するラベルを示します"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistEntryForm.php:33
+msgid "This will be the visible caption for this entry"
+msgstr "この項目に入力した名前がユーザに見えるラベルになります"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:123
+msgid "This will invalidate the former key"
+msgstr "これは以前のキーを無効にします"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:224
+msgid "Ticket"
+msgstr "チケット"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:21
+msgid "Time Period"
+msgstr "スケジュール"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:26
+msgid "Time Periods"
+msgstr "スケジュール"
+
+#: ../../../../modules/director/application/forms/DirectorJobForm.php:60
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:138
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:229
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:158
+msgid "Time period"
+msgstr "スケジュール"
+
+#: ../../../../modules/director/application/controllers/TimeperiodController.php:17
+msgid "Time period ranges"
+msgstr "スケジュールの期間"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:319
+msgid "Time ranges"
+msgstr "スケジュール"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:70
+msgid "Timeout"
+msgstr "タイムアウト"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php:13
+msgid "Timeperiod Templates"
+msgstr "スケジュールテンプレート"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php:16
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php:13
+#: ../../../../modules/director/library/Director/Web/Table/IcingaTimePeriodRangeTable.php:46
+msgid "Timeperiods"
+msgstr "スケジュール"
+
+msgid "Timeperiod"
+msgstr "スケジュール"
+
+msgid "TimePeriod"
+msgstr "スケジュール"
+
+msgid "TimePeriods"
+msgstr "スケジュール"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodRangeForm.php:28
+msgid "Timerperiods"
+msgstr "スケジュール"
+
+#: ../../../../modules/director/library/Director/Web/Table/ImportrunTable.php:31
+msgid "Timestamp"
+msgstr "タイムスタンプ"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:46
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:154
+msgid ""
+"To ensure downloaded packages are build by the Icinga Team and not "
+"compromised by third parties, you will be able to provide an array of SHA1 "
+"hashes here. In case you have defined any hashses, the module will not "
+"continue with updating / installing the Agent in case the SHA1 hash of the "
+"downloaded MSI package is not matching one of the provided hashes of this "
+"setting"
+msgstr "ダウンロードしたパッケージがIcingaチームによって構築され、"
+"第三者によって侵害されないようにするために、ここでSHA1ハッシュの配列を"
+"提供することができます。 ハッシュを定義した場合、ダウンロードしたMSI"
+"パッケージのSHA1ハッシュがこの設定で提供されているハッシュの1つと一致"
+"しない場合、モジュールはエージェントの更新/インストールを続行しません。"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:197
+msgid "Top Down"
+msgstr "トップダウン"
+
+#: ../../../../modules/director/library/Director/Web/Table/TemplateUsageTable.php:57
+msgid "Total"
+msgstr "合計"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:31
+msgid "Transform Host Name"
+msgstr "ホスト名を変換"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:37
+msgid "Transform to lowercase"
+msgstr "小文字に変換"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:38
+msgid "Transform to uppercase"
+msgstr "大文字に変換"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1599
+msgid "Transition types"
+msgstr "状態遷移タイプ"
+
+#: ../../../../modules/director/library/Director/Web/ActionBar/TemplateActionBar.php:30
+msgid "Tree"
+msgstr "ツリー"
+
+#: ../../../../modules/director/application/forms/ImportRunForm.php:23
+msgid "Trigger Import Run"
+msgstr "インポート実行をトリガ"
+
+#: ../../../../modules/director/application/forms/SyncRunForm.php:23
+msgid "Trigger this Sync"
+msgstr "この同期をトリガ"
+
+#: ../../../../modules/director/application/forms/ImportRunForm.php:45
+msgid "Triggering this Import Source failed"
+msgstr "このインポートソースのトリガに失敗しました"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/SettingsDashlet.php:17
+msgid "Tweak some global Director settings"
+msgstr "グローバルなdirector設定を調整します"
+
+#: ../../../../modules/director/library/Director/Web/Table/CoreApiFieldsTable.php:73
+msgid "Type"
+msgstr "タイプ"
+
+#: ../../../../modules/director/application/controllers/InspectController.php:84
+msgid "Type attributes"
+msgstr "タイプ属性"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:167
+#: ../../../../modules/director/library/Director/DataType/DataTypeSqlQuery.php:27
+#, php-format
+msgid "Unable to fetch data: %s"
+msgstr "データを取得できません: %s"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:364
+msgid ""
+"Unable to store a host with the given properties because of insufficient "
+"permissions"
+msgstr "権限が不十分なため、指定されたプロパティでホストを保存できません"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:339
+#, php-format
+msgid ""
+"Unable to store the configuration to \"%s\". Please check file permissions "
+"or manually store the content shown below"
+msgstr "設定を\"%s\"に保存できません。 ファイルのアクセス権を確認するか、"
+"以下に示す内容を手動で保存してください。"
+
+#: ../../../../modules/director/library/Director/Db/Housekeeping.php:49
+msgid "Undeployed configurations"
+msgstr "未反映の設定"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/StateFilterSet.php:27
+msgid "Unknown"
+msgstr "Unknown (不明)"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:136
+msgid "Unknown, failed to collect related information"
+msgstr "不明、関連情報の収集に失敗しました"
+
+#: ../../../../modules/director/library/Director/Web/Widget/DeploymentInfo.php:134
+msgid "Unknown, still waiting for config check outcome"
+msgstr "不明、まだ設定の検査の結果を待っています"
+
+#: ../../../../modules/director/library/Director/Db/Housekeeping.php:53
+msgid "Unlinked imported properties"
+msgstr "インポートされたプロパティをリンク解除しました"
+
+#: ../../../../modules/director/library/Director/Db/Housekeeping.php:51
+msgid "Unlinked imported row sets"
+msgstr "インポートされた行セットをリンク解除しました"
+
+#: ../../../../modules/director/library/Director/Db/Housekeeping.php:52
+msgid "Unlinked imported rows"
+msgstr "インポートされた行をリンク解除しました"
+
+#: ../../../../modules/director/application/controllers/PhperrorController.php:19
+msgid "Unsatisfied dependencies"
+msgstr "未解決の依存関係"
+
+#: ../../../../modules/director/library/Director/Db/Housekeeping.php:50
+msgid "Unused rendered files"
+msgstr "使用されていない設定ファイル"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/StateFilterSet.php:20
+msgid "Up"
+msgstr "Up (起動)"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodForm.php:25
+msgid "Update Method"
+msgstr "アップデート方法"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:49
+msgid "Update Policy"
+msgstr "アップデートポリシー"
+
+#: ../../../../modules/director/application/controllers/BasketsController.php:26
+#: ../../../../modules/director/application/forms/BasketUploadForm.php:43
+msgid "Upload"
+msgstr "アップロード"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:120
+msgid "Upload a Basket"
+msgstr "バスケットをアップロード"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:121
+msgid "Upload a Configuration Basket"
+msgstr "構成バスケットをアップロード"
+
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:295
+msgid "Usage"
+msgstr "使用状況"
+
+#: ../../../../modules/director/library/Director/Web/Widget/AdditionalTableActions.php:101
+#, php-format
+msgid "Usage (%s)"
+msgstr "使用状況(%s)"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:71
+msgid "Use a local file or network share"
+msgstr "ローカルファイルまたはネットワーク共有を使用"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:18
+msgid "Use lowercase first"
+msgstr "最初に小文字を使用"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:271
+msgid "Used sources"
+msgstr "ソースを使用"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:17
+#: ../../../../modules/director/library/Director/TranslationDummy.php:17
+msgid "User"
+msgstr "ユーザ"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:18
+msgid "User Group"
+msgstr "ユーザグループ"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/UserGroupsDashlet.php:11
+msgid "UserGroups"
+msgstr "ユーザグループ"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/UserTemplateDashlet.php:13
+msgid "User Templates"
+msgstr "ユーザテンプレート"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:153
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:60
+msgid "User groups"
+msgstr "ユーザグループ"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:113
+msgid ""
+"User groups that should be directly assigned to this user. Groups can be "
+"useful for various reasons. You might prefer to send notifications to groups "
+"instead of single users"
+msgstr "このユーザーに直接割り当てる必要があるユーザーグループ。 "
+"グループはさまざまな理由で役に立ちます。 単一のユーザーではなくグループに"
+"通知を送信することをお勧めします。"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:155
+msgid "User groups that should be notified by this notifications"
+msgstr "通知先のユーザグループ"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:194
+msgid "User properties"
+msgstr "ユーザプロパティ"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:22
+msgid "User template name"
+msgstr "ユーザテンプレート名"
+
+#: ../../../../modules/director/application/forms/IcingaUserGroupForm.php:17
+msgid "Usergroup"
+msgstr "ユーザグループ"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceCoreApi.php:63
+msgid "Usergroups"
+msgstr "ユーザグループ"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:28
+msgid "Username"
+msgstr "ユーザ名"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:128
+#: ../../../../modules/director/library/Director/DataType/DataTypeDirectorObject.php:59
+#: ../../../../modules/director/library/Director/Import/ImportSourceCoreApi.php:62
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarTable.php:47
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarVariantsTable.php:62
+msgid "Users"
+msgstr "ユーザ"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/UserObjectDashlet.php:16
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/UsersDashlet.php:13
+msgid "Users / Contacts"
+msgstr "ユーザ / コンタクト"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:130
+msgid "Users that should be notified by this notifications"
+msgstr "この通知によって通知されるべきユーザ"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php:17
+msgid ""
+"Using Apply Rules a Service can be applied to multiple hosts at once, based "
+"on filters dealing with any combination of their properties"
+msgstr "適用ルールを使用すると、サービスは、それらのプロパティの任意の"
+"組み合わせを処理するフィルタに基づいて、一度に複数のホストに適用できます。"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:317
+#: ../../../../modules/director/application/forms/IcingaHostSelfServiceForm.php:44
+msgid "Usually your hosts main IPv6 address"
+msgstr "通常はホストのメインのIPv6アドレス"
+
+#: ../../../../modules/director/application/forms/CustomvarForm.php:21
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:45
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:54
+#: ../../../../modules/director/application/forms/IcingaHostVarForm.php:27
+#: ../../../../modules/director/application/forms/IcingaServiceVarForm.php:27
+#: ../../../../modules/director/library/Director/Web/Table/IcingaCommandArgumentTable.php:46
+msgid "Value"
+msgstr "値"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:31
+msgid "Value type"
+msgstr "値のタイプ"
+
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarVariantsTable.php:56
+msgid "Variable Value"
+msgstr "変数値"
+
+#: ../../../../modules/director/application/forms/CustomvarForm.php:16
+#: ../../../../modules/director/library/Director/Web/Table/CustomvarTable.php:41
+msgid "Variable name"
+msgstr "変数名"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1412
+msgid "Volatile"
+msgstr "揮発性"
+
+#: ../../../../modules/director/library/Director/Dashboard/TimeperiodsDashboard.php:20
+msgid ""
+"Want to define to execute specific checks only withing specific time "
+"periods? Get mobile notifications only out of office hours, but mail "
+"notifications all around the clock? Time Periods allow you to tackle those "
+"and similar requirements."
+msgstr "スケジュールでは、監視や通知を実施するスケジュールを定義できます。"
+"例えば、通知の設定と組み合わせて、営業時間内と営業時間外で通知する先を変更する"
+"というように定義することもできます。"
+
+#: ../../../../modules/director/library/Director/IcingaConfig/StateFilterSet.php:25
+msgid "Warning"
+msgstr "Warning (警告)"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1071
+msgid ""
+"What kind of object this should be. Templates allow full access to any "
+"property, they are your building blocks for \"real\" objects. External "
+"objects should usually not be manually created or modified. They allow you "
+"to work with objects locally defined on your Icinga nodes, while not "
+"rendering and deploying them with the Director. Apply rules allow to assign "
+"services, notifications and groups to other objects."
+msgstr "テンプレートはあらゆるプロパティの設定を可能にします。"
+"テンプレートは、最終的な作られる設定情報のためのパーツです。 "
+"外部オブジェクト(Directorの管轄外の設定)は、最低限にとどめて、追加・変更しないでください。 "
+"外部オブジェクトとDirectorでの設定を併用することはできません。"
+"適用ルールにより、サービス、通知、およびグループを他の"
+"オブジェクトに割り当てることができます。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierMap.php:28
+msgid ""
+"What should happen if the lookup key does not exist in the data list? You "
+"could return a null value, keep the unmodified imported value or interrupt "
+"the import process"
+msgstr "参照キーがデータリストに存在しない場合の処理を選択します。"
+"null値を返す、変更されていないインポート値を保持する、または"
+"インポートプロセスを中断することができます。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRegexSplit.php:24
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierSplit.php:24
+msgid "What should happen when the given string is empty?"
+msgstr "与えられた文字列が空の場合の処理を選択します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:66
+msgid "What should happen when the result array is empty?"
+msgstr "結果の配列が空の場合の処理を選択します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:50
+msgid "What should happen when the specified element is not available?"
+msgstr "指定された要素が利用できないときの処理を選択します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:53
+msgid "What should happen with matching elements?"
+msgstr "一致する要素の処理を選択します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php:55
+msgid ""
+"What should happen with the row, when this property matches the given "
+"expression?"
+msgstr "このプロパティが、フィルタ表現が行にマッチしたときの処理を選択します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierDnsRecords.php:32
+msgid "What should we do if the DNS lookup fails?"
+msgstr "DNS参照が失敗した場合の処理を選択します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:28
+msgid "What should we do if the desired part does not exist?"
+msgstr "目的の部分が存在しない場合の処理を選択します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierGetHostByName.php:15
+msgid "What should we do if the host (DNS) lookup fails?"
+msgstr "ホスト(DNS)参照が失敗した場合の処理を選択します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierJsonDecode.php:22
+msgid "What should we do in case we are unable to decode the given string?"
+msgstr "与えられた文字列をデコードできない場合の処理を選択します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php:16
+msgid "What should we extract from the DN?"
+msgstr "DNから抜き出す値を選択します。"
+
+#: ../../../../modules/director/application/forms/BasketForm.php:55
+msgid ""
+"What should we place into this Basket every time we create new snapshot?"
+msgstr "新しいスナップショットを作成するたびに、このバスケットに何を"
+"配置すればよいですか?"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:21
+msgid "What to use as your Icinga 2 Agent's Host Name"
+msgstr "Icinga 2 Agentのホスト名に何を使うか"
+
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:209
+msgid ""
+"When deploying configuration, wait at least this amount of seconds unless "
+"the next deployment should take place"
+msgstr "設定を反映するときは、次の設定反映を実行する必要がある場合を除き、"
+"少なくとも指定した秒数だけ待機します。"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:63
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRegexSplit.php:21
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierSplit.php:21
+msgid "When empty"
+msgstr "空のとき"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:47
+msgid "When not available"
+msgstr "利用できないとき"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:207
+msgid "When the last notification should be sent"
+msgstr "最後の通知を何分あるいは何時間後に送信するかを時間で設定します。"
+
+#: ../../../../modules/director/application/forms/SettingsForm.php:63
+msgid "Whether all configured Jobs should be disabled"
+msgstr "設定したすべてのジョブを無効にするか選択します。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1385
+msgid "Whether flap detection is enabled on this object"
+msgstr "このオブジェクトでフラップ検出が有効になっているか"
+
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:183
+msgid ""
+"Whether rendering should be forced. If not enforced, this job re-renders the "
+"configuration only when there have been activities since the last rendered "
+"config"
+msgstr "生成を強制するべきかを選択します。 適用されていない場合、このジョブは"
+"最後に整形された設定以降にアクティビティがあった場合にのみ設定を再整形します。"
+# smori
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:94
+msgid "Whether the agent is configured to accept config"
+msgstr "エージェントが設定の同期を受け入れるように構成されているかを選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:37
+msgid ""
+"Whether the argument value is a string (allowing macros like $host$) or an "
+"Icinga DSL lambda function (will be enclosed with {{ ... }}"
+msgstr "引数値が文字列($host$のようなマクロを許す)か、あるいはIcinga DSLラムダ関数({{...}}で囲まれる)かを選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaServiceForm.php:661
+msgid ""
+"Whether the check commmand for this service should be executed on the Icinga "
+"agent"
+msgstr "このサービスのcheckコマンドをIcingaエージェントで実行するか選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:112
+msgid ""
+"Whether the parameter name should not be passed to the command. Per default, "
+"the parameter name (e.g. -H) will be appended, so no need to explicitly set "
+"this to \"No\"."
+msgstr "パラメータ名をコマンドに渡さないようにするか選択します。 デフォルトでは、"
+"パラメータ名(たとえば-H)が追加されるので、これを明示的に「いいえ」に"
+"設定する必要はありません。"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:88
+msgid ""
+"Whether the parent (master) node should actively try to connect to this agent"
+msgstr "親(マスター)ノードがこのエージェントへの接続をアクティブに試行する"
+"必要があるか選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:76
+msgid ""
+"Whether the set_if parameter is a string (allowing macros like $host$) or an "
+"Icinga DSL lambda function (will be enclosed with {{ ... }}"
+msgstr "set_if パラメータが文字列($host$のようなマクロを許可する)か、あるいは"
+"Icinga DSLラムダ関数({{...}}で囲まれる)か選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:121
+msgid "Whether this argument should be required"
+msgstr "この引数が必要か選択します。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1413
+msgid "Whether this check is volatile."
+msgstr "「はい」を選択すると監視の一時的な状態を保持しません。つまり最大監視試行回数=1と同等の動きをします。"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:94
+msgid "Whether this dependency should affect hosts or services"
+msgstr "この依存関係オブジェクトがホストまたはサービスに影響を与えるか選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:135
+msgid "Whether this field should be mandatory"
+msgstr "このフィールドが必須か選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaHostForm.php:79
+msgid "Whether this host has the Icinga 2 Agent installed"
+msgstr "このホストにIcinga 2 Agentがインストールされているか選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:83
+msgid "Whether this notification should affect hosts or services"
+msgstr "この通知がホストまたはサービスに影響を与えるか選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:104
+msgid ""
+"Whether this parameter should be repeated when multiple values (read: array) "
+"are given"
+msgstr "複数の値(read:array)が与えられたときにこのパラメータを繰り返すべきか選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaZoneForm.php:24
+msgid ""
+"Whether this zone should be available everywhere. Please note that it rarely "
+"leads to the desired result when you try to distribute global zones in "
+"distrubuted environments"
+msgstr "このゾーンのスコープをグローバルにするか選択します。グローバル"
+"ゾーンの分散構成は推奨されていません。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1361
+msgid "Whether to accept passive check results for this object"
+msgstr "このオブジェクトのパッシブ監視結果を受け入れるか"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1355
+msgid "Whether to actively check this object"
+msgstr "このオブジェクトをアクティブに監視するか選択します。"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:33
+msgid "Whether to adjust your host name"
+msgstr "ホスト名を調整するか選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:158
+msgid ""
+"Whether to disable checks when this dependency fails. Defaults to false."
+msgstr "この依存関係オブジェクトが失敗したときに監視を無効にするか選択します。 デフォルトは「いいえ」です。"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:166
+msgid ""
+"Whether to disable notifications when this dependency fails. Defaults to "
+"true."
+msgstr "この依存関係オブジェクトが失敗したときに通知を無効にするか選択します。デフォルトは「はい」です。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1373
+msgid "Whether to enable event handlers this object"
+msgstr "このオブジェクトのイベントハンドラを有効にするか選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:174
+msgid ""
+"Whether to ignore soft states for the reachability calculation. Defaults to "
+"true."
+msgstr "到達可能性の計算で一時的な異常(soft state)を無視するか選択します。デフォルトは「はい」です。"
+
+#: ../../../../modules/director/application/forms/IcingaTimePeriodForm.php:77
+msgid "Whether to prefer timeperiods includes or excludes. Default to true."
+msgstr "「はい」を指定した場合、スケジュールのインクルードを優先します。デフォルト値は「はい」です。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1379
+msgid "Whether to process performance data provided by this object"
+msgstr "このオブジェクトによって提供されるパフォーマンスデータを処理するか選択します。"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:67
+msgid ""
+"Whether to purge existing objects. This means that objects of the same type "
+"will be removed from Director in case they no longer exist at your import "
+"source."
+msgstr "「はい」の場合、インポートソースに存在しないオブジェクトを"
+"Directorから削除します。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1367
+msgid "Whether to send notifications for this object"
+msgstr "このオブジェクトに関する通知を送信するか選択します。"
+
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:90
+msgid "Whether to send notifications for this user"
+msgstr "ユーザに通知を送信するか選択します。"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:118
+msgid ""
+"Whether you want to merge or replace the destination field. Makes no "
+"difference for strings"
+msgstr "宛先フィールドを結合するか置き換えるか。 文字列に差異はありません"
+
+#: ../../../../modules/director/application/forms/DirectorDatalistEntryForm.php:24
+msgid ""
+"Will be stored as a custom variable value when this entry is chosen from the "
+"list"
+msgstr "カスタム変数の値の入力時に、このキーが選択されると、変数の値になります。"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:225
+msgid "Windows Kickstart Script"
+msgstr "Windowsキックスタートスクリプト"
+
+#: ../../../../modules/director/application/forms/DirectorDatafieldForm.php:51
+msgid "Wipe related vars"
+msgstr "関連する変数を掃除する"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ActivityLogDashlet.php:17
+msgid "Wondering about what changed why? Track your changes!"
+msgstr "設定変更が行われた履歴を記録します。"
+"どんな設定がだれによってどのように変更されたのか、追跡することができます。"
+
+#: ../../../../modules/director/application/views/helpers/FormDataFilter.php:525
+msgid "Wrap this expression into an operator"
+msgstr "この式を演算子にラップする"
+
+#: ../../../../modules/director/application/forms/IcingaDeleteObjectForm.php:17
+#, php-format
+msgid "YES, please delete \"%s\""
+msgstr "はい、\"%s\"を削除してください"
+
+#: ../../../../modules/director/application/forms/IcingaZoneForm.php:30
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:226
+#: ../../../../modules/director/application/forms/SettingsForm.php:59
+#: ../../../../modules/director/application/forms/SettingsForm.php:74
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:73
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:189
+#: ../../../../modules/director/library/Director/Job/ConfigJob.php:201
+#: ../../../../modules/director/library/Director/Job/ImportJob.php:101
+#: ../../../../modules/director/library/Director/Job/SyncJob.php:101
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php:25
+msgid "Yes"
+msgstr "はい"
+
+#: ../../../../modules/director/application/controllers/BasketsController.php:41
+msgid ""
+"You can create Basket snapshots at any time, this will persist a serialized "
+"representation of all involved objects at that moment in time. Snapshots can "
+"be exported, imported, shared and restored - to the very same or another "
+"Director instance."
+msgstr "Basketスナップショットはいつでも作成できます。これにより、その時点"
+"での関連するすべてのオブジェクトのシリアル化された表現が保持されます。"
+"スナップショットは、まったく同じまたは別のDirectorインスタンスに"
+"エクスポート、インポート、共有、および復元できます。"
+
+#: ../../../../modules/director/library/Director/Web/SelfService.php:136
+msgid ""
+"You can stop sharing a Template at any time. This will immediately "
+"invalidate the former key."
+msgstr "テンプレートの共有はいつでも中止できます。 これはすぐに以前のキーを"
+"無効にします。"
+
+#: ../../../../modules/director/library/Director/Job/ImportJob.php:94
+#: ../../../../modules/director/library/Director/Job/SyncJob.php:94
+msgid ""
+"You could immediately apply eventual changes or just learn about them. In "
+"case you do not want them to be applied immediately, defining a job still "
+"makes sense. You will be made aware of available changes in your Director "
+"GUI."
+msgstr "すぐに最終的な変更を適用するか、あるいはそれらについて知ることができます。"
+" すぐに適用したくない場合は、ジョブを定義することにも意味があります。"
+" Director GUIで利用可能な変更点がわかります。"
+
+#: ../../../../modules/director/application/forms/SelfServiceSettingsForm.php:61
+msgid ""
+"You might want to let the generated Powershell script install the Icinga 2 "
+"Agent in an automated way. If so, please choose where your Windows nodes "
+"should fetch the Agent installer"
+msgstr "自動生成で生成されたPowershellスクリプトを使ってIcinga 2 Agentを"
+"インストールすることができます。その場合、Windowsノードが"
+"Agentインストーラーを取得する場所を選択してください。"
+
+#: ../../../../modules/director/application/forms/IcingaObjectFieldForm.php:161
+msgid ""
+"You might want to show this field only when certain conditions are met. "
+"Otherwise it will not be available and values eventually set before will be "
+"cleared once stored"
+msgstr "特定の条件が満たされた場合にのみこのフィールドを表示することを"
+"お勧めします。 そうしなければ利用できないでしょう。また、以前に設定された値は"
+"いったん格納されると消去されます"
+
+#: ../../../../modules/director/application/forms/ImportRowModifierForm.php:46
+msgid ""
+"You might want to write the modified value to another (new) property. This "
+"property name can be defined here, the original property would remain "
+"unmodified. Please leave this blank in case you just want to modify the "
+"value of a specific property"
+msgstr "変更した値を別の(新しい)プロパティに書き込むことができます。 この"
+"プロパティ名はここで定義でき、元のプロパティは変更されないままになります。"
+" 特定のプロパティの値を変更するだけの場合は、空白のままにしてください。"
+
+#: ../../../../modules/director/application/controllers/SyncruleController.php:110
+#, php-format
+msgid "You must define some %s before you can run this Sync Rule"
+msgstr "この同期ルールを実行する前に、いくつかの %s を定義する必要があります。"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:153
+msgid "Your Icinga 2 API username"
+msgstr "Icinga 2 APIユーザ名"
+
+#: ../../../../modules/director/library/Director/Import/ImportSourceLdap.php:49
+msgid ""
+"Your LDAP search base. Often something like OU=Users,OU=HQ,DC=your,"
+"DC=company,DC=tld"
+msgstr "LDAP検索ベース。 多くの場合、OU=Users,OU=HQ,DC=your,DC=company,DC=tld"
+"のようなものです。"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:94
+msgid ""
+"Your configuration looks good. Still, you might want to re-run this "
+"kickstart wizard to (re-)import modified or new manually defined Command "
+"definitions or to get fresh new ITL commands after an Icinga 2 Core upgrade."
+msgstr "設定は良好です。それでも、このキックスタートウィザードを"
+"再実行して、変更したコマンド定義または新しい手動で定義したコマンド定義を"
+"(再)インポートするか、またはIcinga 2 Coreアップグレード後に新しい新しい"
+"ITLコマンドを取得することができます。"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:72
+#, php-format
+msgid "Your database looks good, you are ready to %s"
+msgstr "データベースの設定は良好です。%sへの準備ができています"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:106
+msgid ""
+"Your installation of Icinga Director has not yet been prepared for "
+"deployments. This kickstart wizard will assist you with setting up the "
+"connection to your Icinga 2 server."
+msgstr "Icinga Directorのインストールはまだ設定反映の準備が"
+"できていません。 このキックスタートウィザードは、Icinga 2サーバー"
+"への接続設定を手助けします。"
+
+#: ../../../../modules/director/library/Director/Web/Form/DirectorObjectForm.php:1298
+msgid "Your regular check interval"
+msgstr "通常の監視間隔"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierReplace.php:20
+msgid "Your replacement string"
+msgstr "置換用の文字列"
+
+#: ../../../../modules/director/application/forms/IcingaTemplateChoiceForm.php:67
+msgid "Your users will be allowed to choose among those templates"
+msgstr "ユーザーはそれらのテンプレートの中から選択することが許される"
+
+#: ../../../../modules/director/application/forms/SyncRuleForm.php:23
+#: ../../../../modules/director/library/Director/TranslationDummy.php:15
+#: ../../../../modules/director/library/Director/Web/Table/ObjectsTableEndpoint.php:21
+msgid "Zone"
+msgstr "ゾーン"
+
+#: ../../../../modules/director/application/forms/IcingaZoneForm.php:14
+msgid "Zone name"
+msgstr "ゾーン名"
+
+#: ../../../../modules/director/application/forms/IcingaCommandForm.php:95
+#: ../../../../modules/director/application/forms/IcingaDependencyForm.php:60
+#: ../../../../modules/director/application/forms/IcingaNotificationForm.php:65
+#: ../../../../modules/director/application/forms/IcingaUserForm.php:76
+#: ../../../../modules/director/application/forms/IcingaUserGroupForm.php:42
+msgid "Zone settings"
+msgstr "ゾーン設定"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php:13
+#: ../../../../modules/director/library/Director/Import/ImportSourceCoreApi.php:64
+msgid "Zones"
+msgstr "ゾーン"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:346
+msgid "a list"
+msgstr "リスト"
+
+#: ../../../../modules/director/library/Director/Web/Widget/AdditionalTableActions.php:87
+msgid "all"
+msgstr "すべて"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:96
+#: ../../../../modules/director/application/controllers/BasketController.php:114
+#: ../../../../modules/director/application/controllers/BasketController.php:348
+#: ../../../../modules/director/application/controllers/DataController.php:104
+#: ../../../../modules/director/application/controllers/HostController.php:480
+#: ../../../../modules/director/application/controllers/ImportsourceController.php:276
+#: ../../../../modules/director/application/controllers/ServiceController.php:137
+#: ../../../../modules/director/application/controllers/ServiceController.php:198
+#: ../../../../modules/director/application/controllers/SyncruleController.php:257
+#: ../../../../modules/director/library/Director/Web/ActionBar/DirectorBaseActionBar.php:35
+#: ../../../../modules/director/library/Director/Web/Controller/ActionController.php:150
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:207
+#: ../../../../modules/director/library/Director/Web/Controller/ObjectController.php:492
+msgid "back"
+msgstr "戻る"
+
+#: ../../../../modules/director/application/locale/translateMe.php:11
+msgid "critical"
+msgstr "危険"
+
+#: ../../../../modules/director/application/locale/translateMe.php:6
+msgid "down"
+msgstr "停止"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:27
+msgid "e.g. -H or --hostname, empty means \"skip_key\""
+msgstr "(例) -H、--hostname。空は\"skip_key\"を意味する"
+
+#: ../../../../modules/director/application/forms/IcingaCommandArgumentForm.php:56
+msgid "e.g. 5%, $host.name$, $lower$%:$upper$%"
+msgstr "(例)5%、$host.name$、$lower$%:$upper$%"
+
+#: ../../../../modules/director/application/forms/SyncPropertyForm.php:164
+msgid "failed to fetch"
+msgstr "取得に失敗しました"
+
+#: ../../../../modules/director/application/forms/KickstartForm.php:259
+#: ../../../../modules/director/library/Director/Util.php:177
+msgid "here"
+msgstr "ここ"
+
+#: ../../../../modules/director/application/forms/IcingaHostVarForm.php:23
+msgid "host var name"
+msgstr "ホスト変数名"
+
+#: ../../../../modules/director/application/forms/IcingaHostVarForm.php:28
+msgid "host var value"
+msgstr "ホスト変数値"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:298
+msgid "modified"
+msgstr "修正された"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:276
+msgid "new"
+msgstr "new"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:283
+msgid "no related group exists"
+msgstr "関連グループが存在しません。"
+
+#: ../../../../modules/director/application/locale/translateMe.php:9
+msgid "ok"
+msgstr "ok"
+
+#: ../../../../modules/director/library/Director/Dashboard/Dashlet/Dashlet.php:285
+msgid "one related group exists"
+msgstr "一つの関連グループが存在します。"
+
+#: ../../../../modules/director/application/locale/translateMe.php:8
+msgid "pending"
+msgstr "保留中"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php:55
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:71
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRegexSplit.php:29
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierSplit.php:29
+msgid "return NULL"
+msgstr "NULLを返す"
+
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierArrayFilter.php:70
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierRegexSplit.php:28
+#: ../../../../modules/director/library/Director/PropertyModifier/PropertyModifierSplit.php:28
+msgid "return an empty array"
+msgstr "空配列を返す"
+
+#: ../../../../modules/director/application/forms/IcingaServiceVarForm.php:23
+msgid "service var name"
+msgstr "サービス変数名"
+
+#: ../../../../modules/director/application/forms/IcingaServiceVarForm.php:28
+msgid "service var value"
+msgstr "サービス変数値"
+
+#: ../../../../modules/director/application/controllers/BasketController.php:302
+msgid "unchanged"
+msgstr "変更なし"
+
+#: ../../../../modules/director/application/locale/translateMe.php:12
+msgid "unknown"
+msgstr "未知"
+
+#: ../../../../modules/director/application/locale/translateMe.php:7
+msgid "unreachable"
+msgstr "到達不能"
+
+#: ../../../../modules/director/library/Director/Web/Widget/AdditionalTableActions.php:89
+msgid "unused"
+msgstr "不使用"
+
+#: ../../../../modules/director/application/locale/translateMe.php:5
+msgid "up"
+msgstr "起動"
+
+#: ../../../../modules/director/library/Director/Web/Widget/AdditionalTableActions.php:88
+msgid "used"
+msgstr "使用中"
+
+#: ../../../../modules/director/application/forms/IcingaHostVarForm.php:33
+#: ../../../../modules/director/application/forms/IcingaServiceVarForm.php:33
+msgid "value format"
+msgstr "値のフォーマット"
+
+#: ../../../../modules/director/library/Director/Web/Table/GroupMemberTable.php:61
+#: ../../../../modules/director/library/Director/Web/Table/GroupMemberTable.php:66
+msgid "via"
+msgstr "経由"
+
+#: ../../../../modules/director/application/locale/translateMe.php:10
+msgid "warning"
+msgstr "警告"
+
diff --git a/application/locale/translateMe.php b/application/locale/translateMe.php
new file mode 100644
index 0000000..d746a67
--- /dev/null
+++ b/application/locale/translateMe.php
@@ -0,0 +1,12 @@
+<?php
+
+// Dummy strings helping the translation module
+
+translate('up');
+translate('down');
+translate('unreachable');
+translate('pending');
+translate('ok');
+translate('warning');
+translate('critical');
+translate('unknown');
diff --git a/application/views/helpers/FormDataFilter.php b/application/views/helpers/FormDataFilter.php
new file mode 100644
index 0000000..d8bc508
--- /dev/null
+++ b/application/views/helpers/FormDataFilter.php
@@ -0,0 +1,564 @@
+<?php
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\FilterColumns;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\Element\Boolean;
+use Icinga\Module\Director\Web\Form\IconHelper;
+
+/**
+ * View helper for extensible sets
+ *
+ * Avoid complaints about class names:
+ * @codingStandardsIgnoreStart
+ */
+class Zend_View_Helper_FormDataFilter extends Zend_View_Helper_FormElement
+{
+ private $fieldName;
+
+ private $cachedColumnSelect;
+
+ private $query;
+
+ private $suggestionContext;
+
+ /**
+ * Generates an 'extensible set' element.
+ *
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string|array $name If a string, the element name. If an
+ * array, all other parameters are ignored, and the array elements
+ * are used in place of added parameters.
+ *
+ * @param mixed $value The element value.
+ *
+ * @param array $attribs Attributes for the element tag.
+ *
+ * @return string The element XHTML.
+ * @throws Zend_Form_Exception
+ */
+ public function formDataFilter($name, $value = null, $attribs = null)
+ {
+ $info = $this->_getInfo($name, $value, $attribs);
+ extract($info); // id, name, value, attribs, options, listsep, disable
+ if (array_key_exists('columns', $attribs)) {
+ $this->setColumns($attribs['columns']);
+ unset($attribs['columns']);
+ }
+
+ if (array_key_exists('suggestionContext', $attribs)) {
+ $this->setSuggestionContext($attribs['suggestionContext']);
+ unset($attribs['suggestionContext']);
+ }
+
+ // TODO: check for columns in attribs, preserve & remove them from the
+ // array use attribs? class etc? disabled?
+ // override _getInfo?
+ $this->fieldName = $name;
+
+ if ($value === null) {
+ $value = $this->emptyExpression();
+ } elseif (is_string($value)) {
+ $value = Filter::fromQueryString($value);
+ }
+
+ return $this->beginRoot()
+ . $this->renderFilter($value)
+ . $this->endRoot();
+ }
+
+ /**
+ * @param Filter $filter
+ * @return string
+ * @throws Zend_Form_Exception
+ */
+ protected function renderFilter(Filter $filter)
+ {
+ if ($filter instanceof FilterChain) {
+ return $this->renderFilterChain($filter);
+ } elseif ($filter instanceof FilterExpression) {
+ return $this->renderFilterExpression($filter);
+ } else {
+ throw new InvalidArgumentException('Got a Filter being neither expression nor chain');
+ }
+ }
+
+ protected function beginRoot()
+ {
+ return '<ul class="filter-root">';
+ }
+
+ protected function endRoot()
+ {
+ return '</ul>';
+ }
+
+ /**
+ * @param FilterChain $filter
+ * @return string
+ * @throws Zend_Form_Exception
+ */
+ protected function renderFilterChain(FilterChain $filter)
+ {
+ $parts = array();
+ foreach ($filter->filters() as $f) {
+ $parts[] = $this->renderFilter($f);
+ }
+
+ return $this->beginChain($filter)
+ . implode('', $parts)
+ . $this->endChain($filter);
+ }
+
+ protected function beginChain(FilterChain $filter)
+ {
+ $list = $filter->isEmpty() ? '' : '<ul>' . "\n";
+
+ return '<li class="filter-chain"><span class="handle"> </span>'
+ . $this->selectOperator($filter)
+ . $this->removeLink($filter)
+ . $this->addLink($filter)
+ . ($filter->count() === 1 ? $this->stripLink($filter) : '')
+ . $list;
+ }
+
+ protected function endChain(FilterChain $filter)
+ {
+ $list = $filter->isEmpty() ? '' : "</ul>\n";
+ return $list . "</li>\n";
+ }
+
+ protected function beginExpression(FilterExpression $filter)
+ {
+ return '<div class="filter-expression">' . "\n";
+ }
+
+ protected function endExpression(FilterExpression $filter)
+ {
+ return "</div>\n";
+ }
+
+ protected function beginElement(FilterExpression $filter)
+ {
+ return '<div class="expression-wrapper">' . "\n";
+ }
+
+ protected function endElement(FilterExpression $filter)
+ {
+ return "</div>\n";
+ }
+
+ /**
+ * @param FilterExpression $filter
+ * @return string
+ * @throws Zend_Form_Exception
+ */
+ protected function filterExpressionHtml(FilterExpression $filter)
+ {
+ return $this->selectColumn($filter)
+ . $this->selectSign($filter)
+ . $this->beginElement($filter)
+ . $this->element($filter)
+ . $this->endElement($filter)
+ . $this->removeLink($filter)
+ . $this->expandLink($filter);
+ }
+
+ /**
+ * @param FilterExpression $filter
+ * @return string
+ * @throws Zend_Form_Exception
+ */
+ protected function renderFilterExpression(FilterExpression $filter)
+ {
+ return $this->beginExpression($filter)
+ . $this->filterExpressionHtml($filter)
+ . $this->endExpression($filter);
+ }
+
+ /**
+ * @param FilterExpression|null $filter
+ * @return Boolean|string
+ * @throws Zend_Form_Exception
+ */
+ protected function element(FilterExpression $filter = null)
+ {
+ if ($filter) {
+ // TODO: Make this configurable
+ $type = 'host';
+ $prefixLen = strlen($type) + 1;
+ $filter = clone($filter);
+ $col = $filter->getColumn();
+
+ if ($this->columnIsJson($filter)) {
+ $col = $filter->getExpression();
+ $filter->setExpression(json_decode($filter->getColumn()));
+ } else {
+ $filter->setExpression(json_decode($filter->getExpression()));
+ }
+
+ if (($filter->getExpression() === true) || ($filter->getExpression() === false)) {
+ return '';
+ }
+ $dummy = IcingaObject::createByType($type);
+ if ($dummy->hasProperty($col)) {
+ if ($dummy->propertyIsBoolean($col)) {
+ return $this->boolean($filter);
+ }
+ }
+
+ if (substr($col, -7) === '.groups' && $dummy->supportsGroups()) {
+ $type = substr($col, 0, -7);
+
+ return $this->selectGroup($type, $filter);
+ } elseif (substr($col, $prefixLen, 5) === 'vars.') {
+ $var = substr($col, $prefixLen + 5);
+
+ return $this->text($filter, "DataListValues!${var}");
+ }
+ }
+
+ return $this->text($filter);
+ }
+
+ /**
+ * @param $type
+ * @param FilterExpression $filter
+ * @return Zend_Form_Element
+ */
+ protected function selectGroup($type, FilterExpression $filter)
+ {
+ return $this->view->formText(
+ $this->elementId('value', $filter),
+ $filter->getExpression(),
+ [
+ 'class' => 'director-suggest',
+ 'data-suggestion-context' => "${type}groupnames",
+ ]
+ );
+ }
+
+ /**
+ * @param FilterExpression|null $filter
+ * @return Boolean
+ * @throws Zend_Form_Exception
+ */
+ protected function boolean(FilterExpression $filter = null)
+ {
+ $value = $filter === null ? '' : $filter->getExpression();
+
+ $el = new Boolean(
+ $this->elementId('value', $filter),
+ array(
+ 'value' => $value,
+ 'decorators' => array('ViewHelper'),
+ )
+ );
+
+ return $el;
+ }
+
+ protected function columnIsJson(FilterExpression $filter)
+ {
+ $col = $filter->getColumn();
+ return strlen($col) && $col[0] === '"';
+ }
+
+ /**
+ * @param FilterExpression|null $filter
+ * @param string $suggestionContext
+ *
+ * @return mixed
+ */
+ protected function text(FilterExpression $filter = null, $suggestionContext = null)
+ {
+ $attr = null;
+ if ($suggestionContext !== null) {
+ $attr = [
+ 'class' => 'director-suggest',
+ 'data-suggestion-context' => $suggestionContext,
+ ];
+ }
+
+ $value = $filter === null ? '' : $filter->getExpression();
+ if (is_array($value)) {
+ return $this->view->formIplExtensibleSet(
+ $this->elementId('value', $filter),
+ $value,
+ $attr
+ );
+ }
+
+ return $this->view->formText(
+ $this->elementId('value', $filter),
+ $value,
+ $attr
+ );
+ }
+
+ /**
+ * @return \Icinga\Data\Filter\FilterExpression
+ */
+ protected function emptyExpression()
+ {
+ return Filter::expression('', '=', '');
+ }
+
+ protected function arrayForSelect($array, $flip = false)
+ {
+ $res = array();
+ foreach ($array as $k => $v) {
+ if (is_int($k)) {
+ $res[$v] = ucwords(str_replace('_', ' ', $v));
+ } elseif ($flip) {
+ $res[$v] = $k;
+ } else {
+ $res[$k] = $v;
+ }
+ }
+
+ // sort($res);
+ return $res;
+ }
+
+ protected function elementId($field, Filter $filter = null)
+ {
+ $prefix = $this->fieldName . '[id_';
+ $suffix = '][' . $field . ']';
+
+ return $prefix . $filter->getId() . $suffix;
+ }
+
+ /**
+ * @param FilterChain|null $filter
+ * @return mixed
+ */
+ protected function selectOperator(FilterChain $filter = null)
+ {
+ $ops = [
+ 'AND' => 'AND',
+ 'OR' => 'OR',
+ 'NOT' => 'NOT'
+ ];
+
+ return $this->select(
+ $this->elementId('operator', $filter),
+ $ops,
+ $filter === null ? null : $filter->getOperatorName(),
+ ['class' => 'operator autosubmit']
+ );
+ }
+
+ protected function selectSign(FilterExpression $filter = null)
+ {
+ $signs = [
+ '=' => '=',
+ '!=' => '!=',
+ '>' => '>',
+ '<' => '<',
+ '>=' => '>=',
+ '<=' => '<=',
+ 'in' => 'in',
+ 'contains' => 'contains',
+ 'true' => 'is true (or set)',
+ 'false' => 'is false (or not set)',
+ ];
+
+ if ($filter === null) {
+ $sign = null;
+ } else {
+ if ($this->columnIsJson($filter)) {
+ $sign = 'contains';
+ } else {
+ $expression = json_decode($filter->getExpression());
+ if ($expression === true) {
+ $sign = 'true';
+ } elseif ($expression === false) {
+ $sign = 'false';
+ } elseif (is_array($expression)) {
+ $sign = 'in';
+ } else {
+ $sign = $filter->getSign();
+ }
+ }
+ }
+
+ $class = 'sign autosubmit';
+ if (strlen($sign) > 3) {
+ $class .= ' wide';
+ }
+
+ return $this->select(
+ $this->elementId('sign', $filter),
+ $signs,
+ $sign,
+ array('class' => $class)
+ );
+ }
+
+ public function setColumns(array $columns = null)
+ {
+ $this->cachedColumnSelect = $columns ? $this->arrayForSelect($columns) : null;
+ return $this;
+ }
+
+ protected function getSuggestionContext()
+ {
+ return $this->suggestionContext;
+ }
+
+ protected function setSuggestionContext($context)
+ {
+ $this->suggestionContext = $context;
+ }
+
+ protected function selectColumn(FilterExpression $filter = null)
+ {
+ $active = $filter === null ? null : $filter->getColumn();
+ if ($filter && $this->columnIsJson($filter)) {
+ $active = $filter->getExpression();
+ }
+
+ if ($context = $this->getSuggestionContext()) {
+ return $this->view->formText(
+ $this->elementId('column', $filter),
+ $active,
+ [
+ 'class' => 'column autosubmit director-suggest',
+ 'data-suggestion-context' => $context,
+ ]
+ );
+ }
+ if ($this->hasColumnList()) {
+ $cols = $this->getColumnList();
+ if ($active && !isset($cols[$active])) {
+ $cols[$active] = str_replace(
+ '_',
+ ' ',
+ ucfirst(ltrim($active, '_'))
+ ); // ??
+ }
+
+ $cols = $this->optionalEnum($cols);
+
+ return $this->select(
+ $this->elementId('column', $filter),
+ $cols,
+ $active,
+ ['class' => 'column autosubmit']
+ );
+ } else {
+ return $this->view->formText(
+ $this->elementId('column', $filter),
+ $active,
+ ['class' => 'column autosubmit']
+ );
+ }
+ }
+
+ protected function optionalEnum($enum)
+ {
+ return array_merge(
+ array(null => $this->view->translate('- please choose -')),
+ $enum
+ );
+ }
+
+ protected function hasColumnList()
+ {
+ return $this->cachedColumnSelect !== null || $this->query !== null;
+ }
+
+ protected function getColumnList()
+ {
+ if ($this->cachedColumnSelect === null) {
+ $this->fetchColumnList();
+ }
+
+ return $this->cachedColumnSelect;
+ }
+
+ protected function fetchColumnList()
+ {
+ if ($this->query instanceof FilterColumns) {
+ $this->cachedColumnSelect = $this->arrayForSelect(
+ $this->query->getFilterColumns(),
+ true
+ );
+ asort($this->cachedColumnSelect);
+ } elseif ($this->cachedColumnSelect === null) {
+ throw new RuntimeException('No columns set nor does the query provide any');
+ }
+ }
+
+ protected function select($name, $list, $selected, $attributes = null)
+ {
+ return $this->view->formSelect($name, $selected, $attributes, $list);
+ }
+
+ protected function removeLink(Filter $filter)
+ {
+ return $this->filterActionButton(
+ $filter,
+ 'cancel',
+ t('Remove this part of your filter')
+ );
+ }
+
+ protected function addLink(Filter $filter)
+ {
+ return $this->filterActionButton(
+ $filter,
+ 'plus',
+ t('Add another filter')
+ );
+ }
+
+ protected function expandLink(Filter $filter)
+ {
+ return $this->filterActionButton(
+ $filter,
+ 'angle-double-right',
+ t('Wrap this expression into an operator')
+ );
+ }
+
+ protected function stripLink(Filter $filter)
+ {
+ return $this->filterActionButton(
+ $filter,
+ 'minus',
+ t('Strip this operator, preserve child nodes')
+ );
+ }
+
+ protected function filterActionButton(Filter $filter, $action, $title)
+ {
+ return $this->iconButton(
+ $this->getActionButtonName($filter),
+ $action,
+ $title
+ );
+ }
+
+ protected function getActionButtonName(Filter $filter)
+ {
+ return sprintf(
+ '%s[id_%s][action]',
+ $this->fieldName,
+ $filter->getId()
+ );
+ }
+
+ protected function iconButton($name, $icon, $title)
+ {
+ return $this->view->formSubmit(
+ $name,
+ IconHelper::instance()->iconCharacter($icon),
+ array('class' => 'icon-button', 'title' => $title)
+ );
+ }
+}
diff --git a/application/views/helpers/FormIplExtensibleSet.php b/application/views/helpers/FormIplExtensibleSet.php
new file mode 100644
index 0000000..c782016
--- /dev/null
+++ b/application/views/helpers/FormIplExtensibleSet.php
@@ -0,0 +1,23 @@
+<?php
+
+use Icinga\Module\Director\Web\Form\IplElement\ExtensibleSetElement;
+
+/**
+ * View helper for extensible sets
+ *
+ * @codingStandardsIgnoreStart
+ */
+class Zend_View_Helper_FormIplExtensibleSet extends Zend_View_Helper_FormElement
+{
+ private $currentId;
+
+ /**
+ * @codingStandardsIgnoreEnd
+
+ * @return string The element HTML.
+ */
+ public function formIplExtensibleSet($name, $value = null, $attribs = null)
+ {
+ return ExtensibleSetElement::fromZfDingens($name, $value, $attribs);
+ }
+}
diff --git a/application/views/helpers/FormSimpleNote.php b/application/views/helpers/FormSimpleNote.php
new file mode 100644
index 0000000..d8315f4
--- /dev/null
+++ b/application/views/helpers/FormSimpleNote.php
@@ -0,0 +1,15 @@
+<?php
+
+// Avoid complaints about missing namespace and invalid class name
+// @codingStandardsIgnoreStart
+class Zend_View_Helper_FormSimpleNote extends Zend_View_Helper_FormElement
+{
+ // @codingStandardsIgnoreEnd
+
+ public function formSimpleNote($name, $value = null)
+ {
+ $info = $this->_getInfo($name, $value);
+ extract($info); // name, value, attribs, options, listsep, disable
+ return $value;
+ }
+}
diff --git a/application/views/helpers/FormStoredPassword.php b/application/views/helpers/FormStoredPassword.php
new file mode 100644
index 0000000..e25a1dc
--- /dev/null
+++ b/application/views/helpers/FormStoredPassword.php
@@ -0,0 +1,60 @@
+<?php
+
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+
+/**
+ * Please see StoredPassword (the Form Element) for related documentation
+ *
+ * We're rendering the following fields:
+ *
+ * - ${name}[_value]:
+ * - ${name}[_sent]:
+ *
+ * Avoid complaints about class names:
+ * @codingStandardsIgnoreStart
+ */
+class Zend_View_Helper_FormStoredPassword extends Zend_View_Helper_FormElement
+{
+ public function formStoredPassword($name, $value = null, $attribs = null)
+ {
+ // @codingStandardsIgnoreEnd
+ $info = $this->_getInfo($name, $value, $attribs);
+ \extract($info); // name, value, attribs, options, listsep, disable
+ $sentValue = $this->stripAttribute($attribs, 'sentValue');
+
+ $res = new HtmlDocument();
+ $el = Html::tag('input', [
+ 'type' => 'password',
+ 'name' => "${name}[_value]",
+ 'id' => $id,
+ ]);
+ $res->add($el);
+
+ $res->add(Html::tag('input', [
+ 'type' => 'hidden',
+ 'name' => "${name}[_sent]",
+ 'value' => 'y'
+ ]));
+
+ if ($sentValue !== null && \strlen($sentValue)) {
+ $el->getAttributes()->set('value', $sentValue);
+ } elseif ($value !== null && \strlen($value) > 0) {
+ $el->getAttributes()->set('value', '__UNCHANGED_VALUE__');
+ }
+
+ return $res;
+ }
+
+ protected function stripAttribute(&$attribs, $name, $default = null)
+ {
+ if (\array_key_exists($name, $attribs)) {
+ if (\strlen($attribs[$name])) {
+ return $attribs[$name];
+ }
+ unset($attribs[$name]);
+ }
+
+ return $default;
+ }
+}
diff --git a/application/views/helpers/RenderPlainObject.php b/application/views/helpers/RenderPlainObject.php
new file mode 100644
index 0000000..8486611
--- /dev/null
+++ b/application/views/helpers/RenderPlainObject.php
@@ -0,0 +1,14 @@
+<?php
+// Avoid complaints about missing namespace and invalid class name
+// @codingStandardsIgnoreStart
+
+use Icinga\Module\Director\PlainObjectRenderer;
+
+class Zend_View_Helper_RenderPlainObject extends Zend_View_Helper_Abstract
+// @codingStandardsIgnoreEnd
+{
+ public function renderPlainObject($object)
+ {
+ return PlainObjectRenderer::render($object);
+ }
+}
diff --git a/application/views/scripts/phperror/dependencies.phtml b/application/views/scripts/phperror/dependencies.phtml
new file mode 100644
index 0000000..1cbf31e
--- /dev/null
+++ b/application/views/scripts/phperror/dependencies.phtml
@@ -0,0 +1,9 @@
+<div class="controls">
+<?= $this->tabs ?>
+<h1><?= $this->escape($this->title) ?></h1>
+</div>
+
+<div class="content">
+<p class="legacy-error"><?= $this->escape($this->message) ?></p>
+<?= $this->table ?>
+</div>
diff --git a/application/views/scripts/phperror/error.phtml b/application/views/scripts/phperror/error.phtml
new file mode 100644
index 0000000..260bf72
--- /dev/null
+++ b/application/views/scripts/phperror/error.phtml
@@ -0,0 +1,8 @@
+<div class="controls">
+<?= $this->tabs ?>
+<h1><?= $this->escape($this->title) ?></h1>
+</div>
+
+<div class="content">
+<p class="legacy-error"><?= $this->escape($this->message) ?></p>
+</div>
diff --git a/application/views/scripts/settings/index.phtml b/application/views/scripts/settings/index.phtml
new file mode 100644
index 0000000..9120812
--- /dev/null
+++ b/application/views/scripts/settings/index.phtml
@@ -0,0 +1,7 @@
+<div class="controls">
+<?= $this->tabs ?>
+</div>
+
+<div class="content">
+ <?= $form ?>
+</div>
diff --git a/application/views/scripts/suggest/index.phtml b/application/views/scripts/suggest/index.phtml
new file mode 100644
index 0000000..5f804e1
--- /dev/null
+++ b/application/views/scripts/suggest/index.phtml
@@ -0,0 +1,3 @@
+<?php foreach ($suggestions as $suggest): ?>
+<li><?= $suggest ?></li>
+<?php endforeach ?>
diff --git a/configuration.php b/configuration.php
new file mode 100644
index 0000000..4536d5d
--- /dev/null
+++ b/configuration.php
@@ -0,0 +1,181 @@
+<?php
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Window;
+
+/** @var \Icinga\Application\Modules\Module $this */
+if ($this->getConfig()->get('frontend', 'disabled', 'no') === 'yes') {
+ return;
+}
+$this->providePermission('director/api', $this->translate('Allow to access the director API'));
+$this->providePermission('director/audit', $this->translate('Allow to access the full audit log'));
+$this->providePermission(
+ 'director/showconfig',
+ $this->translate('Allow to show configuration (could contain sensitive information)')
+);
+$this->providePermission(
+ 'director/showsql',
+ $this->translate('Allow to show the full executed SQL queries in some places')
+);
+$this->providePermission('director/deploy', $this->translate('Allow to deploy configuration'));
+$this->providePermission('director/hosts', $this->translate('Allow to configure hosts'));
+$this->providePermission('director/services', $this->translate('Allow to configure services'));
+$this->providePermission('director/servicesets', $this->translate('Allow to configure service sets'));
+$this->providePermission('director/service_set/apply', $this->translate('Allow to define Service Set Apply Rules'));
+$this->providePermission('director/users', $this->translate('Allow to configure users'));
+$this->providePermission('director/notifications', $this->translate('Allow to configure notifications (unrestricted)'));
+$this->providePermission(
+ 'director/scheduled-downtimes',
+ $this->translate('Allow to configure notifications (unrestricted)')
+);
+$this->providePermission(
+ 'director/inspect',
+ $this->translate(
+ 'Allow to inspect objects through the Icinga 2 API (could contain sensitive information)'
+ )
+);
+$this->providePermission(
+ 'director/monitoring/services-ro',
+ $this->translate('Allow readonly users to see where a Service came from')
+);
+$this->providePermission(
+ 'director/monitoring/hosts',
+ $this->translate('Allow users to modify Hosts they are allowed to see in the monitoring module')
+);
+$this->providePermission(
+ 'director/monitoring/services',
+ $this->translate('Allow users to modify Service they are allowed to see in the monitoring module')
+);
+$this->providePermission('director/*', $this->translate('Allow unrestricted access to Icinga Director'));
+
+$this->provideRestriction(
+ 'director/filter/hostgroups',
+ $this->translate(
+ 'Limit access to the given comma-separated list of hostgroups'
+ )
+);
+
+$this->provideRestriction(
+ 'director/monitoring/rw-object-filter',
+ $this->translate(
+ 'Additional (monitoring module) object filter to further restrict write access'
+ )
+);
+
+$this->providePermission(
+ 'director/groups-for-restricted-hosts',
+ $this->translate('Allow users with Hostgroup restrictions to access the Groups field')
+);
+
+$this->provideRestriction(
+ 'director/service/apply/filter-by-name',
+ $this->translate(
+ 'Filter available service apply rules'
+ )
+);
+
+$this->provideRestriction(
+ 'director/notification/apply/filter-by-name',
+ $this->translate(
+ 'Filter available notification apply rules'
+ )
+);
+
+$this->provideRestriction(
+ 'director/scheduled-downtime/apply/filter-by-name',
+ $this->translate(
+ 'Filter available scheduled downtime rules'
+ )
+);
+
+$this->provideRestriction(
+ 'director/service_set/filter-by-name',
+ $this->translate(
+ 'Filter available service set templates. Use asterisks (*) as wildcards,'
+ . ' like in DB* or *net*'
+ )
+);
+
+$this->provideSearchUrl($this->translate('Host configs'), 'director/hosts?limit=10', 60);
+
+/*
+// Disabled unless available
+$this->provideRestriction(
+ 'director/hosttemplates/filter',
+ $this->translate('Allow to use only host templates matching this filter')
+);
+
+$this->provideRestriction(
+ 'director/db_resource',
+ $this->translate('Allow to use only these db resources (comma separated list)')
+);
+*/
+
+$this->provideConfigTab('config', array(
+ 'title' => 'Configuration',
+ 'url' => 'settings'
+));
+$mainTitle = N_('Icinga Director');
+
+try {
+ $app = Icinga::app();
+ if ($app->isWeb()) {
+ $request = $app->getRequest();
+ $id = $request->getHeader('X-Icinga-WindowId');
+ if ($id !== false) {
+ $window = new Window($id);
+ /** @var \Icinga\Web\Session\SessionNamespace $session */
+ $session = $window->getSessionNamespace('director');
+ $dbName = $session->get('db_resource');
+ if ($dbName && $dbName !== $this->getConfig()->get('db', 'resource')) {
+ $dbName = ucfirst(str_replace('_', ' ', $dbName));
+ if (stripos($dbName, 'Director') === false) {
+ $dbName = 'Director: ' . $dbName;
+ }
+ $mainTitle = $dbName;
+ }
+ }
+ }
+} catch (\Exception $e) {
+ // There isn't much we can do, we don't want to break the menu
+ $mainTitle .= ' (?!)';
+}
+
+$section = $this->menuSection(
+ $mainTitle
+)->setUrl('director')->setPriority(60)->setIcon(
+ 'cubes'
+)->setRenderer(array(
+ 'SummaryNavigationItemRenderer',
+ 'state' => 'critical'
+));
+
+$section->add(N_('Hosts'))
+ ->setUrl('director/dashboard?name=hosts')
+ ->setPermission('director/hosts')
+ ->setPriority(30);
+$section->add(N_('Services'))
+ ->setUrl('director/dashboard?name=services')
+ ->setPermission('director/services')
+ ->setPriority(40);
+$section->add(N_('Commands'))
+ ->setUrl('director/dashboard?name=commands')
+ ->setPermission('director/admin')
+ ->setPriority(50);
+$section->add(N_('Notifications'))
+ ->setUrl('director/dashboard?name=notifications')
+ ->setPermission('director/notifications')
+ ->setPriority(70);
+$section->add(N_('Automation'))
+ ->setUrl('director/importsources')
+ ->setPermission('director/admin')
+ ->setPriority(901);
+$section->add(N_('Activity log'))
+ ->setUrl('director/config/activities')
+ ->setPriority(902)
+ ->setPermission('director/audit')
+ ->setRenderer('ConfigHealthItemRenderer');
+$section->add(N_('Deployments'))
+ ->setUrl('director/config/deployments')
+ ->setPriority(902)
+ ->setPermission('director/deployments');
diff --git a/contrib/docker-test.sh b/contrib/docker-test.sh
new file mode 100755
index 0000000..234ff95
--- /dev/null
+++ b/contrib/docker-test.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+
+MYSQL_CONTAINER=icingaweb2_director_mysql
+
+echo "Starting MySQL container..."
+docker run -d \
+ -e MYSQL_ROOT_PASSWORD=onlyforadmin \
+ -e MYSQL_DATABASE=icingaweb2 \
+ -e MYSQL_USER=icingaweb2 \
+ -e MYSQL_PASSWORD=rosebud \
+ --name "$MYSQL_CONTAINER" \
+ mariadb >/dev/null
+
+echo "Running tests..."
+docker run --rm -i \
+ --link "$MYSQL_CONTAINER":mysql \
+ -v `pwd`:/app \
+ -e DIRECTOR_TESTDB_RES="Director MySQL TestDB" \
+ -e DIRECTOR_TESTDB_HOST="mysql" \
+ -e DIRECTOR_TESTDB_USER="icingaweb2" \
+ -e DIRECTOR_TESTDB_PASSWORD="rosebud" \
+ -e DIRECTOR_TESTDB="icingaweb2" \
+ lazyfrosch/icingaweb2-test:5.6 \
+ sh - \
+<<EOF
+ set -ex
+ cd /app
+ composer install
+ {
+ set +x
+ count=0
+ while ! nc -w 1 -z mysql 3306
+ do
+ echo "Waiting for MySQL to get ready..."
+ sleep 2
+ : \$((count++))
+ if [ \$count -gt 10 ]; then
+ echo "Waiting for MySQL timed out!"
+ exit 2
+ fi
+ done
+ }
+ ret=0
+ composer exec phpunit --verbose || ret=\$?
+ #composer exec phpcs application/ library/ test/php/ *.php || ret=\$?
+ exit \$ret
+EOF
+
+echo "Stopping MySQL container..."
+docker kill "$MYSQL_CONTAINER"
+docker rm "$MYSQL_CONTAINER"
diff --git a/contrib/linux-agent-installer/Icinga2Agent.bash b/contrib/linux-agent-installer/Icinga2Agent.bash
new file mode 100644
index 0000000..546ed0e
--- /dev/null
+++ b/contrib/linux-agent-installer/Icinga2Agent.bash
@@ -0,0 +1,318 @@
+#!/bin/bash
+
+# This generates and signs your required certificates. Please do not
+# forget to install the Icinga 2 package and your desired monitoring
+# plugins first.
+
+# Config from Director
+#ICINGA2_NODENAME='@ICINGA2_NODENAME@'
+#ICINGA2_CA_TICKET='@ICINGA2_CA_TICKET@'
+#ICINGA2_PARENT_ZONE='@ICINGA2_PARENT_ZONE@'
+#ICINGA2_PARENT_ENDPOINTS='@ICINGA2_PARENT_ENDPOINTS@'
+#ICINGA2_CA_NODE='@ICINGA2_CA_NODE@'
+#ICINGA2_GLOBAL_ZONES='@ICINGA2_GLOBAL_ZONES@'
+
+# Internal defaults
+: "${ICINGA2_OSFAMILY:=}"
+: "${ICINGA2_HOSTNAME:="$(hostname -f)"}"
+: "${ICINGA2_NODENAME:="${ICINGA2_HOSTNAME}"}"
+: "${ICINGA2_CA_NODE:=}"
+: "${ICINGA2_CA_PORT:=5665}"
+: "${ICINGA2_CA_TICKET:=}"
+: "${ICINGA2_PARENT_ZONE:=master}"
+: "${ICINGA2_PARENT_ENDPOINTS:=()}"
+: "${ICINGA2_GLOBAL_ZONES:=director-global}"
+: "${ICINGA2_DRYRUN:=}"
+: "${ICINGA2_UPDATE_CONFIG:=}"
+
+# Helper functions
+fail() {
+ echo "ERROR: $1" >&2
+ exit 1
+}
+
+warn() {
+ echo "WARNING: $1" >&2
+}
+
+info() {
+ echo "INFO: $1" >&2
+}
+
+check_command() {
+ command -v "$@" &>/dev/null
+}
+
+install_config() {
+ if [ -e "$1" ] && [ ! -e "${1}.orig" ]; then
+ info "Creating a backup at ${1}.orig"
+ cp "$1" "${1}.orig"
+ fi
+ echo "Writing config to ${1}"
+ echo "$2" > "${1}"
+}
+
+[ "$BASH_VERSION" ] || fail "This is a Bash script"
+
+errors=
+for key in NODENAME CA_NODE CA_PORT CA_TICKET PARENT_ZONE PARENT_ENDPOINTS; do
+ var="ICINGA2_${key}"
+ if [ -z "${!var}" ]; then
+ warn "The variable $var needs to be configured!"
+ errors+=1
+ fi
+done
+[ -z "$errors" ] || exit 1
+
+# Detect osfamily
+if [ -n "$ICINGA2_OSFAMILY" ]; then
+ info "Assuming supplied osfamily $ICINGA2_OSFAMILY"
+elif check_command rpm && ! check_command dpkg; then
+ info "This should be a RedHat system"
+ if [ -e /etc/sysconfig/icinga2 ]; then
+ # shellcheck disable=SC1091
+ . /etc/sysconfig/icinga2
+ fi
+ ICINGA2_OSFAMILY=redhat
+elif check_command dpkg; then
+ info "This should be a Debian system"
+ if [ -e /etc/default/icinga2 ]; then
+ # shellcheck disable=SC1091
+ . /etc/default/icinga2
+ fi
+ ICINGA2_OSFAMILY=debian
+elif check_command apk; then
+ info "This should be a Alpine system"
+ if [ -e /etc/icinga2/icinga2.sysconfig ]; then
+ # shellcheck disable=SC1091
+ . /etc/icinga2/icinga2.sysconfig
+ fi
+ ICINGA2_OSFAMILY=alpine
+else
+ fail "Could not determine your os type!"
+fi
+
+# internal defaults
+: "${ICINGA2_CONFIG_FILE:=/etc/icinga2/icinga2.conf}"
+: "${ICINGA2_CONFIGDIR:="$(dirname "$ICINGA2_CONFIG_FILE")"}"
+: "${ICINGA2_DATADIR:=/var/lib/icinga2}"
+: "${ICINGA2_SSLDIR_OLD:="${ICINGA2_CONFIGDIR}"/pki}"
+: "${ICINGA2_SSLDIR_NEW:="${ICINGA2_DATADIR}"/certs}"
+: "${ICINGA2_SSLDIR:=}"
+: "${ICINGA2_BIN:=icinga2}"
+
+case "$ICINGA2_OSFAMILY" in
+debian)
+ : "${ICINGA2_USER:=nagios}"
+ : "${ICINGA2_GROUP:=nagios}"
+ ;;
+redhat)
+ : "${ICINGA2_USER:=icinga}"
+ : "${ICINGA2_GROUP:=icinga}"
+ ;;
+alpine)
+ : "${ICINGA2_USER:=icinga}"
+ : "${ICINGA2_GROUP:=icinga}"
+ ;;
+*)
+ fail "Unknown osfamily '$ICINGA2_OSFAMILY'!"
+ ;;
+esac
+
+icinga_version() {
+ "$ICINGA2_BIN" --version 2>/dev/null | grep -oPi '\(version: [rv]?\K\d+\.\d+\.\d+[^\)]*'
+}
+
+version() {
+ echo "$@" | awk -F. '{ printf("%03d%03d%03d\n", $1,$2,$3); }'
+}
+
+# Make sure icinga2 is installed and running
+echo -n "check: icinga2 installed - "
+if version=$(icinga_version); then
+ echo "OK: $version"
+else
+ fail "You need to install icinga2!"
+fi
+
+if [ -z "${ICINGA2_SSLDIR}" ]; then
+ if [ -f "${ICINGA2_SSLDIR_OLD}/${ICINGA2_NODENAME}.crt" ]; then
+ info "Using old SSL directory: ${ICINGA2_SSLDIR_OLD}"
+ info "Because you already have a certificate in ${ICINGA2_SSLDIR_OLD}/${ICINGA2_NODENAME}.crt"
+ ICINGA2_SSLDIR="${ICINGA2_SSLDIR_OLD}"
+ elif [ $(version $version) -gt $(version 2.8) ]; then
+ info "Using new SSL directory: ${ICINGA2_SSLDIR_NEW}"
+ ICINGA2_SSLDIR="${ICINGA2_SSLDIR_NEW}"
+ else
+ info "Using old SSL directory: ${ICINGA2_SSLDIR_OLD}"
+ ICINGA2_SSLDIR="${ICINGA2_SSLDIR_OLD}"
+ fi
+fi
+
+if [ ! -d "$ICINGA2_SSLDIR" ]; then
+ mkdir "$ICINGA2_SSLDIR"
+ chown "$ICINGA2_USER.$ICINGA2_GROUP" "$ICINGA2_SSLDIR"
+fi
+
+if [ -f "${ICINGA2_SSLDIR}/${ICINGA2_NODENAME}.crt" ]; then
+ warn "ERROR: a certificate for '${ICINGA2_NODENAME}' already exists"
+ warn "Please remove ${ICINGA2_SSLDIR}/${ICINGA2_NODENAME}.??? in case you want a"
+ warn "new certificate to be generated and signed by ${ICINGA2_CA_NODE}"
+
+ if [ -z "${ICINGA2_UPDATE_CONFIG}" ] && [ -z "${ICINGA2_DRYRUN}" ]; then
+ warn "Aborting here, you can can call the script like this to just update config:"
+ info " ICINGA2_UPDATE_CONFIG=1 $0"
+ exit 1
+ fi
+elif [ -z "${ICINGA2_DRYRUN}" ]; then
+ if ! "$ICINGA2_BIN" pki new-cert --cn "${ICINGA2_NODENAME}" \
+ --cert "${ICINGA2_SSLDIR}/${ICINGA2_NODENAME}.crt" \
+ --csr "${ICINGA2_SSLDIR}/${ICINGA2_NODENAME}.csr" \
+ --key "${ICINGA2_SSLDIR}/${ICINGA2_NODENAME}.key"
+ then fail "Could not create self signed certificate!"
+ fi
+
+ if ! "$ICINGA2_BIN" pki save-cert \
+ --host "${ICINGA2_CA_NODE}" \
+ --port "${ICINGA2_CA_PORT}" \
+ --key "${ICINGA2_SSLDIR}/${ICINGA2_NODENAME}.key" \
+ --trustedcert "${ICINGA2_SSLDIR}/trusted-master.crt"
+ then fail "Could not retrieve trusted certificate from host ${ICINGA2_CA_NODE}"
+ fi
+
+ if ! "$ICINGA2_BIN" pki request \
+ --host "${ICINGA2_CA_NODE}" \
+ --port "${ICINGA2_CA_PORT}" \
+ --ticket "${ICINGA2_CA_TICKET}" \
+ --key "${ICINGA2_SSLDIR}/${ICINGA2_NODENAME}.key" \
+ --cert "${ICINGA2_SSLDIR}/${ICINGA2_NODENAME}.crt" \
+ --trustedcert "${ICINGA2_SSLDIR}/trusted-master.crt" \
+ --ca "${ICINGA2_SSLDIR}/ca.crt"
+ then fail "Could not retrieve final certificate from host ${ICINGA2_CA_NODE}"
+ fi
+else
+ info "Would create certificates under ${ICINGA2_SSLDIR}, but in dry-run!"
+fi
+
+# Prepare Config Files
+content_config=$(cat << EOF
+/** Icinga 2 Config - proposed by Icinga Director */
+
+include "constants.conf"
+
+$([ "${ICINGA2_HOSTNAME}" != "${ICINGA2_NODENAME}" ] || echo '// ')const NodeName = "${ICINGA2_NODENAME}"
+
+include "zones.conf"
+include "features-enabled/*.conf"
+
+include <itl>
+include <plugins>
+include <plugins-contrib>
+include <manubulon>
+include <windows-plugins>
+include <nscp>
+EOF
+)
+
+endpoint_list=''
+for item in "${ICINGA2_PARENT_ENDPOINTS[@]}"; do
+ endpoint=$(echo "$item" | cut -d, -f1)
+ endpoint_list+="\"${endpoint}\", "
+done
+
+content_zones=$(cat << EOF
+/** Icinga 2 Config - proposed by Icinga Director */
+
+object Endpoint "${ICINGA2_NODENAME}" {}
+
+object Zone "${ICINGA2_NODENAME}" {
+ parent = "${ICINGA2_PARENT_ZONE}"
+ endpoints = [ "${ICINGA2_NODENAME}" ]
+}
+
+object Zone "${ICINGA2_PARENT_ZONE}" {
+ endpoints = [ ${endpoint_list%, } ]
+}
+EOF
+)
+
+for item in "${ICINGA2_PARENT_ENDPOINTS[@]}"; do
+ endpoint=$(echo "$item" | cut -d, -f1)
+ host=$(echo "$item" | cut -s -d, -f2)
+
+ content_zones+=$(cat << EOF
+
+object Endpoint "${endpoint}" {
+$([ -n "$host" ] && echo " host = \"${host}\"" || echo " //host = \"${endpoint}\"")
+}
+EOF
+)
+done
+
+for zone in "${ICINGA2_GLOBAL_ZONES[@]}"; do
+ content_zones+=$(cat << EOF
+
+object Zone "${zone}" {
+ global = true
+}
+EOF
+)
+done
+
+content_api="/** Icinga 2 Config - proposed by Icinga Director */
+
+object ApiListener \"api\" {"
+
+if [ "${ICINGA2_SSLDIR}" = "${ICINGA2_SSLDIR_OLD}" ]; then
+content_api+="
+ cert_path = SysconfDir + \"/icinga2/pki/${ICINGA2_NODENAME}.crt\"
+ key_path = SysconfDir + \"/icinga2/pki/${ICINGA2_NODENAME}.key\"
+ ca_path = SysconfDir + \"/icinga2/pki/ca.crt\"
+"
+fi
+content_api+="
+ accept_commands = true
+ accept_config = true
+}
+"
+
+if [ -z "${ICINGA2_DRYRUN}" ]; then
+ install_config "$ICINGA2_CONFIGDIR"/icinga2.conf "$content_config"
+ install_config "$ICINGA2_CONFIGDIR"/zones.conf "$content_zones"
+ install_config "$ICINGA2_CONFIGDIR"/features-available/api.conf "$content_api"
+
+ "$ICINGA2_BIN" feature enable api
+
+ "$ICINGA2_BIN" daemon -C
+
+ echo "Please restart icinga2:"
+ case "$ICINGA2_OSFAMILY" in
+ debian)
+ echo " systemctl restart icinga2"
+ ;;
+ redhat)
+ echo " systemctl restart icinga2"
+ ;;
+ alpine)
+ echo " rc-service icinga2 restart"
+ ;;
+ *)
+ fail "Unknown osfamily '$ICINGA2_OSFAMILY'!"
+ ;;
+ esac
+else
+ output_code() {
+ sed 's/^/ /m' <<<"$1"
+ }
+ echo "### $ICINGA2_CONFIGDIR"/icinga2.conf
+ echo
+ output_code "$content_config"
+ echo
+ echo "### $ICINGA2_CONFIGDIR"/zones.conf
+ echo
+ output_code "$content_zones"
+ echo
+ echo "### $ICINGA2_CONFIGDIR"/features-available/api.conf
+ echo
+ output_code "$content_api"
+fi
diff --git a/contrib/systemd/icinga-director.service b/contrib/systemd/icinga-director.service
new file mode 100644
index 0000000..f96f1d7
--- /dev/null
+++ b/contrib/systemd/icinga-director.service
@@ -0,0 +1,21 @@
+[Unit]
+Description=Icinga Director - Monitoring Configuration
+Documentation=https://icinga.com/docs/director/latest/
+Wants=network.target
+
+[Service]
+EnvironmentFile=-/etc/default/icinga-director
+EnvironmentFile=-/etc/sysconfig/icinga-director
+ExecStart=/usr/bin/icingacli director daemon run
+ExecReload=/bin/kill -HUP ${MAINPID}
+User=icingadirector
+SyslogIdentifier=icingadirector
+Type=notify
+
+NotifyAccess=main
+WatchdogSec=10
+RestartSec=30
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/contrib/windows-agent-installer/Icinga2Agent.psm1 b/contrib/windows-agent-installer/Icinga2Agent.psm1
new file mode 100644
index 0000000..468ddb2
--- /dev/null
+++ b/contrib/windows-agent-installer/Icinga2Agent.psm1
@@ -0,0 +1,3402 @@
+<#
+.Synopsis
+ Icinga 2 PowerShell Module - the most flexible and easy way to configure and install Icinga 2 Agents on Windows.
+.DESCRIPTION
+ More Information on https://github.com/Icinga/icinga2-powershell-module
+.EXAMPLE
+ exit $icinga = Icinga2AgentModule `
+ -AgentName 'windows-host-name' `
+ -Ticket '3459843583450834508634856383459' `
+ -ParentZone 'icinga-master' `
+ -ParentEndpoints 'icinga2a', 'icinga2b' `
+ -CAServer 'icinga-master' `
+ -RunInstaller;
+ .NOTES
+
+#>
+function Icinga2AgentModule {
+
+ #
+ # Setup parameters which can be accessed
+ # with -<ParamName>
+ #
+ [CmdletBinding()]
+ param(
+
+ # This is in general the name of your Windows host. It will have to match with your Icinga configuration, as it is part of the Icinga 2 Ticket and Certificate handling to ensure a valid certificate is generated
+ [string]$AgentName,
+ # The Ticket you will receive from your Icinga 2 CA. In combination with the Icinga Director, it will tell you which Ticket you will require for your host
+ [string]$Ticket,
+ # You can either leave this parameter or add it to allow the module to install or update the Icinga 2 Agent on your system
+ [string]$InstallAgentVersion,
+ # Instead of setting the Agent Name with -AgentName, the PowerShell module is capable of retreiving the information automaticly from Windows. Please note this is not the FQDN
+ [switch]$FetchAgentName = $FALSE,
+ # Like -FetchAgentName, this argument will ensure the hostname is set inside the script, will however include the domain to provide the FQDN internally.
+ [switch]$FetchAgentFQDN = $FALSE,
+ # Allows to transform the hostname to either lower or upper case if required. 0: Do nothing 1: To lower case 2: To upper case
+ [int]$TransformHostname = -1,
+
+ # This variable allows to specify on which port the Icinga 2 Agent will listen on
+ [int]$AgentListenPort = -1,
+ # Each Icinga 2 Agent is in general forwarding it's check results to a parent master or satellite zone. Here you will have to specify the name of the parent zone
+ [string]$ParentZone,#
+ # Icinga 2 internals to make it configurable if the Agent is accepting configuration from the Icinga config master.
+ [int]$AcceptConfig = -1,
+ # This argument will define if the Icinga 2 debug log will be enabled or disabled.
+ [switch]$IcingaEnableDebugLog = $FALSE,
+ # This argument will define if we enable or disable the Icinga 2 logging feature
+ [switch]$IcingaDisableLogging = $FALSE,
+ # Allows to specify if the PowerShell Module will add a firewall rule, allowing Icinga 2 masters or Satellites to connect to the Icinga 2 Agent on the defined port
+ [switch]$AgentAddFirewallRule = $FALSE,
+ # This parameter requires an array of string values, to which endpoints the Agent should in general connect to. If you are only having one endpoint, only add one. You will have to specify all endpoints the Agent requires to connect to
+ [array]$ParentEndpoints,
+ # While -ParentEndpoints will define the name of endpoints by an array, this parameter will allow to assign IP address and port configuration, allowing the Icinga 2 Agent to directly connect to parent Icinga 2 instances. To specify IP address and port, you will have to seperate these entries by using ';' without blank spaces. The order of the config has to match the assignment of -ParentEndpoints. You can specify the IP address only without a port definition by just leaving the last part. If you wish to not specify a config for a specific endpoint, simply add an empty string to the correct location.
+ [array]$EndpointsConfig,
+ # Allows to specify global zones, which will be added into the icinga2.conf. Note: In case no global zone will be defined, director-global will be added by default. If you specify zones by yourself, please ensure to add director-global as this is not done automaticly when adding custom global-zones.
+ [array]$GlobalZones = @(),
+
+
+ # Agent installation / update
+ <# This argument will allow to override the user the Icinga 2 service is running with. Windows provides some basic users already which can be configured:
+
+ LocalSystem
+ NT AUTHORITY\NetworkService (Icinga default)
+ NT AUTHORITY\LocalService
+ If you require an own user, you can add that one as well for the argument. If a password is required for the user to login, seperate username and password with a ':'.
+
+ Example: jdoe:mysecretpassword
+
+ Furthermore you can also use domains in combination and pass them over.
+
+ Example: icinga\jdoe:mysecretpassword[string]$IcingaServiceUser,
+ #>
+ [string]$IcingaServiceUser,
+ #With this parameter you can define a download Url or local directory from which the module will download/install a specific Icinga 2 Agent MSI Installer package. Please ensure to only define the base download Url / Directory, as the Module will generate the MSI file name based on your operating system architecture and the version to install. The Icinga 2 MSI Installer name is internally build as follows: Icinga2-v[InstallAgentVersion]-[OSArchitecture].msi
+
+ # Full example: Icinga2-v2.8.0-x86_64.msi
+ [string]$DownloadUrl,
+ # Allows to specify in which directory the Icinga 2 Agent will be installed into. In case of an Agent update you can specify with this argument a new directory the new Agent will be installed into. The old directory will be removed caused by the required uninstaller process.
+ [string]$AgentInstallDirectory,
+ # In case the Icinga 2 Agent is already installed on the system, this parameter will allow you to configure if you wish to upgrade / downgrade to a specified version with the -InstallAgentVersion parameter as well. If none of both parameters is defined, the module will not update or downgrade the agent.
+ # If argument -AgentInstallDirectory is not specified, the Icinga 2 Agent will be installed into the same directory as before. In case defined, the PowerShell Module will use the new directory as installation target.
+ [switch]$AllowUpdates = $FALSE,
+ # To ensure downloaded packages are build by the Icinga Team and not compromised by third parties, you will be able to provide an array of SHA1 hashes here. In case you have defined any hashses, the module will not continue with updating / installing the Agent in case the SHA1 hash of the downloaded MSI package is not matching one of the provided hashes of this parameter.
+ [array]$InstallerHashes,
+ # In case the Icinga Agent will accept configuration from the parent Icinga 2 system, it will possibly write data to /var/lib/icinga2/api/* By adding this parameter to your script call, all content inside the api directory will be flushed once a change is detected by the module which requires a restart of the Icinga 2 Agent
+ [switch]$FlushApiDirectory = $FALSE,
+
+ # Here you can provide a string to the Icinga 2 CA or any other CA responsible to generate the required certificates for the SSL communication between the Icinga 2 Agent and it's parent
+ [string]$CAServer,
+ # TODO
+ [string]$CACertificatePath,
+ # Here you can specify a custom port in case your CA Server is not listening on 5665
+ [int]$CAPort = 5665,
+ # The module will generate the certificates in general only if one of the required files is missing. By adding this parameter to your call, the module will force the re-creation of the certificates.
+ [switch]$ForceCertificateGeneration = $FALSE,
+ # This option will allow the validation of the trusted-master.crt generated during certificate generation, to ensure we are connected to the correct endpoint to prevent possible man-in-the-middle attacks.
+ [string]$CAFingerprint,
+ # Use this switch to enable the CAProxy feature Introduced with Icinga 2.8
+ [switch]$CAProxy = $FALSE,
+
+ # Director communication
+ #This argument will tell the PowerShell where the Icinga Director can be found. Please specify the entire path to the Icinga Director! Example: https://example.com/icingaweb2/director/
+ [string]$DirectorUrl,
+ #To fetch the Ticket for a host, creating host objects or deploying the configuration you will have to authenticate against the Icinga Director. This parameter allows to set the User we shall use to login.
+ [string]$DirectorUser,
+ # To fetch the Ticket for a host, creating host objects or deploying the configuration you will have to authenticate against the Icinga Director. This parameter allows to set the Password we shall use to login.
+ [string]$DirectorPassword,
+ # TODO
+ [string]$DirectorDomain,
+ # API key for specific host templates, allowing the configuration and creation of host objects within the Icinga Director without password authentication. This is the API token assigned to a host template. Hosts created with this token, will automaticly receive the Host-Template assigned to the API key. Furthermore this token allows to access the Icinga Director Self-Service API to fetch basic arguments for the module.
+ # Note: This argument requires Icinga Director API Version 1.4.0 or higher
+ [string]$DirectorAuthToken,
+ # This argument allows you to parse either a valid JSON-String or an hashtable / array, containing all informations for the host object to create. Please note that using arrays or hashtable objects for this argument will require PowerShell version 3 and above.
+ [System.Object]$DirectorHostObject,
+ # If you add this parameter to your script call, the PowerShell module will tell the Icinga Director to deploy outstanding configurations. This parameter can be used in combination with -DirectorHostObject, to create objects and deploy them right away. This argument requires the user and password argument and will not work with the Self Service api.
+ # Caution: If set, all outstanding deployments inside the Icinga Director will be deployed. Use with caution!!!
+ [switch]$DirectorDeployConfig = $FALSE,
+
+ # NSClient Installer
+ [switch]$InstallNSClient = $FALSE,
+ [switch]$NSClientAddDefaults = $FALSE,
+ [switch]$NSClientEnableFirewall = $FALSE,
+ [switch]$NSClientEnableService = $FALSE,
+ [string]$NSClientDirectory,
+ [string]$NSClientInstallerPath,
+
+ # Uninstaller arguments
+ # This argument is only used by the function 'uninstall' and will remove the remaining content from 'C:\Program Data\icinga2' to prepare a clean setup of the Icinga 2 infrastrucure.
+ [switch]$FullUninstallation = $FALSE,
+ # When this argument is set, the installed NSClient++ will be removed from the system as well. This argument is only used by calling the function 'uninstall'
+ [switch]$RemoveNSClient = $FALSE,
+
+ # Dump Icinga Config
+ [switch]$DumpIcingaConfig = $FALSE,
+ # Dump Icinga Objects
+ [switch]$DumpIcingaObjects = $FALSE,
+
+ #Internal handling
+ # This argument allows to shorten the entire call of the module, not requiring to define a custom variable and executing the installation function of the monitoring components.
+ [switch]$RunInstaller = $FALSE,
+ # This argument allows to shorten the entire call of the module, not requiring to define a custom variable and executing the uninstallation function of the monitoring components.
+ [switch]$RunUninstaller = $FALSE,
+ #In certain cases it could be required to ingore SSL certificate validations from the Icinga Web 2 installation (for example in case self-signed certificates are used). By default the PowerShell Module is validating SSL certificates and throws an error if the validation fails.
+ #In case self-signed certificates are used and not included to the local certificate store of the Windows machine, the module will fail. By providing this argument, validation will always be valid and the script will execute as if the certificate was valid.
+ [switch]$IgnoreSSLErrors = $FALSE,
+
+ [switch]$DebugMode = $FALSE,
+ # Specify a path to either a directory or a file to write all output from the PowerShell module into a file for later debugging. In case a directory is specified, the script will automaticly create a new file with a unique name into it. If a file is specified which is not yet present, it will be created.
+ [string]$ModuleLogFile
+ );
+
+ #
+ # Initialise our installer object
+ # and generate our config objects
+ #
+ $installer = New-Object -TypeName PSObject;
+ $installer | Add-Member -membertype NoteProperty -name 'properties' -value @{}
+ $installer | Add-Member -membertype NoteProperty -name 'cfg' -value @{
+ agent_name = $AgentName;
+ ticket = $Ticket;
+ agent_version = $InstallAgentVersion;
+ fetch_agent_name = $FetchAgentName;
+ fetch_agent_fqdn = $FetchAgentFQDN;
+ transform_hostname = $TransformHostname;
+ agent_listen_port = $AgentListenPort;
+ parent_zone = $ParentZone;
+ accept_config = $AcceptConfig;
+ icinga_enable_debug_log = $IcingaEnableDebugLog;
+ icinga_disable_log = $IcingaDisableLogging;
+ agent_add_firewall_rule = $AgentAddFirewallRule;
+ parent_endpoints = $ParentEndpoints;
+ endpoints_config = $EndpointsConfig;
+ global_zones = $GlobalZones;
+ icinga_service_user = $IcingaServiceUser;
+ download_url = $DownloadUrl;
+ agent_install_directory = $AgentInstallDirectory;
+ allow_updates = $AllowUpdates;
+ installer_hashes = $InstallerHashes;
+ flush_api_directory = $FlushApiDirectory;
+ ca_server = $CAServer;
+ ca_certificate_path = $CACertificatePath;
+ ca_port = $CAPort;
+ force_cert = $ForceCertificateGeneration;
+ ca_fingerprint = $CAFingerprint;
+ caproxy = $CAProxy;
+ director_url = $DirectorUrl;
+ director_user = $DirectorUser;
+ director_password = $DirectorPassword;
+ director_domain = $DirectorDomain;
+ director_auth_token = $DirectorAuthToken;
+ director_host_object = $DirectorHostObject;
+ director_deploy_config = $DirectorDeployConfig;
+ install_nsclient = $InstallNSClient;
+ nsclient_add_defaults = $NSClientAddDefaults;
+ nsclient_firewall = $NSClientEnableFirewall;
+ nsclient_service = $NSClientEnableService;
+ nsclient_directory = $NSClientDirectory;
+ nsclient_installer_path = $NSClientInstallerPath;
+ full_uninstallation = $FullUninstallation;
+ remove_nsclient = $RemoveNSClient;
+ ignore_ssl_errors = $IgnoreSSLErrors;
+ debug_mode = $DebugMode;
+ module_log_file = $ModuleLogFile;
+ }
+
+ #
+ # Access default script config parameters
+ # by using this function. These variables
+ # are set during the initial call of
+ # the script with the parameters
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'config' -value {
+ param([string] $key);
+ return $this.cfg[$key];
+ }
+
+ #
+ # In case we run the script not through Icinga Director, we might want to set
+ # script default values
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'setScriptDefaultVariables' -value {
+ if ($this.cfg['transform_hostname'] -eq -1) {
+ $this.cfg['transform_hostname'] = 0;
+ $this.debug('Setting "transform_hostname" to default 0');
+ }
+ if ($this.cfg['download_url'] -eq '') {
+ $this.cfg['download_url'] = 'https://packages.icinga.com/windows/';
+ $this.debug('Setting "download_url" to default "https://packages.icinga.com/windows/"');
+ }
+ if ($this.cfg['agent_listen_port'] -eq -1) {
+ $this.cfg['agent_listen_port'] = 5665;
+ $this.debug('Setting "agent_listen_port" to default 5665');
+ }
+ if ($this.cfg['global_zones'].Count -eq 0) {
+ $this.cfg['global_zones'] = @( 'director-global', 'global-templates' );
+ $this.generateGlobalZones();
+ $this.debug('Setting "global_zones" to default "director-global" and "global-templates"');
+ }
+ if ($this.cfg['accept_config'] -eq -1) {
+ $this.cfg['accept_config'] = $TRUE;
+ $this.debug('Setting "accept_config" to default "true"');
+ }
+ }
+
+ #
+ # Override the given arguments of the PowerShell script with
+ # custom values or edited values
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'overrideConfig' -value {
+ param([string] $key, $value, $keepScriptArguments);
+
+ # Ensure the director will not override our custom config for arguments
+ if ($keepScriptArguments) {
+ $scriptValue = $this.cfg[$key];
+
+ if ([string]::IsNullOrEmpty($scriptValue) -eq $FALSE) {
+ if ($scriptValue.GetType().Name -eq 'SwitchParameter' -And $scriptValue -eq $TRUE) {
+ $this.debug("Skipping overriding of '$key', as set by script. [$scriptValue]");
+ return;
+ }
+
+ if ($scriptValue.GetType().Name -eq 'SwitchParameter' -And $scriptValue -eq $FALSE) {
+ # Do not keep value
+ } elseif ($scriptValue.GetType().Name -eq 'Int32' -And $scriptValue -eq -1) {
+ # Do not keep value
+ } elseif ([string]::IsNullOrEmpty($scriptValue) -eq $FALSE) {
+ $this.debug("Skipping overriding of '$key', as set by script. [$scriptValue]");
+ return;
+ } else {
+ $this.debug("Skipping overriding of '$key', as set by script. [$scriptValue]");
+ return;
+ }
+ }
+ }
+
+ $this.cfg[$key] = $value;
+ }
+
+ #
+ # Convert a boolean value $TRUE $FALSE
+ # to a string value
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'convertBoolToString' -value {
+ param([bool]$key);
+ if ($key) {
+ return 'true';
+ }
+ return 'false';
+ }
+
+ #
+ # Convert a boolean value $TRUE $FALSE
+ # to a int value
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'convertBoolToInt' -value {
+ param([bool]$key);
+ if ($key) {
+ return 1;
+ }
+ return 0;
+ }
+
+ #
+ # Global variables can be accessed
+ # by using this function. Example:
+ # $this.getProperty('agent_version)
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getProperty' -value {
+ param([string] $key);
+
+ # Initialse some variables first
+ # will only be called once
+ if (-Not $this.properties.Get_Item('initialized')) {
+ $this.init();
+ }
+
+ return $this.properties.Get_Item($key);
+ }
+
+ #
+ # Set the value of a global variable
+ # to ensure later usage. Example
+ # $this.setProperty('agent_version', '2.4.10')
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'setProperty' -value {
+ param([string]$key, $value);
+
+ # Initialse some variables first
+ # will only be called once
+ if (-Not $this.properties.Get_Item('initialized')) {
+ $this.properties.Set_Item('initialized', $TRUE);
+ $this.init();
+ }
+
+ $this.properties.Set_Item($key, $value);
+ }
+
+ #
+ # This function will dump all global
+ # variables of the script for debugging
+ # purposes
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'dumpProperties' -value {
+ [string]$dumpData = $this.properties | Out-String;
+ $this.debug('Dumping properties...');
+ $this.debug($dumpData);
+ }
+
+ #
+ # Dump all configured arguments for easier debugging
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'dumpConfig' -value {
+ [string]$dumpData = $this.cfg | Out-String;
+ $this.debug('Dumping config...');
+ $this.debug($dumpData);
+ }
+
+ #
+ # Write all output from consoles to a logfile
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'writeLogFile' -value {
+ param([string]$severity, [string]$content);
+
+ # If no logfile is specified, do nothing
+ if (-Not $this.config('module_log_file')) {
+ return;
+ }
+
+ # Store our logfile into a variable
+ $logFile = $this.config('module_log_file');
+
+ # Have we specified a directory to write into or a file already?
+ try {
+ # Check if we are a directory or a file
+ # Will return false for files or non-existing files
+ $directory = (Get-Item $logFile) -is [System.IO.DirectoryInfo];
+ } catch {
+ # Nothing to catch. Simply get rid of error messages from aboves function in case of error
+ # Will return false anyways on error
+ }
+
+ # If we are a directory, add a file we can write to
+ if ($directory) {
+ $logFile = Join-Path -Path $logFile -ChildPath 'icinga2agent_psmodule.log';
+ }
+
+ # Format a timestamp to get to know the exact date and time. Example: 2017-13-07 22:09:13.263.263
+ $timestamp = Get-Date -Format "yyyy-dd-MM HH:mm:ss.fff";
+ $content = [string]::Format('{0} [{1}]: {2}', $timestamp, $severity, $content);
+
+ # Write the content to our logfile
+ Add-Content -Path $logFile -Value $content;
+ }
+
+ #
+ # This function will print messages as errors, but add them internally to
+ # an exception list. These will re-printed at the end to summarize possible
+ # issues during the run
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'exception' -value {
+ param([string]$message, [string[]]$args);
+ [array]$exceptions = $this.getProperty('exception_messages');
+ if ($exceptions -eq $null) {
+ $exceptions = @();
+ }
+ $exceptions += $message;
+ $this.setProperty('exception_messages', $exceptions);
+ write-host 'Fatal:' $message -ForegroundColor red;
+ $this.writeLogFile('fatal', $message);
+ }
+
+ #
+ # Get the current exit code of the script. Return 0 for no errors and 1 for
+ # possible errors, including a summary of what went wrong
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getScriptExitCode' -value {
+ [array]$exceptions = $this.getProperty('exception_messages');
+
+ $this.dumpProperties();
+ $this.dumpConfig();
+
+ if ($exceptions -eq $null) {
+ return 0;
+ }
+
+ $this.writeLogFile('fatal', '##################################################################');
+ $message = '######## The script encountered several errors during run ########';
+ $this.writeLogFile('fatal', $message);
+ $this.writeLogFile('fatal', '##################################################################');
+ write-host $message -ForegroundColor red;
+ foreach ($err in $exceptions) {
+ write-host 'Fatal:' $err -ForegroundColor red;
+ $this.writeLogFile('fatal', $err);
+ }
+
+ return 1;
+ }
+
+ #
+ # Print the relevant exception
+ # By reading the relevant info
+ # from the stack
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'printLastException' -value {
+ $this.exception($_.Exception.Message);
+ }
+
+ #
+ # this function will print an info message
+ # or throw an exception, based on the
+ # provided exitcode
+ # (0 = ok, anything else => exception)
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'printAndAssertResultBasedOnExitCode' -value {
+ param([string]$result, [string]$exitcode);
+ if ($exitcode -ne 0) {
+ throw $result;
+ } else {
+ $this.info($result);
+ }
+ }
+
+ #
+ # Return an error message with red text
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'error' -value {
+ param([string] $message, [array] $args);
+ Write-Host 'Error:' $message -ForegroundColor red;
+ $this.writeLogFile('error', $message);
+ }
+
+ #
+ # Return a warning message with yellow text
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'warn' -value {
+ param([string] $message, [array] $args);
+ Write-Host 'Warning:' $message -ForegroundColor yellow;
+ $this.writeLogFile('warning', $message);
+ }
+
+ #
+ # Return a info message with green text
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'info' -value {
+ param([string] $message, [array] $args);
+ Write-Host 'Notice:' $message -ForegroundColor green;
+ $this.writeLogFile('info', $message);
+ }
+
+ #
+ # Return a output message with wrhite text
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'output' -value {
+ param([string] $message, [array] $args);
+ Write-Host '' $message -ForegroundColor white;
+ $this.writeLogFile('', $message);
+ }
+
+ #
+ # Return a debug message with blue text
+ # in case debug mode is enabled
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'debug' -value {
+ param([string] $message, [array] $args);
+ if ($this.config('debug_mode')) {
+ Write-Host 'Debug:' $message -ForegroundColor blue;
+ $this.writeLogFile('debug', $message);
+ }
+ }
+
+ #
+ # Initialise certain parts of the
+ # script first
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'init' -value {
+ $this.setProperty('initialized', $TRUE);
+ # Set the default config dir
+ $this.setProperty('config_dir', (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\etc\icinga2\'));
+ $this.setProperty('api_dir', (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\api'));
+ $this.setProperty('cert_dir', (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs'));
+ $this.setProperty('icinga_ticket', $this.config('ticket'));
+ $this.setProperty('local_hostname', $this.config('agent_name'));
+ # Ensure we generate the required configuration content
+ $this.generateConfigContent();
+ }
+
+ #
+ # We require to run this script as admin. Generate the required function here
+ # We might run this script from a non-privileged user. Ensure we have admin
+ # rights first. Otherwise abort the script.
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'isAdmin' -value {
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent();
+ $principal = New-Object System.Security.Principal.WindowsPrincipal($identity);
+
+ if (-not $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) {
+ throw 'You require to run this script as administrator.';
+ return $FALSE;
+ }
+ return $TRUE;
+ }
+
+ #
+ # In case we want to define endpoint configuration (address / port)
+ # we will require to fetch data correctly from a given array
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getEndpointConfigurationByArrayIndex' -value {
+ param([int] $currentIndex);
+
+ # Load the config into a local variable for quicker access
+ [array]$endpoint_config = $this.config('endpoints_config');
+
+ # In case no endpoint config is given, we should do nothing
+ if ($endpoint_config -eq $NULL) {
+ return '';
+ }
+
+ [string]$configArgument = $endpoint_config[$currentIndex];
+ [string]$config_string = '';
+ [array]$configObject = '';
+
+ if ($configArgument -ne '') {
+ $configObject = $configArgument.Split(';');
+ } else {
+ return '';
+ }
+
+ # Write the host data from the first array position
+ if ($configObject[0]) {
+ $config_string += [string]::Format(' host = "{0}"', $configObject[0]);
+ }
+
+ # Write the port data from the second array position
+ if ($configObject[1]) {
+ $config_string += [string]::Format('{0} port = {1}', "`n", $configObject[1]);
+ }
+
+ # Return the host and possible port configuration for this endpoint
+ return $config_string;
+ }
+
+ #
+ # Build endpoint hosts and objects based
+ # on configuration
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'generateEndpointNodes' -value {
+
+ if ($this.config('parent_endpoints')) {
+ [string]$endpoint_objects = '';
+ [string]$endpoint_nodes = '';
+ [int]$endpoint_index = 0;
+
+ foreach ($endpoint in $this.config('parent_endpoints')) {
+ $endpoint_objects += [string]::Format('object Endpoint "{0}" {1}{2}', $endpoint, '{', "`n");
+ $endpoint_objects += $this.getEndpointConfigurationByArrayIndex($endpoint_index);
+ $endpoint_objects += [string]::Format('{0}{1}{2}', "`n", '}', "`n");
+ $endpoint_nodes += [string]::Format('"{0}", ', $endpoint);
+ $endpoint_index += 1;
+ }
+ # Remove the last blank and , from the string
+ if (-Not $endpoint_nodes.length -eq 0) {
+ $endpoint_nodes = $endpoint_nodes.Remove($endpoint_nodes.length - 2, 2);
+ }
+ $this.setProperty('endpoint_nodes', $endpoint_nodes);
+ $this.setProperty('endpoint_objects', $endpoint_objects);
+ $this.setProperty('generate_config', 'true');
+ } else {
+ $this.setProperty('generate_config', 'false');
+ }
+ }
+
+ #
+ # Generate global zones by configuration
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'generateGlobalZones' -value {
+
+ # Load all configured global zones
+ [array]$global_zones = $this.config('global_zones');
+ [string]$zones = '';
+
+ # In case no zones are given, simply add director-global
+ if ($global_zones -eq $NULL) {
+ $this.setProperty('global_zones', $zones);
+ return;
+ }
+
+ # Loop through all given zones and add them to our configuration
+ foreach ($zone in $global_zones) {
+ if ($zone -ne '') {
+ $zones += [string]::Format('object Zone "{0}" {1}{2} global = true{3}{4}{5}', $zone, '{', "`n", "`n", '}', "`n");
+ }
+ }
+ $this.setProperty('global_zones', $zones);
+ }
+
+ #
+ # Generate default config values
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'generateConfigContent' -value {
+ $this.generateEndpointNodes();
+ $this.generateGlobalZones();
+ }
+
+ #
+ # This function will ensure we create a
+ # Web Client object we can use entirely
+ # inside the module to achieve our requirements
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'createWebClientInstance' -value {
+ param([string]$header, [bool]$directorHeader = $FALSE);
+
+ [System.Object]$webClient = New-Object System.Net.WebClient;
+ if ($this.config('director_user') -And $this.config('director_password')) {
+ [string]$domain = $null;
+ if ($this.config('director_domain')) {
+ $domain = $this.config('director_domain');
+ }
+ $webClient.Credentials = New-Object System.Net.NetworkCredential($this.config('director_user'), $this.config('director_password'), $domain);
+ }
+ $webClient.Headers.add('accept', $header);
+ if ($directorHeader) {
+ $webClient.Headers.add('X-Director-Accept', 'text/plain');
+ }
+
+ return $webClient;
+ }
+
+ #
+ # Handle HTTP Requests properly to receive proper status codes in return
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'createHTTPRequest' -value {
+ param([string]$url, [string]$body, [string]$method, [string]$header, [bool]$directorHeader, [bool]$printExceptionMessage);
+
+ $httpRequest = [System.Net.HttpWebRequest]::Create($url);
+ $httpRequest.Method = $method;
+ $httpRequest.Accept = $header;
+ $httpRequest.ContentType = 'application/json; charset=utf-8';
+ if ($directorHeader) {
+ $httpRequest.Headers.Add('X-Director-Accept: text/plain');
+ }
+ $httpRequest.TimeOut = 6000;
+
+ # If we are using self-signed certificates for example, the HTTP request will
+ # fail caused by the SSL certificate. With this we can allow even faulty
+ # certificates. This should be used with caution
+ if ($this.config('ignore_ssl_errors')) {
+ [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
+ }
+
+ if ($this.config('director_user') -And $this.config('director_password')) {
+ [string]$credentials = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([string]::Format('{0}:{1}', $this.config('director_user'), $this.config('director_password'))));
+ $httpRequest.Headers.add([string]::Format('Authorization: Basic {0}', $credentials));
+ }
+
+ # Only send data in case we want to send some data
+ if ($body -ne '') {
+ $transmitBytes = [System.Text.Encoding]::UTF8.GetBytes($body);
+ $httpRequest.ContentLength = $transmitBytes.Length;
+ [System.IO.Stream]$httpOutput = [System.IO.Stream]$httpRequest.GetRequestStream()
+ $httpOutput.Write($transmitBytes, 0, $transmitBytes.Length)
+ $httpOutput.Close()
+ }
+
+ try {
+
+ return $this.readResponseStream($httpRequest.GetResponse());
+
+ } catch [System.Net.WebException] {
+ if ($printExceptionMessage) {
+ # Print an exception message and the possible body in case we received one
+ # to make troubleshooting easier
+ [string]$errorResponse = $this.readResponseStream($_.Exception.Response);
+ $this.error($_.Exception.Message);
+ if ($errorResponse -ne '') {
+ $this.error($errorResponse);
+ }
+ }
+
+ $exceptionMessage = $_.Exception.Response;
+ if ($exceptionMessage.StatusCode) {
+ return [int][System.Net.HttpStatusCode]$exceptionMessage.StatusCode;
+ } else {
+ return 900;
+ }
+ }
+
+ return '';
+ }
+
+ #
+ # Read the content of a response and return it's value as a string
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'readResponseStream' -value {
+ param([System.Object]$response);
+
+ if ($response) {
+ $responseStream = $response.getResponseStream();
+ $streamReader = New-Object IO.StreamReader($responseStream);
+ $result = $streamReader.ReadToEnd();
+ $response.close()
+ $streamReader.close()
+
+ return $result;
+ }
+
+ $this.exception('Could not retreive response from remote server. Response is null. This might be caused by SSL errors. Please try using -IgnoreSSLErrors as argument and try again.');
+ return 'No response from remote server';
+ }
+
+ #
+ # Check if the provided result is an HTTP Response code
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'isHTTPResponseCode' -value {
+ param([string]$httpResult);
+
+ if ($httpResult.length -eq 3) {
+ return $TRUE;
+ }
+
+ return $FALSE;
+ }
+
+ #
+ # Do we require to update the Agent?
+ # Might be disabled by user or current version
+ # is already installed
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'requireAgentUpdate' -value {
+ if (-Not $this.config('allow_updates') -Or -Not $this.config('agent_version')) {
+ $this.warn('Icinga 2 Agent update installation disabled.');
+ return $FALSE;
+ }
+
+ if ($this.getProperty('agent_version') -eq $this.config('agent_version')) {
+ $this.info('Icinga 2 Agent up-to-date. No update required.');
+ return $FALSE;
+ }
+
+ $this.info([string]::Format('Current Icinga 2 Agent Version ({0}) is not matching server version ({1}). Downloading new version...'), $this.getProperty('agent_version'), $this.config('agent_version'));
+
+ return $TRUE;
+ }
+
+ #
+ # We could try to install the Agent from a local directory
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'isDownloadPathLocal' -value {
+ if ($this.config('download_url') -And (Test-Path ($this.config('download_url')))) {
+ return $TRUE;
+ }
+ return $FALSE;
+ }
+
+ #
+ # Download the Icinga 2 Agent Installer from out defined source
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'downloadInstaller' -value {
+ if (-Not $this.config('agent_version')) {
+ return;
+ }
+
+ if ($this.isDownloadPathLocal()) {
+ $this.info('Installing Icinga 2 Agent from local directory');
+ } else {
+ $url = $this.config('download_url') + $this.getProperty('install_msi_package');
+ $this.info([string]::Format('Downloading Icinga 2 Agent Binary from "{0}"', $url));
+
+ Try {
+ [System.Object]$client = New-Object System.Net.WebClient;
+ $client.DownloadFile($url, $this.getInstallerPath());
+
+ if (-Not $this.installerExists()) {
+ $this.exception([string]::Format('Unable to locate downloaded Icinga 2 Agent installer file from {0}. Download destination: {1}', $url, $this.getInstallerPath()));
+ }
+ } catch {
+ $this.exception([string]::Format('Unable to download Icinga 2 Agent from {0}. Please ensure the link does exist and access is possible. Error: {1}', $url, $_.Exception.Message));
+ }
+ }
+ }
+
+ #
+ # In case we provide a list of hashes to very against
+ # we check them to ensure the package we downloaded
+ # for the Agent installation is allowed to be installed
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'verifyInstallerChecksumAndThrowException' -value {
+ if (-Not $this.config('installer_hashes')) {
+ $this.warn("Icinga 2 Agent Installer verification disabled.");
+ return;
+ }
+
+ [string]$installerHash = $this.getInstallerFileHash($this.getInstallerPath());
+ foreach($hash in $this.config('installer_hashes')) {
+ if ($hash -eq $installerHash) {
+ $this.info('Icinga 2 Agent hash verification successfull.');
+ return;
+ }
+ }
+
+ throw 'Failed to verify against any provided installer hash.';
+ return;
+ }
+
+ #
+ # Get the SHA1 hash from our uninstaller file
+ # Own function required because Get-FileHash is not
+ # supported by PowerShell Version 2
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getInstallerFileHash' -value {
+ param([string]$filename);
+
+ [System.Object]$fileInput = New-Object System.IO.FileStream($filename,[System.IO.FileMode]::Open);
+ [System.Object]$hash = New-Object System.Text.StringBuilder;
+ [System.Security.Cryptography.HashAlgorithm]::Create('SHA1').ComputeHash($fileInput) |
+ ForEach-Object {
+ [Void]$hash.Append($_.ToString("x2"));
+ }
+ $fileInput.Close();
+ return $hash.ToString().ToUpper();
+ }
+
+ #
+ # Returns the full path to our installer package
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getInstallerPath' -value {
+ if (-Not $this.config('download_url') -Or -Not $this.getProperty('install_msi_package')) {
+ return '';
+ }
+ [string]$installerPath = '';
+ if (Test-Path ($this.config('download_url'))) {
+ $installerPath = Join-Path -Path $this.config('download_url') -ChildPath $this.getProperty('install_msi_package');
+ } else {
+ $installerPath = [string]::Format('{0}/{1}', $this.config('download_url'), $this.getProperty('install_msi_package'));
+ }
+
+ if ($this.isDownloadPathLocal()) {
+ if (Test-Path $installerPath) {
+ return $installerPath;
+ } else {
+ $this.exception([string]::Format('Failed to locate local Icinga 2 Agent installer at {0}', $installerPath));
+ return '';
+ }
+ } else {
+ return (Join-Path -Path $Env:temp -ChildPath $this.getProperty('install_msi_package'));
+ }
+ }
+
+ #
+ # Verify that the installer package we downloaded
+ # does exist in first place
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'installerExists' -value {
+ if ($this.getInstallerPath() -And (Test-Path $this.getInstallerPath())) {
+ return $TRUE;
+ }
+ return $FALSE;
+ }
+
+ #
+ # Get all arguments for the Icinga 2 Agent installer package
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getIcingaAgentInstallerArguments' -value {
+ # Initialise some basic variables
+ [string]$arguments = '';
+ [string]$installerLocation = '';
+
+ # By default, install the Icinga 2 Agent again in the pre-installed directory
+ # before the update. Will only apply during updates / downgrades of the Agent
+ if ($this.getProperty('cur_install_dir')) {
+ # In case we perform an architecture change, we should use the new default location as source in case
+ # we have installed the Agent into Program Files (x86) for example but are now using a x64 Agent
+ # which should be installed into Program Files instead
+ if ($this.getProperty('agent_architecture_change') -And $this.getProperty('agent_migration_target')) {
+ $installerLocation = [string]::Format(' INSTALL_ROOT="{0}"', $this.getProperty('agent_migration_target'));
+ } else {
+ $installerLocation = [string]::Format(' INSTALL_ROOT="{0}"', $this.getProperty('cur_install_dir'));
+ }
+ }
+
+ # However, if we specified a custom directory over the argument, always use that
+ # one as installer target directory
+ if ($this.config('agent_install_directory')) {
+ $installerLocation = [string]::Format(' INSTALL_ROOT="{0}"', $this.config('agent_install_directory'));
+ $this.setProperty('cur_install_dir', $this.config('agent_install_directory'));
+ }
+
+ $arguments += $installerLocation;
+
+ return $arguments;
+ }
+
+ #
+ # Do we require to migrate data from previous Icinga 2 Agent Directory
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'checkForIcingaMigrationRequirement' -value {
+ if ($this.getProperty('cur_install_dir')) {
+ [string]$installDir = $this.getProperty('cur_install_dir');
+ # Just in case we installed an x86 Agent on the System, we will require to migrate to x64 on a x64 system.
+ if (${Env:ProgramFiles(x86)} -And $installDir.contains(${Env:ProgramFiles(x86)}) -And $this.getProperty('system_architecture') -eq 'x86_64') {
+ [string]$migrationPath = $installDir.Replace(${Env:ProgramFiles(x86)}, ${Env:ProgramFiles});
+ $this.setProperty('agent_architecture_change', $TRUE);
+ $this.setProperty('require_migration', $TRUE);
+ $this.setProperty('agent_migration_source', $installDir);
+ $this.setProperty('agent_migration_target', $migrationPath);
+ $this.setProperty('cur_install_dir', $migrationPath);
+ $this.warn('Detected architecture change. Current installed Agent version is x86, while new installed version will be x64. Possible data will be migrated.');
+ } else {
+ $this.setProperty('agent_migration_source', $this.getProperty('cur_install_dir'));
+ }
+ }
+
+ if ($this.config('agent_install_directory')) {
+ [string]$currentInstallDir = $this.cutLastSlashFromDirectoryPath($this.getProperty('cur_install_dir'));
+ [string]$intendedInstallDir = $this.cutLastSlashFromDirectoryPath($this.config('agent_install_directory'));
+
+ if ($currentInstallDir -ne $intendedInstallDir) {
+ $this.setProperty('agent_migration_target', $this.config('agent_install_directory'));
+ $this.setProperty('require_migration', $TRUE);
+ }
+ }
+ }
+
+ #
+ # To ensure we handle path strings correctly, we always require to cut the last \
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'cutLastSlashFromDirectoryPath' -value {
+ param([string]$path);
+
+ if (-Not $path -Or $path -eq '') {
+ return $path;
+ }
+
+ if ($path[$path.Length - 1] -eq '\') {
+ $path = $path.Substring(0, $path.Length - 1);
+ }
+
+ return $path;
+ }
+
+ #
+ # Install the Icinga 2 agent from the provided installation package
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'installAgent' -value {
+ $this.downloadInstaller();
+ if (-Not $this.installerExists()) {
+ $this.exception('Failed to setup Icinga 2 Agent. Installer package not found.');
+ return;
+ }
+ $this.verifyInstallerChecksumAndThrowException();
+ $this.info('Installing Icinga 2 Agent');
+
+ # Start the installer process
+ $result = $this.startProcess('MsiExec.exe', $TRUE, [string]::Format('/quiet /i "{0}" {1}', $this.getInstallerPath(), $this.getIcingaAgentInstallerArguments()));
+
+ # Exit Code 0 means the Agent was installed successfully
+ # Otherwise we require to throw an error
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.exception('Failed to install Icinga 2 Agent. ' + $result.Get_Item('message'));
+ } else {
+ $this.info('Icinga 2 Agent installed.');
+ }
+
+ # Update the Icinga 2 Agent Directories in case of a version change
+ # Required by updating from older versions to 2.8.0. and newer
+ $return = $this.isAgentInstalled();
+
+ $this.setProperty('require_restart', 'true');
+ }
+
+ #
+ # Updates the Agent in case allowed and required.
+ # Removes previous version of Icinga 2 Agent first
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'updateAgent' -value {
+ $this.downloadInstaller();
+ if (-Not $this.installerExists()) {
+ $this.exception('Failed to update Icinga 2 Agent. Installer package not found.');
+ return;
+ }
+ $this.verifyInstallerChecksumAndThrowException()
+ if (-Not $this.getProperty('uninstall_id')) {
+ $this.exception('Failed to update Icinga 2 Agent. Uninstaller is not specified.');
+ return;
+ }
+
+ $this.info('Removing previous Icinga 2 Agent version');
+ # Start the uninstaller process
+ $result = $this.startProcess('MsiExec.exe', $TRUE, $this.getProperty('uninstall_id') +' /q');
+
+ # Exit Code 0 means the Agent was removed successfully
+ # Otherwise we require to throw an error
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.exception('Failed to remove Icinga 2 Agent. ' + $result.Get_Item('message'));
+ } else {
+ $this.info('Icinga 2 Agent successfully removed.');
+ }
+
+ $this.checkForIcingaMigrationRequirement();
+ $this.applyPossibleAgentMigration();
+
+ $this.info('Installing new Icinga 2 Agent version');
+ # Start the installer process
+ $result = $this.startProcess('MsiExec.exe', $TRUE, [string]::Format('/quiet /i "{0}" {1}', $this.getInstallerPath(), $this.getIcingaAgentInstallerArguments()));
+
+ # Exit Code 0 means the Agent was removed successfully
+ # Otherwise we require to throw an error
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.exception([string]::Format('Failed to install new Icinga 2 Agent. {0}', $result.Get_Item('message')));
+ } else {
+ $this.info('Icinga 2 Agent successfully updated.');
+ }
+
+ # Update the Icinga 2 Agent Directories in case of a version change
+ # Required by updating from older versions to 2.8.0. and newer
+ $return = $this.isAgentInstalled();
+ $this.setProperty('require_restart', 'true');
+ }
+
+ #
+ # Migrate a folder and it's content from a previous Agent installation to
+ # a new target destination
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'doMigrateIcingaDirectory' -value {
+ param([string]$sourcePath, [string]$targetPath, [string]$directory);
+
+ if (Test-Path (Join-Path -Path $sourcePath -ChildPath $directory)) {
+ [string]$source = Join-Path -Path $sourcePath -ChildPath $directory;
+ [string]$target = Join-Path -Path $targetPath -ChildPath $directory;
+ $this.info([string]::Format('Migrating content from "{0}" to "{1}"', $source, $target));
+ $result = Copy-Item $source $target -Recurse;
+ }
+ }
+
+ #
+ # Copy a single file from it's source location to our target location
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'doMigrateIcingaFile' -value {
+ param([string]$sourcePath, [string]$targetPath, [string]$file);
+ $this.info([string]::Format('Migrating file from "{0}" to "{1}\{2}"', $sourcePath, $targetPath, $_));
+ Copy-Item $sourcePath $targetPath;
+ }
+
+ #
+ # This function will determine if we require to migrate content from a previous
+ # Icinga 2 Agent installation to the new location
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'applyPossibleAgentMigration' -value {
+ if (-Not $this.getProperty('require_migration') -Or $this.getProperty('require_migration') -eq $FALSE) {
+ $this.info('No migration of Icinga 2 Agent data required.')
+ return;
+ }
+
+ $this.info([string]::Format('Icinga 2 Agent installation location changed from {0} to {1}. Migrating possible content...', $this.getProperty('agent_migration_source'), $this.getProperty('agent_migration_target')));
+
+ if ($this.getProperty('agent_migration_source') -And (Test-Path ($this.getProperty('agent_migration_source')))) {
+ # Load Directories and Remove \ at the end of the path if present to ensure we have the same path base
+ [string]$sourcePath = $this.cutLastSlashFromDirectoryPath($this.getProperty('agent_migration_source'));
+ [string]$targetPath = $this.cutLastSlashFromDirectoryPath($this.getProperty('agent_migration_target'));
+
+ # Get all objects within our source root and copy it to our target destination
+ $result = Get-ChildItem -Path $sourcePath |
+ ForEach-Object {
+ if ($_.PSIsContainer) {
+ $this.doMigrateIcingaDirectory($sourcePath, $targetPath, $_);
+ } else {
+ $this.doMigrateIcingaFile($_.FullName, $targetPath, $_);
+ }
+ }
+ $this.info([string]::Format('Migration of source folder applied. Please remove content from previous directory {0} if no longer required.', $sourcePath));
+ } else {
+ $this.info('No data for migration found. Setup is clean.');
+ }
+ }
+
+ #
+ # We might have installed the Icinga 2 Agent
+ # already. In case we do, get all data to
+ # ensure we access the Agent correctly
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'isAgentInstalled' -value {
+ [string]$architecture = '';
+ if ([IntPtr]::Size -eq 4) {
+ $architecture = "x86";
+ $regPath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*';
+ } else {
+ $architecture = "x86_64";
+ $regPath = @('HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*');
+ }
+
+ # Try locating current Icinga 2 Agent installation
+ $localData = Get-ItemProperty $regPath |
+ .{
+ process {
+ if ($_.DisplayName) {
+ $_;
+ }
+ }
+ } |
+ Where-Object {
+ $_.DisplayName -eq 'Icinga 2';
+ } |
+ Select-Object -Property InstallLocation, UninstallString, DisplayVersion;
+
+ if ($localData.UninstallString) {
+ $this.setProperty('uninstall_id', $localData.UninstallString.Replace("MsiExec.exe ", ""));
+ }
+ $this.setProperty('cur_install_dir', $localData.InstallLocation);
+ $this.setProperty('agent_version', $localData.DisplayVersion);
+ $this.setProperty('install_msi_package', 'Icinga2-v' + $this.config('agent_version') + '-' + $architecture + '.msi');
+ $this.setProperty('system_architecture', $architecture);
+ $this.setIcinga2AgentVersion($localData.DisplayVersion);
+
+ if (-Not $this.validateVersions('2.8.0', $this.getProperty('icinga2_agent_version'))) {
+ $this.setProperty('cert_dir', (Join-Path -Path $this.getProperty('config_dir') -ChildPath 'pki'));
+ if ($this.getProperty('use_new_cert_dir')) {
+ $this.setProperty('require_cert_migration', $TRUE);
+ $this.info('You are downgrading from a newer Icinga 2 Version to a older one. This will require a certificate migration.');
+ }
+ } else {
+ $this.setProperty('cert_dir', (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs'));
+ $this.setProperty('use_new_cert_dir', $TRUE);
+ }
+
+ $this.info([string]::Format('Using Icinga version "{0}", setting certificate directory to "{1}"',
+ $localData.DisplayVersion,
+ $this.getProperty('cert_dir')
+ )
+ );
+
+ if ($localData.InstallLocation) {
+ $this.info([string]::Format('Found Icinga 2 Agent version {0} installed at "{1}"', $localData.DisplayVersion, $localData.InstallLocation));
+ return $TRUE;
+ } else {
+ $this.warn('Icinga 2 Agent does not seem to be installed on the system');
+ # Set Default value for install dir
+ $this.setProperty('cur_install_dir', (Join-Path $Env:ProgramFiles -ChildPath 'ICINGA2'));
+ }
+ return $FALSE;
+ }
+
+ #
+ # Ensure we are able to install a firewall rule for the Icinga 2 Agent,
+ # allowing masters and satellites to connect to our local agent
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'installIcingaAgentFirewallRule' -value {
+ if ($this.config('agent_add_firewall_rule') -eq $FALSE) {
+ $this.warn('Icinga 2 Agent Firewall Rule will not be installed.');
+ return;
+ }
+
+ $this.info([string]::Format('Trying to install Icinga 2 Agent Firewall Rule for port {0}', $this.config('agent_listen_port')));
+
+ $result = $this.startProcess('netsh', $FALSE, 'advfirewall firewall show rule name="Icinga 2 Agent Inbound by PS-Module"');
+ if ($result.Get_Item('exitcode') -eq 0) {
+ # Firewall rule is already defined -> delete it and add it again
+
+ $this.info('Icinga 2 Agent Firewall Rule already installed. Trying to remove it to add it again...');
+ $result = $this.startProcess('netsh', $TRUE, 'advfirewall firewall delete rule name="Icinga 2 Agent Inbound by PS-Module"');
+
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.error([string]::Format('Failed to remove Icinga 2 Agent Firewall rule before adding it again: {0}', $result.Get_Item('message')));
+ return;
+ } else {
+ $this.info('Icinga 2 Agent Firewall Rule has been removed. Re-Adding now...');
+ }
+ }
+
+ [string]$binaryPath = Join-Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
+ [string]$argument = 'advfirewall firewall add rule'
+ $argument += [string]::Format(' dir=in action=allow program="{0}"', $binaryPath);
+ $argument += ' name="Icinga 2 Agent Inbound by PS-Module"';
+ $argument += ' description="Inbound Firewall Rule to allow Icinga 2 masters/satellites to connect to the Icinga 2 Agent installed on this system."';
+ $argument += ' enable=yes';
+ $argument += ' remoteip=any';
+ $argument += ' localip=any';
+ $argument += [string]::Format(' localport={0}', $this.config('agent_listen_port'));
+ $argument += ' protocol=tcp';
+
+ $result = $this.startProcess('netsh', $FALSE, $argument);
+ if ($result.Get_Item('exitcode') -ne 0) {
+ # Firewall rule was not added -> print error
+ $this.error([string]::Format('Failed to install Icinga 2 Agent Firewall: {0}', $result.Get_Item('message')));
+ return;
+ }
+
+ $this.info([string]::Format('Icinga 2 Agent Firewall Rule successfully installed for port {0}', $this.config('agent_listen_port')));
+ }
+
+ #
+ # Get the default path or our custom path for the NSClient++
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getNSClientDefaultExecutablePath' -value {
+
+ if ($this.config('nsclient_directory')) {
+ return (Join-Path -Path $this.config('nsclient_directory') -ChildPath 'nscp.exe');
+ }
+
+ if (Test-Path ('C:\Program Files\NSClient++\nscp.exe')) {
+ return 'C:\Program Files\NSClient++\nscp.exe';
+ }
+
+ if (Test-Path ('C:\Program Files (x86)\NSClient++\nscp.exe')) {
+ return 'C:\Program Files (x86)\NSClient++\nscp.exe';
+ }
+
+ return '';
+ }
+
+ #
+ # In case have the Agent already installed
+ # We might use a different installation path
+ # for the Agent. This function will return
+ # the correct, valid installation path
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getInstallPath' -value {
+ [string]$agentPath = '';
+ if ($this.getProperty('cur_install_dir')) {
+ $agentPath = $this.getProperty('cur_install_dir');
+ }
+ return $agentPath;
+ }
+
+ #
+ # In case we installed the agent freshly we
+ # require to change configuration once we
+ # would like to use the Director properly
+ # This function will simply do a backup
+ # of the icinga2.conf, ensuring we can
+ # use them later again
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'backupDefaultConfig' -value {
+ [string]$configFile = Join-Path -Path $this.getProperty('config_dir') -ChildPath 'icinga2.conf';
+ [string]$configBackupFile = $configFile + 'director.bak';
+
+ # Check if a config and backup file already exists
+ # Only procceed with backup of the current config if no backup was found
+ if (Test-Path $configFile) {
+ if (-Not (Test-Path $configBackupFile)) {
+ Rename-Item $configFile $configBackupFile;
+ $this.info('Icinga 2 configuration backup successfull');
+ } else {
+ $this.warn('Default icinga2.conf backup detected. Skipping backup');
+ }
+ }
+ }
+
+ #
+ # Allow us to restart the Icinga 2 Agent
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'cleanupAgentInstaller' -value {
+ if (-Not ($this.isDownloadPathLocal())) {
+ if ($this.getInstallerPath() -And (Test-Path $this.getInstallerPath())) {
+ $this.info('Removing downloaded Icinga 2 Agent installer');
+ Remove-Item $this.getInstallerPath() | Out-Null;
+ }
+ }
+ }
+
+ #
+ # Get Api directory if Icinga 2
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getApiDirectory' -value {
+ return $this.getProperty('api_dir');
+ }
+
+ #
+ # Should we remove the Api directory content
+ # from the Agent? Can be defined by setting the
+ # -RemoveApiDirectory argument of the function builder
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'shouldFlushIcingaApiDirectory' -value {
+ return $this.config('flush_api_directory');
+ }
+
+ #
+ # Flush all content from the Icinga 2 Agent
+ # Api directory, but keep the folder structure
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'flushIcingaApiDirectory' -value {
+ if ((Test-Path $this.getApiDirectory()) -And $this.shouldFlushIcingaApiDirectory()) {
+ $this.info([string]::Format('Flushing content of "{0}"', $this.getApiDirectory()));
+ $this.stopIcingaService();
+ [System.Object]$folder = New-Object -ComObject Scripting.FileSystemObject;
+ $folder.DeleteFolder($this.getApiDirectory());
+ $this.setProperty('require_restart', 'true');
+ }
+ }
+
+ #
+ # Modify the user the Icinga services is running with
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'modifyIcingaServiceUser' -value {
+
+ # If no user is specified -> do nothing
+ if ($this.config('icinga_service_user') -eq '') {
+ return;
+ }
+
+ [System.Object]$currentUser = Get-WMIObject win32_service -Filter "Name='icinga2'";
+ [string]$credentials = $this.config('icinga_service_user');
+ [string]$newUser = '';
+ [string]$password = '';
+
+ if ($currentUser -eq $null) {
+ $this.warn('Unable to modify Icinga service user: Service not found.');
+ return;
+ }
+
+ # Check if we defined user name and password (':' cannot appear within a username)
+ # If so split them into seperate variables, otherwise simply use the string as user
+ if ($credentials.Contains(':')) {
+ [int]$delimeter = $credentials.IndexOf(':');
+ $newUser = $credentials.Substring(0, $delimeter);
+ $password = [string]::Format(' password= {0}', $credentials.Substring($delimeter + 1, $credentials.Length - 1 - $delimeter));
+ } else {
+ $newUser = $credentials;
+ }
+
+ # If the user's are identical -> do nothing
+ if ($currentUser.StartName -eq $newUser) {
+ $this.info('Icinga user was not modified. Source and target service user are identical.');
+ return;
+ }
+
+ # Try to update the service name and return an error in case of a failure
+ # In the error case we do not have to deal with cleanup, as no change was made anyway
+ $this.info([string]::Format('Updating Icinga 2 service user to {0}', $newUser));
+ $result = $this.startProcess('sc.exe', $TRUE, [string]::Format('config icinga2 obj= "{0}"{1}', $newUser, $password));
+
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.error($result.Get_Item('message'));
+ return;
+ }
+
+ # Just write the success message
+ $this.info($result.Get_Item('message'));
+
+ # Try to restart the service
+ $result = $this.restartService('icinga2');
+
+ # In case of an error try to rollback to the previous assigned user of the service
+ # If this fails aswell, set the user to 'NT AUTHORITY\NetworkService' and restart the service to
+ # ensure that the agent is atleast running and collecting some data.
+ # Of course we throw plenty of errors to notify the user about problems
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.error($result.Get_Item('message'));
+ $this.info([string]::Format('Reseting user to previous working user {0}', $currentUser.StartName));
+ $result = $this.startProcess('sc.exe', $TRUE, [string]::Format('config icinga2 obj= "{0}"{1}', $currentUser.StartName, $password));
+ $result = $this.restartService('icinga2');
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.error([string]::Format('Failed to reset Icinga 2 service user to the previous user "{0}". Setting user to "NT AUTHORITY\NetworkService" now to ensure the service integrity', $currentUser.StartName));
+ $result = $this.startProcess('sc.exe', $TRUE, 'config icinga2 obj= "NT AUTHORITY\NetworkService" password=dummy');
+ $this.info($result.Get_Item('message'));
+ $result = $this.restartService('icinga2');
+ if ($result.Get_Item('exitcode') -eq 0) {
+ $this.info('Reseting Icinga 2 service user to "NT AUTHORITY\NetworkService" successfull.');
+ return;
+ } else {
+ $this.error([string]::Format('Failed to rollback Icinga 2 service user to "NT AUTHORITY\NetworkService": {0}', $result.Get_Item('message')));
+ return;
+ }
+ }
+ }
+
+ $this.info('Icinga 2 service is running');
+ }
+
+ #
+ # Function to make restart of services easier
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'restartService' -value {
+ param([string]$service);
+
+ $this.info([string]::Format('Restarting service {0}', $service));
+
+ # Stop the current service
+ $result = $this.startProcess("sc.exe", $TRUE, "stop $service");
+
+ # Wait until the service is stopped
+ $serviceResult = $this.waitForServiceToReachState($service, 'Stopped');
+
+ # Start the service again
+ $result = $this.startProcess("sc.exe", $TRUE, "start $service");
+
+ # Wait until the service is started
+ if ($this.waitForServiceToReachState($service, 'Running') -eq $FALSE) {
+ $result.Set_Item('message', [string]::Format('Failed to restart service {0}.', $service));
+ $result.Set_Item('exitcode', '1');
+ }
+
+ return $result;
+ }
+
+ #
+ # Function to stop the Icinga 2 service
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'stopIcingaService' -value {
+ # Stop the Icinga 2 Service
+ $this.info('Stopping the Icinga 2 Service...')
+ $result = $this.startProcess("sc.exe", $TRUE, "stop icinga2");
+
+ # Wait until the service is stopped
+ $serviceResult = $this.waitForServiceToReachState('icinga2', 'Stopped');
+ $this.info('Icinga 2 service has been stopped.')
+ }
+
+ #
+ # This function will wait for a specific service until it reaches
+ # the defined state. Will break after 20 seconds with an error message
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'waitForServiceToReachState' -value {
+ param([string]$service, [string]$state);
+
+ [int]$counter = 0;
+
+ # Wait until the service reached the desired state
+ while ($TRUE) {
+
+ # Get the current state of the service
+ $serviceState = (Get-WMIObject win32_service -Filter "Name='$service'").State;
+ if ($serviceState -eq $state) {
+ break;
+ }
+
+ # Sleep a little to prevent crushing the CPU
+ Start-Sleep -Milliseconds 100;
+ $counter += 1;
+
+ # After 20 seconds break with an error. It look's like the service does not respond
+ if ($counter -gt 200) {
+ $this.error([string]::Format('Timeout reached while waiting for "{0}" to reach state "{1}". Service is not responding.', $service, $state));
+ return $FALSE;
+ }
+ }
+
+ # Wait one second and check the status again to ensure it remains within it's state
+ Start-Sleep -Seconds 1;
+
+ if ($state -ne (Get-WMIObject win32_service -Filter "Name='$service'").State) {
+ return $FALSE;
+ }
+
+ return $TRUE;
+ }
+
+ #
+ # Function to start processes and wait for their exit
+ # Will return a dictionary with results (message, error, exitcode)
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'startProcess' -value {
+ param([string]$executable, [bool]$flushNewLines, [string]$arguments);
+
+ $processData = New-Object System.Diagnostics.ProcessStartInfo;
+ $processData.FileName = $executable;
+ $processData.RedirectStandardError = $true;
+ $processData.RedirectStandardOutput = $true;
+ $processData.UseShellExecute = $false;
+ $processData.Arguments = $arguments;
+ $process = New-Object System.Diagnostics.Process;
+ $process.StartInfo = $processData;
+ $process.Start() | Out-Null;
+ $stdout = $process.StandardOutput.ReadToEnd();
+ $stderr = $process.StandardError.ReadToEnd();
+ $process.WaitForExit();
+
+ if ($flushNewLines) {
+ $stdout = $stdout.Replace("`n", '').Replace("`r", '');
+ $stderr = $stderr.Replace("`n", '').Replace("`r", '');
+ } else {
+ if ($stdout.Contains("`n")) {
+ $stdout = $stdout.Substring(0, $stdout.LastIndexOf("`n"));
+ }
+ }
+
+ $result = @{};
+ $result.Add('message', $stdout);
+ $result.Add('error', $stderr);
+ $result.Add('exitcode', $process.ExitCode);
+
+ return $result;
+ }
+
+ #
+ # Restart the Icinga 2 service and get the
+ # result if the restart failed or everything
+ # worked as expected
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'restartAgent' -value {
+ $result = $this.restartService('icinga2');
+
+ if ($result.Get_Item('exitcode') -eq 0) {
+ $this.info('Icinga 2 Agent successfully restarted.');
+ $this.setProperty('require_restart', '');
+ } else {
+ $this.error($result.Get_Item('message'));
+ }
+ }
+
+ #
+ # This function will determine if and how we create the
+ # API-Listener configuration
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getApiListenerConfiguration' -value {
+ if (-Not $this.hasCertificates() -And -Not $this.getProperty('certs_created')) {
+ $this.warn('Configuring Icinga 2 Agent without ApiListener, as certificates have not been generated.');
+ return [string]::Format('{0}/* ApiListener has not been configured, as certificates have not been generated. */', "`n`n");
+ }
+
+ [string]$apiListenerConfig = '';
+ [string]$certificateConfig = '';
+ # Icinga 2 Agent Versions below 2.8.0 will require cert_path, key_path and ca_path
+ if (-Not $this.validateVersions('2.8.0', $this.getProperty('icinga2_agent_version'))) {
+ $certificateConfig = '
+ cert_path = SysconfDir + "/icinga2/pki/' + $this.getProperty('local_hostname') + '.crt"
+ key_path = SysconfDir + "/icinga2/pki/' + $this.getProperty('local_hostname') + '.key"
+ ca_path = SysconfDir + "/icinga2/pki/ca.crt"';
+ }
+
+ $apiListenerConfig = '
+object ApiListener "api" {' + $certificateConfig + '
+ accept_commands = true
+ accept_config = ' + $this.convertBoolToString($this.config('accept_config')) + '
+ bind_host = "::"
+ bind_port = ' + [int]$this.config('agent_listen_port') + '
+}';
+
+ return $apiListenerConfig;
+ }
+
+ #
+ # Generate the new configuration for Icinga 2
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'generateIcingaConfiguration' -value {
+ if ($this.getProperty('generate_config') -eq 'true') {
+
+ $this.checkConfigInputParametersAndThrowException();
+
+ [string]$icingaCurrentConfig = '';
+ if (Test-Path $this.getIcingaConfigFile()) {
+ $icingaCurrentConfig = [System.IO.File]::ReadAllText($this.getIcingaConfigFile());
+ }
+
+ [string]$icingaNewConfig =
+'/**
+ * Icinga 2 Config - Proposed by Icinga 2 PowerShell Module
+ */
+
+/* Define our includes to run the agent properly. */
+include "constants.conf"
+include <itl>
+include <plugins>
+include <nscp>
+include <windows-plugins>
+
+/* Required for Icinga 2.8.0 and above */
+const NodeName = "' + $this.getProperty('local_hostname') + '"
+
+/* Define our block required to enable or disable Icinga 2 debug log
+ * Enable or disable it by using the PowerShell Module with
+ * argument -IcingaEnableDebugLog or by switching
+ * PowerShellIcinga2EnableDebug to true or false manually.
+ * true: Debug log is active
+ * false: Debug log is deactivated
+ * IMPORTANT: ";" after true or false has to remain to allow the
+ * PowerShell Module to switch this feature on or off.
+ */
+const PowerShellIcinga2EnableDebug = false;
+const PowerShellIcinga2EnableLog = true;
+
+if (PowerShellIcinga2EnableDebug) {
+ object FileLogger "debug-file" {
+ severity = "debug"
+ path = LocalStateDir + "/log/icinga2/debug.log"
+ }
+}
+
+/* Try to define a constant for our NSClient++ installation
+ * IMPORTANT: If the NSClient++ is installed newly to the system, the
+ * Icinga Service has to be restarted in order to set this variable
+ * correctly. If the NSClient++ is installed over the PowerShell Module,
+ * the Icinga 2 Service is restarted automaticly.
+ */
+if (!globals.contains("NscpPath")) {
+ NscpPath = dirname(msi_get_component_path("{5C45463A-4AE9-4325-96DB-6E239C034F93}"))
+}
+
+/* Enable our default main logger feature to write log output. */
+if (PowerShellIcinga2EnableLog) {
+ object FileLogger "main-log" {
+ severity = "information"
+ path = LocalStateDir + "/log/icinga2/icinga2.log"
+ }
+}
+
+/* All informations required to correctly connect to our parent Icinga 2 nodes. */
+object Endpoint "' + $this.getProperty('local_hostname') + '" {}
+' + $this.getProperty('endpoint_objects') + '
+/* Define the zone and its containing endpoints we should communicate with. */
+object Zone "' + $this.config('parent_zone') + '" {
+ endpoints = [ ' + $this.getProperty('endpoint_nodes') +' ]
+}
+
+/* All of our global zones, check commands and other configuration are synced into.
+ * Director global zone must be defined in case the Icinga Director is beeing used.
+ * Default value for this is "director-global".
+ * All additional zones can be configured with -GlobalZones argument.
+ * IMPORTANT: If -GlobalZones argument is used, the Icinga Director global zones has
+ * to be defined as well within the argument array.
+ */
+' + $this.getProperty('global_zones') + '
+/* Define a zone for our current agent and set our parent zone for proper communication. */
+object Zone "' + $this.getProperty('local_hostname') + '" {
+ parent = "' + $this.config('parent_zone') + '"
+ endpoints = [ "' + $this.getProperty('local_hostname') + '" ]
+}
+
+/* Configure all settings we require for our API listener to properly work.
+ * This will include the certificates, if we accept configurations which
+ * can be changed with argument -AcceptConfig and the bind informations.
+ * The bind_port can be modified with argument -AgentListenPort.
+ */' + $this.getApiListenerConfiguration();
+
+ $this.setProperty('new_icinga_config', $icingaNewConfig);
+ $this.setProperty('old_icinga_config', $icingaCurrentConfig);
+ }
+ }
+
+ #
+ # Generate a hash for old and new config
+ # and determine if the configuration has changed
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'hasConfigChanged' -value {
+
+ if ($this.getProperty('generate_config') -eq 'false') {
+ return $FALSE;
+ }
+ if (-Not $this.getProperty('new_icinga_config')) {
+ throw 'New Icinga 2 configuration not generated. Please call "generateIcingaConfiguration" before.';
+ }
+
+ [string]$oldConfigHash = $this.getHashFromString($this.getProperty('old_icinga_config'));
+ [string]$newConfigHash = $this.getHashFromString($this.getProperty('new_icinga_config'));
+
+ $this.debug([string]::Format('Old Config Hash: "{0}" New Hash: "{1}"', $oldConfigHash, $newConfigHash));
+
+ if ($oldConfigHash -eq $newConfigHash) {
+ return $FALSE;
+ }
+
+ return $TRUE;
+ }
+
+ #
+ # Generate a SHA1 Hash from a provided string
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getHashFromString' -value {
+ param([string]$text);
+ [System.Object]$algorithm = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider;
+ $hash = [System.Text.Encoding]::UTF8.GetBytes($text);
+ $hashInBytes = $algorithm.ComputeHash($hash);
+ [string]$result = '';
+ foreach($byte in $hashInBytes) {
+ $result += $byte.ToString();
+ }
+ return $result;
+ }
+
+ #
+ # Return the path to the Icinga 2 config file
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getIcingaConfigFile' -value {
+ return (Join-Path -Path $this.getProperty('config_dir') -ChildPath 'icinga2.conf');
+ }
+
+ #
+ # Create Icinga 2 configuration file based
+ # on Director settings
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'writeConfig' -value {
+ param([string]$configData);
+
+ if (-Not (Test-Path $this.getProperty('config_dir'))) {
+ $this.warn('Unable to write Icinga 2 configuration. The required directory was not found. Possibly the Icinga 2 Agent is not installed.');
+ return;
+ }
+
+ # Write new configuration to file
+ $this.info([string]::Format('Writing icinga2.conf to "{0}"', $this.getProperty('config_dir')));
+ [System.IO.File]::WriteAllText($this.getIcingaConfigFile(), $configData);
+ $this.setProperty('require_restart', 'true');
+ }
+
+ #
+ # Write old coniguration again
+ # just in case we received errors
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'rollbackConfig' -value {
+ # Write new configuration to file
+ $this.info([string]::Format('Rolling back previous icinga2.conf to "{0}"', $this.getProperty('config_dir')));
+ [System.IO.File]::WriteAllText($this.getIcingaConfigFile(), $this.getProperty('old_icinga_config'));
+ $this.setProperty('require_restart', 'true');
+ }
+
+ #
+ # Provide a result of an operation (string) and
+ # the intended match value. In case every was
+ # ok, the function will return an info message
+ # with the result. Otherwise it will thrown an
+ # exception
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'printResultOkOrException' -value {
+ param([string]$result, [string]$expected);
+ if ($result -And $expected) {
+ if (-Not ($result -Like $expected)) {
+ throw $result;
+ } else {
+ $this.info($result);
+ }
+ } elseif ($result) {
+ $this.info($result);
+ }
+ }
+
+ #
+ # Create Host-Certificates for Icinga 2
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'createHostCertificates' -value {
+ param([string]$hostname, [string]$certDir);
+
+ $this.info('Generating Host certificates required by Icinga 2');
+ [string]$icingaBinary = Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
+ $result = $this.startProcess($icingaBinary, $FALSE, [string]::Format('pki new-cert --cn {0} --key {1}{0}.key --cert {1}{0}.crt',
+ $hostname,
+ $certDir
+ )
+ );
+
+ if ($result.Get_Item('exitcode') -ne 0) {
+ throw $result.Get_Item('message');
+ }
+ $this.info($result.Get_Item('message'));
+ }
+
+ #
+ # Fix certificate naming for upper / lower case changes
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'fixCertificateNames' -value {
+ param([string]$hostname, [string]$certDir);
+ # Rename the certificates to apply possible upper / lower case naming changes
+ # which is not done by Windows by default
+ Move-Item (Join-Path -Path $certDir -ChildPath ($hostname + '.key')) (Join-Path -Path $certDir -ChildPath ($hostname + '.key'))
+ Move-Item (Join-Path -Path $certDir -ChildPath ($hostname + '.crt')) (Join-Path -Path $certDir -ChildPath ($hostname + '.crt'))
+ }
+
+ #
+ # Generate the Icinga 2 SSL certificate to ensure the communication between the
+ # Agent and the Master can be established in first place
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'generateCertificates' -value {
+
+ [string]$icingaCertDir = Join-Path $this.getProperty('cert_dir') -ChildPath '\';
+ [string]$icingaBinary = Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
+ [string]$agentName = $this.getProperty('local_hostname');
+
+ if (-Not (Test-Path $icingaBinary)) {
+ $this.warn('Unable to generate Icinga 2 certificates. Icinga 2 executable not found. It looks like the Icinga 2 Agent is not installed.');
+ return;
+ }
+
+ if (-Not $this.getProperty('local_hostname')) {
+ $this.info('Skipping function for generating certificates, as hostname is not specified within the module.');
+ return;
+ }
+
+ # Handling for Icinga 2.8.0 and above: CA-Proxy support
+ if ($this.config('caproxy')) {
+ if (-Not $this.validateVersions('2.8.0', $this.getProperty('icinga2_agent_version'))) {
+ throw 'The argument "-CAProxy" is only supported by Icinga Version 2.8.0 and above.';
+ return;
+ }
+
+ if (-Not $this.config('ca_certificate_path')) {
+ throw 'You will require to specify a source path of your CA certificate with -CACertificatePath in order to use CA proxy certificate generation.';
+ }
+
+ # Generate the certificate
+ [string]$caDestPath = (Join-Path $icingaCertDir -ChildPath '\ca.crt');
+ $this.createHostCertificates($agentName, $icingaCertDir);
+ $this.fixCertificateNames($agentName, $icingaCertDir);
+ $this.setProperty('require_restart', 'true');
+ $this.info('Your host certificate has been generated. Please review the request on your Icinga CA with "icinga2 ca list" and sign it with "icinga2 ca sign <request_id>".');
+ $this.info([string]::Format('Trying to copy your specified CA certificate "{0}" to "{1}".',
+ $this.config('ca_certificate_path'),
+ $caDestPath
+ ));
+ if (-Not (Test-Path $this.config('ca_certificate_path'))) {
+ throw [string]::Format('Failed to copy your CA certificate from "{0}" to "{1}". Your source destination does not exist.',
+ $this.config('ca_certificate_path'),
+ $caDestPath
+ );
+ return;
+ }
+ Copy-Item $this.config('ca_certificate_path') $caDestPath;
+ $this.setProperty('certs_created', $TRUE);
+ return;
+ }
+
+ if ($this.config('ca_server') -And $this.getProperty('icinga_ticket')) {
+ # Generate the certificate
+ $this.createHostCertificates($agentName, $icingaCertDir);
+
+ # Save Certificate
+ $this.info("Storing Icinga 2 certificates");
+ $result = $this.startProcess($icingaBinary, $FALSE, [string]::Format('pki save-cert --key {0}{1}.key --trustedcert {0}trusted-master.crt --host {2}',
+ $icingaCertDir,
+ $agentName,
+ $this.config('ca_server')
+ )
+ );
+ if ($result.Get_Item('exitcode') -ne 0) {
+ throw $result.Get_Item('message');
+ }
+ $this.info($result.Get_Item('message'));
+
+ # Validate if set against a given fingerprint for the CA
+ if (-Not $this.validateCertificate([string]::Format('{0}trusted-master.crt', $icingaCertDir))) {
+ throw 'Failed to validate against CA authority';
+ }
+
+ # Request certificate
+ $this.info("Requesting Icinga 2 certificates");
+ $result = $this.startProcess($icingaBinary, $FALSE, [string]::Format('pki request --host {0} --port {1} --ticket {2} --key {3}{4}.key --cert {3}{4}.crt --trustedcert {3}trusted-master.crt --ca {3}ca.crt',
+ $this.config('ca_server'),
+ $this.config('ca_port'),
+ $this.getProperty('icinga_ticket'),
+ $icingaCertDir,
+ $agentName
+ )
+ );
+ if ($result.Get_Item('exitcode') -ne 0) {
+ if ($this.getProperty('agent_name_change')) {
+ $this.exception('You have changed the naming of the Agent (upper / lower case) and therefor your certificates are no longer valid. Certificate generation failed because of a possible wrong ticket. Please ensure to set the "hostname" within the Icinga 2 configuration correctly and re-run this script.');
+ }
+ throw $result.Get_Item('message');
+ }
+ $this.info($result.Get_Item('message'));
+ $this.fixCertificateNames($agentName, $icingaCertDir);
+ $this.setProperty('require_restart', 'true');
+ $this.setProperty('certs_created', $TRUE);
+ } else {
+ $this.info('Skipping certificate generation. One or more of the following arguments is not set: -CAServer <server> -Ticket <ticket>');
+ }
+ }
+
+ #
+ # Validate against a given fingerprint if we are connected to the correct CA
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'validateCertificate' -value {
+ param([string] $certificate);
+
+ [System.Object]$certFingerprint = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2;
+ $certFingerprint.Import($certificate);
+ $this.info([string]::Format('Certificate fingerprint: "{0}"', $certFingerprint.Thumbprint));
+
+ if ($this.config('ca_fingerprint')) {
+ if (-Not ($this.config('ca_fingerprint') -eq $certFingerprint.Thumbprint)) {
+ $this.error([string]::Format('CA fingerprint does not match! Expected: "{0}", given: "{1}"',
+ $certFingerprint.Thumbprint,
+ $this.config('ca_fingerprint')
+ )
+ );
+ return $FALSE;
+ } else {
+ $this.info('CA fingerprint validation successfull');
+ return $TRUE;
+ }
+ }
+
+ $this.warn('CA fingerprint validation disabled');
+ return $TRUE;
+ }
+
+ #
+ # In case we migrate from an Icinga 2 Version with the new certificate path to
+ # a version with the old one, we require to migrate the certificates
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'migrateCertificates' -value {
+ if (-Not $this.getProperty('require_cert_migration')) {
+ return;
+ }
+
+ [string]$agentName = $this.getProperty('local_hostname');
+
+ [string]$caPath = Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\ca.crt';
+ [string]$newCA = Join-Path -Path $this.getProperty('config_dir') -ChildPath 'pki\ca.crt';
+ [string]$certPath = Join-Path -Path $Env:ProgramData -ChildPath ([string]::Format('icinga2\var\lib\icinga2\certs\{0}.crt', $agentName));
+ [string]$newCertPath = Join-Path -Path $this.getProperty('config_dir') -ChildPath ([string]::Format('pki\{0}.crt', $agentName));
+ [string]$keyPath = Join-Path -Path $Env:ProgramData -ChildPath ([string]::Format('icinga2\var\lib\icinga2\certs\{0}.key', $agentName));
+ [string]$newKeyPath = Join-Path -Path $this.getProperty('config_dir') -ChildPath ([string]::Format('pki\{0}.key', $agentName));
+
+ if (Test-Path $caPath) {
+ Copy-Item $caPath $newCA;
+ $this.info([string]::Format('Migrating ca.crt from "{0}" to "{1}".', $caPath, $newCA));
+ }
+
+ if (Test-Path $certPath) {
+ Copy-Item $certPath $newCertPath;
+ $this.info([string]::Format('Migrating {0}.crt from "{1}" to "{2}".', $agentName, $certPath, $newCertPath));
+ }
+
+ if (Test-Path $keyPath) {
+ Copy-Item $keyPath $newKeyPath;
+ $this.info([string]::Format('Migrating {0}.crt from "{1}" to "{2}".', $agentName, $keyPath, $newKeyPath));
+ }
+ }
+
+ #
+ # Check the Icinga install directory and determine
+ # if the certificates are both available for the
+ # Agent. If not, return FALSE
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'hasCertificates' -value {
+ [string]$icingaCertDir = Join-Path -Path $this.getProperty('cert_dir') -ChildPath '\';
+ [string]$agentName = $this.getProperty('local_hostname');
+ [bool]$filesExist = $FALSE;
+ # First check if the files in generell exist
+ if (
+ ((Test-Path ((Join-Path -Path $icingaCertDir -ChildPath $agentName) + '.key'))) `
+ -And (Test-Path ((Join-Path -Path $icingaCertDir -ChildPath $agentName) + '.crt')) `
+ -And (Test-Path (Join-Path -Path $icingaCertDir -ChildPath 'ca.crt'))
+ ) {
+ $filesExist = $TRUE;
+ }
+
+ # In case they do, check if the characters (upper / lowercase) are matching as well
+ if ($filesExist -eq $TRUE) {
+
+ [string]$hostCRT = [string]::Format('{0}.crt', $agentName);
+ [string]$hostKEY = [string]::Format('{0}.key', $agentName);
+
+ # Get all files inside your certificate directory
+ $certificates = Get-ChildItem -Path $icingaCertDir;
+ # Now loop each file and match their name with our hostname
+ foreach ($cert in $certificates) {
+ if ($cert.Name.toLower() -eq $hostCRT.toLower() -Or $cert.Name.toLower() -eq $hostKEY.toLower()) {
+ $file = $cert.Name.Replace('.key', '').Replace('.crt', '');
+ if (-Not ($file -clike $agentName)) {
+ $this.warn([string]::Format('Certificate file {0} is not matching the hostname {1}. Certificate generation is required.', $cert.Name, $agentName));
+ $this.setProperty('agent_name_change', $true);
+ return $FALSE;
+ }
+ }
+ }
+ }
+
+ return $filesExist;
+ }
+
+ #
+ # Have we passed an argument to force
+ # the creation of the certificates?
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'forceCertificateGeneration' -value {
+ return $this.config('force_cert');
+ }
+
+ #
+ # Is the current Agent the version
+ # we would like to install?
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'isAgentUpToDate' -value {
+ if ($this.canInstallAgent() -And $this.getProperty('agent_version') -eq $this.config('agent_version')) {
+ return $TRUE;
+ }
+
+ return $FALSE;
+ }
+
+ #
+ # Print a message telling us the installed
+ # and intended version of the Agent
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'printAgentUpdateMessage' -value {
+ $this.info([string]::Format('Current Icinga 2 Agent Version ({0}) is not matching intended version ({1}). Downloading new version...',
+ $this.getProperty('agent_version'),
+ $this.config('agent_version')
+ )
+ );
+ }
+
+ #
+ # Do we allow Agent updates / downgrades?
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'allowAgentUpdates' -value {
+ return $this.config('allow_updates');
+ }
+
+ #
+ # Have we specified a version to install the Agent?
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'canInstallAgent' -value {
+ if ($this.config('download_url') -And $this.config('agent_version')) {
+ return $TRUE;
+ }
+
+ if (-Not $this.config('download_url') -And -Not $this.config('agent_version')) {
+ $this.warn('Icinga 2 Agent will not be installed. Arguments -DownloadUrl and -InstallAgentVersion both not defined.');
+ return $FALSE;
+ }
+
+ if (-Not $this.config('agent_version')) {
+ $this.warn('Icinga 2 Agent will not be installed. Argument -InstallAgentVersion is not defined.');
+ return $FALSE;
+ }
+
+ if (-Not $this.config('download_url')) {
+ $this.warn('Icinga 2 Agent will not be installed. Argument -DownloadUrl is not defined.');
+ return $FALSE;
+ }
+
+ return $FALSE;
+ }
+
+ #
+ # Check if all required arguments for writing a valid
+ # configuration are set
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'checkConfigInputParametersAndThrowException' -value {
+ if (-Not $this.getProperty('local_hostname')) {
+ throw 'Argument -AgentName <name> required for config generation.';
+ }
+ if (-Not $this.config('parent_zone')) {
+ throw 'Argument -ParentZone <name> required for config generation.';
+ }
+ if (-Not $this.getProperty('endpoint_nodes') -Or -Not $this.getProperty('endpoint_objects')) {
+ throw 'Argument -Endpoints <name> requires atleast one defined endpoint.';
+ }
+ }
+
+ #
+ # Execute a check with Icinga2 daemon -C
+ # to ensure the configuration is valid
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'isIcingaConfigValid' -value {
+ param([bool] $checkInternal = $TRUE);
+ if (-Not $this.config('parent_zone') -And $checkInternal) {
+ throw 'Parent Zone not defined. Please specify it with -ParentZone <name>';
+ }
+ $icingaBinary = Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
+
+ if (Test-Path $icingaBinary) {
+ $result = $this.startProcess($icingaBinary, $FALSE, 'daemon -C');
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.error($result.Get_Item('message'));
+ return $FALSE;
+ }
+ } else {
+ $this.warn('Icinga 2 config validation not possible. Icinga 2 executable not found. Possibly the Agent is not installed.');
+ }
+ return $TRUE;
+ }
+
+ #
+ # Returns true or false, depending
+ # if any changes were made requiring
+ # the Icinga 2 Agent to become restarted
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'madeChanges' -value {
+ return $this.getProperty('require_restart');
+ }
+
+ #
+ # Apply possible configuration changes to
+ # our Icinga 2 Agent
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'applyPossibleConfigChanges' -value {
+ if ($this.hasConfigChanged() -And $this.getProperty('generate_config') -eq 'true') {
+ $this.backupDefaultConfig();
+ $this.writeConfig($this.getProperty('new_icinga_config'));
+
+ # Check if the config is valid and rollback otherwise
+ if (-Not $this.isIcingaConfigValid()) {
+ $this.error('Icinga 2 config validation failed. Rolling back to previous version.');
+ if (-Not $this.hasCertificates()) {
+ $this.error('Icinga 2 certificates not found. Please generate the certificates over this module or add them manually.');
+ }
+ $this.rollbackConfig();
+ if ($this.isIcingaConfigValid($FALSE)) {
+ $this.info('Rollback of Icinga 2 configuration successfull.');
+ } else {
+ throw 'Icinga 2 config rollback failed. Please check the icinga2.log';
+ }
+ } else {
+ $this.info('Icinga 2 configuration check successfull.');
+ }
+ } else {
+ # Throw an exception in case we use a parent zone which is a global zone
+ foreach ($zone in $this.config('global_zones')) {
+ if ($zone -eq $this.config('parent_zone')) {
+ $this.exception([string]::Format('The zone specified for the Icinga 2 Agent to connect to is set to "{0}". This is a global zone which cannot be used. Please review either your arguments used for this module or the Host-Template within the Icinga Director to use the correct zone for this Agent.', $this.config('parent_zone')));
+ }
+ }
+ # In case no parent endpoints are configured, print a warning as we can't write valid Icinga 2 configuration
+ if (-Not $this.config('parent_endpoints')) {
+ $this.warn('No parent endpoints have been defined within the module call. Either specify them by using the "-ParentEndpoints" argument or ensure you configured your Icinga Director properly in case you are using the Self-Service API. Icinga2.conf has not been generated.');
+ }
+ $this.info('icinga2.conf did not change or required parameters not set. Nothing to do');
+ }
+ }
+
+ #
+ # Enable or disable the Icinga 2 debug log
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'switchIcingaLogging' -value {
+ # In case the config is not valid -> do nothing
+ if (-Not $this.isIcingaConfigValid($FALSE)) {
+ throw 'Unable to process Icinga 2 debug configuration. The icinga2.conf is corrupt! Please check the icinga2.log';
+ }
+
+ # If there is no config file defined -> do nothing
+ if (-Not (Test-Path $this.getIcingaConfigFile())) {
+ return;
+ }
+
+ [string]$icingaCurrentConfig = [System.IO.File]::ReadAllText($this.getIcingaConfigFile());
+ [string]$newIcingaConfig = $icingaCurrentConfig;
+
+ if ($this.config('icinga_enable_debug_log')) {
+ $this.info('Trying to enable debug log for Icinga 2...');
+ if ($newIcingaConfig.Contains('const PowerShellIcinga2EnableDebug = false;')) {
+ $newIcingaConfig = $newIcingaConfig.Replace('const PowerShellIcinga2EnableDebug = false;', 'const PowerShellIcinga2EnableDebug = true;');
+ $this.info('Icinga 2 debug log has been enabled');
+ } else {
+ $this.info('Icinga 2 debug log is already enabled or configuration not found');
+ }
+ } else {
+ $this.info('Trying to disable debug log for Icinga 2...');
+ if ($newIcingaConfig.Contains('const PowerShellIcinga2EnableDebug = true;')) {
+ $newIcingaConfig = $newIcingaConfig.Replace('const PowerShellIcinga2EnableDebug = true;', 'const PowerShellIcinga2EnableDebug = false;');
+ $this.info('Icinga 2 debug log has been disabled');
+ } else {
+ $this.info('Icinga 2 debug log is not enabled or configuration not found');
+ }
+ }
+
+ if ($this.config('icinga_disable_log') -eq $FALSE) {
+ $this.info('Trying to enable logging for Icinga 2...');
+ if ($newIcingaConfig.Contains('const PowerShellIcinga2EnableLog = false;')) {
+ $newIcingaConfig = $newIcingaConfig.Replace('const PowerShellIcinga2EnableLog = false;', 'const PowerShellIcinga2EnableLog = true;');
+ $this.info('Icinga 2 logging has been enabled');
+ } else {
+ $this.info('Icinga 2 logging is already enabled or configuration not found');
+ }
+ } else {
+ $this.info('Trying to disable logging for Icinga 2...');
+ if ($newIcingaConfig.Contains('const PowerShellIcinga2EnableLog = true;')) {
+ $newIcingaConfig = $newIcingaConfig.Replace('const PowerShellIcinga2EnableLog = true;', 'const PowerShellIcinga2EnableLog = false;');
+ $this.info('Icinga 2 logging has been disabled');
+ } else {
+ $this.info('Icinga 2 logging is not enabled or configuration not found');
+ }
+ }
+
+ # In case we made a modification to the configuration -> write it
+ if ($newIcingaConfig -ne $icingaCurrentConfig) {
+ $this.writeConfig($newIcingaConfig);
+ # Validate the config if it is valid
+ if (-Not $this.isIcingaConfigValid($FALSE)) {
+ # if not write the old configuration again
+ $this.writeConfig($icingaCurrentConfig);
+ if (-Not $this.isIcingaConfigValid($FALSE)) {
+ throw 'Critical exception: Something went wrong while processing logging configuration. The Icinga 2 config is corrupt! Please check the icinga2.log';
+ }
+ }
+ }
+ }
+
+ #
+ # Ensure we get the hostname or FQDN
+ # from the PowerShell to make things more
+ # easier
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'fetchHostnameOrFQDN' -value {
+
+ # Add additional variables to our config for more user-friendly usage
+ [string]$host_fqdn = [string]::Format('{0}.{1}',
+ (Get-WmiObject win32_computersystem).DNSHostName,
+ (Get-WmiObject win32_computersystem).Domain
+ );
+
+ if ([string]::IsNullOrEmpty($this.config('agent_name')) -eq $FALSE) {
+ $this.setProperty('local_hostname', $this.config('agent_name'));
+ $this.setProperty('fqdn', $host_fqdn);
+ $this.setProperty('hostname', $this.config('agent_name'));
+ } else {
+ if ($this.config('fetch_agent_fqdn') -And (Get-WmiObject win32_computersystem).Domain) {
+ [string]$hostname = [string]::Format('{0}.{1}',
+ (Get-WmiObject win32_computersystem).DNSHostName,
+ (Get-WmiObject win32_computersystem).Domain
+ );
+ $this.setProperty('local_hostname', $hostname);
+ } elseif ($this.config('fetch_agent_name')) {
+ [string]$hostname = (Get-WmiObject win32_computersystem).DNSHostName;
+ $this.setProperty('local_hostname', $hostname);
+ }
+
+ $this.info([string]::Format('Setting internal Agent Name to "{0}"', $this.getProperty('local_hostname')));
+
+ [string]$hostname = (Get-WmiObject win32_computersystem).DNSHostName;
+
+ $this.setProperty('fqdn', $host_fqdn);
+ $this.setProperty('hostname', $hostname);
+ }
+
+ if (-Not $this.getProperty('local_hostname')) {
+ $this.warn('You have not specified an Agent Name or turned on to auto fetch this information.');
+ }
+ }
+
+ #
+ # Retreive the current IP-Address of the Host
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'fetchHostIPAddress' -value {
+
+ # First try to lookup the IP by the FQDN
+ if ($this.doLookupIPAddressesForHostname($this.getProperty('fqdn'))) {
+ return;
+ }
+
+ # Now take a look for the given hostname
+ if ($this.doLookupIPAddressesForHostname($this.getProperty('hostname'))) {
+ return;
+ }
+
+ # If still nothing is found, look on the entire host
+ if ($this.doLookupIPAddressesForHostname("")) {
+ return;
+ }
+
+ $this.exception('Failed to lookup any IP-Address for this host');
+ }
+
+ #
+ # This function will try to locate the IPv4 address used
+ # for communicating with the network
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'lookupPrimaryIPv4Address' -value {
+ # First execute nslookup for your FQDN and hostname to check if this
+ # host is registered and receive it's IP address
+ [System.Collections.Hashtable]$fqdnLookup = $this.startProcess('nslookup.exe', $TRUE, $this.getProperty('fqdn'));
+ [System.Collections.Hashtable]$hostnameLookup = $this.startProcess('nslookup.exe', $TRUE, $this.getProperty('hostname'));
+
+ # Now get the message of our result we should work with (nslookup output)
+ [string]$fqdnLookup = $fqdnLookup.Get_Item('message');
+ [string]$hostnameLookup = $hostnameLookup.Get_Item('message');
+ # Get our basic IP first
+ [string]$usedIP = $this.getProperty('ipaddress');
+
+ # First try to lookup the basic address. If it is not contained, look further
+ if ($this.isIPv4AddressInsideLookup($fqdnLookup, $hostnameLookup, $usedIP) -eq $FALSE) {
+ [int]$ipCount = $this.getProperty('ipv4_count');
+ [bool]$found = $FALSE;
+ # Loop through all found IPv4 IP's and try to locate the correct one
+ for ($index = 0; $index -lt $ipCount; $index++) {
+ $usedIP = $this.getProperty([string]::Format('ipaddress[{0}]', $index));
+ if ($this.isIPv4AddressInsideLookup($fqdnLookup, $hostnameLookup, $usedIP)) {
+ # Swap IP values once we found a match and exit this loop
+ $this.setProperty([string]::Format('ipaddress[{0}]', $index), $this.getProperty('ipaddress'));
+ $this.setProperty('ipaddress', $usedIP);
+ $found = $TRUE;
+ break;
+ }
+ }
+
+ if ($found -eq $FALSE) {
+ $this.warn([string]::Format('Failed to lookup primary IP for this host. Unable to match nslookup against any IPv4 addresses on this system. Using {0} as default now. Access it with &ipaddress& for all JSON requests.',
+ $this.getProperty('ipaddress')
+ )
+ );
+ return;
+ }
+ }
+
+ $this.info([string]::Format('Setting IP {0} as primary IP for this host for all requests. Access it with &ipaddress& for all JSON requests.',
+ $usedIP
+ )
+ );
+ }
+
+ #
+ # Check if inside our lookup the IP-Address is found
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'isIPv4AddressInsideLookup' -value {
+ param([string]$fqdnLookup, [string]$hostnameLookup, [string]$ipv4Address);
+
+ if ($fqdnLookup.Contains($ipv4Address) -Or $hostnameLookup.Contains($ipv4Address)) {
+ return $TRUE;
+ }
+
+ return $FALSE;
+ }
+
+ #
+ # Add all found IP-Addresses to our property array
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'doLookupIPAddressesForHostname' -value {
+ param([string]$hostname);
+
+ $this.info([string]::Format('Trying to fetch Host IP-Address for hostname: {0}', $hostname));
+ try {
+ [array]$ipAddressArray = [Net.DNS]::GetHostEntry($hostname).AddressList;
+ $this.addHostIPAddressToProperties($ipAddressArray);
+ return $TRUE;
+ } catch {
+ # Write an error in case something went wrong
+ $this.warn([string]::Format('Failed to lookup IP-Address with DNS-Lookup for "{0}": {1}',
+ $hostname,
+ $_.Exception.Message
+ )
+ );
+ }
+ return $FALSE;
+ }
+
+ #
+ # Add all found IP-Addresses to our property array
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'addHostIPAddressToProperties' -value {
+ param($ipArray);
+
+ [int]$ipV4Index = 0;
+ [int]$ipV6Index = 0;
+
+ foreach ($address in $ipArray) {
+ # Split config attributes for IPv4 and IPv6 into different values
+ if ($address.AddressFamily -eq 'InterNetwork') { #IPv4
+ # If the first entry of our default ipaddress is empty -> add it
+ if ($this.getProperty('ipaddress') -eq $null) {
+ $this.setProperty('ipaddress', $address);
+ }
+ # Now add the IP's with an array like construct
+ $this.setProperty([string]::Format('ipaddress[{0}]', $ipV4Index), $address);
+ $ipV4Index += 1;
+ } else { #IPv6
+ # If the first entry of our default ipaddress is empty -> add it
+ if ($this.getProperty('ipaddressV6') -eq $null) {
+ $this.setProperty('ipaddressV6', $address);
+ }
+ # Now add the IP's with an array like construct
+ $this.setProperty([string]::Format('ipaddressV6[{0}]', $ipV6Index), $address);
+ $ipV6Index += 1;
+ }
+ }
+ $this.setProperty('ipv4_count', $ipV4Index);
+ $this.setProperty('ipv6_count', $ipV6Index);
+ }
+
+ #
+ # Transform the hostname to upper or lower case if required
+ # 0: Do nothing (default)
+ # 1: Transform to lower case
+ # 2: Transform to upper case
+ #
+ $installer | Add-Member -MemberType ScriptMethod -name 'doTransformHostname' -Value {
+ [string]$hostname = $this.getProperty('local_hostname');
+ [int]$type = $this.config('transform_hostname');
+ switch ($type) {
+ 1 { $hostname = $hostname.ToLower(); }
+ 2 { $hostname = $hostname.ToUpper(); }
+ Default {} # Do nothing by default
+ }
+
+ if ($hostname -cne $this.getProperty('local_hostname')) {
+ $this.info([string]::Format('Transforming Agent Name to {0}', $hostname));
+ }
+
+ $this.setProperty('local_hostname', $hostname);
+ }
+
+ #
+ # Allow the replacing of placeholders within a JSON-String
+ #
+ $installer | Add-Member -MemberType ScriptMethod -name 'doReplaceJSONPlaceholders' -Value {
+ param([string]$jsonString);
+
+ # Replace the encoded & with the original symbol at first
+ $jsonString = $jsonString.Replace('\u0026', '&');
+
+ # &hostname& => hostname
+ $jsonString = $jsonString.Replace('&hostname&', $this.getProperty('hostname'));
+
+ # &hostname.lowerCase& => hostname to lower
+ $jsonString = $jsonString.Replace('&hostname.lowerCase&', $this.getProperty('hostname').ToLower());
+
+ # &hostname.upperCase& => hostname to upper
+ $jsonString = $jsonString.Replace('&hostname.upperCase&', $this.getProperty('hostname').ToUpper());
+
+ # &fqdn& => fqdn
+ $jsonString = $jsonString.Replace('&fqdn&', $this.getProperty('fqdn'));
+
+ # &fqdn.lowerCase& => fqdn to lower
+ $jsonString = $jsonString.Replace('&fqdn.lowerCase&', $this.getProperty('fqdn').ToLower());
+
+ # &fqdn.upperCase& => fqdn to upper
+ $jsonString = $jsonString.Replace('&fqdn.upperCase&', $this.getProperty('fqdn').ToUpper());
+
+ # hostname_placeholder => current hostname (either FQDN, hostname, with plain, upper or lower case)
+ $jsonString = $jsonString.Replace('&hostname_placeholder&', $this.getProperty('local_hostname'));
+
+ # Try to replace our IP-Address
+ if ($jsonString.Contains('&ipaddressV6')) {
+ $jsonString = $this.doReplaceJSONIPAddress($jsonString, 'ipaddressV6');
+ } elseif ($jsonString.Contains('&ipaddress')) {
+ $jsonString = $this.doReplaceJSONIPAddress($jsonString, 'ipaddress');
+ }
+
+ # Encode the & again to receive a proper JSON
+ $jsonString = $jsonString.Replace('&', '\u0026');
+
+ return $jsonString;
+ }
+
+ #
+ # Allow the replacing of added IPv4 and IPv6 address
+ #
+ $installer | Add-Member -MemberType ScriptMethod -name 'doReplaceJSONIPAddress' -Value {
+ param([string]$jsonString, [string]$ipType);
+
+ # Add our & delimeter to begin with
+ [string]$ipSearchPattern = '&' + $ipType;
+
+ # Now locate the string and cut everything away until only our & tag for the string shall be remaining, including the array placeholder
+ [string]$ipAddressEnd = $jsonString.Substring($jsonString.IndexOf($ipSearchPattern) + $ipType.Length + 1, $jsonString.Length - $jsonString.IndexOf($ipSearchPattern) - $ipType.Length - 1);
+ # Ensure we still got an ending &, otherwise throw an error
+ if ($ipAddressEnd.Contains('&')) {
+ # Now cut everything until the first & we found
+ $ipAddressEnd = $ipAddressEnd.Substring(0, $ipAddressEnd.IndexOf('&'));
+ # Build together our IP-Address string, which could be for example ipaddress[1]
+ [string]$ipAddressString = $ipType + $ipAddressEnd;
+
+ # Now replace this finding with our config attribute
+ $jsonString = $jsonString.Replace('&' + $ipAddressString + '&', $this.getProperty($ipAddressString));
+ } else {
+ # If something goes wrong we require to notify our user
+ $this.error([string]::Format('Failed to replace IP-Address placeholder. Invalid format for IP-Type {0}',
+ $ipType
+ )
+ );
+ }
+
+ # Return our new JSON-String
+ return $jsonString;
+ }
+
+ #
+ # This function will allow us to create a
+ # host object directly inside the Icinga Director
+ # with a provided JSON string
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'createHostInsideIcingaDirector' -value {
+
+ if ($this.config('director_url') -And $this.getProperty('local_hostname')) {
+ if ($this.getProperty('use_self_service_api')) {
+
+ if ($this.getProperty('icinga_host_exist')) {
+ $this.info('Host is already registered within Icinga Director.');
+ return;
+ }
+
+ if ($this.getProperty('no_valid_api_token')) {
+ $this.info('Skipping host creation over Icinga Director Self-Service API, as no valid token has been specified.');
+ return;
+ }
+
+ # If not, try to create the host and fetch the API key
+ [string]$apiKey = $this.config('director_auth_token');
+ [string]$url = [string]::Format('{0}self-service/register-host?name={1}&key={2}',
+ $this.config('director_url'),
+ $this.getProperty('local_hostname'),
+ $apiKey
+ );
+ [string]$json = '';
+ # If no JSON Object is defined (should be default), we shall create one
+ if (-Not $this.config('director_host_object')) {
+ [string]$hostname = $this.getProperty('local_hostname');
+ $json = [string]::Format('{ "address": "{0}", "display_name": "{0}" }',
+ $hostname
+ );
+ } else {
+ # Otherwise use the specified one and replace the host object placeholders
+ $json = $this.doReplaceJSONPlaceholders($this.config('director_host_object'));
+ }
+
+ $this.info([string]::Format('Creating host "{0}" over API token inside Icinga Director.', $this.getProperty('local_hostname')));
+
+ [string]$httpResponse = $this.createHTTPRequest($url, $json, 'POST', 'application/json', $TRUE, $TRUE);
+
+ if ($this.isHTTPResponseCode($httpResponse) -eq $FALSE) {
+ $this.setProperty('director_host_token', $httpResponse);
+ $this.writeHostAPIKeyToDisk();
+ [string]$response = $this.fetchIcingaDirectorSelfServiceAPIConfig($httpResponse, $FALSE);
+ if ($response -ne '200') {
+ $this.error([string]::Format('Failed to fetch config arguments of Icinga Director Self-Service API after adding new host to Icinga Director. Response was "{0}"', $httpResponse));
+ } else {
+ $this.info('Successfully fetched configuration for this host over Self-Service API.')
+ }
+ } else {
+ if ($httpResponse -eq '400') {
+ throw [string]::Format("Received response 400 from Icinga Director. In general this means you tried to re-create an existing host inside the Icinga Director with a host template API key, but the host itself has already a key assigned. Please drop the API key for the host '{0}' and re-run this script to claim ownership. This error usually occures in case the host token was removed manually from the host.", $this.getProperty('local_hostname'));
+ } else {
+ $this.warn([string]::Format('Failed to create host. Response code {0}', $httpResponse));
+ }
+ }
+ } elseif ($this.config('director_host_object')) {
+ # Setup the url we need to call
+ [string]$url = $this.config('director_url') + 'host';
+ # Replace the host object placeholders
+ [string]$host_object_json = $this.doReplaceJSONPlaceholders($this.config('director_host_object'));
+ # Create the host object inside the director
+ [string]$httpResponse = $this.createHTTPRequest($url, $host_object_json, 'PUT', 'application/json', $FALSE, $this.config('debug_mode'));
+
+ if ($this.isHTTPResponseCode($httpResponse) -eq $FALSE) {
+ $this.info([string]::Format('Placed query for creating host "{0}" inside Icinga Director. Config: {1}',
+ $this.getProperty('local_hostname'),
+ $httpResponse
+ )
+ );
+ } else {
+ if ($httpResponse -eq '422') {
+ $this.warn([string]::Format('Failed to create host "{0}" inside Icinga Director. The host seems to already exist.', $this.getProperty('local_hostname')));
+ } else {
+ $this.error([string]::Format('Failed to create host "{0}" inside Icinga Director. Error response {1}',
+ $this.getProperty('local_hostname'),
+ $httpResponse
+ )
+ );
+ }
+ }
+ # Shall we deploy the config for the generated host?
+ if ($this.config('director_deploy_config')) {
+ $url = $this.config('director_url') + 'config/deploy';
+ [string]$httpResponse = $this.createHTTPRequest($url, '', 'POST', 'application/json', $FALSE, $TRUE);
+ $this.info([string]::Format('Deploying configuration from Icinga Director to Icinga. Result: {0}', $httpResponse));
+ }
+ }
+ }
+ }
+
+ #
+ # Write Host API-Key for future usage
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'writeHostAPIKeyToDisk' -value {
+ if (Test-Path ($this.getProperty('config_dir'))) {
+ [string]$apiFile = Join-Path -Path $this.getProperty('config_dir') -ChildPath 'icingadirector.token';
+ $this.info([string]::Format('Writing host API-Key "{0}" to "{1}"',
+ $this.getProperty('director_host_token'),
+ $apiFile
+ )
+ );
+ [System.IO.File]::WriteAllText($apiFile, $this.getProperty('director_host_token'));
+ }
+ }
+
+ #
+ # Read Host API-Key from disk for usage
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'readHostAPIKeyFromDisk' -value {
+ [string]$apiFile = Join-Path -Path $this.getProperty('config_dir') -ChildPath 'icingadirector.token';
+ if (Test-Path ($apiFile)) {
+ [string]$hostToken = [System.IO.File]::ReadAllText($apiFile);
+ $this.setProperty('director_host_token', $hostToken);
+ $this.info([string]::Format('Reading host api token "{0}" from "{1}"',
+ $hostToken,
+ $apiFile
+ )
+ );
+ } else {
+ $this.setProperty('director_host_token', '');
+ }
+ }
+
+ #
+ # Get the API Version from the Icinga Director. In case we are using
+ # an older Version of the Director, we wont get this version
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getIcingaDirectorVersion' -value {
+ if ($this.config('director_url')) {
+ # Do a legacy call to the Icinga Director and get a JSON-Value
+ # Older versions of the Director do not support plain/text and
+ # would result in making this request quite useless
+
+ [string]$url = $this.config('director_url') + 'self-service/api-version';
+ [string]$versionString = $this.createHTTPRequest($url, '', 'POST', 'application/json', $FALSE, $this.config('debug_mode'));
+
+ if ($this.isHTTPResponseCode($versionString) -eq $FALSE) {
+ # Remove all characters we do not need inside the string
+ [string]$versionString = $versionString.Replace('"', '').Replace("`r", '').Replace("`n", '');
+ [array]$version = $versionString.Split('.');
+ $this.setProperty('icinga_director_api_version', $versionString);
+ return;
+ } else {
+ if ($versionString -eq '900') {
+ throw 'Failed to query Icinga Director API. Received error code 900. Please enable debug mode with -DebugMode for the script run to reteive additional information regarding this error.';
+ }
+ $this.warn('You seem to use an older Version of the Icinga Director, as no API version could be retreived.');
+ $this.setProperty('icinga_director_api_version', '0.0.0');
+ return;
+ }
+ }
+ $this.setProperty('icinga_director_api_version', 'false');
+ }
+
+ #
+ # Set Icinga 2 Agent Version based no the installed Agent
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'setIcinga2AgentVersion' -value {
+ param([string]$versionString)
+
+ if (-Not $versionString) {
+ return;
+ }
+
+ $this.setProperty('icinga2_agent_version', $versionString.Split('.'));
+ }
+
+ #
+ # Compare Version-Strings and check if we are running a higher or lower version
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'validateVersions' -value {
+ param([string]$requiredVersion, [array]$providedVersion);
+
+ if (-Not $requiredVersion -Or -Not $providedVersion) {
+ return $FALSE;
+ }
+
+ [array]$requiredVersion = $requiredVersion.Split('.');
+ $currentVersion = $providedVersion;
+
+ if ([int]$requiredVersion[0] -gt [int]$currentVersion[0]) {
+ return $FALSE;
+ }
+
+ if ([int]$requiredVersion[1] -gt [int]$currentVersion[1]) {
+ return $FALSE;
+ }
+
+ if ([int]$requiredVersion[1] -ge [int]$currentVersion[1] -And [int]$requiredVersion[2] -gt [int]$currentVersion[2]) {
+ return $FALSE;
+ }
+
+ return $TRUE;
+ }
+
+ #
+ # Match the Icinga Director API Version against a provided string
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'requireIcingaDirectorAPIVersion' -value {
+ param([string]$version, [string]$functionName);
+
+ # Director URL not specified
+ if ($this.getProperty('icinga_director_api_version') -eq 'false') {
+ return $FALSE;
+ }
+
+ if ($this.getProperty('icinga_director_api_version') -eq '0.0.0') {
+ $this.error([string]::Format('The feature "{0}" requires Icinga Director API-Version {1}. Your Icinga Director version does not support the API.',
+ $functionName,
+ $version
+ )
+ );
+ return $FALSE;
+ }
+
+ [bool]$versionValid = $this.validateVersions($version, $this.getProperty('icinga_director_api_version').Split('.'));
+
+ if ($versionValid -eq $FALSE) {
+ $this.error([string]::Format('The feature "{0}" requires Icinga Director API-Version {1}. Got version {2}',
+ $functionName,
+ $version,
+ $this.getProperty('icinga_director_api_version')
+ )
+ );
+ return $FALSE;
+ }
+
+ return $TRUE;
+ }
+
+ #
+ # This function will convert a [hashtable] or [array] object to string
+ # with function ConvertTo-Json for argument -DirectorHostObject.
+ # It will however only process those if the PowerShell Version is 3
+ # and above, because Version 2 is not providing the required
+ # functionality. In that case the module will throw an exception
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'convertDirectorHostObjectArgument' -value {
+
+ # First add the value to an object we can work with
+ [System.Object]$json = $this.config('director_host_object');
+
+ # Prevent processing of empty data
+ if ($json -eq $null -Or $json -eq '') {
+ return;
+ }
+
+ # In case the argument is already a string -> nothing to do
+ if ($json.GetType() -eq [string]) {
+ # Do nothing
+ return;
+ } elseif ($json.GetType() -eq [hashtable] -Or $json.GetType() -eq [array]) {
+ # Check which PowerShell Version we are using and throw an error in case our Version does not support the argument
+ if ($PSVersionTable.PSVersion.Major -lt 3) {
+ [string]$errorMessage = 'You are trying to pass an object of type [hashtable] or [array] for argument "-DirectorHostObject", but are using ' +
+ 'PowerShell Version 2 or lower. Passing hashtables through this argument is possible, but it requires to be ' +
+ 'converted with function ConvertTo-Json, which is available on PowerShell Version 3 and above only. ' +
+ 'You can still process JSON-Values with this module, even on PowerShell Version 2, but you will have to pass the ' +
+ 'JSON as string instead of an object. This module will now exit with an error code. For further details, please ' +
+ 'read the documentation for the "-DirectorHostObject" argument. ' +
+ 'Documentation: https://github.com/Icinga/icinga2-powershell-module/blob/master/doc/10-Basic-Arguments.md';
+ $this.exception($errorMessage);
+ throw 'PowerShell Version exception.';
+ }
+
+ # If our PowerShell Version is supporting the function, convert it to a valid string
+ $this.overrideConfig('director_host_object', (ConvertTo-Json -Compress $json), $FALSE);
+ }
+ }
+
+ #
+ # This function will connect to the Icinga Director Self-Service API
+ # and try to fetch the configuration for our host or the global
+ # configuraton, depending if the Host-Token does exist and is valid
+ # or in case it does not exist or is invalid if the API-tiken is
+ # specified
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'connectToIcingaDirectorSelfServiceAPI' -value {
+ if (-Not $this.config('director_url')) {
+ return;
+ }
+
+ if ($this.config('director_user') -And $this.config('director_password')) {
+ $this.info('User and Password for Icinga Director have been specified, Self-Service API will not be used.');
+ $this.setProperty('use_password_auth', $TRUE);
+ return;
+ }
+
+ $this.setProperty('icinga_host_exist', $FALSE);
+
+ [string]$response = $this.fetchIcingaDirectorSelfServiceAPIConfig($this.getProperty('director_host_token'), $FALSE);
+ switch ($response) {
+ '200' {
+ $this.info('Connected successfully to Icinga Director Self-Service API over stored host token.');
+ $this.setProperty('icinga_host_exist', $TRUE);
+ $this.setProperty('use_self_service_api', $TRUE);
+ return;
+ };
+ '404' {
+ $this.warn('The local host token could not be found inside the Icinga Director.');
+ };
+ '500' {
+ $this.warn('An internal server error occured while processing your local host token against the Icinga Director Self-Service API.');
+ };
+ }
+
+ if ($this.config('director_auth_token') -eq '' -And $this.getProperty('director_host_token')) {
+ $this.error('No template API token has been specified and the host token seems no longer valid.')
+ $this.setProperty('no_valid_api_token', $TRUE);
+ return;
+ }
+
+ # In case no host-token is set or no longer valid, use our API token if
+ # specified to fetch the global configuration from the API
+ $response = $this.fetchIcingaDirectorSelfServiceAPIConfig($this.config('director_auth_token'), $FALSE);
+ switch ($response) {
+ '200' {
+ $this.info('Connected successfully to Icinga Director Self-Service API over API token.');
+ $this.setProperty('use_self_service_api', $TRUE);
+ return;
+ };
+ '404' {
+ $this.warn('Failed to query Icinga Director Self-Service API.');
+ };
+ '500' {
+ $this.warn('An internal server error occured while processing your API token against the Icinga Director Self-Service API.');
+ };
+ '900' {
+ # Nothing to do
+ return;
+ };
+ }
+
+ if ($this.getProperty('director_host_token') -Or $this.config('director_auth_token')) {
+ $this.error(
+ [string]::Format('Failed to connect to Icinga Director Self-Service API. Tokens were specified but informations could not be fetched. Please review your tokens: Host: "{0}", API: "{1}".',
+ $this.getProperty('director_host_token'),
+ $this.config('director_auth_token')
+ ));
+ $this.setProperty('no_valid_api_token', $TRUE);
+ }
+ }
+
+ #
+ # This function will try to call the Icinga Director API
+ # with either a host-token or our API-token and retreive
+ # our arguments for processing with the configuration of
+ # our Icinga 2 Agent Setup
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'fetchIcingaDirectorSelfServiceAPIConfig' -value {
+ param([string]$token, [bool]$writeError);
+ if (-Not $this.config('director_url') -Or $token -eq '') {
+ return '900';
+ }
+
+ if (-Not $this.requireIcingaDirectorAPIVersion('1.4.0', '[Function::fetchIcingaDirectorSelfServiceAPIConfig]')) {
+ return '900';
+ }
+
+ [string]$url = [string]::Format('{0}self-service/powershell-parameters?key={1}',
+ $this.config('director_url'),
+ $token
+ );
+ [string]$argumentString = $this.createHTTPRequest($url, '', 'POST', 'application/json', $TRUE, $this.config('debug_mode'));
+
+ if ($this.isHTTPResponseCode($argumentString) -eq $FALSE) {
+ # First split the entire result based in new-lines into an array
+ [array]$arguments = $argumentString.Split("`n");
+
+ # Now loop all elements and construct a dictionary for all values
+ foreach ($item in $arguments) {
+ if ($item.Contains(':')) {
+ $this.debug([string]::Format('Processing Director API config argument "{0}"', $item));
+ [int]$argumentPos = $item.IndexOf(":");
+ [string]$argument = $item.Substring(0, $argumentPos);
+ if (($argumentPos + 2) -le $item.Length) {
+ [string]$value = $item.Substring($argumentPos + 2, $item.Length - 2 - $argumentPos);
+ $value = $value.Replace("`r", '');
+ $value = $value.Replace("`n", '');
+
+ if ($value.Contains( '!')) {
+ [array]$valueArray = $value.Split('!');
+ $this.overrideConfig($argument, $valueArray, $TRUE);
+ } else {
+ if ($value.toLower() -eq 'true') {
+ $this.overrideConfig($argument, $TRUE, $TRUE);
+ } elseif ($value.toLower() -eq 'false') {
+ $this.overrideConfig($argument, $FALSE, $TRUE);
+ } else {
+ $this.overrideConfig($argument, $value, $TRUE);
+ }
+ }
+ } else {
+ $this.debug([string]::Format('Got key argument "{0}" without a value.', $argument));
+ }
+ }
+ }
+ } else {
+ if ($writeError) {
+ $this.error([string]::Format('Received "{0}" from Icinga Director. Possibly your API token is no longer valid or the object does not exist.', $argumentString));
+ }
+ return $argumentString;
+ }
+
+ # Ensure we generate the required configuration content
+ $this.generateConfigContent();
+ return '200';
+ }
+
+ #
+ # This function will communicate directly with
+ # the Icinga Director and ensuring that we get
+ # some of the possible required informations
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'fetchTicketFromIcingaDirector' -value {
+
+ if ($this.getProperty('director_host_token') -And -Not $this.getProperty('use_password_auth')) {
+ if ($this.getProperty('no_valid_api_token')) {
+ $this.info('Skipping fetching of SSL ticket, as no valid API token has been specified.');
+ return;
+ }
+ if ($this.requireIcingaDirectorAPIVersion('1.4.0', '[Function::fetchTicketFromIcingaDirector]')) {
+ [string]$url = [string]::Format('{0}self-service/ticket?key={1}',
+ $this.config('director_url'),
+ $this.getProperty('director_host_token')
+ );
+ [string]$httpResponse = $this.createHTTPRequest($url, '', 'POST', 'application/json', $TRUE, $TRUE);
+ if ($this.isHTTPResponseCode($httpResponse) -eq $FALSE) {
+ $this.setProperty('icinga_ticket', $httpResponse);
+ $this.info([string]::Format('Fetched ticket "{0}" from Icinga Director', $httpResponse));
+ } else {
+ $this.error([string]::Format('Failed to fetch Ticket from Icinga Director. Error response {0}', $httpResponse));
+ }
+ }
+ } else {
+ if ($this.config('director_url') -And $this.getProperty('local_hostname')) {
+ [string]$url = $this.config('director_url') + 'host/ticket?name=' + $this.getProperty('local_hostname');
+ [string]$httpResponse = $this.createHTTPRequest($url, '', 'POST', 'application/json', $FALSE, $TRUE);
+
+ if ($this.isHTTPResponseCode($httpResponse) -eq $FALSE) {
+ # Lookup all " inside the return string
+ $quotes = Select-String -InputObject $httpResponse -Pattern '"' -AllMatches;
+
+ # If we only got two ", we should have received a valid ticket
+ # Otherwise we need to throw an error
+ if ($quotes.Matches.Count -ne 2) {
+ throw [string]::Format('Failed to fetch ticket for host "{0}". Got "{1}" as ticket.',
+ $this.getProperty('local_hostname'),
+ $httpResponse
+ );
+ } else {
+ $httpResponse = $httpResponse.subString(1, $httpResponse.length - 3);
+ $this.info([string]::Format('Fetched ticket "{0}" for host "{1}".',
+ $httpResponse,
+ $this.getProperty('local_hostname')
+ )
+ );
+ $this.setProperty('icinga_ticket', $httpResponse);
+ }
+ } else {
+ if ($httpResponse -eq '404') {
+ $this.error('Unable to fetch host ticket from Icinga Director. The Host object could not be found. Ensure the object is already present or created by specifying the -DirectorHostObject argument of this script.');
+ } else {
+ $this.error([string]::Format('Failed to fetch Ticket from Icinga Director. Error response {0}', $httpResponse));
+ }
+ }
+ }
+ }
+ }
+
+ #
+ # Check if NSClient is installed on the system
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'isNSClientInstalled' -value {
+ $nsclient = Get-WmiObject -Class Win32_Product |
+ Where-Object {
+ $_.Name -match 'NSClient*';
+ }
+
+ if ($nsclient -eq $null) {
+ return $FALSE;
+ }
+
+ return $TRUE;
+ }
+
+ #
+ # Shall we install the NSClient as well on the system?
+ # All possible actions are handeled here
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'installNSClient' -value {
+
+ if ($this.config('install_nsclient')) {
+
+ [string]$installerPath = $this.getNSClientInstallerPath();
+ $this.info([string]::Format('Trying to install and configure NSClient++ from "{0}"', $installerPath));
+
+ # First check if the package does exist
+ if (Test-Path ($installerPath)) {
+
+ if ($this.isNSClientInstalled() -eq $FALSE) {
+ # Get all required arguments for installing the NSClient unattended
+ [string]$NSClientArguments = $this.getNSClientInstallerArguments();
+
+ # Start the installer process
+ $result = $this.startProcess('MsiExec.exe', $TRUE, [string]::Format('/quiet /i "{0}" {1}', $installerPath, $NSClientArguments));
+
+ # Exit Code 0 means the NSClient was installed successfully
+ # Otherwise we require to throw an error
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.exception([string]::Format('Failed to install NSClient++. {0}', $result.Get_Item('message')));
+ } else {
+ $this.info('NSClient++ successfully installed.');
+
+ # To tell Icinga 2 we installed the NSClient and to make
+ # the NSCPPath variable available, we require to restart Icinga 2
+ $this.setProperty('require_restart', 'true');
+ }
+ } else {
+ $this.info('NSClient++ is already installed on the system.');
+ }
+
+ # If defined remove the Firewall Rule to secure the system
+ # By default the NSClient is only called from the Icinga 2 Agent locally
+ $this.removeNSClientFirewallRule();
+ # Remove the service if we only call the NSClient locally
+ $this.removeNSClientService();
+ # Add the default NSClient config if we want to do more
+ $this.addNSClientDefaultConfig();
+ } else {
+ $this.error([string]::Format('Failed to locate NSClient++ Installer at "{0}"', $installerPath));
+ }
+ } else {
+ $this.info('NSClient++ will not be installed on the system.');
+ }
+ }
+
+ #
+ # Determine the location of the NSClient installer
+ # By default we are using the shipped NSClient from the Icinga 2 Agent
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getNSClientInstallerPath' -value {
+
+ if ($this.config('nsclient_installer_path') -ne '') {
+
+ # Check of the installer is a local path
+ # If so, use this as installer source
+ if (Test-Path ($this.config('nsclient_installer_path'))) {
+ return $this.config('nsclient_installer_path');
+ }
+
+ $this.info([string]::Format('Trying to download NSClient++ from "{0}"', $this.config('nsclient_installer_path')));
+ [System.Object]$client = New-Object System.Net.WebClient;
+ $client.DownloadFile($this.config('nsclient_installer_path'), (Join-Path -Path $Env:temp -ChildPath 'NSCP.msi'));
+
+ return (Join-Path -Path $Env:temp -ChildPath 'NSCP.msi');
+ } else {
+ # Icinga is shipping a NSClient Version after installation
+ # Install this version if defined
+ return (Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\NSCP.msi');
+ }
+
+ return '';
+ }
+
+ #
+ # If we only want to use the NSClient++ to be called from the Icinga 2 Agent
+ # we do not require an open Firewall Rule to allow traffic.
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'getNSClientInstallerArguments' -value {
+ [string]$NSClientArguments = '';
+
+ if ($this.config('nsclient_directory')) {
+ $NSClientArguments += [string]::Format(' INSTALLLOCATION={0}', $this.config('nsclient_directory'));
+ }
+
+ return $NSClientArguments;
+ }
+
+ #
+ # If we only want to use the NSClient++ to be called from the Icinga 2 Agent
+ # we do not require an open Firewall Rule to allow traffic.
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'removeNSClientFirewallRule' -value {
+ if ($this.config('nsclient_firewall') -eq $FALSE) {
+
+ $result = $this.startProcess('netsh', $FALSE, 'advfirewall firewall show rule name="NSClient++ Monitoring Agent"');
+ if ($result.Get_Item('exitcode') -ne 0) {
+ # Firewall rule was not found. Nothing to do
+ $this.info('NSClient++ Firewall Rule is not installed');
+ return;
+ }
+
+ $this.info('Trying to remove NSClient++ Firewall Rule');
+
+ $result = $this.startProcess('netsh', $TRUE, 'advfirewall firewall delete rule name="NSClient++ Monitoring Agent"');
+
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.error([string]::Format('Failed to remove NSClient++ Firewall rule: {0}', $result.Get_Item('message')));
+ } else {
+ $this.info('NSClient++ Firewall Rule has been successfully removed');
+ }
+ }
+ }
+
+ #
+ # If we only want to use the NSClient++ to be called from the Icinga 2 Agent
+ # we do not require a running NSClient++ Service
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'removeNSClientService' -value {
+ if ($this.config('nsclient_service') -eq $FALSE) {
+ $NSClientService = Get-WmiObject -Class Win32_Service -Filter "Name='nscp'";
+ if ($NSClientService -ne $null) {
+ $this.info('Trying to remove NSClient++ service');
+ # Before we remove the service, stop it (to prevent ghosts)
+ Stop-Service 'nscp';
+ # Now remove it
+ $result = $NSClientService.delete();
+ if ($result.ReturnValue -eq 0) {
+ $this.info('NSClient++ Service has been removed');
+ } else {
+ $this.error('Failed to remove NSClient++ service');
+ }
+ } else {
+ $this.info('NSClient++ Service is not installed')
+ }
+ }
+ }
+
+ #
+ # In case we want to do more with the NSClient, we can auto-generate
+ # all NSClient++ config attributes
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'addNSClientDefaultConfig' -value {
+ if ($this.config('nsclient_add_defaults')) {
+ [string]$NSClientBinary = $this.getNSClientDefaultExecutablePath();
+
+ if ($NSClientBinary -eq '') {
+ $this.error('Unable to generate NSClient++ default config. Executable nscp.exe could not be found ' +
+ 'on default locations or the specified custom location. If you installed the NSClient on a ' +
+ 'custom location, please specify the path with -NSClientDirectory');
+ return;
+ }
+
+ if (Test-Path ($NSClientBinary)) {
+ $this.info('Generating all default NSClient++ config values');
+ $result = $this.startProcess($NSClientBinary, $TRUE, 'settings --generate --add-defaults --load-all');
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.error($result.Get_Item('message'));
+ }
+ } else {
+ $this.error([string]::Format('Failed to generate NSClient++ defaults config. Path to executable is not valid: {0}', $NSClientBinary));
+ }
+ }
+ }
+
+ #
+ # Deprecated function
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'installIcinga2Agent' -value {
+ $this.warn('The function "installIcinga2Agent" is deprecated and will be removed soon. Please use "install" instead.')
+ return $this.install();
+ }
+ $installer | Add-Member -membertype ScriptMethod -name 'installMonitoringComponents' -value {
+ $this.warn('The function "installMonitoringComponents" is deprecated and will be removed soon. Please use "install" instead.')
+ return $this.install();
+ }
+
+ #
+ # This function will try to load all
+ # data from the system and setup the
+ # entire Agent without user interaction
+ # including download and update if
+ # specified. Returnd 0 or 1 as exit code
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'install' -value {
+ try {
+ if (-Not $this.isAdmin()) {
+ return 1;
+ }
+
+ # Write an output to the logfile only, ensuring we always get a proper 'start entry' for the user
+ $this.info('Started script run...');
+ # Get the current API-Version from the Icinga Director
+ $this.getIcingaDirectorVersion();
+ # Convert our DirectorHostObject argument from Object to String if required
+ $this.convertDirectorHostObjectArgument();
+ # Read the Host-API Key in case it exists
+ $this.readHostAPIKeyFromDisk();
+ # Establish connection to Icinga Director Self-Service API if required
+ # and fetch basic / host configuration if tokens are set
+ $this.connectToIcingaDirectorSelfServiceAPI();
+ # Set Script defaults
+ $this.setScriptDefaultVariables();
+ # Get host name or FQDN if required
+ $this.fetchHostnameOrFQDN();
+ # Get IP-Address of host
+ $this.fetchHostIPAddress();
+ # Try to locate the primary IP Address
+ $this.lookupPrimaryIPv4Address();
+ # Transform the hostname if required
+ $this.doTransformHostname();
+ # Before we continue, flush the API Directory if specified. This will require
+ # us to stop the Icinga 2 Agent, but should prevent any false positive in
+ # case dependencies within the API Director are no longer pressent and will
+ # ensure a possible config rollback is working as intended as well
+ $this.flushIcingaApiDirectory();
+
+ # Try to locate the current
+ # Installation data from the Agent
+ if ($this.isAgentInstalled()) {
+ if (-Not $this.isAgentUpToDate()) {
+ if ($this.allowAgentUpdates()) {
+ $this.printAgentUpdateMessage();
+ $this.updateAgent();
+ $this.cleanupAgentInstaller();
+ }
+ } else {
+ $this.info('Icinga 2 Agent is up-to-date. Nothing to do.');
+ }
+ } else {
+ if ($this.canInstallAgent()) {
+ $this.installAgent();
+ $this.cleanupAgentInstaller();
+ } else {
+ $this.warn('Icinga 2 Agent is not installed and not allowed of beeing installed.');
+ }
+ }
+
+ # Try to create a host object inside the Icinga Director
+ $this.createHostInsideIcingaDirector();
+ # First check if we should get some parameters from the Icinga Director
+ $this.fetchTicketFromIcingaDirector();
+
+ # In case we downgrade from Icinga 2.8.0 or above to a older version (like Icinga 2.7.2)
+ $this.migrateCertificates();
+ if (-Not $this.hasCertificates() -Or $this.forceCertificateGeneration()) {
+ $this.generateCertificates();
+ } else {
+ $this.info('Icinga 2 certificates already exist. Nothing to do.');
+ }
+
+ $this.generateIcingaConfiguration();
+ $this.applyPossibleConfigChanges();
+ $this.switchIcingaLogging();
+ $this.installIcingaAgentFirewallRule();
+ $this.installNSClient();
+
+ if ($this.madeChanges()) {
+ $this.restartAgent();
+ } else {
+ $this.info('No changes detected.');
+ }
+
+ # We modify the service user at the very last to ensure
+ # the user we defined for logging in is valid
+ $this.modifyIcingaServiceUser();
+ return $this.getScriptExitCode();
+ } catch {
+ $this.printLastException();
+ [void]$this.getScriptExitCode();
+ return 1;
+ }
+ }
+
+ #
+ # Deprecated function
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'uninstallIcinga2Agent' -value {
+ $this.warn('The function "uninstallIcinga2Agent" is deprecated and will be removed soon. Please use "uninstall" instead.')
+ return $this.uninstall();
+ }
+ $installer | Add-Member -membertype ScriptMethod -name 'uninstallMonitoringComponents' -value {
+ $this.warn('The function "uninstallMonitoringComponents" is deprecated and will be removed soon. Please use "uninstall" instead.')
+ return $this.uninstall();
+ }
+
+ #
+ # Removes the Icinga 2 Agent from the system
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'uninstall' -value {
+ $this.info('Trying to locate Icinga 2 Agent...');
+
+ if ($this.isAgentInstalled()) {
+ $this.info('Removing Icinga 2 Agent from the system');
+ $result = $this.startProcess('MsiExec.exe', $TRUE, [string]::Format('{0} /q', $this.getProperty('uninstall_id')));
+
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.error($result.Get_Item('message'));
+ return [int]$result.Get_Item('exitcode');
+ }
+
+ $this.info('Icinga 2 Agent successfully removed.');
+ }
+
+ if ($this.config('full_uninstallation')) {
+ $this.info('Flushing Icinga 2 program data directory...');
+ if (Test-Path ((Join-Path -Path $Env:ProgramData -ChildPath 'icinga2'))) {
+ try {
+ [System.Object]$folder = New-Object -ComObject Scripting.FileSystemObject;
+ [void]$folder.DeleteFolder((Join-Path -Path $Env:ProgramData -ChildPath 'icinga2'));
+ $this.info('Remaining Icinga 2 configuration successfully removed.');
+ } catch {
+ $this.exception([string]::Format('Failed to delete Icinga 2 Program Data Directory: {0}', $_.Exception.Message));
+ }
+ } else {
+ $this.warn('Icinga 2 Agent program directory not present.');
+ }
+ }
+
+ if ($this.config('remove_nsclient')) {
+ $this.info('Trying to remove installed NSClient++...');
+
+ $nsclient = Get-WmiObject -Class Win32_Product |
+ Where-Object {
+ $_.Name -match 'NSClient*';
+ }
+
+ if ($nsclient -ne $null) {
+ $this.info('Removing installed NSClient++...');
+ [void]$nsclient.Uninstall();
+ $this.info('NSClient++ has been successfully removed.');
+ } else {
+ $this.warn('NSClient++ could not be located on the system. Nothing to remove.');
+ }
+ }
+
+ return $this.getScriptExitCode();
+ }
+
+ #
+ # Locate the current installation of Icinga 2 and dump the icinga2.conf to the window
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'dumpIcinga2Conf' -value {
+ if (-Not $this.isAgentInstalled()) {
+ $this.info('Icinga 2 Agent is not installed on the system. No configuration to dump.');
+ return $this.getScriptExitCode();
+ }
+
+ [string]$icingaConfig = '';
+ if (Test-Path $this.getIcingaConfigFile()) {
+ $icingaConfig = [System.IO.File]::ReadAllText($this.getIcingaConfigFile());
+ $this.info([string]::Format('Dumping content of the Icinga 2 configuration from "{0}".', $this.getIcingaConfigFile()));
+ $this.output($icingaConfig);
+
+ } else {
+ $this.exception([string]::Format('Failed to lookup Icinga 2 configuration at "{0}". File does not exist.', $this.getIcingaConfigFile()));
+ }
+ }
+
+ #
+ # Locate the current installation of Icinga 2 and dump all Icinga 2 objects
+ #
+ $installer | Add-Member -membertype ScriptMethod -name 'dumpIcinga2Objects' -value {
+ if (-Not $this.isAgentInstalled()) {
+ $this.info('Icinga 2 Agent is not installed on the system. No objects to dump.');
+ return $this.getScriptExitCode();
+ }
+
+ [string]$icingaBinary = Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
+
+ if (-Not (Test-Path $icingaBinary)) {
+ $this.exception([string]::Format('Failed to query Icinga 2 objects. Executable at "{0}" does not exist.', $icingaBinary));
+ return $this.getScriptExitCode();
+ }
+
+ $result = $this.startProcess($icingaBinary, $FALSE, 'object list');
+ if ($result.Get_Item('exitcode') -ne 0) {
+ $this.exception($result.Get_Item('message'));
+ } else {
+ $this.info('Dumping all objects from Icinga 2');
+ $this.output($result.Get_Item('message'));
+ }
+ }
+
+ # Make the installation / uninstallation of the script easier and shorter
+ [int]$installerExitCode = 0;
+ [int]$uninstallerExitCode = 0;
+ [int]$dumpConfigExitCode = 0;
+ [int]$dumpObjectsExitCode = 0;
+ # If flag RunUninstaller is set, do the uninstallation of the components
+ if ($RunUninstaller) {
+ $uninstallerExitCode = $installer.uninstall();
+ }
+ # If flag RunInstaller is set, do the installation of the components
+ if ($RunInstaller) {
+ $installerExitCode = $installer.install();
+ }
+ # If flag DumpIcingaConfig is set, print the current Icinga 2 configuration
+ if ($DumpIcingaConfig) {
+ $dumpConfigExitCode = $installer.dumpIcinga2Conf();
+ }
+ if ($DumpIcingaObjects) {
+ $dumpObjectsExitCode = $installer.dumpIcinga2Objects();
+ }
+ if ($RunInstaller -Or $RunUninstaller -Or $DumpIcingaConfig -Or $DumpIcingaObjects) {
+ if ($installerExitCode -ne 0 -Or $uninstallerExitCode -ne 0 -Or $dumpConfigExitCode -ne 0 -Or $dumpObjectsExitCode -ne 0) {
+ return 1;
+ }
+ }
+
+ # Otherwise handle everything as before
+ return $installer;
+}
diff --git a/doc/01-Introduction.md b/doc/01-Introduction.md
new file mode 100644
index 0000000..6d767ae
--- /dev/null
+++ b/doc/01-Introduction.md
@@ -0,0 +1,51 @@
+<a id="Introduction"></a>Introduction
+=====================================
+
+Welcome to the Icinga Director, the bleeding edge configuration tool for
+Icinga 2! Developed as an Icinga Web 2 module it aims to be your new
+favorite Icinga config deployment tool. Even if you prefer plain text
+files and manual configuration, chances are good that the Director will
+change your mind.
+
+Director is here to make your life easier. As an Icinga 2 pro you know
+all the knobs and tricks Icinga2 provides. However, you are not willing
+to do the same work again and again. Someone wants to add a new server,
+tweak some thresholds, adjust notifications? They shouldn't need to
+bother you.
+
+No way, you might think. You do not trust your users, they might break
+things. Well... no. Not with the Director. It provides an audit log that
+shows any single change. You can re-deploy old configurations at any time.
+And you will be allowed to restrict what your users are allowed to do in
+a very granular way.
+
+Doing automation? Want to feed your monitoring from your configuration
+management tool, or from your CMDB? You'll love the endless possibilities
+Director provides.
+
+
+Basic architecture
+------------------
+
+Icinga Director uses the Icinga 2 API to talk to your monitoring system.
+It will help you to deploy your configuration, regardless of whether you
+are using a single node Icinga installation or a distributed setup with
+multiple masters and satellites.
+
+ +------------+ +--------------+ +------------+
+ | Sat 1 / EU | | Sat 2 / Asia | | Sat 3 / US |
+ +------------+ +--------------+ +------------+
+ \ / /
+ \ / /
+ +-------------+ +-------------+
+ | Master 1 | <===> | Master 2 | (Master-Zone)
+ +-------------+ +-------------+
+ ^ ^
+ | Icinga 2 REST API :
+ | :
+ +----------------------------+
+ | Icinga Director |
+ +----------------------------+
+
+Using the Icinga 2 Agent? Perfect, the Director will make your life much
+easier!
diff --git a/doc/02-Installation.md b/doc/02-Installation.md
new file mode 100644
index 0000000..10604d1
--- /dev/null
+++ b/doc/02-Installation.md
@@ -0,0 +1,75 @@
+<!-- {% if index %} -->
+# Installing Icinga Director
+
+The recommended way to install Icinga Director and its dependencies is to use prebuilt packages for
+all supported platforms from our official release repository.
+Please note that [Icinga Web](https://icinga.com/docs/icinga-web) is required to run Icinga Director
+and if it is not already set up, it is best to do this first.
+
+The following steps will guide you through installing and setting up Icinga Director.
+
+To upgrade an existing Icinga Director installation to a newer version,
+see the [upgrading](05-Upgrading.md) documentation for the necessary steps.
+
+If you want to automate the installation, configuration and upgrade,
+you can learn more about it in the [automation](03-Automation.md) section of this documentation.
+
+## Optional Requirements
+
+The following requirements are not necessary for installation,
+but may be needed later if you want to import from one of the listed sources:
+
+* For **IBM Db2** imports: `php-pdo-ibm`
+* For **Microsoft SQL Server** imports: `php-mssql`, `php-pdo-dblib` or `php-sybase` depending on your platform
+* For **Oracle Database** imports: `php-oci8` or `php-pdo-oci` depending on your platform
+* For **SQLite** imports: `php-pdo-sqlite`
+<!-- {% else %} -->
+<!-- {% if not icingaDocs %} -->
+
+## Installing Icinga Director Package
+
+If the [repository](https://packages.icinga.com) is not configured yet, please add it first.
+Then use your distribution's package manager to install the `icinga-director` package
+or install [from source](02-Installation.md.d/From-Source.md).
+<!-- {% endif %} -->
+
+## Setting up the Database
+
+A MySQL (≥5.7), MariaDB (≥10.1), or PostgreSQL (≥9.6) database is required to run Icinga Director.
+Please follow the steps listed for your target database, to set up the database and the user.
+The schema will be imported later via the web interface.
+
+### Setting up a MySQL or MariaDB Database
+
+> **Warning**
+> Make sure to replace `CHANGEME` with a secure password.
+
+```
+mysql -e "CREATE DATABASE director CHARACTER SET 'utf8';
+ CREATE USER director@localhost IDENTIFIED BY 'CHANGEME';
+ GRANT ALL ON director.* TO director@localhost;"
+```
+
+### Setting up a PostgreSQL Database
+
+> **Warning**
+> Make sure to replace `CHANGEME` with a secure password.
+
+```
+psql -q -c "CREATE DATABASE director WITH ENCODING 'UTF8';"
+psql director -q -c "CREATE USER director WITH PASSWORD 'CHANGEME';
+GRANT ALL PRIVILEGES ON DATABASE director TO director;
+CREATE EXTENSION pgcrypto;"
+```
+
+## Configuring Icinga Director
+
+Log in to your running Icinga Web setup with a privileged user
+and follow the steps below to configure Icinga Director:
+
+1. Create a new resource for the Icinga Director [database](#setting-up-the-database) via the
+ `Configuration → Application → Resources` menu.
+ Please make sure that you configure `utf8` as encoding.
+2. Select `Icinga Director` directly from the main menu
+ and you will be taken to the kickstart wizard. Follow the instructions and you are done!
+<!-- {% endif %} --><!-- {# end else if index #} -->
diff --git a/doc/02-Installation.md.d/From-Source.md b/doc/02-Installation.md.d/From-Source.md
new file mode 100644
index 0000000..ea17233
--- /dev/null
+++ b/doc/02-Installation.md.d/From-Source.md
@@ -0,0 +1,83 @@
+# Installing Icinga Director from Source
+
+These are the instructions for manual Director installations.
+
+Please see the Icinga Web documentation on
+[how to install modules](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation) from source.
+Make sure you use `director` as the module name. The following requirements must also be met.
+
+## Requirements
+
+* PHP (≥7.3)
+ * Director v1.10 is the last version with support for PHP v5.6
+* [Icinga 2](https://github.com/Icinga/icinga2) (≥2.8.0)
+ * It is recommended to use the latest feature release of Icinga 2
+ * All versions since 2.4.3 should also work fine, but
+ we do no longer test and support them.
+ * Some features require newer Icinga 2 releases
+ * Flapping requires 2.8 for the thresholds to work - and at least 2.7 on all
+ nodes
+* [Icinga Web](https://github.com/Icinga/icingaweb2) (≥2.8.0). All versions since 2.2 should also work fine, but
+ might show smaller UI bugs and are not actively tested
+* The following Icinga modules must be installed and enabled:
+ * [incubator](https://github.com/Icinga/icingaweb2-module-incubator) (≥0.18.0)
+ * If you are using Icinga Web <2.9.0, the following modules are also required
+ * [ipl](https://github.com/Icinga/icingaweb2-module-ipl) (≥0.5.0)
+ * [reactbundle](https://github.com/Icinga/icingaweb2-module-reactbundle) (≥0.9.0)
+* A database: MariaDB (≥10.1), MySQL (≥5.7), PostgreSQL (≥9.6). Other
+ forks and older versions might work, but are neither tested nor supported
+* `php-pdo-mysql` and/or `php-pdo-pgsql`
+* `php-curl`
+* `php-iconv`
+* `php-pcntl` (might already be built into your PHP binary)
+* `php-posix` or `php-process` depending on your platform
+* `php-sockets` (might already be built into your PHP binary)
+
+## Installing from Release Tarball
+
+Download the [latest version](https://github.com/Icinga/icingaweb2-module-director/releases)
+and extract it to a folder named `director` in one of your Icinga Web module path directories.
+
+You might want to use a script as follows for this task:
+
+```shell
+MODULE_VERSION="1.10.2"
+ICINGAWEB_MODULEPATH="/usr/share/icingaweb2/modules"
+REPO_URL="https://github.com/icinga/icingaweb2-module-director"
+TARGET_DIR="${ICINGAWEB_MODULEPATH}/director"
+URL="${REPO_URL}/archive/v${MODULE_VERSION}.tar.gz"
+
+install -d -m 0755 "${TARGET_DIR}"
+wget -q -O - "$URL" | tar xfz - -C "${TARGET_DIR}" --strip-components 1
+icingacli module enable director
+```
+
+## Installing from Git Repository
+
+Another convenient method is to install directly from our Git repository.
+Simply clone the repository in one of your Icinga web module path directories.
+
+You might want to use a script as follows for this task:
+
+```shell
+MODULE_VERSION="1.10.2"
+ICINGAWEB_MODULEPATH="/usr/share/icingaweb2/modules"
+REPO_URL="https://github.com/icinga/icingaweb2-module-director"
+TARGET_DIR="${ICINGAWEB_MODULEPATH}/director"
+
+git clone "${REPO_URL}" "${TARGET_DIR}" --branch v${MODULE_VERSION}
+icingacli module enable director
+```
+
+## Setting up the Director Daemon
+
+For manual installations, the daemon user, its directory, and the systemd service need to be set up:
+
+```shell
+useradd -r -g icingaweb2 -d /var/lib/icingadirector -s /sbin/nologin icingadirector
+install -d -o icingadirector -g icingaweb2 -m 0750 /var/lib/icingadirector
+install -pm 0644 contrib/systemd/icinga-director.service /etc/systemd/system
+systemctl daemon-reload
+systemctl enable --now icinga-director
+```
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/03-Automation.md b/doc/03-Automation.md
new file mode 100644
index 0000000..ca3d1d3
--- /dev/null
+++ b/doc/03-Automation.md
@@ -0,0 +1,134 @@
+<a id="Automation"></a>Automation - Configuration management
+============================================================
+
+Director has been designed to work in distributed environments. In case
+you're using tools like Puppet, Ansible, Salt (R)?ex or similar, this
+chapter is what you're looking for!
+
+Generic hints
+-------------
+
+Director keeps all of its configuration in a relational database. So,
+all you need to tell him is how it can reach and access that db. In case
+you already rolled out Icinga Web 2 you should already be used to handle
+resource definitions.
+
+The Director needs a `database resource`, and your RDBMS must either by
+MySQL, MariaDB or PostgreSQL. This is how such a resource could look like
+in your `/etc/icingaweb2/resources.ini`:
+
+```ini
+[Director DB]
+type = "db"
+db = "mysql"
+host = "localhost"
+dbname = "director"
+username = "director"
+password = "***"
+charset = "utf8"
+```
+
+Please note that the charset is required and MUST be `utf8`.
+
+Next you need to tell the Director to use this database resource. Create
+its `config.ini` with the only required setting:
+
+```ini
+[db]
+resource = "Director DB"
+```
+
+Hint: `/etc/icingaweb2/modules/director/config.ini` is usually the full
+path to this config file.
+
+#### Schema creation and migrations
+
+You do not need to manually care about creating the schema and to migrate
+it for newer versions. Just `grant` the configured user all permissions on
+his database.
+
+On CLI then please run:
+
+ icingacli director migration run --verbose
+
+You should run this command after each upgrade, or you could also run it
+at a regular interval. Please have a look at...
+
+ icingacli director migration pending --verbose
+
+...in case you are looking for an idempotent way of managing the schema.
+Use `--help` to learn more about those commands.
+
+If you have any good reason for doing so and feel experienced enough you
+can of course also manage the schema on your own. All required files are
+to be found in our `schema` directories.
+
+
+Deploy Icinga Director with Puppet
+----------------------------------
+
+Drop the director source repository to a directory named `director` in
+one of your `module_path`'s and enable the module as you did with all the
+others.
+
+Deploy the mentioned database resource and `config.ini`. Director could
+now be configured and kick-started via the web frontend. But you are here
+for automation, so please read on.
+
+### Handle schema migrations
+
+It doesn't matter whether you already have a schema, did a fresh install
+or just an upgrade. Migrations are as easy as defining:
+
+ exec { 'Icinga Director DB migration':
+ path => '/usr/local/bin:/usr/bin:/bin',
+ command => 'icingacli director migration run',
+ onlyif => 'icingacli director migration pending',
+ }
+
+Hint: please do not travel back in time, schema downgrades are not
+supported.
+
+### Kickstart an empty Director database
+
+The Director kickstart wizard helps you with setting up a connection to
+Icinga2 master node, import its endpoint and zone definition and it also
+syncs already configured command definitions. But this wizard is not only
+available through the web frontend, you can perfectly trigger it in an
+idempotent way with Puppet:
+
+ exec { 'Icinga Director Kickstart':
+ path => '/usr/local/bin:/usr/bin:/bin',
+ command => 'icingacli director kickstart run',
+ onlyif => 'icingacli director kickstart required',
+ require => Exec['Icinga Director DB migration'],
+ }
+
+Nothing will happen so far. Kickstart will not do anything unless you
+drop a `kickstart.ini` allowing the CLI kickstart helper to do so:
+
+```ini
+[config]
+endpoint = icinga-master
+; host = 127.0.0.1
+; port = 5665
+username = director
+password = ***
+```
+
+Usually `/etc/icingaweb2/modules/director/kickstart.ini` should be the
+full path to this file. `endpoint` (master certificate name), `username`
+and `password` (fitting an already configured `ApiUser`) are required.
+`host` can be a resolvable hostname or an IP address. `port` is 5665 per
+default in case none is given. And of course your Icinga2 installation
+needs to have a corresponding `ApiListener` (look at your enabled
+features) listening at the given port.
+
+You can run the `kickstart` from the CLI if you don't use a tool for
+automation.
+
+ icingacli director kickstart run
+
+You can rerun the kickstart if you have to reimport changed local
+config, even when the beforementioned check tells you you don't need to.
+Or you could use the import/synchronisation features of Director.
diff --git a/doc/04-Getting-started.md b/doc/04-Getting-started.md
new file mode 100644
index 0000000..1db80d6
--- /dev/null
+++ b/doc/04-Getting-started.md
@@ -0,0 +1,60 @@
+<a id="Getting-started"></a>Getting started
+===========================================
+
+When new to the Director please make your first steps with a naked Icinga
+environment. Director is not allowed to modify existing configuration in
+`/etc/icinga2`. And while importing existing config is possible (happens for
+example automagically at kickstart time), it is a pretty advanced task you
+should not tackle at the early beginning.
+
+Define a new global zone
+------------------------
+
+This zone must exist on every node directly or indirectly managed by the
+Icinga Director:
+
+```icinga2
+object Zone "director-global" {
+ global = true
+}
+```
+
+Create an API user
+------------------
+
+```icinga2
+object ApiUser "director" {
+ password = "***"
+ permissions = [ "*" ]
+ //client_cn = ""
+}
+```
+
+To allow the configuration of an API user your Icinga 2 instance needs a
+`zone` and an `endpoint` object for itself. If you have a clustered
+setup or you are using agents you already have this. If you are using a
+fresh Icinga 2 installation or a standalone setup with other ways of
+checking your clients, you will have to create them.
+
+The easiest way to set up Icinga 2 with a `zone` and `endpoint` is by
+running the [Icinga 2 Setup Wizard](https://docs.icinga.com/icinga2/latest/doc/module/icinga2/chapter/distributed-monitoring#distributed-monitoring-setup-master).
+
+Take some time to really understand how to work with Icinga Director first.
+
+
+Other topics that might interest you
+------------------------------------
+
+* [Working with agents](24-Working-with-agents.md)
+* [Understanding how Icinga Director works](10-How-it-works.md)
+
+What you should not try to start with
+-------------------------------------
+
+Director has not been built to help you with managing existing hand-crafted
+configuration in /etc/icinga2. There are cases where it absolutely would
+make sense to combine the Director with manual configuration. You can also
+use multiple tools owning separate config packages. But these are pretty
+advanced topics.
+
+
diff --git a/doc/05-Upgrading.md b/doc/05-Upgrading.md
new file mode 100644
index 0000000..63630b6
--- /dev/null
+++ b/doc/05-Upgrading.md
@@ -0,0 +1,230 @@
+<a id="Upgrading"></a>Upgrading
+===============================
+
+Icinga Director is very upgrade-friendly. We never had any complaint referring
+data loss on upgrade. But to be on the safe side, please always [backup](#backup-first)
+your database before running an upgrade.
+
+Then drop the new version to your Icinga Web 2 module folder, and you're all done.
+Eventually refresh the page in your browser<sup>[[1]](#footnote1)</sup>, and you
+are ready to go.
+
+Should there any other actions be required (like [schema migrations](#schema-migrations)),
+you will be told so in your frontend.
+
+Please read more about:
+
+* [Database Backup](#backup-first)
+* [Upgrading to 1.10.x](#upgrade-to-1.10.x)
+* [Upgrading to 1.9.x](#upgrade-to-1.9.x)
+* [Upgrading to 1.8.x](#upgrade-to-1.8.x)
+* [Upgrading to 1.7.x](#upgrade-to-1.7.x)
+* [Upgrading to 1.6.x](#upgrade-to-1.6.x)
+* [Upgrading to 1.5.x](#upgrade-to-1.5.x)
+* [Upgrading to 1.4.x](#upgrade-to-1.4.x)
+* [Upgrading to 1.3.0](#upgrade-to-1.3.0)
+* [Upgrading to 1.2.0](#upgrade-to-1.2.0)
+* [Upgrading to 1.1.0](#upgrade-to-1.1.0)
+* [How to work with the latest GIT master](#git-master)
+* [Database schema upgrades](#schema-migrations)
+* [Job Runner restart](#restart-jobrunner)
+* [Downgrading](#downgrade)
+
+And last but not least, having a look at our [Changelog](82-Changelog.md) is
+usually a good idea before applying an upgrade.
+
+<a name="backup-first"></a>Always take a backup first
+-----------------------------------------------------
+
+All you need for backing up your Director is a snapshot of your database. Please
+use the tools provided by your database backend, like `mysqldump` or `pg_dump`.
+Restoring from a backup is trivial, and Director will always be able to apply
+pending database migrations to an imported old database snapshot.
+
+<a name="upgrade-to-1.9.x"></a>Upgrading to 1.10.x
+--------------------------------------------------
+
+Please check module dependencies, we raised some of them. In case you're missing
+one of them, the Web UI will tell you after the upgrade. You'll then be prompted
+to apply pending Database Migrations.
+
+PHP 7.3 is now claimed to be required, but we still support 5.6+ on Director
+v1.10.x. Same goes for database dependencies: you should upgrade them to recent
+versions, but v1.10 still works on the ones supported with v1.9.x.
+
+<a name="upgrade-to-1.9.x"></a>Upgrading to 1.9.x
+-------------------------------------------------
+
+Please check module dependencies, we raised some of them. In case you're missing
+one of them, the Web UI will tell you after the upgrade. You'll then be prompted
+to apply pending Database Migrations.
+
+<a name="upgrade-to-1.8.x"></a>Upgrading to 1.8.x
+-------------------------------------------------
+
+Before upgrading, please upgrade the [incubator module](https://github.com/Icinga/icingaweb2-module-incubator)
+to (at least) v0.6.0. As always, you'll be prompted to apply pending Database
+Migrations.
+
+<a name="upgrade-to-1.7.x"></a>Upgrading to 1.7.x
+-------------------------------------------------
+
+Since v1.7.0 Icinga Director requires at least PHP 5.6. Also, this version
+introduces new dependencies. Please make sure that the following Icinga Web 2
+modules have been installed and enabled:
+
+* [ipl](https://github.com/Icinga/icingaweb2-module-ipl) (>=0.3.0)
+* [incubator](https://github.com/Icinga/icingaweb2-module-incubator) (>=0.5.0)
+* [reactbundle](https://github.com/Icinga/icingaweb2-module-reactbundle) (>=0.7.0)
+
+Also, the following PHP libraries should be available:
+
+* php-pcntl (might already be built into your PHP binary)
+* php-posix (on RHEL/CentOS this is php-process, or rh-php7x-php-process)
+* php-sockets (might already be built into your PHP binary)
+
+Apart from this, in case you are running 1.6.x or any GIT master since then,
+all you need is to replace the Director module folder with the new one. Or to
+run `git checkout v1.7.x` in case you installed Director from GIT.
+
+As always, you'll then be prompted to apply pending Database Migrations. There
+is now a new, modern (and mandatory) Background Daemon, the old (optional) Jobs
+Daemon must be removed. Please check our [documentation](75-Background-Daemon.md)
+for related instructions.
+
+<a name="upgrade-to-1.6.x"></a>Upgrading to 1.6.x
+-------------------------------------------------
+
+There is nothing special to take care of. In case you are running 1.5.x or any
+GIT master since then, all you need is to replace the Director module folder
+with the new one. Or to run git checkout v1.6.0 in case you installed Director
+from GIT.
+
+As always, you'll then be prompted to apply pending Database Migrations.
+
+<a name="upgrade-to-1.5.x"></a>Upgrading to 1.5.x
+-------------------------------------------------
+
+There is nothing special to take care of. In case you are running 1.4.x or any
+GIT master since then, all you need is to replace the Director module folder
+with the new one. Or to run git checkout v1.5.0 in case you installed Director
+from GIT.
+
+As always, you'll then be prompted to apply pending Database Migrations.
+
+<a name="upgrade-to-1.4.x"></a>Upgrading to 1.4.x
+-------------------------------------------------
+
+Since v1.4.0 Icinga Director requires at least PHP 5.4. Apart from this, there
+is nothing special to take care of. In case you are running 1.3.x or any GIT
+master since then, all you need is to replace the Director module folder with
+the new one. Or to run `git checkout v1.4.x` in case you installed Director
+from GIT.
+
+<a name="upgrade-to-1.3.x"></a>Upgrading to 1.3.x
+-------------------------------------------------
+
+In case you are running 1.2.0 or any GIT master since then, all you need is to
+replace the Director module folder with the new one. Or to run `git checkout v1.3.x`
+in case you installed Director from GIT.
+
+When running Director since 1.1.0 or earlier on PostgreSQL, you might not yet
+have the PostgreSQL crypto extension installed (Package: `postgresql-contrib`) and
+enabled:
+
+ psql -q -c "CREATE EXTENSION pgcrypto;"
+
+
+<a name="upgrade-to-1.2.0"></a>Upgrading to 1.2.0
+-------------------------------------------------
+
+There is nothing special to take care of. In case you are running 1.1.0 or any
+GIT master since then, all you need is to replace the Director module folder with
+the new one. Or to run `git checkout v1.2.0` in case you installed Director from
+GIT.
+
+<a name="upgrade-to-1.1.0"></a>Upgrading to 1.1.0
+-------------------------------------------------
+
+There is nothing special to take care of. In case you are running 1.0.0 or any
+GIT master since then, all you need is to replace the Director module folder with
+the new one. Or to run `git checkout v1.1.0` in case you installed Director from
+GIT.
+
+<a name="git-master"></a>Work with the latest GIT master
+--------------------------------------------------------
+
+Icinga Director is still a very young project. Lots of changes are going on,
+a lot of improvements, bug fixes and new features are still being added every
+month. People living on the bleeding edge might prefer to use all of them as
+soon as possible.
+
+So here is the good news: this is no problem at all. It's absolutely legal and
+encouraged to run Director as a pure GIT clone, installed as such:
+
+```sh
+ICINGAWEB_MODULES=/usr/share/icingaweb2/modules
+DIRECTOR_GIT=https://github.com/Icinga/icingaweb2-module-director.git
+git clone $DIRECTOR_GIT $ICINGAWEB_MODULES/director
+```
+
+Don't worry about schema upgrades. Once they made it into our GIT master there
+will always be a clean upgrade path for you, no manual interaction should ever
+be required. Like every human being, we are not infallible. So, while our strict
+policy says that the master should never break, this might of course happen.
+
+In that case, please [let us know](https://github.com/Icinga/icingaweb2-module-director/issues).
+We'll try to fix your issue as soon as possible.
+
+<a name="schema-migrations"></a>Database schema migrations
+----------------------------------------------------------
+
+In case there are any DB schema upgrades (and that's still often the case) this
+is no problem at all. They will pop up on your Director Dashboard and can be
+applied with a single click. And if your Director is deployed automatically by
+and automation tool like Puppet, also schema upgrades can easily be handled
+that way. Just follow the [related examples](03-Automation.md) in our documentation.
+
+<a name="schema-migrations"></a>Manual schema migrations
+----------------------------------------------------------
+
+Please *do not* manually apply our schema migration files. We are very strict
+about our connection settings, encodings and SQL modes. Client encoding MUST be
+UTF-8, for MySQL and MariaDB we are using the following SQL Mode:
+
+```sql
+SET SESSION SQL_MODE='STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,
+ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,ANSI_QUOTES,PIPES_AS_CONCAT,
+NO_ENGINE_SUBSTITUTION';
+```
+
+Our migration files are written based on the assumption that those rules are
+strictly followed, and there may be other ones in future. So please use one
+of the supported migration methods either on the web or on command line and
+stay away from directly interfering with the schema.
+
+<a name="restart-jobrunner"></a>Restart the Job Runner service
+--------------------------------------------------------------
+
+The Job Runner forks it's jobs, so usually a changed code base will take effect
+immediately. However, there might be (schema or code) changes involving the Runner
+process itself. To be on the safe side please always restart it after an upgrade,
+even when it's just a quick `git pull`:
+
+```sh
+systemctl restart director-jobs.service
+```
+
+<a name="downgrade"></a>Downgrading
+-----------------------------------
+
+Downgrading is **not supported**. Most parts of the code will even refuse to
+work in case there are new fields in their database tables. Migrations are
+intentionally provided for upgrades only. In case you want to travel back in
+time please restore a matching former [Database Backup](#backup-first).
+
+<a name="footnote1">[1]</a>:
+Future Icinga Web 2 version will also take care of this step. We want to be
+able to have the latest JavaScript and CSS for any active module to be shipped
+silently without manual interaction to any connected browser within less then
+15 seconds after the latest module has been installed, enabled or upgraded.
diff --git a/doc/10-How-it-works.md b/doc/10-How-it-works.md
new file mode 100644
index 0000000..5f683fb
--- /dev/null
+++ b/doc/10-How-it-works.md
@@ -0,0 +1,112 @@
+<a id="How-it-works"></a>How it works
+=====================================
+
+This chapter wants to give you some basic understanding of how the
+Director works with your Icinga installation. At least once you start
+to work with satellite zones it might be worth to give this a read.
+
+
+How your configuration is going to be rendered
+----------------------------------------------
+
+First of all, the Director doesn't write to `/etc/icinga2`. That's where
+you keep to store your manual configuration and that's where you are
+required to do the basic config tasks required to get Icinga 2 ready for
+the Director.
+
+The Director uses the Icinga 2 API to ship the configuration. It does
+so by shipping full config packages, it does not deal with single
+objects. This makes deployments much faster. It also makes it easier to
+eventually use Director in parallel with manual configuration or
+configuration shipped by other tools.
+
+Internally, Icinga 2 manages part of its configuration in its `var/lib`
+directory. This is usually to be found in `/var/lib/icinga2`. Config
+packages are stored to `/var/lib/icinga2/api/packages` once shipped
+through the API. So as soon as you deployed your first configuration
+with the Director, there will be a new timestamped subdirectory
+containing the new configuration.
+
+Those subdirectories are called stages. You'll often see more than one
+of them. When a new config is deployed, Icinga 2 tries to restart with
+that new stage. In case it fails, Icinga 2 will keep running with the
+former configuration. When it succeeds, it will terminate the old process
+and keep running with the new configuration.
+
+In either scenario, it writes an exit code and its startup log to the
+corresponding stage directory. This allows the Director to check back
+later on to fetch this information. That's why you see all those nice
+startup log outputs along with your deployment history in your frontend.
+
+The configuration in such a stage directory is structured like your
+Icinga 2 config directory in `/etc`: there is a `conf.d` and a `zones.d`
+subdirectory. In `zones.d` Director will create a subdirectory for each
+Zone it wants to deploy config to.
+
+Please note that those `zones.d` subdirectories are subject to config
+sync. To get them syncronized to other nodes, the following must be
+true for them:
+
+* they must have a zone definition for that zone in their local config
+* they must make part of your deployment endpoints zone or be a direct
+ or indirect subzone of that one
+* the `accept_config` setting must be `true` in their `ApiListener`
+ object definition
+
+The director does not try to create additional zones your nodes do not
+know about. In a distributed environment it is essential that the
+Director can ship parts of the configuration to specific zones and
+other parts to a global zone. The name of its preferred global zone
+is currently hardcoded to `director-global`. Please make sure that such
+a zone exists on all involved nodes that should get config from the
+Director in a direct or indirect way:
+
+```icinga2
+object Zone "director-global" {
+ global = true
+}
+```
+
+Please do not use this zone for your own configuration files.
+There is a zone called `global-templates` available in default Icinga
+setups that's meant for configuration files. `director-global` is reserved
+for use by Icinga Director.
+
+Zone membership handling
+------------------------
+
+Mostly you do not need to care much about Zones when working with the
+Director. In case you have no Satellite node, you wouldn't even notice
+their existence.
+
+You are not required to deal with Agent Zones, as the Director does
+this for you. Please refer to [Working with agents](24-Working-with-agents.md)
+for related examples.
+
+Currently the GUI does not allow you to set the zone property on single
+objects. You can circumvent this through the Director's [REST API](70-REST-API.md),
+with Sync rules and through the CLI. However, that shouldn't be part
+of your normal workflow. So if this restriction causes trouble with what
+you want to build please let us know. Explain your scenario, make us
+understand what you want to achieve.
+
+We think of this restriction being a good idea, as it makes things
+easier for most people. That doesn't mean that we would refuse to change
+our mind on this. At least not if you come up with a very good
+reasonable use case.
+
+
+Object rendering
+----------------
+
+This chapter explains where the Director renders which config object to.
+
+* Most objects are rendered to the master zone per default
+* Templates, commands and apply rules are rendered to the global zone
+* Objects with a zone property are rendered to that zone, even if they
+ inherited that property
+* Host objects configured as an Agent are rendered to the master zone,
+ as Director configures them as a Command Execution Bridge
+* Agents with a zone property respect that setting
+* Every command is rendered to the global zone per default
+
diff --git a/doc/12-Handling-custom-variables.md b/doc/12-Handling-custom-variables.md
new file mode 100644
index 0000000..3c9a9cf
--- /dev/null
+++ b/doc/12-Handling-custom-variables.md
@@ -0,0 +1,13 @@
+<a id="Handling-custom-variables"></a>Working with custom variables
+===================================================================
+
+Icinga Director allows you to work with custom variables in a very
+powerful way. It implements the concept of `Data fields`. If you want
+your users to be able to fill specific custom variables, you need to
+add corresponding `fields` to
+
+Examples
+--------
+* Add fields for existing commands
+* Allow to fill an [array of interfaces](14-Fields-example-interfaces-array.md)
+
diff --git a/doc/14-Fields-example-interfaces-array.md b/doc/14-Fields-example-interfaces-array.md
new file mode 100644
index 0000000..e2a3ba6
--- /dev/null
+++ b/doc/14-Fields-example-interfaces-array.md
@@ -0,0 +1,31 @@
+<a id="Fields-example-interfaces-array"></a>Working with fields - interfaces array example
+==============================================
+
+This example wants to show you how to make use of the `Array` data type
+when creating fields for custom variables. First, please got to the `Dashboard`
+and choose the `Define data fields` dashlet:
+
+![Dashboard - Define data fields](screenshot/director/14_fields-for-interfaces/141_define_datafields.png)
+
+Then create a new data field and select `Array` as its data type:
+
+![Define data field - Array](screenshot/director/14_fields-for-interfaces/142_add_datafield.png)
+
+Then create a new `Host template` (or use an existing one):
+
+![Define host template](screenshot/director/14_fields-for-interfaces/143_add_host_template.png)
+
+Now add your formerly created data field to your template:
+
+![Add field to template](screenshot/director/14_fields-for-interfaces/144_add_template_field.png)
+
+That's it, now you are ready to create your first corresponding host. Once
+you add your formerly created template, a new form field for your custom
+variable will show up:
+
+![Create host with given field](screenshot/director/14_fields-for-interfaces/145_create_host.png)
+
+Have a look at the config preview, it will show you how your `Array`-based
+custom variable will look like once deployed:
+
+![Host config preview with Array](screenshot/director/14_fields-for-interfaces/146_config_preview.png)
diff --git a/doc/15-Service-apply-for-example.md b/doc/15-Service-apply-for-example.md
new file mode 100644
index 0000000..531e4a7
--- /dev/null
+++ b/doc/15-Service-apply-for-example.md
@@ -0,0 +1,44 @@
+<a id="Service-apply-for-example"></a>Working with Apply for rules - tcp ports example
+==============================================
+
+This example wants to show you how to make use of `Apply For` rule for services.
+
+First you need to define a `tcp_ports` data field of type `Array` assigned to a `Host Template`.
+Refer to [Working with fields](14-Fields-example-interfaces-array.md) section to setup a data field.
+You also need to define a `tcp_port` data field of type `String`, we will associate it to a
+`Service Template` later.
+
+Then, please go to the `Dashboard` and choose the `Monitored services` dashlet:
+
+![Dashboard - Monitored services](screenshot/director/15_apply-for-services/151_monitored_services.png)
+
+Then create a new `Service template` with check command `tcp`:
+
+![Define service template - tcp](screenshot/director/15_apply-for-services/152_add_service_template.png)
+
+Then associate the data field `tcp_port` to this `Service template`:
+
+![Associate field to service template - tcp_port](screenshot/director/15_apply-for-services/153_add_service_template_field.png)
+
+Then create a new `apply-rule` for the `Service template`:
+
+![Define apply rule](screenshot/director/15_apply-for-services/154_create_apply_rule.png)
+
+Now define the `Apply For` property, select the previously defined field `tcp_ports` associated to
+the host template. `Apply For` rule define a variable `config` that can be used as `$config$`, it
+corresponds to the item of the array it will iterate on.
+
+Set the `Tcp port` property to `$config$`:
+
+![Add field to template](screenshot/director/15_apply-for-services/155_configure_apply_for.png)
+
+(Side note: if you can't see your `tcp_ports` property in `Apply For` dropdown, try to create one
+host with a non-empty `tcp_ports` value.)
+
+That's it, now all your hosts defining a `tcp_ports` variable will be assigned the `Tcp Check`
+service.
+
+Have a look at the config preview, it will show you how `Apply For` services will look like once
+deployed:
+
+![Host config preview with Array](screenshot/director/15_apply-for-services/156_config_preview.png)
diff --git a/doc/16-Fields-example-SNMP.md b/doc/16-Fields-example-SNMP.md
new file mode 100644
index 0000000..3cc7569
--- /dev/null
+++ b/doc/16-Fields-example-SNMP.md
@@ -0,0 +1,104 @@
+<a id="Fields-example-SNMP"></a>Data Fields example: SNMP
+=========================
+
+Ever wondered how to provide an easy to use SNMP configuration to your users?
+That's what we're going to show in this example. Once completed, all your Hosts
+inheriting a specific (or your "default") Host Template will provide an optional
+`SNMP version` field.
+
+In case you choose no version, nothing special will happen. Otherwise, the host
+offers additional fields depending on the chosen version. `Community String` for
+`SNMPv1` and `SNMPv2c`, and five other fields ranging from `Authentication User`
+to `Auth` and `Priv` types and keys for `SNMPv3`.
+
+Your services should now be applied not only based on various Host properties
+like `Device Type`, `Application`, `Customer` or similar - but also based on
+the fact whether credentials have been given or not.
+
+
+Prepare required Data Fields
+----------------------------
+
+As we already have learned, `Fields` are what allows us to define which custom
+variables can or should be defined following which rules. We want SNMP version
+to be a drop-down, and that's why we first define a `Data List`, followed by
+a `Data Field` using that list:
+
+### Create a new Data List
+
+![Create a new Data List](screenshot/director/16_fields_snmp/161_snmp_versions_create_list.png)
+
+### Fill the new list with SNMP versions
+
+![Fill the new list with SNMP versions](screenshot/director/16_fields_snmp/162_snmp_versions_fill_list.png)
+
+### Create a corresponding Data Field
+
+![Create a Data Field for SNMP Versions](screenshot/director/16_fields_snmp/163_snmp_version_create_field.png)
+
+Next, please also create the following elements:
+
+* a list *SNMPv3 Auth Types* providing `MD5` and `SHA`
+* a list *SNMPv3 Priv Types* providing at least `AES` and `DES`
+* a `String` type field `snmp_community` labelled *SNMP Community*
+* a `String` type field `snmpv3_user` labelled *SNMPv3 User*
+* a `String` type field `snmpv3_auth` labelled *SNMPv3 Auth* (authentication key)
+* a `String` type field `snmpv3_priv` labelled *SNMPv3 Priv* (encryption key)
+* a `Data List` type field `snmpv3_authprot` labelled *SNMPv3 Auth Type*
+* a `Data List` type field `snmpv3_privprot` labelled *SNMPv3 Priv Type*
+
+Please do not forget to add meaningful descriptions, telling your users about
+in-house best practices.
+
+
+Assign your shiny new Fields to a Template
+------------------------------------------
+
+I'm using my default Host Template for this, but one might also choose to provide
+`SNMP version` on Network Devices. Should Network Device be a template? Or just
+an option in a `Device Type` field? You see, the possibilities are endless here.
+
+This screenshot shows part of my assigned Fields:
+
+![SNMP Fields on Default Host](screenshot/director/16_fields_snmp/164_snmp_fields_on_template.png)
+
+While I kept `SNMP Version` optional, all other fields are mandatory.
+
+
+Use your Template
+-----------------
+
+As soon as you choose your template, a new field is shown:
+
+![Choose SNMP version](screenshot/director/16_fields_snmp/165_host_snmp_choose.png)
+
+In case you change it to `SNMPv2c`, a `Community String` will be required:
+
+![Community String for SNMPv2c](screenshot/director/16_fields_snmp/166_host_snmp_v2c.png)
+
+Switch it to SNMPv3 to see completely different fields:
+
+![Auth and Priv properties for SNMPv3](screenshot/director/16_fields_snmp/167_host_snmp_v3.png)
+
+Once stored please check the rendered configuration. Switch the SNMP versions
+forth and back, and you should see that filtered fields will also remove the
+corresponding values from the object.
+
+
+Assign Services based on those properties
+-----------------------------------------
+
+You should design your Commands to use that set of properties. Change the example
+slightly to fit ITL Commands in case you're using those (snmpv3_*_type VS _alg).
+
+Your Cisco Health checks assigned to all:
+
+* routers or switches
+* manifactured by Cisco
+* with SNMP credentials, regardless of which version
+
+...might then look as follows:
+
+![Assign SNMP-based checks](screenshot/director/16_fields_snmp/168_assign_snmp_check.png)
+
+Have fun!
diff --git a/doc/24-Working-with-agents.md b/doc/24-Working-with-agents.md
new file mode 100644
index 0000000..24473db
--- /dev/null
+++ b/doc/24-Working-with-agents.md
@@ -0,0 +1,80 @@
+<a id="Working-with-agents"></a>Working with Agents and Config Zones
+====================================================================
+
+Working with Icinga 2 Agents can be quite tricky, as each Agent needs
+its own Endpoint and Zone definition, correct parent, peering host and
+log settings. There may always be reasons for a completely custom-made
+configuration. However, I'd **strongly suggest** using the **Director-
+assisted** variant. It will save you a lot of headaches.
+
+
+Preparation
+-----------
+
+Agent settings are not available for modification directly on a host
+object. This requires you to create an "Icinga Agent" template. You
+could name it exactly like that; it's important to use meaningful names
+for your templates.
+
+![Create an Agent template](screenshot/director/24-agents/2401_agent_template.png)
+
+As long as you're not using Satellite nodes, a single Agent zone is all
+you need. Otherwise, you should create one Agent template per satellite
+zone. If you want to move an Agent to a specific zone, just assign it
+the correct template and you're all done.
+
+
+Usage
+-----
+
+Well, create a host, choose an Agent template, that's it:
+
+![Create an Agent-based host](screenshot/director/24-agents/2402_create_agent_based_host.png)
+
+Once you import the "Icinga Agent" template, you'll see a new "Agent" tab.
+It tries to assist you with the initial Agent setup by showing a sample
+config:
+
+![Agent instructions 1](screenshot/director/24-agents/2403_show_agent_instructions_1.png)
+
+![Agent instructions 2](screenshot/director/24-agents/2404_show_agent_instructions_2.png)
+
+The preview shows that the Icinga Director would deploy multiple objects
+for your newly created host:
+
+![Agent preview](screenshot/director/24-agents/2405_agent_preview.png)
+
+
+Create Agent-based services
+---------------------------
+
+Similar game for services that should run on your Agents. First, create a
+template with a meaningful name. Then, define that Services inheriting from
+this template should run on your Agents.
+
+![Agent-based service](screenshot/director/24-agents/2406_agent_based_service.png)
+
+Please do not set a cluster zone, as this would rarely be necessary.
+Agent-based services will always be deployed to their Agent's zone by
+default. All you need to do now for services that should be executed
+on your Agents is importing that template:
+
+![Agent-based load check](screenshot/director/24-agents/2407_create_agent_based_load_check.png)
+
+Config preview shows that everything works as expected:
+
+![Agent-based service preview](screenshot/director/24-agents/2409_agent_based_service_rendered_for_host.png)
+
+It's perfectly valid to assign services to host templates. Look how the
+generated config differs now:
+
+![Agent-based service assigned to host template](screenshot/director/24-agents/2410_agent_based_service_rendered_for_host_template.png)
+
+While services added to a host template are implicitly rendered as
+assign rules, you could of course also use your `Agent-based service`
+template in custom apply rules:
+
+![Agent-based service applied](screenshot/director/24-agents/2411_assign_agent_based_service.png)
+
+
+
diff --git a/doc/30-Configuration-Baskets.md b/doc/30-Configuration-Baskets.md
new file mode 100644
index 0000000..f077a74
--- /dev/null
+++ b/doc/30-Configuration-Baskets.md
@@ -0,0 +1,92 @@
+<a id="baskets"></a> Importing Director Configurations with Baskets
+===================================================================
+
+Director already takes care of importing configurations for monitored objects. This same concept
+is also useful for Director's internal configuration. *Configuration Baskets* allow you to
+export, import, share and restore all or parts of your Icinga Director configuration, as many
+times as you like.
+
+Configuration baskets can save or restore the configurations for almost all internal Director
+objects, such as host groups, host templates, service sets, commands, notifications, sync
+rules, and much more. Because configuration baskets are supported directly in Director, all
+customizations included in your Director configuration are imported and exported properly.
+Each snapshot is a persistent, serialized (JSON) representation of all involved objects at that
+moment in time.
+
+Configuration baskets allow you to:
+- Back up (take a snapshot) and restore a Director configuration...
+ - To be able to restore in case of a misconfiguration you have deployed
+ - Copy specific objects as a static JSON file to migrate them from testing to production
+- Understand problems stemming from your changes with a diff between two configurations
+- Share configurations with others, either your entire environment or just specific parts such as commands
+- Choose only some elements to snapshot (using a *custom selection*) in a given category such as
+ a subset of Host Templates
+
+In addition, you can script some changes with the following command:
+```
+# icingacli director basket [options]
+```
+
+
+
+Using Configuration Baskets
+---------------------------
+
+To create or use a configuration basket, select **Icinga Director > Configuration Baskets**. At
+the top of the new panel are options to:
+- Make a completely new configuration basket with the *Create* action
+- Make a new basket by importing a previously saved JSON file with the *Upload* action
+
+At the bottom you will find the list of existing baskets and the number of snapshots in each.
+Selecting a basket will take you to the tabs for editing baskets and for taking snapshots.
+
+
+
+### Create a New Configuration Basket
+
+To create or edit a configuration basket, give it a name, and then select whether each of the
+configuration elements should appear in snapshots for that basket. The following choices
+are available for each element type:
+- **Ignore:** Do not put this element in snapshots (for instance, do not include sync rules).
+- **All of them:** Put all items of this element type in snapshots (for example, all host templates).
+- **Custom Selection:** Put only specified items of this element type in a snapshot. You will
+ have to manually mark each element on the element itself. For instance, if you have marked host
+ templates for custom selection, then you will have to go to each of the desired host templates
+ and select the action *Add to Basket*. This will cause those particular host templates to be
+ included in the next snapshot.
+
+
+
+### Uploading and Editing Saved Baskets
+
+If you or someone else has created a serialized JSON snapshot (see below), you can upload that
+basket from disk. Select the *Upload* action, give it a new name, use the file chooser to select
+the JSON file, and click on the *Upload* button. The new basket will appear in the list of
+configuration baskets.
+
+Editing a basket is simple: Click on its name in the list of configuration baskets to edit either
+the basket name or else whether and how each configuration type will appear in snapshots.
+
+
+
+### Managing Snapshots
+
+From the *Snapshots* panel you can create a new snapshot by clicking on the *Create Snapshot*
+button. The new snapshot should immediately appear in the table below, along with a short
+summary of the included types (e.g., *2x HostTemplate*) and the time. If no configuration types
+were selected for inclusion, the summary for that row will only show a dash instead of types.
+
+Clicking on a row summary will take you to the *Snapshot* panel for that snapshot, with the
+actions
+- **Show Basket:** Edit the basket that the snapshot was created from
+- **Restore:** Requests the target Director database; clicking on the *Restore* button will begin
+ the process of restoring from the snapshot. Configuration types that are not in the snapshot
+ will not be replaced.
+- **Download:** Saves the snapshot as a local JSON file.
+
+followed by its creation date, checksum, and a list of all configured types (or custom
+selections).
+
+For each item in that list, the keywords *unchanged* or *new* will appear to the right.
+Clicking on *new* will show the differences between the version in the snapshot and the
+current configuration.
diff --git a/doc/60-CLI.md b/doc/60-CLI.md
new file mode 100644
index 0000000..5d35244
--- /dev/null
+++ b/doc/60-CLI.md
@@ -0,0 +1,719 @@
+<a id="CLI"></a>Director CLI
+============================
+
+Large parts of the Director's functionality are also available on your CLI.
+
+
+Manage Objects
+--------------
+
+Use `icingacli director <type> <action>` show, create modify or delete
+Icinga objects of a specific type:
+
+| Action | Description |
+|--------------|---------------------------------------|
+| `create` | Create a new object |
+| `delete` | Delete a specific object |
+| `exists` | Whether a specific object exists |
+| `set` | Modify an existing objects properties |
+| `show` | Show a specific object |
+
+
+Currently the following object types are available on CLI:
+
+* command
+* endpoint
+* host
+* hostgroup
+* notification
+* service
+* timeperiod
+* user
+* usergroup
+* zone
+
+
+### Create a new object
+
+Use this command to create a new Icinga object
+
+
+#### Usage
+
+`icingacli director <type> create [<name>] [options]`
+
+
+#### Options
+
+| Option | Description |
+|-------------------|-------------------------------------------------------|
+| `--<key> <value>` | Provide all properties as single command line options |
+| `--json` | Otherwise provide all options as a JSON string |
+
+
+#### Examples
+
+To create a new host you can provide all of its properties as command line
+parameters:
+
+```shell
+icingacli director host create localhost \
+ --imports generic-host \
+ --address 127.0.0.1 \
+ --vars.location 'My datacenter'
+```
+
+It would say:
+
+ Host 'localhost' has been created
+
+Providing structured data could become tricky that way. Therefore you are also
+allowed to provide JSON formatted properties:
+
+```shell
+icingacli director host create localhost \
+ --json '{ "address": "127.0.0.1", "vars": { "test": [ "one", "two" ] } }'
+```
+
+Passing JSON via STDIN is also possible:
+
+```shell
+icingacli director host create localhost --json < my-host.json
+```
+
+
+### Delete a specific object
+
+Use this command to delete a single Icinga object. Just run
+
+ icingacli director <type> delete <name>
+
+That's it. To delete the host created before, this would read
+
+ icingacli director host delete localhost
+
+It will tell you whether your command succeeded:
+
+ Host 'localhost' has been deleted
+
+
+### Whether a specific object exists
+
+Use this command to find out whether a single Icinga object exists. Just
+run:
+
+ icingacli director <type> exists <name>
+
+So if you run...
+
+ icingacli director host exists localhost
+
+...it will either tell you ...
+
+ Host 'localhost' exists
+
+...or:
+
+ Host 'localhost' does not exist
+
+When executed from custom scripts you could also just check the exit code,
+`0` means that the object exists, `1` that it doesn't.
+
+
+### Modify an existing objects properties
+
+Use this command to modify specific properties of an existing Icinga object.
+
+
+#### Usage
+
+ icingacli director <type> set <name> [options]
+
+
+#### Options
+
+| Option | Description |
+|----------------------------|-------------------------------------------------------|
+| `--<key> <value>` | Provide all properties as single command line options |
+| `--append-<key> <value>` | Appends to array values, like `imports`, |
+| | `groups` or `vars.system_owners` |
+| `--remove-<key> [<value>]` | Remove a specific property, eventually only |
+| | when matching `value`. In case the property is an |
+| | array it will remove just `value` when given |
+| `--json` | Otherwise provide all options as a JSON string |
+| `--replace` | Replace all object properties with the given ones |
+| `--auto-create` | Create the object in case it does not exist |
+| `--allow-overrides` | Set variable overrides for virtual Services |
+
+
+#### Examples
+
+```shell
+icingacli director host set localhost \
+ --address 127.0.0.2 \
+ --vars.location 'Somewhere else'
+```
+
+It will either tell you
+
+ Host 'localhost' has been modified
+
+or, when for example issued immediately a second time:
+
+ Host 'localhost' has not been modified
+
+Like create, this also allows you to provide JSON-formatted properties:
+
+```shell
+icingacli director host set localhost --json '{ "address": "127.0.0.2" }'
+```
+
+This command will fail in case the specified object does not exist. This is
+when the `--auto-create` parameter comes in handy. Command output will tell
+you whether an object has either been created or (not) modified.
+
+With `set` you only set the specified properties and do not touch the other
+ones. You could also want to completely override an object, purging all other
+eventually existing and unspecified parameters. Please use `--replace` if this
+is the desired behaviour.
+
+
+### Show a specific object
+
+Use this command to show single objects rendered as Icinga 2 config or
+in JSON format.
+
+
+#### Usage
+
+`icingacli director <type> show <name> [options]`
+
+
+#### Options
+
+| Option | Description |
+|-------------------|------------------------------------------------------|
+| `--resolved` | Resolve all inherited properties and show a flat |
+| | object |
+| `--json` | Use JSON format |
+| `--no-pretty` | JSON is pretty-printed per default (for PHP >= 5.4) |
+| | Use this flag to enforce unformatted JSON |
+| `--no-defaults` | Per default JSON output skips null or default values |
+| | With this flag you will get all properties |
+| `--with-services` | For hosts only, also shows attached services |
+
+### Clone an existing object
+
+Use this command to clone a specific object.
+
+#### Usage
+
+`icingacli director <type> clone <name> --from <original> [options]`
+
+#### Options
+
+| Option | Description |
+|---------------------|-----------------------------------------------------|
+| `--from <original>` | The name of the object you want to clone |
+| `--<key> <value>` | Override specific properties while cloning |
+| `--replace` | In case an object <name> already exists replace it |
+| | with the clone |
+| `--flat` | Do no keep inherited properties but create a flat |
+| | object with all resolved/inherited properties |
+
+#### Examples
+
+```shell
+icingacli director host clone localhost2 --from localhost
+```
+
+```shell
+icingacli director host clone localhost3 --from localhost --address 127.0.0.3
+```
+
+
+### Other interesting tasks
+
+
+#### Rename objects
+
+There is no rename command, but a simple `set` can easily accomplish this task:
+
+ icingacli director host set localhost --object_name localhost2
+
+Please note that it is usually absolutely no problem to rename objects with
+the Director. Even renaming something essential as a template like the famous
+`generic-host` will not cause any trouble. At least not unless you have other
+components outside your Director depending on that template.
+
+
+#### Disable an object
+
+Objects can be disabled. That way they will still exist in your Director DB,
+but they will not be part of your next deployment. Toggling the `disabled`
+property is all you need:
+
+ icingacli director host set localhost --disabled
+
+Valid values for booleans are `y`, `n`, `1` and `0`. So to re-enable an object
+you could use:
+
+ icingacli director host set localhost --disabled n
+
+
+#### Working with booleans
+
+As we learned before, `y`, `n`, `1` and `0` are valid values for booleans. But
+custom variables have no data type. And even if there is such, you could always
+want to change or override this from CLI. So you usually need to provide booleans
+in JSON format in case you need them in a custom variable.
+
+There is however one exception from this rule. CLI parameters without a given
+value are handled as boolean flags by the Icinga Web 2 CLI. That explains why
+the example disabling an object worked without passing `y` or `1`. You could
+use this also to set a custom variable to boolean `true`:
+
+ icingacli director host set localhost --vars.some_boolean
+
+Want to change it to false? No chance this way, you need to pass JSON:
+
+ icingacli director host set localhost --json '{ "vars.some_boolean": false }'
+
+This example shows the dot-notation to set a specific custom variable. If we
+have had used `{ "vars": { "some_boolean": false } }`, all other custom vars
+on this object would have been removed.
+
+
+#### Change object types
+
+The Icinga Director distincts between the following object types:
+
+| Type | Description |
+|-------------------|-------------------------------------------------------------|
+| `object` | The default object type. A host, a command and similar |
+| `template` | An Icinga template |
+| `apply` | An apply rule. This allows for assign rules |
+| `external_object` | An external object. Can be referenced and used, will not be |
+| | deployed |
+
+Example for creating a host template:
+
+```sh
+icingacli director host create 'Some template' \
+ --object_type template \
+ --check_command hostalive
+```
+
+Please take a lot of care when modifying object types, you should not do so for
+a good reason. The CLI allows you to issue operations that are not allowed in the
+web frontend. Do not use this unless you really understand its implications. And
+remember, with great power comes great responsibility.
+
+
+Import/Export Director Objects
+------------------------------
+
+Some objects are not directly related to Icinga Objects but used by the Director
+to manage them. To make it easier for administrators to for example pre-fill an
+empty Director Instance with Import Sources and Sync Rules, related import/export
+commands come in handy.
+
+Use `icingacli director export <type> [options]` to export objects of a specific
+type:
+
+| Type | Description |
+|-----------------------|-------------------------------------------------|
+| `datafields` | Export all DataField definitions |
+| `datalists` | Export all DataList definitions |
+| `hosttemplatechoices` | Export all IcingaTemplateChoiceHost definitions |
+| `importsources` | Export all ImportSource definitions |
+| `jobs` | Export all Job definitions |
+| `syncrules` | Export all SyncRule definitions |
+
+#### Options
+
+| Option | Description |
+|---------------|------------------------------------------------------|
+| `--no-pretty` | JSON is pretty-printed per default. Use this flag to |
+| | enforce unformatted JSON |
+
+Use `icingacli director import <type> < exported.json` to import objects of a
+specific type:
+
+| Type | Description |
+|-----------------------|-------------------------------------------------|
+| `importsources` | Import ImportSource definitions from STDIN |
+| `syncrules` | Import SyncRule definitions from STDIN |
+
+
+This feature is available since v1.5.0.
+
+
+Director Configuration Basket
+-----------------------------
+
+A basket contains a set of Director Configuration objects (like Templates,
+Commands, Import/Sync definitions - but not single Hosts or Services). This
+CLI command allows you to integrate them into your very own workflows
+
+## Available Actions
+
+| Action | Description |
+|------------|---------------------------------------------------|
+| `dump` | JSON-dump for objects related to the given Basket |
+| `list` | List configured Baskets |
+| `restore` | Restore a Basket from JSON dump provided on STDIN |
+| `snapshot` | Take a snapshot for the given Basket |
+
+### Options
+
+| Option | Description |
+|----------|------------------------------------------------------|
+| `--name` | `dump` and `snapshot` require a specific object name |
+
+Use `icingacli director basket restore < exported-basket.json` to restore objects
+from a specific basket. Take a snapshot or a backup first to be on the safe side.
+
+This feature is available since v1.6.0.
+
+
+Health Check Plugin
+-------------------
+
+You can use the Director CLI as an Icinga CheckPlugin and monitor your Director
+Health. This will run all or just one of the following test suites:
+
+| Name | Description |
+|--------------|-------------------------------------------------------------------|
+| `config` | Configuration, Schema, Migrations |
+| `sync` | All configured Sync Rules (pending changes are not a problem) |
+| `import` | All configured Import Sources (pending changes are not a problem) |
+| `jobs` | All configured Jobs (ignores disabled ones) |
+| `deployment` | Deployment Endpoint, last deployment outcome |
+
+#### Usage
+
+`icingacli director health check [options]`
+
+#### Options
+
+| Option | Description |
+|------------------|---------------------------------------|
+| `--check <name>` | Run only a specific test suite |
+| `--<db> <name>` | Use a specific Icinga Web DB resource |
+
+#### Examples
+
+```shell
+icingacli director health check
+```
+
+Example for running a check only for the configuration:
+
+```shell
+icingacli director health check --check config
+```
+
+Sample output:
+
+```
+Director configuration: 5 tests OK
+[OK] Database resource 'Director DB' has been specified'
+[OK] Make sure the DB schema exists
+[OK] There are no pending schema migrations
+[OK] Deployment endpoint is 'icinga.example.com'
+[OK] There is a single un-deployed change
+```
+
+
+Kickstart and schema handling
+-----------------------------
+
+The `kickstart` and the `migration` command are handled in the [automation section](03-Automation.md),
+so they are skipped here.
+
+
+Configuration handling
+----------------------
+
+### Render your configuration
+
+The Director distincts between rendering and deploying your configuration.
+Rendering means that Icinga 2 config will be pre-rendered and stored to the
+Director DB. Nothing bad happens if you decide to render the current config
+thousands of times in a loop. In case a config with the same checksum already
+exists, it will store - nothing.
+
+You can trigger config rendering by running
+
+```shell
+icingacli director config render
+```
+
+In case a new config has been created, it will tell you so:
+```
+New config with checksum b330febd0820493fb12921ad8f5ea42102a5c871 has been generated
+```
+
+Run it once again, and you'll see that the output changes:
+```
+Config with checksum b330febd0820493fb12921ad8f5ea42102a5c871 already exists
+```
+
+
+### Config deployment
+
+#### Usage
+
+`icingacli director config deploy [options]`
+
+#### Options
+
+| Option | Description |
+|----------------------------|------------------------------------------------------------------|
+| `--checksum <checksum>` | Optionally deploy a specific configuration |
+| `--force` | Force a deployment, even when the configuration hasn't changed |
+| `--wait <seconds>` | Optionally wait until Icinga completed it's restart |
+| `--grace-period <seconds>` | Do not deploy if a deployment took place less than <seconds> ago |
+
+#### Examples
+
+You do not need to explicitly render your config before deploying it to your
+Icinga 2 master node. Just trigger a deployment, it will re-render the current
+config:
+
+```shell
+icingacli director config deploy
+```
+
+The output tells you which config has been shipped:
+
+```
+Config 'b330febd0820493fb12921ad8f5ea42102a5c871' has been deployed
+```
+
+Director tries to avoid needless deployments, so in case you immediately deploy
+again, the output changes:
+```
+Config matches active stage, nothing to do
+```
+
+You can override this by adding the `--force` parameter. It will then tell you:
+
+```
+Config matches active stage, deploying anyway
+```
+
+In case you want to do not want `deploy` to waste time to re-render your
+config or in case you decide to re-deploy a specific, eventually older config
+version the `deploy` command allows you to provide a specific checksum:
+
+```shell
+icingacli director config deploy --checksum b330febd0820493fb12921ad8f5ea42102a5c871
+```
+
+When using `icingacli` deployments in an automated way, and want to avoid fast
+consecutive deployments, you can provide a grace period:
+
+```shell
+icingacli director config deploy --grace-period 300
+```
+
+### Deployments status
+In case you want to fetch the information about the deployments status,
+you can call the following CLI command:
+```shell
+icingacli director config deploymentstatus
+```
+```json
+{
+ "active_configuration": {
+ "stage_name": "5c65cae0-4f1b-47b4-a890-766c82681622",
+ "config": "617b9cbad9e141cfc3f4cb636ec684bd60073be1",
+ "activity": "4f7bc6600dd50a989f22f82d3513e561ef333363"
+ }
+}
+```
+In case there is no active stage name related to the Director, active_configuration
+is set to null.
+
+Another possibility is to pass a list of checksums to fetch the status of
+specific deployments and (activity log) activities.
+Following, you can see an example of how to do it:
+```shell
+icingacli director config deploymentstatus \
+ --configs 617b9cbad9e141cfc3f4cb636ec684bd60073be1 \
+ --activities 4f7bc6600dd50a989f22f82d3513e561ef333363
+```
+```json
+{
+ "active_configuration": {
+ "stage_name": "5c65cae0-4f1b-47b4-a890-766c82681622",
+ "config": "617b9cbad9e141cfc3f4cb636ec684bd60073be1",
+ "activity": "4f7bc6600dd50a989f22f82d3513e561ef333363"
+ },
+ "configs": {
+ "617b9cbad9e141cfc3f4cb636ec684bd60073be1": "active"
+ },
+ "activities": {
+ "4f7bc6600dd50a989f22f82d3513e561ef333363": "active"
+ }
+}
+```
+
+You can also decide to access directly to a value inside the result JSON by
+using the `--key` param:
+```shell
+icingacli director config deploymentstatus \
+ --configs 617b9cbad9e141cfc3f4cb636ec684bd60073be1 \
+ --activities 4f7bc6600dd50a989f22f82d3513e561ef333363 \
+ --key active_configuration.config
+```
+```
+617b9cbad9e141cfc3f4cb636ec684bd60073be1
+```
+
+
+
+### Cronjob usage
+
+You could decide to pre-render your config in the background quite often. As of
+this writing this has one nice advantage. It allows the GUI to find out whether
+a bunch of changes still results into the very same config.
+only one
+
+
+Run sync and import jobs
+------------------------
+
+### Import Sources
+
+#### List available Import Sources
+
+This shows a table with your defined Import Sources, their IDs and
+current state. As triggering Imports requires an ID, this is where you
+can look up the desired ID.
+
+`icingacli director importsource list`
+
+#### Check a given Import Source for changes
+
+This command fetches data from the given Import Source and compares it
+to the most recently imported data.
+
+`icingacli director importsource check --id <id>`
+
+##### Options
+
+| Option | Description |
+|---------------|---------------------------------------------------------|
+| `--id <id>` | An Import Source ID. Use the list command to figure out |
+| `--benchmark` | Show timing and memory usage details |
+
+#### Fetch data from a given Import Source
+
+This command fetches data from the given Import Source and outputs them
+as plain JSON
+
+`icingacli director importsource fetch --id <id>`
+
+##### Options
+
+| Option | Description |
+|---------------|---------------------------------------------------------|
+| `--id <id>` | An Import Source ID. Use the list command to figure out |
+| `--benchmark` | Show timing and memory usage details |
+
+#### Trigger an Import Run for a given Import Source
+
+This command fetches data from the given Import Source and stores it to
+the Director DB, so that the next related Sync Rule run can work with
+fresh data. In case data didn't change, nothing is going to be stored.
+
+`icingacli director importsource run --id <id>`
+
+##### Options
+
+| Option | Description |
+|---------------|---------------------------------------------------------|
+| `--id <id>` | An Import Source ID. Use the list command to figure out |
+| `--benchmark` | Show timing and memory usage details |
+
+### Sync Rules
+
+#### List defined Sync Rules
+
+This shows a table with your defined Sync Rules, their IDs and current
+state. As triggering a Sync requires an ID, this is where you can look
+up the desired ID.
+
+`icingacli director syncrule list`
+
+#### Check a given Sync Rule for changes
+
+This command runs a complete Sync in memory but doesn't persist eventual
+changes.
+
+`icingacli director syncrule check --id <id>`
+
+##### Options
+
+| Option | Description |
+|---------------|----------------------------------------------------|
+| `--id <id>` | A Sync Rule ID. Use the list command to figure out |
+| `--benchmark` | Show timing and memory usage details |
+
+#### Trigger a Sync Run for a given Sync Rule
+
+This command builds new objects according your Sync Rule, compares them
+with existing ones and persists eventual changes.
+
+`icingacli director syncrule run --id <id>`
+
+##### Options
+
+| Option | Description |
+|---------------|----------------------------------------------------|
+| `--id <id>` | A Sync Rule ID. Use the list command to figure out |
+| `--benchmark` | Show timing and memory usage details |
+
+
+Database housekeeping
+---------------------
+
+Your database may grow over time and ask for various housekeeping tasks. You
+can usually store a lot of data in your Director DB before you would even
+notice a performance impact.
+
+Still, we started to prepare some tasks that assist with removing useless
+garbage from your DB. You can show available tasks with:
+
+ icingacli director housekeeping tasks
+
+The output might look as follows:
+
+```
+ Housekeeping task (name) | Count
+-----------------------------------------------------------|-------
+ Undeployed configurations (oldUndeployedConfigs) | 3
+ Unused rendered files (unusedFiles) | 0
+ Unlinked imported row sets (unlinkedImportedRowSets) | 0
+ Unlinked imported rows (unlinkedImportedRows) | 0
+ Unlinked imported properties (unlinkedImportedProperties) | 0
+```
+
+You could run a specific task with
+
+ icingacli director housekeeping run <taskName>
+
+...like in:
+
+ icingacli director housekeeping run unlinkedImportedRows
+
+Or you could also run all of them, that's the preferred way of doing this:
+
+ icingacli director housekeeping run ALL
+
+Please note that some tasks once issued create work for other tasks, as
+lost imported rows might appear once you remove lost row sets. So `ALL`
+is usually the best choice as it runs all of them in the best order.
diff --git a/doc/70-Import-and-Sync.md b/doc/70-Import-and-Sync.md
new file mode 100644
index 0000000..a413ab9
--- /dev/null
+++ b/doc/70-Import-and-Sync.md
@@ -0,0 +1,88 @@
+<a id="Import-and-Sync"></a>Import and Synchronization
+======================================================
+
+Icinga Director offers very powerful mechanisms when it comes to fetching data
+from external data sources.
+
+The following examples should give you a quick idea of what you might want to
+use this feature for. Please note that Import Data Sources are implemented as
+hooks in Director. This means that it is absolutely possible and probably very
+easy to create custom data sources for whatever kind of data you have. And you
+do not need to modify the Director source code for this, you can ship your very
+own importer in your very own Icinga Web 2 module.
+
+Examples
+--------
+
+### Import Servers from MS Active Directory
+
+#### Create a new import source
+
+Importing data from LDAP sources is pretty easy. We use MS Active Directory
+as an example source:
+
+![Import source](screenshot/director/08_import-and-sync/081_director_import_source.png)
+
+You must formerly have configured a corresponding LDAP resource in your Icinga Web.
+Then you choose your preferred object class, you might add custom filters, a search
+base should always be set.
+
+The only tricky part here are the chosen Properties. You must know them and you
+are required to fill them in, no way around this right now. Also please choose one
+column as your key column.
+
+In case you want to avoid trouble please make this the column that corresponds to
+your desired object name for the objects you are going to import. Rows duplicating
+this property will be considered erroneous, the Import would fail.
+
+#### Property modifiers
+
+Data sources like SQL databases provide very powerful modifiers themselves. With a
+handcrafted query you can solve lots of data conversion problems. Sometimes this is
+not possible, and some sources (like LDAP) do not even have such features.
+
+This is where property modifiers jump in to the rescue. Your computer names are
+uppercase and you hate this? Use the lowercase modifier:
+
+![Lowercase modifier](screenshot/director/08_import-and-sync/082_director_import_modifier_lowercase.png)
+
+You want to have the object SID as a custom variable, but the data is stored
+binary in your AD? There is a dedicated modifier:
+
+![SID modifier](screenshot/director/08_import-and-sync/083_director_import_modifier_sid.png)
+
+You do not agree with the way Microsoft represents its version numbers? Regular
+expressions are able to fix everything:
+
+![Regular expression modifier](screenshot/director/08_import-and-sync/084_director_import_modifier_regex.png)
+
+#### Preview
+
+A quick look at the preview confirms that we reached a good point, that's the data
+we want:
+
+![Import preview](screenshot/director/08_import-and-sync/085_director_import_preview.png)
+
+#### Synchronization
+
+The Import itself just fetches raw data, it does not yet try to modify any of your
+Icinga objects. That's what the Sync rules have been designed for. This distinction
+has a lot of advantages when it goes to automatic scheduling for various import and
+sync jobs.
+
+When creating a Synchronization rule, you must decide which Icinga objects you want
+to work with. You could decide to use the same import source in various rules with
+different filters and properties.
+
+![Synchronization rule](screenshot/director/08_import-and-sync/086_director_sync_rule_ad_hosts.png)
+
+For every property you must decide whether and how it should be synchronized. You
+can also define custom expressions, combine multiple source fields, set custom
+properties based on custom conditions and so on.
+
+![Synchronization properties](screenshot/director/08_import-and-sync/087_director_sync_properties_ad_host.png)
+
+Now you are all done and ready to a) launch the Import and b) trigger your synchronization
+run.
+
+
diff --git a/doc/70-REST-API.md b/doc/70-REST-API.md
new file mode 100644
index 0000000..dd5d266
--- /dev/null
+++ b/doc/70-REST-API.md
@@ -0,0 +1,684 @@
+<a id="REST-API"></a>The Icinga Director REST API
+=================================================
+
+Introduction
+------------
+
+Icinga Director has been designed with a REST API in mind. Most URLs you
+can access with your browser will also act as valid REST url endpoints.
+
+Base Headers
+------------
+All your requests MUST have a valid accept header. The only acceptable
+variant right now is `application/json`, so please always append a header
+as follows to your requests:
+
+ Accept: application/json
+
+
+Authentication
+--------------
+Please use HTTP authentication and any valid Icinga Web 2 user, granted
+enough permissions to accomplish the desired actions. The restrictions
+and permissions that have been assigned to web users will also be enforced
+for API users. In addition, the permission `director/api` is required for
+any API access.
+
+Versioning
+----------
+
+There are no version strings so far in the Director URLs. We will try hard
+to not break compatibility with future versions. Sure, sooner or later we
+also might be forced to introduce some kind of versioning. But who knows?
+
+As a developer you can trust us to not remove any existing REST url or any
+provided property. However, you must always be ready to accept new properties.
+
+URL scheme and supported methods
+--------------------------------
+
+We support GET, POST, PUT and DELETE.
+
+| Method | Meaning |
+|--------|---------------------------------------------------------------------|
+| GET | Read / fetch data. Not allowed to run operations with the potential |
+| | to cause any harm |
+| POST | Trigger actions, create or modify objects. Can also be used to |
+| | partially modify objects |
+| PUT | Creates or replaces objects, cannot be used to modify single object |
+| | properties |
+| DELETE | Remove a specific object |
+
+TODO: more examples showing the difference between POST and PUT
+
+POST director/host
+ gives 201 on success
+GET director/host?name=hostname.example.com
+PUT director/host?name=hostname.example.com
+ gives 200 ok on success and 304 not modified on no change
+DELETE director/host?name=hostname.example.com
+ gives 200 on success
+
+
+First example request with CURL
+-------------------------------
+
+```sh
+curl -H 'Accept: application/json' \
+ -u 'username:password' \
+ 'https://icinga.example.com/icingaweb2/director/host?name=hostname.example.com'
+```
+
+### CURL helper script
+
+A script like the following makes it easy to play around with curl:
+
+```sh
+METHOD=$1
+URL=$2
+BODY="$3"
+USERNAME="demo"
+PASSWORD="***"
+test -z "$PASSWORD" || USERNAME="$USERNAME:$PASSWORD"
+
+test -z "$BODY" && curl -u "$USERNAME" \
+ -i https://icingaweb/icingaweb/$URL \
+ -H 'Accept: application/json' \
+ -X $METHOD
+
+test -z "$BODY" || curl -u "$USERNAME" \
+ -i https://icingaweb/icingaweb/$URL \
+ -H 'Accept: application/json' \
+ -X $METHOD \
+ -d "$BODY"
+
+echo
+```
+
+It can be used as follows:
+
+```sh
+director-curl GET director/host?name=localhost
+
+director-curl POST director/host '{"object_name": "host2", "... }'
+```
+
+
+Should I use HTTPS?
+-------------------
+
+Sure, absolutely, no doubt. There is no, absolutely no reason to NOT use
+HTTPS these days. Especially not for a configuration tool allowing you to
+configure check commands that are going to be executed on all your servers.
+
+Icinga Objects
+--------------
+
+### Special parameters
+
+| Parameter | Description |
+|----------------|-------------------------------------------------------------|
+| resolved | Resolve all inherited properties and show a flat object |
+| withNull | Retrieve default (null) properties also |
+| withServices | Show services attached to a host. `resolved` and `withNull` |
+| | are applied for services too |
+| allowOverrides | Set variable overrides for virtual Services |
+| showStacktrace | Returns the related stack trace, in case an error occurs |
+
+#### Resolve object properties
+
+In case you add the `resolved` parameter to your URL, all inherited object
+properties will be resolved. Such a URL could look as follows:
+
+ director/host?name=hostname.example.com&resolved
+
+
+#### Retrieve default (null) properties also
+
+Per default properties with `null` value are skipped when shipping a result.
+You can influence this behavior with the `properties` parameter. Just append
+`&withNull` to your URL:
+
+ director/host?name=hostname.example.com&withNull
+
+
+#### Fetch host with it's services
+
+This is what the `withServices` parameter exists:
+
+ director/host?name=hostname.example.com&withServices
+
+
+#### Retrieve only specific properties
+
+The `properties` parameter also allows you to specify a list of specific
+properties. In that case, only the given properties will be returned, even
+when they have no (`null`) value:
+
+ director/host?name=hostname.example.com&properties=object_name,address,vars
+
+
+#### Override vars for inherited/applied Services
+
+Enabling `allowOverrides` allows you to let Director figure out, whether your
+modified Custom Variables need to be applied to a specific individual Service,
+or whether setting Overrides at Host level is the way to go.
+
+ POST director/service?name=Uptime&host=hostname.example.com&allowOverrices
+
+```json
+{ "vars.uptime_warning": 300 }
+```
+
+In case `Uptime` is an Apply Rule, calling this without `allowOverrides` will
+trigger a 404 response. Please note that when modifying the Host object, the
+body for response 200 will show the Host object, as that's the one that has
+been modified.
+
+### Example
+
+GET director/host?name=pe2015.example.com
+```json
+{
+ "address": "127.0.0.3",
+ "check_command": null,
+ "check_interval": null,
+ "display_name": "pe2015 (example.com)",
+ "enable_active_checks": null,
+ "flapping_threshold": null,
+ "groups": [ ],
+ "imports": [
+ "generic-host"
+ ],
+ "retry_interval": null,
+ "vars": {
+ "facts": {
+ "aio_agent_build": "1.2.5",
+ "aio_agent_version": "1.2.5",
+ "architecture": "amd64",
+ "augeas": {
+ "version": "1.4.0"
+ },
+ ...
+}
+```
+
+director/host?name=pe2015.example.com&resolved
+```json
+{
+ "address": "127.0.0.3",
+ "check_command": "tom_ping",
+ "check_interval": "60",
+ "display_name": "pe2015 (example.com)",
+ "enable_active_checks": true,
+ "groups": [ ],
+ "imports": [
+ "generic-host"
+ ],
+ "retry_interval": "10",
+ "vars": {
+ "facts": {
+ "aio_agent_build": "1.2.5",
+ "aio_agent_version": "1.2.5",
+ "architecture": "amd64",
+ "augeas": {
+ "version": "1.4.0"
+ },
+ ...
+}
+```
+
+JSON is pretty-printed per default, at least for PHP >= 5.4
+
+Error handling
+--------------
+
+Director tries hard to return meaningful output and error codes:
+```
+HTTP/1.1 400 Bad Request
+Server: Apache
+Content-Length: 46
+Connection: close
+Content-Type: application/json
+```
+
+```json
+{
+ "error": "Invalid JSON: Syntax error"
+}
+```
+
+Trigger actions
+---------------
+You can of course also use the API to trigger specific actions. Deploying the configuration is as simple as issueing:
+
+ POST director/config/deploy
+
+More
+----
+
+Currently we do not handle Last-Modified und ETag headers. This would involve some work, but could be a cool feature. Let us know your ideas!
+
+
+Sample scenario
+---------------
+
+Let's show you how the REST API works with a couple of practical examples:
+
+### Create a new host
+
+```
+POST director/host
+```
+
+```json
+{
+ "object_name": "apitest",
+ "object_type": "object",
+ "address": "127.0.0.1",
+ "vars": {
+ "location": "Berlin"
+ }
+}
+```
+#### Response
+```
+HTTP/1.1 201 Created
+Date: Tue, 01 Mar 2016 04:43:55 GMT
+Server: Apache
+Content-Length: 140
+Content-Type: application/json
+```
+
+```json
+{
+ "address": "127.0.0.1",
+ "object_name": "apitest",
+ "object_type": "object",
+ "vars": {
+ "location": "Berlin"
+ }
+}
+```
+
+The most important part of the response is the response code: `201`, a resource has been created. Just for fun, let's fire the same request again. The answer obviously changes:
+
+```
+HTTP/1.1 500 Internal Server Error
+Date: Tue, 01 Mar 2016 04:45:04 GMT
+Server: Apache
+Content-Length: 60
+Connection: close
+Content-Type: application/json
+```
+
+```json
+{
+ "error": "Trying to recreate icinga_host (apitest)"
+}
+```
+
+So, let's update this host. To work with existing objects, you must ship their `name` in the URL:
+
+ POST director/host?name=apitest
+
+```json
+{
+ "object_name": "apitest",
+ "object_type": "object",
+ "address": "127.0.0.1",
+ "vars": {
+ "location": "Berlin"
+ }
+}
+```
+
+Same body, so no change:
+```
+HTTP/1.1 304 Not Modified
+Date: Tue, 01 Mar 2016 04:45:33 GMT
+Server: Apache
+```
+
+So let's now try to really change something:
+
+ POST director/host?name=apitest
+
+```json
+{"address": "127.0.0.2", "vars.event": "Icinga CAMP" }
+```
+
+We get status `200`, changes have been applied:
+
+```
+HTTP/1.1 200 OK
+Date: Tue, 01 Mar 2016 04:46:25 GMT
+Server: Apache
+Content-Length: 172
+Content-Type: application/json
+```
+
+```json
+{
+ "address": "127.0.0.2",
+ "object_name": "apitest",
+ "object_type": "object",
+ "vars": {
+ "location": "Berlin",
+ "event": "Icinga CAMP"
+ }
+}
+```
+
+The response always returns the full object on modification. This way you can immediately investigate the merged result. As you can see, `POST` requests only touch the parameters you passed - the rest remains untouched.
+
+One more example to prove this:
+
+```
+POST director/host?name=apitest
+```
+
+```json
+{"address": "127.0.0.2", "vars.event": "Icinga CAMP" }
+```
+
+No modification, you get a `304`. HTTP standards strongly discourage shipping a body in this case:
+```
+HTTP/1.1 304 Not Modified
+Date: Tue, 01 Mar 2016 04:52:05 GMT
+Server: Apache
+```
+
+As you might have noted, we only changed single properties in the vars dictionary. Now lets override the whole dictionary:
+
+```
+POST director/host?name=apitest
+```
+
+```json
+{"address": "127.0.0.2", "vars": { "event": [ "Icinga", "Camp" ] } }
+```
+
+The response shows that this works as expected:
+
+```
+HTTP/1.1 200 OK
+Date: Tue, 01 Mar 2016 04:52:33 GMT
+Server: Apache
+Content-Length: 181
+Content-Type: application/json
+```
+
+```json
+{
+ "address": "127.0.0.2",
+ "object_name": "apitest",
+ "object_type": "object",
+ "vars": {
+ "event": [
+ "Icinga",
+ "Camp"
+ ]
+ }
+}
+```
+
+If merging properties is not what you want, `PUT` comes to the rescue:
+
+ PUT director/host?name=apitest
+
+```
+{ "vars": { "event": [ "Icinga", "Camp" ] }
+```
+
+All other properties vanished, all but name and type:
+```
+HTTP/1.1 200 OK
+Date: Tue, 01 Mar 2016 04:54:33 GMT
+Server: Apache
+Content-Length: 153
+Content-Type: application/json
+```
+
+```json
+{
+ "object_name": "apitest",
+ "object_type": "object",
+ "vars": {
+ "event": [
+ "Icinga",
+ "Camp"
+ ]
+ }
+}
+```
+
+Let's put "nothing":
+
+ PUT director/host?name=apitest
+
+```json
+{}
+```
+
+Works as expected:
+
+```
+HTTP/1.1 200 OK
+Date: Tue, 01 Mar 2016 04:57:35 GMT
+Server: Apache
+Content-Length: 62
+Content-Type: application/json
+```
+
+```json
+{
+ "object_name": "apitest",
+ "object_type": "object"
+}
+```
+
+Of course, `PUT` also supports `304`, you can check this by sending the same request again.
+
+Now let's try to cheat:
+
+ KILL director/host?name=apitest
+
+```
+HTTP/1.1 400 Bad Request
+Date: Tue, 01 Mar 2016 04:54:07 GMT
+Server: Apache
+Content-Length: 43
+Connection: close
+Content-Type: application/json
+```
+
+```json
+{
+ "error": "Unsupported method KILL"
+}
+```
+
+Ok, no way. So let's use the correct method:
+
+ DELETE director/host?name=apitest
+
+```
+HTTP/1.1 200 OK
+Date: Tue, 01 Mar 2016 05:59:22 GMT
+Server: Apache
+Content-Length: 109
+Content-Type: application/json
+```
+
+```json
+{
+ "imports": [
+ "generic-host"
+ ],
+ "object_name": "apitest",
+ "object_type": "object"
+}
+```
+
+### Service Apply Rules
+
+Please note that Service Apply Rule names are not unique in Icinga 2. They are
+not real objects, they are creating other objects in a loop. This makes it
+impossible to distinct them by name. Therefore, a dedicated REST API endpoint
+`director/serviceapplyrules` ships all Service Apply Rules combined with their
+internal ID. This ID can then be used to modify or delete a Rule via
+`director/service`.
+
+### Deployment Status
+In case you want to fetch the information about the deployments status,
+you can call the following API:
+
+ GET director/config/deployment-status
+
+```
+HTTP/1.1 200 OK
+Date: Wed, 07 Oct 2020 13:14:33 GMT
+Server: Apache
+Content-Type: application/json
+```
+
+```json
+{
+ "active_configuration": {
+ "stage_name": "b191211d-05cb-4679-842b-c45170b96421",
+ "config": "617b9cbad9e141cfc3f4cb636ec684bd60073be1",
+ "activity": "028b3a19ca7457f5fc9dbb5e4ea527eaf61616a2"
+ }
+}
+```
+This throws a 500 in case Icinga isn't reachable.
+In case there is no active stage name related to the Director, active_configuration
+is set to null.
+
+Another possibility is to pass a list of checksums to fetch the status of
+specific deployments and (activity log) activities.
+Following, you can see an example of how to do it:
+
+ GET director/config/deployment-status?config_checksums=617b9cbad9e141cfc3f4cb636ec684bd60073be2,
+ 617b9cbad9e141cfc3f4cb636ec684bd60073be1&activity_log_checksums=617b9cbad9e141cfc3f4cb636ec684bd60073be1,
+ 028b3a19ca7457f5fc9dbb5e4ea527eaf61616a2
+
+```json
+{
+ "active_configuration": {
+ "stage_name": "b191211d-05cb-4679-842b-c45170b96421",
+ "config": "617b9cbad9e141cfc3f4cb636ec684bd60073be1",
+ "activity": "028b3a19ca7457f5fc9dbb5e4ea527eaf61616a2"
+ },
+ "configs": {
+ "617b9cbad9e141cfc3f4cb636ec684bd60073be2": "deployed",
+ "617b9cbad9e141cfc3f4cb636ec684bd60073be1": "active"
+ },
+ "activities": {
+ "617b9cbad9e141cfc3f4cb636ec684bd60073be1": "undeployed",
+ "028b3a19ca7457f5fc9dbb5e4ea527eaf61616a2": "active"
+ }
+}
+```
+The list of possible status is:
+* `active`: whether this configuration is currently active
+* `deployed`: whether this configuration has ever been deployed
+* `failed`: whether the deployment of this configuration has failed
+* `undeployed`: whether this configuration has been rendered, but not yet deployed
+* `unknown`: whether no configurations have been found for this checksum
+
+### Agent Tickets
+
+The Director is very helpful when it goes to manage your Icinga Agents. In
+case you want to fetch tickets through the API, please do as follows:
+
+ GET director/host/ticket?name=apitest
+
+```
+HTTP/1.1 200 OK
+Date: Thu, 07 Apr 2016 22:19:24 GMT
+Server: Apache
+Content-Length: 43
+Content-Type: application/json
+```
+
+```json
+"5de9883080e03278039bce57e4fbdbe8fd262c40"
+```
+
+Please expect an error in case the host does not exist or has not been
+configured to be an Icinga Agent.
+
+### Self Service API
+
+#### Theory of operation
+
+Icinga Director offers a Self Service API, allowing new Icinga nodes to register
+themselves. No credentials are required, authentication is based on API keys.
+There are two types of such keys:
+
+* Host Template API keys
+* Host Object API keys
+
+Template keys basically grant the permission to:
+
+* Create a new host based on that template
+* Specify name and address properties for that host
+
+This is a one-time operation and allows one to claim ownership of a specific host.
+Now, there are two possible scenarios:
+
+* The host already exists
+* The host is not known to Icinga Director
+
+In case the host already exists, Director will check whether it's API key matches
+the given one. [..]
+
+#### Request processing for Host registration
+
+A new node will `POST` to `self-service/register-host`, with two parameters in
+the URL:
+
+* `name`: it's desired object name, usually the FQDN
+* `key`: a valid Host Template API key
+
+In it's body it is allowed to specify a specific set of properties. At the time
+of this writing, these are:
+
+* `display_name`
+* `address`
+* `address6`
+
+Director will validate the `key` and load the corresponding *Host Template*. In
+case no such is found, the request is rejected. Then it checks whether a *Host*
+with the given `name` exists. In case it does, the request is rejected unless:
+
+* It inherits the loaded *Host Template*
+* It already has an API key
+
+If these conditions match, the request is processed. The following sketch roughly shows the decision tree (AFTER the key has been
+validated):
+
+```
+ +-----------------------------+
+ +--------------+ | * Validate given properties |
+ | Host exists? | -- NO --> | * Create new host object |-----------+
+ +--------------+ | * Return new Host API key | |
+ | +-----------------------------+ |
+ YES |
+ | |
+ v +-----------------------------+ |
+ +----------------------+ | * Validate given properties | |
+ | Host has an API key? | -- NO --> | * Apply eventual changes |----+
+ +----------------------+ | * Return new Host API key | |
+ | +-----------------------------+ |
+ YES |
+ | +-------------------+
+ v |
+ +--------------------+ v
+ | Reject the request | +---------------------+
+ +--------------------+ | Client persists the |
+ | new Host API key |
+ +---------------------+
+```
diff --git a/doc/74-Self-Service-API.md b/doc/74-Self-Service-API.md
new file mode 100644
index 0000000..897fd72
--- /dev/null
+++ b/doc/74-Self-Service-API.md
@@ -0,0 +1,49 @@
+<a id="Self-Service-API"></a>Self Service API
+=============================================
+
+Introduction
+------------
+
+Icinga Director offers a Self Service API, allowing new Hosts running the Icinga
+Agent to register themselves in a secure way.
+
+### Windows Agents
+
+Windows Agents are the main target audience for this feature. It allows you to
+generate a single Powershell Script based on the [Icinga 2 Powershell Module](
+https://github.com/Icinga/icinga2-powershell-module
+). You can either use the same script for all of your Windows Hosts or generate
+different ones for different kind of systems.
+
+This installation script could then be shipped with your base images, invoked
+remotely via **PowerShell Remoting**, distributed as a module via **Group
+Policies** and/or triggered via **Run-Once** (AD Policies).
+
+### Linux Agents
+
+At the time of this writing, we do not ship a script with all the functionality
+you can find in the Windows Powershell script. Linux and Unix environments are
+mostly highly automated these days, and such a magic shell script is often not
+what people want.
+
+Still, you can also benefit from this feature by directly using our [Self Service
+REST API](70-REST-API.md). It should be easy to integrate it into
+the automation tool of your choice.
+
+Base Configuration
+------------------
+
+You have full control over the automation Script generated by the Icinga Director.
+Please got to the **Infrastructure Dashboard** and choose the **Self Service API**:
+
+![Infrastructure Dashboard - Self Service API](screenshot/director/74_self-service-api/7401-director_self-service-dashboard.png)
+
+This leads to the Self Service API Settings form. Most settings are self-explaining
+and come with detailled inline hints. The most important choice is whether the
+script should automatically install the Icinga Agent:
+
+![Settings - Choose installation source](screenshot/director/74_self-service-api/7402-director_self-service-choose-source.png)
+
+In case you opted for automated installation, more options will pop up:
+
+![Settings - Installer Details](screenshot/director/74_self-service-api/7403-director_self-service-settings.png)
diff --git a/doc/75-Background-Daemon.md b/doc/75-Background-Daemon.md
new file mode 100644
index 0000000..69cecfc
--- /dev/null
+++ b/doc/75-Background-Daemon.md
@@ -0,0 +1,65 @@
+<a id="Background-Daemon"></a>Background-Daemon
+===============================================
+
+The Icinga Director Background Daemon is available (and mandatory) since v1.7.0.
+It is responsible for various background tasks, including fully automated Import,
+Sync & Config Deployment Tasks.
+
+Daemon Installation
+-------------------
+
+To run the Background Daemon, you need to tell `systemd` about your new service.
+First make sure that the system user `icingadirector` exists. In case it doesn't,
+please create one:
+
+```sh
+useradd -r -g icingaweb2 -d /var/lib/icingadirector -s /bin/false icingadirector
+install -d -o icingadirector -g icingaweb2 -m 0750 /var/lib/icingadirector
+```
+
+Then copy the provided Unit-File from our [contrib](../contrib/systemd/icinga-director.service)
+to `/etc/systemd/system`, enable and start the service:
+
+```sh
+MODULE_PATH=/usr/share/icingaweb2/modules/director
+cp "${MODULE_PATH}/contrib/systemd/icinga-director.service" /etc/systemd/system/
+systemctl daemon-reload
+```
+
+Now your system knows about the Icinga Director Daemon. You should make sure that
+it starts automatically each time your system boots:
+
+```sh
+systemctl enable icinga-director.service
+```
+
+Starting the Daemon
+-------------------
+
+You now can start the Background daemon like any other service on your Linux system:
+
+```sh
+systemctl start icinga-director.service
+```
+
+Stopping the Daemon
+-------------------
+
+You now can stop the Background daemon like any other service on your Linux system:
+
+```sh
+systemctl stop icinga-director.service
+```
+
+Getting rid of the old Job Daemon
+---------------------------------
+
+Before v1.7.0, Icinga Director shipped an optional Job Daemon. This one is no longer
+needed and should be removed from your system as follows:
+
+```sh
+systemctl stop director-jobs
+systemctl disable director-jobs
+rm /etc/systemd/system/director-jobs.service
+systemctl daemon-reload
+```
diff --git a/doc/79-Jobs.md b/doc/79-Jobs.md
new file mode 100644
index 0000000..09ab602
--- /dev/null
+++ b/doc/79-Jobs.md
@@ -0,0 +1,40 @@
+<a id="Jobs"></a>Jobs
+=====================
+
+The [background daemon](75-Background-Daemon.md) is responsible for running
+Jobs accoring our schedule. Director allows you to schedule eventually long-
+running tasks so that they can run in the background.
+
+Currently this includes:
+
+* Import runs
+* Sync runs
+* Housekeeping tasks
+* Config rendering and deployment
+
+This component is internally provided as a Hook. This allows other Icinga
+Web 2 modules to benefit from the Job Runner by providing their very own Job
+implementations.
+
+Theory of operation
+-------------------
+
+Jobs are configured via the Web frontend. You can create multiple definitions
+for the very same Job. Every single job will run with a configurable interval.
+Please do not expect this to behave like a scheduler or a cron daemon. Jobs
+are currently not executed in parallel. Therefore if one job takes longer, it
+might have an influence on the scheduling of other jobs.
+
+Some of you might want actions like automated config deployment not to be
+executed all around the clock. That's why you have the possibility to assign
+time periods to your jobs. Choose an Icinga timeperiod, the job will only be
+executed within that period.
+
+Time periods
+------------
+
+Icinga time periods can get pretty complex. You configure them with Director,
+but until now it didn't have the necessity to "understand" them. This of course
+changed with Time Period support in our Job Runner. Director will try to fully
+"understand" periods in future, but right now it is only capable to interpret
+a limited subset of timeperiod range definitions.
diff --git a/doc/80-FAQ.md b/doc/80-FAQ.md
new file mode 100644
index 0000000..0721b58
--- /dev/null
+++ b/doc/80-FAQ.md
@@ -0,0 +1,75 @@
+<a id="FAQ"></a>Frequently Asked Questions
+==========================================
+
+I got an exception...
+---------------------
+
+This section tries to summarize well known pitfalls and their solution.
+
+### Binary data corruption with ZF 1.12.6 and 1.12.17
+
+When deploying your first configuration, you might get this error:
+
+ Refusing to render the configuration, your DB layer corrupts
+ binary data. You might be affected by Zend Framework bug #655
+
+Zend Framework 1.12.16 and 1.12.17 silently [corrupt binary data](https://github.com/zendframework/zf1/issues/655).
+This has been [fixed](https://github.com/zendframework/zf1/pull/670) with
+[1.12.18](https://github.com/zendframework/zf1/releases/tag/release-1.12.18),
+please either upgrade or downgrade to an earlier version. Debian Stable currently
+ships 1.12.9, but as they backported the involved erraneous security bug their
+version has been affected too. In the meantime they also backported the fix for
+the fix, so Debian should no longer show this error.
+
+When you work on a RedHat-based distribution please follow
+[Bug 1328032](https://bugzilla.redhat.com/show_bug.cgi?id=1328032). The new
+release reached Fedora EPEL 6 and EPEL 7, so this should no longer be an issue
+on related platforms.
+
+You could also manually fix this issue in `/usr/share/php/Zend/Db/Adapter/Pdo/Abstract.php`.
+Search for the `_quote` function and delete the line saying:
+
+```php
+$value = addcslashes($value, "\000\032");
+```
+
+Please note that doing so would fix all problems, but re-introduce a potential
+security issue affecting the MSSQL and Sqlite adapters.
+
+### Connection error when setting up the database
+
+When setting up and validating a database connection for Director in Icinga Web 2,
+the following error might occur:
+
+ SQLSTATE[HY000]: General error: 2014 Cannot execute queries while
+ other unbuffered queries are active.
+
+This happens with some PHP versions, we have not been able to figure out which ones
+and why. However, we found a workaround and and fixed this in Icinga Web 2. Please
+upgrade to the latest version, the issue should then be gone.
+
+You probably didn't notice this error before as in most environments the IDO for
+historical reasons isn't configured for UTF-8.
+
+Connection lost to DB while....
+-------------------------------
+
+In case you are creating large configs or handling huge imports with the Director
+it could happen that the default conservative max package size of your MySQL
+server bites you. Raise `max packet size` to a reasonable value, this willi
+usually fix this issue.
+
+Import succeeded but nothing happened
+-------------------------------------
+
+Import and Sync are different tasks, you need to `Run` both of them. This allows
+us to combine multiple import sources, even it if some of them are slow or failing
+from time to time. It's easy to oversee those links right now, we'll fix this soon.
+
+My Director doesn't look as good as on your screenshots
+-------------------------------------------------------
+
+There used to be a bug in older Icinga Web 2 versions that broke automagic cache
+invalidation. So when updating a module you might be forced to do SHIFT-Reload or
+similar in your browser. Please note that proxies in the way between you and
+Icinga Web 2 might currently lead to similar issues.
diff --git a/doc/82-Changelog.md b/doc/82-Changelog.md
new file mode 100644
index 0000000..5867b41
--- /dev/null
+++ b/doc/82-Changelog.md
@@ -0,0 +1,1202 @@
+<a id="Changelog"></a>Changelog
+===============================
+
+Please make sure to always read our [Upgrading](05-Upgrading.md) documentation
+before switching to a new version.
+
+v1.10.2
+-------
+
+This is a minor bugfix release, addressing some Sync-related issues: purge for
+objects with uppercase characters now works as expected, automated Sync jobs run
+again, and manually triggered Sync has been fixed on PostgreSQL.
+
+Some UI glitches have been addressed, and a few problems appearing only in
+certain conditions - related to Configuration Baskets, our Self Service REST API
+and the Activity Log.
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/31?closed=1)
+
+### UI
+* FEATURE: improve Service Set table layout (#2648)
+* FIX: modifying single time-period ranges had no effect (#2525)
+* FIX: activity log pagination is now on a single line (#2649)
+
+### Import and Sync
+* FIX: triggering Sync manually produced an error on PostgreSQL (#2636)
+* FIX: purge stopped working for objects with uppercase characters (#2627)
+* FIX: Notification Apply rule is now possible (wasn't since v1.8) (#2142, #2634)
+* FIX: nested property access with intermediate NULL values now gives NULL (#2474, #2584)
+* FIX: automated Sync jobs stopped working (#2633)
+
+### Configuration Baskets
+* FEATURE: more details shown in error messages related to invalid characters (#2646)
+* FIX: snapshots for Baskets containing Baskets failed since v1.10 (#2644)
+
+### REST API
+* FIX: Self Service API returned invalid JSON on PHP 8.1 (#2614)
+
+### Internals
+* FIX: issue with empty activity log, deprecate outdated method (#2630)
+
+v1.10.1
+-------
+
+This is a minor bugfix release, addressing issues with modifying services via
+the monitoring module, Sync problems and a copy and paste error in the DB schema,
+which caused problems for fresh installations since v1.10.
+
+Please note that a long-standing issue for our Sync Rules has been fixed: with
+"merge" policy, NULL properties have been ignored for quite some time. This has
+now been fixed. If in doubt, please **preview** your Sync Rules to make sure,
+that they behave as expected.
+
+This release brings a small schema migration, cleaning up invalid Sync history
+entries. If in doubt, please create a [database backup](05-Upgrading.md#backup-first) first.
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/30?closed=1)
+
+### Import and Sync
+* FIX: sync lower-cased all object names since v1.10 (#2608)
+* FIX: sync for Datalist entries has been fixed (#2618)
+* FIX: Sync now applied NULL values with merge policy (#2623)
+* FIX: Sync created Sync History entries for every preview (#2632)
+* FIX: "Purge" stopped working for Sync (#2627)
+
+### UI
+* FIX: "Modify" Services via the monitoring module (#2615, #2619)
+
+### Configuration Baskets
+* FIX: restore Import/Sync/Job when exported with v1.10 (#2620)
+* FIX: restoring Job with ImportSource or SyncRule (#2528)
+
+### Database Schema
+* FIX: new DB schema failed due to duplicate line in SQL statement (#2609)
+
+v1.10.0
+-------
+
+An advanced **Sync Preview** is one of the features I'd love to highlight in
+v1.10.0. We have been able to leverage our Configuration Branch support as
+an underlying technology for this:
+
+![Sync Preview - List](https://user-images.githubusercontent.com/553008/191472888-33849b3e-9d96-4113-b960-92708769e90d.png)
+
+In case you have lots of changes, you can browse all of them - formerly this
+hasn't been possible. Also, this now allows you to inspect every single upcoming
+change before applying the Sync:
+
+![Sync Preview - Details](https://user-images.githubusercontent.com/553008/191472900-1968691e-a758-4c99-99ce-059bc3689356.png)
+
+This has been possible based on the code we implemented to support the
+[Director Branches](https://icinga.com/docs/icinga-director-branches/latest/)
+module. In case you never heard about it,
+[here](https://icinga.com/blog/2022/07/21/releasing-icinga-director-branches/)
+you can find the related announcement.
+
+This release also contains a lot of related fixes and new Features. It is now
+possible to deal with **Service Sets** in Configuration Branches, the **commit
+remark** can be proposed with a merge request, and the Activity Log shows not
+only who has merged the change, but also the **original author**.
+
+Powerful new features have been implemented for those who love to orchestrate
+the Director from the outside. Please check our
+[CLI](https://github.com/Icinga/icingaweb2-module-director/blob/v1.10.0/doc/60-CLI.md)
+and [REST API](https://github.com/Icinga/icingaweb2-module-director/blob/v1.10.0/doc/70-REST-API.md)
+documentation for related details, especially look for --with-services (withServices)
+and --allow-overrides (allowOverrides).
+
+CLI now supports **JSON on STDIN**, REST API can request detailed stack traces
+in case an error occurs.
+
+### Breaking Changes
+* Module and system dependencies have been raised, [Upgrading](05-Upgrading.md)
+ and [Installation](02-Installation.md) documentations contain related details
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/27?closed=1)
+
+### User Interface
+* FIX: links from Service Previews (Icinga DSL) to templates (#2554)
+* FIX: daemon health visualization on systems w/o /proc filesystem (#2544)
+
+### Import and Sync
+* FIX: Sync now compares keys in a case-insensitive way (#2598, #2419, #1140)
+* FIX: Sync now preserves Self Service API keys in override mode (#2590)
+* FEATURE: clone a row for nested Dictionary/Hash entries (#2555)
+* FEATURE: Sync in "override" mode now preserves Self Service API keys (#2590)
+* FEATURE: split a row in multiple ones, based on a Dictionary (#2555)
+* FEATURE: it's now possible to sync to a configuration branch (#2552)
+* FEATURE: Sync preview now allows to navigate single changes (#2607)
+
+### Configuration Baskets
+* BREAKING: configuration baskets no longer contain originalId (#2549)
+* FEATURE: exporting/snapshot-logic has been centralized (#2549)
+
+### Configuration Branches
+* FIX: PostgreSQL now allows for the same object in multiple branches (#2605)
+* FEATURE: merge comments can now be proposed (#2604)
+* FEATURE: activity log now shows author and committer (#2606)
+
+### Integrations
+* FIX: Monitoring Hooks are no longer provided with disable Director UI (#2597)
+* FIX: cleanup for IcingaDbCube (#2484)
+
+### Kickstart
+* FIX: breaking change in ipl/html, affected setups with ro INI files (#2595)
+* FEATURE: better explanation for missing DSL bodies fetched from core (#2557)
+
+### REST API
+* FIX: addressing service templates by name has been fixed (#2487)
+* FIX: allow for object_name in body only (#2576)
+* FIX: notice on PHP 8.1 (#2575)
+* FEATURE: Stack traces can now be requested (#2570)
+* FEATURE: Hosts can now be exported with their services (#2568)
+* FEATURE: "magic" variable overrides are now supported (#2569)
+
+### CLI
+* FIX: config deploy doesn't try to wait in case of no deployment (#2522)
+* FIX: renderer now shows full service sets (#2550)
+* FEATURE: improved wording for deployment error messages (#2523)
+* FEATURE: JSON can now be shipped via STDIN (#1570)
+* FEATURE: improved readability for some error messages (#2567)
+* FEATURE: allows showing hosts with their services (#2565)
+* FEATURE: allow showing resolved Host services (#2571)
+* FEATURE: "magic" variable overrides are now supported (#2560)
+* FEATURE: error messages are now friendlier (#2567)
+* FEATURE: STDIN support for --json is now available (#1570)
+
+### Activity Log
+
+* FIX: deleted objects might have been missing related properties (#2559)
+
+### Deployment Log
+* FEATURE: visualization performance has been improved (#2551)
+
+### Internals
+
+* FEATURE: there is now a centralized Exporter implementation (#2549)
+
+1.9.1
+-----
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/28?closed=1)
+
+### User Interface
+* FIX: DataList-backed fields failed to validate (#2475)
+* FIX: No Host list limit when adding a single service globally (#2481)
+* FIX: Cleared activity log caused exception (#2505, #2506)
+* FEATURE: Icinga Web 2.10 dark mode support (#2433)
+
+### Configuration Baskets
+* FIX: failed to export Baskets with Service Sets (#2488)
+* FIX: Sync Rule restore from snapshot on name change (#2467)
+* FIX: Do not export UUIDs with Service Sets (#2488)
+
+### CLI
+* FEATURE: Allow to define deployment grace period on CLI (#2499)
+
+### Integrations
+* FIX: Cleanup IcingaDbCubeLinks (#2484)
+
+### DB Schema
+* FIX: applying DB Schema migrations failed on PostgreSQL (#2482)
+
+1.9.0
+-----
+
+### Breaking Changes
+* Module dependencies have been raised, [Upgrading](05-Upgrading.md) and
+ [Installation](02-Installation.md) documentations contain related details
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/25?closed=1)
+
+### Import and Sync
+* FIX: string property modifiers now preserve NULL values (#2371)
+* FIX: "to int" property modifiers now fails for non-string values (#2372)
+* FEATURE: introduce 'disable' as your purge action on Sync (#2285)
+* FEATURE: there is now a simple "group by" Property Modifier (#2317)
+
+### Configuration Baskets
+* FIX: Notification Apply Rules have not been exported (#2335)
+* FIX: Restore now supports the set_if_format switch (#2291)
+* FEATURE: it's now possible to purge objects of specific types (#2201)
+* FEATURE: exporting Users, User-Templates and -Groups is now possible (#2328)
+* FEATURE: Data Field Categories are now supported (#2256)
+
+### Permissions and Restrictions
+* FEATURE: allow using monitoring module permissions (#2304)
+* FEATURE: it's now possible to grant (global) access to scheduled downtimes (#2086)
+
+### Configuration / Templating
+* FEATURE: offering choices based on a specific imports is now possible (#1178)
+
+### User Interface
+* FIX: allow switching DB config while connection is failing (#2300)
+* FIX: Links to duplicate services in Sets didn't check for deactivation (#2323)
+* FIX: SQL error for Data Fields table on PostgreSQL (#2310)
+* FIX: SQL error when searching for Data Field Categories (#2367)
+* FIX: Icon used for Notifications has been changed (#2455)
+* FEATURE: show "deprecated" flag on object attribute inspection (#2312)
+* FEATURE: Service Template for single Host services provides auto-completion (#1974)
+
+### CLI
+* FEATURE: config deployment now allows to --wait for an Icinga restart (#2314)
+
+### Activity log
+* FEATURE: Activity log now allows for remarks (addon module required, #2471)
+
+### Documentation
+* FIX: configure the daemon with main setup instructions (#2296, #2320)
+
+### Internals
+* FEATURE: PHP 8.1 is now supported, works once available in Icinga Web (#2435)
+* FEATURE: Config Branches have been implemented, leveraged via Hook/Addon (#2376)
+* FEATURE: UUIDs have been implemented for most Icinga objects, more to come
+* FEATURE: new Deployment Hook, triggers onCollect(ing) Icinga startup info (#2315)
+
+1.8.1
+-----
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/24?closed=1)
+
+### User Interface
+* FIX: show Override button when all Fields belong to Field Categories (#2303)
+* FIX: don't fail when showing a Host overriding multiple inherited groups (#2253)
+* FIX: deal with inherited values which are invalid for a select box (#2288)
+* FIX: Service Set preview inline Service Template links (#2334)
+* FIX: show Services applied with Rules involving applied Hostgroups (#2313)
+* FIX: show "deactivated" services as such also for read-only users (#2344)
+* FIX: Overrides for Services belonging to Sets on root Host Templates (#2333)
+* FIX: show no header tabs for search result in web 2.8+ (#2141)
+* FIX: show and link dependencies for web 2.9+ (#2354)
+
+### Icinga Configuration
+* FIX: rare race condition, where generated config might miss some files (#2351)
+
+### Icinga API
+* FIX: use Icinga 2's generate-ticket API, required for v2.13.0 (#2348)
+
+### Import and Sync
+* FIX: Purge didn't remove more than 1000 services at once (#2339)
+
+### Automation, User Interface
+* FIX: error message wording on failing related (or parent) object ref (#2224)
+
+### REST API
+* FIX: creating scheduled downtime via api failed (#1879)
+
+1.8.0
+-----
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/21?closed=1)
+
+### User Interface
+* FIX: It's now possible to set Endpoint ports > 32767 on PostgreSQL (#928)
+* FIX: Group list is no longer prefixed with a comma (#2133)
+* FIX: Change wording, avoid black/whitelist (#2134, #2135)
+* FIX: Inherited values in sets (arrays) are now shown (#1310)
+* FIX: Column layout broke with Web 2.8, has been fixed (#2065)
+* FIX: filter suggestion gave wrong values for DataList fields (#1918)
+* FIX: clone-related scheduled downtime links have been fixes (#1894)
+* FEATURE: Data Fields can now be grouped into categories (#1969)
+* FEATURE: Inspect is now available for Packages, Stages and Files (#1995)
+* FEATURE: Allow to disable the Director frontend / UI (#2007)
+* FEATURE: Endpoints table now shows the object type (e.g. external) (#2050)
+* FEATURE: make sure that form label and fields stay close together (#2136)
+* FEATURE: show more content, reduce padding (expect on mobile) (#2140)
+* FEATURE: location details for non-Director services on "Modify" (#1531)
+* FEATURE: Service Set table can now also be searched for Services (#1873)
+* FEATURE: Apply-Rule-based Service Sets now show related Hosts (#2081)
+* FEATURE: Notification Apply Rules as a DirectorObject DataField (#2199)
+* FEATURE: Hint and Error styling has been unified and improved
+* FEATURE: Form field rendering for sets now deals with invalid values
+* FEATURE: Better descriptions for time-based and other fields (#1897, #1264)
+* FEATURE: Daemon tab now gets red instead of yellow when not running (#2238)
+
+### Translations
+* FEATURE: Italian translation is now available (#2080)
+* FEATURE: German translation has been refreshed (#2240)
+
+### CLI
+* FEATURE: Deployment Status and related utilities (#2189)
+
+### Import and Sync
+* FEATURE: allow defining update-only Sync Rules (#2059)
+* FEATURE: New Property Modifier: ListToObject (#2062)
+* FEATURE: Property Modifier: convert binary UUID to HEX presentation (#2138)
+* FEATURE: Property Modifier: get Host by Address (#2210)
+* FEATURE: Property Modifier: skip duplicates (#2215)
+* FEATURE: Property Modifier: trim strings (#1660)
+* FEATURE: Property Modifier: negate boolean (#2227)
+* FEATURE: Property Modifier Reject/Select: improve usability (#2228)
+* FEATURE: Property Modifier: clone rows for every entry of an Array (#2192)
+* FEATURE: Property Modifier: unique array values (#2229)
+* FEATURE: Property Modifier: allow to rename columns (#2242)
+* FEATURE: Import Sources now allows downloading previewed data as JSON (#2096)
+* FEATURE: REST API Import now allows custom headers (#2132)
+* FEATURE: REST API Import can now extract nested properties (#2132)
+* FEATURE: REST API Form remembers passwords without exposing them (#2070)
+* FEATURE: UTF8 validation for failed imports gives better error message (#2143)
+* FEATURE: ArrayByElementPosition now allows filtering by key name (#1721)
+* FEATURE: Use your Director Objects as an Import Source (#2198)
+* FEATURE: Property modifiers are now granted access the current Property Name (#2241)
+* FIX: Import Source preview now catches all errors
+* FIX: Import Source download sends eventual errors as a valid JSON result
+* FIX: LDAP Import is now able to paginate limited results (#2019)
+
+### Configuration Baskets
+* FIX: Restoring Import Sources creating Modifiers now works (#2053)
+* FEATURE: Support Baskets from Icinca for Windows (#2223)
+* FEATURE: It's now possible to use Notification Templates in Baskets
+* FEATURE: Snapshot status/diff layout has been improved (#2225)
+
+### Authentication and Permissions
+* FIX: Users restricted to Hostgroups can now use related Templates (#2020, #2101)
+* FEATURE: Optionally, restricted users can be allowed to set Groups (#2252)
+
+### Kickstart
+* FEATURE: Friendlier message if object to be removed is still in use (#2206)
+* FEATURE: Kickstart now removes obsolete External Commands (#985)
+
+### Icinga Configuration
+* FIX: Correctly render Service Dependencies with Array-style parent hosts (#2088)
+* FIX: times.begin and times.end are now rendered separately (#2193)
+* REMOVED: magic-apply-for (a hidden deprecated feature) has been removed (#1851)
+
+### Icinga Agent handling
+* FIX: Linux Agent installer now fails when unable to retrieve a certificate
+* FEATURE: Linux Agent installer now supports Alpine Linux (#2216)
+* FEATURE: Icinga for Windows support (#2147)
+
+### REST API
+* FEATURE: Self Service API ignores empty/missing properties (e.g. no address)
+* FEATURE: Search is now also available for the REST API (#1889)
+* FEATURE: Deployment Status is now available (#2187)
+* FEATURE: UTF-8 characters and slashes are no longer escaped (#2243)
+
+### Self Service API
+* FIX: error handling has been fixed (#1728)
+
+### Database Support
+* FIX: Added UTF8 to valid PostgreSQL encodings (used to be UTF-8)
+
+### Background Daemon
+* FIX: Daemon Logger used to not override the given log level (#2139)
+* FEATURE: Daemon: prepare for future reactphp promise versions (#2137)
+* FEATURE: Daemon now logs that it is going to reload itself
+* FEATURE: Now collects the Deployment status from Icinga (#2045, #1988)
+
+### Documentation
+* FEATURE: We now also mention optional/indirect requirements (#2054, #2220)
+
+### Internals
+* FEATURE: Property Modifiers are now able to clone rows (#2060)
+* FEATURE: URL encoding for the Core API has been unified
+* FEATURE: PHP 8.0 has been released and is officially supported (#2233)
+* REMOVED: dipl has been dropped, we're using ipl/incubator since v1.7 (#2209)
+* FIX: typo in DeploymentHook::onSuccessfulDump() has been fixed (#2069)
+* FIX: forms now support dbResourceName (#2064)
+
+1.7.2
+-----
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/23?closed=1)
+
+### DB Schema
+* FIX: Rolling out new installations on MySQL v5.6 fails (#1993)
+
+### Icinga Configuration
+* FIX: Render service\_name for Notifications (#2006)
+
+### User Interface
+* FIX: Cloning Import Sources failed since v1.7.0 (#1997)
+* FIX: Switching Director DBs was broken since Web 2.6.3 (#2063)
+
+### CLI
+* FIX: Importing Import Sources failed since v1.7.0 (#2005)
+
+### Automation
+* FIX: Fixing linux install script version check (#2008)
+* FIX: Windows Kickstart Script - $GlobalZones was empty (#2002)
+
+### Documentation
+* FIX: Missing single quote in mysql example bug (#2003)
+
+1.7.1
+-----
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/22?closed=1)
+
+### User Interface
+* FIX: Cloning Sync rules failed since v1.7.0 (#1982)
+* FIX: It wasn't possible to multi-select Hosts belonging to a Group (#1973)
+* FIX: Removed an un-formatted error in case Icinga is unreachable (#1966)
+* FIX: Check for broken configs has been extended to Icinga v2.11.* (#1985)
+* FEATURE: Show a warning when detecting a downgraded installation (#1992)
+
+### Import and Sync
+* FIX: Upper- and Lowercase property modifiers are now multibyte/UTF8-safe (#710)
+
+### Health Check
+* FIX: do not complain about no-due newly created jobs (#1994)
+
+### Background Daemon
+* FIX: Daemon didn't report DB state to systemd (#1983)
+
+1.7.0
+-----
+### Breaking Changes
+* At least PHP 5.6.3 is now required, Director 1.7.x will refuse to work with
+ older versions
+* New dependencies have been introduced, [Upgrading](05-Upgrading.md) and
+ [Installation](02-Installation.md) documentations contain related details
+
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/18?closed=1)
+
+### User Interface
+* FIX: Service-related links in Activity Log have been corrected (#1377, #1816)
+* FIX: Activity Log now works for Service Sets (#1287, #1786, #1816)
+* FIX: Assign Filters are no longer mandatory when modifying Service Groups (#930)
+* FIX: Object type for CheckCommands has been corrected in config preview (#1799)
+* FIX: Import preview in combination with Black/Whitelisting (#1825)
+* FIX: Routing/redirection when working with Data Fields (#1847)
+* FIX: Auto-suggestion field was positioned wrongly once scrolled down
+* FIX: Timezone inconsistencie have been fixed (#1700)
+* FIX: Link-like buttons where shortened on Icinga Web 2.7 (#1928)
+* FIX: Search in range-filtered Activity Log no longer fails (#1381)
+* FEATURE: It's now possible to clone a Service to a different Host (#1796)
+* FEATURE: Scheduled Downtimes for "Hosts AND their services" (#1831)
+* FEATURE: Auto-suggestion and more for Fields based on Data Lists (#1846)
+* FEATURE: Show missing dependencies (#1938)
+
+### Translations
+* FEATURE: German translation has been refreshed (#1951)
+* FEATURE: Japanese is now available (#1869)
+
+### Import and Sync
+* FIX: Avoid caching between multiple runs of sync (#1836)
+* FIX: Imported Rows Table (history) eventually failed on Icinga Web 2 (#1925)
+* FIX: Improved error handling on preview (#1941)
+* FEATURE: When fetching invalid data, Import refers erroneous rows (#1741)
+* FEATURE: Sync now offers a preview, showing what would happen (#1754)
+* FEATURE: ParseURL property modifier has been added (#1746)
+* FEATURE: There is a new generic REST API Import Source (#1818)
+* FEATURE: Sync now supports Notifications and Dependencies (#1212, #925, #1209)
+* FEATURE: Limits (memory, execution time) raised for Import runs via UI (#1954)
+
+### Configuration Baskets
+* FIX: snapshots do no longer fail for deleted elements on snapshot (#1940)
+* FEATURE: baskets now support External Commands (#1854)
+
+### REST API
+* FIX: Command Arguments can now be managed via API (#1416)
+
+### CLI
+* FIX: importsource fetch did not apply configured property modifiers (#1819)
+* FEATURE: Service Groups are now available on CLI (#1745)
+* FEATURE: A new background daemon has been introduced (#1905)
+
+### Icinga Configuration
+* FIX: Allow to render single configuration files larger than 16MB (#1787)
+* FIX: Icinga v2.11 version detection for Agent Installation script (#1957)
+* DEPRECATED: magic-apply-for (a hidden feature) is now deprecated (#1850)
+* FEATURE: It's now possible to define Scheduled Downtimes (#347, #1828)
+* FEATURE: Allow to render command definitions as (v1.x-like) strings (#1809)
+* FEATURE: host address now allows 255 characters (#1890)
+* FEATURE: Director now assists with Services applied to parent Zones (#1634)
+* FEATURE: Warn affected setups when affected by a specific core issue (#1958)
+
+### Documentation
+* FIX: Installation instructions have been adjusted to fit MySQL 8
+
+### Internals
+* FIX: support different timezones with MySQL (#1332, #1840)
+* FIX: support importing DSL-based Command Arguments (#1812)
+* FEATURE: a new Hook allows to run custom code at deployment time (#1342, #1843)
+* FEATURE: there is a new low-level IcingaObjectFormHook (#1841)
+
+1.6.2
+-----
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/20?closed=1)
+
+### Icinga Configuration
+* FIX: rendering for Service Sets on single Hosts has been fixed (#1788, #1789)
+
+1.6.1
+-----
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/19?closed=1)
+
+### User Interface
+* FIX: restoring a basket fails when there is only one configured DB (#1716)
+* FIX: creating a new Basket with a "Custom Selection" failed with an error (#1733)
+* FIX: some new reserved keywords are now escaped correctly (#1765)
+* FIX: correctly render NOT used in apply rules (fixes #1777)
+* FIX: Activity Log used to ignore Host filters (#1613)
+* FIX: Basket failed to restore depending on PHP version (#1782)
+* FIX: Loop detection works again (#1631)
+* FIX: Snapshots for Baskets with Dependencies are now possible (#1739)
+* FIX: Commands snapshots now carry fields in your Basket (#1747)
+* FIX: Cloning services from one Set to another one no longer fails (#1758)
+* FIX: Blacklisting a Service from a Set on a Host Template is now possible (#1707)
+* FIX: Services from a Set assigned to a single Host can be blacklisted (#1616)
+* FEATURE: Add TimePeriod support to Configuration Baskets (#1735)
+* FEATURE: RO users could want to see where a configured service originated (#1785)
+* FEATURE: introduce director/serviceapplyrules REST API endpoint (#1755)
+
+### REST API
+* FIX: Self Service API now ships optional Service User parameter (#1297)
+
+### DB Schema
+* FIX: it wasn't possible to use the same custom var in multiple notification
+ definitions on PostgreSQL (#1762)
+
+### Icinga Configuration
+* FIX: escape newly introduced Icinga 2 keywords (#1765)
+
+1.6.0
+-----
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/15?closed=1)
+
+### User Interface
+* FIX: link startup log warning even for non-standard package names (#1633)
+* FIX: searching for fields assigned to a template was broken (#1670)
+* FIX: changing an argument type from String to DSL didn't work (#1640)
+* FIX: incorrect links from template-tree to non-template commands (#1544)
+* FIX: drop useless object-type field for Time Periods (#788)
+* FIX: clean up naming for some tabs (#1312)
+* FIX: "remove" now removes the correct Service Set on a Host (#1619)
+* FIX: do not fail when "inspecting" a pending service (#1641)
+* FIX: a problem when selecting multiple host has been fixed (#1647)
+* FIX: allow to remove renamed Service Sets (#1664)
+* FIX: resolved a side-effect triggered by hooked Custom Fields (#1667)
+* FIX: config diff URL behavior has been corrected (#1704)
+* FEATURE: allow to filter templates by usage (#1339)
+* FEATURE: allow to show SQL used for template tables
+* FEATURE: allow to defined Service Groups for Set members and for Services
+ assigned to Host Templates (#619)
+* FEATURE: it's now possible to choose another target Service Set when cloning
+ a member service (#886)
+* FEATURE: Configuration Baskets with snapshot/import/export capabilities (#1630)
+* FEATURE: Allow to clone a Service from one Set to another one (#886)
+* FEATURE: form descriptions are now shown directly below the field, reverting
+ a change from v1.4.0 (#1510)
+* FEATURE: show sub-sets in Config Preview (#1623)
+* FEATURE: show live Health-Check in the frontend (#1669)
+
+### Import and Sync
+* FIX: Core Api imports flapping only for 2.8+ (#1652)
+* FEATURE: new Property Modifier allows to extract specific Array values (#473)
+
+### CLI
+* FIX: Director Health Check no longer warns about no Imports/Syncs/Jobs (#1607)
+* FEATURE: It's now possible to dump data as fetched by an Import Source (#1626)
+* FEATURE: CLI implementation for Configuration Basket features (#1630)
+* FEATURE: allow to append to or remove from array properties (#1666)
+
+### Icinga Configuration
+* FIX: rendering of disabled objects containing `*/` has been fixed (#1263)
+* FEATURE: support for Timeperiod include/exclude (#1639)
+* FEATURE: improve legacy v1.x configuration rendering (#1624)
+
+### Icinga API
+* FIX: ship workarounds for issues with specific Icinga 2 versions
+* FIX: clean up deployed incomplete stages lost by Icinga (#1696)
+* FEATURE: allow to behave differently based on Icinga 2 version (#1695)
+
+### Icinga Agent handling
+* FEATURE: ship latest PowerShell module (#1632)
+* FIX: PowerShell module runs in FIPS enforced mode (#1274)
+
+### DB Schema
+* FIX: enforce strict object_name uniqueness on commands (#1496)
+
+### Documentation
+* FEATURE: improve installation docs, fix URLs (#1656, #1655)
+
+
+1.5.2
+-----
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/17?closed=1)
+
+### Configuration rendering
+* FIX: Fix compatibility with Icinga v2.6, got broken with v1.5.0 (#1614)
+
+### REST API
+* FIX: No more invalid JSON in some special circumstances (#1314)
+
+### User Interface
+* FIX: Hostgroup assignment cache has been fixed (#1574, #1618)
+
+### DB Schema
+* FIX: missing user/timeperiod constraint. We usually do not touch the schema
+ in minor versions, this has been cherry-picked by accident. However, don't
+ worry - the migration has been tested intensively.
+
+1.5.1
+-----
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/16?closed=1)
+
+### Icinga Configuration
+* FIX: Switched Variable-Override related constant names broke the feature (#1601)
+
+### User Interface
+* FIX: Custom Fields attached to a Service Template have not been shown for Apply
+ Rules whose name matched the Template Name (#1602)
+
+### Import and Sync
+* FIX: There was an issue with specific binary checksums on MySQL (#1556)
+
+1.5.0
+-----
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/11?closed=1)
+
+### Security Fixes
+* FIX: users with `director/audit` permission had the possibility to inject SQL.
+ Thanks to Boyd Ansems for reporting this.
+
+### Permissions and Restrictions
+* FEATURE: Showing the executed SQL query now requires the `showsql` permission
+* FEATURE: Grant access to Service Set in a controlled way
+* FIX: do not allow a user to create hosts he wouldn't be allowed to see #1451
+* FIX: Hostgroup-based restrictions worked fine when applied, bug was buggy in
+ combination with directly assigned or inherited groups (#1464)
+
+### Icinga Configuration
+* FEATURE: Add 'is false (or not set)' condition for apply rules (#1436)
+* FEATURE: support flapping settings for Icinga &gt;= 2.8.0 (#330)
+* FEATURE: include all itl packages in Linux Agent sample config (#1450)
+* FEATURE: it's now possible to blacklist inherited or applied Services on
+ single hosts (#907)
+* FEATURE: timestamped startup log rendering for upcoming Icinga v2.9.0 (#1478)
+* FEATURE: allow to switch between multiple Director databases (#1498)
+* FEATURE: it's now possible to specify Zones for UserGroups (#1163)
+* FEATURE: dependencies are no longer considered experimental
+
+### User Interface
+* FEATURE: Admins have now access to JSON download links in many places
+* FEATURE: Users equipped with related permissions can toggle "Show SQL" in the GUI
+* FEATURE: A Service Set can now be assigned to multiple hosts at once #1281
+* FEATURE: Commands can now be filtered by usage (#1480)
+* FEATURE: Show usage of Commands over templates and objects (#335)
+* FEATURE: Allow horizontal size increase of Import Source DB Query field (#299)
+* FEATURE: Small UI improvements like #1308
+* FEATURE: Data Lists can be chosen by name in Sync rules (#1048)
+* FEATURE: Inspect feature got refactored, also for Services (#264, #689, #1396, #1397)
+* FEATURE: The "Modify" hook is now available for Services (#689), regardless
+ of whether they have been directly assigned, inherited or applied
+* FEATURE: Config preview links imports, hosts and commands to related objects (#1521)
+* FEATURE: German translation has been refreshed (#1599)
+* FEATURE: Apply Rule editor shows suggestions for Data-List vars (#1588)
+* FIX: Don't suggest Command templates where Commands are required (#1414)
+* FIX: Do not allow to delete Commands being used by other objects (#1443)
+* FIX: Show 'Inspect' tab only for Endpoints with an ApiUser (#1293)
+* FIX: It's now possible to specify TimePeriods for single Users #944
+* FIX: Redirect after not modifying a Command Argument failed on some RHEL 7
+ setups (#1512)
+* FIX: click on Service Set titles no longer removes them from their host (#1560)
+* FIX: Restoring objects based on compound keys has been fixed (#1597)
+* FIX: Linux Agent kickstart script improved and tweaked for Icinga 2.9 (#1596)
+
+### CLI
+* FEATURE: Director Health Check Plugin (#1278)
+* FEATURE: Show and trigger Import Sources (#1474)
+* FEATURE: Show and trigger Sync Rules ( #1476)
+
+### Import and Sync
+* FIX: Sync is very powerful and allows for actions not available in the GUI. It
+ however allowed to store invalid single Service Objects with no Host. This is
+ now illegal, as it never makes any sense
+* FIX: Performance boost for "purge" on older MySQL/MariaDB systems (#1475)
+* FEATURE: new Property Modifier for IPs formatted as number in Excel files (#1296)
+* FEATURE: new Property Modifier to url-encode values
+* FEATURE: new Property Modifier: uppercase the first character of each word
+* FEATURE: Kickstart Helper now also imports Event Commands (#1389)
+* FEATURE: Preserve _override_servicevars on sync, even when replacing vars (#1307)
+
+### Internals
+* FIX: problems related to users working from different time zones have been
+ fixed (#1270, #1332)
+* FEATURE: Html/Attribute now allows boolean properties
+* FEATURE: Html/Attribute allows colons in attribute names (required for SVGs)
+* FEATURE: Html/Attributes can be prefixed (helps with data-*)
+* FEATURE: Html/Img data:-urls are now supported
+* FEATURE: ipl has been aligned with the upcoming ipl-html library
+* FEATURE: Director now supports multiple Databases, allows to switch between
+ them and to deploy different Config Packages. Other features based on this
+ combined with related documentation will follow.
+
+1.4.3
+-----
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/13?closed=1)
+
+### User Interface
+* FIX: Pagination used to be broken for some tables (#1273)
+
+### Automation
+* FIX: API calls changing only object relations and no "real" property resulted
+ in no change at all (#1315)
+
+1.4.2
+-----
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/13?closed=1)
+
+### Configuration rendering
+* FIX: Caching had an influence on context-specific Custom Variable rendering
+ when those variables contained macros (#1257)
+
+### Sync
+* FIX: The fix for #1223 caused a regression and broke Sync for objects without
+ a 'disabled' property (Sets, List members) (#1279)
+
+1.4.1
+-----
+### Fixed issues
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/12?closed=1)
+
+### Automation
+* FIX: A Sync Rule with `merge` policy used to re-enable manually disabled objects,
+ even when no Sync Property `disabled` has been defined (#1223)
+* FIX: Fix SQL error on PostgreSQL when inspecting Template-Choice (#1242)
+
+### Large environments
+* FIX: Director tries to raise it's memory limit for certain memory-intensive
+ tasks. When granted more (but not infinite) memory however this had the effect
+ that he self-restricted himself to a lower limit (#1222)
+
+### User Interface
+* FIX: Assignment filters suggested only Host properties, you have been required
+ to manually type Service property names (#1207)
+* FIX: Hostgroups Dashlet has been shown to users with restricted permissions,
+ clicking it used to throw an error (#1237)
+
+1.4.0
+-----
+### New requirements
+* Icinga Director now requires PHP 5.4, support for 5.3 has been dropped
+* For best performance we strongly suggest PHP 7
+* When using MySQL, please consider slowly moving to at least version 5.5. One
+ of our next versions will introduce official Emoji support 😱😱😱! That's not
+ possible with older MySQL versions. However, 1.4.x still supports 5.1.x
+
+### Fixed issues and related features
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/6?closed=1)
+
+### Dashboard and Dashlets
+* Multiple new Dashboards have been introduced, their layout has been optimized
+* Dashboards are made aware of newly introduced permissions and try to provide
+ useful hints
+
+### GUI, UX and Responsiveness
+* Many little improvements related to mobile devices have been applied to
+ Dashboards, Forms and Tables
+* Search has been both improved and simplified. On most tables search spawns
+ multiple columns, visible and invisible ones. Multiple search terms are
+ combined in an intuitive way.
+* Pagination (and search) has been added to those tables where it was still
+ missing
+* Some form fields referencing related objects are no longer static drop-down
+ selection elements but offer suggestions as you type. This makes forms faster,
+ especially in larger environments
+* Navigation has been simplified, redirects after form submissions have been
+ improved, more possibilities to jump to related objects have been added
+* Form field description has been moved to the bottom of the screen. Might be
+ easier to overlook this way, but while the former implementation was great
+ for people navigating forms with their Keyboard, it was annoying for Mouse
+ lovers
+* Double-Click a Tab to enlarge it to full width
+* Action Link bar has been unified, all links should now respect permissions
+* All tables showing historic data are now grouped by day
+* Property Modifiers, Sync Rules, Import Sources and more objects now offer
+ description fields. This allows you to explain your colleagues all the magic
+ going on behind the scenes
+
+### Object Types
+* Service Sets got quite some tweaking and bug fixing
+* Groups of all kinds are now able to list their members, even when being
+ applied based on filters
+* Command Argument handling has been improved
+* It is now possible to configure Dependencies through the Icinga Director
+* Cloning Hosts now allows to also optionally clone their Services and Service
+ Sets
+
+### Templates
+* The template resolver has been rewritten, is now easier to test, strict and
+ faster
+* Template Tree has been re-written and now also immediately shows whether a
+ template is in use
+* When navigating to a Template you'll notice a new usage summary page showing
+ you where and how that specific template is being used. Therefor, many tables
+ are now internally able to filter by inheritance
+
+### Template Choices
+* While Host- and Service-Templates are powerful building blocks, having to choose
+ from a single long list might become unintuitive as this list starts growing.
+ That's where Template Choices jump in. They allow you to bundle related Templates
+ together and offer your users to choose amongst them in a meaningful way.
+
+### Apply rules
+* Various related issues have been addressed
+* A new virtual "is true / is set" operator is now available
+
+### Permissions and Restrictions
+* It is now possible to limit access to Hosts belonging to a a list of Hostgroups.
+ This works also for Hostgroups assigned through Apply Rules.
+* Data List entries can be made available based on Roles
+
+### Data Types
+* SQL Query and Data List based Data Fields can now both be offered as Array fields,
+ so that you can choose among specific options when filling such
+* New overview tables give admins a deep look into used Custom Variables, their
+ distinct values and usage
+* Various issues related to Boolean values have been fixed
+
+### Import and Synchronization
+* Many issues have been addressed. Merge behavior, handling of special fields and
+ data types
+* Problems with Import Source deletion on PostgreSQL have been addressed
+* New Property Modifiers are available. When importing single Services you might
+ love the "Combine" modifier
+* It is now possible to re-arrange execution order of Property Modifiers and
+ Sync Properties
+* Preview rendering got some improvements
+* "Replace" policy on Custom Vars is now always respected
+* Using VMware/vSphere/ESX? There is now a new powerful module providing a
+ dedicated Import Source
+
+### REST API
+* A new Self Service API now allows to completely automate your Icinga Agent
+ roll-out, especially (but not only) for Microsoft Windows
+* List views are now officially available. They are very fast and stream the
+ result in a memory-efficient way
+* Documentation better explains how to deal with various objects, especially
+ with different types of Services (!!!!!)
+
+### Internal architecture
+* Many base components have been completely replaced and re-written, based on
+ and early prototype of our upcoming Icinga PHP Library (ipl)
+
+1.3.2
+-----
+
+### Fixed issues and related features
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/10?closed=1)
+
+### Apply Rules
+* Slashes in Apply Rules have not been correctly escaped
+* Services applied based on Arrays (contains) did not show up in the Hosts
+ Services list, and therefor it was not possible to override their vars
+* Some magic has been introduced to detect numbers in apply rules - not perfect
+ yet
+
+### Host Groups
+* It has not been possible to modify Host Groups without defining an apply rule
+* Hostgroups have not been sorted
+* It is now legal to have `external` HostGroup objects
+
+### Rendered Config
+* Custom Endpoint objects are now rendered to their parent zone
+* (Rendering) issues with the `in` operator have been fixed
+* You are now allowed to put Notifications into specific Zones
+
+### Usability and UI
+* Selecting multiple hosts at once and deleting them had no effect
+* Documentation got some little improvements
+* German translation has been refreshed
+* Header alignment has been improved
+* Escaping issues with the Inspect feature have been addressed
+
+### Kickstart
+
+* Kickstart is more robust and now able to deal with renamed Icinga Masters and
+ more
+
+### CLI
+* It is not possible to list and show Service Sets on the CLI
+
+### Import and Sync
+* Synchronizing Data List entries caused problems
+* A new Import Modifier has been added to deal with LConf specialities
+* Issues with special characters like spaces used in column names shipped by
+ Import Sources have been addressed
+* A new Property Modifier allows to filter Arrays based on wildcards or regular
+ expressions
+* A new Property Modifier allowing to "Combine multiple properties" has been
+ introduced. It's main purpose is to provide reliable unique keys when importing
+ single service objects.
+* A new warning hint informs you in case you created a Sync Rule without related
+ properties
+* Synchronization filters failed when built with columns not used in any property
+ mapping
+
+### Auditing
+* The audit log now also carries IP address and username
+
+### Generic bug fixes
+* Fixed erraneous loop detection under certain (rare) conditions
+* Various issues with PHP 5.3 have been fixed
+* Combination of multiple table filters might have failed (in very rare conditions)
+
+1.3.1
+-----
+
+### Fixed issues and related features
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/8?closed=1)
+
+### Service Sets
+* Various little issues have been fixed. You can now remove Sets from hosts,
+ even when being empty. Services from Sets assigned to parents or via apply
+ rule are now shown for every single host, and their custom vars can be
+ overridden at a single host level
+* Sets assigned to single hosts have been shown, variable overrides have been
+ offered - but rendering did not include the Director-generated template
+ necessary to really put them into place. This has been fixed
+
+### Usability
+* A nasty bug hindered fields inherited from Commands from being shown ad a
+ Service level - works fine right now
+* There is now a pagination for Zones
+* Multiedit no longer showed custom fields, now it works again as it should
+
+### Rendering
+* Disabling a host now also disables rendering of related objects (Endpoint,
+ Zone) for hosts using the Icinga Agent
+
+### REST API
+* Ticket creation through the REST API has been broken, is now fixed
+
+### Performance, Internals
+* A data encoding inconsistency slowed down apply rule editing where a lot of
+ host custom vars exists
+* Some internal changes have been made to make parts of the code easier to be
+ used by other modules
+
+1.3.0
+-----
+
+### Fixed issues and related features
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/7?closed=1)
+
+### Service Sets
+* You are now allowed to create sets of services and assign all of them at
+ once with an apply rule
+* Sets can be assigned to host templates or directly to single hosts
+
+### Service Variable Overrides
+* When switching to a host view's services tab, you'll now not only see its
+ very own services, but also ones that result from an apply rule
+* You can override those services custom field values for every single host
+* Same goes for services belonging to Service Sets
+
+### Apply rules
+* A new "contains" operator gives more possibilities when working with arrays
+* Service vars are now also offered in the apply rule form wizard
+
+### Custom Variables and Fields
+* Issues with special characters in custom variables have been fixed
+* In case mandatory fields should not have been enforced, this should work
+ fine right now
+* Fields can now be shown based on filter rules. Example use case: show a
+ `Community String` field in case `SNMPv2` has been selected, but show
+ five other fields for `SNMPv3`. This allows one to build powerful little
+ wizard-like forms like shown [here](16-Fields-example-SNMP.md)
+
+### Agents and Satellites
+* It is now possible to set Agent and Zone settings on every single host. This
+ means that you no longer need to provide dedicated Templates for Satellite
+ nodes
+* The proposed Agent Deployment script has been improved for Windows and Linux
+* Infrastructure management got a dedicated dashboard
+* Kickstart Wizard helps when working with Satellites. This has formerly been
+ a hidden, now it can be accessed through the Infrastructure dashboard
+
+### Commands
+* Command arguments are now always appended when inheriting a template. This
+ slightly changes the former behavior, but should mostly be what one would
+ expect anyways.
+
+### Testing
+* [Testing instructions](93-Testing.md) have been improved
+* Running the test suite has been simplified
+* While we keep running our own [tests](93-Testing.md) on software platforms, tests
+ are now also visible on Travis-CI and triggered for all pull requests
+
+### Compatibility
+* We worked around a bug in very old PHP 5.3 versions on CentOS 6
+
+### Activity log
+* You can now search and filter in the Activity log
+* In case you have hundreds of thousands of changes you'll notice that pagination
+ performance improve a lot
+* A quick-filter allows you to see just your very own changes with a single click
+
+### Deployment
+* More performance tweaking took place. 1.2.0 was already very fast, 1.3.0 should
+ beat it
+* Deployment log got better at detecting files and linking them directly from the
+ log output, in case any error occured
+
+### Work related to Icinga 1.x
+* Deploying to Icinga 1.x is completely unsupported. However, it works and a
+ lot of effort has been put into this feature, so it should be mentioned here
+* Please note that the Icinga Director has not been designed to deploy legacy
+ 1.x configuration. This is a sponsored feature for a larger migration project
+ and has therefore been built in a very opinionated way. You shouldn't even
+ try to use it. And if so, you're on your own. Nobody will help you when
+ running into trouble
+
+### Translation
+* German translation is now again at 100%
+
+### REST API
+* Issues related to fetching object lists have been fixed
+
+### Integrations
+* We now hook into the [Cube](https://github.com/icinga/icingaweb2-module-cube)
+ module, this gives one more possibility to benefit from our multi-edit feature
+* Icinga Web 2.4 caused some minor issues for 1.2.0. It works, but an upgrade to
+ Director 1.3.0 is strongly suggested
+
+1.2.0
+-----
+
+### Fixed a lot of issues and related features
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/5?closed=1)
+
+### Permissions and restrictions
+* Permissions are now enforced. Please check your role definitions, permission
+ names have changed and are now enforced everywhere
+* Configuration preview, Inspect action, Deployment and others can be granted
+ independently
+
+### Auditing
+* Director provides a nice activity log. Now it is also possible to additionally
+ log to Syslog or File in case you want to archive all actions elsewhere. Access
+ to the audit log in the Director can be controlled with a new permission
+
+### Configuration kickstart
+* Now imports also existing notification commands
+* Kickstart can be re-triggered on demand at any time
+
+### Performance
+* Config rendering got a huge performance boost. In large environments we
+ managed it to deploy a real-world configuration 5 times as fast as before
+
+### Import / Sync
+* Various improvements have been applied, mostly hidden small features that should
+ make work easier. Better form field descriptions, more possibilities when it
+ goes to syncing special fields like "imports"
+* Property modifiers can now generate new modified columns at import time
+* New property modifiers are available. There is a pretty flexible DNS lookup, you
+ can cast to Integer or Boolean, JSON decoding and more is offered
+* Datalist entries can now be imported and synchronized, this was broken in 1.1
+
+### Configuration possibilities
+* You can now define assign rules nested as deep as you want, based on all host
+ and/or service properties
+* It is now possible to define "assign for" constructs, looping over hashes or
+ dictionaries
+* Improved Icinga 2 DSL support in commands, implicit support for skip\_key
+* More and more developers are contributing code. We therefore simplified the
+ way to launch our unit tests and provided related documentation
+* Other objects can be referred as a dropdown or similar in custom variables
+
+### GUI and usability
+* Form error handling got a lot of tweaking, eventual exceptions are caught in
+ various places and presented in a readable way
+* The deployment button is now easier to find
+* Configuration preview has been improved and allows a full config diff even
+ before deploying the configuration
+* Inheritance loops are now shown in a nice way and can be resolved in the GUI
+* A new hidden gem is the multiedit functionality. Press SHIFT/CTRL while
+ selecting multiple hosts and modify imports, custom vars and other properties
+ for all of them at once
+* Errors or warnings in all historic startup logs now link directly to the
+ related config file at the time being, pointing to the referred line
+
+### Agent setup
+* The Windows kickstart script got some small improvements and now enables all
+ related ITL commands per default
+
+### CLI
+* You can find a few new commands, with the ability to list or fetch all hosts
+ at once in various ways being the most prominent one
+
+### Related modules
+* There are now more additional modules implementing Director Hooks. AWS import
+ for EC2 instances, ELBs and Autoscaling Groups. File import for CSV, JSON,
+ YAML and XML. We heard from various successful Import source implementations
+ in custom projects and would love to see more of those being publicly available!
+
+1.1.0
+-----
+
+### Fixed a lot of issues and related features
+* You can find issues and feature requests related to this release on our
+ [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/4?closed=1)
+
+### Icinga Agent handling
+* A lot of effort has been put into making config deployment easier for
+ environments with lots of Icinga Agents
+* Related bugs have been fixed, the generated configuration should now work fine
+ in distributed environments
+* A customized Powershell Script for automatic Windows Agent setup is provided
+
+### Apply Rules
+* It's now possible to work with apply rules in various places
+
+### Notifications
+* All components required to deploy notifications are now available. ENV for
+ commands is still missing, however it's pretty easy to work around this
+
+### Automation
+* Job Scheduler and Job Runner have been introduced. Import, Sync, Deploy and
+ run Housekeeping in the background with full control and feedback in the GUI
+* There is a new intelligent `purge` option allowing one to purge only those
+ objects that vanished at involved Import Sources between multiple Import and
+ Sync Runs.
+
+### Data Types
+* Booleans, Integers and Arrays are now first-class citizens when dealing with
+ custom variables
diff --git a/doc/91-Want-more.md b/doc/91-Want-more.md
new file mode 100644
index 0000000..cc22588
--- /dev/null
+++ b/doc/91-Want-more.md
@@ -0,0 +1,17 @@
+<a id="Want-more"></a>Want more?
+================================
+
+Icinga 2 configuration is a full-blown DSL, not just a configuration
+format. It provides endless possibilities, the very same thing can
+usually be accomplished in various ways. Director tries hard to offer
+you as many of those as possible while strictly trying to keep things
+simple.
+
+You are absolutely right if you think that this might not be an easy
+task. We do our best to give you as much flexibility as possible. In
+case you miss a feature or have a cool idea of what else we could add
+please let us know. Just file an issue or a feature request to our
+[issue tracker](https://github.com/Icinga/icingaweb2-module-director/issues).
+
+The Icinga project is and remains Open Source Software and lives from
+community ideas and contributions.
diff --git a/doc/93-Testing.md b/doc/93-Testing.md
new file mode 100644
index 0000000..7d2f8fb
--- /dev/null
+++ b/doc/93-Testing.md
@@ -0,0 +1,304 @@
+<a id="Testing"></a>Running Unit-Tests for the Director
+=======================================================
+
+There are basically multiple ways of running our Unit-Tests. All of them
+are explained here.
+
+Let others do the job
+---------------------
+
+Well, as there are tests available you might come to the conclusion that
+there is probably already someone running them from time to time. So, just
+lean back with full trust in our development toolchain and spend your time
+elsewhere ;-) Cheers!
+
+### Tests on Travis-CI
+
+When pushing to [GitHub](https://github.com/Icinga/icingaweb2-module-director/)
+or sending pull requests, Unit-Tests are automatically triggered on
+[Travis-CI](https://travis-ci.org/Icinga/icingaweb2-module-director):
+
+[![Build Status](https://travis-ci.org/Icinga/icingaweb2-module-director.svg?branch=master)](https://travis-ci.org/Icinga/icingaweb2-module-director)
+
+We run our tests against MySQL and PostgreSQL, with PHP versions ranging from
+5.3 to 7.1, including nightly builds.
+
+### Tests for supported Platforms
+
+As far as we know, Director is currently mostly used on CentOS (or RHEL)
+versions 6 and 7, Debian Stable (Jessie) and Ubuntu LTS (Xenial). So we are
+running our tests on our own platforms for exactly those systems. All of them
+with PostgreSQL, MySQL (or MariaDB).
+
+This way we reach the mostly used Database and PHP versions:
+
+![Test result](screenshot/director/93_testing/931_director_testing_duration.png)
+
+
+Run tests on demand
+-------------------
+
+The easiest variant is to run the tests directly on the system where you
+have installed your Director.
+
+### Requirements
+
+* Icinga Web 2 configured
+* Director module installed
+* A dedicated DB resource
+* PHPUnit installed
+
+### Configuration
+
+You can use your existing database resource or create a dedicated one. This
+might be either MySQL or PostgreSQL, you just need to tell the Director the
+name of your resource:
+
+```ini
+; /etc/icingaweb2/modules/director/config.ini
+
+[db]
+resource = "Director DB"
+
+[testing]
+db_resource = "Director Test DB"
+```
+
+### Run your tests
+
+Just move to your Director module path...
+
+ cd /usr/share/icingaweb2/modules/director
+
+...tell Director where to find your configuration...
+
+ export ICINGAWEB_CONFIGDIR=/etc/icingaweb2
+
+...and finally run the tests:
+
+ phpunit
+
+Try parameters like `--testdox` or `--verbose` or check the PHPUnit documentation
+to get an output that fits your needs. Depending on your parameters the output
+might look like this...
+
+```
+PHPUnit 5.1.3 by Sebastian Bergmann and contributors.
+
+.................................................S............... 65 / 81 ( 80%)
+..S............. 81 / 81 (100%)
+
+Time: 1.8 seconds, Memory: 10.00Mb
+
+OK, but incomplete, skipped, or risky tests!
+Tests: 81, Assertions: 166, Skipped: 2.
+```
+
+...or this:
+
+```
+PHPUnit 5.1.3 by Sebastian Bergmann and contributors.
+
+s\Icinga\Module\Director\CustomVariable\CustomVariables
+ [x] Whether special key names
+ [x] Vars can be unset and set again
+ [x] Variables to expression
+
+s\Icinga\Module\Director\IcingaConfig\AssignRenderer
+ [x] Whether equal match is correctly rendered
+ [x] Whether wildcards render a match method
+ [x] Whether a combined filter renders correctly
+
+s\Icinga\Module\Director\IcingaConfig\ExtensibleSet
+ [x] No values result in empty set
+ [x] Values passed to constructor are accepted
+ [x] Constructor accepts single values
+ [x] Single values can be blacklisted
+ [x] Multiple values can be blacklisted
+ [x] Simple inheritance works fine
+ [x] We can inherit from multiple parents
+ [x] Own values override parents
+ [x] Inherited values can be blacklisted
+ [x] Inherited values can be extended
+ [x] Combined definition renders correctly
+
+s\Icinga\Module\Director\IcingaConfig\IcingaConfigHelper
+ [x] Whether interval string is correctly parsed
+ [x] Whether invalid interval string raises exception
+ [x] Whether an empty value gives null
+ [x] Whether interval string is correctly rendered
+ [x] Correctly identifies reserved words
+ ...
+
+```
+
+The very same output could look as follows when shown by your CI-Tool:
+
+![Test result - testdox](screenshot/director/93_testing/932_director_testing_output_testdox.png)
+
+Running with Gitlab-CI
+----------------------
+
+This chapter assumes that you have Gitlab and Gitlab-CI up and running. Our
+`gitlab-ci.yml` file currently supposes you to have shared runners providing
+specific tags and the following operating systems, each of them with MySQL and
+PostgreSQL installed locally:
+
+* CentOS 7
+* Debian Stable (Jessie)
+* Ubuntu 16.04 LTS (Xenial)
+
+### Preparing the Gitlab Runners
+
+The following instructions suppose that you provide runner instances with a very
+basic installation of each operating system. We used naked LXC instances immediately
+after launching them.
+
+For all of them, please define the following variables first:
+
+```sh
+# The URL pointing to your Gitlab installation
+GITLAB_URL=https://your.gitlab.example.com
+
+# The registration token for your Gitlab runners
+REGISTRATION_TOKEN=iwQs************kLbH7
+```
+
+#### CentOS 7
+
+```sh
+yum makecache
+curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | bash
+
+# Sad workaround for container/chroot issues
+sed -i'' 's/repo_gpgcheck=1/repo_gpgcheck=0/' /etc/yum.repos.d/runner_gitlab-ci-multi-runner.repo
+
+# Package installation
+yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
+yum install -y https://packages.icinga.com/epel/7/release/noarch/icinga-rpm-release-7-1.el7.centos.noarch.rpm
+yum install -y php-Icinga icingaweb2-common phpunit mariadb-server postgresql-server \
+ postgresql-contrib gitlab-ci-multi-runner
+
+# Initialize the PostgreSQL data directory
+postgresql-setup initdb
+
+# We do not want to use Ident-based auth
+sed -ri 's/(all\s+127.0.0.1\/32\s+)ident/\1md5/' /var/lib/pgsql/data/pg_hba.conf
+
+# Start and enable all services
+systemctl enable postgresql
+systemctl start postgresql
+systemctl enable mariadb
+systemctl start mariadb
+
+# Fix platform-specific encoding issues
+echo "UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1'" | su - postgres -c psql
+echo "DROP DATABASE template1" | su - postgres -c psql
+echo "CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE'" | su - postgres -c psql
+echo "UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1'" | su - postgres -c psql
+echo "VACUUM FREEZE" | su - postgres -c psql template1
+
+# Grant the gitlab-runner ident-based admin access
+su - postgres -c 'createuser -a -d gitlab-runner'
+
+# Register the runner with your Gitlab installation
+gitlab-ci-multi-runner register -n \
+ -r "$REGISTRATION_TOKEN" \
+ --executor shell \
+ -u "$GITLAB_URL" \
+ --tag-list centos7,director
+```
+
+#### CentOS 6
+
+```
+yum makecache
+curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | bash
+
+# Package installation
+yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm
+yum install -y https://packages.icinga.com/epel/6/release/noarch/icinga-rpm-release-6-1.el6.noarch.rpm
+yum install -y php-Icinga icingaweb2-common phpunit mysql-server gitlab-ci-multi-runner
+
+# Start and enable MySQL
+/etc/init.d/mysqld start
+chkconfig mysqld on
+
+# No PostgeSQL, 8.4 on CentOS 6 is too old
+
+# Register the runner with your Gitlab installation
+gitlab-ci-multi-runner register -n \
+ -r "$REGISTRATION_TOKEN" \
+ --executor shell \
+ -u "$GITLAB_URL" \
+ --tag-list centos6,director
+```
+
+#### Debian Stable (Jessie)
+
+```sh
+# Package installation
+apt-get update -q -q
+apt-get install -y -q wget curl
+wget -q -O - http://packages.icinga.com/icinga.key | apt-key add -
+echo 'deb http://packages.icinga.com/debian icinga-jessie main' > /etc/apt/sources.list.d/icinga.list
+curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | bash
+apt-get update -q -q
+DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -q -y \
+ php5-cli phpunit php5-mysql php5-json php5-fpm zend-framework php-icinga php5-pgsql \
+ mysql-server postgresql postgresql-client postgresql-contrib-9.4 \
+ gitlab-ci-multi-runner
+
+# Grant the gitlab-runner ident-based admin access
+su - postgres -c 'createuser -a -d gitlab-runner'
+
+# Register the runner with your Gitlab installation
+gitlab-ci-multi-runner register -n \
+ -r "$REGISTRATION_TOKEN" \
+ --executor shell \
+ -u "$GITLAB_URL" \
+ --tag-list debian,jessie,director
+```
+
+#### Ubuntu 16.04 LTS (Xenial)
+
+```sh
+# Package installation
+apt-get update -q -q
+apt-get install -y -q wget curl
+wget -q -O - http://packages.icinga.com/icinga.key | apt-key add -
+echo 'deb http://packages.icinga.com/ubuntu icinga-xenial main' > /etc/apt/sources.list.d/icinga.list
+curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | bash
+apt-get update -q -q
+DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -q -y \
+ php7.0-cli php7.0-mysql php7.0-pgsql php7.0-json php7.0-fpm phpunit zend-framework php-icinga \
+ mariadb-server mariadb-client postgresql postgresql-client postgresql-contrib-9.5 \
+ gitlab-ci-multi-runner
+
+# Zend Framework is not in our include_path
+ln -sf /usr/share/php/libzend-framework-php/Zend /usr/share/php/
+
+# Allow non-root users to use password-less root
+mysql -e "UPDATE mysql.user SET plugin = 'mysql_native_password' WHERE User='root'; FLUSH PRIVILEGES;"
+
+# Grant the gitlab-runner ident-based admin access
+su - postgres -c 'createuser -a -d gitlab-runner'
+
+# Register the runner with your Gitlab installation
+gitlab-ci-multi-runner register -n \
+ -r "$REGISTRATION_TOKEN" \
+ --executor shell \
+ -u "$GITLAB_URL" \
+ --tag-list ubuntu,xenial,director
+```
+
+A lot of work, sure. But it gives us a lot of confidence when shipping our
+software for various platforms.
+
+![Test results - History](screenshot/director/93_testing/933_director_testing_history.png)
+
+### Docker, Docker, Docker...
+
+Yes, yes, yes. In future we might eventually provide a similar solution based on
+Docker images. Working with throw-away containers seems to be overkill here, as
+tearing up those containers would waste much more resources than running the tests.
diff --git a/doc/screenshot/director/08_import-and-sync/081_director_import_source.png b/doc/screenshot/director/08_import-and-sync/081_director_import_source.png
new file mode 100644
index 0000000..66126a5
--- /dev/null
+++ b/doc/screenshot/director/08_import-and-sync/081_director_import_source.png
Binary files differ
diff --git a/doc/screenshot/director/08_import-and-sync/082_director_import_modifier_lowercase.png b/doc/screenshot/director/08_import-and-sync/082_director_import_modifier_lowercase.png
new file mode 100644
index 0000000..ed8c1aa
--- /dev/null
+++ b/doc/screenshot/director/08_import-and-sync/082_director_import_modifier_lowercase.png
Binary files differ
diff --git a/doc/screenshot/director/08_import-and-sync/083_director_import_modifier_sid.png b/doc/screenshot/director/08_import-and-sync/083_director_import_modifier_sid.png
new file mode 100644
index 0000000..c0dbd71
--- /dev/null
+++ b/doc/screenshot/director/08_import-and-sync/083_director_import_modifier_sid.png
Binary files differ
diff --git a/doc/screenshot/director/08_import-and-sync/084_director_import_modifier_regex.png b/doc/screenshot/director/08_import-and-sync/084_director_import_modifier_regex.png
new file mode 100644
index 0000000..63a6aec
--- /dev/null
+++ b/doc/screenshot/director/08_import-and-sync/084_director_import_modifier_regex.png
Binary files differ
diff --git a/doc/screenshot/director/08_import-and-sync/085_director_import_preview.png b/doc/screenshot/director/08_import-and-sync/085_director_import_preview.png
new file mode 100644
index 0000000..b5aaa38
--- /dev/null
+++ b/doc/screenshot/director/08_import-and-sync/085_director_import_preview.png
Binary files differ
diff --git a/doc/screenshot/director/08_import-and-sync/086_director_sync_rule_ad_hosts.png b/doc/screenshot/director/08_import-and-sync/086_director_sync_rule_ad_hosts.png
new file mode 100644
index 0000000..55a7c0e
--- /dev/null
+++ b/doc/screenshot/director/08_import-and-sync/086_director_sync_rule_ad_hosts.png
Binary files differ
diff --git a/doc/screenshot/director/08_import-and-sync/087_director_sync_properties_ad_host.png b/doc/screenshot/director/08_import-and-sync/087_director_sync_properties_ad_host.png
new file mode 100644
index 0000000..50cec5c
--- /dev/null
+++ b/doc/screenshot/director/08_import-and-sync/087_director_sync_properties_ad_host.png
Binary files differ
diff --git a/doc/screenshot/director/14_fields-for-interfaces/141_define_datafields.png b/doc/screenshot/director/14_fields-for-interfaces/141_define_datafields.png
new file mode 100644
index 0000000..26bc80c
--- /dev/null
+++ b/doc/screenshot/director/14_fields-for-interfaces/141_define_datafields.png
Binary files differ
diff --git a/doc/screenshot/director/14_fields-for-interfaces/142_add_datafield.png b/doc/screenshot/director/14_fields-for-interfaces/142_add_datafield.png
new file mode 100644
index 0000000..42e7d8b
--- /dev/null
+++ b/doc/screenshot/director/14_fields-for-interfaces/142_add_datafield.png
Binary files differ
diff --git a/doc/screenshot/director/14_fields-for-interfaces/143_add_host_template.png b/doc/screenshot/director/14_fields-for-interfaces/143_add_host_template.png
new file mode 100644
index 0000000..4961bb8
--- /dev/null
+++ b/doc/screenshot/director/14_fields-for-interfaces/143_add_host_template.png
Binary files differ
diff --git a/doc/screenshot/director/14_fields-for-interfaces/144_add_template_field.png b/doc/screenshot/director/14_fields-for-interfaces/144_add_template_field.png
new file mode 100644
index 0000000..8973409
--- /dev/null
+++ b/doc/screenshot/director/14_fields-for-interfaces/144_add_template_field.png
Binary files differ
diff --git a/doc/screenshot/director/14_fields-for-interfaces/145_create_host.png b/doc/screenshot/director/14_fields-for-interfaces/145_create_host.png
new file mode 100644
index 0000000..ed7b61b
--- /dev/null
+++ b/doc/screenshot/director/14_fields-for-interfaces/145_create_host.png
Binary files differ
diff --git a/doc/screenshot/director/14_fields-for-interfaces/146_config_preview.png b/doc/screenshot/director/14_fields-for-interfaces/146_config_preview.png
new file mode 100644
index 0000000..526b3d9
--- /dev/null
+++ b/doc/screenshot/director/14_fields-for-interfaces/146_config_preview.png
Binary files differ
diff --git a/doc/screenshot/director/15_apply-for-services/151_monitored_services.png b/doc/screenshot/director/15_apply-for-services/151_monitored_services.png
new file mode 100644
index 0000000..bbb321d
--- /dev/null
+++ b/doc/screenshot/director/15_apply-for-services/151_monitored_services.png
Binary files differ
diff --git a/doc/screenshot/director/15_apply-for-services/152_add_service_template.png b/doc/screenshot/director/15_apply-for-services/152_add_service_template.png
new file mode 100644
index 0000000..8e3652d
--- /dev/null
+++ b/doc/screenshot/director/15_apply-for-services/152_add_service_template.png
Binary files differ
diff --git a/doc/screenshot/director/15_apply-for-services/153_add_service_template_field.png b/doc/screenshot/director/15_apply-for-services/153_add_service_template_field.png
new file mode 100644
index 0000000..f2677ce
--- /dev/null
+++ b/doc/screenshot/director/15_apply-for-services/153_add_service_template_field.png
Binary files differ
diff --git a/doc/screenshot/director/15_apply-for-services/154_create_apply_rule.png b/doc/screenshot/director/15_apply-for-services/154_create_apply_rule.png
new file mode 100644
index 0000000..3b621b9
--- /dev/null
+++ b/doc/screenshot/director/15_apply-for-services/154_create_apply_rule.png
Binary files differ
diff --git a/doc/screenshot/director/15_apply-for-services/155_configure_apply_for.png b/doc/screenshot/director/15_apply-for-services/155_configure_apply_for.png
new file mode 100644
index 0000000..b17ce9e
--- /dev/null
+++ b/doc/screenshot/director/15_apply-for-services/155_configure_apply_for.png
Binary files differ
diff --git a/doc/screenshot/director/15_apply-for-services/156_config_preview.png b/doc/screenshot/director/15_apply-for-services/156_config_preview.png
new file mode 100644
index 0000000..c9b20f8
--- /dev/null
+++ b/doc/screenshot/director/15_apply-for-services/156_config_preview.png
Binary files differ
diff --git a/doc/screenshot/director/16_fields_snmp/161_snmp_versions_create_list.png b/doc/screenshot/director/16_fields_snmp/161_snmp_versions_create_list.png
new file mode 100644
index 0000000..a0de485
--- /dev/null
+++ b/doc/screenshot/director/16_fields_snmp/161_snmp_versions_create_list.png
Binary files differ
diff --git a/doc/screenshot/director/16_fields_snmp/162_snmp_versions_fill_list.png b/doc/screenshot/director/16_fields_snmp/162_snmp_versions_fill_list.png
new file mode 100644
index 0000000..6eadd98
--- /dev/null
+++ b/doc/screenshot/director/16_fields_snmp/162_snmp_versions_fill_list.png
Binary files differ
diff --git a/doc/screenshot/director/16_fields_snmp/163_snmp_version_create_field.png b/doc/screenshot/director/16_fields_snmp/163_snmp_version_create_field.png
new file mode 100644
index 0000000..e0bd71c
--- /dev/null
+++ b/doc/screenshot/director/16_fields_snmp/163_snmp_version_create_field.png
Binary files differ
diff --git a/doc/screenshot/director/16_fields_snmp/164_snmp_fields_on_template.png b/doc/screenshot/director/16_fields_snmp/164_snmp_fields_on_template.png
new file mode 100644
index 0000000..6773c9c
--- /dev/null
+++ b/doc/screenshot/director/16_fields_snmp/164_snmp_fields_on_template.png
Binary files differ
diff --git a/doc/screenshot/director/16_fields_snmp/165_host_snmp_choose.png b/doc/screenshot/director/16_fields_snmp/165_host_snmp_choose.png
new file mode 100644
index 0000000..138b63b
--- /dev/null
+++ b/doc/screenshot/director/16_fields_snmp/165_host_snmp_choose.png
Binary files differ
diff --git a/doc/screenshot/director/16_fields_snmp/166_host_snmp_v2c.png b/doc/screenshot/director/16_fields_snmp/166_host_snmp_v2c.png
new file mode 100644
index 0000000..6a9186a
--- /dev/null
+++ b/doc/screenshot/director/16_fields_snmp/166_host_snmp_v2c.png
Binary files differ
diff --git a/doc/screenshot/director/16_fields_snmp/167_host_snmp_v3.png b/doc/screenshot/director/16_fields_snmp/167_host_snmp_v3.png
new file mode 100644
index 0000000..a64b831
--- /dev/null
+++ b/doc/screenshot/director/16_fields_snmp/167_host_snmp_v3.png
Binary files differ
diff --git a/doc/screenshot/director/16_fields_snmp/168_assign_snmp_check.png b/doc/screenshot/director/16_fields_snmp/168_assign_snmp_check.png
new file mode 100644
index 0000000..320c7c2
--- /dev/null
+++ b/doc/screenshot/director/16_fields_snmp/168_assign_snmp_check.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2401_agent_template.png b/doc/screenshot/director/24-agents/2401_agent_template.png
new file mode 100644
index 0000000..28ec8cf
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2401_agent_template.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2402_create_agent_based_host.png b/doc/screenshot/director/24-agents/2402_create_agent_based_host.png
new file mode 100644
index 0000000..2fcd3e4
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2402_create_agent_based_host.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2403_show_agent_instructions_1.png b/doc/screenshot/director/24-agents/2403_show_agent_instructions_1.png
new file mode 100644
index 0000000..9ccf848
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2403_show_agent_instructions_1.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2404_show_agent_instructions_2.png b/doc/screenshot/director/24-agents/2404_show_agent_instructions_2.png
new file mode 100644
index 0000000..607bba8
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2404_show_agent_instructions_2.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2405_agent_preview.png b/doc/screenshot/director/24-agents/2405_agent_preview.png
new file mode 100644
index 0000000..97d9271
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2405_agent_preview.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2406_agent_based_service.png b/doc/screenshot/director/24-agents/2406_agent_based_service.png
new file mode 100644
index 0000000..bb4a717
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2406_agent_based_service.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2407_create_agent_based_load_check.png b/doc/screenshot/director/24-agents/2407_create_agent_based_load_check.png
new file mode 100644
index 0000000..8c173c2
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2407_create_agent_based_load_check.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2409_agent_based_service_rendered_for_host.png b/doc/screenshot/director/24-agents/2409_agent_based_service_rendered_for_host.png
new file mode 100644
index 0000000..53f1e67
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2409_agent_based_service_rendered_for_host.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2410_agent_based_service_rendered_for_host_template.png b/doc/screenshot/director/24-agents/2410_agent_based_service_rendered_for_host_template.png
new file mode 100644
index 0000000..4d3d1c9
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2410_agent_based_service_rendered_for_host_template.png
Binary files differ
diff --git a/doc/screenshot/director/24-agents/2411_assign_agent_based_service.png b/doc/screenshot/director/24-agents/2411_assign_agent_based_service.png
new file mode 100644
index 0000000..f545da5
--- /dev/null
+++ b/doc/screenshot/director/24-agents/2411_assign_agent_based_service.png
Binary files differ
diff --git a/doc/screenshot/director/74_self-service-api/7401-director_self-service-dashboard.png b/doc/screenshot/director/74_self-service-api/7401-director_self-service-dashboard.png
new file mode 100644
index 0000000..e9b54f5
--- /dev/null
+++ b/doc/screenshot/director/74_self-service-api/7401-director_self-service-dashboard.png
Binary files differ
diff --git a/doc/screenshot/director/74_self-service-api/7402-director_self-service-choose-source.png b/doc/screenshot/director/74_self-service-api/7402-director_self-service-choose-source.png
new file mode 100644
index 0000000..bbc7559
--- /dev/null
+++ b/doc/screenshot/director/74_self-service-api/7402-director_self-service-choose-source.png
Binary files differ
diff --git a/doc/screenshot/director/74_self-service-api/7403-director_self-service-settings.png b/doc/screenshot/director/74_self-service-api/7403-director_self-service-settings.png
new file mode 100644
index 0000000..b7d3226
--- /dev/null
+++ b/doc/screenshot/director/74_self-service-api/7403-director_self-service-settings.png
Binary files differ
diff --git a/doc/screenshot/director/93_testing/931_director_testing_duration.png b/doc/screenshot/director/93_testing/931_director_testing_duration.png
new file mode 100644
index 0000000..f51adae
--- /dev/null
+++ b/doc/screenshot/director/93_testing/931_director_testing_duration.png
Binary files differ
diff --git a/doc/screenshot/director/93_testing/932_director_testing_output_testdox.png b/doc/screenshot/director/93_testing/932_director_testing_output_testdox.png
new file mode 100644
index 0000000..48f436c
--- /dev/null
+++ b/doc/screenshot/director/93_testing/932_director_testing_output_testdox.png
Binary files differ
diff --git a/doc/screenshot/director/93_testing/933_director_testing_history.png b/doc/screenshot/director/93_testing/933_director_testing_history.png
new file mode 100644
index 0000000..884f80a
--- /dev/null
+++ b/doc/screenshot/director/93_testing/933_director_testing_history.png
Binary files differ
diff --git a/doc/screenshot/director/readme/director_main_screen.png b/doc/screenshot/director/readme/director_main_screen.png
new file mode 100644
index 0000000..2a8e9f2
--- /dev/null
+++ b/doc/screenshot/director/readme/director_main_screen.png
Binary files differ
diff --git a/library/Director/Acl.php b/library/Director/Acl.php
new file mode 100644
index 0000000..4aa2bd2
--- /dev/null
+++ b/library/Director/Acl.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+use Icinga\Authentication\Auth;
+use Icinga\Authentication\Role;
+use Icinga\Exception\AuthenticationException;
+
+class Acl
+{
+ /** @var Auth */
+ protected $auth;
+
+ /** @var self */
+ private static $instance;
+
+ /**
+ * @return self
+ */
+ public static function instance()
+ {
+ if (self::$instance === null) {
+ self::$instance = new static(Auth::getInstance());
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Acl constructor
+ *
+ * @param Auth $auth
+ */
+ public function __construct(Auth $auth)
+ {
+ $this->auth = $auth;
+ }
+
+ /**
+ * Whether the given permission is available
+ *
+ * @param $name
+ *
+ * @return bool
+ */
+ public function hasPermission($name)
+ {
+ return $this->auth->hasPermission($name);
+ }
+
+ /**
+ * List all given roles
+ *
+ * @return array
+ */
+ public function listRoleNames()
+ {
+ return array_map(
+ array($this, 'getNameForRole'),
+ $this->getUser()->getRoles()
+ );
+ }
+
+ /**
+ * Get our user object, throws auth error if not available
+ *
+ * @return \Icinga\User
+ * @throws AuthenticationException
+ */
+ protected function getUser()
+ {
+ if (null === ($user = $this->auth->getUser())) {
+ throw new AuthenticationException('Authenticated user required');
+ }
+
+ return $user;
+ }
+
+ /**
+ * Get the name for a given role
+ *
+ * @param Role $role
+ *
+ * @return string
+ */
+ protected function getNameForRole(Role $role)
+ {
+ return $role->getName();
+ }
+}
diff --git a/library/Director/Application/Dependency.php b/library/Director/Application/Dependency.php
new file mode 100644
index 0000000..0100e69
--- /dev/null
+++ b/library/Director/Application/Dependency.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Icinga\Module\Director\Application;
+
+class Dependency
+{
+ /** @var string */
+ protected $name;
+
+ /** @var string|null */
+ protected $installedVersion;
+
+ /** @var bool|null */
+ protected $enabled;
+
+ /** @var string */
+ protected $operator;
+
+ /** @var string */
+ protected $requiredVersion;
+
+ /** @var string */
+ protected $requirement;
+
+ /**
+ * Dependency constructor.
+ * @param string $name Usually a module name
+ * @param string $requirement e.g. >=1.7.0
+ * @param string $installedVersion
+ * @param bool $enabled
+ */
+ public function __construct($name, $requirement, $installedVersion = null, $enabled = null)
+ {
+ $this->name = $name;
+ $this->setRequirement($requirement);
+ if ($installedVersion !== null) {
+ $this->setInstalledVersion($installedVersion);
+ }
+ if ($enabled !== null) {
+ $this->setEnabled($enabled);
+ }
+ }
+
+ public function setRequirement($requirement)
+ {
+ if (preg_match('/^([<>=]+)\s*v?(\d+\.\d+\.\d+)$/', $requirement, $match)) {
+ $this->operator = $match[1];
+ $this->requiredVersion = $match[2];
+ $this->requirement = $requirement;
+ } else {
+ throw new \InvalidArgumentException("'$requirement' is not a valid version constraint");
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ public function isInstalled()
+ {
+ return $this->installedVersion !== null;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getInstalledVersion()
+ {
+ return $this->installedVersion;
+ }
+
+ /**
+ * @param string $version
+ */
+ public function setInstalledVersion($version)
+ {
+ $this->installedVersion = ltrim($version, 'v'); // v0.6.0 VS 0.6.0
+ }
+
+ /**
+ * @return bool
+ */
+ public function isEnabled()
+ {
+ return $this->enabled === true;
+ }
+
+ /**
+ * @param bool $enabled
+ */
+ public function setEnabled($enabled = true)
+ {
+ $this->enabled = $enabled;
+ }
+
+ public function isSatisfied()
+ {
+ if (! $this->isInstalled() || ! $this->isEnabled()) {
+ return false;
+ }
+
+ return version_compare($this->installedVersion, $this->requiredVersion, $this->operator);
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getRequirement()
+ {
+ return $this->requirement;
+ }
+}
diff --git a/library/Director/Application/DependencyChecker.php b/library/Director/Application/DependencyChecker.php
new file mode 100644
index 0000000..d726b0b
--- /dev/null
+++ b/library/Director/Application/DependencyChecker.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Icinga\Module\Director\Application;
+
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Application\Modules\Module;
+use Icinga\Application\Version;
+
+class DependencyChecker
+{
+ /** @var ApplicationBootstrap */
+ protected $app;
+
+ /** @var \Icinga\Application\Modules\Manager */
+ protected $modules;
+
+ public function __construct(ApplicationBootstrap $app)
+ {
+ $this->app = $app;
+ $this->modules = $app->getModuleManager();
+ }
+
+ /**
+ * @param Module $module
+ * @return Dependency[]
+ */
+ public function getDependencies(Module $module)
+ {
+ $dependencies = [];
+ $isV290 = version_compare(Version::VERSION, '2.9.0', '>=');
+ foreach ($module->getDependencies() as $moduleName => $required) {
+ if ($isV290 && in_array($moduleName, ['ipl', 'reactbundle'], true)) {
+ continue;
+ }
+ $dependency = new Dependency($moduleName, $required);
+ $dependency->setEnabled($this->modules->hasEnabled($moduleName));
+ if ($this->modules->hasInstalled($moduleName)) {
+ $dependency->setInstalledVersion($this->modules->getModule($moduleName, false)->getVersion());
+ }
+ $dependencies[] = $dependency;
+ }
+ if ($isV290) {
+ $libs = $this->app->getLibraries();
+ foreach ($module->getRequiredLibraries() as $libraryName => $required) {
+ $dependency = new Dependency($libraryName, $required);
+ if ($libs->has($libraryName)) {
+ $dependency->setInstalledVersion($libs->get($libraryName)->getVersion());
+ $dependency->setEnabled();
+ }
+ $dependencies[] = $dependency;
+ }
+ }
+
+ return $dependencies;
+ }
+
+ // if (version_compare(Version::VERSION, '2.9.0', 'ge')) {
+ // }
+ /**
+ * @param Module $module
+ * @return bool
+ */
+ public function satisfiesDependencies(Module $module)
+ {
+ foreach ($this->getDependencies($module) as $dependency) {
+ if (! $dependency->isSatisfied()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/library/Director/Application/MemoryLimit.php b/library/Director/Application/MemoryLimit.php
new file mode 100644
index 0000000..beb0460
--- /dev/null
+++ b/library/Director/Application/MemoryLimit.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Icinga\Module\Director\Application;
+
+class MemoryLimit
+{
+ public static function raiseTo($string)
+ {
+ $current = static::getBytes();
+ $desired = static::parsePhpIniByteString($string);
+ if ($current !== -1 && $current < $desired) {
+ ini_set('memory_limit', $string);
+ }
+ }
+
+ public static function getBytes()
+ {
+ return static::parsePhpIniByteString((string) ini_get('memory_limit'));
+ }
+
+ /**
+ * Return Bytes from PHP shorthand bytes notation
+ *
+ * http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
+ *
+ * > The available options are K (for Kilobytes), M (for Megabytes) and G
+ * > (for Gigabytes), and are all case-insensitive. Anything else assumes
+ * > bytes.
+ *
+ * @param $string
+ * @return int
+ */
+ public static function parsePhpIniByteString($string)
+ {
+ $val = trim($string);
+
+ if (preg_match('/^(\d+)([KMG])$/', $val, $m)) {
+ $val = $m[1];
+ switch ($m[2]) {
+ case 'G':
+ $val *= 1024;
+ // Intentional fall-through
+ case 'M':
+ $val *= 1024;
+ // Intentional fall-through
+ case 'K':
+ $val *= 1024;
+ }
+ }
+
+ return intval($val);
+ }
+}
diff --git a/library/Director/CheckPlugin/Check.php b/library/Director/CheckPlugin/Check.php
new file mode 100644
index 0000000..d05f5a7
--- /dev/null
+++ b/library/Director/CheckPlugin/Check.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Director\CheckPlugin;
+
+use Exception;
+
+class Check extends CheckResults
+{
+ public function call(callable $check, $errorState = 'CRITICAL')
+ {
+ try {
+ $check();
+ } catch (Exception $e) {
+ $this->fail($e, $errorState);
+ }
+
+ return $this;
+ }
+
+ public function assertTrue($check, $message, $errorState = 'CRITICAL')
+ {
+ if ($this->makeBool($check, $message) === true) {
+ $this->succeed($message);
+ } else {
+ $this->fail($message, $errorState);
+ }
+
+ return $this;
+ }
+
+ public function assertFalse($check, $message, $errorState = 'CRITICAL')
+ {
+ if ($this->makeBool($check, $message) === false) {
+ $this->succeed($message);
+ } else {
+ $this->fail($message, $errorState);
+ }
+
+ return $this;
+ }
+
+ protected function makeBool($check, &$message)
+ {
+ if (is_callable($check)) {
+ try {
+ $check = $check();
+ } catch (Exception $e) {
+ $message .= ': ' . $e->getMessage();
+ return false;
+ }
+ }
+
+ if (! is_bool($check)) {
+ return null;
+ }
+
+ return $check;
+ }
+}
diff --git a/library/Director/CheckPlugin/CheckResult.php b/library/Director/CheckPlugin/CheckResult.php
new file mode 100644
index 0000000..cdf9b0d
--- /dev/null
+++ b/library/Director/CheckPlugin/CheckResult.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\CheckPlugin;
+
+class CheckResult
+{
+ protected $state;
+
+ protected $output;
+
+ public function __construct($output, $state = 0)
+ {
+ if ($state instanceof PluginState) {
+ $this->state = $state;
+ } else {
+ $this->state = new PluginState($state);
+ }
+
+ $this->output = $output;
+ }
+
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ public function getOutput()
+ {
+ return $this->output;
+ }
+}
diff --git a/library/Director/CheckPlugin/CheckResults.php b/library/Director/CheckPlugin/CheckResults.php
new file mode 100644
index 0000000..7e20225
--- /dev/null
+++ b/library/Director/CheckPlugin/CheckResults.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace Icinga\Module\Director\CheckPlugin;
+
+use Exception;
+
+class CheckResults
+{
+ /** @var string */
+ protected $name;
+
+ /** @var PluginState */
+ protected $state;
+
+ /** @var CheckResult[] */
+ protected $results = [];
+
+ protected $stateCounters = [
+ 0 => 0,
+ 1 => 0,
+ 2 => 0,
+ 3 => 0,
+ ];
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ $this->state = new PluginState(0);
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function add(CheckResult $result)
+ {
+ $this->results[] = $result;
+ $this->state->raise($result->getState());
+ $this->stateCounters[$result->getState()->getNumeric()]++;
+
+ return $this;
+ }
+
+ public function getStateCounters()
+ {
+ return $this->stateCounters;
+ }
+
+ public function getProblemSummary()
+ {
+ $summary = [];
+ for ($i = 1; $i <= 3; $i++) {
+ $count = $this->stateCounters[$i];
+ if ($count === 0) {
+ continue;
+ }
+ $summary[PluginState::create($i)->getName()] = $count;
+ }
+
+ return $summary;
+ }
+
+ public function getStateSummaryString()
+ {
+ $summary = [sprintf(
+ '%d tests OK',
+ $this->stateCounters[0]
+ )];
+
+ for ($i = 1; $i <= 3; $i++) {
+ $count = $this->stateCounters[$i];
+ if ($count === 0) {
+ continue;
+ }
+ $summary[] = sprintf(
+ '%dx %s',
+ $count,
+ PluginState::create($i)->getName()
+ );
+ }
+
+ return implode(', ', $summary);
+ }
+
+ public function getOutput()
+ {
+ $output = sprintf(
+ "%s: %s\n",
+ $this->name,
+ $this->getStateSummaryString()
+ );
+
+ foreach ($this->results as $result) {
+ $output .= sprintf(
+ "[%s] %s\n",
+ $result->getState()->getName(),
+ $result->getOutput()
+ );
+ }
+
+ return $output;
+ }
+
+ public function getResults()
+ {
+ return $this->results;
+ }
+
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ public function hasProblems()
+ {
+ return $this->getState()->getNumeric() !== 0;
+ }
+
+ public function hasErrors()
+ {
+ $state = $this->getState()->getNumeric();
+ return $state !== 0 && $state !== 1;
+ }
+
+ public function succeed($message)
+ {
+ $this->add(new CheckResult($message));
+
+ return $this;
+ }
+
+ public function warn($message)
+ {
+ $this->add(new CheckResult($message, 1));
+
+ return $this;
+ }
+
+ public function fail($message, $errorState = 'CRITICAL')
+ {
+ if ($message instanceof Exception) {
+ $message = $message->getMessage();
+ }
+
+ $this->add(new CheckResult($message, $errorState));
+
+ return $this;
+ }
+}
diff --git a/library/Director/CheckPlugin/PluginState.php b/library/Director/CheckPlugin/PluginState.php
new file mode 100644
index 0000000..d68ec70
--- /dev/null
+++ b/library/Director/CheckPlugin/PluginState.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Icinga\Module\Director\CheckPlugin;
+
+use Icinga\Exception\ProgrammingError;
+
+class PluginState
+{
+ protected static $stateCodes = [
+ 'UNKNOWN' => 3,
+ 'CRITICAL' => 2,
+ 'WARNING' => 1,
+ 'OK' => 0,
+ ];
+
+ protected static $stateNames = [
+ 'OK',
+ 'WARNING',
+ 'CRITICAL',
+ 'UNKNOWN',
+ ];
+
+ protected static $sortSeverity = [0, 1, 3, 2];
+
+ /** @var int */
+ protected $state;
+
+ public function __construct($state)
+ {
+ $this->set($state);
+ }
+
+ public function isProblem()
+ {
+ return $this->state > 0;
+ }
+
+ public function set($state)
+ {
+ $this->state = $this->getNumericStateFor($state);
+ }
+
+ public function getNumeric()
+ {
+ return $this->state;
+ }
+
+ public function getSortSeverity()
+ {
+ return static::getSortSeverityFor($this->getNumeric());
+ }
+
+ public function getName()
+ {
+ return self::$stateNames[$this->getNumeric()];
+ }
+
+ public function raise(PluginState $state)
+ {
+ if ($this->getSortSeverity() < $state->getSortSeverity()) {
+ $this->state = $state->getNumeric();
+ }
+
+ return $this;
+ }
+
+ public static function create($state)
+ {
+ return new static($state);
+ }
+
+ public static function ok()
+ {
+ return new static(0);
+ }
+
+ public static function warning()
+ {
+ return new static(1);
+ }
+
+ public static function critical()
+ {
+ return new static(2);
+ }
+
+ public static function unknown()
+ {
+ return new static(3);
+ }
+
+ protected static function getNumericStateFor($state)
+ {
+ if ((is_int($state) || ctype_digit($state)) && $state >= 0 && $state <= 3) {
+ return (int) $state;
+ } elseif (is_string($state) && array_key_exists($state, self::$stateCodes)) {
+ return self::$stateCodes[$state];
+ } else {
+ throw new ProgrammingError('Expected valid state, got: %s', $state);
+ }
+ }
+
+ protected static function getSortSeverityFor($state)
+ {
+ if (array_key_exists($state, self::$sortSeverity)) {
+ return self::$sortSeverity[$state];
+ } else {
+ throw new ProgrammingError(
+ 'Unable to retrieve sort severity for invalid state: %s',
+ $state
+ );
+ }
+ }
+}
diff --git a/library/Director/CheckPlugin/Range.php b/library/Director/CheckPlugin/Range.php
new file mode 100644
index 0000000..d7b582e
--- /dev/null
+++ b/library/Director/CheckPlugin/Range.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Icinga\Module\Director\CheckPlugin;
+
+use Icinga\Exception\ConfigurationError;
+
+class Range
+{
+ /** @var float|null */
+ protected $start = 0;
+
+ /** @var float|null */
+ protected $end = null;
+
+ /** @var bool */
+ protected $mustBeWithinRange = true;
+
+ public function __construct($start = 0, $end = null, $mustBeWithinRange = true)
+ {
+ $this->start = $start;
+ $this->end = $end;
+ $this->mustBeWithinRange = $mustBeWithinRange;
+ }
+
+ public function valueIsValid($value)
+ {
+ if ($this->valueIsWithinRange($value)) {
+ return $this->valueMustBeWithinRange();
+ } else {
+ return ! $this->valueMustBeWithinRange();
+ }
+ }
+
+ public function valueIsWithinRange($value)
+ {
+ if ($this->start !== null && $value < $this->start) {
+ return false;
+ }
+ if ($this->end !== null && $value > $this->end) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function valueMustBeWithinRange()
+ {
+ return $this->mustBeWithinRange;
+ }
+
+ /**
+ * @param $any
+ * @return static
+ */
+ public static function wantRange($any)
+ {
+ if ($any instanceof static) {
+ return $any;
+ } else {
+ return static::parse($any);
+ }
+ }
+
+ /**
+ * @param $string
+ * @return static
+ * @throws ConfigurationError
+ */
+ public static function parse($string)
+ {
+ $string = str_replace(' ', '', $string);
+ $value = '[-+]?[\d\.]+';
+ $valueRe = "$value(?:e$value)?";
+ $regex = "/^(@)?($valueRe|~)(:$valueRe|~)?/";
+ if (! preg_match($regex, $string, $match)) {
+ throw new ConfigurationError('Invalid range definition: %s', $string);
+ }
+
+ $inside = $match[1] === '@';
+
+ if (strlen($match[3]) === 0) {
+ $start = 0;
+ $end = static::parseValue($match[2]);
+ } else {
+ $start = static::parseValue($match[2]);
+ $end = static::parseValue($match[3]);
+ }
+ $range = new static($start, $end, $inside);
+
+ return $range;
+ }
+
+ protected static function parseValue($value)
+ {
+ if ($value === '~') {
+ return null;
+ } else {
+ return $value;
+ }
+ }
+}
diff --git a/library/Director/CheckPlugin/Threshold.php b/library/Director/CheckPlugin/Threshold.php
new file mode 100644
index 0000000..76aac4e
--- /dev/null
+++ b/library/Director/CheckPlugin/Threshold.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Icinga\Module\Director\CheckPlugin;
+
+class Threshold
+{
+ /** @var Range */
+ protected $warning;
+
+ /** @var Range */
+ protected $critical;
+
+ public function __construct($warning = null, $critical = null)
+ {
+ if ($warning !== null) {
+ $this->warning = Range::wantRange($warning);
+ }
+
+ if ($critical !== null) {
+ $this->critical = Range::wantRange($critical);
+ }
+ }
+
+ public static function check($value, $message, $warning = null, $critical = null)
+ {
+ $threshold = new static($warning, $critical);
+ $state = $threshold->checkValue($value);
+ return new CheckResult($message, $state);
+ }
+
+ public function checkValue($value)
+ {
+ if ($this->critical !== null) {
+ if (! $this->critical->valueIsValid($value)) {
+ return PluginState::critical();
+ }
+ }
+
+ if ($this->warning !== null) {
+ if (! $this->warning->valueIsValid($value)) {
+ return PluginState::warning();
+ }
+ }
+
+ return PluginState::ok();
+ }
+}
diff --git a/library/Director/Cli/Command.php b/library/Director/Cli/Command.php
new file mode 100644
index 0000000..69d61b1
--- /dev/null
+++ b/library/Director/Cli/Command.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Icinga\Module\Director\Cli;
+
+use gipfl\Json\JsonDecodeException;
+use gipfl\Json\JsonString;
+use Icinga\Cli\Command as CliCommand;
+use Icinga\Module\Director\Application\MemoryLimit;
+use Icinga\Module\Director\Core\CoreApi;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Application\Config;
+use RuntimeException;
+
+class Command extends CliCommand
+{
+ /** @var Db */
+ protected $db;
+
+ /** @var CoreApi */
+ private $api;
+
+ protected function renderJson($object, $pretty = true)
+ {
+ return JsonString::encode($object, $pretty ? JSON_PRETTY_PRINT : null) . "\n";
+ }
+
+ /**
+ * @param $json
+ * @return mixed
+ */
+ protected function parseJson($json)
+ {
+ try {
+ return JsonString::decode($json);
+ } catch (JsonDecodeException $e) {
+ $this->fail('Invalid JSON: %s', $e->getMessage());
+ }
+ }
+
+ /**
+ * @param string $msg
+ * @return never-return
+ */
+ public function fail($msg)
+ {
+ $args = func_get_args();
+ array_shift($args);
+ if (count($args)) {
+ $msg = vsprintf($msg, $args);
+ }
+ echo $this->screen->colorize("ERROR", 'red') . ": $msg\n";
+ exit(1);
+ }
+
+ /**
+ * @param null $endpointName
+ * @return CoreApi|\Icinga\Module\Director\Core\LegacyDeploymentApi
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function api($endpointName = null)
+ {
+ if ($this->api === null) {
+ if ($endpointName === null) {
+ $endpoint = $this->db()->getDeploymentEndpoint();
+ } else {
+ $endpoint = IcingaEndpoint::load($endpointName, $this->db());
+ }
+
+ $this->api = $endpoint->api();
+ }
+
+ return $this->api;
+ }
+
+ /**
+ * Raise PHP resource limits
+ *
+ * @return self;
+ */
+ protected function raiseLimits()
+ {
+ MemoryLimit::raiseTo('1024M');
+
+ ini_set('max_execution_time', 0);
+ if (version_compare(PHP_VERSION, '7.0.0') < 0) {
+ ini_set('zend.enable_gc', 0);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return Db
+ */
+ protected function db()
+ {
+ if ($this->db === null) {
+ $resourceName = $this->params->get('dbResourceName');
+
+ if ($resourceName === null) {
+ // Hint: not using $this->Config() intentionally. This allows
+ // CLI commands in other modules to use this as a base class.
+ $resourceName = Config::module('director')->get('db', 'resource');
+ }
+ if ($resourceName) {
+ $this->db = Db::fromResourceName($resourceName);
+ } else {
+ throw new RuntimeException('Director is not configured correctly');
+ }
+ }
+
+ return $this->db;
+ }
+}
diff --git a/library/Director/Cli/ObjectCommand.php b/library/Director/Cli/ObjectCommand.php
new file mode 100644
index 0000000..ca68213
--- /dev/null
+++ b/library/Director/Cli/ObjectCommand.php
@@ -0,0 +1,517 @@
+<?php
+
+namespace Icinga\Module\Director\Cli;
+
+use Icinga\Cli\Params;
+use Icinga\Exception\MissingParameterException;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Data\Exporter;
+use Icinga\Module\Director\Data\PropertyMangler;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use InvalidArgumentException;
+
+class ObjectCommand extends Command
+{
+ protected $type;
+
+ private $name;
+
+ private $object;
+
+ private $experimentalFlags = array();
+
+ public function init()
+ {
+ $this->shiftExperimentalFlags();
+ }
+
+ /**
+ * Show a specific object
+ *
+ * Use this command to show single objects rendered as Icinga 2
+ * config or in JSON format.
+ *
+ * USAGE
+ *
+ * icingacli director <type> show <name> [options]
+ *
+ * OPTIONS
+ *
+ * --resolved Resolve all inherited properties and show a flat
+ * object
+ * --json Use JSON format
+ * --no-pretty JSON is pretty-printed per default. Use this flag
+ * to enforce un-formatted JSON
+ * --no-defaults Per default JSON output ships null or default
+ * values. This flag skips those properties
+ * --with-services For hosts only, also shows attached services
+ * --all-services For hosts only, show applied and inherited services
+ * too
+ */
+ public function showAction()
+ {
+ $db = $this->db();
+ $object = $this->getObject();
+ $exporter = new Exporter($db);
+ $resolve = (bool) $this->params->shift('resolved');
+ $withServices = (bool) $this->params->get('with-services');
+ $allServices = (bool) $this->params->get('all-services');
+ if ($withServices) {
+ if (!$object instanceof IcingaHost) {
+ $this->fail('--with-services is available for Hosts only');
+ }
+ $exporter->enableHostServices();
+ }
+ if ($allServices) {
+ if (!$object instanceof IcingaHost) {
+ $this->fail('--all-services is available for Hosts only');
+ }
+ $exporter->serviceLoader()->resolveHostServices();
+ }
+
+ $exporter->resolveObjects($resolve);
+ $exporter->showDefaults($this->params->shift('no-defaults', false));
+
+ if ($this->params->shift('json')) {
+ echo $this->renderJson($exporter->export($object), !$this->params->shift('no-pretty'));
+ } else {
+ $config = new IcingaConfig($db);
+ if ($resolve) {
+ $object = $object::fromPlainObject($object->toPlainObject(true, false, null, false), $db);
+ }
+ $object->renderToConfig($config);
+ if ($withServices) {
+ foreach ($exporter->serviceLoader()->fetchServicesForHost($object) as $service) {
+ $service->renderToConfig($config);
+ }
+ }
+ foreach ($config->getFiles() as $filename => $content) {
+ printf("/** %s **/\n\n", $filename);
+ echo $content;
+ }
+ }
+ }
+
+ /**
+ * Create a new object
+ *
+ * Use this command to create a new Icinga object
+ *
+ * USAGE
+ *
+ * icingacli director <type> create [<name>] [options]
+ *
+ * OPTIONS
+ *
+ * --<key> <value> Provide all properties as single command line
+ * options
+ * --json Otherwise provide all options as a JSON string
+ *
+ * EXAMPLES
+ *
+ * icingacli director host create localhost \
+ * --imports generic-host \
+ * --address 127.0.0.1 \
+ * --vars.location 'My datacenter'
+ *
+ * icingacli director host create localhost \
+ * --json '{ "address": "127.0.0.1" }'
+ */
+ public function createAction()
+ {
+ $type = $this->getType();
+ $props = $this->getObjectProperties();
+ $name = $props['object_name'];
+ $object = IcingaObject::createByType($type, $props, $this->db());
+
+ if ($object->store()) {
+ printf("%s '%s' has been created\n", $type, $name);
+ if ($this->hasExperimental('live-creation')) {
+ if ($this->api()->createObjectAtRuntime($object)) {
+ echo "Live creation for '$name' succeeded\n";
+ } else {
+ echo "Live creation for '$name' succeeded\n";
+ exit(1);
+ }
+
+ if ($type === 'Host' && $this->hasExperimental('immediate-check')) {
+ echo "Waiting for check result...";
+ flush();
+ if ($res = $this->api()->checkHostAndWaitForResult($name)) {
+ echo " done\n" . $res->output . "\n";
+ } else {
+ echo "TIMEOUT\n";
+ }
+ }
+ }
+
+ exit(0);
+ } else {
+ printf("%s '%s' has not been created\n", $type, $name);
+ exit(1);
+ }
+ }
+
+ /**
+ * Modify an existing objects properties
+ *
+ * Use this command to modify specific properties of an existing
+ * Icinga object
+ *
+ * USAGE
+ *
+ * icingacli director <type> set <name> [options]
+ *
+ * OPTIONS
+ *
+ * --<key> <value> Provide all properties as single command line
+ * options
+ * --append-<key> <value> Appends to array values, like `imports`,
+ * `groups` or `vars.system_owners`
+ * --remove-<key> [<value>] Remove a specific property, eventually only
+ * when matching `value`. In case the property is an
+ * array it will remove just `value` when given
+ * --json Otherwise provide all options as a JSON string
+ * --replace Replace all object properties with the given ones
+ * --auto-create Create the object in case it does not exist
+ *
+ * EXAMPLES
+ *
+ * icingacli director host set localhost \
+ * --address 127.0.0.2 \
+ * --vars.location 'Somewhere else'
+ *
+ * icingacli director host set localhost \
+ * --json '{ "address": "127.0.0.2" }'
+ */
+ public function setAction()
+ {
+ $name = $this->getName();
+ $type = $this->getType();
+
+ if ($this->params->shift('auto-create') && ! $this->exists($name)) {
+ $action = 'created';
+ $object = $this->create($type, $name);
+ } else {
+ $action = 'modified';
+ $object = $this->getObject();
+ }
+
+ $appends = self::stripPrefixedProperties($this->params, 'append-');
+ $remove = self::stripPrefixedProperties($this->params, 'remove-');
+
+ if ($this->params->shift('replace')) {
+ $object->replaceWith($this->create($type, $name, $this->remainingParams()));
+ } else {
+ $object->setProperties($this->remainingParams());
+ }
+
+ PropertyMangler::appendToArrayProperties($object, $appends);
+ PropertyMangler::removeProperties($object, $remove);
+ $this->persistChanges($object, $type, $name, $action);
+ }
+
+ protected function persistChanges(DbObject $object, $type, $name, $action)
+ {
+ if ($object->hasBeenModified() && $object->store()) {
+ printf("%s '%s' has been %s\n", $type, $name, $action);
+ exit(0);
+ }
+
+ printf("%s '%s' has not been modified\n", $type, $name);
+ exit(0);
+ }
+
+ /**
+ * Delete a specific object
+ *
+ * Use this command to delete a single Icinga object
+ *
+ * USAGE
+ *
+ * icingacli director <type> delete <name>
+ *
+ * EXAMPLES
+ *
+ * icingacli director host delete localhost2
+ *
+ * icingacli director host delete localhost{3..8}
+ */
+ public function deleteAction()
+ {
+ $type = $this->getType();
+
+ foreach ($this->shiftOneOrMoreNames() as $name) {
+ if ($this->load($name)->delete()) {
+ printf("%s '%s' has been deleted\n", $type, $name);
+ } else {
+ printf("Something went wrong while deleting %s '%s'\n", $type, $name);
+ exit(1);
+ }
+
+ $this->object = null;
+ }
+ exit(0);
+ }
+
+ /**
+ * Whether a specific object exists
+ *
+ * Use this command to find out whether a single Icinga object exists
+ *
+ * USAGE
+ *
+ * icingacli director <type> exists <name>
+ */
+ public function existsAction()
+ {
+ $name = $this->getName();
+ $type = $this->getType();
+ if ($this->exists($name)) {
+ printf("%s '%s' exists\n", $type, $name);
+ exit(0);
+ } else {
+ printf("%s '%s' does not exist\n", $type, $name);
+ exit(1);
+ }
+ }
+
+ /**
+ * Clone an existing object
+ *
+ * Use this command to clone a specific object
+ *
+ * USAGE
+ *
+ * icingacli director <type> clone <name> --from <original> [options]
+ *
+ * OPTIONS
+ * --from <original> The name of the object you want to clone
+ * --<key> <value> Override specific properties while cloning
+ * --replace In case an object <name> already exists replace
+ * it with the clone
+ * --flat Do no keep inherited properties but create a flat
+ * object with all resolved/inherited properties
+ *
+ * EXAMPLES
+ *
+ * icingacli director host clone localhost2 --from localhost
+ *
+ * icingacli director host clone localhost{3..8} --from localhost2
+ *
+ * icingacli director host clone localhost3 --from localhost \
+ * --address 127.0.0.3
+ */
+ public function cloneAction()
+ {
+ $fromName = $this->params->shiftRequired('from');
+ $from = $this->load($fromName);
+
+ // $name = $this->getName();
+ $type = $this->getType();
+
+ $resolve = $this->params->shift('flat');
+ $replace = $this->params->shift('replace');
+
+ $from->setProperties($this->remainingParams());
+
+ foreach ($this->shiftOneOrMoreNames() as $name) {
+ $object = $from::fromPlainObject(
+ $from->toPlainObject($resolve),
+ $from->getConnection()
+ );
+
+ $object->set('object_name', $name);
+
+ if ($replace && $this->exists($name)) {
+ $object = $this->load($name)->replaceWith($object);
+ }
+
+ if ($object->hasBeenModified() && $object->store()) {
+ printf("%s '%s' has been cloned from %s\n", $type, $name, $fromName);
+ } else {
+ printf("%s '%s' has not been modified\n", $this->getType(), $name);
+ }
+ }
+
+ exit(0);
+ }
+
+ protected static function stripPrefixedProperties(Params $params, $prefix = 'append-')
+ {
+ $appends = [];
+ $len = strlen($prefix);
+
+ foreach ($params->getParams() as $key => $value) {
+ if (substr($key, 0, $len) === $prefix) {
+ $appends[substr($key, $len)] = $value;
+ }
+ }
+
+ foreach ($appends as $key => $value) {
+ $params->shift("$prefix$key");
+ }
+
+ return $appends;
+ }
+
+ protected function getObjectProperties()
+ {
+ $name = $this->params->shift();
+
+ $props = $this->remainingParams();
+ if (! array_key_exists('object_type', $props)) {
+ $props['object_type'] = 'object';
+ }
+
+ // Normalize object_name, compare to given name
+ if ($name) {
+ if (array_key_exists('object_name', $props)) {
+ if ($name !== $props['object_name']) {
+ $this->fail(sprintf(
+ "Name '%s' conflicts with object_name '%s'\n",
+ $name,
+ $props['object_name']
+ ));
+ }
+ } else {
+ $props['object_name'] = $name;
+ }
+ } else {
+ if (! array_key_exists('object_name', $props)) {
+ $this->fail('Cannot create an object with at least an object name');
+ }
+ }
+
+ return $props;
+ }
+
+ protected function shiftOneOrMoreNames()
+ {
+ $names = array();
+ while ($name = $this->params->shift()) {
+ $names[] = $name;
+ }
+
+ if (empty($names)) {
+ throw new MissingParameterException('Required object name is missing');
+ }
+
+ return $names;
+ }
+
+ protected function remainingParams()
+ {
+ if ($json = $this->params->shift('json')) {
+ if ($json === true) {
+ $json = $this->readFromStdin();
+ if ($json === null) {
+ $this->fail('Please pass JSON either via STDIN or via --json');
+ }
+ }
+ return (array) $this->parseJson($json);
+ } else {
+ return $this->params->getParams();
+ }
+ }
+
+ protected function readFromStdin()
+ {
+ if (!defined('STDIN')) {
+ define('STDIN', fopen('php://stdin', 'r'));
+ }
+ $inputIsTty = function_exists('posix_isatty') && posix_isatty(STDIN);
+ if ($inputIsTty) {
+ return null;
+ }
+
+ $stdin = file_get_contents('php://stdin');
+ if (strlen($stdin) === 0) {
+ return null;
+ }
+
+ return $stdin;
+ }
+
+ protected function exists($name)
+ {
+ return IcingaObject::existsByType(
+ $this->getType(),
+ $name,
+ $this->db()
+ );
+ }
+
+ protected function load($name)
+ {
+ return IcingaObject::loadByType(
+ $this->getType(),
+ $name,
+ $this->db()
+ );
+ }
+
+ protected function create($type, $name, $properties = [])
+ {
+ return IcingaObject::createByType($type, $properties + [
+ 'object_type' => 'object',
+ 'object_name' => $name
+ ], $this->db());
+ }
+
+ /**
+ * @return IcingaObject
+ */
+ protected function getObject()
+ {
+ if ($this->object === null) {
+ $this->object = $this->load($this->getName());
+ }
+
+ return $this->object;
+ }
+
+ protected function getType()
+ {
+ if ($this->type === null) {
+ // Extract the command class name...
+ $className = substr(strrchr(get_class($this), '\\'), 1);
+ // ...and strip the Command extension
+ $this->type = substr($className, 0, -7);
+ }
+
+ return $this->type;
+ }
+
+ protected function getName()
+ {
+ if ($this->name === null) {
+ $name = $this->params->shift();
+ if (! $name) {
+ throw new InvalidArgumentException('Object name parameter is required');
+ }
+
+ $this->name = $name;
+ }
+
+ return $this->name;
+ }
+
+ protected function hasExperimental($flag)
+ {
+ return array_key_exists($flag, $this->experimentalFlags);
+ }
+
+ protected function shiftExperimentalFlags()
+ {
+ if ($flags = $this->params->shift('experimental')) {
+ foreach (preg_split('/,/', $flags) as $flag) {
+ $this->experimentalFlags[$flag] = true;
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/Cli/ObjectsCommand.php b/library/Director/Cli/ObjectsCommand.php
new file mode 100644
index 0000000..3e0844a
--- /dev/null
+++ b/library/Director/Cli/ObjectsCommand.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Icinga\Module\Director\Cli;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class ObjectsCommand extends Command
+{
+ protected $type;
+
+ private $objects;
+
+ /**
+ * List all objects of this type
+ *
+ * Use this command to get a list of all matching objects
+ *
+ * USAGE
+ *
+ * icingacli director <types> list [options]
+ *
+ * OPTIONS
+ *
+ * --json Use JSON format
+ * --no-pretty JSON is pretty-printed per default (for PHP >= 5.4)
+ * Use this flag to enforce unformatted JSON
+ */
+ public function listAction()
+ {
+ $db = $this->db();
+ $result = array();
+ foreach ($this->getObjects() as $o) {
+ $result[] = $o->getObjectName();
+ }
+
+ sort($result);
+
+ if ($this->params->shift('json')) {
+ echo $this->renderJson($result, !$this->params->shift('no-pretty'));
+ } else {
+ foreach ($result as $name) {
+ echo $name . "\n";
+ }
+ }
+ }
+
+ /**
+ * Fetch all objects of this type
+ *
+ * Use this command to fetch all matching objects
+ *
+ * USAGE
+ *
+ * icingacli director <types> fetch [options]
+ *
+ * OPTIONS
+ *
+ * --resolved Resolve all inherited properties and show a flat
+ * object
+ * --json Use JSON format
+ * --no-pretty JSON is pretty-printed per default (for PHP >= 5.4)
+ * Use this flag to enforce unformatted JSON
+ * --no-defaults Per default JSON output ships null or default values
+ * With this flag you will skip those properties
+ */
+ public function fetchAction()
+ {
+ $resolved = $this->params->shift('resolved');
+
+ if ($this->params->shift('json')) {
+ $noDefaults = $this->params->shift('no-defaults', false);
+ } else {
+ $this->fail('Currently only json is supported when fetching objects');
+ }
+
+ $db = $this->db();
+ $res = array();
+ foreach ($this->getObjects() as $object) {
+ if ($resolved) {
+ $object = $object::fromPlainObject($object->toPlainObject(true), $db);
+ }
+
+ $res[$object->getObjectName()] = $object->toPlainObject(false, $noDefaults);
+ }
+
+ echo $this->renderJson($res, !$this->params->shift('no-pretty'));
+ }
+
+ /**
+ * @return IcingaObject[]
+ */
+ protected function getObjects()
+ {
+ if ($this->objects === null) {
+ $this->objects = IcingaObject::loadAllByType(
+ $this->getType(),
+ $this->db()
+ );
+ }
+
+ return $this->objects;
+ }
+
+ protected function getType()
+ {
+ if ($this->type === null) {
+ // Extract the command class name...
+ $className = substr(strrchr(get_class($this), '\\'), 1);
+ // ...and strip the Command extension
+ $this->type = rtrim(substr($className, 0, -7), 's');
+ }
+
+ return $this->type;
+ }
+}
diff --git a/library/Director/Cli/PluginOutputBeautifier.php b/library/Director/Cli/PluginOutputBeautifier.php
new file mode 100644
index 0000000..18a18f2
--- /dev/null
+++ b/library/Director/Cli/PluginOutputBeautifier.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Icinga\Module\Director\Cli;
+
+use Icinga\Cli\Screen;
+
+class PluginOutputBeautifier
+{
+ /** @var Screen */
+ protected $screen;
+
+ protected $isTty;
+
+ protected $colorized;
+
+ public function __construct(Screen $screen)
+ {
+ $this->screen = $screen;
+ }
+
+ public static function beautify($string, Screen $screen)
+ {
+ $self = new static($screen);
+ if ($self->isTty()) {
+ return $self->colorizeStates($string);
+ } else {
+ return $string;
+ }
+ }
+
+ protected function colorizeStates($string)
+ {
+ $string = preg_replace_callback(
+ "/'([^']+)'/",
+ [$this, 'highlightNames'],
+ $string
+ );
+
+ $string = preg_replace_callback(
+ '/(OK|WARNING|CRITICAL|UNKNOWN)/',
+ [$this, 'getColorized'],
+ $string
+ );
+
+ return $string;
+ }
+
+ protected function isTty()
+ {
+ if ($this->isTty === null) {
+ $this->isTty = function_exists('posix_isatty') && posix_isatty(STDOUT);
+ }
+
+ return $this->isTty;
+ }
+
+ protected function highlightNames($match)
+ {
+ return "'" . $this->screen->colorize($match[1], 'darkgray') . "'";
+ }
+
+ protected function getColorized($match)
+ {
+ if ($this->colorized === null) {
+ $this->colorized = [
+ 'OK' => $this->screen->colorize('OK', 'lightgreen'),
+ 'WARNING' => $this->screen->colorize('WARNING', 'yellow'),
+ 'CRITICAL' => $this->screen->colorize('CRITICAL', 'lightred'),
+ 'UNKNOWN' => $this->screen->colorize('UNKNOWN', 'lightpurple'),
+ ];
+ }
+
+ return $this->colorized[$match[1]];
+ }
+}
diff --git a/library/Director/ConfigDiff.php b/library/Director/ConfigDiff.php
new file mode 100644
index 0000000..acf5f7b
--- /dev/null
+++ b/library/Director/ConfigDiff.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+use gipfl\Diff\HtmlRenderer\InlineDiff;
+use gipfl\Diff\HtmlRenderer\SideBySideDiff;
+use gipfl\Diff\PhpDiff;
+use ipl\Html\ValidHtml;
+use InvalidArgumentException;
+
+/**
+ * @deprecated will be removed with v1.9 - please use gipfl\Diff
+ */
+class ConfigDiff implements ValidHtml
+{
+ protected $renderClass;
+
+ /** @var PhpDiff */
+ protected $phpDiff;
+
+ public function __construct($a, $b)
+ {
+ $this->phpDiff = new PhpDiff($a, $b);
+ }
+
+ public function render()
+ {
+ $class = $this->renderClass;
+ return (new $class($this->phpDiff))->render();
+ }
+
+ public function setHtmlRenderer($name)
+ {
+ switch ($name) {
+ case 'SideBySide':
+ $this->renderClass = SideBySideDiff::class;
+ break;
+ case 'Inline':
+ $this->renderClass = InlineDiff::class;
+ break;
+ default:
+ throw new InvalidArgumentException("There is no known '$name' renderer");
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/Core/CoreApi.php b/library/Director/Core/CoreApi.php
new file mode 100644
index 0000000..ea10916
--- /dev/null
+++ b/library/Director/Core/CoreApi.php
@@ -0,0 +1,940 @@
+<?php
+
+namespace Icinga\Module\Director\Core;
+
+use Exception;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Hook\DeploymentHook;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+use Icinga\Module\Director\Objects\IcingaZone;
+use Icinga\Web\Hook;
+use RuntimeException;
+
+class CoreApi implements DeploymentApiInterface
+{
+ protected $client;
+
+ protected $initialized = false;
+
+ /** @var Db */
+ protected $db;
+
+ public function __construct(RestApiClient $client)
+ {
+ $this->client = $client;
+ }
+
+ // Todo: type
+ public function setDb(Db $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getVersion()
+ {
+ return $this->parseVersion($this->getRawVersion());
+ }
+
+ public function enableWorkaroundForConnectionIssues()
+ {
+ $version = $this->getVersion();
+
+ if ($version === null ||
+ ((version_compare($version, '2.8.2', '>=') && version_compare($version, '2.10.2', '<')))
+ ) {
+ $this->client->disconnect();
+ $this->client->setKeepAlive(false);
+ }
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getRawVersion()
+ {
+ try {
+ return $this->client()->get('')->getRaw('version');
+ } catch (Exception $exception) {
+ return null;
+ }
+ }
+
+ /**
+ * @param $version
+ * @return string|null
+ */
+ protected function parseVersion($version)
+ {
+ if ($version === null) {
+ return null;
+ }
+
+ if (preg_match('/^[rv]?(\d\.\d+\.\d+)/', $version, $match)) {
+ return $match[1];
+ } else {
+ return null;
+ }
+ }
+
+ public function getObjects($pluralType, $attrs = array(), $ignorePackage = null)
+ {
+ $params = (object) [];
+ if ($ignorePackage) {
+ $params->filter = 'obj.package!="' . $ignorePackage . '"';
+ }
+
+ if (! empty($attrs)) {
+ $params->attrs = $attrs;
+ }
+
+ return $this->client()->get(
+ 'objects/' . urlencode(strtolower($pluralType)),
+ $params
+ )->getResult('name');
+ }
+
+ public function onEvent($callback, $raw = false)
+ {
+ $this->client->onEvent($callback, $raw);
+
+ return $this;
+ }
+
+ public function getObject($name, $pluraltype, $attrs = array())
+ {
+ $params = (object) array(
+ );
+
+ if (! empty($attrs)) {
+ $params->attrs = $attrs;
+ }
+ $url = 'objects/' . urlencode(strtolower($pluraltype)) . '/' . rawurlencode($name) . '?all_joins=1';
+ $res = $this->client()->get($url, $params)->getResult('name');
+
+ // TODO: check key, throw
+ return $res[$name];
+ }
+
+ /**
+ * Get a PKI ticket for CSR auto-signing
+ *
+ * @param string $cn The host’s common name for which the ticket should be generated
+ *
+ * @return string|null
+ */
+ public function getTicket($cn)
+ {
+ $r = $this->client()->post(
+ 'actions/generate-ticket',
+ ['cn' => $cn]
+ );
+ if (! $r->succeeded()) {
+ throw new RuntimeException($r->getErrorMessage());
+ }
+
+ $ticket = $r->getRaw('ticket');
+ if ($ticket === null) {
+ // RestApiResponse::succeeded() returns true if Icinga 2 reports an error in the results key, e.g.
+ // {
+ // "results": [
+ // {
+ // "code": 500.0,
+ // "status": "Ticket salt is not configured in ApiListener object"
+ // }
+ // ]
+ // }
+ throw new RuntimeException($r->getRaw('status', 'Ticket is empty'));
+ }
+
+ return $ticket;
+ }
+
+ public function checkHostNow($host)
+ {
+ $filter = 'host.name == "' . $host . '"';
+
+ return $this->client()->post(
+ 'actions/reschedule-check?filter=' . rawurlencode($filter),
+ (object) array(
+ 'type' => 'Host'
+ )
+ );
+ }
+
+ public function checkServiceNow($host, $service)
+ {
+ $filter = 'host.name == "' . $host . '" && service.name == "' . $service . '"';
+ $this->client()->post(
+ 'actions/reschedule-check?filter=' . rawurlencode($filter),
+ (object) array(
+ 'type' => 'Service'
+ )
+ );
+ }
+
+ public function acknowledgeHostProblem($host, $author, $comment)
+ {
+ $filter = 'host.name == "' . $host . '"';
+ return $this->client()->post(
+ 'actions/acknowledge-problem?type=Host&filter=' . rawurlencode($filter),
+ (object) array(
+ 'author' => $author,
+ 'comment' => $comment
+ )
+ );
+ }
+
+ public function removeHostAcknowledgement($host)
+ {
+ $filter = 'host.name == "' . $host . '"';
+ return $this->client()->post(
+ 'actions/remove-acknowledgement?type=Host&filter=' . rawurlencode($filter)
+ );
+ }
+
+ public function reloadNow()
+ {
+ try {
+ $this->client()->post('actions/restart-process');
+
+ return true;
+ } catch (Exception $e) {
+ return $e->getMessage();
+ }
+ }
+
+ public function getHostOutput($host)
+ {
+ try {
+ $object = $this->getObject($host, 'hosts');
+ } catch (Exception $e) {
+ return 'Unable to fetch the requested object';
+ }
+ if (isset($object->attrs->last_check_result)) {
+ return $object->attrs->last_check_result->output;
+ } else {
+ return '(no check result available)';
+ }
+ }
+
+ public function checkHostAndWaitForResult($host, $timeout = 10)
+ {
+ $object = $this->getObject($host, 'hosts');
+ if (isset($object->attrs->last_check_result)) {
+ $oldOutput = $object->attrs->last_check_result->output;
+ } else {
+ $oldOutput = '';
+ }
+
+ $now = microtime(true);
+ $this->checkHostNow($host);
+
+ while (true) {
+ try {
+ $object = $this->getObject($host, 'hosts');
+ if (isset($object->attrs->last_check_result)) {
+ $res = $object->attrs->last_check_result;
+ if ($res->execution_start > $now || $res->output !== $oldOutput) {
+ return $res;
+ }
+ } else {
+ // no check result available
+ }
+ } catch (Exception $e) {
+ // Unable to fetch the requested object
+ throw new RuntimeException(sprintf(
+ 'Unable to fetch the requested host "%s"',
+ $host
+ ));
+ }
+ if (microtime(true) > ($now + $timeout)) {
+ break;
+ }
+
+ usleep(50000);
+ }
+
+ return false;
+ }
+
+ public function checkServiceAndWaitForResult($host, $service, $timeout = 10)
+ {
+ $now = microtime(true);
+ $this->checkServiceNow($host, $service);
+
+ while (true) {
+ try {
+ $object = $this->getObject("$host!$service", 'services');
+ if (isset($object->attrs->last_check_result)) {
+ $res = $object->attrs->last_check_result;
+ if ($res->execution_start > $now) {
+ return $res;
+ }
+ } else {
+ // no check result available
+ }
+ } catch (Exception $e) {
+ // Unable to fetch the requested object
+ throw new RuntimeException(sprintf(
+ 'Unable to fetch the requested service "%s" on "%s"',
+ $service,
+ $host
+ ));
+ }
+ if (microtime(true) > ($now + $timeout)) {
+ break;
+ }
+
+ usleep(150000);
+ }
+
+ return false;
+ }
+
+ public function getServiceOutput($host, $service)
+ {
+ try {
+ $object = $this->getObject($host . '!' . $service, 'services');
+ } catch (\Exception $e) {
+ return 'Unable to fetch the requested object';
+ }
+ if (isset($object->attrs->last_check_result)) {
+ return $object->attrs->last_check_result->output;
+ } else {
+ return '(no check result available)';
+ }
+ }
+
+ public function supportsRuntimeCreationFor(IcingaObject $object)
+ {
+ $valid = array('host');
+ return in_array($object->getShortTableName(), $valid);
+ }
+
+ protected function assertRuntimeCreationSupportFor(IcingaObject $object)
+ {
+ if (!$this->supportsRuntimeCreationFor($object)) {
+ throw new RuntimeException(sprintf(
+ 'Object creation at runtime is not supported for "%s"',
+ $object->getShortTableName()
+ ));
+ }
+ }
+
+ // Note: this is for testing purposes only, NOT production-ready
+ public function createObjectAtRuntime(IcingaObject $object)
+ {
+ $this->assertRuntimeCreationSupportFor($object);
+
+ $key = $object->getShortTableName();
+
+ $command = sprintf(
+ "f = function() {\n"
+ . ' existing = get_%s("%s")'
+ . "\n if (existing) { return false }"
+ . "\n%s\n}\nInternal.run_with_activation_context(f)\n",
+ $key,
+ $object->get('object_name'),
+ (string) $object
+ );
+
+ return $this->runConsoleCommand($command)->getSingleResult();
+ }
+
+ public function getConstants()
+ {
+ $constants = array();
+ $command = 'var constants = [];
+for (k => v in globals) {
+ if (typeof(v) in [String, Number, Boolean]) {
+ res = { name = k, value = v }
+ constants.add({name = k, value = v})
+ }
+};
+constants
+';
+
+ foreach ($this->runConsoleCommand($command)->getSingleResult() as $row) {
+ $constants[$row->name] = $row->value;
+ }
+
+ return $constants;
+ }
+
+ public function runConsoleCommand($command)
+ {
+ return $this->client()->post(
+ 'console/execute-script',
+ array('command' => $command)
+ );
+ }
+
+ public function getConstant($name)
+ {
+ $constants = $this->getConstants();
+ if (array_key_exists($name, $constants)) {
+ return $constants[$name];
+ }
+
+ return null;
+ }
+
+ public function getTypes()
+ {
+ return $this->client()->get('types')->getResult('name');
+ }
+
+ public function getType($type)
+ {
+ $res = $this->client()->get('types', array('name' => $type))->getResult('name');
+ return $res[$type]; // TODO: error checking
+ }
+
+ public function getStatus()
+ {
+ return $this->client()->get('status')->getResult('name');
+ }
+
+ public function listObjects($type, $pluralType)
+ {
+ // TODO: more abstraction needed
+ // TODO: autofetch and cache pluraltypes
+ try {
+ $result = $this->client()->get(
+ 'objects/' . $pluralType,
+ array(
+ 'attrs' => array('__name')
+ )
+ )->getResult('name');
+ } catch (NotFoundError $e) {
+ $result = [];
+ }
+
+ return array_keys($result);
+ }
+
+ public function getPackages()
+ {
+ return $this->client()->get('config/packages')->getResult('name');
+ }
+
+ public function getActiveStageName()
+ {
+ return current($this->listPackageStages($this->getPackageName(), true));
+ }
+
+ protected function getPackageName()
+ {
+ return $this->db->settings()->get('icinga_package_name');
+ }
+
+ public function getActiveChecksum(Db $conn)
+ {
+ $db = $conn->getDbAdapter();
+ $stage = $this->getActiveStageName();
+ if (! $stage) {
+ return null;
+ }
+
+ $query = $db->select()->from(
+ array('l' => 'director_deployment_log'),
+ array('checksum' => $conn->dbHexFunc('l.config_checksum'))
+ )->where('l.stage_name = ?', $stage);
+
+ return $db->fetchOne($query);
+ }
+
+ protected function getDirectorObjects($type, $plural, $map)
+ {
+ $attrs = array_merge(
+ array_keys($map),
+ array('package', 'templates', 'active')
+ );
+
+ $objects = array();
+ $result = $this->getObjects($plural, $attrs, $this->getPackageName());
+ foreach ($result as $name => $row) {
+ $attrs = $row->attrs;
+
+ $properties = array(
+ 'object_name' => $name,
+ 'object_type' => 'external_object'
+ );
+
+ foreach ($map as $key => $prop) {
+ if (property_exists($attrs, $key)) {
+ $properties[$prop] = $attrs->$key;
+ }
+ }
+
+ $objects[$name] = IcingaObject::createByType($type, $properties, $this->db);
+ }
+
+ return $objects;
+ }
+
+ /**
+ * @return IcingaZone[]
+ */
+ public function getZoneObjects()
+ {
+ return $this->getDirectorObjects('Zone', 'zones', [
+ 'parent' => 'parent',
+ 'global' => 'is_global',
+ ]);
+ }
+
+ public function getUserObjects()
+ {
+ return $this->getDirectorObjects('User', 'users', [
+ 'display_name' => 'display_name',
+ 'email' => 'email',
+ 'groups' => 'groups',
+ 'vars' => 'vars',
+ ]);
+ }
+
+ protected function buildEndpointZoneMap()
+ {
+ $zones = $this->getObjects('zones', ['endpoints'], $this->getPackageName());
+ $zoneMap = array();
+
+ foreach ($zones as $name => $zone) {
+ if (! is_array($zone->attrs->endpoints)) {
+ continue;
+ }
+ foreach ($zone->attrs->endpoints as $endpoint) {
+ $zoneMap[$endpoint] = $name;
+ }
+ }
+
+ return $zoneMap;
+ }
+
+ public function getEndpointObjects()
+ {
+ $zoneMap = $this->buildEndpointZoneMap();
+ $objects = $this->getDirectorObjects('Endpoint', 'endpoints', [
+ 'host' => 'host',
+ 'port' => 'port',
+ 'log_duration' => 'log_duration',
+ ]);
+
+ foreach ($objects as $object) {
+ $name = $object->object_name;
+ if (array_key_exists($name, $zoneMap)) {
+ $object->zone = $zoneMap[$name];
+ }
+ }
+
+ return $objects;
+ }
+
+ public function getHostObjects()
+ {
+ $params = [
+ 'display_name' => 'display_name',
+ 'address' => 'address',
+ 'address6' => 'address6',
+ 'templates' => 'imports',
+ 'groups' => 'groups',
+ 'vars' => 'vars',
+ 'check_command' => 'check_command',
+ 'max_check_attempts' => 'max_check_attempts',
+ 'check_period' => 'check_period',
+ 'check_interval' => 'check_interval',
+ 'retry_interval' => 'retry_interval',
+ 'enable_notifications' => 'enable_notifications',
+ 'enable_active_checks' => 'enable_active_checks',
+ 'enable_passive_checks' => 'enable_passive_checks',
+ 'enable_event_handler' => 'enable_event_handler',
+ 'enable_flapping' => 'enable_flapping',
+ 'enable_perfdata' => 'enable_perfdata',
+ 'event_command' => 'event_command',
+ 'volatile' => 'volatile',
+ 'zone' => 'zone',
+ 'command_endpoint' => 'command_endpoint',
+ 'notes' => 'notes',
+ 'notes_url' => 'notes_url',
+ 'action_url' => 'action_url',
+ 'icon_image' => 'icon_image',
+ 'icon_image_alt' => 'icon_image_alt',
+ ];
+
+ if (version_compare($this->getVersion(), '2.8.0', '>=')) {
+ $params['flapping_threshold_high'] = 'flapping_threshold_high';
+ $params['flapping_threshold_low'] = 'flapping_threshold_low';
+ }
+
+ return $this->getDirectorObjects('Host', 'hosts', $params);
+ }
+
+ public function getHostGroupObjects()
+ {
+ return $this->getDirectorObjects('HostGroup', 'hostgroups', [
+ 'display_name' => 'display_name',
+ ]);
+ }
+
+ public function getUserGroupObjects()
+ {
+ return $this->getDirectorObjects('UserGroup', 'usergroups', [
+ 'display_name' => 'display_name',
+ ]);
+ }
+
+ /**
+ * @return IcingaCommand[]
+ */
+ public function getCheckCommandObjects()
+ {
+ return $this->getSpecificCommandObjects('Check');
+ }
+
+ /**
+ * @return IcingaCommand[]
+ */
+ public function getNotificationCommandObjects()
+ {
+ return $this->getSpecificCommandObjects('Notification');
+ }
+
+ /**
+ * @return IcingaCommand[]
+ */
+ public function getEventCommandObjects()
+ {
+ return $this->getSpecificCommandObjects('Event');
+ }
+
+ /**
+ * @return IcingaCommand[]
+ */
+ public function getSpecificCommandObjects($type)
+ {
+ IcingaCommand::setPluginDir($this->getConstant('PluginDir'));
+
+ $objects = $this->getDirectorObjects('Command', "${type}Commands", [
+ 'arguments' => 'arguments',
+ // 'env' => 'env',
+ 'timeout' => 'timeout',
+ 'command' => 'command',
+ 'vars' => 'vars',
+ ]);
+ foreach ($objects as $obj) {
+ $obj->methods_execute = "Plugin$type";
+ }
+
+ return $objects;
+ }
+
+ public function listPackageStages($name, $active = null)
+ {
+ $packages = $this->getPackages();
+ $found = array();
+
+ if (array_key_exists($name, $packages)) {
+ $package = $packages[$name];
+ $current = $package->{'active-stage'};
+ foreach ($package->stages as $stage) {
+ if ($active === null) {
+ $found[] = $stage;
+ } elseif ($active === true) {
+ if ($current === $stage) {
+ $found[] = $stage;
+ }
+ } elseif ($active === false) {
+ if ($current !== $stage) {
+ $found[] = $stage;
+ }
+ }
+ }
+ }
+
+ return $found;
+ }
+
+ public function collectLogFiles(Db $db)
+ {
+ $existing = $this->listPackageStages($this->getPackageName());
+ $missing = [];
+ $empty = [];
+ foreach (DirectorDeploymentLog::getUncollected($db) as $deployment) {
+ $stage = $deployment->get('stage_name');
+ if (! in_array($stage, $existing)) {
+ $missing[] = $deployment;
+ continue;
+ }
+
+ try {
+ $availableFiles = $this->listStageFiles($stage);
+ } catch (Exception $e) {
+ // Could not collect stage files. Doesn't matter, let's try next time
+ continue;
+ }
+
+ if (in_array('startup.log', $availableFiles)
+ && in_array('status', $availableFiles)
+ ) {
+ if ($this->getStagedFile($stage, 'status') === '0') {
+ $deployment->set('startup_succeeded', 'y');
+ } else {
+ $deployment->set('startup_succeeded', 'n');
+ }
+ $deployment->set('startup_log', $this->shortenStartupLog(
+ $this->getStagedFile($stage, 'startup.log')
+ ));
+ } else {
+ // Stage seems to be incomplete, let's try again next time
+ $empty[] = $deployment;
+ continue;
+ }
+ $deployment->set('stage_collected', 'y');
+
+ $deployment->store();
+
+ /** @var DeploymentHook[] $hooks */
+ $hooks = Hook::all('director/Deployment');
+ foreach ($hooks as $hook) {
+ $hook->onCollect($deployment);
+ }
+ }
+
+ foreach ($missing as $deployment) {
+ $deployment->set('stage_collected', 'n');
+ $deployment->store();
+ }
+
+ $running = DirectorDeploymentLog::getRelatedToActiveStage($this, $db);
+ if ($running !== null) {
+ foreach ($empty as $deployment) {
+ if ($deployment->getDeploymentTimestamp() < $running->getDeploymentTimestamp()) {
+ $deployment->set('stage_collected', 'n');
+ $deployment->store();
+ $this->deleteStage($this->getPackageName(), $deployment->get('stage_name'));
+ }
+ }
+ }
+ }
+
+ public function wipeInactiveStages(Db $db)
+ {
+ $uncollected = DirectorDeploymentLog::getUncollected($db);
+ $packageName = $this->getPackageName();
+ foreach ($this->listPackageStages($packageName, false) as $stage) {
+ if (array_key_exists($stage, $uncollected)) {
+ continue;
+ }
+ $this->client()->delete($this->prepareStageUrl($packageName, $stage));
+ }
+ }
+
+ public function listStageFiles($stage, $packageName = null)
+ {
+ if ($packageName === null) {
+ $packageName = $this->getPackageName();
+ }
+ return array_keys(
+ $this->client()
+ ->get($this->prepareStageUrl($packageName, $stage))
+ ->getResult('name', ['type' => 'file'])
+ );
+ }
+
+ public function getStagedFile($stage, $file, $packageName = null)
+ {
+ if ($packageName === null) {
+ $packageName = $this->getPackageName();
+ }
+ return $this->client()
+ ->getRaw($this->prepareFileUrl($packageName, $stage, $file));
+ }
+
+ public function hasPackage($name)
+ {
+ $modules = $this->getPackages();
+ return \array_key_exists($name, $modules);
+ }
+
+ public function createPackage($name)
+ {
+ return $this->client()->post($this->preparePackageUrl($name))->succeeded();
+ }
+
+ public function deletePackage($name)
+ {
+ return $this->client()->delete($this->preparePackageUrl($name))->succeeded();
+ }
+
+ public function assertPackageExists($name)
+ {
+ if (! $this->hasPackage($name)) {
+ if (! $this->createPackage($name)) {
+ throw new RuntimeException(sprintf(
+ 'Failed to create the package "%s" through the REST API',
+ $name
+ ));
+ }
+ }
+
+ return $this;
+ }
+
+ public function deleteStage($packageName, $stageName)
+ {
+ $this->client()->delete(
+ $this->prepareStageUrl($packageName, $stageName)
+ )->succeeded();
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function stream()
+ {
+ $allTypes = array(
+ 'CheckResult',
+ 'StateChange',
+ 'Notification',
+ 'AcknowledgementSet',
+ 'AcknowledgementCleared',
+ 'CommentAdded',
+ 'CommentRemoved',
+ 'DowntimeAdded',
+ 'DowntimeRemoved',
+ 'DowntimeTriggered'
+ );
+
+ $queue = 'director-rand';
+
+ $url = sprintf('events?queue=%s&types=%s', $queue, implode('&types=', $allTypes));
+
+ $this->client()->request('post', $url, null, false, true);
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @param Db $db
+ * @param null $packageName
+ * @return DirectorDeploymentLog
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function dumpConfig(IcingaConfig $config, Db $db, $packageName = null)
+ {
+ if ($packageName === null) {
+ $packageName = $db->settings()->get('icinga_package_name');
+ }
+ $start = microtime(true);
+ /** @var DirectorDeploymentLog $deployment */
+ $deployment = DirectorDeploymentLog::create(array(
+ // 'config_id' => $config->id,
+ // 'peer_identity' => $endpoint->object_name,
+ 'peer_identity' => $this->client->getPeerIdentity(),
+ 'start_time' => date('Y-m-d H:i:s'),
+ 'config_checksum' => $config->getChecksum(),
+ 'last_activity_checksum' => $config->getLastActivityChecksum()
+ // 'triggered_by' => Util::getUsername(),
+ // 'username' => Util::getUsername(),
+ // 'module_name' => $moduleName,
+ ));
+
+ /** @var DeploymentHook[] $hooks */
+ $hooks = Hook::all('director/Deployment');
+ foreach ($hooks as $hook) {
+ $hook->beforeDeploy($deployment);
+ }
+
+ $this->assertPackageExists($packageName);
+
+ $response = $this->client()->post('config/stages/' . \rawurlencode($packageName), [
+ 'files' => $config->getFileContents()
+ ]);
+
+ $duration = (int) ((microtime(true) - $start) * 1000);
+ // $deployment->duration_ms = $duration;
+ $deployment->set('duration_dump', $duration);
+
+ $succeeded = 'n';
+ if ($response->succeeded()) {
+ if ($stage = $response->getResult('stage', ['package' => $packageName])) { // Status?
+ $deployment->set('stage_name', key($stage));
+ $succeeded = 'y';
+ }
+ }
+ $deployment->set('dump_succeeded', $succeeded);
+ $deployment->store($db);
+
+ if ($succeeded === 'y') {
+ foreach ($hooks as $hook) {
+ $hook->triggerSuccessfulDump($deployment);
+ }
+ }
+
+ return $deployment;
+ }
+
+ protected function shortenStartupLog($log)
+ {
+ $logLen = strlen($log);
+ if ($logLen < 1024 * 60) {
+ return $log;
+ }
+
+ $part = substr($log, 0, 1024 * 20);
+ $parts = explode("\n", $part);
+ array_pop($parts);
+ $begin = implode("\n", $parts) . "\n\n";
+
+ $part = substr($log, -1024 * 20);
+ $parts = explode("\n", $part);
+ array_shift($parts);
+ $end = "\n\n" . implode("\n", $parts);
+
+ return $begin . sprintf(
+ '[..] %d bytes removed by Director [..]',
+ $logLen - (strlen($begin) + strlen($end))
+ ) . $end;
+ }
+
+ protected function preparePackageUrl($packageName)
+ {
+ return 'config/packages/' . \rawurlencode($packageName);
+ }
+
+ protected function prepareStageUrl($packageName, $stage)
+ {
+ return \sprintf(
+ 'config/stages/%s/%s',
+ \rawurlencode($packageName),
+ \rawurlencode($stage)
+ );
+ }
+
+ protected function prepareFileUrl($packageName, $stage, $file)
+ {
+ return \sprintf(
+ 'config/files/%s/%s/%s',
+ \rawurlencode($packageName),
+ \rawurlencode($stage),
+ \rawurlencode($file)
+ );
+ }
+
+ protected function client()
+ {
+ if ($this->initialized === false) {
+ $this->initialized = true;
+ $this->enableWorkaroundForConnectionIssues();
+ }
+
+ return $this->client;
+ }
+}
diff --git a/library/Director/Core/DeploymentApiInterface.php b/library/Director/Core/DeploymentApiInterface.php
new file mode 100644
index 0000000..026f0fd
--- /dev/null
+++ b/library/Director/Core/DeploymentApiInterface.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Icinga\Module\Director\Core;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+
+/**
+ * Interface to a deployment API of the monitoring configuration
+ *
+ * @package Icinga\Module\Director\Core
+ */
+interface DeploymentApiInterface
+{
+ /**
+ * Collecting log files from the deployment system
+ * and write them into the database.
+ *
+ * @param Db $db
+ */
+ public function collectLogFiles(Db $db);
+
+ /**
+ * Cleanup old stages that are collected and not active
+ *
+ * @param Db $db
+ */
+ public function wipeInactiveStages(Db $db);
+
+ /**
+ * Returns the active configuration stage
+ *
+ * @return string
+ */
+ public function getActiveStageName();
+
+ /**
+ * List files in a named stage
+ *
+ * @param string $stage name of the stage
+ * @return string[]
+ */
+ public function listStageFiles($stage);
+
+ /**
+ * Retrieve a raw file from the named stage
+ *
+ * @param string $stage Stage name
+ * @param string $file Relative file path
+ *
+ * @return string
+ */
+ public function getStagedFile($stage, $file);
+
+ /**
+ * Explicitly delete a stage
+ *
+ * @param string $packageName
+ * @param string $stageName
+ *
+ * @return bool
+ */
+ public function deleteStage($packageName, $stageName);
+
+ /**
+ * Deploy the config and activate it
+ *
+ * @param IcingaConfig $config
+ * @param Db $db
+ * @param string $packageName
+ *
+ * @return mixed
+ */
+ public function dumpConfig(IcingaConfig $config, Db $db, $packageName = null);
+}
diff --git a/library/Director/Core/Json.php b/library/Director/Core/Json.php
new file mode 100644
index 0000000..507349c
--- /dev/null
+++ b/library/Director/Core/Json.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\Core;
+
+use Icinga\Module\Director\Exception\JsonEncodeException;
+
+class Json
+{
+ public static function encode($mixed, $flags = null)
+ {
+ if ($flags === null) {
+ $result = \json_encode($mixed);
+ } else {
+ $result = \json_encode($mixed, $flags);
+ }
+
+ if ($result === false && json_last_error() !== JSON_ERROR_NONE) {
+ throw JsonEncodeException::forLastJsonError();
+ }
+
+ return $result;
+ }
+
+ public static function decode($string)
+ {
+ $result = \json_decode($string);
+
+ if ($result === null && json_last_error() !== JSON_ERROR_NONE) {
+ throw JsonEncodeException::forLastJsonError();
+ }
+
+ return $result;
+ }
+}
diff --git a/library/Director/Core/LegacyDeploymentApi.php b/library/Director/Core/LegacyDeploymentApi.php
new file mode 100644
index 0000000..7287c4a
--- /dev/null
+++ b/library/Director/Core/LegacyDeploymentApi.php
@@ -0,0 +1,466 @@
+<?php
+
+namespace Icinga\Module\Director\Core;
+
+use Exception;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+
+/**
+ * Legacy DeploymentApi for Icinga 1.x configuration deployment
+ *
+ * @package Icinga\Module\Director\Core
+ */
+class LegacyDeploymentApi implements DeploymentApiInterface
+{
+ protected $db;
+ protected $deploymentPath;
+ protected $activationScript;
+
+ protected $dir_mode;
+ protected $file_mode;
+
+ public function __construct(Db $db)
+ {
+ $this->db = $db;
+ $settings = $this->db->settings();
+ $this->deploymentPath = $settings->deployment_path_v1;
+ $this->activationScript = $settings->activation_script_v1;
+
+ $this->dir_mode = base_convert($settings->get('deployment_file_mode_v1', '2775'), 8, 10);
+ $this->file_mode = base_convert($settings->get('deployment_dir_mode_v1', '0664'), 8, 10);
+ }
+
+ /**
+ * TODO: merge in common class
+ * @inheritdoc
+ */
+ public function collectLogFiles(Db $db)
+ {
+ $packageName = $db->settings()->get('icinga_package_name');
+ $existing = $this->listPackageStages($packageName);
+
+ foreach (DirectorDeploymentLog::getUncollected($db) as $deployment) {
+ $stage = $deployment->get('stage_name');
+ if (! in_array($stage, $existing)) {
+ continue;
+ }
+
+ try {
+ $availableFiles = $this->listStageFiles($stage);
+ } catch (Exception $e) {
+ // Could not collect stage files. Doesn't matter, let's try next time
+ continue;
+ }
+
+ if (in_array('startup.log', $availableFiles)
+ && in_array('status', $availableFiles)
+ ) {
+ $status = $this->getStagedFile($stage, 'status');
+ $status = trim($status);
+ if ($status === '0') {
+ $deployment->set('startup_succeeded', 'y');
+ } else {
+ $deployment->set('startup_succeeded', 'n');
+ }
+ $deployment->set('startup_log', $this->shortenStartupLog(
+ $this->getStagedFile($stage, 'startup.log')
+ ));
+ } else {
+ // Stage seems to be incomplete, let's try again next time
+ continue;
+ }
+ $deployment->set('stage_collected', 'y');
+
+ $deployment->store();
+ }
+ }
+
+ /**
+ * TODO: merge in common class
+ * @inheritdoc
+ */
+ public function wipeInactiveStages(Db $db)
+ {
+ $uncollected = DirectorDeploymentLog::getUncollected($db);
+ $packageName = $db->settings()->get('icinga_package_name');
+ $currentStage = $this->getActiveStageName();
+
+ // try to expire old deployments
+ foreach ($uncollected as $name => $deployment) {
+ /** @var DirectorDeploymentLog $deployment */
+ if ($deployment->get('dump_succeeded') === 'n'
+ || $deployment->get('startup_succeeded') === null
+ ) {
+ $start_time = strtotime($deployment->start_time);
+
+ // older than an hour and no startup
+ if ($start_time + 3600 < time()) {
+ $deployment->set('startup_succeeded', 'n');
+ $deployment->set('startup_log', 'Activation timed out...');
+ $deployment->store();
+ }
+ }
+ }
+
+ foreach ($this->listPackageStages($packageName) as $stage) {
+ if (array_key_exists($stage, $uncollected)
+ && $uncollected[$stage]->get('startup_succeeded') === null
+ ) {
+ continue;
+ } elseif ($stage === $currentStage) {
+ continue;
+ } else {
+ $this->deleteStage($packageName, $stage);
+ }
+ }
+ }
+
+ /** @inheritdoc */
+ public function getActiveStageName()
+ {
+ $this->assertDeploymentPath();
+
+ $path = $this->deploymentPath . DIRECTORY_SEPARATOR . 'active';
+
+ if (file_exists($path)) {
+ if (is_link($path)) {
+ $linkTarget = readlink($path);
+ $linkTargetDir = dirname($linkTarget);
+ $linkTargetName = basename($linkTarget);
+
+ if ($linkTargetDir === $this->deploymentPath || $linkTargetDir === '.') {
+ return $linkTargetName;
+ } else {
+ throw new IcingaException(
+ 'Active stage link pointing to a invalid target: %s -> %s',
+ $path,
+ $linkTarget
+ );
+ }
+ } else {
+ throw new IcingaException('Active stage is not a symlink: %s', $path);
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /** @inheritdoc */
+ public function listStageFiles($stage)
+ {
+ $path = $this->getStagePath($stage);
+ if (! is_dir($path)) {
+ throw new IcingaException('Deployment stage "%s" does not exist at: %s', $stage, $path);
+ }
+ return $this->listDirectoryContents($path);
+ }
+
+ /** @inheritdoc */
+ public function listPackageStages($packageName)
+ {
+ $this->assertPackageName($packageName);
+ $this->assertDeploymentPath();
+
+ $dh = @opendir($this->deploymentPath);
+ if ($dh === null) {
+ throw new IcingaException('Can not list contents of %s', $this->deploymentPath);
+ }
+
+ $stages = array();
+ while ($file = readdir($dh)) {
+ if ($file === '.' || $file === '..') {
+ continue;
+ } elseif (is_dir($this->deploymentPath . DIRECTORY_SEPARATOR . $file)
+ && substr($file, 0, 9) === 'director-'
+ ) {
+ $stages[] = $file;
+ }
+ }
+
+ return $stages;
+ }
+
+ /** @inheritdoc */
+ public function getStagedFile($stage, $file)
+ {
+ $path = $this->getStagePath($stage);
+
+ $filePath = $path . DIRECTORY_SEPARATOR . $file;
+
+ if (! file_exists($filePath)) {
+ throw new IcingaException('Could not find file %s', $filePath);
+ } else {
+ return file_get_contents($filePath);
+ }
+ }
+
+ /** @inheritdoc */
+ public function deleteStage($packageName, $stageName)
+ {
+ $this->assertPackageName($packageName);
+ $this->assertDeploymentPath();
+
+ $path = $this->getStagePath($stageName);
+
+ static::rrmdir($path);
+ }
+
+ /** @inheritdoc */
+ public function dumpConfig(IcingaConfig $config, Db $db, $packageName = null)
+ {
+ if ($packageName === null) {
+ $packageName = $db->settings()->get('icinga_package_name');
+ }
+ $this->assertPackageName($packageName);
+ $this->assertDeploymentPath();
+
+ $start = microtime(true);
+ $deployment = DirectorDeploymentLog::create(array(
+ // 'config_id' => $config->id,
+ // 'peer_identity' => $endpoint->object_name,
+ 'peer_identity' => $this->deploymentPath,
+ 'start_time' => date('Y-m-d H:i:s'),
+ 'config_checksum' => $config->getChecksum(),
+ 'last_activity_checksum' => $config->getLastActivityChecksum()
+ // 'triggered_by' => Util::getUsername(),
+ // 'username' => Util::getUsername(),
+ // 'module_name' => $moduleName,
+ ));
+
+ $stage_name = 'director-' .date('Ymd-His');
+ $deployment->set('stage_name', $stage_name);
+
+ try {
+ $succeeded = $this->deployStage($stage_name, $config->getFileContents());
+ if ($succeeded === true) {
+ $succeeded = $this->activateStage($stage_name);
+ }
+ } catch (Exception $e) {
+ $deployment->set('dump_succeeded', 'n');
+ $deployment->set('startup_log', $e->getMessage());
+ $deployment->set('startup_succeeded', 'n');
+ $deployment->store($db);
+ throw $e;
+ }
+
+ $duration = (int) ((microtime(true) - $start) * 1000);
+ $deployment->set('duration_dump', $duration);
+
+ $deployment->set('dump_succeeded', $succeeded === true ? 'y' : 'n');
+
+ $deployment->store($db);
+ return $succeeded;
+ }
+
+ /**
+ * Deploy a new stage, and write all files to it
+ *
+ * @param string $stage Name of the stage
+ * @param array $files Array of files, $fileName => $content
+ *
+ * @return bool Success status
+ *
+ * @throws IcingaException When something could not be accessed
+ */
+ protected function deployStage($stage, $files)
+ {
+ $path = $this->deploymentPath . DIRECTORY_SEPARATOR . $stage;
+
+ if (file_exists($path)) {
+ throw new IcingaException('Stage "%s" does already exist at: ', $stage, $path);
+ } else {
+ $this->mkdir($path);
+
+ foreach ($files as $file => $content) {
+ $fullPath = $path . DIRECTORY_SEPARATOR . $file;
+ $this->mkdir(dirname($fullPath), true);
+
+ $fh = @fopen($fullPath, 'w');
+ if ($fh === null) {
+ throw new IcingaException('Could not open file "%s" for writing.', $fullPath);
+ }
+ chmod($fullPath, $this->file_mode);
+
+ fwrite($fh, $content);
+ fclose($fh);
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * Starts activation of
+ *
+ * Note: script should probably fork to background?
+ *
+ * @param string $stage Stage to activate
+ *
+ * @return bool
+ *
+ * @throws IcingaException For an execution error
+ */
+ protected function activateStage($stage)
+ {
+ if ($this->activationScript === null || trim($this->activationScript) === '') {
+ // skip activation, could be done by external cron worker
+ return true;
+ } else {
+ $command = sprintf('%s %s 2>&1', escapeshellcmd($this->activationScript), escapeshellarg($stage));
+ $output = null;
+ $rc = null;
+ exec($command, $output, $rc);
+ $output = join("\n", $output);
+ if ($rc !== 0) {
+ throw new IcingaException("Activation script did exit with return code %d:\n\n%s", $rc, $output);
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Recursively dump directory contents, with relative path
+ *
+ * @param string $path Absolute path to read from
+ * @param int $depth Internal counter
+ *
+ * @return string[]
+ *
+ * @throws IcingaException When directory could not be opened
+ */
+ protected function listDirectoryContents($path, $depth = 0)
+ {
+ $dh = @opendir($path);
+ if ($dh === null) {
+ throw new IcingaException('Can not list contents of %s', $path);
+ }
+
+ $files = array();
+ while ($file = readdir($dh)) {
+ $fullPath = $path . DIRECTORY_SEPARATOR . $file;
+ if ($file === '.' || $file === '..') {
+ continue;
+ } elseif (is_dir($fullPath)) {
+ $subdirFiles = $this->listDirectoryContents($fullPath, $depth + 1);
+ foreach ($subdirFiles as $subFile) {
+ $files[] = $file . DIRECTORY_SEPARATOR . $subFile;
+ }
+ } else {
+ $files[] = $file;
+ }
+ }
+
+ if ($depth === 0) {
+ sort($files);
+ }
+
+ return $files;
+ }
+
+ /**
+ * Assert that only the director module is interacted with
+ *
+ * @param string $packageName
+ * @throws IcingaException When another module is requested
+ */
+ protected function assertPackageName($packageName)
+ {
+ if ($packageName !== 'director') {
+ throw new IcingaException('Does not supported different modules!');
+ }
+ }
+
+ /**
+ * Assert the deployment path to be configured, existing, and writeable
+ *
+ * @throws IcingaException
+ */
+ protected function assertDeploymentPath()
+ {
+ if ($this->deploymentPath === null) {
+ throw new IcingaException('Deployment path is not configured for legacy config!');
+ } elseif (! is_dir($this->deploymentPath)) {
+ throw new IcingaException('Deployment path is not a directory: %s', $this->deploymentPath);
+ } elseif (! is_writeable($this->deploymentPath)) {
+ throw new IcingaException('Deployment path is not a writeable: %s', $this->deploymentPath);
+ }
+ }
+
+ /**
+ * TODO: avoid code duplication: copied from CoreApi
+ *
+ * @param string $log The log contents to shorten
+ * @return string
+ */
+ protected function shortenStartupLog($log)
+ {
+ $logLen = strlen($log);
+ if ($logLen < 1024 * 60) {
+ return $log;
+ }
+
+ $part = substr($log, 0, 1024 * 20);
+ $parts = explode("\n", $part);
+ array_pop($parts);
+ $begin = implode("\n", $parts) . "\n\n";
+
+ $part = substr($log, -1024 * 20);
+ $parts = explode("\n", $part);
+ array_shift($parts);
+ $end = "\n\n" . implode("\n", $parts);
+
+ return $begin . sprintf(
+ '[..] %d bytes removed by Director [..]',
+ $logLen - (strlen($begin) + strlen($end))
+ ) . $end;
+ }
+
+ /**
+ * Return the full path of a stage
+ *
+ * @param string $stage Name of the stage
+ *
+ * @return string
+ */
+ public function getStagePath($stage)
+ {
+ $this->assertDeploymentPath();
+ return $this->deploymentPath . DIRECTORY_SEPARATOR . $stage;
+ }
+
+ /**
+ * @from https://php.net/manual/de/function.rmdir.php#108113
+ * @param $dir
+ */
+ protected static function rrmdir($dir)
+ {
+ foreach (glob($dir . '/*') as $file) {
+ if (is_dir($file)) {
+ static::rrmdir($file);
+ } else {
+ unlink($file);
+ }
+ }
+
+ rmdir($dir);
+ }
+
+ protected function mkdir($path, $recursive = false)
+ {
+ if (! file_exists($path)) {
+ if ($recursive) {
+ $this->mkdir(dirname($path));
+ }
+
+ try {
+ mkdir($path);
+ chmod($path, $this->dir_mode);
+ } catch (Exception $e) {
+ throw new IcingaException('Could not create path "%s": %s', $path, $e->getMessage());
+ }
+ }
+ }
+}
diff --git a/library/Director/Core/RestApiClient.php b/library/Director/Core/RestApiClient.php
new file mode 100644
index 0000000..b0854ff
--- /dev/null
+++ b/library/Director/Core/RestApiClient.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace Icinga\Module\Director\Core;
+
+use Icinga\Application\Benchmark;
+use RuntimeException;
+
+class RestApiClient
+{
+ protected $version = 'v1';
+
+ protected $peer;
+
+ protected $port;
+
+ protected $user;
+
+ protected $pass;
+
+ protected $curl;
+
+ protected $readBuffer = '';
+
+ protected $onEvent;
+
+ protected $onEventWantsRaw;
+
+ protected $keepAlive = true;
+
+ public function __construct($peer, $port = 5665, $cn = null)
+ {
+ $this->peer = $peer;
+ $this->port = $port;
+ }
+
+ // TODO: replace with Web2 CA trust resource plus cert and get rid
+ // of user/pass or at least strongly advise against using it
+ public function setCredentials($user, $pass)
+ {
+ $this->user = $user;
+ $this->pass = $pass;
+
+ return $this;
+ }
+
+ public function onEvent($callback, $raw = false)
+ {
+ $this->onEventWantsRaw = $raw;
+ $this->onEvent = $callback;
+
+ return $this;
+ }
+
+ public function getPeerIdentity()
+ {
+ return $this->peer;
+ }
+
+ public function setKeepAlive($keepAlive = true)
+ {
+ $this->keepAlive = (bool) $keepAlive;
+
+ return $this;
+ }
+
+ protected function url($url)
+ {
+ return sprintf('https://%s:%d/%s/%s', $this->peer, $this->port, $this->version, $url);
+ }
+
+ /**
+ * @param $method
+ * @param $url
+ * @param null $body
+ * @param bool $raw
+ * @param bool $stream
+ * @return RestApiResponse
+ */
+ public function request($method, $url, $body = null, $raw = false, $stream = false)
+ {
+ if (function_exists('curl_version')) {
+ return $this->curlRequest($method, $url, $body, $raw, $stream);
+ } else {
+ throw new RuntimeException(
+ 'No CURL extension detected, it must be installed and enabled'
+ );
+ }
+ }
+
+ protected function curlRequest($method, $url, $body = null, $raw = false, $stream = false)
+ {
+ $auth = sprintf('%s:%s', $this->user, $this->pass);
+ $headers = [
+ 'Host: ' . $this->getPeerIdentity(),
+ ];
+
+ if (! $this->keepAlive) {
+ // This fails on Icinga 2.9:
+ // $headers[] = 'Connection: close';
+ }
+
+ if (! $raw) {
+ $headers[] = 'Accept: application/json';
+ }
+
+ if ($body !== null) {
+ $body = Json::encode($body);
+ $headers[] = 'Content-Type: application/json';
+ }
+
+ $curl = $this->curl();
+ $opts = [
+ CURLOPT_URL => $this->url($url),
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_USERPWD => $auth,
+ CURLOPT_CUSTOMREQUEST => strtoupper($method),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CONNECTTIMEOUT => 3,
+
+ // TODO: Fix this!
+ CURLOPT_SSL_VERIFYHOST => false,
+ CURLOPT_SSL_VERIFYPEER => false,
+ ];
+
+ if ($body !== null) {
+ $opts[CURLOPT_POSTFIELDS] = $body;
+ }
+
+ if ($stream) {
+ $opts[CURLOPT_WRITEFUNCTION] = [$this, 'readPart'];
+ $opts[CURLOPT_TCP_NODELAY] = 1;
+ }
+
+ curl_setopt_array($curl, $opts);
+ // TODO: request headers, validate status code
+
+ Benchmark::measure('Rest Api, sending ' . $url);
+ $res = curl_exec($curl);
+ if ($res === false) {
+ $error = curl_error($curl);
+ $this->disconnect();
+
+ throw new RuntimeException("CURL ERROR: $error");
+ }
+
+ $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ if ($statusCode === 401) {
+ $this->disconnect();
+ throw new RuntimeException(
+ 'Unable to authenticate, please check your API credentials'
+ );
+ }
+
+ Benchmark::measure('Rest Api, got response');
+ if (! $this->keepAlive) {
+ $this->disconnect();
+ }
+
+ if ($stream) {
+ return $this;
+ }
+
+ if ($raw) {
+ return $res;
+ } else {
+ return RestApiResponse::fromJsonResult($res);
+ }
+ }
+
+ /**
+ * @param resource $curl
+ * @param $data
+ * @return int
+ */
+ protected function readPart($curl, &$data)
+ {
+ $length = strlen($data);
+ $this->readBuffer .= $data;
+ $this->processEvents();
+ return $length;
+ }
+
+ public function get($url, $body = null)
+ {
+ return $this->request('get', $url, $body);
+ }
+
+ public function getRaw($url, $body = null)
+ {
+ return $this->request('get', $url, $body, true);
+ }
+
+ public function post($url, $body = null)
+ {
+ return $this->request('post', $url, $body);
+ }
+
+ public function put($url, $body = null)
+ {
+ return $this->request('put', $url, $body);
+ }
+
+ public function delete($url, $body = null)
+ {
+ return $this->request('delete', $url, $body);
+ }
+
+ /**
+ * @return resource
+ */
+ protected function curl()
+ {
+ if ($this->curl === null) {
+ $this->curl = curl_init(sprintf('https://%s:%d', $this->peer, $this->port));
+ if (! $this->curl) {
+ throw new RuntimeException('CURL INIT ERROR: ' . curl_error($this->curl));
+ }
+ }
+
+ return $this->curl;
+ }
+
+ protected function processEvents()
+ {
+ $offset = 0;
+ while (false !== ($pos = strpos($this->readBuffer, "\n", $offset))) {
+ if ($pos === $offset) {
+ // echo "Got empty line $offset / $pos\n";
+ $offset = $pos + 1;
+ continue;
+ }
+ $this->processReadBuffer($offset, $pos);
+
+ $offset = $pos + 1;
+ }
+
+ if ($offset > 0) {
+ $this->readBuffer = substr($this->readBuffer, $offset + 1);
+ }
+
+ // echo "REMAINING: " . strlen($this->readBuffer) . "\n";
+ }
+
+ protected function processReadBuffer($offset, $pos)
+ {
+ if ($this->onEvent === null) {
+ return;
+ }
+
+ $func = $this->onEvent;
+ $str = substr($this->readBuffer, $offset, $pos);
+ // printf("Processing %s bytes\n", strlen($str));
+
+ if ($this->onEventWantsRaw) {
+ $func($str);
+ } else {
+ $func(Json::decode($str));
+ }
+ }
+
+ public function disconnect()
+ {
+ if ($this->curl !== null) {
+ if (is_resource($this->curl)) {
+ @curl_close($this->curl);
+ }
+
+ $this->curl = null;
+ }
+ }
+
+ public function __destruct()
+ {
+ $this->disconnect();
+ }
+}
diff --git a/library/Director/Core/RestApiResponse.php b/library/Director/Core/RestApiResponse.php
new file mode 100644
index 0000000..523ed35
--- /dev/null
+++ b/library/Director/Core/RestApiResponse.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Icinga\Module\Director\Core;
+
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+
+class RestApiResponse
+{
+ protected $errorMessage;
+
+ protected $results;
+
+ protected function __construct()
+ {
+ }
+
+ public static function fromJsonResult($json)
+ {
+ $response = new static;
+ return $response->parseJsonResult($json);
+ }
+
+ public static function fromErrorMessage($error)
+ {
+ $response = new static;
+ $response->errorMessage = $error;
+
+ return $response;
+ }
+
+ public function getResult($desiredKey, $filter = array())
+ {
+ return $this->extractResult($this->results, $desiredKey, $filter);
+ }
+
+ public function getRaw($key = null, $default = null)
+ {
+ if ($key === null) {
+ return $this->results;
+ } elseif (isset($this->results[0]) && property_exists($this->results[0], $key)) {
+ return $this->results[0]->$key;
+ } else {
+ return $default;
+ }
+ }
+
+ public function getSingleResult()
+ {
+ if ($this->isErrorCode($this->results[0]->code)) {
+ throw new IcingaException(
+ $this->results[0]->status
+ );
+ } else {
+ return $this->results[0]->result;
+ }
+ }
+
+ protected function isErrorCode($code)
+ {
+ $code = (int) ceil($code);
+ return $code >= 400;
+ }
+
+ protected function extractResult($results, $desiredKey, $filter = array())
+ {
+ $response = array();
+ foreach ($results as $result) {
+ foreach ($filter as $key => $val) {
+ if (! property_exists($result, $key)) {
+ continue;
+ }
+ if ($result->$key !== $val) {
+ continue;
+ }
+ }
+ if (! property_exists($result, $desiredKey)) {
+ continue;
+ }
+
+ $response[$result->$desiredKey] = $result;
+ }
+ return $response;
+ }
+
+ public function getErrorMessage()
+ {
+ return $this->errorMessage;
+ }
+
+ public function succeeded()
+ {
+ return $this->errorMessage === null;
+ }
+
+ protected function parseJsonResult($json)
+ {
+ $result = @json_decode($json);
+ if ($result === null) {
+ $this->setJsonError();
+ // <h1>Bad Request</h1><p><pre>bad version</pre></p>
+ throw new IcingaException(
+ 'Parsing JSON result failed: '
+ . $this->errorMessage
+ . ' (Got: ' . substr($json, 0, 60) . ')'
+ );
+ }
+ if (property_exists($result, 'error')) {
+ if (property_exists($result, 'status')) {
+ if ((int) $result->error === 404) {
+ throw new NotFoundError($result->status);
+ } else {
+ throw new IcingaException('API request failed: ' . $result->status);
+ }
+ } else {
+ throw new IcingaException('API request failed: ' . var_export($result, 1));
+ }
+ }
+
+ $this->results = $result->results; // TODO: Check if set
+ return $this;
+ }
+
+ // TODO: just return json_last_error_msg() for PHP >= 5.5.0
+ protected function setJsonError()
+ {
+ switch (json_last_error()) {
+ case JSON_ERROR_DEPTH:
+ $this->errorMessage = 'The maximum stack depth has been exceeded';
+ break;
+ case JSON_ERROR_CTRL_CHAR:
+ $this->errorMessage = 'Control character error, possibly incorrectly encoded';
+ break;
+ case JSON_ERROR_STATE_MISMATCH:
+ $this->errorMessage = 'Invalid or malformed JSON';
+ break;
+ case JSON_ERROR_SYNTAX:
+ $this->errorMessage = 'Syntax error';
+ break;
+ case JSON_ERROR_UTF8:
+ $this->errorMessage = 'Malformed UTF-8 characters, possibly incorrectly encoded';
+ break;
+ default:
+ $this->errorMessage = 'An error occured when parsing a JSON string';
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/CoreBeta/ApiStream.php b/library/Director/CoreBeta/ApiStream.php
new file mode 100644
index 0000000..478fd40
--- /dev/null
+++ b/library/Director/CoreBeta/ApiStream.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Icinga\Module\Director\CoreBeta;
+
+use Exception;
+
+class ApiClient extends Stream
+{
+ protected $port;
+
+ public static function create($peer, $port = 5665)
+ {
+ $stream = new static();
+ }
+
+ protected function createClientConnection()
+ {
+ $context = $this->createSslContext();
+ if ($context === false) {
+ echo "Unable to set SSL options\n";
+ return false;
+ }
+
+ $conn = stream_socket_client(
+ 'ssl://' . $this->peername . ':' . $this->peerport,
+ $errno,
+ $errstr,
+ 15,
+ STREAM_CLIENT_CONNECT,
+ $context
+ );
+
+ return $conn;
+ }
+
+ protected function createSslContext()
+ {
+ $local = 'ssl://' . $this->local;
+ $context = stream_context_create();
+
+ // Hack, we need key and cert:
+ $certfile = preg_replace('~\..+$~', '', $this->certname) . '.combi';
+
+ $options = array(
+ 'ssl' => array(
+ 'verify_host' => true,
+ 'cafile' => $this->ssldir . '/ca.crt',
+ 'local_cert' => $this->ssldir . '/' . $certfile,
+ 'CN_match' => 'monitor1',
+ )
+ );
+
+ $result = stream_context_set_option($context, $options);
+
+ return $context;
+ }
+}
diff --git a/library/Director/CoreBeta/Stream.php b/library/Director/CoreBeta/Stream.php
new file mode 100644
index 0000000..5add9a3
--- /dev/null
+++ b/library/Director/CoreBeta/Stream.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\CoreBeta;
+
+abstract class Stream
+{
+ protected $stream;
+
+ protected $buffer = '';
+
+ protected $bufferLength = 0;
+
+ protected function __construct($stream)
+ {
+ $this->stream = $stream;
+ }
+}
diff --git a/library/Director/CoreBeta/StreamContext.php b/library/Director/CoreBeta/StreamContext.php
new file mode 100644
index 0000000..4844b79
--- /dev/null
+++ b/library/Director/CoreBeta/StreamContext.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Icinga\Module\Director\CoreBeta;
+
+use Icinga\Exception\ProgrammingError;
+
+class StreamContext
+{
+ protected $options = array();
+
+ public function ssl()
+ {
+ if ($this->ssl === null) {
+ $this->ssl = new StreamContextSslOptions();
+ }
+
+ return $this->ssl;
+ }
+
+ public function isSsl()
+ {
+ return $this->ssl !== null;
+ }
+
+ public function setCA(CA $ca)
+ {
+ // $this->options
+ }
+
+ protected function createSslContext()
+ {
+ $local = 'ssl://' . $this->local;
+ $context = stream_context_create();
+
+ // Hack, we need key and cert:
+ $certfile = preg_replace('~\..+$~', '', $this->certname) . '.combi';
+
+ $options = array(
+ 'ssl' => array(
+ 'verify_host' => true,
+ 'cafile' => $this->ssldir . '/ca.crt',
+ 'local_cert' => $this->ssldir . '/' . $certfile,
+ 'CN_match' => 'monitor1',
+ )
+ );
+
+ $result = stream_context_set_option($context, $options);
+
+ return $context;
+ }
+
+ public function setContextOptions($options)
+ {
+ if (array_key_exists('ssl', $options)) {
+ throw new ProgrammingError('Direct access to ssl options is not allowed');
+ }
+ }
+
+ protected function reallySetContextOptions($options)
+ {
+ if ($this->context === null) {
+ $this->options = $options;
+ } else {
+ stream_context_set_option($this->context, $options);
+ }
+ }
+
+ protected function lazyContext()
+ {
+ if ($this->context === null) {
+ $this->context = stream_context_create();
+ $this->setContextOptions($this->getOptions());
+
+ // stream_context_set_option($this->context
+ if ($this->isSsl()) {
+ $this->options['ssl'] = $this->ssl()->getOptions();
+ }
+
+ $result = stream_context_set_option($this->context, $this->options);
+ }
+
+ return $this->context;
+ }
+
+ public function getRawContext()
+ {
+ return $this->lazyContext();
+ }
+}
diff --git a/library/Director/CoreBeta/StreamContextSslOptions.php b/library/Director/CoreBeta/StreamContextSslOptions.php
new file mode 100644
index 0000000..d01d4a5
--- /dev/null
+++ b/library/Director/CoreBeta/StreamContextSslOptions.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Icinga\Module\Director\CoreBeta;
+
+use Icinga\Exception\ProgrammingError;
+
+class StreamContextSslOptions
+{
+ protected $options = array(
+ 'verify_peer' => true,
+ );
+
+ public function setCA(CA $ca)
+ {
+ $this->ca = $ca;
+ }
+
+ public function capturePeerCert($capture = true)
+ {
+ $this->options['capture_peer_cert'] = (bool) $capture;
+ return $this;
+ }
+
+ public function capturePeerChain($capture = true)
+ {
+ $this->options['capture_peer_chain'] = (bool) $capture;
+ return $this;
+ }
+
+ public function setCiphers($ciphers)
+ {
+ $this->options['ciphers'] = $ciphers;
+ return $this;
+ }
+
+ public function setPeerName($name)
+ {
+ if (version_compare(PHP_VERSION, '5.6.0') >= 0) {
+ $this->options['peer_name'] = $name;
+ $this->options['verify_peer_name'] = true;
+ } else {
+ $this->options['CN_match'] = $name;
+ }
+ return $this;
+ }
+
+ public function getOptions()
+ {
+ // TODO: Fail on missing cert
+ return $this->options;
+ }
+}
diff --git a/library/Director/CustomVariable/CustomVariable.php b/library/Director/CustomVariable/CustomVariable.php
new file mode 100644
index 0000000..98eda84
--- /dev/null
+++ b/library/Director/CustomVariable/CustomVariable.php
@@ -0,0 +1,286 @@
+<?php
+
+namespace Icinga\Module\Director\CustomVariable;
+
+use Exception;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
+use InvalidArgumentException;
+use LogicException;
+
+abstract class CustomVariable implements IcingaConfigRenderer
+{
+ protected $key;
+
+ protected $value;
+
+ protected $storedValue;
+
+ protected $type;
+
+ protected $modified = false;
+
+ protected $loadedFromDb = false;
+
+ protected $deleted = false;
+
+ protected $checksum;
+
+ protected function __construct($key, $value = null)
+ {
+ $this->key = $key;
+ $this->setValue($value);
+ }
+
+ public function is($type)
+ {
+ return $this->getType() === $type;
+ }
+
+ public function getType()
+ {
+ if ($this->type === null) {
+ $parts = explode('\\', get_class($this));
+ $class = end($parts);
+ // strlen('CustomVariable') === 14
+ $this->type = substr($class, 14);
+ }
+
+ return $this->type;
+ }
+
+ // TODO: implement delete()
+ public function hasBeenDeleted()
+ {
+ return $this->deleted;
+ }
+
+ public function delete()
+ {
+ $this->deleted = true;
+ return $this;
+ }
+
+ // TODO: abstract
+ public function getDbValue()
+ {
+ return $this->getValue();
+ }
+
+ public function toJson()
+ {
+ if ($this->getDbFormat() === 'string') {
+ return json_encode($this->getDbValue());
+ } else {
+ return $this->getDbValue();
+ }
+ }
+
+ // TODO: abstract
+ public function getDbFormat()
+ {
+ return 'string';
+ }
+
+ public function getKey()
+ {
+ return $this->key;
+ }
+
+ /**
+ * @param $value
+ * @return $this
+ */
+ abstract public function setValue($value);
+
+ abstract public function getValue();
+
+ /**
+ * @param bool $renderExpressions
+ * @return string
+ */
+ public function toConfigString($renderExpressions = false)
+ {
+ // TODO: this should be an abstract method once we deprecate PHP < 5.3.9
+ throw new LogicException(sprintf(
+ '%s has no toConfigString() implementation',
+ get_class($this)
+ ));
+ }
+
+ public function flatten(array &$flat, $prefix)
+ {
+ $flat[$prefix] = $this->getDbValue();
+ }
+
+ public function render($renderExpressions = false)
+ {
+ return c::renderKeyValue(
+ $this->renderKeyName($this->getKey()),
+ $this->toConfigStringPrefetchable($renderExpressions)
+ );
+ }
+
+ protected function renderKeyName($key)
+ {
+ if (preg_match('/^[a-z][a-z0-9_]*$/i', $key)) {
+ return 'vars.' . c::escapeIfReserved($key);
+ } else {
+ return 'vars[' . c::renderString($key) . ']';
+ }
+ }
+
+ public function checksum()
+ {
+ // TODO: remember checksum, invalidate on change
+ return sha1($this->getKey() . '=' . $this->toJson(), true);
+ }
+
+ public function isNew()
+ {
+ return ! $this->loadedFromDb;
+ }
+
+ public function hasBeenModified()
+ {
+ return $this->modified;
+ }
+
+ public function toConfigStringPrefetchable($renderExpressions = false)
+ {
+ if (PrefetchCache::shouldBeUsed()) {
+ return PrefetchCache::instance()->renderVar($this, $renderExpressions);
+ } else {
+ return $this->toConfigString($renderExpressions);
+ }
+ }
+
+ public function setModified($modified = true)
+ {
+ $this->modified = $modified;
+ if (! $this->modified) {
+ if (is_object($this->value)) {
+ $this->storedValue = clone($this->value);
+ } else {
+ $this->storedValue = $this->value;
+ }
+ }
+
+ return $this;
+ }
+
+ public function setUnmodified()
+ {
+ return $this->setModified(false);
+ }
+
+ public function setLoadedFromDb($loaded = true)
+ {
+ $this->loadedFromDb = $loaded;
+ return $this;
+ }
+
+ abstract public function equals(CustomVariable $var);
+
+ public function differsFrom(CustomVariable $var)
+ {
+ return ! $this->equals($var);
+ }
+
+ protected function setChecksum($checksum)
+ {
+ $this->checksum = $checksum;
+ return $this;
+ }
+
+ public function getChecksum()
+ {
+ return $this->checksum;
+ }
+
+ public static function wantCustomVariable($key, $value)
+ {
+ if ($value instanceof CustomVariable) {
+ return $value;
+ }
+
+ return self::create($key, $value);
+ }
+
+ public static function create($key, $value)
+ {
+ if (is_null($value)) {
+ return new CustomVariableNull($key, $value);
+ }
+
+ if (is_bool($value)) {
+ return new CustomVariableBoolean($key, $value);
+ }
+
+ if (is_int($value) || is_float($value)) {
+ return new CustomVariableNumber($key, $value);
+ }
+
+ if (is_string($value)) {
+ return new CustomVariableString($key, $value);
+ } elseif (is_array($value)) {
+ foreach (array_keys($value) as $k) {
+ if (! (is_int($k) || ctype_digit($k))) {
+ return new CustomVariableDictionary($key, $value);
+ }
+ }
+
+ return new CustomVariableArray($key, array_values($value));
+ } elseif (is_object($value)) {
+ // TODO: check for specific class/stdClass/interface?
+ return new CustomVariableDictionary($key, $value);
+ } else {
+ throw new LogicException(sprintf('WTF (%s): %s', $key, var_export($value, 1)));
+ }
+ }
+
+ public static function fromDbRow($row)
+ {
+ switch ($row->format) {
+ case 'string':
+ $var = new CustomVariableString($row->varname, $row->varvalue);
+ break;
+ case 'json':
+ $var = self::create($row->varname, json_decode($row->varvalue));
+ break;
+ case 'expression':
+ throw new InvalidArgumentException(
+ 'Icinga code expressions are not yet supported'
+ );
+ default:
+ throw new InvalidArgumentException(sprintf(
+ '%s is not a supported custom variable format',
+ $row->format
+ ));
+ }
+ if (property_exists($row, 'checksum')) {
+ $var->setChecksum($row->checksum);
+ }
+
+ $var->loadedFromDb = true;
+ $var->setUnmodified();
+ return $var;
+ }
+
+ public function __toString()
+ {
+ try {
+ return $this->toConfigString();
+ } catch (Exception $e) {
+ trigger_error($e);
+ $previousHandler = set_exception_handler(
+ function () {
+ }
+ );
+ restore_error_handler();
+ call_user_func($previousHandler, $e);
+ die();
+ }
+ }
+}
diff --git a/library/Director/CustomVariable/CustomVariableArray.php b/library/Director/CustomVariable/CustomVariableArray.php
new file mode 100644
index 0000000..7e430a4
--- /dev/null
+++ b/library/Director/CustomVariable/CustomVariableArray.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Icinga\Module\Director\CustomVariable;
+
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+
+class CustomVariableArray extends CustomVariable
+{
+ /** @var CustomVariable[] */
+ protected $value;
+
+ public function equals(CustomVariable $var)
+ {
+ if (! $var instanceof CustomVariableArray) {
+ return false;
+ }
+
+ return $var->getDbValue() === $this->getDbValue();
+ }
+
+ public function getValue()
+ {
+ $ret = array();
+ foreach ($this->value as $var) {
+ $ret[] = $var->getValue();
+ }
+
+ return $ret;
+ }
+
+ public function getDbValue()
+ {
+ return json_encode($this->getValue());
+ }
+
+ public function getDbFormat()
+ {
+ return 'json';
+ }
+
+ public function setValue($value)
+ {
+ $new = array();
+
+ foreach ($value as $k => $v) {
+ $new[] = self::wantCustomVariable($k, $v);
+ }
+
+ $equals = true;
+ if (is_array($this->value) && count($new) === count($this->value)) {
+ foreach ($this->value as $k => $v) {
+ if (! $new[$k]->equals($v)) {
+ $equals = false;
+ break;
+ }
+ }
+ } else {
+ $equals = false;
+ }
+
+ if (! $equals) {
+ $this->value = $new;
+ $this->setModified();
+ }
+
+ $this->deleted = false;
+
+ return $this;
+ }
+
+ public function flatten(array &$flat, $prefix)
+ {
+ foreach ($this->value as $k => $v) {
+ $v->flatten($flat, sprintf('%s[%d]', $prefix, $k));
+ }
+ }
+
+ public function toConfigString($renderExpressions = false)
+ {
+ $parts = array();
+ foreach ($this->value as $k => $v) {
+ $parts[] = $v->toConfigString($renderExpressions);
+ }
+
+ return c::renderEscapedArray($parts);
+ }
+
+ public function __clone()
+ {
+ foreach ($this->value as $key => $value) {
+ $this->value[$key] = clone($value);
+ }
+ }
+
+ public function toLegacyConfigString()
+ {
+ return c1::renderArray($this->value);
+ }
+}
diff --git a/library/Director/CustomVariable/CustomVariableBoolean.php b/library/Director/CustomVariable/CustomVariableBoolean.php
new file mode 100644
index 0000000..9953fae
--- /dev/null
+++ b/library/Director/CustomVariable/CustomVariableBoolean.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Icinga\Module\Director\CustomVariable;
+
+use Icinga\Exception\ProgrammingError;
+
+class CustomVariableBoolean extends CustomVariable
+{
+ public function equals(CustomVariable $var)
+ {
+ return $var->getValue() === $this->getValue();
+ }
+
+ public function getDbFormat()
+ {
+ return 'json';
+ }
+
+ public function getDbValue()
+ {
+ return json_encode($this->getValue());
+ }
+
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ public function setValue($value)
+ {
+ if (! is_bool($value)) {
+ throw new ProgrammingError(
+ 'Expected a boolean, got %s',
+ var_export($value, 1)
+ );
+ }
+
+ $this->value = $value;
+ $this->deleted = false;
+
+ return $this;
+ }
+
+ public function toConfigString($renderExpressions = false)
+ {
+ return $this->value ? 'true' : 'false';
+ }
+
+ public function toLegacyConfigString()
+ {
+ return $this->toConfigString();
+ }
+}
diff --git a/library/Director/CustomVariable/CustomVariableDictionary.php b/library/Director/CustomVariable/CustomVariableDictionary.php
new file mode 100644
index 0000000..d84be4f
--- /dev/null
+++ b/library/Director/CustomVariable/CustomVariableDictionary.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Icinga\Module\Director\CustomVariable;
+
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+use Countable;
+
+class CustomVariableDictionary extends CustomVariable implements Countable
+{
+ /** @var CustomVariable[] */
+ protected $value;
+
+ public function equals(CustomVariable $var)
+ {
+ if (! $var instanceof CustomVariableDictionary) {
+ return false;
+ }
+
+ $myKeys = $this->listKeys();
+ $foreignKeys = $var->listKeys();
+ if ($myKeys !== $foreignKeys) {
+ return false;
+ }
+
+ foreach ($this->value as $key => $value) {
+ if (! $value->equals($var->getInternalValue($key))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function getDbFormat()
+ {
+ return 'json';
+ }
+
+ public function getDbValue()
+ {
+ return json_encode($this->getValue());
+ }
+
+ public function setValue($value)
+ {
+ $new = array();
+
+ foreach ($value as $key => $val) {
+ $new[$key] = self::wantCustomVariable($key, $val);
+ }
+
+ $this->deleted = false;
+
+ // WTF?
+ if ($this->value === $new) {
+ return $this;
+ }
+
+ $this->value = $new;
+ $this->setModified();
+
+ return $this;
+ }
+
+ public function getValue()
+ {
+ $ret = (object) array();
+ ksort($this->value);
+
+ foreach ($this->value as $key => $var) {
+ $ret->$key = $var->getValue();
+ }
+
+ return $ret;
+ }
+
+ public function flatten(array &$flat, $prefix)
+ {
+ foreach ($this->value as $k => $v) {
+ $v->flatten($flat, sprintf('%s["%s"]', $prefix, $k));
+ }
+ }
+
+ public function listKeys()
+ {
+ $keys = array_keys($this->value);
+ sort($keys);
+ return $keys;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return count($this->value);
+ }
+
+ public function __clone()
+ {
+ foreach ($this->value as $key => $value) {
+ $this->value[$key] = clone($value);
+ }
+ }
+
+ public function __get($key)
+ {
+ return $this->value[$key];
+ }
+
+ public function __isset($key)
+ {
+ return array_key_exists($key, $this->value);
+ }
+
+ public function getInternalValue($key)
+ {
+ return $this->value[$key];
+ }
+
+ public function toConfigString($renderExpressions = false)
+ {
+ // TODO
+ return c::renderDictionary($this->value);
+ }
+
+ public function toLegacyConfigString()
+ {
+ return c1::renderDictionary($this->value);
+ }
+}
diff --git a/library/Director/CustomVariable/CustomVariableNull.php b/library/Director/CustomVariable/CustomVariableNull.php
new file mode 100644
index 0000000..f87ccfa
--- /dev/null
+++ b/library/Director/CustomVariable/CustomVariableNull.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Icinga\Module\Director\CustomVariable;
+
+use Icinga\Exception\ProgrammingError;
+
+class CustomVariableNull extends CustomVariable
+{
+ public function equals(CustomVariable $var)
+ {
+ return $var instanceof CustomVariableNull;
+ }
+
+ public function getValue()
+ {
+ return null;
+ }
+
+ public function getDbValue()
+ {
+ return json_encode($this->getValue());
+ }
+
+ public function getDbFormat()
+ {
+ return 'json';
+ }
+
+ public function setValue($value)
+ {
+ if (! is_null($value)) {
+ throw new ProgrammingError(
+ 'Null can only be null, got %s',
+ var_export($value, 1)
+ );
+ }
+
+ $this->deleted = false;
+
+ return $this;
+ }
+
+ public function toConfigString($renderExpressions = false)
+ {
+ return 'null';
+ }
+
+ public function toLegacyConfigString()
+ {
+ return $this->toConfigString();
+ }
+}
diff --git a/library/Director/CustomVariable/CustomVariableNumber.php b/library/Director/CustomVariable/CustomVariableNumber.php
new file mode 100644
index 0000000..62838a9
--- /dev/null
+++ b/library/Director/CustomVariable/CustomVariableNumber.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Icinga\Module\Director\CustomVariable;
+
+use Icinga\Exception\ProgrammingError;
+
+class CustomVariableNumber extends CustomVariable
+{
+ // Hint: 'F' is intentional, this MUST NOT respect locales
+ const PRECISION = '%.9F';
+
+ public function equals(CustomVariable $var)
+ {
+ if (! $var instanceof CustomVariableNumber) {
+ return false;
+ }
+
+ $cur = $this->getValue();
+ $new = $var->getValue();
+
+ // Be tolerant when comparing floats:
+ if (is_float($cur) || is_float($new)) {
+ return sprintf(self::PRECISION, $cur)
+ === sprintf(self::PRECISION, $new);
+ }
+
+ return $cur === $new;
+ }
+
+ public function getDbFormat()
+ {
+ return 'json';
+ }
+
+ public function getDbValue()
+ {
+ return json_encode($this->getValue());
+ }
+
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ public function setValue($value)
+ {
+ if (! is_int($value) && ! is_float($value)) {
+ throw new ProgrammingError(
+ 'Expected a number, got %s',
+ var_export($value, 1)
+ );
+ }
+
+ $this->value = $value;
+ $this->deleted = false;
+
+ return $this;
+ }
+
+ public function toConfigString($renderExpressions = false)
+ {
+ if (is_int($this->value)) {
+ return (string) $this->value;
+ } else {
+ return sprintf(self::PRECISION, $this->value);
+ }
+ }
+
+ public function toLegacyConfigString()
+ {
+ return $this->toConfigString();
+ }
+}
diff --git a/library/Director/CustomVariable/CustomVariableString.php b/library/Director/CustomVariable/CustomVariableString.php
new file mode 100644
index 0000000..2d50968
--- /dev/null
+++ b/library/Director/CustomVariable/CustomVariableString.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Director\CustomVariable;
+
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+
+class CustomVariableString extends CustomVariable
+{
+ public function equals(CustomVariable $var)
+ {
+ if (! $var instanceof CustomVariableString) {
+ return false;
+ }
+
+ return $var->getValue() === $this->getValue();
+ }
+
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ public function setValue($value)
+ {
+ if (! is_string($value)) {
+ $value = (string) $value;
+ }
+
+ if ($value !== $this->value) {
+ $this->value = $value;
+ $this->setModified();
+ }
+
+ $this->deleted = false;
+
+ return $this;
+ }
+
+ public function flatten(array &$flat, $prefix)
+ {
+ // TODO: we should get rid of type=string and always use JSON
+ $flat[$prefix] = json_encode($this->getValue());
+ }
+
+ public function toConfigString($renderExpressions = false)
+ {
+ if ($renderExpressions) {
+ return c::renderStringWithVariables($this->getValue(), ['config']);
+ } else {
+ return c::renderString($this->getValue());
+ }
+ }
+
+ public function toLegacyConfigString()
+ {
+ return c1::renderString($this->getValue());
+ }
+}
diff --git a/library/Director/CustomVariable/CustomVariables.php b/library/Director/CustomVariable/CustomVariables.php
new file mode 100644
index 0000000..cdcc4bd
--- /dev/null
+++ b/library/Director/CustomVariable/CustomVariables.php
@@ -0,0 +1,488 @@
+<?php
+
+namespace Icinga\Module\Director\CustomVariable;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Countable;
+use Exception;
+use Iterator;
+
+class CustomVariables implements Iterator, Countable, IcingaConfigRenderer
+{
+ /** @var CustomVariable[] */
+ protected $storedVars = array();
+
+ /** @var CustomVariable[] */
+ protected $vars = array();
+
+ protected $modified = false;
+
+ private $position = 0;
+
+ private $overrideKeyName;
+
+ protected $idx = array();
+
+ protected static $allTables = array(
+ 'icinga_command_var',
+ 'icinga_host_var',
+ 'icinga_notification_var',
+ 'icinga_service_set_var',
+ 'icinga_service_var',
+ 'icinga_user_var',
+ );
+
+ public static function countAll($varname, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $parts = array();
+ $where = $db->quoteInto('varname = ?', $varname);
+ foreach (static::$allTables as $table) {
+ $parts[] = "SELECT COUNT(*) as cnt FROM $table WHERE $where";
+ }
+
+ $sub = implode(' UNION ALL ', $parts);
+ $query = "SELECT SUM(sub.cnt) AS cnt FROM ($sub) sub";
+
+ return (int) $db->fetchOne($query);
+ }
+
+ public static function deleteAll($varname, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $where = $db->quoteInto('varname = ?', $varname);
+ foreach (static::$allTables as $table) {
+ $db->delete($table, $where);
+ }
+ }
+
+ public static function renameAll($oldname, $newname, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $where = $db->quoteInto('varname = ?', $oldname);
+ foreach (static::$allTables as $table) {
+ $db->update($table, ['varname' => $newname], $where);
+ }
+ }
+
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ $count = 0;
+ foreach ($this->vars as $var) {
+ if (! $var->hasBeenDeleted()) {
+ $count++;
+ }
+ }
+
+ return $count;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->position = 0;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ if (! $this->valid()) {
+ return null;
+ }
+
+ return $this->vars[$this->idx[$this->position]];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->idx[$this->position];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ ++$this->position;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ return array_key_exists($this->position, $this->idx);
+ }
+
+ /**
+ * Generic setter
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return self
+ */
+ public function set($key, $value)
+ {
+ $key = (string) $key;
+
+ if ($value instanceof CustomVariable) {
+ $value = clone($value);
+ } else {
+ if ($value === null) {
+ $this->__unset($key);
+ return $this;
+ }
+ $value = CustomVariable::create($key, $value);
+ }
+
+ // Hint: isset($this->$key) wouldn't conflict with protected properties
+ if ($this->__isset($key)) {
+ if ($value->equals($this->get($key))) {
+ return $this;
+ } else {
+ if (get_class($this->vars[$key]) === get_class($value)) {
+ $this->vars[$key]->setValue($value->getValue())->setModified();
+ } else {
+ $this->vars[$key] = $value->setLoadedFromDb()->setModified();
+ }
+ }
+ } else {
+ $this->vars[$key] = $value->setModified();
+ }
+
+ $this->modified = true;
+ $this->refreshIndex();
+
+ return $this;
+ }
+
+ protected function refreshIndex()
+ {
+ $this->idx = array();
+ ksort($this->vars);
+ foreach ($this->vars as $name => $var) {
+ if (! $var->hasBeenDeleted()) {
+ $this->idx[] = $name;
+ }
+ }
+ }
+
+ public static function loadForStoredObject(IcingaObject $object)
+ {
+ $db = $object->getDb();
+
+ $query = $db->select()->from(
+ array('v' => $object->getVarsTableName()),
+ array(
+ 'v.varname',
+ 'v.varvalue',
+ 'v.format',
+ )
+ )->where(sprintf('v.%s = ?', $object->getVarsIdColumn()), $object->get('id'));
+
+ $vars = new CustomVariables;
+ foreach ($db->fetchAll($query) as $row) {
+ $vars->vars[$row->varname] = CustomVariable::fromDbRow($row);
+ }
+ $vars->refreshIndex();
+ $vars->setBeingLoadedFromDb();
+ return $vars;
+ }
+
+ public static function forStoredRows($rows)
+ {
+ $vars = new CustomVariables;
+ foreach ($rows as $row) {
+ $vars->vars[$row->varname] = CustomVariable::fromDbRow($row);
+ }
+ $vars->refreshIndex();
+ $vars->setBeingLoadedFromDb();
+
+ return $vars;
+ }
+
+ public function storeToDb(IcingaObject $object)
+ {
+ $db = $object->getDb();
+ $table = $object->getVarsTableName();
+ $foreignColumn = $object->getVarsIdColumn();
+ $foreignId = $object->get('id');
+
+
+ foreach ($this->vars as $var) {
+ if ($var->isNew()) {
+ $db->insert(
+ $table,
+ array(
+ $foreignColumn => $foreignId,
+ 'varname' => $var->getKey(),
+ 'varvalue' => $var->getDbValue(),
+ 'format' => $var->getDbFormat()
+ )
+ );
+ $var->setLoadedFromDb();
+ continue;
+ }
+
+ $where = $db->quoteInto(sprintf('%s = ?', $foreignColumn), (int) $foreignId)
+ . $db->quoteInto(' AND varname = ?', $var->getKey());
+
+ if ($var->hasBeenDeleted()) {
+ $db->delete($table, $where);
+ } elseif ($var->hasBeenModified()) {
+ $db->update(
+ $table,
+ array(
+ 'varvalue' => $var->getDbValue(),
+ 'format' => $var->getDbFormat()
+ ),
+ $where
+ );
+ }
+ }
+
+ $this->setBeingLoadedFromDb();
+ }
+
+ public function get($key)
+ {
+ if (array_key_exists($key, $this->vars)) {
+ return $this->vars[$key];
+ }
+
+ return null;
+ }
+
+ public function hasBeenModified()
+ {
+ if ($this->modified) {
+ return true;
+ }
+
+ foreach ($this->vars as $var) {
+ if ($var->hasBeenModified()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ $this->modified = false;
+ $this->storedVars = array();
+ foreach ($this->vars as $key => $var) {
+ $this->storedVars[$key] = clone($var);
+ $var->setUnmodified();
+ $var->setLoadedFromDb();
+ }
+
+ return $this;
+ }
+
+ public function restoreStoredVar($key)
+ {
+ if (array_key_exists($key, $this->storedVars)) {
+ $this->vars[$key] = clone($this->storedVars[$key]);
+ $this->vars[$key]->setUnmodified();
+ $this->recheckForModifications();
+ $this->refreshIndex();
+ } elseif (array_key_exists($key, $this->vars)) {
+ unset($this->vars[$key]);
+ $this->recheckForModifications();
+ $this->refreshIndex();
+ }
+ }
+
+ protected function recheckForModifications()
+ {
+ $this->modified = false;
+ foreach ($this->vars as $var) {
+ if ($var->hasBeenModified()) {
+ $this->modified = true;
+
+ return;
+ }
+ }
+ }
+
+ public function getOriginalVars()
+ {
+ return $this->storedVars;
+ }
+
+ public function flatten()
+ {
+ $flat = array();
+ foreach ($this->vars as $key => $var) {
+ $var->flatten($flat, $key);
+ }
+
+ return $flat;
+ }
+
+ public function checksum()
+ {
+ $sums = array();
+ foreach ($this->vars as $key => $var) {
+ $sums[] = $key . '=' . $var->checksum();
+ }
+
+ return sha1(implode('|', $sums), true);
+ }
+
+ public function setOverrideKeyName($name)
+ {
+ $this->overrideKeyName = $name;
+ return $this;
+ }
+
+ public function toConfigString($renderExpressions = false)
+ {
+ $out = '';
+
+ foreach ($this as $key => $var) {
+ // TODO: ctype_alnum + underscore?
+ $out .= $this->renderSingleVar($key, $var, $renderExpressions);
+ }
+
+ return $out;
+ }
+
+ public function toLegacyConfigString()
+ {
+ $out = '';
+
+ ksort($this->vars);
+ foreach ($this->vars as $key => $var) {
+ // TODO: ctype_alnum + underscore?
+ // vars with ARGn will be handled by IcingaObject::renderLegacyCheck_command
+ if (substr($key, 0, 3) == 'ARG') {
+ continue;
+ }
+
+ switch ($type = $var->getType()) {
+ case 'String':
+ case 'Number':
+ # TODO: Make Prefetchable
+ $out .= c1::renderKeyValue(
+ '_' . $key,
+ $var->toLegacyConfigString()
+ );
+ break;
+ default:
+ $out .= c1::renderKeyValue(
+ '# _' . $key,
+ sprintf('(unsupported: %s)', $type)
+ );
+ }
+ }
+
+ if ($out !== '') {
+ $out = "\n".$out;
+ }
+
+ return $out;
+ }
+
+ /**
+ * @param string $key
+ * @param CustomVariable $var
+ * @param bool $renderExpressions
+ *
+ * @return string
+ */
+ protected function renderSingleVar($key, $var, $renderExpressions = false)
+ {
+ if ($key === $this->overrideKeyName) {
+ return c::renderKeyOperatorValue(
+ $this->renderKeyName($key),
+ '+=',
+ $var->toConfigStringPrefetchable($renderExpressions)
+ );
+ } else {
+ return c::renderKeyValue(
+ $this->renderKeyName($key),
+ $var->toConfigStringPrefetchable($renderExpressions)
+ );
+ }
+ }
+
+ protected function renderKeyName($key)
+ {
+ if (preg_match('/^[a-z][a-z0-9_]*$/i', $key)) {
+ return 'vars.' . c::escapeIfReserved($key);
+ } else {
+ return 'vars[' . c::renderString($key) . ']';
+ }
+ }
+
+ public function __get($key)
+ {
+ return $this->get($key);
+ }
+
+ /**
+ * Magic setter
+ *
+ * @param string $key Key
+ * @param mixed $val Value
+ *
+ * @return void
+ */
+ public function __set($key, $val)
+ {
+ $this->set($key, $val);
+ }
+
+ /**
+ * Magic isset check
+ *
+ * @param string $key
+ *
+ * @return boolean
+ */
+ public function __isset($key)
+ {
+ return array_key_exists($key, $this->vars);
+ }
+
+ /**
+ * Magic unsetter
+ *
+ * @param string $key
+ *
+ * @return void
+ */
+ public function __unset($key)
+ {
+ if (! array_key_exists($key, $this->vars)) {
+ return;
+ }
+
+ $this->vars[$key]->delete();
+ $this->modified = true;
+
+ $this->refreshIndex();
+ }
+
+ public function __toString()
+ {
+ try {
+ return $this->toConfigString();
+ } catch (Exception $e) {
+ trigger_error($e);
+ $previousHandler = set_exception_handler(
+ function () {
+ }
+ );
+ restore_error_handler();
+ call_user_func($previousHandler, $e);
+ die();
+ }
+ }
+}
diff --git a/library/Director/Daemon/BackgroundDaemon.php b/library/Director/Daemon/BackgroundDaemon.php
new file mode 100644
index 0000000..34cc28b
--- /dev/null
+++ b/library/Director/Daemon/BackgroundDaemon.php
@@ -0,0 +1,235 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use Exception;
+use gipfl\Cli\Process;
+use gipfl\IcingaCliDaemon\DbResourceConfigWatch;
+use gipfl\SystemD\NotifySystemD;
+use React\EventLoop\Factory as Loop;
+use React\EventLoop\LoopInterface;
+use Ramsey\Uuid\Uuid;
+
+class BackgroundDaemon
+{
+ /** @var LoopInterface */
+ private $loop;
+
+ /** @var NotifySystemD|boolean */
+ protected $systemd;
+
+ /** @var JobRunner */
+ protected $jobRunner;
+
+ /** @var string|null */
+ protected $dbResourceName;
+
+ /** @var DaemonDb */
+ protected $daemonDb;
+
+ /** @var DaemonProcessState */
+ protected $processState;
+
+ /** @var DaemonProcessDetails */
+ protected $processDetails;
+
+ /** @var LogProxy */
+ protected $logProxy;
+
+ /** @var bool */
+ protected $reloading = false;
+
+ /** @var bool */
+ protected $shuttingDown = false;
+
+ public function run(LoopInterface $loop = null)
+ {
+ if ($ownLoop = ($loop === null)) {
+ $loop = Loop::create();
+ }
+ $this->loop = $loop;
+ $this->loop->futureTick(function () {
+ $this->initialize();
+ });
+ if ($ownLoop) {
+ $loop->run();
+ }
+ }
+
+ public function setDbResourceName($name)
+ {
+ $this->dbResourceName = $name;
+
+ return $this;
+ }
+
+ protected function initialize()
+ {
+ $this->registerSignalHandlers($this->loop);
+ $this->processState = new DaemonProcessState('icinga::director');
+ $this->jobRunner = new JobRunner($this->loop);
+ $this->systemd = $this->eventuallyInitializeSystemd();
+ $this->processState->setSystemd($this->systemd);
+ if ($this->systemd) {
+ $this->systemd->setReady();
+ }
+ $this->setState('ready');
+ $this->processDetails = $this
+ ->initializeProcessDetails($this->systemd)
+ ->registerProcessList($this->jobRunner->getProcessList());
+ $this->logProxy = new LogProxy($this->processDetails->getInstanceUuid());
+ $this->jobRunner->forwardLog($this->logProxy);
+ $this->daemonDb = $this->initializeDb(
+ $this->processDetails,
+ $this->processState,
+ $this->dbResourceName
+ );
+ $this->daemonDb
+ ->register($this->jobRunner)
+ ->register($this->logProxy)
+ ->register(new DeploymentChecker($this->loop))
+ ->run($this->loop);
+ $this->setState('running');
+ }
+
+ /**
+ * @param NotifySystemD|false $systemd
+ * @return DaemonProcessDetails
+ */
+ protected function initializeProcessDetails($systemd)
+ {
+ if ($systemd && $systemd->hasInvocationId()) {
+ $uuid = $systemd->getInvocationId();
+ } else {
+ try {
+ $uuid = \bin2hex(Uuid::uuid4()->getBytes());
+ } catch (Exception $e) {
+ $uuid = 'deadc0de' . \substr(\md5(\getmypid()), 0, 24);
+ }
+ }
+ $processDetails = new DaemonProcessDetails($uuid);
+ if ($systemd) {
+ $processDetails->set('running_with_systemd', 'y');
+ }
+
+ return $processDetails;
+ }
+
+ protected function eventuallyInitializeSystemd()
+ {
+ $systemd = NotifySystemD::ifRequired($this->loop);
+ if ($systemd) {
+ Logger::replaceRunningInstance(new SystemdLogWriter());
+ Logger::info(sprintf(
+ "Started by systemd, notifying watchdog every %0.2Gs via %s",
+ $systemd->getWatchdogInterval(),
+ $systemd->getSocketPath()
+ ));
+ } else {
+ Logger::debug('Running without systemd');
+ }
+
+ return $systemd;
+ }
+
+ /**
+ * @return DaemonProcessDetails
+ */
+ public function getProcessDetails()
+ {
+ return $this->processDetails;
+ }
+
+ /**
+ * @return DaemonProcessState
+ */
+ public function getProcessState()
+ {
+ return $this->processState;
+ }
+
+ protected function initializeDb(
+ DaemonProcessDetails $processDetails,
+ DaemonProcessState $processState,
+ $dbResourceName = null
+ ) {
+ $db = new DaemonDb($processDetails);
+ $db->on('state', function ($state, $level = null) use ($processState) {
+ // TODO: level is sent but not used
+ $processState->setComponentState('db', $state);
+ });
+ $db->on('schemaChange', function ($startupSchema, $dbSchema) {
+ Logger::info(sprintf(
+ "DB schema version changed. Started with %d, DB has %d. Restarting.",
+ $startupSchema,
+ $dbSchema
+ ));
+ $this->reload();
+ });
+
+ $db->setConfigWatch(
+ $dbResourceName
+ ? DbResourceConfigWatch::name($dbResourceName)
+ : DbResourceConfigWatch::module('director')
+ );
+
+ return $db;
+ }
+
+ protected function registerSignalHandlers(LoopInterface $loop)
+ {
+ $func = function ($signal) use (&$func) {
+ $this->shutdownWithSignal($signal, $func);
+ };
+ $funcReload = function () {
+ $this->reload();
+ };
+ $loop->addSignal(SIGHUP, $funcReload);
+ $loop->addSignal(SIGINT, $func);
+ $loop->addSignal(SIGTERM, $func);
+ }
+
+ protected function shutdownWithSignal($signal, &$func)
+ {
+ $this->loop->removeSignal($signal, $func);
+ $this->shutdown();
+ }
+
+ public function reload()
+ {
+ if ($this->reloading) {
+ Logger::error('Ignoring reload request, reload is already in progress');
+ return;
+ }
+ $this->reloading = true;
+ Logger::info('Going gown for reload now');
+ $this->setState('reloading the main process');
+ $this->daemonDb->disconnect()->then(function () {
+ Process::restart();
+ });
+ }
+
+ protected function shutdown()
+ {
+ if ($this->shuttingDown) {
+ Logger::error('Ignoring shutdown request, shutdown is already in progress');
+ return;
+ }
+ Logger::info('Shutting down');
+ $this->shuttingDown = true;
+ $this->setState('shutting down');
+ $this->daemonDb->disconnect()->then(function () {
+ Logger::info('DB has been disconnected, shutdown finished');
+ $this->loop->stop();
+ });
+ }
+
+ protected function setState($state)
+ {
+ if ($this->processState) {
+ $this->processState->setState($state);
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/Daemon/DaemonDb.php b/library/Director/Daemon/DaemonDb.php
new file mode 100644
index 0000000..7772b3a
--- /dev/null
+++ b/library/Director/Daemon/DaemonDb.php
@@ -0,0 +1,365 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use Exception;
+use gipfl\IcingaCliDaemon\DbResourceConfigWatch;
+use gipfl\IcingaCliDaemon\RetryUnless;
+use Icinga\Data\ConfigObject;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Migrations;
+use ipl\Stdlib\EventEmitter;
+use React\EventLoop\LoopInterface;
+use React\Promise\Deferred;
+use RuntimeException;
+use SplObjectStorage;
+use function React\Promise\reject;
+use function React\Promise\resolve;
+
+class DaemonDb
+{
+ use EventEmitter;
+
+ /** @var LoopInterface */
+ private $loop;
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var DaemonProcessDetails */
+ protected $details;
+
+ /** @var DbBasedComponent[] */
+ protected $registeredComponents = [];
+
+ /** @var DbResourceConfigWatch|null */
+ protected $configWatch;
+
+ /** @var array|null */
+ protected $dbConfig;
+
+ /** @var RetryUnless|null */
+ protected $pendingReconnection;
+
+ /** @var Deferred|null */
+ protected $pendingDisconnect;
+
+ /** @var \React\EventLoop\TimerInterface */
+ protected $refreshTimer;
+
+ /** @var \React\EventLoop\TimerInterface */
+ protected $schemaCheckTimer;
+
+ /** @var int */
+ protected $startupSchemaVersion;
+
+ public function __construct(DaemonProcessDetails $details, $dbConfig = null)
+ {
+ $this->details = $details;
+ $this->dbConfig = $dbConfig;
+ }
+
+ public function register(DbBasedComponent $component)
+ {
+ $this->registeredComponents[] = $component;
+
+ return $this;
+ }
+
+ public function setConfigWatch(DbResourceConfigWatch $configWatch)
+ {
+ $this->configWatch = $configWatch;
+ $configWatch->notify(function ($config) {
+ $this->disconnect()->then(function () use ($config) {
+ return $this->onNewConfig($config);
+ });
+ });
+ if ($this->loop) {
+ $configWatch->run($this->loop);
+ }
+
+ return $this;
+ }
+
+ public function run(LoopInterface $loop)
+ {
+ $this->loop = $loop;
+ $this->connect();
+ $this->refreshTimer = $loop->addPeriodicTimer(3, function () {
+ $this->refreshMyState();
+ });
+ $this->schemaCheckTimer = $loop->addPeriodicTimer(15, function () {
+ $this->checkDbSchema();
+ });
+ if ($this->configWatch) {
+ $this->configWatch->run($this->loop);
+ }
+ }
+
+ protected function onNewConfig($config)
+ {
+ if ($config === null) {
+ if ($this->dbConfig === null) {
+ Logger::error('DB configuration is not valid');
+ } else {
+ Logger::error('DB configuration is no longer valid');
+ }
+ $this->emitStatus('no configuration');
+ $this->dbConfig = $config;
+
+ return resolve();
+ } else {
+ $this->emitStatus('configuration loaded');
+ $this->dbConfig = $config;
+
+ return $this->establishConnection($config);
+ }
+ }
+
+ protected function establishConnection($config)
+ {
+ if ($this->connection !== null) {
+ Logger::error('Trying to establish a connection while being connected');
+ return reject();
+ }
+ $callback = function () use ($config) {
+ $this->reallyEstablishConnection($config);
+ };
+ $onSuccess = function () {
+ $this->pendingReconnection = null;
+ $this->onConnected();
+ };
+ if ($this->pendingReconnection) {
+ $this->pendingReconnection->reset();
+ $this->pendingReconnection = null;
+ }
+ $this->emitStatus('connecting');
+
+ return $this->pendingReconnection = RetryUnless::succeeding($callback)
+ ->setInterval(0.2)
+ ->slowDownAfter(10, 10)
+ ->run($this->loop)
+ ->then($onSuccess)
+ ;
+ }
+
+ protected function reallyEstablishConnection($config)
+ {
+ $connection = new Db(new ConfigObject($config));
+ $connection->getDbAdapter()->getConnection();
+ $migrations = new Migrations($connection);
+ if (! $migrations->hasSchema()) {
+ $this->emitStatus('no schema', 'error');
+ throw new RuntimeException('DB has no schema');
+ }
+ $this->wipeOrphanedInstances($connection);
+ if ($this->hasAnyOtherActiveInstance($connection)) {
+ $this->emitStatus('locked by other instance', 'warning');
+ throw new RuntimeException('DB is locked by a running daemon instance, will retry');
+ }
+ $this->startupSchemaVersion = $migrations->getLastMigrationNumber();
+ $this->details->set('schema_version', $this->startupSchemaVersion);
+
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ $this->loop->futureTick(function () {
+ $this->refreshMyState();
+ });
+
+ return $connection;
+ }
+
+ protected function checkDbSchema()
+ {
+ if ($this->connection === null) {
+ return;
+ }
+
+ if ($this->schemaIsOutdated()) {
+ $this->emit('schemaChange', [
+ $this->getStartupSchemaVersion(),
+ $this->getDbSchemaVersion()
+ ]);
+ }
+ }
+
+ protected function schemaIsOutdated()
+ {
+ return $this->getStartupSchemaVersion() < $this->getDbSchemaVersion();
+ }
+
+ protected function getStartupSchemaVersion()
+ {
+ return $this->startupSchemaVersion;
+ }
+
+ protected function getDbSchemaVersion()
+ {
+ if ($this->connection === null) {
+ throw new RuntimeException(
+ 'Cannot determine DB schema version without an established DB connection'
+ );
+ }
+ $migrations = new Migrations($this->connection);
+
+ return $migrations->getLastMigrationNumber();
+ }
+
+ protected function onConnected()
+ {
+ $this->emitStatus('connected');
+ Logger::info('Connected to the database');
+ foreach ($this->registeredComponents as $component) {
+ $component->initDb($this->connection);
+ }
+ }
+
+ /**
+ * @return \React\Promise\PromiseInterface
+ */
+ protected function reconnect()
+ {
+ return $this->disconnect()->then(function () {
+ return $this->connect();
+ }, function (Exception $e) {
+ Logger::error('Disconnect failed. This should never happen: ' . $e->getMessage());
+ exit(1);
+ });
+ }
+
+ /**
+ * @return \React\Promise\ExtendedPromiseInterface
+ */
+ public function connect()
+ {
+ if ($this->connection === null) {
+ if ($this->dbConfig) {
+ return $this->establishConnection($this->dbConfig);
+ }
+ }
+
+ return resolve();
+ }
+
+ /**
+ * @return \React\Promise\ExtendedPromiseInterface
+ */
+ public function disconnect()
+ {
+ if (! $this->connection) {
+ return resolve();
+ }
+ if ($this->pendingDisconnect) {
+ return $this->pendingDisconnect->promise();
+ }
+
+ $this->eventuallySetStopped();
+ $this->pendingDisconnect = new Deferred();
+ $pendingComponents = new SplObjectStorage();
+ foreach ($this->registeredComponents as $component) {
+ $pendingComponents->attach($component);
+ $resolve = function () use ($pendingComponents, $component) {
+ $pendingComponents->detach($component);
+ if ($pendingComponents->count() === 0) {
+ $this->pendingDisconnect->resolve();
+ }
+ };
+ // TODO: What should we do in case they don't?
+ $component->stopDb()->then($resolve);
+ }
+
+ try {
+ if ($this->db) {
+ $this->db->closeConnection();
+ }
+ } catch (Exception $e) {
+ Logger::error('Failed to disconnect: ' . $e->getMessage());
+ }
+
+ return $this->pendingDisconnect->promise()->then(function () {
+ $this->connection = null;
+ $this->db = null;
+ $this->pendingDisconnect = null;
+ });
+ }
+
+ protected function emitStatus($message, $level = 'info')
+ {
+ $this->emit('state', [$message, $level]);
+
+ return $this;
+ }
+
+ protected function hasAnyOtherActiveInstance(Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+
+ return (int) $db->fetchOne(
+ $db->select()
+ ->from('director_daemon_info', 'COUNT(*)')
+ ->where('ts_stopped IS NULL')
+ ) > 0;
+ }
+
+ protected function wipeOrphanedInstances(Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $db->delete('director_daemon_info', 'ts_stopped IS NOT NULL');
+ $db->delete('director_daemon_info', $db->quoteInto(
+ 'instance_uuid_hex = ?',
+ $this->details->getInstanceUuid()
+ ));
+ $count = $db->delete(
+ 'director_daemon_info',
+ 'ts_stopped IS NULL AND ts_last_update < ' . (
+ DaemonUtil::timestampWithMilliseconds() - (60 * 1000)
+ )
+ );
+ if ($count > 1) {
+ Logger::error("Removed $count orphaned daemon instance(s) from DB");
+ }
+ }
+
+ protected function refreshMyState()
+ {
+ if ($this->db === null || $this->pendingReconnection || $this->pendingDisconnect) {
+ return;
+ }
+ try {
+ $updated = $this->db->update(
+ 'director_daemon_info',
+ $this->details->getPropertiesToUpdate(),
+ $this->db->quoteInto('instance_uuid_hex = ?', $this->details->getInstanceUuid())
+ );
+
+ if (! $updated) {
+ $this->db->insert(
+ 'director_daemon_info',
+ $this->details->getPropertiesToInsert()
+ );
+ }
+ } catch (Exception $e) {
+ Logger::error($e->getMessage());
+ $this->reconnect();
+ }
+ }
+
+ protected function eventuallySetStopped()
+ {
+ try {
+ if (! $this->db) {
+ return;
+ }
+ $this->db->update(
+ 'director_daemon_info',
+ ['ts_stopped' => DaemonUtil::timestampWithMilliseconds()],
+ $this->db->quoteInto('instance_uuid_hex = ?', $this->details->getInstanceUuid())
+ );
+ } catch (Exception $e) {
+ Logger::error('Failed to update daemon info (setting ts_stopped): ' . $e->getMessage());
+ }
+ }
+}
diff --git a/library/Director/Daemon/DaemonProcessDetails.php b/library/Director/Daemon/DaemonProcessDetails.php
new file mode 100644
index 0000000..454e31f
--- /dev/null
+++ b/library/Director/Daemon/DaemonProcessDetails.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use gipfl\LinuxHealth\Memory;
+use Icinga\Application\Platform;
+use React\ChildProcess\Process;
+use gipfl\Cli\Process as CliProcess;
+
+class DaemonProcessDetails
+{
+ /** @var string */
+ protected $instanceUuid;
+
+ /** @var \stdClass */
+ protected $info;
+
+ /** @var ProcessList[] */
+ protected $processLists = [];
+
+ protected $myArgs;
+
+ protected $myPid;
+
+ public function __construct($instanceUuid)
+ {
+ $this->instanceUuid = $instanceUuid;
+ $this->initialize();
+ }
+
+ public function getInstanceUuid()
+ {
+ return $this->instanceUuid;
+ }
+
+ public function getPropertiesToInsert()
+ {
+ return $this->getPropertiesToUpdate() + (array) $this->info;
+ }
+
+ public function getPropertiesToUpdate()
+ {
+ return [
+ 'ts_last_update' => DaemonUtil::timestampWithMilliseconds(),
+ 'ts_stopped' => null,
+ 'process_info' => \json_encode($this->collectProcessInfo()),
+ ];
+ }
+
+ public function set($property, $value)
+ {
+ if (\property_exists($this->info, $property)) {
+ $this->info->$property = $value;
+ } else {
+ throw new \InvalidArgumentException("Trying to set invalid daemon info property: $property");
+ }
+ }
+
+ public function registerProcessList(ProcessList $list)
+ {
+ $refresh = function (Process $process) {
+ $this->refreshProcessInfo();
+ };
+ $list->on('start', $refresh)->on('exit', $refresh);
+ $this->processLists[] = $list;
+
+ return $this;
+ }
+
+ protected function refreshProcessInfo()
+ {
+ $this->set('process_info', \json_encode($this->collectProcessInfo()));
+ }
+
+ protected function collectProcessInfo()
+ {
+ $info = (object) [$this->myPid => (object) [
+ 'command' => implode(' ', $this->myArgs),
+ 'running' => true,
+ 'memory' => Memory::getUsageForPid($this->myPid)
+ ]];
+
+ foreach ($this->processLists as $processList) {
+ foreach ($processList->getOverview() as $pid => $details) {
+ $info->$pid = $details;
+ }
+ }
+
+ return $info;
+ }
+
+ protected function initialize()
+ {
+ global $argv;
+ CliProcess::getInitialCwd();
+ $this->myArgs = $argv;
+ $this->myPid = \posix_getpid();
+ if (isset($_SERVER['_'])) {
+ $self = $_SERVER['_'];
+ } else {
+ // Process does a better job, but want the relative path (if such)
+ $self = $_SERVER['PHP_SELF'];
+ }
+ $this->info = (object) [
+ 'instance_uuid_hex' => $this->instanceUuid,
+ 'running_with_systemd' => 'n',
+ 'ts_started' => (int) ((float) $_SERVER['REQUEST_TIME_FLOAT'] * 1000),
+ 'ts_stopped' => null,
+ 'pid' => \posix_getpid(),
+ 'fqdn' => Platform::getFqdn(),
+ 'username' => Platform::getPhpUser(),
+ 'schema_version' => null,
+ 'php_version' => Platform::getPhpVersion(),
+ 'binary_path' => $self,
+ 'binary_realpath' => CliProcess::getBinaryPath(),
+ 'php_integer_size' => PHP_INT_SIZE,
+ 'php_binary_path' => PHP_BINARY,
+ 'php_binary_realpath' => \realpath(PHP_BINARY), // TODO: useless?
+ 'process_info' => null,
+ ];
+ }
+}
diff --git a/library/Director/Daemon/DaemonProcessState.php b/library/Director/Daemon/DaemonProcessState.php
new file mode 100644
index 0000000..6ae3cd2
--- /dev/null
+++ b/library/Director/Daemon/DaemonProcessState.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use gipfl\Cli\Process;
+use gipfl\SystemD\NotifySystemD;
+
+class DaemonProcessState
+{
+ /** @var NotifySystemD|null */
+ protected $systemd;
+
+ protected $components = [];
+
+ protected $currentMessage;
+
+ protected $processTitle;
+
+ protected $state;
+
+ public function __construct($processTitle)
+ {
+ $this->processTitle = $processTitle;
+ $this->refreshMessage();
+ }
+
+ /**
+ * @param NotifySystemD|false $systemd
+ * @return $this
+ */
+ public function setSystemd($systemd)
+ {
+ if ($systemd) {
+ $this->systemd = $systemd;
+ } else {
+ $this->systemd = null;
+ }
+
+ return $this;
+ }
+
+ public function setState($message)
+ {
+ $this->state = $message;
+ $this->refreshMessage();
+
+ return $this;
+ }
+
+ public function setComponentState($name, $stateMessage)
+ {
+ if ($stateMessage === null) {
+ unset($this->components[$name]);
+ } else {
+ $this->components[$name] = $stateMessage;
+ }
+ $this->refreshMessage();
+ }
+
+ protected function refreshMessage()
+ {
+ $messageParts = [];
+ if ($this->state !== null && \strlen($this->state)) {
+ $messageParts[] = $this->state;
+ }
+ foreach ($this->components as $component => $message) {
+ $messageParts[] = "$component: $message";
+ }
+
+ $message = \implode(', ', $messageParts);
+
+ if ($message !== $this->currentMessage) {
+ $this->currentMessage = $message;
+ if (\strlen($message) === 0) {
+ Process::setTitle($this->processTitle);
+ } else {
+ Process::setTitle($this->processTitle . ": $message");
+ }
+
+ if ($this->systemd) {
+ $this->systemd->setStatus($message);
+ }
+ }
+ }
+}
diff --git a/library/Director/Daemon/DaemonUtil.php b/library/Director/Daemon/DaemonUtil.php
new file mode 100644
index 0000000..c978d11
--- /dev/null
+++ b/library/Director/Daemon/DaemonUtil.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+class DaemonUtil
+{
+ /**
+ * @return int
+ */
+ public static function timestampWithMilliseconds()
+ {
+ $mTime = explode(' ', microtime());
+
+ return (int) round($mTime[0] * 1000) + (int) $mTime[1] * 1000;
+ }
+}
diff --git a/library/Director/Daemon/DbBasedComponent.php b/library/Director/Daemon/DbBasedComponent.php
new file mode 100644
index 0000000..c176c14
--- /dev/null
+++ b/library/Director/Daemon/DbBasedComponent.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use Icinga\Module\Director\Db;
+
+interface DbBasedComponent
+{
+ /**
+ * @param Db $db
+ * @return \React\Promise\ExtendedPromiseInterface;
+ */
+ public function initDb(Db $db);
+
+ /**
+ * @return \React\Promise\ExtendedPromiseInterface;
+ */
+ public function stopDb();
+}
diff --git a/library/Director/Daemon/DeploymentChecker.php b/library/Director/Daemon/DeploymentChecker.php
new file mode 100644
index 0000000..82d6d05
--- /dev/null
+++ b/library/Director/Daemon/DeploymentChecker.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use Exception;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+use React\EventLoop\LoopInterface;
+use function React\Promise\resolve;
+
+class DeploymentChecker implements DbBasedComponent
+{
+ /** @var Db */
+ protected $connection;
+
+ public function __construct(LoopInterface $loop)
+ {
+ $loop->addPeriodicTimer(5, function () {
+ if ($db = $this->connection) {
+ try {
+ if (DirectorDeploymentLog::hasUncollected($db)) {
+ $db->getDeploymentEndpoint()->api()->collectLogFiles($db);
+ }
+ } catch (Exception $e) {
+ // Ignore eventual issues while talking to Icinga
+ }
+ }
+ });
+ }
+
+ /**
+ * @param Db $connection
+ * @return \React\Promise\ExtendedPromiseInterface
+ */
+ public function initDb(Db $connection)
+ {
+ $this->connection = $connection;
+
+ return resolve();
+ }
+
+ /**
+ * @return \React\Promise\ExtendedPromiseInterface
+ */
+ public function stopDb()
+ {
+ $this->connection = null;
+
+ return resolve();
+ }
+}
diff --git a/library/Director/Daemon/JobRunner.php b/library/Director/Daemon/JobRunner.php
new file mode 100644
index 0000000..78d7747
--- /dev/null
+++ b/library/Director/Daemon/JobRunner.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use gipfl\IcingaCliDaemon\FinishedProcessState;
+use gipfl\IcingaCliDaemon\IcingaCliRpc;
+use Icinga\Application\Logger;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorJob;
+use React\ChildProcess\Process;
+use React\EventLoop\LoopInterface;
+use React\Promise\Promise;
+use function React\Promise\resolve;
+
+class JobRunner implements DbBasedComponent
+{
+ /** @var Db */
+ protected $db;
+
+ /** @var LoopInterface */
+ protected $loop;
+
+ /** @var int[] */
+ protected $scheduledIds = [];
+
+ /** @var Promise[] */
+ protected $runningIds = [];
+
+ protected $checkInterval = 10;
+
+ /** @var \React\EventLoop\TimerInterface */
+ protected $timer;
+
+ /** @var LogProxy */
+ protected $logProxy;
+
+ /** @var ProcessList */
+ protected $running;
+
+ public function __construct(LoopInterface $loop)
+ {
+ $this->loop = $loop;
+ $this->running = new ProcessList($loop);
+ }
+
+ public function forwardLog(LogProxy $logProxy)
+ {
+ $this->logProxy = $logProxy;
+
+ return $this;
+ }
+
+ /**
+ * @param Db $db
+ * @return \React\Promise\ExtendedPromiseInterface
+ */
+ public function initDb(Db $db)
+ {
+ $this->db = $db;
+ $check = function () {
+ try {
+ $this->checkForPendingJobs();
+ $this->runNextPendingJob();
+ } catch (\Exception $e) {
+ Logger::error($e->getMessage());
+ }
+ };
+ if ($this->timer === null) {
+ $this->loop->futureTick($check);
+ }
+ if ($this->timer !== null) {
+ Logger::info('Cancelling former timer');
+ $this->loop->cancelTimer($this->timer);
+ }
+ $this->timer = $this->loop->addPeriodicTimer($this->checkInterval, $check);
+
+ return resolve();
+ }
+
+ /**
+ * @return \React\Promise\ExtendedPromiseInterface
+ */
+ public function stopDb()
+ {
+ $this->scheduledIds = [];
+ if ($this->timer !== null) {
+ $this->loop->cancelTimer($this->timer);
+ $this->timer = null;
+ }
+ $allFinished = $this->running->killOrTerminate();
+ foreach ($this->runningIds as $id => $promise) {
+ $promise->cancel();
+ }
+ $this->runningIds = [];
+
+ return $allFinished;
+ }
+
+ protected function hasBeenDisabled()
+ {
+ $db = $this->db->getDbAdapter();
+ return $db->fetchOne(
+ $db->select()
+ ->from('director_setting', 'setting_value')
+ ->where('setting_name = ?', 'disable_all_jobs')
+ ) === 'y';
+ }
+
+ protected function checkForPendingJobs()
+ {
+ if ($this->hasBeenDisabled()) {
+ $this->scheduledIds = [];
+ // TODO: disable jobs currently going on?
+ return;
+ }
+ if (empty($this->scheduledIds)) {
+ $this->loadNextIds();
+ }
+ }
+
+ protected function runNextPendingJob()
+ {
+ if ($this->timer === null) {
+ // Reset happened. Stopping?
+ return;
+ }
+
+ if (! empty($this->runningIds)) {
+ return;
+ }
+ while (! empty($this->scheduledIds)) {
+ if ($this->runNextJob()) {
+ break;
+ }
+ }
+ }
+
+ protected function loadNextIds()
+ {
+ $db = $this->db->getDbAdapter();
+
+ foreach ($db->fetchCol(
+ $db->select()->from('director_job', 'id')->where('disabled = ?', 'n')
+ ) as $id) {
+ $this->scheduledIds[] = (int) $id;
+ };
+ }
+
+ /**
+ * @return bool
+ */
+ protected function runNextJob()
+ {
+ $id = \array_shift($this->scheduledIds);
+ try {
+ $job = DirectorJob::loadWithAutoIncId((int) $id, $this->db);
+ if ($job->shouldRun()) {
+ $this->runJob($job);
+ return true;
+ }
+ } catch (\Exception $e) {
+ Logger::error('Trying to schedule Job failed: ' . $e->getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * @param DirectorJob $job
+ */
+ protected function runJob(DirectorJob $job)
+ {
+ $id = $job->get('id');
+ $jobName = $job->get('job_name');
+ Logger::debug("Job ($jobName) starting");
+ $arguments = [
+ 'director',
+ 'job',
+ 'run',
+ '--id',
+ $job->get('id'),
+ '--debug',
+ '--rpc'
+ ];
+ $cli = new IcingaCliRpc();
+ $cli->setArguments($arguments);
+ $cli->on('start', function (Process $process) {
+ $this->onProcessStarted($process);
+ });
+
+ // Happens on protocol (Netstring) errors or similar:
+ $cli->on('error', function (\Exception $e) {
+ Logger::error('UNEXPECTED: ' . rtrim($e->getMessage()));
+ });
+ if ($this->logProxy) {
+ $logger = clone($this->logProxy);
+ $logger->setPrefix("Job ($jobName): ");
+ $cli->rpc()->setHandler($logger, 'logger');
+ }
+ unset($this->scheduledIds[$id]);
+ $this->runningIds[$id] = $cli->run($this->loop)->then(function () use ($id, $jobName) {
+ Logger::debug("Job ($jobName) finished");
+ })->otherwise(function (\Exception $e) use ($id, $jobName) {
+ Logger::error("Job ($jobName) failed: " . $e->getMessage());
+ })->otherwise(function (FinishedProcessState $state) use ($jobName) {
+ Logger::error("Job ($jobName) failed: " . $state->getReason());
+ })->always(function () use ($id) {
+ unset($this->runningIds[$id]);
+ $this->loop->futureTick(function () {
+ $this->runNextPendingJob();
+ });
+ });
+ }
+
+ /**
+ * @return ProcessList
+ */
+ public function getProcessList()
+ {
+ return $this->running;
+ }
+
+ protected function onProcessStarted(Process $process)
+ {
+ $this->running->attach($process);
+ }
+
+ public function __destruct()
+ {
+ $this->stopDb();
+ $this->logProxy = null;
+ $this->loop = null;
+ }
+}
diff --git a/library/Director/Daemon/JsonRpcLogWriter.php b/library/Director/Daemon/JsonRpcLogWriter.php
new file mode 100644
index 0000000..edfa23e
--- /dev/null
+++ b/library/Director/Daemon/JsonRpcLogWriter.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use gipfl\Protocol\JsonRpc\Connection;
+use gipfl\Protocol\JsonRpc\Notification;
+use Icinga\Application\Logger\LogWriter;
+use Icinga\Data\ConfigObject;
+
+class JsonRpcLogWriter extends LogWriter
+{
+ protected $connection;
+
+ protected static $severityMap = [
+ Logger::DEBUG => 'debug',
+ Logger::INFO => 'info',
+ Logger::WARNING => 'warning',
+ Logger::ERROR => 'error',
+ ];
+
+ public function __construct(Connection $connection)
+ {
+ parent::__construct(new ConfigObject([]));
+ $this->connection = $connection;
+ }
+
+ public function log($severity, $message)
+ {
+ $message = \iconv('UTF-8', 'UTF-8//IGNORE', $message);
+ $this->connection->sendNotification(
+ Notification::create('logger.log', [
+ static::$severityMap[$severity],
+ $message
+ ])
+ );
+ }
+}
diff --git a/library/Director/Daemon/LogProxy.php b/library/Director/Daemon/LogProxy.php
new file mode 100644
index 0000000..0b58ae8
--- /dev/null
+++ b/library/Director/Daemon/LogProxy.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use Exception;
+use Icinga\Module\Director\Db;
+use function React\Promise\resolve;
+
+class LogProxy implements DbBasedComponent
+{
+ protected $connection;
+
+ protected $db;
+
+ protected $server;
+
+ protected $instanceUuid;
+
+ protected $prefix = '';
+
+ public function __construct($instanceUuid)
+ {
+ $this->instanceUuid = $instanceUuid;
+ }
+
+ public function setPrefix($prefix)
+ {
+ $this->prefix = $prefix;
+
+ return $this;
+ }
+
+ /**
+ * @param Db $connection
+ * @return \React\Promise\ExtendedPromiseInterface
+ */
+ public function initDb(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+
+ return resolve();
+ }
+
+ /**
+ * @return \React\Promise\ExtendedPromiseInterface
+ */
+ public function stopDb()
+ {
+ $this->connection = null;
+ $this->db = null;
+
+ return resolve();
+ }
+
+ public function log($severity, $message)
+ {
+ Logger::$severity($this->prefix . $message);
+ /*
+ // Not yet
+ try {
+ if ($this->db) {
+ $this->db->insert('director_daemonlog', [
+ // environment/installation/db?
+ 'instance_uuid' => $this->instanceUuid,
+ 'ts_create' => DaemonUtil::timestampWithMilliseconds(),
+ 'level' => $severity,
+ 'message' => $message,
+ ]);
+ }
+ } catch (Exception $e) {
+ Logger::error($e->getMessage());
+ }
+ */
+ }
+}
diff --git a/library/Director/Daemon/Logger.php b/library/Director/Daemon/Logger.php
new file mode 100644
index 0000000..27fcbf5
--- /dev/null
+++ b/library/Director/Daemon/Logger.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use Icinga\Application\Logger as IcingaLogger;
+use Icinga\Application\Logger\LogWriter;
+use Icinga\Exception\ConfigurationError;
+
+class Logger extends IcingaLogger
+{
+ public static function replaceRunningInstance(LogWriter $writer, $level = null)
+ {
+ try {
+ $instance = static::$instance;
+ if ($level !== null) {
+ $instance->setLevel($level);
+ }
+
+ $instance->writer = $writer;
+ } catch (ConfigurationError $e) {
+ self::$instance->error($e->getMessage());
+ }
+ }
+}
diff --git a/library/Director/Daemon/ProcessList.php b/library/Director/Daemon/ProcessList.php
new file mode 100644
index 0000000..85b9aac
--- /dev/null
+++ b/library/Director/Daemon/ProcessList.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use gipfl\LinuxHealth\Memory;
+use Icinga\Application\Logger;
+use ipl\Stdlib\EventEmitter;
+use React\ChildProcess\Process;
+use React\EventLoop\LoopInterface;
+use React\Promise\Deferred;
+use function React\Promise\resolve;
+
+class ProcessList
+{
+ use EventEmitter;
+
+ /** @var LoopInterface */
+ protected $loop;
+
+ /** @var \SplObjectStorage */
+ protected $processes;
+
+ /**
+ * ProcessList constructor.
+ * @param LoopInterface $loop
+ * @param Process[] $processes
+ */
+ public function __construct(LoopInterface $loop, array $processes = [])
+ {
+ $this->loop = $loop;
+ $this->processes = new \SplObjectStorage();
+ foreach ($processes as $process) {
+ $this->attach($process);
+ }
+ }
+
+ public function attach(Process $process)
+ {
+ $this->processes->attach($process);
+ $this->emit('start', [$process]);
+ $process->on('exit', function () use ($process) {
+ $this->detach($process);
+ $this->emit('exit', [$process]);
+ });
+
+ return $this;
+ }
+
+ public function detach(Process $process)
+ {
+ $this->processes->detach($process);
+
+ return $this;
+ }
+
+ /**
+ * @param int $timeout
+ * @return \React\Promise\ExtendedPromiseInterface
+ */
+ public function killOrTerminate($timeout = 5)
+ {
+ if ($this->processes->count() === 0) {
+ return resolve();
+ }
+ $deferred = new Deferred();
+ $killTimer = $this->loop->addTimer($timeout, function () use ($deferred) {
+ /** @var Process $process */
+ foreach ($this->processes as $process) {
+ $pid = $process->getPid();
+ Logger::error("Process $pid is still running, sending SIGKILL");
+ $process->terminate(SIGKILL);
+ }
+
+ // Let's a little bit of delay after KILLing
+ $this->loop->addTimer(0.1, function () use ($deferred) {
+ $deferred->resolve();
+ });
+ });
+
+ $timer = $this->loop->addPeriodicTimer($timeout / 20, function () use (
+ $deferred,
+ &$timer,
+ $killTimer
+ ) {
+ $stopped = [];
+ /** @var Process $process */
+ foreach ($this->processes as $process) {
+ if (! $process->isRunning()) {
+ $stopped[] = $process;
+ }
+ }
+ foreach ($stopped as $process) {
+ $this->processes->detach($process);
+ }
+ if ($this->processes->count() === 0) {
+ $this->loop->cancelTimer($timer);
+ $this->loop->cancelTimer($killTimer);
+ $deferred->resolve();
+ }
+ });
+ /** @var Process $process */
+ foreach ($this->processes as $process) {
+ $process->terminate(SIGTERM);
+ }
+
+ return $deferred->promise();
+ }
+
+ public function getOverview()
+ {
+ $info = [];
+
+ /** @var Process $process */
+ foreach ($this->processes as $process) {
+ $pid = $process->getPid();
+ $info[$pid] = (object) [
+ 'command' => preg_replace('/^exec /', '', $process->getCommand()),
+ 'running' => $process->isRunning(),
+ 'memory' => Memory::getUsageForPid($pid)
+ ];
+ }
+
+ return $info;
+ }
+}
diff --git a/library/Director/Daemon/RunningDaemonInfo.php b/library/Director/Daemon/RunningDaemonInfo.php
new file mode 100644
index 0000000..adb3549
--- /dev/null
+++ b/library/Director/Daemon/RunningDaemonInfo.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+class RunningDaemonInfo
+{
+ /** @var object */
+ protected $info;
+
+ public function __construct($info = null)
+ {
+ $this->setInfo($info);
+ }
+
+ public function setInfo($info)
+ {
+ if (empty($info)) {
+ $this->info = $this->createEmptyInfo();
+ } else {
+ $this->info = $info;
+ }
+
+ return $this;
+ }
+
+ public function isRunning()
+ {
+ return $this->getPid() !== null && ! $this->isOutdated();
+ }
+
+ public function getPid()
+ {
+ return (int) $this->info->pid;
+ }
+
+ public function getUsername()
+ {
+ return $this->info->username;
+ }
+
+ public function getFqdn()
+ {
+ return $this->info->fqdn;
+ }
+
+ public function getLastUpdate()
+ {
+ return $this->info->ts_last_update;
+ }
+
+ public function getLastModification()
+ {
+ return $this->info->ts_last_modification;
+ }
+
+ public function getPhpVersion()
+ {
+ return $this->info->php_version;
+ }
+
+ public function hasBeenStopped()
+ {
+ return $this->getTimestampStopped() !== null;
+ }
+
+ public function getTimestampStarted()
+ {
+ return $this->info->ts_started;
+ }
+
+ public function getTimestampStopped()
+ {
+ return $this->info->ts_stopped;
+ }
+
+ public function isOutdated($seconds = 5)
+ {
+ return (
+ DaemonUtil::timestampWithMilliseconds() - $this->info->ts_last_update
+ ) > $seconds * 1000;
+ }
+
+ public function isRunningWithSystemd()
+ {
+ return $this->info->running_with_systemd === 'y';
+ }
+
+ public function getBinaryPath()
+ {
+ return $this->info->binary_path;
+ }
+
+ public function getBinaryRealpath()
+ {
+ return $this->info->binary_realpath;
+ }
+
+ public function binaryRealpathDiffers()
+ {
+ return $this->getBinaryPath() !== $this->getBinaryRealpath();
+ }
+
+ public function getPhpBinaryPath()
+ {
+ return $this->info->php_binary_path;
+ }
+
+ public function getPhpBinaryRealpath()
+ {
+ return $this->info->php_binary_realpath;
+ }
+
+ public function phpBinaryRealpathDiffers()
+ {
+ return $this->getPhpBinaryPath() !== $this->getPhpBinaryRealpath();
+ }
+
+ public function getPhpIntegerSize()
+ {
+ return (int) $this->info->php_integer_size;
+ }
+
+ public function has64bitIntegers()
+ {
+ return $this->getPhpIntegerSize() === 8;
+ }
+
+ /*
+ // TODO: not yet
+ public function isMaster()
+ {
+ return $this->info->is_master === 'y';
+ }
+
+ public function isStandby()
+ {
+ return ! $this->isMaster();
+ }
+ */
+
+ protected function createEmptyInfo()
+ {
+ return (object) [
+ 'pid' => null,
+ 'fqdn' => null,
+ 'username' => null,
+ 'php_version' => null,
+ // 'is_master' => null,
+ // Only if not running. Does this make any sense in 'empty info'?
+ 'ts_last_update' => null,
+ 'ts_last_modification' => null
+ ];
+ }
+}
diff --git a/library/Director/Daemon/SystemdLogWriter.php b/library/Director/Daemon/SystemdLogWriter.php
new file mode 100644
index 0000000..8b64442
--- /dev/null
+++ b/library/Director/Daemon/SystemdLogWriter.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Director\Daemon;
+
+use Icinga\Application\Logger\LogWriter;
+use Icinga\Data\ConfigObject;
+
+class SystemdLogWriter extends LogWriter
+{
+ protected static $severityMap = [
+ Logger::DEBUG => 7,
+ Logger::INFO => 6,
+ Logger::WARNING => 4,
+ Logger::ERROR => 3,
+ ];
+
+ public function __construct()
+ {
+ parent::__construct(new ConfigObject([]));
+ }
+
+ public function log($severity, $message)
+ {
+ $severity = self::$severityMap[$severity];
+ echo "<$severity>$message\n";
+ }
+}
diff --git a/library/Director/Dashboard/AlertsDashboard.php b/library/Director/Dashboard/AlertsDashboard.php
new file mode 100644
index 0000000..447f74f
--- /dev/null
+++ b/library/Director/Dashboard/AlertsDashboard.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class AlertsDashboard extends Dashboard
+{
+ protected $dashletNames = array(
+ 'Notifications',
+ 'Users',
+ 'Timeperiods',
+ 'DependencyObject',
+ 'ScheduledDowntimeApply',
+ );
+
+ public function getTitle()
+ {
+ return $this->translate('Get alerts when something goes wrong');
+ }
+}
diff --git a/library/Director/Dashboard/AutomationDashboard.php b/library/Director/Dashboard/AutomationDashboard.php
new file mode 100644
index 0000000..dd07d71
--- /dev/null
+++ b/library/Director/Dashboard/AutomationDashboard.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class AutomationDashboard extends Dashboard
+{
+ protected $dashletNames = array(
+ 'ImportSource',
+ 'Sync',
+ 'Job'
+ );
+
+ public function getTitle()
+ {
+ return $this->translate('Automate all tasks');
+ }
+}
diff --git a/library/Director/Dashboard/BranchesDashboard.php b/library/Director/Dashboard/BranchesDashboard.php
new file mode 100644
index 0000000..fe8b385
--- /dev/null
+++ b/library/Director/Dashboard/BranchesDashboard.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Application\Hook;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchStore;
+use Icinga\Module\Director\Hook\BranchSupportHook;
+use ipl\Html\Html;
+
+class BranchesDashboard extends Dashboard
+{
+ public function getTitle()
+ {
+ $branch = Branch::detect(new BranchStore($this->getDb()));
+ if ($branch->isBranch()) {
+ $this->prepend(Hint::info(Html::sprintf(
+ $this->translate('You\'re currently working in a Configuration Branch: %s'),
+ Branch::requireHook()->linkToBranch($branch, $this->getAuth(), $branch->getName())
+ )));
+ }
+
+ return $this->translate('Prepare your configuration in a safe Environment');
+ }
+
+ public function loadDashlets()
+ {
+ /** @var BranchSupportHook $hook */
+ if ($hook = Hook::first('director/BranchSupport')) {
+ $this->dashlets = $hook->loadDashlets($this->getDb());
+ } else {
+ $this->dashlets = [];
+ }
+ }
+}
diff --git a/library/Director/Dashboard/CommandsDashboard.php b/library/Director/Dashboard/CommandsDashboard.php
new file mode 100644
index 0000000..13f4e42
--- /dev/null
+++ b/library/Director/Dashboard/CommandsDashboard.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class CommandsDashboard extends Dashboard
+{
+ protected $dashletNames = array(
+ 'CheckCommands',
+ 'ExternalCheckCommands',
+ // 'NotificationCommands',
+ // 'ExternalNotificationCommands',
+ 'CommandTemplates',
+ );
+
+ public function getTitle()
+ {
+ return $this->translate('Manage your Icinga Commands');
+ }
+
+ public function getDescription()
+ {
+ return $this->translate(
+ 'Define Check-, Notification- or Event-Commands. Command definitions'
+ . ' are the glue between your Host- and Service-Checks and the Check'
+ . ' plugins on your Monitoring (or monitored) systems'
+ );
+ }
+
+ public function getTabs()
+ {
+ return $this->createTabsForDashboards(
+ ['hosts', 'services', 'commands']
+ );
+ }
+}
diff --git a/library/Director/Dashboard/Dashboard.php b/library/Director/Dashboard/Dashboard.php
new file mode 100644
index 0000000..de8970c
--- /dev/null
+++ b/library/Director/Dashboard/Dashboard.php
@@ -0,0 +1,305 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+use Exception;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\Tabs;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlString;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Restriction\HostgroupRestriction;
+use Icinga\Module\Director\Dashboard\Dashlet\Dashlet;
+use Icinga\Module\Director\Db;
+use Icinga\Web\Widget\Tab;
+use ipl\Html\ValidHtml;
+use Zend_Db_Select as ZfSelect;
+
+abstract class Dashboard extends HtmlDocument
+{
+ use TranslationHelper;
+
+ protected $name;
+
+ /** @var Dashlet[] */
+ protected $dashlets;
+
+ protected $dashletNames;
+
+ /** @var Db */
+ protected $db;
+
+ final private function __construct()
+ {
+ }
+
+ /**
+ * @param $name
+ * @param Db $db
+ *
+ * @return self
+ */
+ public static function loadByName($name, Db $db)
+ {
+ $class = __NAMESPACE__ . '\\' . ucfirst($name) . 'Dashboard';
+ $dashboard = new $class();
+ $dashboard->db = $db;
+ $dashboard->name = $name;
+ return $dashboard;
+ }
+
+ public static function exists($name)
+ {
+ return class_exists(__NAMESPACE__ . '\\' . ucfirst($name) . 'Dashboard');
+ }
+
+ /**
+ * @param $description
+ * @return $this
+ */
+ protected function addDescription($description)
+ {
+ if ($description instanceof ValidHtml) {
+ $this->add(Html::tag('p', $description));
+ } elseif ($description !== null) {
+ $this->add(Html::tag(
+ 'p',
+ null,
+ HtmlString::create(nl2br(Html::escape($description)))
+ ));
+ }
+
+ return $this;
+ }
+
+ public function render()
+ {
+ $this
+ ->setSeparator("\n")
+ ->add(Html::tag('h1', null, $this->getTitle()))
+ ->addDescription($this->getDescription())
+ ->add($this->renderDashlets());
+
+ return parent::render();
+ }
+
+ public function renderDashlets()
+ {
+ $ul = Html::tag('ul', [
+ 'class' => 'main-actions',
+ 'data-base-target' => '_next'
+ ]);
+
+ foreach ($this->dashlets() as $dashlet) {
+ if ($dashlet->shouldBeShown()) {
+ $ul->add($dashlet);
+ }
+ }
+
+ return $ul;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ abstract public function getTitle();
+
+ public function getDescription()
+ {
+ return null;
+ }
+
+ public function getTabs()
+ {
+ $lName = $this->getName();
+ $tabs = new Tabs();
+ $tabs->add($lName, new Tab([
+ 'label' => $this->translate(ucfirst($this->getName())),
+ 'url' => 'director/dashboard',
+ 'urlParams' => ['name' => $lName]
+ ]));
+
+ return $tabs;
+ }
+
+ protected function createTabsForDashboards($names)
+ {
+ $tabs = new Tabs();
+ foreach ($names as $name) {
+ $dashboard = Dashboard::loadByName($name, $this->getDb());
+ if ($dashboard->isAvailable()) {
+ $tabs->add($name, $this->createTabForDashboard($dashboard));
+ }
+ }
+
+ return $tabs;
+ }
+
+ protected function createTabForDashboard(Dashboard $dashboard)
+ {
+ $name = $dashboard->getName();
+ return new Tab([
+ 'label' => $this->translate(ucfirst($name)),
+ 'url' => 'director/dashboard',
+ 'urlParams' => ['name' => $name]
+ ]);
+ }
+
+ public function count()
+ {
+ return count($this->dashlets());
+ }
+
+ public function isAvailable()
+ {
+ return $this->count() > 0;
+ }
+
+ public function getDb()
+ {
+ return $this->db;
+ }
+
+ public function dashlets()
+ {
+ if ($this->dashlets === null) {
+ $this->loadDashlets();
+ $this->fetchDashletSummaries();
+ }
+
+ return $this->dashlets;
+ }
+
+ public function loadDashlets()
+ {
+ $names = $this->getDashletNames();
+
+ if (empty($names)) {
+ $this->dashlets = array();
+ } else {
+ $this->dashlets = Dashlet::loadByNames(
+ $this->dashletNames,
+ $this->getDb()
+ );
+ }
+ }
+
+ public function getDashletNames()
+ {
+ return $this->dashletNames;
+ }
+
+ protected function fetchDashletSummaries()
+ {
+ $types = array();
+ foreach ($this->dashlets as $dashlet) {
+ foreach ($dashlet->listRequiredStats() as $objectType) {
+ $types[$objectType] = $objectType;
+ }
+ }
+
+ if (empty($types)) {
+ return;
+ }
+
+ try {
+ $stats = $this->getObjectSummary($types);
+ } catch (Exception $e) {
+ $stats = array();
+ }
+
+ $failing = array();
+ foreach ($this->dashlets as $key => $dashlet) {
+ foreach ($dashlet->listRequiredStats() as $objectType) {
+ if (array_key_exists($objectType, $stats)) {
+ $dashlet->addStats($objectType, $stats[$objectType]);
+ } else {
+ $failing[] = $key;
+ }
+ }
+ }
+
+ foreach ($failing as $key) {
+ unset($this->dashlets[$key]);
+ }
+ }
+
+ public function getObjectSummary($types)
+ {
+ $queries = array();
+
+ foreach ($types as $type) {
+ $queries[] = $this->makeSummaryQuery($type);
+ }
+ $query = $this->db->select()->union($queries, ZfSelect::SQL_UNION_ALL);
+
+ $result = array();
+ foreach ($this->db->fetchAll($query) as $row) {
+ $result[$row->icinga_type] = $row;
+ }
+
+ return $result;
+ }
+
+ protected function makeSummaryQuery($type)
+ {
+ $columns = array(
+ 'icinga_type' => "('" . $type . "')",
+ 'cnt_object' => $this->getCntSql('object'),
+ 'cnt_template' => $this->getCntSql('template'),
+ 'cnt_external' => $this->getCntSql('external_object'),
+ 'cnt_apply' => $this->getCntSql('apply'),
+ 'cnt_total' => 'COUNT(*)',
+ );
+
+ if ($this->db->isPgsql()) {
+ $dummy = IcingaObject::createByType($type);
+ if (! $dummy->supportsApplyRules()) {
+ $columns['cnt_apply'] = '(0)';
+ }
+ }
+
+ $query = $this->db->getDbAdapter()->select()->from(
+ array('o' => 'icinga_' . $type),
+ $columns
+ );
+
+ return $this->applyRestrictions($type, $query);
+ }
+
+ protected function applyRestrictions($type, $query)
+ {
+ switch ($type) {
+ case 'host':
+ case 'hostgroup':
+ $r = new HostgroupRestriction($this->getDb(), $this->getAuth());
+ $r->applyToQuery($query);
+ break;
+ }
+
+ return $query;
+ }
+
+ protected function applyHostgroupRestrictions($query)
+ {
+ $restrictions = new HostgroupRestriction($this->getDb(), $this->getAuth());
+ $restrictions->applyToHostGroupsQuery($query);
+ }
+
+ protected function getAuth()
+ {
+ return Auth::getInstance();
+ }
+
+ protected function getCntSql($objectType)
+ {
+ return sprintf(
+ "COALESCE(SUM(CASE WHEN o.object_type = '%s' THEN 1 ELSE 0 END), 0)",
+ $objectType
+ );
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ActivityLogDashlet.php b/library/Director/Dashboard/Dashlet/ActivityLogDashlet.php
new file mode 100644
index 0000000..9794986
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ActivityLogDashlet.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ActivityLogDashlet extends Dashlet
+{
+ protected $icon = 'book';
+
+ public function getTitle()
+ {
+ return $this->translate('Activity Log');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Wondering about what changed why? Track your changes!'
+ );
+ }
+
+ public function listCssClasses()
+ {
+ return 'state-ok';
+ }
+
+ public function getUrl()
+ {
+ return 'director/config/activities';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/audit');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php b/library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php
new file mode 100644
index 0000000..419859d
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ApiUserObjectDashlet extends Dashlet
+{
+ protected $icon = 'lock-open-alt';
+
+ protected $requiredStats = array('apiuser');
+
+ public function getTitle()
+ {
+ return $this->translate('Icinga Api users');
+ }
+
+ public function getUrl()
+ {
+ return 'director/apiusers';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/BasketDashlet.php b/library/Director/Dashboard/Dashlet/BasketDashlet.php
new file mode 100644
index 0000000..10f2b81
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/BasketDashlet.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class BasketDashlet extends Dashlet
+{
+ protected $icon = 'tag';
+
+ public function getTitle()
+ {
+ return $this->translate('Configuration Baskets');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Preserve specific configuration objects in a specific state'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/baskets';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php b/library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php
new file mode 100644
index 0000000..65d8c8c
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class CheckCommandsDashlet extends Dashlet
+{
+ protected $icon = 'wrench';
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Manage definitions for your Commands that should be executed as'
+ . ' Check Plugins, Notifications or based on Events'
+ );
+ }
+
+ public function getTitle()
+ {
+ return $this->translate('Commands');
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+
+ public function getUrl()
+ {
+ return 'director/commands';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ChoicesDashlet.php b/library/Director/Dashboard/Dashlet/ChoicesDashlet.php
new file mode 100644
index 0000000..efdbba5
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ChoicesDashlet.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+abstract class ChoicesDashlet extends Dashlet
+{
+ protected $icon = 'flapping';
+
+ public function getTitle()
+ {
+ return $this->translate('Choices');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Combine multiple templates into meaningful Choices, making life'
+ . ' easier for your users'
+ );
+ }
+
+ protected function getType()
+ {
+ return strtolower(substr(
+ substr(get_called_class(), strlen(__NAMESPACE__) + 1),
+ 0,
+ - strlen('ChoicesDashlet')
+ ));
+ }
+
+ public function getUrl()
+ {
+
+ return 'director/templatechoices/' . $this->getType();
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/CommandObjectDashlet.php b/library/Director/Dashboard/Dashlet/CommandObjectDashlet.php
new file mode 100644
index 0000000..083172e
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/CommandObjectDashlet.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class CommandObjectDashlet extends Dashlet
+{
+ protected $icon = 'wrench';
+
+ protected $requiredStats = array('command');
+
+ public function getTitle()
+ {
+ return $this->translate('Commands');
+ }
+
+ public function getUrl()
+ {
+ return 'director/dashboard?name=commands';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php b/library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php
new file mode 100644
index 0000000..512298a
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/CommandTemplatesDashlet.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class CommandTemplatesDashlet extends CheckCommandsDashlet
+{
+ protected $icon = 'cubes';
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'External Notification Commands have been defined in your local Icinga 2'
+ . ' Configuration.'
+ );
+ }
+
+ public function getTitle()
+ {
+ return $this->translate('Command Templates');
+ }
+
+ public function getUrl()
+ {
+ return 'director/commands/templates';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/CustomvarDashlet.php b/library/Director/Dashboard/Dashlet/CustomvarDashlet.php
new file mode 100644
index 0000000..919c06b
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/CustomvarDashlet.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class CustomvarDashlet extends Dashlet
+{
+ protected $icon = 'keyboard';
+
+ public function getTitle()
+ {
+ return $this->translate('CustomVar Overview');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Get an overview of used CustomVars and their variants'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/data/vars';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/Dashlet.php b/library/Director/Dashboard/Dashlet/Dashlet.php
new file mode 100644
index 0000000..f8bc708
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/Dashlet.php
@@ -0,0 +1,239 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+use Icinga\Module\Director\Acl;
+use Icinga\Module\Director\Db;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+
+abstract class Dashlet extends BaseHtmlElement
+{
+ use TranslationHelper;
+
+ /** @var Db */
+ protected $db;
+
+ protected $tag = 'li';
+
+ protected $icon = 'help';
+
+ protected $stats;
+
+ protected $requiredStats = [];
+
+ public function __construct(Db $db)
+ {
+ $this->db = $db;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function listRequiredStats()
+ {
+ return $this->requiredStats;
+ }
+
+ public function addStats($type, $stats)
+ {
+ $this->stats[$type] = $stats;
+ }
+
+ /**
+ * @param $name
+ * @param Db $db
+ * @return Dashlet
+ */
+ public static function loadByName($name, Db $db)
+ {
+ /** @var Dashlet */
+ $class = __NAMESPACE__ . '\\' . $name . 'Dashlet';
+ return new $class($db);
+ }
+
+ public static function loadByNames(array $names, Db $db)
+ {
+ $dashlets = [];
+ foreach ($names as $name) {
+ $dashlet = static::loadByName($name, $db);
+
+ if ($dashlet->isAllowed()) {
+ $dashlets[] = $dashlet;
+ }
+ }
+
+ return $dashlets;
+ }
+
+ public function listCssClasses()
+ {
+ return [];
+ }
+
+ public function getIconName()
+ {
+ return $this->icon;
+ }
+
+ abstract public function getTitle();
+
+ abstract public function getUrl();
+
+ protected function assemble()
+ {
+ $this->add(Link::create([
+ $this->getTitle(),
+ Icon::create($this->getIconName()),
+ Html::tag('p', null, $this->getSummary())
+ ], $this->getUrl(), null, [
+ 'class' => $this->listCssClasses()
+ ]));
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array($this->getUrl());
+ }
+
+ public function isAllowed()
+ {
+ $acl = Acl::instance();
+ foreach ($this->listRequiredPermissions() as $perm) {
+ if (! $acl->hasPermission($perm)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function shouldBeShown()
+ {
+ return true;
+ }
+
+ public function getSummary()
+ {
+ $result = '';
+ if (! empty($this->requiredStats)) {
+ reset($this->requiredStats);
+ $result .= $this->statSummary(current($this->requiredStats));
+ }
+
+ return $result;
+ }
+
+ public function getStats($type, $name = null)
+ {
+ if ($name === null) {
+ return $this->stats[$type];
+ } else {
+ return $this->stats[$type]->{'cnt_' . $name};
+ }
+ }
+
+ protected function getTemplateSummaryText($type)
+ {
+ $cnt = (int) $this->stats[$type]->cnt_template;
+
+ if ($cnt === 0) {
+ return $this->translate('No template has been defined yet');
+ }
+
+ if ($cnt === 1) {
+ return $this->translate('One template has been defined');
+ }
+
+ return sprintf(
+ $this->translate('%d templates have been defined'),
+ $cnt
+ );
+ }
+
+ protected function getApplySummaryText($type)
+ {
+ $cnt = (int) $this->stats[$type]->cnt_apply;
+
+ if ($cnt === 0) {
+ return $this->translate('No apply rule has been defined yet');
+ }
+
+ if ($cnt === 1) {
+ return $this->translate('One apply rule has been defined');
+ }
+
+ return sprintf(
+ $this->translate('%d apply rules have been defined'),
+ $cnt
+ );
+ }
+
+ protected function statSummary($type)
+ {
+ $stat = $this->stats[$type];
+
+ if ((int) $stat->cnt_total === 0) {
+ return $this->translate('No object has been defined yet');
+ }
+
+ if ((int) $stat->cnt_total === 1) {
+ if ($stat->cnt_template > 0) {
+ $msg = $this->translate('One template has been defined');
+ } elseif ($stat->cnt_external > 0) {
+ $msg = $this->translate(
+ 'One external object has been defined, it will not be deployed'
+ );
+ } else {
+ $msg = $this->translate('One object has been defined');
+ }
+ } else {
+ $msg = sprintf(
+ $this->translate('%d objects have been defined'),
+ $stat->cnt_total
+ );
+ }
+
+ $extra = array();
+ if ($stat->cnt_total !== $stat->cnt_object) {
+ if ($stat->cnt_template > 0) {
+ $extra[] = sprintf(
+ $this->translate('%d of them are templates'),
+ $stat->cnt_template
+ );
+ }
+
+ if ($stat->cnt_external > 0) {
+ $extra[] = sprintf(
+ $this->translate(
+ '%d have been externally defined and will not be deployed'
+ ),
+ $stat->cnt_external
+ );
+ }
+ }
+
+ if (array_key_exists($type . 'group', $this->stats)) {
+ $groupstat = $this->stats[$type . 'group'];
+ if ((int) $groupstat->cnt_total === 0) {
+ $extra[] = $this->translate('no related group exists');
+ } elseif ((int) $groupstat->cnt_total === 1) {
+ $extra[] = $this->translate('one related group exists');
+ } else {
+ $extra[] = sprintf(
+ $this->translate('%s related group objects have been created'),
+ $groupstat->cnt_total
+ );
+ }
+ }
+
+ if (empty($extra)) {
+ return $msg;
+ }
+
+ return $msg . ', ' . implode(', ', $extra);
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php b/library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php
new file mode 100644
index 0000000..6efb4ca
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class DatafieldCategoryDashlet extends Dashlet
+{
+ protected $icon = 'th-list';
+
+ public function getTitle()
+ {
+ return $this->translate('Data Field Categories');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Categories bring structure to your Data Fields'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/data/fieldcategories';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/DatafieldDashlet.php b/library/Director/Dashboard/Dashlet/DatafieldDashlet.php
new file mode 100644
index 0000000..03f2d8d
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/DatafieldDashlet.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class DatafieldDashlet extends Dashlet
+{
+ protected $icon = 'edit';
+
+ public function getTitle()
+ {
+ return $this->translate('Define Data Fields');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Data fields make sure that configuration fits your rules'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/data/fields';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/DatalistDashlet.php b/library/Director/Dashboard/Dashlet/DatalistDashlet.php
new file mode 100644
index 0000000..bdf179f
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/DatalistDashlet.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class DatalistDashlet extends Dashlet
+{
+ protected $icon = 'sort-name-up';
+
+ public function getTitle()
+ {
+ return $this->translate('Provide Data Lists');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Provide data lists to make life easier for your users'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/data/lists';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php b/library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php
new file mode 100644
index 0000000..47a18aa
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class DependencyObjectDashlet extends Dashlet
+{
+ protected $icon = 'sitemap';
+
+ protected $requiredStats = array('dependency');
+
+ public function getTitle()
+ {
+ return $this->translate('Dependencies');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate('Object dependency relationships.')
+ . ' ' . parent::getSummary();
+ }
+
+ public function getUrl()
+ {
+ return 'director/dependencies/applyrules';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/DeploymentDashlet.php b/library/Director/Dashboard/Dashlet/DeploymentDashlet.php
new file mode 100644
index 0000000..7a52793
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/DeploymentDashlet.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+use Exception;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+
+class DeploymentDashlet extends Dashlet
+{
+ protected $icon = 'wrench';
+
+ protected $undeployedActivities;
+
+ protected $lastDeployment;
+
+ public function getTitle()
+ {
+ return $this->translate('Config Deployment');
+ }
+
+ public function hasUndeployedActivities()
+ {
+ return $this->undeployedActivities() > 0;
+ }
+
+ public function undeployedActivities()
+ {
+ if ($this->undeployedActivities === null) {
+ try {
+ $this->undeployedActivities = $this->db
+ ->countActivitiesSinceLastDeployedConfig();
+ } catch (Exception $e) {
+ $this->undeployedActivities = 0;
+ }
+ }
+
+ return $this->undeployedActivities;
+ }
+
+ public function lastDeploymentFailed()
+ {
+ return ! $this->lastDeployment()->succeeded();
+ }
+
+ public function lastDeploymentPending()
+ {
+ return $this->lastDeployment()->isPending();
+ }
+
+ public function listCssClasses()
+ {
+ try {
+ if ($this->lastDeploymentFailed()) {
+ return array('state-critical');
+ } elseif ($this->lastDeploymentPending()) {
+ return array('state-pending');
+ } elseif ($this->hasUndeployedActivities()) {
+ return array('state-warning');
+ } else {
+ return array('state-ok');
+ }
+ } catch (Exception $e) {
+ return null;
+ }
+ }
+
+ protected function lastDeployment()
+ {
+ if ($this->lastDeployment === null) {
+ $this->lastDeployment = DirectorDeploymentLog::loadLatest($this->db);
+ }
+
+ return $this->lastDeployment;
+ }
+
+ public function getSummary()
+ {
+ $msgs = array();
+ $cnt = $this->undeployedActivities();
+
+ try {
+ if ($this->lastDeploymentFailed()) {
+ $msgs[] = $this->translate('The last deployment did not succeed');
+ } elseif ($this->lastDeploymentPending()) {
+ $msgs[] = $this->translate('The last deployment is currently pending');
+ }
+ } catch (Exception $e) {
+ }
+
+ if ($cnt === 0) {
+ $msgs[] = $this->translate('There are no pending changes');
+ } else {
+ $msgs[] = sprintf(
+ $this->translate(
+ 'A total of %d config changes happened since your last'
+ . ' deployed config has been rendered'
+ ),
+ $cnt
+ );
+ }
+
+ return implode('. ', $msgs) . '.';
+ }
+
+ public function getUrl()
+ {
+ return 'director/config/deployments';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/deploy');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php b/library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php
new file mode 100644
index 0000000..9dd9467
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+use Exception;
+
+class EndpointObjectDashlet extends Dashlet
+{
+ protected $icon = 'cloud';
+
+ protected $requiredStats = array('endpoint');
+
+ protected $hasDeploymentEndpoint;
+
+ public function getTitle()
+ {
+ return $this->translate('Endpoints');
+ }
+
+ public function getUrl()
+ {
+ return 'director/endpoints';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+
+ protected function hasDeploymentEndpoint()
+ {
+ if ($this->hasDeploymentEndpoint === null) {
+ try {
+ $this->hasDeploymentEndpoint = $this->db->hasDeploymentEndpoint();
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ return $this->hasDeploymentEndpoint;
+ }
+
+ public function listCssClasses()
+ {
+ if (! $this->hasDeploymentEndpoint()) {
+ return 'state-critical';
+ }
+
+ return null;
+ }
+
+ public function getSummary()
+ {
+ $msg = parent::getSummary();
+ if (! $this->hasDeploymentEndpoint()) {
+ $msg .= '. ' . $this->translate(
+ 'None could be used for deployments right now'
+ );
+ }
+
+ return $msg;
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php b/library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php
new file mode 100644
index 0000000..2711fb9
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ExternalCheckCommandsDashlet.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ExternalCheckCommandsDashlet extends CheckCommandsDashlet
+{
+ protected $icon = 'download';
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'External Commands have been defined in your local Icinga 2'
+ . ' Configuration.'
+ );
+ }
+
+ public function getTitle()
+ {
+ return $this->translate('External Commands');
+ }
+
+ public function getUrl()
+ {
+ return 'director/commands?type=external_object';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php b/library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php
new file mode 100644
index 0000000..435d0cb
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ExternalNotificationCommandsDashlet.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ExternalNotificationCommandsDashlet extends CheckCommandsDashlet
+{
+ protected $icon = 'wrench';
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'External Notification Commands have been defined in your local Icinga 2'
+ . ' Configuration.'
+ );
+ }
+
+ public function getTitle()
+ {
+ return $this->translate('External Notification Commands');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/HostChoicesDashlet.php b/library/Director/Dashboard/Dashlet/HostChoicesDashlet.php
new file mode 100644
index 0000000..98bfe32
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/HostChoicesDashlet.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class HostChoicesDashlet extends ChoicesDashlet
+{
+}
diff --git a/library/Director/Dashboard/Dashlet/HostGroupsDashlet.php b/library/Director/Dashboard/Dashlet/HostGroupsDashlet.php
new file mode 100644
index 0000000..5d3b25f
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/HostGroupsDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class HostGroupsDashlet extends Dashlet
+{
+ protected $icon = 'tags';
+
+ public function getTitle()
+ {
+ return $this->translate('Host Groups');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Define Host Groups to give your configuration more structure. They'
+ . ' are useful for Dashboards, Notifications or Restrictions'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/hostgroups';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/hostgroups');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/HostObjectDashlet.php b/library/Director/Dashboard/Dashlet/HostObjectDashlet.php
new file mode 100644
index 0000000..10cff94
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/HostObjectDashlet.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class HostObjectDashlet extends Dashlet
+{
+ protected $icon = 'host';
+
+ protected $requiredStats = array('host', 'hostgroup');
+
+ public function getTitle()
+ {
+ return $this->translate('Host objects');
+ }
+
+ public function listRequiredPermissions()
+ {
+ return ['director/hosts'];
+ }
+
+ public function getUrl()
+ {
+ return 'director/dashboard?name=hosts';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php b/library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php
new file mode 100644
index 0000000..09bed17
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class HostTemplatesDashlet extends Dashlet
+{
+ protected $icon = 'cubes';
+
+ public function getTitle()
+ {
+ return $this->translate('Host Templates');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Manage your Host Templates. Use Fields to make it easy for'
+ . ' your users to get them customized.'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/hosts/templates';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/HostsDashlet.php b/library/Director/Dashboard/Dashlet/HostsDashlet.php
new file mode 100644
index 0000000..39c1421
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/HostsDashlet.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class HostsDashlet extends Dashlet
+{
+ protected $icon = 'host';
+
+ public function getTitle()
+ {
+ return $this->translate('Hosts');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'This is where you add all your servers, containers, network or'
+ . ' sensor devices - and much more. Every subject worth to be'
+ . ' monitored'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/hosts';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return ['director/hosts'];
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ImportSourceDashlet.php b/library/Director/Dashboard/Dashlet/ImportSourceDashlet.php
new file mode 100644
index 0000000..302c1ed
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ImportSourceDashlet.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+use Exception;
+use Icinga\Module\Director\Objects\ImportSource;
+
+class ImportSourceDashlet extends Dashlet
+{
+ protected $icon = 'database';
+
+ public function getTitle()
+ {
+ return $this->translate('Import data sources');
+ }
+
+ public function listCssClasses()
+ {
+ try {
+ return $this->fetchStateClass();
+ } catch (Exception $e) {
+ return 'state-critical';
+ }
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Define and manage imports from various data sources'
+ );
+ }
+
+ protected function fetchStateClass()
+ {
+ $srcs = ImportSource::loadAll($this->db);
+ if (count($srcs) > 0) {
+ $state = 'state-ok';
+ } else {
+ $state = null;
+ }
+
+ foreach ($srcs as $src) {
+ if ($src->import_state !== 'in-sync') {
+ if ($src->import_state === 'failing') {
+ $state = 'state-critical';
+ break;
+ } else {
+ $state = 'state-warning';
+ }
+ }
+ }
+
+ return $state;
+ }
+
+ public function getUrl()
+ {
+ return 'director/importsources';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/InfrastructureDashlet.php b/library/Director/Dashboard/Dashlet/InfrastructureDashlet.php
new file mode 100644
index 0000000..328df72
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/InfrastructureDashlet.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class InfrastructureDashlet extends Dashlet
+{
+ protected $icon = 'cloud';
+
+ public function getTitle()
+ {
+ return $this->translate('Icinga Infrastructure');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Manage your Icinga 2 infrastructure: Masters, Zones, Satellites and more'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/dashboard?name=infrastructure';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/JobDashlet.php b/library/Director/Dashboard/Dashlet/JobDashlet.php
new file mode 100644
index 0000000..d7452e0
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/JobDashlet.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+use Exception;
+use Icinga\Module\Director\Objects\DirectorJob;
+
+class JobDashlet extends Dashlet
+{
+ protected $icon = 'clock';
+
+ public function getTitle()
+ {
+ return $this->translate('Jobs');
+ }
+
+ public function listCssClasses()
+ {
+ try {
+ return $this->fetchStateClass();
+ } catch (Exception $e) {
+ return 'state-critical';
+ }
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Schedule and automate Import, Syncronization, Config Deployment,'
+ . ' Housekeeping and more'
+ );
+ }
+
+ protected function fetchStateClass()
+ {
+ /** @var DirectorJob[] $jobs */
+ $jobs = DirectorJob::loadAll($this->db);
+ if (count($jobs) > 0) {
+ $state = 'state-ok';
+ } else {
+ $state = null;
+ }
+
+ foreach ($jobs as $job) {
+ if ($job->isPending()) {
+ $state = 'state-pending';
+ } elseif (! $job->lastAttemptSucceeded()) {
+ $state = 'state-critical';
+ break;
+ }
+ }
+
+ return $state;
+ }
+
+ public function getUrl()
+ {
+ return 'director/jobs';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/KickstartDashlet.php b/library/Director/Dashboard/Dashlet/KickstartDashlet.php
new file mode 100644
index 0000000..09801f5
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/KickstartDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class KickstartDashlet extends Dashlet
+{
+ protected $icon = 'gauge';
+
+ public function getTitle()
+ {
+ return $this->translate('Kickstart Wizard');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'This synchronizes Icinga Director to your Icinga 2 infrastructure.'
+ . ' A new run should be triggered on infrastructure changes'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/kickstart';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php b/library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php
new file mode 100644
index 0000000..e0b0443
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class NotificationApplyDashlet extends Dashlet
+{
+ protected $icon = 'bell';
+
+ protected $requiredStats = array('notification');
+
+ public function getTitle()
+ {
+ return $this->translate('Notifications');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Apply notifications with specific properties according to given rules.'
+ ) . ' ' . $this->getApplySummaryText('notification');
+ }
+
+ public function shouldBeShown()
+ {
+ return $this->getStats('notification', 'template') > 0;
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/notifications');
+ }
+
+ public function getUrl()
+ {
+ return 'director/notifications/applyrules';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php b/library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php
new file mode 100644
index 0000000..0a640ae
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/NotificationCommandsDashlet.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class NotificationCommandsDashlet extends CheckCommandsDashlet
+{
+ protected $icon = 'wrench';
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Notification Commands allow you to trigger any action you want when'
+ . ' a notification takes place'
+ );
+ }
+
+ public function getTitle()
+ {
+ return $this->translate('Notification Commands');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php b/library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php
new file mode 100644
index 0000000..a58b5d0
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class NotificationTemplateDashlet extends Dashlet
+{
+ protected $icon = 'cubes';
+
+ protected $requiredStats = array('notification');
+
+ public function getTitle()
+ {
+ return $this->translate('Notification templates');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate('Provide templates for your notifications.')
+ . ' ' . $this->getTemplateSummaryText('notification');
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+
+ public function getUrl()
+ {
+ return 'director/notifications/templates';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/NotificationsDashlet.php b/library/Director/Dashboard/Dashlet/NotificationsDashlet.php
new file mode 100644
index 0000000..85610f0
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/NotificationsDashlet.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class NotificationsDashlet extends Dashlet
+{
+ protected $icon = 'bell';
+
+ protected $requiredStats = array('notification');
+
+ public function getTitle()
+ {
+ return $this->translate('Notifications');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Schedule your notifications. Define who should be notified, when,'
+ . ' and for which kind of problem'
+ );
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/notifications');
+ }
+
+ public function getUrl()
+ {
+ return 'director/dashboard?name=notifications';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php b/library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php
new file mode 100644
index 0000000..45bcfa2
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ScheduledDowntimeApplyDashlet extends Dashlet
+{
+ protected $icon = 'plug';
+
+ protected $requiredStats = ['scheduled_downtime'];
+
+ public function getTitle()
+ {
+ return $this->translate('Scheduled Downtimes');
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/scheduled-downtimes');
+ }
+
+ public function getUrl()
+ {
+ return 'director/scheduled-downtimes/applyrules';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/SelfServiceDashlet.php b/library/Director/Dashboard/Dashlet/SelfServiceDashlet.php
new file mode 100644
index 0000000..32b1cfa
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/SelfServiceDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class SelfServiceDashlet extends Dashlet
+{
+ protected $icon = 'chat';
+
+ public function getTitle()
+ {
+ return $this->translate('Self Service API');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Icinga Director offers a Self Service API, allowing new Icinga'
+ . ' nodes to register themselves'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/settings/self-service';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php b/library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php
new file mode 100644
index 0000000..b4bee04
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ServiceApplyRulesDashlet extends Dashlet
+{
+ protected $icon = 'resize-full-alt';
+
+ public function getTitle()
+ {
+ return $this->translate('Service Apply Rules');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Using Apply Rules a Service can be applied to multiple hosts at once,'
+ . ' based on filters dealing with any combination of their properties'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/services/applyrules';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ServiceChoicesDashlet.php b/library/Director/Dashboard/Dashlet/ServiceChoicesDashlet.php
new file mode 100644
index 0000000..ff23321
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ServiceChoicesDashlet.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ServiceChoicesDashlet extends ChoicesDashlet
+{
+}
diff --git a/library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php b/library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php
new file mode 100644
index 0000000..ad47768
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ServiceGroupsDashlet extends Dashlet
+{
+ protected $icon = 'tags';
+
+ public function getTitle()
+ {
+ return $this->translate('Service Groups');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Defining Service Groups get more structure. Great for Dashboards.'
+ . ' Notifications and Permissions might be based on groups.'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/servicegroups';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php b/library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php
new file mode 100644
index 0000000..01fb800
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+use Icinga\Module\Director\Acl;
+
+class ServiceObjectDashlet extends Dashlet
+{
+ protected $icon = 'services';
+
+ protected $requiredStats = array('service', 'servicegroup');
+
+ public function getTitle()
+ {
+ return $this->translate('Monitored Services');
+ }
+
+ public function getUrl()
+ {
+ return 'director/dashboard?name=services';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return ['director/services'];
+ }
+
+ public function isAllowed()
+ {
+ $acl = Acl::instance();
+ return $acl->hasPermission('director/services')
+ || $acl->hasPermission('director/service_sets');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php b/library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php
new file mode 100644
index 0000000..f971d42
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ServiceSetsDashlet extends Dashlet
+{
+ protected $icon = 'services';
+
+ public function getTitle()
+ {
+ return $this->translate('Service Sets');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Grouping your Services into Sets allow you to quickly assign services'
+ . ' often used together in a single operation all at once'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/services/sets';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/servicesets');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php b/library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php
new file mode 100644
index 0000000..62d1b41
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ServiceTemplatesDashlet extends Dashlet
+{
+ protected $icon = 'cubes';
+
+ public function getTitle()
+ {
+ return $this->translate('Service Templates');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Manage your Service Templates. Use Fields to make it easy for'
+ . ' your users to get them customized.'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/services/templates';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/SettingsDashlet.php b/library/Director/Dashboard/Dashlet/SettingsDashlet.php
new file mode 100644
index 0000000..716e565
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/SettingsDashlet.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class SettingsDashlet extends Dashlet
+{
+ protected $icon = 'edit';
+
+ public function getTitle()
+ {
+ return $this->translate('Director Settings');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Tweak some global Director settings'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/config/settings';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/SingleServicesDashlet.php b/library/Director/Dashboard/Dashlet/SingleServicesDashlet.php
new file mode 100644
index 0000000..297b3f8
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/SingleServicesDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class SingleServicesDashlet extends Dashlet
+{
+ protected $icon = 'service';
+
+ public function getTitle()
+ {
+ return $this->translate('Single Services');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Here you can find all single services directly attached to single'
+ . ' hosts'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/services';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/services');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/SyncDashlet.php b/library/Director/Dashboard/Dashlet/SyncDashlet.php
new file mode 100644
index 0000000..4ac689a
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/SyncDashlet.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+use Exception;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class SyncDashlet extends Dashlet
+{
+ protected $icon = 'flapping';
+
+ public function getTitle()
+ {
+ return $this->translate('Synchronize');
+ }
+
+ public function listCssClasses()
+ {
+ try {
+ return $this->fetchStateClass();
+ } catch (Exception $e) {
+ return 'state-critical';
+ }
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Define how imported data should be synchronized with Icinga'
+ );
+ }
+
+ protected function fetchStateClass()
+ {
+ $syncs = SyncRule::loadAll($this->db);
+ if (count($syncs) > 0) {
+ $state = 'state-ok';
+ } else {
+ $state = null;
+ }
+
+ foreach ($syncs as $sync) {
+ if ($sync->sync_state !== 'in-sync') {
+ if ($sync->sync_state === 'failing') {
+ $state = 'state-critical';
+ break;
+ } else {
+ $state = 'state-warning';
+ }
+ }
+ }
+
+ return $state;
+ }
+
+ public function getUrl()
+ {
+ return 'director/syncrules';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php b/library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php
new file mode 100644
index 0000000..ba4c1db
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+use DirectoryIterator;
+use Icinga\Exception\ProgrammingError;
+
+class TimeperiodObjectDashlet extends Dashlet
+{
+ protected $icon = 'calendar';
+
+ protected $requiredStats = array('timeperiod');
+
+ public function getTitle()
+ {
+ return $this->translate('Timeperiods');
+ }
+
+ public function getUrl()
+ {
+ return 'director/timeperiods';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php b/library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php
new file mode 100644
index 0000000..26339e4
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class TimeperiodTemplateDashlet extends Dashlet
+{
+ protected $icon = 'cubes';
+
+ protected $requiredStats = array('timeperiod');
+
+ public function getTitle()
+ {
+ return $this->translate('Timeperiod Templates');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate('Provide templates for your TimePeriod objects.')
+ . ' ' . $this->getTemplateSummaryText('timeperiod');
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+
+ public function getUrl()
+ {
+ return 'director/timeperiods/templates';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php b/library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php
new file mode 100644
index 0000000..5a54bec
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class TimeperiodsDashlet extends Dashlet
+{
+ protected $icon = 'calendar';
+
+ protected $requiredStats = array('timeperiod');
+
+ public function getTitle()
+ {
+ return $this->translate('Timeperiods');
+ }
+
+ public function getUrl()
+ {
+ return 'director/dashboard?name=timeperiods';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/UserGroupsDashlet.php b/library/Director/Dashboard/Dashlet/UserGroupsDashlet.php
new file mode 100644
index 0000000..3fba4ba
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/UserGroupsDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class UserGroupsDashlet extends Dashlet
+{
+ protected $icon = 'tags';
+
+ public function getTitle()
+ {
+ return $this->translate('User Groups');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate(
+ 'Defining Notifications for User Groups instead of single Users'
+ . ' gives more flexibility'
+ );
+ }
+
+ public function getUrl()
+ {
+ return 'director/usergroups';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/UserObjectDashlet.php b/library/Director/Dashboard/Dashlet/UserObjectDashlet.php
new file mode 100644
index 0000000..463b84c
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/UserObjectDashlet.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+use DirectoryIterator;
+use Icinga\Exception\ProgrammingError;
+
+class UserObjectDashlet extends Dashlet
+{
+ protected $icon = 'users';
+
+ protected $requiredStats = array('user', 'usergroup');
+
+ public function getTitle()
+ {
+ return $this->translate('Users / Contacts');
+ }
+
+ public function getUrl()
+ {
+ return 'director/users';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/UserTemplateDashlet.php b/library/Director/Dashboard/Dashlet/UserTemplateDashlet.php
new file mode 100644
index 0000000..291ab05
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/UserTemplateDashlet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class UserTemplateDashlet extends Dashlet
+{
+ protected $icon = 'cubes';
+
+ protected $requiredStats = array('user');
+
+ public function getTitle()
+ {
+ return $this->translate('User Templates');
+ }
+
+ public function getSummary()
+ {
+ return $this->translate('Provide templates for your User objects.')
+ . ' ' . $this->getTemplateSummaryText('user');
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+
+ public function getUrl()
+ {
+ return 'director/users/templates';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/UsersDashlet.php b/library/Director/Dashboard/Dashlet/UsersDashlet.php
new file mode 100644
index 0000000..43ddc26
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/UsersDashlet.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class UsersDashlet extends Dashlet
+{
+ protected $icon = 'users';
+
+ protected $requiredStats = array('user', 'usergroup');
+
+ public function getTitle()
+ {
+ return $this->translate('Users / Contacts');
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/users');
+ }
+
+ public function getUrl()
+ {
+ return 'director/dashboard?name=users';
+ }
+}
diff --git a/library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php b/library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php
new file mode 100644
index 0000000..ee789f2
--- /dev/null
+++ b/library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard\Dashlet;
+
+class ZoneObjectDashlet extends Dashlet
+{
+ protected $icon = 'globe';
+
+ protected $requiredStats = array('zone');
+
+ public function getTitle()
+ {
+ return $this->translate('Zones');
+ }
+
+ public function getUrl()
+ {
+ return 'director/zones';
+ }
+
+ public function listRequiredPermissions()
+ {
+ return array('director/admin');
+ }
+}
diff --git a/library/Director/Dashboard/DataDashboard.php b/library/Director/Dashboard/DataDashboard.php
new file mode 100644
index 0000000..36a807b
--- /dev/null
+++ b/library/Director/Dashboard/DataDashboard.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class DataDashboard extends Dashboard
+{
+ protected $dashletNames = [
+ 'Datafield',
+ 'DatafieldCategory',
+ 'Datalist',
+ 'Customvar'
+ ];
+
+ public function getTitle()
+ {
+ return $this->translate('Do more with custom data');
+ }
+}
diff --git a/library/Director/Dashboard/DeploymentDashboard.php b/library/Director/Dashboard/DeploymentDashboard.php
new file mode 100644
index 0000000..6cd2005
--- /dev/null
+++ b/library/Director/Dashboard/DeploymentDashboard.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class DeploymentDashboard extends Dashboard
+{
+ protected $dashletNames = array(
+ 'ActivityLog',
+ 'Deployment',
+ 'Infrastructure',
+ );
+
+ public function getTitle()
+ {
+ return $this->translate('Deploy configuration to your Icinga nodes');
+ }
+}
diff --git a/library/Director/Dashboard/DirectorDashboard.php b/library/Director/Dashboard/DirectorDashboard.php
new file mode 100644
index 0000000..47a17af
--- /dev/null
+++ b/library/Director/Dashboard/DirectorDashboard.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class DirectorDashboard extends Dashboard
+{
+ protected $dashletNames = array(
+ 'Settings',
+ 'Basket',
+ 'SelfService',
+ );
+
+ public function getTitle()
+ {
+ return $this->translate('Icinga Director Configuration');
+ }
+}
diff --git a/library/Director/Dashboard/HostsDashboard.php b/library/Director/Dashboard/HostsDashboard.php
new file mode 100644
index 0000000..281accb
--- /dev/null
+++ b/library/Director/Dashboard/HostsDashboard.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class HostsDashboard extends Dashboard
+{
+ protected $dashletNames = array(
+ 'Hosts',
+ 'HostTemplates',
+ 'HostGroups',
+ 'HostChoices',
+ );
+
+ public function getTitle()
+ {
+ return $this->translate('Manage your Icinga Hosts');
+ }
+
+ public function getDescription()
+ {
+ return $this->translate(
+ 'This is where you manage your Icinga 2 Host Checks. Host templates'
+ . ' are your main building blocks. You can bundle them to "choices",'
+ . ' allowing (or forcing) your users to choose among a given set of'
+ . ' preconfigured templates.'
+ );
+ }
+
+ public function getTabs()
+ {
+ return $this->createTabsForDashboards(
+ ['hosts', 'services', 'commands']
+ );
+ }
+}
diff --git a/library/Director/Dashboard/InfrastructureDashboard.php b/library/Director/Dashboard/InfrastructureDashboard.php
new file mode 100644
index 0000000..2b369fc
--- /dev/null
+++ b/library/Director/Dashboard/InfrastructureDashboard.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Web\Tabs\InfraTabs;
+use Icinga\Module\Director\Web\Widget\Documentation;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+
+class InfrastructureDashboard extends Dashboard
+{
+ protected $dashletNames = array(
+ 'Kickstart',
+ 'ApiUserObject',
+ 'EndpointObject',
+ 'ZoneObject',
+ );
+
+ public function getTitle()
+ {
+ return $this->translate('Manage your Icinga Infrastructure');
+ }
+
+ public function getDescription()
+ {
+ $documentation = new Documentation(Icinga::app(), Auth::getInstance());
+
+ $link = $documentation->getModuleLink(
+ $this->translate('documentation'),
+ 'director',
+ '24-Working-with-agents',
+ $this->translate('Working with Agents and Config Zones')
+ );
+ return (new HtmlDocument())->add([
+ $this->translate(
+ 'This is where you manage your Icinga 2 infrastructure. When adding'
+ . ' a new Icinga Master or Satellite please re-run the Kickstart'
+ . ' Helper once.'
+ ),
+ Hint::warning($this->translate(
+ 'When you feel the desire to manually create Zone or Endpoint'
+ . ' objects please rethink this twice. Doing so is mostly the wrong'
+ . ' way, might lead to a dead end, requiring quite some effort to'
+ . ' clean up the whole mess afterwards.'
+ )),
+ Html::sprintf(
+ $this->translate('Want to connect to your Icinga Agents? Have a look at our %s!'),
+ $link
+ )
+ ]);
+ }
+
+ public function getTabs()
+ {
+ return new InfraTabs($this->getAuth());
+ }
+}
diff --git a/library/Director/Dashboard/NotificationsDashboard.php b/library/Director/Dashboard/NotificationsDashboard.php
new file mode 100644
index 0000000..b7d72f5
--- /dev/null
+++ b/library/Director/Dashboard/NotificationsDashboard.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class NotificationsDashboard extends Dashboard
+{
+ protected $dashletNames = [
+ 'NotificationApply',
+ 'NotificationTemplate',
+ ];
+
+ public function getTitle()
+ {
+ return $this->translate('Schedule your notifications');
+ }
+
+ public function getDescription()
+ {
+ return $this->translate(
+ 'Notifications are sent when a host or service reaches a non-ok hard'
+ . ' state or recovers from such. One might also want to send them for'
+ . ' special events like when a Downtime starts, a problem gets'
+ . ' acknowledged and much more. You can send specific notifications'
+ . ' only within specific time periods, you can delay them and of course'
+ . ' re-notify at specific intervals.'
+ . "\n\n"
+ . ' Combine those possibilities in case you need to define escalation'
+ . ' levels, like notifying operators first and your management later on'
+ . ' in case the problem remains unhandled for a certain time.'
+ . "\n\n"
+ . ' You might send E-Mail or SMS, make phone calls or page on various'
+ . ' channels. You could also delegate notifications to external service'
+ . ' providers. The possibilities are endless, as you are allowed to'
+ . ' define as many custom notification commands as you want'
+ );
+ }
+
+ public function getTabs()
+ {
+ return $this->createTabsForDashboards(
+ ['notifications', 'users', 'timeperiods']
+ );
+ }
+}
diff --git a/library/Director/Dashboard/ObjectsDashboard.php b/library/Director/Dashboard/ObjectsDashboard.php
new file mode 100644
index 0000000..02c2a4b
--- /dev/null
+++ b/library/Director/Dashboard/ObjectsDashboard.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class ObjectsDashboard extends Dashboard
+{
+ protected $dashletNames = array(
+ 'HostObject',
+ 'ServiceObject',
+ 'CommandObject',
+ );
+
+ public function getTitle()
+ {
+ return $this->translate('Define whatever you want to be monitored');
+ }
+}
diff --git a/library/Director/Dashboard/ServicesDashboard.php b/library/Director/Dashboard/ServicesDashboard.php
new file mode 100644
index 0000000..65c8f0a
--- /dev/null
+++ b/library/Director/Dashboard/ServicesDashboard.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class ServicesDashboard extends Dashboard
+{
+ protected $dashletNames = array(
+ 'SingleServices',
+ 'ServiceApplyRules',
+ 'ServiceTemplates',
+ 'ServiceGroups',
+ 'ServiceChoices',
+ 'ServiceSets'
+ );
+
+ public function getTitle()
+ {
+ return $this->translate('Manage your Icinga Service Checks');
+ }
+
+ public function getDescription()
+ {
+ return $this->translate(
+ 'This is where you manage your Icinga 2 Service Checks. Service'
+ . ' Templates are your base building blocks, Service Sets allow'
+ . ' you to assign multiple Services at once. Apply Rules make it'
+ . ' possible to assign Services based on Host properties. And'
+ . ' the list of all single Service Objects gives you the possibility'
+ . ' to still modify (or delete) many of them at once.'
+ );
+ }
+
+ public function getTabs()
+ {
+ return $this->createTabsForDashboards(
+ ['hosts', 'services', 'commands']
+ );
+ }
+}
diff --git a/library/Director/Dashboard/TimeperiodsDashboard.php b/library/Director/Dashboard/TimeperiodsDashboard.php
new file mode 100644
index 0000000..9821b94
--- /dev/null
+++ b/library/Director/Dashboard/TimeperiodsDashboard.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class TimeperiodsDashboard extends Dashboard
+{
+ protected $dashletNames = [
+ 'TimeperiodObject',
+ 'TimeperiodTemplate',
+ ];
+
+ public function getTitle()
+ {
+ return $this->translate('Define custom Time Periods');
+ }
+
+ public function getDescription()
+ {
+ return $this->translate(
+ 'Want to define to execute specific checks only withing specific'
+ . ' time periods? Get mobile notifications only out of office hours,'
+ . ' but mail notifications all around the clock? Time Periods allow'
+ . ' you to tackle those and similar requirements.'
+ );
+ }
+
+ public function getTabs()
+ {
+ return $this->createTabsForDashboards(
+ ['notifications', 'users', 'timeperiods']
+ );
+ }
+}
diff --git a/library/Director/Dashboard/UsersDashboard.php b/library/Director/Dashboard/UsersDashboard.php
new file mode 100644
index 0000000..036d149
--- /dev/null
+++ b/library/Director/Dashboard/UsersDashboard.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Dashboard;
+
+class UsersDashboard extends Dashboard
+{
+ protected $dashletNames = [
+ 'UserObject',
+ 'UserTemplate',
+ 'UserGroups',
+ ];
+
+ public function getTitle()
+ {
+ return $this->translate('Schedule your notifications');
+ }
+
+ public function getDescription()
+ {
+ return $this->translate(
+ 'This is where you manage your Icinga 2 User (Contact) objects. Try'
+ . ' to keep your User objects simply by movin complexity to your'
+ . ' templates. Bundle your users in groups and build Notifications'
+ . ' based on them. Running MS Active Directory or another central'
+ . ' User inventory? Stay away from fiddling with manual config, try'
+ . ' to automate all the things with Imports and related Sync Rules!'
+ );
+ }
+
+ public function getTabs()
+ {
+ return $this->createTabsForDashboards(
+ ['notifications', 'users', 'timeperiods']
+ );
+ }
+}
diff --git a/library/Director/Data/AssignFilterHelper.php b/library/Director/Data/AssignFilterHelper.php
new file mode 100644
index 0000000..b0253cf
--- /dev/null
+++ b/library/Director/Data/AssignFilterHelper.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Filter\FilterNot;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Exception\NotImplementedError;
+
+/**
+ * Class ApplyFilterMatches
+ *
+ * A wrapper for Icinga Filter to evaluate filters against Director's objects
+ */
+class AssignFilterHelper
+{
+ /** @var Filter */
+ protected $filter;
+
+ public function __construct(Filter $filter)
+ {
+ $this->filter = $filter;
+ }
+
+ /**
+ * @param object $object
+ *
+ * @return bool
+ * @throws NotImplementedError
+ */
+ public function matches($object)
+ {
+ return $this->matchesPart($this->filter, $object);
+ }
+
+ /**
+ * @param Filter $filter
+ * @param object $object
+ *
+ * @return bool
+ */
+ public static function matchesFilter(Filter $filter, $object)
+ {
+ $helper = new static($filter);
+ return $helper->matches($object);
+ }
+
+ /**
+ * @param Filter $filter
+ * @param object $object
+ *
+ * @return bool
+ * @throws NotImplementedError
+ */
+ protected function matchesPart(Filter $filter, $object)
+ {
+ if ($filter->isChain()) {
+ return $this->matchesChain($filter, $object);
+ } elseif ($filter->isExpression()) {
+ /** @var FilterExpression $filter */
+ return $this->matchesExpression($filter, $object);
+ } else {
+ return $filter->matches($object);
+ }
+ }
+
+ /**
+ * @param Filter $filter
+ * @param object $object
+ *
+ * @return bool
+ * @throws NotImplementedError
+ */
+ protected function matchesChain(Filter $filter, $object)
+ {
+ if ($filter instanceof FilterAnd) {
+ foreach ($filter->filters() as $f) {
+ if (! $this->matchesPart($f, $object)) {
+ return false;
+ }
+ }
+
+ return true;
+ } elseif ($filter instanceof FilterOr) {
+ foreach ($filter->filters() as $f) {
+ if ($this->matchesPart($f, $object)) {
+ return true;
+ }
+ }
+
+ return false;
+ } elseif ($filter instanceof FilterNot) {
+ foreach ($filter->filters() as $f) {
+ if ($this->matchesPart($f, $object)) {
+ return false;
+ }
+ }
+
+ return true;
+ } else {
+ $class = \get_class($filter);
+ $parts = \preg_split('/\\\/', $class);
+
+ throw new NotImplementedError(
+ 'Matching for Filter of type "%s" is not implemented',
+ \end($parts)
+ );
+ }
+ }
+
+ /**
+ * @param FilterExpression $filter
+ * @param object $object
+ *
+ * @return bool
+ */
+ protected function matchesExpression(FilterExpression $filter, $object)
+ {
+ $column = $filter->getColumn();
+ $sign = $filter->getSign();
+ $expression = $filter->getExpression();
+
+ if ($sign === '=') {
+ if ($expression === true) {
+ return property_exists($object, $column) && ! empty($object->{$column});
+ } elseif ($expression === false) {
+ return ! property_exists($object, $column) || empty($object->{$column});
+ } elseif (is_string($expression) && strpos($expression, '*') !== false) {
+ if (! property_exists($object, $column) || empty($object->{$column})) {
+ return false;
+ }
+ $value = $object->{$column};
+
+ $parts = array();
+ foreach (preg_split('~\*~', $expression) as $part) {
+ $parts[] = preg_quote($part);
+ }
+ // match() is case insensitive
+ $pattern = '/^' . implode('.*', $parts) . '$/i';
+
+ if (is_array($value)) {
+ foreach ($value as $candidate) {
+ if (preg_match($pattern, $candidate)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ return (bool) preg_match($pattern, $value);
+ }
+ }
+
+ // fallback to default behavior
+ return $filter->matches($object);
+ }
+}
diff --git a/library/Director/Data/DataArrayHelper.php b/library/Director/Data/DataArrayHelper.php
new file mode 100644
index 0000000..442eb0f
--- /dev/null
+++ b/library/Director/Data/DataArrayHelper.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use InvalidArgumentException;
+use function array_diff;
+use function array_key_exists;
+use function implode;
+use function is_array;
+use function is_object;
+
+class DataArrayHelper
+{
+ public static function wantArray($value)
+ {
+ if (is_object($value)) {
+ return (array) $value;
+ }
+ if (! is_array($value)) {
+ throw new InvalidDataException('Object', $value);
+ }
+
+ return $value;
+ }
+
+ public static function failOnUnknownProperties(array $values, array $knownProperties)
+ {
+ $unknownProperties = array_diff($knownProperties, array_keys($values));
+
+ if (! empty($unknownProperties)) {
+ throw new InvalidArgumentException('Unexpected properties: ' . implode(', ', $unknownProperties));
+ }
+ }
+
+ public static function requireProperties(array $value, array $properties)
+ {
+ $missing = [];
+ foreach ($properties as $property) {
+ if (! array_key_exists($property, $value)) {
+ $missing[] = $property;
+ }
+ }
+
+ if (! empty($missing)) {
+ throw new InvalidArgumentException('Missing properties: ' . implode(', ', $missing));
+ }
+ }
+}
diff --git a/library/Director/Data/Db/DbConnection.php b/library/Director/Data/Db/DbConnection.php
new file mode 100644
index 0000000..146b0e8
--- /dev/null
+++ b/library/Director/Data/Db/DbConnection.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Data\Db\DbConnection as IcingaDbConnection;
+use Icinga\Module\Director\Db\DbUtil;
+use RuntimeException;
+use Zend_Db_Expr;
+
+class DbConnection extends IcingaDbConnection
+{
+ public function isMysql()
+ {
+ return $this->getDbType() === 'mysql';
+ }
+
+ public function isPgsql()
+ {
+ return $this->getDbType() === 'pgsql';
+ }
+
+ /**
+ * @deprecated
+ * @param ?string $binary
+ * @return Zend_Db_Expr|Zend_Db_Expr[]|null
+ */
+ public function quoteBinary($binary)
+ {
+ return DbUtil::quoteBinaryLegacy($binary, $this->getDbAdapter());
+ }
+
+ public function binaryDbResult($value)
+ {
+ if (is_resource($value)) {
+ return stream_get_contents($value);
+ }
+
+ return $value;
+ }
+
+ public function hasPgExtension($name)
+ {
+ $db = $this->db();
+ $query = $db->select()->from(
+ array('e' => 'pg_extension'),
+ array('cnt' => 'COUNT(*)')
+ )->where('extname = ?', $name);
+
+ return (int) $db->fetchOne($query) === 1;
+ }
+}
diff --git a/library/Director/Data/Db/DbDataFormatter.php b/library/Director/Data/Db/DbDataFormatter.php
new file mode 100644
index 0000000..d6e4eeb
--- /dev/null
+++ b/library/Director/Data/Db/DbDataFormatter.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use InvalidArgumentException;
+
+class DbDataFormatter
+{
+ public static function normalizeBoolean($value)
+ {
+ if ($value === 'y' || $value === '1' || $value === true || $value === 1) {
+ return 'y';
+ }
+ if ($value === 'n' || $value === '0' || $value === false || $value === 0) {
+ return 'n';
+ }
+ if ($value === '' || $value === null) {
+ return null;
+ }
+
+ throw new InvalidArgumentException(sprintf(
+ 'Got invalid boolean: %s',
+ var_export($value, 1)
+ ));
+ }
+}
diff --git a/library/Director/Data/Db/DbObject.php b/library/Director/Data/Db/DbObject.php
new file mode 100644
index 0000000..6ecae8b
--- /dev/null
+++ b/library/Director/Data/Db/DbObject.php
@@ -0,0 +1,1487 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Data\InvalidDataException;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\UuidLookup;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use InvalidArgumentException;
+use LogicException;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+use RuntimeException;
+use Zend_Db_Adapter_Abstract;
+use Zend_Db_Exception;
+
+/**
+ * Base class for ...
+ */
+abstract class DbObject
+{
+ /** @var DbConnection $connection */
+ protected $connection;
+
+ /** @var string Table name. MUST be set when extending this class */
+ protected $table;
+
+ /** @var Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /**
+ * Default columns. MUST be set when extending this class. Each table
+ * column MUST be defined with a default value. Default value may be null.
+ *
+ * @var array
+ */
+ protected $defaultProperties;
+
+ /**
+ * Properties as loaded from db
+ */
+ protected $loadedProperties;
+
+ /**
+ * Whether at least one property has been modified
+ */
+ protected $hasBeenModified = false;
+
+ /**
+ * Whether this object has been loaded from db
+ */
+ protected $loadedFromDb = false;
+
+ /**
+ * Object properties
+ */
+ protected $properties = array();
+
+ /**
+ * Property names that have been modified since object creation
+ */
+ protected $modifiedProperties = array();
+
+ /**
+ * Unique key name, could be primary
+ */
+ protected $keyName;
+
+ /**
+ * Set this to an eventual autoincrementing column. May equal $keyName
+ */
+ protected $autoincKeyName;
+
+ /** @var string optional uuid column */
+ protected $uuidColumn;
+
+ /** @var bool forbid updates to autoinc values */
+ protected $protectAutoinc = true;
+
+ protected $binaryProperties = [];
+
+ /**
+ * Filled with object instances when prefetchAll is used
+ */
+ protected static $prefetched = array();
+
+ /**
+ * object_name => id map for prefetched objects
+ */
+ protected static $prefetchedNames = array();
+
+ protected static $prefetchStats = array();
+
+ /** @var ?DbObjectStore */
+ protected static $dbObjectStore;
+
+ /**
+ * Constructor is not accessible and should not be overridden
+ */
+ protected function __construct()
+ {
+ if ($this->table === null
+ || $this->keyName === null
+ || $this->defaultProperties === null
+ ) {
+ throw new LogicException("Someone extending this class didn't RTFM");
+ }
+
+ $this->properties = $this->defaultProperties;
+ $this->beforeInit();
+ }
+
+ public function getTableName()
+ {
+ return $this->table;
+ }
+
+ /************************************************************************\
+ * When extending this class one might want to override any of the *
+ * following hooks. Try to use them whenever possible, especially *
+ * instead of overriding other essential methods like store(). *
+ \************************************************************************/
+
+ /**
+ * One can override this to allow for cross checks and more before storing
+ * the object. Please note that the method is public and allows to check
+ * object consistence at any time.
+ *
+ * @return boolean Whether this object is valid
+ */
+ public function validate()
+ {
+ return true;
+ }
+
+ /**
+ * This is going to be executed before any initialization method takes *
+ * (load from DB, populate from Array...) takes place
+ *
+ * @return void
+ */
+ protected function beforeInit()
+ {
+ }
+
+ /**
+ * Will be executed every time an object has successfully been loaded from
+ * Database
+ *
+ * @return void
+ */
+ protected function onLoadFromDb()
+ {
+ }
+
+ /**
+ * Will be executed before an Object is going to be stored. In case you
+ * want to prevent the store() operation from taking place, please throw
+ * an Exception.
+ *
+ * @return void
+ */
+ protected function beforeStore()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, nachdem ein Objekt erfolgreich gespeichert worden ist
+ *
+ * @return void
+ */
+ protected function onStore()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, nachdem ein Objekt erfolgreich der Datenbank hinzu-
+ * gefügt worden ist
+ *
+ * @return void
+ */
+ protected function onInsert()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich der Datenbank
+ * geändert worden ist
+ *
+ * @return void
+ */
+ protected function onUpdate()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, bevor ein Objekt gelöscht wird. Die Operation wird
+ * aber auf jeden Fall durchgeführt, außer man wirft eine Exception
+ *
+ * @return void
+ */
+ protected function beforeDelete()
+ {
+ }
+
+ /**
+ * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich aud der
+ * Datenbank gelöscht worden ist
+ *
+ * @return void
+ */
+ protected function onDelete()
+ {
+ }
+
+ /**
+ * Set database connection
+ *
+ * @param DbConnection $connection Database connection
+ *
+ * @return self
+ */
+ public function setConnection(DbConnection $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+
+ return $this;
+ }
+
+ public static function setDbObjectStore(DbObjectStore $store)
+ {
+ self::$dbObjectStore = $store;
+ }
+
+ /**
+ * Getter
+ *
+ * @param string $property Property
+ *
+ * @return mixed
+ */
+ public function get($property)
+ {
+ $func = 'get' . ucfirst($property);
+ if (substr($func, -2) === '[]') {
+ $func = substr($func, 0, -2);
+ }
+ // TODO: id check avoids collision with getId. Rethink this.
+ if ($property !== 'id' && method_exists($this, $func)) {
+ return $this->$func();
+ }
+
+ $this->assertPropertyExists($property);
+ return $this->properties[$property];
+ }
+
+ public function getProperty($key)
+ {
+ $this->assertPropertyExists($key);
+ return $this->properties[$key];
+ }
+
+ protected function assertPropertyExists($key)
+ {
+ if (! array_key_exists($key, $this->properties)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Trying to get invalid property "%s"',
+ $key
+ ));
+ }
+
+ return $this;
+ }
+
+ public function hasProperty($key)
+ {
+ if (array_key_exists($key, $this->properties)) {
+ return true;
+ } elseif ($key === 'id') {
+ // There is getId, would give false positive
+ return false;
+ }
+
+ return $this->hasGetterForProperty($key);
+ }
+
+ protected function hasGetterForProperty($key)
+ {
+ $func = 'get' . ucfirst($key);
+ if (\substr($func, -2) === '[]') {
+ $func = substr($func, 0, -2);
+ }
+
+ return \method_exists($this, $func);
+ }
+
+ protected function hasSetterForProperty($key)
+ {
+ $func = 'set' . ucfirst($key);
+ if (\substr($func, -2) === '[]') {
+ $func = substr($func, 0, -2);
+ }
+
+ return \method_exists($this, $func);
+ }
+
+ /**
+ * Generic setter
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return self
+ */
+ public function set($key, $value)
+ {
+ $key = (string) $key;
+ if ($value === '') {
+ $value = null;
+ }
+
+ if (is_resource($value)) {
+ $value = stream_get_contents($value);
+ }
+ $func = 'validate' . ucfirst($key);
+ if (method_exists($this, $func) && $this->$func($value) !== true) {
+ throw new InvalidArgumentException(sprintf(
+ 'Got invalid value "%s" for "%s"',
+ $value,
+ $key
+ ));
+ }
+ $func = 'munge' . ucfirst($key);
+ if (method_exists($this, $func)) {
+ $value = $this->$func($value);
+ }
+
+ $func = 'set' . ucfirst($key);
+ if (substr($func, -2) === '[]') {
+ $func = substr($func, 0, -2);
+ }
+
+ if (method_exists($this, $func)) {
+ return $this->$func($value);
+ }
+
+ if (! $this->hasProperty($key)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Trying to set invalid key "%s"',
+ $key
+ ));
+ }
+
+ if ((is_numeric($value) || is_string($value))
+ && (string) $value === (string) $this->get($key)
+ ) {
+ return $this;
+ }
+
+ if ($key === $this->getAutoincKeyName() && $this->hasBeenLoadedFromDb()) {
+ throw new InvalidArgumentException('Changing autoincremental key is not allowed');
+ }
+
+ return $this->reallySet($key, $value);
+ }
+
+ protected function reallySet($key, $value)
+ {
+ if ($value === $this->properties[$key]) {
+ return $this;
+ }
+ if ($key === 'id' || substr($key, -3) === '_id') {
+ if ((int) $value === (int) $this->properties[$key]) {
+ return $this;
+ }
+ }
+
+ if ($this->hasBeenLoadedFromDb()) {
+ if ($value === $this->loadedProperties[$key]) {
+ unset($this->modifiedProperties[$key]);
+ if (empty($this->modifiedProperties)) {
+ $this->hasBeenModified = false;
+ }
+ } else {
+ $this->hasBeenModified = true;
+ $this->modifiedProperties[$key] = true;
+ }
+ } else {
+ $this->hasBeenModified = true;
+ $this->modifiedProperties[$key] = true;
+ }
+
+ $this->properties[$key] = $value;
+ return $this;
+ }
+
+ /**
+ * Magic getter
+ *
+ * @param mixed $key
+ *
+ * @return mixed
+ */
+ public function __get($key)
+ {
+ return $this->get($key);
+ }
+
+ /**
+ * Magic setter
+ *
+ * @param string $key Key
+ * @param mixed $val Value
+ *
+ * @return void
+ */
+ public function __set($key, $val)
+ {
+ $this->set($key, $val);
+ }
+
+ /**
+ * Magic isset check
+ *
+ * @param string $key
+ * @return boolean
+ */
+ public function __isset($key)
+ {
+ return array_key_exists($key, $this->properties);
+ }
+
+ /**
+ * Magic unsetter
+ *
+ * @param string $key
+ * @return void
+ */
+ public function __unset($key)
+ {
+ if (! array_key_exists($key, $this->properties)) {
+ throw new InvalidArgumentException('Trying to unset invalid key');
+ }
+ $this->properties[$key] = $this->defaultProperties[$key];
+ }
+
+ /**
+ * Runs set() for every key/value pair of the given Array
+ *
+ * @param array $props Array of properties
+ * @return self
+ */
+ public function setProperties($props)
+ {
+ if (! is_array($props)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Array required, got %s',
+ gettype($props)
+ ));
+ }
+ foreach ($props as $key => $value) {
+ $this->set($key, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * Return an array with all object properties
+ *
+ * @return array
+ */
+ public function getProperties()
+ {
+ //return $this->properties;
+ $res = array();
+ foreach ($this->listProperties() as $key) {
+ $res[$key] = $this->get($key);
+ }
+
+ return $res;
+ }
+
+ protected function getPropertiesForDb()
+ {
+ return $this->properties;
+ }
+
+ public function listProperties()
+ {
+ return array_keys($this->properties);
+ }
+
+ public function getDefaultProperties()
+ {
+ return $this->defaultProperties;
+ }
+
+ /**
+ * Return all properties that changed since object creation
+ *
+ * @return array
+ */
+ public function getModifiedProperties()
+ {
+ $props = array();
+ foreach (array_keys($this->modifiedProperties) as $key) {
+ if ($key === $this->autoincKeyName) {
+ if ($this->protectAutoinc) {
+ continue;
+ } elseif ($this->properties[$key] === null) {
+ continue;
+ }
+ }
+
+ $props[$key] = $this->properties[$key];
+ }
+ return $props;
+ }
+
+ /**
+ * List all properties that changed since object creation
+ *
+ * @return array
+ */
+ public function listModifiedProperties()
+ {
+ return array_keys($this->modifiedProperties);
+ }
+
+ /**
+ * Whether this object has been modified
+ *
+ * @return bool
+ */
+ public function hasBeenModified()
+ {
+ return $this->hasBeenModified;
+ }
+
+ /**
+ * Whether the given property has been modified
+ *
+ * @param string $key Property name
+ * @return boolean
+ */
+ protected function hasModifiedProperty($key)
+ {
+ return array_key_exists($key, $this->modifiedProperties);
+ }
+
+ /**
+ * Unique key name
+ *
+ * @return string
+ */
+ public function getKeyName()
+ {
+ return $this->keyName;
+ }
+
+ /**
+ * Autoinc key name
+ *
+ * @return string
+ */
+ public function getAutoincKeyName()
+ {
+ return $this->autoincKeyName;
+ }
+
+ /**
+ * @return ?string
+ */
+ public function getUuidColumn()
+ {
+ return $this->uuidColumn;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasUuidColumn()
+ {
+ return $this->uuidColumn !== null;
+ }
+
+ /**
+ * @return \Ramsey\Uuid\UuidInterface
+ */
+ public function getUniqueId()
+ {
+ if ($this->hasUuidColumn()) {
+ $binaryValue = $this->properties[$this->uuidColumn];
+ if (is_resource($binaryValue)) {
+ throw new RuntimeException('Properties contain binary UUID, probably a programming error');
+ }
+ if ($binaryValue === null) {
+ $uuid = Uuid::uuid4();
+ $this->reallySet($this->uuidColumn, $uuid->getBytes());
+ return $uuid;
+ }
+
+ return Uuid::fromBytes($binaryValue);
+ }
+
+ throw new InvalidArgumentException(sprintf('%s has no UUID column', $this->getTableName()));
+ }
+
+ public function getKeyParams()
+ {
+ $params = array();
+ $key = $this->getKeyName();
+ if (is_array($key)) {
+ foreach ($key as $k) {
+ $params[$k] = $this->get($k);
+ }
+ } else {
+ $params[$key] = $this->get($this->keyName);
+ }
+
+ return $params;
+ }
+
+ /**
+ * Return the unique identifier
+ *
+ * // TODO: may conflict with ->id
+ *
+ * @throws InvalidArgumentException When key can not be calculated
+ *
+ * @return string|array
+ */
+ public function getId()
+ {
+ if (is_array($this->keyName)) {
+ $id = array();
+ foreach ($this->keyName as $key) {
+ if (isset($this->properties[$key])) {
+ $id[$key] = $this->properties[$key];
+ }
+ }
+
+ if (empty($id)) {
+ throw new InvalidArgumentException('Could not evaluate id for multi-column object!');
+ }
+
+ return $id;
+ } else {
+ if (isset($this->properties[$this->keyName])) {
+ return $this->properties[$this->keyName];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the autoinc value if set
+ *
+ * @return int
+ */
+ public function getAutoincId()
+ {
+ if (isset($this->properties[$this->autoincKeyName])) {
+ return (int) $this->properties[$this->autoincKeyName];
+ }
+ return null;
+ }
+
+ protected function forgetAutoincId()
+ {
+ if (isset($this->properties[$this->autoincKeyName])) {
+ $this->properties[$this->autoincKeyName] = null;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Liefert das benutzte Datenbank-Handle
+ *
+ * @return Zend_Db_Adapter_Abstract
+ */
+ public function getDb()
+ {
+ return $this->db;
+ }
+
+ public function hasConnection()
+ {
+ return $this->connection !== null;
+ }
+
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Lädt einen Datensatz aus der Datenbank und setzt die entsprechenden
+ * Eigenschaften dieses Objekts
+ *
+ * @throws NotFoundError
+ * @return self
+ */
+ protected function loadFromDb()
+ {
+ $select = $this->db->select()->from($this->table)->where($this->createWhere());
+ $properties = $this->db->fetchRow($select);
+
+ if (empty($properties)) {
+ if (is_array($this->getKeyName())) {
+ throw new NotFoundError(
+ 'Failed to load %s for %s',
+ $this->table,
+ $this->createWhere()
+ );
+ } else {
+ throw new NotFoundError(
+ 'Failed to load %s "%s"',
+ $this->table,
+ $this->getLogId()
+ );
+ }
+ }
+
+ return $this->setDbProperties($properties);
+ }
+
+ /**
+ * @param object|array $row
+ * @param Db $db
+ * @return self
+ */
+ public static function fromDbRow($row, Db $db)
+ {
+ $self = (new static())->setConnection($db);
+ if (is_object($row)) {
+ return $self->setDbProperties((array) $row);
+ }
+
+ if (is_array($row)) {
+ return $self->setDbProperties($row);
+ }
+
+ throw new InvalidDataException('array or object', $row);
+ }
+
+ protected function setDbProperties($properties)
+ {
+ foreach ($properties as $key => $val) {
+ if (! array_key_exists($key, $this->properties)) {
+ throw new LogicException(sprintf(
+ 'Trying to set invalid %s key "%s". DB schema change?',
+ $this->table,
+ $key
+ ));
+ }
+ if ($val === null) {
+ $this->properties[$key] = null;
+ } elseif (is_resource($val)) {
+ $this->properties[$key] = stream_get_contents($val);
+ } else {
+ $this->properties[$key] = (string) $val;
+ }
+ }
+
+ $this->setBeingLoadedFromDb();
+ $this->onLoadFromDb();
+ return $this;
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ $this->loadedFromDb = true;
+ $this->loadedProperties = $this->properties;
+ $this->hasBeenModified = false;
+ $this->modifiedProperties = [];
+ }
+
+ public function setLoadedProperty($key, $value)
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ $this->set($key, $value);
+ $this->loadedProperties[$key] = $this->get($key);
+ } else {
+ throw new RuntimeException('Cannot set loaded property for new object');
+ }
+ }
+
+ public function getOriginalProperties()
+ {
+ return $this->loadedProperties;
+ }
+
+ public function getOriginalProperty($key)
+ {
+ $this->assertPropertyExists($key);
+ if ($this->hasBeenLoadedFromDb()) {
+ return $this->loadedProperties[$key];
+ }
+
+ return null;
+ }
+
+ public function resetProperty($key)
+ {
+ $this->set($key, $this->getOriginalProperty($key));
+ if ($this->listModifiedProperties() === [$key]) {
+ $this->hasBeenModified = false;
+ }
+
+ return $this;
+ }
+
+ public function hasBeenLoadedFromDb()
+ {
+ return $this->loadedFromDb;
+ }
+
+ /**
+ * Ändert den entsprechenden Datensatz in der Datenbank
+ *
+ * @return int Anzahl der geänderten Zeilen
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function updateDb()
+ {
+ $properties = $this->getModifiedProperties();
+ if (empty($properties)) {
+ // Fake true, we might have manually set this to "modified"
+ return true;
+ }
+ $this->quoteBinaryProperties($properties);
+
+ // TODO: Remember changed data for audit and log
+ return $this->db->update(
+ $this->table,
+ $properties,
+ $this->createWhere()
+ );
+ }
+
+ /**
+ * Fügt der Datenbank-Tabelle einen entsprechenden Datensatz hinzu
+ *
+ * @return int Anzahl der betroffenen Zeilen
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function insertIntoDb()
+ {
+ $properties = $this->getPropertiesForDb();
+ if ($this->autoincKeyName !== null) {
+ if ($this->protectAutoinc || $properties[$this->autoincKeyName] === null) {
+ unset($properties[$this->autoincKeyName]);
+ }
+ }
+ if ($column = $this->getUuidColumn()) {
+ $properties[$column] = $this->getUniqueId()->getBytes();
+ }
+ $this->quoteBinaryProperties($properties);
+
+ return $this->db->insert($this->table, $properties);
+ }
+
+ protected function quoteBinaryProperties(&$properties)
+ {
+ foreach ($properties as $key => $value) {
+ if ($this->isBinaryColumn($key)) {
+ $properties[$key] = $this->getConnection()->quoteBinary($value);
+ }
+ }
+ }
+
+ protected function isBinaryColumn($column)
+ {
+ return in_array($column, $this->binaryProperties) || $this->getUuidColumn() === $column;
+ }
+
+ /**
+ * Store object to database
+ *
+ * @param DbConnection $db
+ * @return bool Whether storing succeeded. Always true, throws otherwise
+ * @throws DuplicateKeyException
+ */
+ public function store(DbConnection $db = null)
+ {
+ if ($db !== null) {
+ $this->setConnection($db);
+ }
+
+ if ($this->validate() !== true) {
+ throw new InvalidArgumentException(sprintf(
+ '%s[%s] validation failed',
+ $this->table,
+ $this->getLogId()
+ ));
+ }
+
+ if ($this->hasBeenLoadedFromDb() && ! $this->hasBeenModified()) {
+ return true;
+ }
+
+ $this->beforeStore();
+ $table = $this->table;
+ $id = $this->getId();
+
+ try {
+ if ($this->hasBeenLoadedFromDb()) {
+ if ($this->updateDb() !== false) {
+ $this->onUpdate();
+ } else {
+ throw new RuntimeException(sprintf(
+ 'FAILED storing %s "%s"',
+ $table,
+ $this->getLogId()
+ ));
+ }
+ } else {
+ if ($id && $this->existsInDb()) {
+ $logId = '"' . $this->getLogId() . '"';
+
+ if ($autoId = $this->getAutoincId()) {
+ $logId .= sprintf(', %s=%s', $this->autoincKeyName, $autoId);
+ }
+ throw new DuplicateKeyException(
+ 'Trying to recreate %s (%s)',
+ $table,
+ $logId
+ );
+ }
+
+ if ($this->insertIntoDb()) {
+ if ($this->autoincKeyName && $this->getProperty($this->autoincKeyName) === null) {
+ if ($this->connection->isPgsql()) {
+ $this->properties[$this->autoincKeyName] = $this->db->lastInsertId(
+ $table,
+ $this->autoincKeyName
+ );
+ } else {
+ $this->properties[$this->autoincKeyName] = $this->db->lastInsertId();
+ }
+ }
+ // $this->log(sprintf('New %s "%s" has been stored', $table, $id));
+ $this->onInsert();
+ } else {
+ throw new RuntimeException(sprintf(
+ 'FAILED to store new %s "%s"',
+ $table,
+ $this->getLogId()
+ ));
+ }
+ }
+ } catch (Zend_Db_Exception $e) {
+ throw new RuntimeException(sprintf(
+ 'Storing %s[%s] failed: %s {%s}',
+ $this->table,
+ $this->getLogId(),
+ $e->getMessage(),
+ var_export($this->getProperties(), 1) // TODO: Remove properties
+ ));
+ }
+
+ // Hint: order is differs from setBeingLoadedFromDb() as of the onStore hook
+ $this->modifiedProperties = [];
+ $this->hasBeenModified = false;
+ $this->loadedProperties = $this->properties;
+ $this->onStore();
+ $this->loadedFromDb = true;
+
+ return true;
+ }
+
+ /**
+ * Delete item from DB
+ *
+ * @return int Affected rows
+ */
+ protected function deleteFromDb()
+ {
+ return $this->db->delete(
+ $this->table,
+ $this->createWhere()
+ );
+ }
+
+ /**
+ * @param string $key
+ * @return self
+ * @throws InvalidArgumentException
+ */
+ protected function setKey($key)
+ {
+ $keyname = $this->getKeyName();
+ if (is_array($keyname)) {
+ if (! is_array($key)) {
+ throw new InvalidArgumentException(sprintf(
+ '%s has a multicolumn key, array required',
+ $this->table
+ ));
+ }
+ foreach ($keyname as $k) {
+ if (! array_key_exists($k, $key)) {
+ // We allow for null in multicolumn keys:
+ $key[$k] = null;
+ }
+ $this->set($k, $key[$k]);
+ }
+ } else {
+ $this->set($keyname, $key);
+ }
+ return $this;
+ }
+
+ protected function existsInDb()
+ {
+ $result = $this->db->fetchRow(
+ $this->db->select()->from($this->table)->where($this->createWhere())
+ );
+ return $result !== false;
+ }
+
+ public function createWhere()
+ {
+ if ($this->hasUuidColumn() && $this->properties[$this->uuidColumn] !== null) {
+ return $this->db->quoteInto(
+ sprintf('%s = ?', $this->getUuidColumn()),
+ $this->connection->quoteBinary($this->getUniqueId()->getBytes())
+ );
+ }
+ if ($id = $this->getAutoincId()) {
+ if ($originalId = $this->getOriginalProperty($this->autoincKeyName)) {
+ return $this->db->quoteInto(
+ sprintf('%s = ?', $this->autoincKeyName),
+ $originalId
+ );
+ }
+ return $this->db->quoteInto(
+ sprintf('%s = ?', $this->autoincKeyName),
+ $id
+ );
+ }
+
+ $key = $this->getKeyName();
+
+ if (is_array($key) && ! empty($key)) {
+ $where = array();
+ foreach ($key as $k) {
+ if ($this->hasBeenLoadedFromDb()) {
+ if ($this->loadedProperties[$k] === null) {
+ $where[] = sprintf('%s IS NULL', $k);
+ } else {
+ $where[] = $this->createQuotedWhere($k, $this->loadedProperties[$k]);
+ }
+ } else {
+ if ($this->properties[$k] === null) {
+ $where[] = sprintf('%s IS NULL', $k);
+ } else {
+ $where[] = $this->createQuotedWhere($k, $this->properties[$k]);
+ }
+ }
+ }
+
+ return implode(' AND ', $where);
+ } else {
+ if ($this->hasBeenLoadedFromDb()) {
+ return $this->createQuotedWhere($key, $this->loadedProperties[$key]);
+ } else {
+ return $this->createQuotedWhere($key, $this->properties[$key]);
+ }
+ }
+ }
+
+ protected function createQuotedWhere($column, $value)
+ {
+ return $this->db->quoteInto(
+ sprintf('%s = ?', $column),
+ $this->eventuallyQuoteBinary($value, $column)
+ );
+ }
+
+ protected function eventuallyQuoteBinary($value, $column)
+ {
+ if ($this->isBinaryColumn($column)) {
+ return $this->connection->quoteBinary($value);
+ } else {
+ return $value;
+ }
+ }
+
+ protected function getLogId()
+ {
+ $id = $this->getId();
+ if (is_array($id)) {
+ $logId = json_encode($id);
+ } else {
+ $logId = $id;
+ }
+
+ if ($logId === null && $this->autoincKeyName) {
+ $logId = $this->getAutoincId();
+ }
+
+ return $logId;
+ }
+
+ public function delete()
+ {
+ $table = $this->table;
+
+ if (! $this->hasBeenLoadedFromDb()) {
+ throw new LogicException(sprintf(
+ 'Cannot delete %s "%s", it has not been loaded from Db',
+ $table,
+ $this->getLogId()
+ ));
+ }
+
+ if (! $this->existsInDb()) {
+ throw new InvalidArgumentException(sprintf(
+ 'Cannot delete %s "%s", it does not exist',
+ $table,
+ $this->getLogId()
+ ));
+ }
+ $this->beforeDelete();
+ if (! $this->deleteFromDb()) {
+ throw new RuntimeException(sprintf(
+ 'Deleting %s (%s) FAILED',
+ $table,
+ $this->getLogId()
+ ));
+ }
+ // $this->log(sprintf('%s "%s" has been DELETED', $table, this->getLogId()));
+ $this->onDelete();
+ $this->loadedFromDb = false;
+ return true;
+ }
+
+ public function __clone()
+ {
+ $this->onClone();
+ $this->forgetAutoincId();
+ $this->loadedFromDb = false;
+ $this->hasBeenModified = true;
+ }
+
+ protected function onClone()
+ {
+ }
+
+ /**
+ * @param array $properties
+ * @param DbConnection|null $connection
+ *
+ * @return static
+ */
+ public static function create($properties = array(), DbConnection $connection = null)
+ {
+ $obj = new static();
+ if ($connection !== null) {
+ $obj->setConnection($connection);
+ }
+ $obj->setProperties($properties);
+ return $obj;
+ }
+
+ protected static function classWasPrefetched()
+ {
+ $class = get_called_class();
+ return array_key_exists($class, self::$prefetched);
+ }
+
+ /**
+ * @param $key
+ * @return static|bool
+ */
+ protected static function getPrefetched($key)
+ {
+ $class = get_called_class();
+ if (static::hasPrefetched($key)) {
+ if (is_string($key)
+ && array_key_exists($class, self::$prefetchedNames)
+ && array_key_exists($key, self::$prefetchedNames[$class])
+ ) {
+ return self::$prefetched[$class][
+ self::$prefetchedNames[$class][$key]
+ ];
+ } else {
+ return self::$prefetched[$class][$key];
+ }
+ } else {
+ return false;
+ }
+ }
+
+ protected static function hasPrefetched($key)
+ {
+ $class = get_called_class();
+ if (! array_key_exists($class, self::$prefetchStats)) {
+ self::$prefetchStats[$class] = (object) array(
+ 'miss' => 0,
+ 'hits' => 0,
+ 'hitNames' => 0,
+ 'combinedMiss' => 0
+ );
+ }
+
+ if (is_array($key)) {
+ self::$prefetchStats[$class]->combinedMiss++;
+ return false;
+ }
+
+ if (array_key_exists($class, self::$prefetched)) {
+ if (is_string($key)
+ && array_key_exists($class, self::$prefetchedNames)
+ && array_key_exists($key, self::$prefetchedNames[$class])
+ ) {
+ self::$prefetchStats[$class]->hitNames++;
+ return true;
+ } elseif (array_key_exists($key, self::$prefetched[$class])) {
+ self::$prefetchStats[$class]->hits++;
+ return true;
+ } else {
+ self::$prefetchStats[$class]->miss++;
+ return false;
+ }
+ } else {
+ self::$prefetchStats[$class]->miss++;
+ return false;
+ }
+ }
+
+ public static function getPrefetchStats()
+ {
+ return self::$prefetchStats;
+ }
+
+ /**
+ * @param $id
+ * @param DbConnection $connection
+ * @return static
+ * @throws NotFoundError
+ */
+ public static function loadWithAutoIncId($id, DbConnection $connection)
+ {
+ /* Need to cast to int, otherwise the id will be matched against
+ * object_name, which may wreak havoc if an object has a
+ * object_name matching some id. Note that DbObject::set() and
+ * DbObject::setDbProperties() will convert any property to
+ * string, including ids.
+ */
+ $id = (int) $id;
+
+ if ($prefetched = static::getPrefetched($id)) {
+ return $prefetched;
+ }
+
+ $obj = new static;
+ if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) {
+ $table = $obj->getTableName();
+ assert($connection instanceof Db);
+ $uuid = UuidLookup::requireUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch());
+
+ return self::$dbObjectStore->load($table, $uuid);
+ }
+
+ $obj->setConnection($connection)
+ ->set($obj->autoincKeyName, $id)
+ ->loadFromDb();
+
+ return $obj;
+ }
+
+ /**
+ * @param $id
+ * @param DbConnection $connection
+ * @return static
+ * @throws NotFoundError
+ */
+ public static function load($id, DbConnection $connection)
+ {
+ if ($prefetched = static::getPrefetched($id)) {
+ return $prefetched;
+ }
+ /** @var DbObject $obj */
+ $obj = new static;
+
+ if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) {
+ $table = $obj->getTableName();
+ assert($connection instanceof Db);
+ $uuid = UuidLookup::requireUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch());
+
+ return self::$dbObjectStore->load($table, $uuid);
+ }
+
+ $obj->setConnection($connection)->setKey($id)->loadFromDb();
+
+ return $obj;
+ }
+
+ /**
+ * @param DbConnection $connection
+ * @param \Zend_Db_Select $query
+ * @param string|null $keyColumn
+ *
+ * @return static[]
+ */
+ public static function loadAll(DbConnection $connection, $query = null, $keyColumn = null)
+ {
+ $objects = array();
+ $db = $connection->getDbAdapter();
+
+ if ($query === null) {
+ $dummy = new static;
+ $select = $db->select()->from($dummy->table);
+ } else {
+ $select = $query;
+ }
+ $rows = $db->fetchAll($select);
+
+ foreach ($rows as $row) {
+ /** @var DbObject $obj */
+ $obj = new static;
+ $obj->setConnection($connection)->setDbProperties($row);
+ if ($keyColumn === null) {
+ $objects[] = $obj;
+ } else {
+ $objects[$row->$keyColumn] = $obj;
+ }
+ }
+
+ return $objects;
+ }
+
+ /**
+ * @param DbConnection $connection
+ * @param bool $force
+ *
+ * @return static[]
+ */
+ public static function prefetchAll(DbConnection $connection, $force = false)
+ {
+ $dummy = static::create();
+ $class = get_class($dummy);
+ $autoInc = $dummy->getAutoincKeyName();
+ $keyName = $dummy->getKeyName();
+
+ if ($force || ! array_key_exists($class, self::$prefetched)) {
+ self::$prefetched[$class] = static::loadAll($connection, null, $autoInc);
+ if (! is_array($keyName) && $keyName !== $autoInc) {
+ foreach (self::$prefetched[$class] as $k => $v) {
+ self::$prefetchedNames[$class][$v->$keyName] = $k;
+ }
+ }
+ }
+
+ return self::$prefetched[$class];
+ }
+
+ public static function clearPrefetchCache()
+ {
+ $class = get_called_class();
+ if (! array_key_exists($class, self::$prefetched)) {
+ return;
+ }
+
+ unset(self::$prefetched[$class]);
+ unset(self::$prefetchedNames[$class]);
+ unset(self::$prefetchStats[$class]);
+ }
+
+ public static function clearAllPrefetchCaches()
+ {
+ self::$prefetched = array();
+ self::$prefetchedNames = array();
+ self::$prefetchStats = array();
+ }
+
+ /**
+ * @param $id
+ * @param DbConnection $connection
+ * @return bool
+ */
+ public static function exists($id, DbConnection $connection)
+ {
+ if (static::getPrefetched($id)) {
+ return true;
+ } elseif (static::classWasPrefetched()) {
+ return false;
+ }
+
+ /** @var DbObject $obj */
+ $obj = new static;
+ if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) {
+ $table = $obj->getTableName();
+ assert($connection instanceof Db);
+ $uuid = UuidLookup::findUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch());
+ if ($uuid) {
+ return self::$dbObjectStore->exists($table, $uuid);
+ }
+
+ return false;
+ }
+
+ $obj->setConnection($connection)->setKey($id);
+ return $obj->existsInDb();
+ }
+
+ public static function uniqueIdExists(UuidInterface $uuid, DbConnection $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $obj = new static;
+ $column = $obj->getUuidColumn();
+ $query = $db->select()
+ ->from($obj->getTableName(), $column)
+ ->where("$column = ?", $connection->quoteBinary($uuid->getBytes()));
+
+ $result = $db->fetchRow($query);
+
+ return $result !== false;
+ }
+
+ public static function requireWithUniqueId(UuidInterface $uuid, DbConnection $connection)
+ {
+ if ($object = static::loadWithUniqueId($uuid, $connection)) {
+ return $object;
+ }
+
+ throw new NotFoundError(sprintf(
+ 'No %s with UUID=%s has been found',
+ (new static)->getTableName(),
+ $uuid->toString()
+ ));
+ }
+
+ public static function loadWithUniqueId(UuidInterface $uuid, DbConnection $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $obj = new static;
+
+ if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) {
+ $table = $obj->getTableName();
+ assert($connection instanceof Db);
+ return self::$dbObjectStore->load($table, $uuid);
+ }
+
+ $query = $db->select()
+ ->from($obj->getTableName())
+ ->where($obj->getUuidColumn() . ' = ?', $connection->quoteBinary($uuid->getBytes()));
+
+ $result = $db->fetchRow($query);
+
+ if ($result) {
+ return $obj->setConnection($connection)->setDbProperties($result);
+ }
+
+ return null;
+ }
+
+ public function setUniqueId(UuidInterface $uuid)
+ {
+ if ($column = $this->getUuidColumn()) {
+ $binary = $uuid->getBytes();
+ $current = $this->get($column);
+ if ($current === null) {
+ $this->set($column, $binary);
+ } else {
+ if ($current !== $binary) {
+ throw new RuntimeException(sprintf(
+ 'Changing the UUID (from %s to %s) is not allowed',
+ Uuid::fromBytes($current)->toString(),
+ Uuid::fromBytes($binary)->toString()
+ ));
+ }
+ }
+ }
+ }
+
+ public function __destruct()
+ {
+ unset($this->db);
+ unset($this->connection);
+ }
+}
diff --git a/library/Director/Data/Db/DbObjectStore.php b/library/Director/Data/Db/DbObjectStore.php
new file mode 100644
index 0000000..bc69b5a
--- /dev/null
+++ b/library/Director/Data/Db/DbObjectStore.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchActivity;
+use Icinga\Module\Director\Db\Branch\BranchedObject;
+use Icinga\Module\Director\Db\Branch\MergeErrorDeleteMissingObject;
+use Icinga\Module\Director\Db\Branch\MergeErrorModificationForMissingObject;
+use Icinga\Module\Director\Db\Branch\MergeErrorRecreateOnMerge;
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Ramsey\Uuid\UuidInterface;
+
+/**
+ * Loader for Icinga/DbObjects
+ *
+ * Is aware of branches and prefetching. I would prefer to see a StoreInterface,
+ * with one of the above wrapping the other. But for now, this helps to clean things
+ * up
+ */
+class DbObjectStore
+{
+ /** @var Db */
+ protected $connection;
+
+ /** @var ?Branch */
+ protected $branch;
+
+ public function __construct(Db $connection, Branch $branch = null)
+ {
+ $this->connection = $connection;
+ $this->branch = $branch;
+ }
+
+ /**
+ * @param $tableName
+ * @param UuidInterface $uuid
+ * @return DbObject|null
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function load($tableName, UuidInterface $uuid)
+ {
+ $branchedObject = BranchedObject::load($this->connection, $tableName, $uuid, $this->branch);
+ $object = $branchedObject->getBranchedDbObject($this->connection);
+ if ($object === null) {
+ return null;
+ }
+
+ $object->setBeingLoadedFromDb();
+
+ return $object;
+ }
+
+ /**
+ * @param string $tableName
+ * @param string $arrayIdx
+ * @return DbObject[]|IcingaObject[]
+ * @throws MergeErrorRecreateOnMerge
+ * @throws MergeErrorDeleteMissingObject
+ * @throws MergeErrorModificationForMissingObject
+ */
+ public function loadAll($tableName, $arrayIdx = 'uuid')
+ {
+ $db = $this->connection->getDbAdapter();
+ $class = DbObjectTypeRegistry::classByType($tableName);
+ $query = $db->select()->from($tableName)->order($arrayIdx);
+ $result = [];
+ foreach ($db->fetchAll($query) as $row) {
+ $row->uuid = DbUtil::binaryResult($row->uuid);
+ $result[$row->uuid] = $class::create((array) $row, $this->connection);
+ $result[$row->uuid]->setBeingLoadedFromDb();
+ }
+ if ($this->branch && $this->branch->isBranch()) {
+ $query = $db->select()
+ ->from(BranchActivity::DB_TABLE)
+ ->where('branch_uuid = ?', $this->connection->quoteBinary($this->branch->getUuid()->getBytes()))
+ ->order('timestamp_ns ASC');
+ $rows = $db->fetchAll($query);
+ foreach ($rows as $row) {
+ $activity = BranchActivity::fromDbRow($row);
+ if ($activity->getObjectTable() !== $tableName) {
+ continue;
+ }
+ $uuid = $activity->getObjectUuid();
+ $binaryUuid = $uuid->getBytes();
+
+ $exists = isset($result[$binaryUuid]);
+ if ($activity->isActionCreate()) {
+ if ($exists) {
+ throw new MergeErrorRecreateOnMerge($activity);
+ } else {
+ $new = $activity->createDbObject($this->connection);
+ $new->setBeingLoadedFromDb();
+ $result[$binaryUuid] = $new;
+ }
+ } elseif ($activity->isActionDelete()) {
+ if ($exists) {
+ unset($result[$binaryUuid]);
+ } else {
+ throw new MergeErrorDeleteMissingObject($activity);
+ }
+ } else {
+ if ($exists) {
+ $activity->applyToDbObject($result[$binaryUuid])->setBeingLoadedFromDb();
+ } else {
+ throw new MergeErrorModificationForMissingObject($activity);
+ }
+ }
+ }
+ }
+
+ if ($arrayIdx === 'uuid') {
+ return $result;
+ }
+
+ $indexedResult = [];
+ foreach ($result as $object) {
+ $indexedResult[$object->get($arrayIdx)] = $object;
+ }
+
+ return $indexedResult;
+ }
+
+ public function exists($tableName, UuidInterface $uuid)
+ {
+ return BranchedObject::exists($this->connection, $tableName, $uuid, $this->branch->getUuid());
+ }
+
+ public function store(DbObject $object)
+ {
+ if ($this->branch && $this->branch->isBranch()) {
+ $activity = BranchActivity::forDbObject($object, $this->branch);
+ $this->connection->runFailSafeTransaction(function () use ($activity) {
+ $activity->store($this->connection);
+ BranchedObject::withActivity($activity, $this->connection)->store($this->connection);
+ });
+
+ return true;
+ } else {
+ return $object->store($this->connection);
+ }
+ }
+
+ public function delete(DbObject $object)
+ {
+ if ($this->branch && $this->branch->isBranch()) {
+ $activity = BranchActivity::deleteObject($object, $this->branch);
+ $this->connection->runFailSafeTransaction(function () use ($activity) {
+ $activity->store($this->connection);
+ BranchedObject::load(
+ $this->connection,
+ $activity->getObjectTable(),
+ $activity->getObjectUuid(),
+ $this->branch
+ )->delete($this->connection);
+ });
+ return true;
+ }
+
+ return $object->delete();
+ }
+
+ public function getBranch()
+ {
+ return $this->branch;
+ }
+}
diff --git a/library/Director/Data/Db/DbObjectTypeRegistry.php b/library/Director/Data/Db/DbObjectTypeRegistry.php
new file mode 100644
index 0000000..0c226d6
--- /dev/null
+++ b/library/Director/Data/Db/DbObjectTypeRegistry.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class DbObjectTypeRegistry
+{
+ /**
+ * @param $type
+ * @return string|DbObject Fake typehint for IDE
+ */
+ public static function classByType($type)
+ {
+ // allow for icinga_host and host
+ $type = lcfirst(preg_replace('/^icinga_/', '', $type));
+
+ // Hint: Sync/Import are not IcingaObjects, this should be reconsidered:
+ if (strpos($type, 'import') === 0 || strpos($type, 'sync') === 0) {
+ $prefix = '';
+ } elseif (strpos($type, 'data') === false) {
+ $prefix = 'Icinga';
+ } else {
+ $prefix = 'Director';
+ }
+
+ // TODO: Provide a more sophisticated solution
+ if ($type === 'hostgroup') {
+ $type = 'hostGroup';
+ } elseif ($type === 'usergroup') {
+ $type = 'userGroup';
+ } elseif ($type === 'timeperiod') {
+ $type = 'timePeriod';
+ } elseif ($type === 'servicegroup') {
+ $type = 'serviceGroup';
+ } elseif ($type === 'service_set' || $type === 'serviceset') {
+ $type = 'serviceSet';
+ } elseif ($type === 'apiuser') {
+ $type = 'apiUser';
+ } elseif ($type === 'host_template_choice') {
+ $type = 'templateChoiceHost';
+ } elseif ($type === 'service_template_choice') {
+ $type = 'TemplateChoiceService';
+ } elseif ($type === 'scheduled_downtime' || $type === 'scheduled-downtime') {
+ $type = 'ScheduledDowntime';
+ }
+
+ return 'Icinga\\Module\\Director\\Objects\\' . $prefix . ucfirst($type);
+ }
+
+ public static function tableNameByType($type)
+ {
+ $class = static::classByType($type);
+ $dummy = $class::create([]);
+
+ return $dummy->getTableName();
+ }
+
+ public static function shortTypeForObject(DbObject $object)
+ {
+ if ($object instanceof IcingaObject) {
+ return $object->getShortTableName();
+ }
+
+ return $object->getTableName();
+ }
+
+ public static function newObject($type, $properties = [], Db $db = null)
+ {
+ /** @var DbObject $class fake hint for the IDE, it's a string */
+ $class = self::classByType($type);
+ return $class::create($properties, $db);
+ }
+}
diff --git a/library/Director/Data/Db/DbObjectWithSettings.php b/library/Director/Data/Db/DbObjectWithSettings.php
new file mode 100644
index 0000000..4f6b139
--- /dev/null
+++ b/library/Director/Data/Db/DbObjectWithSettings.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Module\Director\Db;
+
+abstract class DbObjectWithSettings extends DbObject
+{
+ /** @var Db $connection */
+ protected $connection;
+
+ protected $settingsTable = 'your_table_name';
+
+ protected $settingsRemoteId = 'column_pointing_to_main_table_id';
+
+ protected $settings = [];
+
+ public function set($key, $value)
+ {
+ if ($this->hasProperty($key)) {
+ return parent::set($key, $value);
+ } elseif ($this->hasSetterForProperty($key)) { // Hint: hasProperty checks only for Getters
+ return parent::set($key, $value);
+ }
+
+ if (! \array_key_exists($key, $this->settings) || $value !== $this->settings[$key]) {
+ $this->hasBeenModified = true;
+ }
+
+ $this->settings[$key] = $value;
+ return $this;
+ }
+
+ public function get($key)
+ {
+ if ($this->hasProperty($key)) {
+ return parent::get($key);
+ }
+
+ if (array_key_exists($key, $this->settings)) {
+ return $this->settings[$key];
+ }
+
+ return parent::get($key);
+ }
+
+ public function setSettings($settings)
+ {
+ $settings = (array) $settings;
+ ksort($settings);
+ if ($settings !== $this->settings) {
+ $this->settings = $settings;
+ $this->hasBeenModified = true;
+ }
+
+ return $this;
+ }
+
+ public function getSettings()
+ {
+ // Sort them, important only for new objects
+ ksort($this->settings);
+ return $this->settings;
+ }
+
+ public function getSetting($name, $default = null)
+ {
+ if (array_key_exists($name, $this->settings)) {
+ return $this->settings[$name];
+ }
+
+ return $default;
+ }
+
+ public function getStoredSetting($name, $default = null)
+ {
+ $stored = $this->fetchSettingsFromDb();
+ if (array_key_exists($name, $stored)) {
+ return $stored[$name];
+ }
+
+ return $default;
+ }
+
+ public function __unset($key)
+ {
+ if ($this->hasProperty($key)) {
+ parent::__unset($key);
+ }
+
+ if (array_key_exists($key, $this->settings)) {
+ unset($this->settings[$key]);
+ $this->hasBeenModified = true;
+ }
+ }
+
+ protected function onStore()
+ {
+ $old = $this->fetchSettingsFromDb();
+ $oldKeys = array_keys($old);
+ $newKeys = array_keys($this->settings);
+ $add = [];
+ $mod = [];
+ $del = [];
+ $id = $this->get('id');
+
+ foreach ($this->settings as $key => $val) {
+ if (array_key_exists($key, $old)) {
+ if ($old[$key] !== $this->settings[$key]) {
+ $mod[$key] = $this->settings[$key];
+ }
+ } else {
+ $add[$key] = $this->settings[$key];
+ }
+ }
+
+ foreach (array_diff($oldKeys, $newKeys) as $key) {
+ $del[] = $key;
+ }
+
+ $where = sprintf($this->settingsRemoteId . ' = %d AND setting_name = ?', $id);
+ $db = $this->getDb();
+ foreach ($mod as $key => $val) {
+ $db->update(
+ $this->settingsTable,
+ ['setting_value' => $val],
+ $db->quoteInto($where, $key)
+ );
+ }
+
+ foreach ($add as $key => $val) {
+ $db->insert(
+ $this->settingsTable,
+ [
+ $this->settingsRemoteId => $id,
+ 'setting_name' => $key,
+ 'setting_value' => $val
+ ]
+ );
+ }
+
+ if (! empty($del)) {
+ $where = sprintf($this->settingsRemoteId . ' = %d AND setting_name IN (?)', $id);
+ $db->delete($this->settingsTable, $db->quoteInto($where, $del));
+ }
+ }
+
+ protected function fetchSettingsFromDb()
+ {
+ $db = $this->getDb();
+ $id = $this->get('id');
+ if (! $id) {
+ return [];
+ }
+
+ return $db->fetchPairs(
+ $db->select()
+ ->from($this->settingsTable, ['setting_name', 'setting_value'])
+ ->where($this->settingsRemoteId . ' = ?', $id)
+ ->order('setting_name')
+ );
+ }
+
+ protected function onLoadFromDb()
+ {
+ $this->settings = $this->fetchSettingsFromDb();
+ }
+}
diff --git a/library/Director/Data/Db/IcingaObjectFilterRenderer.php b/library/Director/Data/Db/IcingaObjectFilterRenderer.php
new file mode 100644
index 0000000..de2ec79
--- /dev/null
+++ b/library/Director/Data/Db/IcingaObjectFilterRenderer.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterException;
+use Icinga\Data\Filter\FilterExpression;
+
+class IcingaObjectFilterRenderer
+{
+ /** @var Filter */
+ protected $filter;
+
+ /** @var IcingaObjectQuery */
+ protected $query;
+
+ protected $columnMap = [
+ 'host.name' => 'host.object_name',
+ 'service.name' => 'service.object_name',
+ ];
+
+ public function __construct(Filter $filter, IcingaObjectQuery $query)
+ {
+ $this->filter = clone($filter);
+ $this->fixFilterColumns($this->filter);
+ $this->query = $query;
+ }
+
+ /**
+ * @param Filter $filter
+ * @param IcingaObjectQuery $query
+ *
+ * @return IcingaObjectQuery
+ */
+ public static function apply(Filter $filter, IcingaObjectQuery $query)
+ {
+ $self = new static($filter, $query);
+ return $self->applyFilterToQuery();
+ }
+
+ /**
+ * @return IcingaObjectQuery
+ */
+ protected function applyFilterToQuery()
+ {
+ $this->query->escapedWhere($this->renderFilter($this->filter));
+ return $this->query;
+ }
+
+ /**
+ * @param Filter $filter
+ * @return string
+ */
+ protected function renderFilter(Filter $filter)
+ {
+ if ($filter->isChain()) {
+ /** @var FilterChain $filter */
+ return $this->renderFilterChain($filter);
+ } else {
+ /** @var FilterExpression $filter */
+ return $this->renderFilterExpression($filter);
+ }
+ }
+
+ /**
+ * @param FilterChain $filter
+ *
+ * @throws FilterException
+ *
+ * @return string
+ */
+ protected function renderFilterChain(FilterChain $filter)
+ {
+ $parts = array();
+ foreach ($filter->filters() as $sub) {
+ $parts[] = $this->renderFilter($sub);
+ }
+
+ $op = $filter->getOperatorName();
+ if ($op === 'NOT') {
+ if (count($parts) !== 1) {
+ throw new FilterException(
+ 'NOT should have exactly one child, got %s',
+ count($parts)
+ );
+ }
+
+ return $op . ' ' . $parts[0];
+ } else {
+ if ($filter->isRootNode()) {
+ return implode(' ' . $op . ' ', $parts);
+ } else {
+ return '(' . implode(' ' . $op . ' ', $parts) . ')';
+ }
+ }
+ }
+
+ protected function fixFilterColumns(Filter $filter)
+ {
+ if ($filter->isExpression()) {
+ /** @var FilterExpression $filter */
+ $col = $filter->getColumn();
+ if (array_key_exists($col, $this->columnMap)) {
+ $filter->setColumn($this->columnMap[$col]);
+ }
+ if (strpos($col, 'vars.') === false) {
+ $filter->setExpression(json_decode($filter->getExpression()));
+ }
+ } else {
+ /** @var FilterChain $filter */
+ foreach ($filter->filters() as $sub) {
+ $this->fixFilterColumns($sub);
+ }
+ }
+ }
+
+ /**
+ * @param FilterExpression $filter
+ *
+ * @return string
+ */
+ protected function renderFilterExpression(FilterExpression $filter)
+ {
+ $query = $this->query;
+ $column = $query->getAliasForRequiredFilterColumn($filter->getColumn());
+ return $query->whereToSql(
+ $column,
+ $filter->getSign(),
+ $filter->getExpression()
+ );
+ }
+}
diff --git a/library/Director/Data/Db/IcingaObjectQuery.php b/library/Director/Data/Db/IcingaObjectQuery.php
new file mode 100644
index 0000000..4556ba7
--- /dev/null
+++ b/library/Director/Data/Db/IcingaObjectQuery.php
@@ -0,0 +1,255 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Data\Db\DbQuery;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Module\Director\Db;
+use Zend_Db_Expr as ZfDbExpr;
+use Zend_Db_Select as ZfDbSelect;
+
+class IcingaObjectQuery
+{
+ const BASE_ALIAS = 'o';
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var string */
+ protected $type;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var ZfDbSelect */
+ protected $query;
+
+ /** @var bool */
+ protected $resolved;
+
+ /** @var array joined tables, alias => table */
+ protected $requiredTables;
+
+ /** @var array maps table aliases, alias => table*/
+ protected $aliases;
+
+ /** @var DbQuery */
+ protected $dummyQuery;
+
+ /** @var array varname => alias */
+ protected $joinedVars = array();
+
+ protected $customVarTable;
+
+ protected $baseQuery;
+
+ /**
+ * IcingaObjectQuery constructor.
+ *
+ * @param string $type
+ * @param Db $connection
+ * @param bool $resolved
+ */
+ public function __construct($type, Db $connection, $resolved = true)
+ {
+ $this->type = $type;
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ $this->resolved = $resolved;
+ $baseTable = 'icinga_' . $type;
+ $this->baseQuery = $this->db->select()
+ ->from(
+ array(self::BASE_ALIAS => $baseTable),
+ array('name' => 'object_name')
+ )->order(self::BASE_ALIAS . '.object_name');
+ }
+
+ public function joinVar($name)
+ {
+ if (! $this->hasJoinedVar($name)) {
+ $type = $this->type;
+ $alias = $this->safeVarAlias($name);
+ $varAlias = "v_$alias";
+ // TODO: optionally $varRelation = sprintf('icinga_%s_resolved_var', $type);
+ $varRelation = sprintf('icinga_%s_var', $type);
+ $idCol = sprintf('%s.%s_id', $alias, $type);
+
+ $joinOn = sprintf('%s = %s.id', $idCol, self::BASE_ALIAS);
+ $joinVarOn = $this->db->quoteInto(
+ sprintf('%s.checksum = %s.checksum AND %s.varname = ?', $alias, $varAlias, $alias),
+ $name
+ );
+
+ $this->baseQuery->join(
+ array($alias => $varRelation),
+ $joinOn,
+ array()
+ )->join(
+ array($varAlias => 'icinga_var'),
+ $joinVarOn,
+ array($alias => $varAlias . '.varvalue')
+ );
+
+ $this->joinedVars[$name] = $varAlias . '.varvalue';
+ }
+
+ return $this;
+ }
+
+ // Debug only
+ public function getSql()
+ {
+ return (string) $this->baseQuery;
+ }
+
+ public function listNames()
+ {
+ return $this->db->fetchCol(
+ $this->baseQuery
+ );
+ }
+
+ protected function hasJoinedVar($name)
+ {
+ return array_key_exists($name, $this->joinedVars);
+ }
+
+ public function getJoinedVarAlias($name)
+ {
+ return $this->joinedVars[$name];
+ }
+
+ // TODO: recheck this
+ protected function safeVarAlias($name)
+ {
+ $alias = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $name);
+ $cnt = 1;
+ $checkAlias = $alias;
+ while (in_array($checkAlias, $this->joinedVars)) {
+ $cnt++;
+ $checkAlias = $alias . '_' . $cnt;
+ }
+
+ return $checkAlias;
+ }
+
+ public function escapedWhere($where)
+ {
+ $this->baseQuery->where(new ZfDbExpr($where));
+ }
+
+ /**
+ * @param $column
+ * @return string
+ * @throws NotFoundError
+ * @throws NotImplementedError
+ */
+ public function getAliasForRequiredFilterColumn($column)
+ {
+ list($key, $sub) = $this->splitFilterKey($column);
+ if ($sub === null) {
+ return $key;
+ } else {
+ $objectType = $key;
+ }
+
+ if ($objectType === $this->type) {
+ list($key, $sub) = $this->splitFilterKey($sub);
+ if ($sub === null) {
+ return $key;
+ }
+
+ if ($key === 'vars') {
+ return $this->joinVar($sub)->getJoinedVarAlias($sub);
+ } else {
+ throw new NotFoundError('Not yet, my type: %s - %s', $objectType, $key);
+ }
+ } else {
+ throw new NotImplementedError('Not yet: %s - %s', $objectType, $sub);
+ }
+ }
+
+ protected function splitFilterKey($key)
+ {
+ $dot = strpos($key, '.');
+ if ($dot === false) {
+ return [$key, null];
+ } else {
+ return [substr($key, 0, $dot), substr($key, $dot + 1)];
+ }
+ }
+
+ protected function requireTable($name)
+ {
+ if ($alias = $this->getTableAliasFromQuery($name)) {
+ return $alias;
+ }
+
+ $this->joinTable($name);
+ }
+
+ protected function joinTable($name)
+ {
+ if (!array_key_exists($name, $this->requiredTables)) {
+ $alias = $this->makeAlias($name);
+ }
+
+ return $this->tableAliases($name);
+ }
+
+ protected function hasAlias($name)
+ {
+ return array_key_exists($name, $this->aliases);
+ }
+
+ protected function makeAlias($name)
+ {
+ if (substr($name, 0, 7) === 'icinga_') {
+ $shortName = substr($name, 7);
+ } else {
+ $shortName = $name;
+ }
+
+ $parts = preg_split('/_/', $shortName, -1);
+ $alias = '';
+ foreach ($parts as $part) {
+ $alias .= $part[0];
+ if (! $this->hasAlias($alias)) {
+ return $alias;
+ }
+ }
+
+ $cnt = 1;
+ do {
+ $cnt++;
+ if (! $this->hasAlias($alias . $cnt)) {
+ return $alias . $cnt;
+ }
+ } while (! $this->hasAlias($alias));
+
+ return $alias;
+ }
+
+ protected function getTableAliasFromQuery($table)
+ {
+ $tables = $this->query->getPart('from');
+ $key = array_search($table, $tables);
+ if ($key === null || $key === false) {
+ return false;
+ }
+ /*
+ 'joinType' => $type,
+ 'schema' => $schema,
+ 'tableName' => $tableName,
+ 'joinCondition' => $cond
+ */
+ return $key;
+ }
+
+ public function whereToSql($col, $sign, $expression)
+ {
+ return $this->connection->renderFilter(Filter::expression($col, $sign, $expression));
+ }
+}
diff --git a/library/Director/Data/Db/ServiceSetQueryBuilder.php b/library/Director/Data/Db/ServiceSetQueryBuilder.php
new file mode 100644
index 0000000..7841d1e
--- /dev/null
+++ b/library/Director/Data/Db/ServiceSetQueryBuilder.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace Icinga\Module\Director\Data\Db;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\BranchSupport;
+use Icinga\Module\Director\Db\DbSelectParenthesis;
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Web\Table\TableWithBranchSupport;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+
+class ServiceSetQueryBuilder
+{
+ use TableWithBranchSupport;
+
+ const TABLE = BranchSupport::TABLE_ICINGA_SERVICE;
+ const BRANCHED_TABLE = BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE;
+ const SET_TABLE = BranchSupport::TABLE_ICINGA_SERVICE_SET;
+ const BRANCHED_SET_TABLE = BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE_SET;
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /**
+ * @param ?UuidInterface $uuid
+ */
+ public function __construct(Db $connection, $uuid = null)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ if ($uuid) {
+ $this->setBranchUuid($uuid);
+ }
+ }
+
+ /**
+ * @return \Zend_Db_Select
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function selectServicesForSet(IcingaServiceSet $set)
+ {
+ $db = $this->connection->getDbAdapter();
+ if ($this->branchUuid) {
+ $right = $this->selectRightBranchedServices($set)->columns($this->getRightBranchedColumns());
+ $left = $this->selectLeftBranchedServices($set)->columns($this->getLeftBranchedColumns());
+ $query = $this->db->select()->from(['u' => $db->select()->union([
+ 'l' => new DbSelectParenthesis($left),
+ 'r' => new DbSelectParenthesis($right),
+ ])]);
+ $query->order('service_set');
+ } else {
+ $query = $this->selectServices($set)->columns($this->getColumns());
+ }
+
+ return $query;
+ }
+
+ protected function selectServices(IcingaServiceSet $set)
+ {
+ return $this->db
+ ->select()
+ ->from(['o' =>self::TABLE], [])
+ ->joinLeft(['os' => self::SET_TABLE], 'os.id = o.service_set_id', [])
+ ->where('os.uuid = ?', $this->connection->quoteBinary($set->getUniqueId()->getBytes()));
+ }
+
+ protected function selectLeftBranchedServices(IcingaServiceSet $set)
+ {
+ return $this
+ ->selectServices($set)
+ ->joinLeft(
+ ['bo' => self::BRANCHED_TABLE],
+ $this->db->quoteInto('bo.uuid = o.uuid AND bo.branch_uuid = ?', $this->getQuotedBranchUuid()),
+ []
+ );
+ }
+
+ protected function selectRightBranchedServices(IcingaServiceSet $set)
+ {
+ return $this->db
+ ->select()
+ ->from(['o' => self::TABLE], [])
+ ->joinRight(['bo' => self::BRANCHED_TABLE], 'bo.uuid = o.uuid', [])
+ ->where('bo.service_set = ?', $set->get('object_name'))
+ ->where('bo.branch_uuid = ?', $this->getQuotedBranchUuid());
+ }
+
+ protected static function resetQueryProperties(\Zend_Db_Select $query)
+ {
+ // TODO: Keep existing UUID, becomes important when using this for other tables too (w/o UNION)
+ // $columns = $query->getPart($query::COLUMNS);
+ $query->reset($query::COLUMNS);
+ $query->columns('uuid');
+ return $query;
+ }
+
+ public function fetchServicesWithQuery(\Zend_Db_Select $query)
+ {
+ static::resetQueryProperties($query);
+ $db = $this->connection->getDbAdapter();
+ $uuids = $db->fetchCol($query);
+
+ $services = [];
+ foreach ($uuids as $uuid) {
+ $service = IcingaService::loadWithUniqueId(Uuid::fromBytes(DbUtil::binaryResult($uuid)), $this->connection);
+ $service->set('service_set', null); // TODO: CHECK THIS!!!!
+
+ $services[$service->getObjectName()] = $service;
+ }
+
+ return $services;
+ }
+
+ protected function getColumns()
+ {
+ return [
+ 'uuid' => 'o.uuid', // MUST be first because of UNION column order, see branchifyColumns()
+ 'id' => 'o.id',
+ 'branch_uuid' => '(null)',
+ 'service_set' => 'os.object_name',
+ 'service' => 'o.object_name',
+ 'disabled' => 'o.disabled',
+ 'object_type' => 'o.object_type',
+ 'blacklisted' => "('n')",
+ ];
+ }
+
+ protected function getLeftBranchedColumns()
+ {
+ $columns = $this->getColumns();
+ $columns['branch_uuid'] = 'bo.branch_uuid';
+ $columns['service_set'] = 'COALESCE(os.object_name, bo.service_set)';
+
+ return $this->branchifyColumns($columns);
+ }
+
+ protected function getRightBranchedColumns()
+ {
+ $columns = $this->getColumns();
+ $columns = $this->branchifyColumns($columns);
+ $columns['branch_uuid'] = 'bo.branch_uuid';
+ $columns['service_set'] = 'bo.service_set';
+ $columns['id'] = '(NULL)';
+
+ return $columns;
+ }
+
+ protected function getQuotedBranchUuid()
+ {
+ return $this->connection->quoteBinary($this->branchUuid->getBytes());
+ }
+}
diff --git a/library/Director/Data/Exporter.php b/library/Director/Data/Exporter.php
new file mode 100644
index 0000000..a2e3191
--- /dev/null
+++ b/library/Director/Data/Exporter.php
@@ -0,0 +1,303 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use gipfl\ZfDb\Adapter\Adapter;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use Icinga\Module\Director\Objects\DirectorDatalistEntry;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Objects\IcingaTemplateChoice;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\InstantiatedViaHook;
+use Icinga\Module\Director\Objects\SyncRule;
+use RuntimeException;
+
+class Exporter
+{
+ /** @var Adapter|\Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var FieldReferenceLoader */
+ protected $fieldReferenceLoader;
+
+ /** @var ?HostServiceLoader */
+ protected $serviceLoader = null;
+
+ protected $exportHostServices = false;
+ protected $showDefaults = false;
+ protected $showIds = false;
+ protected $resolveObjects = false;
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var ?array */
+ protected $chosenProperties = null;
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ $this->fieldReferenceLoader = new FieldReferenceLoader($connection);
+ }
+
+ public function export(DbObject $object)
+ {
+ $props = $object instanceof IcingaObject
+ ? $this->exportIcingaObject($object)
+ : $this->exportDbObject($object);
+
+ ImportExportDeniedProperties::strip($props, $object, $this->showIds);
+ $this->appendTypeSpecificRelations($props, $object);
+
+ if ($this->chosenProperties !== null) {
+ $chosen = [];
+ foreach ($this->chosenProperties as $k) {
+ if (array_key_exists($k, $props)) {
+ $chosen[$k] = $props[$k];
+ }
+ }
+
+ $props = $chosen;
+ }
+
+ ksort($props);
+ return (object) $props;
+ }
+
+ public function enableHostServices($enable = true)
+ {
+ $this->exportHostServices = $enable;
+ return $this;
+ }
+
+ public function showDefaults($show = true)
+ {
+ $this->showDefaults = $show;
+ return $this;
+ }
+
+ public function showIds($show = true)
+ {
+ $this->showIds = $show;
+ return $this;
+ }
+
+ public function resolveObjects($resolve = true)
+ {
+ $this->resolveObjects = $resolve;
+ if ($this->serviceLoader) {
+ $this->serviceLoader->resolveObjects($resolve);
+ }
+
+ return $this;
+ }
+
+ public function filterProperties(array $properties)
+ {
+ $this->chosenProperties = $properties;
+ return $this;
+ }
+
+ protected function appendTypeSpecificRelations(array &$props, DbObject $object)
+ {
+ if ($object instanceof DirectorDatalist) {
+ $props['entries'] = $this->exportDatalistEntries($object);
+ } elseif ($object instanceof DirectorDatafield) {
+ if (isset($props['settings']->datalist_id)) {
+ $props['settings']->datalist = $this->getDatalistNameForId($props['settings']->datalist_id);
+ unset($props['settings']->datalist_id);
+ }
+
+ $props['category'] = isset($props['category_id'])
+ ? $this->getDatafieldCategoryNameForId($props['category_id'])
+ : null;
+ unset($props['category_id']);
+ } elseif ($object instanceof ImportSource) {
+ $props['modifiers'] = $this->exportRowModifiers($object);
+ } elseif ($object instanceof SyncRule) {
+ $props['properties'] = $this->exportSyncProperties($object);
+ } elseif ($object instanceof IcingaCommand) {
+ if (isset($props['arguments'])) {
+ foreach ($props['arguments'] as $key => $argument) {
+ if (property_exists($argument, 'command_id')) {
+ unset($props['arguments'][$key]->command_id);
+ }
+ }
+ }
+ } elseif ($object instanceof DirectorJob) {
+ if ($object->hasTimeperiod()) {
+ $props['timeperiod'] = $object->timeperiod()->getObjectName();
+ }
+ unset($props['timeperiod_id']);
+ } elseif ($object instanceof IcingaTemplateChoice) {
+ if (isset($props['required_template_id'])) {
+ $requiredId = $props['required_template_id'];
+ unset($props['required_template_id']);
+ $props = $this->loadTemplateName($object->getObjectTableName(), $requiredId);
+ }
+
+ $props['members'] = array_values($object->getMembers());
+ } elseif ($object instanceof IcingaServiceSet) {
+ if ($object->get('host_id')) {
+ // Sets on Host
+ throw new RuntimeException('Not yet');
+ }
+ $props['services'] = [];
+ foreach ($object->getServiceObjects() as $serviceObject) {
+ $props['services'][$serviceObject->getObjectName()] = $this->export($serviceObject);
+ }
+ ksort($props['services']);
+ } elseif ($object instanceof IcingaHost) {
+ if ($this->exportHostServices) {
+ $services = [];
+ foreach ($this->serviceLoader()->fetchServicesForHost($object) as $service) {
+ $services[] = $this->export($service);
+ }
+
+ $props['services'] = $services;
+ }
+ }
+ }
+
+ public function serviceLoader()
+ {
+ if ($this->serviceLoader === null) {
+ $this->serviceLoader = new HostServiceLoader($this->connection);
+ $this->serviceLoader->resolveObjects($this->resolveObjects);
+ }
+
+ return $this->serviceLoader;
+ }
+
+ protected function loadTemplateName($table, $id)
+ {
+ $db = $this->db;
+ $query = $db->select()
+ ->from(['o' => $table], 'o.object_name')->where("o.object_type = 'template'")
+ ->where('o.id = ?', $id);
+
+ return $db->fetchOne($query);
+ }
+
+ protected function getDatalistNameForId($id)
+ {
+ $db = $this->db;
+ $query = $db->select()->from('director_datalist', 'list_name')->where('id = ?', (int) $id);
+ return $db->fetchOne($query);
+ }
+
+ protected function getDatafieldCategoryNameForId($id)
+ {
+ $db = $this->db;
+ $query = $db->select()->from('director_datafield_category', 'category_name')->where('id = ?', (int) $id);
+ return $db->fetchOne($query);
+ }
+
+ protected function exportRowModifiers(ImportSource $object)
+ {
+ $modifiers = [];
+ // Hint: they're sorted by priority
+ foreach ($object->fetchRowModifiers() as $modifier) {
+ $modifiers[] = $this->export($modifier);
+ }
+
+ return $modifiers;
+ }
+
+ public function exportSyncProperties(SyncRule $object)
+ {
+ $all = [];
+ $db = $this->db;
+ $sourceNames = $db->fetchPairs(
+ $db->select()->from('import_source', ['id', 'source_name'])
+ );
+
+ foreach ($object->getSyncProperties() as $property) {
+ $properties = $property->getProperties();
+ $properties['source'] = $sourceNames[$properties['source_id']];
+ unset($properties['id']);
+ unset($properties['rule_id']);
+ unset($properties['source_id']);
+ ksort($properties);
+ $all[] = (object) $properties;
+ }
+
+ return $all;
+ }
+
+ /**
+ * @param DbObject $object
+ * @return array
+ */
+ protected function exportDbObject(DbObject $object)
+ {
+ $props = $object->getProperties();
+ if ($object instanceof DbObjectWithSettings) {
+ if ($object instanceof InstantiatedViaHook) {
+ $props['settings'] = (object) $object->getInstance()->exportSettings();
+ } else {
+ $props['settings'] = (object) $object->getSettings(); // Already sorted
+ }
+ }
+ unset($props['uuid']); // Not yet
+ if (! $this->showDefaults) {
+ foreach ($props as $key => $value) {
+ // We assume NULL as a default value for all non-IcingaObject properties
+ if ($value === null) {
+ unset($props[$key]);
+ }
+ }
+ }
+
+ return $props;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return array
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function exportIcingaObject(IcingaObject $object)
+ {
+ $props = (array) $object->toPlainObject($this->resolveObjects, !$this->showDefaults);
+ if ($object->supportsFields()) {
+ $props['fields'] = $this->fieldReferenceLoader->loadFor($object);
+ }
+
+ return $props;
+ }
+
+ protected function exportDatalistEntries(DirectorDatalist $list)
+ {
+ $entries = [];
+ $id = $list->get('id');
+ if ($id === null) {
+ return $entries;
+ }
+
+ $dbEntries = DirectorDatalistEntry::loadAllForList($list);
+ // Hint: they are loaded with entry_name key
+ ksort($dbEntries);
+
+ foreach ($dbEntries as $entry) {
+ if ($entry->shouldBeRemoved()) {
+ continue;
+ }
+ $plainEntry = $entry->getProperties();
+ unset($plainEntry['list_id']);
+
+ $entries[] = $plainEntry;
+ }
+
+ return $entries;
+ }
+}
diff --git a/library/Director/Data/FieldReferenceLoader.php b/library/Director/Data/FieldReferenceLoader.php
new file mode 100644
index 0000000..1e3d92e
--- /dev/null
+++ b/library/Director/Data/FieldReferenceLoader.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use gipfl\ZfDb\Adapter\Adapter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class FieldReferenceLoader
+{
+ /** @var Adapter|\Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ public function __construct(Db $connection)
+ {
+ $this->db = $connection->getDbAdapter();
+ }
+
+ /**
+ * @param int $id
+ * @return array
+ */
+ public function loadFor(IcingaObject $object)
+ {
+ $db = $this->db;
+ $id = $object->get('id');
+ if ($id === null) {
+ return [];
+ }
+ $type = $object->getShortTableName();
+ $res = $db->fetchAll(
+ $db->select()->from(['f' => "icinga_${type}_field"], [
+ 'f.datafield_id',
+ 'f.is_required',
+ 'f.var_filter',
+ ])->join(['df' => 'director_datafield'], 'df.id = f.datafield_id', [])
+ ->where("${type}_id = ?", (int) $id)
+ ->order('varname ASC')
+ );
+
+ if (empty($res)) {
+ return [];
+ }
+
+ foreach ($res as $field) {
+ $field->datafield_id = (int) $field->datafield_id;
+ }
+
+ return $res;
+ }
+}
diff --git a/library/Director/Data/HostServiceLoader.php b/library/Director/Data/HostServiceLoader.php
new file mode 100644
index 0000000..4cc4b96
--- /dev/null
+++ b/library/Director/Data/HostServiceLoader.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use gipfl\IcingaWeb2\Table\QueryBasedTable;
+use gipfl\ZfDb\Select;
+use Icinga\Data\SimpleQuery;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\AppliedServiceSetLoader;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use Icinga\Module\Director\Web\Table\IcingaHostAppliedServicesTable;
+use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable;
+use Icinga\Module\Director\Web\Table\ObjectsTableService;
+use Ramsey\Uuid\Uuid;
+use RuntimeException;
+use Zend_Db_Select;
+
+class HostServiceLoader
+{
+ /** @var Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var bool */
+ protected $resolveHostServices = false;
+
+ /** @var bool */
+ protected $resolveObjects = false;
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ }
+
+ public function fetchServicesForHost(IcingaHost $host)
+ {
+ $table = (new ObjectsTableService($this->connection))->setHost($host);
+ $services = $this->fetchServicesForTable($table);
+ if ($this->resolveHostServices) {
+ foreach ($this->fetchAllServicesForHost($host) as $service) {
+ $services[] = $service;
+ }
+ }
+
+ return $services;
+ }
+
+ public function resolveHostServices($enable = true)
+ {
+ $this->resolveHostServices = $enable;
+ return $this;
+ }
+
+ public function resolveObjects($resolve = true)
+ {
+ $this->resolveObjects = $resolve;
+ return $this;
+ }
+
+ protected function fetchAllServicesForHost(IcingaHost $host)
+ {
+ $services = [];
+ /** @var IcingaHost[] $parents */
+ $parents = IcingaTemplateRepository::instanceByObject($host)->getTemplatesFor($host, true);
+ foreach ($parents as $parent) {
+ $table = (new ObjectsTableService($this->connection))
+ ->setHost($parent)
+ ->setInheritedBy($host);
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+ }
+
+ foreach ($this->getHostServiceSetTables($host) as $table) {
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+ }
+ foreach ($parents as $parent) {
+ foreach ($this->getHostServiceSetTables($parent, $host) as $table) {
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+ }
+ }
+
+ $appliedSets = AppliedServiceSetLoader::fetchForHost($host);
+ foreach ($appliedSets as $set) {
+ $table = IcingaServiceSetServiceTable::load($set)
+ // ->setHost($host)
+ ->setAffectedHost($host);
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+ }
+
+ $table = IcingaHostAppliedServicesTable::load($host);
+ foreach ($this->fetchServicesForTable($table) as $service) {
+ $services[] = $service;
+ }
+
+ return $services;
+ }
+
+ /**
+ * Duplicates Logic in HostController
+ *
+ * @param IcingaHost $host
+ * @param IcingaHost|null $affectedHost
+ * @return IcingaServiceSetServiceTable[]
+ */
+ protected function getHostServiceSetTables(IcingaHost $host, IcingaHost $affectedHost = null)
+ {
+ $tables = [];
+ $db = $this->connection;
+ if ($affectedHost === null) {
+ $affectedHost = $host;
+ }
+ if ($host->get('id') === null) {
+ return $tables;
+ }
+
+ $query = $db->getDbAdapter()->select()
+ ->from(['ss' => 'icinga_service_set'], 'ss.*')
+ ->join(['hsi' => 'icinga_service_set_inheritance'], 'hsi.parent_service_set_id = ss.id', [])
+ ->join(['hs' => 'icinga_service_set'], 'hs.id = hsi.service_set_id', [])
+ ->where('hs.host_id = ?', $host->get('id'));
+
+ $sets = IcingaServiceSet::loadAll($db, $query, 'object_name');
+ /** @var IcingaServiceSet $set*/
+ foreach ($sets as $name => $set) {
+ $tables[] = IcingaServiceSetServiceTable::load($set)
+ ->setHost($host)
+ ->setAffectedHost($affectedHost);
+ }
+
+ return $tables;
+ }
+
+ protected function fetchServicesForTable(QueryBasedTable $table)
+ {
+ $query = $table->getQuery();
+ if ($query instanceof Select || $query instanceof Zend_Db_Select) {
+ // What about SimpleQuery? IcingaHostAppliedServicesTable with branch in place?
+ $query->reset(Select::LIMIT_COUNT);
+ $query->reset(Select::LIMIT_OFFSET);
+ $rows = $this->db->fetchAll($query);
+ } elseif ($query instanceof SimpleQuery) {
+ $rows = $query->fetchAll();
+ } else {
+ throw new RuntimeException('Table query needs to be either a Select or a SimpleQuery instance');
+ }
+ $services = [];
+ foreach ($rows as $row) {
+ $service = IcingaService::loadWithUniqueId(Uuid::fromBytes($row->uuid), $this->connection);
+ if ($this->resolveObjects) {
+ $service = $service::fromPlainObject($service->toPlainObject(true), $this->connection);
+ }
+ $services[] = $service;
+ }
+
+ return $services;
+ }
+}
diff --git a/library/Director/Data/ImportExportDeniedProperties.php b/library/Director/Data/ImportExportDeniedProperties.php
new file mode 100644
index 0000000..747eb0f
--- /dev/null
+++ b/library/Director/Data/ImportExportDeniedProperties.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\ImportRowModifier;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class ImportExportDeniedProperties
+{
+ protected static $denyProperties = [
+ DirectorJob::class => [
+ 'last_attempt_succeeded',
+ 'last_error_message',
+ 'ts_last_attempt',
+ 'ts_last_error',
+ ],
+ ImportSource::class => [
+ // No state export
+ 'import_state',
+ 'last_error_message',
+ 'last_attempt',
+ ],
+ ImportRowModifier::class => [
+ // Not state, but to be removed:
+ 'source_id',
+ ],
+ SyncRule::class => [
+ 'sync_state',
+ 'last_error_message',
+ 'last_attempt',
+ ],
+ ];
+
+ public static function strip(array &$props, DbObject $object, $showIds = false)
+ {
+ // TODO: this used to exist. Double-check all imports to verify it's not in use
+ // $originalId = $props['id'];
+
+ if (! $showIds) {
+ unset($props['id']);
+ }
+ $class = get_class($object);
+ if (isset(self::$denyProperties[$class])) {
+ foreach (self::$denyProperties[$class] as $key) {
+ unset($props[$key]);
+ }
+ }
+ }
+}
diff --git a/library/Director/Data/InvalidDataException.php b/library/Director/Data/InvalidDataException.php
new file mode 100644
index 0000000..9abaf7c
--- /dev/null
+++ b/library/Director/Data/InvalidDataException.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use InvalidArgumentException;
+
+class InvalidDataException extends InvalidArgumentException
+{
+ /**
+ * @param string $expected
+ * @param mixed $value
+ */
+ public function __construct($expected, $value)
+ {
+ parent::__construct("$expected expected, got " . static::getPhpType($value));
+ }
+
+ public static function getPhpType($var)
+ {
+ if (is_object($var)) {
+ return get_class($var);
+ }
+
+ return gettype($var);
+ }
+}
diff --git a/library/Director/Data/Json.php b/library/Director/Data/Json.php
new file mode 100644
index 0000000..78b3e67
--- /dev/null
+++ b/library/Director/Data/Json.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use Icinga\Module\Director\Exception\JsonEncodeException;
+use function json_decode;
+use function json_encode;
+use function json_last_error;
+
+class Json
+{
+ const DEFAULT_FLAGS = JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
+
+ /**
+ * Encode with well-known flags, as we require the result to be reproducible
+ *
+ * @param $mixed
+ * @param int|null $flags
+ * @return string
+ * @throws JsonEncodeException
+ */
+ public static function encode($mixed, $flags = null)
+ {
+ if ($flags === null) {
+ $flags = self::DEFAULT_FLAGS;
+ } else {
+ $flags = self::DEFAULT_FLAGS | $flags;
+ }
+ $result = json_encode($mixed, $flags);
+
+ if ($result === false && json_last_error() !== JSON_ERROR_NONE) {
+ throw JsonEncodeException::forLastJsonError();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Decode the given JSON string and make sure we get a meaningful Exception
+ *
+ * @param string $string
+ * @return mixed
+ * @throws JsonEncodeException
+ */
+ public static function decode($string)
+ {
+ $result = json_decode($string);
+
+ if ($result === null && json_last_error() !== JSON_ERROR_NONE) {
+ throw JsonEncodeException::forLastJsonError();
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param $string
+ * @return ?string
+ * @throws JsonEncodeException
+ */
+ public static function decodeOptional($string)
+ {
+ if ($string === null) {
+ return null;
+ }
+
+ return static::decode($string);
+ }
+}
diff --git a/library/Director/Data/PropertiesFilter.php b/library/Director/Data/PropertiesFilter.php
new file mode 100644
index 0000000..a8c3906
--- /dev/null
+++ b/library/Director/Data/PropertiesFilter.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+class PropertiesFilter
+{
+ public static $CUSTOM_PROPERTY = 'CUSTOM_PROPERTY';
+ public static $HOST_PROPERTY = 'HOST_PROPERTY';
+ public static $SERVICE_PROPERTY = 'SERVICE_PROPERTY';
+
+ protected $blacklist = array(
+ 'id',
+ 'object_name',
+ 'object_type',
+ 'disabled',
+ 'has_agent',
+ 'master_should_connect',
+ 'accept_config',
+ );
+
+ public function match($type, $name, $object = null)
+ {
+ return ($type != self::$HOST_PROPERTY || !in_array($name, $this->blacklist));
+ }
+}
diff --git a/library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php b/library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php
new file mode 100644
index 0000000..ef9f2d4
--- /dev/null
+++ b/library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Director\Data\PropertiesFilter;
+
+class ArrayCustomVariablesFilter extends CustomVariablesFilter
+{
+ public function match($type, $name, $object = null)
+ {
+ return parent::match($type, $name, $object)
+ && $object !== null
+ && isset($object->datatype)
+ && (
+ preg_match('/DataTypeArray[\w]*$/', $object->datatype)
+ || (
+ preg_match('/DataTypeDatalist$/', $object->datatype)
+ && $object->format === 'json'
+ )
+ );
+ }
+}
diff --git a/library/Director/Data/PropertiesFilter/CustomVariablesFilter.php b/library/Director/Data/PropertiesFilter/CustomVariablesFilter.php
new file mode 100644
index 0000000..91ef9cd
--- /dev/null
+++ b/library/Director/Data/PropertiesFilter/CustomVariablesFilter.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Icinga\Module\Director\Data\PropertiesFilter;
+
+use Icinga\Module\Director\Data\PropertiesFilter;
+
+class CustomVariablesFilter extends PropertiesFilter
+{
+ public function match($type, $name, $object = null)
+ {
+ return parent::match($type, $name, $object) && $type === self::$CUSTOM_PROPERTY;
+ }
+}
diff --git a/library/Director/Data/PropertyMangler.php b/library/Director/Data/PropertyMangler.php
new file mode 100644
index 0000000..a457f1d
--- /dev/null
+++ b/library/Director/Data/PropertyMangler.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use InvalidArgumentException;
+
+class PropertyMangler
+{
+ public static function appendToArrayProperties(IcingaObject $object, $properties)
+ {
+ foreach ($properties as $key => $value) {
+ $current = $object->$key;
+ if ($current === null) {
+ $current = [$value];
+ } elseif (is_array($current)) {
+ $current[] = $value;
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'I can only append to arrays, %s is %s',
+ $key,
+ var_export($current, 1)
+ ));
+ }
+
+ $object->$key = $current;
+ }
+ }
+
+ public static function removeProperties(IcingaObject $object, $properties)
+ {
+ foreach ($properties as $key => $value) {
+ if ($value === true) {
+ $object->$key = null;
+ }
+ $current = $object->$key;
+ if ($current === null) {
+ continue;
+ } elseif (is_array($current)) {
+ $new = [];
+ foreach ($current as $item) {
+ if ($item !== $value) {
+ $new[] = $item;
+ }
+ }
+ $object->$key = $new;
+ } elseif (is_string($current)) {
+ if ($current === $value) {
+ $object->$key = null;
+ }
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'I can only remove strings or from arrays, %s is %s',
+ $key,
+ var_export($current, 1)
+ ));
+ }
+ }
+ }
+}
diff --git a/library/Director/Data/RecursiveUtf8Validator.php b/library/Director/Data/RecursiveUtf8Validator.php
new file mode 100644
index 0000000..cadfc21
--- /dev/null
+++ b/library/Director/Data/RecursiveUtf8Validator.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use InvalidArgumentException;
+use ipl\Html\Error;
+
+class RecursiveUtf8Validator
+{
+ protected static $rowNum;
+
+ protected static $column;
+
+ /**
+ * @param array $rows Usually array of stdClass
+ * @return bool
+ */
+ public static function validateRows($rows)
+ {
+ foreach ($rows as self::$rowNum => $row) {
+ foreach ($row as self::$column => $value) {
+ static::assertUtf8($value);
+ }
+ }
+
+ return true;
+ }
+
+ protected static function assertUtf8($value)
+ {
+ if (\is_string($value)) {
+ static::assertUtf8String($value);
+ } elseif (\is_array($value) || $value instanceof \stdClass) {
+ foreach ((array) $value as $k => $v) {
+ static::assertUtf8($k);
+ static::assertUtf8($v);
+ }
+ } elseif ($value !== null && !\is_scalar($value)) {
+ throw new InvalidArgumentException("Cannot validate " . Error::getPhpTypeName($value));
+ }
+ }
+
+ protected static function assertUtf8String($string)
+ {
+ if (@\iconv('UTF-8', 'UTF-8', $string) != $string) {
+ $row = self::$rowNum;
+ if (is_int($row)) {
+ $row++;
+ }
+ throw new InvalidArgumentException(\sprintf(
+ 'Invalid UTF-8 on row %s, column %s: "%s" (%s)',
+ $row,
+ self::$column,
+ \iconv('UTF-8', 'UTF-8//IGNORE', $string),
+ '0x' . \bin2hex($string)
+ ));
+ }
+ }
+}
diff --git a/library/Director/Data/Serializable.php b/library/Director/Data/Serializable.php
new file mode 100644
index 0000000..9f8cb63
--- /dev/null
+++ b/library/Director/Data/Serializable.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use JsonSerializable;
+
+interface Serializable extends JsonSerializable
+{
+ public static function fromSerialization($value);
+}
diff --git a/library/Director/Data/SerializableValue.php b/library/Director/Data/SerializableValue.php
new file mode 100644
index 0000000..5784224
--- /dev/null
+++ b/library/Director/Data/SerializableValue.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Data;
+
+use InvalidArgumentException;
+use JsonSerializable;
+use stdClass;
+use function get_class;
+use function gettype;
+use function is_array;
+use function is_object;
+use function is_scalar;
+
+class SerializableValue implements Serializable
+{
+ protected $value = [];
+
+ /**
+ * @param stdClass|array $object
+ * @return static
+ */
+ public static function fromSerialization($value)
+ {
+ $self = new static;
+ static::assertSerializableValue($value);
+ $self->value = $value;
+
+ return $self;
+ }
+
+ public static function wantSerializable($value)
+ {
+ if ($value instanceof SerializableValue) {
+ return $value;
+ }
+
+ return static::fromSerialization($value);
+ }
+
+ /**
+ * TODO: Check whether json_encode() is faster
+ *
+ * @param mixed $value
+ * @return bool
+ */
+ protected static function assertSerializableValue($value)
+ {
+ if ($value === null || is_scalar($value)) {
+ return true;
+ }
+ if (is_object($value)) {
+ if ($value instanceof JsonSerializable) {
+ return true;
+ }
+
+ if ($value instanceof stdClass) {
+ foreach ((array) $value as $val) {
+ static::assertSerializableValue($val);
+ }
+
+ return true;
+ }
+ }
+
+ if (is_array($value)) {
+ foreach ($value as $val) {
+ static::assertSerializableValue($val);
+ }
+
+ return true;
+ }
+
+ throw new InvalidArgumentException('Serializable value expected, got ' . static::getPhpType($value));
+ }
+
+ protected static function getPhpType($var)
+ {
+ if (is_object($var)) {
+ return get_class($var);
+ }
+
+ return gettype($var);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ return $this->value;
+ }
+}
diff --git a/library/Director/Data/ValueFilter.php b/library/Director/Data/ValueFilter.php
new file mode 100644
index 0000000..926214f
--- /dev/null
+++ b/library/Director/Data/ValueFilter.php
@@ -0,0 +1,10 @@
+<?php
+
+// TODO: move elsewhere, this is for forms
+namespace Icinga\Module\Director\Data;
+
+use Zend_Filter_Interface;
+
+interface ValueFilter extends Zend_Filter_Interface
+{
+}
diff --git a/library/Director/Data/ValueFilter/FilterBoolean.php b/library/Director/Data/ValueFilter/FilterBoolean.php
new file mode 100644
index 0000000..1fadec3
--- /dev/null
+++ b/library/Director/Data/ValueFilter/FilterBoolean.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\Data\ValueFilter;
+
+use Icinga\Module\Director\Data\ValueFilter;
+
+class FilterBoolean implements ValueFilter
+{
+ public function filter($value)
+ {
+ if ($value === 'y' || $value === true) {
+ return true;
+ } elseif ($value === 'n' || $value === false) {
+ return false;
+ }
+
+ return null;
+ }
+}
diff --git a/library/Director/Data/ValueFilter/FilterInt.php b/library/Director/Data/ValueFilter/FilterInt.php
new file mode 100644
index 0000000..d51ce8d
--- /dev/null
+++ b/library/Director/Data/ValueFilter/FilterInt.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Icinga\Module\Director\Data\ValueFilter;
+
+use Icinga\Module\Director\Data\ValueFilter;
+
+class FilterInt implements ValueFilter
+{
+ public function filter($value)
+ {
+ if ($value === '' || $value === null) {
+ return null;
+ }
+
+ if (is_string($value) && ! ctype_digit($value)) {
+ return $value;
+ }
+
+ return (int) ((string) $value);
+ }
+}
diff --git a/library/Director/DataType/DataTypeArray.php b/library/Director/DataType/DataTypeArray.php
new file mode 100644
index 0000000..a7667d9
--- /dev/null
+++ b/library/Director/DataType/DataTypeArray.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\DataType;
+
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class DataTypeArray extends DataTypeHook
+{
+ public function getFormElement($name, QuickForm $form)
+ {
+ return $form->createElement('extensibleSet', $name);
+ }
+}
diff --git a/library/Director/DataType/DataTypeBoolean.php b/library/Director/DataType/DataTypeBoolean.php
new file mode 100644
index 0000000..d9edc60
--- /dev/null
+++ b/library/Director/DataType/DataTypeBoolean.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\DataType;
+
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Web\Form\Decorator\ViewHelperRaw;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Zend_Form_Element as ZfElement;
+
+class DataTypeBoolean extends DataTypeHook
+{
+ public function getFormElement($name, QuickForm $form)
+ {
+ return $this->applyRawViewHelper(
+ $form->createElement('boolean', $name)
+ );
+ }
+
+ protected function applyRawViewHelper(ZfElement $element)
+ {
+ $vhClass = 'Zend_Form_Decorator_ViewHelper';
+ $decorators = $element->getDecorators();
+ if (array_key_exists($vhClass, $decorators)) {
+ $decorators[$vhClass] = new ViewHelperRaw;
+ $element->setDecorators($decorators);
+ }
+
+ return $element;
+ }
+}
diff --git a/library/Director/DataType/DataTypeDatalist.php b/library/Director/DataType/DataTypeDatalist.php
new file mode 100644
index 0000000..354c7c3
--- /dev/null
+++ b/library/Director/DataType/DataTypeDatalist.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Icinga\Module\Director\DataType;
+
+use Icinga\Module\Director\Acl;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Objects\DirectorDatalistEntry;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\Validate\IsDataListEntry;
+
+class DataTypeDatalist extends DataTypeHook
+{
+ /**
+ * @param $name
+ * @param QuickForm $form
+ * @return \Zend_Form_Element
+ * @throws \Zend_Form_Exception
+ */
+ public function getFormElement($name, QuickForm $form)
+ {
+ $params = [];
+ $behavior = $this->getSetting('behavior', 'strict');
+ $targetDataType = $this->getSetting('data_type', 'string');
+ $listId = $this->getSetting('datalist_id');
+
+ if ($behavior === 'strict') {
+ $enum = $this->getEntries($form);
+ if ($targetDataType === 'string') {
+ $params['sorted'] = true;
+ $params = ['multiOptions' => $form->optionalEnum($enum)];
+ $type = 'select';
+ } else {
+ $params = ['multiOptions' => $form->optionalEnum($enum)];
+ $type = 'extensibleSet';
+ }
+ } else {
+ if ($targetDataType === 'string') {
+ $type = 'text';
+ } else {
+ $type = 'extensibleSet';
+ }
+ $params['class'] = 'director-suggest';
+ $params['data-suggestion-context'] = "dataListValuesForListId!$listId";
+ }
+ $element = $form->createElement($type, $name, $params);
+ if ($behavior === 'suggest_strict') {
+ $element->addValidator(new IsDataListEntry($listId, $form->getDb()));
+ }
+
+ if ($behavior === 'suggest_extend') {
+ $form->callOnSuccess(function (DirectorForm $form) use ($name, $listId) {
+ $value = (array) $form->getValue($name);
+ if ($value === null) {
+ return;
+ }
+
+ $db = $form->getDb();
+ foreach ($value as $entry) {
+ if ($entry !== '') {
+ $this->createEntryIfNotExists($db, $listId, $entry);
+ }
+ }
+ });
+ }
+
+ return $element;
+ }
+
+ /**
+ * @param Db $db
+ * @param $listId
+ * @param $entry
+ */
+ protected function createEntryIfNotExists(Db $db, $listId, $entry)
+ {
+ if (! DirectorDatalistEntry::exists([
+ 'list_id' => $listId,
+ 'entry_name' => $entry,
+ ], $db)) {
+ DirectorDatalistEntry::create([
+ 'list_id' => $listId,
+ 'entry_name' => $entry,
+ 'entry_value' => $entry,
+ ])->store($db);
+ }
+ }
+
+ protected function getEntries(QuickForm $form)
+ {
+ /** @var DirectorObjectForm $form */
+ $db = $form->getDb()->getDbAdapter();
+ $roles = Acl::instance()->listRoleNames();
+ $select = $db->select()
+ ->from('director_datalist_entry', ['entry_name', 'entry_value'])
+ ->where('list_id = ?', $this->getSetting('datalist_id'))
+ ->order('entry_value ASC');
+
+ if (empty($roles)) {
+ $select->where('allowed_roles IS NULL');
+ } else {
+ $parts = ['allowed_roles IS NULL'];
+ foreach ($roles as $role) {
+ $parts[] = $db->quoteInto("allowed_roles LIKE ?", '%' . \json_encode($role) . '%');
+ }
+ $select->where('(' . \implode(' OR ', $parts) . ')');
+ }
+
+ return $db->fetchPairs($select);
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ /** @var DirectorObjectForm $form */
+ $db = $form->getDb();
+
+ $form->addElement('select', 'datalist_id', [
+ 'label' => 'List name',
+ 'required' => true,
+ 'multiOptions' => $form->optionalEnum($db->enumDatalist()),
+ ]);
+
+ $form->addElement('select', 'data_type', [
+ 'label' => $form->translate('Target data type'),
+ 'multiOptions' => $form->optionalEnum([
+ 'string' => $form->translate('String'),
+ 'array' => $form->translate('Array'),
+ ]),
+ 'description' => $form->translate(
+ 'Whether this should be a String or an Array in the generated'
+ . ' Icinga configuration. In case you opt for Array, Director'
+ . ' users will be able to select multiple elements from the list'
+ ),
+ 'required' => true,
+ ]);
+
+ $form->addElement('select', 'behavior', [
+ 'label' => $form->translate('Element behavior'),
+ 'value' => 'strict',
+ 'description' => $form->translate(
+ 'This allows to show either a drop-down list or an auto-completion'
+ ),
+ 'multiOptions' => [
+ 'strict' => $form->translate('Dropdown (list values only)'),
+ $form->translate('Autocomplete') => [
+ 'suggest_strict' => $form->translate('Strict, list values only'),
+ 'suggest_optional' => $form->translate('Allow for values not on the list'),
+ 'suggest_extend' => $form->translate('Extend the list with new values'),
+ ]
+ ]
+ ]);
+ }
+}
diff --git a/library/Director/DataType/DataTypeDictionary.php b/library/Director/DataType/DataTypeDictionary.php
new file mode 100644
index 0000000..7880698
--- /dev/null
+++ b/library/Director/DataType/DataTypeDictionary.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Icinga\Module\Director\DataType;
+
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use InvalidArgumentException;
+use ipl\Html\Html;
+use RuntimeException;
+
+class DataTypeDictionary extends DataTypeHook
+{
+ public function getFormElement($name, QuickForm $form)
+ {
+ if (strpos($name, 'var_') !== 0) {
+ throw new InvalidArgumentException(
+ "'$name' is not a valid candidate for a Nested Dictionary, 'var_*' expected"
+ );
+ }
+ /** @var DirectorObjectForm $form */
+ $object = $form->getObject();
+ if ($form->isTemplate()) {
+ return $form->createElement('simpleNote', $name, [
+ 'ignore' => true,
+ 'value' => Html::tag('span', $form->translate('To be managed on objects only')),
+ ]);
+ }
+ if (! $object->hasBeenLoadedFromDb()) {
+ return $form->createElement('simpleNote', $name, [
+ 'ignore' => true,
+ 'value' => Html::tag(
+ 'span',
+ $form->translate('Can be managed once this object has been created')
+ ),
+ ]);
+ }
+ $params = [
+ 'varname' => substr($name, 4),
+ ];
+ if ($object instanceof IcingaHost) {
+ $params['host'] = $object->getObjectName();
+ } elseif ($object instanceof IcingaService) {
+ $params['host'] = $object->get('host');
+ $params['service'] = $object->getObjectName();
+ }
+ return $form->createElement('InstanceSummary', $name, [
+ 'linkParams' => $params
+ ]);
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ /** @var DirectorObjectForm $form */
+ $db = $form->getDb()->getDbAdapter();
+ $enum = [
+ 'host' => $form->translate('Hosts'),
+ 'service' => $form->translate('Services'),
+ ];
+
+ $form->addElement('select', 'template_object_type', [
+ 'label' => $form->translate('Template (Object) Type'),
+ 'description' => $form->translate(
+ 'Please choose a specific Icinga object type'
+ ),
+ 'class' => 'autosubmit',
+ 'required' => true,
+ 'multiOptions' => $form->optionalEnum($enum),
+ 'sorted' => true,
+ ]);
+
+ // There should be a helper method for this
+ if ($form->hasBeenSent()) {
+ $type = $form->getSentOrObjectValue('template_object_type');
+ } else {
+ $type = $form->getObject()->getSetting('template_object_type');
+ }
+ if (empty($type)) {
+ return $form;
+ }
+
+ if (array_key_exists($type, $enum)) {
+ $form->addElement('select', 'template_name', [
+ 'label' => $form->translate('Template'),
+ 'multiOptions' => $form->optionalEnum(self::fetchTemplateNames($db, $type)),
+ 'required' => true,
+ ]);
+ } else {
+ throw new RuntimeException("$type is not a valid Dictionary object type");
+ }
+
+ return $form;
+ }
+
+ protected static function fetchTemplateNames($db, $type)
+ {
+ $query = $db->select()
+ ->from("icinga_$type", ['a' => 'object_name', 'b' => 'object_name'])
+ ->where('object_type = ?', 'template')
+ ->where('template_choice_id IS NULL')
+ ->order('object_name');
+
+ return $db->fetchPairs($query);
+ }
+}
diff --git a/library/Director/DataType/DataTypeDirectorObject.php b/library/Director/DataType/DataTypeDirectorObject.php
new file mode 100644
index 0000000..7f313e0
--- /dev/null
+++ b/library/Director/DataType/DataTypeDirectorObject.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Icinga\Module\Director\DataType;
+
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class DataTypeDirectorObject extends DataTypeHook
+{
+ public function getFormElement($name, QuickForm $form)
+ {
+ /** @var DirectorObjectForm $form */
+ $db = $form->getDb()->getDbAdapter();
+
+ $type = $this->getSetting('icinga_object_type');
+ $dummy = IcingaObject::createByType($type);
+
+ $display = in_array($type, ['service_set', 'notification'])
+ ? 'object_name'
+ : 'COALESCE(display_name, object_name)';
+ $query = $db->select()->from($dummy->getTableName(), [
+ 'object_name' => 'object_name',
+ 'display_name' => $display
+ ])->order($display);
+
+ if ($type === 'service_set') {
+ $query->where('host_id IS NULL');
+ } elseif ($type === 'notification') {
+ $query->where('object_type = ?', 'apply');
+ } else {
+ $query->where('object_type = ?', 'object');
+ }
+
+ $enum = $db->fetchPairs($query);
+
+ $params = [];
+ if ($this->getSetting('data_type') === 'array') {
+ $elementType = $type === 'notification' ? 'select' : 'extensibleSet';
+ $params['sorted'] = true;
+ $params = ['multiOptions' => $enum];
+ } else {
+ $params = ['multiOptions' => [
+ null => $form->translate('- please choose -'),
+ ] + $enum];
+ $elementType = 'select';
+ }
+
+ return $form->createElement($elementType, $name, $params);
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $enum = [
+ 'host' => $form->translate('Hosts'),
+ 'hostgroup' => $form->translate('Host groups'),
+ 'notification' => $form->translate('Notification Apply Rules'),
+ 'service' => $form->translate('Services'),
+ 'servicegroup' => $form->translate('Service groups'),
+ 'service_set' => $form->translate('Service Set'),
+ 'user' => $form->translate('Users'),
+ 'usergroup' => $form->translate('User groups'),
+ ];
+
+ $form->addElement('select', 'icinga_object_type', [
+ 'label' => $form->translate('Object'),
+ 'description' => $form->translate(
+ 'Please choose a specific Icinga object type'
+ ),
+ 'required' => true,
+ 'multiOptions' => $form->optionalEnum($enum),
+ 'sorted' => true,
+ ]);
+
+ $form->addElement('select', 'data_type', [
+ 'label' => $form->translate('Target data type'),
+ 'multiOptions' => $form->optionalEnum([
+ 'string' => $form->translate('String'),
+ 'array' => $form->translate('Array'),
+ ]),
+ 'required' => true,
+ ]);
+
+ return $form;
+ }
+}
diff --git a/library/Director/DataType/DataTypeNumber.php b/library/Director/DataType/DataTypeNumber.php
new file mode 100644
index 0000000..cd47f88
--- /dev/null
+++ b/library/Director/DataType/DataTypeNumber.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\DataType;
+
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Icinga\Module\Director\Data\ValueFilter\FilterInt;
+
+class DataTypeNumber extends DataTypeHook
+{
+ public function getFormElement($name, QuickForm $form)
+ {
+ $element = $form->createElement('text', $name)
+ ->addValidator('int')
+ ->addFilter(new FilterInt);
+
+ return $element;
+ }
+}
diff --git a/library/Director/DataType/DataTypeSqlQuery.php b/library/Director/DataType/DataTypeSqlQuery.php
new file mode 100644
index 0000000..07e7418
--- /dev/null
+++ b/library/Director/DataType/DataTypeSqlQuery.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Icinga\Module\Director\DataType;
+
+use Exception;
+use Icinga\Data\Db\DbConnection;
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Icinga\Module\Director\Util;
+
+class DataTypeSqlQuery extends DataTypeHook
+{
+ /** @var \Zend_Db_Adapter_Pdo_Abstract */
+ protected $db;
+
+ protected static $cachedResult;
+
+ protected static $cacheTime = 0;
+
+ public function getFormElement($name, QuickForm $form)
+ {
+ try {
+ $data = $this->fetchData();
+ $error = false;
+ } catch (Exception $e) {
+ $data = array();
+ $error = sprintf($form->translate('Unable to fetch data: %s'), $e->getMessage());
+ }
+
+ $params = [];
+ if ($this->getSetting('data_type') === 'array') {
+ $type = 'extensibleSet';
+ $params['sorted'] = true;
+ $params = ['multiOptions' => $data];
+ } else {
+ $params = ['multiOptions' => [
+ null => $form->translate('- please choose -'),
+ ] + $data];
+ $type = 'select';
+ }
+
+ $element = $form->createElement($type, $name, $params);
+
+ if ($error) {
+ $element->addError($error);
+ }
+
+ return $element;
+ }
+
+ protected function fetchData()
+ {
+ // TODO: Hash _:)
+ //if (self::$cachedResult === null || (time() - self::$cacheTime > 3)) {
+ self::$cachedResult = $this->db()->fetchPairs($this->settings['query']);
+ self::$cacheTime = time();
+ // }
+
+ return self::$cachedResult;
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ Util::addDbResourceFormElement($form, 'resource');
+
+ $form->addElement('textarea', 'query', array(
+ 'label' => 'DB Query',
+ 'description' => 'This query should return exactly two columns, value and label',
+ 'required' => true,
+ 'rows' => 10,
+ ));
+
+ $form->addElement('select', 'data_type', [
+ 'label' => $form->translate('Target data type'),
+ 'multiOptions' => $form->optionalEnum([
+ 'string' => $form->translate('String'),
+ 'array' => $form->translate('Array'),
+ ]),
+ 'value' => 'string',
+ 'required' => true,
+ ]);
+
+ return $form;
+ }
+
+ protected function db()
+ {
+ if ($this->db === null) {
+ $this->db = DbConnection::fromResourceName($this->settings['resource'])->getDbAdapter();
+ // TODO: should be handled by resources:
+ $this->db->exec("SET NAMES 'utf8'");
+ }
+
+ return $this->db;
+ }
+}
diff --git a/library/Director/DataType/DataTypeString.php b/library/Director/DataType/DataTypeString.php
new file mode 100644
index 0000000..a2dc196
--- /dev/null
+++ b/library/Director/DataType/DataTypeString.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Icinga\Module\Director\DataType;
+
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class DataTypeString extends DataTypeHook
+{
+ public function getFormElement($name, QuickForm $form)
+ {
+ if ($this->getSetting('visibility', 'visible') === 'visible') {
+ $element = $form->createElement('text', $name);
+ } else {
+ $element = $form->createElement('storedPassword', $name);
+ }
+
+ return $element;
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'visibility', [
+ 'label' => $form->translate('Visibility'),
+ 'multiOptions' => $form->optionalEnum([
+ 'visible' => $form->translate('Visible'),
+ 'hidden' => $form->translate('Hidden'),
+ ]),
+ 'value' => 'visible',
+ 'required' => true,
+ ]);
+
+ return $form;
+ }
+}
diff --git a/library/Director/DataType/DataTypeTime.php b/library/Director/DataType/DataTypeTime.php
new file mode 100644
index 0000000..13b9635
--- /dev/null
+++ b/library/Director/DataType/DataTypeTime.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\DataType;
+
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class DataTypeTime extends DataTypeHook
+{
+ public function getFormElement($name, QuickForm $form)
+ {
+ $element = $form->createElement('text', $name);
+
+ return $element;
+ }
+}
diff --git a/library/Director/Db.php b/library/Director/Db.php
new file mode 100644
index 0000000..2a8d6c0
--- /dev/null
+++ b/library/Director/Db.php
@@ -0,0 +1,755 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+use DateTime;
+use DateTimeZone;
+use Exception;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Director\Data\Db\DbConnection;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\Objects\IcingaObject;
+use RuntimeException;
+use Zend_Db_Select;
+
+class Db extends DbConnection
+{
+ /** @var Settings */
+ protected $settings;
+
+ /** @var string */
+ protected $masterZoneName;
+
+ protected function db()
+ {
+ return $this->getDbAdapter();
+ }
+
+ /**
+ * @param $callable
+ * @return $this
+ * @throws Exception
+ */
+ public function runFailSafeTransaction($callable)
+ {
+ if (! is_callable($callable)) {
+ throw new RuntimeException(__METHOD__ . ' needs a Callable');
+ }
+
+ $db = $this->db();
+ $db->beginTransaction();
+ try {
+ $callable();
+ $db->commit();
+ } catch (Exception $e) {
+ try {
+ $db->rollback();
+ } catch (Exception $e) {
+ // Well... there is nothing we can do here.
+ }
+ throw $e;
+ }
+
+ return $this;
+ }
+
+ public static function fromResourceName($name)
+ {
+ $connection = new static(ResourceFactory::getResourceConfig($name));
+
+ if ($connection->isMysql()) {
+ $connection->setClientTimezoneForMysql();
+ } elseif ($connection->isPgsql()) {
+ $connection->setClientTimezoneForPgsql();
+ }
+
+ return $connection;
+ }
+
+ protected function getTimezoneOffset()
+ {
+ $tz = new DateTimeZone(date_default_timezone_get());
+ $offset = $tz->getOffset(new DateTime());
+ $prefix = $offset >= 0 ? '+' : '-';
+ $offset = abs($offset);
+
+ $hours = (int) floor($offset / 3600);
+ $minutes = (int) floor(($offset % 3600) / 60);
+
+ return sprintf('%s%d:%02d', $prefix, $hours, $minutes);
+ }
+
+ protected function setClientTimezoneForMysql()
+ {
+ $db = $this->getDbAdapter();
+ $db->query($db->quoteInto('SET time_zone = ?', $this->getTimezoneOffset()));
+ }
+
+ protected function setClientTimezoneForPgsql()
+ {
+ $db = $this->getDbAdapter();
+ $db->query($db->quoteInto('SET TIME ZONE INTERVAL ? HOUR TO MINUTE', $this->getTimezoneOffset()));
+ }
+
+ public function countActivitiesSinceLastDeployedConfig(IcingaObject $object = null)
+ {
+ $db = $this->db();
+
+ $query = 'SELECT COUNT(*) FROM director_activity_log WHERE id > COALESCE(('
+ . ' SELECT id FROM director_activity_log WHERE checksum = ('
+ . ' SELECT last_activity_checksum FROM director_generated_config WHERE checksum = ('
+ . ' SELECT config_checksum FROM director_deployment_log ORDER by id desc limit 1'
+ . ' )'
+ . ' )'
+ . '), 0)';
+
+ if ($object !== null) {
+ $query .= $db->quoteInto(' AND object_type = ?', $object->getTableName());
+ $query .= $db->quoteInto(' AND object_name = ?', $object->getObjectName());
+ }
+ return (int) $db->fetchOne($query);
+ }
+
+ // TODO: use running config?!
+ public function getLastDeploymentActivityLogId()
+ {
+ $db = $this->db();
+
+ $query = ' SELECT COALESCE(id, 0) AS id FROM director_activity_log WHERE checksum = ('
+ . ' SELECT last_activity_checksum FROM director_generated_config WHERE checksum = ('
+ . ' SELECT config_checksum FROM director_deployment_log ORDER by id desc limit 1'
+ . ' )'
+ . ')';
+
+ return (int) $db->fetchOne($query);
+ }
+
+ public function settings()
+ {
+ if ($this->settings === null) {
+ $this->settings = new Settings($this);
+ }
+
+ return $this->settings;
+ }
+
+ public function getMasterZoneName()
+ {
+ if ($this->masterZoneName === null) {
+ $this->masterZoneName = $this->detectMasterZoneName();
+ }
+
+ return $this->masterZoneName;
+ }
+
+ protected function detectMasterZoneName()
+ {
+ if ($zone = $this->settings()->master_zone) {
+ return $zone;
+ }
+
+ $db = $this->db();
+ $query = $db->select()
+ ->from('icinga_zone', 'object_name')
+ ->where('parent_id IS NULL')
+ ->where('is_global = ?', 'n');
+
+ $zones = $db->fetchCol($query);
+
+ if (count($zones) === 1) {
+ return $zones[0];
+ }
+
+ return 'master';
+ }
+
+ public function getDefaultGlobalZoneName()
+ {
+ return $this->settings()->default_global_zone;
+ }
+
+ public function hasDeploymentEndpoint()
+ {
+ $db = $this->db();
+ $query = $db->select()->from(
+ array('z' => 'icinga_zone'),
+ array('cnt' => 'COUNT(*)')
+ )->join(
+ array('e' => 'icinga_endpoint'),
+ 'e.zone_id = z.id',
+ array()
+ )->join(
+ array('au' => 'icinga_apiuser'),
+ 'e.apiuser_id = au.id',
+ array()
+ )->where('z.object_name = ?', $this->getMasterZoneName());
+
+ return $db->fetchOne($query) > 0;
+ }
+
+ public function getEndpointNamesInDeploymentZone()
+ {
+ $db = $this->db();
+ $query = $db->select()->from(
+ array('z' => 'icinga_zone'),
+ array('object_name' => 'e.object_name')
+ )->join(
+ array('e' => 'icinga_endpoint'),
+ 'e.zone_id = z.id',
+ array()
+ )->join(
+ array('au' => 'icinga_apiuser'),
+ 'e.apiuser_id = au.id',
+ array()
+ )->where('z.object_name = ?', $this->getMasterZoneName())
+ ->order('e.object_name ASC');
+
+ return $db->fetchCol($query) ?: [];
+ }
+
+ public function getDeploymentEndpointName()
+ {
+ $db = $this->db();
+ $query = $db->select()->from(
+ array('z' => 'icinga_zone'),
+ array('object_name' => 'e.object_name')
+ )->join(
+ array('e' => 'icinga_endpoint'),
+ 'e.zone_id = z.id',
+ array()
+ )->join(
+ array('au' => 'icinga_apiuser'),
+ 'e.apiuser_id = au.id',
+ array()
+ )->where('z.object_name = ?', $this->getMasterZoneName())
+ ->order('e.object_name ASC')
+ ->limit(1);
+
+ $name = $db->fetchOne($query);
+
+ if (! $name) {
+ throw new ConfigurationError(
+ 'Unable to detect your deployment endpoint. I was looking for'
+ . ' the first endpoint configured with an assigned API user'
+ . ' in the "%s" zone.',
+ $this->getMasterZoneName()
+ );
+ }
+
+ return $name;
+ }
+
+ /**
+ * @return IcingaEndpoint
+ */
+ public function getDeploymentEndpoint()
+ {
+ return IcingaEndpoint::load($this->getDeploymentEndpointName(), $this);
+ }
+
+ public function getActivitylogNeighbors($id, $type = null, $name = null)
+ {
+ $db = $this->db();
+
+ $greater = $db->select()->from(
+ array('g' => 'director_activity_log'),
+ array('id' => 'MIN(g.id)')
+ )->where('id > ?', (int) $id);
+
+ $smaller = $db->select()->from(
+ array('l' => 'director_activity_log'),
+ array('id' => 'MAX(l.id)')
+ )->where('id < ?', (int) $id);
+
+ if ($type !== null) {
+ $greater->where('object_type = ?', $type);
+ $smaller->where('object_type = ?', $type);
+ }
+
+ if ($name !== null) {
+ $greater->where('object_name = ?', $name);
+ $smaller->where('object_name = ?', $name);
+ }
+
+ $query = $db->select()->from(
+ array('gt' => $greater),
+ array(
+ 'prev' => 'lt.id',
+ 'next' => 'gt.id'
+ )
+ )->join(
+ array('lt' => $smaller),
+ '1 = 1',
+ array()
+ );
+
+ return $db->fetchRow($query);
+ }
+
+ public function fetchActivityLogEntryById($id)
+ {
+ $sql = 'SELECT id, object_type, object_name, action_name,'
+ . ' old_properties, new_properties, author, change_time,'
+ . ' UNIX_TIMESTAMP(change_time) AS change_time_ts,'
+ . ' %s AS checksum, %s AS parent_checksum'
+ . ' FROM director_activity_log WHERE id = %d';
+
+ $sql = sprintf(
+ $sql,
+ $this->dbHexFunc('checksum'),
+ $this->dbHexFunc('parent_checksum'),
+ $id
+ );
+
+ return $this->db()->fetchRow($sql);
+ }
+
+ public function fetchActivityLogChecksumById($id, $binary = true)
+ {
+ $sql = sprintf(
+ 'SELECT' . ' %s AS checksum FROM director_activity_log WHERE id = %d',
+ $this->dbHexFunc('checksum'),
+ (int) $id
+ );
+
+ $result = $this->db()->fetchOne($sql);
+
+ if ($binary) {
+ return hex2bin($result);
+ } else {
+ return $result;
+ }
+ }
+
+ public function fetchActivityLogIdByChecksum($checksum)
+ {
+ $sql = 'SELECT id FROM director_activity_log WHERE checksum = ?';
+ return $this->db()->fetchOne(
+ $this->db()->quoteInto($sql, $this->quoteBinary($checksum))
+ );
+ }
+
+ public function fetchActivityLogEntry($checksum)
+ {
+ $db = $this->db();
+
+ $sql = 'SELECT id, object_type, object_name, action_name,'
+ . ' old_properties, new_properties, author, change_time,'
+ . ' UNIX_TIMESTAMP(change_time) AS change_time_ts,'
+ . ' %s AS checksum, %s AS parent_checksum'
+ . ' FROM director_activity_log WHERE checksum = ?';
+
+ $sql = sprintf(
+ $sql,
+ $this->dbHexFunc('checksum'),
+ $this->dbHexFunc('parent_checksum')
+ );
+
+ return $db->fetchRow(
+ $db->quoteInto($sql, $this->quoteBinary(hex2bin($checksum)))
+ );
+ }
+
+ public function getLastActivityChecksum()
+ {
+ $select = "SELECT checksum FROM (SELECT * FROM (SELECT 1 AS pos, "
+ . $this->dbHexFunc('checksum')
+ . " AS checksum"
+ . " FROM director_activity_log ORDER BY id DESC LIMIT 1) a"
+ . " UNION SELECT 2 AS pos, '' AS checksum) u ORDER BY pos LIMIT 1";
+
+ return $this->db()->fetchOne($select);
+ }
+
+ public function fetchImportStatistics()
+ {
+ $query = "SELECT 'imported_properties' AS stat_name, COUNT(*) AS stat_value"
+ . " FROM import_run i"
+ . " JOIN imported_rowset_row rs ON i.rowset_checksum = rs.rowset_checksum"
+ . " JOIN imported_row_property rp ON rp.row_checksum = rs.row_checksum"
+ . " UNION ALL"
+ . " SELECT 'imported_rows' AS stat_name, COUNT(*) AS stat_value"
+ . " FROM import_run i"
+ . " JOIN imported_rowset_row rs ON i.rowset_checksum = rs.rowset_checksum"
+ . " UNION ALL"
+ . " SELECT 'unique_rows' AS stat_name, COUNT(*) AS stat_value"
+ . " FROM imported_row"
+ . " UNION ALL"
+ . " SELECT 'unique_properties' AS stat_name, COUNT(*) AS stat_value"
+ . " FROM imported_property"
+ ;
+ return $this->db()->fetchPairs($query);
+ }
+
+ public function getImportrunRowsetChecksum($id)
+ {
+ $db = $this->db();
+ $query = $db->select()
+ ->from(array('r' => 'import_run'), $this->dbHexFunc('r.rowset_checksum'))
+ ->where('r.id = ?', $id);
+
+ return $db->fetchOne($query);
+ }
+
+ protected function fetchTemplateRelations($type)
+ {
+ $db = $this->db();
+ $query = $db->select()->from(
+ array('p' => 'icinga_' . $type),
+ array(
+ 'name' => 'o.object_name',
+ 'parent' => 'p.object_name'
+ )
+ )->join(
+ array('i' => 'icinga_' . $type . '_inheritance'),
+ 'p.id = i.parent_' . $type . '_id',
+ array()
+ )->join(
+ array('o' => 'icinga_' . $type),
+ 'o.id = i.' . $type . '_id',
+ array()
+ )->where("o.object_type = 'template'")
+ ->order('p.object_name')
+ ->order('o.object_name');
+
+ return $db->fetchAll($query);
+ }
+
+ public function fetchTemplateTree($type)
+ {
+ $relations = $this->fetchTemplateRelations($type);
+ $children = array();
+ $objects = array();
+ foreach ($relations as $rel) {
+ foreach (array('name', 'parent') as $col) {
+ if (! array_key_exists($rel->$col, $objects)) {
+ $objects[$rel->$col] = (object) array(
+ 'name' => $rel->$col,
+ 'children' => array()
+ );
+ }
+ }
+ }
+
+ foreach ($relations as $rel) {
+ $objects[$rel->parent]->children[$rel->name] = $objects[$rel->name];
+ $children[$rel->name] = $rel->parent;
+ }
+
+ foreach ($children as $name => $object) {
+ unset($objects[$name]);
+ }
+
+ return $objects;
+ }
+
+ public function getLatestImportedChecksum($source)
+ {
+ $db = $this->db();
+ $lastRun = $db->select()->from(
+ array('r' => 'import_run'),
+ array('last_checksum' => $this->dbHexFunc('r.rowset_checksum'))
+ );
+
+ if (is_int($source) || ctype_digit($source)) {
+ $lastRun->where('source_id = ?', (int) $source);
+ } else {
+ $lastRun->where('source_name = ?', $source);
+ }
+
+ $lastRun->order('start_time DESC')->limit(1);
+ return $db->fetchOne($lastRun);
+ }
+
+ public function getObjectSummary()
+ {
+ $types = array(
+ 'host',
+ 'hostgroup',
+ 'service',
+ 'servicegroup',
+ 'user',
+ 'usergroup',
+ 'command',
+ 'timeperiod',
+ 'scheduled_downtime',
+ 'notification',
+ 'apiuser',
+ 'endpoint',
+ 'zone',
+ 'dependency',
+ );
+
+ $queries = array();
+ $db = $this->db();
+ $cnt = "COALESCE(SUM(CASE WHEN o.object_type = '%s' THEN 1 ELSE 0 END), 0)";
+
+ foreach ($types as $type) {
+ $queries[] = $db->select()->from(
+ array('o' => 'icinga_' . $type),
+ array(
+ 'icinga_type' => "('" . $type . "')",
+ 'cnt_object' => sprintf($cnt, 'object'),
+ 'cnt_template' => sprintf($cnt, 'template'),
+ 'cnt_external' => sprintf($cnt, 'external_object'),
+ 'cnt_total' => 'COUNT(*)',
+ )
+ );
+ }
+
+ $query = $this->db()->select()->union($queries, Zend_Db_Select::SQL_UNION_ALL);
+
+ $result = array();
+
+ foreach ($db->fetchAll($query) as $row) {
+ $result[$row->icinga_type] = $row;
+ }
+
+ return $result;
+ }
+
+ public function enumCommands()
+ {
+ return $this->enumIcingaObjects('command');
+ }
+
+ public function enumCommandTemplates()
+ {
+ return $this->enumIcingaTemplates('command');
+ }
+
+ public function enumTimeperiods()
+ {
+ return $this->enumIcingaObjects('timeperiod');
+ }
+
+ public function enumCheckcommands()
+ {
+ $filters = array(
+ 'methods_execute IN (?)' => array('PluginCheck', 'IcingaCheck'),
+
+ );
+ return $this->enumIcingaObjects('command', $filters);
+ }
+
+ public function enumEventcommands()
+ {
+ $filters = array(
+ 'methods_execute = ?' => 'PluginEvent',
+
+ );
+ return $this->enumIcingaObjects('command', $filters);
+ }
+
+ public function enumNotificationCommands()
+ {
+ $filters = array(
+ 'methods_execute IN (?)' => array('PluginNotification'),
+ );
+ return $this->enumIcingaObjects('command', $filters);
+ }
+
+ public function getZoneName($id)
+ {
+ $objects = $this->enumZones();
+ return $objects[$id];
+ }
+
+ public function getCommandName($id)
+ {
+ $objects = $this->enumCommands();
+ return $objects[$id];
+ }
+
+ public function enumZones()
+ {
+ return $this->enumIcingaObjects('zone');
+ }
+
+ public function enumNonglobalZones()
+ {
+ $filters = array('is_global = ?' => 'n');
+ return $this->enumIcingaObjects('zone', $filters);
+ }
+
+ public function enumZoneTemplates()
+ {
+ return $this->enumIcingaTemplates('zone');
+ }
+
+ public function enumHosts()
+ {
+ return $this->enumIcingaObjects('host');
+ }
+
+ public function enumHostTemplates()
+ {
+ return $this->enumIcingaTemplates('host');
+ }
+
+ public function enumHostgroups()
+ {
+ return $this->enumIcingaObjects('hostgroup');
+ }
+
+ public function enumServices()
+ {
+ return $this->enumIcingaObjects('service');
+ }
+
+ public function enumServiceTemplates()
+ {
+ return $this->enumIcingaTemplates('service');
+ }
+
+ public function enumServicegroups()
+ {
+ return $this->enumIcingaObjects('servicegroup');
+ }
+
+ public function enumUsers()
+ {
+ return $this->enumIcingaObjects('user');
+ }
+
+ public function enumUserTemplates()
+ {
+ return $this->enumIcingaTemplates('user');
+ }
+
+ public function enumUsergroups()
+ {
+ return $this->enumIcingaObjects('usergroup');
+ }
+
+ public function enumApiUsers()
+ {
+ return $this->enumIcingaObjects('apiuser');
+ }
+
+ public function enumSyncRule()
+ {
+ return $this->enum('sync_rule', array('id', 'rule_name'));
+ }
+
+ public function enumImportSource()
+ {
+ return $this->enum('import_source', array('id', 'source_name'));
+ }
+
+ public function enumDatalist()
+ {
+ return $this->enum('director_datalist', array('id', 'list_name'));
+ }
+
+ public function enumDatafields()
+ {
+ return $this->enum('director_datafield', array(
+ 'id',
+ "caption || ' (' || varname || ')'",
+ ));
+ }
+
+ public function enum($table, $columns = null, $filters = array())
+ {
+ if ($columns === null) {
+ $columns = array('id', 'object_name');
+ }
+
+ $select = $this->db()->select()->from($table, $columns)->order($columns[1]);
+ foreach ($filters as $key => $val) {
+ $select->where($key, $val);
+ }
+
+ return $this->db()->fetchPairs($select);
+ }
+
+ public function enumIcingaObjects($type, $filters = array())
+ {
+ $filters = array(
+ 'object_type IN (?)' => array('object', 'external_object')
+ ) + $filters;
+
+ return $this->enum('icinga_' . $type, null, $filters);
+ }
+
+ public function enumIcingaTemplates($type, $filters = array())
+ {
+ $filters = array('object_type = ?' => 'template') + $filters;
+ return $this->enum('icinga_' . $type, null, $filters);
+ }
+
+ public function fetchDistinctHostVars()
+ {
+ $select = $this->db()->select()->distinct()->from(
+ array('hv' => 'icinga_host_var'),
+ array(
+ 'varname' => 'hv.varname',
+ 'format' => 'hv.format',
+ 'caption' => 'df.caption',
+ 'datatype' => 'df.datatype'
+ )
+ )->joinLeft(
+ array('df' => 'director_datafield'),
+ 'df.varname = hv.varname',
+ array()
+ )->order('varname');
+
+ return $this->db()->fetchAll($select);
+ }
+
+ public function fetchDistinctServiceVars()
+ {
+ $select = $this->db()->select()->distinct()->from(
+ array('sv' => 'icinga_service_var'),
+ array(
+ 'varname' => 'sv.varname',
+ 'format' => 'sv.format',
+ 'caption' => 'df.caption',
+ 'datatype' => 'df.datatype'
+ )
+ )->joinLeft(
+ array('df' => 'director_datafield'),
+ 'df.varname = sv.varname',
+ array()
+ )->order('varname');
+
+ return $this->db()->fetchAll($select);
+ }
+
+ public function dbHexFunc($column)
+ {
+ if ($this->isPgsql()) {
+ return sprintf("LOWER(ENCODE(%s, 'hex'))", $column);
+ } else {
+ return sprintf("LOWER(HEX(%s))", $column);
+ }
+ }
+
+ public function enumDeployedConfigs()
+ {
+ $db = $this->db();
+
+ $columns = array(
+ 'checksum' => $this->dbHexFunc('c.checksum'),
+ );
+
+ if ($this->isPgsql()) {
+ $columns['caption'] = 'SUBSTRING(' . $columns['checksum'] . ' FROM 1 FOR 7)';
+ } else {
+ $columns['caption'] = 'SUBSTRING(' . $columns['checksum'] . ', 1, 7)';
+ }
+
+ $query = $db->select()->from(
+ array('l' => 'director_deployment_log'),
+ $columns
+ )->joinLeft(
+ array('c' => 'director_generated_config'),
+ 'c.checksum = l.config_checksum',
+ array()
+ )->order('l.start_time DESC');
+
+ return $db->fetchPairs($query);
+ }
+}
diff --git a/library/Director/Db/AppliedServiceSetLoader.php b/library/Director/Db/AppliedServiceSetLoader.php
new file mode 100644
index 0000000..b1e9408
--- /dev/null
+++ b/library/Director/Db/AppliedServiceSetLoader.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Icinga\Module\Director\Db;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Objects\HostApplyMatches;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+
+class AppliedServiceSetLoader
+{
+ protected $host;
+
+ public function __construct(IcingaHost $host)
+ {
+ $this->host = $host;
+ }
+
+ /**
+ * @return IcingaServiceSet[]
+ */
+ public static function fetchForHost(IcingaHost $host)
+ {
+ $loader = new static($host);
+ return $loader->fetchAppliedServiceSets();
+ }
+
+ /**
+ * @return IcingaServiceSet[]
+ */
+ protected function fetchAppliedServiceSets()
+ {
+ $sets = array();
+ $matcher = HostApplyMatches::prepare($this->host);
+ foreach ($this->fetchAllServiceSets() as $set) {
+ $filter = Filter::fromQueryString($set->get('assign_filter'));
+ if ($matcher->matchesFilter($filter)) {
+ $sets[] = $set;
+ }
+ }
+
+ return $sets;
+ }
+
+ /**
+ * @return IcingaServiceSet[]
+ */
+ protected function fetchAllServiceSets()
+ {
+ $db = $this->host->getDb();
+ $query = $db
+ ->select()
+ ->from('icinga_service_set')
+ ->where('assign_filter IS NOT NULL');
+
+ return IcingaServiceSet::loadAll($this->host->getConnection(), $query);
+ }
+}
diff --git a/library/Director/Db/Branch/Branch.php b/library/Director/Db/Branch/Branch.php
new file mode 100644
index 0000000..cd68ff0
--- /dev/null
+++ b/library/Director/Db/Branch/Branch.php
@@ -0,0 +1,216 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Hook\BranchSupportHook;
+use Icinga\Web\Hook;
+use Icinga\Web\Request;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+use RuntimeException;
+use stdClass;
+
+/**
+ * Knows whether we're in a branch
+ */
+class Branch
+{
+ const PREFIX_SYNC_PREVIEW = '/syncpreview';
+
+ /** @var UuidInterface|null */
+ protected $branchUuid;
+
+ /** @var string */
+ protected $name;
+
+ /** @var string */
+ protected $owner;
+
+ /** @var @var string */
+ protected $description;
+
+ /** @var ?int */
+ protected $tsMergeRequest;
+
+ /** @var int */
+ protected $cntActivities;
+
+ public static function fromDbRow(stdClass $row)
+ {
+ $self = new static;
+ if (is_resource($row->uuid)) {
+ $row->uuid = stream_get_contents($row->uuid);
+ }
+ if (strlen($row->uuid) !== 16) {
+ throw new RuntimeException('Valid UUID expected, got ' . var_export($row->uuid, 1));
+ }
+ $self->branchUuid = Uuid::fromBytes(Db\DbUtil::binaryResult($row->uuid));
+ $self->name = $row->branch_name;
+ $self->owner = $row->owner;
+ $self->description = $row->description;
+ $self->tsMergeRequest = $row->ts_merge_request;
+ if (isset($row->cnt_activities)) {
+ $self->cntActivities = $row->cnt_activities;
+ } else {
+ $self->cntActivities = 0;
+ }
+
+ return $self;
+ }
+
+ /**
+ * @return Branch
+ */
+ public static function detect(BranchStore $store)
+ {
+ try {
+ return static::forRequest(Icinga::app()->getRequest(), $store, Auth::getInstance());
+ } catch (\Exception $e) {
+ return new static();
+ }
+ }
+
+ /**
+ * @param Request $request
+ * @param Db $db
+ * @param Auth $auth
+ * @return Branch
+ */
+ public static function forRequest(Request $request, BranchStore $store, Auth $auth)
+ {
+ if ($hook = static::optionalHook()) {
+ return $hook->getBranchForRequest($request, $store, $auth);
+ }
+
+ return new Branch;
+ }
+
+ /**
+ * @return BranchSupportHook
+ */
+ public static function requireHook()
+ {
+ if ($hook = static::optionalHook()) {
+ return $hook;
+ }
+
+ throw new RuntimeException('BranchSupport Hook requested where not available');
+ }
+
+ /**
+ * @return BranchSupportHook|null
+ */
+ public static function optionalHook()
+ {
+ return Hook::first('director/BranchSupport');
+ }
+
+ /**
+ * @param UuidInterface $uuid
+ * @return Branch
+ */
+ public static function withUuid(UuidInterface $uuid)
+ {
+ $self = new static();
+ $self->branchUuid = $uuid;
+ return $self;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isBranch()
+ {
+ return $this->branchUuid !== null;
+ }
+
+ public function assertBranch()
+ {
+ if ($this->isMain()) {
+ throw new RuntimeException('Branch expected, but working in main branch');
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ public function isMain()
+ {
+ return $this->branchUuid === null;
+ }
+
+ /**
+ * @return bool
+ */
+ public function shouldBeMerged()
+ {
+ return $this->tsMergeRequest !== null;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return $this->cntActivities === 0;
+ }
+
+ /**
+ * @return int
+ */
+ public function getActivityCount()
+ {
+ return $this->cntActivities;
+ }
+
+ /**
+ * @return UuidInterface|null
+ */
+ public function getUuid()
+ {
+ return $this->branchUuid;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @since v1.10.0
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @since v1.10.0
+ * @param ?string $description
+ * @return void
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOwner()
+ {
+ return $this->owner;
+ }
+
+ public function isSyncPreview()
+ {
+ return (bool) preg_match('/^' . preg_quote(self::PREFIX_SYNC_PREVIEW, '/') . '\//', $this->getName());
+ }
+}
diff --git a/library/Director/Db/Branch/BranchActivity.php b/library/Director/Db/Branch/BranchActivity.php
new file mode 100644
index 0000000..3812e75
--- /dev/null
+++ b/library/Director/Db/Branch/BranchActivity.php
@@ -0,0 +1,390 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use Icinga\Authentication\Auth;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Data\Json;
+use Icinga\Module\Director\Data\SerializableValue;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorActivityLog;
+use Icinga\Module\Director\Objects\IcingaObject;
+use InvalidArgumentException;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+use RuntimeException;
+
+class BranchActivity
+{
+ const DB_TABLE = 'director_branch_activity';
+
+ const ACTION_CREATE = DirectorActivityLog::ACTION_CREATE;
+ const ACTION_MODIFY = DirectorActivityLog::ACTION_MODIFY;
+ const ACTION_DELETE = DirectorActivityLog::ACTION_DELETE;
+
+ /** @var int */
+ protected $timestampNs;
+
+ /** @var UuidInterface */
+ protected $objectUuid;
+
+ /** @var UuidInterface */
+ protected $branchUuid;
+
+ /** @var string create, modify, delete */
+ protected $action;
+
+ /** @var string */
+ protected $objectTable;
+
+ /** @var string */
+ protected $author;
+
+ /** @var SerializableValue */
+ protected $modifiedProperties;
+
+ /** @var ?SerializableValue */
+ protected $formerProperties;
+
+ public function __construct(
+ UuidInterface $objectUuid,
+ UuidInterface $branchUuid,
+ $action,
+ $objectType,
+ $author,
+ SerializableValue $modifiedProperties,
+ SerializableValue $formerProperties
+ ) {
+ $this->objectUuid = $objectUuid;
+ $this->branchUuid = $branchUuid;
+ $this->action = $action;
+ $this->objectTable = $objectType;
+ $this->author = $author;
+ $this->modifiedProperties = $modifiedProperties;
+ $this->formerProperties = $formerProperties;
+ }
+
+ public static function deleteObject(DbObject $object, Branch $branch)
+ {
+ return new static(
+ $object->getUniqueId(),
+ $branch->getUuid(),
+ self::ACTION_DELETE,
+ $object->getTableName(),
+ Auth::getInstance()->getUser()->getUsername(),
+ SerializableValue::fromSerialization(null),
+ SerializableValue::fromSerialization(self::getFormerObjectProperties($object))
+ );
+ }
+
+ public static function forDbObject(DbObject $object, Branch $branch)
+ {
+ if (! $object->hasBeenModified()) {
+ throw new InvalidArgumentException('Cannot get modifications for unmodified object');
+ }
+ if (! $branch->isBranch()) {
+ throw new InvalidArgumentException('Branch activity requires an active branch');
+ }
+
+ $author = Auth::getInstance()->getUser()->getUsername();
+ if ($object instanceof IcingaObject && $object->shouldBeRemoved()) {
+ $action = self::ACTION_DELETE;
+ $old = self::getFormerObjectProperties($object);
+ $new = null;
+ } elseif ($object->hasBeenLoadedFromDb()) {
+ $action = self::ACTION_MODIFY;
+ $old = self::getFormerObjectProperties($object);
+ $new = self::getObjectProperties($object);
+ } else {
+ $action = self::ACTION_CREATE;
+ $old = null;
+ $new = self::getObjectProperties($object);
+ }
+
+ if ($new !== null) {
+ $new = PlainObjectPropertyDiff::calculate(
+ $old,
+ $new
+ );
+ }
+
+ return new static(
+ $object->getUniqueId(),
+ $branch->getUuid(),
+ $action,
+ $object->getTableName(),
+ $author,
+ SerializableValue::fromSerialization($new),
+ SerializableValue::fromSerialization($old)
+ );
+ }
+
+ public static function fixFakeTimestamp($timestampNs)
+ {
+ if ($timestampNs < 1600000000 * 1000000) {
+ // fake TS for cloned branch in sync preview
+ return (int) $timestampNs * 1000000;
+ }
+
+ return $timestampNs;
+ }
+
+ public function applyToDbObject(DbObject $object)
+ {
+ if (!$this->isActionModify()) {
+ throw new RuntimeException('Only BranchActivity instances with action=modify can be applied');
+ }
+
+ foreach ($this->getModifiedProperties()->jsonSerialize() as $key => $value) {
+ $object->set($key, $value);
+ }
+
+ return $object;
+ }
+
+ /**
+ * Hint: $connection is required, because setting groups triggered loading them.
+ * Should be investigated, as in theory $hostWithoutConnection->groups = 'group'
+ * is expected to work
+ * @param Db $connection
+ * @return DbObject|string
+ */
+ public function createDbObject(Db $connection)
+ {
+ if (!$this->isActionCreate()) {
+ throw new RuntimeException('Only BranchActivity instances with action=create can create objects');
+ }
+
+ $class = DbObjectTypeRegistry::classByType($this->getObjectTable());
+ $object = $class::create([], $connection);
+ $object->setUniqueId($this->getObjectUuid());
+ foreach ($this->getModifiedProperties()->jsonSerialize() as $key => $value) {
+ $object->set($key, $value);
+ }
+
+ return $object;
+ }
+
+ public function deleteDbObject(DbObject $object)
+ {
+ if (!$this->isActionDelete()) {
+ throw new RuntimeException('Only BranchActivity instances with action=delete can delete objects');
+ }
+
+ return $object->delete();
+ }
+
+ public static function load($ts, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $row = $db->fetchRow(
+ $db->select()->from('director_branch_activity')->where('timestamp_ns = ?', $ts)
+ );
+
+ if ($row) {
+ return static::fromDbRow($row);
+ }
+
+ throw new NotFoundError('Not found');
+ }
+
+ protected static function fixPgResource(&$value)
+ {
+ if (is_resource($value)) {
+ $value = stream_get_contents($value);
+ }
+ }
+
+ public static function fromDbRow($row)
+ {
+ static::fixPgResource($row->object_uuid);
+ static::fixPgResource($row->branch_uuid);
+ $activity = new static(
+ Uuid::fromBytes($row->object_uuid),
+ Uuid::fromBytes($row->branch_uuid),
+ $row->action,
+ $row->object_table,
+ $row->author,
+ SerializableValue::fromSerialization(Json::decodeOptional($row->modified_properties)),
+ SerializableValue::fromSerialization(Json::decodeOptional($row->former_properties))
+ );
+ $activity->timestampNs = $row->timestamp_ns;
+
+ return $activity;
+ }
+
+ /**
+ * Must be run in a transaction! Repeatable read?
+ * @param Db $connection
+ * @throws \Icinga\Module\Director\Exception\JsonEncodeException
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function store(Db $connection)
+ {
+ if ($this->timestampNs !== null) {
+ throw new InvalidArgumentException(sprintf(
+ 'Cannot store activity with a given timestamp: %s',
+ $this->timestampNs
+ ));
+ }
+ $db = $connection->getDbAdapter();
+ $last = $db->fetchRow(
+ $db->select()->from('director_branch_activity', ['timestamp_ns' => 'MAX(timestamp_ns)'])
+ );
+ if (PHP_INT_SIZE !== 8) {
+ throw new RuntimeException('PHP with 64bit integer support is required');
+ }
+ $timestampNs = (int) floor(microtime(true) * 1000000);
+ if ($last) {
+ if ($last->timestamp_ns >= $timestampNs) {
+ $timestampNs = $last + 1;
+ }
+ }
+ $old = Json::encode($this->formerProperties);
+ $new = Json::encode($this->modifiedProperties);
+
+ $db->insert(self::DB_TABLE, [
+ 'timestamp_ns' => $timestampNs,
+ 'object_uuid' => $connection->quoteBinary($this->objectUuid->getBytes()),
+ 'branch_uuid' => $connection->quoteBinary($this->branchUuid->getBytes()),
+ 'action' => $this->action,
+ 'object_table' => $this->objectTable,
+ 'author' => $this->author,
+ 'former_properties' => $old,
+ 'modified_properties' => $new,
+ ]);
+ }
+
+ /**
+ * @return int
+ */
+ public function getTimestampNs()
+ {
+ return $this->timestampNs;
+ }
+
+ /**
+ * @return int
+ */
+ public function getTimestamp()
+ {
+ return (int) floor(BranchActivity::fixFakeTimestamp($this->timestampNs) / 1000000);
+ }
+
+ /**
+ * @return UuidInterface
+ */
+ public function getObjectUuid()
+ {
+ return $this->objectUuid;
+ }
+
+ /**
+ * @return UuidInterface
+ */
+ public function getBranchUuid()
+ {
+ return $this->branchUuid;
+ }
+
+ /**
+ * @return string
+ */
+ public function getObjectName()
+ {
+ return $this->getProperty('object_name', 'unknown object name');
+ }
+
+ /**
+ * @return string
+ */
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ public function isActionDelete()
+ {
+ return $this->action === self::ACTION_DELETE;
+ }
+
+ public function isActionCreate()
+ {
+ return $this->action === self::ACTION_CREATE;
+ }
+
+ public function isActionModify()
+ {
+ return $this->action === self::ACTION_MODIFY;
+ }
+
+ /**
+ * @return string
+ */
+ public function getObjectTable()
+ {
+ return $this->objectTable;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAuthor()
+ {
+ return $this->author;
+ }
+
+ /**
+ * @return ?SerializableValue
+ */
+ public function getModifiedProperties()
+ {
+ return $this->modifiedProperties;
+ }
+
+ /**
+ * @return ?SerializableValue
+ */
+ public function getFormerProperties()
+ {
+ return $this->formerProperties;
+ }
+
+ public function getProperty($key, $default = null)
+ {
+ if ($this->modifiedProperties) {
+ $properties = $this->modifiedProperties->jsonSerialize();
+ if (isset($properties->$key)) {
+ return $properties->$key;
+ }
+ }
+ if ($this->formerProperties) {
+ $properties = $this->formerProperties->jsonSerialize();
+ if (isset($properties->$key)) {
+ return $properties->$key;
+ }
+ }
+
+ return $default;
+ }
+
+ protected static function getFormerObjectProperties(DbObject $object)
+ {
+ if (! $object instanceof IcingaObject) {
+ throw new RuntimeException('Plain object helpers for DbObject must be implemented');
+ }
+
+ return (array) $object->getPlainUnmodifiedObject();
+ }
+
+ protected static function getObjectProperties(DbObject $object)
+ {
+ if (! $object instanceof IcingaObject) {
+ throw new RuntimeException('Plain object helpers for DbObject must be implemented');
+ }
+
+ return (array) $object->toPlainObject(false, true);
+ }
+}
diff --git a/library/Director/Db/Branch/BranchMerger.php b/library/Director/Db/Branch/BranchMerger.php
new file mode 100644
index 0000000..2e84863
--- /dev/null
+++ b/library/Director/Db/Branch/BranchMerger.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorActivityLog;
+use Ramsey\Uuid\UuidInterface;
+
+class BranchMerger
+{
+ /** @var Branch */
+ protected $branchUuid;
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var array */
+ protected $ignoreActivities = [];
+
+ /** @var bool */
+ protected $ignoreDeleteWhenMissing = false;
+
+ /** @var bool */
+ protected $ignoreModificationWhenMissing = false;
+
+ /**
+ * Apply branch modifications
+ *
+ * TODO: allow to skip or ignore modifications, in case modified properties have
+ * been changed in the meantime
+ *
+ * @param UuidInterface $branchUuid
+ * @param Db $connection
+ */
+ public function __construct(UuidInterface $branchUuid, Db $connection)
+ {
+ $this->branchUuid = $branchUuid;
+ $this->db = $connection->getDbAdapter();
+ $this->connection = $connection;
+ }
+
+ /**
+ * Skip a delete operation, when the object to be deleted does not exist
+ *
+ * @param bool $ignore
+ */
+ public function ignoreDeleteWhenMissing($ignore = true)
+ {
+ $this->ignoreDeleteWhenMissing = $ignore;
+ }
+
+ /**
+ * Skip a modification, when the related object does not exist
+ * @param bool $ignore
+ */
+ public function ignoreModificationWhenMissing($ignore = true)
+ {
+ $this->ignoreModificationWhenMissing = $ignore;
+ }
+
+ /**
+ * @param int $key
+ */
+ public function ignoreActivity($key)
+ {
+ $this->ignoreActivities[$key] = true;
+ }
+
+ /**
+ * @param BranchActivity $activity
+ * @return bool
+ */
+ public function ignoresActivity(BranchActivity $activity)
+ {
+ return isset($this->ignoreActivities[$activity->getTimestampNs()]);
+ }
+
+ /**
+ * @throws MergeError
+ */
+ public function merge($comment = null)
+ {
+ $username = DirectorActivityLog::username();
+ $this->connection->runFailSafeTransaction(function () use ($comment, $username) {
+ $formerActivityId = (int) DirectorActivityLog::loadLatest($this->connection)->get('id');
+ $query = $this->db->select()
+ ->from(BranchActivity::DB_TABLE)
+ ->where('branch_uuid = ?', $this->connection->quoteBinary($this->branchUuid->getBytes()))
+ ->order('timestamp_ns ASC');
+ $rows = $this->db->fetchAll($query);
+ foreach ($rows as $row) {
+ $activity = BranchActivity::fromDbRow($row);
+ $author = $activity->getAuthor();
+ if ($username !== $author) {
+ DirectorActivityLog::overrideUsername("$author/$username");
+ }
+ $this->applyModification($activity);
+ }
+ (new BranchStore($this->connection))->deleteByUuid($this->branchUuid);
+ $currentActivityId = (int) DirectorActivityLog::loadLatest($this->connection)->get('id');
+ $firstActivityId = (int) $this->db->fetchOne(
+ $this->db->select()->from('director_activity_log', 'MIN(id)')->where('id > ?', $formerActivityId)
+ );
+ if ($comment && strlen($comment)) {
+ $this->db->insert('director_activity_log_remark', [
+ 'first_related_activity' => $firstActivityId,
+ 'last_related_activity' => $currentActivityId,
+ 'remark' => $comment,
+ ]);
+ }
+ });
+ DirectorActivityLog::restoreUsername();
+ }
+
+ /**
+ * @param BranchActivity $activity
+ * @throws MergeError
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function applyModification(BranchActivity $activity)
+ {
+ /** @var string|DbObject $class */
+ $class = DbObjectTypeRegistry::classByType($activity->getObjectTable());
+ $uuid = $activity->getObjectUuid();
+
+ $exists = $class::uniqueIdExists($uuid, $this->connection);
+ if ($activity->isActionCreate()) {
+ if ($exists) {
+ if (! $this->ignoresActivity($activity)) {
+ throw new MergeErrorRecreateOnMerge($activity);
+ }
+ } else {
+ $activity->createDbObject($this->connection)->store($this->connection);
+ }
+ } elseif ($activity->isActionDelete()) {
+ if ($exists) {
+ $activity->deleteDbObject($class::requireWithUniqueId($uuid, $this->connection));
+ } elseif (! $this->ignoreDeleteWhenMissing && ! $this->ignoresActivity($activity)) {
+ throw new MergeErrorDeleteMissingObject($activity);
+ }
+ } else {
+ if ($exists) {
+ $activity->applyToDbObject($class::requireWithUniqueId($uuid, $this->connection))->store();
+ // TODO: you modified an object, and related properties have been changed in the meantime.
+ // We're able to detect this with the given data, and might want to offer a rebase.
+ } elseif (! $this->ignoreModificationWhenMissing && ! $this->ignoresActivity($activity)) {
+ throw new MergeErrorModificationForMissingObject($activity);
+ }
+ }
+ }
+}
diff --git a/library/Director/Db/Branch/BranchModificationInspection.php b/library/Director/Db/Branch/BranchModificationInspection.php
new file mode 100644
index 0000000..978ca5d
--- /dev/null
+++ b/library/Director/Db/Branch/BranchModificationInspection.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use gipfl\Translation\StaticTranslator;
+use gipfl\Translation\TranslationHelper;
+use Icinga\Module\Director\Db;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use Ramsey\Uuid\UuidInterface;
+
+class BranchModificationInspection
+{
+ use TranslationHelper;
+
+ protected $connection;
+
+ protected $db;
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ }
+
+ public function describe($table, UuidInterface $uuid)
+ {
+ return static::describeModificationStatistics($this->loadSingleTableStats($table, $uuid));
+ }
+
+ public function describeBranch(UuidInterface $uuid)
+ {
+ $tables = [
+ $this->translate('API Users') => BranchSupport::BRANCHED_TABLE_ICINGA_APIUSER,
+ $this->translate('Endpoints') => BranchSupport::BRANCHED_TABLE_ICINGA_COMMAND,
+ $this->translate('Zones') => BranchSupport::BRANCHED_TABLE_ICINGA_DEPENDENCY,
+ $this->translate('Commands') => BranchSupport::BRANCHED_TABLE_ICINGA_ENDPOINT,
+ $this->translate('Hosts') => BranchSupport::BRANCHED_TABLE_ICINGA_HOST,
+ $this->translate('Hostgroups') => BranchSupport::BRANCHED_TABLE_ICINGA_HOSTGROUP,
+ $this->translate('Services') => BranchSupport::BRANCHED_TABLE_ICINGA_NOTIFICATION,
+ $this->translate('Servicegroups') => BranchSupport::BRANCHED_TABLE_ICINGA_SCHEDULED_DOWNTIME,
+ $this->translate('Servicesets') => BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE_SET,
+ $this->translate('Users') => BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE,
+ $this->translate('Usergroups') => BranchSupport::BRANCHED_TABLE_ICINGA_SERVICEGROUP,
+ $this->translate('Timeperiods') => BranchSupport::BRANCHED_TABLE_ICINGA_TIMEPERIOD,
+ $this->translate('Notifications') => BranchSupport::BRANCHED_TABLE_ICINGA_USER,
+ $this->translate('Dependencies') => BranchSupport::BRANCHED_TABLE_ICINGA_USERGROUP,
+ $this->translate('Scheduled Downtimes') => BranchSupport::BRANCHED_TABLE_ICINGA_ZONE,
+ ];
+
+ $parts = new HtmlDocument();
+ $parts->setSeparator(Html::tag('br'));
+ foreach ($tables as $label => $table) {
+ $info = $this->describe($table, $uuid);
+ if (! empty($info) && $info !== '-') {
+ $parts->add("$label: $info");
+ }
+ }
+
+ return $parts;
+ }
+
+ public static function describeModificationStatistics($stats)
+ {
+ $t = StaticTranslator::get();
+ $relevantStats = [];
+ if ($stats->cnt_created > 0) {
+ $relevantStats[] = sprintf($t->translate('%d created'), $stats->cnt_created);
+ }
+ if ($stats->cnt_deleted > 0) {
+ $relevantStats[] = sprintf($t->translate('%d deleted'), $stats->cnt_deleted);
+ }
+ if ($stats->cnt_modified > 0) {
+ $relevantStats[] = sprintf($t->translate('%d modified'), $stats->cnt_modified);
+ }
+ if (empty($relevantStats)) {
+ return '-';
+ }
+
+ return implode(', ', $relevantStats);
+ }
+
+ public function loadSingleTableStats($table, UuidInterface $uuid)
+ {
+ $query = $this->db->select()->from($table, [
+ 'cnt_created' => "SUM(CASE WHEN branch_created = 'y' THEN 1 ELSE 0 END)",
+ 'cnt_deleted' => "SUM(CASE WHEN branch_deleted = 'y' THEN 1 ELSE 0 END)",
+ 'cnt_modified' => "SUM(CASE WHEN branch_deleted = 'n' AND branch_created = 'n' THEN 1 ELSE 0 END)",
+ ])->where('branch_uuid = ?', $this->connection->quoteBinary($uuid->getBytes()));
+
+ return $this->db->fetchRow($query);
+ }
+}
diff --git a/library/Director/Db/Branch/BranchSettings.php b/library/Director/Db/Branch/BranchSettings.php
new file mode 100644
index 0000000..b3fd164
--- /dev/null
+++ b/library/Director/Db/Branch/BranchSettings.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use Icinga\Module\Director\Data\Json;
+use function in_array;
+
+/**
+ * Hardcoded branch-related settings
+ */
+class BranchSettings
+{
+ // TODO: Ranges is weird. key = scheduled_downtime_id, range_type, range_key
+ const ENCODED_ARRAYS = ['imports', 'groups', 'ranges', 'users', 'usergroups'];
+
+ const ENCODED_DICTIONARIES = ['vars', 'arguments'];
+
+ const BRANCH_SPECIFIC_PROPERTIES = [
+ 'uuid',
+ 'branch_uuid',
+ 'branch_created',
+ 'branch_deleted',
+ 'set_null',
+ ];
+
+ const BRANCH_BOOLEANS = [
+ 'branch_created',
+ 'branch_deleted',
+ ];
+
+ const RELATED_SETS = [
+ 'types',
+ 'states',
+ ];
+
+ public static function propertyIsEncodedArray($property)
+ {
+ return in_array($property, self::ENCODED_ARRAYS, true);
+ }
+
+ public static function propertyIsRelatedSet($property)
+ {
+ // TODO: get from object class
+ return in_array($property, self::RELATED_SETS, true);
+ }
+
+ public static function propertyIsEncodedDictionary($property)
+ {
+ return in_array($property, self::ENCODED_DICTIONARIES, true);
+ }
+
+ public static function propertyIsBranchSpecific($property)
+ {
+ return in_array($property, self::BRANCH_SPECIFIC_PROPERTIES, true);
+ }
+
+ public static function flattenEncodedDicationaries(array &$properties)
+ {
+ foreach (self::ENCODED_DICTIONARIES as $property) {
+ self::flattenProperty($properties, $property);
+ }
+ }
+
+ public static function normalizeBranchedObjectFromDb($row)
+ {
+ $normalized = [];
+ $row = (array) $row;
+ foreach ($row as $key => $value) {
+ if (! static::propertyIsBranchSpecific($key)) {
+ if (is_resource($value)) {
+ $value = stream_get_contents($value);
+ }
+ if ($value !== null && static::propertyIsEncodedArray($key)) {
+ $value = Json::decode($value);
+ }
+ if ($value !== null && static::propertyIsRelatedSet($key)) {
+ // TODO: We might want to combine them (current VS branched)
+ $value = Json::decode($value);
+ }
+ if ($value !== null && static::propertyIsEncodedDictionary($key)) {
+ $value = Json::decode($value);
+ }
+ if ($value !== null) {
+ $normalized[$key] = $value;
+ }
+ }
+ }
+ static::flattenEncodedDicationaries($row);
+ if (isset($row['set_null'])) {
+ foreach (Json::decode($row['set_null']) as $property) {
+ $normalized[$property] = null;
+ }
+ }
+ foreach (self::BRANCH_BOOLEANS as $key) {
+ if ($row[$key] === 'y') {
+ $row[$key] = true;
+ } elseif ($row[$key] === 'n') {
+ $row[$key] = false;
+ } else {
+ throw new \RuntimeException(sprintf(
+ "Boolean DB property expected, got '%s' for '%s'",
+ $row[$key],
+ $key
+ ));
+ }
+ }
+
+ return $normalized;
+ }
+
+ public static function flattenProperty(array &$properties, $property)
+ {
+ // TODO: dots in varnames -> throw or escape?
+ if (isset($properties[$property])) {
+ foreach ((array) $properties[$property] as $key => $value) {
+ $properties["$property.$key"] = $value;
+ }
+ unset($properties[$property]);
+ }
+ }
+}
diff --git a/library/Director/Db/Branch/BranchStore.php b/library/Director/Db/Branch/BranchStore.php
new file mode 100644
index 0000000..196d079
--- /dev/null
+++ b/library/Director/Db/Branch/BranchStore.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\DbUtil;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+
+class BranchStore
+{
+ const TABLE = 'director_branch';
+ const TABLE_ACTIVITY = 'director_branch_activity';
+
+ protected $connection;
+
+ protected $db;
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ }
+
+ /**
+ * @param UuidInterface $uuid
+ * @return ?Branch
+ */
+ public function fetchBranchByUuid(UuidInterface $uuid)
+ {
+ return $this->newFromDbResult(
+ $this->select()->where('b.uuid = ?', $this->connection->quoteBinary($uuid->getBytes()))
+ );
+ }
+
+ /**
+ * @param string $name
+ * @return ?Branch
+ */
+ public function fetchBranchByName($name)
+ {
+ return $this->newFromDbResult($this->select()->where('b.branch_name = ?', $name));
+ }
+
+ public function cloneBranchForSync(Branch $branch, $newName, $owner)
+ {
+ $this->runTransaction(function ($db) use ($branch, $newName, $owner) {
+ $tables = BranchSupport::BRANCHED_TABLES;
+ $tables[] = self::TABLE_ACTIVITY;
+ $newBranch = $this->createBranchByName($newName, $owner);
+ $oldQuotedUuid = DbUtil::quoteBinaryCompat($branch->getUuid()->getBytes(), $db);
+ $quotedUuid = DbUtil::quoteBinaryCompat($newBranch->getUuid()->getBytes(), $db);
+ // $timestampNs = (int)floor(microtime(true) * 1000000);
+ // Hint: would love to do SELECT *, $quotedUuid AS branch_uuid FROM $table INTO $table
+ foreach ($tables as $table) {
+ $rows = $db->fetchAll($db->select()->from($table)->where('branch_uuid = ?', $oldQuotedUuid));
+ foreach ($rows as $row) {
+ $modified = (array)$row;
+ $modified['branch_uuid'] = $quotedUuid;
+ if ($table === self::TABLE_ACTIVITY) {
+ $modified['timestamp_ns'] = round($modified['timestamp_ns'] / 1000000);
+ }
+ $db->insert($table, $modified);
+ }
+ }
+ });
+
+ return $this->fetchBranchByName($newName);
+ }
+
+ protected function runTransaction($callback)
+ {
+ $db = $this->db;
+ $db->beginTransaction();
+ try {
+ $callback($db);
+ $db->commit();
+ } catch (\Exception $e) {
+ try {
+ $db->rollBack();
+ } catch (\Exception $ignored) {
+ //
+ }
+ throw $e;
+ }
+ }
+
+ public function wipeBranch(Branch $branch, $after = null)
+ {
+ $this->runTransaction(function ($db) use ($branch, $after) {
+ $tables = BranchSupport::BRANCHED_TABLES;
+ $tables[] = self::TABLE_ACTIVITY;
+ $quotedUuid = DbUtil::quoteBinaryCompat($branch->getUuid()->getBytes(), $db);
+ $where = $db->quoteInto('branch_uuid = ?', $quotedUuid);
+ foreach ($tables as $table) {
+ if ($after && $table === self::TABLE_ACTIVITY) {
+ $db->delete($table, $where . ' AND timestamp_ns > ' . (int) $after);
+ } else {
+ $db->delete($table, $where);
+ }
+ }
+ });
+
+ }
+
+ protected function newFromDbResult($query)
+ {
+ if ($row = $this->db->fetchRow($query)) {
+ if (is_resource($row->uuid)) {
+ $row->uuid = stream_get_contents($row->uuid);
+ }
+ return Branch::fromDbRow($row);
+ }
+
+ return null;
+ }
+
+ public function setReadyForMerge(Branch $branch)
+ {
+ $update = [
+ 'ts_merge_request' => (int) floor(microtime(true) * 1000000),
+ 'description' => $branch->getDescription(),
+ ];
+
+ $name = $branch->getName();
+ if (preg_match('#^/enforced/(.+)$#', $name, $match)) {
+ $update['branch_name'] = '/merge/' . substr(sha1($branch->getUuid()->getBytes()), 0, 7) . '/' . $match[1];
+ }
+ $this->db->update('director_branch', $update, $this->db->quoteInto(
+ 'uuid = ?',
+ $this->connection->quoteBinary($branch->getUuid()->getBytes())
+ ));
+ }
+
+ protected function select()
+ {
+ return $this->db->select()->from(['b' => 'director_branch'], [
+ 'uuid' => 'b.uuid',
+ 'owner' => 'b.owner',
+ 'branch_name' => 'b.branch_name',
+ 'description' => 'b.description',
+ 'ts_merge_request' => 'b.ts_merge_request',
+ 'cnt_activities' => 'COUNT(ba.timestamp_ns)',
+ ])->joinLeft(
+ ['ba' => self::TABLE_ACTIVITY],
+ 'b.uuid = ba.branch_uuid',
+ []
+ )->group('b.uuid');
+ }
+
+ /**
+ * @param string $name
+ * @return Branch
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function fetchOrCreateByName($name, $owner)
+ {
+ if ($branch = $this->fetchBranchByName($name)) {
+ return $branch;
+ }
+
+ return $this->createBranchByName($name, $owner);
+ }
+
+ /**
+ * @param string $branchName
+ * @param string $owner
+ * @return Branch
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function createBranchByName($branchName, $owner)
+ {
+ $uuid = Uuid::uuid4();
+ $properties = [
+ 'uuid' => $this->connection->quoteBinary($uuid->getBytes()),
+ 'branch_name' => $branchName,
+ 'owner' => $owner,
+ 'description' => null,
+ 'ts_merge_request' => null,
+ ];
+ $this->db->insert(self::TABLE, $properties);
+
+ if ($branch = static::fetchBranchByUuid($uuid)) {
+ return $branch;
+ }
+
+ throw new \RuntimeException(sprintf(
+ 'Branch with UUID=%s has been created, but could not be fetched from DB',
+ $uuid->toString()
+ ));
+ }
+
+ public function deleteByUuid(UuidInterface $uuid)
+ {
+ return $this->db->delete(self::TABLE, $this->db->quoteInto(
+ 'uuid = ?',
+ $this->connection->quoteBinary($uuid->getBytes())
+ ));
+ }
+
+ /**
+ * @param string $name
+ * @return int
+ */
+ public function deleteByName($name)
+ {
+ return $this->db->delete(self::TABLE, $this->db->quoteInto(
+ 'branch_name = ?',
+ $name
+ ));
+ }
+
+ public function delete(Branch $branch)
+ {
+ return $this->deleteByUuid($branch->getUuid());
+ }
+
+ /**
+ * @param Branch $branch
+ * @param ?int $after
+ * @return float|null
+ */
+ public function getLastActivityTime(Branch $branch, $after = null)
+ {
+ $db = $this->db;
+ $query = $db->select()
+ ->from(self::TABLE_ACTIVITY, 'MAX(timestamp_ns)')
+ ->where('branch_uuid = ?', DbUtil::quoteBinaryCompat($branch->getUuid()->getBytes(), $db));
+ if ($after) {
+ $query->where('timestamp_ns > ?', (int) $after);
+ }
+
+ $last = $db->fetchOne($query);
+ if ($last) {
+ return $last / 1000000;
+ }
+
+ return null;
+ }
+}
diff --git a/library/Director/Db/Branch/BranchSupport.php b/library/Director/Db/Branch/BranchSupport.php
new file mode 100644
index 0000000..74be021
--- /dev/null
+++ b/library/Director/Db/Branch/BranchSupport.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class BranchSupport
+{
+ const BRANCHED_TABLE_PREFIX = 'branched_';
+
+ const TABLE_ICINGA_APIUSER = 'icinga_apiuser';
+ const TABLE_ICINGA_COMMAND = 'icinga_command';
+ const TABLE_ICINGA_DEPENDENCY = 'icinga_dependency';
+ const TABLE_ICINGA_ENDPOINT = 'icinga_endpoint';
+ const TABLE_ICINGA_HOST = 'icinga_host';
+ const TABLE_ICINGA_HOSTGROUP = 'icinga_hostgroup';
+ const TABLE_ICINGA_NOTIFICATION = 'icinga_notification';
+ const TABLE_ICINGA_SCHEDULED_DOWNTIME = 'icinga_scheduled_downtime';
+ const TABLE_ICINGA_SERVICE = 'icinga_service';
+ const TABLE_ICINGA_SERVICEGROUP = 'icinga_servicegroup';
+ const TABLE_ICINGA_SERVICE_SET = 'icinga_service_set';
+ const TABLE_ICINGA_TIMEPERIOD = 'icinga_timeperiod';
+ const TABLE_ICINGA_USER = 'icinga_user';
+ const TABLE_ICINGA_USERGROUP = 'icinga_usergroup';
+ const TABLE_ICINGA_ZONE = 'icinga_zone';
+
+ const BRANCHED_TABLE_ICINGA_APIUSER = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_APIUSER;
+ const BRANCHED_TABLE_ICINGA_COMMAND = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_COMMAND;
+ const BRANCHED_TABLE_ICINGA_DEPENDENCY = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_DEPENDENCY;
+ const BRANCHED_TABLE_ICINGA_ENDPOINT = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_ENDPOINT;
+ const BRANCHED_TABLE_ICINGA_HOST = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_HOST;
+ const BRANCHED_TABLE_ICINGA_HOSTGROUP = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_HOSTGROUP;
+ const BRANCHED_TABLE_ICINGA_NOTIFICATION = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_NOTIFICATION;
+ const BRANCHED_TABLE_ICINGA_SCHEDULED_DOWNTIME = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SCHEDULED_DOWNTIME;
+ const BRANCHED_TABLE_ICINGA_SERVICE = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SERVICE;
+ const BRANCHED_TABLE_ICINGA_SERVICEGROUP = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SERVICEGROUP;
+ const BRANCHED_TABLE_ICINGA_SERVICE_SET = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SERVICE_SET;
+ const BRANCHED_TABLE_ICINGA_TIMEPERIOD = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_TIMEPERIOD;
+ const BRANCHED_TABLE_ICINGA_USER = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_USER;
+ const BRANCHED_TABLE_ICINGA_USERGROUP = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_USERGROUP;
+ const BRANCHED_TABLE_ICINGA_ZONE = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_ZONE;
+
+ const OBJECT_TABLES = [
+ self::TABLE_ICINGA_APIUSER,
+ self::TABLE_ICINGA_COMMAND,
+ self::TABLE_ICINGA_DEPENDENCY,
+ self::TABLE_ICINGA_ENDPOINT,
+ self::TABLE_ICINGA_HOST,
+ self::TABLE_ICINGA_HOSTGROUP,
+ self::TABLE_ICINGA_NOTIFICATION,
+ self::TABLE_ICINGA_SCHEDULED_DOWNTIME,
+ self::TABLE_ICINGA_SERVICE,
+ self::TABLE_ICINGA_SERVICEGROUP,
+ self::TABLE_ICINGA_SERVICE_SET,
+ self::TABLE_ICINGA_TIMEPERIOD,
+ self::TABLE_ICINGA_USER,
+ self::TABLE_ICINGA_USERGROUP,
+ self::TABLE_ICINGA_ZONE,
+ ];
+
+ const BRANCHED_TABLES = [
+ self::BRANCHED_TABLE_ICINGA_APIUSER,
+ self::BRANCHED_TABLE_ICINGA_COMMAND,
+ self::BRANCHED_TABLE_ICINGA_DEPENDENCY,
+ self::BRANCHED_TABLE_ICINGA_ENDPOINT,
+ self::BRANCHED_TABLE_ICINGA_HOST,
+ self::BRANCHED_TABLE_ICINGA_HOSTGROUP,
+ self::BRANCHED_TABLE_ICINGA_NOTIFICATION,
+ self::BRANCHED_TABLE_ICINGA_SCHEDULED_DOWNTIME,
+ self::BRANCHED_TABLE_ICINGA_SERVICE,
+ self::BRANCHED_TABLE_ICINGA_SERVICEGROUP,
+ self::BRANCHED_TABLE_ICINGA_SERVICE_SET,
+ self::BRANCHED_TABLE_ICINGA_TIMEPERIOD,
+ self::BRANCHED_TABLE_ICINGA_USER,
+ self::BRANCHED_TABLE_ICINGA_USERGROUP,
+ self::BRANCHED_TABLE_ICINGA_ZONE,
+ ];
+
+ public static function existsForTableName($table)
+ {
+ return in_array($table, self::OBJECT_TABLES, true);
+ }
+
+ public static function existsForSyncRule(SyncRule $rule)
+ {
+ return static::existsForTableName(
+ DbObjectTypeRegistry::tableNameByType($rule->get('object_type'))
+ );
+ }
+}
diff --git a/library/Director/Db/Branch/BranchedObject.php b/library/Director/Db/Branch/BranchedObject.php
new file mode 100644
index 0000000..0f276c2
--- /dev/null
+++ b/library/Director/Db/Branch/BranchedObject.php
@@ -0,0 +1,404 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Data\Json;
+use Icinga\Module\Director\Db;
+use Ramsey\Uuid\UuidInterface;
+use stdClass;
+
+class BranchedObject
+{
+ /** @var UuidInterface */
+ protected $branchUuid;
+
+ /** @var ?DbObject */
+ protected $object;
+
+ /** @var ?stdClass */
+ protected $changes;
+
+ /** @var bool */
+ protected $branchDeleted;
+
+ /** @var bool */
+ protected $branchCreated;
+
+ /** @var UuidInterface */
+ private $objectUuid;
+
+ /** @var string */
+ private $objectTable;
+
+ /** @var bool */
+ private $loadedAsBranchedObject = false;
+
+ /**
+ * @param BranchActivity $activity
+ * @param Db $connection
+ * @return static
+ */
+ public static function withActivity(BranchActivity $activity, Db $connection)
+ {
+ return self::loadOptional(
+ $connection,
+ $activity->getObjectTable(),
+ $activity->getObjectUuid(),
+ $activity->getBranchUuid()
+ )->applyActivity($activity, $connection);
+ }
+
+ public function store(Db $connection)
+ {
+ if ($this->object && ! $this->object->hasBeenModified() && empty($this->changes)) {
+ return false;
+ }
+ $db = $connection->getDbAdapter();
+
+ $properties = [
+ 'branch_deleted' => $this->branchDeleted ? 'y' : 'n',
+ 'branch_created' => $this->branchCreated ? 'y' : 'n',
+ ] + $this->prepareChangedProperties();
+
+ $table = 'branched_' . $this->objectTable;
+ if ($this->loadedAsBranchedObject) {
+ return $db->update(
+ $table,
+ $properties,
+ $this->prepareWhereString($connection)
+ ) === 1;
+ } else {
+ try {
+ return $db->insert($table, $this->prepareKeyProperties($connection) + $properties) === 1;
+ } catch (\Exception $e) {
+ var_dump($e->getMessage());
+ var_dump($this->prepareKeyProperties($connection) + $properties);
+ exit;
+ }
+ }
+ }
+
+ public function delete(Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $table = 'branched_' . $this->objectTable;
+ $branchCreated = $db->fetchOne($this->filterQuery($db->select()->from($table, 'branch_created'), $connection));
+ // We do not want to nullify all properties, therefore: delete & insert
+ $db->delete($table, $this->prepareWhereString($connection));
+
+ if (! $branchCreated) {
+ // No need to insert a deleted object in case this object lived in this branch only
+ return $db->insert($table, $this->prepareKeyProperties($connection) + [
+ 'branch_deleted' => 'y',
+ 'branch_created' => 'n',
+ ]) === 1;
+ }
+
+ return true;
+ }
+
+ protected function prepareKeyProperties(Db $connection)
+ {
+ return [
+ 'uuid' => $connection->quoteBinary($this->objectUuid->getBytes()),
+ 'branch_uuid' => $connection->quoteBinary($this->branchUuid->getBytes()),
+ ];
+ }
+
+ protected function prepareWhereString(Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $objectUuid = $connection->quoteBinary($this->objectUuid->getBytes());
+ $branchUuid = $connection->quoteBinary($this->branchUuid->getBytes());
+
+ return $db->quoteInto('uuid = ?', $objectUuid) . $db->quoteInto(' AND branch_uuid = ?', $branchUuid);
+ }
+
+ /**
+ * @param \Zend_Db_Select $query
+ * @param Db $connection
+ * @return \Zend_Db_Select
+ */
+ protected function filterQuery(\Zend_Db_Select $query, Db $connection)
+ {
+ return $query->where('uuid = ?', $connection->quoteBinary($this->objectUuid->getBytes()))
+ ->where('branch_uuid = ?', $connection->quoteBinary($this->branchUuid->getBytes()));
+ }
+
+ protected function prepareChangedProperties()
+ {
+ $properties = (array) $this->changes;
+
+ foreach (BranchSettings::ENCODED_DICTIONARIES as $property) {
+ $this->combineFlatDictionaries($properties, $property);
+ }
+ foreach (BranchSettings::ENCODED_DICTIONARIES as $property) {
+ if (isset($properties[$property])) {
+ $properties[$property] = Json::encode($properties[$property]);
+ }
+ }
+ foreach (BranchSettings::ENCODED_ARRAYS as $property) {
+ if (isset($properties[$property])) {
+ $properties[$property] = Json::encode($properties[$property]);
+ }
+ }
+ foreach (BranchSettings::RELATED_SETS as $property) {
+ if (isset($properties[$property])) {
+ $properties[$property] = Json::encode($properties[$property]);
+ }
+ }
+ $setNull = [];
+ if (array_key_exists('disabled', $properties) && $properties['disabled'] === null) {
+ unset($properties['disabled']);
+ }
+ foreach ($properties as $key => $value) {
+ if ($value === null) {
+ $setNull[] = $key;
+ }
+ }
+ if (empty($setNull)) {
+ $properties['set_null'] = null;
+ } else {
+ $properties['set_null'] = Json::encode($setNull);
+ }
+
+ return $properties;
+ }
+
+ protected function combineFlatDictionaries(&$properties, $prefix)
+ {
+ $vars = [];
+ $length = strlen($prefix) + 1;
+ foreach ($properties as $key => $value) {
+ if (substr($key, 0, $length) === "$prefix.") {
+ $vars[substr($key, $length)] = $value;
+ }
+ }
+ if (! empty($vars)) {
+ foreach (array_keys($vars) as $key) {
+ unset($properties["$prefix.$key"]);
+ }
+ $properties[$prefix] = (object) $vars;
+ }
+ }
+
+ public function applyActivity(BranchActivity $activity, Db $connection)
+ {
+ if ($activity->isActionDelete()) {
+ throw new \RuntimeException('Cannot apply a delete action');
+ }
+ if ($activity->isActionCreate()) {
+ if ($this->hasBeenTouchedByBranch()) {
+ throw new \RuntimeException('Cannot apply a CREATE activity to an already branched object');
+ } else {
+ $this->branchCreated = true;
+ }
+ }
+
+ foreach ($activity->getModifiedProperties()->jsonSerialize() as $key => $value) {
+ $this->changes[$key] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param Db $connection
+ * @param string $objectTable
+ * @param UuidInterface $uuid
+ * @param Branch $branch
+ * @return static
+ * @throws NotFoundError
+ */
+ public static function load(Db $connection, $objectTable, UuidInterface $uuid, Branch $branch)
+ {
+ $object = static::loadOptional($connection, $objectTable, $uuid, $branch->getUuid());
+ if ($object->getOriginalDbObject() === null && ! $object->hasBeenTouchedByBranch()) {
+ throw new NotFoundError('Not found');
+ }
+
+ return $object;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasBeenTouchedByBranch()
+ {
+ return $this->loadedAsBranchedObject;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasBeenDeletedByBranch()
+ {
+ return $this->branchDeleted;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasBeenCreatedByBranch()
+ {
+ return $this->branchCreated;
+ }
+
+ /**
+ * @return ?DbObject
+ */
+ public function getOriginalDbObject()
+ {
+ return $this->object;
+ }
+
+ /**
+ * @return ?DbObject
+ */
+ public function getBranchedDbObject(Db $connection)
+ {
+ if ($this->object) {
+ $branched = DbObjectTypeRegistry::newObject($this->objectTable, [], $connection);
+ // object_type first, to avoid:
+ // I can only assign for applied objects or objects with native support for assignments
+ if ($this->object->hasProperty('object_type')) {
+ $branched->set('object_type', $this->object->get('object_type'));
+ }
+ $branched->set('id', $this->object->get('id'));
+ $branched->set('uuid', $this->object->get('uuid'));
+ foreach ((array) $this->object->toPlainObject(false, true) as $key => $value) {
+ if ($key === 'object_type') {
+ continue;
+ }
+ $branched->set($key, $value);
+ }
+ } else {
+ $branched = DbObjectTypeRegistry::newObject($this->objectTable, [], $connection);
+ $branched->setUniqueId($this->objectUuid);
+ }
+ if ($this->changes === null) {
+ return $branched;
+ }
+ foreach ($this->changes as $key => $value) {
+ if ($key === 'set_null') {
+ if ($value !== null) {
+ foreach ($value as $k) {
+ $branched->set($k, null);
+ }
+ }
+ } else {
+ $branched->set($key, $value);
+ }
+ }
+
+ return $branched;
+ }
+
+ /**
+ * @return UuidInterface
+ */
+ public function getBranchUuid()
+ {
+ return $this->branchUuid;
+ }
+
+ /**
+ * @param Db $connection
+ * @param string $table
+ * @param UuidInterface $uuid
+ * @param ?UuidInterface $branchUuid
+ * @return static
+ */
+ protected static function loadOptional(
+ Db $connection,
+ $table,
+ UuidInterface $uuid,
+ UuidInterface $branchUuid = null
+ ) {
+ $class = DbObjectTypeRegistry::classByType($table);
+ if ($row = static::optionalTableRowByUuid($connection, $table, $uuid)) {
+ $object = $class::fromDbRow((array) $row, $connection);
+ } else {
+ $object = null;
+ }
+
+ $self = new static();
+ $self->object = $object;
+ $self->objectUuid = $uuid;
+ $self->branchUuid = $branchUuid;
+ $self->objectTable = $table;
+
+ if ($branchUuid && $row = static::optionalBranchedTableRowByUuid($connection, $table, $uuid, $branchUuid)) {
+ if ($row->branch_deleted === 'y') {
+ $self->branchDeleted = true;
+ } elseif ($row->branch_created === 'y') {
+ $self->branchCreated = true;
+ }
+ $self->changes = BranchSettings::normalizeBranchedObjectFromDb($row);
+ $self->loadedAsBranchedObject = true;
+ }
+
+ return $self;
+ }
+
+ public static function exists(
+ Db $connection,
+ $table,
+ UuidInterface $uuid,
+ UuidInterface $branchUuid = null
+ ) {
+ if (static::optionalTableRowByUuid($connection, $table, $uuid)) {
+ return true;
+ }
+
+ if ($branchUuid && static::optionalBranchedTableRowByUuid($connection, $table, $uuid, $branchUuid)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param Db $connection
+ * @param string $table
+ * @param UuidInterface $uuid
+ * @return stdClass|boolean
+ */
+ protected static function optionalTableRowByUuid(Db $connection, $table, UuidInterface $uuid)
+ {
+ $db = $connection->getDbAdapter();
+
+ return $db->fetchRow(
+ $db->select()->from($table)->where('uuid = ?', $connection->quoteBinary($uuid->getBytes()))
+ );
+ }
+
+ /**
+ * @param Db $connection
+ * @param string $table
+ * @param UuidInterface $uuid
+ * @return stdClass|boolean
+ */
+ protected static function optionalBranchedTableRowByUuid(
+ Db $connection,
+ $table,
+ UuidInterface $uuid,
+ UuidInterface $branchUuid
+ ) {
+ $db = $connection->getDbAdapter();
+
+ $query = $db->select()
+ ->from("branched_$table")
+ ->where('uuid = ?', $connection->quoteBinary($uuid->getBytes()))
+ ->where('branch_uuid = ?', $connection->quoteBinary($branchUuid->getBytes()));
+
+ return $db->fetchRow($query);
+ }
+
+ protected function __construct()
+ {
+ }
+}
diff --git a/library/Director/Db/Branch/MergeError.php b/library/Director/Db/Branch/MergeError.php
new file mode 100644
index 0000000..45c7b5e
--- /dev/null
+++ b/library/Director/Db/Branch/MergeError.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use Exception;
+use gipfl\Translation\TranslationHelper;
+
+abstract class MergeError extends Exception
+{
+ use TranslationHelper;
+
+ /** @var BranchActivity */
+ protected $activity;
+
+ public function __construct(BranchActivity $activity)
+ {
+ $this->activity = $activity;
+ parent::__construct($this->prepareMessage());
+ }
+
+ abstract protected function prepareMessage();
+
+ public function getObjectTypeName()
+ {
+ return preg_replace('/^icinga_/', '', $this->getActivity()->getObjectTable());
+ }
+
+ public function getNiceObjectName()
+ {
+ return $this->activity->getObjectName();
+ }
+
+ public function getActivity()
+ {
+ return $this->activity;
+ }
+}
diff --git a/library/Director/Db/Branch/MergeErrorDeleteMissingObject.php b/library/Director/Db/Branch/MergeErrorDeleteMissingObject.php
new file mode 100644
index 0000000..71f89d1
--- /dev/null
+++ b/library/Director/Db/Branch/MergeErrorDeleteMissingObject.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+class MergeErrorDeleteMissingObject extends MergeError
+{
+ public function prepareMessage()
+ {
+ return sprintf(
+ $this->translate('Cannot delete %s %s, it does not exist'),
+ $this->getObjectTypeName(),
+ $this->getNiceObjectName()
+ );
+ }
+}
diff --git a/library/Director/Db/Branch/MergeErrorModificationForMissingObject.php b/library/Director/Db/Branch/MergeErrorModificationForMissingObject.php
new file mode 100644
index 0000000..fa4e724
--- /dev/null
+++ b/library/Director/Db/Branch/MergeErrorModificationForMissingObject.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+class MergeErrorModificationForMissingObject extends MergeError
+{
+ public function prepareMessage()
+ {
+ return sprintf(
+ $this->translate('Cannot apply modification for %s %s, object does not exist'),
+ $this->getObjectTypeName(),
+ $this->getNiceObjectName()
+ );
+ }
+}
diff --git a/library/Director/Db/Branch/MergeErrorRecreateOnMerge.php b/library/Director/Db/Branch/MergeErrorRecreateOnMerge.php
new file mode 100644
index 0000000..0bb8c40
--- /dev/null
+++ b/library/Director/Db/Branch/MergeErrorRecreateOnMerge.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+class MergeErrorRecreateOnMerge extends MergeError
+{
+ public function prepareMessage()
+ {
+ return sprintf(
+ $this->translate('Cannot recreate %s %s'),
+ $this->getObjectTypeName(),
+ $this->getNiceObjectName()
+ );
+ }
+}
diff --git a/library/Director/Db/Branch/PlainObjectPropertyDiff.php b/library/Director/Db/Branch/PlainObjectPropertyDiff.php
new file mode 100644
index 0000000..0256798
--- /dev/null
+++ b/library/Director/Db/Branch/PlainObjectPropertyDiff.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+class PlainObjectPropertyDiff
+{
+ public static function calculate(array $old = null, array $new = null)
+ {
+ if ($new === null) {
+ throw new \RuntimeException('Cannot diff for delete');
+ }
+ if ($old === null) {
+ foreach (BranchSettings::ENCODED_DICTIONARIES as $property) {
+ self::flattenProperty($new, $property);
+ }
+
+ return $new;
+ }
+ $unchangedKeys = [];
+ foreach (BranchSettings::ENCODED_DICTIONARIES as $property) {
+ self::flattenProperty($old, $property);
+ self::flattenProperty($new, $property);
+ }
+ foreach ($old as $key => $value) {
+ if (array_key_exists($key, $new)) {
+ if ($value === $new[$key]) {
+ $unchangedKeys[] = $key;
+ }
+ } else {
+ $new[$key] = null;
+ }
+ }
+ foreach ($unchangedKeys as $key) {
+ unset($new[$key]);
+ }
+
+ return $new;
+ }
+
+ protected static function flattenProperty(array &$properties, $property)
+ {
+ // TODO: dots in varnames -> throw or escape?
+ if (isset($properties[$property])) {
+ foreach ((array) $properties[$property] as $key => $value) {
+ $properties["$property.$key"] = $value;
+ }
+ unset($properties[$property]);
+ }
+ }
+}
diff --git a/library/Director/Db/Branch/UuidLookup.php b/library/Director/Db/Branch/UuidLookup.php
new file mode 100644
index 0000000..b340e07
--- /dev/null
+++ b/library/Director/Db/Branch/UuidLookup.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Branch;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+use function is_int;
+use function is_resource;
+use function is_string;
+
+class UuidLookup
+{
+ /**
+ * @param Db $connection
+ * @param Branch $branch
+ * @param string $objectType
+ * @param int|string $key
+ * @param IcingaHost|null $host
+ * @param IcingaServiceSet $set
+ * @return ?UuidInterface
+ */
+ public static function findServiceUuid(
+ Db $connection,
+ Branch $branch,
+ $objectType = null,
+ $key = null,
+ IcingaHost $host = null,
+ IcingaServiceSet $set = null
+ ) {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from('icinga_service', 'uuid');
+ if ($objectType) {
+ $query->where('object_type = ?', $objectType);
+ }
+ $query = self::addKeyToQuery($connection, $query, $key);
+ if ($host) {
+ $query->where('host_id = ?', $host->get('id'));
+ }
+ if ($set) {
+ $query->where('service_set_id = ?', $set->get('id'));
+ }
+ $uuid = self::fetchOptionalUuid($connection, $query);
+
+ if ($uuid === null && $branch->isBranch()) {
+ // TODO: use different tables?
+ $query = $db->select()
+ ->from('branched_icinga_service', 'uuid')
+ ->where('branch_uuid = ?', $connection->quoteBinary($branch->getUuid()->getBytes()));
+ if ($objectType) {
+ $query->where('object_type = ?', $objectType);
+ }
+ $query = self::addKeyToQuery($connection, $query, $key);
+ if ($host) {
+ // TODO: uuid?
+ $query->where('host = ?', $host->getObjectName());
+ }
+ if ($set) {
+ $query->where('service_set = ?', $set->getObjectName());
+ }
+
+ $uuid = self::fetchOptionalUuid($connection, $query);
+ }
+
+ return $uuid;
+ }
+
+ /**
+ * @param int|string|array $key
+ * @param string $table
+ * @param Db $connection
+ * @param Branch $branch
+ * @return UuidInterface
+ * @throws NotFoundError
+ */
+ public static function requireUuidForKey($key, $table, Db $connection, Branch $branch)
+ {
+ $uuid = self::findUuidForKey($key, $table, $connection, $branch);
+ if ($uuid === null) {
+ throw new NotFoundError('No such object available');
+ }
+
+ return $uuid;
+ }
+
+ /**
+ * @param int|string|array $key
+ * @param string $table
+ * @param Db $connection
+ * @param Branch $branch
+ * @return ?UuidInterface
+ */
+ public static function findUuidForKey($key, $table, Db $connection, Branch $branch)
+ {
+ $db = $connection->getDbAdapter();
+ $query = self::addKeyToQuery($connection, $db->select()->from($table, 'uuid'), $key);
+ $uuid = self::fetchOptionalUuid($connection, $query);
+ if ($uuid === null && $branch->isBranch()) {
+ if (is_array($key) && isset($key['host_id'])) {
+ $key['host'] = IcingaHost::load($key['host_id'], $connection)->getObjectName();
+ unset($key['host_id']);
+ }
+ $query = self::addKeyToQuery($connection, $db->select()->from("branched_$table", 'uuid'), $key);
+ $query->where('branch_uuid = ?', $connection->quoteBinary($branch->getUuid()->getBytes()));
+ $uuid = self::fetchOptionalUuid($connection, $query);
+ }
+
+ return $uuid;
+ }
+
+ protected static function addKeyToQuery(Db $connection, $query, $key)
+ {
+ if (is_int($key)) {
+ $query->where('id = ?', $key);
+ } elseif (is_string($key)) {
+ $query->where('object_name = ?', $key);
+ } else {
+ foreach ($key as $k => $v) {
+ $query->where($connection->getDbAdapter()->quoteIdentifier($k) . ' = ?', $v);
+ }
+ }
+
+ return $query;
+ }
+
+ protected static function fetchOptionalUuid(Db $connection, $query)
+ {
+ $result = $connection->getDbAdapter()->fetchOne($query);
+ if (is_resource($result)) {
+ $result = stream_get_contents($result);
+ }
+ if (is_string($result)) {
+ return Uuid::fromBytes($result);
+ }
+
+ return null;
+ }
+}
diff --git a/library/Director/Db/Cache/CustomVariableCache.php b/library/Director/Db/Cache/CustomVariableCache.php
new file mode 100644
index 0000000..243ecae
--- /dev/null
+++ b/library/Director/Db/Cache/CustomVariableCache.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Cache;
+
+use Icinga\Application\Benchmark;
+use Icinga\Module\Director\CustomVariable\CustomVariables;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class CustomVariableCache
+{
+ protected $type;
+
+ protected $rowsById = array();
+
+ protected $varsById = array();
+
+ public function __construct(IcingaObject $object)
+ {
+ Benchmark::measure('Initializing CustomVariableCache');
+ $connection = $object->getConnection();
+ $db = $connection->getDbAdapter();
+
+ $columns = array(
+ 'id' => sprintf('v.%s', $object->getVarsIdColumn()),
+ 'varname' => 'v.varname',
+ 'varvalue' => 'v.varvalue',
+ 'format' => 'v.format',
+ 'checksum' => '(NULL)',
+ );
+
+ if ($connection->isPgsql()) {
+ if ($connection->hasPgExtension('pgcrypto')) {
+ $columns['checksum'] = "DIGEST(v.varvalue || ';' || v.format, 'sha1')";
+ }
+ } else {
+ $columns['checksum'] = "UNHEX(SHA1(v.varvalue || ';' || v.format))";
+ }
+
+ $query = $db->select()->from(
+ array('v' => $object->getVarsTableName()),
+ $columns
+ );
+
+ foreach ($db->fetchAll($query) as $row) {
+ $id = $row->id;
+ unset($row->id);
+
+ if (is_resource($row->checksum)) {
+ $row->checksum = stream_get_contents($row->checksum);
+ }
+
+ if (array_key_exists($id, $this->rowsById)) {
+ $this->rowsById[$id][] = $row;
+ } else {
+ $this->rowsById[$id] = array($row);
+ }
+ }
+
+ Benchmark::measure('Filled CustomVariableCache');
+ }
+
+ public function getVarsForObject(IcingaObject $object)
+ {
+ $id = $object->id;
+
+ if (array_key_exists($id, $this->rowsById)) {
+ if (! array_key_exists($id, $this->varsById)) {
+ $this->varsById[$id] = CustomVariables::forStoredRows(
+ $this->rowsById[$id]
+ );
+ }
+
+ return $this->varsById[$id];
+ } else {
+ return new CustomVariables();
+ }
+ }
+
+ public function __destruct()
+ {
+ unset($this->db);
+ }
+}
diff --git a/library/Director/Db/Cache/GroupMembershipCache.php b/library/Director/Db/Cache/GroupMembershipCache.php
new file mode 100644
index 0000000..d6d9e8b
--- /dev/null
+++ b/library/Director/Db/Cache/GroupMembershipCache.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Cache;
+
+use Icinga\Application\Benchmark;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class GroupMembershipCache
+{
+ protected $type;
+
+ protected $table;
+
+ protected $groupClass;
+
+ protected $memberships;
+
+ /** @var Db Director database connection */
+ protected $connection;
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->table = $object->getTableName();
+ $this->type = $object->getShortTableName();
+
+ $this->groupClass = 'Icinga\\Module\\Director\\Objects\\Icinga'
+ . ucfirst($this->type) . 'Group';
+
+ Benchmark::measure('Initializing GroupMemberShipCache');
+ $this->connection = $object->getConnection();
+ $this->loadAllMemberships();
+ Benchmark::measure('Filled GroupMemberShipCache');
+ }
+
+ protected function loadAllMemberships()
+ {
+ $db = $this->connection->getDbAdapter();
+ $this->memberships = array();
+
+ $type = $this->type;
+ $table = $this->table;
+
+ $query = $db->select()->from(
+ array('o' => $table),
+ array(
+ 'object_id' => 'o.id',
+ 'group_id' => 'g.id',
+ 'group_name' => 'g.object_name',
+ )
+ )->join(
+ array('go' => $table . 'group_' . $type),
+ 'o.id = go.' . $type . '_id',
+ array()
+ )->join(
+ array('g' => $table . 'group'),
+ 'go.' . $type . 'group_id = g.id',
+ array()
+ )->order('g.object_name');
+
+ foreach ($db->fetchAll($query) as $row) {
+ if (! array_key_exists($row->object_id, $this->memberships)) {
+ $this->memberships[$row->object_id] = array();
+ }
+
+ $this->memberships[$row->object_id][$row->group_id] = $row->group_name;
+ }
+ }
+
+ public function listGroupNamesForObject(IcingaObject $object)
+ {
+ if (array_key_exists($object->id, $this->memberships)) {
+ return array_values($this->memberships[$object->id]);
+ }
+
+ return array();
+ }
+
+ public function listGroupIdsForObject(IcingaObject $object)
+ {
+ if (array_key_exists($object->id, $this->memberships)) {
+ return array_keys($this->memberships[$object->id]);
+ }
+
+ return array();
+ }
+
+ public function getGroupsForObject(IcingaObject $object)
+ {
+ $groups = array();
+ $class = $this->groupClass;
+ foreach ($this->listGroupIdsForObject($object) as $id) {
+ $object = $class::loadWithAutoIncId($id, $this->connection);
+ $groups[$object->object_name] = $object;
+ }
+
+ return $groups;
+ }
+
+ public function __destruct()
+ {
+ unset($this->connection);
+ }
+}
diff --git a/library/Director/Db/Cache/PrefetchCache.php b/library/Director/Db/Cache/PrefetchCache.php
new file mode 100644
index 0000000..aa9f950
--- /dev/null
+++ b/library/Director/Db/Cache/PrefetchCache.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace Icinga\Module\Director\Db\Cache;
+
+use Icinga\Module\Director\CustomVariable\CustomVariable;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Resolver\HostServiceBlacklist;
+use Icinga\Module\Director\Resolver\TemplateTree;
+use LogicException;
+
+/**
+ * Central prefetch cache
+ *
+ * Might be improved, accept various caches based on an interface and then
+ * finally replace prefetch logic in DbObject itself. This would also allow
+ * to get rid of IcingaObject-related code in this place
+ */
+class PrefetchCache
+{
+ protected $db;
+
+ protected static $instance;
+
+ protected $varsCaches = array();
+
+ protected $groupsCaches = array();
+
+ protected $templateResolvers = array();
+
+ protected $renderedVars = array();
+
+ protected $templateTrees = array();
+
+ protected $hostServiceBlacklist;
+
+ public static function initialize(Db $db)
+ {
+ self::forget();
+ self::$instance = new static($db);
+ }
+
+ protected function __construct(Db $db)
+ {
+ $this->db = $db;
+ }
+
+ /**
+ * @throws LogicException
+ *
+ * @return self
+ */
+ public static function instance()
+ {
+ if (static::$instance === null) {
+ throw new LogicException('Prefetch cache has not been loaded');
+ }
+
+ return static::$instance;
+ }
+
+ public static function forget()
+ {
+ DbObject::clearAllPrefetchCaches();
+ self::$instance = null;
+ }
+
+ public static function shouldBeUsed()
+ {
+ return self::$instance !== null;
+ }
+
+ public function vars(IcingaObject $object)
+ {
+ return $this->varsCache($object)->getVarsForObject($object);
+ }
+
+ public function groups(IcingaObject $object)
+ {
+ return $this->groupsCache($object)->getGroupsForObject($object);
+ }
+
+ /* Hint: not implemented, this happens in DbObject right now
+ public function byObjectType($type)
+ {
+ if (! array_key_exists($type, $this->caches)) {
+ $this->caches[$type] = new ObjectCache($type);
+ }
+
+ return $this->caches[$type];
+ }
+ */
+
+ public function renderVar(CustomVariable $var, $renderExpressions = false)
+ {
+ $checksum = $var->getChecksum();
+ if (null === $checksum) {
+ return $var->toConfigString($renderExpressions);
+ } else {
+ $checksum .= (int) $renderExpressions;
+ if (! array_key_exists($checksum, $this->renderedVars)) {
+ $this->renderedVars[$checksum] = $var->toConfigString($renderExpressions);
+ }
+
+ return $this->renderedVars[$checksum];
+ }
+ }
+
+ public function hostServiceBlacklist()
+ {
+ if ($this->hostServiceBlacklist === null) {
+ $this->hostServiceBlacklist = new HostServiceBlacklist($this->db);
+ $this->hostServiceBlacklist->preloadMappings();
+ }
+
+ return $this->hostServiceBlacklist;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return CustomVariableCache
+ */
+ protected function varsCache(IcingaObject $object)
+ {
+ $key = $object->getShortTableName();
+
+ if (! array_key_exists($key, $this->varsCaches)) {
+ $this->varsCaches[$key] = new CustomVariableCache($object);
+ }
+
+ return $this->varsCaches[$key];
+ }
+
+ protected function groupsCache(IcingaObject $object)
+ {
+ $key = $object->getShortTableName();
+
+ if (! array_key_exists($key, $this->groupsCaches)) {
+ $this->groupsCaches[$key] = new GroupMembershipCache($object);
+ }
+
+ return $this->groupsCaches[$key];
+ }
+
+ protected function templateTree(IcingaObject $object)
+ {
+ $key = $object->getShortTableName();
+ if (! array_key_exists($key, $this->templateTrees)) {
+ $this->templateTrees[$key] = new TemplateTree(
+ $key,
+ $object->getConnection()
+ );
+ }
+
+ return $this->templateTrees[$key];
+ }
+
+ public function __destruct()
+ {
+ unset($this->groupsCaches);
+ unset($this->varsCaches);
+ unset($this->templateResolvers);
+ unset($this->renderedVars);
+ }
+}
diff --git a/library/Director/Db/DbSelectParenthesis.php b/library/Director/Db/DbSelectParenthesis.php
new file mode 100644
index 0000000..191ad85
--- /dev/null
+++ b/library/Director/Db/DbSelectParenthesis.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Icinga\Module\Director\Db;
+
+class DbSelectParenthesis extends \Zend_Db_Expr
+{
+ protected $select;
+
+ public function __construct(\Zend_Db_Select $select)
+ {
+ parent::__construct('');
+ $this->select = $select;
+ }
+
+ public function getSelect()
+ {
+ return $this->select;
+ }
+
+ public function __toString()
+ {
+ return '(' . $this->select . ')';
+ }
+}
diff --git a/library/Director/Db/DbUtil.php b/library/Director/Db/DbUtil.php
new file mode 100644
index 0000000..f98e213
--- /dev/null
+++ b/library/Director/Db/DbUtil.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Icinga\Module\Director\Db;
+
+use gipfl\ZfDb\Adapter\Adapter;
+use gipfl\ZfDb\Adapter\Pdo\Pgsql;
+use gipfl\ZfDb\Expr;
+use Zend_Db_Adapter_Abstract;
+use Zend_Db_Adapter_Pdo_Pgsql;
+use Zend_Db_Expr;
+use function bin2hex;
+use function is_array;
+use function is_resource;
+use function stream_get_contents;
+
+class DbUtil
+{
+ public static function binaryResult($value)
+ {
+ if (is_resource($value)) {
+ return stream_get_contents($value);
+ }
+
+ return $value;
+ }
+
+
+ /**
+ * @param string|array $binary
+ * @param Zend_Db_Adapter_Abstract $db
+ * @return Zend_Db_Expr|Zend_Db_Expr[]
+ */
+ public static function quoteBinaryLegacy($binary, $db)
+ {
+ if (is_array($binary)) {
+ return static::quoteArray($binary, 'quoteBinaryLegacy', $db);
+ }
+
+ if ($binary === null) {
+ return null;
+ }
+
+ if ($db instanceof Zend_Db_Adapter_Pdo_Pgsql) {
+ return new Zend_Db_Expr("'\\x" . bin2hex($binary) . "'");
+ }
+
+ return new Zend_Db_Expr('0x' . bin2hex($binary));
+ }
+
+ /**
+ * @param string|array $binary
+ * @param Adapter $db
+ * @return Expr|Expr[]
+ */
+ public static function quoteBinary($binary, $db)
+ {
+ if (is_array($binary)) {
+ return static::quoteArray($binary, 'quoteBinary', $db);
+ }
+
+ if ($binary === null) {
+ return null;
+ }
+
+ if ($db instanceof Pgsql) {
+ return new Expr("'\\x" . bin2hex($binary) . "'");
+ }
+
+ return new Expr('0x' . bin2hex($binary));
+ }
+
+ /**
+ * @param string|array $binary
+ * @param Adapter|Zend_Db_Adapter_Abstract $db
+ * @return Expr|Zend_Db_Expr|Expr[]|Zend_Db_Expr[]
+ */
+ public static function quoteBinaryCompat($binary, $db)
+ {
+ if ($db instanceof Adapter) {
+ return static::quoteBinary($binary, $db);
+ }
+
+ return static::quoteBinaryLegacy($binary, $db);
+ }
+
+ protected static function quoteArray($array, $method, $db)
+ {
+ $result = [];
+ foreach ($array as $bin) {
+ $quoted = static::$method($bin, $db);
+ $result[] = $quoted;
+ }
+
+ return $result;
+ }
+}
diff --git a/library/Director/Db/HostMembershipHousekeeping.php b/library/Director/Db/HostMembershipHousekeeping.php
new file mode 100644
index 0000000..3a2de05
--- /dev/null
+++ b/library/Director/Db/HostMembershipHousekeeping.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Director\Db;
+
+class HostMembershipHousekeeping extends MembershipHousekeeping
+{
+ protected $type = 'host';
+}
diff --git a/library/Director/Db/Housekeeping.php b/library/Director/Db/Housekeeping.php
new file mode 100644
index 0000000..82fd6b9
--- /dev/null
+++ b/library/Director/Db/Housekeeping.php
@@ -0,0 +1,249 @@
+<?php
+
+namespace Icinga\Module\Director\Db;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db;
+
+class Housekeeping
+{
+ /**
+ * @var Db
+ */
+ protected $connection;
+
+ /**
+ * @var \Zend_Db_Adapter_Abstract
+ */
+ protected $db;
+
+ /**
+ * @var int
+ */
+ protected $version;
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ }
+
+ public function getTaskSummary()
+ {
+ $summary = array();
+ foreach ($this->listTasks() as $name => $title) {
+ $func = 'count' . ucfirst($name);
+ $summary[$name] = (object) array(
+ 'name' => $name,
+ 'title' => $title,
+ 'count' => $this->$func()
+ );
+ }
+
+ return $summary;
+ }
+
+ public function listTasks()
+ {
+ return array(
+ 'oldUndeployedConfigs' => N_('Undeployed configurations'),
+ 'unusedFiles' => N_('Unused rendered files'),
+ 'unlinkedImportedRowSets' => N_('Unlinked imported row sets'),
+ 'unlinkedImportedRows' => N_('Unlinked imported rows'),
+ 'unlinkedImportedProperties' => N_('Unlinked imported properties'),
+ 'resolveCache' => N_('(Host) group resolve cache'),
+ );
+ }
+
+ public function getPendingTaskSummary()
+ {
+ return array_filter(
+ $this->getTaskSummary(),
+ function ($task) {
+ return $task->count > 0;
+ }
+ );
+ }
+
+ public function hasPendingTasks()
+ {
+ return count($this->getPendingTaskSummary()) > 0;
+ }
+
+ public function runAllTasks()
+ {
+ $result = array();
+
+ foreach ($this->listTasks() as $name => $task) {
+ $this->runTask($name);
+ }
+
+ return $this;
+ }
+
+ public function runTask($name)
+ {
+ $func = 'wipe' . ucfirst($name);
+ if (!method_exists($this, $func)) {
+ throw new NotFoundError(
+ 'There is no such task: %s',
+ $name
+ );
+ }
+
+ return $this->$func();
+ }
+
+ public function countOldUndeployedConfigs()
+ {
+ $conn = $this->connection;
+ $lastActivity = $conn->getLastActivityChecksum();
+
+ $sql = 'SELECT COUNT(*) FROM director_generated_config c'
+ . ' LEFT JOIN director_deployment_log d ON c.checksum = d.config_checksum'
+ . ' WHERE d.config_checksum IS NULL'
+ . ' AND ? != ' . $conn->dbHexFunc('c.last_activity_checksum');
+
+ return $this->db->fetchOne($sql, $lastActivity);
+ }
+
+ public function wipeOldUndeployedConfigs()
+ {
+ $conn = $this->connection;
+ $lastActivity = $conn->getLastActivityChecksum();
+
+ if ($this->connection->isPgsql()) {
+ $sql = 'DELETE FROM director_generated_config'
+ . ' USING director_generated_config AS c'
+ . ' LEFT JOIN director_deployment_log d ON c.checksum = d.config_checksum'
+ . ' WHERE director_generated_config.checksum = c.checksum'
+ . ' AND d.config_checksum IS NULL'
+ . ' AND ? != ' . $conn->dbHexFunc('c.last_activity_checksum');
+ } else {
+ $sql = 'DELETE c.* FROM director_generated_config c'
+ . ' LEFT JOIN director_deployment_log d ON c.checksum = d.config_checksum'
+ . ' WHERE d.config_checksum IS NULL'
+ . ' AND ? != ' . $conn->dbHexFunc('c.last_activity_checksum');
+ }
+
+ return $this->db->query($sql, $lastActivity);
+ }
+
+ public function countUnusedFiles()
+ {
+ $sql = 'SELECT COUNT(*) FROM director_generated_file f'
+ . ' LEFT JOIN director_generated_config_file cf ON f.checksum = cf.file_checksum'
+ . ' WHERE cf.file_checksum IS NULL';
+
+ return $this->db->fetchOne($sql);
+ }
+
+ public function wipeUnusedFiles()
+ {
+ if ($this->connection->isPgsql()) {
+ $sql = 'DELETE FROM director_generated_file'
+ . ' USING director_generated_file AS f'
+ . ' LEFT JOIN director_generated_config_file cf ON f.checksum = cf.file_checksum'
+ . ' WHERE director_generated_file.checksum = f.checksum'
+ . ' AND cf.file_checksum IS NULL';
+ } else {
+ $sql = 'DELETE f FROM director_generated_file f'
+ . ' LEFT JOIN director_generated_config_file cf ON f.checksum = cf.file_checksum'
+ . ' WHERE cf.file_checksum IS NULL';
+ }
+
+ return $this->db->exec($sql);
+ }
+
+ public function countUnlinkedImportedRowSets()
+ {
+ $sql = 'SELECT COUNT(*) FROM imported_rowset rs LEFT JOIN import_run r'
+ . ' ON r.rowset_checksum = rs.checksum WHERE r.id IS NULL';
+
+ return $this->db->fetchOne($sql);
+ }
+
+ public function wipeUnlinkedImportedRowSets()
+ {
+ // This one removes imported_rowset and imported_rowset_row
+ // entries no longer used by any historic import<F12>
+ if ($this->connection->isPgsql()) {
+ $sql = 'DELETE FROM imported_rowset'
+ . ' USING imported_rowset AS rs'
+ . ' LEFT JOIN import_run r ON r.rowset_checksum = rs.checksum'
+ . ' WHERE imported_rowset.checksum = rs.checksum'
+ . ' AND r.id IS NULL';
+ } else {
+ $sql = 'DELETE rs.* FROM imported_rowset rs'
+ . ' LEFT JOIN import_run r ON r.rowset_checksum = rs.checksum'
+ . ' WHERE r.id IS NULL';
+ }
+
+ return $this->db->exec($sql);
+ }
+
+ public function countUnlinkedImportedRows()
+ {
+ $sql = 'SELECT COUNT(*) FROM imported_row r LEFT JOIN imported_rowset_row rsr'
+ . ' ON rsr.row_checksum = r.checksum WHERE rsr.row_checksum IS NULL';
+
+ return $this->db->fetchOne($sql);
+ }
+
+ public function wipeUnlinkedImportedRows()
+ {
+ // This query removes imported_row and imported_row_property columns
+ // without related rowset
+ if ($this->connection->isPgsql()) {
+ $sql = 'DELETE FROM imported_row'
+ . ' USING imported_row AS r'
+ . ' LEFT JOIN imported_rowset_row rsr ON rsr.row_checksum = r.checksum'
+ . ' WHERE imported_row.checksum = r.checksum'
+ . ' AND rsr.row_checksum IS NULL';
+ } else {
+ $sql = 'DELETE r.* FROM imported_row r'
+ . ' LEFT JOIN imported_rowset_row rsr ON rsr.row_checksum = r.checksum'
+ . ' WHERE rsr.row_checksum IS NULL';
+ }
+
+ return $this->db->exec($sql);
+ }
+
+ public function countUnlinkedImportedProperties()
+ {
+ $sql = 'SELECT COUNT(*) FROM imported_property p LEFT JOIN imported_row_property rp'
+ . ' ON rp.property_checksum = p.checksum WHERE rp.property_checksum IS NULL';
+
+ return $this->db->fetchOne($sql);
+ }
+
+ public function wipeUnlinkedImportedProperties()
+ {
+ // This query removes unlinked imported properties
+ if ($this->connection->isPgsql()) {
+ $sql = 'DELETE FROM imported_property'
+ . ' USING imported_property AS p'
+ . ' LEFT JOIN imported_row_property rp ON rp.property_checksum = p.checksum'
+ . ' WHERE imported_property.checksum = p.checksum'
+ . ' AND rp.property_checksum IS NULL';
+ } else {
+ $sql = 'DELETE p.* FROM imported_property p'
+ . ' LEFT JOIN imported_row_property rp ON rp.property_checksum = p.checksum'
+ . ' WHERE rp.property_checksum IS NULL';
+ }
+
+ return $this->db->exec($sql);
+ }
+
+ public function countResolveCache()
+ {
+ $helper = MembershipHousekeeping::instance('host', $this->connection);
+ return array_sum($helper->check());
+ }
+
+ public function wipeResolveCache()
+ {
+ $helper = MembershipHousekeeping::instance('host', $this->connection);
+ return $helper->update();
+ }
+}
diff --git a/library/Director/Db/IcingaObjectFilterHelper.php b/library/Director/Db/IcingaObjectFilterHelper.php
new file mode 100644
index 0000000..2eef406
--- /dev/null
+++ b/library/Director/Db/IcingaObjectFilterHelper.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Icinga\Module\Director\Db;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Resolver\TemplateTree;
+use InvalidArgumentException;
+use RuntimeException;
+use Zend_Db_Select as ZfSelect;
+
+class IcingaObjectFilterHelper
+{
+ const INHERIT_DIRECT = 'direct';
+ const INHERIT_INDIRECT = 'indirect';
+ const INHERIT_DIRECT_OR_INDIRECT = 'total';
+
+ /**
+ * @param IcingaObject|int|string $id
+ * @return int
+ */
+ public static function wantId($id)
+ {
+ if (is_int($id)) {
+ return $id;
+ } elseif ($id instanceof IcingaObject) {
+ return (int) $id->get('id');
+ } elseif (is_string($id) && ctype_digit($id)) {
+ return (int) $id;
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'Numeric ID or IcingaObject expected, got %s',
+ // TODO: just type/class info?
+ var_export($id, 1)
+ ));
+ }
+ }
+
+ /**
+ * @param ZfSelect $query
+ * @param IcingaObject|int|string $template
+ * @param string $tableAlias
+ * @param string $inheritanceType
+ * @return ZfSelect
+ */
+ public static function filterByTemplate(
+ ZfSelect $query,
+ $template,
+ $tableAlias = 'o',
+ $inheritanceType = self::INHERIT_DIRECT
+ ) {
+ $i = $tableAlias . 'i';
+ $o = $tableAlias;
+ $type = $template->getShortTableName();
+ $db = $template->getDb();
+ $id = static::wantId($template);
+ $sub = $db->select()->from(
+ array($i => "icinga_${type}_inheritance"),
+ array('e' => '(1)')
+ )->where("$i.${type}_id = $o.id");
+
+ if ($inheritanceType === self::INHERIT_DIRECT) {
+ $sub->where("$i.parent_${type}_id = ?", $id);
+ } elseif ($inheritanceType === self::INHERIT_INDIRECT
+ || $inheritanceType === self::INHERIT_DIRECT_OR_INDIRECT
+ ) {
+ $tree = new TemplateTree($type, $template->getConnection());
+ $ids = $tree->listDescendantIdsFor($template);
+ if ($inheritanceType === self::INHERIT_DIRECT_OR_INDIRECT) {
+ $ids[] = $template->getAutoincId();
+ }
+
+ if (empty($ids)) {
+ $sub->where('(1 = 0)');
+ } else {
+ $sub->where("$i.parent_${type}_id IN (?)", $ids);
+ }
+ } else {
+ throw new RuntimeException(sprintf(
+ 'Unable to understand "%s" inheritance',
+ $inheritanceType
+ ));
+ }
+
+ return $query->where('EXISTS ?', $sub);
+ }
+
+ public static function filterByHostgroups(
+ ZfSelect $query,
+ $type,
+ $groups,
+ $tableAlias = 'o'
+ ) {
+ if (empty($groups)) {
+ // Asked for an empty set of groups? Give no result
+ $query->where('(1 = 0)');
+ } else {
+ $sub = $query->getAdapter()->select()->from(
+ array('go' => "icinga_${type}group_${type}"),
+ array('e' => '(1)')
+ )->join(
+ array('g' => "icinga_${type}group"),
+ "go.${type}group_id = g.id"
+ )->where("go.${type}_id = ${tableAlias}.id")
+ ->where('g.object_name IN (?)', $groups);
+
+ $query->where('EXISTS ?', $sub);
+ }
+ }
+
+ public static function filterByResolvedHostgroups(
+ ZfSelect $query,
+ $type,
+ $groups,
+ $tableAlias = 'o'
+ ) {
+ if (empty($groups)) {
+ // Asked for an empty set of groups? Give no result
+ $query->where('(1 = 0)');
+ } else {
+ $sub = $query->getAdapter()->select()->from(
+ array('go' => "icinga_${type}group_${type}_resolved"),
+ array('e' => '(1)')
+ )->join(
+ array('g' => "icinga_${type}group"),
+ "go.${type}group_id = g.id",
+ []
+ )->where("go.${type}_id = ${tableAlias}.id")
+ ->where('g.object_name IN (?)', $groups);
+
+ $query->where('EXISTS ?', $sub);
+ }
+ }
+}
diff --git a/library/Director/Db/MembershipHousekeeping.php b/library/Director/Db/MembershipHousekeeping.php
new file mode 100644
index 0000000..4d1ae88
--- /dev/null
+++ b/library/Director/Db/MembershipHousekeeping.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Icinga\Module\Director\Db;
+
+use Icinga\Module\Director\Application\MemoryLimit;
+use Icinga\Module\Director\Data\Db\DbConnection;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\Objects\GroupMembershipResolver;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaObjectGroup;
+
+abstract class MembershipHousekeeping
+{
+ protected $type;
+
+ protected $groupType;
+
+ protected $connection;
+
+ /** @var GroupMembershipResolver */
+ protected $resolver;
+
+ /** @var IcingaObject[] */
+ protected $objects;
+
+ /** @var IcingaObjectGroup[] */
+ protected $groups;
+
+ protected $prepared = false;
+
+ protected static $instances = [];
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+
+ if ($this->groupType === null) {
+ $this->groupType = $this->type . 'Group';
+ }
+ }
+
+ /**
+ * @param string $type
+ * @param DbConnection $connection
+ *
+ * @return static
+ */
+ public static function instance($type, $connection)
+ {
+ if (! array_key_exists($type, self::$instances)) {
+ /** @var MembershipHousekeeping $class */
+ $class = 'Icinga\\Module\\Director\\Db\\' . ucfirst($type) . 'MembershipHousekeeping';
+
+ /** @var MembershipHousekeeping $helper */
+ self::$instances[$type] = new $class($connection);
+ }
+
+ return self::$instances[$type];
+ }
+
+ protected function prepare()
+ {
+ if ($this->prepared) {
+ return $this;
+ }
+
+ $this->prepareCache();
+ $this->resolver()->defer();
+
+ $this->objects = IcingaObject::loadAllByType($this->type, $this->connection);
+ $this->resolver()->addObjects($this->objects);
+
+ $this->groups = IcingaObject::loadAllByType($this->groupType, $this->connection);
+ $this->resolver()->addGroups($this->groups);
+
+ MemoryLimit::raiseTo('1024M');
+
+ $this->prepared = true;
+
+ return $this;
+ }
+
+ public function check()
+ {
+ $this->prepare();
+
+ $resolver = $this->resolver()->checkDb();
+
+ return array($resolver->getNewMappings(), $resolver->getOutdatedMappings());
+ }
+
+ public function update()
+ {
+ $this->prepare();
+
+ $this->resolver()->refreshDb(true);
+
+ return true;
+ }
+
+ protected function prepareCache()
+ {
+ PrefetchCache::initialize($this->connection);
+
+ IcingaObject::prefetchAllRelationsByType($this->type, $this->connection);
+ }
+
+ protected function resolver()
+ {
+ if ($this->resolver === null) {
+ /** @var GroupMembershipResolver $class */
+ $class = 'Icinga\\Module\\Director\\Objects\\' . ucfirst($this->type) . 'GroupMembershipResolver';
+ $this->resolver = new $class($this->connection);
+ }
+
+ return $this->resolver;
+ }
+
+ /**
+ * @return IcingaObject[]
+ */
+ public function getObjects()
+ {
+ return $this->objects;
+ }
+
+ /**
+ * @return IcingaObjectGroup[]
+ */
+ public function getGroups()
+ {
+ return $this->groups;
+ }
+}
diff --git a/library/Director/Db/Migration.php b/library/Director/Db/Migration.php
new file mode 100644
index 0000000..5685121
--- /dev/null
+++ b/library/Director/Db/Migration.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Icinga\Module\Director\Db;
+
+use Exception;
+use Icinga\Module\Director\Data\Db\DbConnection;
+use RuntimeException;
+
+class Migration
+{
+ /**
+ * @var string
+ */
+ protected $sql;
+
+ /**
+ * @var int
+ */
+ protected $version;
+
+ public function __construct($version, $sql)
+ {
+ $this->version = $version;
+ $this->sql = $sql;
+ }
+
+ /**
+ * @param DbConnection $connection
+ * @return $this
+ */
+ public function apply(DbConnection $connection)
+ {
+ /** @var \Zend_Db_Adapter_Pdo_Abstract $db */
+ $db = $connection->getDbAdapter();
+
+ // TODO: this is fragile and depends on accordingly written schema files:
+ $queries = preg_split(
+ '/[\n\s\t]*\;[\n\s\t]+/s',
+ $this->sql,
+ -1,
+ PREG_SPLIT_NO_EMPTY
+ );
+
+ if (empty($queries)) {
+ throw new RuntimeException(sprintf(
+ 'Migration %d has no queries',
+ $this->version
+ ));
+ }
+
+ try {
+ foreach ($queries as $query) {
+ if (preg_match('/^(?:OPTIMIZE|EXECUTE) /i', $query)) {
+ $db->query($query);
+ } else {
+ $db->exec($query);
+ }
+ }
+ } catch (Exception $e) {
+ throw new RuntimeException(sprintf(
+ 'Migration %d failed (%s) while running %s',
+ $this->version,
+ $e->getMessage(),
+ $query
+ ));
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/Db/Migrations.php b/library/Director/Db/Migrations.php
new file mode 100644
index 0000000..2310408
--- /dev/null
+++ b/library/Director/Db/Migrations.php
@@ -0,0 +1,239 @@
+<?php
+
+namespace Icinga\Module\Director\Db;
+
+use DirectoryIterator;
+use Exception;
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Data\Db\DbConnection;
+use RuntimeException;
+
+class Migrations
+{
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /**
+ * @var DbConnection
+ */
+ protected $connection;
+
+ protected $migrationsDir;
+
+ public function __construct(DbConnection $connection)
+ {
+ if (version_compare(PHP_VERSION, '5.4.0') < 0) {
+ throw new RuntimeException(
+ "PHP version 5.4.x is required for Director >= 1.4.0, you're running %s."
+ . ' Please either upgrade PHP or downgrade Icinga Director',
+ PHP_VERSION
+ );
+ }
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ }
+
+ public function getLastMigrationNumber()
+ {
+ try {
+ $query = $this->db->select()->from(
+ array('m' => $this->getTableName()),
+ array('schema_version' => 'MAX(schema_version)')
+ );
+
+ return (int) $this->db->fetchOne($query);
+ } catch (Exception $e) {
+ return 0;
+ }
+ }
+
+ protected function getTableName()
+ {
+ return $this->getModuleName() . '_schema_migration';
+ }
+
+ public function hasSchema()
+ {
+ return $this->listPendingMigrations() !== array(0);
+ }
+
+ public function hasPendingMigrations()
+ {
+ return $this->countPendingMigrations() > 0;
+ }
+
+ public function countPendingMigrations()
+ {
+ return count($this->listPendingMigrations());
+ }
+
+ /**
+ * @return Migration[]
+ */
+ public function getPendingMigrations()
+ {
+ $migrations = array();
+ foreach ($this->listPendingMigrations() as $version) {
+ $migrations[] = new Migration(
+ $version,
+ $this->loadMigrationFile($version)
+ );
+ }
+
+ return $migrations;
+ }
+
+ /**
+ * @return $this
+ */
+ public function applyPendingMigrations()
+ {
+ // Ensure we have enough time to migrate
+ ini_set('max_execution_time', 0);
+
+ foreach ($this->getPendingMigrations() as $migration) {
+ $migration->apply($this->connection);
+ }
+
+ return $this;
+ }
+
+ public function listPendingMigrations()
+ {
+ $lastMigration = $this->getLastMigrationNumber();
+ if ($lastMigration === 0) {
+ return array(0);
+ }
+
+ return $this->listMigrationsAfter($this->getLastMigrationNumber());
+ }
+
+ public function listAllMigrations()
+ {
+ $dir = $this->getMigrationsDir();
+ if (! is_readable($dir)) {
+ return array();
+ }
+
+ $versions = array();
+
+ foreach (new DirectoryIterator($this->getMigrationsDir()) as $file) {
+ if ($file->isDot()) {
+ continue;
+ }
+
+ $filename = $file->getFilename();
+ if (preg_match('/^upgrade_(\d+)\.sql$/', $filename, $match)) {
+ $versions[] = $match[1];
+ }
+ }
+
+ sort($versions);
+
+ return $versions;
+ }
+
+ public function loadMigrationFile($version)
+ {
+ if ($version === 0) {
+ $filename = $this->getFullSchemaFile();
+ } else {
+ $filename = $this->getMigrationFileName($version);
+ }
+
+ return file_get_contents($filename);
+ }
+
+ public function hasBeenDowngraded()
+ {
+ return ! $this->hasMigrationFile($this->getLastMigrationNumber());
+ }
+
+ public function hasMigrationFile($version)
+ {
+ return \file_exists($this->getMigrationFileName($version));
+ }
+
+ protected function getMigrationFileName($version)
+ {
+ return sprintf(
+ '%s/upgrade_%d.sql',
+ $this->getMigrationsDir(),
+ $version
+ );
+ }
+
+ protected function listMigrationsAfter($version)
+ {
+ $filtered = array();
+ foreach ($this->listAllMigrations() as $available) {
+ if ($available > $version) {
+ $filtered[] = $available;
+ }
+ }
+
+ return $filtered;
+ }
+
+ protected function getMigrationsDir()
+ {
+ if ($this->migrationsDir === null) {
+ $this->migrationsDir = $this->getSchemaDir(
+ $this->connection->getDbType() . '-migrations'
+ );
+ }
+
+ return $this->migrationsDir;
+ }
+
+ protected function getFullSchemaFile()
+ {
+ return $this->getSchemaDir(
+ $this->connection->getDbType() . '.sql'
+ );
+ }
+
+ protected function getSchemaDir($sub = null)
+ {
+ try {
+ $dir = $this->getModuleDir('/schema');
+ } catch (ProgrammingError $e) {
+ throw new RuntimeException(
+ 'Unable to detect the schema directory for this module',
+ 0,
+ $e
+ );
+ }
+ if ($sub === null) {
+ return $dir;
+ } else {
+ return $dir . '/' . ltrim($sub, '/');
+ }
+ }
+
+ /**
+ * @param string $sub
+ * @return string
+ * @throws ProgrammingError
+ */
+ protected function getModuleDir($sub = '')
+ {
+ return Icinga::app()->getModuleManager()->getModuleDir(
+ $this->getModuleName(),
+ $sub
+ );
+ }
+
+ protected function getModuleName()
+ {
+ return $this->getModuleNameForObject($this);
+ }
+
+ protected function getModuleNameForObject($object)
+ {
+ $class = get_class($object);
+ // Hint: Icinga\Module\ -> 14 chars
+ return lcfirst(substr($class, 14, strpos($class, '\\', 15) - 14));
+ }
+}
diff --git a/library/Director/Deployment/ConditionalConfigRenderer.php b/library/Director/Deployment/ConditionalConfigRenderer.php
new file mode 100644
index 0000000..0b24418
--- /dev/null
+++ b/library/Director/Deployment/ConditionalConfigRenderer.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Director\Deployment;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\DirectorActivityLog;
+
+class ConditionalConfigRenderer
+{
+ /** @var Db */
+ protected $db;
+
+ protected $forceRendering = false;
+
+ public function __construct(Db $connection)
+ {
+ $this->db = $connection;
+ }
+
+ public function forceRendering($force = true)
+ {
+ $this->forceRendering = $force;
+
+ return $this;
+ }
+
+ public function getConfig()
+ {
+ if ($this->shouldGenerate()) {
+ return IcingaConfig::generate($this->db);
+ }
+
+ return $this->loadLatestActivityConfig();
+ }
+
+ protected function loadLatestActivityConfig()
+ {
+ $db = $this->db;
+
+ return IcingaConfig::loadByActivityChecksum($db->getLastActivityChecksum(), $db);
+ }
+
+ protected function shouldGenerate()
+ {
+ return $this->forceRendering || !$this->configForLatestActivityExists();
+ }
+
+ protected function configForLatestActivityExists()
+ {
+ $db = $this->db;
+ try {
+ $latestActivity = DirectorActivityLog::loadLatest($db);
+ } catch (NotFoundError $e) {
+ return false;
+ }
+
+ return IcingaConfig::existsForActivityChecksum(
+ bin2hex($latestActivity->get('checksum')),
+ $db
+ );
+ }
+}
diff --git a/library/Director/Deployment/ConditionalDeployment.php b/library/Director/Deployment/ConditionalDeployment.php
new file mode 100644
index 0000000..0f64028
--- /dev/null
+++ b/library/Director/Deployment/ConditionalDeployment.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Icinga\Module\Director\Deployment;
+
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Core\CoreApi;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use Psr\Log\NullLogger;
+
+class ConditionalDeployment implements LoggerAwareInterface
+{
+ use LoggerAwareTrait;
+
+ /** @var Db */
+ protected $db;
+
+ /** @var CoreApi */
+ protected $api;
+
+ /** @var ?DeploymentGracePeriod */
+ protected $gracePeriod = null;
+
+ protected $force = false;
+
+ protected $hasBeenForced = false;
+
+ /** @var ?string */
+ protected $noDeploymentReason = null;
+
+ public function __construct(Db $connection, CoreApi $api = null)
+ {
+ $this->setLogger(new NullLogger());
+ $this->db = $connection;
+ if ($api === null) {
+ $this->api = $connection->getDeploymentEndpoint()->api();
+ } else {
+ $this->api = $api;
+ }
+ $this->refresh();
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @return ?DirectorDeploymentLog
+ */
+ public function deploy(IcingaConfig $config)
+ {
+ $this->hasBeenForced = false;
+ if ($this->shouldDeploy($config)) {
+ return $this->reallyDeploy($config);
+ } elseif ($this->force) {
+ $deployment = $this->reallyDeploy($config);
+ $this->hasBeenForced = true;
+
+ return $deployment;
+ }
+
+ return null;
+ }
+
+ /**
+ * @param bool $force
+ * @return $this
+ */
+ public function force($force = true)
+ {
+ $this->force = $force;
+ return $this;
+ }
+
+ public function setGracePeriod(DeploymentGracePeriod $gracePeriod)
+ {
+ $this->gracePeriod = $gracePeriod;
+ return $this;
+ }
+
+ public function refresh()
+ {
+ $this->api->collectLogFiles($this->db);
+ $this->api->wipeInactiveStages($this->db);
+ }
+
+ public function waitForStartupAfterDeploy(DirectorDeploymentLog $deploymentLog, $timeout)
+ {
+ $startTime = time();
+ while ((time() - $startTime) <= $timeout) {
+ $deploymentFromDB = DirectorDeploymentLog::load($deploymentLog->getId(), $this->db);
+ $stageCollected = $deploymentFromDB->get('stage_collected');
+ if ($stageCollected === null) {
+ usleep(500000);
+ continue;
+ }
+ if ($stageCollected === 'n') {
+ return 'stage has not been collected (Icinga "lost" the deployment)';
+ }
+ if ($deploymentFromDB->get('startup_succeeded') === 'y') {
+ return true;
+ }
+ return 'deployment failed during startup (usually a Configuration Error)';
+ }
+ return 'deployment timed out (while waiting for an Icinga restart)';
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getNoDeploymentReason()
+ {
+ return $this->noDeploymentReason;
+ }
+
+ public function hasBeenForced()
+ {
+ return $this->hasBeenForced;
+ }
+
+ protected function shouldDeploy(IcingaConfig $config)
+ {
+ $this->noDeploymentReason = null;
+ if ($this->hasNeverDeployed()) {
+ return true;
+ }
+
+ if ($this->isWithinGracePeriod()) {
+ $this->noDeploymentReason = 'Grace period is active';
+ return false;
+ }
+
+ if ($this->deployedConfigMatches($config)) {
+ $this->noDeploymentReason = 'Config matches last deployed one';
+ return false;
+ }
+
+ if ($this->getActiveChecksum() === $config->getHexChecksum()) {
+ $this->noDeploymentReason = 'Config matches active stage';
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function hasNeverDeployed()
+ {
+ return !DirectorDeploymentLog::hasDeployments($this->db);
+ }
+
+ protected function isWithinGracePeriod()
+ {
+ return $this->gracePeriod && $this->gracePeriod->isActive();
+ }
+
+ protected function deployedConfigMatches(IcingaConfig $config)
+ {
+ if ($deployment = DirectorDeploymentLog::optionalLatest($this->db)) {
+ return $deployment->getConfigHexChecksum() === $config->getHexChecksum();
+ }
+
+ return false;
+ }
+
+ protected function getActiveChecksum()
+ {
+ return DirectorDeploymentLog::getConfigChecksumForStageName(
+ $this->db,
+ $this->api->getActiveStageName()
+ );
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @return bool|DirectorDeploymentLog
+ * @throws IcingaException
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function reallyDeploy(IcingaConfig $config)
+ {
+ $checksum = $config->getHexChecksum();
+ $this->logger->info(sprintf('Director ConfigJob ready to deploy "%s"', $checksum));
+ if ($deployment = $this->api->dumpConfig($config, $this->db)) {
+ $this->logger->notice(sprintf('Director ConfigJob deployed config "%s"', $checksum));
+ return $deployment;
+ } else {
+ throw new IcingaException('Failed to deploy config "%s"', $checksum);
+ }
+ }
+}
diff --git a/library/Director/Deployment/DeploymentGracePeriod.php b/library/Director/Deployment/DeploymentGracePeriod.php
new file mode 100644
index 0000000..6cde25a
--- /dev/null
+++ b/library/Director/Deployment/DeploymentGracePeriod.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Icinga\Module\Director\Deployment;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+
+class DeploymentGracePeriod
+{
+ /** @var int */
+ protected $graceTimeSeconds;
+
+ /** @var Db */
+ protected $db;
+
+ /**
+ * @param int $graceTimeSeconds
+ * @param Db $db
+ */
+ public function __construct($graceTimeSeconds, Db $db)
+ {
+ $this->graceTimeSeconds = $graceTimeSeconds;
+ $this->db = $db;
+ }
+
+ /**
+ * Whether we're still within a grace period
+ * @return bool
+ */
+ public function isActive()
+ {
+ if ($deployment = $this->lastDeployment()) {
+ return $deployment->getDeploymentTimestamp() > $this->getGracePeriodStart();
+ }
+
+ return false;
+ }
+
+ protected function getGracePeriodStart()
+ {
+ return time() - $this->graceTimeSeconds;
+ }
+
+ public function getRemainingGraceTime()
+ {
+ if ($this->isActive()) {
+ if ($deployment = $this->lastDeployment()) {
+ return $deployment->getDeploymentTimestamp() - $this->getGracePeriodStart();
+ } else {
+ return null;
+ }
+ }
+
+ return 0;
+ }
+
+ protected function lastDeployment()
+ {
+ return DirectorDeploymentLog::optionalLatest($this->db);
+ }
+}
diff --git a/library/Director/Deployment/DeploymentInfo.php b/library/Director/Deployment/DeploymentInfo.php
new file mode 100644
index 0000000..77d52de
--- /dev/null
+++ b/library/Director/Deployment/DeploymentInfo.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Director\Deployment;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class DeploymentInfo
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ protected $db;
+
+ /** @var int */
+ protected $totalChanges;
+
+ /** @var int */
+ protected $objectChanges;
+
+ public function __construct(Db $db)
+ {
+ $this->db = $db;
+ }
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+
+ public function getTotalChanges()
+ {
+ if ($this->totalChanges === null) {
+ $this->totalChanges = $this->db->countActivitiesSinceLastDeployedConfig();
+ }
+
+ return $this->totalChanges;
+ }
+
+ public function getSingleObjectChanges()
+ {
+ if ($this->objectChanges === null) {
+ if ($this->object === null) {
+ $this->objectChanges = 0;
+ } else {
+ $this->objectChanges = $this->db
+ ->countActivitiesSinceLastDeployedConfig($this->object);
+ }
+ }
+
+ return $this->objectChanges;
+ }
+
+ public function hasUndeployedChanges()
+ {
+ return $this->getSingleObjectChanges() > 0 && $this->getTotalChanges() > 0;
+ }
+}
diff --git a/library/Director/Deployment/DeploymentStatus.php b/library/Director/Deployment/DeploymentStatus.php
new file mode 100644
index 0000000..ae850c6
--- /dev/null
+++ b/library/Director/Deployment/DeploymentStatus.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace Icinga\Module\Director\Deployment;
+
+use Exception;
+use Icinga\Module\Director\Core\CoreApi;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+
+class DeploymentStatus
+{
+ protected $db;
+
+ protected $api;
+
+ public function __construct(Db $db, CoreApi $api)
+ {
+ $this->db = $db;
+ $this->api = $api;
+ }
+
+ public function getDeploymentStatus($configs = null, $activities = null)
+ {
+ try {
+ if (DirectorDeploymentLog::hasUncollected($this->db)) {
+ $this->api->collectLogFiles($this->db);
+ }
+ } catch (Exception $e) {
+ // Ignore eventual issues while talking to Icinga
+ }
+
+ $activeConfiguration = null;
+ $lastActivityLogChecksum = null;
+ $configChecksum = null;
+ if ($stageName = $this->api->getActiveStageName()) {
+ $activityLogChecksum = DirectorDeploymentLog::getRelatedToActiveStage($this->api, $this->db);
+ if ($activityLogChecksum === null) {
+ $activeConfiguration = [
+ 'stage_name' => $stageName,
+ 'config' => null,
+ 'activity' => null
+ ];
+ } else {
+ $lastActivityLogChecksum = bin2hex($activityLogChecksum->get('last_activity_checksum'));
+ $configChecksum = $this->getConfigChecksumForStageName($stageName);
+ $activeConfiguration = [
+ 'stage_name' => $stageName,
+ 'config' => ($configChecksum) ? : null,
+ 'activity' => $lastActivityLogChecksum
+ ];
+ }
+ }
+ $result = [
+ 'active_configuration' => (object) $activeConfiguration,
+ ];
+
+ if ($configs) {
+ $result['configs'] = (object) $this->getDeploymentStatusForConfigChecksums(
+ explode(',', $configs),
+ $configChecksum
+ );
+ }
+
+ if ($activities) {
+ $result['activities'] = (object) $this->getDeploymentStatusForActivityLogChecksums(
+ explode(',', $activities),
+ $lastActivityLogChecksum
+ );
+ }
+ return (object) $result;
+ }
+
+ public function getConfigChecksumForStageName($stageName)
+ {
+ $db = $this->db->getDbAdapter();
+ $query = $db->select()->from(
+ ['l' => 'director_deployment_log'],
+ ['checksum' => $this->db->dbHexFunc('l.config_checksum')]
+ )->where('l.stage_name = ?', $stageName);
+
+ return $db->fetchOne($query);
+ }
+
+ public function getDeploymentStatusForConfigChecksums($configChecksums, $activeConfigChecksum)
+ {
+ $db = $this->db->getDbAdapter();
+ $results = array_combine($configChecksums, array_map(function () {
+ return 'unknown';
+ }, $configChecksums));
+ $binaryConfigChecksums = [];
+ foreach ($configChecksums as $singleConfigChecksum) {
+ $binaryConfigChecksums[$singleConfigChecksum] = $this->db->quoteBinary(hex2bin($singleConfigChecksum));
+ }
+ $deployedConfigs = $this->getDeployedConfigs(array_values($binaryConfigChecksums));
+
+ foreach ($results as $singleChecksum => &$status) {
+ // active if it's equal to the provided active
+ if ($singleChecksum === $activeConfigChecksum) {
+ $status = 'active';
+ } else {
+ if (isset($deployedConfigs[$singleChecksum])) {
+ $status = ($deployedConfigs[$singleChecksum] === 'y') ? 'deployed' : 'failed';
+ } else {
+ // check if it's in generated_config table it is undeployed
+ $generatedConfigQuery = $db->select()->from(
+ ['g' => 'director_generated_config'],
+ ['checksum' => 'g.checksum']
+ )->where('g.checksum = ?', $binaryConfigChecksums[$singleChecksum]);
+ if ($db->fetchOne($generatedConfigQuery)) {
+ $status = 'undeployed';
+ }
+ }
+ // otherwise leave unknown
+ }
+ }
+
+ return $results;
+ }
+
+ public function getDeploymentStatusForActivityLogChecksums($activityLogChecksums, $activeActivityLogChecksum)
+ {
+ $db = $this->db->getDbAdapter();
+ $results = array_combine($activityLogChecksums, array_map(function () {
+ return 'unknown';
+ }, $activityLogChecksums));
+
+ foreach ($results as $singleActivityLogChecksum => &$status) {
+ // active if it's equal to the provided active
+ if ($singleActivityLogChecksum === $activeActivityLogChecksum) {
+ $status = 'active';
+ } else {
+ // get last deployed activity id and check if it's less than the passed one
+ $generatedConfigQuery = $db->select()->from(
+ ['a' => 'director_activity_log'],
+ ['id' => 'a.id']
+ )->where('a.checksum = ?', $this->db->quoteBinary(hex2bin($singleActivityLogChecksum)));
+ if ($singleActivityLogData = $db->fetchOne($generatedConfigQuery)) {
+ if ($lastDeploymentActivityLogId = $this->db->getLastDeploymentActivityLogId()) {
+ if ((int) $singleActivityLogData > $lastDeploymentActivityLogId) {
+ $status = 'undeployed';
+ } else {
+ $status = 'deployed';
+ }
+ }
+ }
+ }
+ }
+ return $results;
+ }
+
+ /**
+ * @param array $binaryConfigChecksums
+ * @return array
+ */
+ public function getDeployedConfigs(array $binaryConfigChecksums)
+ {
+ $db = $this->db->getDbAdapter();
+ $deploymentLogQuery = $db->select()->from(['l' => 'director_deployment_log'], [
+ 'checksum' => $this->db->dbHexFunc('l.config_checksum'),
+ 'deployed' => 'l.startup_succeeded'
+ ])->where('l.config_checksum IN (?)', $binaryConfigChecksums);
+ return $db->fetchPairs($deploymentLogQuery);
+ }
+}
diff --git a/library/Director/DirectorObject/Automation/Basket.php b/library/Director/DirectorObject/Automation/Basket.php
new file mode 100644
index 0000000..f7eb8e5
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/Basket.php
@@ -0,0 +1,232 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+
+/**
+ * Class Basket
+ *
+ * TODO
+ * - create a UUID like in RFC4122
+ */
+class Basket extends DbObject implements ExportInterface
+{
+ const SELECTION_ALL = true;
+ const SELECTION_NONE = false;
+
+ protected $table = 'director_basket';
+
+ protected $keyName = 'basket_name';
+
+ protected $chosenObjects = [];
+
+ protected $protectedFormerChosenObjects;
+
+ protected $defaultProperties = [
+ 'uuid' => null,
+ 'basket_name' => null,
+ 'objects' => null,
+ 'owner_type' => null,
+ 'owner_value' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'uuid'
+ ];
+
+ public function getHexUuid()
+ {
+ return bin2hex($this->get('uuid'));
+ }
+
+ public function listObjectTypes()
+ {
+ return array_keys($this->objects);
+ }
+
+ public function getChosenObjects()
+ {
+ return $this->chosenObjects;
+ }
+
+ public function isEmpty()
+ {
+ return count($this->getChosenObjects()) === 0;
+ }
+
+ protected function onLoadFromDb()
+ {
+ $this->chosenObjects = (array) Json::decode($this->get('objects'));
+ unset($this->chosenObjects['Datafield']); // Might be in old baskets
+ }
+
+ public function getUniqueIdentifier()
+ {
+ return $this->get('basket_name');
+ }
+
+ public function export()
+ {
+ $result = $this->getProperties();
+ unset($result['uuid']);
+ $result['objects'] = Json::decode($result['objects']);
+ ksort($result);
+
+ return (object) $result;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return static
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['basket_name'];
+
+ if ($replace && static::exists($name, $db)) {
+ $object = static::load($name, $db);
+ } elseif (static::exists($name, $db)) {
+ throw new DuplicateKeyException(
+ 'Basket "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ public function supportsCustomSelectionFor($type)
+ {
+ if (! array_key_exists($type, $this->chosenObjects)) {
+ return false;
+ }
+
+ return is_array($this->chosenObjects[$type]);
+ }
+
+ public function setObjects($objects)
+ {
+ if (empty($objects)) {
+ $this->chosenObjects = [];
+ } else {
+ $this->protectedFormerChosenObjects = $this->chosenObjects;
+ $this->chosenObjects = [];
+ foreach ((array) $objects as $type => $object) {
+ $this->addObjects($type, $object);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * This is a weird method, as it is required to deal with raw form data
+ *
+ * @param $type
+ * @param ExportInterface[]|bool $objects
+ */
+ public function addObjects($type, $objects = true)
+ {
+ BasketSnapshot::assertValidType($type);
+ // '1' -> from Form!
+ if ($objects === 'ALL') {
+ $objects = true;
+ } elseif ($objects === null || $objects === 'IGNORE') {
+ return;
+ } elseif ($objects === '[]' || is_array($objects)) {
+ if (! isset($this->chosenObjects[$type]) || ! is_array($this->chosenObjects[$type])) {
+ $this->chosenObjects[$type] = [];
+ }
+ if (isset($this->protectedFormerChosenObjects[$type])) {
+ if (is_array($this->protectedFormerChosenObjects[$type])) {
+ $this->chosenObjects[$type] = $this->protectedFormerChosenObjects[$type];
+ } else {
+ $this->chosenObjects[$type] = [];
+ }
+ }
+
+ if ($objects === '[]') {
+ $objects = [];
+ }
+ }
+
+ if ($objects === true) {
+ $this->chosenObjects[$type] = true;
+ } elseif ($objects === '0') {
+ // nothing
+ } else {
+ foreach ($objects as $object) {
+ $this->addObject($type, $object);
+ }
+
+ if (array_key_exists($type, $this->chosenObjects)) {
+ ksort($this->chosenObjects[$type]);
+ }
+ }
+
+ $this->reallySet('objects', Json::encode($this->chosenObjects));
+ }
+
+ public function hasObject($type, $object)
+ {
+ if (! $this->hasType($type)) {
+ return false;
+ }
+
+ if ($this->chosenObjects[$type] === true) {
+ return true;
+ }
+
+ if ($object instanceof ExportInterface) {
+ $object = $object->getUniqueIdentifier();
+ }
+
+ if (is_array($this->chosenObjects[$type])) {
+ return in_array($object, $this->chosenObjects[$type]);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param $type
+ * @param string $object
+ */
+ public function addObject($type, $object)
+ {
+ if (is_array($this->chosenObjects[$type])) {
+ $this->chosenObjects[$type][] = $object;
+ } else {
+ throw new \InvalidArgumentException(sprintf(
+ 'The Basket "%s" has not been configured for single objects of type "%s"',
+ $this->get('basket_name'),
+ $type
+ ));
+ }
+ }
+
+ public function hasType($type)
+ {
+ return isset($this->chosenObjects[$type]);
+ }
+
+ protected function beforeStore()
+ {
+ if (! $this->hasBeenLoadedFromDb()) {
+ // TODO: This is BS, use a real UUID
+ $this->set('uuid', hex2bin(substr(sha1(microtime(true) . rand(1, 100000)), 0, 32)));
+ }
+ }
+}
diff --git a/library/Director/DirectorObject/Automation/BasketContent.php b/library/Director/DirectorObject/Automation/BasketContent.php
new file mode 100644
index 0000000..e59c0ae
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/BasketContent.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+
+class BasketContent extends DbObject
+{
+ protected $objects;
+
+ protected $table = 'director_basket_content';
+
+ protected $keyName = 'checksum';
+
+ protected $defaultProperties = [
+ 'checksum' => null,
+ 'summary' => null,
+ 'content' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'checksum'
+ ];
+}
diff --git a/library/Director/DirectorObject/Automation/BasketSnapshot.php b/library/Director/DirectorObject/Automation/BasketSnapshot.php
new file mode 100644
index 0000000..4ddf2ce
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/BasketSnapshot.php
@@ -0,0 +1,531 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use gipfl\Json\JsonEncodeException;
+use gipfl\Json\JsonString;
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\Data\Exporter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\DirectorDatafieldCategory;
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaDependency;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaHostGroup;
+use Icinga\Module\Director\Objects\IcingaNotification;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceGroup;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Objects\IcingaTemplateChoiceHost;
+use Icinga\Module\Director\Objects\IcingaTemplateChoiceService;
+use Icinga\Module\Director\Objects\IcingaTimePeriod;
+use Icinga\Module\Director\Objects\IcingaUser;
+use Icinga\Module\Director\Objects\IcingaUserGroup;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\SyncRule;
+use InvalidArgumentException;
+use RuntimeException;
+
+class BasketSnapshot extends DbObject
+{
+ protected static $typeClasses = [
+ 'DatafieldCategory' => DirectorDatafieldCategory::class,
+ 'Datafield' => DirectorDatafield::class,
+ 'TimePeriod' => IcingaTimePeriod::class,
+ 'CommandTemplate' => [IcingaCommand::class, ['object_type' => 'template']],
+ 'ExternalCommand' => [IcingaCommand::class, ['object_type' => 'external_object']],
+ 'Command' => [IcingaCommand::class, ['object_type' => 'object']],
+ 'HostGroup' => IcingaHostGroup::class,
+ 'IcingaTemplateChoiceHost' => IcingaTemplateChoiceHost::class,
+ 'HostTemplate' => IcingaHost::class,
+ 'ServiceGroup' => IcingaServiceGroup::class,
+ 'IcingaTemplateChoiceService' => IcingaTemplateChoiceService::class,
+ 'ServiceTemplate' => IcingaService::class,
+ 'ServiceSet' => IcingaServiceSet::class,
+ 'UserGroup' => IcingaUserGroup::class,
+ 'UserTemplate' => [IcingaUser::class, ['object_type' => 'template']],
+ 'User' => [IcingaUser::class, ['object_type' => 'object']],
+ 'NotificationTemplate' => IcingaNotification::class,
+ 'Notification' => [IcingaNotification::class, ['object_type' => 'apply']],
+ 'DataList' => DirectorDatalist::class,
+ 'Dependency' => IcingaDependency::class,
+ 'ImportSource' => ImportSource::class,
+ 'SyncRule' => SyncRule::class,
+ 'DirectorJob' => DirectorJob::class,
+ 'Basket' => Basket::class,
+ ];
+
+ protected $objects = [];
+
+ protected $content;
+
+ protected $table = 'director_basket_snapshot';
+
+ protected $keyName = [
+ 'basket_uuid',
+ 'ts_create',
+ ];
+
+ protected $restoreOrder = [
+ 'CommandTemplate',
+ 'ExternalCommand',
+ 'Command',
+ 'TimePeriod',
+ 'HostGroup',
+ 'IcingaTemplateChoiceHost',
+ 'HostTemplate',
+ 'ServiceGroup',
+ 'IcingaTemplateChoiceService',
+ 'ServiceTemplate',
+ 'ServiceSet',
+ 'UserGroup',
+ 'UserTemplate',
+ 'User',
+ 'NotificationTemplate',
+ 'Notification',
+ 'Dependency',
+ 'ImportSource',
+ 'SyncRule',
+ 'DirectorJob',
+ 'Basket',
+ ];
+
+ protected $defaultProperties = [
+ 'basket_uuid' => null,
+ 'content_checksum' => null,
+ 'ts_create' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'basket_uuid',
+ 'content_checksum',
+ ];
+
+ public static function supports($type)
+ {
+ return isset(self::$typeClasses[$type]);
+ }
+
+ public static function assertValidType($type)
+ {
+ if (! static::supports($type)) {
+ throw new InvalidArgumentException("Basket does not support '$type'");
+ }
+ }
+
+ public static function getClassForType($type)
+ {
+ static::assertValidType($type);
+
+ if (is_array(self::$typeClasses[$type])) {
+ return self::$typeClasses[$type][0];
+ }
+
+ return self::$typeClasses[$type];
+ }
+
+ public static function getClassAndObjectTypeForType($type)
+ {
+ if (is_array(self::$typeClasses[$type])) {
+ return self::$typeClasses[$type];
+ }
+
+ return [self::$typeClasses[$type], null];
+ }
+
+ /**
+ * @param Basket $basket
+ * @param Db $db
+ * @return BasketSnapshot
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function createForBasket(Basket $basket, Db $db)
+ {
+ $snapshot = static::create([
+ 'basket_uuid' => $basket->get('uuid')
+ ], $db);
+ $snapshot->addObjectsChosenByBasket($basket);
+ $snapshot->resolveRequiredFields();
+
+ return $snapshot;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function resolveRequiredFields()
+ {
+ /** @var Db $db */
+ $db = $this->getConnection();
+ $fieldResolver = new BasketSnapshotFieldResolver($this->objects, $db);
+ /** @var DirectorDatafield[] $fields */
+ $fields = $fieldResolver->loadCurrentFields($db);
+ $categories = [];
+ if (! empty($fields)) {
+ $plain = [];
+ foreach ($fields as $id => $field) {
+ $plain[$id] = $field->export();
+ if ($category = $field->getCategory()) {
+ $categories[$category->get('category_name')] = $category->export();
+ }
+ }
+ $this->objects['Datafield'] = $plain;
+ }
+ if (! empty($categories)) {
+ $this->objects['DatafieldCategory'] = $categories;
+ }
+ }
+
+ protected function addObjectsChosenByBasket(Basket $basket)
+ {
+ foreach ($basket->getChosenObjects() as $typeName => $selection) {
+ if ($selection === true) {
+ $this->addAll($typeName);
+ } elseif (! empty($selection)) {
+ $this->addByIdentifiers($typeName, $selection);
+ }
+ }
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function beforeStore()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ throw new RuntimeException('A basket snapshot cannot be modified');
+ }
+ $json = $this->getJsonDump();
+ $checksum = sha1($json, true);
+ if (! BasketContent::exists($checksum, $this->getConnection())) {
+ BasketContent::create([
+ 'checksum' => $checksum,
+ 'summary' => $this->getJsonSummary(),
+ 'content' => $json,
+ ], $this->getConnection())->store();
+ }
+
+ $this->set('content_checksum', $checksum);
+ $this->set('ts_create', round(microtime(true) * 1000));
+ }
+
+ /**
+ * @param Db $connection
+ * @param bool $replace
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function restoreTo(Db $connection, $replace = true)
+ {
+ static::restoreJson(
+ $this->getJsonDump(),
+ $connection,
+ $replace
+ );
+ }
+
+ /**
+ * @param Basket $basket
+ * @param $string
+ * @return BasketSnapshot
+ */
+ public static function forBasketFromJson(Basket $basket, $string)
+ {
+ $snapshot = static::create([
+ 'basket_uuid' => $basket->get('uuid')
+ ]);
+ $snapshot->objects = [];
+ foreach ((array) Json::decode($string) as $type => $objects) {
+ $snapshot->objects[$type] = (array) $objects;
+ }
+
+ return $snapshot;
+ }
+
+ public static function restoreJson($string, Db $connection, $replace = true)
+ {
+ $snapshot = new static();
+ $snapshot->restoreObjects(
+ Json::decode($string),
+ $connection,
+ $replace
+ );
+ }
+
+ /**
+ * @param $all
+ * @param Db $connection
+ * @param bool $replace
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Zend_Db_Adapter_Exception
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function restoreObjects($all, Db $connection, $replace = true)
+ {
+ $db = $connection->getDbAdapter();
+ $db->beginTransaction();
+ $fieldResolver = new BasketSnapshotFieldResolver($all, $connection);
+ $this->restoreType($all, 'DataList', $fieldResolver, $connection, $replace);
+ $this->restoreType($all, 'DatafieldCategory', $fieldResolver, $connection, $replace);
+ $fieldResolver->storeNewFields();
+ foreach ($this->restoreOrder as $typeName) {
+ $this->restoreType($all, $typeName, $fieldResolver, $connection, $replace);
+ }
+ $db->commit();
+ }
+
+ /**
+ * @param $all
+ * @param $typeName
+ * @param BasketSnapshotFieldResolver $fieldResolver
+ * @param Db $connection
+ * @param $replace
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function restoreType(
+ &$all,
+ $typeName,
+ BasketSnapshotFieldResolver $fieldResolver,
+ Db $connection,
+ $replace
+ ) {
+ if (isset($all->$typeName)) {
+ $objects = (array) $all->$typeName;
+ } else {
+ return;
+ }
+ $class = static::getClassForType($typeName);
+
+ $changed = [];
+ foreach ($objects as $key => $object) {
+ /** @var DbObject $new */
+ $new = $class::import($object, $connection, $replace);
+ if ($new->hasBeenModified()) {
+ if ($new instanceof IcingaObject && $new->supportsImports()) {
+ /** @var ExportInterface $new */
+ $changed[$new->getUniqueIdentifier()] = $new;
+ } else {
+ $new->store();
+ // Linking fields right now, as we're not in $changed
+ if ($new instanceof IcingaObject) {
+ $fieldResolver->relinkObjectFields($new, $object);
+ }
+ }
+ } else {
+ // No modification on the object, still, fields might have
+ // been changed
+ if ($new instanceof IcingaObject) {
+ $fieldResolver->relinkObjectFields($new, $object);
+ }
+ }
+ $allObjects[spl_object_hash($new)] = $object;
+ }
+
+ /** @var IcingaObject $object */
+ foreach ($changed as $object) {
+ $this->recursivelyStore($object, $changed);
+ }
+ foreach ($changed as $key => $new) {
+ // Store related fields. As objects might have formerly been
+ // un-stored, let's to it right here
+ if ($new instanceof IcingaObject) {
+ $fieldResolver->relinkObjectFields($new, $objects[$key]);
+ }
+ }
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @param $list
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function recursivelyStore(IcingaObject $object, &$list)
+ {
+ foreach ($object->listImportNames() as $parent) {
+ if (array_key_exists($parent, $list)) {
+ $this->recursivelyStore($list[$parent], $list);
+ }
+ }
+
+ $object->store();
+ }
+
+ /**
+ * @return BasketContent
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getContent()
+ {
+ if ($this->content === null) {
+ $this->content = BasketContent::load($this->get('content_checksum'), $this->getConnection());
+ }
+
+ return $this->content;
+ }
+
+ protected function onDelete()
+ {
+ $db = $this->getDb();
+ $db->delete(
+ ['bc' => 'director_basket_content'],
+ 'NOT EXISTS (SELECT director_basket_checksum WHERE content_checksum = bc.checksum)'
+ );
+ }
+
+ /**
+ * @return string
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getJsonSummary()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ return $this->getContent()->get('summary');
+ }
+
+ return Json::encode($this->getSummary(), JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * @return array|mixed
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getSummary()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ return Json::decode($this->getContent()->get('summary'));
+ }
+
+ $summary = [];
+ foreach (array_keys($this->objects) as $key) {
+ $summary[$key] = count($this->objects[$key]);
+ }
+
+ return $summary;
+ }
+
+ /**
+ * @return string
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getJsonDump()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ return $this->getContent()->get('content');
+ }
+
+ try {
+ return JsonString::encode($this->objects, JSON_PRETTY_PRINT);
+ } catch (JsonEncodeException $e) {
+ foreach ($this->objects as $type => $objects) {
+ foreach ($objects as $object) {
+ try {
+ JsonString::encode($object);
+ } catch (JsonEncodeException $singleError) {
+ $dump = var_export($object, 1);
+ if (function_exists('iconv')) {
+ $dump = iconv('UTF-8', 'UTF-8//IGNORE', $dump);
+ }
+ throw new JsonEncodeException(sprintf(
+ 'Failed to encode object ot type "%s": %s, %s',
+ $type,
+ $dump,
+ $singleError->getMessage()
+ ), $singleError->getCode());
+ }
+ }
+ }
+
+ throw $e;
+ }
+ }
+
+ protected function addAll($typeName)
+ {
+ list($class, $filter) = static::getClassAndObjectTypeForType($typeName);
+ $connection = $this->getConnection();
+ assert($connection instanceof Db);
+
+ /** @var IcingaObject $dummy */
+ $dummy = $class::create();
+ if ($dummy instanceof IcingaObject && $dummy->supportsImports()) {
+ $db = $this->getDb();
+ $select = $db->select()->from($dummy->getTableName());
+ if ($filter) {
+ foreach ($filter as $column => $value) {
+ $select->where("$column = ?", $value);
+ }
+ } elseif (! $dummy->isGroup()
+ // TODO: this is ugly.
+ && ! $dummy instanceof IcingaDependency
+ && ! $dummy instanceof IcingaTimePeriod
+ ) {
+ $select->where('object_type = ?', 'template');
+ }
+ $all = $class::loadAll($connection, $select);
+ } else {
+ $all = $class::loadAll($connection);
+ }
+ $exporter = new Exporter($connection);
+ foreach ($all as $object) {
+ $this->objects[$typeName][$object->getUniqueIdentifier()] = $exporter->export($object);
+ }
+ }
+
+ protected function addByIdentifiers($typeName, $identifiers)
+ {
+ foreach ($identifiers as $identifier) {
+ $this->addByIdentifier($typeName, $identifier);
+ }
+ }
+
+ /**
+ * @param $typeName
+ * @param $identifier
+ * @param Db $connection
+ * @return ExportInterface|DbObject|null
+ */
+ public static function instanceByIdentifier($typeName, $identifier, Db $connection)
+ {
+ $class = static::getClassForType($typeName);
+ if (substr($class, -13) === 'IcingaService') {
+ $identifier = [
+ 'object_type' => 'template',
+ 'object_name' => $identifier,
+ ];
+ }
+ /** @var ExportInterface $object */
+ if ($class::exists($identifier, $connection)) {
+ $object = $class::load($identifier, $connection);
+ } else {
+ $object = null;
+ }
+
+ return $object;
+ }
+
+ /**
+ * @param $typeName
+ * @param $identifier
+ */
+ protected function addByIdentifier($typeName, $identifier)
+ {
+ /** @var Db $connection */
+ $connection = $this->getConnection();
+ $exporter = new Exporter($connection);
+ $object = static::instanceByIdentifier(
+ $typeName,
+ $identifier,
+ $connection
+ );
+ if ($object !== null) {
+ $this->objects[$typeName][$identifier] = $exporter->export($object);
+ }
+ }
+}
diff --git a/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php
new file mode 100644
index 0000000..4653255
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php
@@ -0,0 +1,226 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class BasketSnapshotFieldResolver
+{
+ /** @var BasketSnapshot */
+ protected $snapshot;
+
+ /** @var \Icinga\Module\Director\Data\Db\DbConnection */
+ protected $targetDb;
+
+ /** @var array|null */
+ protected $requiredIds;
+
+ protected $objects;
+
+ /** @var int */
+ protected $nextNewId = 1;
+
+ /** @var array|null */
+ protected $idMap;
+
+ /** @var DirectorDatafield[]|null */
+ protected $targetFields;
+
+ public function __construct($objects, Db $targetDb)
+ {
+ $this->objects = $objects;
+ $this->targetDb = $targetDb;
+ }
+
+ /**
+ * @param Db $db
+ * @return DirectorDatafield[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function loadCurrentFields(Db $db)
+ {
+ $fields = [];
+ foreach ($this->getRequiredIds() as $id) {
+ $fields[$id] = DirectorDatafield::loadWithAutoIncId((int) $id, $db);
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function storeNewFields()
+ {
+ $this->targetFields = null; // Clear Cache
+ foreach ($this->getTargetFields() as $id => $field) {
+ if ($field->hasBeenModified()) {
+ $field->store();
+ $this->idMap[$id] = $field->get('id');
+ }
+ }
+ }
+
+ /**
+ * @param IcingaObject $new
+ * @param $object
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function relinkObjectFields(IcingaObject $new, $object)
+ {
+ if (! $new->supportsFields() || ! isset($object->fields)) {
+ return;
+ }
+ $fieldMap = $this->getIdMap();
+
+ $objectId = (int) $new->get('id');
+ $table = $new->getTableName() . '_field';
+ $objectKey = $new->getShortTableName() . '_id';
+ $existingFields = [];
+
+ $db = $this->targetDb->getDbAdapter();
+
+ foreach ($db->fetchAll(
+ $db->select()->from($table)->where("$objectKey = ?", $objectId)
+ ) as $mapping) {
+ $existingFields[(int) $mapping->datafield_id] = $mapping;
+ }
+ foreach ($object->fields as $field) {
+ $id = $fieldMap[(int) $field->datafield_id];
+ if (isset($existingFields[$id])) {
+ unset($existingFields[$id]);
+ } else {
+ $db->insert($table, [
+ $objectKey => $objectId,
+ 'datafield_id' => $id,
+ 'is_required' => $field->is_required,
+ 'var_filter' => $field->var_filter,
+ ]);
+ }
+ }
+ if (! empty($existingFields)) {
+ $db->delete(
+ $table,
+ $db->quoteInto(
+ "$objectKey = $objectId AND datafield_id IN (?)",
+ array_keys($existingFields)
+ )
+ );
+ }
+ }
+
+ /**
+ * @param object $object
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function tweakTargetIds($object)
+ {
+ $forward = $this->getIdMap();
+ $map = array_flip($forward);
+ if (isset($object->fields)) {
+ foreach ($object->fields as $field) {
+ $id = $field->datafield_id;
+ if (isset($map[$id])) {
+ $field->datafield_id = $map[$id];
+ } else {
+ $field->datafield_id = "(NEW)";
+ }
+ }
+ }
+ }
+
+ /**
+ * @return int
+ */
+ protected function getNextNewId()
+ {
+ return $this->nextNewId++;
+ }
+
+ protected function getRequiredIds()
+ {
+ if ($this->requiredIds === null) {
+ if (isset($this->objects['Datafield'])) {
+ $this->requiredIds = array_keys($this->objects['Datafield']);
+ } else {
+ $ids = [];
+ foreach ($this->objects as $typeName => $objects) {
+ foreach ($objects as $key => $object) {
+ if (isset($object->fields)) {
+ foreach ($object->fields as $field) {
+ $ids[$field->datafield_id] = true;
+ }
+ }
+ }
+ }
+
+ $this->requiredIds = array_keys($ids);
+ }
+ }
+
+ return $this->requiredIds;
+ }
+
+ /**
+ * @param $type
+ * @return object[]
+ */
+ protected function getObjectsByType($type)
+ {
+ if (isset($this->objects->$type)) {
+ return (array) $this->objects->$type;
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * @return DirectorDatafield[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getTargetFields()
+ {
+ if ($this->targetFields === null) {
+ $this->calculateIdMap();
+ }
+
+ return $this->targetFields;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getIdMap()
+ {
+ if ($this->idMap === null) {
+ $this->calculateIdMap();
+ }
+
+ return $this->idMap;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function calculateIdMap()
+ {
+ $this->idMap = [];
+ $this->targetFields = [];
+ foreach ($this->getObjectsByType('Datafield') as $id => $object) {
+ unset($object->category_id); // Fix old baskets
+ // Hint: import() doesn't store!
+ $new = DirectorDatafield::import($object, $this->targetDb);
+ if ($new->hasBeenLoadedFromDb()) {
+ $newId = (int) $new->get('id');
+ } else {
+ $newId = sprintf('NEW(%s)', $this->getNextNewId());
+ }
+ $this->idMap[$id] = $newId;
+ $this->targetFields[$id] = $new;
+ }
+ }
+}
diff --git a/library/Director/DirectorObject/Automation/CompareBasketObject.php b/library/Director/DirectorObject/Automation/CompareBasketObject.php
new file mode 100644
index 0000000..ef2e9e2
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/CompareBasketObject.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use Icinga\Module\Director\Core\Json;
+use ipl\Html\Error;
+use RuntimeException;
+use function array_key_exists;
+use function is_array;
+use function is_object;
+use function is_scalar;
+
+class CompareBasketObject
+{
+ public static function normalize(&$value)
+ {
+ if (is_scalar($value)) {
+ return;
+ }
+ if (is_array($value)) {
+ foreach ($value as $k => &$v) {
+ static::normalize($v);
+ }
+ unset($v);
+ }
+ if (is_object($value)) {
+ $sorted = (array) $value;
+ // foreign baskets might not sort as we do:
+ ksort($sorted);
+ foreach ($sorted as $k => &$v) {
+ static::normalize($v);
+ }
+ unset($v);
+ $value = $sorted;
+
+ // foreign baskets might not sort those lists correctly:
+ if (isset($value->list_name) && isset($value->entries)) {
+ static::sortListBy('entry_name', $value->entries);
+ }
+ if (isset($value->fields)) {
+ static::sortListBy('datafield_id', $value->fields);
+ }
+ }
+ }
+
+ protected static function sortListBy($key, &$list)
+ {
+ usort($list, function ($a, $b) use ($key) {
+ return $a->$key > $b->$key ? -1 : 1;
+ });
+ }
+
+ public static function equals($a, $b)
+ {
+ if (is_scalar($a)) {
+ return $a === $b;
+ }
+
+ if ($a === null) {
+ return $b === null;
+ }
+
+ // Well... this is annoying :-/
+ $a = Json::decode(Json::encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
+ $b = Json::decode(Json::encode($b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
+ if (is_array($a)) {
+ // Empty arrays VS empty objects :-( This is a fallback, not needed unless en/decode takes place
+ if (empty($a) && is_object($b) && (array) $b === []) {
+ return true;
+ }
+ if (! is_array($b)) {
+ return false;
+ }
+ if (array_keys($a) !== array_keys($b)) {
+ return false;
+ }
+ foreach ($a as $k => $v) {
+ if (array_key_exists($k, $b) && static::equals($b[$k], $v)) {
+ continue;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ if (is_object($a)) {
+ // Well... empty arrays VS empty objects :-(
+ if ($b === [] && (array) $a === []) {
+ return true;
+ }
+ if (! is_object($b)) {
+ return false;
+ }
+
+ // Workaround, same as above
+ if (isset($a->list_name) && isset($a->entries)) {
+ if (! isset($b->entries)) {
+ return false;
+ }
+ static::sortListBy('entry_name', $a->entries);
+ static::sortListBy('entry_name', $b->entries);
+ }
+ if (isset($a->fields) && isset($b->fields)) {
+ static::sortListBy('datafield_id', $a->fields);
+ static::sortListBy('datafield_id', $b->fields);
+ }
+ foreach ((array) $a as $k => $v) {
+ if (property_exists($b, $k) && static::equals($v, $b->$k)) {
+ continue;
+ }
+ if (! property_exists($b, $k)) {
+ if ($v === null) {
+ continue;
+ }
+ // Deal with two special defaults:
+ if ($k === 'set_if_format' && $v === 'string') {
+ continue;
+ }
+ if ($k === 'disabled' && $v === false) {
+ continue;
+ }
+ }
+ return false;
+ }
+ foreach ((array) $b as $k => $v) {
+ if (! property_exists($a, $k)) {
+ if ($v === null) {
+ continue;
+ }
+ // Once again:
+ if ($k === 'set_if_format' && $v === 'string') {
+ continue;
+ }
+ if ($k === 'disabled' && $v === false) {
+ continue;
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ throw new RuntimeException("Cannot compare " . Error::getPhpTypeName($a));
+ }
+}
diff --git a/library/Director/DirectorObject/Automation/ExportInterface.php b/library/Director/DirectorObject/Automation/ExportInterface.php
new file mode 100644
index 0000000..275dfed
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/ExportInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use Icinga\Module\Director\Db;
+
+interface ExportInterface
+{
+ /**
+ * @deprecated
+ * @return \stdClass
+ */
+ public function export();
+
+ public static function import($plain, Db $db, $replace = false);
+
+ // TODO:
+ // public function getXyzChecksum();
+ public function getUniqueIdentifier();
+}
diff --git a/library/Director/DirectorObject/Automation/ImportExport.php b/library/Director/DirectorObject/Automation/ImportExport.php
new file mode 100644
index 0000000..a5e72fa
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/ImportExport.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use Icinga\Module\Director\Data\Exporter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\IcingaHostGroup;
+use Icinga\Module\Director\Objects\IcingaServiceGroup;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Objects\IcingaTemplateChoiceHost;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class ImportExport
+{
+ /** @var Db */
+ protected $connection;
+
+ /** @var Exporter */
+ protected $exporter;
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->exporter = new Exporter($connection);
+ }
+
+ public function serializeAllServiceSets()
+ {
+ $res = [];
+ foreach (IcingaServiceSet::loadAll($this->connection) as $object) {
+ if ($object->get('host_id')) {
+ continue;
+ }
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllHostTemplateChoices()
+ {
+ $res = [];
+ foreach (IcingaTemplateChoiceHost::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllHostGroups()
+ {
+ $res = [];
+ foreach (IcingaHostGroup::loadAll($this->connection) as $object) {
+ $res[] = $object->toPlainObject();
+ }
+
+ return $res;
+ }
+
+ public function serializeAllServiceGroups()
+ {
+ $res = [];
+ foreach (IcingaServiceGroup::loadAll($this->connection) as $object) {
+ $res[] = $object->toPlainObject();
+ }
+
+ return $res;
+ }
+
+ public function serializeAllDataFields()
+ {
+ $res = [];
+ foreach (DirectorDatafield::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllDataLists()
+ {
+ $res = [];
+ foreach (DirectorDatalist::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllJobs()
+ {
+ $res = [];
+ foreach (DirectorJob::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllImportSources()
+ {
+ $res = [];
+ foreach (ImportSource::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllSyncRules()
+ {
+ $res = [];
+ foreach (SyncRule::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function unserializeImportSources($objects)
+ {
+ $count = 0;
+ $this->connection->runFailSafeTransaction(function () use ($objects, &$count) {
+ foreach ($objects as $object) {
+ ImportSource::import($object, $this->connection)->store();
+ $count++;
+ }
+ });
+
+ return $count;
+ }
+
+ public function unserializeSyncRules($objects)
+ {
+ $count = 0;
+ $this->connection->runFailSafeTransaction(function () use ($objects, &$count) {
+ foreach ($objects as $object) {
+ SyncRule::import($object, $this->connection)->store();
+ }
+ $count++;
+ });
+
+ return $count;
+ }
+}
diff --git a/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php b/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php
new file mode 100644
index 0000000..abda497
--- /dev/null
+++ b/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Lookup;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\HostApplyMatches;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+
+/**
+ * A Service Apply Rule matching this Host, generating a Service with the given
+ * name
+ */
+class AppliedServiceInfo implements ServiceInfo
+{
+ /** @var string */
+ protected $hostName;
+
+ /** @var string */
+ protected $serviceName;
+
+ /** @var int */
+ protected $serviceApplyRuleId;
+
+ /** @var UuidInterface */
+ protected $uuid;
+
+ public function __construct($hostName, $serviceName, $serviceApplyRuleId, UuidInterface $uuid)
+ {
+ $this->hostName = $hostName;
+ $this->serviceName= $serviceName;
+ $this->serviceApplyRuleId = $serviceApplyRuleId;
+ $this->uuid = $uuid;
+ }
+
+ public static function find(IcingaHost $host, $serviceName)
+ {
+ $matcher = HostApplyMatches::prepare($host);
+ $connection = $host->getConnection();
+ foreach (static::fetchApplyRulesByServiceName($connection, $serviceName) as $rule) {
+ if ($matcher->matchesFilter($rule->filter)) {
+ return new static($host->getObjectName(), $serviceName, (int) $rule->id, $rule->uuid);
+ }
+ }
+
+ return null;
+ }
+
+ public function getHostName()
+ {
+ return $this->hostName;
+ }
+
+ /**
+ * @return int
+ */
+ public function getServiceApplyRuleId()
+ {
+ return $this->serviceApplyRuleId;
+ }
+
+ public function getName()
+ {
+ return $this->serviceName;
+ }
+
+ public function getUuid()
+ {
+ return $this->uuid;
+ }
+
+ public function getUrl()
+ {
+ return Url::fromPath('director/host/appliedservice', [
+ 'name' => $this->hostName,
+ 'service_id' => $this->serviceApplyRuleId,
+ ]);
+ }
+
+ public function requiresOverrides()
+ {
+ return true;
+ }
+
+ protected static function fetchApplyRulesByServiceName(Db $connection, $serviceName)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()
+ ->from(['s' => 'icinga_service'], [
+ 'id' => 's.id',
+ 'uuid' => 's.uuid',
+ 'name' => 's.object_name',
+ 'assign_filter' => 's.assign_filter',
+ ])
+ ->where('object_name = ?', $serviceName)
+ ->where('object_type = ? AND assign_filter IS NOT NULL', 'apply');
+
+ $allRules = $db->fetchAll($query);
+ foreach ($allRules as $rule) {
+ $rule->uuid = Uuid::fromBytes(Db\DbUtil::binaryResult($rule->uuid));
+ $rule->filter = Filter::fromQueryString($rule->assign_filter);
+ }
+
+ return $allRules;
+ }
+}
diff --git a/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php b/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php
new file mode 100644
index 0000000..b5785d5
--- /dev/null
+++ b/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Lookup;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\HostApplyMatches;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+
+/**
+ * A Service that makes part of a Service Set Apply Rule matching this Host,
+ * generating a Service with the given name
+ */
+class AppliedServiceSetServiceInfo implements ServiceInfo
+{
+ /** @var string */
+ protected $hostName;
+
+ /** @var string */
+ protected $serviceName;
+
+ /** @var string */
+ protected $serviceSetName;
+
+ /** @var UuidInterface */
+ protected $uuid;
+
+ public function __construct($hostName, $serviceName, $serviceSetName, UuidInterface $uuid)
+ {
+ $this->hostName = $hostName;
+ $this->serviceName = $serviceName;
+ $this->serviceSetName = $serviceSetName;
+ $this->uuid = $uuid;
+ }
+
+ public static function find(IcingaHost $host, $serviceName)
+ {
+ $matcher = HostApplyMatches::prepare($host);
+ $connection = $host->getConnection();
+ foreach (static::fetchServiceSetApplyRulesByServiceName($connection, $host->get('id'), $serviceName) as $rule) {
+ if ($matcher->matchesFilter($rule->filter)) {
+ return new static(
+ $host->getObjectName(),
+ $serviceName,
+ $rule->service_set_name,
+ $rule->uuid
+ );
+ }
+ }
+
+ return null;
+ }
+
+ public function getHostName()
+ {
+ return $this->hostName;
+ }
+
+ public function getUuid()
+ {
+ return $this->uuid;
+ }
+
+ /**
+ * @return string
+ */
+ public function getServiceSetName()
+ {
+ return $this->serviceSetName;
+ }
+
+ public function getName()
+ {
+ return $this->serviceName;
+ }
+
+ public function getUrl()
+ {
+ return Url::fromPath('director/host/servicesetservice', [
+ 'name' => $this->hostName,
+ 'service' => $this->serviceName,
+ 'set' => $this->serviceSetName,
+ ]);
+ }
+
+ public function requiresOverrides()
+ {
+ return true;
+ }
+
+ protected static function fetchServiceSetApplyRulesByServiceName(Db $connection, $hostId, $serviceName)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()
+ ->from(['s' => 'icinga_service'], [
+ 'id' => 's.id',
+ 'uuid' => 'ss.uuid',
+ 'name' => 's.object_name',
+ 'assign_filter' => 'ss.assign_filter',
+ 'service_set_name' => 'ss.object_name',
+ ])
+ ->join(
+ ['ss' => 'icinga_service_set'],
+ 's.service_set_id = ss.id',
+ []
+ )
+ ->where('s.object_name = ?', $serviceName)
+ ->where('ss.assign_filter IS NOT NULL')
+ ->where( // Ignore deactivated Services:
+ 'NOT EXISTS (SELECT 1 FROM icinga_host_service_blacklist hsb'
+ . ' WHERE hsb.host_id = ? AND hsb.service_id = s.id)',
+ (int) $hostId
+ );
+ ;
+
+ $allRules = $db->fetchAll($query);
+ foreach ($allRules as $rule) {
+ $rule->uuid = Uuid::fromBytes(Db\DbUtil::binaryResult($rule->uuid));
+ $rule->filter = Filter::fromQueryString($rule->assign_filter);
+ }
+
+ return $allRules;
+ }
+}
diff --git a/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php b/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php
new file mode 100644
index 0000000..875d5fb
--- /dev/null
+++ b/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Lookup;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use Ramsey\Uuid\UuidInterface;
+
+/**
+ * A Service attached to a parent Service Template. This is a shortcut for
+ * 'assign where "Template Name" in templates'
+ */
+class InheritedServiceInfo implements ServiceInfo
+{
+ /** @var string */
+ protected $hostName;
+
+ /** @var string */
+ protected $hostTemplateName;
+
+ /** @var string */
+ protected $serviceName;
+
+ /** @var UuidInterface */
+ protected $uuid;
+
+ public function __construct($hostName, $hostTemplateName, $serviceName, UuidInterface $uuid)
+ {
+ $this->hostName = $hostName;
+ $this->hostTemplateName = $hostTemplateName;
+ $this->serviceName= $serviceName;
+ $this->uuid = $uuid;
+ }
+
+ public static function find(IcingaHost $host, $serviceName)
+ {
+ $db = $host->getConnection();
+ foreach (IcingaTemplateRepository::instanceByObject($host)->getTemplatesFor($host, true) as $parent) {
+ $key = [
+ 'host_id' => $parent->get('id'),
+ 'object_name' => $serviceName
+ ];
+ if (IcingaService::exists($key, $db)) {
+ return new static(
+ $host->getObjectName(),
+ $parent->getObjectName(),
+ $serviceName,
+ IcingaService::load($key, $db)->getUniqueId()
+ );
+ }
+ }
+
+ return false;
+ }
+
+ public function getHostName()
+ {
+ return $this->hostName;
+ }
+
+ public function getUuid()
+ {
+ return $this->uuid;
+ }
+
+ /**
+ * @return string
+ */
+ public function getHostTemplateName()
+ {
+ return $this->hostTemplateName;
+ }
+
+ public function getName()
+ {
+ return $this->serviceName;
+ }
+
+ public function getUrl()
+ {
+ return Url::fromPath('director/host/inheritedservice', [
+ 'name' => $this->hostName,
+ 'service' => $this->serviceName,
+ 'inheritedFrom' => $this->hostTemplateName
+ ]);
+ }
+
+ public function requiresOverrides()
+ {
+ return true;
+ }
+}
diff --git a/library/Director/DirectorObject/Lookup/ServiceFinder.php b/library/Director/DirectorObject/Lookup/ServiceFinder.php
new file mode 100644
index 0000000..fb8d74c
--- /dev/null
+++ b/library/Director/DirectorObject/Lookup/ServiceFinder.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Lookup;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Objects\HostApplyMatches;
+use Icinga\Module\Director\Objects\IcingaHost;
+use RuntimeException;
+
+class ServiceFinder
+{
+ /** @var IcingaHost */
+ protected $host;
+
+ /** @var ?Auth */
+ protected $auth;
+
+ /** @var IcingaHost[] */
+ protected $parents;
+
+ /** @var HostApplyMatches */
+ protected $applyMatcher;
+
+ /** @var \Icinga\Module\Director\Db */
+ protected $db;
+
+ public function __construct(IcingaHost $host, Auth $auth = null)
+ {
+ $this->host = $host;
+ $this->auth = $auth;
+ $this->db = $host->getConnection();
+ }
+
+ public static function find(IcingaHost $host, $serviceName)
+ {
+ foreach ([
+ SingleServiceInfo::class,
+ InheritedServiceInfo::class,
+ ServiceSetServiceInfo::class,
+ AppliedServiceInfo::class,
+ AppliedServiceSetServiceInfo::class,
+ ] as $class) {
+ /** @var ServiceInfo $class */
+ if ($info = $class::find($host, $serviceName)) {
+ return $info;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $serviceName
+ * @return Url
+ */
+ public function getRedirectionUrl($serviceName)
+ {
+ if ($this->auth === null) {
+ throw new RuntimeException('Auth is required for ServiceFinder when dealing when asking for URLs');
+ }
+ if ($this->auth->hasPermission('director/host')) {
+ if ($info = $this::find($this->host, $serviceName)) {
+ return $info->getUrl();
+ }
+ }
+ if ($this->auth->hasPermission('director/monitoring/services-ro')) {
+ return Url::fromPath('director/host/servicesro', [
+ 'name' => $this->host->getObjectName(),
+ 'service' => $serviceName
+ ]);
+ }
+
+ return Url::fromPath('director/host/invalidservice', [
+ 'name' => $this->host->getObjectName(),
+ 'service' => $serviceName,
+ ]);
+ }
+}
diff --git a/library/Director/DirectorObject/Lookup/ServiceInfo.php b/library/Director/DirectorObject/Lookup/ServiceInfo.php
new file mode 100644
index 0000000..3c8c51b
--- /dev/null
+++ b/library/Director/DirectorObject/Lookup/ServiceInfo.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Lookup;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Ramsey\Uuid\UuidInterface;
+
+interface ServiceInfo
+{
+ /**
+ * The final Service name
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * The host the final (rendered, processed) Service belongs to
+ *
+ * @return string
+ */
+ public function getHostName();
+
+ /**
+ * @return Url
+ */
+ public function getUrl();
+
+ /**
+ * @return UuidInterface
+ */
+ public function getUuid();
+
+ /**
+ * @return bool
+ */
+ public function requiresOverrides();
+
+ /**
+ * @param IcingaHost $host
+ * @param $serviceName
+ * @return ServiceInfo|false
+ */
+ public static function find(IcingaHost $host, $serviceName);
+}
diff --git a/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php b/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php
new file mode 100644
index 0000000..a980da8
--- /dev/null
+++ b/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Lookup;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+
+/**
+ * A service belonging to a Service Set, attached either directly to the given
+ * Host or to one of it's inherited Host Templates
+ */
+class ServiceSetServiceInfo implements ServiceInfo
+{
+ /** @var string */
+ protected $hostName;
+
+ /** @var string */
+ protected $serviceName;
+
+ /** @var string */
+ protected $serviceSetName;
+
+ /** @var UuidInterface */
+ protected $uuid;
+
+ public function __construct($hostName, $serviceName, $serviceSetName, UuidInterface $uuid)
+ {
+ $this->hostName = $hostName;
+ $this->serviceName = $serviceName;
+ $this->serviceSetName = $serviceSetName;
+ $this->uuid = $uuid;
+ }
+
+ public static function find(IcingaHost $host, $serviceName)
+ {
+ $ids = [$host->get('id')];
+
+ foreach (IcingaTemplateRepository::instanceByObject($host)->getTemplatesFor($host, true) as $parent) {
+ $ids[] = $parent->get('id');
+ }
+
+ $db = $host->getConnection()->getDbAdapter();
+ $query = $db->select()
+ ->from(
+ ['s' => 'icinga_service'],
+ [
+ 'service_set_name' => 'ss.object_name',
+ 'uuid' => 's.uuid',
+ ]
+ )->join(
+ ['ss' => 'icinga_service_set'],
+ 's.service_set_id = ss.id',
+ []
+ )->join(
+ ['hsi' => 'icinga_service_set_inheritance'],
+ 'hsi.parent_service_set_id = ss.id',
+ []
+ )->join(
+ ['hs' => 'icinga_service_set'],
+ 'hs.id = hsi.service_set_id',
+ []
+ )->where('hs.host_id IN (?)', $ids)
+ ->where('s.object_name = ?', $serviceName)
+ ->where( // Ignore deactivated Services:
+ 'NOT EXISTS (SELECT 1 FROM icinga_host_service_blacklist hsb'
+ . ' WHERE hsb.host_id = ? AND hsb.service_id = s.id)',
+ (int) $host->get('id')
+ );
+
+ if ($row = $db->fetchRow($query)) {
+ return new static(
+ $host->getObjectName(),
+ $serviceName,
+ $row->service_set_name,
+ Uuid::fromBytes($row->uuid)
+ );
+ }
+
+ return null;
+ }
+
+ public function getHostName()
+ {
+ return $this->hostName;
+ }
+
+ public function getName()
+ {
+ return $this->serviceName;
+ }
+
+ public function getUuid()
+ {
+ return $this->uuid;
+ }
+
+ /**
+ * @return string
+ */
+ public function getServiceSetName()
+ {
+ return $this->serviceSetName;
+ }
+
+ public function getUrl()
+ {
+ return Url::fromPath('director/host/servicesetservice', [
+ 'name' => $this->hostName,
+ 'service' => $this->serviceName,
+ 'set' => $this->serviceSetName,
+ ]);
+ }
+
+ public function requiresOverrides()
+ {
+ return true;
+ }
+}
diff --git a/library/Director/DirectorObject/Lookup/SingleServiceInfo.php b/library/Director/DirectorObject/Lookup/SingleServiceInfo.php
new file mode 100644
index 0000000..af54fc7
--- /dev/null
+++ b/library/Director/DirectorObject/Lookup/SingleServiceInfo.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Lookup;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Ramsey\Uuid\UuidInterface;
+
+/**
+ * A single service, directly attached to a Host Object. Overrides might
+ * still be used when use_var_overrides is true.
+ */
+class SingleServiceInfo implements ServiceInfo
+{
+ /** @var string */
+ protected $hostName;
+
+ /** @var string */
+ protected $serviceName;
+
+ /** @var bool */
+ protected $useOverrides;
+
+ /** @var UuidInterface */
+ protected $uuid;
+
+ public function __construct($hostName, $serviceName, UuidInterface $uuid, $useOverrides)
+ {
+ $this->hostName = $hostName;
+ $this->serviceName = $serviceName;
+ $this->useOverrides = $useOverrides;
+ $this->uuid = $uuid;
+ }
+
+ public static function find(IcingaHost $host, $serviceName)
+ {
+ $keyParams = [
+ 'host_id' => $host->get('id'),
+ 'object_name' => $serviceName
+ ];
+ $connection = $host->getConnection();
+ if (IcingaService::exists($keyParams, $connection)) {
+ $service = IcingaService::load($keyParams, $connection);
+ $useOverrides = $service->getResolvedVar('use_var_overrides') === 'y';
+
+ return new static($host->getObjectName(), $serviceName, $service->getUniqueId(), $useOverrides);
+ }
+
+ return false;
+ }
+
+ public function getHostName()
+ {
+ return $this->hostName;
+ }
+
+ public function getName()
+ {
+ return $this->serviceName;
+ }
+
+ /**
+ * @return UuidInterface
+ */
+ public function getUuid()
+ {
+ return $this->uuid;
+ }
+
+ public function getUrl()
+ {
+ return Url::fromPath('director/service/edit', [
+ 'host' => $this->hostName,
+ 'name' => $this->serviceName,
+ ]);
+ }
+
+ public function requiresOverrides()
+ {
+ return $this->useOverrides;
+ }
+}
diff --git a/library/Director/DirectorObject/ObjectPurgeHelper.php b/library/Director/DirectorObject/ObjectPurgeHelper.php
new file mode 100644
index 0000000..a043965
--- /dev/null
+++ b/library/Director/DirectorObject/ObjectPurgeHelper.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+use InvalidArgumentException;
+
+class ObjectPurgeHelper
+{
+ protected $db;
+
+ protected $force = false;
+
+ public function __construct(Db $db)
+ {
+ $this->db = $db;
+ }
+
+ public function force($force = true)
+ {
+ $this->force = $force;
+ return $this;
+ }
+
+ public function purge(array $keep, $class, $objectType = null)
+ {
+ if (empty($keep) && ! $this->force) {
+ throw new InvalidArgumentException('I will NOT purge all object unless being forced to do so');
+ }
+ $db = $this->db->getDbAdapter();
+ /** @var IcingaObject $class cheating, it's a class name, not an object */
+ $dummy = $class::create();
+ assert($dummy instanceof IcingaObject);
+ $keyCols = (array) $dummy->getKeyName();
+ if ($objectType !== null) {
+ $keyCols[] = 'object_type';
+ }
+
+ $keepKeys = [];
+ foreach ($keep as $object) {
+ if ($object instanceof \stdClass) {
+ $properties = (array) $object;
+ // TODO: this is object-specific and to be found in the ::import() function!
+ unset($properties['fields']);
+ $object = $class::fromPlainObject($properties);
+ } elseif (\get_class($object) !== $class) {
+ throw new InvalidArgumentException(
+ 'Can keep only matching objects, expected "%s", got "%s',
+ $class,
+ \get_class($keep)
+ );
+ }
+ $key = [];
+ foreach ($keyCols as $col) {
+ $key[$col] = $object->get($col);
+ }
+ $keepKeys[$this->makeRowKey($key)] = true;
+ }
+
+ $query = $db->select()->from(['o' => $dummy->getTableName()], $keyCols);
+ if ($objectType !== null) {
+ $query->where('object_type = ?', $objectType);
+ }
+ $allExisting = [];
+ foreach ($db->fetchAll($query) as $row) {
+ $allExisting[$this->makeRowKey($row)] = $row;
+ }
+ $remove = [];
+ foreach ($allExisting as $key => $keyProperties) {
+ if (! isset($keepKeys[$key])) {
+ $remove[] = $keyProperties;
+ }
+ }
+ $db->beginTransaction();
+ foreach ($remove as $keyProperties) {
+ $keyColumn = $class::getKeyColumnName();
+ if (is_array($keyColumn)) {
+ $object = $class::load((array) $keyProperties, $this->db);
+ } else {
+ $object = $class::load($keyProperties->$keyColumn, $this->db);
+ }
+ $object->delete();
+ }
+ $db->commit();
+ }
+
+ public static function listObjectTypesAvailableForPurge()
+ {
+ return [
+ 'Basket',
+ 'Command',
+ 'CommandTemplate',
+ 'Dependency',
+ 'DirectorJob',
+ 'ExternalCommand',
+ 'HostGroup',
+ 'HostTemplate',
+ 'IcingaTemplateChoiceHost',
+ 'IcingaTemplateChoiceService',
+ 'ImportSource',
+ 'Notification',
+ 'NotificationTemplate',
+ 'ServiceGroup',
+ 'ServiceSet',
+ 'ServiceTemplate',
+ 'SyncRule',
+ 'TimePeriod',
+ ];
+ }
+
+ public static function objectTypeIsEligibleForPurge($type)
+ {
+ return in_array($type, static::listObjectTypesAvailableForPurge(), true);
+ }
+
+ public static function assertObjectTypesAreEligibleForPurge($types)
+ {
+ $invalid = [];
+ foreach ($types as $type) {
+ if (! static::objectTypeIsEligibleForPurge($type)) {
+ $invalid[] = $type;
+ }
+ }
+
+ if (empty($invalid)) {
+ return;
+ }
+
+ if (count($invalid) === 1) {
+ $message = sprintf('"%s" is not eligible for purge', $invalid[0]);
+ } else {
+ $message = 'The following types are not eligible for purge: '
+ . implode(', ', $invalid);
+ }
+
+ throw new InvalidArgumentException(
+ "$message. Valid types: "
+ . implode(', ', static::listObjectTypesAvailableForPurge())
+ );
+ }
+
+ protected function makeRowKey($row)
+ {
+ $row = (array) $row;
+ ksort($row);
+ return json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ }
+}
diff --git a/library/Director/Exception/DuplicateKeyException.php b/library/Director/Exception/DuplicateKeyException.php
new file mode 100644
index 0000000..a9cba65
--- /dev/null
+++ b/library/Director/Exception/DuplicateKeyException.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Exception;
+
+use Icinga\Exception\IcingaException;
+
+class DuplicateKeyException extends IcingaException
+{
+}
diff --git a/library/Director/Exception/JsonEncodeException.php b/library/Director/Exception/JsonEncodeException.php
new file mode 100644
index 0000000..7db2f77
--- /dev/null
+++ b/library/Director/Exception/JsonEncodeException.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Icinga\Module\Director\Exception;
+
+class JsonEncodeException extends JsonException
+{
+}
diff --git a/library/Director/Exception/JsonException.php b/library/Director/Exception/JsonException.php
new file mode 100644
index 0000000..dad848d
--- /dev/null
+++ b/library/Director/Exception/JsonException.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Icinga\Module\Director\Exception;
+
+use Icinga\Exception\IcingaException;
+
+class JsonException extends IcingaException
+{
+ public static function forLastJsonError($msg = null)
+ {
+ if ($msg === null) {
+ return new static(static::getJsonErrorMessage(\json_last_error()));
+ } else {
+ return new static($msg . ': ' . static::getJsonErrorMessage(\json_last_error()));
+ }
+ }
+
+ public static function getJsonErrorMessage($code)
+ {
+ $map = [
+ JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded',
+ JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
+ JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
+ JSON_ERROR_SYNTAX => 'JSON Syntax error',
+ JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded'
+ ];
+ if (\array_key_exists($code, $map)) {
+ return $map[$code];
+ }
+
+ if (PHP_VERSION_ID >= 50500) {
+ $map = [
+ JSON_ERROR_RECURSION => 'One or more recursive references in the value to be encoded',
+ JSON_ERROR_INF_OR_NAN => 'One or more NAN or INF values in the value to be encoded',
+ JSON_ERROR_UNSUPPORTED_TYPE => 'A value of a type that cannot be encoded was given',
+ ];
+ if (\array_key_exists($code, $map)) {
+ return $map[$code];
+ }
+ }
+
+ if (PHP_VERSION_ID >= 70000) {
+ $map = [
+ JSON_ERROR_INVALID_PROPERTY_NAME => 'A property name that cannot be encoded was given',
+ JSON_ERROR_UTF16 => 'Malformed UTF-16 characters, possibly incorrectly encoded',
+ ];
+
+ if (\array_key_exists($code, $map)) {
+ return $map[$code];
+ }
+ }
+
+ return 'An error occured when parsing a JSON string';
+ }
+}
diff --git a/library/Director/Exception/NestingError.php b/library/Director/Exception/NestingError.php
new file mode 100644
index 0000000..bc191aa
--- /dev/null
+++ b/library/Director/Exception/NestingError.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Exception;
+
+use Icinga\Exception\IcingaException;
+
+class NestingError extends IcingaException
+{
+}
diff --git a/library/Director/Field/FieldSpec.php b/library/Director/Field/FieldSpec.php
new file mode 100644
index 0000000..29baa0b
--- /dev/null
+++ b/library/Director/Field/FieldSpec.php
@@ -0,0 +1,206 @@
+<?php
+
+
+namespace Icinga\Module\Director\Field;
+
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class FieldSpec
+{
+ /** @var string */
+ protected $varName;
+
+ /** @var string */
+ protected $category;
+
+ /** @var string */
+ protected $caption;
+
+ /** @var boolean */
+ protected $isRequired = false;
+
+ /** @var string */
+ protected $description;
+
+ /** @var string */
+ protected $dataType;
+
+ /** @var string */
+ protected $varFilter;
+
+ /** @var string */
+ protected $format = "string";
+
+ /**
+ * FieldSpec constructor.
+ * @param $dataType
+ * @param $varName
+ * @param $caption
+ */
+ public function __construct($dataType, $varName, $caption)
+ {
+ $this->dataType = $dataType;
+ $this->varName = $varName;
+ $this->caption = $caption;
+ }
+
+ public function toDataField(IcingaObject $object)
+ {
+ return DirectorDatafield::create([
+ 'varname' => $this->getVarName(),
+ 'category' => $this->getCategory(),
+ 'caption' => $this->getCaption(),
+ 'description' => $this->getDescription(),
+ 'datatype' => $this->getDataType(),
+ 'format' => $this->getFormat(),
+ 'var_filter' => $this->getVarFilter(),
+ 'icinga_type' => $object->getShortTableName(),
+ 'object_id' => $object->get('id'),
+ ]);
+ }
+
+ /**
+ * @return string
+ */
+ public function getVarName()
+ {
+ return $this->varName;
+ }
+
+ /**
+ * @param string $varName
+ * @return FieldSpec
+ */
+ public function setVarName($varName)
+ {
+ $this->varName = $varName;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCaption()
+ {
+ return $this->caption;
+ }
+
+ /**
+ * @param string $caption
+ * @return FieldSpec
+ */
+ public function setCaption($caption)
+ {
+ $this->caption = $caption;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isRequired()
+ {
+ return $this->isRequired;
+ }
+
+ /**
+ * @param bool $isRequired
+ * @return FieldSpec
+ */
+ public function setIsRequired($isRequired)
+ {
+ $this->isRequired = $isRequired;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @param string $description
+ * @return FieldSpec
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDataType()
+ {
+ return $this->dataType;
+ }
+
+ /**
+ * @param string $dataType
+ * @return FieldSpec
+ */
+ public function setDataType($dataType)
+ {
+ $this->dataType = $dataType;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getVarFilter()
+ {
+ return $this->varFilter;
+ }
+
+ /**
+ * @param string $varFilter
+ * @return FieldSpec
+ */
+ public function setVarFilter($varFilter)
+ {
+ $this->varFilter = $varFilter;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFormat()
+ {
+ return $this->format;
+ }
+
+ /**
+ * @param string $format
+ * @return FieldSpec
+ */
+ public function setFormat($format)
+ {
+ $this->format = $format;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCategory()
+ {
+ return $this->category;
+ }
+
+ /**
+ * @param string $category
+ * @return FieldSpec
+ */
+ public function setCategory($category)
+ {
+ $this->category = $category;
+ return $this;
+ }
+}
diff --git a/library/Director/Health.php b/library/Director/Health.php
new file mode 100644
index 0000000..0d85d18
--- /dev/null
+++ b/library/Director/Health.php
@@ -0,0 +1,285 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+use Icinga\Application\Config;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Director\CheckPlugin\Check;
+use Icinga\Module\Director\CheckPlugin\CheckResults;
+use Icinga\Module\Director\Db\Migrations;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\SyncRule;
+use Exception;
+
+class Health
+{
+ /** @var Db */
+ protected $connection;
+
+ /** @var string */
+ protected $dbResourceName;
+
+ protected $checks = [
+ 'config' => 'checkConfig',
+ 'sync' => 'checkSyncRules',
+ 'import' => 'checkImportSources',
+ 'jobs' => 'checkDirectorJobs',
+ 'deployment' => 'checkDeployments',
+ ];
+
+ public function setDbResourceName($name)
+ {
+ $this->dbResourceName = $name;
+
+ return $this;
+ }
+
+ public function getCheck($name)
+ {
+ if (array_key_exists($name, $this->checks)) {
+ $func = $this->checks[$name];
+ $check = $this->$func();
+ } else {
+ $check = new CheckResults('Invalid Parameter');
+ $check->fail("There is no check named '$name'");
+ }
+
+ return $check;
+ }
+
+ public function getAllChecks()
+ {
+ /** @var CheckResults[] $checks */
+ $checks = [$this->checkConfig()];
+
+ if ($checks[0]->hasErrors()) {
+ return $checks;
+ }
+
+ $checks[] = $this->checkDeployments();
+ $checks[] = $this->checkImportSources();
+ $checks[] = $this->checkSyncRules();
+ $checks[] = $this->checkDirectorJobs();
+
+ return $checks;
+ }
+
+ protected function hasDeploymentEndpoint()
+ {
+ try {
+ return $this->connection->hasDeploymentEndpoint();
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ public function hasResourceConfig()
+ {
+ return $this->getDbResourceName() !== null;
+ }
+
+ protected function getDbResourceName()
+ {
+ if ($this->dbResourceName === null) {
+ $this->dbResourceName = Config::module('director')->get('db', 'resource');
+ }
+
+ return $this->dbResourceName;
+ }
+
+ protected function getConnection()
+ {
+ if ($this->connection === null) {
+ $this->connection = Db::fromResourceName($this->getDbResourceName());
+ }
+
+ return $this->connection;
+ }
+
+ public function checkConfig()
+ {
+ $check = new Check('Director configuration');
+ $name = $this->getDbResourceName();
+ if ($name) {
+ $check->succeed("Database resource '$name' has been specified");
+ } else {
+ return $check->fail('No database resource has been specified');
+ }
+
+ try {
+ $db = $this->getConnection();
+ } catch (Exception $e) {
+ return $check->fail($e);
+ }
+
+ $migrations = new Migrations($db);
+ $check->assertTrue(
+ [$migrations, 'hasSchema'],
+ 'Make sure the DB schema exists'
+ );
+
+ if ($check->hasProblems()) {
+ return $check;
+ }
+
+ $check->call(function () use ($check, $migrations) {
+ $count = $migrations->countPendingMigrations();
+
+ if ($count === 0) {
+ $check->succeed('There are no pending schema migrations');
+ } elseif ($count === 1) {
+ $check->warn('There is a pending schema migration');
+ } else {
+ $check->warn(sprintf(
+ 'There are %s pending schema migrations',
+ $count
+ ));
+ }
+ });
+
+ return $check;
+ }
+
+ public function checkSyncRules()
+ {
+ $check = new CheckResults('Sync Rules');
+ $rules = SyncRule::loadAll($this->getConnection(), null, 'rule_name');
+ if (empty($rules)) {
+ $check->succeed('No Sync Rules have been defined');
+ return $check;
+ }
+ ksort($rules);
+
+ foreach ($rules as $rule) {
+ $state = $rule->get('sync_state');
+ $name = $rule->get('rule_name');
+ if ($state === 'failing') {
+ $message = $rule->get('last_error_message');
+ $check->fail("'$name' is failing: $message");
+ } elseif ($state === 'pending-changes') {
+ $check->succeed("'$name' is fine, but there are pending changes");
+ } elseif ($state === 'in-sync') {
+ $check->succeed("'$name' is in sync");
+ } else {
+ $check->fail("'$name' has never been checked", 'UNKNOWN');
+ }
+ }
+
+ return $check;
+ }
+
+ public function checkImportSources()
+ {
+ $check = new CheckResults('Import Sources');
+ $sources = ImportSource::loadAll($this->getConnection(), null, 'source_name');
+ if (empty($sources)) {
+ $check->succeed('No Import Sources have been defined');
+ return $check;
+ }
+
+ ksort($sources);
+ foreach ($sources as $src) {
+ $state = $src->get('import_state');
+ $name = $src->get('source_name');
+ if ($state === 'failing') {
+ $message = $src->get('last_error_message');
+ $check->fail("'$name' is failing: $message");
+ } elseif ($state === 'pending-changes') {
+ $check->succeed("'$name' is fine, but there are pending changes");
+ } elseif ($state === 'in-sync') {
+ $check->succeed("'$name' is in sync");
+ } else {
+ $check->fail("'$name' has never been checked", 'UNKNOWN');
+ }
+ }
+
+ return $check;
+ }
+
+ public function checkDirectorJobs()
+ {
+ $check = new CheckResults('Director Jobs');
+ $jobs = DirectorJob::loadAll($this->getConnection(), null, 'job_name');
+ if (empty($jobs)) {
+ $check->succeed('No Jobs have been defined');
+ return $check;
+ }
+ ksort($jobs);
+
+ foreach ($jobs as $job) {
+ $name = $job->get('job_name');
+ if ($job->hasBeenDisabled()) {
+ $check->succeed("'$name' has been disabled");
+ } elseif ($job->lastAttemptFailed()) {
+ $message = $job->get('last_error_message');
+ $check->fail("Last attempt for '$name' failed: $message");
+ } elseif ($job->isOverdue()) {
+ $check->fail("'$name' is overdue");
+ } elseif ($job->shouldRun()) {
+ $check->succeed("'$name' is fine, but should run now");
+ } else {
+ $check->succeed("'$name' is fine");
+ }
+ }
+
+ return $check;
+ }
+
+ public function checkDeployments()
+ {
+ $check = new Check('Director Deployments');
+
+ $db = $this->getConnection();
+
+ $check->call(function () use ($check, $db) {
+ $check->succeed(sprintf(
+ "Deployment endpoint is '%s'",
+ $db->getDeploymentEndpointName()
+ ));
+ })->call(function () use ($check, $db) {
+ $count = $db->countActivitiesSinceLastDeployedConfig();
+
+ if ($count === 1) {
+ $check->succeed('There is a single un-deployed change');
+ } else {
+ $check->succeed(sprintf(
+ 'There are %d un-deployed changes',
+ $count
+ ));
+ }
+ });
+
+ if (! DirectorDeploymentLog::hasDeployments($db)) {
+ $check->warn('Configuration has never been deployed');
+ return $check;
+ }
+
+ $latest = DirectorDeploymentLog::loadLatest($db);
+
+ $ts = $latest->getDeploymentTimestamp();
+ $time = DateFormatter::timeAgo($ts);
+ if ($latest->succeeded()) {
+ $check->succeed("The last Deployment was successful $time");
+ } elseif ($latest->isPending()) {
+ if ($ts + 180 < time()) {
+ $check->warn("The last Deployment started $time and is still pending");
+ } else {
+ $check->succeed("The last Deployment started $time and is still pending");
+ }
+ } else {
+ $check->fail("The last Deployment failed $time");
+ }
+
+ return $check;
+ }
+
+ public function __destruct()
+ {
+ if ($this->connection !== null) {
+ // We created our own connection, so let's tear it down
+ $this->connection->getDbAdapter()->closeConnection();
+ }
+ }
+}
diff --git a/library/Director/Hook/BranchSupportHook.php b/library/Director/Hook/BranchSupportHook.php
new file mode 100644
index 0000000..6615cbe
--- /dev/null
+++ b/library/Director/Hook/BranchSupportHook.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Dashboard\Dashlet\Dashlet;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchSTore;
+use Icinga\Web\Request;
+use ipl\Html\ValidHtml;
+
+abstract class BranchSupportHook
+{
+ /**
+ * @param Request $request
+ * @param BranchSTore $store
+ * @param Auth $auth
+ * @return Branch
+ */
+ abstract public function getBranchForRequest(Request $request, BranchStore $store, Auth $auth);
+
+ /**
+ * @param Branch $branch
+ * @param Auth $auth
+ * @param ?string $label
+ * @return ?ValidHtml
+ */
+ abstract public function linkToBranch(Branch $branch, Auth $auth, $label = null);
+
+ /**
+ * @param Db $db
+ * @return Dashlet[]
+ */
+ public function loadDashlets(Db $db)
+ {
+ return [];
+ }
+}
diff --git a/library/Director/Hook/DataTypeHook.php b/library/Director/Hook/DataTypeHook.php
new file mode 100644
index 0000000..1eb58d6
--- /dev/null
+++ b/library/Director/Hook/DataTypeHook.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+abstract class DataTypeHook
+{
+ protected $settings = array();
+
+ public function getName()
+ {
+ $parts = explode('\\', get_class($this));
+ $class = preg_replace('/DataType/', '', array_pop($parts));
+
+ if (array_shift($parts) === 'Icinga' && array_shift($parts) === 'Module') {
+ $module = array_shift($parts);
+ if ($module !== 'Director') {
+ return sprintf('%s (%s)', $class, $module);
+ }
+ }
+
+ return $class;
+ }
+
+ public static function getFormat()
+ {
+ return 'string';
+ }
+
+ /**
+ * @param $name
+ * @param QuickForm|DirectorObjectForm $form
+ *
+ * @return \Zend_Form_Element
+ */
+ abstract public function getFormElement($name, QuickForm $form);
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ return $form;
+ }
+
+ public function setSettings($settings)
+ {
+ $this->settings = $settings;
+ return $this;
+ }
+
+ public function getSetting($name, $default = null)
+ {
+ if (array_key_exists($name, $this->settings)) {
+ return $this->settings[$name];
+ } else {
+ return $default;
+ }
+ }
+}
diff --git a/library/Director/Hook/DeploymentHook.php b/library/Director/Hook/DeploymentHook.php
new file mode 100644
index 0000000..c8a834b
--- /dev/null
+++ b/library/Director/Hook/DeploymentHook.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+
+abstract class DeploymentHook
+{
+ /**
+ * Please override this method if you want to change the behaviour
+ * of the deploy (stop it by throwing an exception for some reason)
+ *
+ * @param DirectorDeploymentLog $deployment
+ */
+ public function beforeDeploy(DirectorDeploymentLog $deployment)
+ {
+ }
+
+ /**
+ * Please override this method if you want to trigger custom actions
+ * on a successful dump of the Icinga configuration
+ *
+ * @param DirectorDeploymentLog $deployment
+ */
+ public function onSuccessfulDump(DirectorDeploymentLog $deployment)
+ {
+ }
+
+ /**
+ * There is a typo in this method name, do not use this.
+ *
+ * @deprecated Please use onSuccessfulDump
+ * @param DirectorDeploymentLog $deployment
+ */
+ public function onSuccessfullDump(DirectorDeploymentLog $deployment)
+ {
+ }
+
+ /**
+ * Compatibility helper
+ *
+ * The initial version of this hook had a typo in the onSuccessfulDump method
+ * That's why we call this hook, which then calls both the correct and the
+ * erroneous method to make sure that we do not break existing implementations.
+ *
+ * @param DirectorDeploymentLog $deploymentLog
+ */
+ final public function triggerSuccessfulDump(DirectorDeploymentLog $deploymentLog)
+ {
+ $this->onSuccessfulDump($deploymentLog);
+ $this->onSuccessfullDump($deploymentLog);
+ }
+
+ /**
+ * Please override this method if you want to trigger custom actions
+ * once success (or failure) information have been collected for a deployed
+ * stage. startup_succeeded will then be filled, and startup_log might be
+ * available
+ *
+ * @param DirectorDeploymentLog $deployment
+ */
+ public function onCollect(DirectorDeploymentLog $deployment)
+ {
+ }
+}
diff --git a/library/Director/Hook/HostFieldHook.php b/library/Director/Hook/HostFieldHook.php
new file mode 100644
index 0000000..c0199d0
--- /dev/null
+++ b/library/Director/Hook/HostFieldHook.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+use Icinga\Module\Director\Field\FieldSpec;
+use Icinga\Module\Director\Objects\IcingaHost;
+
+abstract class HostFieldHook
+{
+ public function wants(IcingaHost $host)
+ {
+ return true;
+ }
+
+ /**
+ * @return FieldSpec
+ */
+ abstract public function getFieldSpec(IcingaHost $host);
+}
diff --git a/library/Director/Hook/IcingaObjectFormHook.php b/library/Director/Hook/IcingaObjectFormHook.php
new file mode 100644
index 0000000..1d20ee1
--- /dev/null
+++ b/library/Director/Hook/IcingaObjectFormHook.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Web\Hook;
+
+abstract class IcingaObjectFormHook
+{
+ protected $settings = [];
+
+ abstract public function onSetup(DirectorObjectForm $form);
+
+ public static function callOnSetup(DirectorObjectForm $form)
+ {
+ /** @var static[] $implementations */
+ $implementations = Hook::all('director/IcingaObjectForm');
+ foreach ($implementations as $implementation) {
+ $implementation->onSetup($form);
+ }
+ }
+}
diff --git a/library/Director/Hook/ImportSourceHook.php b/library/Director/Hook/ImportSourceHook.php
new file mode 100644
index 0000000..dba1c87
--- /dev/null
+++ b/library/Director/Hook/ImportSourceHook.php
@@ -0,0 +1,136 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Icinga\Module\Director\Db;
+use Icinga\Exception\ConfigurationError;
+
+abstract class ImportSourceHook
+{
+ protected $settings = [];
+
+ public function getName()
+ {
+ $parts = explode('\\', get_class($this));
+ $class = preg_replace('/ImportSource/', '', array_pop($parts));
+
+ if (array_shift($parts) === 'Icinga' && array_shift($parts) === 'Module') {
+ $module = array_shift($parts);
+ if ($module !== 'Director') {
+ if ($class === '') {
+ return sprintf('%s module', $module);
+ }
+ return sprintf('%s (%s)', $class, $module);
+ }
+ }
+
+ return $class;
+ }
+
+ public static function forImportSource(ImportSource $source)
+ {
+ $db = $source->getDb();
+ $settings = $db->fetchPairs(
+ $db->select()->from(
+ 'import_source_setting',
+ ['setting_name', 'setting_value']
+ )->where('source_id = ?', $source->get('id'))
+ );
+
+ $className = $source->get('provider_class');
+ if (! class_exists($className)) {
+ throw new ConfigurationError(
+ 'Cannot load import provider class %s',
+ $className
+ );
+ }
+
+ /** @var ImportSourceHook $obj */
+ $obj = new $className;
+ $obj->setSettings($settings);
+ return $obj;
+ }
+
+ public static function loadByName($name, Db $db)
+ {
+ $db = $db->getDbAdapter();
+ $source = $db->fetchRow(
+ $db->select()->from(
+ 'import_source',
+ array('id', 'provider_class')
+ )->where('source_name = ?', $name)
+ );
+
+ $settings = $db->fetchPairs(
+ $db->select()->from(
+ 'import_source_setting',
+ array('setting_name', 'setting_value')
+ )->where('source_id = ?', $source->id)
+ );
+
+ if (! class_exists($source->provider_class)) {
+ throw new ConfigurationError(
+ 'Cannot load import provider class %s',
+ $source->provider_class
+ );
+ }
+ /** @var ImportSourceHook $obj */
+ $obj = new $source->provider_class;
+ $obj->setSettings($settings);
+
+ return $obj;
+ }
+
+ public function setSettings($settings)
+ {
+ $this->settings = $settings;
+ return $this;
+ }
+
+ public function getSetting($name, $default = null)
+ {
+ if (array_key_exists($name, $this->settings)) {
+ return $this->settings[$name];
+ } else {
+ return $default;
+ }
+ }
+
+ /**
+ * Returns an array containing importable objects
+ *
+ * @return array
+ */
+ abstract public function fetchData();
+
+ /**
+ * Returns a list of all available columns
+ *
+ * @return array
+ */
+ abstract public function listColumns();
+
+ /**
+ * Override this method in case you want to suggest a default
+ * key column
+ *
+ * @return string|null Default key column
+ */
+ public static function getDefaultKeyColumnName()
+ {
+ return null;
+ }
+
+ /**
+ * Override this method if you want to extend the settings form
+ *
+ * @param QuickForm $form QuickForm that should be extended
+ * @return QuickForm
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ return $form;
+ }
+}
diff --git a/library/Director/Hook/JobHook.php b/library/Director/Hook/JobHook.php
new file mode 100644
index 0000000..d9a81a9
--- /dev/null
+++ b/library/Director/Hook/JobHook.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+abstract class JobHook
+{
+ /** @var Db */
+ private $db;
+
+ /** @var DirectorJob */
+ private $jobDefinition;
+
+ public static function getDescription(QuickForm $form)
+ {
+ return false;
+ }
+
+ abstract public function run();
+
+ public function isPending()
+ {
+ // TODO: Can be overridden, double-check whether this is necessary
+ }
+
+ public function setDefinition(DirectorJob $definition)
+ {
+ $this->jobDefinition = $definition;
+ return $this;
+ }
+
+ protected function getSetting($key, $default = null)
+ {
+ return $this->jobDefinition->getSetting($key, $default);
+ }
+
+ public function getName()
+ {
+ $parts = explode('\\', get_class($this));
+ $class = preg_replace('/Job$/', '', array_pop($parts));
+
+ if (array_shift($parts) === 'Icinga' && array_shift($parts) === 'Module') {
+ $module = array_shift($parts);
+ if ($module !== 'Director') {
+ return sprintf('%s (%s)', $class, $module);
+ }
+ }
+
+ return $class;
+ }
+
+ public function exportSettings()
+ {
+ return $this->jobDefinition->getSettings();
+ }
+
+ public static function getSuggestedRunInterval(QuickForm $form)
+ {
+ return 900;
+ }
+
+ /**
+ * Override this method if you want to extend the settings form
+ *
+ * @param QuickForm $form QuickForm that should be extended
+ * @return QuickForm
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ return $form;
+ }
+
+ public function setDb(Db $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ protected function db()
+ {
+ return $this->db;
+ }
+}
diff --git a/library/Director/Hook/PropertyModifierHook.php b/library/Director/Hook/PropertyModifierHook.php
new file mode 100644
index 0000000..5d8736d
--- /dev/null
+++ b/library/Director/Hook/PropertyModifierHook.php
@@ -0,0 +1,258 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Icinga\Module\Director\Db;
+
+abstract class PropertyModifierHook
+{
+ /** @var array */
+ protected $settings = [];
+
+ /** @var string */
+ private $targetProperty;
+
+ /** @var string */
+ private $propertyName;
+
+ /** @var Db */
+ private $db;
+
+ /** @var bool */
+ private $rejected = false;
+
+ /** @var \stdClass */
+ private $row;
+
+ /**
+ * Methode to transform the given value
+ *
+ * Your custom property modifier needs to implement this method.
+ *
+ * @return mixed $value
+ */
+ abstract public function transform($value);
+
+ public function getName()
+ {
+ $parts = explode('\\', get_class($this));
+ $class = preg_replace('/^PropertyModifier/', '', array_pop($parts)); // right?
+
+ if (array_shift($parts) === 'Icinga' && array_shift($parts) === 'Module') {
+ $module = array_shift($parts);
+ if ($module !== 'Director') {
+ return sprintf('%s (%s)', $class, $module);
+ }
+ }
+
+ return $class;
+ }
+
+ /**
+ * Whether this PropertyModifier wants to deal with array on it's own
+ *
+ * When true, the whole array value will be passed to transform(), otherwise
+ * transform() will be called for every single array member
+ *
+ * @return bool
+ */
+ public function hasArraySupport()
+ {
+ return false;
+ }
+
+ /**
+ * This creates one cloned row for every entry of the result array
+ *
+ * When set to true and given that the property modifier returns an Array,
+ * the current row will be cloned for every entry of that array. The modified
+ * property will then be replace each time accordingly. An empty Array
+ * completely removes the corrent row.
+ *
+ * @return bool
+ */
+ public function expandsRows()
+ {
+ return false;
+ }
+
+ /**
+ * Reject this whole row
+ *
+ * Allows your property modifier to reject specific rows
+ *
+ * @param bool $reject
+ * @return $this
+ */
+ public function rejectRow($reject = true)
+ {
+ $this->rejected = (bool) $reject;
+
+ return $this;
+ }
+
+ /**
+ * Whether this PropertyModifier wants access to the current row
+ *
+ * When true, the your modifier can access the current row via $this->getRow()
+ *
+ * @return bool
+ */
+ public function requiresRow()
+ {
+ return false;
+ }
+
+ /**
+ * Whether this modifier wants to reject the current row
+ *
+ * @return bool
+ */
+ public function rejectsRow()
+ {
+ return $this->rejected;
+ }
+
+ /**
+ * Get the current row
+ *
+ * Will be null when requiresRow was not null. Please do not modify the
+ * row. It might work right now, as we pass in an object reference for
+ * performance reasons. However, modifying row properties is not supported,
+ * and the outcome of such operation might change without pre-announcement
+ * in any future version.
+ *
+ * @return \stdClass|null
+ */
+ public function getRow()
+ {
+ return $this->row;
+ }
+
+ /**
+ * Sets the current row
+ *
+ * Please see requiresRow/getRow for related details. This method is called
+ * by the Import implementation, you should never need to call this on your
+ * own - apart from writing tests of course.
+ *
+ * @param \stdClass $row
+ * @return $this
+ */
+ public function setRow($row)
+ {
+ $this->row = $row;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPropertyName()
+ {
+ return $this->propertyName;
+ }
+
+ /**
+ * @param string $propertyName
+ * @return $this
+ */
+ public function setPropertyName($propertyName)
+ {
+ $this->propertyName = $propertyName;
+ return $this;
+ }
+
+ /**
+ * The desired target property. Modifiers might want to have their outcome
+ * written to another property of the current row.
+ *
+ * @param $property
+ * @return $this
+ */
+ public function setTargetProperty($property)
+ {
+ $this->targetProperty = $property;
+ return $this;
+ }
+
+ /**
+ * Whether the result of transform() should be written to a new property
+ *
+ * The Import implementation deals with this
+ *
+ * @return bool
+ */
+ public function hasTargetProperty()
+ {
+ return $this->targetProperty !== null;
+ }
+
+ /**
+ * Get the configured target property
+ *
+ * @return string
+ */
+ public function getTargetProperty($default = null)
+ {
+ if ($this->targetProperty === null) {
+ return $default;
+ }
+
+ return $this->targetProperty;
+ }
+
+ public function setDb(Db $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ public function getDb()
+ {
+ return $this->db;
+ }
+
+ public function setSettings(array $settings)
+ {
+ $this->settings = $settings;
+ return $this;
+ }
+
+ public function getSetting($name, $default = null)
+ {
+ if (array_key_exists($name, $this->settings)) {
+ return $this->settings[$name];
+ } else {
+ return $default;
+ }
+ }
+
+ public function setSetting($name, $value)
+ {
+ $this->settings[$name] = $value;
+ return $this;
+ }
+
+ public function exportSettings()
+ {
+ return (object) $this->settings;
+ }
+
+ public function getSettings()
+ {
+ return $this->settings;
+ }
+
+ /**
+ * Override this method if you want to extend the settings form
+ *
+ * @param QuickForm $form QuickForm that should be extended
+ * @return QuickForm
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ return $form;
+ }
+}
diff --git a/library/Director/Hook/ServiceFieldHook.php b/library/Director/Hook/ServiceFieldHook.php
new file mode 100644
index 0000000..bca0836
--- /dev/null
+++ b/library/Director/Hook/ServiceFieldHook.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+use Icinga\Module\Director\Field\FieldSpec;
+use Icinga\Module\Director\Objects\IcingaService;
+
+abstract class ServiceFieldHook
+{
+ public function wants(IcingaService $service)
+ {
+ return true;
+ }
+
+ /**
+ * @return FieldSpec
+ */
+ abstract public function getFieldSpec(IcingaService $service);
+}
diff --git a/library/Director/Hook/ShipConfigFilesHook.php b/library/Director/Hook/ShipConfigFilesHook.php
new file mode 100644
index 0000000..0026c59
--- /dev/null
+++ b/library/Director/Hook/ShipConfigFilesHook.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Icinga\Module\Director\Hook;
+
+abstract class ShipConfigFilesHook
+{
+ public function fetchFiles()
+ {
+ return array();
+ }
+}
diff --git a/library/Director/IcingaConfig/AgentWizard.php b/library/Director/IcingaConfig/AgentWizard.php
new file mode 100644
index 0000000..aceddb1
--- /dev/null
+++ b/library/Director/IcingaConfig/AgentWizard.php
@@ -0,0 +1,337 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaZone;
+use Icinga\Module\Director\Util;
+use LogicException;
+
+class AgentWizard
+{
+ protected $db;
+
+ protected $host;
+
+ protected $parentZone;
+
+ protected $parentEndpoints;
+
+ /** @var string PKI ticket */
+ protected $ticket;
+
+ public function __construct(IcingaHost $host)
+ {
+ $this->host = $host;
+ }
+
+ protected function assertAgent()
+ {
+ if ($this->host->getResolvedProperty('has_agent') !== 'y') {
+ throw new ProgrammingError(
+ 'The given host "%s" is not an Agent',
+ $this->host->getObjectName()
+ );
+ }
+ }
+
+ protected function getCaServer()
+ {
+ return $this->db()->getDeploymentEndpointName();
+
+ // TODO: This is a problem with Icinga 2. Should look like this:
+ // return current($this->getParentEndpoints())->object_name;
+ }
+
+ protected function shouldConnectToMaster()
+ {
+ return $this->host->getResolvedProperty('master_should_connect') !== 'y';
+ }
+
+ protected function getParentZone()
+ {
+ if ($this->parentZone === null) {
+ $this->parentZone = $this->loadParentZone();
+ }
+
+ return $this->parentZone;
+ }
+
+ protected function loadParentZone()
+ {
+ $db = $this->db();
+
+ if ($zoneId = $this->host->getResolvedProperty('zone_id')) {
+ return IcingaZone::loadWithAutoIncId($zoneId, $db);
+ } else {
+ return IcingaZone::load($db->getMasterZoneName(), $db);
+ }
+ }
+
+ protected function getParentEndpoints()
+ {
+ if ($this->parentEndpoints === null) {
+ $this->parentEndpoints = $this->loadParentEndpoints();
+ }
+
+ return $this->parentEndpoints;
+ }
+
+ protected function loadParentEndpoints()
+ {
+ $db = $this->db()->getDbAdapter();
+
+ $query = $db->select()
+ ->from('icinga_endpoint')
+ ->where(
+ 'zone_id = ?',
+ $this->getParentZone()->get('id')
+ );
+
+ return IcingaEndpoint::loadAll(
+ $this->db(),
+ $query,
+ 'object_name'
+ );
+ }
+
+ /**
+ * Get the PKI ticket
+ *
+ * @return string
+ *
+ * @throws LogicException If ticket has not been set
+ */
+ protected function getTicket()
+ {
+ if ($this->ticket === null) {
+ throw new LogicException('Ticket is null');
+ }
+
+ return $this->ticket;
+ }
+
+ /**
+ * Set the PKI ticket
+ *
+ * @param string $ticket
+ *
+ * @return $this
+ */
+ public function setTicket($ticket)
+ {
+ $this->ticket = $ticket;
+ }
+
+ protected function loadPowershellModule()
+ {
+ return $this->getContribFile('windows-agent-installer/Icinga2Agent.psm1');
+ }
+
+ public function renderWindowsInstaller()
+ {
+ return $this->loadPowershellModule()
+ . "\n\n"
+ . 'exit Icinga2AgentModule `' . "\n "
+ . $this->renderPowershellParameters([
+ 'AgentName' => $this->host->getEndpointName(),
+ 'Ticket' => $this->getTicket(),
+ 'ParentZone' => $this->getParentZone()->getObjectName(),
+ 'ParentEndpoints' => array_keys($this->getParentEndpoints()),
+ 'CAServer' => $this->getCaServer(),
+ 'RunInstaller'
+ ]);
+ }
+
+ public function renderIcinga4WindowsWizardCommand($token)
+ {
+ $script = "Use-Icinga;\n"
+ . 'Start-IcingaAgentInstallWizard `' . "\n "
+ . $this->renderPowershellParameters([
+ 'DirectorUrl' => $this->getDirectorUrl(),
+ 'SelfServiceAPIKey' => $token,
+ 'UseDirectorSelfService' => 1,
+ 'OverrideDirectorVars' => 0,
+ 'Reconfigure',
+ 'RunInstaller'
+ ]);
+
+ return $script;
+ }
+
+ public function renderPowershellModuleInstaller($token, $withModule = false)
+ {
+ if ($withModule) {
+ $script = $this->loadPowershellModule() . "\n\n";
+ } else {
+ $script = '';
+ }
+
+ $script .= 'exit Icinga2AgentModule `' . "\n "
+ . $this->renderPowershellParameters([
+ 'DirectorUrl' => $this->getDirectorUrl(),
+ 'DirectorAuthToken' => $token,
+ 'RunInstaller'
+ ]);
+
+ return $script;
+ }
+
+ protected function getDirectorUrl()
+ {
+ $r = Icinga::app()->getRequest();
+ $scheme = $r->getServer('HTTP_X_FORWARDED_PROTO', $r->getScheme());
+
+ return sprintf(
+ '%s://%s%s/director/',
+ $scheme,
+ $r->getHttpHost(),
+ $r->getBaseUrl()
+ );
+ }
+
+ protected function renderPowershellParameters($parameters)
+ {
+ $maxKeyLength = max(array_map('strlen', array_keys($parameters)));
+ foreach ($parameters as $key => $value) {
+ if (is_int($key)) {
+ $maxKeyLength = max($maxKeyLength, strlen($value));
+ }
+ }
+ $parts = array();
+
+ foreach ($parameters as $key => $value) {
+ if (is_int($key)) {
+ $parts[] = $this->renderPowershellParameter($value, null, $maxKeyLength);
+ } else {
+ $parts[] = $this->renderPowershellParameter($key, $value, $maxKeyLength);
+ }
+ }
+
+ return implode(' `' . "\n ", $parts);
+ }
+
+ protected function renderPowershellParameter($key, $value, $maxKeyLength = null)
+ {
+ $ret = '-' . $key;
+ if ($value === null) {
+ return $ret;
+ }
+
+ $ret .= ' ';
+
+ if ($maxKeyLength !== null) {
+ $ret .= str_repeat(' ', $maxKeyLength - strlen($key));
+ }
+
+ if (is_array($value)) {
+ $vals = array();
+ foreach ($value as $val) {
+ $vals[] = $this->renderPowershellString($val);
+ }
+ $ret .= implode(', ', $vals);
+ } elseif (is_int($value)) {
+ $ret .= $value;
+ } else {
+ $ret .= $this->renderPowershellString($value);
+ }
+
+ return $ret;
+ }
+
+ protected function renderPowershellString($string)
+ {
+ // TODO: Escaping
+ return "'" . $string . "'";
+ }
+
+ protected function db()
+ {
+ if ($this->db === null) {
+ $this->db = $this->host->getConnection();
+ }
+
+ return $this->db;
+ }
+
+ public function renderLinuxInstaller()
+ {
+ $script = $this->loadBashModule();
+
+ $endpoints = [];
+ foreach ($this->getParentEndpoints() as $endpoint) {
+ $endpoints[$endpoint->getObjectName()] = $endpoint->get('host');
+ }
+
+ return $this->replaceBashTemplate($script, [
+ 'ICINGA2_NODENAME' => $this->host->getEndpointName(),
+ 'ICINGA2_CA_TICKET' => $this->getTicket(),
+ 'ICINGA2_PARENT_ZONE' => $this->getParentZone()->getObjectName(),
+ 'ICINGA2_PARENT_ENDPOINTS' => $endpoints,
+ 'ICINGA2_CA_NODE' => $this->getCaServer(),
+ 'ICINGA2_GLOBAL_ZONES' => [$this->db()->getDefaultGlobalZoneName()],
+ ]);
+ }
+
+ protected function loadBashModule()
+ {
+ return $this->getContribFile('linux-agent-installer/Icinga2Agent.bash');
+ }
+
+ protected function replaceBashTemplate($script, $parameters)
+ {
+ foreach ($parameters as $key => $value) {
+ $quotedKey = preg_quote($key, '~');
+ if (is_array($value)) {
+ $list = [];
+ foreach ($value as $k => $v) {
+ if (!is_numeric($k)) {
+ $v = "$k,$v";
+ }
+ $list[] = escapeshellarg($v);
+ }
+ $value = '(' . join(' ', $list) . ')';
+ } else {
+ $value = escapeshellarg($value);
+ }
+ $script = preg_replace("~^#?$quotedKey='@$quotedKey@'$~m", "${key}=${value}", $script);
+ }
+
+ return $script;
+ }
+
+ protected function renderBashParameter($key, $value)
+ {
+ $ret = $key . '=';
+
+ // Cheating, this doesn't really help. We should ship the rendered config
+ if (is_array($value) && count($value) === 1) {
+ $value = array_shift($value);
+ }
+
+ if (is_array($value)) {
+ $vals = array();
+ foreach ($value as $val) {
+ $vals[] = $this->renderPowershellString($val);
+ }
+ $ret .= '(' . implode(' ', $vals) . ')';
+ } else {
+ $ret .= $this->renderPowershellString($value);
+ }
+
+ return $ret;
+ }
+
+ protected function getContribDir()
+ {
+ return dirname(dirname(dirname(__DIR__))) . '/contrib';
+ }
+
+ protected function getContribFile($path)
+ {
+ return file_get_contents($this->getContribDir() . '/' . $path);
+ }
+}
diff --git a/library/Director/IcingaConfig/AssignRenderer.php b/library/Director/IcingaConfig/AssignRenderer.php
new file mode 100644
index 0000000..6acbfee
--- /dev/null
+++ b/library/Director/IcingaConfig/AssignRenderer.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use gipfl\Json\JsonString;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Data\Filter\FilterNot;
+use Icinga\Data\Filter\FilterEqualOrGreaterThan;
+use Icinga\Data\Filter\FilterEqualOrLessThan;
+use Icinga\Data\Filter\FilterEqual;
+use Icinga\Data\Filter\FilterGreaterThan;
+use Icinga\Data\Filter\FilterLessThan;
+use Icinga\Data\Filter\FilterMatch;
+use Icinga\Data\Filter\FilterMatchNot;
+use Icinga\Data\Filter\FilterNotEqual;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Director\Data\Json;
+use InvalidArgumentException;
+
+class AssignRenderer
+{
+ protected $filter;
+
+ public function __construct(Filter $filter)
+ {
+ $this->filter = $filter;
+ }
+
+ public static function forFilter(Filter $filter)
+ {
+ return new static($filter);
+ }
+
+ public function renderAssign()
+ {
+ return $this->render('assign');
+ }
+
+ public function renderIgnore()
+ {
+ return $this->render('ignore');
+ }
+
+ public function render($type)
+ {
+ return $type . ' where ' . $this->renderFilter($this->filter);
+ }
+
+ protected function renderFilter(Filter $filter)
+ {
+ if ($filter instanceof FilterNot) {
+ $parts = [];
+ foreach ($filter->filters() as $sub) {
+ $parts[] = $this->renderFilter($sub);
+ }
+
+ return '!(' . implode(' && ', $parts) . ')';
+ }
+ if ($filter->isChain()) {
+ /** @var FilterChain $filter */
+ return $this->renderFilterChain($filter);
+ } else {
+ /** @var FilterExpression $filter */
+ return $this->renderFilterExpression($filter);
+ }
+ }
+
+ protected function renderEquals($column, $expression)
+ {
+ if (substr($column, -7) === '.groups') {
+ return sprintf(
+ '%s in %s',
+ $expression,
+ $column
+ );
+ } else {
+ return sprintf(
+ '%s == %s',
+ $column,
+ $expression
+ );
+ }
+ }
+
+ protected function renderNotEquals($column, $expression)
+ {
+ if (substr($column, -7) === '.groups') {
+ return sprintf(
+ '!(%s in %s)',
+ $expression,
+ $column
+ );
+ } else {
+ return sprintf(
+ '%s != %s',
+ $column,
+ $expression
+ );
+ }
+ }
+
+ protected function renderInArray($column, $expression)
+ {
+ return sprintf(
+ '%s in %s',
+ $column,
+ $expression
+ );
+ }
+
+ protected function renderContains(FilterExpression $filter)
+ {
+ return sprintf(
+ '%s in %s',
+ $this->renderExpressionValue(json_decode($filter->getColumn())),
+ $filter->getExpression()
+ );
+ }
+
+ protected function renderFilterExpression(FilterExpression $filter)
+ {
+ if ($this->columnIsJson($filter)) {
+ return $this->renderContains($filter);
+ }
+
+ $column = $filter->getColumn();
+ $rawExpression = Json::decode($filter->getExpression());
+ $expression = $this->renderExpressionValue($rawExpression);
+
+ if (is_array($rawExpression) && $filter instanceof FilterMatch) {
+ return $this->renderInArray($column, $expression);
+ }
+
+ if (is_string($rawExpression) && ctype_digit($rawExpression)) {
+ // TODO: doing this for compat reasons, should work for all filters
+ if ($filter instanceof FilterEqualOrGreaterThan
+ || $filter instanceof FilterGreaterThan
+ || $filter instanceof FilterEqualOrLessThan
+ || $filter instanceof FilterLessThan
+ ) {
+ $expression = $rawExpression;
+ }
+ }
+
+ if ($filter instanceof FilterEqual) {
+ if (is_array($rawExpression)) {
+ return sprintf(
+ '%s in %s',
+ $column,
+ $expression
+ );
+ } else {
+ return sprintf(
+ '%s == %s',
+ $column,
+ $expression
+ );
+ }
+ } elseif ($filter instanceof FilterMatch) {
+ if ($rawExpression === true) {
+ return $column;
+ }
+ if ($rawExpression === false) {
+ return sprintf(
+ '! %s',
+ $column
+ );
+ }
+ if (strpos($expression, '*') === false) {
+ return $this->renderEquals($column, $expression);
+ } else {
+ return sprintf(
+ 'match(%s, %s)',
+ $expression,
+ $column
+ );
+ }
+ } elseif ($filter instanceof FilterMatchNot) {
+ if (strpos($expression, '*') === false) {
+ return $this->renderNotEquals($column, $expression);
+ } else {
+ return sprintf(
+ '! match(%s, %s)',
+ $expression,
+ $column
+ );
+ }
+ } elseif ($filter instanceof FilterNotEqual) {
+ return sprintf(
+ '%s != %s',
+ $column,
+ $expression
+ );
+ } elseif ($filter instanceof FilterEqualOrGreaterThan) {
+ return sprintf(
+ '%s >= %s',
+ $column,
+ $expression
+ );
+ } elseif ($filter instanceof FilterEqualOrLessThan) {
+ return sprintf(
+ '%s <= %s',
+ $column,
+ $expression
+ );
+ } elseif ($filter instanceof FilterGreaterThan) {
+ return sprintf(
+ '%s > %s',
+ $column,
+ $expression
+ );
+ } elseif ($filter instanceof FilterLessThan) {
+ return sprintf(
+ '%s < %s',
+ $column,
+ $expression
+ );
+ } else {
+ throw new QueryException(
+ 'Filter expression of type "%s" is not supported',
+ get_class($filter)
+ );
+ }
+ }
+
+ protected function renderExpressionValue($value)
+ {
+ return IcingaConfigHelper::renderPhpValue($value);
+ }
+
+ protected function columnIsJson(FilterExpression $filter)
+ {
+ $col = $filter->getColumn();
+ return strlen($col) && $col[0] === '"';
+ }
+
+ protected function renderFilterChain(FilterChain $filter)
+ {
+ // TODO: brackets if deeper level?
+ if ($filter instanceof FilterAnd) {
+ $op = ' && ';
+ } elseif ($filter instanceof FilterOr) {
+ $op = ' || ';
+ } elseif ($filter instanceof FilterNot) {
+ throw new InvalidArgumentException('renderFilterChain should never get a FilterNot instance');
+ } else {
+ throw new InvalidArgumentException('Cannot render filter: %s', $filter);
+ }
+
+ $parts = array();
+ if (! $filter->isEmpty()) {
+ /** @var Filter $f */
+ foreach ($filter->filters() as $f) {
+ if ($f instanceof FilterChain && $f->count() > 1) {
+ $parts[] = '(' . $this->renderFilter($f) . ')';
+ } else {
+ $parts[] = $this->renderFilter($f);
+ }
+ }
+ }
+
+ return implode($op, $parts);
+ }
+}
diff --git a/library/Director/IcingaConfig/ExtensibleSet.php b/library/Director/IcingaConfig/ExtensibleSet.php
new file mode 100644
index 0000000..9120816
--- /dev/null
+++ b/library/Director/IcingaConfig/ExtensibleSet.php
@@ -0,0 +1,574 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class ExtensibleSet
+{
+ protected $ownValues;
+
+ protected $plusValues = [];
+
+ protected $minusValues = [];
+
+ protected $resolvedValues;
+
+ protected $allowedValues;
+
+ protected $inheritedValues = [];
+
+ protected $fromDb;
+
+ /**
+ * @var IcingaObject
+ */
+ protected $object;
+
+ /**
+ * Object property name pointing to this set
+ *
+ * This also implies set table called <object_table>_<propertyName>_set
+ *
+ * @var string
+ */
+ protected $propertyName;
+
+ public function __construct($values = null)
+ {
+ if (null !== $values) {
+ $this->override($values);
+ }
+ }
+
+ public static function forIcingaObject(IcingaObject $object, $propertyName)
+ {
+ $set = new static;
+ $set->object = $object;
+ $set->propertyName = $propertyName;
+
+ if ($object->hasBeenLoadedFromDb()) {
+ $set->loadFromDb();
+ }
+
+ return $set;
+ }
+
+ public function set($set)
+ {
+ if (null === $set) {
+ $this->reset();
+
+ return $this;
+ } elseif (is_array($set) || is_string($set)) {
+ $this->reset();
+ $this->override($set);
+ } elseif (is_object($set)) {
+ $this->reset();
+
+ foreach (['override', 'extend', 'blacklist'] as $method) {
+ if (property_exists($set, $method)) {
+ $this->$method($set->$method);
+ }
+ }
+ } else {
+ throw new ProgrammingError(
+ 'ExtensibleSet::set accepts only plain arrays or objects'
+ );
+ }
+
+ return $this;
+ }
+
+ public function isEmpty()
+ {
+ return $this->ownValues === null
+ && empty($this->plusValues)
+ && empty($this->minusValues);
+ }
+
+ public function toPlainObject()
+ {
+ if ($this->ownValues !== null) {
+ if (empty($this->minusValues) && empty($this->plusValues)) {
+ return $this->ownValues;
+ }
+ }
+
+ $plain = (object) [];
+
+ if ($this->ownValues !== null) {
+ $plain->override = $this->ownValues;
+ }
+ if (! empty($this->plusValues)) {
+ $plain->extend = $this->plusValues;
+ }
+ if (! empty($this->minusValues)) {
+ $plain->blacklist = $this->minusValues;
+ }
+
+ return $plain;
+ }
+
+ public function getPlainUnmodifiedObject()
+ {
+ if ($this->fromDb === null) {
+ return null;
+ }
+
+ $old = $this->fromDb;
+
+ if ($old['override'] !== null) {
+ if (empty($old['blacklist']) && empty($old['extend'])) {
+ return $old['override'];
+ }
+ }
+
+ $plain = (object) [];
+
+ if ($old['override'] !== null) {
+ $plain->override = $old['override'];
+ }
+ if (! empty($old['extend'])) {
+ $plain->extend = $old['extend'];
+ }
+ if (! empty($old['blacklist'])) {
+ $plain->blacklist = $old['blacklist'];
+ }
+
+ return $plain;
+ }
+
+ public function hasBeenLoadedFromDb()
+ {
+ return $this->fromDb !== null;
+ }
+
+ public function hasBeenModified()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ if ($this->ownValues !== $this->fromDb['override']) {
+ return true;
+ }
+
+ if ($this->plusValues !== $this->fromDb['extend']) {
+ return true;
+ }
+
+ if ($this->minusValues !== $this->fromDb['blacklist']) {
+ return true;
+ }
+
+ return false;
+ } else {
+ if ($this->ownValues === null
+ && empty($this->plusValues)
+ && empty($this->minusValues)
+ ) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ protected function loadFromDb()
+ {
+ $db = $this->object->getDb();
+
+ $query = $db->select()->from($this->tableName(), [
+ 'property',
+ 'merge_behaviour'
+ ])->where($this->foreignKey() . ' = ?', $this->object->get('id'));
+
+ $byBehaviour = [
+ 'override' => [],
+ 'extend' => [],
+ 'blacklist' => [],
+ ];
+
+ foreach ($db->fetchAll($query) as $row) {
+ if (! array_key_exists($row->merge_behaviour, $byBehaviour)) {
+ throw new ProgrammingError(
+ 'Got unknown merge_behaviour "%s". Schema change?',
+ $row->merge_behaviour
+ );
+ }
+
+ $byBehaviour[$row->merge_behaviour][] = $row->property;
+ }
+
+ foreach ($byBehaviour as $method => &$values) {
+ if (empty($values)) {
+ continue;
+ }
+
+ sort($values);
+ $this->$method($values);
+ }
+
+ if (empty($byBehaviour['override'])) {
+ $byBehaviour['override'] = null;
+ }
+
+ $this->fromDb = $byBehaviour;
+
+ return $this;
+ }
+
+ protected function foreignKey()
+ {
+ return $this->object->getShortTableName() . '_id';
+ }
+
+ protected function tableName()
+ {
+ return implode('_', [
+ $this->object->getTableName(),
+ $this->propertyName,
+ 'set'
+ ]);
+ }
+
+ public function getObject()
+ {
+ return $this->object;
+ }
+
+ public function store()
+ {
+ if (null === $this->object) {
+ throw new ProgrammingError(
+ 'Cannot store ExtensibleSet with no assigned object'
+ );
+ }
+
+ if (! $this->hasBeenModified()) {
+ return false;
+ }
+
+ $this->storeToDb();
+ return true;
+ }
+
+ protected function storeToDb()
+ {
+ $db = $this->object->getDb();
+
+ if ($db === null) {
+ throw new ProgrammingError(
+ 'Cannot store a set for an unstored related object'
+ );
+ }
+
+ $table = $this->tableName();
+ $props = [
+ $this->foreignKey() => $this->object->get('id')
+ ];
+
+ $db->delete(
+ $this->tableName(),
+ $db->quoteInto(
+ $this->foreignKey() . ' = ?',
+ $this->object->get('id')
+ )
+ );
+
+ if ($this->ownValues !== null) {
+ $props['merge_behaviour'] = 'override';
+ foreach ($this->ownValues as $value) {
+ $db->insert(
+ $table,
+ array_merge($props, ['property' => $value])
+ );
+ }
+ }
+
+ if (! empty($this->plusValues)) {
+ $props['merge_behaviour'] = 'extend';
+ foreach ($this->plusValues as $value) {
+ $db->insert(
+ $table,
+ array_merge($props, ['property' => $value])
+ );
+ }
+ }
+
+ if (! empty($this->minusValues)) {
+ $props['merge_behaviour'] = 'blacklist';
+ foreach ($this->minusValues as $value) {
+ $db->insert(
+ $table,
+ array_merge($props, ['property' => $value])
+ );
+ }
+ }
+
+ $this->setBeingLoadedFromDb();
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ $this->fromDb = [
+ 'override' => $this->ownValues ?: [],
+ 'extend' => $this->plusValues ?: [],
+ 'blacklist' => $this->minusValues ?: [],
+ ];
+ }
+
+ public function override($values)
+ {
+ $this->ownValues = [];
+ $this->inheritedValues = [];
+
+ $this->addValuesTo($this->ownValues, $values);
+
+ return $this->addResolvedValues($values);
+ }
+
+ public function extend($values)
+ {
+ $this->addValuesTo($this->plusValues, $values);
+ return $this->addResolvedValues($values);
+ }
+
+ public function blacklist($values)
+ {
+ $this->addValuesTo($this->minusValues, $values);
+
+ if ($this->hasBeenResolved()) {
+ $this->removeValuesFrom($this->resolvedValues, $values);
+ }
+
+ return $this;
+ }
+
+ public function getResolvedValues()
+ {
+ if (! $this->hasBeenResolved()) {
+ $this->recalculate();
+ }
+
+ sort($this->resolvedValues);
+
+ return $this->resolvedValues;
+ }
+
+ public function inheritFrom(ExtensibleSet $parent)
+ {
+ if ($this->ownValues !== null) {
+ return $this;
+ }
+
+ if ($this->hasBeenResolved()) {
+ $this->resolvedValues = null;
+ }
+
+ $this->inheritedValues = [];
+
+ $this->addValuesTo(
+ $this->inheritedValues,
+ $this->stripBlacklistedValues($parent->getResolvedValues())
+ );
+
+ return $this->recalculate();
+ }
+
+ public function forgetInheritedValues()
+ {
+ $this->inheritedValues = [];
+ return $this;
+ }
+
+ protected function renderArray($array)
+ {
+ $safe = [];
+ foreach ($array as $value) {
+ $safe[] = c::alreadyRendered($value);
+ }
+
+ return c::renderArray($safe);
+ }
+
+ public function renderAs($key, $prefix = ' ')
+ {
+ $parts = [];
+
+ // TODO: It would be nice if we could use empty arrays to override
+ // inherited ones
+ // if ($this->ownValues !== null) {
+ if (!empty($this->ownValues)) {
+ $parts[] = c::renderKeyValue(
+ $key,
+ $this->renderArray($this->ownValues),
+ $prefix
+ );
+ }
+
+ if (!empty($this->plusValues)) {
+ $parts[] = c::renderKeyOperatorValue(
+ $key,
+ '+=',
+ $this->renderArray($this->plusValues),
+ $prefix
+ );
+ }
+
+ if (!empty($this->minusValues)) {
+ $parts[] = c::renderKeyOperatorValue(
+ $key,
+ '-=',
+ $this->renderArray($this->minusValues),
+ $prefix
+ );
+ }
+
+ return implode('', $parts);
+ }
+
+ public function isRestricted()
+ {
+ return $this->allowedValues === null;
+ }
+
+ public function enumAllowedValues()
+ {
+ if ($this->isRestricted()) {
+ throw new ProgrammingError(
+ 'No allowed value set available, this set is not restricted'
+ );
+ }
+
+ if (empty($this->allowedValues)) {
+ return [];
+ }
+
+ return array_combine($this->allowedValues, $this->allowedValues);
+ }
+
+ protected function hasBeenResolved()
+ {
+ return $this->resolvedValues !== null;
+ }
+
+ protected function stripBlacklistedValues($array)
+ {
+ $this->removeValuesFrom($array, $this->minusValues);
+
+ return $array;
+ }
+
+ protected function assertValidValue($value)
+ {
+ if (null === $this->allowedValues) {
+ return $this;
+ }
+
+ if (in_array($value, $this->allowedValues)) {
+ return $this;
+ }
+
+ throw new InvalidPropertyException(
+ 'Got invalid property "%s", allowed are: (%s)',
+ $value,
+ implode(', ', $this->allowedValues)
+ );
+ }
+
+ protected function addValuesTo(&$array, $values)
+ {
+ foreach ($this->wantArray($values) as $value) {
+ // silently ignore null or empty strings
+ if (strlen($value) === 0) {
+ continue;
+ }
+
+ $this->addTo($array, $value);
+ }
+
+ return $this;
+ }
+
+ protected function addResolvedValues($values)
+ {
+ if (! $this->hasBeenResolved()) {
+ $this->resolvedValues = [];
+ }
+
+ return $this->addValuesTo(
+ $this->resolvedValues,
+ $this->stripBlacklistedValues($this->wantArray($values))
+ );
+ }
+
+ protected function removeValuesFrom(&$array, $values)
+ {
+ foreach ($this->wantArray($values) as $value) {
+ $this->removeFrom($array, $value);
+ }
+
+ return $this;
+ }
+
+ protected function addTo(&$array, $value)
+ {
+ if (! in_array($value, $array)) {
+ $this->assertValidValue($value);
+ $array[] = $value;
+ }
+
+ return $this;
+ }
+
+ protected function removeFrom(&$array, $value)
+ {
+ if (false !== ($pos = array_search($value, $array))) {
+ unset($array[$pos]);
+ }
+
+ return $this;
+ }
+
+ protected function recalculate()
+ {
+ $this->resolvedValues = [];
+
+ if ($this->ownValues === null) {
+ $this->addValuesTo($this->resolvedValues, $this->inheritedValues);
+ } else {
+ $this->addValuesTo($this->resolvedValues, $this->ownValues);
+ }
+ $this->addValuesTo($this->resolvedValues, $this->plusValues);
+ $this->removeFrom($this->resolvedValues, $this->minusValues);
+
+ return $this;
+ }
+
+ protected function reset()
+ {
+ $this->ownValues = null;
+ $this->plusValues = [];
+ $this->minusValues = [];
+ $this->resolvedValues = null;
+ $this->inheritedValues = [];
+
+ return $this;
+ }
+
+ protected function translate($string)
+ {
+ return mt('director', $string);
+ }
+
+ protected function wantArray($values)
+ {
+ if (is_array($values)) {
+ return $values;
+ }
+
+ return [$values];
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfig.php b/library/Director/IcingaConfig/IcingaConfig.php
new file mode 100644
index 0000000..72edd7e
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfig.php
@@ -0,0 +1,781 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Application\Benchmark;
+use Icinga\Application\Hook;
+use Icinga\Application\Icinga;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Application\MemoryLimit;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Hook\ShipConfigFilesHook;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaZone;
+use InvalidArgumentException;
+use LogicException;
+use RuntimeException;
+
+class IcingaConfig
+{
+ protected $files = array();
+
+ protected $checksum;
+
+ protected $zoneMap = array();
+
+ protected $lastActivityChecksum;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ protected $connection;
+
+ protected $generationTime;
+
+ protected $configFormat;
+
+ protected $deploymentModeV1;
+
+ public static $table = 'director_generated_config';
+
+ public function __construct(Db $connection)
+ {
+ // Make sure module hooks are loaded:
+ Icinga::app()->getModuleManager()->loadEnabledModules();
+
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ $this->configFormat = $this->connection->settings()->config_format;
+ $this->deploymentModeV1 = $this->connection->settings()->deployment_mode_v1;
+ }
+
+ public function getSize()
+ {
+ $size = 0;
+ foreach ($this->getFiles() as $file) {
+ $size += $file->getSize();
+ }
+ return $size;
+ }
+
+ public function getDuration()
+ {
+ return $this->generationTime;
+ }
+
+ public function getFileCount()
+ {
+ return count($this->files);
+ }
+
+ public function getConfigFormat()
+ {
+ return $this->configFormat;
+ }
+
+ public function getDeploymentMode()
+ {
+ if ($this->isLegacy()) {
+ return $this->deploymentModeV1;
+ } else {
+ throw new LogicException('There is no deployment mode for Icinga 2 config format!');
+ }
+ }
+
+ public function setConfigFormat($format)
+ {
+ if (! in_array($format, array('v1', 'v2'))) {
+ throw new InvalidArgumentException(sprintf(
+ 'Only Icinga v1 and v2 config format is supported, got "%s"',
+ $format
+ ));
+ }
+
+ $this->configFormat = $format;
+
+ return $this;
+ }
+
+ public function isLegacy()
+ {
+ return $this->configFormat === 'v1';
+ }
+
+ public function getObjectCount()
+ {
+ $cnt = 0;
+ foreach ($this->getFiles() as $file) {
+ $cnt += $file->getObjectCount();
+ }
+ return $cnt;
+ }
+
+ public function getTemplateCount()
+ {
+ $cnt = 0;
+ foreach ($this->getFiles() as $file) {
+ $cnt += $file->getTemplateCount();
+ }
+ return $cnt;
+ }
+
+ public function getApplyCount()
+ {
+ $cnt = 0;
+ foreach ($this->getFiles() as $file) {
+ $cnt += $file->getApplyCount();
+ }
+ return $cnt;
+ }
+
+ public function getChecksum()
+ {
+ return $this->checksum;
+ }
+
+ public function getHexChecksum()
+ {
+ return bin2hex($this->checksum);
+ }
+
+ /**
+ * @return IcingaConfigFile[]
+ */
+ public function getFiles()
+ {
+ return $this->files;
+ }
+
+ public function getFileContents()
+ {
+ $result = array();
+ foreach ($this->files as $name => $file) {
+ $result[$name] = $file->getContent();
+ }
+
+ return $result;
+ }
+
+ /**
+ * @return array
+ */
+ public function getFileNames()
+ {
+ return array_keys($this->files);
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return IcingaConfigFile
+ */
+ public function getFile($name)
+ {
+ return $this->files[$name];
+ }
+
+ /**
+ * @param string $checksum
+ * @param Db $connection
+ *
+ * @return static
+ */
+ public static function load($checksum, Db $connection)
+ {
+ $config = new static($connection);
+ $config->loadFromDb($checksum);
+ return $config;
+ }
+
+ /**
+ * @param string $checksum
+ * @param Db $connection
+ *
+ * @return bool
+ */
+ public static function exists($checksum, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from(
+ array('c' => self::$table),
+ array('checksum' => $connection->dbHexFunc('c.checksum'))
+ )->where(
+ 'checksum = ?',
+ $connection->quoteBinary(hex2bin($checksum))
+ );
+
+ return $db->fetchOne($query) === $checksum;
+ }
+
+ public static function loadByActivityChecksum($checksum, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from(
+ array('c' => self::$table),
+ array('checksum' => 'c.checksum')
+ )->join(
+ array('l' => 'director_activity_log'),
+ 'l.checksum = c.last_activity_checksum',
+ array()
+ )->where(
+ 'last_activity_checksum = ?',
+ $connection->quoteBinary(hex2bin($checksum))
+ )->order('l.id DESC')->limit(1);
+
+ return self::load($db->fetchOne($query), $connection);
+ }
+
+ public static function existsForActivityChecksum($checksum, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from(
+ array('c' => self::$table),
+ array('checksum' => $connection->dbHexFunc('c.checksum'))
+ )->join(
+ array('l' => 'director_activity_log'),
+ 'l.checksum = c.last_activity_checksum',
+ array()
+ )->where(
+ 'last_activity_checksum = ?',
+ $connection->quoteBinary(hex2bin($checksum))
+ )->order('l.id DESC')->limit(1);
+
+ return $db->fetchOne($query) === $checksum;
+ }
+
+ /**
+ * @param Db $connection
+ *
+ * @return mixed
+ */
+ public static function generate(Db $connection)
+ {
+ $config = new static($connection);
+ return $config->storeIfModified();
+ }
+
+ public static function wouldChange(Db $connection)
+ {
+ $config = new static($connection);
+ return $config->hasBeenModified();
+ }
+
+ public function hasBeenModified()
+ {
+ $this->generateFromDb();
+ $this->collectExtraFiles();
+ $checksum = $this->calculateChecksum();
+ $activity = $this->getLastActivityChecksum();
+
+ $lastActivity = $this->connection->binaryDbResult(
+ $this->db->fetchOne(
+ $this->db->select()->from(
+ self::$table,
+ 'last_activity_checksum'
+ )->where(
+ 'checksum = ?',
+ $this->dbBin($checksum)
+ )
+ )
+ );
+
+ if ($lastActivity === false || $lastActivity === null) {
+ return true;
+ }
+
+ if ($lastActivity !== $activity) {
+ $this->db->update(
+ self::$table,
+ array(
+ 'last_activity_checksum' => $this->dbBin($activity)
+ ),
+ $this->db->quoteInto('checksum = ?', $this->dbBin($checksum))
+ );
+ }
+
+ return false;
+ }
+
+ protected function storeIfModified()
+ {
+ if ($this->hasBeenModified()) {
+ $this->store();
+ }
+
+ return $this;
+ }
+
+ protected function dbBin($binary)
+ {
+ return $this->connection->quoteBinary($binary);
+ }
+
+ protected function calculateChecksum()
+ {
+ $files = array();
+ $sortedFiles = $this->files;
+ ksort($sortedFiles);
+ /** @var IcingaConfigFile $file */
+ foreach ($sortedFiles as $name => $file) {
+ $files[] = $name . '=' . $file->getHexChecksum();
+ }
+
+ $this->checksum = sha1(implode(';', $files), true);
+ return $this->checksum;
+ }
+
+ public function getFilesChecksums()
+ {
+ $checksums = array();
+
+ /** @var IcingaConfigFile $file */
+ foreach ($this->files as $name => $file) {
+ $checksums[] = $file->getChecksum();
+ }
+
+ return $checksums;
+ }
+
+ // TODO: prepare lookup cache if empty?
+ public function getZoneName($id)
+ {
+ if (! array_key_exists($id, $this->zoneMap)) {
+ $zone = IcingaZone::loadWithAutoIncId($id, $this->connection);
+ $this->zoneMap[$id] = $zone->get('object_name');
+ }
+
+ return $this->zoneMap[$id];
+ }
+
+ /**
+ * @return self
+ */
+ public function store()
+ {
+ $fileTable = IcingaConfigFile::$table;
+ $fileKey = IcingaConfigFile::$keyName;
+
+ $existingQuery = $this->db->select()
+ ->from($fileTable, 'checksum')
+ ->where('checksum IN (?)', array_map(array($this, 'dbBin'), $this->getFilesChecksums()));
+
+ $existing = $this->db->fetchCol($existingQuery);
+
+ foreach ($existing as $key => $val) {
+ if (is_resource($val)) {
+ $existing[$key] = stream_get_contents($val);
+ }
+ }
+
+ $missing = array_diff($this->getFilesChecksums(), $existing);
+ $stored = array();
+
+ /** @var IcingaConfigFile $file */
+ foreach ($this->files as $name => $file) {
+ $checksum = $file->getChecksum();
+ if (! in_array($checksum, $missing)) {
+ continue;
+ }
+
+ if (array_key_exists($checksum, $stored)) {
+ continue;
+ }
+
+ $stored[$checksum] = true;
+
+ $this->db->insert(
+ $fileTable,
+ array(
+ $fileKey => $this->dbBin($checksum),
+ 'content' => $file->getContent(),
+ 'cnt_object' => $file->getObjectCount(),
+ 'cnt_template' => $file->getTemplateCount()
+ )
+ );
+ }
+
+ $activity = $this->dbBin($this->getLastActivityChecksum());
+ $this->db->beginTransaction();
+ try {
+ $this->db->insert(self::$table, [
+ 'duration' => $this->generationTime,
+ 'first_activity_checksum' => $activity,
+ 'last_activity_checksum' => $activity,
+ 'checksum' => $this->dbBin($this->getChecksum()),
+ ]);
+ /** @var IcingaConfigFile $file */
+ foreach ($this->files as $name => $file) {
+ $this->db->insert('director_generated_config_file', [
+ 'config_checksum' => $this->dbBin($this->getChecksum()),
+ 'file_checksum' => $this->dbBin($file->getChecksum()),
+ 'file_path' => $name,
+ ]);
+ }
+ $this->db->commit();
+ } catch (\Exception $e) {
+ try {
+ $this->db->rollBack();
+ } catch (\Exception $ignored) {
+ // Well...
+ }
+
+ throw $e;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function generateFromDb()
+ {
+ PrefetchCache::initialize($this->connection);
+ $start = microtime(true);
+
+ MemoryLimit::raiseTo('1024M');
+ ini_set('max_execution_time', 0);
+ // Workaround for https://bugs.php.net/bug.php?id=68606 or similar
+ ini_set('zend.enable_gc', 0);
+
+ if (! $this->connection->isPgsql() && $this->db->quote("1\0") !== '\'1\\0\'') {
+ throw new RuntimeException(
+ 'Refusing to render the configuration, your DB layer corrupts binary data.'
+ . ' You might be affected by Zend Framework bug #655'
+ );
+ }
+
+ $this
+ ->prepareGlobalBasics()
+ ->createFileFromDb('zone')
+ ->createFileFromDb('endpoint')
+ ->createFileFromDb('command')
+ ->createFileFromDb('timePeriod')
+ ->createFileFromDb('hostGroup')
+ ->createFileFromDb('host')
+ ->createFileFromDb('serviceGroup')
+ ->createFileFromDb('service')
+ ->createFileFromDb('serviceSet')
+ ->createFileFromDb('userGroup')
+ ->createFileFromDb('user')
+ ->createFileFromDb('notification')
+ ->createFileFromDb('dependency')
+ ->createFileFromDb('scheduledDowntime')
+ ;
+
+ PrefetchCache::forget();
+ IcingaHost::clearAllPrefetchCaches();
+
+ $this->generationTime = (int) ((microtime(true) - $start) * 1000);
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function prepareGlobalBasics()
+ {
+ if ($this->isLegacy()) {
+ $this->configFile(
+ sprintf(
+ 'director/%s/001-director-basics',
+ $this->connection->getDefaultGlobalZoneName()
+ ),
+ '.cfg'
+ )->prepend(
+ $this->renderLegacyDefaultNotification()
+ );
+
+ return $this;
+ }
+
+ $this->configFile(
+ sprintf(
+ 'zones.d/%s/001-director-basics',
+ $this->connection->getDefaultGlobalZoneName()
+ )
+ )->prepend(
+ "\nconst DirectorStageDir = dirname(dirname(current_filename))\n"
+ . $this->renderFlappingLogHelper()
+ . $this->renderHostOverridableVars()
+ );
+
+ return $this;
+ }
+
+ protected function renderFlappingLogHelper()
+ {
+ return '
+globals.directorWarnedOnceForThresholds = false;
+globals.directorWarnOnceForThresholds = function() {
+ if (globals.directorWarnedOnceForThresholds == false) {
+ globals.directorWarnedOnceForThresholds = true
+ log(LogWarning, "config", "Director: flapping_threshold_high/low is not supported in this Icinga 2 version!")
+ }
+}
+';
+ }
+
+ protected function renderHostOverridableVars()
+ {
+ $settings = $this->connection->settings();
+
+ return sprintf(
+ '
+const DirectorOverrideTemplate = "%s"
+if (! globals.contains(DirectorOverrideTemplate)) {
+ const DirectorOverrideVars = "%s"
+
+ globals.directorWarnedOnceForServiceWithoutHost = false;
+ globals.directorWarnOnceForServiceWithoutHost = function() {
+ if (globals.directorWarnedOnceForServiceWithoutHost == false) {
+ globals.directorWarnedOnceForServiceWithoutHost = true
+ log(
+ LogWarning,
+ "config",
+ "Director: Custom Variable Overrides will not work in this Icinga 2 version. See Director issue #1579"
+ )
+ }
+ }
+
+ template Service DirectorOverrideTemplate {
+ /**
+ * Seems that host is missing when used in a service object, works fine for
+ * apply rules
+ */
+ if (! host) {
+ var host = get_host(host_name)
+ }
+ if (! host) {
+ globals.directorWarnOnceForServiceWithoutHost()
+ }
+
+ if (vars) {
+ vars += host.vars[DirectorOverrideVars][name]
+ } else {
+ vars = host.vars[DirectorOverrideVars][name]
+ }
+ }
+}
+',
+ $settings->override_services_templatename,
+ $settings->override_services_varname
+ );
+ }
+
+ /**
+ * @param string $checksum
+ *
+ * @throws NotFoundError
+ *
+ * @return self
+ */
+ protected function loadFromDb($checksum)
+ {
+ $query = $this->db->select()->from(
+ self::$table,
+ array('checksum', 'last_activity_checksum', 'duration')
+ )->where('checksum = ?', $this->dbBin($checksum));
+ $result = $this->db->fetchRow($query);
+
+ if (empty($result)) {
+ throw new NotFoundError('Got no config for %s', bin2hex($checksum));
+ }
+
+ $this->checksum = $this->connection->binaryDbResult($result->checksum);
+ $this->generationTime = $result->duration;
+ $this->lastActivityChecksum = $this->connection->binaryDbResult($result->last_activity_checksum);
+
+ $query = $this->db->select()->from(
+ array('cf' => 'director_generated_config_file'),
+ array(
+ 'file_path' => 'cf.file_path',
+ 'checksum' => 'f.checksum',
+ 'content' => 'f.content',
+ 'cnt_object' => 'f.cnt_object',
+ 'cnt_template' => 'f.cnt_template',
+ 'cnt_apply' => 'f.cnt_apply',
+ )
+ )->join(
+ array('f' => 'director_generated_file'),
+ 'cf.file_checksum = f.checksum',
+ array()
+ )->where('cf.config_checksum = ?', $this->dbBin($checksum));
+
+ foreach ($this->db->fetchAll($query) as $row) {
+ $file = new IcingaConfigFile();
+ $this->files[$row->file_path] = $file
+ ->setContent($row->content)
+ ->setObjectCount($row->cnt_object)
+ ->setTemplateCount($row->cnt_template)
+ ->setApplyCount($row->cnt_apply);
+ }
+
+ return $this;
+ }
+
+ protected function createFileFromDb($type)
+ {
+ /** @var IcingaObject $class */
+ $class = 'Icinga\\Module\\Director\\Objects\\Icinga' . ucfirst($type);
+ Benchmark::measure(sprintf('Prefetching %s', $type));
+ $objects = $class::prefetchAll($this->connection);
+ return $this->createFileForObjects($type, $objects);
+ }
+
+ /**
+ * @param string $type Short object type, like 'service' or 'zone'
+ * @param IcingaObject[] $objects
+ *
+ * @return self
+ */
+ protected function createFileForObjects($type, $objects)
+ {
+ if (empty($objects)) {
+ return $this;
+ }
+
+ Benchmark::measure(sprintf('Generating %ss: %s', $type, count($objects)));
+ foreach ($objects as $object) {
+ if ($object->isExternal()) {
+ if ($type === 'zone') {
+ $this->zoneMap[$object->get('id')] = $object->getObjectName();
+ }
+ }
+ $object->renderToConfig($this);
+ }
+
+ Benchmark::measure(sprintf('%ss done', $type));
+ return $this;
+ }
+
+ protected function typeWantsGlobalZone($type)
+ {
+ $types = array(
+ 'command',
+ );
+
+ return in_array($type, $types);
+ }
+
+ protected function typeWantsMasterZone($type)
+ {
+ $types = array(
+ 'host',
+ 'hostGroup',
+ 'service',
+ 'serviceGroup',
+ 'endpoint',
+ 'user',
+ 'userGroup',
+ 'timePeriod',
+ 'notification',
+ 'dependency'
+ );
+
+ return in_array($type, $types);
+ }
+
+ /**
+ * @param string $name Relative config file name
+ * @param string $suffix Config file suffix, defaults to '.conf'
+ *
+ * @return IcingaConfigFile
+ */
+ public function configFile($name, $suffix = '.conf')
+ {
+ $filename = $name . $suffix;
+ if (! array_key_exists($filename, $this->files)) {
+ $this->files[$filename] = new IcingaConfigFile();
+ }
+
+ return $this->files[$filename];
+ }
+
+ protected function collectExtraFiles()
+ {
+ /** @var ShipConfigFilesHook $hook */
+ foreach (Hook::all('Director\\ShipConfigFiles') as $hook) {
+ foreach ($hook->fetchFiles() as $filename => $file) {
+ if (array_key_exists($filename, $this->files)) {
+ throw new LogicException(sprintf(
+ 'Cannot ship one file twice: %s',
+ $filename
+ ));
+ }
+ if ($file instanceof IcingaConfigFile) {
+ $this->files[$filename] = $file;
+ } else {
+ $this->configFile($filename, '')->setContent((string) $file);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ public function getLastActivityHexChecksum()
+ {
+ return bin2hex($this->getLastActivityChecksum());
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getLastActivityChecksum()
+ {
+ if ($this->lastActivityChecksum === null) {
+ $query = $this->db->select()
+ ->from('director_activity_log', 'checksum')
+ ->order('id DESC')
+ ->limit(1);
+
+ $this->lastActivityChecksum = $this->db->fetchOne($query);
+
+ // PgSQL workaround:
+ if (is_resource($this->lastActivityChecksum)) {
+ $this->lastActivityChecksum = stream_get_contents($this->lastActivityChecksum);
+ }
+ }
+
+ return $this->lastActivityChecksum;
+ }
+
+ protected function renderLegacyDefaultNotification()
+ {
+ return preg_replace('~^ {12}~m', '', '
+ #
+ # Default objects to avoid warnings
+ #
+
+ define contact {
+ contact_name icingaadmin
+ alias Icinga Admin
+ host_notifications_enabled 0
+ host_notification_commands notify-never-default
+ host_notification_period notification_none
+ service_notifications_enabled 0
+ service_notification_commands notify-never-default
+ service_notification_period notification_none
+ }
+
+ define contactgroup {
+ contactgroup_name icingaadmins
+ members icingaadmin
+ }
+
+ define timeperiod {
+ timeperiod_name notification_none
+ alias No Notifications
+ }
+
+ define command {
+ command_name notify-never-default
+ command_line /bin/echo "NOOP"
+ }
+ ');
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfigFile.php b/library/Director/IcingaConfig/IcingaConfigFile.php
new file mode 100644
index 0000000..109eb8a
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfigFile.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Util;
+
+class IcingaConfigFile
+{
+ public static $table = 'director_generated_file';
+
+ public static $keyName = 'checksum';
+
+ protected $content;
+
+ protected $checksum;
+
+ protected $cntObject = 0;
+
+ protected $cntTemplate = 0;
+
+ protected $cntApply = 0;
+
+ /**
+ * @param $content
+ *
+ * @return self
+ */
+ public function prepend($content)
+ {
+ $this->content = $content . $this->content;
+ $this->checksum = null;
+ return $this;
+ }
+
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ public function setContent($content)
+ {
+ $this->content = $content;
+ $this->checksum = null;
+ return $this;
+ }
+
+ public function addContent($content)
+ {
+ if ($this->content === null) {
+ $this->content = $content;
+ } else {
+ $this->content .= $content;
+ }
+ $this->checksum = null;
+ return $this;
+ }
+
+ public function getObjectCount()
+ {
+ return $this->cntObject;
+ }
+
+ public function getTemplateCount()
+ {
+ return $this->cntTemplate;
+ }
+
+ public function getApplyCount()
+ {
+ return $this->cntApply;
+ }
+
+ public function getSize()
+ {
+ return strlen($this->content);
+ }
+
+ public function setObjectCount($cnt)
+ {
+ $this->cntObject = $cnt;
+ return $this;
+ }
+
+ public function setTemplateCount($cnt)
+ {
+ $this->cntTemplate = $cnt;
+ return $this;
+ }
+
+ public function setApplyCount($cnt)
+ {
+ $this->cntApply = $cnt;
+ return $this;
+ }
+
+ public function getHexChecksum()
+ {
+ return bin2hex($this->getChecksum());
+ }
+
+ public function getChecksum()
+ {
+ if ($this->checksum === null) {
+ $this->checksum = sha1($this->content, true);
+ }
+
+ return $this->checksum;
+ }
+
+ public function addLegacyObjects($objects)
+ {
+ foreach ($objects as $object) {
+ $this->addLegacyObject($object);
+ }
+
+ return $this;
+ }
+
+ public function addObjects($objects)
+ {
+ foreach ($objects as $object) {
+ $this->addObject($object);
+ }
+
+ return $this;
+ }
+
+ public function addObject(IcingaObject $object)
+ {
+ $this->content .= $object->toConfigString();
+ $this->checksum = null;
+ return $this->addObjectStats($object);
+ }
+
+ public function addLegacyObject(IcingaObject $object)
+ {
+ $this->content .= $object->toLegacyConfigString();
+ $this->checksum = null;
+ return $this->addObjectStats($object);
+ }
+
+ protected function addObjectStats(IcingaObject $object)
+ {
+ if ($object->hasProperty('object_type')) {
+ $type = $object->object_type;
+
+ switch ($type) {
+ case 'object':
+ $this->cntObject++;
+ break;
+ case 'template':
+ $this->cntTemplate++;
+ break;
+ case 'apply':
+ $this->cntApply++;
+ break;
+ }
+ }
+
+ return $this;
+ }
+
+ public function __toString()
+ {
+ return $this->getContent();
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfigHelper.php b/library/Director/IcingaConfig/IcingaConfigHelper.php
new file mode 100644
index 0000000..03c017e
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfigHelper.php
@@ -0,0 +1,430 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use InvalidArgumentException;
+use function ctype_digit;
+use function explode;
+use function floor;
+use function implode;
+use function preg_match;
+use function preg_split;
+use function sprintf;
+use function strlen;
+use function strpos;
+use function substr;
+
+class IcingaConfigHelper
+{
+ /**
+ * Reserved words according to
+ * https://icinga.com/docs/icinga2/latest/doc/17-language-reference/#reserved-keywords
+ */
+ protected static $reservedWords = [
+ 'object',
+ 'template',
+ 'include',
+ 'include_recursive',
+ 'include_zones',
+ 'library',
+ 'null',
+ 'true',
+ 'false',
+ 'const',
+ 'var',
+ 'this',
+ 'globals',
+ 'locals',
+ 'use',
+ 'default',
+ 'ignore_on_error',
+ 'current_filename',
+ 'current_line',
+ 'apply',
+ 'to',
+ 'where',
+ 'import',
+ 'assign',
+ 'ignore',
+ 'function',
+ 'return',
+ 'break',
+ 'continue',
+ 'for',
+ 'if',
+ 'else',
+ 'while',
+ 'throw',
+ 'try',
+ 'except',
+ 'in',
+ 'using',
+ 'namespace',
+ ];
+
+ public static function renderKeyValue($key, $value, $prefix = ' ')
+ {
+ return self::renderKeyOperatorValue($key, '=', $value, $prefix);
+ }
+
+ public static function renderKeyOperatorValue($key, $operator, $value, $prefix = ' ')
+ {
+ $string = sprintf(
+ "%s %s %s",
+ $key,
+ $operator,
+ $value
+ );
+
+ if ($prefix && strpos($string, "\n") !== false) {
+ return $prefix . implode("\n" . $prefix, explode("\n", $string)) . "\n";
+ }
+
+ return $prefix . $string . "\n";
+ }
+
+ public static function renderBoolean($value)
+ {
+ if ($value === 'y' || $value === true) {
+ return 'true';
+ }
+ if ($value === 'n' || $value === false) {
+ return 'false';
+ }
+
+ throw new InvalidArgumentException(sprintf(
+ '%s is not a valid boolean',
+ $value
+ ));
+ }
+
+ protected static function renderInteger($value)
+ {
+ return (string) $value;
+ }
+
+ public static function renderFloat($value)
+ {
+ // Render .0000 floats as integers, mainly because of some JSON
+ // implementations:
+ if ((string) (int) $value === (string) $value) {
+ return static::renderInteger((int) $value);
+ }
+
+ return sprintf('%F', $value);
+ }
+
+ protected static function renderNull()
+ {
+ return 'null';
+ }
+
+ // TODO: Find out how to allow multiline {{{...}}} strings.
+ // Parameter? Dedicated method? Always if \n is found?
+ public static function renderString($string)
+ {
+ $special = [
+ '/\\\/',
+ '/"/',
+ '/\$/',
+ '/\t/',
+ '/\r/',
+ '/\n/',
+ // '/\b/', -> doesn't work
+ '/\f/',
+ ];
+
+ $replace = [
+ '\\\\\\',
+ '\\"',
+ '\\$',
+ '\\t',
+ '\\r',
+ '\\n',
+ // '\\b',
+ '\\f',
+ ];
+
+ $string = preg_replace($special, $replace, $string);
+
+ return '"' . $string . '"';
+ }
+
+ public static function renderPhpValue($value)
+ {
+ if (is_null($value)) {
+ return static::renderNull();
+ }
+ if (is_bool($value)) {
+ return static::renderBoolean($value);
+ }
+ if (is_int($value)) {
+ return static::renderInteger($value);
+ }
+ if (is_float($value)) {
+ return static::renderFloat($value);
+ }
+ // TODO:
+ // if (is_object($value) || static::isAssocArray($value)) {
+ // return static::renderHash($value, $prefix)
+ // TODO: also check array
+ if (is_array($value)) {
+ return static::renderArray($value);
+ }
+ if (is_string($value)) {
+ return static::renderString($value);
+ }
+
+ throw new InvalidArgumentException(sprintf(
+ 'Unexpected type %s',
+ var_export($value, 1)
+ ));
+ }
+
+ public static function renderDictionaryKey($key)
+ {
+ if (preg_match('/^[a-z_]+[a-z0-9_]*$/i', $key)) {
+ return static::escapeIfReserved($key);
+ }
+
+ return static::renderString($key);
+ }
+
+ // Requires an array
+ public static function renderArray($array)
+ {
+ $data = [];
+ foreach ($array as $entry) {
+ if ($entry instanceof IcingaConfigRenderer) {
+ $data[] = $entry;
+ } else {
+ $data[] = self::renderString($entry);
+ }
+ }
+
+ return static::renderEscapedArray($data);
+ }
+
+ public static function renderEscapedArray($array)
+ {
+ $str = '[ ' . implode(', ', $array) . ' ]';
+
+ if (strlen($str) < 60) {
+ return $str;
+ }
+
+ // Prefix for toConfigString?
+ return "[\n " . implode(",\n ", $array) . "\n]";
+ }
+
+ public static function renderDictionary($dictionary)
+ {
+ $values = [];
+ foreach ($dictionary as $key => $value) {
+ $values[$key] = rtrim(
+ self::renderKeyValue(
+ self::renderDictionaryKey($key),
+ $value
+ )
+ );
+ }
+
+ if (empty($values)) {
+ return '{}';
+ }
+ ksort($values, SORT_STRING);
+
+ // Prefix for toConfigString?
+ return "{\n" . implode("\n", $values) . "\n}";
+ }
+
+ public static function renderExpression($string)
+ {
+ return "{{\n " . $string . "\n}}";
+ }
+
+ public static function alreadyRendered($string)
+ {
+ return new IcingaConfigRendered($string);
+ }
+
+ public static function isReserved($string)
+ {
+ return in_array($string, self::$reservedWords, true);
+ }
+
+ public static function escapeIfReserved($string)
+ {
+ if (self::isReserved($string)) {
+ return '@' . $string;
+ }
+
+ return $string;
+ }
+
+ public static function isValidInterval($interval)
+ {
+ if (ctype_digit($interval)) {
+ return true;
+ }
+
+ $parts = preg_split('/\s+/', $interval, -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($parts as $part) {
+ if (! preg_match('/^(\d+)([dhms]?)$/', $part)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static function parseInterval($interval)
+ {
+ if ($interval === null || $interval === '') {
+ return null;
+ }
+
+ if (is_int($interval) || ctype_digit($interval)) {
+ return (int) $interval;
+ }
+
+ $parts = preg_split('/\s+/', $interval, -1, PREG_SPLIT_NO_EMPTY);
+ $value = 0;
+ foreach ($parts as $part) {
+ if (! preg_match('/^(\d+)([dhms]?)$/', $part, $m)) {
+ throw new InvalidArgumentException(sprintf(
+ '"%s" is not a valid time (duration) definition',
+ $interval
+ ));
+ }
+
+ switch ($m[2]) {
+ case 'd':
+ $value += $m[1] * 86400;
+ break;
+ case 'h':
+ $value += $m[1] * 3600;
+ break;
+ case 'm':
+ $value += $m[1] * 60;
+ break;
+ default:
+ $value += (int) $m[1];
+ }
+ }
+
+ return $value;
+ }
+
+ public static function renderInterval($interval)
+ {
+ // TODO: compat only, do this at munge time. All db fields should be int
+ $seconds = self::parseInterval($interval);
+ if ($seconds === 0) {
+ return '0s';
+ }
+
+ $steps = [
+ 'd' => 86400,
+ 'h' => 3600,
+ 'm' => 60,
+ ];
+
+ foreach ($steps as $unit => $duration) {
+ if ($seconds % $duration === 0) {
+ return (int) floor($seconds / $duration) . $unit;
+ }
+ }
+
+ return $seconds . 's';
+ }
+
+ public static function stringHasMacro($string, $macroName = null)
+ {
+ $len = strlen($string);
+ $start = false;
+ // TODO: robust UTF8 support. It works, but it is not 100% correct
+ for ($i = 0; $i < $len; $i++) {
+ if ($string[$i] === '$') {
+ if ($start === false) {
+ $start = $i;
+ } else {
+ // Escaping, $$
+ if ($start + 1 === $i) {
+ $start = false;
+ } else {
+ if ($macroName === null) {
+ return true;
+ }
+ if ($macroName === substr($string, $start + 1, $i - $start - 1)) {
+ return true;
+ }
+
+ $start = false;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Hint: this isn't complete, but let's restrict ourselves right now
+ *
+ * @param $name
+ * @return bool
+ */
+ public static function isValidMacroName($name)
+ {
+ return preg_match('/^[A-z_][A-z_.\d]+$/', $name)
+ && ! preg_match('/\.$/', $name);
+ }
+
+ public static function renderStringWithVariables($string, array $whiteList = null)
+ {
+ $len = strlen($string);
+ $start = false;
+ $parts = [];
+ // TODO: UTF8...
+ $offset = 0;
+ for ($i = 0; $i < $len; $i++) {
+ if ($string[$i] === '$') {
+ if ($start === false) {
+ $start = $i;
+ } else {
+ // Ignore $$
+ if ($start + 1 === $i) {
+ $start = false;
+ } else {
+ // We got a macro
+ $macroName = substr($string, $start + 1, $i - $start - 1);
+ if (static::isValidMacroName($macroName)) {
+ if ($whiteList === null || in_array($macroName, $whiteList)) {
+ if ($start > $offset) {
+ $parts[] = static::renderString(
+ substr($string, $offset, $start - $offset)
+ );
+ }
+ $parts[] = $macroName;
+ $offset = $i + 1;
+ }
+ }
+
+ $start = false;
+ }
+ }
+ }
+ }
+
+ if ($offset < $i) {
+ $parts[] = static::renderString(substr($string, $offset, $i - $offset));
+ }
+
+ if (! empty($parts)) {
+ return implode(' + ', $parts);
+ }
+
+ return '""';
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfigRendered.php b/library/Director/IcingaConfig/IcingaConfigRendered.php
new file mode 100644
index 0000000..90b710e
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfigRendered.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use InvalidArgumentException;
+
+class IcingaConfigRendered implements IcingaConfigRenderer
+{
+ protected $rendered;
+
+ public function __construct($string)
+ {
+ if (! is_string($string)) {
+ throw new InvalidArgumentException('IcingaConfigRendered accepts only strings');
+ }
+
+ $this->rendered = $string;
+ }
+
+ public function toConfigString()
+ {
+ return $this->rendered;
+ }
+
+ public function __toString()
+ {
+ return $this->toConfigString();
+ }
+
+ public function toLegacyConfigString()
+ {
+ return $this->rendered;
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfigRenderer.php b/library/Director/IcingaConfig/IcingaConfigRenderer.php
new file mode 100644
index 0000000..108956d
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfigRenderer.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+interface IcingaConfigRenderer
+{
+ public function toConfigString();
+ public function toLegacyConfigString();
+ public function __toString();
+}
diff --git a/library/Director/IcingaConfig/IcingaLegacyConfigHelper.php b/library/Director/IcingaConfig/IcingaLegacyConfigHelper.php
new file mode 100644
index 0000000..38d93ee
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaLegacyConfigHelper.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use InvalidArgumentException;
+
+class IcingaLegacyConfigHelper
+{
+ public static function renderKeyValue($key, $value, $prefix = ' ')
+ {
+ return self::renderKeyOperatorValue($key, "\t", $value, $prefix);
+ }
+
+ public static function renderKeyOperatorValue($key, $operator, $value, $prefix = ' ')
+ {
+ $string = sprintf(
+ "%s%s%s",
+ $key,
+ $operator,
+ $value
+ );
+
+ if ($prefix && strpos($string, "\n") !== false) {
+ return $prefix . implode("\n" . $prefix, explode("\n", $string)) . "\n";
+ }
+
+ return $prefix . $string . "\n";
+ }
+
+ public static function renderBoolean($value)
+ {
+ if ($value === 'y') {
+ return '1';
+ } elseif ($value === 'n') {
+ return '0';
+ } else {
+ throw new InvalidArgumentException('%s is not a valid boolean', $value);
+ }
+ }
+
+ // TODO: Double-check legacy "encoding"
+ public static function renderString($string)
+ {
+ $special = [
+ '/\\\/',
+ '/\$/',
+ '/\t/',
+ '/\r/',
+ '/\n/',
+ // '/\b/', -> doesn't work
+ '/\f/',
+ ];
+
+ $replace = [
+ '\\\\\\',
+ '\\$',
+ '\\t',
+ '\\r',
+ '\\n',
+ // '\\b',
+ '\\f',
+ ];
+
+ $string = preg_replace($special, $replace, $string);
+
+ return $string;
+ }
+
+ /**
+ * @param array $array
+ * @return string
+ */
+ public static function renderArray($array)
+ {
+ $data = [];
+ foreach ($array as $entry) {
+ if ($entry instanceof IcingaConfigRenderer) {
+ // $data[] = $entry;
+ $data[] = 'INVALID_ARRAY_MEMBER';
+ } else {
+ $data[] = self::renderString($entry);
+ }
+ }
+
+ return implode(', ', $data);
+ }
+
+ public static function renderDictionary($dictionary)
+ {
+ return 'INVALID_DICTIONARY';
+ }
+
+ public static function renderExpression($string)
+ {
+ return 'INVALID_EXPRESSION';
+ }
+
+ public static function alreadyRendered($string)
+ {
+ return new IcingaConfigRendered($string);
+ }
+
+ public static function renderInterval($interval)
+ {
+ if ($interval < 60) {
+ $interval = 60;
+ }
+ return $interval / 60;
+ }
+}
diff --git a/library/Director/IcingaConfig/StateFilterSet.php b/library/Director/IcingaConfig/StateFilterSet.php
new file mode 100644
index 0000000..7a2daec
--- /dev/null
+++ b/library/Director/IcingaConfig/StateFilterSet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+class StateFilterSet extends ExtensibleSet
+{
+ protected $allowedValues = array(
+ 'Up',
+ 'Down',
+ 'OK',
+ 'Warning',
+ 'Critical',
+ 'Unknown',
+ );
+
+ public function enumAllowedValues()
+ {
+ return array(
+ $this->translate('Hosts') => array(
+ 'Up' => $this->translate('Up'),
+ 'Down' => $this->translate('Down')
+ ),
+ $this->translate('Services') => array(
+ 'OK' => $this->translate('OK'),
+ 'Warning' => $this->translate('Warning'),
+ 'Critical' => $this->translate('Critical'),
+ 'Unknown' => $this->translate('Unknown'),
+ ),
+ );
+ }
+}
diff --git a/library/Director/IcingaConfig/TypeFilterSet.php b/library/Director/IcingaConfig/TypeFilterSet.php
new file mode 100644
index 0000000..dffd4cf
--- /dev/null
+++ b/library/Director/IcingaConfig/TypeFilterSet.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+class TypeFilterSet extends ExtensibleSet
+{
+ protected $allowedValues = array(
+ 'Problem',
+ 'Recovery',
+ 'Custom',
+ 'Acknowledgement',
+ 'DowntimeStart',
+ 'DowntimeEnd',
+ 'DowntimeRemoved',
+ 'FlappingStart',
+ 'FlappingEnd',
+ );
+
+ public function enumAllowedValues()
+ {
+ return array(
+ $this->translate('State changes') => array(
+ 'Problem' => $this->translate('Problem'),
+ 'Recovery' => $this->translate('Recovery'),
+ 'Custom' => $this->translate('Custom notification'),
+ ),
+ $this->translate('Problem handling') => array(
+ 'Acknowledgement' => $this->translate('Acknowledgement'),
+ 'DowntimeStart' => $this->translate('Downtime starts'),
+ 'DowntimeEnd' => $this->translate('Downtime ends'),
+ 'DowntimeRemoved' => $this->translate('Downtime removed'),
+ ),
+ $this->translate('Flapping') => array(
+ 'FlappingStart' => $this->translate('Flapping starts'),
+ 'FlappingEnd' => $this->translate('Flapping ends')
+ )
+ );
+ }
+}
diff --git a/library/Director/Import/Import.php b/library/Director/Import/Import.php
new file mode 100644
index 0000000..f82454d
--- /dev/null
+++ b/library/Director/Import/Import.php
@@ -0,0 +1,481 @@
+<?php
+
+namespace Icinga\Module\Director\Import;
+
+use Exception;
+use Icinga\Application\Benchmark;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Data\RecursiveUtf8Validator;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Util;
+use stdClass;
+
+class Import
+{
+ /**
+ * @var ImportSource
+ */
+ protected $source;
+
+ /**
+ * @var Db
+ */
+ protected $connection;
+
+ /**
+ * @var \Zend_Db_Adapter_Abstract
+ */
+ protected $db;
+
+ /**
+ * Raw data that should be imported, array of stdClass objects
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * Checksum of the rowset that should be imported
+ *
+ * @var string
+ */
+ private $rowsetChecksum;
+
+ /**
+ * Checksum-indexed rows
+ *
+ * @var array
+ */
+ private $rows;
+
+ /**
+ * Checksum-indexed row -> property
+ *
+ * @var array
+ */
+ private $rowProperties;
+
+ /**
+ * Whether this rowset exists, for caching purposes
+ *
+ * @var boolean
+ */
+ private $rowsetExists;
+
+ protected $properties = array();
+
+ /**
+ * Checksums of all rows
+ */
+ private $rowChecksums;
+
+ public function __construct(ImportSource $source)
+ {
+ $this->source = $source;
+ $this->connection = $source->getConnection();
+ $this->db = $this->connection->getDbAdapter();
+ }
+
+ /**
+ * Whether this import provides modified data
+ *
+ * @return boolean
+ */
+ public function providesChanges()
+ {
+ return ! $this->rowsetExists()
+ || ! $this->lastRowsetIs($this->rowsetChecksum());
+ }
+
+ /**
+ * Trigger an import run
+ *
+ * @return int Last import run ID
+ */
+ public function run()
+ {
+ if ($this->providesChanges() && ! $this->rowsetExists()) {
+ $this->storeRowset();
+ }
+
+ $this->db->insert(
+ 'import_run',
+ array(
+ 'source_id' => $this->source->get('id'),
+ 'rowset_checksum' => $this->quoteBinary($this->rowsetChecksum()),
+ 'start_time' => date('Y-m-d H:i:s'),
+ 'succeeded' => 'y'
+ )
+ );
+ if ($this->connection->isPgsql()) {
+ return $this->db->lastInsertId('import_run', 'id');
+ } else {
+ return $this->db->lastInsertId();
+ }
+ }
+
+ /**
+ * Whether there are no rows to be fetched from import source
+ *
+ * @return boolean
+ */
+ public function isEmpty()
+ {
+ $rows = $this->checksummedRows();
+ return empty($rows);
+ }
+
+ /**
+ * Checksum of all available rows
+ *
+ * @return string
+ */
+ protected function & rowsetChecksum()
+ {
+ if ($this->rowsetChecksum === null) {
+ $this->prepareChecksummedRows();
+ }
+
+ return $this->rowsetChecksum;
+ }
+
+ /**
+ * All rows
+ *
+ * @return array
+ */
+ protected function & checksummedRows()
+ {
+ if ($this->rows === null) {
+ $this->prepareChecksummedRows();
+ }
+
+ return $this->rows;
+ }
+
+ /**
+ * Checksum of all available rows
+ *
+ * @return array
+ */
+ protected function & rawData()
+ {
+ if ($this->data === null) {
+ $this->data = ImportSourceHook::forImportSource(
+ $this->source
+ )->fetchData();
+ Benchmark::measure('Fetched all data from Import Source');
+ $this->source->applyModifiers($this->data);
+ Benchmark::measure('Applied Property Modifiers to imported data');
+ }
+
+ return $this->data;
+ }
+
+ /**
+ * Prepare and remember an ImportedProperty
+ *
+ * @param string $key
+ * @param mixed $rawValue
+ *
+ * @return array
+ */
+ protected function prepareImportedProperty($key, $rawValue)
+ {
+ if (is_array($rawValue) || is_bool($rawValue) || is_int($rawValue) || is_float($rawValue)) {
+ $value = json_encode($rawValue);
+ $format = 'json';
+ } elseif ($rawValue instanceof stdClass) {
+ $value = json_encode($this->sortObject($rawValue));
+ $format = 'json';
+ } else {
+ $value = $rawValue;
+ $format = 'string';
+ }
+
+ $checksum = sha1(sprintf('%s=(%s)%s', $key, $format, $value), true);
+
+ if (! array_key_exists($checksum, $this->properties)) {
+ $this->properties[$checksum] = array(
+ 'checksum' => $this->quoteBinary($checksum),
+ 'property_name' => $key,
+ 'property_value' => $value,
+ 'format' => $format
+ );
+ }
+
+ return $this->properties[$checksum];
+ }
+
+ /**
+ * Walk through each row, prepare properties and calculate checksums
+ */
+ protected function prepareChecksummedRows()
+ {
+ $keyColumn = $this->source->get('key_column');
+ $this->rows = array();
+ $this->rowProperties = array();
+ $objects = array();
+ $rowCount = 0;
+
+ foreach ($this->rawData() as $row) {
+ $rowCount++;
+
+ // Key column must be set
+ if (! isset($row->$keyColumn)) {
+ throw new IcingaException(
+ 'No key column "%s" in row %d',
+ $keyColumn,
+ $rowCount
+ );
+ }
+
+ $object_name = $row->$keyColumn;
+
+ // Check for name collision
+ if (array_key_exists($object_name, $objects)) {
+ throw new IcingaException(
+ 'Duplicate entry: %s',
+ $object_name
+ );
+ }
+
+ $rowChecksums = array();
+ $keys = array_keys((array) $row);
+ sort($keys);
+
+ foreach ($keys as $key) {
+ // TODO: Specify how to treat NULL values. Ignoring for now.
+ // One option might be to import null (checksum '(null)')
+ // and to provide a flag at sync time
+ if ($row->$key === null) {
+ continue;
+ }
+
+ $property = $this->prepareImportedProperty($key, $row->$key);
+ $rowChecksums[] = $property['checksum'];
+ }
+
+ $checksum = sha1($object_name . ';' . implode(';', $rowChecksums), true);
+ if (array_key_exists($checksum, $this->rows)) {
+ die('WTF, collision?');
+ }
+
+ $this->rows[$checksum] = array(
+ 'checksum' => $this->quoteBinary($checksum),
+ 'object_name' => $object_name
+ );
+
+ $this->rowProperties[$checksum] = $rowChecksums;
+
+ $objects[$object_name] = $checksum;
+ }
+
+ $this->rowChecksums = array_keys($this->rows);
+ $this->rowsetChecksum = sha1(implode(';', $this->rowChecksums), true);
+ return $this;
+ }
+
+ /**
+ * Store our new rowset
+ */
+ protected function storeRowset()
+ {
+ $db = $this->db;
+ $rowset = $this->rowsetChecksum();
+ $rows = $this->checksummedRows();
+
+ $db->beginTransaction();
+
+ try {
+ if ($this->isEmpty()) {
+ $newRows = array();
+ $newProperties = array();
+ } else {
+ $newRows = $this->newChecksums('imported_row', $this->rowChecksums);
+ $newProperties = $this->newChecksums('imported_property', array_keys($this->properties));
+ }
+
+ $db->insert('imported_rowset', array('checksum' => $this->quoteBinary($rowset)));
+
+ foreach ($newProperties as $checksum) {
+ $db->insert('imported_property', $this->properties[$checksum]);
+ }
+
+ foreach ($newRows as $row) {
+ try {
+ $db->insert('imported_row', $rows[$row]);
+ foreach ($this->rowProperties[$row] as $property) {
+ $db->insert('imported_row_property', array(
+ 'row_checksum' => $this->quoteBinary($row),
+ 'property_checksum' => $property
+ ));
+ }
+ } catch (Exception $e) {
+ throw new IcingaException(
+ "Error while storing a row for '%s' into database: %s",
+ $rows[$row]['object_name'],
+ $e->getMessage()
+ );
+ }
+ }
+
+ foreach (array_keys($rows) as $row) {
+ $db->insert(
+ 'imported_rowset_row',
+ array(
+ 'rowset_checksum' => $this->quoteBinary($rowset),
+ 'row_checksum' => $this->quoteBinary($row)
+ )
+ );
+ }
+
+ $db->commit();
+
+ $this->rowsetExists = true;
+ } catch (Exception $e) {
+ try {
+ $db->rollBack();
+ } catch (Exception $e) {
+ // Well...
+ }
+ // Eventually throws details for invalid UTF8 characters
+ RecursiveUtf8Validator::validateRows($this->data);
+ throw $e;
+ }
+ }
+
+ /**
+ * Whether the last run of this import matches the given checksum
+ *
+ * @param string $checksum Binary checksum
+ *
+ * @return bool
+ */
+ protected function lastRowsetIs($checksum)
+ {
+ return $this->connection->getLatestImportedChecksum($this->source->get('id'))
+ === bin2hex($checksum);
+ }
+
+ /**
+ * Whether our rowset already exists in the database
+ *
+ * @return boolean
+ */
+ protected function rowsetExists()
+ {
+ if (null === $this->rowsetExists) {
+ $this->rowsetExists = 0 === count(
+ $this->newChecksums(
+ 'imported_rowset',
+ array($this->rowsetChecksum())
+ )
+ );
+ }
+
+ return $this->rowsetExists;
+ }
+
+ /**
+ * Finde new checksums for a specific table
+ *
+ * Accepts an array of checksums and gives you an array with those checksums
+ * that are missing in the given table
+ *
+ * @param string $table Database table name
+ * @param array $checksums Array with the checksums that should be verified
+ *
+ * @return array
+ */
+ protected function newChecksums($table, $checksums)
+ {
+ $db = $this->db;
+
+ // TODO: The following is a quickfix for binary data corrpution reported
+ // in https://github.com/zendframework/zf1/issues/655 caused by
+ // https://github.com/zendframework/zf1/commit/2ac9c30f
+ //
+ // Should be reverted once fixed, eventually with a check continueing
+ // to use this workaround for specific ZF versions (1.12.16 and 1.12.17
+ // so far). Alternatively we could also use a custom quoteInto method.
+
+ // The former query looked as follows:
+ //
+ // $query = $db->select()->from($table, 'checksum')
+ // ->where('checksum IN (?)', $checksums)
+ // ...
+ // return array_diff($checksums, $existing);
+
+ $hexed = array_map('bin2hex', $checksums);
+
+ $conn = $this->connection;
+ $query = $db
+ ->select()
+ ->from(
+ array('c' => $table),
+ array('checksum' => $conn->dbHexFunc('c.checksum'))
+ )->where(
+ $conn->dbHexFunc('c.checksum') . ' IN (?)',
+ $hexed
+ );
+
+ $existing = $db->fetchCol($query);
+ $new = array_diff($hexed, $existing);
+
+ return array_map('hex2bin', $new);
+ }
+
+ /**
+ * Sort a given stdClass object by property name
+ *
+ * @param stdClass $object
+ *
+ * @return object
+ */
+ protected function sortObject($object)
+ {
+ $array = (array) $object;
+ foreach ($array as $key => $val) {
+ $this->sortElement($val);
+ }
+ ksort($array);
+ return (object) $array;
+ }
+
+ /**
+ * Walk through a given array and sort all children
+ *
+ * Please note that the array itself will NOT be sorted, as arrays must
+ * keep their ordering
+ *
+ * @param array $array
+ */
+ protected function sortArrayObject(&$array)
+ {
+ foreach ($array as $key => $val) {
+ $this->sortElement($val);
+ }
+ }
+
+ /**
+ * Recursively sort a given property
+ *
+ * @param mixed $el
+ */
+ protected function sortElement(&$el)
+ {
+ if (is_array($el)) {
+ $this->sortArrayObject($el);
+ } elseif ($el instanceof stdClass) {
+ $el = $this->sortObject($el);
+ }
+ }
+
+ protected function quoteBinary($bin)
+ {
+ return $this->connection->quoteBinary($bin);
+ }
+}
diff --git a/library/Director/Import/ImportSourceCoreApi.php b/library/Director/Import/ImportSourceCoreApi.php
new file mode 100644
index 0000000..6d590ec
--- /dev/null
+++ b/library/Director/Import/ImportSourceCoreApi.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Icinga\Module\Director\Import;
+
+use Icinga\Application\Config;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class ImportSourceCoreApi extends ImportSourceHook
+{
+ protected $connection;
+
+ protected $db;
+
+ protected $api;
+
+ public function fetchData()
+ {
+ $func = 'get' . $this->getSetting('object_type') . 'Objects';
+ $objects = $this->api()->$func();
+ $result = array();
+ foreach ($objects as $object) {
+ $result[] = $object->toPlainObject();
+ }
+
+ return $result;
+ }
+
+ public function listColumns()
+ {
+ $res = $this->fetchData();
+ if (empty($data)) {
+ return array('object_name');
+ }
+
+ return array_keys((array) $res[0]);
+ }
+
+ public static function getDefaultKeyColumnName()
+ {
+ return 'object_name';
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'object_type', array(
+ 'label' => 'Object type',
+ 'required' => true,
+ 'multiOptions' => $form->optionalEnum(self::enumObjectTypes($form))
+ ));
+ }
+
+ protected static function enumObjectTypes($form)
+ {
+ $types = array(
+ 'CheckCommand' => $form->translate('Check Commands'),
+ 'NotificationCommand' => $form->translate('Notification Commands'),
+ 'Endpoint' => $form->translate('Endpoints'),
+ 'Host' => $form->translate('Hosts'),
+ 'HostGroup' => $form->translate('Hostgroups'),
+ 'User' => $form->translate('Users'),
+ 'UserGroup' => $form->translate('Usergroups'),
+ 'Zone' => $form->translate('Zones'),
+ );
+
+ asort($types);
+ return $types;
+ }
+
+ protected function api()
+ {
+ if ($this->api === null) {
+ $endpoint = $this->db()->getDeploymentEndpoint();
+ $this->api = $endpoint->api()->setDb($this->db());
+ }
+
+ return $this->api;
+ }
+
+ protected function db()
+ {
+ if ($this->db === null) {
+ $resourceName = Config::module('director')->get('db', 'resource');
+ if ($resourceName) {
+ $this->db = Db::fromResourceName($resourceName);
+ }
+ }
+
+ return $this->db;
+ }
+}
diff --git a/library/Director/Import/ImportSourceDirectorObject.php b/library/Director/Import/ImportSourceDirectorObject.php
new file mode 100644
index 0000000..e3f56fc
--- /dev/null
+++ b/library/Director/Import/ImportSourceDirectorObject.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Icinga\Module\Director\Import;
+
+use Icinga\Application\Config;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Forms\ImportSourceForm;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Util;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class ImportSourceDirectorObject extends ImportSourceHook
+{
+ protected $db;
+
+ public function getName()
+ {
+ return 'Director Objects';
+ }
+
+ public static function getDefaultKeyColumnName()
+ {
+ return 'object_name';
+ }
+
+ public function fetchData()
+ {
+ $db = $this->db();
+ $objectClass = $this->getSetting('object_class');
+ $objectType = $this->getSetting('object_type');
+ /** @var IcingaObject $class fake type hint, it's a string */
+ $class = DbObjectTypeRegistry::classByType($objectClass);
+ if ($objectType) {
+ $dummy = $class::create();
+ $query = $db->getDbAdapter()->select()
+ ->from($dummy->getTableName())
+ ->where('object_type = ?', $objectType);
+ } else {
+ $query = null;
+ }
+ $result = [];
+ $resolved = $this->getSetting('resolved') === 'y';
+ foreach ($class::loadAllByType($objectClass, $db, $query) as $object) {
+ $result[] = $object->toPlainObject($resolved);
+ }
+ if ($objectClass === 'zone') {
+ $this->enrichZonesWithDeploymentZone($result);
+ }
+ return $result;
+ }
+
+ protected function enrichZonesWithDeploymentZone(&$zones)
+ {
+ $masterZone = $this->db()->getMasterZoneName();
+ foreach ($zones as $zone) {
+ $zone->is_master_zone = $zone->object_name === $masterZone;
+ }
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ /** @var ImportSourceForm $form */
+ Util::addDbResourceFormElement($form, 'resource');
+ $form->getElement('resource')
+ ->setValue(Config::module('director')->get('db', 'resource'));
+ $form->addElement('select', 'object_class', [
+ 'label' => $form->translate('Director Object'),
+ 'multiOptions' => [
+ 'host' => $form->translate('Host'),
+ 'endpoint' => $form->translate('Endpoint'),
+ 'zone' => $form->translate('Zone'),
+ ],
+ 'required' => true,
+ ]);
+ $form->addElement('select', 'object_type', [
+ 'label' => $form->translate('Object Type'),
+ 'multiOptions' => [
+ null => $form->translate('All Object Types'),
+ 'object' => $form->translate('Objects'),
+ 'template' => $form->translate('Templates'),
+ 'external_object' => $form->translate('External Objects'),
+ 'apply' => $form->translate('Apply Rules'),
+ ],
+ ]);
+
+ /** @var $form \Icinga\Module\Director\Web\Form\DirectorObjectForm */
+ $form->addBoolean('resolved', [
+ 'label' => $form->translate('Resolved'),
+ ], 'n');
+
+ return $form;
+ }
+
+ protected function db()
+ {
+ if ($this->db === null) {
+ $this->db = Db::fromResourceName($this->settings['resource']);
+ }
+
+ return $this->db;
+ }
+
+ public function listColumns()
+ {
+ $rows = $this->fetchData();
+ $columns = [];
+
+ foreach ($rows as $object) {
+ foreach (array_keys((array) $object) as $column) {
+ if (! isset($columns[$column])) {
+ $columns[] = $column;
+ }
+ }
+ }
+
+ return $columns;
+ }
+}
diff --git a/library/Director/Import/ImportSourceLdap.php b/library/Director/Import/ImportSourceLdap.php
new file mode 100644
index 0000000..4518565
--- /dev/null
+++ b/library/Director/Import/ImportSourceLdap.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Import;
+
+use Icinga\Data\ResourceFactory;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Util;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class ImportSourceLdap extends ImportSourceHook
+{
+ protected $connection;
+
+ public function fetchData()
+ {
+ $columns = $this->listColumns();
+ $query = $this->connection()
+ ->select()
+ ->setUsePagedResults()
+ ->from($this->settings['objectclass'], $columns);
+
+ if ($base = $this->settings['base']) {
+ $query->setBase($base);
+ }
+ if ($filter = $this->settings['filter']) {
+ $query->setNativeFilter($filter);
+ }
+
+ if (in_array('dn', $columns)) {
+ $result = $query->fetchAll();
+ foreach ($result as $dn => $row) {
+ $row->dn = $dn;
+ }
+
+ return $result;
+ } else {
+ return $query->fetchAll();
+ }
+ }
+
+ public function listColumns()
+ {
+ return preg_split('/,\s*/', $this->settings['query'], -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ Util::addLDAPResourceFormElement($form, 'resource');
+ $form->addElement('text', 'base', array(
+ 'label' => $form->translate('LDAP Search Base'),
+ 'description' => $form->translate(
+ 'Your LDAP search base. Often something like OU=Users,OU=HQ,DC=your,DC=company,DC=tld'
+ )
+ ));
+ $form->addElement('text', 'objectclass', array(
+ 'label' => $form->translate('Object class'),
+ 'description' => $form->translate(
+ 'An object class to search for. Might be "user", "group", "computer" or similar'
+ )
+ ));
+ $form->addElement('text', 'filter', array(
+ 'label' => 'LDAP filter',
+ 'description' => $form->translate(
+ 'A custom LDAP filter to use in addition to the object class. This allows'
+ . ' for a lot of flexibility but requires LDAP filter skills. Simple filters'
+ . ' might look as follows: operatingsystem=*server*'
+ )
+ ));
+ $form->addElement('textarea', 'query', array(
+ 'label' => $form->translate('Properties'),
+ 'description' => $form->translate(
+ 'The LDAP properties that should be fetched. This is required to be a'
+ . ' comma-separated list like: "cn, dnshostname, operatingsystem, sAMAccountName"'
+ ),
+ 'spellcheck' => 'false',
+ 'required' => true,
+ 'rows' => 5,
+ ));
+ return $form;
+ }
+
+ protected function connection()
+ {
+ if ($this->connection === null) {
+ $this->connection = ResourceFactory::create($this->settings['resource']);
+ }
+
+ return $this->connection;
+ }
+}
diff --git a/library/Director/Import/ImportSourceRestApi.php b/library/Director/Import/ImportSourceRestApi.php
new file mode 100644
index 0000000..dc772e1
--- /dev/null
+++ b/library/Director/Import/ImportSourceRestApi.php
@@ -0,0 +1,380 @@
+<?php
+
+namespace Icinga\Module\Director\Import;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\RestApi\RestApiClient;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use InvalidArgumentException;
+
+class ImportSourceRestApi extends ImportSourceHook
+{
+ public function getName()
+ {
+ return 'REST API';
+ }
+
+ public function fetchData()
+ {
+ $result = $this->getRestApi()->get(
+ $this->getUrl(),
+ null,
+ $this->buildHeaders()
+ );
+ $result = $this->extractProperty($result);
+
+ return (array) $result;
+ }
+
+ public function listColumns()
+ {
+ $rows = $this->fetchData();
+ $columns = [];
+
+ foreach ($rows as $object) {
+ foreach (array_keys((array) $object) as $column) {
+ if (! isset($columns[$column])) {
+ $columns[] = $column;
+ }
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Extract result from a property specified
+ *
+ * A simple key, like "objects", will take the final result from key objects
+ *
+ * If you have a deeper key like "objects" under the key "results", specify this as "results.objects".
+ *
+ * When a key of the JSON object contains a literal ".", this can be escaped as
+ *
+ * @param $result
+ *
+ * @return mixed
+ */
+ protected function extractProperty($result)
+ {
+ $property = $this->getSetting('extract_property');
+ if (! $property) {
+ return $result;
+ }
+
+ $parts = preg_split('~(?<!\\\\)\.~', $property);
+
+ // iterate over parts of the attribute path
+ $data = $result;
+ foreach ($parts as $part) {
+ // un-escape any dots
+ $part = preg_replace('~\\\\.~', '.', $part);
+
+ if (property_exists($data, $part)) {
+ $data = $data->$part;
+ } else {
+ throw new \RuntimeException(sprintf(
+ 'Result has no "%s" property. Available keys: %s',
+ $part,
+ implode(', ', array_keys((array) $data))
+ ));
+ }
+ }
+
+ return $data;
+ }
+
+ protected function buildHeaders()
+ {
+ $headers = [];
+
+ $text = $this->getSetting('headers', '');
+ foreach (preg_split('~\r?\n~', $text, -1, PREG_SPLIT_NO_EMPTY) as $header) {
+ $header = trim($header);
+ $parts = preg_split('~\s*:\s*~', $header, 2);
+ if (count($parts) < 2) {
+ throw new InvalidPropertyException('Could not parse header: "%s"', $header);
+ }
+
+ $headers[$parts[0]] = $parts[1];
+ }
+
+ return $headers;
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ static::addScheme($form);
+ static::addSslOptions($form);
+ static::addUrl($form);
+ static::addResultProperty($form);
+ static::addAuthentication($form);
+ static::addHeader($form);
+ static::addProxy($form);
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addScheme(QuickForm $form)
+ {
+ $form->addElement('select', 'scheme', [
+ 'label' => $form->translate('Protocol'),
+ 'description' => $form->translate(
+ 'Whether to use encryption when talking to the REST API'
+ ),
+ 'multiOptions' => [
+ 'HTTPS' => $form->translate('HTTPS (strongly recommended)'),
+ 'HTTP' => $form->translate('HTTP (this is plaintext!)'),
+ ],
+ 'class' => 'autosubmit',
+ 'value' => 'HTTPS',
+ 'required' => true,
+ ]);
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addHeader(QuickForm $form)
+ {
+ $form->addElement('textarea', 'headers', [
+ 'label' => $form->translate('HTTP Header'),
+ 'description' => implode(' ', [
+ $form->translate('Additional headers for the HTTP request.'),
+ $form->translate('Specify headers in text format "Header: Value", each header on a new line.'),
+ ]),
+ 'class' => 'preformatted',
+ 'rows' => 4,
+ ]);
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addSslOptions(QuickForm $form)
+ {
+ $ssl = ! ($form->getSentOrObjectSetting('scheme', 'HTTPS') === 'HTTP');
+
+ if ($ssl) {
+ static::addBoolean($form, 'ssl_verify_peer', [
+ 'label' => $form->translate('Verify Peer'),
+ 'description' => $form->translate(
+ 'Whether we should check that our peer\'s certificate has'
+ . ' been signed by a trusted CA. This is strongly recommended.'
+ )
+ ], 'y');
+ static::addBoolean($form, 'ssl_verify_host', [
+ 'label' => $form->translate('Verify Host'),
+ 'description' => $form->translate(
+ 'Whether we should check that the certificate matches the'
+ . 'configured host'
+ )
+ ], 'y');
+ }
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addUrl(QuickForm $form)
+ {
+ $form->addElement('text', 'url', [
+ 'label' => 'REST API URL',
+ 'description' => $form->translate(
+ 'Something like https://api.example.com/rest/v2/objects'
+ ),
+ 'required' => true,
+ ]);
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addResultProperty(QuickForm $form)
+ {
+ $form->addElement('text', 'extract_property', [
+ 'label' => 'Extract property',
+ 'description' => implode("\n", [
+ $form->translate('Often the expected result is provided in a property like "objects".'
+ . ' Please specify this if required.'),
+ $form->translate('Also deeper keys can be specific by a dot-notation:'),
+ '"result.objects", "key.deeper_key.very_deep"',
+ $form->translate('Literal dots in a key name can be written in the escape notation:'),
+ '"key\.with\.dots"',
+ ])
+ ]);
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addAuthentication(QuickForm $form)
+ {
+ $form->addElement('text', 'username', [
+ 'label' => $form->translate('Username'),
+ 'description' => $form->translate(
+ 'Will be used to authenticate against your REST API'
+ ),
+ ]);
+
+ $form->addElement('storedPassword', 'password', [
+ 'label' => $form->translate('Password'),
+ ]);
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addProxy(QuickForm $form)
+ {
+ $form->addElement('select', 'proxy_type', [
+ 'label' => $form->translate('Proxy'),
+ 'description' => $form->translate(
+ 'In case your API is only reachable through a proxy, please'
+ . ' choose it\'s protocol right here'
+ ),
+ 'multiOptions' => $form->optionalEnum([
+ 'HTTP' => $form->translate('HTTP proxy'),
+ 'SOCKS5' => $form->translate('SOCKS5 proxy'),
+ ]),
+ 'class' => 'autosubmit'
+ ]);
+
+ $proxyType = $form->getSentOrObjectSetting('proxy_type');
+
+ if ($proxyType) {
+ $form->addElement('text', 'proxy', [
+ 'label' => $form->translate('Proxy Address'),
+ 'description' => $form->translate(
+ 'Hostname, IP or <host>:<port>'
+ ),
+ 'required' => true,
+ ]);
+ if ($proxyType === 'HTTP') {
+ $form->addElement('text', 'proxy_user', [
+ 'label' => $form->translate('Proxy Username'),
+ 'description' => $form->translate(
+ 'In case your proxy requires authentication, please'
+ . ' configure this here'
+ ),
+ ]);
+
+ $passRequired = strlen($form->getSentOrObjectSetting('proxy_user')) > 0;
+
+ $form->addElement('storedPassword', 'proxy_pass', [
+ 'label' => $form->translate('Proxy Password'),
+ 'required' => $passRequired
+ ]);
+ }
+ }
+ }
+
+ protected function getUrl()
+ {
+ $url = $this->getSetting('url');
+ $parts = \parse_url($url);
+ if (isset($parts['path'])) {
+ $path = $parts['path'];
+ } else {
+ $path = '/';
+ }
+
+ if (isset($parts['query'])) {
+ $url = "$path?" . $parts['query'];
+ } else {
+ $url = $path;
+ }
+
+ return $url;
+ }
+
+ protected function getRestApi()
+ {
+ $url = $this->getSetting('url');
+ $parts = \parse_url($url);
+ if (isset($parts['host'])) {
+ $host = $parts['host'];
+ } else {
+ throw new InvalidArgumentException("URL '$url' has no host");
+ }
+
+ $api = new RestApiClient(
+ $host,
+ $this->getSetting('username'),
+ $this->getSetting('password')
+ );
+
+ $api->setScheme($this->getSetting('scheme'));
+ if (isset($parts['port'])) {
+ $api->setPort($parts['port']);
+ }
+
+ if ($api->getScheme() === 'HTTPS') {
+ if ($this->getSetting('ssl_verify_peer', 'y') === 'n') {
+ $api->disableSslPeerVerification();
+ }
+ if ($this->getSetting('ssl_verify_host', 'y') === 'n') {
+ $api->disableSslHostVerification();
+ }
+ }
+
+ if ($proxy = $this->getSetting('proxy')) {
+ if ($proxyType = $this->getSetting('proxy_type')) {
+ $api->setProxy($proxy, $proxyType);
+ } else {
+ $api->setProxy($proxy);
+ }
+
+ if ($user = $this->getSetting('proxy_user')) {
+ $api->setProxyAuth($user, $this->getSetting('proxy_pass'));
+ }
+ }
+
+ return $api;
+ }
+
+ /**
+ * @param QuickForm $form
+ * @param string $key
+ * @param array $options
+ * @param string|null $default
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addBoolean(QuickForm $form, $key, $options, $default = null)
+ {
+ if ($default === null) {
+ $form->addElement('OptionalYesNo', $key, $options);
+ } else {
+ $form->addElement('YesNo', $key, $options);
+ $form->getElement($key)->setValue($default);
+ }
+ }
+
+ /**
+ * @param QuickForm $form
+ * @param string $key
+ * @param string $label
+ * @param string $description
+ * @throws \Zend_Form_Exception
+ */
+ protected static function optionalBoolean(QuickForm $form, $key, $label, $description)
+ {
+ static::addBoolean($form, $key, [
+ 'label' => $label,
+ 'description' => $description
+ ]);
+ }
+}
diff --git a/library/Director/Import/ImportSourceSql.php b/library/Director/Import/ImportSourceSql.php
new file mode 100644
index 0000000..b08a3f3
--- /dev/null
+++ b/library/Director/Import/ImportSourceSql.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Icinga\Module\Director\Import;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Data\Db\DbConnection;
+use Icinga\Module\Director\Forms\ImportSourceForm;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Util;
+use Icinga\Module\Director\Web\Form\Filter\QueryColumnsFromSql;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use ipl\Html\Html;
+
+class ImportSourceSql extends ImportSourceHook
+{
+ protected $db;
+
+ public function fetchData()
+ {
+ return $this->db()->fetchAll($this->settings['query']);
+ }
+
+ public function listColumns()
+ {
+ if ($columns = $this->getSetting('column_cache')) {
+ return explode(', ', $columns);
+ } else {
+ return array_keys((array) current($this->fetchData()));
+ }
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ /** @var ImportSourceForm $form */
+ Util::addDbResourceFormElement($form, 'resource');
+ /** @var ImportSource $current */
+ $current = $form->getObject();
+
+ $form->addElement('textarea', 'query', [
+ 'label' => $form->translate('DB Query'),
+ 'required' => true,
+ 'rows' => 15,
+ ]);
+ $form->addElement('hidden', 'column_cache', [
+ 'value' => '',
+ 'filters' => [new QueryColumnsFromSql($form)],
+ 'required' => true
+ ]);
+ if ($current) {
+ if ($columns = $current->getSetting('column_cache')) {
+ $form->addHtmlHint('Columns: ' . $columns);
+ } else {
+ $form->addHtmlHint(Hint::warning($form->translate(
+ 'Please click "Store" once again to determine query columns'
+ )));
+ }
+ }
+ return $form;
+ }
+
+ protected function db()
+ {
+ if ($this->db === null) {
+ $this->db = DbConnection::fromResourceName($this->settings['resource'])->getDbAdapter();
+ }
+
+ return $this->db;
+ }
+}
diff --git a/library/Director/Import/PurgeStrategy/ImportRunBasedPurgeStrategy.php b/library/Director/Import/PurgeStrategy/ImportRunBasedPurgeStrategy.php
new file mode 100644
index 0000000..9f0e8ab
--- /dev/null
+++ b/library/Director/Import/PurgeStrategy/ImportRunBasedPurgeStrategy.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Import\PurgeStrategy;
+
+use Icinga\Module\Director\Import\SyncUtils;
+use Icinga\Module\Director\Objects\ImportRun;
+use Icinga\Module\Director\Objects\ImportSource;
+
+class ImportRunBasedPurgeStrategy extends PurgeStrategy
+{
+ public function listObjectsToPurge()
+ {
+ $remove = array();
+
+ foreach ($this->getSyncRule()->fetchInvolvedImportSources() as $source) {
+ $remove += $this->checkImportSource($source);
+ }
+
+ return $remove;
+ }
+
+ protected function getLastSync()
+ {
+ return strtotime($this->getSyncRule()->getLastSyncTimestamp());
+ }
+
+ // TODO: NAMING!
+ protected function checkImportSource(ImportSource $source)
+ {
+ if (null === ($lastSync = $this->getLastSync())) {
+ // No last sync, nothing to purge
+ return array();
+ }
+
+ $runA = $source->fetchLastRunBefore($lastSync);
+ if ($runA === null) {
+ // Nothing to purge for this source
+ return array();
+ }
+
+ $runB = $source->fetchLastRun();
+ if ($runA->rowset_checksum === $runB->rowset_checksum) {
+ // Same source data, nothing to purge
+ return array();
+ }
+
+ return $this->listKeysRemovedBetween($runA, $runB);
+ }
+
+ public function listKeysRemovedBetween(ImportRun $runA, ImportRun $runB)
+ {
+ $rule = $this->getSyncRule();
+ $db = $rule->getDb();
+
+ $selectA = $runA->prepareImportedObjectQuery();
+ $selectB = $runB->prepareImportedObjectQuery();
+
+ $query = $db->select()->from(
+ array('a' => $selectA),
+ 'a.object_name'
+ )->where('a.object_name NOT IN (?)', $selectB);
+
+ $result = $db->fetchCol($query);
+
+ if (empty($result)) {
+ return array();
+ }
+
+ if ($rule->hasCombinedKey()) {
+ $pattern = $rule->getSourceKeyPattern();
+ $columns = SyncUtils::getRootVariables(
+ SyncUtils::extractVariableNames($pattern)
+ );
+ $resultForCombinedKey = array();
+ foreach (array_chunk($result, 1000) as $keys) {
+ $rows = $runA->fetchRows($columns, null, $keys);
+ foreach ($rows as $row) {
+ $resultForCombinedKey[] = SyncUtils::fillVariables($pattern, $row);
+ }
+ }
+ $result = $resultForCombinedKey;
+ }
+
+ if (empty($result)) {
+ return array();
+ }
+
+ return array_combine($result, $result);
+ }
+}
diff --git a/library/Director/Import/PurgeStrategy/PurgeNothingPurgeStrategy.php b/library/Director/Import/PurgeStrategy/PurgeNothingPurgeStrategy.php
new file mode 100644
index 0000000..3da8d4f
--- /dev/null
+++ b/library/Director/Import/PurgeStrategy/PurgeNothingPurgeStrategy.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Icinga\Module\Director\Import\PurgeStrategy;
+
+class PurgeNothingPurgeStrategy extends PurgeStrategy
+{
+ public function listObjectsToPurge()
+ {
+ return array();
+ }
+}
diff --git a/library/Director/Import/PurgeStrategy/PurgeStrategy.php b/library/Director/Import/PurgeStrategy/PurgeStrategy.php
new file mode 100644
index 0000000..ffbe14f
--- /dev/null
+++ b/library/Director/Import/PurgeStrategy/PurgeStrategy.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Import\PurgeStrategy;
+
+use Icinga\Module\Director\Objects\SyncRule;
+
+abstract class PurgeStrategy
+{
+ private $rule;
+
+ public function __construct(SyncRule $rule)
+ {
+ $this->rule = $rule;
+ }
+
+ protected function getSyncRule()
+ {
+ return $this->rule;
+ }
+
+ abstract public function listObjectsToPurge();
+
+ /**
+ * @return PurgeStrategy
+ */
+ public static function load($name, SyncRule $rule)
+ {
+ $class = __NAMESPACE__ . '\\' . $name . 'PurgeStrategy';
+ return new $class($rule);
+ }
+}
diff --git a/library/Director/Import/Sync.php b/library/Director/Import/Sync.php
new file mode 100644
index 0000000..8fea46c
--- /dev/null
+++ b/library/Director/Import/Sync.php
@@ -0,0 +1,942 @@
+<?php
+
+namespace Icinga\Module\Director\Import;
+
+use Exception;
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Application\MemoryLimit;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\BranchSupport;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\Objects\HostGroupMembershipResolver;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaHostGroup;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\SyncProperty;
+use Icinga\Module\Director\Objects\SyncRule;
+use Icinga\Module\Director\Objects\SyncRun;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use InvalidArgumentException;
+use RuntimeException;
+
+class Sync
+{
+ /** @var SyncRule */
+ protected $rule;
+
+ /** @var Db */
+ protected $db;
+
+ /** @var array Related ImportSource objects */
+ protected $sources;
+
+ /** @var array Source columns we want to fetch from our sources */
+ protected $sourceColumns;
+
+ /** @var array Imported data */
+ protected $imported;
+
+ /** @var IcingaObject[] Objects to work with */
+ protected $objects;
+
+ /** @var array<mixed, array<int, string>> key => [property, property]*/
+ protected $setNull = [];
+
+ /** @var bool Whether we already prepared your sync */
+ protected $isPrepared = false;
+
+ /** @var bool Whether we applied strtolower() to existing object keys */
+ protected $usedLowerCasedKeys = false;
+
+ protected $modify = [];
+
+ protected $remove = [];
+
+ protected $create = [];
+
+ protected $errors = [];
+
+ /** @var SyncProperty[] */
+ protected $syncProperties;
+
+ protected $replaceVars = false;
+
+ protected $hasPropertyDisabled = false;
+
+ protected $serviceOverrideKeyName;
+
+ /**
+ * @var SyncRun
+ */
+ protected $run;
+
+ protected $runStartTime;
+
+ /** @var Filter[] */
+ protected $columnFilters = [];
+
+ /** @var HostGroupMembershipResolver|bool */
+ protected $hostGroupMembershipResolver;
+
+ /** @var ?DbObjectStore */
+ protected $store;
+
+ /**
+ * @param SyncRule $rule
+ * @param ?DbObjectStore $store
+ */
+ public function __construct(SyncRule $rule, DbObjectStore $store = null)
+ {
+ $this->rule = $rule;
+ $this->db = $rule->getConnection();
+ $this->store = $store;
+ }
+
+ /**
+ * Whether the given sync rule would apply modifications
+ *
+ * @return boolean
+ * @throws Exception
+ */
+ public function hasModifications()
+ {
+ return count($this->getExpectedModifications()) > 0;
+ }
+
+ /**
+ * Retrieve modifications a given SyncRule would apply
+ *
+ * @return array Array of IcingaObject elements
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function getExpectedModifications()
+ {
+ $modified = [];
+ $objects = $this->prepare();
+ $updateOnly = $this->rule->get('update_policy') === 'update-only';
+ $allowCreate = ! $updateOnly;
+ foreach ($objects as $object) {
+ if ($object->hasBeenModified()) {
+ if ($allowCreate || $object->hasBeenLoadedFromDb()) {
+ $modified[] = $object;
+ }
+ } elseif (! $updateOnly && $object->shouldBeRemoved()) {
+ $modified[] = $object;
+ }
+ }
+
+ return $modified;
+ }
+
+ /**
+ * Transform the given value to an array
+ *
+ * @param array|string|null $value
+ *
+ * @return array
+ */
+ protected function wantArray($value)
+ {
+ if (is_array($value)) {
+ return $value;
+ } elseif ($value === null) {
+ return [];
+ } else {
+ return [$value];
+ }
+ }
+
+ /**
+ * Raise PHP resource limits
+ *
+ * @return self;
+ */
+ protected function raiseLimits()
+ {
+ MemoryLimit::raiseTo('1024M');
+ ini_set('max_execution_time', 0);
+
+ return $this;
+ }
+
+ /**
+ * Initialize run summary measurements
+ *
+ * @return self;
+ */
+ protected function startMeasurements()
+ {
+ $this->run = SyncRun::start($this->rule);
+ $this->runStartTime = microtime(true);
+ Benchmark::measure('Starting sync');
+ return $this;
+ }
+
+ /**
+ * Fetch the configured properties involved in this sync
+ *
+ * @return self
+ */
+ protected function fetchSyncProperties()
+ {
+ $this->syncProperties = $this->rule->getSyncProperties();
+ foreach ($this->syncProperties as $key => $prop) {
+ $destinationField = $prop->get('destination_field');
+ if ($destinationField === 'vars' && $prop->get('merge_policy') === 'override') {
+ $this->replaceVars = true;
+ }
+
+ if ($destinationField === 'disabled') {
+ $this->hasPropertyDisabled = true;
+ }
+
+ if ($prop->get('filter_expression') === null || strlen($prop->get('filter_expression')) === 0) {
+ continue;
+ }
+
+ $this->columnFilters[$key] = Filter::fromQueryString(
+ $prop->get('filter_expression')
+ );
+ }
+
+ return $this;
+ }
+
+ protected function rowMatchesPropertyFilter($row, $key)
+ {
+ if (!array_key_exists($key, $this->columnFilters)) {
+ return true;
+ }
+
+ return $this->columnFilters[$key]->matches($row);
+ }
+
+ /**
+ * Instantiates all related ImportSource objects
+ *
+ * @return self
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function prepareRelatedImportSources()
+ {
+ $this->sources = [];
+ foreach ($this->syncProperties as $p) {
+ $id = $p->get('source_id');
+ if (! array_key_exists($id, $this->sources)) {
+ $this->sources[$id] = ImportSource::loadWithAutoIncId(
+ (int) $id,
+ $this->db
+ );
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Prepare the source columns we want to fetch
+ *
+ * @return self
+ */
+ protected function prepareSourceColumns()
+ {
+ // $fieldMap = [];
+ $this->sourceColumns = [];
+
+ foreach ($this->syncProperties as $p) {
+ $sourceId = $p->get('source_id');
+ if (! array_key_exists($sourceId, $this->sourceColumns)) {
+ $this->sourceColumns[$sourceId] = [];
+ }
+
+ foreach (SyncUtils::extractVariableNames($p->get('source_expression')) as $varname) {
+ $this->sourceColumns[$sourceId][$varname] = $varname;
+ // -> ? $fieldMap[
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Fetch latest imported data rows from all involved import sources
+ * @return Sync
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function fetchImportedData()
+ {
+ Benchmark::measure('Begin loading imported data');
+ if ($this->rule->get('object_type') === 'host') {
+ $this->serviceOverrideKeyName = $this->db->settings()->override_services_varname;
+ }
+
+ $this->imported = [];
+
+ $sourceKeyPattern = $this->rule->getSourceKeyPattern();
+ $combinedKey = $this->rule->hasCombinedKey();
+
+ foreach ($this->sources as $source) {
+ /** @var ImportSource $source */
+ $sourceId = $source->get('id');
+
+ // Provide an alias column for our key. TODO: double-check this!
+ $key = $source->key_column;
+ $this->sourceColumns[$sourceId][$key] = $key;
+ $run = $source->fetchLastRun(true);
+
+ $usedColumns = SyncUtils::getRootVariables($this->sourceColumns[$sourceId]);
+
+ $filterColumns = [];
+ foreach ($this->columnFilters as $filter) {
+ foreach ($filter->listFilteredColumns() as $column) {
+ $filterColumns[$column] = $column;
+ }
+ }
+ if (($ruleFilter = $this->rule->filter()) !== null) {
+ foreach ($ruleFilter->listFilteredColumns() as $column) {
+ $filterColumns[$column] = $column;
+ }
+ }
+
+ if (! empty($filterColumns)) {
+ foreach (SyncUtils::getRootVariables($filterColumns) as $column) {
+ $usedColumns[$column] = $column;
+ }
+ }
+ Benchmark::measure(sprintf('Done pre-processing columns for source %s', $source->source_name));
+
+ $rows = $run->fetchRows($usedColumns);
+ Benchmark::measure(sprintf('Fetched source %s', $source->source_name));
+
+ $this->imported[$sourceId] = [];
+ foreach ($rows as $row) {
+ if ($combinedKey) {
+ $key = SyncUtils::fillVariables($sourceKeyPattern, $row);
+ if ($this->usedLowerCasedKeys) {
+ $key = strtolower($key);
+ }
+
+ if (array_key_exists($key, $this->imported[$sourceId])) {
+ throw new InvalidArgumentException(sprintf(
+ 'Trying to import row "%s" (%s) twice: %s VS %s',
+ $key,
+ $sourceKeyPattern,
+ json_encode($this->imported[$sourceId][$key]),
+ json_encode($row)
+ ));
+ }
+ } else {
+ if (! property_exists($row, $key)) {
+ throw new InvalidArgumentException(sprintf(
+ 'There is no key column "%s" in this row from "%s": %s',
+ $key,
+ $source->source_name,
+ json_encode($row)
+ ));
+ }
+ }
+
+ if (! $this->rule->matches($row)) {
+ continue;
+ }
+
+ if ($combinedKey) {
+ $this->imported[$sourceId][$key] = $row;
+ } else {
+ if ($this->usedLowerCasedKeys) {
+ $this->imported[$sourceId][strtolower($row->$key)] = $row;
+ } else {
+ $this->imported[$sourceId][$row->$key] = $row;
+ }
+ }
+ }
+
+ unset($rows);
+ }
+
+ Benchmark::measure('Done loading imported data');
+
+ return $this;
+ }
+
+ /**
+ * TODO: This is rubbish, we need to filter at fetch time
+ */
+ protected function removeForeignListEntries()
+ {
+ $listId = null;
+ foreach ($this->syncProperties as $prop) {
+ if ($prop->get('destination_field') === 'list_id') {
+ $listId = (int) $prop->get('source_expression');
+ }
+ }
+
+ if ($listId === null) {
+ throw new InvalidArgumentException(
+ 'Cannot sync datalist entry without list_id'
+ );
+ }
+
+ $no = [];
+ foreach ($this->objects as $k => $o) {
+ if ((int) $o->get('list_id') !== $listId) {
+ $no[] = $k;
+ }
+ }
+
+ foreach ($no as $k) {
+ unset($this->objects[$k]);
+ }
+ }
+
+ /**
+ * @return $this
+ */
+ protected function loadExistingObjects()
+ {
+ Benchmark::measure('Begin loading existing objects');
+
+ $ruleObjectType = $this->rule->get('object_type');
+ $useLowerCaseKeys = $ruleObjectType !== 'datalistEntry';
+ // TODO: Make object_type (template, object...) and object_name mandatory?
+ if ($this->rule->hasCombinedKey()) {
+ $this->objects = [];
+ $destinationKeyPattern = $this->rule->getDestinationKeyPattern();
+ $table = DbObjectTypeRegistry::tableNameByType($ruleObjectType);
+ if ($this->store && BranchSupport::existsForTableName($table)) {
+ $objects = $this->store->loadAll($table);
+ } else {
+ $objects = IcingaObject::loadAllByType($ruleObjectType, $this->db);
+ }
+
+ foreach ($objects as $object) {
+ if ($object instanceof IcingaService) {
+ if (strstr($destinationKeyPattern, '${host}')
+ && $object->get('host_id') === null
+ ) {
+ continue;
+ } elseif (strstr($destinationKeyPattern, '${service_set}')
+ && $object->get('service_set_id') === null
+ ) {
+ continue;
+ }
+ }
+
+ $key = SyncUtils::fillVariables(
+ $destinationKeyPattern,
+ $object
+ );
+ if ($useLowerCaseKeys) {
+ $key = strtolower($key);
+ }
+
+ if (array_key_exists($key, $this->objects)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Combined destination key "%s" is not unique, got "%s" twice',
+ $destinationKeyPattern,
+ $key
+ ));
+ }
+
+ $this->objects[$key] = $object;
+ }
+ } else {
+ if ($this->store) {
+ $objects = $this->store->loadAll(DbObjectTypeRegistry::tableNameByType($ruleObjectType), 'object_name');
+ } else {
+ $objects = IcingaObject::loadAllByType($ruleObjectType, $this->db);
+ }
+
+ if ($useLowerCaseKeys) {
+ $this->objects = [];
+ foreach ($objects as $key => $object) {
+ $this->objects[strtolower($key)] = $object;
+ }
+ } else {
+ $this->objects = $objects;
+ }
+ }
+
+ $this->usedLowerCasedKeys = $useLowerCaseKeys;
+ // TODO: should be obsoleted by a better "loadFiltered" method
+ if ($ruleObjectType === 'datalistEntry') {
+ $this->removeForeignListEntries();
+ }
+
+ Benchmark::measure('Done loading existing objects');
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function prepareNewObjects()
+ {
+ $objects = [];
+ $ruleObjectType = $this->rule->get('object_type');
+
+ foreach ($this->sources as $source) {
+ $sourceId = $source->id;
+ $keyColumn = $source->get('key_column');
+
+ foreach ($this->imported[$sourceId] as $key => $row) {
+ // Workaround: $a["10"] = "val"; -> array_keys($a) = [(int) 10]
+ $key = (string) $key;
+ $originalKey = $row->$keyColumn;
+ if ($this->usedLowerCasedKeys) {
+ $key = strtolower($key);
+ }
+ if (! array_key_exists($key, $objects)) {
+ // Safe default values for object_type and object_name
+ if ($ruleObjectType === 'datalistEntry') {
+ $props = [];
+ } else {
+ $props = [
+ 'object_type' => 'object',
+ 'object_name' => $originalKey,
+ ];
+ }
+
+ $objects[$key] = IcingaObject::createByType(
+ $ruleObjectType,
+ $props,
+ $this->db
+ );
+ }
+
+ $object = $objects[$key];
+ $this->prepareNewObject($row, $object, $key, $sourceId);
+ }
+ }
+
+ return $objects;
+ }
+
+ /**
+ * @param $row
+ * @param DbObject $object
+ * @param $sourceId
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function prepareNewObject($row, DbObject $object, $objectKey, $sourceId)
+ {
+ foreach ($this->syncProperties as $propertyKey => $p) {
+ if ($p->get('source_id') !== $sourceId) {
+ continue;
+ }
+
+ if (! $this->rowMatchesPropertyFilter($row, $propertyKey)) {
+ continue;
+ }
+
+ $prop = $p->get('destination_field');
+ $val = SyncUtils::fillVariables($p->get('source_expression'), $row);
+
+ if ($object instanceof IcingaObject) {
+ if ($prop === 'import') {
+ if ($val !== null) {
+ $object->imports()->add($val);
+ }
+ } elseif ($prop === 'groups') {
+ if ($val !== null) {
+ $object->groups()->add($val);
+ }
+ } elseif (substr($prop, 0, 5) === 'vars.') {
+ $varName = substr($prop, 5);
+ if (substr($varName, -2) === '[]') {
+ $varName = substr($varName, 0, -2);
+ $current = $this->wantArray($object->vars()->$varName);
+ $object->vars()->$varName = array_merge(
+ $current,
+ $this->wantArray($val)
+ );
+ } else {
+ if ($val === null) {
+ $this->setNull[$objectKey][$prop] = $prop;
+ } else {
+ unset($this->setNull[$objectKey][$prop]);
+ $object->vars()->$varName = $val;
+ }
+ }
+ } else {
+ if ($val === null) {
+ $this->setNull[$objectKey][$prop] = $prop;
+ } else {
+ unset($this->setNull[$objectKey][$prop]);
+ $object->set($prop, $val);
+ }
+ }
+ } else {
+ if ($val === null) {
+ $this->setNull[$objectKey][$prop] = $prop;
+ } else {
+ unset($this->setNull[$objectKey][$prop]);
+ $object->set($prop, $val);
+ }
+ }
+ }
+ }
+
+ /**
+ * @return $this
+ */
+ protected function deferResolvers()
+ {
+ if (in_array($this->rule->get('object_type'), ['host', 'hostgroup'])) {
+ $resolver = $this->getHostGroupMembershipResolver();
+ $resolver->defer()->setUseTransactions(false);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param DbObject $object
+ * @return $this
+ */
+ protected function setResolver($object)
+ {
+ if (! ($object instanceof IcingaHost || $object instanceof IcingaHostGroup)) {
+ return $this;
+ }
+ if ($resolver = $this->getHostGroupMembershipResolver()) {
+ $object->setHostGroupMembershipResolver($resolver);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function notifyResolvers()
+ {
+ if ($resolver = $this->getHostGroupMembershipResolver()) {
+ $resolver->refreshDb(true);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return bool|HostGroupMembershipResolver
+ */
+ protected function getHostGroupMembershipResolver()
+ {
+ if ($this->hostGroupMembershipResolver === null) {
+ if (in_array(
+ $this->rule->get('object_type'),
+ ['host', 'hostgroup']
+ )) {
+ $this->hostGroupMembershipResolver = new HostGroupMembershipResolver(
+ $this->db
+ );
+ } else {
+ $this->hostGroupMembershipResolver = false;
+ }
+ }
+
+ return $this->hostGroupMembershipResolver;
+ }
+
+ /**
+ * Evaluates a SyncRule and returns a list of modified objects
+ *
+ * TODO: Split this into smaller methods
+ *
+ * @return DbObject|IcingaObject[] List of modified IcingaObjects
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function prepare()
+ {
+ if ($this->isPrepared) {
+ return $this->objects;
+ }
+
+ $this->raiseLimits()
+ ->startMeasurements()
+ ->prepareCache()
+ ->fetchSyncProperties()
+ ->prepareRelatedImportSources()
+ ->prepareSourceColumns()
+ ->loadExistingObjects()
+ ->fetchImportedData()
+ ->deferResolvers();
+
+ Benchmark::measure('Begin preparing updated objects');
+ $newObjects = $this->prepareNewObjects();
+
+ Benchmark::measure('Ready to process objects');
+ /** @var DbObject|IcingaObject $object */
+ foreach ($newObjects as $key => $object) {
+ $this->processObject($key, $object);
+ }
+
+ Benchmark::measure('Modified objects are ready, applying purge strategy');
+ $noAction = [];
+ $purgeAction = $this->rule->get('purge_action');
+ foreach ($this->rule->purgeStrategy()->listObjectsToPurge() as $key) {
+ $key = strtolower($key);
+ if (array_key_exists($key, $newObjects)) {
+ // Object has been touched, do not delete
+ continue;
+ }
+
+ if (array_key_exists($key, $this->objects)) {
+ $object = $this->objects[$key];
+ if (! $object->hasBeenModified()) {
+ switch ($purgeAction) {
+ case 'delete':
+ $object->markForRemoval();
+ break;
+ case 'disable':
+ $object->set('disabled', 'y');
+ break;
+ default:
+ throw new RuntimeException(
+ "Unsupported purge action: '$purgeAction'"
+ );
+ }
+ }
+ }
+ }
+
+ Benchmark::measure('Done marking objects for purge');
+
+ foreach ($this->objects as $key => $object) {
+ if (! $object->hasBeenModified() && ! $object->shouldBeRemoved()) {
+ $noAction[] = $key;
+ }
+ }
+
+ foreach ($noAction as $key) {
+ unset($this->objects[$key]);
+ }
+
+ $this->isPrepared = true;
+
+ Benchmark::measure('Done preparing objects');
+
+ return $this->objects;
+ }
+
+ /**
+ * @param $key
+ * @param DbObject|IcingaObject $object
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function processObject($key, $object)
+ {
+ if (array_key_exists($key, $this->objects)) {
+ $this->refreshObject($key, $object);
+ } else {
+ $this->addNewObject($key, $object);
+ }
+ }
+
+ /**
+ * @param $key
+ * @param DbObject|IcingaObject $object
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function refreshObject($key, $object)
+ {
+ $policy = $this->rule->get('update_policy');
+
+ switch ($policy) {
+ case 'override':
+ if ($object instanceof IcingaHost
+ && !in_array('api_key', $this->rule->getSyncProperties())
+ ) {
+ $this->objects[$key]->replaceWith($object, ['api_key']);
+ } else {
+ $this->objects[$key]->replaceWith($object);
+ }
+ break;
+
+ case 'merge':
+ case 'update-only':
+ // TODO: re-evaluate merge settings. vars.x instead of
+ // just "vars" might suffice.
+ $this->objects[$key]->merge($object, $this->replaceVars);
+ if (! $this->hasPropertyDisabled && $object->hasProperty('disabled')) {
+ $this->objects[$key]->resetProperty('disabled');
+ }
+ break;
+
+ default:
+ // policy 'ignore', no action
+ }
+
+ if ($policy === 'override' || $policy === 'merge') {
+ if ($object instanceof IcingaHost) {
+ $keyName = $this->serviceOverrideKeyName;
+ if (! $object->hasInitializedVars() || ! isset($object->vars()->$key)) {
+ $this->objects[$key]->vars()->restoreStoredVar($keyName);
+ }
+ }
+ }
+
+ if (isset($this->setNull[$key])) {
+ foreach ($this->setNull[$key] as $property) {
+ $this->objects[$key]->set($property, null);
+ }
+ }
+ }
+
+ /**
+ * @param $key
+ * @param DbObject|IcingaObject $object
+ */
+ protected function addNewObject($key, $object)
+ {
+ $this->objects[$key] = $object;
+ }
+
+ /**
+ * Runs a SyncRule and applies all resulting changes
+ * @return int
+ * @throws Exception
+ * @throws IcingaException
+ */
+ public function apply()
+ {
+ Benchmark::measure('Begin applying objects');
+
+ $objects = $this->prepare();
+ $db = $this->db;
+ $dba = $db->getDbAdapter();
+ if (! $this->store) { // store has it's own transaction
+ $dba->beginTransaction();
+ }
+
+ $object = null;
+ $updateOnly = $this->rule->get('update_policy') === 'update-only';
+ $allowCreate = ! $updateOnly;
+
+ try {
+ $formerActivityChecksum = hex2bin(
+ $db->getLastActivityChecksum()
+ );
+ $created = 0;
+ $modified = 0;
+ $deleted = 0;
+ // TODO: Count also failed ones, once we allow such
+ // $failed = 0;
+ foreach ($objects as $object) {
+ $this->setResolver($object);
+ if (! $updateOnly && $object->shouldBeRemoved()) {
+ if ($this->store) {
+ $this->store->delete($object);
+ } else {
+ $object->delete();
+ }
+ $deleted++;
+ continue;
+ }
+
+ if ($object->hasBeenModified()) {
+ $existing = $object->hasBeenLoadedFromDb();
+ if ($existing) {
+ if ($this->store) {
+ $this->store->store($object);
+ } else {
+ $object->store($db);
+ }
+ $modified++;
+ } elseif ($allowCreate) {
+ if ($this->store) {
+ $this->store->store($object);
+ } else {
+ $object->store($db);
+ }
+ $created++;
+ }
+ }
+ }
+
+ $runProperties = [
+ 'objects_created' => $created,
+ 'objects_deleted' => $deleted,
+ 'objects_modified' => $modified,
+ ];
+
+ if ($created + $deleted + $modified > 0) {
+ // TODO: What if this has been the very first activity?
+ $runProperties['last_former_activity'] = $db->quoteBinary($formerActivityChecksum);
+ $runProperties['last_related_activity'] = $db->quoteBinary(hex2bin(
+ $db->getLastActivityChecksum()
+ ));
+ }
+
+ $this->run->setProperties($runProperties);
+ if (!$this->store || !$this->store->getBranch()->isBranch()) {
+ $this->run->store();
+ }
+ $this->notifyResolvers();
+ if (! $this->store) {
+ $dba->commit();
+ }
+
+ // Store duration after commit, as the commit might take some time
+ $this->run->set('duration_ms', (int) round(
+ (microtime(true) - $this->runStartTime) * 1000
+ ));
+ if (!$this->store || !$this->store->getBranch()->isBranch()) {
+ $this->run->store();
+ }
+
+ Benchmark::measure('Done applying objects');
+ } catch (Exception $e) {
+ if (! $this->store) {
+ $dba->rollBack();
+ }
+
+ if ($object instanceof IcingaObject) {
+ throw new IcingaException(
+ 'Exception while syncing %s %s: %s',
+ get_class($object),
+ $object->getObjectName(),
+ $e->getMessage(),
+ $e
+ );
+ } else {
+ throw $e;
+ }
+ }
+
+ return $this->run->get('id');
+ }
+
+ protected function prepareCache()
+ {
+ if ($this->store) {
+ return $this;
+ }
+ PrefetchCache::initialize($this->db);
+ IcingaTemplateRepository::clear();
+
+ $ruleObjectType = $this->rule->get('object_type');
+
+ $dummy = IcingaObject::createByType($ruleObjectType);
+ if ($dummy instanceof IcingaObject) {
+ IcingaObject::prefetchAllRelationsByType($ruleObjectType, $this->db);
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/Import/SyncUtils.php b/library/Director/Import/SyncUtils.php
new file mode 100644
index 0000000..5528b2d
--- /dev/null
+++ b/library/Director/Import/SyncUtils.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Icinga\Module\Director\Import;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use InvalidArgumentException;
+
+class SyncUtils
+{
+ /**
+ * Extract variable names in the form ${var_name} from a given string
+ *
+ * @param string $string
+ *
+ * @return array List of variable names (without ${})
+ */
+ public static function extractVariableNames($string)
+ {
+ if (preg_match_all('/\${([^}]+)}/', $string, $m, PREG_PATTERN_ORDER)) {
+ return $m[1];
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * Whether the given string contains variable names in the form ${var_name}
+ *
+ * @param string $string
+ *
+ * @return bool
+ */
+ public static function hasVariables($string)
+ {
+ return preg_match('/\${([^}]+)}/', $string);
+ }
+
+ /**
+ * Recursively extract a value from a nested structure
+ *
+ * For a $val looking like
+ *
+ * { 'vars' => { 'disk' => { 'sda' => { 'size' => '256G' } } } }
+ *
+ * and a key vars.disk.sda given as [ 'vars', 'disk', 'sda' ] this would
+ * return { size => '255GB' }
+ *
+ * @param string $val The value to extract data from
+ * @param array $keys A list of nested keys pointing to desired data
+ *
+ * @return mixed
+ */
+ public static function getDeepValue($val, array $keys)
+ {
+ $key = array_shift($keys);
+ if (! property_exists($val, $key)) {
+ return null;
+ }
+
+ if (empty($keys)) {
+ return $val->$key;
+ }
+
+ return static::getDeepValue($val->$key, $keys);
+ }
+
+ /**
+ * Return a specific value from a given row object
+ *
+ * Supports also keys pointing to nested structures like vars.disk.sda
+ *
+ * @param object $row stdClass object providing property values
+ * @param string $var Variable/property name
+ *
+ * @return mixed
+ */
+ public static function getSpecificValue($row, $var)
+ {
+ if (strpos($var, '.') === false) {
+ if ($row instanceof DbObject) {
+ return $row->$var;
+ }
+ if (! property_exists($row, $var)) {
+ return null;
+ }
+
+ return $row->$var;
+ } else {
+ $parts = explode('.', $var);
+ $main = array_shift($parts);
+ if (! property_exists($row, $main)) {
+ return null;
+ }
+ if ($row->$main === null) {
+ return null;
+ }
+
+ if (! is_object($row->$main)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Data is not nested, cannot access %s: %s',
+ $var,
+ var_export($row, 1)
+ ));
+ }
+
+ return static::getDeepValue($row->$main, $parts);
+ }
+ }
+
+ /**
+ * Fill variables in the given string pattern
+ *
+ * This replaces all occurrences of ${var_name} with the corresponding
+ * property $row->var_name of the given row object. Missing variables are
+ * replaced by an empty string. This works also fine in case there are
+ * multiple variables to be found in your string.
+ *
+ * @param string $string String with optional variables/placeholders
+ * @param object $row stdClass object providing property values
+ *
+ * @return string
+ */
+ public static function fillVariables($string, $row)
+ {
+ if (preg_match('/^\${([^}]+)}$/', $string, $m)) {
+ return static::getSpecificValue($row, $m[1]);
+ }
+
+ $func = function ($match) use ($row) {
+ return SyncUtils::getSpecificValue($row, $match[1]);
+ };
+
+ return preg_replace_callback('/\${([^}]+)}/', $func, $string);
+ }
+
+ public static function getRootVariables($vars)
+ {
+ $res = array();
+ foreach ($vars as $p) {
+ if (false === ($pos = strpos($p, '.')) || $pos === strlen($p) - 1) {
+ $res[] = $p;
+ } else {
+ $res[] = substr($p, 0, $pos);
+ }
+ }
+
+ if (empty($res)) {
+ return array();
+ }
+
+ return array_combine($res, $res);
+ }
+}
diff --git a/library/Director/Job/ConfigJob.php b/library/Director/Job/ConfigJob.php
new file mode 100644
index 0000000..fda3043
--- /dev/null
+++ b/library/Director/Job/ConfigJob.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Icinga\Module\Director\Job;
+
+use Icinga\Module\Director\Deployment\ConditionalConfigRenderer;
+use Icinga\Module\Director\Deployment\ConditionalDeployment;
+use Icinga\Module\Director\Deployment\DeploymentGracePeriod;
+use Icinga\Module\Director\Hook\JobHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class ConfigJob extends JobHook
+{
+ public function run()
+ {
+ $db = $this->db();
+ $deployer = new ConditionalDeployment($db);
+ $renderer = new ConditionalConfigRenderer($db);
+ if ($grace = $this->getSetting('grace_period')) {
+ $deployer->setGracePeriod(new DeploymentGracePeriod((int) $grace, $db));
+ }
+ if ($this->getSetting('force_generate') === 'y') {
+ $renderer->forceRendering();
+ }
+
+ $deployer->deploy($renderer->getConfig());
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'force_generate', [
+ 'label' => $form->translate('Force rendering'),
+ 'description' => $form->translate(
+ 'Whether rendering should be forced. If not enforced, this'
+ . ' job re-renders the configuration only when there have been'
+ . ' activities since the last rendered config'
+ ),
+ 'value' => 'n',
+ 'multiOptions' => [
+ 'y' => $form->translate('Yes'),
+ 'n' => $form->translate('No'),
+ ]
+ ]);
+
+ $form->addElement('select', 'deploy_when_changed', [
+ 'label' => $form->translate('Deploy modified config'),
+ 'description' => $form->translate(
+ 'This allows you to immediately deploy a modified configuration'
+ ),
+ 'value' => 'n',
+ 'multiOptions' => [
+ 'y' => $form->translate('Yes'),
+ 'n' => $form->translate('No'),
+ ]
+ ]);
+
+ $form->addElement('text', 'grace_period', array(
+ 'label' => $form->translate('Grace period'),
+ 'description' => $form->translate(
+ 'When deploying configuration, wait at least this amount of'
+ . ' seconds unless the next deployment should take place'
+ ),
+ 'value' => 600,
+ ));
+
+ return $form;
+ }
+
+ public static function getDescription(QuickForm $form)
+ {
+ return $form->translate(
+ 'The Config job allows you to generate and eventually deploy your'
+ . ' Icinga 2 configuration'
+ );
+ }
+}
diff --git a/library/Director/Job/HousekeepingJob.php b/library/Director/Job/HousekeepingJob.php
new file mode 100644
index 0000000..9f3f596
--- /dev/null
+++ b/library/Director/Job/HousekeepingJob.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Director\Job;
+
+use Icinga\Module\Director\Db\Housekeeping;
+use Icinga\Module\Director\Hook\JobHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class HousekeepingJob extends JobHook
+{
+ protected $housekeeping;
+
+ public function run()
+ {
+ $this->housekeeping()->runAllTasks();
+ }
+
+ public static function getDescription(QuickForm $form)
+ {
+ return $form->translate(
+ 'The Housekeeping job provides various task that keep your Director'
+ . ' database fast and clean'
+ );
+ }
+
+ public function isPending()
+ {
+ return $this->housekeeping()->hasPendingTasks();
+ }
+
+ protected function housekeeping()
+ {
+ if ($this->housekeeping === null) {
+ $this->housekeeping = new Housekeeping($this->db());
+ }
+
+ return $this->housekeeping;
+ }
+}
diff --git a/library/Director/Job/ImportJob.php b/library/Director/Job/ImportJob.php
new file mode 100644
index 0000000..5f2c81c
--- /dev/null
+++ b/library/Director/Job/ImportJob.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Icinga\Module\Director\Job;
+
+use Icinga\Module\Director\Hook\JobHook;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class ImportJob extends JobHook
+{
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function run()
+ {
+ $db = $this->db();
+ $id = $this->getSetting('source_id');
+ if ($id === '__ALL__') {
+ foreach (ImportSource::loadAll($db) as $source) {
+ $this->runForSource($source);
+ }
+ } else {
+ $this->runForSource(ImportSource::loadWithAutoIncId($id, $db));
+ }
+ }
+
+ /**
+ * @return array
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function exportSettings()
+ {
+ $settings = parent::exportSettings();
+ if (array_key_exists('source_id', $settings)) {
+ $id = $settings['source_id'];
+ if ($id !== '__ALL__') {
+ $settings['source'] = ImportSource::loadWithAutoIncId(
+ $id,
+ $this->db()
+ )->get('source_name');
+ }
+
+ unset($settings['source_id']);
+ }
+
+ return $settings;
+ }
+
+ /**
+ * @param ImportSource $source
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function runForSource(ImportSource $source)
+ {
+ if ($this->getSetting('run_import') === 'y') {
+ $source->runImport();
+ } else {
+ $source->checkForChanges();
+ }
+ }
+
+ public static function getDescription(QuickForm $form)
+ {
+ return $form->translate(
+ 'The "Import" job allows to run import actions at regular intervals'
+ );
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $rules = self::enumImportSources($form);
+
+ $form->addElement('select', 'source_id', array(
+ 'label' => $form->translate('Import source'),
+ 'description' => $form->translate(
+ 'Please choose your import source that should be executed.'
+ . ' You could create different schedules for different sources'
+ . ' or also opt for running all of them at once.'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $rules
+ ));
+
+ $form->addElement('select', 'run_import', array(
+ 'label' => $form->translate('Run import'),
+ 'description' => $form->translate(
+ 'You could immediately apply eventual changes or just learn about them.'
+ . ' In case you do not want them to be applied immediately, defining a'
+ . ' job still makes sense. You will be made aware of available changes'
+ . ' in your Director GUI.'
+ ),
+ 'value' => 'n',
+ 'multiOptions' => array(
+ 'y' => $form->translate('Yes'),
+ 'n' => $form->translate('No'),
+ )
+ ));
+ }
+
+ protected static function enumImportSources(QuickForm $form)
+ {
+ /** @var DirectorObjectForm $form */
+ $db = $form->getDb();
+ $query = $db->select()->from(
+ 'import_source',
+ array('id', 'source_name')
+ )->order('source_name');
+
+ $res = $db->fetchPairs($query);
+ return array(
+ null => $form->translate('- please choose -'),
+ '__ALL__' => $form->translate('Run all imports at once')
+ ) + $res;
+ }
+}
diff --git a/library/Director/Job/SyncJob.php b/library/Director/Job/SyncJob.php
new file mode 100644
index 0000000..0a5aa37
--- /dev/null
+++ b/library/Director/Job/SyncJob.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Icinga\Module\Director\Job;
+
+use Icinga\Module\Director\Hook\JobHook;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class SyncJob extends JobHook
+{
+ protected $rule;
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function run()
+ {
+ $db = $this->db();
+ $id = $this->getSetting('rule_id');
+ if ($id === '__ALL__') {
+ foreach (SyncRule::loadAll($db) as $rule) {
+ $this->runForRule($rule);
+ }
+ } else {
+ $this->runForRule(SyncRule::loadWithAutoIncId((int) $id, $db));
+ }
+ }
+
+ /**
+ * @return array
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function exportSettings()
+ {
+ $settings = [
+ 'apply_changes' => $this->getSetting('apply_changes') === 'y'
+ ];
+ $id = $this->getSetting('rule_id');
+ if ($id !== '__ALL__') {
+ $settings['rule'] = SyncRule::loadWithAutoIncId((int) $id, $this->db())
+ ->get('rule_name');
+ }
+
+ return $settings;
+ }
+
+ /**
+ * @param SyncRule $rule
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function runForRule(SyncRule $rule)
+ {
+ if ($this->getSetting('apply_changes') === 'y') {
+ $rule->applyChanges();
+ } else {
+ $rule->checkForChanges();
+ }
+ }
+
+ public static function getDescription(QuickForm $form)
+ {
+ return $form->translate(
+ 'The "Sync" job allows to run sync actions at regular intervals'
+ );
+ }
+
+ /**
+ * @param QuickForm $form
+ * @return DirectorObjectForm|QuickForm
+ * @throws \Zend_Form_Exception
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ /** @var DirectorObjectForm $form */
+ $rules = self::enumSyncRules($form);
+
+ $form->addElement('select', 'rule_id', array(
+ 'label' => $form->translate('Synchronization rule'),
+ 'description' => $form->translate(
+ 'Please choose your synchronization rule that should be executed.'
+ . ' You could create different schedules for different rules or also'
+ . ' opt for running all of them at once.'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $rules
+ ));
+
+ $form->addElement('select', 'apply_changes', array(
+ 'label' => $form->translate('Apply changes'),
+ 'description' => $form->translate(
+ 'You could immediately apply eventual changes or just learn about them.'
+ . ' In case you do not want them to be applied immediately, defining a'
+ . ' job still makes sense. You will be made aware of available changes'
+ . ' in your Director GUI.'
+ ),
+ 'value' => 'n',
+ 'multiOptions' => array(
+ 'y' => $form->translate('Yes'),
+ 'n' => $form->translate('No'),
+ )
+ ));
+
+ if ((string) $form->getSentOrObjectValue('job_name') !== '') {
+ if (($ruleId = $form->getSentValue('rule_id')) && array_key_exists($ruleId, $rules)) {
+ $name = sprintf('Sync job: %s', $rules[$ruleId]);
+ $form->getElement('job_name')->setValue($name);
+ ///$form->getObject()->set('job_name', $name);
+ }
+ }
+
+ return $form;
+ }
+
+ protected static function enumSyncRules(QuickForm $form)
+ {
+ /** @var DirectorObjectForm $form */
+ $db = $form->getDb();
+ $query = $db->select()->from('sync_rule', array('id', 'rule_name'))->order('rule_name');
+ $res = $db->fetchPairs($query);
+ return array(
+ null => $form->translate('- please choose -'),
+ '__ALL__' => $form->translate('Run all rules at once')
+ ) + $res;
+ }
+}
diff --git a/library/Director/KickstartHelper.php b/library/Director/KickstartHelper.php
new file mode 100644
index 0000000..5010255
--- /dev/null
+++ b/library/Director/KickstartHelper.php
@@ -0,0 +1,555 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Objects\IcingaApiUser;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaZone;
+use Icinga\Module\Director\Core\CoreApi;
+use Icinga\Module\Director\Core\RestApiClient;
+use RuntimeException;
+
+class KickstartHelper
+{
+ /** @var Db */
+ protected $db;
+
+ /** @var CoreApi */
+ protected $api;
+
+ /** @var IcingaApiUser */
+ protected $apiUser;
+
+ /** @var IcingaEndpoint */
+ protected $deploymentEndpoint;
+
+ /** @var IcingaEndpoint[] */
+ protected $loadedEndpoints;
+
+ /** @var IcingaEndpoint[] */
+ protected $removeEndpoints;
+
+ /** @var IcingaZone[] */
+ protected $loadedZones;
+
+ /** @var IcingaZone[] */
+ protected $removeZones;
+
+ /** @var IcingaCommand[] */
+ protected $loadedCommands;
+
+ /** @var IcingaCommand[] */
+ protected $removeCommands;
+
+ protected $config = [
+ 'endpoint' => null,
+ 'host' => null,
+ 'port' => null,
+ 'username' => null,
+ 'password' => null,
+ ];
+
+ /**
+ * KickstartHelper constructor.
+ * @param Db $db
+ */
+ public function __construct(Db $db)
+ {
+ $this->db = $db;
+ }
+
+ /**
+ * Trigger a complete kickstart run
+ */
+ public function run()
+ {
+ $this->fetchEndpoints()
+ ->reconnectToDeploymentEndpoint()
+ ->fetchZones()
+ ->fetchCommands()
+ ->storeZones()
+ ->storeEndpoints()
+ ->storeCommands()
+ ->removeEndpoints()
+ ->removeZones()
+ ->removeCommands();
+
+ $this->apiUser()->store();
+ }
+
+ /**
+ * @return bool
+ */
+ public function isConfigured()
+ {
+ $config = $this->fetchConfigFileSection();
+ return array_key_exists('endpoint', $config)
+ && array_key_exists('username', $config);
+ }
+
+ /**
+ * @return KickstartHelper
+ * @throws ProgrammingError
+ */
+ public function loadConfigFromFile()
+ {
+ return $this->setConfig($this->fetchConfigFileSection());
+ }
+
+ /**
+ * @return array
+ */
+ protected function fetchConfigFileSection()
+ {
+ return Config::module('director', 'kickstart')
+ ->getSection('config')
+ ->toArray();
+ }
+
+ /**
+ * @param array $config
+ * @return $this
+ * @throws ProgrammingError
+ */
+ public function setConfig($config)
+ {
+ foreach ($config as $key => $value) {
+ if ($value === '') {
+ continue;
+ }
+
+ if (! array_key_exists($key, $this->config)) {
+ throw new ProgrammingError(
+ '"%s" is not a valid config setting for the kickstart helper',
+ $key
+ );
+ }
+
+ $this->config[$key] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isRequired()
+ {
+ $stats = $this->db->getObjectSummary();
+ return (int) $stats['apiuser']->cnt_total === 0;
+ }
+
+ /**
+ * @param $key
+ * @param mixed $default
+ * @return mixed
+ */
+ protected function getValue($key, $default = null)
+ {
+ if ($this->config[$key] === null) {
+ return $default;
+ }
+
+ return $this->config[$key];
+ }
+
+ /**
+ * @return IcingaApiUser
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function apiUser()
+ {
+ if ($this->apiUser === null) {
+ $name = $this->getValue('username');
+
+ $user = IcingaApiUser::create(array(
+ 'object_name' => $this->getValue('username'),
+ 'object_type' => 'external_object',
+ 'password' => $this->getValue('password')
+ ), $this->db);
+
+ if (IcingaApiUser::exists($name, $this->db)) {
+ $this->apiUser = IcingaApiUser::load($name, $this->db)->replaceWith($user);
+ } else {
+ $this->apiUser = $user;
+ }
+
+ $this->apiUser->store();
+ }
+
+ return $this->apiUser;
+ }
+
+ /**
+ * @param IcingaObject[] $objects
+ * @return IcingaObject[]
+ * @throws NestingError
+ */
+ protected function sortByParent(array $objects)
+ {
+ $sorted = array();
+
+ $cnt = 0;
+ while (! empty($objects)) {
+ $cnt++;
+ if ($cnt > 20) {
+ $this->throwObjectLoop($objects);
+ }
+
+ $unset = array();
+ foreach ($objects as $key => $object) {
+ $parentName = $object->get('parent');
+ if ($parentName === null || array_key_exists($parentName, $sorted)) {
+ $sorted[$object->getObjectName()] = $object;
+ $unset[] = $key;
+ }
+ }
+
+ foreach ($unset as $key) {
+ unset($objects[$key]);
+ }
+ }
+
+ return $sorted;
+ }
+
+ /**
+ * @param IcingaObject[] $objects
+ * @throws NestingError
+ */
+ protected function throwObjectLoop(array $objects)
+ {
+ $names = array();
+ if (empty($objects)) {
+ $class = 'Nothing';
+ } else {
+ $class = explode('/\\/', get_class(current($objects)))[0];
+ }
+
+ foreach ($objects as $object) {
+ $names[] = $object->getObjectName();
+ }
+
+ throw new NestingError(
+ 'Loop detected while resolving %s: %s',
+ $class,
+ implode(', ', $names)
+ );
+ }
+
+ /**
+ * @return $this
+ * @throws NestingError
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function fetchZones()
+ {
+ $db = $this->db;
+ $this->loadedZones = $this->sortByParent(
+ $this->api()->setDb($db)->getZoneObjects()
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function storeZones()
+ {
+ $db = $this->db;
+ $existing = IcingaObject::loadAllExternalObjectsByType('zone', $db);
+
+ foreach ($this->loadedZones as $name => $object) {
+ if (array_key_exists($name, $existing)) {
+ $object = $existing[$name]->replaceWith($object);
+ unset($existing[$name]);
+ }
+
+ $object->store();
+ }
+
+ $this->removeZones = $existing;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function removeZones()
+ {
+ return $this->removeObjects($this->removeEndpoints, 'External Zone');
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function fetchEndpoints()
+ {
+ $db = $this->db;
+ $this->loadedEndpoints = $this->api()->setDb($db)->getEndpointObjects();
+
+ $master = $this->getValue('endpoint');
+ if (array_key_exists($master, $this->loadedEndpoints)) {
+ $apiuser = $this->apiUser();
+ $apiuser->store();
+ $object = $this->loadedEndpoints[$master];
+ $object->apiuser = $apiuser->object_name;
+ $this->deploymentEndpoint = $object;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function storeEndpoints()
+ {
+ $db = $this->db;
+ $existing = IcingaObject::loadAllExternalObjectsByType('endpoint', $db);
+
+ foreach ($this->loadedEndpoints as $name => $object) {
+ if (array_key_exists($name, $existing)) {
+ $object = $existing[$name]->replaceWith($object);
+ unset($existing[$name]);
+ }
+
+ $object->store();
+ }
+
+ $this->removeEndpoints = $existing;
+
+ $db->settings()->master_zone = $this->deploymentEndpoint->zone;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function removeEndpoints()
+ {
+ return $this->removeObjects($this->removeEndpoints, 'External Endpoint');
+ }
+
+ /**
+ * @return $this
+ * @throws ConfigurationError
+ */
+ protected function reconnectToDeploymentEndpoint()
+ {
+ $master = $this->getValue('endpoint');
+
+ if (! $this->deploymentEndpoint) {
+ throw new ConfigurationError(
+ 'I found no Endpoint object called "%s" on %s:%d',
+ $master,
+ $this->getHost(),
+ $this->getPort()
+ );
+ }
+
+ $ep = $this->deploymentEndpoint;
+
+ $epHost = $ep->get('host');
+ if (! $epHost) {
+ $epHost = $ep->getObjectName();
+ }
+
+ try {
+ $this->switchToDeploymentApi()->getStatus();
+ } catch (Exception $e) {
+ throw new ConfigurationError(
+ 'I was unable to re-establish a connection to the Endpoint "%s" (%s:%d).'
+ . ' When reconnecting to the configured Endpoint (%s:%d) I get an error: %s'
+ . ' Please re-check your Icinga 2 endpoint configuration',
+ $master,
+ $this->getHost(),
+ $this->getPort(),
+ $epHost,
+ $ep->get('port'),
+ $e->getMessage()
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function fetchCommands()
+ {
+ $api = $this->api()->setDb($this->db);
+ $this->loadedCommands = array_merge(
+ $api->getSpecificCommandObjects('Check'),
+ $api->getSpecificCommandObjects('Notification'),
+ $api->getSpecificCommandObjects('Event')
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function storeCommands()
+ {
+ $db = $this->db;
+ $existing = IcingaObject::loadAllExternalObjectsByType('command', $db);
+
+ foreach ($this->loadedCommands as $name => $object) {
+ if (array_key_exists($name, $existing)) {
+ $object = $existing[$name]->replaceWith($object);
+ unset($existing[$name]);
+ }
+
+ $object->store();
+ }
+
+ $this->removeCommands = $existing;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function removeCommands()
+ {
+ return $this->removeObjects($this->removeCommands, 'External Command');
+ }
+
+ protected function removeObjects(array $objects, $typeName)
+ {
+ foreach ($objects as $object) {
+ try {
+ $object->delete();
+ } catch (Exception $e) {
+ throw new RuntimeException(sprintf(
+ "Failed to remove %s '%s', it's eventually still in use",
+ $typeName,
+ $object->getObjectName()
+ ), 0, $e);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param Db $db
+ * @return $this
+ */
+ public function setDb(Db $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ protected function getHost()
+ {
+ return $this->getValue('host', $this->getValue('endpoint'));
+ }
+
+ /**
+ * @return int
+ */
+ protected function getPort()
+ {
+ return (int) $this->getValue('port', 5665);
+ }
+
+ /**
+ * @return CoreApi
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getDeploymentApi()
+ {
+ unset($this->api);
+ $ep = $this->deploymentEndpoint;
+
+ $epHost = $ep->get('host');
+ if (!$epHost) {
+ $epHost = $ep->object_name;
+ }
+
+ $client = new RestApiClient(
+ $epHost,
+ $ep->get('port')
+ );
+
+ $apiuser = $this->apiUser();
+ $client->setCredentials($apiuser->object_name, $apiuser->password);
+
+ $api = new CoreApi($client);
+ $api->setDb($this->db);
+
+ return $api;
+ }
+
+ /**
+ * @return CoreApi
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getConfiguredApi()
+ {
+ unset($this->api);
+ $client = new RestApiClient(
+ $this->getHost(),
+ $this->getPort()
+ );
+
+ $apiuser = $this->apiUser();
+ $client->setCredentials($apiuser->object_name, $apiuser->password);
+
+ $api = new CoreApi($client);
+ $api->setDb($this->db);
+
+ return $api;
+ }
+
+ /**
+ * @return CoreApi
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function switchToDeploymentApi()
+ {
+ return $this->api = $this->getDeploymentApi();
+ }
+
+ /**
+ * @return CoreApi
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function api()
+ {
+ if ($this->api === null) {
+ $this->api = $this->getConfiguredApi();
+ }
+
+ return $this->api;
+ }
+}
diff --git a/library/Director/Monitoring.php b/library/Director/Monitoring.php
new file mode 100644
index 0000000..f5d4108
--- /dev/null
+++ b/library/Director/Monitoring.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+
+class Monitoring
+{
+ protected $backend;
+
+ public function __construct()
+ {
+ $app = Icinga::app();
+ $modules = $app->getModuleManager();
+ if (!$modules->hasLoaded('monitoring') && $app->isCli()) {
+ $app->getModuleManager()->loadEnabledModules();
+ }
+
+ if ($modules->hasLoaded('monitoring')) {
+ $this->backend = MonitoringBackend::instance();
+ }
+ }
+
+ public function isAvailable()
+ {
+ return $this->backend !== null;
+ }
+
+ public function hasHost($hostname)
+ {
+ return $this->backend->select()->from('hostStatus', [
+ 'hostname' => 'host_name',
+ ])->where('host_name', $hostname)->fetchOne() === $hostname;
+ }
+
+ public function hasService($hostname, $service)
+ {
+ return (array) $this->prepareServiceKeyColumnQuery($hostname, $service)->fetchRow() === [
+ 'hostname' => $hostname,
+ 'service' => $service,
+ ];
+ }
+
+ public function authCanEditHost(Auth $auth, $hostname)
+ {
+ if ($auth->hasPermission('director/monitoring/hosts')) {
+ $restriction = null;
+ foreach ($auth->getRestrictions('director/monitoring/rw-object-filter') as $restriction) {
+ if ($this->hasHostWithExtraFilter($hostname, Filter::fromQueryString($restriction))) {
+ return true;
+ }
+ }
+ if ($restriction === null) {
+ return $this->hasHost($hostname);
+ }
+ }
+
+ return false;
+ }
+
+ public function authCanEditService(Auth $auth, $hostname, $service)
+ {
+ if ($hostname === null || $service === null) {
+ // TODO: UUID support!
+ return false;
+ }
+ if ($auth->hasPermission('director/monitoring/services')) {
+ $restriction = null;
+ foreach ($auth->getRestrictions('director/monitoring/rw-object-filter') as $restriction) {
+ if ($this->hasServiceWithExtraFilter($hostname, $service, Filter::fromQueryString($restriction))) {
+ return true;
+ }
+ }
+ if ($restriction === null) {
+ return $this->hasService($hostname, $service);
+ }
+ }
+
+ return false;
+ }
+
+ public function hasHostWithExtraFilter($hostname, Filter $filter)
+ {
+ return $this->backend->select()->from('hostStatus', [
+ 'hostname' => 'host_name',
+ ])->where('host_name', $hostname)->applyFilter($filter)->fetchOne() === $hostname;
+ }
+
+ public function hasServiceWithExtraFilter($hostname, $service, Filter $filter)
+ {
+ return (array) $this
+ ->prepareServiceKeyColumnQuery($hostname, $service)
+ ->applyFilter($filter)
+ ->fetchRow() === [
+ 'hostname' => $hostname,
+ 'service' => $service,
+ ];
+ }
+
+ public function getHostState($hostname)
+ {
+ $hostStates = [
+ '0' => 'up',
+ '1' => 'down',
+ '2' => 'unreachable',
+ '99' => 'pending',
+ ];
+
+ $query = $this->backend->select()->from('hostStatus', [
+ 'hostname' => 'host_name',
+ 'state' => 'host_state',
+ 'problem' => 'host_problem',
+ 'acknowledged' => 'host_acknowledged',
+ 'in_downtime' => 'host_in_downtime',
+ 'output' => 'host_output',
+ ])->where('host_name', $hostname);
+
+ $res = $query->fetchRow();
+ if ($res === false) {
+ $res = (object) [
+ 'hostname' => $hostname,
+ 'state' => '99',
+ 'problem' => '0',
+ 'acknowledged' => '0',
+ 'in_downtime' => '0',
+ 'output' => null,
+ ];
+ }
+
+ $res->state = $hostStates[$res->state];
+
+ return $res;
+ }
+
+ protected function prepareServiceKeyColumnQuery($hostname, $service)
+ {
+ return $this->backend
+ ->select()
+ ->from('serviceStatus', [
+ 'hostname' => 'host_name',
+ 'service' => 'service_description',
+ ])
+ ->where('host_name', $hostname)
+ ->where('service_description', $service);
+ }
+}
diff --git a/library/Director/Objects/DirectorActivityLog.php b/library/Director/Objects/DirectorActivityLog.php
new file mode 100644
index 0000000..cb041b6
--- /dev/null
+++ b/library/Director/Objects/DirectorActivityLog.php
@@ -0,0 +1,232 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+use Icinga\Authentication\Auth;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+
+class DirectorActivityLog extends DbObject
+{
+ const ACTION_CREATE = 'create';
+ const ACTION_DELETE = 'delete';
+ const ACTION_MODIFY = 'modify';
+
+ /** @deprecated */
+ const AUDIT_REMOVE = 'remove';
+
+ protected $table = 'director_activity_log';
+
+ protected $keyName = 'id';
+
+ protected $autoincKeyName = 'id';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'object_name' => null,
+ 'action_name' => null,
+ 'object_type' => null,
+ 'old_properties' => null,
+ 'new_properties' => null,
+ 'author' => null,
+ 'change_time' => null,
+ 'checksum' => null,
+ 'parent_checksum' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'checksum',
+ 'parent_checksum'
+ ];
+
+ /** @var ?string */
+ protected static $overriddenUsername = null;
+
+ /**
+ * @param $name
+ *
+ * @codingStandardsIgnoreStart
+ *
+ * @return self
+ */
+ protected function setObject_Name($name)
+ {
+ // @codingStandardsIgnoreEnd
+
+ if ($name === null) {
+ $name = '';
+ }
+
+ return $this->reallySet('object_name', $name);
+ }
+
+ public static function username()
+ {
+ if (self::$overriddenUsername) {
+ return self::$overriddenUsername;
+ }
+
+ if (Icinga::app()->isCli()) {
+ return 'cli';
+ }
+
+ $auth = Auth::getInstance();
+ if ($auth->isAuthenticated()) {
+ return $auth->getUser()->getUsername();
+ } elseif (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {
+ return '<' . $_SERVER['HTTP_X_FORWARDED_FOR'] . '>';
+ } elseif (array_key_exists('REMOTE_ADDR', $_SERVER)) {
+ return '<' . $_SERVER['REMOTE_ADDR'] . '>';
+ } else {
+ return '<unknown>';
+ }
+ }
+
+ protected static function ip()
+ {
+ if (Icinga::app()->isCli()) {
+ return 'cli';
+ }
+
+ if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
+ return $_SERVER['REMOTE_ADDR'];
+ } else {
+ return '0.0.0.0';
+ }
+ }
+
+ /**
+ * @param Db $connection
+ * @return DirectorActivityLog
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function loadLatest(Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from('director_activity_log', ['id' => 'MAX(id)']);
+
+ return static::load($db->fetchOne($query), $connection);
+ }
+
+ public static function logCreation(IcingaObject $object, Db $db)
+ {
+ // TODO: extend this to support non-IcingaObjects and multikey objects
+ $name = $object->getObjectName();
+ $type = $object->getTableName();
+ $newProps = $object->toJson(null, true);
+
+ $data = [
+ 'object_name' => $name,
+ 'action_name' => self::ACTION_CREATE,
+ 'author' => static::username(),
+ 'object_type' => $type,
+ 'new_properties' => $newProps,
+ 'change_time' => date('Y-m-d H:i:s'),
+ 'parent_checksum' => $db->getLastActivityChecksum()
+ ];
+
+ $data['checksum'] = sha1(json_encode($data), true);
+ $data['parent_checksum'] = hex2bin($data['parent_checksum']);
+
+ static::audit($db, [
+ 'action' => self::ACTION_CREATE,
+ 'object_type' => $type,
+ 'object_name' => $name,
+ 'new_props' => $newProps,
+ ]);
+
+ return static::create($data)->store($db);
+ }
+
+ public static function logModification(IcingaObject $object, Db $db)
+ {
+ $name = $object->getObjectName();
+ $type = $object->getTableName();
+ $oldProps = json_encode($object->getPlainUnmodifiedObject());
+ $newProps = $object->toJson(null, true);
+
+ $data = [
+ 'object_name' => $name,
+ 'action_name' => self::ACTION_MODIFY,
+ 'author' => static::username(),
+ 'object_type' => $type,
+ 'old_properties' => $oldProps,
+ 'new_properties' => $newProps,
+ 'change_time' => date('Y-m-d H:i:s'),
+ 'parent_checksum' => $db->getLastActivityChecksum()
+ ];
+
+ $data['checksum'] = sha1(json_encode($data), true);
+ $data['parent_checksum'] = hex2bin($data['parent_checksum']);
+
+ static::audit($db, [
+ 'action' => self::ACTION_MODIFY,
+ 'object_type' => $type,
+ 'object_name' => $name,
+ 'old_props' => $oldProps,
+ 'new_props' => $newProps,
+ ]);
+
+ return static::create($data)->store($db);
+ }
+
+ public static function logRemoval(IcingaObject $object, Db $db)
+ {
+ $name = $object->getObjectName();
+ $type = $object->getTableName();
+ $oldProps = json_encode($object->getPlainUnmodifiedObject());
+
+ $data = [
+ 'object_name' => $name,
+ 'action_name' => self::ACTION_DELETE,
+ 'author' => static::username(),
+ 'object_type' => $type,
+ 'old_properties' => $oldProps,
+ 'change_time' => date('Y-m-d H:i:s'),
+ 'parent_checksum' => $db->getLastActivityChecksum()
+ ];
+
+ $data['checksum'] = sha1(json_encode($data), true);
+ $data['parent_checksum'] = hex2bin($data['parent_checksum']);
+
+ static::audit($db, [
+ 'action' => self::AUDIT_REMOVE,
+ 'object_type' => $type,
+ 'object_name' => $name,
+ 'old_props' => $oldProps
+ ]);
+
+ return static::create($data)->store($db);
+ }
+
+ public static function audit(Db $db, $properties)
+ {
+ if ($db->settings()->get('enable_audit_log') !== 'y') {
+ return;
+ }
+
+ $log = [];
+ $properties = array_merge([
+ 'username' => static::username(),
+ 'address' => static::ip(),
+ ], $properties);
+
+ foreach ($properties as $key => $val) {
+ $log[] = "$key=" . json_encode($val);
+ }
+
+ Logger::info('(director) ' . implode(' ', $log));
+ }
+
+ public static function overrideUsername($username)
+ {
+ self::$overriddenUsername = $username;
+ }
+
+ public static function restoreUsername()
+ {
+ self::$overriddenUsername = null;
+ }
+}
diff --git a/library/Director/Objects/DirectorDatafield.php b/library/Director/Objects/DirectorDatafield.php
new file mode 100644
index 0000000..84db068
--- /dev/null
+++ b/library/Director/Objects/DirectorDatafield.php
@@ -0,0 +1,344 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\CompareBasketObject;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\Forms\IcingaServiceForm;
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Resolver\OverriddenVarsResolver;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use InvalidArgumentException;
+use Zend_Form_Element as ZfElement;
+
+class DirectorDatafield extends DbObjectWithSettings
+{
+ protected $table = 'director_datafield';
+
+ protected $keyName = 'id';
+
+ protected $autoincKeyName = 'id';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'category_id' => null,
+ 'varname' => null,
+ 'caption' => null,
+ 'description' => null,
+ 'datatype' => null,
+ 'format' => null,
+ ];
+
+ protected $relations = [
+ 'category' => 'DirectorDatafieldCategory'
+ ];
+
+ protected $settingsTable = 'director_datafield_setting';
+
+ protected $settingsRemoteId = 'datafield_id';
+
+ /** @var DirectorDatafieldCategory|null */
+ private $category;
+
+ private $object;
+
+ public static function fromDbRow($row, Db $connection)
+ {
+ $obj = static::create((array) $row, $connection);
+ $obj->loadedFromDb = true;
+ // TODO: $obj->setUnmodified();
+ $obj->hasBeenModified = false;
+ $obj->modifiedProperties = array();
+ $settings = $obj->getSettings();
+ // TODO: eventually prefetch
+ $obj->onLoadFromDb();
+
+ // Restoring values eventually destroyed by onLoadFromDb
+ foreach ($settings as $key => $value) {
+ $obj->settings[$key] = $value;
+ }
+
+ return $obj;
+ }
+
+ public function hasCategory()
+ {
+ return $this->category !== null || $this->get('category_id') !== null;
+ }
+
+ /**
+ * @return DirectorDatafieldCategory|null
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getCategory()
+ {
+ if ($this->category) {
+ return $this->category;
+ } elseif ($id = $this->get('category_id')) {
+ return DirectorDatafieldCategory::loadWithAutoIncId($id, $this->getConnection());
+ } else {
+ return null;
+ }
+ }
+
+ public function getCategoryName()
+ {
+ $category = $this->getCategory();
+ if ($category === null) {
+ return null;
+ } else {
+ return $category->get('category_name');
+ }
+ }
+
+ public function setCategory($category)
+ {
+ if ($category === null) {
+ $this->category = null;
+ $this->set('category_id', null);
+ } elseif ($category instanceof DirectorDatafieldCategory) {
+ if ($category->hasBeenLoadedFromDb()) {
+ $this->set('category_id', $category->get('id'));
+ }
+ $this->category = $category;
+ } else {
+ if (DirectorDatafieldCategory::exists($category, $this->getConnection())) {
+ $this->setCategory(DirectorDatafieldCategory::load($category, $this->getConnection()));
+ } else {
+ $this->setCategory(DirectorDatafieldCategory::create([
+ 'category_name' => $category
+ ], $this->getConnection()));
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return object
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ $plain = (object) $this->getProperties();
+ $plain->originalId = $plain->id;
+ unset($plain->id);
+ $plain->settings = (object) $this->getSettings();
+
+ if (property_exists($plain->settings, 'datalist_id')) {
+ $plain->settings->datalist = DirectorDatalist::loadWithAutoIncId(
+ $plain->settings->datalist_id,
+ $this->getConnection()
+ )->get('list_name');
+ unset($plain->settings->datalist_id);
+ }
+ if (property_exists($plain, 'category_id')) {
+ $plain->category = $this->getCategoryName();
+ unset($plain->category_id);
+ }
+
+ return $plain;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return DirectorDatafield
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ if (isset($properties['originalId'])) {
+ $id = $properties['originalId'];
+ unset($properties['originalId']);
+ } else {
+ $id = null;
+ }
+
+ if (isset($properties['settings']->datalist)) {
+ // Just try to load the list, import should fail if missing
+ $list = DirectorDatalist::load(
+ $properties['settings']->datalist,
+ $db
+ );
+ } else {
+ $list = null;
+ }
+
+ $compare = Json::decode(Json::encode($properties));
+ if ($id && static::exists($id, $db)) {
+ $existing = static::loadWithAutoIncId($id, $db);
+ $existingProperties = (array) $existing->export();
+ unset($existingProperties['originalId']);
+ if (CompareBasketObject::equals((object) $compare, (object) $existingProperties)) {
+ return $existing;
+ }
+ }
+
+ if ($list) {
+ unset($properties['settings']->datalist);
+ $properties['settings']->datalist_id = $list->get('id');
+ }
+
+ $dba = $db->getDbAdapter();
+ $query = $dba->select()
+ ->from('director_datafield')
+ ->where('varname = ?', $plain->varname);
+ $candidates = DirectorDatafield::loadAll($db, $query);
+
+ foreach ($candidates as $candidate) {
+ $export = $candidate->export();
+ unset($export->originalId);
+ CompareBasketObject::normalize($export);
+ if (CompareBasketObject::equals($export, $compare)) {
+ return $candidate;
+ }
+ }
+
+ return static::create($properties, $db);
+ }
+
+ protected function beforeStore()
+ {
+ if ($this->category) {
+ if (!$this->category->hasBeenLoadedFromDb()) {
+ throw new \RuntimeException('Trying to store a datafield with an unstored Category');
+ }
+ $this->set('category_id', $this->category->get('id'));
+ }
+ }
+
+ protected function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ }
+
+ protected function getObject()
+ {
+ return $this->object;
+ }
+
+ public function getFormElement(DirectorObjectForm $form, $name = null)
+ {
+ $className = $this->get('datatype');
+
+ if ($name === null) {
+ $name = 'var_' . $this->get('varname');
+ }
+
+ if (! class_exists($className)) {
+ $form->addElement('text', $name, array('disabled' => 'disabled'));
+ $el = $form->getElement($name);
+ $msg = $form->translate('Form element could not be created, %s is missing');
+ $el->addError(sprintf($msg, $className));
+ return $el;
+ }
+
+ /** @var DataTypeHook $dataType */
+ $dataType = new $className;
+ $dataType->setSettings($this->getSettings());
+ $el = $dataType->getFormElement($name, $form);
+
+ if ($this->getSetting('icinga_type') !== 'command'
+ && $this->getSetting('is_required') === 'y'
+ ) {
+ $el->setRequired(true);
+ }
+ if ($caption = $this->get('caption')) {
+ $el->setLabel($caption);
+ }
+
+ if ($description = $this->get('description')) {
+ $el->setDescription($description);
+ }
+
+ $this->applyObjectData($el, $form);
+
+ return $el;
+ }
+
+ protected function applyObjectData(ZfElement $el, DirectorObjectForm $form)
+ {
+ $object = $form->getObject();
+ if (! ($object instanceof IcingaObject)) {
+ return;
+ }
+ if ($object->isTemplate()) {
+ $el->setRequired(false);
+ }
+
+ $varName = $this->get('varname');
+ $inherited = $origin = null;
+
+ if ($form instanceof IcingaServiceForm && $form->providesOverrides()) {
+ $resolver = new OverriddenVarsResolver($form->getDb());
+ $vars = $resolver->fetchForServiceName($form->getHost(), $object->getObjectName());
+ foreach ($vars as $host => $values) {
+ if (\property_exists($values, $varName)) {
+ $inherited = $values->$varName;
+ $origin = $host;
+ }
+ }
+ }
+
+ if ($inherited === null) {
+ $inherited = $object->getInheritedVar($varName);
+ if (null !== $inherited) {
+ $origin = $object->getOriginForVar($varName);
+ }
+ }
+
+ if ($inherited === null) {
+ $cmd = $this->eventuallyGetResolvedCommandVar($object, $varName);
+ if ($cmd !== null) {
+ list($inherited, $origin) = $cmd;
+ }
+ }
+
+ if ($inherited !== null) {
+ $form->setInheritedValue($el, $inherited, $origin);
+ }
+ }
+
+ protected function eventuallyGetResolvedCommandVar(IcingaObject $object, $varName)
+ {
+ if (! $object->hasRelation('check_command')) {
+ return null;
+ }
+
+ // TODO: Move all of this elsewhere and test it
+ try {
+ /** @var IcingaCommand $command */
+ $command = $object->getResolvedRelated('check_command');
+ if ($command === null) {
+ return null;
+ }
+ $inherited = $command->vars()->get($varName);
+ $inheritedFrom = null;
+
+ if ($inherited !== null) {
+ $inherited = $inherited->getValue();
+ }
+
+ if ($inherited === null) {
+ $inherited = $command->getResolvedVar($varName);
+ if ($inherited === null) {
+ $inheritedFrom = $command->getOriginForVar($varName);
+ }
+ } else {
+ $inheritedFrom = $command->getObjectName();
+ }
+
+ $inherited = $command->getResolvedVar($varName);
+
+ return [$inherited, $inheritedFrom];
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+}
diff --git a/library/Director/Objects/DirectorDatafieldCategory.php b/library/Director/Objects/DirectorDatafieldCategory.php
new file mode 100644
index 0000000..6cb4fb4
--- /dev/null
+++ b/library/Director/Objects/DirectorDatafieldCategory.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+
+class DirectorDatafieldCategory extends DbObject
+{
+ protected $table = 'director_datafield_category';
+
+ protected $keyName = 'category_name';
+
+ protected $autoincKeyName = 'id';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'category_name' => null,
+ 'description' => null,
+ ];
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @return object
+ */
+ public function export()
+ {
+ $plain = (object) $this->getProperties();
+ $plain->originalId = $plain->id;
+ unset($plain->id);
+ return $plain;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return static
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ unset($properties['originalId']);
+ $key = $properties['category_name'];
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Cannot import, DatafieldCategory "%s" already exists',
+ $key
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ $object->setProperties($properties);
+
+ return $object;
+ }
+}
diff --git a/library/Director/Objects/DirectorDatalist.php b/library/Director/Objects/DirectorDatalist.php
new file mode 100644
index 0000000..ae5c983
--- /dev/null
+++ b/library/Director/Objects/DirectorDatalist.php
@@ -0,0 +1,225 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Exception;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+
+class DirectorDatalist extends DbObject implements ExportInterface
+{
+ protected $table = 'director_datalist';
+
+ protected $keyName = 'list_name';
+
+ protected $autoincKeyName = 'id';
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'list_name' => null,
+ 'owner' => null
+ );
+
+ /** @var DirectorDatalistEntry[] */
+ protected $storedEntries;
+
+ public function getUniqueIdentifier()
+ {
+ return $this->get('list_name');
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return static
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws DuplicateKeyException
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ if (isset($properties['originalId'])) {
+ unset($properties['originalId']);
+ } else {
+ $id = null;
+ }
+ $name = $properties['list_name'];
+
+ if ($replace && static::exists($name, $db)) {
+ $object = static::load($name, $db);
+ } elseif (static::exists($name, $db)) {
+ throw new DuplicateKeyException(
+ 'Data List %s already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ public function setEntries($entries)
+ {
+ $existing = $this->getStoredEntries();
+
+ $new = [];
+ $seen = [];
+ $modified = false;
+
+ foreach ($entries as $entry) {
+ $name = $entry->entry_name;
+ $entry = DirectorDatalistEntry::create((array) $entry);
+ $seen[$name] = true;
+ if (isset($existing[$name])) {
+ $existing[$name]->replaceWith($entry);
+ if (! $modified && $existing[$name]->hasBeenModified()) {
+ $modified = true;
+ }
+ } else {
+ $modified = true;
+ $new[] = $entry;
+ }
+ }
+
+ foreach (array_keys($existing) as $key) {
+ if (! isset($seen[$key])) {
+ $existing[$key]->markForRemoval();
+ $modified = true;
+ }
+ }
+
+ foreach ($new as $entry) {
+ $existing[$entry->get('entry_name')] = $entry;
+ }
+
+ if ($modified) {
+ $this->hasBeenModified = true;
+ }
+
+ $this->storedEntries = $existing;
+ ksort($this->storedEntries);
+
+ return $this;
+ }
+
+ protected function beforeDelete()
+ {
+ if ($this->hasBeenUsed()) {
+ throw new Exception(
+ sprintf(
+ "Cannot delete '%s', as the datalist '%s' is currently being used.",
+ $this->get('list_name'),
+ $this->get('list_name')
+ )
+ );
+ }
+ }
+
+ protected function hasBeenUsed()
+ {
+ $datalistType = 'Icinga\\Module\\Director\\DataType\\DataTypeDatalist';
+ $db = $this->getDb();
+
+ $dataFieldsCheck = $db->select()
+ ->from(['df' =>'director_datafield'], ['varname'])
+ ->join(
+ ['dfs' => 'director_datafield_setting'],
+ 'dfs.datafield_id = df.id AND dfs.setting_name = \'datalist_id\'',
+ []
+ )
+ ->join(
+ ['l' => 'director_datalist'],
+ 'l.id = dfs.setting_value',
+ []
+ )
+ ->where('datatype = ?', $datalistType)
+ ->where('setting_value = ?', $this->get('id'));
+
+ if ($db->fetchOne($dataFieldsCheck)) {
+ return true;
+ }
+
+ $syncCheck = $db->select()
+ ->from(['sp' =>'sync_property'], ['source_expression'])
+ ->where('sp.destination_field = ?', 'list_id')
+ ->where('sp.source_expression = ?', $this->get('id'));
+
+ if ($db->fetchOne($syncCheck)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @throws DuplicateKeyException
+ */
+ public function onStore()
+ {
+ if ($this->storedEntries) {
+ $db = $this->getConnection();
+ $removedKeys = [];
+ $myId = $this->get('id');
+
+ foreach ($this->storedEntries as $key => $entry) {
+ if ($entry->shouldBeRemoved()) {
+ $entry->delete();
+ $removedKeys[] = $key;
+ } else {
+ if (! $entry->hasBeenLoadedFromDb()) {
+ $entry->set('list_id', $myId);
+ }
+ $entry->set('list_id', $myId);
+ $entry->store($db);
+ }
+ }
+
+ foreach ($removedKeys as $key) {
+ unset($this->storedEntries[$key]);
+ }
+ }
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @return object
+ */
+ public function export()
+ {
+ $plain = (object) $this->getProperties();
+ $plain->originalId = $plain->id;
+ unset($plain->id);
+
+ $plain->entries = [];
+ foreach ($this->getStoredEntries() as $key => $entry) {
+ if ($entry->shouldBeRemoved()) {
+ continue;
+ }
+ $plainEntry = (object) $entry->getProperties();
+ unset($plainEntry->list_id);
+
+ $plain->entries[] = $plainEntry;
+ }
+
+ return $plain;
+ }
+
+ protected function getStoredEntries()
+ {
+ if ($this->storedEntries === null) {
+ if ($id = $this->get('id')) {
+ $this->storedEntries = DirectorDatalistEntry::loadAllForList($this);
+ ksort($this->storedEntries);
+ } else {
+ $this->storedEntries = [];
+ }
+ }
+
+ return $this->storedEntries;
+ }
+}
diff --git a/library/Director/Objects/DirectorDatalistEntry.php b/library/Director/Objects/DirectorDatalistEntry.php
new file mode 100644
index 0000000..086686a
--- /dev/null
+++ b/library/Director/Objects/DirectorDatalistEntry.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use RuntimeException;
+
+class DirectorDatalistEntry extends DbObject
+{
+ protected $keyName = ['list_id', 'entry_name'];
+
+ protected $table = 'director_datalist_entry';
+
+ private $shouldBeRemoved = false;
+
+ protected $defaultProperties = [
+ 'list_id' => null,
+ 'entry_name' => null,
+ 'entry_value' => null,
+ 'format' => null,
+ 'allowed_roles' => null,
+ ];
+
+ /**
+ * @param DirectorDatalist $list
+ * @return static[]
+ */
+ public static function loadAllForList(DirectorDatalist $list)
+ {
+ $query = $list->getDb()
+ ->select()
+ ->from('director_datalist_entry')
+ ->where('list_id = ?', $list->get('id'))
+ ->order('entry_name ASC');
+
+ return static::loadAll($list->getConnection(), $query, 'entry_name');
+ }
+
+ /**
+ * @param $roles
+ * @codingStandardsIgnoreStart
+ */
+ public function setAllowed_roles($roles)
+ {
+ // @codingStandardsIgnoreEnd
+ $key = 'allowed_roles';
+ if (is_array($roles)) {
+ $this->reallySet($key, json_encode($roles));
+ } elseif (null === $roles) {
+ $this->reallySet($key, null);
+ } else {
+ throw new RuntimeException(
+ 'Expected array or null for allowed_roles, got %s',
+ var_export($roles, 1)
+ );
+ }
+ }
+
+ /**
+ * @return array|null
+ * @codingStandardsIgnoreStart
+ */
+ public function getAllowed_roles()
+ {
+ // @codingStandardsIgnoreEnd
+ $roles = $this->getProperty('allowed_roles');
+ if (is_string($roles)) {
+ return json_decode($roles);
+ } else {
+ return $roles;
+ }
+ }
+
+ public function replaceWith(DirectorDatalistEntry $object)
+ {
+ $this->set('entry_value', $object->get('entry_value'));
+ if ($object->get('format')) {
+ $this->set('format', $object->get('format'));
+ }
+
+ return $this;
+ }
+
+ public function merge(DirectorDatalistEntry $object)
+ {
+ return $this->replaceWith($object);
+ }
+
+ public function markForRemoval($remove = true)
+ {
+ $this->shouldBeRemoved = $remove;
+
+ return $this;
+ }
+
+ public function shouldBeRemoved()
+ {
+ return $this->shouldBeRemoved;
+ }
+
+ public function onInsert()
+ {
+ }
+
+ public function onUpdate()
+ {
+ }
+
+ public function onDelete()
+ {
+ }
+}
diff --git a/library/Director/Objects/DirectorDeploymentLog.php b/library/Director/Objects/DirectorDeploymentLog.php
new file mode 100644
index 0000000..0794a3c
--- /dev/null
+++ b/library/Director/Objects/DirectorDeploymentLog.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Exception;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Core\CoreApi;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Util;
+
+class DirectorDeploymentLog extends DbObject
+{
+ protected $table = 'director_deployment_log';
+
+ protected $keyName = 'id';
+
+ protected $autoincKeyName = 'id';
+
+ protected $config;
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'config_checksum' => null,
+ 'last_activity_checksum' => null,
+ 'peer_identity' => null,
+ 'start_time' => null,
+ 'end_time' => null,
+ 'abort_time' => null,
+ 'duration_connection' => null,
+ 'duration_dump' => null,
+ 'stage_name' => null,
+ 'stage_collected' => null,
+ 'connection_succeeded' => null,
+ 'dump_succeeded' => null,
+ 'startup_succeeded' => null,
+ 'username' => null,
+ 'startup_log' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'config_checksum',
+ 'last_activity_checksum'
+ ];
+
+ public function getConfigHexChecksum()
+ {
+ return bin2hex($this->config_checksum);
+ }
+
+ public function getConfig()
+ {
+ if ($this->config === null) {
+ $this->config = IcingaConfig::load($this->config_checksum, $this->connection);
+ }
+
+ return $this->config;
+ }
+
+ public function isPending()
+ {
+ return $this->dump_succeeded === 'y' && $this->startup_log === null;
+ }
+
+ public function succeeded()
+ {
+ return $this->startup_succeeded === 'y';
+ }
+
+ public function configEquals(IcingaConfig $config)
+ {
+ return $this->config_checksum === $config->getChecksum();
+ }
+
+ public function getDeploymentTimestamp()
+ {
+ return strtotime($this->start_time);
+ }
+
+ public static function hasDeployments(Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from(
+ 'director_deployment_log',
+ array('c' => 'COUNT(*)')
+ );
+
+ return (int) $db->fetchOne($query) > 0;
+ }
+
+ public static function getConfigChecksumForStageName(Db $connection, $stage)
+ {
+ if ($stage === null) {
+ return null;
+ }
+ $db = $connection->getDbAdapter();
+ $query = $db->select()
+ ->from(
+ array('l' => 'director_deployment_log'),
+ array('c' => $connection->dbHexFunc('l.config_checksum'))
+ )->where('l.stage_name = ?');
+
+ return $db->fetchOne($query, $stage);
+ }
+
+ /**
+ * @param Db $connection
+ * @return DirectorDeploymentLog
+ * @throws NotFoundError
+ */
+ public static function loadLatest(Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from(
+ array('l' => 'director_deployment_log'),
+ array('id' => 'MAX(l.id)')
+ );
+
+ return static::load($db->fetchOne($query), $connection);
+ }
+
+ /**
+ * @param Db $connection
+ * @return ?DirectorDeploymentLog
+ */
+ public static function optionalLatest(Db $connection)
+ {
+ try {
+ return static::loadLatest($connection);
+ } catch (NotFoundError $exception) {
+ return null;
+ }
+ }
+
+ /**
+ * @param CoreApi $api
+ * @param Db $connection
+ * @return DirectorDeploymentLog
+ */
+ public static function getRelatedToActiveStage(CoreApi $api, Db $connection)
+ {
+ try {
+ return static::requireRelatedToActiveStage($api, $connection);
+ } catch (Exception $e) {
+ return null;
+ }
+ }
+
+ /**
+ * @param CoreApi $api
+ * @param Db $connection
+ * @return DirectorDeploymentLog
+ * @throws NotFoundError
+ */
+ public static function requireRelatedToActiveStage(CoreApi $api, Db $connection)
+ {
+ $stage = $api->getActiveStageName();
+
+ if (! strlen($stage)) {
+ throw new NotFoundError('Got no active stage name');
+ }
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from(
+ ['l' => 'director_deployment_log'],
+ ['id' => 'MAX(l.id)']
+ )->where('l.stage_name = ?', $stage);
+
+ return static::load($db->fetchOne($query), $connection);
+ }
+
+ /**
+ * @return static[]
+ */
+ public static function getUncollected(Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()
+ ->from('director_deployment_log')
+ ->where('stage_name IS NOT NULL')
+ ->where('stage_collected IS NULL')
+ ->where('startup_succeeded IS NULL')
+ ->order('stage_name');
+
+ return static::loadAll($connection, $query, 'stage_name');
+ }
+
+ public static function hasUncollected(Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()
+ ->from('director_deployment_log', ['cnt' => 'COUNT(*)'])
+ ->where('stage_name IS NOT NULL')
+ ->where('stage_collected IS NULL')
+ ->where('startup_succeeded IS NULL');
+
+ return $db->fetchOne($query) > 0;
+ }
+}
diff --git a/library/Director/Objects/DirectorJob.php b/library/Director/Objects/DirectorJob.php
new file mode 100644
index 0000000..361f764
--- /dev/null
+++ b/library/Director/Objects/DirectorJob.php
@@ -0,0 +1,314 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Daemon\Logger;
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\Hook\JobHook;
+use Exception;
+use InvalidArgumentException;
+
+class DirectorJob extends DbObjectWithSettings implements ExportInterface, InstantiatedViaHook
+{
+ /** @var JobHook */
+ protected $job;
+
+ protected $table = 'director_job';
+
+ protected $keyName = 'job_name';
+
+ protected $autoincKeyName = 'id';
+
+ protected $protectAutoinc = false;
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'job_name' => null,
+ 'job_class' => null,
+ 'disabled' => null,
+ 'run_interval' => null,
+ 'last_attempt_succeeded' => null,
+ 'ts_last_attempt' => null,
+ 'ts_last_error' => null,
+ 'last_error_message' => null,
+ 'timeperiod_id' => null,
+ ];
+
+ protected $stateProperties = [
+ 'last_attempt_succeeded',
+ 'last_error_message',
+ 'ts_last_attempt',
+ 'ts_last_error',
+ ];
+
+ protected $settingsTable = 'director_job_setting';
+
+ protected $settingsRemoteId = 'job_id';
+
+ public function getUniqueIdentifier()
+ {
+ return $this->get('job_name');
+ }
+
+ /**
+ * @deprecated please use JobHook::getInstance()
+ * @return JobHook
+ */
+ public function job()
+ {
+ return $this->getInstance();
+ }
+
+ /**
+ * @return JobHook
+ */
+ public function getInstance()
+ {
+ if ($this->job === null) {
+ $class = $this->get('job_class');
+ $this->job = new $class;
+ $this->job->setDb($this->connection);
+ $this->job->setDefinition($this);
+ }
+
+ return $this->job;
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function run()
+ {
+ $job = $this->getInstance();
+ $this->set('ts_last_attempt', date('Y-m-d H:i:s'));
+
+ try {
+ $job->run();
+ $this->set('last_attempt_succeeded', 'y');
+ $success = true;
+ } catch (Exception $e) {
+ Logger::error($e->getMessage());
+ $this->set('ts_last_error', date('Y-m-d H:i:s'));
+ $this->set('last_error_message', $e->getMessage());
+ $this->set('last_attempt_succeeded', 'n');
+ $success = false;
+ }
+
+ if ($this->hasBeenModified()) {
+ $this->store();
+ }
+
+ return $success;
+ }
+
+ /**
+ * @return bool
+ */
+ public function shouldRun()
+ {
+ return (! $this->hasBeenDisabled()) && $this->isPending();
+ }
+
+ /**
+ * @return bool
+ */
+ public function isOverdue()
+ {
+ if (! $this->shouldRun()) {
+ return false;
+ }
+
+ return (
+ strtotime((int) $this->get('ts_last_attempt')) + $this->get('run_interval') * 2
+ ) < time();
+ }
+
+ public function hasBeenDisabled()
+ {
+ return $this->get('disabled') === 'y';
+ }
+
+ /**
+ * @return bool
+ */
+ public function isPending()
+ {
+ if ($this->get('ts_last_attempt') === null) {
+ return $this->isWithinTimeperiod();
+ }
+
+ if (strtotime($this->get('ts_last_attempt')) + $this->get('run_interval') < time()) {
+ return $this->isWithinTimeperiod();
+ }
+
+ return false;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isWithinTimeperiod()
+ {
+ if ($this->hasTimeperiod()) {
+ return $this->timeperiod()->isActive();
+ } else {
+ return true;
+ }
+ }
+
+ public function lastAttemptSucceeded()
+ {
+ return $this->get('last_attempt_succeeded') === 'y';
+ }
+
+ public function lastAttemptFailed()
+ {
+ return $this->get('last_attempt_succeeded') === 'n';
+ }
+
+ public function hasTimeperiod()
+ {
+ return $this->get('timeperiod_id') !== null;
+ }
+
+ /**
+ * @param $timeperiod
+ * @return $this
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function setTimeperiod($timeperiod)
+ {
+ if (is_string($timeperiod)) {
+ $timeperiod = IcingaTimePeriod::load($timeperiod, $this->connection);
+ } elseif (! $timeperiod instanceof IcingaTimePeriod) {
+ throw new InvalidArgumentException('TimePeriod expected');
+ }
+
+ $this->set('timeperiod_id', $timeperiod->get('id'));
+
+ return $this;
+ }
+
+ /**
+ * @return object
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ $plain = (object) $this->getProperties();
+ $plain->originalId = $plain->id;
+ unset($plain->id);
+ unset($plain->timeperiod_id);
+ if ($this->hasTimeperiod()) {
+ $plain->timeperiod = $this->timeperiod()->getObjectName();
+ }
+
+ foreach ($this->stateProperties as $key) {
+ unset($plain->$key);
+ }
+ $plain->settings = $this->getInstance()->exportSettings();
+
+ return $plain;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return DirectorJob
+ * @throws DuplicateKeyException
+ * @throws NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $dummy = new static;
+ $idCol = $dummy->autoincKeyName;
+ $keyCol = $dummy->keyName;
+ $properties = (array) $plain;
+ if (isset($properties['originalId'])) {
+ $id = $properties['originalId'];
+ unset($properties['originalId']);
+ } else {
+ $id = null;
+ }
+ $name = $properties[$keyCol];
+
+ if ($replace && $id && static::existsWithNameAndId($name, $id, $db)) {
+ $object = static::loadWithAutoIncId($id, $db);
+ } elseif ($replace && static::exists($name, $db)) {
+ $object = static::load($name, $db);
+ } elseif (static::exists($name, $db)) {
+ throw new DuplicateKeyException(
+ 'Director Job "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ $settings = (array) $properties['settings'];
+
+ if (array_key_exists('source', $settings) && ! (array_key_exists('source_id', $settings))) {
+ $val = ImportSource::load($settings['source'], $db)->get('id');
+ $settings['source_id'] = $val;
+ unset($settings['source']);
+ }
+
+ if (array_key_exists('rule', $settings) && ! (array_key_exists('rule_id', $settings))) {
+ $val = SyncRule::load($settings['rule'], $db)->get('id');
+ $settings['rule_id'] = $val;
+ unset($settings['rule']);
+ }
+
+ $properties['settings'] = (object) $settings;
+ $object->setProperties($properties);
+ if ($id !== null) {
+ $object->reallySet($idCol, $id);
+ }
+
+ return $object;
+ }
+
+ /**
+ * @param string $name
+ * @param int $id
+ * @param Db $connection
+ * @api internal
+ * @return bool
+ */
+ protected static function existsWithNameAndId($name, $id, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $dummy = new static;
+ $idCol = $dummy->autoincKeyName;
+ $keyCol = $dummy->keyName;
+
+ return (string) $id === (string) $db->fetchOne(
+ $db->select()
+ ->from($dummy->table, $idCol)
+ ->where("$idCol = ?", $id)
+ ->where("$keyCol = ?", $name)
+ );
+ }
+
+ /**
+ * @api internal Exporter only
+ * @return IcingaTimePeriod
+ */
+ public function timeperiod()
+ {
+ try {
+ return IcingaTimePeriod::loadWithAutoIncId($this->get('timeperiod_id'), $this->connection);
+ } catch (NotFoundError $e) {
+ throw new \RuntimeException(sprintf(
+ 'The TimePeriod configured for Job "%s" could not have been found',
+ $this->get('name')
+ ));
+ }
+ }
+}
diff --git a/library/Director/Objects/DynamicApplyMatches.php b/library/Director/Objects/DynamicApplyMatches.php
new file mode 100644
index 0000000..9341d1a
--- /dev/null
+++ b/library/Director/Objects/DynamicApplyMatches.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class DynamicApplyMatches extends ObjectApplyMatches
+{
+ protected static $type = '';
+
+ public static function setType($type)
+ {
+ static::$type = $type;
+ return static::$type;
+ }
+}
diff --git a/library/Director/Objects/Extension/Arguments.php b/library/Director/Objects/Extension/Arguments.php
new file mode 100644
index 0000000..3acbdd3
--- /dev/null
+++ b/library/Director/Objects/Extension/Arguments.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Icinga\Module\Director\Objects\Extension;
+
+use Icinga\Module\Director\Objects\IcingaArguments;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+trait Arguments
+{
+ private $arguments;
+
+ public function arguments()
+ {
+ /** @var IcingaObject $this */
+ if ($this->arguments === null) {
+ if ($this->hasBeenLoadedFromDb()) {
+ $this->arguments = IcingaArguments::loadForStoredObject($this);
+ } else {
+ $this->arguments = new IcingaArguments($this);
+ }
+ }
+
+ return $this->arguments;
+ }
+
+ public function gotArguments()
+ {
+ return null !== $this->arguments;
+ }
+
+ public function unsetArguments()
+ {
+ unset($this->arguments);
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderArguments()
+ {
+ return $this->arguments()->toConfigString();
+ }
+
+ /**
+ * @param $value
+ * @return $this
+ */
+ protected function setArguments($value)
+ {
+ $this->arguments()->setArguments($value);
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getArguments()
+ {
+ return $this->arguments()->toPlainObject();
+ }
+}
diff --git a/library/Director/Objects/Extension/FlappingSupport.php b/library/Director/Objects/Extension/FlappingSupport.php
new file mode 100644
index 0000000..a86f10d
--- /dev/null
+++ b/library/Director/Objects/Extension/FlappingSupport.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\Objects\Extension;
+
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+
+trait FlappingSupport
+{
+ /**
+ * @param $value
+ * @return string
+ * @codingStandardsIgnoreStart
+ */
+ protected function renderFlapping_threshold_high($value)
+ {
+ return $this->renderFlappingThreshold('flapping_threshold_high', $value);
+ }
+
+ /**
+ * @param $value
+ * @return string
+ */
+ protected function renderFlapping_threshold_low($value)
+ {
+ return $this->renderFlappingThreshold('flapping_threshold_low', $value);
+ }
+
+ protected function renderFlappingThreshold($key, $value)
+ {
+ return sprintf(
+ " try { // This setting is only available in Icinga >= 2.8.0\n"
+ . " %s"
+ . " } except { globals.directorWarnOnceForThresholds() }\n",
+ c::renderKeyValue($key, c::renderFloat($value))
+ );
+ }
+
+ protected function renderLegacyEnable_flapping($value)
+ {
+ return c1::renderKeyValue('flap_detection_enabled', c1::renderBoolean($value));
+ }
+
+ protected function renderLegacyFlapping_threshold_high($value)
+ {
+ return c1::renderKeyValue('high_flap_threshold', $value);
+ }
+
+ protected function renderLegacyFlapping_threshold_low($value)
+ {
+ // @codingStandardsIgnoreEnd
+ return c1::renderKeyValue('low_flap_threshold', $value);
+ }
+}
diff --git a/library/Director/Objects/Extension/PriorityColumn.php b/library/Director/Objects/Extension/PriorityColumn.php
new file mode 100644
index 0000000..638bdc6
--- /dev/null
+++ b/library/Director/Objects/Extension/PriorityColumn.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Objects\Extension;
+
+use Zend_Db_Expr as Expr;
+
+trait PriorityColumn
+{
+ public function setNextPriority($prioSetColumn = null, $prioColumn = 'priority')
+ {
+ /** @var \Zend_Db_Adapter_Abstract $db */
+ $db = $this->getDb();
+ $prioValue = '(CASE WHEN MAX(priosub.priority) IS NULL THEN 1'
+ . ' ELSE MAX(priosub.priority) + 1 END)';
+ $query = $db->select()
+ ->from(
+ ['priosub' => $this->getTableName()],
+ "$prioValue"
+ );
+
+ if ($prioSetColumn !== null) {
+ $query->where("priosub.$prioSetColumn = ?", $this->get($prioSetColumn));
+ }
+
+ $this->set($prioColumn, new Expr('(' . $query . ')'));
+
+ return $this;
+ }
+
+ protected function refreshPriortyProperty($prioColumn = 'priority')
+ {
+ /** @var \Zend_Db_Adapter_Abstract $db */
+ $db = $this->getDb();
+ $idCol = $this->getAutoincKeyName();
+ $query = $db->select()
+ ->from($this->getTableName(), $prioColumn)
+ ->where("$idCol = ?", $this->get($idCol));
+ $this->reallySet($prioColumn, $db->fetchOne($query));
+ }
+}
diff --git a/library/Director/Objects/GroupMembershipResolver.php b/library/Director/Objects/GroupMembershipResolver.php
new file mode 100644
index 0000000..f5ef418
--- /dev/null
+++ b/library/Director/Objects/GroupMembershipResolver.php
@@ -0,0 +1,689 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\IcingaObjectFilterHelper;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use InvalidArgumentException;
+use LogicException;
+use Zend_Db_Select as ZfSelect;
+
+/**
+ * Class GroupMembershipResolver
+ *
+ * - Fetches all involved assignments
+ * - Fetch all (or one) object
+ * - Fetch all (or one) group
+ */
+abstract class GroupMembershipResolver
+{
+ /** @var string Object type, 'host', 'service', 'user' or similar */
+ protected $type;
+
+ /** @var array */
+ protected $existingMappings;
+
+ /** @var array */
+ protected $newMappings;
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var IcingaObject[] */
+ protected $objects;
+
+ /** @var IcingaObjectGroup[] */
+ protected $groups = array();
+
+ /** @var array */
+ protected $staticGroups = array();
+
+ /** @var bool */
+ protected $deferred = false;
+
+ /** @var bool */
+ protected $checked = false;
+
+ /** @var bool */
+ protected $useTransactions = false;
+
+ protected $groupMap;
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function refreshAllMappings()
+ {
+ return $this->clearGroups()->clearObjects()->refreshDb(true);
+ }
+
+ public function checkDb()
+ {
+ if ($this->checked) {
+ return $this;
+ }
+
+ if ($this->isDeferred()) {
+ // ensure we are not working with cached data
+ IcingaTemplateRepository::clear();
+ }
+
+ Benchmark::measure('Rechecking all objects');
+ $this->recheckAllObjects($this->getAppliedGroups());
+ if (empty($this->objects) && empty($this->groups)) {
+ Benchmark::measure('Nothing to check, got no qualified object');
+ return $this;
+ }
+
+ Benchmark::measure('Recheck done, loading existing mappings');
+ $this->fetchStoredMappings();
+ Benchmark::measure('Got stored group mappings');
+
+ $this->checked = true;
+ return $this;
+ }
+
+ /**
+ * @param bool $force
+ * @return $this
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function refreshDb($force = false)
+ {
+ if ($force || ! $this->isDeferred()) {
+ $this->checkDb();
+
+ if (empty($this->objects) && empty($this->groups)) {
+ Benchmark::measure('Nothing to check, got no qualified object');
+
+ return $this;
+ }
+
+ Benchmark::measure('Ready, going to store new mappings');
+ $this->storeNewMappings();
+ $this->removeOutdatedMappings();
+ Benchmark::measure('Updated group mappings in db');
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param bool $defer
+ * @return $this
+ */
+ public function defer($defer = true)
+ {
+ $this->deferred = $defer;
+ return $this;
+ }
+
+ /**
+ * @param $use
+ * @return $this
+ */
+ public function setUseTransactions($use)
+ {
+ $this->useTransactions = $use;
+ return $this;
+ }
+
+ public function getType()
+ {
+ if ($this->type === null) {
+ throw new LogicException(sprintf(
+ '"type" is required when extending %s, got none in %s',
+ __CLASS__,
+ get_class($this)
+ ));
+ }
+
+ return $this->type;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isDeferred()
+ {
+ return $this->deferred;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return $this
+ */
+ public function addObject(IcingaObject $object)
+ {
+ // Hint: cannot use hasBeenLoadedFromDB, as it is false in onStore()
+ // for new objects
+ if (null === ($id = $object->get('id'))) {
+ return $this;
+ }
+ // Disabling for now, how should this work?
+ // $this->assertBeenLoadedFromDb($object);
+ if ($this->objects === null) {
+ $this->objects = [];
+ }
+
+ if ($object->isTemplate()) {
+ $this->includeChildObjects($object);
+ } else {
+ $this->objects[$id] = $object;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaObject[] $objects
+ * @return $this
+ */
+ public function addObjects(array $objects)
+ {
+ foreach ($objects as $object) {
+ $this->addObject($object);
+ }
+
+ return $this;
+ }
+
+ protected function includeChildObjects(IcingaObject $object)
+ {
+ $query = $this->db->select()
+ ->from(['o' => $object->getTableName()])
+ ->where('o.object_type = ?', 'object');
+
+ IcingaObjectFilterHelper::filterByTemplate(
+ $query,
+ $object,
+ 'o',
+ Db\IcingaObjectFilterHelper::INHERIT_DIRECT_OR_INDIRECT
+ );
+
+ foreach ($object::loadAll($this->connection, $query) as $child) {
+ $this->objects[$child->getProperty('id')] = $child;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return $this
+ */
+ public function setObject(IcingaObject $object)
+ {
+ $this->clearObjects();
+ return $this->addObject($object);
+ }
+
+ /**
+ * @param IcingaObject[] $objects
+ * @return $this
+ */
+ public function setObjects(array $objects)
+ {
+ $this->clearObjects();
+ return $this->addObjects($objects);
+ }
+
+ /**
+ * @return $this
+ */
+ public function clearObjects()
+ {
+ $this->objects = array();
+ return $this;
+ }
+
+ /**
+ * @param IcingaObjectGroup $group
+ * @return $this
+ */
+ public function addGroup(IcingaObjectGroup $group)
+ {
+ $this->assertBeenLoadedFromDb($group);
+ $this->groups[$group->get('id')] = $group;
+
+ $this->checked = false;
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaObjectGroup[] $groups
+ * @return $this
+ */
+ public function addGroups(array $groups)
+ {
+ foreach ($groups as $group) {
+ $this->addGroup($group);
+ }
+
+ $this->checked = false;
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaObjectGroup $group
+ * @return $this
+ */
+ public function setGroup(IcingaObjectGroup $group)
+ {
+ $this->clearGroups();
+ return $this->addGroup($group);
+ }
+
+ /**
+ * @param array $groups
+ * @return $this
+ */
+ public function setGroups(array $groups)
+ {
+ $this->clearGroups();
+ return $this->addGroups($groups);
+ }
+
+ /**
+ * @return $this
+ */
+ public function clearGroups()
+ {
+ $this->objects = array();
+ $this->checked = false;
+ return $this;
+ }
+
+ public function getNewMappings()
+ {
+ if ($this->newMappings !== null && $this->existingMappings !== null) {
+ return $this->getDifference($this->newMappings, $this->existingMappings);
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function storeNewMappings()
+ {
+ $diff = $this->getNewMappings();
+ $count = count($diff);
+ if ($count === 0) {
+ return;
+ }
+
+ $db = $this->db;
+ $this->beginTransaction();
+ foreach ($diff as $row) {
+ $db->insert(
+ $this->getResolvedTableName(),
+ $row
+ );
+ }
+
+ $this->commit();
+ Benchmark::measure(
+ sprintf(
+ 'Stored %d new resolved group memberships',
+ $count
+ )
+ );
+ }
+
+ protected function getGroupId($name)
+ {
+ $type = $this->type;
+ if ($this->groupMap === null) {
+ $this->groupMap = $this->db->fetchPairs(
+ $this->db->select()->from("icinga_${type}group", ['object_name', 'id'])
+ );
+ }
+
+ if (array_key_exists($name, $this->groupMap)) {
+ return $this->groupMap[$name];
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'Unable to lookup the group name for "%s"',
+ $name
+ ));
+ }
+ }
+
+ public function getOutdatedMappings()
+ {
+ if ($this->newMappings !== null && $this->existingMappings !== null) {
+ return $this->getDifference($this->existingMappings, $this->newMappings);
+ } else {
+ return [];
+ }
+ }
+
+ protected function removeOutdatedMappings()
+ {
+ $diff = $this->getOutdatedMappings();
+ $count = count($diff);
+ if ($count === 0) {
+ return;
+ }
+
+ $type = $this->getType();
+ $db = $this->db;
+ $this->beginTransaction();
+ foreach ($diff as $row) {
+ $db->delete(
+ $this->getResolvedTableName(),
+ sprintf(
+ "(${type}group_id = %d AND ${type}_id = %d)",
+ $row["${type}group_id"],
+ $row["${type}_id"]
+ )
+ );
+ }
+
+ $this->commit();
+ Benchmark::measure(
+ sprintf(
+ 'Removed %d outdated group memberships',
+ $count
+ )
+ );
+ }
+
+ protected function getDifference(&$left, &$right)
+ {
+ $diff = array();
+
+ $type = $this->getType();
+ foreach ($left as $groupId => $objectIds) {
+ if (array_key_exists($groupId, $right)) {
+ foreach ($objectIds as $objectId) {
+ if (! array_key_exists($objectId, $right[$groupId])) {
+ $diff[] = array(
+ "${type}group_id" => $groupId,
+ "${type}_id" => $objectId,
+ );
+ }
+ }
+ } else {
+ foreach ($objectIds as $objectId) {
+ $diff[] = array(
+ "${type}group_id" => $groupId,
+ "${type}_id" => $objectId,
+ );
+ }
+ }
+ }
+
+ return $diff;
+ }
+
+ /**
+ * This fetches already resolved memberships
+ */
+ protected function fetchStoredMappings()
+ {
+ $mappings = array();
+
+ $type = $this->getType();
+ $query = $this->db->select()->from(
+ array('hgh' => $this->getResolvedTableName()),
+ array(
+ 'group_id' => "${type}group_id",
+ 'object_id' => "${type}_id",
+ )
+ );
+
+ $this->addMembershipWhere($query, "${type}_id", $this->objects);
+ $this->addMembershipWhere($query, "${type}group_id", $this->groups);
+ if (! empty($this->groups)) {
+ // load staticGroups (we touched here) additionally, so we can compare changes
+ $this->addMembershipWhere($query, "${type}group_id", $this->staticGroups);
+ }
+
+ foreach ($this->db->fetchAll($query) as $row) {
+ $groupId = $row->group_id;
+ $objectId = $row->object_id;
+ if (! array_key_exists($groupId, $mappings)) {
+ $mappings[$groupId] = array();
+ }
+
+ $mappings[$groupId][$objectId] = $objectId;
+ }
+
+ $this->existingMappings = $mappings;
+ }
+
+ /**
+ * @param ZfSelect $query
+ * @param string $column
+ * @param IcingaObject[]|int[] $objects
+ * @return ZfSelect
+ */
+ protected function addMembershipWhere(ZfSelect $query, $column, &$objects)
+ {
+ if (empty($objects)) {
+ return $query;
+ }
+
+ $ids = array();
+ foreach ($objects as $k => $object) {
+ if (is_int($object)) {
+ $ids[] = $k;
+ } elseif (is_string($object)) {
+ $ids[] = (int) $object;
+ } else {
+ $ids[] = (int) $object->get('id');
+ }
+ }
+
+ if (count($ids) === 1) {
+ $query->orWhere($column . ' = ?', $ids[0]);
+ } else {
+ $query->orWhere($column . ' IN (?)', $ids);
+ }
+
+ return $query;
+ }
+
+ protected function recheckAllObjects($groups)
+ {
+ $mappings = [];
+ $staticGroups = [];
+
+ if ($this->objects === null) {
+ $objects = $this->fetchAllObjects();
+ } else {
+ $objects = & $this->objects;
+ }
+
+ $times = array();
+
+ foreach ($objects as $object) {
+ if ($object->shouldBeRemoved()) {
+ continue;
+ }
+ if ($object->isTemplate()) {
+ continue;
+ }
+
+ $mt = microtime(true);
+ $id = $object->get('id');
+
+ DynamicApplyMatches::setType($this->type);
+ $resolver = DynamicApplyMatches::prepare($object);
+ foreach ($groups as $groupId => $filter) {
+ if ($resolver->matchesFilter($filter)) {
+ if (! array_key_exists($groupId, $mappings)) {
+ $mappings[$groupId] = [];
+ }
+ $mappings[$groupId][$id] = $id;
+ }
+ }
+
+ // can only be run reliably when updating for all groups
+ $groupNames = $object->get('groups');
+ if (empty($groupNames)) {
+ $groupNames = $object->listInheritedGroupNames();
+ }
+ foreach ($groupNames as $name) {
+ $groupId = $this->getGroupId($name);
+ if (! array_key_exists($groupId, $mappings)) {
+ $mappings[$groupId] = [];
+ }
+
+ $mappings[$groupId][$id] = $id;
+ $staticGroups[$groupId] = $groupId;
+ }
+
+ $times[] = (microtime(true) - $mt) * 1000;
+ }
+
+ $count = count($times);
+ $min = $max = $avg = 0;
+ if ($count > 0) {
+ $min = min($times);
+ $max = max($times);
+ $avg = array_sum($times) / $count;
+ }
+
+ Benchmark::measure(sprintf(
+ '%sgroup apply recalculated: objects=%d groups=%d min=%d max=%d avg=%d (in ms)',
+ $this->type,
+ $count,
+ count($groups),
+ $min,
+ $max,
+ $avg
+ ));
+
+ Benchmark::measure('Done with single assignments');
+
+ $this->newMappings = $mappings;
+ $this->staticGroups = $staticGroups;
+ }
+
+ protected function getAppliedGroups()
+ {
+ if (empty($this->groups)) {
+ return $this->fetchAppliedGroups();
+ } else {
+ return $this->buildAppliedGroups();
+ }
+ }
+
+ protected function buildAppliedGroups()
+ {
+ $list = array();
+ foreach ($this->groups as $id => $group) {
+ $list[$id] = $group->get('assign_filter');
+ }
+
+ return $this->parseFilters($list);
+ }
+
+ protected function fetchAppliedGroups()
+ {
+ $type = $this->getType();
+ $query = $this->db->select()->from(
+ array('hg' => "icinga_${type}group"),
+ array(
+ 'id',
+ 'assign_filter',
+ )
+ )->where("assign_filter IS NOT NULL AND assign_filter != ''");
+
+ return $this->parseFilters($this->db->fetchPairs($query));
+ }
+
+ /**
+ * Parsing a list of query strings to Filter
+ *
+ * @param string[] $list List of query strings
+ *
+ * @return Filter[]
+ */
+ protected function parseFilters($list)
+ {
+ return array_map(function ($s) {
+ return Filter::fromQueryString($s);
+ }, $list);
+ }
+
+ protected function getTableName()
+ {
+ $type = $this->getType();
+ return "icinga_${type}group_${type}";
+ }
+
+ protected function getResolvedTableName()
+ {
+ return $this->getTableName() . '_resolved';
+ }
+
+ /**
+ * @return $this
+ */
+ protected function beginTransaction()
+ {
+ if ($this->useTransactions) {
+ $this->db->beginTransaction();
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function commit()
+ {
+ if ($this->useTransactions) {
+ $this->db->commit();
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return IcingaObject[]
+ */
+ protected function getObjects()
+ {
+ if ($this->objects === null) {
+ $this->objects = $this->fetchAllObjects();
+ }
+
+ return $this->objects;
+ }
+
+ protected function fetchAllObjects()
+ {
+ return IcingaObject::loadAllByType($this->getType(), $this->connection);
+ }
+
+ protected function assertBeenLoadedFromDb(IcingaObject $object)
+ {
+ if (! is_int($object->get('id')) && ! ctype_digit($object->get('id'))) {
+ throw new LogicException(
+ 'Group resolver does not support unstored objects'
+ );
+ }
+ }
+}
diff --git a/library/Director/Objects/HostApplyMatches.php b/library/Director/Objects/HostApplyMatches.php
new file mode 100644
index 0000000..5feaee7
--- /dev/null
+++ b/library/Director/Objects/HostApplyMatches.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class HostApplyMatches extends ObjectApplyMatches
+{
+ protected static $type = 'host';
+}
diff --git a/library/Director/Objects/HostGroupMembershipResolver.php b/library/Director/Objects/HostGroupMembershipResolver.php
new file mode 100644
index 0000000..b597017
--- /dev/null
+++ b/library/Director/Objects/HostGroupMembershipResolver.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class HostGroupMembershipResolver extends GroupMembershipResolver
+{
+ protected $type = 'host';
+}
diff --git a/library/Director/Objects/IcingaApiUser.php b/library/Director/Objects/IcingaApiUser.php
new file mode 100644
index 0000000..bb4f9f8
--- /dev/null
+++ b/library/Director/Objects/IcingaApiUser.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+
+class IcingaApiUser extends IcingaObject
+{
+ protected $table = 'icinga_apiuser';
+
+ protected $uuidColumn = 'uuid';
+
+ // TODO: Enable (and add table) if required
+ protected $supportsImports = false;
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'password' => null,
+ 'client_dn' => null,
+ 'permissions' => null,
+ ];
+
+ protected function renderPassword()
+ {
+ return c::renderKeyValue('password', c::renderString('***'));
+ }
+}
diff --git a/library/Director/Objects/IcingaArguments.php b/library/Director/Objects/IcingaArguments.php
new file mode 100644
index 0000000..e788da8
--- /dev/null
+++ b/library/Director/Objects/IcingaArguments.php
@@ -0,0 +1,442 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Countable;
+use Exception;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use InvalidArgumentException;
+use Iterator;
+
+class IcingaArguments implements Iterator, Countable, IcingaConfigRenderer
+{
+ const COMMENT_DSL_UNSUPPORTED = '/* Icinga 2 does not export DSL function bodies via API */';
+
+ /** @var IcingaCommandArgument[] */
+ protected $storedArguments = [];
+
+ /** @var IcingaCommandArgument[] */
+ protected $arguments = [];
+
+ protected $modified = false;
+
+ protected $object;
+
+ private $position = 0;
+
+ protected $idx = [];
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->object = $object;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return count($this->arguments);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->position = 0;
+ }
+
+ public function hasBeenModified()
+ {
+ return $this->modified;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ if (! $this->valid()) {
+ return null;
+ }
+
+ return $this->arguments[$this->idx[$this->position]];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->idx[$this->position];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ ++$this->position;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ return array_key_exists($this->position, $this->idx);
+ }
+
+ public function get($key)
+ {
+ if (array_key_exists($key, $this->arguments)) {
+ if ($this->arguments[$key]->shouldBeRemoved()) {
+ return null;
+ }
+
+ return $this->arguments[$key];
+ }
+
+ return null;
+ }
+
+ public function set($key, $value)
+ {
+ if ($value === null) {
+ return $this->remove($key);
+ }
+
+ if ($value instanceof IcingaCommandArgument) {
+ $argument = $value;
+ } else {
+ $argument = IcingaCommandArgument::create(
+ $this->mungeCommandArgument($key, $value)
+ );
+ }
+
+ $argument->set('command_id', $this->object->get('id'));
+
+ $key = $argument->argument_name;
+ if (array_key_exists($key, $this->arguments)) {
+ $this->arguments[$key]->replaceWith($argument);
+ if ($this->arguments[$key]->hasBeenModified()) {
+ $this->modified = true;
+ }
+ } elseif (array_key_exists($key, $this->storedArguments)) {
+ $this->arguments[$key] = clone($this->storedArguments[$key]);
+ $this->arguments[$key]->replaceWith($argument);
+ if ($this->arguments[$key]->hasBeenModified()) {
+ $this->modified = true;
+ }
+ } else {
+ $this->add($argument);
+ $this->modified = true;
+ }
+
+ return $this;
+ }
+
+ protected function mungeCommandArgument($key, $value)
+ {
+ $attrs = [
+ 'argument_name' => (string) $key,
+ ];
+
+ $map = [
+ 'skip_key' => 'skip_key',
+ 'repeat_key' => 'repeat_key',
+ 'required' => 'required',
+ // 'order' => 'sort_order',
+ 'description' => 'description',
+ 'set_if' => 'set_if',
+ ];
+
+ $argValue = null;
+ if (is_object($value)) {
+ if (property_exists($value, 'order')) {
+ $attrs['sort_order'] = (string) $value->order;
+ }
+
+ foreach ($map as $apiKey => $dbKey) {
+ if (property_exists($value, $apiKey)) {
+ $attrs[$dbKey] = $value->$apiKey;
+ }
+ }
+ if (property_exists($value, 'type')) {
+ // argument is directly set as function, no further properties
+ if ($value->type === 'Function') {
+ $attrs['argument_value'] = self::COMMENT_DSL_UNSUPPORTED;
+ $attrs['argument_format'] = 'expression';
+ }
+ } elseif (property_exists($value, 'value')) {
+ // argument is a dictionary with further settings
+ if (is_object($value->value)) {
+ if ($value->value->type === 'Function' && property_exists($value->value, 'body')) {
+ // likely an export from Baskets that contains the actual function body
+ $attrs['argument_value'] = $value->value->body;
+ $attrs['argument_format'] = 'expression';
+ } elseif ($value->value->type === 'Function') {
+ $attrs['argument_value'] = self::COMMENT_DSL_UNSUPPORTED;
+ $attrs['argument_format'] = 'expression';
+ } else {
+ die('Unable to resolve command argument');
+ }
+ } else {
+ $argValue = $value->value;
+ if (is_string($argValue)) {
+ $attrs['argument_value'] = $argValue;
+ $attrs['argument_format'] = 'string';
+ } else {
+ $attrs['argument_value'] = $argValue;
+ $attrs['argument_format'] = 'json';
+ }
+ }
+ }
+ } else {
+ if (is_string($value)) {
+ $attrs['argument_value'] = $value;
+ $attrs['argument_format'] = 'string';
+ } else {
+ $attrs['argument_value'] = $value;
+ $attrs['argument_format'] = 'json';
+ }
+ }
+
+ if (array_key_exists('set_if', $attrs)) {
+ if (is_object($attrs['set_if']) && $attrs['set_if']->type === 'Function') {
+ $attrs['set_if'] = self::COMMENT_DSL_UNSUPPORTED;
+ $attrs['set_if_format'] = 'expression';
+ } elseif (property_exists($value, 'set_if_format')) {
+ if (in_array($value->set_if_format, ['string', 'expression', 'json'])) {
+ $attrs['set_if_format'] = $value->set_if_format;
+ }
+ }
+ }
+
+ return $attrs;
+ }
+
+ public function setArguments($arguments)
+ {
+ $arguments = (array) $arguments;
+
+ foreach ($arguments as $arg => $val) {
+ $this->set($arg, $val);
+ }
+
+ foreach (array_diff(
+ array_keys($this->arguments),
+ array_keys($arguments)
+ ) as $arg) {
+ if ($this->arguments[$arg]->hasBeenLoadedFromDb()) {
+ $this->arguments[$arg]->markForRemoval();
+ $this->modified = true;
+ } else {
+ unset($this->arguments[$arg]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Magic isset check
+ *
+ * @param string $argument
+ * @return boolean
+ */
+ public function __isset($argument)
+ {
+ return array_key_exists($argument, $this->arguments);
+ }
+
+ public function remove($argument)
+ {
+ if (array_key_exists($argument, $this->arguments)) {
+ $this->arguments[$argument]->markForRemoval();
+ $this->modified = true;
+ $this->refreshIndex();
+ }
+
+ return $this;
+ }
+
+ protected function refreshIndex()
+ {
+ ksort($this->arguments);
+ $this->idx = array_keys($this->arguments);
+ }
+
+ public function add(IcingaCommandArgument $argument)
+ {
+ $name = $argument->get('argument_name');
+ if (array_key_exists($name, $this->arguments)) {
+ // TODO: Fail unless $argument equals existing one
+ return $this;
+ }
+
+ $this->arguments[$name] = $argument;
+ $this->modified = true;
+ $this->refreshIndex();
+
+ return $this;
+ }
+
+ protected function getGroupTableName()
+ {
+ return $this->object->getTableName() . 'group';
+ }
+
+ protected function loadFromDb()
+ {
+ $db = $this->object->getDb();
+ $connection = $this->object->getConnection();
+
+ $table = $this->object->getTableName();
+ $query = $db->select()->from(
+ ['o' => $table],
+ []
+ )->join(
+ ['a' => 'icinga_command_argument'],
+ 'o.id = a.command_id',
+ '*'
+ )->where('o.object_name = ?', $this->object->getObjectName())
+ ->order('a.sort_order')->order('a.argument_name');
+
+ $this->arguments = IcingaCommandArgument::loadAll($connection, $query, 'argument_name');
+ $this->cloneStored();
+ $this->refreshIndex();
+
+ return $this;
+ }
+
+ public function toPlainObject(
+ $resolved = false,
+ $skipDefaults = false,
+ array $chosenProperties = null,
+ $resolveIds = true
+ ) {
+ if ($chosenProperties !== null) {
+ throw new InvalidArgumentException(
+ 'IcingaArguments does not support chosenProperties[]'
+ );
+ }
+
+ $args = [];
+ foreach ($this->arguments as $arg) {
+ if ($arg->shouldBeRemoved()) {
+ continue;
+ }
+
+ $args[$arg->get('argument_name')] = $arg->toPlainObject(
+ $resolved,
+ $skipDefaults,
+ null,
+ $resolveIds
+ );
+ }
+
+ return $args;
+ }
+
+ public function toUnmodifiedPlainObject()
+ {
+ $args = [];
+ foreach ($this->storedArguments as $key => $arg) {
+ $args[$arg->argument_name] = $arg->toPlainObject();
+ }
+
+ return $args;
+ }
+
+ protected function cloneStored()
+ {
+ $this->storedArguments = [];
+ foreach ($this->arguments as $k => $v) {
+ $this->storedArguments[$k] = clone($v);
+ }
+ }
+
+ public static function loadForStoredObject(IcingaObject $object)
+ {
+ $arguments = new static($object);
+ return $arguments->loadFromDb();
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ foreach ($this->arguments as $argument) {
+ $argument->setBeingLoadedFromDb();
+ }
+ $this->refreshIndex();
+ $this->cloneStored();
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function store()
+ {
+ $db = $this->object->getConnection();
+ $deleted = [];
+ foreach ($this->arguments as $key => $argument) {
+ if ($argument->shouldBeRemoved()) {
+ $deleted[] = $key;
+ } else {
+ if ($argument->hasBeenModified()) {
+ if ($argument->hasBeenLoadedFromDb()) {
+ $argument->setLoadedProperty('command_id', $this->object->get('id'));
+ } else {
+ $argument->set('command_id', $this->object->get('id'));
+ }
+ $argument->store($db);
+ }
+ }
+ }
+
+ foreach ($deleted as $key) {
+ $argument = $this->arguments[$key];
+ $argument->setLoadedProperty('command_id', $this->object->get('id'));
+ $argument->setConnection($this->object->getConnection());
+ $argument->delete();
+ unset($this->arguments[$key]);
+ }
+
+ $this->cloneStored();
+
+ return $this;
+ }
+
+ public function toConfigString()
+ {
+ if (empty($this->arguments)) {
+ return '';
+ }
+
+ $args = [];
+ foreach ($this->arguments as $arg) {
+ if ($arg->shouldBeRemoved()) {
+ continue;
+ }
+
+ $args[$arg->get('argument_name')] = $arg->toConfigString();
+ }
+ return c::renderKeyOperatorValue('arguments', '+=', c::renderDictionary($args));
+ }
+
+ public function __toString()
+ {
+ try {
+ return $this->toConfigString();
+ } catch (Exception $e) {
+ trigger_error($e);
+ $previousHandler = set_exception_handler(
+ function () {
+ }
+ );
+ restore_error_handler();
+ if ($previousHandler !== null) {
+ call_user_func($previousHandler, $e);
+ die();
+ } else {
+ die($e->getMessage());
+ }
+ }
+ }
+
+ public function toLegacyConfigString()
+ {
+ return 'UNSUPPORTED';
+ }
+}
diff --git a/library/Director/Objects/IcingaCommand.php b/library/Director/Objects/IcingaCommand.php
new file mode 100644
index 0000000..35f38a4
--- /dev/null
+++ b/library/Director/Objects/IcingaCommand.php
@@ -0,0 +1,365 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+use Icinga\Module\Director\Objects\Extension\Arguments;
+use Zend_Db_Select as DbSelect;
+
+class IcingaCommand extends IcingaObject implements ObjectWithArguments, ExportInterface
+{
+ use Arguments;
+
+ protected $table = 'icinga_command';
+
+ protected $type = 'CheckCommand';
+
+ protected $uuidColumn = 'uuid';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'methods_execute' => null,
+ 'command' => null,
+ 'timeout' => null,
+ 'zone_id' => null,
+ 'is_string' => null,
+ ];
+
+ protected $booleans = [
+ 'is_string' => 'is_string',
+ ];
+
+ protected $supportsCustomVars = true;
+
+ protected $supportsFields = true;
+
+ protected $supportsImports = true;
+
+ protected $supportedInLegacy = true;
+
+ protected $intervalProperties = [
+ 'timeout' => 'timeout',
+ ];
+
+ protected $relations = [
+ 'zone' => 'IcingaZone',
+ ];
+
+ protected static $pluginDir;
+
+ protected $hiddenExecuteTemplates = [
+ 'PluginCheck' => 'plugin-check-command',
+ 'PluginNotification' => 'plugin-notification-command',
+ 'PluginEvent' => 'plugin-event-command',
+
+ // Special, internal:
+ 'IcingaCheck' => 'icinga-check-command',
+ 'ClusterCheck' => 'cluster-check-command',
+ 'ClusterZoneCheck' => 'plugin-check-command',
+ 'IdoCheck' => 'ido-check-command',
+ 'RandomCheck' => 'random-check-command',
+ ];
+
+ /**
+ * Render the 'medhods_execute' property as 'execute'
+ *
+ * Execute is a reserved word in SQL, column name was prefixed
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ protected function renderMethods_execute()
+ {
+ // @codingStandardsIgnoreEnd
+ return '';
+ }
+
+ protected function renderObjectHeader()
+ {
+ if ($execute = $this->get('methods_execute')) {
+ $itlImport = sprintf(
+ ' import "%s"' . "\n",
+ $this->hiddenExecuteTemplates[$execute]
+ );
+ } else {
+ $itlImport = '';
+ }
+
+ $execute = $this->getSingleResolvedProperty('methods_execute');
+ if ($execute === 'PluginNotification') {
+ return $this->renderObjectHeaderWithType('NotificationCommand') . $itlImport;
+ } elseif ($execute === 'PluginEvent') {
+ return $this->renderObjectHeaderWithType('EventCommand') . $itlImport;
+ } else {
+ return parent::renderObjectHeader() . $itlImport;
+ }
+ }
+
+ /**
+ * @param $type
+ * @return string
+ */
+ protected function renderObjectHeaderWithType($type)
+ {
+ return sprintf(
+ "%s %s %s {\n",
+ $this->getObjectTypeName(),
+ $type,
+ c::renderString($this->getObjectName())
+ );
+ }
+
+ public function mungeCommand($value)
+ {
+ if (is_array($value)) {
+ $value = implode(' ', $value);
+ } elseif (is_object($value)) {
+ // { type => Function } -> really??
+ return null;
+ // return $value;
+ }
+
+ if (self::$pluginDir !== null) {
+ if (($pos = strpos($value, self::$pluginDir)) === 0) {
+ $value = substr($value, strlen(self::$pluginDir) + 1);
+ }
+ }
+
+ return $value;
+ }
+
+ public function getNextSkippableKeyName()
+ {
+ $key = $this->makeSkipKey();
+ $cnt = 1;
+ while (isset($this->arguments()->$key)) {
+ $cnt++;
+ $key = $this->makeSkipKey($cnt);
+ }
+
+ return $key;
+ }
+
+ protected function makeSkipKey($num = null)
+ {
+ if ($num === null) {
+ return '(no key)';
+ }
+
+ return sprintf('(no key.%d)', $num);
+ }
+
+ protected function prefersGlobalZone()
+ {
+ return true;
+ }
+
+ /**
+ * @return string
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function countDirectUses()
+ {
+ $db = $this->getDb();
+ $id = (int) $this->get('id');
+
+ $qh = $db->select()->from(
+ array('h' => 'icinga_host'),
+ array('cnt' => 'COUNT(*)')
+ )->where('h.check_command_id = ?', $id)
+ ->orWhere('h.event_command_id = ?', $id);
+ $qs = $db->select()->from(
+ array('s' => 'icinga_service'),
+ array('cnt' => 'COUNT(*)')
+ )->where('s.check_command_id = ?', $id)
+ ->orWhere('s.event_command_id = ?', $id);
+ $qn = $db->select()->from(
+ array('n' => 'icinga_notification'),
+ array('cnt' => 'COUNT(*)')
+ )->where('n.command_id = ?', $id);
+ $query = $db->select()->union(
+ [$qh, $qs, $qn],
+ DbSelect::SQL_UNION_ALL
+ );
+
+ return $db->fetchOne($db->select()->from(
+ ['all_cnts' => $query],
+ ['cnt' => 'SUM(cnt)']
+ ));
+ }
+
+ /**
+ * @return bool
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function isInUse()
+ {
+ return $this->countDirectUses() > 0;
+ }
+
+ public function getUniqueIdentifier()
+ {
+ return $this->getObjectName();
+ }
+
+ /**
+ * @return object
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ $props = (array) $this->toPlainObject();
+ if (isset($props['arguments'])) {
+ foreach ($props['arguments'] as $key => $argument) {
+ if (property_exists($argument, 'command_id')) {
+ unset($props['arguments'][$key]->command_id);
+ }
+ }
+ }
+ $props['fields'] = $this->loadFieldReferences();
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return IcingaCommand
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ $key = $name;
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Command "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ unset($properties['fields']);
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader
+ * @return array
+ */
+ protected function loadFieldReferences()
+ {
+ $db = $this->getDb();
+
+ $res = $db->fetchAll(
+ $db->select()->from([
+ 'cf' => 'icinga_command_field'
+ ], [
+ 'cf.datafield_id',
+ 'cf.is_required',
+ 'cf.var_filter',
+ ])->join(['df' => 'director_datafield'], 'df.id = cf.datafield_id', [])
+ ->where('command_id = ?', $this->get('id'))
+ ->order('varname ASC')
+ );
+
+ if (empty($res)) {
+ return [];
+ } else {
+ foreach ($res as $field) {
+ $field->datafield_id = (int) $field->datafield_id;
+ }
+
+ return $res;
+ }
+ }
+
+ protected function renderCommand()
+ {
+ $command = $this->get('command');
+ $prefix = '';
+ if (preg_match('~^([A-Z][A-Za-z0-9_]+\s\+\s)(.+?)$~', $command, $m)) {
+ $prefix = $m[1];
+ $command = $m[2];
+ } elseif (! $this->isAbsolutePath($command)) {
+ $prefix = 'PluginDir + ';
+ $command = '/' . $command;
+ }
+
+ $inherited = $this->getInheritedProperties();
+
+ if ($this->get('is_string') === 'y' || ($this->get('is_string') === null
+ && property_exists($inherited, 'is_string') && $inherited->is_string === 'y')) {
+ return c::renderKeyValue('command', $prefix . c::renderString($command));
+ } else {
+ $parts = preg_split('/\s+/', $command, -1, PREG_SPLIT_NO_EMPTY);
+ array_unshift($parts, c::alreadyRendered($prefix . c::renderString(array_shift($parts))));
+
+ return c::renderKeyValue('command', c::renderArray($parts));
+ }
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ protected function renderIs_string()
+ {
+ // @codingStandardsIgnoreEnd
+ return '';
+ }
+
+ protected function isAbsolutePath($path)
+ {
+ return $path[0] === '/'
+ || $path[0] === '\\'
+ || preg_match('/^[A-Za-z]:\\\/', substr($path, 0, 3))
+ || preg_match('/^%[A-Z][A-Za-z0-9\(\)-]*%/', $path);
+ }
+
+ public static function setPluginDir($pluginDir)
+ {
+ self::$pluginDir = $pluginDir;
+ }
+
+ public function getLegacyObjectType()
+ {
+ // there is only one type of command in Icinga 1.x
+ return 'command';
+ }
+
+ protected function renderLegacyCommand()
+ {
+ $command = $this->get('command');
+ if (preg_match('~^(\$USER\d+\$/?)(.+)$~', $command)) {
+ // should be fine, since the user decided to use a macro
+ } elseif (! $this->isAbsolutePath($command)) {
+ $command = '$USER1$/'.$command;
+ }
+
+ return c1::renderKeyValue(
+ $this->getLegacyObjectType().'_line',
+ c1::renderString($command)
+ );
+ }
+}
diff --git a/library/Director/Objects/IcingaCommandArgument.php b/library/Director/Objects/IcingaCommandArgument.php
new file mode 100644
index 0000000..96101ce
--- /dev/null
+++ b/library/Director/Objects/IcingaCommandArgument.php
@@ -0,0 +1,263 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use RuntimeException;
+
+class IcingaCommandArgument extends IcingaObject
+{
+ protected $keyName = ['command_id', 'argument_name'];
+
+ protected $autoincKeyName = 'id';
+
+ protected $table = 'icinga_command_argument';
+
+ protected $supportsImports = false;
+
+ protected $booleans = array(
+ 'skip_key' => 'skip_key',
+ 'repeat_key' => 'repeat_key',
+ 'required' => 'required'
+ );
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'command_id' => null,
+ 'argument_name' => null,
+ 'argument_value' => null,
+ 'argument_format' => null,
+ 'key_string' => null,
+ 'description' => null,
+ 'skip_key' => null,
+ 'set_if' => null,
+ 'sort_order' => null,
+ 'repeat_key' => null,
+ 'set_if_format' => null,
+ 'required' => null,
+ );
+
+ public function onInsert()
+ {
+ // No log right now, we have to handle "sub-objects"
+ }
+
+ public function onUpdate()
+ {
+ // No log right now, we have to handle "sub-objects"
+ }
+
+ public function onDelete()
+ {
+ // No log right now, we have to handle "sub-objects"
+ }
+
+ public function isSkippingKey()
+ {
+ return $this->get('skip_key') === 'y' || $this->get('argument_name') === null;
+ }
+
+ // Preserve is not supported
+ public function replaceWith(IcingaObject $object, $preserve = null)
+ {
+ $this->setProperties((array) $object->toPlainObject(
+ false,
+ false,
+ null,
+ false
+ ));
+ return $this;
+ }
+
+ protected function makePlainArgumentValue($value, $format)
+ {
+ if ($format === 'expression') {
+ return (object) [
+ 'type' => 'Function',
+ // TODO: Not for dummy comment
+ 'body' => $value
+ ];
+ } else {
+ // json or string
+ return $value;
+ }
+ }
+
+ protected function extractValueFromPlain($plain)
+ {
+ if ($plain->argument_value) {
+ return $this->makePlainArgumentValue(
+ $plain->argument_value,
+ $plain->argument_format
+ );
+ } else {
+ return null;
+ }
+ }
+
+ protected function transformPlainArgumentValue($plain)
+ {
+ if (property_exists($plain, 'argument_value')) {
+ if (property_exists($plain, 'argument_format')) {
+ $format = $plain->argument_format;
+ } else {
+ $format = 'string';
+ }
+ $plain->value = $this->makePlainArgumentValue(
+ $plain->argument_value,
+ $format
+ );
+ unset($plain->argument_value);
+ unset($plain->argument_format);
+ }
+ }
+
+ public function toCompatPlainObject()
+ {
+ $plain = parent::toPlainObject(
+ false,
+ true,
+ null,
+ false
+ );
+
+ unset($plain->id);
+ unset($plain->argument_name);
+ if (! isset($plain->argument_value)) {
+ unset($plain->argument_format);
+ }
+ if (! isset($plain->set_if)) {
+ unset($plain->set_if_format);
+ }
+
+ $this->transformPlainArgumentValue($plain);
+ unset($plain->command_id);
+
+ // Will happen only combined with $skipDefaults
+ if (array_keys((array) $plain) === ['value']) {
+ return $plain->value;
+ } else {
+ if (property_exists($plain, 'sort_order') && $plain->sort_order !== null) {
+ $plain->order = $plain->sort_order;
+ unset($plain->sort_order);
+ }
+
+ return $plain;
+ }
+ }
+
+ public function toFullPlainObject($skipDefaults = false)
+ {
+ $plain = parent::toPlainObject(
+ false,
+ $skipDefaults,
+ null,
+ false
+ );
+
+ unset($plain->id);
+
+ return $plain;
+ }
+
+ public function toPlainObject(
+ $resolved = false,
+ $skipDefaults = false,
+ array $chosenProperties = null,
+ $resolveIds = true,
+ $keepId = false
+ ) {
+ if ($resolved) {
+ throw new RuntimeException(
+ 'A single CommandArgument cannot be resolved'
+ );
+ }
+
+ if ($chosenProperties) {
+ throw new RuntimeException(
+ 'IcingaCommandArgument does not support chosenProperties[]'
+ );
+ }
+
+ if ($keepId) {
+ throw new RuntimeException(
+ 'IcingaCommandArgument does not support $keepId'
+ );
+ }
+
+ // $resolveIds is misused here
+ if ($resolveIds) {
+ return $this->toCompatPlainObject();
+ } else {
+ return $this->toFullPlainObject($skipDefaults);
+ }
+ }
+
+ public function toConfigString()
+ {
+ $data = array();
+ $value = $this->get('argument_value');
+ if ($value) {
+ switch ($this->get('argument_format')) {
+ case 'string':
+ $data['value'] = c::renderString($value);
+ break;
+ case 'json':
+ if (is_object($value)) {
+ $data['value'] = c::renderDictionary($value);
+ } elseif (is_array($value)) {
+ $data['value'] = c::renderArray($value);
+ } elseif (is_null($value)) {
+ // TODO: recheck all this. I bet we never reach this:
+ $data['value'] = 'null';
+ } elseif (is_bool($value)) {
+ $data['value'] = c::renderBoolean($value);
+ } else {
+ $data['value'] = $value;
+ }
+ break;
+ case 'expression':
+ $data['value'] = c::renderExpression($value);
+ break;
+ }
+ }
+
+ if ($this->get('sort_order') !== null) {
+ $data['order'] = $this->get('sort_order');
+ }
+
+ if (null !== $this->get('set_if')) {
+ switch ($this->get('set_if_format')) {
+ case 'expression':
+ $data['set_if'] = c::renderExpression($this->get('set_if'));
+ break;
+ case 'string':
+ default:
+ $data['set_if'] = c::renderString($this->get('set_if'));
+ break;
+ }
+ }
+
+ if (null !== $this->get('required')) {
+ $data['required'] = c::renderBoolean($this->get('required'));
+ }
+
+ if ($this->isSkippingKey()) {
+ $data['skip_key'] = c::renderBoolean('y');
+ }
+
+ if (null !== $this->get('repeat_key')) {
+ $data['repeat_key'] = c::renderBoolean($this->get('repeat_key'));
+ }
+
+ if (null !== $this->get('description')) {
+ $data['description'] = c::renderString($this->get('description'));
+ }
+
+ if (array_keys($data) === ['value']) {
+ return $data['value'];
+ } else {
+ return c::renderDictionary($data);
+ }
+ }
+}
diff --git a/library/Director/Objects/IcingaCommandField.php b/library/Director/Objects/IcingaCommandField.php
new file mode 100644
index 0000000..086cb56
--- /dev/null
+++ b/library/Director/Objects/IcingaCommandField.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaCommandField extends IcingaObjectField
+{
+ protected $keyName = array('command_id', 'datafield_id');
+
+ protected $table = 'icinga_command_field';
+
+ protected $defaultProperties = array(
+ 'command_id' => null,
+ 'datafield_id' => null,
+ 'is_required' => null,
+ 'var_filter' => null,
+ );
+}
diff --git a/library/Director/Objects/IcingaDependency.php b/library/Director/Objects/IcingaDependency.php
new file mode 100644
index 0000000..c9d9b89
--- /dev/null
+++ b/library/Director/Objects/IcingaDependency.php
@@ -0,0 +1,631 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\Filter\Filter;
+
+class IcingaDependency extends IcingaObject implements ExportInterface
+{
+ protected $table = 'icinga_dependency';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'apply_to' => null,
+ 'parent_host_id' => null,
+ 'parent_host_var' => null,
+ 'parent_service_id' => null,
+ 'child_host_id' => null,
+ 'child_service_id' => null,
+ 'disable_checks' => null,
+ 'disable_notifications' => null,
+ 'ignore_soft_states' => null,
+ 'period_id' => null,
+ 'zone_id' => null,
+ 'assign_filter' => null,
+ 'parent_service_by_name' => null,
+ ];
+
+ protected $uuidColumn = 'uuid';
+
+ protected $supportsCustomVars = false;
+
+ protected $supportsImports = true;
+
+ protected $supportsApplyRules = true;
+
+ /**
+ * @internal
+ * @var bool
+ */
+ protected $renderApplyForArray = false;
+
+ protected $relatedSets = [
+ 'states' => 'StateFilterSet',
+ ];
+
+ protected $relations = [
+ 'zone' => 'IcingaZone',
+ 'parent_host' => 'IcingaHost',
+ 'parent_service' => 'IcingaService',
+ 'child_host' => 'IcingaHost',
+ 'child_service' => 'IcingaService',
+ 'period' => 'IcingaTimePeriod',
+ ];
+
+ protected $booleans = [
+ 'disable_checks' => 'disable_checks',
+ 'disable_notifications' => 'disable_notifications',
+ 'ignore_soft_states' => 'ignore_soft_states'
+ ];
+
+ protected $propertiesNotForRendering = [
+ 'id',
+ 'object_name',
+ 'object_type',
+ 'apply_to',
+ ];
+
+ public function getUniqueIdentifier()
+ {
+ return $this->getObjectName();
+ }
+
+ /**
+ * @return object
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ $props = (array) $this->toPlainObject();
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return static
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ $key = $name;
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Dependency "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ public function parentHostIsVar()
+ {
+ return $this->get('parent_host_var') !== null;
+ }
+
+ /**
+ * @return string
+ * @throws ConfigurationError
+ */
+ protected function renderObjectHeader()
+ {
+ if ($this->isApplyRule()) {
+ if (($to = $this->get('apply_to')) === null) {
+ throw new ConfigurationError(
+ 'Applied dependency "%s" has no valid object type',
+ $this->getObjectName()
+ );
+ }
+
+ if ($this->renderApplyForArray) {
+ return $this->renderArrayObjectHeader($to);
+ }
+
+ return $this->renderSingleObjectHeader($to);
+ }
+
+ return parent::renderObjectHeader();
+ }
+
+ protected function renderSingleObjectHeader($to)
+ {
+ return sprintf(
+ "%s %s %s to %s {\n",
+ $this->getObjectTypeName(),
+ $this->getType(),
+ c::renderString($this->getObjectName()),
+ ucfirst($to)
+ );
+ }
+
+ protected function renderArrayObjectHeader($to)
+ {
+ return sprintf(
+ "%s %s %s for (host_parent_name in %s) to %s {\n",
+ $this->getObjectTypeName(),
+ $this->getType(),
+ c::renderString($this->getObjectName()),
+ $this->get('parent_host_var'),
+ ucfirst($to)
+ );
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderSuffix()
+ {
+ if (! $this->parentHostIsVar()) {
+ return parent::renderSuffix();
+ }
+
+ if ((string) $this->get('assign_filter') !== '') {
+ $suffix = parent::renderSuffix();
+ } else {
+ $suffix = ' assign where ' . $this->renderAssignFilterExtension('')
+ . "\n" . parent::renderSuffix();
+ }
+
+ if ($this->renderApplyForArray) {
+ return $suffix;
+ }
+
+ return $suffix . $this->renderApplyForArrayClone();
+ }
+
+ protected function renderApplyForArrayClone()
+ {
+ $clone = clone($this);
+ $clone->renderApplyForArray = true;
+
+ return $clone->toConfigString();
+ }
+
+ public function isApplyForArrayClone()
+ {
+ return $this->renderApplyForArray;
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ public function renderAssign_Filter()
+ {
+ if ($this->parentHostIsVar()) {
+ return preg_replace(
+ '/\n$/m',
+ $this->renderAssignFilterExtension() . "\n",
+ parent::renderAssign_Filter()
+ );
+ }
+
+ return parent::renderAssign_Filter();
+ }
+
+ protected function renderAssignFilterExtension($pre = ' && ')
+ {
+ $varName = $this->get('parent_host_var');
+ if ($this->renderApplyForArray) {
+ return sprintf('%stypeof(%s) == Array', $pre, $varName);
+ }
+
+ return sprintf('%stypeof(%s) == String', $pre, $varName);
+ }
+
+ protected function setKey($key)
+ {
+ // TODO: Check if this method can be removed
+ if (is_int($key)) {
+ $this->id = $key;
+ } elseif (is_array($key)) {
+ $keys = [
+ 'id',
+ 'parent_host_id',
+ 'parent_service_id',
+ 'child_host_id',
+ 'child_service_id',
+ 'object_name'
+ ];
+
+ foreach ($keys as $k) {
+ if (array_key_exists($k, $key)) {
+ $this->set($k, $key[$k]);
+ }
+ }
+ } else {
+ return parent::setKey($key);
+ }
+
+ return $this;
+ }
+
+ protected function renderAssignments()
+ {
+ // TODO: this will never be reached
+ if ($this->hasBeenAssignedToServiceApply()) {
+ /** @var IcingaService $tmpService */
+ $tmpService = $this->getRelatedObject(
+ 'child_service',
+ $this->get('child_service_id')
+ );
+ // TODO: fix this, will crash:
+ $assigns = $tmpService->assignments()->toConfigString();
+
+ $filter = sprintf(
+ '%s && service.name == "%s"',
+ trim($assigns),
+ $this->get('child_service')
+ );
+ return "\n " . $filter . "\n";
+ }
+
+ if ($this->hasBeenAssignedToHostTemplateService()) {
+ $filter = sprintf(
+ 'assign where "%s" in host.templates && service.name == "%s"',
+ $this->get('child_host'),
+ $this->get('child_service')
+ );
+ return "\n " . $filter . "\n";
+ }
+ if ($this->hasBeenAssignedToHostTemplate()) {
+ $filter = sprintf(
+ 'assign where "%s" in host.templates',
+ $this->get('child_host')
+ );
+ return "\n " . $filter . "\n";
+ }
+
+ if ($this->hasBeenAssignedToServiceTemplate()) {
+ $filter = sprintf(
+ 'assign where "%s" in service.templates',
+ $this->get('child_service')
+ );
+ return "\n " . $filter . "\n";
+ }
+
+ return parent::renderAssignments();
+ }
+
+ protected function hasBeenAssignedToHostTemplate()
+ {
+ try {
+ $id = $this->get('child_host_id');
+ return $id && $this->getRelatedObject(
+ 'child_host',
+ $id
+ )->isTemplate();
+ } catch (NotFoundError $e) {
+ return false;
+ }
+ }
+
+ protected function hasBeenAssignedToServiceTemplate()
+ {
+ try {
+ $id = $this->get('child_service_id');
+ return $id && $this->getRelatedObject(
+ 'child_service',
+ $id
+ )->isTemplate();
+ } catch (NotFoundError $e) {
+ return false;
+ }
+ }
+
+ protected function hasBeenAssignedToHostTemplateService()
+ {
+ if (!$this->hasBeenAssignedToHostTemplate()) {
+ return false;
+ }
+ try {
+ $id = $this->get('child_service_id');
+ return $id && $this->getRelatedObject(
+ 'child_service',
+ $id
+ )->isObject();
+ } catch (NotFoundError $e) {
+ return false;
+ }
+ }
+
+ protected function hasBeenAssignedToServiceApply()
+ {
+ try {
+ $id = $this->get('child_service_id');
+ return $id && $this->getRelatedObject(
+ 'child_service',
+ $id
+ )->isApplyRule();
+ } catch (NotFoundError $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Render child_host_id as host_name
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderChild_host_id()
+ {
+ // @codingStandardsIgnoreEnd
+
+ if ($this->hasBeenAssignedToHostTemplate()) {
+ return '';
+ }
+
+ return $this->renderRelationProperty(
+ 'child_host',
+ $this->get('child_host_id'),
+ 'child_host_name'
+ );
+ }
+
+ /**
+ * Render parent_host_id as parent_host_name
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderParent_host_id()
+ {
+ // @codingStandardsIgnoreEnd
+ return $this->renderRelationProperty(
+ 'parent_host',
+ $this->get('parent_host_id'),
+ 'parent_host_name'
+ );
+ }
+
+ /**
+ * Render parent_host_var as parent_host
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderParent_host_var()
+ {
+ // @codingStandardsIgnoreEnd
+ if ($this->renderApplyForArray) {
+ return c::renderKeyValue(
+ 'parent_host_name',
+ 'host_parent_name'
+ );
+ }
+
+ // @codingStandardsIgnoreEnd
+ return c::renderKeyValue(
+ 'parent_host_name',
+ $this->get('parent_host_var')
+ );
+ }
+
+ /**
+ * Render child_service_id as host_name
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderChild_service_id()
+ {
+ // @codingStandardsIgnoreEnd
+ if ($this->hasBeenAssignedToServiceTemplate()
+ || $this->hasBeenAssignedToHostTemplateService()
+ || $this->hasBeenAssignedToServiceApply()
+ ) {
+ return '';
+ }
+
+ return $this->renderRelationProperty(
+ 'child_service',
+ $this->get('child_service_id'),
+ 'child_service_name'
+ );
+ }
+
+ /**
+ * Render parent_service_id as parent_service_name
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderParent_service_id()
+ {
+ return $this->renderRelationProperty(
+ 'parent_service',
+ $this->get('parent_service_id'),
+ 'parent_service_name'
+ );
+ }
+
+ //
+ /**
+ * Render parent_service_by_name as parent_service_name
+ *
+ * Special case for parent service set as plain string for Apply rules
+ *
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderParent_service_by_name()
+ {
+ // @codingStandardsIgnoreEnd
+ return c::renderKeyValue(
+ 'parent_service_name',
+ c::renderString($this->get('parent_service_by_name'))
+ );
+ }
+
+ public function isApplyRule()
+ {
+ if ($this->hasBeenAssignedToHostTemplate()
+ || $this->hasBeenAssignedToServiceTemplate()
+ || $this->hasBeenAssignedToServiceApply()
+ ) {
+ return true;
+ }
+
+ return parent::isApplyRule();
+ }
+
+ protected function resolveUnresolvedRelatedProperty($name)
+ {
+ $short = substr($name, 0, -3);
+ /** @var IcingaObject $class */
+ $class = $this->getRelationClass($short);
+ $objKey = $this->unresolvedRelatedProperties[$name];
+
+ # related services need array key
+ if ($class === IcingaService::class) {
+ if ($name === 'parent_service_id' && $this->get('object_type') === 'apply') {
+ //special case , parent service can be set as simple string for Apply
+ if ($this->properties['parent_host_id'] === null) {
+ $this->reallySet(
+ 'parent_service_by_name',
+ $this->unresolvedRelatedProperties[$name]
+ );
+ $this->reallySet('parent_service_id', null);
+ unset($this->unresolvedRelatedProperties[$name]);
+ return;
+ }
+ }
+
+ $this->reallySet('parent_service_by_name', null);
+ $hostIdProperty = str_replace('service', 'host', $name);
+ if (isset($this->properties[$hostIdProperty])) {
+ $objKey = [
+ 'host_id' => $this->properties[$hostIdProperty],
+ 'object_name' => $this->unresolvedRelatedProperties[$name]
+ ];
+ } else {
+ $objKey = [
+ 'host_id' => null,
+ 'object_name' => $this->unresolvedRelatedProperties[$name]
+ ];
+ }
+
+ try {
+ $class::load($objKey, $this->connection);
+ } catch (NotFoundError $e) {
+ // Not a simple service on host
+ // Hunt through inherited services, use service assigned to
+ // template if found
+ $tmpHost = IcingaHost::loadWithAutoIncId(
+ $this->properties[$hostIdProperty],
+ $this->connection
+ );
+
+ // services for applicable templates
+ $resolver = $tmpHost->templateResolver();
+ foreach ($resolver->fetchResolvedParents() as $template_obj) {
+ $objKey = [
+ 'host_id' => $template_obj->id,
+ 'object_name' => $this->unresolvedRelatedProperties[$name]
+ ];
+ try {
+ $object = $class::load($objKey, $this->connection);
+ } catch (NotFoundError $e) {
+ continue;
+ }
+ break;
+ }
+
+ if (!isset($object)) {
+ // Not an inherited service, now try apply rules
+ $matcher = HostApplyMatches::prepare($tmpHost);
+ foreach ($this->getAllApplyRules() as $rule) {
+ if ($matcher->matchesFilter($rule->filter)) {
+ if ($rule->name === $this->unresolvedRelatedProperties[$name]) {
+ $object = IcingaService::loadWithAutoIncId(
+ $rule->id,
+ $this->connection
+ );
+ break;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ $object = $class::load($objKey, $this->connection);
+ }
+
+ if (isset($object)) {
+ $this->reallySet($name, $object->get('id'));
+ unset($this->unresolvedRelatedProperties[$name]);
+ } else {
+ throw new NotFoundError('Unable to resolve related property: "%s"', $name);
+ }
+ }
+
+ protected function getAllApplyRules()
+ {
+ $allApplyRules = $this->fetchAllApplyRules();
+ foreach ($allApplyRules as $rule) {
+ $rule->filter = Filter::fromQueryString($rule->assign_filter);
+ }
+
+ return $allApplyRules;
+ }
+
+ protected function fetchAllApplyRules()
+ {
+ $db = $this->connection->getDbAdapter();
+ $query = $db->select()->from(['s' => 'icinga_service'], [
+ 'id' => 's.id',
+ 'name' => 's.object_name',
+ 'assign_filter' => 's.assign_filter',
+ ])->where('object_type = ? AND assign_filter IS NOT NULL', 'apply');
+
+ return $db->fetchAll($query);
+ }
+
+ protected function getRelatedProperty($key)
+ {
+ $related = parent::getRelatedProperty($key);
+ // handle special case for plain string parent service on Dependency
+ // Apply rules
+ if ($related === null && $key === 'parent_service'
+ && null !== $this->get('parent_service_by_name')
+ ) {
+ return $this->get('parent_service_by_name');
+ }
+
+ return $related;
+ }
+}
diff --git a/library/Director/Objects/IcingaEndpoint.php b/library/Director/Objects/IcingaEndpoint.php
new file mode 100644
index 0000000..030183b
--- /dev/null
+++ b/library/Director/Objects/IcingaEndpoint.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Core\CoreApi;
+use Icinga\Module\Director\Core\LegacyDeploymentApi;
+use Icinga\Module\Director\Core\RestApiClient;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use InvalidArgumentException;
+use RuntimeException;
+
+class IcingaEndpoint extends IcingaObject
+{
+ protected $table = 'icinga_endpoint';
+
+ protected $supportsImports = true;
+
+ protected $uuidColumn = 'uuid';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'zone_id' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'host' => null,
+ 'port' => null,
+ 'log_duration' => null,
+ 'apiuser_id' => null,
+ ];
+
+ protected $relations = [
+ 'zone' => 'IcingaZone',
+ 'apiuser' => 'IcingaApiUser',
+ ];
+
+ public function hasApiUser()
+ {
+ return $this->getResolvedProperty('apiuser_id') !== null;
+ }
+
+ public function getApiUser()
+ {
+ $id = $this->getResolvedProperty('apiuser_id');
+ if ($id === null) {
+ throw new RuntimeException('Trying to get API User for Endpoint without such: ' . $this->getObjectName());
+ }
+
+ return $this->getRelatedObject('apiuser', $id);
+ }
+
+ /**
+ * Return a core API, depending on the configuration format
+ *
+ * @return CoreApi|LegacyDeploymentApi
+ */
+ public function api()
+ {
+ $format = $this->connection->settings()->config_format;
+ if ($format === 'v2') {
+ $api = new CoreApi($this->getRestApiClient());
+ $api->setDb($this->getConnection());
+
+ return $api;
+ } elseif ($format === 'v1') {
+ return new LegacyDeploymentApi($this->connection);
+ } else {
+ throw new InvalidArgumentException("Unsupported config format: $format");
+ }
+ }
+
+ /**
+ * @return RestApiClient
+ */
+ public function getRestApiClient()
+ {
+ $client = new RestApiClient(
+ $this->getResolvedProperty('host', $this->getObjectName()),
+ $this->getResolvedProperty('port')
+ );
+
+ $user = $this->getApiUser();
+ $client->setCredentials(
+ // TODO: $user->client_dn,
+ $user->object_name,
+ $user->password
+ );
+
+ return $client;
+ }
+
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ try {
+ if ($zone = $this->getResolvedRelated('zone')) {
+ return $zone->getRenderingZone($config);
+ }
+ } catch (NestingError $e) {
+ return self::RESOLVE_ERROR;
+ }
+
+ return parent::getRenderingZone($config);
+ }
+
+ /**
+ * @return int
+ */
+ public function getResolvedPort()
+ {
+ $port = $this->getSingleResolvedProperty('port');
+ if (null === $port) {
+ return 5665;
+ } else {
+ return (int) $port;
+ }
+ }
+
+ public function getDescriptiveUrl()
+ {
+ return sprintf(
+ 'https://%s@%s:%d/v1/',
+ $this->getApiUser()->getObjectName(),
+ $this->getResolvedProperty('host', $this->getObjectName()),
+ $this->getResolvedPort()
+ );
+ }
+
+ /**
+ * Use duration time renderer helper
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ protected function renderLog_duration()
+ {
+ // @codingStandardsIgnoreEnd
+ return $this->renderPropertyAsSeconds('log_duration');
+ }
+
+ /**
+ * Internal property, will not be rendered
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ protected function renderApiuser_id()
+ {
+ // @codingStandardsIgnoreEnd
+ return '';
+ }
+}
diff --git a/library/Director/Objects/IcingaFlatVar.php b/library/Director/Objects/IcingaFlatVar.php
new file mode 100644
index 0000000..3bbf81c
--- /dev/null
+++ b/library/Director/Objects/IcingaFlatVar.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\CustomVariable\CustomVariable;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+
+class IcingaFlatVar extends DbObject
+{
+ protected $table = 'icinga_flat_var';
+
+ protected $keyName = [
+ 'var_checksum',
+ 'flatname_checksum'
+ ];
+
+ protected $defaultProperties = [
+ 'var_checksum' => null,
+ 'flatname_checksum' => null,
+ 'flatname' => null,
+ 'flatvalue' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'var_checksum',
+ 'flatname_checksum',
+ ];
+
+ public static function generateForCustomVar(CustomVariable $var, Db $db)
+ {
+ $flatVars = static::forCustomVar($var, $db);
+ foreach ($flatVars as $flat) {
+ $flat->store();
+ }
+
+ return $flatVars;
+ }
+
+ public static function forCustomVar(CustomVariable $var, Db $db)
+ {
+ $flat = [];
+ $varSum = $var->checksum();
+ $var->flatten($flat, $var->getKey());
+ $flatVars = [];
+
+ foreach ($flat as $name => $value) {
+ $flatVar = static::create([
+ 'var_checksum' => $varSum,
+ 'flatname_checksum' => sha1($name, true),
+ 'flatname' => $name,
+ 'flatvalue' => $value,
+ ], $db);
+
+ $flatVar->store();
+ $flatVars[] = $flatVar;
+ }
+
+ return $flatVars;
+ }
+}
diff --git a/library/Director/Objects/IcingaHost.php b/library/Director/Objects/IcingaHost.php
new file mode 100644
index 0000000..2731f4a
--- /dev/null
+++ b/library/Director/Objects/IcingaHost.php
@@ -0,0 +1,668 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Data\Db\DbConnection;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Data\PropertiesFilter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+use Icinga\Module\Director\Objects\Extension\FlappingSupport;
+use InvalidArgumentException;
+use RuntimeException;
+
+class IcingaHost extends IcingaObject implements ExportInterface
+{
+ use FlappingSupport;
+
+ protected $table = 'icinga_host';
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'display_name' => null,
+ 'address' => null,
+ 'address6' => null,
+ 'check_command_id' => null,
+ 'max_check_attempts' => null,
+ 'check_period_id' => null,
+ 'check_interval' => null,
+ 'retry_interval' => null,
+ 'check_timeout' => null,
+ 'enable_notifications' => null,
+ 'enable_active_checks' => null,
+ 'enable_passive_checks' => null,
+ 'enable_event_handler' => null,
+ 'enable_flapping' => null,
+ 'enable_perfdata' => null,
+ 'event_command_id' => null,
+ 'flapping_threshold_high' => null,
+ 'flapping_threshold_low' => null,
+ 'volatile' => null,
+ 'zone_id' => null,
+ 'command_endpoint_id' => null,
+ 'notes' => null,
+ 'notes_url' => null,
+ 'action_url' => null,
+ 'icon_image' => null,
+ 'icon_image_alt' => null,
+ 'has_agent' => null,
+ 'master_should_connect' => null,
+ 'accept_config' => null,
+ 'custom_endpoint_name' => null,
+ 'api_key' => null,
+ 'template_choice_id' => null,
+ );
+
+ protected $relations = array(
+ 'check_command' => 'IcingaCommand',
+ 'event_command' => 'IcingaCommand',
+ 'check_period' => 'IcingaTimePeriod',
+ 'command_endpoint' => 'IcingaEndpoint',
+ 'zone' => 'IcingaZone',
+ 'template_choice' => 'IcingaTemplateChoiceHost',
+ );
+
+ protected $booleans = array(
+ 'enable_notifications' => 'enable_notifications',
+ 'enable_active_checks' => 'enable_active_checks',
+ 'enable_passive_checks' => 'enable_passive_checks',
+ 'enable_event_handler' => 'enable_event_handler',
+ 'enable_flapping' => 'enable_flapping',
+ 'enable_perfdata' => 'enable_perfdata',
+ 'volatile' => 'volatile',
+ 'has_agent' => 'has_agent',
+ 'master_should_connect' => 'master_should_connect',
+ 'accept_config' => 'accept_config',
+ );
+
+ protected $intervalProperties = array(
+ 'check_interval' => 'check_interval',
+ 'check_timeout' => 'check_timeout',
+ 'retry_interval' => 'retry_interval',
+ );
+
+ protected $supportsCustomVars = true;
+
+ protected $supportsGroups = true;
+
+ protected $supportsImports = true;
+
+ protected $supportsFields = true;
+
+ protected $supportsChoices = true;
+
+ protected $supportedInLegacy = true;
+
+ /** @var HostGroupMembershipResolver */
+ protected $hostgroupMembershipResolver;
+
+ protected $uuidColumn = 'uuid';
+
+ public static function enumProperties(
+ DbConnection $connection = null,
+ $prefix = '',
+ $filter = null
+ ) {
+ $hostProperties = array();
+ if ($filter === null) {
+ $filter = new PropertiesFilter();
+ }
+ $realProperties = array_merge(['templates'], static::create()->listProperties());
+ sort($realProperties);
+
+ if ($filter->match(PropertiesFilter::$HOST_PROPERTY, 'name')) {
+ $hostProperties[$prefix . 'name'] = 'name';
+ }
+ foreach ($realProperties as $prop) {
+ if (!$filter->match(PropertiesFilter::$HOST_PROPERTY, $prop)) {
+ continue;
+ }
+
+ if (substr($prop, -3) === '_id') {
+ if ($prop === 'template_choice_id') {
+ continue;
+ }
+ $prop = substr($prop, 0, -3);
+ }
+
+ $hostProperties[$prefix . $prop] = $prop;
+ }
+ unset($hostProperties[$prefix . 'uuid']);
+ unset($hostProperties[$prefix . 'custom_endpoint_name']);
+
+ $hostVars = array();
+
+ if ($connection instanceof Db) {
+ foreach ($connection->fetchDistinctHostVars() as $var) {
+ if ($filter->match(PropertiesFilter::$CUSTOM_PROPERTY, $var->varname, $var)) {
+ if ($var->datatype) {
+ $hostVars[$prefix . 'vars.' . $var->varname] = sprintf(
+ '%s (%s)',
+ $var->varname,
+ $var->caption
+ );
+ } else {
+ $hostVars[$prefix . 'vars.' . $var->varname] = $var->varname;
+ }
+ }
+ }
+ }
+
+ //$properties['vars.*'] = 'Other custom variable';
+ ksort($hostVars);
+
+
+ $props = mt('director', 'Host properties');
+ $vars = mt('director', 'Custom variables');
+
+ $properties = array();
+ if (!empty($hostProperties)) {
+ $properties[$props] = $hostProperties;
+ $properties[$props][$prefix . 'groups'] = 'Groups';
+ }
+
+ if (!empty($hostVars)) {
+ $properties[$vars] = $hostVars;
+ }
+
+ return $properties;
+ }
+
+ public function getCheckCommand()
+ {
+ $id = $this->getSingleResolvedProperty('check_command_id');
+ return IcingaCommand::loadWithAutoIncId(
+ $id,
+ $this->getConnection()
+ );
+ }
+
+ public function hasCheckCommand()
+ {
+ return $this->getSingleResolvedProperty('check_command_id') !== null;
+ }
+
+ public function renderToConfig(IcingaConfig $config)
+ {
+ parent::renderToConfig($config);
+
+ // TODO: We might alternatively let the whole config fail in case we have
+ // used use_agent together with a legacy config
+ if (! $config->isLegacy()) {
+ $this->renderAgentZoneAndEndpoint($config);
+ }
+ }
+
+ public function renderAgentZoneAndEndpoint(IcingaConfig $config = null)
+ {
+ if (!$this->isObject()) {
+ return;
+ }
+
+ if ($this->isDisabled()) {
+ return;
+ }
+
+ if ($this->getRenderingZone($config) === self::RESOLVE_ERROR) {
+ return;
+ }
+
+ if ($this->getSingleResolvedProperty('has_agent') !== 'y') {
+ return;
+ }
+
+ $name = $this->getEndpointName();
+
+ if (IcingaEndpoint::exists($name, $this->connection)) {
+ return;
+ }
+
+ $props = array(
+ 'object_name' => $name,
+ 'object_type' => 'object',
+ 'log_duration' => 0
+ );
+
+ if ($this->getSingleResolvedProperty('master_should_connect') === 'y') {
+ $props['host'] = $this->getSingleResolvedProperty('address');
+ }
+
+ $props['zone_id'] = $this->getSingleResolvedProperty('zone_id');
+
+ $endpoint = IcingaEndpoint::create($props, $this->connection);
+
+ $zone = IcingaZone::create(array(
+ 'object_name' => $name,
+ ), $this->connection)->setEndpointList(array($name));
+
+ if ($props['zone_id']) {
+ $zone->parent_id = $props['zone_id'];
+ } else {
+ $zone->parent = $this->connection->getMasterZoneName();
+ }
+
+ $pre = 'zones.d/' . $this->getRenderingZone($config) . '/';
+ $config->configFile($pre . 'agent_endpoints')->addObject($endpoint);
+ $config->configFile($pre . 'agent_zones')->addObject($zone);
+ }
+
+ /**
+ // @codingStandardsIgnoreStart
+ * @param $value
+ * @return string
+ */
+ protected function renderCustom_endpoint_name($value)
+ {
+ // @codingStandardsIgnoreEnd
+ // When feature flag feature_custom_endpoint is enabled, render custom var
+ if ($this->connection->settings()->get('feature_custom_endpoint') === 'y') {
+ return c::renderKeyValue('vars._director_custom_endpoint_name', c::renderString($value));
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns the hostname or custom endpoint name of the Icinga agent
+ *
+ * @return string
+ */
+ public function getEndpointName()
+ {
+ $name = $this->getObjectName();
+
+ if ($this->connection->settings()->get('feature_custom_endpoint') === 'y') {
+ if (($customName = $this->get('custom_endpoint_name')) !== null) {
+ $name = $customName;
+ }
+ }
+
+ return $name;
+ }
+
+ public function getAgentListenPort()
+ {
+ $conn = $this->connection;
+ $name = $this->getObjectName();
+ if (IcingaEndpoint::exists($name, $conn)) {
+ return IcingaEndpoint::load($name, $conn)->getResolvedPort();
+ } else {
+ return 5665;
+ }
+ }
+
+ public function getUniqueIdentifier()
+ {
+ if ($this->isTemplate()) {
+ return $this->getObjectName();
+ } else {
+ throw new RuntimeException(
+ 'getUniqueIdentifier() is supported by Host Templates only'
+ );
+ }
+ }
+
+ /**
+ * @return object
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ // TODO: ksort in toPlainObject?
+ $props = (array) $this->toPlainObject();
+ $props['fields'] = $this->loadFieldReferences();
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return IcingaHost
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ if ($properties['object_type'] !== 'template') {
+ throw new InvalidArgumentException(sprintf(
+ 'Can import only Templates, got "%s" for "%s"',
+ $properties['object_type'],
+ $name
+ ));
+ }
+ $key = $name;
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Service Template "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ // $object->newFields = $properties['fields'];
+ unset($properties['fields']);
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader
+ * @return array
+ */
+ protected function loadFieldReferences()
+ {
+ $db = $this->getDb();
+
+ $res = $db->fetchAll(
+ $db->select()->from([
+ 'hf' => 'icinga_host_field'
+ ], [
+ 'hf.datafield_id',
+ 'hf.is_required',
+ 'hf.var_filter',
+ ])->join(['df' => 'director_datafield'], 'df.id = hf.datafield_id', [])
+ ->where('host_id = ?', $this->get('id'))
+ ->order('varname ASC')
+ );
+
+ if (empty($res)) {
+ return [];
+ } else {
+ foreach ($res as $field) {
+ $field->datafield_id = (int) $field->datafield_id;
+ }
+ return $res;
+ }
+ }
+
+ public function beforeDelete()
+ {
+ foreach ($this->fetchServices() as $service) {
+ $service->delete();
+ }
+ foreach ($this->fetchServiceSets() as $set) {
+ $set->delete();
+ }
+
+ parent::beforeDelete();
+ }
+
+ public function hasAnyOverridenServiceVars()
+ {
+ $varname = $this->getServiceOverrivesVarname();
+ return isset($this->vars()->$varname);
+ }
+
+ public function getAllOverriddenServiceVars()
+ {
+ if ($this->hasAnyOverridenServiceVars()) {
+ $varname = $this->getServiceOverrivesVarname();
+ return $this->vars()->$varname->getValue();
+ } else {
+ return (object) array();
+ }
+ }
+
+ public function hasOverriddenServiceVars($service)
+ {
+ $all = $this->getAllOverriddenServiceVars();
+ return property_exists($all, $service);
+ }
+
+ public function getOverriddenServiceVars($service)
+ {
+ if ($this->hasOverriddenServiceVars($service)) {
+ $all = $this->getAllOverriddenServiceVars();
+ return $all->$service;
+ } else {
+ return (object) array();
+ }
+ }
+
+ public function overrideServiceVars($service, $vars)
+ {
+ // For PHP < 5.5.0:
+ $array = (array) $vars;
+ if (empty($array)) {
+ return $this->unsetOverriddenServiceVars($service);
+ }
+
+ $all = $this->getAllOverriddenServiceVars();
+ $all->$service = $vars;
+ $varname = $this->getServiceOverrivesVarname();
+ $this->vars()->$varname = $all;
+
+ return $this;
+ }
+
+ public function unsetOverriddenServiceVars($service)
+ {
+ if ($this->hasOverriddenServiceVars($service)) {
+ $all = (array) $this->getAllOverriddenServiceVars();
+ unset($all[$service]);
+
+ $varname = $this->getServiceOverrivesVarname();
+ if (empty($all)) {
+ unset($this->vars()->$varname);
+ } else {
+ $this->vars()->$varname = (object) $all;
+ }
+ }
+
+ return $this;
+ }
+
+ protected function notifyResolvers()
+ {
+ $resolver = $this->getHostGroupMembershipResolver();
+ $resolver->addObject($this);
+ $resolver->refreshDb();
+
+ return $this;
+ }
+
+ protected function getHostGroupMembershipResolver()
+ {
+ if ($this->hostgroupMembershipResolver === null) {
+ $this->hostgroupMembershipResolver = new HostGroupMembershipResolver(
+ $this->getConnection()
+ );
+ }
+
+ return $this->hostgroupMembershipResolver;
+ }
+
+ public function setHostGroupMembershipResolver(HostGroupMembershipResolver $resolver)
+ {
+ $this->hostgroupMembershipResolver = $resolver;
+ return $this;
+ }
+
+ protected function getServiceOverrivesVarname()
+ {
+ return $this->connection->settings()->override_services_varname;
+ }
+
+ /**
+ * Internal property, will not be rendered
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ protected function renderHas_Agent()
+ {
+ return '';
+ }
+
+ /**
+ * Internal property, will not be rendered
+ *
+ * @return string
+ */
+ protected function renderMaster_should_connect()
+ {
+ return '';
+ }
+
+ /**
+ * Internal property, will not be rendered
+ *
+ * @return string
+ */
+ protected function renderApi_key()
+ {
+ return '';
+ }
+
+ /**
+ * Internal property, will not be rendered
+ *
+ * @return string
+ */
+ protected function renderTemplate_choice_id()
+ {
+ return '';
+ }
+
+ /**
+ * Internal property, will not be rendered
+ *
+ * @return string
+ */
+ protected function renderAccept_config()
+ {
+ // @codingStandardsIgnoreEnd
+ return '';
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ protected function renderLegacyDisplay_Name()
+ {
+ // @codingStandardsIgnoreEnd
+ return c1::renderKeyValue('display_name', $this->display_name);
+ }
+
+ protected function renderLegacyVolatile()
+ {
+ // not available for hosts in Icinga 1.x
+ return;
+ }
+
+ protected function renderLegacyCustomExtensions()
+ {
+ $str = parent::renderLegacyCustomExtensions();
+
+ if (($alias = $this->vars()->get('alias')) !== null) {
+ $str .= c1::renderKeyValue('alias', $alias->getValue());
+ }
+
+ return $str;
+ }
+
+ /**
+ * @return IcingaService[]
+ */
+ public function fetchServices()
+ {
+ $connection = $this->getConnection();
+ $db = $connection->getDbAdapter();
+
+ /** @var IcingaService[] $services */
+ $services = IcingaService::loadAll(
+ $connection,
+ $db->select()->from('icinga_service')
+ ->where('host_id = ?', $this->get('id'))
+ );
+
+ return $services;
+ }
+
+ /**
+ * @return IcingaServiceSet[]
+ */
+ public function fetchServiceSets()
+ {
+ $connection = $this->getConnection();
+ $db = $connection->getDbAdapter();
+
+ /** @var IcingaServiceSet[] $sets */
+ $sets = IcingaServiceSet::loadAll(
+ $connection,
+ $db->select()->from('icinga_service_set')
+ ->where('host_id = ?', $this->get('id'))
+ );
+
+ return $sets;
+ }
+
+ /**
+ * @return string
+ */
+ public function generateApiKey()
+ {
+ $key = sha1(
+ (string) microtime(false)
+ . $this->getObjectName()
+ . rand(1, 1000000)
+ );
+
+ if ($this->dbHasApiKey($key)) {
+ $key = $this->generateApiKey();
+ }
+
+ $this->set('api_key', $key);
+
+ return $key;
+ }
+
+ protected function dbHasApiKey($key)
+ {
+ $db = $this->getDb();
+ $query = $db->select()->from(
+ ['o' => $this->getTableName()],
+ 'o.api_key'
+ )->where('api_key = ?', $key);
+
+ return $db->fetchOne($query) === $key;
+ }
+
+ public static function loadWithApiKey($key, Db $db)
+ {
+ $query = $db->getDbAdapter()
+ ->select()
+ ->from('icinga_host')
+ ->where('api_key = ?', $key);
+
+ $result = self::loadAll($db, $query);
+ if (count($result) !== 1) {
+ throw new NotFoundError('Got invalid API key "%s"', $key);
+ }
+
+ return current($result);
+ }
+}
diff --git a/library/Director/Objects/IcingaHostField.php b/library/Director/Objects/IcingaHostField.php
new file mode 100644
index 0000000..b68c9d4
--- /dev/null
+++ b/library/Director/Objects/IcingaHostField.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaHostField extends IcingaObjectField
+{
+ protected $keyName = array('host_id', 'datafield_id');
+
+ protected $table = 'icinga_host_field';
+
+ protected $defaultProperties = array(
+ 'host_id' => null,
+ 'datafield_id' => null,
+ 'is_required' => null,
+ 'var_filter' => null,
+ );
+}
diff --git a/library/Director/Objects/IcingaHostGroup.php b/library/Director/Objects/IcingaHostGroup.php
new file mode 100644
index 0000000..e11f672
--- /dev/null
+++ b/library/Director/Objects/IcingaHostGroup.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaHostGroup extends IcingaObjectGroup
+{
+ protected $table = 'icinga_hostgroup';
+
+ /** @var HostGroupMembershipResolver */
+ protected $hostgroupMembershipResolver;
+
+ public function supportsAssignments()
+ {
+ return true;
+ }
+
+ protected function getHostGroupMembershipResolver()
+ {
+ if ($this->hostgroupMembershipResolver === null) {
+ $this->hostgroupMembershipResolver = new HostGroupMembershipResolver(
+ $this->getConnection()
+ );
+ }
+
+ return $this->hostgroupMembershipResolver;
+ }
+
+ public function setHostGroupMembershipResolver(HostGroupMembershipResolver $resolver)
+ {
+ $this->hostgroupMembershipResolver = $resolver;
+ return $this;
+ }
+
+ protected function notifyResolvers()
+ {
+ $resolver = $this->getHostGroupMembershipResolver();
+ $resolver->addGroup($this);
+ $resolver->refreshDb();
+
+ return $this;
+ }
+}
diff --git a/library/Director/Objects/IcingaHostGroupAssignment.php b/library/Director/Objects/IcingaHostGroupAssignment.php
new file mode 100644
index 0000000..4e0e5a2
--- /dev/null
+++ b/library/Director/Objects/IcingaHostGroupAssignment.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaHostGroupAssignment extends IcingaObject
+{
+ protected $table = 'icinga_hostgroup_assignment';
+
+ protected $keyName = 'id';
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'service_id' => null,
+ 'filter_string' => null,
+ );
+
+ protected $relations = array(
+ 'service' => 'IcingaHostGroup',
+ );
+}
diff --git a/library/Director/Objects/IcingaHostVar.php b/library/Director/Objects/IcingaHostVar.php
new file mode 100644
index 0000000..45656d5
--- /dev/null
+++ b/library/Director/Objects/IcingaHostVar.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaHostVar extends IcingaObject
+{
+ protected $keyName = array('host_id', 'varname');
+
+ protected $table = 'icinga_host_var';
+
+ protected $defaultProperties = array(
+ 'host_id' => null,
+ 'varname' => null,
+ 'varvalue' => null,
+ 'format' => null,
+ );
+
+ public function onInsert()
+ {
+ }
+
+ public function onUpdate()
+ {
+ }
+
+ public function onDelete()
+ {
+ }
+}
diff --git a/library/Director/Objects/IcingaNotification.php b/library/Director/Objects/IcingaNotification.php
new file mode 100644
index 0000000..9c5d08d
--- /dev/null
+++ b/library/Director/Objects/IcingaNotification.php
@@ -0,0 +1,254 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use RuntimeException;
+
+class IcingaNotification extends IcingaObject implements ExportInterface
+{
+ protected $table = 'icinga_notification';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'apply_to' => null,
+ 'host_id' => null,
+ 'service_id' => null,
+ // 'users' => null,
+ // 'user_groups' => null,
+ 'times_begin' => null,
+ 'times_end' => null,
+ 'command_id' => null,
+ 'notification_interval' => null,
+ 'period_id' => null,
+ 'zone_id' => null,
+ 'assign_filter' => null,
+ ];
+
+ protected $uuidColumn = 'uuid';
+
+ protected $supportsCustomVars = true;
+
+ protected $supportsFields = true;
+
+ protected $supportsImports = true;
+
+ protected $supportsApplyRules = true;
+
+ protected $relatedSets = [
+ 'states' => 'StateFilterSet',
+ 'types' => 'TypeFilterSet',
+ ];
+
+ protected $multiRelations = [
+ 'users' => 'IcingaUser',
+ 'user_groups' => 'IcingaUserGroup',
+ ];
+
+ protected $relations = [
+ 'zone' => 'IcingaZone',
+ 'host' => 'IcingaHost',
+ 'service' => 'IcingaService',
+ 'command' => 'IcingaCommand',
+ 'period' => 'IcingaTimePeriod',
+ ];
+
+ protected $intervalProperties = [
+ 'notification_interval' => 'interval',
+ 'times_begin' => 'times_begin',
+ 'times_end' => 'times_end',
+ ];
+
+ protected function prefersGlobalZone()
+ {
+ return false;
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ * @return string
+ */
+ protected function renderTimes_begin()
+ {
+ // @codingStandardsIgnoreEnd
+ return c::renderKeyValue('times.begin', c::renderInterval($this->get('times_begin')));
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ * @return string
+ */
+ protected function renderTimes_end()
+ {
+ // @codingStandardsIgnoreEnd
+ return c::renderKeyValue('times.end', c::renderInterval($this->get('times_end')));
+ }
+
+ public function getUniqueIdentifier()
+ {
+ return $this->getObjectName();
+ }
+
+ /**
+ * @return \stdClass
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ // TODO: ksort in toPlainObject?
+ $props = (array) $this->toPlainObject();
+ $props['fields'] = $this->loadFieldReferences();
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return static
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ $key = $name;
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Notification "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ // $object->newFields = $properties['fields'];
+ unset($properties['fields']);
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader
+ * @return array
+ */
+ protected function loadFieldReferences()
+ {
+ $db = $this->getDb();
+
+ $res = $db->fetchAll(
+ $db->select()->from([
+ 'nf' => 'icinga_notification_field'
+ ], [
+ 'nf.datafield_id',
+ 'nf.is_required',
+ 'nf.var_filter',
+ ])->join(['df' => 'director_datafield'], 'df.id = nf.datafield_id', [])
+ ->where('notification_id = ?', $this->get('id'))
+ ->order('varname ASC')
+ );
+
+ if (empty($res)) {
+ return [];
+ } else {
+ foreach ($res as $field) {
+ $field->datafield_id = (int) $field->datafield_id;
+ }
+ return $res;
+ }
+ }
+
+ /**
+ * Do not render internal property apply_to
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderApply_to()
+ {
+ // @codingStandardsIgnoreEnd
+ return '';
+ }
+
+ protected function renderObjectHeader()
+ {
+ if ($this->isApplyRule()) {
+ if (($to = $this->get('apply_to')) === null) {
+ throw new RuntimeException(sprintf(
+ 'No "apply_to" object type has been set for Applied notification "%s"',
+ $this->getObjectName()
+ ));
+ }
+
+ return sprintf(
+ "%s %s %s to %s {\n",
+ $this->getObjectTypeName(),
+ $this->getType(),
+ c::renderString($this->getObjectName()),
+ ucfirst($to)
+ );
+ } else {
+ return parent::renderObjectHeader();
+ }
+ }
+
+ /**
+ * Render host_id as host_name
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderHost_id()
+ {
+ // @codingStandardsIgnoreEnd
+ return $this->renderRelationProperty('host', $this->get('host_id'), 'host_name');
+ }
+
+ /**
+ * Render service_id as service_name
+ *
+ * @codingStandardsIgnoreStart
+ * @return string
+ */
+ public function renderService_id()
+ {
+ // @codingStandardsIgnoreEnd
+ return $this->renderRelationProperty('service', $this->get('service_id'), 'service_name');
+ }
+
+ protected function setKey($key)
+ {
+ if (is_int($key)) {
+ $this->id = $key;
+ } elseif (is_array($key)) {
+ foreach (['id', 'host_id', 'service_id', 'object_name'] as $k) {
+ if (array_key_exists($k, $key)) {
+ $this->set($k, $key[$k]);
+ }
+ }
+ } else {
+ return parent::setKey($key);
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/Objects/IcingaNotificationField.php b/library/Director/Objects/IcingaNotificationField.php
new file mode 100644
index 0000000..d51f9e6
--- /dev/null
+++ b/library/Director/Objects/IcingaNotificationField.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaNotificationField extends IcingaObjectField
+{
+ protected $keyName = array('notification_id', 'datafield_id');
+
+ protected $table = 'icinga_notification_field';
+
+ protected $defaultProperties = array(
+ 'notification_id' => null,
+ 'datafield_id' => null,
+ 'is_required' => null,
+ 'var_filter' => null,
+ );
+}
diff --git a/library/Director/Objects/IcingaObject.php b/library/Director/Objects/IcingaObject.php
new file mode 100644
index 0000000..04ae32b
--- /dev/null
+++ b/library/Director/Objects/IcingaObject.php
@@ -0,0 +1,3258 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\CustomVariable\CustomVariables;
+use Icinga\Module\Director\Data\Db\DbDataFormatter;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\IcingaConfig\AssignRenderer;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\IcingaConfig\ExtensibleSet;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use LogicException;
+use RuntimeException;
+
+abstract class IcingaObject extends DbObject implements IcingaConfigRenderer
+{
+ const RESOLVE_ERROR = '(unable to resolve)';
+
+ protected $keyName = 'object_name';
+
+ protected $autoincKeyName = 'id';
+
+ /** @var bool Whether this Object supports custom variables */
+ protected $supportsCustomVars = false;
+
+ /** @var bool Whether there exist Groups for this object type */
+ protected $supportsGroups = false;
+
+ /** @var bool Whether this Object makes use of (time) ranges */
+ protected $supportsRanges = false;
+
+ /** @var bool Whether inheritance via "imports" property is supported */
+ protected $supportsImports = false;
+
+ /** @var bool Allows controlled custom var access through Fields */
+ protected $supportsFields = false;
+
+ /** @var bool Whether this object can be rendered as 'apply Object' */
+ protected $supportsApplyRules = false;
+
+ /** @var bool Whether Sets of object can be defined */
+ protected $supportsSets = false;
+
+ /** @var bool Whether this Object supports template-based Choices */
+ protected $supportsChoices = false;
+
+ /** @var bool If the object is rendered in legacy config */
+ protected $supportedInLegacy = false;
+
+ protected $rangeClass;
+
+ protected $type;
+
+ /* key/value!! */
+ protected $booleans = [];
+
+ // Property suffixed with _id must exist
+ protected $relations = [
+ // property => PropertyClass
+ ];
+
+ protected $relatedSets = [
+ // property => ExtensibleSetClass
+ ];
+
+ protected $multiRelations = [
+ // property => IcingaObjectClass
+ ];
+
+ /** @var IcingaObjectMultiRelations[] */
+ protected $loadedMultiRelations = [];
+
+ /**
+ * Allows to set properties pointing to related objects by name without
+ * loading the related object.
+ *
+ * @var array
+ */
+ protected $unresolvedRelatedProperties = [];
+
+ protected $loadedRelatedSets = [];
+
+ // Will be rendered first, before imports
+ protected $prioritizedProperties = [];
+
+ protected $propertiesNotForRendering = [
+ 'id',
+ 'object_name',
+ 'object_type',
+ ];
+
+ /**
+ * Array of interval property names
+ *
+ * Those will be automagically munged to integers (seconds) and rendered
+ * as durations (e.g. 2m 10s). Array expects (propertyName => renderedKey)
+ *
+ * @var array
+ */
+ protected $intervalProperties = [];
+
+ /** @var Db */
+ protected $connection;
+
+ private $vars;
+
+ /** @var IcingaObjectGroups */
+ private $groups;
+
+ private $imports;
+
+ /** @var IcingaTimePeriodRanges - TODO: generic ranges */
+ private $ranges;
+
+ private $shouldBeRemoved = false;
+
+ private $resolveCache = [];
+
+ private $cachedPlainUnmodified;
+
+ private $templateResolver;
+
+ protected static $tree;
+
+ /**
+ * @return Db
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ public function propertyIsBoolean($property)
+ {
+ return array_key_exists($property, $this->booleans);
+ }
+
+ public function propertyIsInterval($property)
+ {
+ return array_key_exists($property, $this->intervalProperties);
+ }
+
+ /**
+ * Whether a property ends with _id and might refer another object
+ *
+ * @param $property string Property name, like zone_id
+ *
+ * @return bool
+ */
+ public function propertyIsRelation($property)
+ {
+ if ($key = $this->stripIdSuffix($property)) {
+ return $this->hasRelation($key);
+ }
+
+ return false;
+ }
+
+ protected function stripIdSuffix($key)
+ {
+ $end = substr($key, -3);
+
+ if ('_id' === $end) {
+ return substr($key, 0, -3);
+ }
+
+ return false;
+ }
+
+ public function propertyIsRelatedSet($property)
+ {
+ return array_key_exists($property, $this->relatedSets);
+ }
+
+ public function propertyIsMultiRelation($property)
+ {
+ return array_key_exists($property, $this->multiRelations);
+ }
+
+ public function listMultiRelations()
+ {
+ return array_keys($this->multiRelations);
+ }
+
+ public function getMultiRelation($property)
+ {
+ if (! $this->hasLoadedMultiRelation($property)) {
+ $this->loadMultiRelation($property);
+ }
+
+ return $this->loadedMultiRelations[$property];
+ }
+
+ public function setMultiRelation($property, $values)
+ {
+ $this->getMultiRelation($property)->set($values);
+ return $this;
+ }
+
+ private function loadMultiRelation($property)
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ $rel = IcingaObjectMultiRelations::loadForStoredObject(
+ $this,
+ $property,
+ $this->multiRelations[$property]
+ );
+ } else {
+ $rel = new IcingaObjectMultiRelations(
+ $this,
+ $property,
+ $this->multiRelations[$property]
+ );
+ }
+
+ $this->loadedMultiRelations[$property] = $rel;
+ }
+
+ private function hasLoadedMultiRelation($property)
+ {
+ return array_key_exists($property, $this->loadedMultiRelations);
+ }
+
+ private function loadAllMultiRelations()
+ {
+ foreach (array_keys($this->multiRelations) as $key) {
+ if (! $this->hasLoadedMultiRelation($key)) {
+ $this->loadMultiRelation($key);
+ }
+ }
+
+ ksort($this->loadedMultiRelations);
+ return $this->loadedMultiRelations;
+ }
+
+ protected function getRelatedSetClass($property)
+ {
+ $prefix = '\\Icinga\\Module\\Director\\IcingaConfig\\';
+ return $prefix . $this->relatedSets[$property];
+ }
+
+ /**
+ * @param $property
+ * @return ExtensibleSet
+ */
+ protected function getRelatedSet($property)
+ {
+ if (! array_key_exists($property, $this->loadedRelatedSets)) {
+ /** @var ExtensibleSet $class */
+ $class = $this->getRelatedSetClass($property);
+ $this->loadedRelatedSets[$property]
+ = $class::forIcingaObject($this, $property);
+ }
+
+ return $this->loadedRelatedSets[$property];
+ }
+
+ /**
+ * @return ExtensibleSet[]
+ */
+ protected function relatedSets()
+ {
+ $sets = [];
+ foreach ($this->relatedSets as $key => $class) {
+ $sets[$key] = $this->getRelatedSet($key);
+ }
+
+ return $sets;
+ }
+
+ /**
+ * Whether the given property name is a short name for a relation
+ *
+ * This might be 'zone' for 'zone_id'
+ *
+ * @param string $property Property name
+ *
+ * @return bool
+ */
+ public function hasRelation($property)
+ {
+ return array_key_exists($property, $this->relations);
+ }
+
+ protected function getRelationClass($property)
+ {
+ return __NAMESPACE__ . '\\' . $this->relations[$property];
+ }
+
+ protected function getRelationObjectClass($property)
+ {
+ return $this->relations[$property];
+ }
+
+ /**
+ * @param $property
+ * @return IcingaObject
+ */
+ public function getRelated($property)
+ {
+ return $this->getRelatedObject($property, $this->{$property . '_id'});
+ }
+
+ /**
+ * @param $property
+ * @param $id
+ * @return string
+ */
+ public function getRelatedObjectName($property, $id)
+ {
+ return $this->getRelatedObject($property, $id)->getObjectName();
+ }
+
+ /**
+ * @param $property
+ * @param $id
+ * @return IcingaObject
+ */
+ protected function getRelatedObject($property, $id)
+ {
+ /** @var IcingaObject $class */
+ $class = $this->getRelationClass($property);
+ try {
+ $object = $class::loadWithAutoIncId($id, $this->connection);
+ } catch (NotFoundError $e) {
+ throw new RuntimeException($e->getMessage(), 0, $e);
+ }
+
+ return $object;
+ }
+
+ /**
+ * @param $property
+ * @return IcingaObject|null
+ */
+ public function getResolvedRelated($property)
+ {
+ $id = $this->getSingleResolvedProperty($property . '_id');
+
+ if ($id) {
+ return $this->getRelatedObject($property, $id);
+ }
+
+ return null;
+ }
+
+ public function prefetchAllRelatedTypes()
+ {
+ foreach (array_unique(array_values($this->relations)) as $relClass) {
+ /** @var static $class */
+ $class = __NAMESPACE__ . '\\' . $relClass;
+ $class::prefetchAll($this->getConnection());
+ }
+ }
+
+ public static function prefetchAllRelationsByType($type, Db $db)
+ {
+ /** @var static $class */
+ $class = DbObjectTypeRegistry::classByType($type);
+ /** @var static $dummy */
+ $dummy = $class::create([], $db);
+ $dummy->prefetchAllRelatedTypes();
+ }
+
+ /**
+ * Whether this Object supports custom variables
+ *
+ * @return bool
+ */
+ public function supportsCustomVars()
+ {
+ return $this->supportsCustomVars;
+ }
+
+ /**
+ * Whether there exist Groups for this object type
+ *
+ * @return bool
+ */
+ public function supportsGroups()
+ {
+ return $this->supportsGroups;
+ }
+
+ /**
+ * Whether this Object makes use of (time) ranges
+ *
+ * @return bool
+ */
+ public function supportsRanges()
+ {
+ return $this->supportsRanges;
+ }
+
+ /**
+ * Whether this object supports (command) Arguments
+ *
+ * @return bool
+ */
+ public function supportsArguments()
+ {
+ return $this instanceof ObjectWithArguments;
+ }
+
+ /**
+ * Whether this object supports inheritance through the "imports" property
+ *
+ * @return bool
+ */
+ public function supportsImports()
+ {
+ return $this->supportsImports;
+ }
+
+ /**
+ * Whether this object allows controlled custom var access through fields
+ *
+ * @return bool
+ */
+ public function supportsFields()
+ {
+ return $this->supportsFields;
+ }
+
+ /**
+ * Whether this object can be rendered as 'apply Object'
+ *
+ * @return bool
+ */
+ public function supportsApplyRules()
+ {
+ return $this->supportsApplyRules;
+ }
+
+ /**
+ * Whether this object supports 'assign' properties
+ *
+ * @return bool
+ */
+ public function supportsAssignments()
+ {
+ return $this->isApplyRule();
+ }
+
+ /**
+ * Whether this object can be part of a 'set'
+ *
+ * @return bool
+ */
+ public function supportsSets()
+ {
+ return $this->supportsSets;
+ }
+
+ /**
+ * Whether this object supports template-based Choices
+ *
+ * @return bool
+ */
+ public function supportsChoices()
+ {
+ return $this->supportsChoices;
+ }
+
+ public function setAssignments($value)
+ {
+ return IcingaObjectLegacyAssignments::applyToObject($this, $value);
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ *
+ * @param Filter|string $filter
+ *
+ * @throws LogicException
+ *
+ * @return self
+ */
+ public function setAssign_filter($filter)
+ {
+ if (! $this->supportsAssignments() && $filter !== null) {
+ if ($this->hasProperty('object_type')) {
+ $type = $this->get('object_type');
+ } else {
+ $type = get_class($this);
+ }
+
+ if ($type === null) {
+ throw new LogicException(
+ 'Cannot set assign_filter unless object_type has been set'
+ );
+ }
+ throw new LogicException(sprintf(
+ 'I can only assign for applied objects or objects with native'
+ . ' support for assignments, got %s',
+ $type
+ ));
+ }
+
+ // @codingStandardsIgnoreEnd
+ if ($filter instanceof Filter) {
+ $filter = $filter->toQueryString();
+ }
+
+ return $this->reallySet('assign_filter', $filter);
+ }
+
+ /**
+ * It sometimes makes sense to defer lookups for related properties. This
+ * kind of lazy-loading allows us to for example set host = 'localhost' and
+ * render an object even when no such host exists. Think of the activity log,
+ * one might want to visualize a history host or service template even when
+ * the related command has been deleted in the meantime.
+ *
+ * @return self
+ */
+ public function resolveUnresolvedRelatedProperties()
+ {
+ foreach ($this->unresolvedRelatedProperties as $name => $p) {
+ $this->resolveUnresolvedRelatedProperty($name);
+ }
+
+ return $this;
+ }
+
+ public function getUnresolvedRelated($property)
+ {
+ if ($this->hasRelation($property)) {
+ $property .= '_id';
+ if (isset($this->unresolvedRelatedProperties[$property])) {
+ return $this->unresolvedRelatedProperties[$property];
+ }
+
+ return null;
+ }
+
+ throw new RuntimeException(sprintf(
+ '%s "%s" has no %s reference',
+ $this->getShortTableName(),
+ $this->getObjectName(),
+ $property
+ ));
+ }
+
+ /**
+ * @param $name
+ */
+ protected function resolveUnresolvedRelatedProperty($name)
+ {
+ $short = substr($name, 0, -3);
+ /** @var IcingaObject $class */
+ $class = $this->getRelationClass($short);
+ try {
+ $object = $class::load(
+ $this->unresolvedRelatedProperties[$name],
+ $this->connection
+ );
+ } catch (NotFoundError $e) {
+ // Hint: eventually a NotFoundError would be better
+ throw new RuntimeException(sprintf(
+ 'Unable to load object (%s: %s) referenced from %s "%s", %s',
+ $short,
+ $this->unresolvedRelatedProperties[$name],
+ $this->getShortTableName(),
+ $this->getObjectName(),
+ lcfirst($e->getMessage())
+ ), $e->getCode(), $e);
+ }
+
+ $id = $object->get('id');
+ // Happens when load() get's a branched object, created in the branch
+ if ($id !== null) {
+ $this->reallySet($name, $id);
+ unset($this->unresolvedRelatedProperties[$name]);
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasBeenModified()
+ {
+ if (parent::hasBeenModified()) {
+ return true;
+ }
+
+ if ($this->hasUnresolvedRelatedProperties()) {
+ $this->resolveUnresolvedRelatedProperties();
+
+ // Duplicates above code, but this makes it faster:
+ if (parent::hasBeenModified()) {
+ return true;
+ }
+ }
+
+ if ($this->supportsCustomVars() && $this->vars !== null && $this->vars()->hasBeenModified()) {
+ return true;
+ }
+
+ if ($this->supportsGroups() && $this->groups !== null && $this->groups()->hasBeenModified()) {
+ return true;
+ }
+
+ if ($this->supportsImports() && $this->imports !== null && $this->imports()->hasBeenModified()) {
+ return true;
+ }
+
+ if ($this->supportsRanges() && $this->ranges !== null && $this->ranges()->hasBeenModified()) {
+ return true;
+ }
+
+ if ($this instanceof ObjectWithArguments
+ && $this->gotArguments()
+ && $this->arguments()->hasBeenModified()
+ ) {
+ return true;
+ }
+
+ foreach ($this->loadedRelatedSets as $set) {
+ if ($set->hasBeenModified()) {
+ return true;
+ }
+ }
+
+ foreach ($this->loadedMultiRelations as $rel) {
+ if ($rel->hasBeenModified()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function hasUnresolvedRelatedProperties()
+ {
+ return ! empty($this->unresolvedRelatedProperties);
+ }
+
+ protected function hasUnresolvedRelatedProperty($name)
+ {
+ return array_key_exists($name, $this->unresolvedRelatedProperties);
+ }
+
+ /**
+ * @param $key
+ * @return mixed
+ */
+ protected function getRelationId($key)
+ {
+ if ($this->hasUnresolvedRelatedProperty($key)) {
+ $this->resolveUnresolvedRelatedProperty($key);
+ }
+
+ return parent::get($key);
+ }
+
+ /**
+ * @param $key
+ * @return string|null
+ */
+ protected function getRelatedProperty($key)
+ {
+ $idKey = $key . '_id';
+ if ($this->hasUnresolvedRelatedProperty($idKey)) {
+ return $this->unresolvedRelatedProperties[$idKey];
+ }
+
+ if ($id = $this->get($idKey)) {
+ /** @var IcingaObject $class */
+ $class = $this->getRelationClass($key);
+ try {
+ $object = $class::loadWithAutoIncId($id, $this->connection);
+ } catch (NotFoundError $e) {
+ throw new RuntimeException($e->getMessage(), 0, $e);
+ }
+
+ return $object->getObjectName();
+ }
+
+ return null;
+ }
+
+ /**
+ * @param string $key
+ * @return \Icinga\Module\Director\CustomVariable\CustomVariable|mixed|null
+ */
+ public function get($key)
+ {
+ if (substr($key, 0, 5) === 'vars.') {
+ $var = $this->vars()->get(substr($key, 5));
+ if ($var === null) {
+ return $var;
+ }
+
+ return $var->getValue();
+ }
+
+ // e.g. zone_id
+ if ($this->propertyIsRelation($key)) {
+ return $this->getRelationId($key);
+ }
+
+ // e.g. zone
+ if ($this->hasRelation($key)) {
+ return $this->getRelatedProperty($key);
+ }
+
+ if ($this->propertyIsRelatedSet($key)) {
+ return $this->getRelatedSet($key)->toPlainObject();
+ }
+
+ if ($this->propertyIsMultiRelation($key)) {
+ return $this->getMultiRelation($key)->listRelatedNames();
+ }
+
+ return parent::get($key);
+ }
+
+ public function setProperties($props)
+ {
+ if (is_array($props)) {
+ if (array_key_exists('object_type', $props) && key($props) !== 'object_type') {
+ $type = $props['object_type'];
+ unset($props['object_type']);
+ $props = ['object_type' => $type] + $props;
+ }
+ }
+ return parent::setProperties($props);
+ }
+
+ public function set($key, $value)
+ {
+ if ($key === 'vars') {
+ $value = (array) $value;
+ $unset = [];
+ foreach ($this->vars() as $k => $f) {
+ if (! array_key_exists($k, $value)) {
+ $unset[] = $k;
+ }
+ }
+ foreach ($unset as $k) {
+ unset($this->vars()->$k);
+ }
+ foreach ($value as $k => $v) {
+ $this->vars()->set($k, $v);
+ }
+ return $this;
+ }
+
+ if (substr($key, 0, 5) === 'vars.') {
+ //TODO: allow for deep keys
+ $this->vars()->set(substr($key, 5), $value);
+ return $this;
+ }
+
+ if ($this instanceof ObjectWithArguments
+ && substr($key, 0, 10) === 'arguments.') {
+ $this->arguments()->set(substr($key, 10), $value);
+ return $this;
+ }
+
+ if ($this->propertyIsBoolean($key)) {
+ return parent::set($key, DbDataFormatter::normalizeBoolean($value));
+ }
+
+ // e.g. zone_id
+ if ($this->propertyIsRelation($key)) {
+ return $this->setRelation($key, $value);
+ }
+
+ // e.g. zone
+ if ($this->hasRelation($key)) {
+ return $this->setUnresolvedRelation($key, $value);
+ }
+
+ if ($this->propertyIsMultiRelation($key)) {
+ $this->setMultiRelation($key, $value);
+ return $this;
+ }
+
+ if ($this->propertyIsRelatedSet($key)) {
+ $this->getRelatedSet($key)->set($value);
+ return $this;
+ }
+
+ if ($this->propertyIsInterval($key)) {
+ return parent::set($key, c::parseInterval($value));
+ }
+
+ return parent::set($key, $value);
+ }
+
+ private function setRelation($key, $value)
+ {
+ if ((int) $key !== (int) $this->$key) {
+ unset($this->unresolvedRelatedProperties[$key]);
+ }
+ return parent::set($key, $value);
+ }
+
+ private function setUnresolvedRelation($key, $value)
+ {
+ if ($value === null || strlen($value) === 0) {
+ unset($this->unresolvedRelatedProperties[$key . '_id']);
+ return parent::set($key . '_id', null);
+ }
+
+ $this->unresolvedRelatedProperties[$key . '_id'] = $value;
+ return $this;
+ }
+
+ protected function setRanges($ranges)
+ {
+ $this->ranges()->set((array) $ranges);
+ return $this;
+ }
+
+ protected function getRanges()
+ {
+ return $this->ranges()->getValues();
+ }
+
+ protected function setDisabled($disabled)
+ {
+ return $this->reallySet('disabled', DbDataFormatter::normalizeBoolean($disabled));
+ }
+
+ public function isDisabled()
+ {
+ return $this->get('disabled') === 'y';
+ }
+
+ public function markForRemoval($remove = true)
+ {
+ $this->shouldBeRemoved = $remove;
+ return $this;
+ }
+
+ public function shouldBeRemoved()
+ {
+ return $this->shouldBeRemoved;
+ }
+
+ public function shouldBeRenamed()
+ {
+ return $this->hasBeenLoadedFromDb()
+ && $this->getOriginalProperty('object_name') !== $this->getObjectName();
+ }
+
+ /**
+ * @return IcingaObjectGroups
+ */
+ public function groups()
+ {
+ $this->assertGroupsSupport();
+ if ($this->groups === null) {
+ if ($this->hasBeenLoadedFromDb() && $this->get('id')) {
+ $this->groups = IcingaObjectGroups::loadForStoredObject($this);
+ } else {
+ $this->groups = new IcingaObjectGroups($this);
+ }
+ }
+
+ return $this->groups;
+ }
+
+ public function hasModifiedGroups()
+ {
+ $this->assertGroupsSupport();
+ if ($this->groups === null) {
+ return false;
+ }
+
+ return $this->groups->hasBeenModified();
+ }
+
+ public function getAppliedGroups()
+ {
+ $this->assertGroupsSupport();
+ if (! $this instanceof IcingaHost) {
+ throw new RuntimeException('getAppliedGroups is only available for hosts currently!');
+ }
+ if (! $this->hasBeenLoadedFromDb()) {
+ // There are no stored related/resolved groups. We'll also not resolve
+ // them here on demand.
+ return [];
+ }
+ $id = $this->get('id');
+ if ($id === null) {
+ // Do not fail for branches. Should be handled otherwise
+ // TODO: throw an Exception, once we are able to deal with this
+ return [];
+ }
+
+ $type = strtolower($this->getType());
+ $query = $this->db->select()->from(
+ ['gr' => "icinga_${type}group_${type}_resolved"],
+ ['g.object_name']
+ )->join(
+ ['g' => "icinga_${type}group"],
+ "g.id = gr.${type}group_id",
+ []
+ )->joinLeft(
+ ['go' => "icinga_${type}group_${type}"],
+ "go.${type}group_id = gr.${type}group_id AND go.${type}_id = " . (int) $id,
+ []
+ )->where(
+ "gr.${type}_id = ?",
+ (int) $id
+ )->where("go.${type}_id IS NULL")->order('g.object_name');
+
+ return $this->db->fetchCol($query);
+ }
+
+ /**
+ * @return IcingaTimePeriodRanges
+ */
+ public function ranges()
+ {
+ $this->assertRangesSupport();
+ if ($this->ranges === null) {
+ /** @var IcingaTimePeriodRanges $class */
+ $class = $this->getRangeClass();
+ if ($this->hasBeenLoadedFromDb()) {
+ $this->ranges = $class::loadForStoredObject($this);
+ } else {
+ $this->ranges = new $class($this);
+ }
+ }
+
+ return $this->ranges;
+ }
+
+ protected function getRangeClass()
+ {
+ if ($this->rangeClass === null) {
+ $this->rangeClass = get_class($this) . 'Ranges';
+ }
+
+ return $this->rangeClass;
+ }
+
+ /**
+ * @return IcingaObjectImports
+ */
+ public function imports()
+ {
+ $this->assertImportsSupport();
+ if ($this->imports === null) {
+ // can not use hasBeenLoadedFromDb() when in onStore()
+ if ($this->getProperty('id') !== null) {
+ $this->imports = IcingaObjectImports::loadForStoredObject($this);
+ } else {
+ $this->imports = new IcingaObjectImports($this);
+ }
+ }
+
+ return $this->imports;
+ }
+
+ public function gotImports()
+ {
+ return $this->imports !== null;
+ }
+
+ public function setImports($imports)
+ {
+ if (! is_array($imports) && $imports !== null) {
+ $imports = [$imports];
+ }
+
+ try {
+ $this->imports()->set($imports);
+ } catch (NestingError $e) {
+ $this->imports = new IcingaObjectImports($this);
+ // Force modification, otherwise it won't be stored when empty
+ $this->imports->setModified()->set($imports);
+ }
+
+ if ($this->imports()->hasBeenModified()) {
+ $this->invalidateResolveCache();
+ }
+ }
+
+ public function getImports()
+ {
+ return $this->listImportNames();
+ }
+
+ /**
+ * @deprecated This should no longer be in use
+ * @return IcingaTemplateResolver
+ */
+ public function templateResolver()
+ {
+ if ($this->templateResolver === null) {
+ $this->templateResolver = new IcingaTemplateResolver($this);
+ }
+
+ return $this->templateResolver;
+ }
+
+ public function getResolvedProperty($key, $default = null)
+ {
+ if (array_key_exists($key, $this->unresolvedRelatedProperties)) {
+ $this->resolveUnresolvedRelatedProperty($key);
+ $this->invalidateResolveCache();
+ }
+
+ $properties = $this->getResolvedProperties();
+ if (property_exists($properties, $key)) {
+ return $properties->$key;
+ }
+
+ return $default;
+ }
+
+ public function getInheritedProperty($key, $default = null)
+ {
+ if (array_key_exists($key, $this->unresolvedRelatedProperties)) {
+ $this->resolveUnresolvedRelatedProperty($key);
+ $this->invalidateResolveCache();
+ }
+
+ $properties = $this->getInheritedProperties();
+ if (property_exists($properties, $key)) {
+ return $properties->$key;
+ }
+
+ return $default;
+ }
+
+ public function getInheritedVar($varname)
+ {
+ try {
+ $vars = $this->getInheritedVars();
+ } catch (NestingError $e) {
+ return null;
+ }
+
+ if (property_exists($vars, $varname)) {
+ return $vars->$varname;
+ }
+
+ return null;
+ }
+
+ public function getResolvedVar($varName)
+ {
+ try {
+ $vars = $this->getResolvedVars();
+ } catch (NestingError $e) {
+ return null;
+ }
+
+ if (property_exists($vars, $varName)) {
+ return $vars->$varName;
+ }
+
+ return null;
+ }
+
+ public function getOriginForVar($varName)
+ {
+ try {
+ $origins = $this->getOriginsVars();
+ } catch (NestingError $e) {
+ return null;
+ }
+
+ if (property_exists($origins, $varName)) {
+ return $origins->$varName;
+ }
+
+ return null;
+ }
+
+ public function getResolvedProperties()
+ {
+ return $this->getResolved('Properties');
+ }
+
+ public function getInheritedProperties()
+ {
+ return $this->getInherited('Properties');
+ }
+
+ public function getOriginsProperties()
+ {
+ return $this->getOrigins('Properties');
+ }
+
+ public function resolveProperties()
+ {
+ return $this->resolve('Properties');
+ }
+
+ public function getResolvedVars()
+ {
+ return $this->getResolved('Vars');
+ }
+
+ public function getInheritedVars()
+ {
+ return $this->getInherited('Vars');
+ }
+
+ public function resolveVars()
+ {
+ return $this->resolve('Vars');
+ }
+
+ public function getOriginsVars()
+ {
+ return $this->getOrigins('Vars');
+ }
+
+ public function getVars()
+ {
+ $vars = [];
+ foreach ($this->vars() as $key => $var) {
+ if ($var->hasBeenDeleted()) {
+ continue;
+ }
+
+ $vars[$key] = $var->getValue();
+ }
+ ksort($vars);
+
+ return (object) $vars;
+ }
+
+ /**
+ * This is mostly for magic getters
+ * @return array
+ */
+ public function getGroups()
+ {
+ return $this->groups()->listGroupNames();
+ }
+
+ /**
+ * @return array
+ * @throws NotFoundError
+ */
+ public function listInheritedGroupNames()
+ {
+ $parents = $this->imports()->getObjects();
+ /** @var IcingaObject $parent */
+ foreach (array_reverse($parents) as $parent) {
+ $inherited = $parent->getGroups();
+ if (! empty($inherited)) {
+ return $inherited;
+ }
+ }
+
+ return [];
+ }
+
+ public function setGroups($groups)
+ {
+ $this->groups()->set($groups);
+ return $this;
+ }
+
+ /**
+ * @return array
+ * @throws NotFoundError
+ */
+ public function listResolvedGroupNames()
+ {
+ $groups = $this->groups()->listGroupNames();
+ if (empty($groups)) {
+ return $this->listInheritedGroupNames();
+ }
+
+ return $groups;
+ }
+
+ /**
+ * @param $group
+ * @return bool
+ * @throws NotFoundError
+ */
+ public function hasGroup($group)
+ {
+ if ($group instanceof static) {
+ $group = $group->getObjectName();
+ }
+
+ return in_array($group, $this->listResolvedGroupNames());
+ }
+
+ protected function getResolved($what)
+ {
+ $func = 'resolve' . $what;
+ $res = $this->$func();
+ return $res['_MERGED_'];
+ }
+
+ protected function getInherited($what)
+ {
+ $func = 'resolve' . $what;
+ $res = $this->$func();
+ return $res['_INHERITED_'];
+ }
+
+ protected function getOrigins($what)
+ {
+ $func = 'resolve' . $what;
+ $res = $this->$func();
+ return $res['_ORIGINS_'];
+ }
+
+ protected function hasResolveCached($what)
+ {
+ return array_key_exists($what, $this->resolveCache);
+ }
+
+ protected function & getResolveCached($what)
+ {
+ return $this->resolveCache[$what];
+ }
+
+ protected function storeResolvedCache($what, $vals)
+ {
+ $this->resolveCache[$what] = $vals;
+ }
+
+ public function invalidateResolveCache()
+ {
+ $this->resolveCache = [];
+ return $this;
+ }
+
+ public function countDirectDescendants()
+ {
+ $db = $this->getDb();
+ $table = $this->getTableName();
+ $type = $this->getShortTableName();
+
+ $query = $db->select()->from(
+ ['oi' => $table . '_inheritance'],
+ ['cnt' => 'COUNT(*)']
+ )->where('oi.parent_' . $type . '_id = ?', (int) $this->get('id'));
+
+ return $db->fetchOne($query);
+ }
+
+ protected function triggerLoopDetection()
+ {
+ // $this->templateResolver()->listResolvedParentIds();
+ }
+
+ public function getSingleResolvedProperty($key, $default = null)
+ {
+ if (array_key_exists($key, $this->unresolvedRelatedProperties)) {
+ $this->resolveUnresolvedRelatedProperty($key);
+ $this->invalidateResolveCache();
+ }
+
+ if ($my = $this->get($key)) {
+ if ($my !== null) {
+ return $my;
+ }
+ }
+
+ /** @var IcingaObject[] $imports */
+ try {
+ $imports = array_reverse($this->imports()->getObjects());
+ } catch (NotFoundError $e) {
+ throw new RuntimeException($e->getMessage(), 0, $e);
+ }
+
+ // Eventually trigger loop detection
+ $this->listAncestorIds();
+
+ foreach ($imports as $object) {
+ $v = $object->getSingleResolvedProperty($key);
+ if (null !== $v) {
+ return $v;
+ }
+ }
+
+ return $default;
+ }
+
+ protected function resolve($what)
+ {
+ if ($this->hasResolveCached($what)) {
+ return $this->getResolveCached($what);
+ }
+
+ // Force exception
+ if ($this->hasBeenLoadedFromDb()) {
+ $this->triggerLoopDetection();
+ }
+
+ $vals = [];
+ $vals['_MERGED_'] = (object) [];
+ $vals['_INHERITED_'] = (object) [];
+ $vals['_ORIGINS_'] = (object) [];
+ // $objects = $this->imports()->getObjects();
+ $objects = IcingaTemplateRepository::instanceByObject($this)
+ ->getTemplatesIndexedByNameFor($this, true);
+
+ $get = 'get' . $what;
+ $getInherited = 'getInherited' . $what;
+ $getOrigins = 'getOrigins' . $what;
+
+ $blacklist = ['id', 'uuid', 'object_type', 'object_name', 'disabled'];
+ foreach ($objects as $name => $object) {
+ $origins = $object->$getOrigins();
+
+ foreach ($object->$getInherited() as $key => $value) {
+ if (in_array($key, $blacklist)) {
+ continue;
+ }
+
+ if (! property_exists($origins, $key)) {
+ // TODO: Introduced with group membership resolver or
+ // choices - this should not be required. Check this!
+ continue;
+ }
+
+ // $vals[$name]->$key = $value;
+ $vals['_MERGED_']->$key = $value;
+ $vals['_INHERITED_']->$key = $value;
+ $vals['_ORIGINS_']->$key = $origins->$key;
+ }
+
+ foreach ($object->$get() as $key => $value) {
+ // TODO: skip if default value?
+ if ($value === null) {
+ continue;
+ }
+ if (in_array($key, $blacklist)) {
+ continue;
+ }
+ $vals['_MERGED_']->$key = $value;
+ $vals['_INHERITED_']->$key = $value;
+ $vals['_ORIGINS_']->$key = $name;
+ }
+ }
+
+ foreach ($this->$get() as $key => $value) {
+ if ($value === null) {
+ continue;
+ }
+
+ $vals['_MERGED_']->$key = $value;
+ }
+
+ $this->storeResolvedCache($what, $vals);
+
+ return $vals;
+ }
+
+ public function matches(Filter $filter)
+ {
+ // TODO: speed up by passing only desired properties (filter columns) to
+ // toPlainObject method
+ /** @var FilterChain|FilterExpression $filter */
+ return $filter->matches($this->toPlainObject());
+ }
+
+ protected function assertCustomVarsSupport()
+ {
+ if (! $this->supportsCustomVars()) {
+ throw new LogicException(sprintf(
+ 'Objects of type "%s" have no custom vars',
+ $this->getType()
+ ));
+ }
+
+ return $this;
+ }
+
+ protected function assertGroupsSupport()
+ {
+ if (! $this->supportsGroups()) {
+ throw new LogicException(sprintf(
+ 'Objects of type "%s" have no groups',
+ $this->getType()
+ ));
+ }
+
+ return $this;
+ }
+
+ protected function assertRangesSupport()
+ {
+ if (! $this->supportsRanges()) {
+ throw new LogicException(sprintf(
+ 'Objects of type "%s" have no ranges',
+ $this->getType()
+ ));
+ }
+
+ return $this;
+ }
+
+ protected function assertImportsSupport()
+ {
+ if (! $this->supportsImports()) {
+ throw new LogicException(sprintf(
+ 'Objects of type "%s" have no imports',
+ $this->getType()
+ ));
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return CustomVariables
+ */
+ public function vars()
+ {
+ $this->assertCustomVarsSupport();
+ if ($this->vars === null) {
+ if ($this->hasBeenLoadedFromDb()) {
+ if (PrefetchCache::shouldBeUsed()) {
+ $this->vars = PrefetchCache::instance()->vars($this);
+ } else {
+ if ($this->get('id')) {
+ $this->vars = CustomVariables::loadForStoredObject($this);
+ } else {
+ $this->vars = new CustomVariables();
+ }
+ }
+
+ if ($this->getShortTableName() === 'host') {
+ $this->vars->setOverrideKeyName(
+ $this->getConnection()->settings()->override_services_varname
+ );
+ }
+ } else {
+ $this->vars = new CustomVariables();
+ }
+ }
+
+ return $this->vars;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasInitializedVars()
+ {
+ $this->assertCustomVarsSupport();
+
+ return $this->vars !== null;
+ }
+
+ public function getVarsTableName()
+ {
+ return $this->getTableName() . '_var';
+ }
+
+ public function getShortTableName()
+ {
+ // strlen('icinga_') = 7
+ return substr($this->getTableName(), 7);
+ }
+
+ public function getVarsIdColumn()
+ {
+ return $this->getShortTableName() . '_id';
+ }
+
+ public function hasProperty($key)
+ {
+ if ($this->propertyIsRelatedSet($key)) {
+ return true;
+ }
+
+ if ($this->propertyIsMultiRelation($key)) {
+ return true;
+ }
+
+ return parent::hasProperty($key);
+ }
+
+ public function isObject()
+ {
+ return $this->hasProperty('object_type')
+ && $this->get('object_type') === 'object';
+ }
+
+ public function isTemplate()
+ {
+ return $this->hasProperty('object_type')
+ && $this->get('object_type') === 'template';
+ }
+
+ public function isExternal()
+ {
+ return $this->hasProperty('object_type')
+ && $this->get('object_type') === 'external_object';
+ }
+
+ public function isApplyRule()
+ {
+ return $this->hasProperty('object_type')
+ && $this->get('object_type') === 'apply';
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ if ($this instanceof ObjectWithArguments && $this->gotArguments()) {
+ $this->arguments()->setBeingLoadedFromDb();
+ }
+ if ($this->supportsImports() && $this->gotImports()) {
+ $this->imports()->setBeingLoadedFromDb();
+ }
+ if ($this->supportsCustomVars() && $this->vars !== null) {
+ $this->vars()->setBeingLoadedFromDb();
+ }
+ if ($this->supportsGroups() && $this->groups !== null) {
+ $this->groups()->setBeingLoadedFromDb();
+ }
+ if ($this->supportsRanges() && $this->ranges !== null) {
+ $this->ranges()->setBeingLoadedFromDb();
+ }
+
+ foreach ($this->loadedRelatedSets as $set) {
+ $set->setBeingLoadedFromDb();
+ }
+
+ foreach ($this->loadedMultiRelations as $multiRelation) {
+ $multiRelation->setBeingLoadedFromDb();
+ }
+ // This might trigger DB requests and 404's. We might want to defer this, but a call to
+ // hasBeenModified triggers anyway:
+ $this->resolveUnresolvedRelatedProperties();
+
+ parent::setBeingLoadedFromDb();
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function storeRelatedObjects()
+ {
+ $this
+ ->storeCustomVars()
+ ->storeGroups()
+ ->storeMultiRelations()
+ ->storeImports()
+ ->storeRanges()
+ ->storeRelatedSets()
+ ->storeArguments();
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ protected function beforeStore()
+ {
+ $this->resolveUnresolvedRelatedProperties();
+ if ($this->gotImports()) {
+ $this->imports()->getObjects();
+ }
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function onInsert()
+ {
+ DirectorActivityLog::logCreation($this, $this->connection);
+ $this->storeRelatedObjects();
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function onUpdate()
+ {
+ DirectorActivityLog::logModification($this, $this->connection);
+ $this->storeRelatedObjects();
+ }
+
+ public function onStore()
+ {
+ $this->notifyResolvers();
+ }
+
+ /**
+ * @return self
+ */
+ protected function storeCustomVars()
+ {
+ if ($this->supportsCustomVars()) {
+ $this->vars !== null && $this->vars()->storeToDb($this);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function storeGroups()
+ {
+ if ($this->supportsGroups()) {
+ $this->groups !== null && $this->groups()->store();
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function storeMultiRelations()
+ {
+ foreach ($this->loadedMultiRelations as $rel) {
+ $rel->store();
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function storeRanges()
+ {
+ if ($this->supportsRanges()) {
+ $this->ranges !== null && $this->ranges()->store();
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function storeArguments()
+ {
+ if ($this instanceof ObjectWithArguments) {
+ $this->gotArguments() && $this->arguments()->store();
+ }
+
+ return $this;
+ }
+
+ protected function notifyResolvers()
+ {
+ }
+
+ /**
+ * @return $this
+ */
+ protected function storeRelatedSets()
+ {
+ foreach ($this->loadedRelatedSets as $set) {
+ if ($set->hasBeenModified()) {
+ $set->store();
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws NotFoundError
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function storeImports()
+ {
+ if ($this->supportsImports()) {
+ $this->imports !== null && $this->imports()->store();
+ }
+
+ return $this;
+ }
+
+ public function beforeDelete()
+ {
+ $this->cachedPlainUnmodified = $this->getPlainUnmodifiedObject();
+ }
+
+ public function getCachedUnmodifiedObject()
+ {
+ return $this->cachedPlainUnmodified;
+ }
+
+ public function onDelete()
+ {
+ DirectorActivityLog::logRemoval($this, $this->connection);
+ }
+
+ public function toSingleIcingaConfig()
+ {
+ $config = new IcingaConfig($this->connection);
+ $object = $this;
+ if ($object->isExternal()) {
+ $object->set('object_type', 'object');
+ $wasExternal = true;
+ } else {
+ $wasExternal = false;
+ }
+
+ try {
+ $object->renderToConfig($config);
+ } catch (Exception $e) {
+ $message = $e->getMessage();
+ $showTrace = false;
+ if ($showTrace) {
+ $message .= "\n" . $e->getTraceAsString();
+ }
+ $config->configFile(
+ 'failed-to-render'
+ )->prepend(
+ "/** Failed to render this object **/\n"
+ . '/* ' . $message . ' */'
+ );
+ }
+ if ($wasExternal) {
+ $object->set('object_type', 'external_object');
+ }
+
+ return $config;
+ }
+
+ public function isSupportedInLegacy()
+ {
+ return $this->supportedInLegacy;
+ }
+
+ public function renderToLegacyConfig(IcingaConfig $config)
+ {
+ if ($this->isExternal()) {
+ return;
+ }
+
+ if (! $this->isSupportedInLegacy()) {
+ $config->configFile(
+ 'director/ignored-objects',
+ '.cfg'
+ )->prepend(
+ sprintf(
+ "# Not supported for legacy config: %s object_name=%s\n",
+ get_class($this),
+ $this->getObjectName()
+ )
+ );
+ return;
+ }
+
+ $filename = $this->getRenderingFilename();
+
+ $deploymentMode = $config->getDeploymentMode();
+ if ($deploymentMode === 'active-passive') {
+ if ($this->getSingleResolvedProperty('zone_id')
+ && array_key_exists('enable_active_checks', $this->defaultProperties)
+ ) {
+ $passive = clone($this);
+ $passive->set('enable_active_checks', false);
+
+ $config->configFile(
+ 'director/master/' . $filename,
+ '.cfg'
+ )->addLegacyObject($passive);
+ }
+ } elseif ($deploymentMode === 'masterless') {
+ // no additional config
+ } else {
+ throw new LogicException(sprintf(
+ 'Unsupported deployment mode: %s',
+ $deploymentMode
+ ));
+ }
+
+ $config->configFile(
+ 'director/' . $this->getRenderingZone($config) . '/' . $filename,
+ '.cfg'
+ )->addLegacyObject($this);
+ }
+
+ public function renderToConfig(IcingaConfig $config)
+ {
+ if ($config->isLegacy()) {
+ $this->renderToLegacyConfig($config);
+ return;
+ }
+
+ if ($this->isExternal()) {
+ return;
+ }
+
+ $config->configFile(
+ 'zones.d/' . $this->getRenderingZone($config) . '/' . $this->getRenderingFilename()
+ )->addObject($this);
+ }
+
+ public function getRenderingFilename()
+ {
+ $type = $this->getShortTableName();
+
+ if ($this->isTemplate()) {
+ $filename = strtolower($type) . '_templates';
+ } elseif ($this->isApplyRule()) {
+ $filename = strtolower($type) . '_apply';
+ } else {
+ $filename = strtolower($type) . 's';
+ }
+
+ return $filename;
+ }
+
+ /**
+ * @param $zoneId
+ * @param IcingaConfig|null $config
+ * @return string
+ * @throws NotFoundError
+ */
+ protected function getNameForZoneId($zoneId, IcingaConfig $config = null)
+ {
+ // TODO: this is still ugly.
+ if ($config === null) {
+ return IcingaZone::loadWithAutoIncId(
+ $zoneId,
+ $this->getConnection()
+ )->getObjectName();
+ }
+
+ // Config has a lookup cache, is faster:
+ return $config->getZoneName($zoneId);
+ }
+
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ if ($this->hasUnresolvedRelatedProperty('zone_id')) {
+ return $this->get('zone');
+ }
+
+ if ($this->hasProperty('zone_id')) {
+ try {
+ if (! $this->supportsImports()) {
+ if ($zoneId = $this->get('zone_id')) {
+ return $this->getNameForZoneId($zoneId, $config);
+ }
+ }
+
+ if ($zoneId = $this->getSingleResolvedProperty('zone_id')) {
+ return $this->getNameForZoneId($zoneId, $config);
+ }
+ } catch (NestingError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ return self::RESOLVE_ERROR;
+ }
+ }
+
+ return $this->getDefaultZone($config);
+ }
+
+ protected function getDefaultZone(IcingaConfig $config = null)
+ {
+ if ($this->prefersGlobalZone()) {
+ return $this->connection->getDefaultGlobalZoneName();
+ }
+
+ return $this->connection->getMasterZoneName();
+ }
+
+ protected function prefersGlobalZone()
+ {
+ return $this->isTemplate() || $this->isApplyRule();
+ }
+
+ protected function renderImports()
+ {
+ if (! $this->supportsImports()) {
+ return '';
+ }
+
+ $ret = '';
+ foreach ($this->getImports() as $name) {
+ $ret .= ' import ' . c::renderString($name) . "\n";
+ }
+
+ if ($ret !== '') {
+ $ret .= "\n";
+ }
+
+ return $ret;
+ }
+
+ protected function renderLegacyImports()
+ {
+ if ($this->supportsImports()) {
+ return $this->imports()->toLegacyConfigString();
+ }
+
+ return '';
+ }
+
+ protected function renderLegacyRelationProperty($propertyName, $id, $renderKey = null)
+ {
+ return $this->renderLegacyObjectProperty(
+ $renderKey ?: $propertyName,
+ c1::renderString($this->getRelatedObjectName($propertyName, $id))
+ );
+ }
+
+ // Disabled is a virtual property
+ protected function renderDisabled()
+ {
+ return '';
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ protected function renderLegacyHost_id($value)
+ {
+ if (is_array($value)) {
+ return c1::renderKeyValue('host_name', c1::renderArray($value));
+ }
+
+ return $this->renderLegacyRelationProperty(
+ 'host',
+ $this->get('host_id'),
+ 'host_name'
+ );
+ }
+
+ /**
+ * Display Name only exists for host/service in Icinga 1
+ *
+ * Render it as alias for everything by default.
+ *
+ * Alias does not exist in Icinga 2 currently!
+ *
+ * @return string
+ */
+ protected function renderLegacyDisplay_Name()
+ {
+ return c1::renderKeyValue('alias', $this->display_name);
+ }
+
+ protected function renderLegacyTimeout()
+ {
+ return '';
+ }
+
+ protected function renderLegacyEnable_active_checks()
+ {
+ return $this->renderLegacyBooleanProperty(
+ 'enable_active_checks',
+ 'active_checks_enabled'
+ );
+ }
+
+ protected function renderLegacyEnable_passive_checks()
+ {
+ return $this->renderLegacyBooleanProperty(
+ 'enable_passive_checks',
+ 'passive_checks_enabled'
+ );
+ }
+
+ protected function renderLegacyEnable_event_handler()
+ {
+ return $this->renderLegacyBooleanProperty(
+ 'enable_active_checks',
+ 'event_handler_enabled'
+ );
+ }
+
+ protected function renderLegacyEnable_notifications()
+ {
+ return $this->renderLegacyBooleanProperty(
+ 'enable_notifications',
+ 'notifications_enabled'
+ );
+ }
+
+ protected function renderLegacyEnable_perfdata()
+ {
+ return $this->renderLegacyBooleanProperty(
+ 'enable_perfdata',
+ 'process_perf_data'
+ );
+ }
+
+ protected function renderLegacyVolatile()
+ {
+ // @codingStandardsIgnoreEnd
+ return $this->renderLegacyBooleanProperty(
+ 'volatile',
+ 'is_volatile'
+ );
+ }
+
+ protected function renderLegacyBooleanProperty($property, $legacyKey)
+ {
+ return c1::renderKeyValue(
+ $legacyKey,
+ c1::renderBoolean($this->get($property))
+ );
+ }
+
+ protected function renderProperties()
+ {
+ $out = '';
+ $blacklist = array_merge(
+ $this->propertiesNotForRendering,
+ $this->prioritizedProperties
+ );
+
+ foreach ($this->properties as $key => $value) {
+ if (in_array($key, $blacklist)) {
+ continue;
+ }
+
+ $out .= $this->renderObjectProperty($key, $value);
+ }
+
+ return $out;
+ }
+
+ protected function renderLegacyProperties()
+ {
+ $out = '';
+ $blacklist = array_merge(
+ $this->propertiesNotForRendering,
+ [] /* $this->prioritizedProperties */
+ );
+
+ foreach ($this->properties as $key => $value) {
+ if (in_array($key, $blacklist)) {
+ continue;
+ }
+
+ $out .= $this->renderLegacyObjectProperty($key, $value);
+ }
+
+ return $out;
+ }
+
+ protected function renderPrioritizedProperties()
+ {
+ $out = '';
+
+ foreach ($this->prioritizedProperties as $key) {
+ $out .= $this->renderObjectProperty($key, $this->properties[$key]);
+ }
+
+ return $out;
+ }
+
+ protected function renderObjectProperty($key, $value)
+ {
+ if (substr($key, -3) === '_id') {
+ $short = substr($key, 0, -3);
+ if ($this->hasUnresolvedRelatedProperty($key)) {
+ return c::renderKeyValue(
+ $short, // NOT
+ c::renderString($this->$short)
+ );
+ }
+ }
+
+ if ($value === null) {
+ return '';
+ }
+
+ $method = 'render' . ucfirst($key);
+ if (method_exists($this, $method)) {
+ return $this->$method($value);
+ }
+
+ if ($this->propertyIsBoolean($key)) {
+ if ($value === $this->defaultProperties[$key]) {
+ return '';
+ }
+
+ return c::renderKeyValue(
+ $this->booleans[$key],
+ c::renderBoolean($value)
+ );
+ }
+
+ if ($this->propertyIsInterval($key)) {
+ return c::renderKeyValue(
+ $this->intervalProperties[$key],
+ c::renderInterval($value)
+ );
+ }
+
+ if (substr($key, -3) === '_id'
+ && $this->hasRelation($relKey = substr($key, 0, -3))
+ ) {
+ return $this->renderRelationProperty($relKey, $value);
+ }
+
+ return c::renderKeyValue(
+ $key,
+ $this->isApplyRule() ?
+ c::renderStringWithVariables($value) :
+ c::renderString($value)
+ );
+ }
+
+ protected function renderLegacyObjectProperty($key, $value)
+ {
+ if (substr($key, -3) === '_id') {
+ $short = substr($key, 0, -3);
+ if ($this->hasUnresolvedRelatedProperty($key)) {
+ return c1::renderKeyValue(
+ $short, // NOT
+ c1::renderString($this->$short)
+ );
+ }
+ }
+
+ if ($value === null) {
+ return '';
+ }
+
+ $method = 'renderLegacy' . ucfirst($key);
+ if (method_exists($this, $method)) {
+ return $this->$method($value);
+ }
+
+ $method = 'render' . ucfirst($key);
+ if (method_exists($this, $method)) {
+ return $this->$method($value);
+ }
+
+ if ($this->propertyIsBoolean($key)) {
+ if ($value === $this->defaultProperties[$key]) {
+ return '';
+ }
+
+ return c1::renderKeyValue(
+ $this->booleans[$key],
+ c1::renderBoolean($value)
+ );
+ }
+
+ if ($this->propertyIsInterval($key)) {
+ return c1::renderKeyValue(
+ $this->intervalProperties[$key],
+ c1::renderInterval($value)
+ );
+ }
+
+ if (substr($key, -3) === '_id'
+ && $this->hasRelation($relKey = substr($key, 0, -3))
+ ) {
+ return $this->renderLegacyRelationProperty($relKey, $value);
+ }
+
+ return c1::renderKeyValue($key, c1::renderString($value));
+ }
+
+ protected function renderBooleanProperty($key)
+ {
+ return c::renderKeyValue($key, c::renderBoolean($this->get($key)));
+ }
+
+ protected function renderPropertyAsSeconds($key)
+ {
+ return c::renderKeyValue($key, c::renderInterval($this->get($key)));
+ }
+
+ protected function renderSuffix()
+ {
+ return "}\n\n";
+ }
+
+ protected function renderLegacySuffix()
+ {
+ return "}\n\n";
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderCustomVars()
+ {
+ if ($this->supportsCustomVars()) {
+ return $this->vars()->toConfigString($this->isApplyRule());
+ }
+
+ return '';
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderLegacyCustomVars()
+ {
+ if ($this->supportsCustomVars()) {
+ return $this->vars()->toLegacyConfigString();
+ }
+
+ return '';
+ }
+
+ public function renderUuid()
+ {
+ return '';
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderGroups()
+ {
+ if ($this->supportsGroups()) {
+ return $this->groups()->toConfigString();
+ }
+
+ return '';
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderLegacyGroups()
+ {
+ if ($this->supportsGroups() && $this->hasBeenLoadedFromDb()) {
+ $applied = [];
+ if ($this instanceof IcingaHost) {
+ $applied = $this->getAppliedGroups();
+ }
+ return $this->groups()->toLegacyConfigString($applied);
+ }
+
+ return '';
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderMultiRelations()
+ {
+ $out = '';
+ foreach ($this->loadAllMultiRelations() as $rel) {
+ $out .= $rel->toConfigString();
+ }
+
+ return $out;
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderLegacyMultiRelations()
+ {
+ $out = '';
+ foreach ($this->loadAllMultiRelations() as $rel) {
+ $out .= $rel->toLegacyConfigString();
+ }
+
+ return $out;
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderRanges()
+ {
+ if ($this->supportsRanges()) {
+ return $this->ranges()->toConfigString();
+ }
+
+ return '';
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderLegacyRanges()
+ {
+ if ($this->supportsRanges()) {
+ return $this->ranges()->toLegacyConfigString();
+ }
+
+ return '';
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderArguments()
+ {
+ return '';
+ }
+
+ protected function renderRelatedSets()
+ {
+ $config = '';
+ foreach ($this->relatedSets as $property => $class) {
+ $config .= $this->getRelatedSet($property)->renderAs($property);
+ }
+ return $config;
+ }
+
+ protected function renderRelationProperty($propertyName, $id, $renderKey = null)
+ {
+ return c::renderKeyValue(
+ $renderKey ?: $propertyName,
+ c::renderString($this->getRelatedObjectName($propertyName, $id))
+ );
+ }
+
+ protected function renderCommandProperty($commandId, $propertyName = 'check_command')
+ {
+ return c::renderKeyValue(
+ $propertyName,
+ c::renderString($this->connection->getCommandName($commandId))
+ );
+ }
+
+ /**
+ * @param $value
+ * @return string
+ * @codingStandardsIgnoreStart
+ */
+ protected function renderLegacyCheck_command($value)
+ {
+ // @codingStandardsIgnoreEnd
+ $args = [];
+ foreach ($this->vars() as $k => $v) {
+ if (substr($k, 0, 3) === 'ARG') {
+ $args[] = $v->getValue();
+ }
+ }
+ array_unshift($args, $value);
+
+ return c1::renderKeyValue('check_command', implode('!', $args));
+ }
+
+ /**
+ * @param $value
+ * @return string
+ * @codingStandardsIgnoreStart
+ */
+ protected function renderLegacyEvent_command($value)
+ {
+ // @codingStandardsIgnoreEnd
+ return c1::renderKeyValue('event_handler', $value);
+ }
+
+ /**
+ * We do not render zone properties, objects are stored to zone dirs
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ protected function renderZone_id()
+ {
+ // @codingStandardsIgnoreEnd
+ return '';
+ }
+
+ protected function renderCustomExtensions()
+ {
+ return '';
+ }
+
+ protected function renderLegacyCustomExtensions()
+ {
+ $str = '';
+
+ // Set notification settings for the object to suppress warnings
+ if (array_key_exists('enable_notifications', $this->defaultProperties)
+ && $this->isTemplate()
+ ) {
+ $str .= c1::renderKeyValue('notification_period', 'notification_none');
+ $str .= c1::renderKeyValue('notification_interval', '0');
+ $str .= c1::renderKeyValue('contact_groups', 'icingaadmins');
+ }
+
+ // force rendering of check_command when ARG1 is set
+ if ($this->supportsCustomVars() && array_key_exists('check_command_id', $this->defaultProperties)) {
+ if ($this->get('check_command') === null
+ && $this->vars()->get('ARG1') !== null
+ ) {
+ $command = $this->getResolvedRelated('check_command');
+ $str .= $this->renderLegacyCheck_command($command->getObjectName());
+ }
+ }
+
+ return $str;
+ }
+
+ protected function renderObjectHeader()
+ {
+ return sprintf(
+ "%s %s %s {\n",
+ $this->getObjectTypeName(),
+ $this->getType(),
+ c::renderString($this->getObjectName())
+ );
+ }
+
+ public function getLegacyObjectType()
+ {
+ return strtolower($this->getType());
+ }
+
+ protected function renderLegacyObjectHeader()
+ {
+ $type = $this->getLegacyObjectType();
+
+ if ($this->isTemplate()) {
+ $name = c1::renderKeyValue(
+ $this->getLegacyObjectKeyName(),
+ c1::renderString($this->getObjectName())
+ );
+ } else {
+ $name = c1::renderKeyValue(
+ $this->getLegacyObjectKeyName(),
+ c1::renderString($this->getObjectName())
+ );
+ }
+
+ $str = "define $type {\n$name";
+ if ($this->isTemplate()) {
+ $str .= c1::renderKeyValue('register', '0');
+ }
+
+ return $str;
+ }
+
+ protected function getLegacyObjectKeyName()
+ {
+ if ($this->isTemplate()) {
+ return 'name';
+ }
+
+ return $this->getLegacyObjectType() . '_name';
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ public function renderAssign_Filter()
+ {
+ return ' ' . AssignRenderer::forFilter(
+ Filter::fromQueryString($this->get('assign_filter'))
+ )->renderAssign() . "\n";
+ }
+
+ public function renderLegacyAssign_Filter()
+ {
+ // @codingStandardsIgnoreEnd
+ if ($this instanceof IcingaHostGroup) {
+ $c = " # resolved memberships are set via the individual object\n";
+ } elseif ($this instanceof IcingaService) {
+ $c = " # resolved objects are listed here\n";
+ } else {
+ $c = " # assign is not supported for " . $this->type . "\n";
+ }
+ $c .= ' #' . AssignRenderer::forFilter(
+ Filter::fromQueryString($this->get('assign_filter'))
+ )->renderAssign() . "\n";
+ return $c;
+ }
+
+ public function toLegacyConfigString()
+ {
+ $str = implode([
+ $this->renderLegacyObjectHeader(),
+ $this->renderLegacyImports(),
+ $this->renderLegacyProperties(),
+ //$this->renderArguments(),
+ //$this->renderRelatedSets(),
+ $this->renderLegacyGroups(),
+ $this->renderLegacyMultiRelations(),
+ $this->renderLegacyRanges(),
+ $this->renderLegacyCustomExtensions(),
+ $this->renderLegacyCustomVars(),
+ $this->renderLegacySuffix()
+ ]);
+
+ $str = $this->alignLegacyProperties($str);
+
+ if ($this->isDisabled()) {
+ return
+ "# --- This object has been disabled ---\n"
+ . preg_replace('~^~m', '# ', trim($str))
+ . "\n\n";
+ }
+
+ return $str;
+ }
+
+ protected function alignLegacyProperties($configString)
+ {
+ $lines = explode("\n", $configString);
+ $len = 24;
+
+ foreach ($lines as &$line) {
+ if (preg_match('/^\s{4}([^\t]+)\t+(.+)$/', $line, $m)) {
+ if ($len - strlen($m[1]) < 0) {
+ $fill = ' ';
+ } else {
+ $fill = str_repeat(' ', $len - strlen($m[1]));
+ }
+
+ $line = ' ' . $m[1] . $fill . $m[2];
+ }
+ }
+
+ return implode("\n", $lines);
+ }
+
+ public function toConfigString()
+ {
+ $str = implode([
+ $this->renderObjectHeader(),
+ $this->renderPrioritizedProperties(),
+ $this->renderImports(),
+ $this->renderProperties(),
+ $this->renderArguments(),
+ $this->renderRelatedSets(),
+ $this->renderGroups(),
+ $this->renderMultiRelations(),
+ $this->renderRanges(),
+ $this->renderCustomExtensions(),
+ $this->renderCustomVars(),
+ $this->renderSuffix()
+ ]);
+
+ if ($this->isDisabled()) {
+ return "/* --- This object has been disabled ---\n"
+ // Do not allow strings to break our comment
+ . str_replace('*/', "* /", $str) . "*/\n";
+ }
+
+ return $str;
+ }
+
+ public function isGroup()
+ {
+ return substr($this->getType(), -5) === 'Group';
+ }
+
+ public function hasCheckCommand()
+ {
+ return false;
+ }
+
+ protected function getType()
+ {
+ if ($this->type === null) {
+ $parts = explode('\\', get_class($this));
+ // 6 = strlen('Icinga');
+ $this->type = substr(end($parts), 6);
+ }
+
+ return $this->type;
+ }
+
+ protected function getObjectTypeName()
+ {
+ if ($this->isTemplate()) {
+ return 'template';
+ }
+ if ($this->isApplyRule()) {
+ return 'apply';
+ }
+
+ return 'object';
+ }
+
+ public function getObjectName()
+ {
+ $property = static::getKeyColumnName();
+ if ($this->hasProperty($property)) {
+ return $this->get($property);
+ }
+
+ throw new LogicException(sprintf(
+ 'Trying to access "%s" for an instance of "%s"',
+ $property,
+ get_class($this)
+ ));
+ }
+
+ /**
+ * @deprecated use DbObjectTypeRegistry::classByType()
+ * @param $type
+ * @return string
+ */
+ public static function classByType($type)
+ {
+ return DbObjectTypeRegistry::classByType($type);
+ }
+
+ /**
+ * @param $type
+ * @param array $properties
+ * @param Db|null $db
+ *
+ * @return IcingaObject
+ */
+ public static function createByType($type, $properties = [], Db $db = null)
+ {
+ /** @var IcingaObject $class */
+ $class = DbObjectTypeRegistry::classByType($type);
+ return $class::create($properties, $db);
+ }
+
+ /**
+ * @param $type
+ * @param $id
+ * @param Db $db
+ *
+ * @return IcingaObject
+ * @throws NotFoundError
+ */
+ public static function loadByType($type, $id, Db $db)
+ {
+ /** @var IcingaObject $class */
+ $class = DbObjectTypeRegistry::classByType($type);
+ return $class::load($id, $db);
+ }
+
+ /**
+ * @param $type
+ * @param $id
+ * @param Db $db
+ *
+ * @return bool
+ */
+ public static function existsByType($type, $id, Db $db)
+ {
+ /** @var IcingaObject $class */
+ $class = DbObjectTypeRegistry::classByType($type);
+ return $class::exists($id, $db);
+ }
+
+ public static function getKeyColumnName()
+ {
+ return 'object_name';
+ }
+
+ public static function loadAllByType($type, Db $db, $query = null, $keyColumn = null)
+ {
+ /** @var DbObject $class */
+ $class = DbObjectTypeRegistry::classByType($type);
+
+ if ($keyColumn === null) {
+ if (method_exists($class, 'getKeyColumnName')) {
+ $keyColumn = $class::getKeyColumnName();
+ }
+ }
+
+ if (is_array($class::create()->getKeyName())) {
+ return $class::loadAll($db, $query);
+ }
+
+ if (PrefetchCache::shouldBeUsed()
+ && $query === null
+ && $keyColumn === static::getKeyColumnName()
+ ) {
+ $result = [];
+ foreach ($class::prefetchAll($db) as $row) {
+ $result[$row->$keyColumn] = $row;
+ }
+
+ return $result;
+ }
+
+ return $class::loadAll($db, $query, $keyColumn);
+ }
+
+ /**
+ * @param $type
+ * @param Db $db
+ * @return IcingaObject[]
+ */
+ public static function loadAllExternalObjectsByType($type, Db $db)
+ {
+ /** @var IcingaObject $class */
+ $class = DbObjectTypeRegistry::classByType($type);
+ $dummy = $class::create();
+
+ if (is_array($dummy->getKeyName())) {
+ throw new LogicException(sprintf(
+ 'There is no support for loading external objects of type "%s"',
+ $type
+ ));
+ }
+
+ $query = $db->getDbAdapter()
+ ->select()
+ ->from($dummy->getTableName())
+ ->where('object_type = ?', 'external_object');
+
+ return $class::loadAll($db, $query, 'object_name');
+ }
+
+ public static function fromJson($json, Db $connection = null)
+ {
+ return static::fromPlainObject(json_decode($json), $connection);
+ }
+
+ public static function fromPlainObject($plain, Db $connection = null)
+ {
+ return static::create((array) $plain, $connection);
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @param null $preserve
+ * @return $this
+ * @throws NotFoundError
+ */
+ public function replaceWith(IcingaObject $object, $preserve = [])
+ {
+ return $this->replaceWithProperties($object->toPlainObject(), $preserve);
+ }
+
+ /**
+ * @param array|object $properties
+ * @param array $preserve
+ * @return $this
+ * @throws NotFoundError
+ */
+ public function replaceWithProperties($properties, $preserve = [])
+ {
+ $properties = (array) $properties;
+ foreach ($preserve as $k) {
+ $v = $this->get($k);
+ if ($v !== null) {
+ $properties[$k] = $v;
+ }
+ }
+ $this->setProperties($properties);
+
+ return $this;
+ }
+
+ /**
+ * TODO: with rules? What if I want to override vars? Drop in favour of vars.x?
+ *
+ * @param IcingaObject $object
+ * @param bool $replaceVars
+ * @return $this
+ * @throws NotFoundError
+ */
+ public function merge(IcingaObject $object, $replaceVars = false)
+ {
+ $object = clone($object);
+
+ if ($object->supportsCustomVars()) {
+ $vars = $object->getVars();
+ $object->set('vars', []);
+ }
+
+ if ($object->supportsGroups()) {
+ $groups = $object->getGroups();
+ $object->set('groups', []);
+ }
+
+ if ($object->supportsImports()) {
+ $imports = $object->listImportNames();
+ $object->set('imports', []);
+ }
+
+ $plain = (array) $object->toPlainObject(false, false);
+ unset($plain['vars'], $plain['groups'], $plain['imports']);
+ foreach ($plain as $p => $v) {
+ if ($v === null) {
+ // We want default values, but no null values
+ continue;
+ }
+
+ $this->set($p, $v);
+ }
+
+ if ($object->supportsCustomVars()) {
+ $myVars = $this->vars();
+ if ($replaceVars) {
+ $this->set('vars', $vars);
+ } else {
+ /** @var CustomVariables $vars */
+ foreach ($vars as $key => $var) {
+ $myVars->set($key, $var);
+ }
+ }
+ }
+
+ if ($object->supportsGroups()) {
+ if (! empty($groups)) {
+ $this->set('groups', $groups);
+ }
+ }
+
+ if ($object->supportsImports()) {
+ if (! empty($imports)) {
+ $this->set('imports', $imports);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param bool $resolved
+ * @param bool $skipDefaults
+ * @param array|null $chosenProperties
+ * @param bool $resolveIds
+ * @param bool $keepId
+ * @return object
+ * @throws NotFoundError
+ */
+ public function toPlainObject(
+ $resolved = false,
+ $skipDefaults = false,
+ array $chosenProperties = null,
+ $resolveIds = true,
+ $keepId = false
+ ) {
+ $props = [];
+
+ if ($resolved) {
+ $p = $this->getInheritedProperties();
+ foreach ($this->properties as $k => $v) {
+ if ($v === null && property_exists($p, $k)) {
+ continue;
+ }
+ $p->$k = $v;
+ }
+ } else {
+ $p = $this->properties;
+ }
+
+ foreach ($p as $k => $v) {
+ // Do not ship ids for IcingaObjects:
+ if ($k === $this->getUuidColumn()) {
+ continue;
+ }
+ if ($resolveIds) {
+ if ($k === 'id' && $keepId === false && $this->hasProperty('object_name')) {
+ continue;
+ }
+
+ if ('_id' === substr($k, -3)) {
+ $relKey = substr($k, 0, -3);
+
+ if ($this->hasRelation($relKey)) {
+ if ($this->hasUnresolvedRelatedProperty($k)) {
+ $v = $this->$relKey;
+ } elseif ($v !== null) {
+ $v = $this->getRelatedObjectName($relKey, $v);
+ }
+
+ $k = $relKey;
+ } else {
+ throw new LogicException(sprintf(
+ 'No such relation: %s',
+ $relKey
+ ));
+ }
+ }
+ }
+
+ // TODO: Do not ship null properties based on flag?
+ if (!$skipDefaults || $this->differsFromDefaultValue($k, $v)) {
+ if ($k === 'disabled' || $this->propertyIsBoolean($k)) {
+ $props[$k] = $this->booleanForDbValue($v);
+ } else {
+ $props[$k] = $v;
+ }
+ }
+ }
+
+ if ($this->supportsGroups()) {
+ // TODO: resolve
+ $groups = $this->groups()->listGroupNames();
+ if ($resolved && empty($groups)) {
+ $groups = $this->listInheritedGroupNames();
+ }
+
+ $props['groups'] = $groups;
+ }
+
+ foreach ($this->loadAllMultiRelations() as $key => $rel) {
+ if (count($rel) || !$skipDefaults) {
+ $props[$key] = $rel->listRelatedNames();
+ }
+ }
+
+ if ($this instanceof ObjectWithArguments) {
+ $props['arguments'] = $this->arguments()->toPlainObject(
+ false,
+ $skipDefaults
+ );
+ }
+
+ if ($this->supportsCustomVars()) {
+ if ($resolved) {
+ $props['vars'] = $this->getResolvedVars();
+ } else {
+ $props['vars'] = $this->getVars();
+ }
+ }
+
+ if ($this->supportsImports()) {
+ if ($resolved) {
+ $props['imports'] = [];
+ } else {
+ $props['imports'] = $this->listImportNames();
+ }
+ }
+
+ if ($this->supportsRanges()) {
+ // TODO: resolve
+ $props['ranges'] = $this->get('ranges');
+ }
+
+ if ($skipDefaults) {
+ foreach (['imports', 'ranges', 'arguments'] as $key) {
+ if (empty($props[$key])) {
+ unset($props[$key]);
+ }
+ }
+
+ if (array_key_exists('vars', $props)) {
+ if (count((array) $props['vars']) === 0) {
+ unset($props['vars']);
+ }
+ }
+ if (empty($props['groups'])) {
+ unset($props['groups']);
+ }
+ }
+
+ foreach ($this->relatedSets() as $property => $set) {
+ if ($resolved) {
+ if ($this->supportsImports()) {
+ $set = clone($set);
+ foreach ($this->imports()->getObjects() as $parent) {
+ $set->inheritFrom($parent->getRelatedSet($property));
+ }
+ }
+
+ $values = $set->getResolvedValues();
+ if (empty($values)) {
+ if (!$skipDefaults) {
+ $props[$property] = null;
+ }
+ } else {
+ $props[$property] = $values;
+ }
+ } else {
+ if ($set->isEmpty()) {
+ if (!$skipDefaults) {
+ $props[$property] = null;
+ }
+ } else {
+ $props[$property] = $set->toPlainObject();
+ }
+ }
+ }
+
+ if ($chosenProperties !== null) {
+ $chosen = [];
+ foreach ($chosenProperties as $k) {
+ if (array_key_exists($k, $props)) {
+ $chosen[$k] = $props[$k];
+ }
+ }
+
+ $props = $chosen;
+ }
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ protected function booleanForDbValue($value)
+ {
+ if ($value === 'y') {
+ return true;
+ }
+ if ($value === 'n') {
+ return false;
+ }
+
+ return $value; // let this fail elsewhere, if not null
+ }
+
+ public function listImportNames()
+ {
+ if ($this->gotImports()) {
+ return $this->imports()->listImportNames();
+ }
+
+ return $this->templateTree()->listParentNamesFor($this);
+ }
+
+ public function listFlatResolvedImportNames()
+ {
+ return $this->templateTree()->getAncestorsFor($this);
+ }
+
+ public function listImportIds()
+ {
+ return $this->templateTree()->listParentIdsFor($this);
+ }
+
+ public function listAncestorIds()
+ {
+ return $this->templateTree()->listAncestorIdsFor($this);
+ }
+
+ protected function templateTree()
+ {
+ return $this->templates()->tree();
+ }
+
+ protected function templates()
+ {
+ return IcingaTemplateRepository::instanceByObject($this, $this->getConnection());
+ }
+
+ protected function differsFromDefaultValue($key, $value)
+ {
+ if (array_key_exists($key, $this->defaultProperties)) {
+ return $value !== $this->defaultProperties[$key];
+ }
+
+ return $value !== null;
+ }
+
+ protected function mapHostsToZones($names)
+ {
+ $map = [];
+
+ foreach ($names as $hostname) {
+ /** @var IcingaHost $host */
+ $host = IcingaHost::load($hostname, $this->connection);
+
+ $zone = $host->getRenderingZone();
+ if (! array_key_exists($zone, $map)) {
+ $map[$zone] = [];
+ }
+
+ $map[$zone][] = $hostname;
+ }
+
+ ksort($map);
+
+ return $map;
+ }
+
+ public function getUrlParams()
+ {
+ $params = [];
+ if ($column = $this->getUuidColumn()) {
+ return [$column => $this->getUniqueId()->toString()];
+ }
+
+ if ($this->isApplyRule() && ! $this instanceof IcingaScheduledDowntime) {
+ $params['id'] = $this->get('id');
+ } else {
+ $params = ['name' => $this->getObjectName()];
+
+ if ($this->hasProperty('host_id') && $this->get('host_id')) {
+ $params['host'] = $this->get('host');
+ }
+
+ if ($this->hasProperty('service_id') && $this->get('service_id')) {
+ $params['service'] = $this->get('service');
+ }
+
+ if ($this->hasProperty('service_set_id') && $this->get('service_set_id')) {
+ $params['set'] = $this->get('service_set');
+ }
+ }
+
+ return $params;
+ }
+
+ public function getOnDeleteUrl()
+ {
+ $plural= preg_replace('/cys$/', 'cies', strtolower($this->getShortTableName()) . 's');
+ return 'director/' . $plural;
+ }
+
+ /**
+ * @param bool $resolved
+ * @param bool $skipDefaults
+ * @param array|null $chosenProperties
+ * @return string
+ * @throws NotFoundError
+ */
+ public function toJson(
+ $resolved = false,
+ $skipDefaults = false,
+ array $chosenProperties = null
+ ) {
+
+ return json_encode($this->toPlainObject($resolved, $skipDefaults, $chosenProperties));
+ }
+
+ public function getPlainUnmodifiedObject()
+ {
+ $props = [];
+
+ foreach ($this->getOriginalProperties() as $k => $v) {
+ // Do not ship ids for IcingaObjects:
+ if ($k === 'id' && $this->hasProperty('object_name')) {
+ continue;
+ }
+ if ($k === $this->getUuidColumn()) {
+ continue;
+ }
+ if ($k === 'disabled' && $v === null) {
+ continue;
+ }
+
+ if ('_id' === substr($k, -3)) {
+ $relKey = substr($k, 0, -3);
+
+ if ($this->hasRelation($relKey)) {
+ if ($v !== null) {
+ $v = $this->getRelatedObjectName($relKey, $v);
+ }
+
+ $k = $relKey;
+ }
+ }
+
+ if ($this->differsFromDefaultValue($k, $v)) {
+ if ($k === 'disabled' || $this->propertyIsBoolean($k)) {
+ $props[$k] = $this->booleanForDbValue($v);
+ } else {
+ $props[$k] = $v;
+ }
+ }
+ }
+
+ if ($this->supportsCustomVars()) {
+ $originalVars = $this->vars()->getOriginalVars();
+ if (! empty($originalVars)) {
+ $props['vars'] = (object) [];
+ foreach ($originalVars as $name => $var) {
+ $props['vars']->$name = $var->getValue();
+ }
+ }
+ }
+ if ($this->supportsGroups()) {
+ $groups = $this->groups()->listOriginalGroupNames();
+ if (! empty($groups)) {
+ $props['groups'] = $groups;
+ }
+ }
+ if ($this->supportsImports()) {
+ $imports = $this->imports()->listOriginalImportNames();
+ if (! empty($imports)) {
+ $props['imports'] = $imports;
+ }
+ }
+
+ if ($this instanceof ObjectWithArguments) {
+ $args = $this->arguments()->toUnmodifiedPlainObject();
+ if (! empty($args)) {
+ $props['arguments'] = $args;
+ }
+ }
+
+ if ($this->supportsRanges()) {
+ $ranges = $this->ranges()->getOriginalValues();
+ if (!empty($ranges)) {
+ $props['ranges'] = $ranges;
+ }
+ }
+
+ foreach ($this->relatedSets() as $property => $set) {
+ if ($set->isEmpty()) {
+ continue;
+ }
+
+ $props[$property] = $set->getPlainUnmodifiedObject();
+ }
+
+ foreach ($this->loadAllMultiRelations() as $key => $rel) {
+ $old = $rel->listOriginalNames();
+ if (! empty($old)) {
+ $props[$key] = $old;
+ }
+ }
+
+ return (object) $props;
+ }
+
+ public function __toString()
+ {
+ try {
+ return $this->toConfigString();
+ } catch (Exception $e) {
+ trigger_error($e);
+ $previousHandler = set_exception_handler(
+ function () {
+ }
+ );
+ restore_error_handler();
+ if ($previousHandler !== null) {
+ call_user_func($previousHandler, $e);
+ die();
+ }
+
+ die($e->getMessage());
+ }
+ }
+
+ public function __destruct()
+ {
+ unset($this->resolveCache);
+ unset($this->vars);
+ unset($this->groups);
+ unset($this->imports);
+ unset($this->ranges);
+ if ($this instanceof ObjectWithArguments) {
+ $this->unsetArguments();
+ }
+
+ parent::__destruct();
+ }
+}
diff --git a/library/Director/Objects/IcingaObjectField.php b/library/Director/Objects/IcingaObjectField.php
new file mode 100644
index 0000000..e18965b
--- /dev/null
+++ b/library/Director/Objects/IcingaObjectField.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Data\Db\DbObject;
+
+abstract class IcingaObjectField extends DbObject
+{
+ /**
+ *
+ * @param Filter|string $filter
+ *
+ * @return $this
+ * @codingStandardsIgnoreStart
+ */
+ protected function setVar_filter($value)
+ {
+ // @codingStandardsIgnoreEnd
+ if ($value instanceof Filter) {
+ $value = $value->toQueryString();
+ }
+
+ return $this->reallySet('var_filter', $value);
+ }
+}
diff --git a/library/Director/Objects/IcingaObjectGroup.php b/library/Director/Objects/IcingaObjectGroup.php
new file mode 100644
index 0000000..c0bec54
--- /dev/null
+++ b/library/Director/Objects/IcingaObjectGroup.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+
+abstract class IcingaObjectGroup extends IcingaObject implements ExportInterface
+{
+ protected $supportsImports = true;
+
+ protected $supportedInLegacy = true;
+
+ protected $uuidColumn = 'uuid';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'display_name' => null,
+ 'assign_filter' => null,
+ ];
+
+ public function getUniqueIdentifier()
+ {
+ return $this->getObjectName();
+ }
+
+ /**
+ * @return object
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ return $this->toPlainObject();
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return IcingaObjectGroup
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ $key = $name;
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Group "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ protected function prefersGlobalZone()
+ {
+ return true;
+ }
+}
diff --git a/library/Director/Objects/IcingaObjectGroups.php b/library/Director/Objects/IcingaObjectGroups.php
new file mode 100644
index 0000000..8bef1b1
--- /dev/null
+++ b/library/Director/Objects/IcingaObjectGroups.php
@@ -0,0 +1,408 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Countable;
+use Exception;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+use Iterator;
+use RuntimeException;
+
+class IcingaObjectGroups implements Iterator, Countable, IcingaConfigRenderer
+{
+ protected $storedGroups = array();
+
+ protected $groups = array();
+
+ protected $modified = false;
+
+ protected $object;
+
+ private $position = 0;
+
+ protected $idx = array();
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->object = $object;
+
+ if (! $object->hasBeenLoadedFromDb() && PrefetchCache::shouldBeUsed()) {
+ /** @var IcingaObjectGroup $class */
+ $class = $this->getGroupClass();
+ $class::prefetchAll($this->object->getConnection());
+ }
+ }
+
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return count($this->groups);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->position = 0;
+ }
+
+ public function hasBeenModified()
+ {
+ return $this->modified;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ if (! $this->valid()) {
+ return null;
+ }
+
+ return $this->groups[$this->idx[$this->position]];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->idx[$this->position];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ ++$this->position;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ return array_key_exists($this->position, $this->idx);
+ }
+
+ public function get($key)
+ {
+ if (array_key_exists($key, $this->groups)) {
+ return $this->groups[$key];
+ }
+
+ return null;
+ }
+
+ /**
+ * @param $group
+ * @return $this
+ * @throws NotFoundError
+ */
+ public function set($group)
+ {
+ if (! is_array($group)) {
+ $group = array($group);
+ }
+
+ $existing = array_keys($this->groups);
+ $new = array();
+ $class = $this->getGroupClass();
+ $unset = array();
+
+ foreach ($group as $k => $g) {
+ if ($g instanceof $class) {
+ $new[] = $g->object_name;
+ } else {
+ if (empty($g)) {
+ $unset[] = $k;
+ continue;
+ }
+
+ $new[] = $g;
+ }
+ }
+
+ foreach ($unset as $k) {
+ unset($group[$k]);
+ }
+
+ sort($existing);
+ sort($new);
+ if ($existing === $new) {
+ return $this;
+ }
+
+ $this->groups = array();
+ if (empty($group)) {
+ $this->modified = true;
+ $this->refreshIndex();
+ return $this;
+ }
+
+ return $this->add($group);
+ }
+
+ /**
+ * Magic isset check
+ *
+ * @return boolean
+ */
+ public function __isset($group)
+ {
+ return array_key_exists($group, $this->groups);
+ }
+
+ public function remove($group)
+ {
+ if (array_key_exists($group, $this->groups)) {
+ unset($this->groups[$group]);
+ }
+
+ $this->modified = true;
+ $this->refreshIndex();
+ }
+
+ protected function refreshIndex()
+ {
+ ksort($this->groups);
+ $this->idx = array_keys($this->groups);
+ }
+
+ /**
+ * @param $group
+ * @param string $onError
+ * @return $this
+ * @throws NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function add($group, $onError = 'fail')
+ {
+ // TODO: only one query when adding array
+ if (is_array($group)) {
+ foreach ($group as $g) {
+ $this->add($g, $onError);
+ }
+ return $this;
+ }
+
+ /** @var IcingaObjectGroup $class */
+ $class = $this->getGroupClass();
+
+ if ($group instanceof $class) {
+ if (array_key_exists($group->getObjectName(), $this->groups)) {
+ return $this;
+ }
+
+ $this->groups[$group->object_name] = $group;
+ } elseif (is_string($group)) {
+ if (array_key_exists($group, $this->groups)) {
+ return $this;
+ }
+
+ $connection = $this->object->getConnection();
+
+ try {
+ $this->groups[$group] = $class::load($group, $connection);
+ } catch (NotFoundError $e) {
+ switch ($onError) {
+ case 'autocreate':
+ $newGroup = $class::create(array(
+ 'object_type' => 'object',
+ 'object_name' => $group
+ ));
+ $newGroup->store($connection);
+ $this->groups[$group] = $newGroup;
+ break;
+ case 'fail':
+ throw new NotFoundError(
+ 'The group "%s" doesn\'t exist.',
+ $group
+ );
+ break;
+ case 'ignore':
+ return $this;
+ }
+ }
+ } else {
+ throw new RuntimeException(
+ 'Invalid group object: %s',
+ var_export($group, 1)
+ );
+ }
+
+ $this->modified = true;
+ $this->refreshIndex();
+
+ return $this;
+ }
+
+ protected function getGroupTableName()
+ {
+ return $this->object->getTableName() . 'group';
+ }
+
+
+ protected function getGroupMemberTableName()
+ {
+ return $this->object->getTableName() . 'group_' . $this->getType();
+ }
+
+ public function listGroupNames()
+ {
+ return array_keys($this->groups);
+ }
+
+ public function listOriginalGroupNames()
+ {
+ return array_keys($this->storedGroups);
+ }
+
+ public function getType()
+ {
+ return $this->object->getShortTableName();
+ }
+
+ protected function loadFromDb()
+ {
+ $db = $this->object->getDb();
+ $connection = $this->object->getConnection();
+
+ $type = $this->getType();
+
+ $table = $this->object->getTableName();
+ $query = $db->select()->from(
+ array('go' => $table . 'group_' . $type),
+ array()
+ )->join(
+ array('g' => $table . 'group'),
+ 'go.' . $type . 'group_id = g.id',
+ '*'
+ )->where('go.' . $type . '_id = ?', $this->object->id)
+ ->order('g.object_name');
+
+ $class = $this->getGroupClass();
+ $this->groups = $class::loadAll($connection, $query, 'object_name');
+ $this->setBeingLoadedFromDb();
+
+ return $this;
+ }
+
+ public function store()
+ {
+ $storedGroups = array_keys($this->storedGroups);
+ $groups = array_keys($this->groups);
+
+ $objectId = $this->object->id;
+ $type = $this->getType();
+
+ $objectCol = $type . '_id';
+ $groupCol = $type . 'group_id';
+
+ $toDelete = array_diff($storedGroups, $groups);
+ foreach ($toDelete as $group) {
+ $where = sprintf(
+ $objectCol . ' = %d AND ' . $groupCol . ' = %d',
+ $objectId,
+ $this->storedGroups[$group]->id
+ );
+
+ $this->object->db->delete(
+ $this->getGroupMemberTableName(),
+ $where
+ );
+ }
+
+ $toAdd = array_diff($groups, $storedGroups);
+ foreach ($toAdd as $group) {
+ $this->object->db->insert(
+ $this->getGroupMemberTableName(),
+ array(
+ $objectCol => $objectId,
+ $groupCol => $this->groups[$group]->id
+ )
+ );
+ }
+ $this->setBeingLoadedFromDb();
+
+ return true;
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ $this->storedGroups = array();
+ foreach ($this->groups as $k => $v) {
+ $this->storedGroups[$k] = clone($v);
+ $this->storedGroups[$k]->id = $v->id;
+ }
+
+ $this->modified = false;
+ }
+
+ protected function getGroupClass()
+ {
+ return __NAMESPACE__ . '\\Icinga' .ucfirst($this->object->getShortTableName()) . 'Group';
+ }
+
+ public static function loadForStoredObject(IcingaObject $object)
+ {
+ $groups = new static($object);
+
+ if (PrefetchCache::shouldBeUsed()) {
+ $groups->groups = PrefetchCache::instance()->groups($object);
+ $groups->setBeingLoadedFromDb();
+ } else {
+ $groups->loadFromDb();
+ }
+
+ return $groups;
+ }
+
+ public function toConfigString()
+ {
+ $groups = array_keys($this->groups);
+
+ if (empty($groups)) {
+ return '';
+ }
+
+ return c::renderKeyValue('groups', c::renderArray($groups));
+ }
+
+ public function toLegacyConfigString($additionalGroups = array())
+ {
+ $groups = array_merge(array_keys($this->groups), $additionalGroups);
+ $groups = array_unique($groups);
+
+ if (empty($groups)) {
+ return '';
+ }
+
+ $type = $this->object->getLegacyObjectType();
+ return c1::renderKeyValue($type.'groups', c1::renderArray($groups));
+ }
+
+ public function __toString()
+ {
+ try {
+ return $this->toConfigString();
+ } catch (Exception $e) {
+ trigger_error($e);
+ $previousHandler = set_exception_handler(
+ function () {
+ }
+ );
+ restore_error_handler();
+ if ($previousHandler !== null) {
+ call_user_func($previousHandler, $e);
+ die();
+ } else {
+ die($e->getMessage());
+ }
+ }
+ }
+
+ public function __destruct()
+ {
+ unset($this->storedGroups);
+ unset($this->groups);
+ unset($this->object);
+ }
+}
diff --git a/library/Director/Objects/IcingaObjectImports.php b/library/Director/Objects/IcingaObjectImports.php
new file mode 100644
index 0000000..384fa1c
--- /dev/null
+++ b/library/Director/Objects/IcingaObjectImports.php
@@ -0,0 +1,439 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Countable;
+use Exception;
+use Icinga\Exception\NotFoundError;
+use Iterator;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use RuntimeException;
+
+class IcingaObjectImports implements Iterator, Countable, IcingaConfigRenderer
+{
+ protected $storedNames = [];
+
+ /** @var array A list of our imports, key and value are the import name */
+ protected $imports = [];
+
+ /** @var IcingaObject[] A list of all objects we have seen, referred by name */
+ protected $objects = [];
+
+ protected $modified = false;
+
+ /** @var IcingaObject The parent object */
+ protected $object;
+
+ private $position = 0;
+
+ protected $idx = [];
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->object = $object;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return count($this->imports);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->position = 0;
+ }
+
+ public function setModified()
+ {
+ $this->modified = true;
+ return $this;
+ }
+
+ public function hasBeenModified()
+ {
+ return $this->modified;
+ }
+
+ /**
+ * @return IcingaObject|null
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ if (! $this->valid()) {
+ return null;
+ }
+
+ return $this->getObject(
+ $this->imports[$this->idx[$this->position]]
+ );
+ }
+
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->idx[$this->position];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ ++$this->position;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ return array_key_exists($this->position, $this->idx);
+ }
+
+ /**
+ * @param $key
+ * @return IcingaObject|null
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function get($key)
+ {
+ if (array_key_exists($key, $this->imports)) {
+ return $this->getObject($this->imports[$key]);
+ }
+
+ return null;
+ }
+
+ public function set($import)
+ {
+ if (empty($import)) {
+ if (empty($this->imports)) {
+ return $this;
+ } else {
+ return $this->clear();
+ }
+ }
+
+ if (! is_array($import)) {
+ $import = [$import];
+ }
+
+ $existing = $this->listImportNames();
+ $new = $this->listNamesForGivenImports($import);
+
+ if ($existing === $new) {
+ return $this;
+ }
+
+ $this->imports = [];
+ return $this->add($import);
+ }
+
+ protected function listNamesForGivenImports($imports)
+ {
+ $list = [];
+ $class = $this->getImportClass();
+
+ foreach ($imports as $i) {
+ if ($i instanceof $class) {
+ $list[] = $i->object_name;
+ } else {
+ $list[] = $i;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Magic isset check
+ *
+ * @param string $import
+ *
+ * @return boolean
+ */
+ public function __isset($import)
+ {
+ return array_key_exists($import, $this->imports);
+ }
+
+ public function clear()
+ {
+ if ($this->imports === []) {
+ return $this;
+ }
+
+ $this->imports = [];
+ $this->modified = true;
+
+ return $this->refreshIndex();
+ }
+
+ public function remove($import)
+ {
+ if (array_key_exists($import, $this->imports)) {
+ unset($this->imports[$import]);
+ }
+
+ $this->modified = true;
+
+ return $this->refreshIndex();
+ }
+
+ protected function refreshIndex()
+ {
+ $this->idx = array_keys($this->imports);
+ // $this->object->templateResolver()->refreshObject($this->object);
+
+ return $this;
+ }
+
+ public function add($import)
+ {
+ $class = $this->getImportClass();
+
+ if (is_array($import)) {
+ foreach ($import as $i) {
+ // Gracefully ignore null members or empty strings
+ if (! $i instanceof $class && ($i === null || strlen($i) === 0)) {
+ continue;
+ }
+
+ $this->add($i);
+ }
+
+ return $this;
+ }
+
+ if ($import instanceof $class) {
+ $name = $import->object_name;
+ if (array_key_exists($name, $this->imports)) {
+ return $this;
+ }
+
+ $this->imports[$name] = $name;
+ $this->objects[$name] = $import;
+ } elseif (is_string($import)) {
+ if (array_key_exists($import, $this->imports)) {
+ return $this;
+ }
+
+ $this->imports[$import] = $import;
+ }
+
+ $this->modified = true;
+ $this->refreshIndex();
+
+ return $this;
+ }
+
+ /**
+ * @return IcingaObject[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getObjects()
+ {
+ $list = [];
+ foreach ($this->listImportNames() as $name) {
+ $name = (string) $name;
+ $list[$name] = $this->getObject($name);
+ }
+
+ return $list;
+ }
+
+ /**
+ * @param $name
+ * @return IcingaObject
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getObject($name)
+ {
+ if (array_key_exists($name, $this->objects)) {
+ return $this->objects[$name];
+ }
+
+ $connection = $this->object->getConnection();
+ /** @var IcingaObject $class */
+ $class = $this->getImportClass();
+ try {
+ if (is_array($this->object->getKeyName())) {
+ // Services only
+ $import = $class::load([
+ 'object_name' => $name,
+ 'object_type' => 'template'
+ ], $connection);
+ } else {
+ $import = $class::load($name, $connection);
+ }
+ } catch (NotFoundError $e) {
+ throw new NotFoundError(sprintf(
+ 'Unable to load parent referenced from %s "%s", %s',
+ $this->object->getShortTableName(),
+ $this->object->getObjectName(),
+ lcfirst($e->getMessage())
+ ), $e->getCode(), $e);
+ }
+
+ return $this->objects[$import->getObjectName()] = $import;
+ }
+
+ protected function getImportTableName()
+ {
+ return $this->object->getTableName() . '_inheritance';
+ }
+
+ public function listImportNames()
+ {
+ return array_keys($this->imports);
+ }
+
+ public function listOriginalImportNames()
+ {
+ return $this->storedNames;
+ }
+
+ public function getType()
+ {
+ return $this->object->getShortTableName();
+ }
+
+ protected function loadFromDb()
+ {
+ // $resolver = $this->object->templateResolver();
+ // $this->objects = $resolver->fetchParents();
+ $this->objects = IcingaTemplateRepository::instanceByObject($this->object)
+ ->getTemplatesIndexedByNameFor($this->object);
+ if (empty($this->objects)) {
+ $this->imports = [];
+ } else {
+ $keys = array_keys($this->objects);
+ $this->imports = array_combine($keys, $keys);
+ }
+
+ $this->setBeingLoadedFromDb();
+ return $this;
+ }
+
+ /**
+ * @return bool
+ * @throws \Zend_Db_Adapter_Exception
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function store()
+ {
+ if (! $this->hasBeenModified()) {
+ return true;
+ }
+
+ $objectId = $this->object->get('id');
+ if ($objectId === null) {
+ throw new RuntimeException(
+ 'Cannot store imports for unstored object with no ID'
+ );
+ } else {
+ $objectId = (int) $objectId;
+ }
+
+ $type = $this->getType();
+
+ $objectCol = $type . '_id';
+ $importCol = 'parent_' . $type . '_id';
+ $table = $this->getImportTableName();
+ $db = $this->object->getDb();
+
+ if ($this->object->hasBeenLoadedFromDb()) {
+ $db->delete(
+ $table,
+ $objectCol . ' = ' . $objectId
+ );
+ }
+
+ $weight = 1;
+ foreach ($this->getObjects() as $import) {
+ $db->insert($table, [
+ $objectCol => $objectId,
+ $importCol => $import->get('id'),
+ 'weight' => $weight++
+ ]);
+ }
+
+ $this->setBeingLoadedFromDb();
+
+ return true;
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ $this->storedNames = $this->listImportNames();
+ $this->modified = false;
+ }
+
+ protected function getImportClass()
+ {
+ return get_class($this->object);
+ }
+
+ public static function loadForStoredObject(IcingaObject $object)
+ {
+ $obj = new static($object);
+ return $obj->loadFromDb();
+ }
+
+ public function toConfigString()
+ {
+ $ret = '';
+
+ foreach ($this->listImportNames() as $name) {
+ $ret .= ' import ' . c::renderString($name) . "\n";
+ }
+
+ if ($ret !== '') {
+ $ret .= "\n";
+ }
+ return $ret;
+ }
+
+ public function toLegacyConfigString()
+ {
+ $ret = '';
+
+ foreach ($this->listImportNames() as $name) {
+ $ret .= c1::renderKeyValue('use', c1::renderString($name)) . "\n";
+ }
+
+ if ($ret !== '') {
+ $ret .= "\n";
+ }
+ return $ret;
+ }
+
+ public function __toString()
+ {
+ try {
+ return $this->toConfigString();
+ } catch (Exception $e) {
+ trigger_error($e);
+ $previousHandler = set_exception_handler(
+ function () {
+ }
+ );
+ restore_error_handler();
+ if ($previousHandler !== null) {
+ call_user_func($previousHandler, $e);
+ die();
+ } else {
+ die($e->getMessage());
+ }
+ }
+ }
+
+ public function __destruct()
+ {
+ unset($this->object);
+ unset($this->objects);
+ }
+}
diff --git a/library/Director/Objects/IcingaObjectLegacyAssignments.php b/library/Director/Objects/IcingaObjectLegacyAssignments.php
new file mode 100644
index 0000000..6ab75c8
--- /dev/null
+++ b/library/Director/Objects/IcingaObjectLegacyAssignments.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use LogicException;
+
+/**
+ * This class is required for historical reasons
+ *
+ * Objects with assignments in your activity log would otherwise not be able
+ * to render themselves
+ */
+class IcingaObjectLegacyAssignments
+{
+ public static function applyToObject(IcingaObject $object, $values)
+ {
+ if (! $object->supportsAssignments()) {
+ throw new LogicException(sprintf(
+ 'I can only assign for applied objects, got %s',
+ $object->object_type
+ ));
+ }
+
+ if ($values === null) {
+ return $object;
+ }
+
+ if (! is_array($values)) {
+ static::throwCompatError();
+ }
+
+ if (empty($values)) {
+ return $object;
+ }
+
+ $assigns = array();
+ $ignores = array();
+ foreach ($values as $type => $value) {
+ if (strpos($value, '|') !== false || strpos($value, '&' !== false)) {
+ $value = '(' . $value . ')';
+ }
+
+ if ($type === 'assign') {
+ $assigns[] = $value;
+ } elseif ($type === 'ignore') {
+ $ignores[] = $value;
+ } else {
+ static::throwCompatError();
+ }
+ }
+
+ $assign = implode('|', $assigns);
+ $ignore = implode('&', $ignores);
+ if (empty($assign)) {
+ $filter = $ignore;
+ } elseif (empty($ignore)) {
+ $filter = $assign;
+ } else {
+ if (count($assigns) === 1) {
+ $filter = $assign . '&' . $ignore;
+ } else {
+ $filter = '(' . $assign . ')&(' . $ignore . ')';
+ }
+ }
+
+ $object->assign_filter = $filter;
+
+ return $object;
+ }
+
+ protected static function throwCompatError()
+ {
+ throw new LogicException(
+ 'You ran into an unexpected compatibility issue. Please report'
+ . ' this with details helping us to reproduce this to the'
+ . ' Icinga project'
+ );
+ }
+}
diff --git a/library/Director/Objects/IcingaObjectMultiRelations.php b/library/Director/Objects/IcingaObjectMultiRelations.php
new file mode 100644
index 0000000..a1ec9a2
--- /dev/null
+++ b/library/Director/Objects/IcingaObjectMultiRelations.php
@@ -0,0 +1,454 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Exception;
+use Icinga\Exception\ProgrammingError;
+use Iterator;
+use Countable;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+
+class IcingaObjectMultiRelations implements Iterator, Countable, IcingaConfigRenderer
+{
+ protected $stored = array();
+
+ protected $relations = array();
+
+ protected $modified = false;
+
+ protected $object;
+
+ protected $propertyName;
+
+ protected $relatedObjectClass;
+
+ protected $relatedTableName;
+
+ protected $relationIdColumn;
+
+ protected $relatedShortName;
+
+ protected $legacyPropertyName;
+
+ private $position = 0;
+
+ private $db;
+
+ protected $idx = array();
+
+ public function __construct(IcingaObject $object, $propertyName, $config)
+ {
+ $this->object = $object;
+ $this->propertyName = $propertyName;
+
+ if (is_object($config) || is_array($config)) {
+ foreach ($config as $k => $v) {
+ $this->$k = $v;
+ }
+ } else {
+ $this->relatedObjectClass = $config;
+ }
+ }
+
+ public function getObjects()
+ {
+ return $this->relations;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return count($this->relations);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->position = 0;
+ }
+
+ public function hasBeenModified()
+ {
+ return $this->modified;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ if (! $this->valid()) {
+ return null;
+ }
+
+ return $this->relations[$this->idx[$this->position]];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->idx[$this->position];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ ++$this->position;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ return array_key_exists($this->position, $this->idx);
+ }
+
+ public function get($key)
+ {
+ if (array_key_exists($key, $this->relations)) {
+ return $this->relations[$key];
+ }
+
+ return null;
+ }
+
+ public function set($relation)
+ {
+ if (! is_array($relation)) {
+ if ($relation === null) {
+ $relation = array();
+ } else {
+ $relation = array($relation);
+ }
+ }
+
+ $existing = array_keys($this->relations);
+ $new = array();
+ $class = $this->getRelatedClassName();
+ $unset = array();
+
+ foreach ($relation as $k => $ro) {
+ if ($ro instanceof $class) {
+ $new[] = $ro->object_name;
+ } else {
+ if (empty($ro)) {
+ $unset[] = $k;
+ continue;
+ }
+
+ $new[] = $ro;
+ }
+ }
+
+ foreach ($unset as $k) {
+ unset($relation[$k]);
+ }
+
+ sort($existing);
+ sort($new);
+ if ($existing === $new) {
+ return $this;
+ }
+
+ $this->relations = array();
+ if (empty($relation)) {
+ $this->modified = true;
+ $this->refreshIndex();
+ return $this;
+ }
+
+ return $this->add($relation);
+ }
+
+ /**
+ * Magic isset check
+ *
+ * @return boolean
+ */
+ public function __isset($relation)
+ {
+ return array_key_exists($relation, $this->relations);
+ }
+
+ public function remove($relation)
+ {
+ if (array_key_exists($relation, $this->relations)) {
+ unset($this->relations[$relation]);
+ }
+
+ $this->modified = true;
+ $this->refreshIndex();
+ }
+
+ protected function refreshIndex()
+ {
+ ksort($this->relations);
+ $this->idx = array_keys($this->relations);
+ }
+
+ public function add($relation, $onError = 'fail')
+ {
+ // TODO: only one query when adding array
+ if (is_array($relation)) {
+ foreach ($relation as $r) {
+ $this->add($r, $onError);
+ }
+ return $this;
+ }
+
+ if (array_key_exists($relation, $this->relations)) {
+ return $this;
+ }
+
+ $class = $this->getRelatedClassName();
+
+ if ($relation instanceof $class) {
+ $this->relations[$relation->object_name] = $relation;
+ } elseif (is_string($relation)) {
+ $connection = $this->object->getConnection();
+ try {
+ // Related services can only be objects, used by ServiceSets
+ if ($class === 'Icinga\\Module\\Director\\Objects\\IcingaService') {
+ $relation = $class::load(array(
+ 'object_name' => $relation,
+ 'object_type' => 'template'
+ ), $connection);
+ } else {
+ $relation = $class::load($relation, $connection);
+ }
+ } catch (Exception $e) {
+ switch ($onError) {
+ case 'autocreate':
+ $relation = $class::create(array(
+ 'object_type' => 'object',
+ 'object_name' => $relation
+ ));
+ $relation->store($connection);
+ // TODO
+ case 'fail':
+ throw new ProgrammingError(
+ 'The related %s "%s" doesn\'t exists: %s',
+ $this->getRelatedTableName(),
+ $relation,
+ $e->getMessage()
+ );
+ break;
+ case 'ignore':
+ return $this;
+ }
+ }
+ } else {
+ throw new ProgrammingError(
+ 'Invalid related object: %s',
+ var_export($relation, 1)
+ );
+ }
+
+ $this->relations[$relation->object_name] = $relation;
+ $this->modified = true;
+ $this->refreshIndex();
+
+ return $this;
+ }
+
+ protected function getPropertyName()
+ {
+ return $this->propertyName;
+ }
+
+ protected function getRelatedShortName()
+ {
+ if ($this->relatedShortName === null) {
+ /** @var IcingaObject $class */
+ $class = $this->getRelatedClassName();
+ $this->relatedShortName = $class::create()->getShortTableName();
+ }
+
+ return $this->relatedShortName;
+ }
+
+ protected function getTableName()
+ {
+ return $this->object->getTableName() . '_' . $this->getRelatedShortName();
+ }
+
+ protected function getRelatedTableName()
+ {
+ if ($this->relatedTableName === null) {
+ /** @var IcingaObject $class */
+ $class = $this->getRelatedClassName();
+ $this->relatedTableName = $class::create()->getTableName();
+ }
+
+ return $this->relatedTableName;
+ }
+
+ protected function getRelationIdColumn()
+ {
+ if ($this->relationIdColumn === null) {
+ $this->relationIdColumn = $this->getRelatedShortName();
+ }
+
+ return $this->relationIdColumn;
+ }
+
+ public function listRelatedNames()
+ {
+ return array_keys($this->relations);
+ }
+
+ public function listOriginalNames()
+ {
+ return array_keys($this->stored);
+ }
+
+ public function getType()
+ {
+ return $this->object->getShortTableName();
+ }
+
+ protected function loadFromDb()
+ {
+ $db = $this->getDb();
+ $connection = $this->object->getConnection();
+
+ $type = $this->getType();
+ $objectIdCol = $type . '_id';
+ $relationIdCol = $this->getRelationIdColumn() . '_id';
+
+ $query = $db->select()->from(
+ array('r' => $this->getTableName()),
+ array()
+ )->join(
+ array('ro' => $this->getRelatedTableName()),
+ sprintf('r.%s = ro.id', $relationIdCol),
+ '*'
+ )->where(
+ sprintf('r.%s = ?', $objectIdCol),
+ (int) $this->object->id
+ )->order('ro.object_name');
+
+ $class = $this->getRelatedClassName();
+ $this->relations = $class::loadAll($connection, $query, 'object_name');
+ $this->setBeingLoadedFromDb();
+
+ return $this;
+ }
+
+ public function store()
+ {
+ $db = $this->getDb();
+ $stored = array_keys($this->stored);
+ $relations = array_keys($this->relations);
+
+ $objectId = $this->object->id;
+ $type = $this->getType();
+ $objectCol = $type . '_id';
+ $relationCol = $this->getRelationIdColumn() . '_id';
+
+ $toDelete = array_diff($stored, $relations);
+ foreach ($toDelete as $relation) {
+ // We work with cloned objects. (why?)
+ // As __clone drops the id, we need to access original properties
+ $orig = $this->stored[$relation]->getOriginalProperties();
+ $where = sprintf(
+ $objectCol . ' = %d AND ' . $relationCol . ' = %d',
+ $objectId,
+ $orig['id']
+ );
+
+ $db->delete(
+ $this->getTableName(),
+ $where
+ );
+ }
+
+ $toAdd = array_diff($relations, $stored);
+ foreach ($toAdd as $related) {
+ $db->insert(
+ $this->getTableName(),
+ array(
+ $objectCol => $objectId,
+ $relationCol => $this->relations[$related]->id
+ )
+ );
+ }
+ $this->setBeingLoadedFromDb();
+
+ return true;
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ $this->stored = array();
+ foreach ($this->relations as $k => $v) {
+ $this->stored[$k] = clone($v);
+ }
+ }
+
+ protected function getRelatedClassName()
+ {
+ return __NAMESPACE__ . '\\' . $this->relatedObjectClass;
+ }
+
+ protected function getDb()
+ {
+ if ($this->db === null) {
+ $this->db = $this->object->getDb();
+ }
+
+ return $this->db;
+ }
+
+ public static function loadForStoredObject(IcingaObject $object, $propertyName, $relatedObjectClass)
+ {
+ $relations = new static($object, $propertyName, $relatedObjectClass);
+ return $relations->loadFromDb();
+ }
+
+ public function toConfigString()
+ {
+ $relations = array_keys($this->relations);
+
+ if (empty($relations)) {
+ return '';
+ }
+
+ return c::renderKeyValue($this->propertyName, c::renderArray($relations));
+ }
+
+ public function __toString()
+ {
+ try {
+ return $this->toConfigString();
+ } catch (Exception $e) {
+ trigger_error($e);
+ $previousHandler = set_exception_handler(
+ function () {
+ }
+ );
+ restore_error_handler();
+ if ($previousHandler !== null) {
+ call_user_func($previousHandler, $e);
+ die();
+ } else {
+ die($e->getMessage());
+ }
+ }
+ }
+
+ public function toLegacyConfigString()
+ {
+ $relations = array_keys($this->relations);
+
+ if (empty($relations)) {
+ return '';
+ }
+
+ if ($this->legacyPropertyName === null) {
+ return ' # not supported in legacy: ' .
+ c1::renderKeyValue($this->propertyName, c1::renderArray($relations), '');
+ }
+
+ return c1::renderKeyValue($this->legacyPropertyName, c1::renderArray($relations));
+ }
+}
diff --git a/library/Director/Objects/IcingaRanges.php b/library/Director/Objects/IcingaRanges.php
new file mode 100644
index 0000000..c14c588
--- /dev/null
+++ b/library/Director/Objects/IcingaRanges.php
@@ -0,0 +1,321 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Exception;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+
+abstract class IcingaRanges
+{
+ /** @var IcingaTimePeriodRange[]|IcingaScheduledDowntimeRange[] */
+ protected $storedRanges = [];
+
+ /** @var IcingaTimePeriodRange[]|IcingaScheduledDowntimeRange[] */
+ protected $ranges = [];
+
+ protected $modified = false;
+
+ protected $object;
+
+ private $position = 0;
+
+ protected $idx = array();
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->object = $object;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ return count($this->ranges);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->position = 0;
+ }
+
+ public function hasBeenModified()
+ {
+ return $this->modified;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ if (! $this->valid()) {
+ return null;
+ }
+
+ return $this->ranges[$this->idx[$this->position]];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->idx[$this->position];
+ }
+
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ ++$this->position;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ return array_key_exists($this->position, $this->idx);
+ }
+
+ public function get($key)
+ {
+ if (array_key_exists($key, $this->ranges)) {
+ return $this->ranges[$key];
+ }
+
+ return null;
+ }
+
+ public function getValues()
+ {
+ $res = array();
+ foreach ($this->ranges as $key => $range) {
+ $res[$key] = $range->range_value;
+ }
+
+ return (object) $res;
+ }
+
+ public function getOriginalValues()
+ {
+ $res = array();
+ foreach ($this->storedRanges as $key => $range) {
+ $res[$key] = $range->range_value;
+ }
+
+ return (object) $res;
+ }
+
+ public function getRanges()
+ {
+ return $this->ranges;
+ }
+
+ protected function modify($range, $value)
+ {
+ $this->ranges[$range]->range_key = $value;
+ }
+
+ public function set($ranges)
+ {
+ foreach ($ranges as $range => $value) {
+ $this->setRange($range, $value);
+ }
+
+ $toDelete = array_diff(array_keys($this->ranges), array_keys($ranges));
+ foreach ($toDelete as $range) {
+ $this->remove($range);
+ }
+
+ return $this;
+ }
+
+ public function setRange($range, $value)
+ {
+ if ($value === null && array_key_exists($range, $this->ranges)) {
+ $this->remove($range);
+ return $this;
+ }
+
+ if (array_key_exists($range, $this->ranges)) {
+ if ($this->ranges[$range]->range_value === $value) {
+ return $this;
+ } else {
+ $this->ranges[$range]->range_value = $value;
+ $this->modified = true;
+ }
+ } else {
+ $class = $this->getRangeClass();
+ $this->ranges[$range] = $class::create([
+ $this->objectIdColumn => $this->object->get('id'),
+ 'range_key' => $range,
+ 'range_value' => $value,
+ ]);
+ $this->modified = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Magic isset check
+ *
+ * @return boolean
+ */
+ public function __isset($range)
+ {
+ return array_key_exists($range, $this->ranges);
+ }
+
+ public function remove($range)
+ {
+ if (array_key_exists($range, $this->ranges)) {
+ unset($this->ranges[$range]);
+ }
+
+ $this->modified = true;
+ $this->refreshIndex();
+ }
+
+ public function clear()
+ {
+ $this->ranges = [];
+ $this->modified = true;
+ $this->refreshIndex();
+ }
+
+ protected function refreshIndex()
+ {
+ ksort($this->ranges);
+ $this->idx = array_keys($this->ranges);
+ }
+
+ public function listRangesNames()
+ {
+ return array_keys($this->ranges);
+ }
+
+ public function getType()
+ {
+ return $this->object->getShortTableName();
+ }
+
+ public function getRangeTableName()
+ {
+ return $this->object->getTableName() . '_range';
+ }
+
+ protected function loadFromDb()
+ {
+ $db = $this->object->getDb();
+ $connection = $this->object->getConnection();
+
+ $table = $this->getRangeTableName();
+
+ $query = $db->select()
+ ->from(['o' => $table])
+ ->where('o.' . $this->objectIdColumn . ' = ?', (int) $this->object->get('id'))
+ ->order('o.range_key');
+
+ $class = $this->getRangeClass();
+ $this->ranges = $class::loadAll($connection, $query, 'range_key');
+ $this->setBeingLoadedFromDb();
+
+ return $this;
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ $this->storedRanges = [];
+
+ foreach ($this->ranges as $key => $range) {
+ $range->setBeingLoadedFromDb();
+ $this->storedRanges[$key] = clone($range);
+ }
+ $this->refreshIndex();
+ $this->modified = false;
+ }
+
+ public function store()
+ {
+ $db = $this->object->getConnection();
+ if (! $this->hasBeenModified()) {
+ return false;
+ }
+
+ $table = $this->getRangeTableName();
+ $objectId = (int) $this->object->get('id');
+ $idColumn = $this->objectIdColumn;
+ foreach ($this->ranges as $range) {
+ if ($range->hasBeenModified()) {
+ $range->setConnection($db);
+ if ((int) $range->get($idColumn) !== $objectId) {
+ $range->set($idColumn, $objectId);
+ }
+ }
+ }
+
+ foreach (array_diff(array_keys($this->storedRanges), array_keys($this->ranges)) as $delete) {
+ $range = $this->storedRanges[$delete];
+ $range->setConnection($db);
+ $range->set($idColumn, $objectId);
+ $db->getDbAdapter()->delete($table, $range->createWhere());
+ unset($this->ranges[$delete]);
+ }
+ foreach ($this->ranges as $range) {
+ $range->store();
+ }
+ $this->setBeingLoadedFromDb();
+
+ return true;
+ }
+
+ /**
+ * @return IcingaTimePeriodRange|IcingaScheduledDowntimeRange|string IDE hint
+ */
+ protected function getRangeClass()
+ {
+ return $this->rangeClass;
+ }
+
+ public static function loadForStoredObject(IcingaObject $object)
+ {
+ $ranges = new static($object);
+ return $ranges->loadFromDb();
+ }
+
+ public function toConfigString()
+ {
+ if (empty($this->ranges) && $this->object->object_type === 'template') {
+ return '';
+ }
+
+ $string = " ranges = {\n";
+
+ foreach ($this->ranges as $range) {
+ $string .= sprintf(
+ " %s\t= %s\n",
+ c::renderString($range->range_key),
+ c::renderString($range->range_value)
+ );
+ }
+
+ return $string . " }\n";
+ }
+
+ abstract public function toLegacyConfigString();
+
+ public function __toString()
+ {
+ try {
+ return $this->toConfigString();
+ } catch (Exception $e) {
+ trigger_error($e);
+ $previousHandler = set_exception_handler(
+ function () {
+ }
+ );
+ restore_error_handler();
+ if ($previousHandler !== null) {
+ call_user_func($previousHandler, $e);
+ die();
+ } else {
+ die($e->getMessage());
+ }
+ }
+ }
+}
diff --git a/library/Director/Objects/IcingaRelatedObject.php b/library/Director/Objects/IcingaRelatedObject.php
new file mode 100644
index 0000000..d35bcb0
--- /dev/null
+++ b/library/Director/Objects/IcingaRelatedObject.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+/**
+ * Related Object
+ *
+ * This class comes in handy when working with simple foreign key references. In
+ * contrast to an ORM it helps to deal with lazy-loaded objects in a way allowing
+ * us to render objects with references which to no longer (or not yet) exist
+ */
+class IcingaRelatedObject
+{
+ /** @var IcingaObject Main object with (optional) relation */
+ protected $owner;
+
+ /** @var int Related object id */
+ protected $id;
+
+ /** @var int Related object name */
+ protected $name;
+
+ /** @var int Relation property name, e.g. 'host' */
+ protected $key;
+
+ /** @var int Relation property, e.g. 'host_id' */
+ protected $idKey;
+
+ /** @var IcingaObject Related object once loaded */
+ protected $object;
+
+ /** @var string Related class name */
+ protected $className;
+
+ /**
+ * IcingaRelatedObject constructor
+ *
+ * @param IcingaObject $owner Main object referring a related one
+ * @param string $key Main objects (short) property name for this
+ */
+ public function __construct(IcingaObject $owner, $key)
+ {
+ $this->owner = $owner;
+ $this->key = $key;
+ $this->idKey = $key . '_id';
+ }
+
+ /**
+ * Set a specific id
+ *
+ * @param $id int
+ *
+ * @return self
+ */
+ public function setId($id)
+ {
+ if (! is_int($id)) {
+ throw new ProgrammingError(
+ 'An id must be an integer'
+ );
+ }
+
+ if ($this->object !== null) {
+ if ($this->object->id === $id) {
+ return $this;
+ } else {
+ $this->object = null;
+ }
+ }
+
+ if ($this->object === null) {
+ $this->name = null;
+ }
+
+ $this->id = $id;
+ $this->owner->set($this->getRealPropertyName(), $id);
+
+ return $this;
+ }
+
+ /**
+ * Return the related objects id
+ *
+ * @return int
+ */
+ public function getId()
+ {
+ if ($this->id === null) {
+ $this->id = $this->getObject()->id;
+ }
+
+ return $this->id;
+ }
+
+ /**
+ * Lazy-load the related object
+ *
+ * @return IcingaObject
+ */
+ public function getObject()
+ {
+ // TODO: This is unfinished
+
+ if ($this->object === null) {
+ $class = $this->getClassName();
+
+ if ($this->name === null) {
+ if ($id = $this->getId()) {
+ }
+ } else {
+ $this->object = $class::load($this->name, $this->owner->getConnection());
+ }
+ }
+ return $this->object;
+ }
+
+ /**
+ * The real property name pointing to this relation, e.g. 'host_id'
+ *
+ * @return string
+ */
+ public function getRealPropertyName()
+ {
+ return $this->key . '_id';
+ }
+
+ /**
+ * Full related class name
+ *
+ * @return string
+ */
+ public function getClassName()
+ {
+ if ($this->className === null) {
+ $this->className = __NAMESPACE__ . '\\' . $this->getShortClassName();
+ }
+
+ return $this->className;
+ }
+
+ /**
+ * Related class name relative to Icinga\Module\Director\Objects
+ *
+ * @return string
+ */
+ public function getShortClassName()
+ {
+ return $this->owner->getRelationObjectClass($this->key);
+ }
+
+ /**
+ * Set a related property
+ *
+ * This might be a string or an object
+ * @param $related string|IcingaObject
+ * @throws ProgrammingError
+ *
+ * return self
+ */
+ public function set($related)
+ {
+ if (is_string($related)) {
+ $this->name = $related;
+ } elseif (is_object($related)) {
+ $className = $this->getClassName();
+ if ($related instanceof $className) {
+ $this->object = $related;
+ $this->name = $object->object_name;
+ $this->id = $object->id;
+ } else {
+ throw new ProgrammingError(
+ 'Trying to set a related "%s" while expecting "%s"',
+ get_class($related),
+ $this->getShortClassName()
+ );
+ }
+ } else {
+ throw new ProgrammingError(
+ 'Related object can be name or object, got: %s',
+ var_export($related, 1)
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the name of the related object
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ if ($this->name === null) {
+ return $this->owner->{$this->key};
+ } else {
+ return $this->name;
+ }
+ }
+
+ /**
+ * Conservative constructor to avoid issued with PHP GC
+ */
+ public function __destruct()
+ {
+ unset($this->owner);
+ }
+}
diff --git a/library/Director/Objects/IcingaScheduledDowntime.php b/library/Director/Objects/IcingaScheduledDowntime.php
new file mode 100644
index 0000000..7fc3f78
--- /dev/null
+++ b/library/Director/Objects/IcingaScheduledDowntime.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use RuntimeException;
+
+class IcingaScheduledDowntime extends IcingaObject
+{
+ protected $table = 'icinga_scheduled_downtime';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'zone_id' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'author' => null,
+ 'comment' => null,
+ 'fixed' => null,
+ 'duration' => null,
+ 'apply_to' => null,
+ 'assign_filter' => null,
+ 'with_services' => null,
+ ];
+
+ protected $uuidColumn = 'uuid';
+
+ protected $supportsImports = true;
+
+ protected $supportsRanges = true;
+
+ protected $supportsApplyRules = true;
+
+ protected $relations = [
+ 'zone' => 'IcingaZone',
+ ];
+
+ protected $booleans = [
+ 'fixed' => 'fixed',
+ ];
+
+ protected $intervalProperties = [
+ 'duration' => 'duration',
+ ];
+
+ protected $propertiesNotForRendering = [
+ 'id',
+ 'apply_to',
+ 'object_name',
+ 'object_type',
+ 'with_services',
+ ];
+
+ /**
+ * @return string
+ */
+ protected function renderObjectHeader()
+ {
+ if ($this->isApplyRule()) {
+ if (($to = $this->get('apply_to')) === null) {
+ throw new RuntimeException(sprintf(
+ 'Applied notification "%s" has no valid object type',
+ $this->getObjectName()
+ ));
+ }
+
+ return sprintf(
+ "%s %s %s to %s {\n",
+ $this->getObjectTypeName(),
+ $this->getType(),
+ c::renderString($this->getObjectName()),
+ ucfirst($to)
+ );
+ } else {
+ return parent::renderObjectHeader();
+ }
+ }
+
+ public function getOnDeleteUrl()
+ {
+ if ($this->isApplyRule()) {
+ return 'director/scheduled-downtimes/applyrules';
+ } elseif ($this->isTemplate()) {
+ return 'director/scheduled-downtimes/templates';
+ } else {
+ return 'director/scheduled-downtimes';
+ }
+ }
+
+ public function isActive($now = null)
+ {
+ if ($now === null) {
+ $now = time();
+ }
+
+ foreach ($this->ranges()->getRanges() as $range) {
+ if ($range->isActive($now)) {
+ return true;
+ }
+ }
+
+ // TODO: no range currently means (and renders) "never", Icinga behaves
+ // different. Figure out whether and how we should support this
+ return false;
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderSuffix()
+ {
+ if ($this->get('with_services') === 'y' && $this->get('apply_to') === 'host') {
+ return parent::renderSuffix() . $this->renderCloneForServices();
+ } else {
+ return parent::renderSuffix();
+ }
+ }
+
+ protected function prefersGlobalZone()
+ {
+ return false;
+ }
+
+ protected function renderCloneForServices()
+ {
+ $services = clone($this);
+ $services
+ ->set('with_services', 'n')
+ ->set('apply_to', 'service');
+
+ return $services->toConfigString();
+ }
+}
diff --git a/library/Director/Objects/IcingaScheduledDowntimeRange.php b/library/Director/Objects/IcingaScheduledDowntimeRange.php
new file mode 100644
index 0000000..6280990
--- /dev/null
+++ b/library/Director/Objects/IcingaScheduledDowntimeRange.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+
+class IcingaScheduledDowntimeRange extends DbObject
+{
+ protected $keyName = ['scheduled_downtime_id', 'range_key', 'range_type'];
+
+ protected $table = 'icinga_scheduled_downtime_range';
+
+ protected $defaultProperties = [
+ 'scheduled_downtime_id' => null,
+ 'range_key' => null,
+ 'range_value' => null,
+ 'range_type' => 'include',
+ 'merge_behaviour' => 'set',
+ ];
+
+ public function isActive($now = null)
+ {
+ if ($now === null) {
+ $now = time();
+ }
+
+ if (false === ($weekDay = $this->getWeekDay($this->get('range_key')))) {
+ // TODO, dates are not yet supported
+ return false;
+ }
+
+ if ((int) date('w', $now) !== $weekDay) {
+ return false;
+ }
+
+ $timeRanges = preg_split('/\s*,\s*/', $this->get('range_value'), -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($timeRanges as $timeRange) {
+ if ($this->timeRangeIsActive($timeRange, $now)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function timeRangeIsActive($rangeString, $now)
+ {
+ $hBegin = $mBegin = $hEnd = $mEnd = null;
+ if (sscanf($rangeString, '%2d:%2d-%2d:%2d', $hBegin, $mBegin, $hEnd, $mEnd) === 4) {
+ if ($this->timeFromHourMin($hBegin, $mBegin, $now) <= $now
+ && $this->timeFromHourMin($hEnd, $mEnd, $now) >= $now
+ ) {
+ return true;
+ }
+ } else {
+ // TODO: throw exception?
+ }
+
+ return false;
+ }
+
+ protected function timeFromHourMin($hour, $min, $now)
+ {
+ return strtotime(sprintf('%s %02d:%02d:00', date('Y-m-d', $now), $hour, $min));
+ }
+
+ protected function getWeekDay($day)
+ {
+ switch ($day) {
+ case 'sunday':
+ return 0;
+ case 'monday':
+ return 1;
+ case 'tuesday':
+ return 2;
+ case 'wednesday':
+ return 3;
+ case 'thursday':
+ return 4;
+ case 'friday':
+ return 5;
+ case 'saturday':
+ return 6;
+ }
+
+ return false;
+ }
+}
diff --git a/library/Director/Objects/IcingaScheduledDowntimeRanges.php b/library/Director/Objects/IcingaScheduledDowntimeRanges.php
new file mode 100644
index 0000000..ac8483e
--- /dev/null
+++ b/library/Director/Objects/IcingaScheduledDowntimeRanges.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Countable;
+use Iterator;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
+
+class IcingaScheduledDowntimeRanges extends IcingaRanges implements Iterator, Countable, IcingaConfigRenderer
+{
+ protected $rangeClass = IcingaScheduledDowntimeRange::class;
+ protected $objectIdColumn = 'scheduled_downtime_id';
+
+ public function toLegacyConfigString()
+ {
+ return '';
+ }
+}
diff --git a/library/Director/Objects/IcingaService.php b/library/Director/Objects/IcingaService.php
new file mode 100644
index 0000000..9479ef7
--- /dev/null
+++ b/library/Director/Objects/IcingaService.php
@@ -0,0 +1,828 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Data\PropertiesFilter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+use Icinga\Module\Director\Objects\Extension\FlappingSupport;
+use Icinga\Module\Director\Resolver\HostServiceBlacklist;
+use InvalidArgumentException;
+use RuntimeException;
+
+class IcingaService extends IcingaObject implements ExportInterface
+{
+ use FlappingSupport;
+
+ protected $table = 'icinga_service';
+
+ protected $uuidColumn = 'uuid';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'display_name' => null,
+ 'host_id' => null,
+ 'service_set_id' => null,
+ 'check_command_id' => null,
+ 'max_check_attempts' => null,
+ 'check_period_id' => null,
+ 'check_interval' => null,
+ 'retry_interval' => null,
+ 'check_timeout' => null,
+ 'enable_notifications' => null,
+ 'enable_active_checks' => null,
+ 'enable_passive_checks' => null,
+ 'enable_event_handler' => null,
+ 'enable_flapping' => null,
+ 'enable_perfdata' => null,
+ 'event_command_id' => null,
+ 'flapping_threshold_high' => null,
+ 'flapping_threshold_low' => null,
+ 'volatile' => null,
+ 'zone_id' => null,
+ 'command_endpoint_id' => null,
+ 'notes' => null,
+ 'notes_url' => null,
+ 'action_url' => null,
+ 'icon_image' => null,
+ 'icon_image_alt' => null,
+ 'use_agent' => null,
+ 'apply_for' => null,
+ 'use_var_overrides' => null,
+ 'assign_filter' => null,
+ 'template_choice_id' => null,
+ ];
+
+ protected $relations = [
+ 'host' => 'IcingaHost',
+ 'service_set' => 'IcingaServiceSet',
+ 'check_command' => 'IcingaCommand',
+ 'event_command' => 'IcingaCommand',
+ 'check_period' => 'IcingaTimePeriod',
+ 'command_endpoint' => 'IcingaEndpoint',
+ 'zone' => 'IcingaZone',
+ 'template_choice' => 'IcingaTemplateChoiceService',
+ ];
+
+ protected $booleans = [
+ 'enable_notifications' => 'enable_notifications',
+ 'enable_active_checks' => 'enable_active_checks',
+ 'enable_passive_checks' => 'enable_passive_checks',
+ 'enable_event_handler' => 'enable_event_handler',
+ 'enable_flapping' => 'enable_flapping',
+ 'enable_perfdata' => 'enable_perfdata',
+ 'volatile' => 'volatile',
+ 'use_agent' => 'use_agent',
+ 'use_var_overrides' => 'use_var_overrides',
+ ];
+
+ protected $intervalProperties = [
+ 'check_interval' => 'check_interval',
+ 'check_timeout' => 'check_timeout',
+ 'retry_interval' => 'retry_interval',
+ ];
+
+ protected $supportsGroups = true;
+
+ protected $supportsCustomVars = true;
+
+ protected $supportsFields = true;
+
+ protected $supportsImports = true;
+
+ protected $supportsApplyRules = true;
+
+ protected $supportsSets = true;
+
+ protected $supportsChoices = true;
+
+ protected $supportedInLegacy = true;
+
+ protected $keyName = ['host_id', 'service_set_id', 'object_name'];
+
+ protected $prioritizedProperties = ['host_id'];
+
+ protected $propertiesNotForRendering = [
+ 'id',
+ 'object_name',
+ 'object_type',
+ 'apply_for'
+ ];
+
+ /** @var ServiceGroupMembershipResolver */
+ protected $servicegroupMembershipResolver;
+
+ /**
+ * @return IcingaCommand
+ * @throws IcingaException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getCheckCommand()
+ {
+ $id = $this->getSingleResolvedProperty('check_command_id');
+ return IcingaCommand::loadWithAutoIncId(
+ $id,
+ $this->getConnection()
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function isApplyRule()
+ {
+ if ($this->hasBeenAssignedToHostTemplate()) {
+ return true;
+ }
+
+ return $this->hasProperty('object_type')
+ && $this->get('object_type') === 'apply';
+ }
+
+ /**
+ * @return bool
+ */
+ public function usesVarOverrides()
+ {
+ return $this->get('use_var_overrides') === 'y';
+ }
+
+ public function getUniqueIdentifier()
+ {
+ if ($this->isTemplate()) {
+ return $this->getObjectName();
+ } else {
+ throw new RuntimeException(
+ 'getUniqueIdentifier() is supported by Service Templates only'
+ );
+ }
+ }
+
+ /**
+ * @return object
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ // TODO: ksort in toPlainObject?
+ $props = (array) $this->toPlainObject();
+ $props['fields'] = $this->loadFieldReferences();
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return IcingaService
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ if ($properties['object_type'] !== 'template') {
+ throw new InvalidArgumentException(sprintf(
+ 'Can import only Templates, got "%s" for "%s"',
+ $properties['object_type'],
+ $name
+ ));
+ }
+ $key = [
+ 'object_type' => 'template',
+ 'object_name' => $name
+ ];
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Service Template "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ // $object->newFields = $properties['fields'];
+ unset($properties['fields']);
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader
+ * @return array
+ */
+ protected function loadFieldReferences()
+ {
+ $db = $this->getDb();
+
+ $res = $db->fetchAll(
+ $db->select()->from([
+ 'sf' => 'icinga_service_field'
+ ], [
+ 'sf.datafield_id',
+ 'sf.is_required',
+ 'sf.var_filter',
+ ])->join(['df' => 'director_datafield'], 'df.id = sf.datafield_id', [])
+ ->where('service_id = ?', $this->get('id'))
+ ->order('varname ASC')
+ );
+
+ if (empty($res)) {
+ return [];
+ } else {
+ foreach ($res as $field) {
+ $field->datafield_id = (int) $field->datafield_id;
+ }
+
+ return $res;
+ }
+ }
+
+ /**
+ * @param string $key
+ * @return $this
+ */
+ protected function setKey($key)
+ {
+ if (is_int($key)) {
+ $this->set('id', $key);
+ } elseif (is_array($key)) {
+ foreach (['id', 'host_id', 'service_set_id', 'object_name'] as $k) {
+ if (array_key_exists($k, $key)) {
+ $this->set($k, $key[$k]);
+ }
+ }
+ } else {
+ parent::setKey($key);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $name
+ * @return $this
+ * @codingStandardsIgnoreStart
+ */
+ protected function setObject_Name($name)
+ {
+ // @codingStandardsIgnoreEnd
+
+ if ($name === null && $this->isApplyRule()) {
+ $name = '';
+ }
+
+ return $this->reallySet('object_name', $name);
+ }
+
+ /**
+ * Render host_id as host_name
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderHost_id()
+ {
+ if ($this->hasBeenAssignedToHostTemplate()) {
+ return '';
+ }
+
+ return $this->renderRelationProperty('host', $this->get('host_id'), 'host_name');
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ protected function renderLegacyHost_id($value)
+ {
+ // @codingStandardsIgnoreEnd
+ if (is_array($value)) {
+ $blacklisted = $this->getBlacklistedHostnames();
+ $c = c1::renderKeyValue('host_name', c1::renderArray(array_diff($value, $blacklisted)));
+
+ // blacklisted in this (zoned) scope?
+ $bl = array_intersect($blacklisted, $value);
+ if (! empty($bl)) {
+ $c .= c1::renderKeyValue('# ignored on', c1::renderArray($bl));
+ }
+
+ return $c;
+ } else {
+ return parent::renderLegacyHost_id($value);
+ }
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @throws IcingaException
+ */
+ public function renderToLegacyConfig(IcingaConfig $config)
+ {
+ if ($this->get('service_set_id') !== null) {
+ return;
+ } elseif ($this->isApplyRule()) {
+ $this->renderLegacyApplyToConfig($config);
+ } else {
+ parent::renderToLegacyConfig($config);
+ }
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @throws IcingaException
+ */
+ protected function renderLegacyApplyToConfig(IcingaConfig $config)
+ {
+ $conn = $this->getConnection();
+
+ $assign_filter = $this->get('assign_filter');
+ $filter = Filter::fromQueryString($assign_filter);
+ $hostnames = HostApplyMatches::forFilter($filter, $conn);
+
+ $this->set('object_type', 'object');
+
+ foreach ($this->mapHostsToZones($hostnames) as $zone => $names) {
+ $blacklisted = $this->getBlacklistedHostnames();
+ $zoneNames = array_diff($names, $blacklisted);
+
+ $disabled = [];
+ foreach ($zoneNames as $name) {
+ if (IcingaHost::load($name, $this->getConnection())->isDisabled()) {
+ $disabled[] = $name;
+ }
+ }
+ $zoneNames = array_diff($zoneNames, $disabled);
+
+ if (empty($zoneNames)) {
+ continue;
+ }
+
+ $this->set('host_id', $zoneNames);
+
+ $config->configFile('director/' . $zone . '/service_apply', '.cfg')
+ ->addLegacyObject($this);
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function toLegacyConfigString()
+ {
+ if ($this->get('service_set_id') !== null) {
+ return '';
+ }
+
+ $str = parent::toLegacyConfigString();
+
+ if (! $this->isDisabled()
+ && $this->get('host_id')
+ && $this->getRelated('host')->isDisabled()
+ ) {
+ return "# --- This services host has been disabled ---\n"
+ . preg_replace('~^~m', '# ', trim($str))
+ . "\n\n";
+ } else {
+ return $str;
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function toConfigString()
+ {
+ if ($this->get('service_set_id')) {
+ return '';
+ }
+ $str = parent::toConfigString();
+
+ if (! $this->isDisabled()
+ && $this->get('host_id')
+ && $this->getRelated('host')->isDisabled()
+ ) {
+ return "/* --- This services host has been disabled ---\n"
+ // Do not allow strings to break our comment
+ . str_replace('*/', "* /", $str) . "*/\n";
+ } else {
+ return $str;
+ }
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderObjectHeader()
+ {
+ if ($this->isApplyRule()
+ && !$this->hasBeenAssignedToHostTemplate()
+ && $this->get('apply_for') !== null
+ ) {
+ $name = $this->getObjectName();
+ $extraName = '';
+
+ if (c::stringHasMacro($name)) {
+ $extraName = c::renderKeyValue('name', c::renderStringWithVariables($name));
+ $name = '';
+ } elseif ($name !== '') {
+ $name = ' ' . c::renderString($name);
+ }
+
+ return sprintf(
+ "%s %s%s for (config in %s) {\n",
+ $this->getObjectTypeName(),
+ $this->getType(),
+ $name,
+ $this->get('apply_for')
+ ) . $extraName;
+ }
+
+ return parent::renderObjectHeader();
+ }
+
+ /**
+ * @return string
+ */
+ protected function getLegacyObjectKeyName()
+ {
+ if ($this->isTemplate()) {
+ return 'name';
+ } else {
+ return 'service_description';
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasBeenAssignedToHostTemplate()
+ {
+ // Branches would fail
+ if ($this->properties['host_id'] === null) {
+ return null;
+ }
+ $hostId = $this->get('host_id');
+
+ return $hostId && $this->getRelatedObject(
+ 'host',
+ $hostId
+ )->isTemplate();
+ }
+
+ /**
+ * @return string
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\NestingError
+ */
+ protected function renderSuffix()
+ {
+ $suffix = '';
+ if ($this->isApplyRule()) {
+ $zoneName = $this->getRenderingZone();
+ if (!IcingaZone::zoneNameIsGlobal($zoneName, $this->connection)) {
+ $suffix .= c::renderKeyValue('zone', c::renderString($zoneName));
+ }
+ }
+
+ if ($this->isApplyRule() || $this->usesVarOverrides()) {
+ $suffix .= $this->renderImportHostVarOverrides();
+ }
+
+ return $suffix . parent::renderSuffix();
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderImportHostVarOverrides()
+ {
+ if (! $this->connection) {
+ throw new RuntimeException(
+ 'Cannot render services without an assigned DB connection'
+ );
+ }
+
+ return "\n import DirectorOverrideTemplate\n";
+ }
+
+ /**
+ * @return string
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function renderCustomExtensions()
+ {
+ $output = '';
+
+ if ($this->hasBeenAssignedToHostTemplate()) {
+ // TODO: use assignment renderer?
+ $filter = sprintf(
+ 'assign where %s in host.templates',
+ c::renderString($this->get('host'))
+ );
+
+ $output .= "\n " . $filter . "\n";
+ }
+
+ $blacklist = $this->getBlacklistedHostnames();
+ $blacklistedTemplates = [];
+ $blacklistedHosts = [];
+ foreach ($blacklist as $hostname) {
+ if (IcingaHost::load($hostname, $this->connection)->isTemplate()) {
+ $blacklistedTemplates[] = $hostname;
+ } else {
+ $blacklistedHosts[] = $hostname;
+ }
+ }
+ foreach ($blacklistedTemplates as $template) {
+ $output .= sprintf(
+ " ignore where %s in host.templates\n",
+ c::renderString($template)
+ );
+ }
+ if (! empty($blacklistedHosts)) {
+ if (count($blacklistedHosts) === 1) {
+ $output .= sprintf(
+ " ignore where host.name == %s\n",
+ c::renderString($blacklistedHosts[0])
+ );
+ } else {
+ $output .= sprintf(
+ " ignore where host.name in %s\n",
+ c::renderArray($blacklistedHosts)
+ );
+ }
+ }
+
+ // A hand-crafted command endpoint overrides use_agent
+ if ($this->get('command_endpoint_id') !== null) {
+ return $output;
+ }
+
+ if ($this->get('use_agent') === 'y') {
+ // When feature flag feature_custom_endpoint is enabled, render additional code
+ if ($this->connection->settings()->get('feature_custom_endpoint') === 'y') {
+ return $output . "
+ // Set command_endpoint dynamically with Director
+ if (!host) {
+ var host = get_host(host_name)
+ }
+ if (host.vars._director_custom_endpoint_name) {
+ command_endpoint = host.vars._director_custom_endpoint_name
+ } else {
+ command_endpoint = host_name
+ }
+";
+ } else {
+ return $output . c::renderKeyValue('command_endpoint', 'host_name');
+ }
+ } elseif ($this->get('use_agent') === 'n') {
+ return $output . c::renderKeyValue('command_endpoint', c::renderPhpValue(null));
+ } else {
+ return $output;
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getBlacklistedHostnames()
+ {
+ // Hint: if ($this->isApplyRule()) would be nice, but apply rules are
+ // not enough, one might want to blacklist single services from Sets
+ // assigned to single Hosts.
+ if (PrefetchCache::shouldBeUsed()) {
+ $lookup = PrefetchCache::instance()->hostServiceBlacklist();
+ } else {
+ $lookup = new HostServiceBlacklist($this->getConnection());
+ }
+
+ return $lookup->getBlacklistedHostnamesForService($this);
+ }
+
+ /**
+ * Do not render internal property
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderUse_agent()
+ {
+ return '';
+ }
+
+ public function renderUse_var_overrides()
+ {
+ return '';
+ }
+
+ protected function renderTemplate_choice_id()
+ {
+ return '';
+ }
+
+ protected function renderLegacyDisplay_Name()
+ {
+ // @codingStandardsIgnoreEnd
+ return c1::renderKeyValue('display_name', $this->get('display_name'));
+ }
+
+ public function hasCheckCommand()
+ {
+ return $this->getSingleResolvedProperty('check_command_id') !== null;
+ }
+
+ public function getOnDeleteUrl()
+ {
+ if ($this->get('host_id')) {
+ return 'director/host/services?name=' . rawurlencode($this->get('host'));
+ } elseif ($this->get('service_set_id')) {
+ return 'director/serviceset/services?name=' . rawurlencode($this->get('service_set'));
+ } else {
+ return parent::getOnDeleteUrl();
+ }
+ }
+
+ protected function getDefaultZone(IcingaConfig $config = null)
+ {
+ if ($this->get('host_id') === null) {
+ return parent::getDefaultZone();
+ } else {
+ return $this->getRelatedObject('host', $this->get('host_id'))
+ ->getRenderingZone($config);
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function createWhere()
+ {
+ $where = parent::createWhere();
+ if (! $this->hasBeenLoadedFromDb()) {
+ if (null === $this->get('service_set_id')
+ && null === $this->get('host_id')
+ && null === $this->get('id')
+ ) {
+ $where .= " AND object_type = 'template'";
+ }
+ }
+
+ return $where;
+ }
+
+
+ /**
+ * TODO: Duplicate code, clean this up, split it into multiple methods
+ * @param Db|null $connection
+ * @param string $prefix
+ * @param null $filter
+ * @return array
+ */
+ public static function enumProperties(
+ Db $connection = null,
+ $prefix = '',
+ $filter = null
+ ) {
+ $serviceProperties = [];
+ if ($filter === null) {
+ $filter = new PropertiesFilter();
+ }
+ $realProperties = static::create()->listProperties();
+ sort($realProperties);
+
+ if ($filter->match(PropertiesFilter::$SERVICE_PROPERTY, 'name')) {
+ $serviceProperties[$prefix . 'name'] = 'name';
+ }
+ foreach ($realProperties as $prop) {
+ if (!$filter->match(PropertiesFilter::$SERVICE_PROPERTY, $prop)) {
+ continue;
+ }
+
+ if (substr($prop, -3) === '_id') {
+ if ($prop === 'template_choice_id') {
+ continue;
+ }
+ $prop = substr($prop, 0, -3);
+ }
+
+ $serviceProperties[$prefix . $prop] = $prop;
+ }
+
+ $serviceVars = [];
+
+ if ($connection !== null) {
+ foreach ($connection->fetchDistinctServiceVars() as $var) {
+ if ($filter->match(PropertiesFilter::$CUSTOM_PROPERTY, $var->varname, $var)) {
+ if ($var->datatype) {
+ $serviceVars[$prefix . 'vars.' . $var->varname] = sprintf(
+ '%s (%s)',
+ $var->varname,
+ $var->caption
+ );
+ } else {
+ $serviceVars[$prefix . 'vars.' . $var->varname] = $var->varname;
+ }
+ }
+ }
+ }
+
+ //$properties['vars.*'] = 'Other custom variable';
+ ksort($serviceVars);
+
+ $props = mt('director', 'Service properties');
+ $vars = mt('director', 'Custom variables');
+
+ $properties = [];
+ if (!empty($serviceProperties)) {
+ $properties[$props] = $serviceProperties;
+ $properties[$props][$prefix . 'groups'] = 'Groups';
+ }
+
+ if (!empty($serviceVars)) {
+ $properties[$vars] = $serviceVars;
+ }
+
+ $hostProps = mt('director', 'Host properties');
+ $hostVars = mt('director', 'Host Custom variables');
+
+ $hostProperties = IcingaHost::enumProperties($connection, 'host.');
+
+ if (array_key_exists($hostProps, $hostProperties)) {
+ $p = $hostProperties[$hostProps];
+ if (!empty($p)) {
+ $properties[$hostProps] = $p;
+ }
+ }
+
+ if (array_key_exists($vars, $hostProperties)) {
+ $p = $hostProperties[$vars];
+ if (!empty($p)) {
+ $properties[$hostVars] = $p;
+ }
+ }
+
+ return $properties;
+ }
+
+ protected function beforeStore()
+ {
+ parent::beforeStore();
+ if ($this->isObject()
+ && $this->get('service_set_id') === null
+ && $this->get('host_id') === null
+ ) {
+ throw new InvalidArgumentException(
+ 'Cannot store a Service object without a related host or set: ' . $this->getObjectName()
+ );
+ }
+ }
+
+ protected function notifyResolvers()
+ {
+ $resolver = $this->getServiceGroupMembershipResolver();
+ $resolver->addObject($this);
+ $resolver->refreshDb();
+
+ return $this;
+ }
+
+ protected function getServiceGroupMembershipResolver()
+ {
+ if ($this->servicegroupMembershipResolver === null) {
+ $this->servicegroupMembershipResolver = new ServiceGroupMembershipResolver(
+ $this->getConnection()
+ );
+ }
+
+ return $this->servicegroupMembershipResolver;
+ }
+
+ public function setServiceGroupMembershipResolver(ServiceGroupMembershipResolver $resolver)
+ {
+ $this->servicegroupMembershipResolver = $resolver;
+ return $this;
+ }
+}
diff --git a/library/Director/Objects/IcingaServiceAssignment.php b/library/Director/Objects/IcingaServiceAssignment.php
new file mode 100644
index 0000000..9910f7c
--- /dev/null
+++ b/library/Director/Objects/IcingaServiceAssignment.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaServiceAssignment extends IcingaObject
+{
+ protected $table = 'icinga_service_assignment';
+
+ protected $keyName = 'id';
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'service_id' => null,
+ 'filter_string' => null,
+ );
+
+ protected $relations = array(
+ 'service' => 'IcingaService',
+ );
+}
diff --git a/library/Director/Objects/IcingaServiceField.php b/library/Director/Objects/IcingaServiceField.php
new file mode 100644
index 0000000..c43ec2d
--- /dev/null
+++ b/library/Director/Objects/IcingaServiceField.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaServiceField extends IcingaObjectField
+{
+ protected $keyName = array('service_id', 'datafield_id');
+
+ protected $table = 'icinga_service_field';
+
+ protected $defaultProperties = array(
+ 'service_id' => null,
+ 'datafield_id' => null,
+ 'is_required' => null,
+ 'var_filter' => null,
+ );
+}
diff --git a/library/Director/Objects/IcingaServiceGroup.php b/library/Director/Objects/IcingaServiceGroup.php
new file mode 100644
index 0000000..ae43ff3
--- /dev/null
+++ b/library/Director/Objects/IcingaServiceGroup.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaServiceGroup extends IcingaObjectGroup
+{
+ protected $table = 'icinga_servicegroup';
+
+ /** @var ServiceGroupMembershipResolver */
+ protected $servicegroupMembershipResolver;
+
+ public function supportsAssignments()
+ {
+ return true;
+ }
+
+ protected function getServiceGroupMembershipResolver()
+ {
+ if ($this->servicegroupMembershipResolver === null) {
+ $this->servicegroupMembershipResolver = new ServiceGroupMembershipResolver(
+ $this->getConnection()
+ );
+ }
+
+ return $this->servicegroupMembershipResolver;
+ }
+
+ public function setServiceGroupMembershipResolver(ServiceGroupMembershipResolver $resolver)
+ {
+ $this->servicegroupMembershipResolver = $resolver;
+ return $this;
+ }
+
+ protected function notifyResolvers()
+ {
+ $resolver = $this->getServiceGroupMembershipResolver();
+ $resolver->addGroup($this);
+ $resolver->refreshDb();
+
+ return $this;
+ }
+}
diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php
new file mode 100644
index 0000000..8217a59
--- /dev/null
+++ b/library/Director/Objects/IcingaServiceSet.php
@@ -0,0 +1,591 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Data\Db\ServiceSetQueryBuilder;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Resolver\HostServiceBlacklist;
+use InvalidArgumentException;
+use Ramsey\Uuid\Uuid;
+use RuntimeException;
+
+class IcingaServiceSet extends IcingaObject implements ExportInterface
+{
+ protected $table = 'icinga_service_set';
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'uuid' => null,
+ 'host_id' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'description' => null,
+ 'assign_filter' => null,
+ );
+
+ protected $uuidColumn = 'uuid';
+
+ protected $keyName = array('host_id', 'object_name');
+
+ protected $supportsImports = true;
+
+ protected $supportsCustomVars = true;
+
+ protected $supportsApplyRules = true;
+
+ protected $supportedInLegacy = true;
+
+ protected $relations = array(
+ 'host' => 'IcingaHost',
+ );
+
+ public function isDisabled()
+ {
+ return false;
+ }
+
+ public function supportsAssignments()
+ {
+ return true;
+ }
+
+ protected function setKey($key)
+ {
+ if (is_int($key)) {
+ $this->set('id', $key);
+ } elseif (is_string($key)) {
+ $keyComponents = preg_split('~!~', $key);
+ if (count($keyComponents) === 1) {
+ $this->set('object_name', $keyComponents[0]);
+ $this->set('object_type', 'template');
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'Can not parse key: %s',
+ $key
+ ));
+ }
+ } else {
+ return parent::setKey($key);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return IcingaService[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getServiceObjects()
+ {
+ // don't try to resolve services for unstored objects - as in getServiceObjectsForSet()
+ // also for diff in activity log
+ if ($this->get('id') === null) {
+ return [];
+ }
+
+ if ($this->get('host_id')) {
+ $imports = $this->imports()->getObjects();
+ if (empty($imports)) {
+ return array();
+ }
+ $parent = array_shift($imports);
+ assert($parent instanceof IcingaServiceSet);
+ return $this->getServiceObjectsForSet($parent);
+ } else {
+ return $this->getServiceObjectsForSet($this);
+ }
+ }
+
+ /**
+ * @param IcingaServiceSet $set
+ * @return IcingaService[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getServiceObjectsForSet(IcingaServiceSet $set)
+ {
+ $connection = $this->getConnection();
+ if (self::$dbObjectStore !== null) {
+ $branchUuid = self::$dbObjectStore->getBranch()->getUuid();
+ } else {
+ $branchUuid = null;
+ }
+
+ $builder = new ServiceSetQueryBuilder($connection, $branchUuid);
+ return $builder->fetchServicesWithQuery($builder->selectServicesForSet($set));
+ }
+
+ public function getUniqueIdentifier()
+ {
+ return $this->getObjectName();
+ }
+
+ /**
+ * @return object
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ if ($this->get('host_id')) {
+ $result = $this->exportSetOnHost();
+ } else {
+ $result = $this->exportTemplate();
+ }
+
+ unset($result->uuid);
+ return $result;
+ }
+
+ protected function exportSetOnHost()
+ {
+ // TODO.
+ throw new RuntimeException('Not yet');
+ }
+
+ /**
+ * @return object
+ * @deprecated
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function exportTemplate()
+ {
+ $props = $this->getProperties();
+ unset($props['id'], $props['host_id']);
+ $props['services'] = [];
+ foreach ($this->getServiceObjects() as $serviceObject) {
+ $props['services'][$serviceObject->getObjectName()] = $serviceObject->export();
+ }
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return IcingaServiceSet
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ if (isset($properties['services'])) {
+ $services = $properties['services'];
+ unset($properties['services']);
+ } else {
+ $services = [];
+ }
+
+ if ($properties['object_type'] !== 'template') {
+ throw new InvalidArgumentException(sprintf(
+ 'Can import only Templates, got "%s" for "%s"',
+ $properties['object_type'],
+ $name
+ ));
+ }
+ if ($replace && static::exists($name, $db)) {
+ $object = static::load($name, $db);
+ } elseif (static::exists($name, $db)) {
+ throw new DuplicateKeyException(
+ 'Service Set "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ $object->setProperties($properties);
+
+ // This is not how other imports work, but here we need an ID
+ if (! $object->hasBeenLoadedFromDb()) {
+ $object->store();
+ }
+
+ $setId = $object->get('id');
+ $sQuery = $db->getDbAdapter()->select()->from(
+ ['s' => 'icinga_service'],
+ 's.*'
+ )->where('service_set_id = ?', $setId);
+ $existingServices = IcingaService::loadAll($db, $sQuery, 'object_name');
+ $serviceNames = [];
+ foreach ($services as $service) {
+ if (isset($service->fields)) {
+ unset($service->fields);
+ }
+ $name = $service->object_name;
+ $serviceNames[] = $name;
+ if (isset($existingServices[$name])) {
+ $existing = $existingServices[$name];
+ $existing->setProperties((array) $service);
+ $existing->set('service_set_id', $setId);
+ if ($existing->hasBeenModified()) {
+ $existing->store();
+ }
+ } else {
+ $new = IcingaService::create((array) $service, $db);
+ $new->set('service_set_id', $setId);
+ $new->store();
+ }
+ }
+
+ foreach ($existingServices as $existing) {
+ if (!in_array($existing->getObjectName(), $serviceNames)) {
+ $existing->delete();
+ }
+ }
+
+ return $object;
+ }
+
+ public function beforeDelete()
+ {
+ // check if this is a template, or directly assigned to a host
+ if ($this->get('host_id') === null) {
+ // find all host sets and delete them
+ foreach ($this->fetchHostSets() as $set) {
+ $set->delete();
+ }
+ }
+
+ parent::beforeDelete();
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function onDelete()
+ {
+ $hostId = $this->get('host_id');
+ if ($hostId) {
+ $deleteIds = [];
+ foreach ($this->getServiceObjects() as $service) {
+ if ($idToDelete = $service->get('id')) {
+ $deleteIds[] = (int) $idToDelete;
+ }
+ }
+
+ if (! empty($deleteIds)) {
+ $db = $this->getDb();
+ $db->delete(
+ 'icinga_host_service_blacklist',
+ $db->quoteInto(
+ sprintf('host_id = %s AND service_id IN (?)', $hostId),
+ $deleteIds
+ )
+ );
+ }
+ }
+
+ parent::onDelete();
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function renderToConfig(IcingaConfig $config)
+ {
+ // always print the header, so you have minimal info present
+ $file = $this->getConfigFileWithHeader($config);
+
+ if ($this->get('assign_filter') === null && $this->isTemplate()) {
+ return;
+ }
+
+ if ($config->isLegacy()) {
+ $this->renderToLegacyConfig($config);
+ return;
+ }
+
+ $services = $this->getServiceObjects();
+ if (empty($services)) {
+ return;
+ }
+
+ // Loop over all services belonging to this set
+ // add our assign rules and then add the service to the config
+ // eventually clone them beforehand to not get into trouble with caches
+ // figure out whether we might need a zone property
+ foreach ($services as $service) {
+ if ($filter = $this->get('assign_filter')) {
+ $service->set('object_type', 'apply');
+ $service->set('assign_filter', $filter);
+ } elseif ($hostId = $this->get('host_id')) {
+ $host = $this->getRelatedObject('host', $hostId)->getObjectName();
+ if (in_array($host, $this->getBlacklistedHostnames($service))) {
+ continue;
+ }
+ $service->set('object_type', 'object');
+ $service->set('use_var_overrides', 'y');
+ $service->set('host_id', $hostId);
+ } else {
+ // Service set template without assign filter or host
+ continue;
+ }
+
+ $this->copyVarsToService($service);
+ $file->addObject($service);
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getBlacklistedHostnames($service)
+ {
+ // Hint: if ($this->isApplyRule()) would be nice, but apply rules are
+ // not enough, one might want to blacklist single services from Sets
+ // assigned to single Hosts.
+ if (PrefetchCache::shouldBeUsed()) {
+ $lookup = PrefetchCache::instance()->hostServiceBlacklist();
+ } else {
+ $lookup = new HostServiceBlacklist($this->getConnection());
+ }
+
+ return $lookup->getBlacklistedHostnamesForService($service);
+ }
+
+ protected function getConfigFileWithHeader(IcingaConfig $config)
+ {
+ $file = $config->configFile(
+ 'zones.d/' . $this->getRenderingZone($config) . '/servicesets'
+ );
+
+ $file->addContent($this->getConfigHeaderComment($config));
+ return $file;
+ }
+
+ protected function getConfigHeaderComment(IcingaConfig $config)
+ {
+ $name = $this->getObjectName();
+ $assign = $this->get('assign_filter');
+
+ if ($config->isLegacy()) {
+ if ($assign !== null) {
+ return "## applied Service Set '${name}'\n\n";
+ } else {
+ return "## Service Set '${name}' on this host\n\n";
+ }
+ } else {
+ $comment = [
+ "Service Set: ${name}",
+ ];
+
+ if (($host = $this->get('host')) !== null) {
+ $comment[] = 'on host ' . $host;
+ }
+
+ if (($description = $this->get('description')) !== null) {
+ $comment[] = '';
+ foreach (preg_split('~\\n~', $description) as $line) {
+ $comment[] = $line;
+ }
+ }
+
+ if ($assign !== null) {
+ $comment[] = '';
+ $comment[] = trim($this->renderAssign_Filter());
+ }
+
+ return "/**\n * " . join("\n * ", $comment) . "\n */\n\n";
+ }
+ }
+
+ public function copyVarsToService(IcingaService $service)
+ {
+ $serviceVars = $service->vars();
+
+ foreach ($this->vars() as $k => $var) {
+ $serviceVars->$k = $var;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function renderToLegacyConfig(IcingaConfig $config)
+ {
+ if ($this->get('assign_filter') === null && $this->isTemplate()) {
+ return;
+ }
+
+ // evaluate my assign rules once, get related hosts
+ // Loop over all services belonging to this set
+ // generate every service with host_name host1,host2... -> not yet. And Zones?
+
+ $conn = $this->getConnection();
+
+ // Delegating this to the service would look, but this way it's faster
+ if ($filter = $this->get('assign_filter')) {
+ $filter = Filter::fromQueryString($filter);
+
+ $hostnames = HostApplyMatches::forFilter($filter, $conn);
+ } else {
+ $hostnames = array($this->getRelated('host')->getObjectName());
+ }
+
+ $blacklists = [];
+
+ foreach ($this->mapHostsToZones($hostnames) as $zone => $names) {
+ $file = $config->configFile('director/' . $zone . '/servicesets', '.cfg');
+ $file->addContent($this->getConfigHeaderComment($config));
+
+ foreach ($this->getServiceObjects() as $service) {
+ $object_name = $service->getObjectName();
+
+ if (! array_key_exists($object_name, $blacklists)) {
+ $blacklists[$object_name] = $service->getBlacklistedHostnames();
+ }
+
+ // check if all hosts in the zone ignore this service
+ $zoneNames = array_diff($names, $blacklists[$object_name]);
+
+ $disabled = [];
+ foreach ($zoneNames as $name) {
+ if (IcingaHost::load($name, $this->getConnection())->isDisabled()) {
+ $disabled[] = $name;
+ }
+ }
+ $zoneNames = array_diff($zoneNames, $disabled);
+
+ if (empty($zoneNames)) {
+ continue;
+ }
+
+ $service->set('object_type', 'object');
+ $service->set('host_id', $names);
+
+ $this->copyVarsToService($service);
+
+ $file->addLegacyObject($service);
+ }
+ }
+ }
+
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ if ($this->get('host_id') === null) {
+ if ($hostname = $this->get('host')) {
+ $host = IcingaHost::load($hostname, $this->getConnection());
+ } else {
+ return $this->connection->getDefaultGlobalZoneName();
+ }
+ } else {
+ $host = $this->getRelatedObject('host', $this->get('host_id'));
+ }
+
+ return $host->getRenderingZone($config);
+ }
+
+ public function createWhere()
+ {
+ $where = parent::createWhere();
+ if (! $this->hasBeenLoadedFromDb()) {
+ if (null === $this->get('host_id') && null === $this->get('id')) {
+ $where .= " AND object_type = 'template'";
+ }
+ }
+
+ return $where;
+ }
+
+ /**
+ * @return IcingaService[]
+ */
+ public function fetchServices()
+ {
+ if ($store = self::$dbObjectStore) {
+ $uuid = $store->getBranch()->getUuid();
+ } else {
+ $uuid = null;
+ }
+ $builder = new ServiceSetQueryBuilder($this->getConnection(), $uuid);
+ return $builder->fetchServicesWithQuery($builder->selectServicesForSet($this));
+ }
+
+ /**
+ * Fetch IcingaServiceSet that are based on this set and added to hosts directly
+ *
+ * @return IcingaServiceSet[]
+ */
+ public function fetchHostSets()
+ {
+ $id = $this->get('id');
+ if ($id === null) {
+ return [];
+ }
+
+ $query = $this->db->select()
+ ->from(
+ ['o' => $this->table]
+ )->join(
+ ['ssi' => $this->table . '_inheritance'],
+ 'ssi.service_set_id = o.id',
+ []
+ )->where(
+ 'ssi.parent_service_set_id = ?',
+ $id
+ );
+
+ return static::loadAll($this->connection, $query);
+ }
+
+ /**
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function beforeStore()
+ {
+ parent::beforeStore();
+
+ $name = $this->getObjectName();
+
+ if ($this->isObject() && $this->get('host_id') === null && $this->get('host') === null) {
+ throw new InvalidArgumentException(
+ 'A Service Set cannot be an object with no related host'
+ );
+ }
+ // checking if template object_name is unique
+ // TODO: Move to IcingaObject
+ if (! $this->hasBeenLoadedFromDb() && $this->isTemplate() && static::exists($name, $this->connection)) {
+ throw new DuplicateKeyException(
+ '%s template "%s" already existing in database!',
+ $this->getType(),
+ $name
+ );
+ }
+ }
+
+ public function toSingleIcingaConfig()
+ {
+ $config = parent::toSingleIcingaConfig();
+
+ try {
+ foreach ($this->fetchHostSets() as $set) {
+ $set->renderToConfig($config);
+ }
+ } catch (Exception $e) {
+ $config->configFile(
+ 'failed-to-render'
+ )->prepend(
+ "/** Failed to render this Service Set **/\n"
+ . '/* ' . $e->getMessage() . ' */'
+ );
+ }
+
+ return $config;
+ }
+}
diff --git a/library/Director/Objects/IcingaServiceSetAssignment.php b/library/Director/Objects/IcingaServiceSetAssignment.php
new file mode 100644
index 0000000..4a6ebbc
--- /dev/null
+++ b/library/Director/Objects/IcingaServiceSetAssignment.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaServiceSetAssignment extends IcingaObject
+{
+ protected $table = 'icinga_service_set_assignment';
+
+ protected $keyName = 'id';
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'service_set_id' => null,
+ 'filter_string' => null,
+ );
+
+ protected $relations = array(
+ 'service_set' => 'IcingaServiceSet',
+ );
+}
diff --git a/library/Director/Objects/IcingaServiceVar.php b/library/Director/Objects/IcingaServiceVar.php
new file mode 100644
index 0000000..0b855b2
--- /dev/null
+++ b/library/Director/Objects/IcingaServiceVar.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaServiceVar extends IcingaObject
+{
+ protected $keyName = array('service_id', 'varname');
+
+ protected $table = 'icinga_service_var';
+
+ protected $defaultProperties = array(
+ 'service_id' => null,
+ 'varname' => null,
+ 'varvalue' => null,
+ 'format' => null,
+ );
+
+ public function onInsert()
+ {
+ }
+
+ public function onUpdate()
+ {
+ }
+
+ public function onDelete()
+ {
+ }
+}
diff --git a/library/Director/Objects/IcingaTemplateChoice.php b/library/Director/Objects/IcingaTemplateChoice.php
new file mode 100644
index 0000000..1a1be90
--- /dev/null
+++ b/library/Director/Objects/IcingaTemplateChoice.php
@@ -0,0 +1,321 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class IcingaTemplateChoice extends IcingaObject implements ExportInterface
+{
+ protected $objectTable;
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'object_name' => null,
+ 'description' => null,
+ 'min_required' => 0,
+ 'max_allowed' => 1,
+ 'required_template_id' => null,
+ 'allowed_roles' => null,
+ ];
+
+ private $choices;
+
+ private $newChoices;
+
+ public function getObjectShortTableName()
+ {
+ return substr(substr($this->table, 0, -16), 7);
+ }
+
+ public function getUniqueIdentifier()
+ {
+ return $this->getObjectName();
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return IcingaTemplateChoice
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ if (isset($properties['originalId'])) {
+ unset($properties['originalId']);
+ }
+ $name = $properties['object_name'];
+ $key = $name;
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Template Choice "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @return array|object|\stdClass
+ */
+ public function export()
+ {
+ $plain = (object) $this->getProperties();
+ $plain->originalId = $plain->id;
+ unset($plain->id);
+ $requiredId = $plain->required_template_id;
+ unset($plain->required_template_id);
+ if ($requiredId) {
+ $db = $this->getDb();
+ $query = $db->select()
+ ->from(['o' => $this->getObjectTableName()], 'o.object_name')->where("o.object_type = 'template'")
+ ->where('o.id = ?', $this->get('id'));
+ $plain->required_template = $db->fetchOne($query);
+ }
+
+ $plain->members = array_values($this->getMembers());
+
+ return $plain;
+ }
+
+ public function isMainChoice()
+ {
+ return $this->hasBeenLoadedFromDb()
+ && $this->connection->settings()->get('main_host_choice');
+ }
+
+ public function getObjectTableName()
+ {
+ return substr($this->table, 0, -16);
+ }
+
+ /**
+ * @param QuickForm $form
+ * @param array $imports
+ * @param string $namePrefix
+ * @return \Zend_Form_Element
+ * @throws \Zend_Form_Exception
+ */
+ public function createFormElement(QuickForm $form, $imports = [], $namePrefix = 'choice')
+ {
+ $required = $this->isRequired() && !$this->isTemplate();
+ $type = $this->allowsMultipleChoices() ? 'multiselect' : 'select';
+ $choices = $this->enumChoices();
+
+ $chosen = [];
+ foreach ($imports as $import) {
+ if (array_key_exists($import, $choices)) {
+ $chosen[] = $import;
+ }
+ }
+
+ $attributes = [
+ 'label' => $this->getObjectName(),
+ 'description' => $this->get('description'),
+ 'required' => $required,
+ 'ignore' => true,
+ 'value' => $chosen,
+ 'multiOptions' => $form->optionalEnum($choices),
+ 'class' => 'autosubmit'
+ ];
+
+ // unused
+ if ($type === 'extensibleSet') {
+ $attributes['sorted'] = true;
+ }
+
+ $key = $namePrefix . $this->get('id');
+ return $form->createElement($type, $key, $attributes);
+ }
+
+ public function isRequired()
+ {
+ return (int) $this->get('min_required') > 0;
+ }
+
+ public function allowsMultipleChoices()
+ {
+ return (int) $this->get('max_allowed') > 1;
+ }
+
+ public function hasBeenModified()
+ {
+ if ($this->newChoices !== null && $this->choices !== $this->newChoices) {
+ return true;
+ }
+
+ return parent::hasBeenModified();
+ }
+
+ public function getMembers()
+ {
+ return $this->enumChoices();
+ }
+
+ public function setMembers($members)
+ {
+ if (empty($members)) {
+ $this->newChoices = array();
+ return $this;
+ }
+ $db = $this->getDb();
+ $query = $db->select()->from(
+ ['o' => $this->getObjectTableName()],
+ ['o.id', 'o.object_name']
+ )->where("o.object_type = 'template'")
+ ->where('o.object_name IN (?)', $members)
+ ->order('o.object_name');
+
+ $this->newChoices = $db->fetchPairs($query);
+ return $this;
+ }
+
+ public function getChoices()
+ {
+ if ($this->newChoices !== null) {
+ return $this->newChoices;
+ }
+
+ if ($this->choices === null) {
+ $this->choices = $this->fetchChoices();
+ }
+
+ return $this->choices;
+ }
+
+ public function fetchChoices()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ $db = $this->getDb();
+ $query = $db->select()->from(
+ ['o' => $this->getObjectTableName()],
+ ['o.id', 'o.object_name']
+ )->where("o.object_type = 'template'")
+ ->where('o.template_choice_id = ?', $this->get('id'))
+ ->order('o.object_name');
+
+ return $db->fetchPairs($query);
+ } else {
+ return [];
+ }
+ }
+
+ public function enumChoices()
+ {
+ $choices = $this->getChoices();
+ return array_combine($choices, $choices);
+ }
+
+ /**
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function onStore()
+ {
+ parent::onStore();
+ if ($this->newChoices !== $this->choices) {
+ $this->storeChoices();
+ }
+ }
+
+ /**
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function storeChoices()
+ {
+ $id = $this->getProperty('id');
+ $db = $this->getDb();
+ $ids = array_keys($this->newChoices);
+ $table = $this->getObjectTableName();
+
+ if (empty($ids)) {
+ $db->update(
+ $table,
+ ['template_choice_id' => null],
+ $db->quoteInto(
+ sprintf('template_choice_id = %d', $id),
+ $ids
+ )
+ );
+ } else {
+ $db->update(
+ $table,
+ ['template_choice_id' => null],
+ $db->quoteInto(
+ sprintf('template_choice_id = %d AND id NOT IN (?)', $id),
+ $ids
+ )
+ );
+ $db->update(
+ $table,
+ ['template_choice_id' => $id],
+ $db->quoteInto('id IN (?)', $ids)
+ );
+ }
+ }
+
+ /**
+ * @param $roles
+ * @throws ProgrammingError
+ * @codingStandardsIgnoreStart
+ */
+ public function setAllowed_roles($roles)
+ {
+ // @codingStandardsIgnoreEnd
+ $key = 'allowed_roles';
+ if (is_array($roles)) {
+ $this->reallySet($key, json_encode($roles));
+ } elseif (null === $roles) {
+ $this->reallySet($key, null);
+ } else {
+ throw new ProgrammingError(
+ 'Expected array or null for allowed_roles, got %s',
+ var_export($roles, 1)
+ );
+ }
+ }
+
+ /**
+ * @return array|null
+ * @codingStandardsIgnoreStart
+ */
+ public function getAllowed_roles()
+ {
+ // @codingStandardsIgnoreEnd
+
+ // Might be removed once all choice types have allowed_roles
+ if (! array_key_exists('allowed_roles', $this->properties)) {
+ return null;
+ }
+
+ $roles = $this->getProperty('allowed_roles');
+ if (is_string($roles)) {
+ return json_decode($roles);
+ } else {
+ return $roles;
+ }
+ }
+
+ /**
+ * @param $type
+ * @codingStandardsIgnoreStart
+ */
+ public function setObject_type($type)
+ {
+ // @codingStandardsIgnoreEnd
+ }
+}
diff --git a/library/Director/Objects/IcingaTemplateChoiceHost.php b/library/Director/Objects/IcingaTemplateChoiceHost.php
new file mode 100644
index 0000000..10ddedd
--- /dev/null
+++ b/library/Director/Objects/IcingaTemplateChoiceHost.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaTemplateChoiceHost extends IcingaTemplateChoice
+{
+ protected $table = 'icinga_host_template_choice';
+
+ protected $objectTable = 'icinga_host';
+
+ protected $relations = array(
+ 'required_template' => 'IcingaHost',
+ );
+}
diff --git a/library/Director/Objects/IcingaTemplateChoiceService.php b/library/Director/Objects/IcingaTemplateChoiceService.php
new file mode 100644
index 0000000..5cdb43e
--- /dev/null
+++ b/library/Director/Objects/IcingaTemplateChoiceService.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaTemplateChoiceService extends IcingaTemplateChoice
+{
+ protected $table = 'icinga_service_template_choice';
+
+ protected $objectTable = 'icinga_service';
+
+ protected $relations = array(
+ 'required_template' => 'IcingaService',
+ );
+}
diff --git a/library/Director/Objects/IcingaTemplateResolver.php b/library/Director/Objects/IcingaTemplateResolver.php
new file mode 100644
index 0000000..61122a0
--- /dev/null
+++ b/library/Director/Objects/IcingaTemplateResolver.php
@@ -0,0 +1,479 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Application\Benchmark;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Exception\NestingError;
+
+// TODO: move the 'type' layer to another class
+class IcingaTemplateResolver
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ protected $type;
+
+ protected static $templates = array();
+
+ protected static $idIdx = array();
+
+ protected static $reverseIdIdx = array();
+
+ protected static $nameIdx = array();
+
+ protected static $idToName = array();
+
+ protected static $nameToId = array();
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->setObject($object);
+ }
+
+ /**
+ * Set a specific object for this resolver instance
+ */
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ $this->type = $object->getShortTableName();
+ $this->table = $object->getTableName();
+ $this->connection = $object->getConnection();
+ $this->db = $this->connection->getDbAdapter();
+
+ return $this;
+ }
+
+ /**
+ * Forget all template relation of the given object type
+ *
+ * @return self
+ */
+ public function clearCache()
+ {
+ unset(self::$templates[$this->type]);
+ return $this;
+ }
+
+ /**
+ * Fetch direct parents
+ *
+ * return IcingaObject[]
+ */
+ public function fetchParents()
+ {
+ // TODO: involve lookup cache
+ $res = array();
+ $class = $this->object;
+ foreach ($this->listParentIds() as $id) {
+ $object = $class::loadWithAutoIncId($id, $this->connection);
+ $res[$object->object_name] = $object;
+ }
+
+ return $res;
+ }
+
+ public function listParentIds($id = null)
+ {
+ $this->requireTemplates();
+
+ if ($id === null) {
+ $object = $this->object;
+
+ if ($object->hasBeenLoadedFromDb()) {
+ if ($object->gotImports() && $object->imports()->hasBeenModified()) {
+ return $this->listUnstoredParentIds();
+ }
+
+ $id = $object->id;
+ } else {
+ return $this->listUnstoredParentIds();
+ }
+ }
+
+ $type = $this->type;
+
+ if (array_key_exists($id, self::$idIdx[$type])) {
+ return array_keys(self::$idIdx[$type][$id]);
+ }
+
+ return array();
+ }
+
+ protected function listUnstoredParentIds()
+ {
+ return $this->getIdsForNames($this->listUnstoredParentNames());
+ }
+
+ protected function listUnstoredParentNames()
+ {
+ return $this->object->imports()->listImportNames();
+ }
+
+ public function listParentNames($name = null)
+ {
+ $this->requireTemplates();
+
+ if ($name === null) {
+ $object = $this->object;
+
+ if ($object->hasBeenLoadedFromDb()) {
+ if ($object->gotImports() && $object->imports()->hasBeenModified()) {
+ return $this->listUnstoredParentNames();
+ }
+
+ $name = $object->object_name;
+ } else {
+ return $this->listUnstoredParentNames();
+ }
+ }
+
+ $type = $this->type;
+
+ if (array_key_exists($name, self::$nameIdx[$type])) {
+ return array_keys(self::$nameIdx[$type][$name]);
+ }
+
+ return array();
+ }
+
+ public function fetchResolvedParents()
+ {
+ if ($this->object->hasBeenLoadedFromDb()) {
+ return $this->fetchObjectsById($this->listResolvedParentIds());
+ }
+
+ $objects = array();
+ foreach ($this->object->imports()->getObjects() as $parent) {
+ $objects += $parent->templateResolver()->fetchResolvedParents();
+ }
+
+ return $objects;
+ }
+
+ public function listResolvedParentIds()
+ {
+ $this->requireTemplates();
+ return $this->resolveParentIds();
+ }
+
+ /**
+ * TODO: unfinished and not used currently
+ *
+ * @return array
+ */
+ public function listResolvedParentNames()
+ {
+ $this->requireTemplates();
+ if (array_key_exists($name, self::$nameIdx[$type])) {
+ return array_keys(self::$nameIdx[$type][$name]);
+ }
+
+ return $this->resolveParentNames($this->object->object_name);
+ }
+
+ public function listParentsById($id)
+ {
+ return $this->getNamesForIds($this->resolveParentIds($id));
+ }
+
+ public function listParentsByName($name)
+ {
+ return $this->resolveParentNames($name);
+ }
+
+ /**
+ * Gives a list of all object ids met when walking through ancestry
+ *
+ * Tree is walked in import order, duplicates are preserved, the given
+ * objectId is added last
+ *
+ * @param int $objectId
+ *
+ * @return array
+ */
+ public function listFullInheritancePathIds($objectId = null)
+ {
+ $parentIds = $this->listParentIds($objectId);
+ $ids = array();
+
+ foreach ($parentIds as $parentId) {
+ foreach ($this->listFullInheritancePathIds($parentId) as $id) {
+ $ids[] = $id;
+ }
+
+ $ids[] = $parentId;
+ }
+
+ $object = $this->object;
+ if ($objectId === null && $object->hasBeenLoadedFromDb()) {
+ $ids[] = $object->id;
+ }
+
+ return $ids;
+ }
+
+ public function listChildren($objectId = null)
+ {
+ if ($objectId === null) {
+ $objectId = $this->object->id;
+ }
+
+ if (array_key_exists($objectId, self::$reverseIdIdx[$this->type])) {
+ return self::$reverseIdIdx[$this->type][$objectId];
+ } else {
+ return array();
+ }
+ }
+
+ public function listChildIds($objectId = null)
+ {
+ return array_keys($this->listChildren($objectId));
+ }
+
+ public function listDescendantIds($objectId = null)
+ {
+ if ($objectId === null) {
+ $objectId = $this->object->id;
+ }
+ }
+
+ public function listInheritancePathIds($objectId = null)
+ {
+ return $this->uniquePathIds($this->listFullInheritancePathIds($objectId));
+ }
+
+ public function uniquePathIds(array $ids)
+ {
+ $single = array();
+ foreach (array_reverse($ids) as $id) {
+ if (array_key_exists($id, $single)) {
+ continue;
+ }
+ $single[$id] = $id;
+ }
+
+ return array_reverse(array_keys($single));
+ }
+
+ protected function resolveParentNames($name, &$list = array(), $path = array())
+ {
+ $this->assertNotInList($name, $path);
+ $path[$name] = true;
+ foreach ($this->listParentNames($name) as $parent) {
+ $list[$parent] = true;
+ $this->resolveParentNames($parent, $list, $path);
+ unset($list[$parent]);
+ $list[$parent] = true;
+ }
+
+ return array_keys($list);
+ }
+
+ protected function resolveParentIds($id = null, &$list = array(), $path = array())
+ {
+ if ($id === null) {
+ if ($check = $this->object->id) {
+ $this->assertNotInList($check, $path);
+ $path[$check] = true;
+ }
+ } else {
+ $this->assertNotInList($id, $path);
+ $path[$id] = true;
+ }
+
+ foreach ($this->listParentIds($id) as $parent) {
+ $list[$parent] = true;
+ $this->resolveParentIds($parent, $list, $path);
+ unset($list[$parent]);
+ $list[$parent] = true;
+ }
+
+ return array_keys($list);
+ }
+
+ protected function assertNotInList($id, &$list)
+ {
+ if (array_key_exists($id, $list)) {
+ $list = array_keys($list);
+ $list[] = $id;
+ if (is_numeric($id)) {
+ throw new NestingError(
+ 'Loop detected: %s',
+ implode(' -> ', $this->getNamesForIds($list))
+ );
+ } else {
+ throw new NestingError(
+ 'Loop detected: %s',
+ implode(' -> ', $list)
+ );
+ }
+ }
+ }
+
+ protected function getNamesForIds($ids)
+ {
+ $names = array();
+ foreach ($ids as $id) {
+ $names[] = $this->getNameForId($id);
+ }
+
+ return $names;
+ }
+
+ protected function getNameForId($id)
+ {
+ return self::$idToName[$this->type][$id];
+ }
+
+ protected function getIdsForNames($names)
+ {
+ $this->requireTemplates();
+ $ids = array();
+ foreach ($names as $name) {
+ $ids[] = $this->getIdForName($name);
+ }
+
+ return $ids;
+ }
+
+ protected function getIdForName($name)
+ {
+ if (! array_key_exists($name, self::$nameToId[$this->type])) {
+ throw new NotFoundError('There is no such import: "%s"', $name);
+ }
+
+ return self::$nameToId[$this->type][$name];
+ }
+
+ protected function fetchObjectsById($ids)
+ {
+ $class = $this->object;
+ $connection = $this->connection;
+ $res = array();
+
+ foreach ($ids as $id) {
+ $res[] = $class::loadWithAutoIncId($id, $connection);
+ }
+
+ return $res;
+ }
+
+ protected function requireTemplates()
+ {
+ if (! array_key_exists($this->type, self::$templates)) {
+ $this->prepareLookupTables();
+ }
+
+ return $this;
+ }
+
+ protected function prepareLookupTables()
+ {
+ $type = $this->type;
+
+ Benchmark::measure("Preparing '$type' TemplateResolver lookup tables");
+ $templates = $this->fetchTemplates();
+
+ $ids = array();
+ $reverseIds = array();
+ $names = array();
+ $idToName = array();
+ $nameToId = array();
+
+ foreach ($templates as $row) {
+ $id = $row->id;
+ $idToName[$id] = $row->name;
+ $nameToId[$row->name] = $id;
+
+ if ($row->parent_id === null) {
+ continue;
+ }
+ $parentId = $row->parent_id;
+ $parentName = $row->parent_name;
+
+ if (array_key_exists($id, $ids)) {
+ $ids[$id][$parentId] = $parentName;
+ $names[$row->name][$parentName] = $row->parent_id;
+ } else {
+ $ids[$id] = array(
+ $parentId => $parentName
+ );
+
+ $names[$row->name] = array(
+ $parentName => $parentId
+ );
+ }
+
+ if (! array_key_exists($parentId, $reverseIds)) {
+ $reverseIds[$parentId] = array();
+ }
+ $reverseIds[$parentId][$id] = $row->name;
+ }
+
+ self::$idIdx[$type] = $ids;
+ self::$reverseIdIdx[$type] = $reverseIds;
+ self::$nameIdx[$type] = $names;
+ self::$templates[$type] = $templates; // TODO: this is unused, isn't it?
+ self::$idToName[$type] = $idToName;
+ self::$nameToId[$type] = $nameToId;
+ Benchmark::measure('Preparing TemplateResolver lookup tables');
+ }
+
+ protected function fetchTemplates()
+ {
+ $db = $this->db;
+ $type = $this->type;
+ $table = $this->object->getTableName();
+
+ $query = $db->select()->from(
+ array('o' => $table),
+ array(
+ 'id' => 'o.id',
+ 'name' => 'o.object_name',
+ 'parent_id' => 'p.id',
+ 'parent_name' => 'p.object_name',
+ )
+ )->joinLeft(
+ array('i' => $table . '_inheritance'),
+ 'o.id = i.' . $type . '_id',
+ array()
+ )->joinLeft(
+ array('p' => $table),
+ 'p.id = i.parent_' . $type . '_id',
+ array()
+ )->order('o.id')->order('i.weight');
+
+ return $db->fetchAll($query);
+ }
+
+ public function __destruct()
+ {
+ unset($this->connection);
+ unset($this->db);
+ unset($this->object);
+ }
+
+ public function refreshObject(IcingaObject $object)
+ {
+ $type = $object->getShortTableName();
+ $name = $object->getObjectName();
+ $parentNames = $object->imports;
+ self::$nameIdx[$type][$name] = $parentNames;
+ if ($object->hasBeenLoadedFromDb()) {
+ $id = $object->getProperty('id');
+ self::$idIdx[$type][$id] = $this->getIdsForNames($parentNames);
+ self::$idToName[$type][$id] = $name;
+ self::$nameToId[$type][$name] = $id;
+ }
+ return $this;
+ }
+}
diff --git a/library/Director/Objects/IcingaTimePeriod.php b/library/Director/Objects/IcingaTimePeriod.php
new file mode 100644
index 0000000..1232366
--- /dev/null
+++ b/library/Director/Objects/IcingaTimePeriod.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+
+class IcingaTimePeriod extends IcingaObject implements ExportInterface
+{
+ protected $table = 'icinga_timeperiod';
+
+ protected $uuidColumn = 'uuid';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'zone_id' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'prefer_includes' => null,
+ 'display_name' => null,
+ 'update_method' => null,
+ ];
+
+ protected $booleans = [
+ 'prefer_includes' => 'prefer_includes',
+ ];
+
+ protected $supportsImports = true;
+
+ protected $supportsRanges = true;
+
+ protected $supportedInLegacy = true;
+
+ protected $relations = array(
+ 'zone' => 'IcingaZone',
+ );
+
+ protected $multiRelations = [
+ 'includes' => [
+ 'relatedObjectClass' => 'IcingaTimeperiod',
+ 'relatedShortName' => 'include',
+ ],
+ 'excludes' => [
+ 'relatedObjectClass' => 'IcingaTimeperiod',
+ 'relatedShortName' => 'exclude',
+ 'legacyPropertyName' => 'exclude'
+ ],
+ ];
+
+ public function getUniqueIdentifier()
+ {
+ return $this->getObjectName();
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @return object
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ $props = (array) $this->toPlainObject();
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return static
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ $key = $name;
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Time Period "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ /**
+ * Render update property
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderUpdate_method()
+ {
+ // @codingStandardsIgnoreEnd
+ return '';
+ }
+
+ protected function renderObjectHeader()
+ {
+ return parent::renderObjectHeader()
+ . ' import "legacy-timeperiod"' . "\n";
+ }
+
+ protected function checkPeriodInRange($now, $name = null)
+ {
+ if ($name !== null) {
+ $period = static::load($name, $this->connection);
+ } else {
+ $period = $this;
+ }
+
+ foreach ($period->ranges()->getRanges() as $range) {
+ if ($range->isActive($now)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function isActive($now = null)
+ {
+ if ($now === null) {
+ $now = time();
+ }
+
+ $preferIncludes = $this->get('prefer_includes') !== 'n';
+
+ $active = $this->checkPeriodInRange($now);
+ $included = false;
+ $excluded = false;
+
+ $variants = [
+ 'includes' => &$included,
+ 'excludes' => &$excluded
+ ];
+
+ foreach ($variants as $key => &$var) {
+ foreach ($this->get($key) as $name) {
+ if ($this->checkPeriodInRange($now, $name)) {
+ $var = true;
+ break;
+ }
+ }
+ }
+
+ if ($preferIncludes) {
+ if ($included) {
+ return true;
+ } elseif ($excluded) {
+ return false;
+ } else {
+ return $active;
+ }
+ } else {
+ if ($excluded) {
+ return false;
+ } elseif ($included) {
+ return true;
+ } else {
+ return $active;
+ }
+ }
+
+ // TODO: no range currently means (and renders) "never", Icinga behaves
+ // different. Figure out whether and how we should support this
+ return false;
+ }
+
+ protected function prefersGlobalZone()
+ {
+ return true;
+ }
+}
diff --git a/library/Director/Objects/IcingaTimePeriodRange.php b/library/Director/Objects/IcingaTimePeriodRange.php
new file mode 100644
index 0000000..55c1a3e
--- /dev/null
+++ b/library/Director/Objects/IcingaTimePeriodRange.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+
+class IcingaTimePeriodRange extends DbObject
+{
+ protected $keyName = array('timeperiod_id', 'range_key', 'range_type');
+
+ protected $table = 'icinga_timeperiod_range';
+
+ protected $defaultProperties = array(
+ 'timeperiod_id' => null,
+ 'range_key' => null,
+ 'range_value' => null,
+ 'range_type' => 'include',
+ 'merge_behaviour' => 'set',
+ );
+
+ public function isActive($now = null)
+ {
+ if ($now === null) {
+ $now = time();
+ }
+
+ if (false === ($weekDay = $this->getWeekDay($this->get('range_key')))) {
+ // TODO, dates are not yet supported
+ return false;
+ }
+
+ if ((int) date('w', $now) !== $weekDay) {
+ return false;
+ }
+
+ $timeRanges = preg_split('/\s*,\s*/', $this->get('range_value'), -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($timeRanges as $timeRange) {
+ if ($this->timeRangeIsActive($timeRange, $now)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function timeRangeIsActive($rangeString, $now)
+ {
+ $hBegin = $mBegin = $hEnd = $mEnd = null;
+ if (sscanf($rangeString, '%2d:%2d-%2d:%2d', $hBegin, $mBegin, $hEnd, $mEnd) === 4) {
+ if ($this->timeFromHourMin($hBegin, $mBegin, $now) <= $now
+ && $this->timeFromHourMin($hEnd, $mEnd, $now) >= $now
+ ) {
+ return true;
+ }
+ } else {
+ // TODO: throw exception?
+ }
+
+ return false;
+ }
+
+ protected function timeFromHourMin($hour, $min, $now)
+ {
+ return strtotime(sprintf('%s %02d:%02d:00', date('Y-m-d', $now), $hour, $min));
+ }
+
+ protected function getWeekDay($day)
+ {
+ switch ($day) {
+ case 'sunday':
+ return 0;
+ case 'monday':
+ return 1;
+ case 'tuesday':
+ return 2;
+ case 'wednesday':
+ return 3;
+ case 'thursday':
+ return 4;
+ case 'friday':
+ return 5;
+ case 'saturday':
+ return 6;
+ }
+
+ return false;
+ }
+}
diff --git a/library/Director/Objects/IcingaTimePeriodRanges.php b/library/Director/Objects/IcingaTimePeriodRanges.php
new file mode 100644
index 0000000..b18437d
--- /dev/null
+++ b/library/Director/Objects/IcingaTimePeriodRanges.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Countable;
+use Iterator;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+
+class IcingaTimePeriodRanges extends IcingaRanges implements Iterator, Countable, IcingaConfigRenderer
+{
+ protected $rangeClass = IcingaTimePeriodRange::class;
+ protected $objectIdColumn = 'timeperiod_id';
+
+ public function toLegacyConfigString()
+ {
+ if (empty($this->ranges) && $this->object->isTemplate()) {
+ return '';
+ }
+
+ $out = '';
+
+ foreach ($this->ranges as $range) {
+ $out .= c1::renderKeyValue(
+ $range->get('range_key'),
+ $range->get('range_value')
+ );
+ }
+ if ($out !== '') {
+ $out = "\n".$out;
+ }
+
+ return $out;
+ }
+}
diff --git a/library/Director/Objects/IcingaUser.php b/library/Director/Objects/IcingaUser.php
new file mode 100644
index 0000000..394e849
--- /dev/null
+++ b/library/Director/Objects/IcingaUser.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+
+class IcingaUser extends IcingaObject implements ExportInterface
+{
+ protected $table = 'icinga_user';
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'display_name' => null,
+ 'email' => null,
+ 'pager' => null,
+ 'enable_notifications' => null,
+ 'period_id' => null,
+ 'zone_id' => null,
+ );
+
+ protected $uuidColumn = 'uuid';
+
+ protected $supportsGroups = true;
+
+ protected $supportsCustomVars = true;
+
+ protected $supportsFields = true;
+
+ protected $supportsImports = true;
+
+ protected $booleans = array(
+ 'enable_notifications' => 'enable_notifications'
+ );
+
+ protected $relatedSets = array(
+ 'states' => 'StateFilterSet',
+ 'types' => 'TypeFilterSet',
+ );
+
+ protected $relations = array(
+ 'period' => 'IcingaTimePeriod',
+ 'zone' => 'IcingaZone',
+ );
+
+ public function export()
+ {
+ return ImportExportHelper::simpleExport($this);
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return IcingaUser
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $key = $properties['object_name'];
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Cannot import, %s "%s" already exists',
+ static::create([])->getShortTableName(),
+ $key
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ // $object->newFields = $properties['fields'];
+ unset($properties['fields']);
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ public function getUniqueIdentifier()
+ {
+ return $this->getObjectName();
+ }
+}
diff --git a/library/Director/Objects/IcingaUserField.php b/library/Director/Objects/IcingaUserField.php
new file mode 100644
index 0000000..4a6432c
--- /dev/null
+++ b/library/Director/Objects/IcingaUserField.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaUserField extends IcingaObjectField
+{
+ protected $keyName = array('user_id', 'datafield_id');
+
+ protected $table = 'icinga_user_field';
+
+ protected $defaultProperties = array(
+ 'user_id' => null,
+ 'datafield_id' => null,
+ 'is_required' => null,
+ 'var_filter' => null,
+ );
+}
diff --git a/library/Director/Objects/IcingaUserGroup.php b/library/Director/Objects/IcingaUserGroup.php
new file mode 100644
index 0000000..656235a
--- /dev/null
+++ b/library/Director/Objects/IcingaUserGroup.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class IcingaUserGroup extends IcingaObjectGroup
+{
+ protected $table = 'icinga_usergroup';
+
+ protected $uuidColumn = 'uuid';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'display_name' => null,
+ 'zone_id' => null,
+ ];
+
+ protected $relations = [
+ 'zone' => 'IcingaZone',
+ ];
+
+ protected function prefersGlobalZone()
+ {
+ return false;
+ }
+}
diff --git a/library/Director/Objects/IcingaVar.php b/library/Director/Objects/IcingaVar.php
new file mode 100644
index 0000000..10addf2
--- /dev/null
+++ b/library/Director/Objects/IcingaVar.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\CustomVariable\CustomVariable;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+
+class IcingaVar extends DbObject
+{
+ protected $table = 'icinga_var';
+
+ protected $keyName = 'checksum';
+
+ /** @var CustomVariable */
+ protected $var;
+
+ protected $defaultProperties = [
+ 'checksum' => null,
+ 'rendered_checksum' => null,
+ 'varname' => null,
+ 'varvalue' => null,
+ 'rendered' => null
+ ];
+
+ protected $binaryProperties = [
+ 'checksum',
+ 'rendered_checksum',
+ ];
+
+ /**
+ * @param CustomVariable $customVar
+ * @param Db $db
+ *
+ * @return static
+ */
+ public static function forCustomVar(CustomVariable $customVar, Db $db)
+ {
+ $rendered = $customVar->render();
+
+ $var = static::create(array(
+ 'checksum' => $customVar->checksum(),
+ 'rendered_checksum' => sha1($rendered, true),
+ 'varname' => $customVar->getKey(),
+ 'varvalue' => $customVar->toJson(),
+ 'rendered' => $rendered,
+ ), $db);
+
+ $var->var = $customVar;
+
+ return $var;
+ }
+
+ /**
+ * @param CustomVariable $customVar
+ * @param Db $db
+ *
+ * @return static
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public static function generateForCustomVar(CustomVariable $customVar, Db $db)
+ {
+ $var = static::forCustomVar($customVar, $db);
+ $var->store();
+ return $var;
+ }
+
+ protected function onInsert()
+ {
+ IcingaFlatVar::generateForCustomVar($this->var, $this->getConnection());
+ }
+}
diff --git a/library/Director/Objects/IcingaZone.php b/library/Director/Objects/IcingaZone.php
new file mode 100644
index 0000000..8d77e47
--- /dev/null
+++ b/library/Director/Objects/IcingaZone.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+
+class IcingaZone extends IcingaObject
+{
+ protected $table = 'icinga_zone';
+
+ protected $uuidColumn = 'uuid';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'parent_id' => null,
+ 'is_global' => 'n',
+ ];
+
+ protected $booleans = [
+ // Global is a reserved word in SQL, column name was prefixed
+ 'is_global' => 'global'
+ ];
+
+ protected $relations = [
+ 'parent' => 'IcingaZone',
+ ];
+
+ protected $supportsImports = true;
+
+ protected static $globalZoneNames;
+
+ private $endpointList;
+
+ protected function renderCustomExtensions()
+ {
+ $endpoints = $this->listEndpoints();
+ if (empty($endpoints)) {
+ return '';
+ }
+
+ return c::renderKeyValue('endpoints', c::renderArray($endpoints));
+ }
+
+ public function isGlobal()
+ {
+ return $this->get('is_global') === 'y';
+ }
+
+ public static function zoneNameIsGlobal($name, Db $connection)
+ {
+ if (self::$globalZoneNames === null) {
+ $db = $connection->getDbAdapter();
+ self::setCachedGlobalZoneNames($db->fetchCol(
+ $db->select()->from('icinga_zone', 'object_name')->where('is_global = ?', 'y')
+ ));
+ }
+
+ return \in_array($name, self::$globalZoneNames);
+ }
+
+ public static function setCachedGlobalZoneNames($names)
+ {
+ self::$globalZoneNames = $names;
+ }
+
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ // If the zone has a parent zone...
+ if ($this->get('parent_id')) {
+ // ...we render the zone object to the parent zone
+ return $this->get('parent');
+ } elseif ($this->get('is_global') === 'y') {
+ // ...additional global zones are rendered to our global zone...
+ return $this->connection->getDefaultGlobalZoneName();
+ } else {
+ // ...and all the other zones are rendered to our master zone
+ return $this->connection->getMasterZoneName();
+ }
+ }
+
+ public function setEndpointList($list)
+ {
+ $this->endpointList = $list;
+
+ return $this;
+ }
+
+ // TODO: Move this away, should be prefetchable:
+ public function listEndpoints()
+ {
+ $id = $this->get('id');
+ if ($id && $this->endpointList === null) {
+ $db = $this->getDb();
+ $query = $db->select()
+ ->from('icinga_endpoint', 'object_name')
+ ->where('zone_id = ?', $id)
+ ->order('object_name');
+
+ $this->endpointList = $db->fetchCol($query);
+ }
+
+ return $this->endpointList;
+ }
+}
diff --git a/library/Director/Objects/ImportExportHelper.php b/library/Director/Objects/ImportExportHelper.php
new file mode 100644
index 0000000..98d34c6
--- /dev/null
+++ b/library/Director/Objects/ImportExportHelper.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Db;
+
+/**
+ * Helper class, allows to reduce duplicate code. Might be moved elsewhere
+ * afterwards
+ */
+class ImportExportHelper
+{
+ /**
+ * Does not support every type out of the box
+ *
+ * @param IcingaObject $object
+ * @return object
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function simpleExport(IcingaObject $object)
+ {
+ $props = (array) $object->toPlainObject();
+ $props['fields'] = static::fetchFields($object);
+ ksort($props); // TODO: ksort in toPlainObject?
+
+ return (object) $props;
+ }
+
+ public static function fetchFields(IcingaObject $object)
+ {
+ return static::loadFieldReferences(
+ $object->getConnection(),
+ $object->getShortTableName(),
+ $object->get('id')
+ );
+ }
+
+ /**
+ * @param Db $connection
+ * @param string $type Warning: this will not be validated.
+ * @param int $id
+ * @return array
+ */
+ public static function loadFieldReferences(Db $connection, $type, $id)
+ {
+ $db = $connection->getDbAdapter();
+ $res = $db->fetchAll(
+ $db->select()->from([
+ 'f' => "icinga_${type}_field"
+ ], [
+ 'f.datafield_id',
+ 'f.is_required',
+ 'f.var_filter',
+ ])->join(['df' => 'director_datafield'], 'df.id = f.datafield_id', [])
+ ->where("${type}_id = ?", $id)
+ ->order('varname ASC')
+ );
+
+ if (empty($res)) {
+ return [];
+ }
+
+ foreach ($res as $field) {
+ $field->datafield_id = (int) $field->datafield_id;
+ }
+ return $res;
+ }
+}
diff --git a/library/Director/Objects/ImportRowModifier.php b/library/Director/Objects/ImportRowModifier.php
new file mode 100644
index 0000000..76982c2
--- /dev/null
+++ b/library/Director/Objects/ImportRowModifier.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Objects\Extension\PriorityColumn;
+use RuntimeException;
+
+class ImportRowModifier extends DbObjectWithSettings implements InstantiatedViaHook
+{
+ use PriorityColumn;
+
+ protected $table = 'import_row_modifier';
+
+ protected $keyName = 'id';
+
+ protected $autoincKeyName = 'id';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'source_id' => null,
+ 'property_name' => null,
+ 'provider_class' => null,
+ 'target_property' => null,
+ 'priority' => null,
+ 'description' => null,
+ ];
+
+ protected $settingsTable = 'import_row_modifier_setting';
+
+ protected $settingsRemoteId = 'row_modifier_id';
+
+ private $hookInstance;
+
+ public function getInstance()
+ {
+ if ($this->hookInstance === null) {
+ $class = $this->get('provider_class');
+ /** @var PropertyModifierHook $obj */
+ if (! class_exists($class)) {
+ throw new RuntimeException(sprintf(
+ 'Cannot instantiate Property modifier %s',
+ $class
+ ));
+ }
+ $obj = new $class;
+ $obj->setSettings($this->getSettings());
+ $obj->setPropertyName($this->get('property_name'));
+ $obj->setTargetProperty($this->get('target_property'));
+ $obj->setDb($this->connection);
+ $this->hookInstance = $obj;
+ }
+
+ return $this->hookInstance;
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @return \stdClass
+ */
+ public function export()
+ {
+ $properties = $this->getProperties();
+ unset($properties['id']);
+ unset($properties['source_id']);
+ $properties['settings'] = $this->getInstance()->exportSettings();
+ ksort($properties);
+
+ return (object) $properties;
+ }
+
+ public function setSettings($settings)
+ {
+ $settings = $this->getInstance()->setSettings((array) $settings)->getSettings();
+
+ return parent::setSettings($settings); // TODO: Change the autogenerated stub
+ }
+
+ protected function beforeStore()
+ {
+ if (! $this->hasBeenLoadedFromDb() && $this->get('priority') === null) {
+ $this->setNextPriority('source_id');
+ }
+ }
+
+ protected function onInsert()
+ {
+ $this->refreshPriortyProperty();
+ }
+}
diff --git a/library/Director/Objects/ImportRun.php b/library/Director/Objects/ImportRun.php
new file mode 100644
index 0000000..d3bdb7c
--- /dev/null
+++ b/library/Director/Objects/ImportRun.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+
+class ImportRun extends DbObject
+{
+ protected $table = 'import_run';
+
+ protected $keyName = 'id';
+
+ protected $autoincKeyName = 'id';
+
+ /** @var ImportSource */
+ protected $importSource = null;
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'source_id' => null,
+ 'rowset_checksum' => null,
+ 'start_time' => null,
+ 'end_time' => null,
+ // TODO: Check whether succeeded could be dropped
+ 'succeeded' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'rowset_checksum',
+ ];
+
+ public function prepareImportedObjectQuery($columns = array('object_name'))
+ {
+ return $this->getDb()->select()->from(
+ array('r' => 'imported_row'),
+ $columns
+ )->joinLeft(
+ array('rsr' => 'imported_rowset_row'),
+ 'rsr.row_checksum = r.checksum',
+ array()
+ )->where(
+ 'rsr.rowset_checksum = ?',
+ $this->getConnection()->quoteBinary($this->rowset_checksum)
+ );
+ }
+
+ public function listColumnNames()
+ {
+ $db = $this->getDb();
+
+ $query = $db->select()->distinct()->from(
+ array('p' => 'imported_property'),
+ 'property_name'
+ )->join(
+ array('rp' => 'imported_row_property'),
+ 'rp.property_checksum = p.checksum',
+ array()
+ )->join(
+ array('rsr' => 'imported_rowset_row'),
+ 'rsr.row_checksum = rp.row_checksum',
+ array()
+ )->where('rsr.rowset_checksum = ?', $this->getConnection()->quoteBinary($this->rowset_checksum));
+
+ return $db->fetchCol($query);
+ }
+
+ public function fetchRows($columns, $filter = null, $keys = null)
+ {
+ $db = $this->getDb();
+ /** @var Db $connection */
+ $connection = $this->getConnection();
+ $binchecksum = $this->rowset_checksum;
+
+ $query = $db->select()->from(
+ array('rsr' => 'imported_rowset_row'),
+ array(
+ 'object_name' => 'r.object_name',
+ 'property_name' => 'p.property_name',
+ 'property_value' => 'p.property_value',
+ 'format' => 'p.format'
+ )
+ )->join(
+ array('r' => 'imported_row'),
+ 'rsr.row_checksum = r.checksum',
+ array()
+ )->join(
+ array('rp' => 'imported_row_property'),
+ 'r.checksum = rp.row_checksum',
+ array()
+ )->join(
+ array('p' => 'imported_property'),
+ 'p.checksum = rp.property_checksum',
+ array()
+ )->order('r.object_name');
+ if ($connection->isMysql()) {
+ $query->where('rsr.rowset_checksum = :checksum')->bind([
+ 'checksum' => $binchecksum
+ ]);
+ } else {
+ $query->where(
+ 'rsr.rowset_checksum = ?',
+ $connection->quoteBinary($binchecksum)
+ );
+ }
+
+ if ($columns === null) {
+ $columns = $this->listColumnNames();
+ } else {
+ $query->where('p.property_name IN (?)', $columns);
+ }
+
+ $result = array();
+ $empty = (object) array();
+ foreach ($columns as $k => $v) {
+ $empty->$k = null;
+ }
+
+ if ($keys !== null) {
+ $query->where('r.object_name IN (?)', $keys);
+ }
+
+ foreach ($db->fetchAll($query) as $row) {
+ if (! array_key_exists($row->object_name, $result)) {
+ $result[$row->object_name] = clone($empty);
+ }
+
+ if ($row->format === 'json') {
+ $result[$row->object_name]->{$row->property_name} = json_decode($row->property_value);
+ } else {
+ $result[$row->object_name]->{$row->property_name} = $row->property_value;
+ }
+ }
+
+ if ($filter) {
+ $filtered = array();
+ foreach ($result as $key => $row) {
+ if ($filter->matches($row)) {
+ $filtered[$key] = $row;
+ }
+ }
+
+ return $filtered;
+ }
+
+ return $result;
+ }
+
+ public function importSource()
+ {
+ if ($this->importSource === null) {
+ $this->importSource = ImportSource::loadWithAutoIncId(
+ (int) $this->get('source_id'),
+ $this->connection
+ );
+ }
+ return $this->importSource;
+ }
+}
diff --git a/library/Director/Objects/ImportSource.php b/library/Director/Objects/ImportSource.php
new file mode 100644
index 0000000..fd892ef
--- /dev/null
+++ b/library/Director/Objects/ImportSource.php
@@ -0,0 +1,537 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Application\Benchmark;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Application\MemoryLimit;
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Import\Import;
+use Icinga\Module\Director\Import\SyncUtils;
+use InvalidArgumentException;
+use Exception;
+
+class ImportSource extends DbObjectWithSettings implements ExportInterface
+{
+ protected $table = 'import_source';
+
+ protected $keyName = 'source_name';
+
+ protected $autoincKeyName = 'id';
+
+ protected $protectAutoinc = false;
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'source_name' => null,
+ 'provider_class' => null,
+ 'key_column' => null,
+ 'import_state' => 'unknown',
+ 'last_error_message' => null,
+ 'last_attempt' => null,
+ 'description' => null,
+ ];
+
+ protected $stateProperties = [
+ 'import_state',
+ 'last_error_message',
+ 'last_attempt',
+ ];
+
+ protected $settingsTable = 'import_source_setting';
+
+ protected $settingsRemoteId = 'source_id';
+
+ private $rowModifiers;
+
+ private $loadedRowModifiers;
+
+ private $newRowModifiers;
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader
+ * @return \stdClass
+ */
+ public function export()
+ {
+ $plain = $this->getProperties();
+ $plain['originalId'] = $plain['id'];
+ unset($plain['id']);
+
+ foreach ($this->stateProperties as $key) {
+ unset($plain[$key]);
+ }
+
+ $plain['settings'] = (object) $this->getSettings();
+ $plain['modifiers'] = $this->exportRowModifiers();
+ ksort($plain);
+
+ return (object) $plain;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return ImportSource
+ * @throws DuplicateKeyException
+ * @throws NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ if (isset($properties['originalId'])) {
+ $id = $properties['originalId'];
+ unset($properties['originalId']);
+ } else {
+ $id = null;
+ }
+ $name = $properties['source_name'];
+
+ if ($replace && $id && static::existsWithNameAndId($name, $id, $db)) {
+ $object = static::loadWithAutoIncId($id, $db);
+ } elseif ($replace && static::exists($name, $db)) {
+ $object = static::load($name, $db);
+ } elseif (static::existsWithName($name, $db)) {
+ throw new DuplicateKeyException(
+ 'Import Source %s already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ if (! isset($properties['modifiers'])) {
+ $properties['modifiers'] = [];
+ }
+
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ public function setModifiers(array $modifiers)
+ {
+ if ($this->loadedRowModifiers === null && $this->hasBeenLoadedFromDb()) {
+ $this->loadedRowModifiers = $this->fetchRowModifiers();
+ }
+ $current = (array) $this->loadedRowModifiers;
+ if (\count($current) !== \count($modifiers)) {
+ $this->newRowModifiers = $modifiers;
+ } else {
+ $i = 0;
+ $modified = false;
+ foreach ($modifiers as $props) {
+ $this->loadedRowModifiers[$i]->setProperties((array) $props);
+ if ($this->loadedRowModifiers[$i]->hasBeenModified()) {
+ $modified = true;
+ }
+ $i++;
+ }
+ if ($modified) {
+ $this->newRowModifiers = $modifiers;
+ }
+ }
+ }
+
+ public function hasBeenModified()
+ {
+ return $this->newRowModifiers !== null
+ || parent::hasBeenModified();
+ }
+
+ public function getUniqueIdentifier()
+ {
+ return $this->get('source_name');
+ }
+
+ /**
+ * @param $name
+ * @param Db $connection
+ * @return ImportSource
+ * @throws NotFoundError
+ */
+ public static function loadByName($name, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $properties = $db->fetchRow(
+ $db->select()->from('import_source')->where('source_name = ?', $name)
+ );
+ if ($properties === false) {
+ throw new NotFoundError(sprintf(
+ 'There is no such Import Source: "%s"',
+ $name
+ ));
+ }
+
+ return static::create([], $connection)->setDbProperties($properties);
+ }
+
+ public static function existsWithName($name, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+
+ return (string) $name === (string) $db->fetchOne(
+ $db->select()
+ ->from('import_source', 'source_name')
+ ->where('source_name = ?', $name)
+ );
+ }
+
+ /**
+ * @param string $name
+ * @param int $id
+ * @param Db $connection
+ * @api internal
+ * @return bool
+ */
+ protected static function existsWithNameAndId($name, $id, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $dummy = new static;
+ $idCol = $dummy->autoincKeyName;
+ $keyCol = $dummy->keyName;
+
+ return (string) $id === (string) $db->fetchOne(
+ $db->select()
+ ->from($dummy->table, $idCol)
+ ->where("$idCol = ?", $id)
+ ->where("$keyCol = ?", $name)
+ );
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader
+ * @return array
+ */
+ protected function exportRowModifiers()
+ {
+ $modifiers = [];
+ foreach ($this->fetchRowModifiers() as $modifier) {
+ $modifiers[] = $modifier->export();
+ }
+
+ return $modifiers;
+ }
+
+ /**
+ * @param bool $required
+ * @return ImportRun|null
+ * @throws NotFoundError
+ */
+ public function fetchLastRun($required = false)
+ {
+ return $this->fetchLastRunBefore(time() + 1, $required);
+ }
+
+ /**
+ * @throws DuplicateKeyException
+ */
+ protected function onStore()
+ {
+ parent::onStore();
+ if ($this->newRowModifiers !== null) {
+ $connection = $this->getConnection();
+ $db = $connection->getDbAdapter();
+ $myId = $this->get('id');
+ if ($this->hasBeenLoadedFromDb()) {
+ $db->delete(
+ 'import_row_modifier',
+ $db->quoteInto('source_id = ?', $myId)
+ );
+ }
+
+ foreach ($this->newRowModifiers as $modifier) {
+ $modifier = ImportRowModifier::create((array) $modifier, $connection);
+ $modifier->set('source_id', $myId);
+ $modifier->store();
+ }
+ }
+ }
+
+ /**
+ * @param $timestamp
+ * @param bool $required
+ * @return ImportRun|null
+ * @throws NotFoundError
+ */
+ public function fetchLastRunBefore($timestamp, $required = false)
+ {
+ if (! $this->hasBeenLoadedFromDb()) {
+ return $this->nullUnlessRequired($required);
+ }
+
+ if ($timestamp === null) {
+ $timestamp = time();
+ }
+
+ $db = $this->getDb();
+ $query = $db->select()->from(
+ ['ir' => 'import_run'],
+ 'ir.id'
+ )->where('ir.source_id = ?', $this->get('id'))
+ ->where('ir.start_time < ?', date('Y-m-d H:i:s', $timestamp))
+ ->order('ir.start_time DESC')
+ ->limit(1);
+
+ $runId = $db->fetchOne($query);
+
+ if ($runId) {
+ return ImportRun::load($runId, $this->getConnection());
+ } else {
+ return $this->nullUnlessRequired($required);
+ }
+ }
+
+ /**
+ * @param $required
+ * @return null
+ * @throws NotFoundError
+ */
+ protected function nullUnlessRequired($required)
+ {
+ if ($required) {
+ throw new NotFoundError(
+ 'No data has been imported for "%s" yet',
+ $this->get('source_name')
+ );
+ }
+
+ return null;
+ }
+
+ public function applyModifiers(&$data)
+ {
+ $modifiers = $this->fetchFlatRowModifiers();
+
+ if (empty($modifiers)) {
+ return $this;
+ }
+
+ foreach ($modifiers as $modPair) {
+ /** @var PropertyModifierHook $modifier */
+ list($property, $modifier) = $modPair;
+ $rejected = [];
+ $newRows = [];
+ foreach ($data as $key => $row) {
+ $this->applyPropertyModifierToRow($modifier, $property, $row);
+ if ($modifier->rejectsRow()) {
+ $rejected[] = $key;
+ $modifier->rejectRow(false);
+ }
+ if ($modifier->expandsRows()) {
+ $target = $modifier->getTargetProperty($property);
+
+ $newValue = $row->$target;
+ if (\is_array($newValue)) {
+ foreach ($newValue as $val) {
+ $newRow = clone $row;
+ $newRow->$target = $val;
+ $newRows[] = $newRow;
+ }
+ $rejected[] = $key;
+ }
+ }
+ }
+
+ foreach ($rejected as $key) {
+ unset($data[$key]);
+ }
+ foreach ($newRows as $row) {
+ $data[] = $row;
+ }
+ }
+
+ return $this;
+ }
+
+ public function getObjectName()
+ {
+ return $this->get('source_name');
+ }
+
+ public static function getKeyColumnName()
+ {
+ return 'source_name';
+ }
+
+ protected function applyPropertyModifierToRow(PropertyModifierHook $modifier, $key, $row)
+ {
+ if (! is_object($row)) {
+ throw new InvalidArgumentException('Every imported row MUST be an object');
+ }
+ if ($modifier->requiresRow()) {
+ $modifier->setRow($row);
+ }
+
+ if (property_exists($row, $key)) {
+ $value = $row->$key;
+ } elseif (strpos($key, '.') !== false) {
+ $value = SyncUtils::getSpecificValue($row, $key);
+ } else {
+ $value = null;
+ }
+
+ $target = $modifier->getTargetProperty($key);
+ if (strpos($target, '.') !== false) {
+ throw new InvalidArgumentException(sprintf(
+ 'Cannot set value for nested key "%s"',
+ $target
+ ));
+ }
+
+ if (is_array($value) && ! $modifier->hasArraySupport()) {
+ $new = [];
+ foreach ($value as $k => $v) {
+ $new[$k] = $modifier->transform($v);
+ }
+ $row->$target = $new;
+ } else {
+ $row->$target = $modifier->transform($value);
+ }
+ }
+
+ public function getRowModifiers()
+ {
+ if ($this->rowModifiers === null) {
+ $this->prepareRowModifiers();
+ }
+
+ return $this->rowModifiers;
+ }
+
+ public function hasRowModifiers()
+ {
+ return count($this->getRowModifiers()) > 0;
+ }
+
+ /**
+ * @return ImportRowModifier[]
+ */
+ public function fetchRowModifiers()
+ {
+ $db = $this->getDb();
+ $modifiers = ImportRowModifier::loadAll(
+ $this->getConnection(),
+ $db->select()
+ ->from('import_row_modifier')
+ ->where('source_id = ?', $this->get('id'))
+ ->order('priority ASC')
+ );
+
+ if ($modifiers) {
+ return $modifiers;
+ } else {
+ return [];
+ }
+ }
+
+ protected function fetchFlatRowModifiers()
+ {
+ $mods = [];
+ foreach ($this->fetchRowModifiers() as $mod) {
+ $mods[] = [$mod->get('property_name'), $mod->getInstance()];
+ }
+
+ return $mods;
+ }
+
+ protected function prepareRowModifiers()
+ {
+ $modifiers = [];
+
+ foreach ($this->fetchRowModifiers() as $mod) {
+ $name = $mod->get('property_name');
+ if (! array_key_exists($name, $modifiers)) {
+ $modifiers[$name] = [];
+ }
+
+ $modifiers[$name][] = $mod->getInstance();
+ }
+
+ $this->rowModifiers = $modifiers;
+ }
+
+ public function listModifierTargetProperties()
+ {
+ $list = [];
+ foreach ($this->getRowModifiers() as $rowMods) {
+ /** @var PropertyModifierHook $mod */
+ foreach ($rowMods as $mod) {
+ if ($mod->hasTargetProperty()) {
+ $list[$mod->getTargetProperty()] = true;
+ }
+ }
+ }
+
+ return array_keys($list);
+ }
+
+ /**
+ * @param bool $runImport
+ * @return bool
+ * @throws DuplicateKeyException
+ */
+ public function checkForChanges($runImport = false)
+ {
+ $hadChanges = false;
+
+ $name = $this->get('source_name');
+ Benchmark::measure("Starting with import $name");
+ $this->raiseLimits();
+ try {
+ $import = new Import($this);
+ $this->set('last_attempt', date('Y-m-d H:i:s'));
+ if ($import->providesChanges()) {
+ Benchmark::measure("Found changes for $name");
+ $hadChanges = true;
+ $this->set('import_state', 'pending-changes');
+
+ if ($runImport && $import->run()) {
+ Benchmark::measure("Import succeeded for $name");
+ $this->set('import_state', 'in-sync');
+ }
+ } else {
+ $this->set('import_state', 'in-sync');
+ }
+
+ $this->set('last_error_message', null);
+ } catch (Exception $e) {
+ $this->set('import_state', 'failing');
+ Benchmark::measure("Import failed for $name");
+ $this->set('last_error_message', $e->getMessage());
+ }
+
+ if ($this->hasBeenModified()) {
+ $this->store();
+ }
+
+ return $hadChanges;
+ }
+
+ /**
+ * @return bool
+ * @throws DuplicateKeyException
+ */
+ public function runImport()
+ {
+ return $this->checkForChanges(true);
+ }
+
+ /**
+ * Raise PHP resource limits
+ *
+ * @return $this;
+ */
+ protected function raiseLimits()
+ {
+ MemoryLimit::raiseTo('1024M');
+ ini_set('max_execution_time', 0);
+
+ return $this;
+ }
+}
diff --git a/library/Director/Objects/InstantiatedViaHook.php b/library/Director/Objects/InstantiatedViaHook.php
new file mode 100644
index 0000000..79f3442
--- /dev/null
+++ b/library/Director/Objects/InstantiatedViaHook.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Hook\JobHook;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+interface InstantiatedViaHook
+{
+ /**
+ * @return mixed|PropertyModifierHook|JobHook
+ */
+ public function getInstance();
+}
diff --git a/library/Director/Objects/ObjectApplyMatches.php b/library/Director/Objects/ObjectApplyMatches.php
new file mode 100644
index 0000000..018c880
--- /dev/null
+++ b/library/Director/Objects/ObjectApplyMatches.php
@@ -0,0 +1,239 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Application\MemoryLimit;
+use Icinga\Module\Director\Data\AssignFilterHelper;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use stdClass;
+
+abstract class ObjectApplyMatches
+{
+ protected static $flatObjects;
+
+ protected static $columnMap = array(
+ 'name' => 'object_name'
+ );
+
+ protected $object;
+
+ protected $flatObject;
+
+ protected static $type;
+
+ protected static $preparedFilters = array();
+
+ public static function prepare(IcingaObject $object)
+ {
+ return new static($object);
+ }
+
+ /**
+ * Prepare a Filter with fixed columns, and store the result
+ *
+ * @param Filter $filter
+ *
+ * @return Filter
+ */
+ protected static function getPreparedFilter(Filter $filter)
+ {
+ $hash = spl_object_hash($filter);
+ if (! array_key_exists($hash, self::$preparedFilters)) {
+ $filter = clone($filter);
+ static::fixFilterColumns($filter);
+ self::$preparedFilters[$hash] = $filter;
+ }
+ return self::$preparedFilters[$hash];
+ }
+
+ public function matchesFilter(Filter $filter)
+ {
+ $filterObj = static::getPreparedFilter($filter);
+ if ($filterObj->isExpression() || ! $filterObj->isEmpty()) {
+ return AssignFilterHelper::matchesFilter($filterObj, $this->flatObject);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param Filter $filter
+ * @param Db $db
+ *
+ * @return array
+ */
+ public static function forFilter(Filter $filter, Db $db)
+ {
+ $result = array();
+ Benchmark::measure(sprintf('Starting Filter %s', $filter));
+ $filter = clone($filter);
+ static::fixFilterColumns($filter);
+ $helper = new AssignFilterHelper($filter);
+
+ foreach (static::flatObjects($db) as $object) {
+ if ($helper->matches($object)) {
+ $name = $object->object_name;
+ $result[] = $name;
+ }
+ }
+ Benchmark::measure(sprintf('Got %d results for %s', count($result), $filter));
+
+ return array_values($result);
+ }
+
+ protected static function getType()
+ {
+ if (static::$type === null) {
+ throw new ProgrammingError(
+ 'Implementations of %s need ::$type to be defined, %s has not',
+ __CLASS__,
+ get_called_class()
+ );
+ }
+
+ return static::$type;
+ }
+
+ protected static function flatObjects(Db $db)
+ {
+ if (self::$flatObjects === null) {
+ self::$flatObjects = static::fetchFlatObjects($db);
+ }
+
+ return self::$flatObjects;
+ }
+
+ protected static function raiseLimits()
+ {
+ // Note: IcingaConfig also raises the limit for generation, **but** we
+ // need the higher limit for preview.
+ MemoryLimit::raiseTo('1024M');
+ }
+
+ protected static function fetchFlatObjects(Db $db)
+ {
+ return static::fetchFlatObjectsByType($db, static::getType());
+ }
+
+ protected static function fetchFlatObjectsByType(Db $db, $type)
+ {
+ self::raiseLimits();
+
+ Benchmark::measure("ObjectApplyMatches: prefetching $type");
+ PrefetchCache::initialize($db);
+ /** @var IcingaObject $class */
+ $class = DbObjectTypeRegistry::classByType($type);
+ $all = $class::prefetchAll($db);
+ Benchmark::measure("ObjectApplyMatches: related objects for $type");
+ $class::prefetchAllRelationsByType($type, $db);
+ Benchmark::measure("ObjectApplyMatches: preparing flat $type objects");
+
+ $objects = array();
+ foreach ($all as $object) {
+ if ($object->isTemplate()) {
+ continue;
+ }
+
+ $flat = $object->toPlainObject(true, false);
+ static::flattenVars($flat);
+ $objects[$object->getObjectName()] = $flat;
+ }
+ Benchmark::measure("ObjectApplyMatches: $type cache ready");
+
+ return $objects;
+ }
+
+ public static function fixFilterColumns(Filter $filter)
+ {
+ if ($filter->isExpression()) {
+ /** @var FilterExpression $filter */
+ static::fixFilterExpressionColumn($filter);
+ } else {
+ foreach ($filter->filters() as $sub) {
+ static::fixFilterColumns($sub);
+ }
+ }
+ }
+
+ protected static function fixFilterExpressionColumn(FilterExpression $filter)
+ {
+ if (static::columnIsJson($filter)) {
+ $column = $filter->getExpression();
+ $filter->setExpression($filter->getColumn());
+ $filter->setColumn($column);
+ }
+
+ $col = $filter->getColumn();
+ $type = static::$type;
+
+ if ($type && substr($col, 0, strlen($type) + 1) === "${type}.") {
+ $filter->setColumn($col = substr($col, strlen($type) + 1));
+ }
+
+ if (array_key_exists($col, self::$columnMap)) {
+ $filter->setColumn(self::$columnMap[$col]);
+ }
+
+ $filter->setExpression(json_decode($filter->getExpression()));
+ }
+
+ protected static function columnIsJson(FilterExpression $filter)
+ {
+ $col = $filter->getColumn();
+ return strlen($col) && $col[0] === '"';
+ }
+
+ /**
+ * Helper, flattens all vars of a given object
+ *
+ * The object itself will be modified, and the 'vars' property will be
+ * replaced with corresponding 'vars.whatever' properties
+ *
+ * @param $object
+ * @param string $key
+ */
+ protected static function flattenVars(stdClass $object, $key = 'vars')
+ {
+ if (property_exists($object, 'vars')) {
+ foreach ($object->vars as $k => $v) {
+ if (is_object($v)) {
+ static::flattenVars($v, $k);
+ }
+ $object->{$key . '.' . $k} = $v;
+ }
+ unset($object->vars);
+ }
+ }
+
+ protected function __construct(IcingaObject $object)
+ {
+ $this->object = $object;
+ $flat = $object->toPlainObject(true, false);
+ // Sure, we are flat - but we might still want to match templates.
+ unset($flat->imports);
+ $flat->templates = $object->listFlatResolvedImportNames();
+ $this->addAppliedGroupsToFlatObject($flat, $object);
+ static::flattenVars($flat);
+ $this->flatObject = $flat;
+ }
+
+ protected function addAppliedGroupsToFlatObject($flat, IcingaObject $object)
+ {
+ if ($object instanceof IcingaHost) {
+ $appliedGroups = $object->getAppliedGroups();
+ if (! empty($appliedGroups)) {
+ if (isset($flat->groups)) {
+ $flat->groups = array_merge($flat->groups, $appliedGroups);
+ } else {
+ $flat->groups = $appliedGroups;
+ }
+ }
+ }
+ }
+}
diff --git a/library/Director/Objects/ObjectWithArguments.php b/library/Director/Objects/ObjectWithArguments.php
new file mode 100644
index 0000000..2f99460
--- /dev/null
+++ b/library/Director/Objects/ObjectWithArguments.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+interface ObjectWithArguments
+{
+ /**
+ * @return boolean
+ */
+ public function gotArguments();
+
+ /**
+ * @return IcingaArguments
+ */
+ public function arguments();
+
+ public function unsetArguments();
+}
diff --git a/library/Director/Objects/ServiceGroupMembershipResolver.php b/library/Director/Objects/ServiceGroupMembershipResolver.php
new file mode 100644
index 0000000..4649212
--- /dev/null
+++ b/library/Director/Objects/ServiceGroupMembershipResolver.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+class ServiceGroupMembershipResolver extends GroupMembershipResolver
+{
+ protected $type = 'service';
+}
diff --git a/library/Director/Objects/SyncProperty.php b/library/Director/Objects/SyncProperty.php
new file mode 100644
index 0000000..20c4700
--- /dev/null
+++ b/library/Director/Objects/SyncProperty.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Objects\Extension\PriorityColumn;
+
+class SyncProperty extends DbObject
+{
+ use PriorityColumn;
+
+ protected $table = 'sync_property';
+
+ protected $keyName = 'id';
+
+ protected $autoincKeyName = 'id';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'rule_id' => null,
+ 'source_id' => null,
+ 'source_expression' => null,
+ 'destination_field' => null,
+ 'priority' => null,
+ 'filter_expression' => null,
+ 'merge_policy' => null
+ ];
+
+ protected function beforeStore()
+ {
+ if (! $this->hasBeenLoadedFromDb() && $this->get('priority') === null) {
+ $this->setNextPriority('rule_id');
+ }
+ }
+
+ public function setSource($name)
+ {
+ $source = ImportSource::loadByName($name, $this->getConnection());
+ $this->set('source_id', $source->get('id'));
+
+ return $this;
+ }
+
+ protected function onInsert()
+ {
+ $this->refreshPriortyProperty();
+ }
+}
diff --git a/library/Director/Objects/SyncRule.php b/library/Director/Objects/SyncRule.php
new file mode 100644
index 0000000..89f7fd1
--- /dev/null
+++ b/library/Director/Objects/SyncRule.php
@@ -0,0 +1,553 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\Import\PurgeStrategy\PurgeStrategy;
+use Icinga\Module\Director\Import\Sync;
+use Exception;
+
+class SyncRule extends DbObject implements ExportInterface
+{
+ protected $table = 'sync_rule';
+
+ protected $keyName = 'rule_name';
+
+ protected $autoincKeyName = 'id';
+
+ protected $protectAutoinc = false;
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'rule_name' => null,
+ 'object_type' => null,
+ 'update_policy' => null,
+ 'purge_existing' => null,
+ 'purge_action' => null,
+ 'filter_expression' => null,
+ 'sync_state' => 'unknown',
+ 'last_error_message' => null,
+ 'last_attempt' => null,
+ 'description' => null,
+ ];
+
+ protected $stateProperties = [
+ 'sync_state',
+ 'last_error_message',
+ 'last_attempt',
+ ];
+
+ private $sync;
+
+ private $purgeStrategy;
+
+ private $filter;
+
+ private $hasCombinedKey;
+
+ /** @var SyncProperty[] */
+ private $syncProperties;
+
+ private $sourceKeyPattern;
+
+ private $destinationKeyPattern;
+
+ private $newSyncProperties;
+
+ private $originalId;
+
+ public function listInvolvedSourceIds()
+ {
+ if (! $this->hasBeenLoadedFromDb()) {
+ return [];
+ }
+
+ $db = $this->getDb();
+ return array_map('intval', array_unique(
+ $db->fetchCol(
+ $db->select()
+ ->from(['p' => 'sync_property'], 'p.source_id')
+ ->join(['s' => 'import_source'], 's.id = p.source_id', array())
+ ->where('rule_id = ?', $this->get('id'))
+ ->order('s.source_name')
+ )
+ ));
+ }
+
+ /**
+ * @return array
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function fetchInvolvedImportSources()
+ {
+ $sources = [];
+
+ foreach ($this->listInvolvedSourceIds() as $sourceId) {
+ $sources[$sourceId] = ImportSource::loadWithAutoIncId($sourceId, $this->getConnection());
+ }
+
+ return $sources;
+ }
+
+ public function getLastSyncTimestamp()
+ {
+ if (! $this->hasBeenLoadedFromDb()) {
+ return null;
+ }
+
+ $db = $this->getDb();
+ $query = $db->select()->from(
+ ['sr' => 'sync_run'],
+ 'sr.start_time'
+ )->where('sr.rule_id = ?', $this->get('id'))
+ ->order('sr.start_time DESC')
+ ->limit(1);
+
+ return $db->fetchOne($query);
+ }
+
+ public function getLastSyncRunId()
+ {
+ if (! $this->hasBeenLoadedFromDb()) {
+ return null;
+ }
+
+ $db = $this->getDb();
+ $query = $db->select()->from(
+ ['sr' => 'sync_run'],
+ 'sr.id'
+ )->where('sr.rule_id = ?', $this->get('id'))
+ ->order('sr.start_time DESC')
+ ->limit(1);
+
+ return $db->fetchOne($query);
+ }
+
+ public function matches($row)
+ {
+ if ($this->get('filter_expression') === null) {
+ return true;
+ }
+
+ return $this->filter()->matches($row);
+ }
+
+ /**
+ * @param bool $apply
+ * @return bool
+ * @throws DuplicateKeyException
+ */
+ public function checkForChanges($apply = false)
+ {
+ $hadChanges = false;
+
+ Benchmark::measure('Checking sync rule ' . $this->get('rule_name'));
+ try {
+ $this->set('last_attempt', date('Y-m-d H:i:s'));
+ $this->set('sync_state', 'unknown');
+ $sync = $this->sync();
+ if ($sync->hasModifications()) {
+ Benchmark::measure('Got modifications for sync rule ' . $this->get('rule_name'));
+ $this->set('sync_state', 'pending-changes');
+ if ($apply && $runId = $sync->apply()) {
+ Benchmark::measure('Successfully synced rule ' . $this->get('rule_name'));
+ $this->set('sync_state', 'in-sync');
+ }
+
+ $hadChanges = true;
+ } else {
+ Benchmark::measure('No modifications for sync rule ' . $this->get('rule_name'));
+ $this->set('sync_state', 'in-sync');
+ }
+
+ $this->set('last_error_message', null);
+ } catch (Exception $e) {
+ $this->set('sync_state', 'failing');
+ $this->set('last_error_message', $e->getMessage());
+ // TODO: Store last error details / trace?
+ }
+
+ if ($this->hasBeenModified()) {
+ $this->store();
+ }
+
+ return $hadChanges;
+ }
+
+ /**
+ * @return IcingaObject[]
+ * @throws IcingaException
+ */
+ public function getExpectedModifications()
+ {
+ return $this->sync()->getExpectedModifications();
+ }
+
+ /**
+ * @return bool
+ * @throws DuplicateKeyException
+ */
+ public function applyChanges()
+ {
+ return $this->checkForChanges(true);
+ }
+
+ public function getSourceKeyPattern()
+ {
+ if ($this->hasCombinedKey()) {
+ return $this->sourceKeyPattern;
+ } else {
+ return null; // ??
+ }
+ }
+
+ public function getDestinationKeyPattern()
+ {
+ if ($this->hasCombinedKey()) {
+ return $this->destinationKeyPattern;
+ } else {
+ return null; // ??
+ }
+ }
+
+ protected function sync()
+ {
+ if ($this->sync === null) {
+ $this->sync = new Sync($this);
+ }
+
+ return $this->sync;
+ }
+
+ /**
+ * @return Filter
+ */
+ public function filter()
+ {
+ if ($this->filter === null) {
+ $this->filter = Filter::fromQueryString($this->get('filter_expression'));
+ }
+
+ return $this->filter;
+ }
+
+ public function purgeStrategy()
+ {
+ if ($this->purgeStrategy === null) {
+ $this->purgeStrategy = $this->loadConfiguredPurgeStrategy();
+ }
+
+ return $this->purgeStrategy;
+ }
+
+ // TODO: Allow for more
+ protected function loadConfiguredPurgeStrategy()
+ {
+ if ($this->get('purge_existing') === 'y') {
+ return PurgeStrategy::load('ImportRunBased', $this);
+ } else {
+ return PurgeStrategy::load('PurgeNothing', $this);
+ }
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @return object
+ */
+ public function export()
+ {
+ $plain = $this->getProperties();
+ $plain['originalId'] = $plain['id'];
+ unset($plain['id']);
+
+ foreach ($this->stateProperties as $key) {
+ unset($plain[$key]);
+ }
+ $plain['properties'] = $this->exportSyncProperties();
+ ksort($plain);
+
+ return (object) $plain;
+ }
+
+ /**
+ * @param object $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return static
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ if (isset($properties['originalId'])) {
+ $id = $properties['originalId'];
+ unset($properties['originalId']);
+ } else {
+ $id = null;
+ }
+ $name = $properties['rule_name'];
+
+ if ($replace && $id && static::existsWithNameAndId($name, $id, $db)) {
+ $object = static::loadWithAutoIncId($id, $db);
+ } elseif ($replace && static::exists($name, $db)) {
+ $object = static::load($name, $db);
+ } elseif (static::existsWithName($name, $db)) {
+ throw new DuplicateKeyException(
+ 'Sync Rule %s already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ $object->newSyncProperties = $properties['properties'];
+ unset($properties['properties']);
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ public function getUniqueIdentifier()
+ {
+ return $this->get('rule_name');
+ }
+
+ /**
+ * @throws DuplicateKeyException
+ */
+ protected function onStore()
+ {
+ parent::onStore();
+ if ($this->newSyncProperties !== null) {
+ $connection = $this->getConnection();
+ $db = $connection->getDbAdapter();
+ $myId = $this->get('id');
+ if ($this->originalId === null) {
+ $originalId = $myId;
+ } else {
+ $originalId = $this->originalId;
+ $this->originalId = null;
+ }
+ if ($this->hasBeenLoadedFromDb()) {
+ $db->delete(
+ 'sync_property',
+ $db->quoteInto('rule_id = ?', $myId)
+ );
+ }
+
+ foreach ($this->newSyncProperties as $property) {
+ unset($property->rule_name);
+ $property = SyncProperty::create((array) $property, $connection);
+ $property->set('rule_id', $myId);
+ $property->store();
+ }
+ }
+ }
+
+ /**
+ * @deprecated
+ * @return array
+ */
+ protected function exportSyncProperties()
+ {
+ $all = [];
+ $db = $this->getDb();
+ $sourceNames = $db->fetchPairs(
+ $db->select()->from('import_source', ['id', 'source_name'])
+ );
+
+ foreach ($this->getSyncProperties() as $property) {
+ $properties = $property->getProperties();
+ $properties['source'] = $sourceNames[$properties['source_id']];
+ unset($properties['id']);
+ unset($properties['rule_id']);
+ unset($properties['source_id']);
+ ksort($properties);
+ $all[] = (object) $properties;
+ }
+
+ return $all;
+ }
+
+ /**
+ * Whether we have a combined key (e.g. services on hosts)
+ *
+ * @return bool
+ */
+ public function hasCombinedKey()
+ {
+ if ($this->hasCombinedKey === null) {
+ $this->hasCombinedKey = false;
+
+ // TODO: Move to Objects
+ if ($this->get('object_type') === 'service') {
+ $hasHost = false;
+ $hasObjectName = false;
+ $hasServiceSet = false;
+
+ foreach ($this->getSyncProperties() as $key => $property) {
+ if ($property->destination_field === 'host') {
+ $hasHost = $property->source_expression;
+ }
+ if ($property->destination_field === 'service_set') {
+ $hasServiceSet = $property->source_expression;
+ }
+ if ($property->destination_field === 'object_name') {
+ $hasObjectName = $property->source_expression;
+ }
+ }
+
+ if ($hasHost !== false && $hasObjectName !== false) {
+ $this->hasCombinedKey = true;
+ $this->sourceKeyPattern = sprintf(
+ '%s!%s',
+ $hasHost,
+ $hasObjectName
+ );
+
+ $this->destinationKeyPattern = '${host}!${object_name}';
+ } elseif ($hasServiceSet !== false && $hasObjectName !== false) {
+ $this->hasCombinedKey = true;
+ $this->sourceKeyPattern = sprintf(
+ '%s!%s',
+ $hasServiceSet,
+ $hasObjectName
+ );
+
+ $this->destinationKeyPattern = '${service_set}!${object_name}';
+ }
+ } elseif ($this->get('object_type') === 'serviceSet') {
+ $hasHost = false;
+ $hasObjectName = false;
+
+ foreach ($this->getSyncProperties() as $key => $property) {
+ if ($property->destination_field === 'host') {
+ $hasHost = $property->source_expression;
+ }
+ if ($property->destination_field === 'object_name') {
+ $hasObjectName = $property->source_expression;
+ }
+ }
+
+ if ($hasHost !== false && $hasObjectName !== false) {
+ $this->hasCombinedKey = true;
+ $this->sourceKeyPattern = sprintf(
+ '%s!%s',
+ $hasHost,
+ $hasObjectName
+ );
+
+ $this->destinationKeyPattern = '${host}!${object_name}';
+ }
+ } elseif ($this->get('object_type') === 'datalistEntry') {
+ $hasList = false;
+ $hasName = false;
+
+ foreach ($this->getSyncProperties() as $key => $property) {
+ if ($property->destination_field === 'list_id') {
+ $hasList = $property->source_expression;
+ }
+ if ($property->destination_field === 'entry_name') {
+ $hasName = $property->source_expression;
+ }
+ }
+
+ if ($hasList !== false && $hasName !== false) {
+ $this->hasCombinedKey = true;
+ $this->sourceKeyPattern = sprintf(
+ '%s!%s',
+ $hasList,
+ $hasName
+ );
+
+ $this->destinationKeyPattern = '${list_id}!${entry_name}';
+ }
+ }
+ }
+
+ return $this->hasCombinedKey;
+ }
+
+ public function hasSyncProperties()
+ {
+ $properties = $this->getSyncProperties();
+ return ! empty($properties);
+ }
+
+ /**
+ * @return SyncProperty[]
+ */
+ public function getSyncProperties()
+ {
+ if (! $this->hasBeenLoadedFromDb()) {
+ return [];
+ }
+
+ if ($this->syncProperties === null) {
+ $this->syncProperties = $this->fetchSyncProperties();
+ }
+
+ return $this->syncProperties;
+ }
+
+ public function fetchSyncProperties()
+ {
+ $db = $this->getDb();
+
+ return SyncProperty::loadAll(
+ $this->getConnection(),
+ $db->select()
+ ->from('sync_property')
+ ->where('rule_id = ?', $this->get('id'))
+ ->order('priority ASC')
+ );
+ }
+
+ /**
+ * TODO: implement in a generic way, this is duplicated code
+ *
+ * @param string $name
+ * @param Db $connection
+ * @api internal
+ * @return bool
+ */
+ public static function existsWithName($name, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+
+ return (string) $name === (string) $db->fetchOne(
+ $db->select()
+ ->from('sync_rule', 'rule_name')
+ ->where('rule_name = ?', $name)
+ );
+ }
+
+ /**
+ * @param string $name
+ * @param int $id
+ * @param Db $connection
+ * @api internal
+ * @return bool
+ */
+ protected static function existsWithNameAndId($name, $id, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $dummy = new static;
+ $idCol = $dummy->autoincKeyName;
+ $keyCol = $dummy->keyName;
+
+ return (string) $id === (string) $db->fetchOne(
+ $db->select()
+ ->from($dummy->table, $idCol)
+ ->where("$idCol = ?", $id)
+ ->where("$keyCol = ?", $name)
+ );
+ }
+}
diff --git a/library/Director/Objects/SyncRun.php b/library/Director/Objects/SyncRun.php
new file mode 100644
index 0000000..62f7378
--- /dev/null
+++ b/library/Director/Objects/SyncRun.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+
+class SyncRun extends DbObject
+{
+ protected $table = 'sync_run';
+
+ protected $keyName = 'id';
+
+ protected $autoincKeyName = 'id';
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'rule_id' => null,
+ 'rule_name' => null,
+ 'start_time' => null,
+ 'duration_ms' => null,
+ 'objects_created' => null,
+ 'objects_deleted' => null,
+ 'objects_modified' => null,
+ 'last_former_activity' => null,
+ 'last_related_activity' => null,
+ );
+
+ public static function start(SyncRule $rule)
+ {
+ return static::create(
+ array(
+ 'start_time' => date('Y-m-d H:i:s'),
+ 'rule_id' => $rule->id,
+ 'rule_name' => $rule->rule_name,
+ ),
+ $rule->getConnection()
+ );
+ }
+
+ public function countActivities()
+ {
+ return (int) $this->get('objects_deleted')
+ + (int) $this->get('objects_created')
+ + (int) $this->get('objects_modified');
+ }
+}
diff --git a/library/Director/PlainObjectRenderer.php b/library/Director/PlainObjectRenderer.php
new file mode 100644
index 0000000..4dadf4f
--- /dev/null
+++ b/library/Director/PlainObjectRenderer.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+class PlainObjectRenderer
+{
+ const INDENTATION = ' ';
+
+ public static function render($object)
+ {
+ return self::renderObject($object);
+ }
+
+ protected static function renderBoolean($value)
+ {
+ return $value ? 'true' : 'false';
+ }
+
+ protected static function renderInteger($value)
+ {
+ return (string) $value;
+ }
+
+ protected static function renderFloat($value)
+ {
+ // Render .0000 floats as integers, mainly because of some JSON
+ // implementations:
+ if ((string) (int) $value === (string) $value) {
+ return static::renderInteger((int) $value);
+ } else {
+ return sprintf('%F', $value);
+ }
+ }
+
+ protected static function renderNull()
+ {
+ return 'null';
+ }
+
+ protected static function renderString($value)
+ {
+ return '"' . addslashes($value) . '"';
+ }
+
+ protected static function renderArray($array, $prefix = '')
+ {
+ if (empty($array)) {
+ return '[]';
+ }
+
+ $vals = array();
+
+ foreach ($array as $val) {
+ $vals[] = $prefix
+ . self::INDENTATION
+ . self::renderObject($val, $prefix . self::INDENTATION);
+ }
+ return "[\n" . implode(",\n", $vals) . "\n$prefix]";
+ }
+
+ protected static function renderHash($hash, $prefix = '')
+ {
+ $vals = array();
+ $hash = (array) $hash;
+ if (empty($hash)) {
+ return '{}';
+ }
+
+ if (count($hash) === 1) {
+ $current = self::renderObject(current($hash), $prefix . self::INDENTATION);
+ if (strlen($current) < 62) {
+ return sprintf(
+ '{ %s: %s }',
+ key($hash),
+ $current
+ );
+ }
+ }
+
+ ksort($hash);
+ foreach ($hash as $key => $val) {
+ $vals[] = $prefix
+ . self::INDENTATION
+ . $key
+ . ': '
+ . self::renderObject($val, $prefix . self::INDENTATION);
+ }
+ return "{\n" . implode(",\n", $vals) . "\n$prefix}";
+ }
+
+ protected static function renderObject($object, $prefix = '')
+ {
+ if (is_null($object)) {
+ return self::renderNull();
+ } elseif (is_bool($object)) {
+ return self::renderBoolean($object);
+ } elseif (is_integer($object)) {
+ return self::renderInteger($object);
+ } elseif (is_float($object)) {
+ return self::renderFloat($object);
+ } elseif (is_object($object) || static::isAssocArray($object)) {
+ return self::renderHash($object, $prefix);
+ } elseif (is_array($object)) {
+ return self::renderArray($object, $prefix);
+ } elseif (is_string($object)) {
+ return self::renderString($object);
+ } else {
+ return '(UNKNOWN TYPE) ' . var_export($object, 1);
+ }
+ }
+
+ /**
+ * Check if an array contains assoc keys
+ *
+ * @from https://stackoverflow.com/questions/173400/how-to-check-if-php-array-is-associative-or-sequential
+ * @param $arr
+ * @return bool
+ */
+ protected static function isAssocArray($arr)
+ {
+ if (! is_array($arr)) {
+ return false;
+ }
+ if (array() === $arr) {
+ return false;
+ }
+
+ return array_keys($arr) !== range(0, count($arr) - 1);
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php b/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php
new file mode 100644
index 0000000..0ffc8af
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierArrayElementByPosition.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use InvalidArgumentException;
+use stdClass;
+
+class PropertyModifierArrayElementByPosition extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Get a specific Array Element';
+ }
+
+ public function hasArraySupport()
+ {
+ return true;
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'position_type', [
+ 'label' => $form->translate('Position Type'),
+ 'required' => true,
+ 'multiOptions' => $form->optionalEnum([
+ 'first' => $form->translate('First Element'),
+ 'last' => $form->translate('Last Element'),
+ 'fixed' => $form->translate('Specific Element (by position)'),
+ 'keyname' => $form->translate('Specific Element (by key name)'),
+ ]),
+ ]);
+
+ $form->addElement('text', 'position', [
+ 'label' => $form->translate('Position'),
+ 'description' => $form->translate(
+ 'Numeric position or key name'
+ ),
+ ]);
+
+ $form->addElement('select', 'when_missing', [
+ 'label' => $form->translate('When not available'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'What should happen when the specified element is not available?'
+ ),
+ 'value' => 'null',
+ 'multiOptions' => $form->optionalEnum([
+ 'fail' => $form->translate('Let the whole Import Run fail'),
+ 'null' => $form->translate('return NULL'),
+ ])
+ ]);
+ }
+
+ /**
+ * @param $value
+ * @return string|null
+ * @throws ConfigurationError
+ * @throws InvalidArgumentException
+ */
+ public function transform($value)
+ {
+ // First and Last will work with hashes too:
+ if ($value instanceof stdClass) {
+ $value = (array) $value;
+ }
+
+ if (! is_array($value)) {
+ return $this->emptyValue($value);
+ }
+
+ switch ($this->getSetting('position_type')) {
+ case 'first':
+ if (empty($value)) {
+ return $this->emptyValue($value);
+ } else {
+ return array_shift($value);
+ }
+ // https://github.com/squizlabs/PHP_CodeSniffer/pull/1363
+ case 'last':
+ if (empty($value)) {
+ return $this->emptyValue($value);
+ } else {
+ return array_pop($value);
+ }
+ // https://github.com/squizlabs/PHP_CodeSniffer/pull/1363
+ case 'fixed':
+ $pos = $this->getSetting('position');
+ if (! is_int($pos) && ! ctype_digit($pos)) {
+ throw new InvalidArgumentException(sprintf(
+ '"%s" is not a valid array position',
+ $pos
+ ));
+ }
+ $pos = (int) $pos;
+
+ if (array_key_exists($pos, $value)) {
+ return $value[$pos];
+ } else {
+ return $this->emptyValue($value);
+ }
+ // https://github.com/squizlabs/PHP_CodeSniffer/pull/1363
+ case 'keyname':
+ $pos = $this->getSetting('position');
+ if (! is_string($pos)) {
+ throw new InvalidArgumentException(sprintf(
+ '"%s" is not a valid array key name',
+ $pos
+ ));
+ }
+
+ if (array_key_exists($pos, $value)) {
+ return $value[$pos];
+ } else {
+ return $this->emptyValue($value);
+ }
+ // https://github.com/squizlabs/PHP_CodeSniffer/pull/1363
+ default:
+ throw new ConfigurationError(
+ '"%s" is not a valid array position_type',
+ $this->getSetting('position_type')
+ );
+ }
+ }
+
+ /**
+ * @return string
+ * @throws ConfigurationError
+ */
+ protected function getPositionForError()
+ {
+ switch ($this->getSetting('position_type')) {
+ case 'first':
+ return 'first';
+ case 'last':
+ return 'last';
+ case 'fixed':
+ return '#' . $this->getSetting('position');
+ case 'keyname':
+ return '#' . $this->getSetting('position');
+ default:
+ throw new ConfigurationError(
+ '"%s" is not a valid array position_type',
+ $this->getSetting('position_type')
+ );
+ }
+ }
+
+ /**
+ * @param $value
+ * @return null
+ * @throws ConfigurationError
+ */
+ protected function emptyValue($value)
+ {
+ if ($this->getSetting('when_missing', 'fail') === 'null') {
+ return null;
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'There is no %s element in %s',
+ $this->getPositionForError(),
+ json_encode($value)
+ ));
+ }
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierArrayFilter.php b/library/Director/PropertyModifier/PropertyModifierArrayFilter.php
new file mode 100644
index 0000000..0b52987
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierArrayFilter.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierArrayFilter extends PropertyModifierHook
+{
+ /** @var FilterExpression */
+ private $filterExpression;
+
+ public function getName()
+ {
+ return 'Filter Array Values';
+ }
+
+ public function hasArraySupport()
+ {
+ return true;
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'filter_method', array(
+ 'label' => $form->translate('Filter method'),
+ 'required' => true,
+ 'value' => 'wildcard',
+ 'multiOptions' => $form->optionalEnum(array(
+ 'wildcard' => $form->translate('Simple match with wildcards (*)'),
+ 'regex' => $form->translate('Regular Expression'),
+ )),
+ ));
+
+ $form->addElement('text', 'filter_string', array(
+ 'label' => 'Filter',
+ 'description' => $form->translate(
+ 'The string/pattern you want to search for. Depends on the'
+ . ' chosen method, use www.* or *linux* for wildcard matches'
+ . ' and expression like /^www\d+\./ in case you opted for a'
+ . ' regular expression'
+ ),
+ 'required' => true,
+ ));
+
+ $form->addElement('select', 'policy', array(
+ 'label' => $form->translate('Policy'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'What should happen with matching elements?'
+ ),
+ 'value' => 'keep',
+ 'multiOptions' => array(
+ 'keep' => $form->translate('Keep matching elements'),
+ 'reject' => $form->translate('Reject matching elements'),
+ ),
+ ));
+
+ $form->addElement('select', 'when_empty', array(
+ 'label' => $form->translate('When empty'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'What should happen when the result array is empty?'
+ ),
+ 'value' => 'empty_array',
+ 'multiOptions' => $form->optionalEnum(array(
+ 'empty_array' => $form->translate('return an empty array'),
+ 'null' => $form->translate('return NULL'),
+ ))
+ ));
+ }
+
+ public function matchesRegexp($string, $expression)
+ {
+ return preg_match($expression, $string);
+ }
+
+ public function matchesWildcard($string, $expression)
+ {
+ return $this->filterExpression->matches(
+ (object) array('value' => $string)
+ );
+ }
+
+ public function transform($value)
+ {
+ if (empty($value)) {
+ return $this->emptyValue();
+ }
+
+ if (is_string($value)) {
+ $value = [$value];
+ }
+
+ if (! is_array($value)) {
+ throw new InvalidPropertyException(
+ 'The ArrayFilter property modifier be applied to arrays only'
+ );
+ }
+
+ $method = $this->getSetting('filter_method');
+ $filter = $this->getSetting('filter_string');
+ $policy = $this->getSetting('policy');
+
+ switch ($method) {
+ case 'wildcard':
+ $func = 'matchesWildcard';
+ $this->filterExpression = new FilterExpression('value', '=', $filter);
+ break;
+ case 'regex':
+ $func = 'matchesRegexp';
+ break;
+ default:
+ throw new ConfigurationError(
+ '%s is not a valid value for an ArrayFilter filter_method',
+ var_export($method, 1)
+ );
+ }
+
+ $result = array();
+
+ foreach ($value as $val) {
+ if ($this->$func($val, $filter)) {
+ if ($policy === 'keep') {
+ $result[] = $val;
+ }
+ } else {
+ if ($policy === 'reject') {
+ $result[] = $val;
+ }
+ }
+ }
+
+ if (empty($result)) {
+ return $this->emptyValue();
+ }
+
+ return $result;
+ }
+
+ protected function emptyValue()
+ {
+ if ($this->getSetting('when_empty', 'empty_array') === 'empty_array') {
+ return array();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierArrayToRow.php b/library/Director/PropertyModifier/PropertyModifierArrayToRow.php
new file mode 100644
index 0000000..35ae063
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierArrayToRow.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use InvalidArgumentException;
+use ipl\Html\Error;
+
+class PropertyModifierArrayToRow extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Clone the row for every entry of an Array';
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'on_empty', [
+ 'label' => 'When empty',
+ 'description' => $form->translate('What should we do in case the given value is empty?'),
+ 'multiOptions' => $form->optionalEnum([
+ 'reject' => $form->translate('Drop the current row'),
+ 'fail' => $form->translate('Let the whole import run fail'),
+ 'keep' => $form->translate('Keep the row, set the column value to null'),
+ ]),
+ 'value' => 'reject',
+ 'required' => true,
+ ]);
+ }
+
+ public function hasArraySupport()
+ {
+ return true;
+ }
+
+ public function expandsRows()
+ {
+ return true;
+ }
+
+ public function transform($value)
+ {
+ if (empty($value)) {
+ $onDuplicate = $this->getSetting('on_empty', 'reject');
+ switch ($onDuplicate) {
+ case 'reject':
+ return [];
+ case 'keep':
+ return [null];
+ case 'fail':
+ throw new InvalidArgumentException('Failed to clone row, value is empty');
+ default:
+ throw new InvalidArgumentException(
+ "'$onDuplicate' is not a valid 'on_duplicate' setting"
+ );
+ }
+ }
+
+ if (! \is_array($value)) {
+ throw new InvalidArgumentException(
+ "Array required to clone this row, got " . Error::getPhpTypeName($value)
+ );
+ }
+
+ return $value;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierArrayUnique.php b/library/Director/PropertyModifier/PropertyModifierArrayUnique.php
new file mode 100644
index 0000000..e3446f9
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierArrayUnique.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use function array_unique;
+use function array_values;
+use function is_array;
+
+class PropertyModifierArrayUnique extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Unique Array Values';
+ }
+
+ public function hasArraySupport()
+ {
+ return true;
+ }
+
+ public function transform($value)
+ {
+ if (empty($value)) {
+ return $value;
+ }
+
+ if (! is_array($value)) {
+ throw new InvalidPropertyException(
+ 'The ArrayUnique property modifier can be applied to arrays only'
+ );
+ }
+
+ return array_values(array_unique($value));
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierBitmask.php b/library/Director/PropertyModifier/PropertyModifierBitmask.php
new file mode 100644
index 0000000..d334f09
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierBitmask.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierBitmask extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'bitmask', array(
+ 'label' => 'Bitmask',
+ 'description' => $form->translate(
+ 'The numeric bitmask you want to apply. In case you have a hexadecimal'
+ . ' or binary mask please transform it to a decimal number first. The'
+ . ' result of this modifier is a boolean value, telling whether the'
+ . ' given mask applies to the numeric value in your source column'
+ ),
+ 'required' => true,
+ ));
+ }
+
+ public function getName()
+ {
+ return 'Bitmask match (numeric)';
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ $mask = (int) $this->getSetting('bitmask');
+ return (((int) $value) & $mask) === $mask;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierCombine.php b/library/Director/PropertyModifier/PropertyModifierCombine.php
new file mode 100644
index 0000000..5be09ea
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierCombine.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Import\SyncUtils;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierCombine extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'pattern', array(
+ 'label' => $form->translate('Pattern'),
+ 'required' => false,
+ 'description' => $form->translate(
+ 'This pattern will be evaluated, and variables like ${some_column}'
+ . ' will be filled accordingly. A typical use-case is generating'
+ . ' unique service identifiers via ${host}!${service} in case your'
+ . ' data source doesn\'t allow you to ship such. The chosen "property"'
+ . ' has no effect here and will be ignored.'
+ )
+ ));
+ }
+
+ public function getName()
+ {
+ return 'Combine multiple properties';
+ }
+
+ public function requiresRow()
+ {
+ return true;
+ }
+
+ public function transform($value)
+ {
+ return SyncUtils::fillVariables($this->getSetting('pattern'), $this->getRow());
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php b/library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php
new file mode 100644
index 0000000..2a60ab3
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Data\InvalidDataException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use InvalidArgumentException;
+use ipl\Html\Error;
+
+class PropertyModifierDictionaryToRow extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Clone the row for every entry of a nested Dictionary/Hash structure';
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'key_column', [
+ 'label' => $form->translate('Key Property Name'),
+ 'description' => $form->translate(
+ 'Every Dictionary entry has a key, its value will be provided in this column'
+ )
+ ]);
+ $form->addElement('select', 'on_empty', [
+ 'label' => $form->translate('When empty'),
+ 'description' => $form->translate('What should we do in case the given value is empty?'),
+ 'multiOptions' => $form->optionalEnum([
+ 'reject' => $form->translate('Drop the current row'),
+ 'fail' => $form->translate('Let the whole import run fail'),
+ 'keep' => $form->translate('Keep the row, set the column value to null'),
+ ]),
+ 'value' => 'reject',
+ 'required' => true,
+ ]);
+ }
+
+ public function requiresRow()
+ {
+ return true;
+ }
+
+ public function hasArraySupport()
+ {
+ return true;
+ }
+
+ public function expandsRows()
+ {
+ return true;
+ }
+
+ public function transform($value)
+ {
+ if (empty($value)) {
+ $onDuplicate = $this->getSetting('on_empty', 'reject');
+ switch ($onDuplicate) {
+ case 'reject':
+ return [];
+ case 'keep':
+ return [null];
+ case 'fail':
+ throw new InvalidArgumentException('Failed to clone row, value is empty');
+ default:
+ throw new InvalidArgumentException(
+ "'$onDuplicate' is not a valid 'on_duplicate' setting"
+ );
+ }
+ }
+
+ $keyColumn = $this->getSetting('key_column');
+
+ if (! \is_object($value)) {
+ throw new InvalidArgumentException(
+ "Object required to clone this row, got " . Error::getPhpTypeName($value)
+ );
+ }
+ $result = [];
+ foreach ($value as $key => $properties) {
+ if (! is_object($properties)) {
+ throw new InvalidDataException(
+ sprintf('Nested "%s" dictionary', $key),
+ $properties
+ );
+ }
+
+ $properties->$keyColumn = $key;
+ $result[] = $properties;
+ }
+
+ return $result;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierDnsRecords.php b/library/Director/PropertyModifier/PropertyModifierDnsRecords.php
new file mode 100644
index 0000000..d5d8d41
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierDnsRecords.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierDnsRecords extends PropertyModifierHook
+{
+ protected static $types = array(
+ 'A' => DNS_A,
+ 'AAAA' => DNS_AAAA,
+ 'CNAME' => DNS_CNAME,
+ 'MX' => DNS_MX,
+ 'NS' => DNS_NS,
+ 'PTR' => DNS_PTR,
+ 'TXT' => DNS_TXT,
+ );
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'record_type', array(
+ 'label' => 'Record type',
+ 'description' => $form->translate('DNS record type'),
+ 'multiOptions' => $form->optionalEnum(static::enumTypes()),
+ 'required' => true,
+ ));
+
+ $form->addElement('select', 'on_failure', array(
+ 'label' => 'On failure',
+ 'description' => $form->translate('What should we do if the DNS lookup fails?'),
+ 'multiOptions' => $form->optionalEnum(array(
+ 'null' => $form->translate('Set no value (null)'),
+ 'keep' => $form->translate('Keep the property as is'),
+ 'fail' => $form->translate('Let the whole import run fail'),
+ )),
+ 'required' => true,
+ ));
+ }
+
+ protected static function enumTypes()
+ {
+ $types = array_keys(self::$types);
+ return array_combine($types, $types);
+ }
+
+ public function getName()
+ {
+ return 'Get DNS records of a specific type';
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ $type = self::$types[$this->getSetting('record_type')];
+ $response = dns_get_record($value, $type);
+
+ if ($response === false) {
+ switch ($this->getSetting('on_failure')) {
+ case 'null':
+ return null;
+ case 'keep':
+ return $value;
+ case 'fail':
+ default:
+ throw new InvalidPropertyException(
+ 'DNS lookup failed for "%s"',
+ $value
+ );
+ }
+ }
+
+ $result = array();
+ switch ($type) {
+ case DNS_A:
+ return $this->extractProperty('ip', $response);
+ case DNS_AAAA:
+ return $this->extractProperty('ipv6', $response);
+ case DNS_CNAME:
+ case DNS_MX:
+ case DNS_NS:
+ case DNS_PTR:
+ return $this->extractProperty('target', $response);
+ case DNS_TXT:
+ return $this->extractProperty('txt', $response);
+ return $response;
+ }
+
+ return $result;
+ }
+
+ protected function extractProperty($key, $response)
+ {
+ $result = array();
+ foreach ($response as $entry) {
+ $result[] = $entry[$key];
+ }
+
+ if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
+ sort($result, SORT_NATURAL);
+ } else {
+ natsort($result);
+ $result = array_values($result);
+ }
+
+ return $result;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php b/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php
new file mode 100644
index 0000000..c79c5b2
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Icinga\Protocol\Ldap\LdapUtils;
+
+class PropertyModifierExtractFromDN extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'dn_component', array(
+ 'label' => $form->translate('DN component'),
+ 'description' => $form->translate('What should we extract from the DN?'),
+ 'multiOptions' => $form->optionalEnum(array(
+ 'cn' => $form->translate('The first (leftmost) CN'),
+ 'ou' => $form->translate('The first (leftmost) OU'),
+ 'first' => $form->translate('Any first (leftmost) component'),
+ 'last_ou' => $form->translate('The last (rightmost) OU'),
+ )),
+ 'required' => true,
+ ));
+
+ $form->addElement('select', 'on_failure', array(
+ 'label' => $form->translate('On failure'),
+ 'description' => $form->translate('What should we do if the desired part does not exist?'),
+ 'multiOptions' => $form->optionalEnum(array(
+ 'null' => $form->translate('Set no value (null)'),
+ 'keep' => $form->translate('Keep the DN as is'),
+ 'fail' => $form->translate('Let the whole import run fail'),
+ )),
+ 'required' => true,
+ ));
+ }
+
+ public function getName()
+ {
+ return 'Extract from a Distinguished Name (DN)';
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ $parts = LdapUtils::explodeDN($value);
+ $result = null;
+
+ switch ($this->getSetting('dn_component')) {
+ case 'cn':
+ $result = $this->extractFirst($parts, 'cn');
+ break;
+ case 'ou':
+ $result = $this->extractFirst($parts, 'ou');
+ break;
+ case 'last_ou':
+ $result = $this->extractFirst(array_reverse($parts), 'ou');
+ break;
+ case 'first':
+ $result = $this->extractFirst($parts);
+ break;
+ }
+
+ if ($result === null) {
+ switch ($this->getSetting('on_failure')) {
+ case 'null':
+ return null;
+ case 'keep':
+ return $value;
+ case 'fail':
+ default:
+ throw new InvalidPropertyException(
+ 'DN part extraction failed for %s',
+ var_export($value, 1)
+ );
+ }
+ }
+
+ return $result;
+ }
+
+ protected function extractFirst($parts, $what = null)
+ {
+ foreach ($parts as $part) {
+ if (false === ($pos = strpos($part, '='))) {
+ continue;
+ }
+
+ if (null === $what || strtolower(substr($part, 0, $pos)) === $what) {
+ return substr($part, $pos + 1);
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierFromAdSid.php b/library/Director/PropertyModifier/PropertyModifierFromAdSid.php
new file mode 100644
index 0000000..ee306e3
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierFromAdSid.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+class PropertyModifierFromAdSid extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Decode a binary object SID (MSAD)';
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ // Strongly inspired by
+ // http://www.chadsikorra.com/blog/decoding-and-encoding-active-directory-objectsid-php
+ //
+ // Not perfect yet, but should suffice for now. When improving this please also see:
+ // https://blogs.msdn.microsoft.com/oldnewthing/20040315-00/?p=40253
+
+ $sid = $value;
+ $sidHex = unpack('H*hex', $value);
+ $sidHex = $sidHex['hex'];
+ $subAuths = implode('-', unpack('H2/H2/n/N/V*', $sid));
+
+ $revLevel = hexdec(substr($sidHex, 0, 2));
+ $authIdent = hexdec(substr($sidHex, 4, 12));
+
+ return sprintf('S-%s-%s-%s', $revLevel, $authIdent, $subAuths);
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierFromLatin1.php b/library/Director/PropertyModifier/PropertyModifierFromLatin1.php
new file mode 100644
index 0000000..272956c
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierFromLatin1.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use function iconv;
+
+class PropertyModifierFromLatin1 extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Convert a latin1 string to utf8';
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ return iconv('ISO-8859-15', 'UTF-8', $value);
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierGetHostByAddr.php b/library/Director/PropertyModifier/PropertyModifierGetHostByAddr.php
new file mode 100644
index 0000000..2b0f9c3
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierGetHostByAddr.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierGetHostByAddr extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'on_failure', array(
+ 'label' => 'On failure',
+ 'description' => $form->translate('What should we do if the host (DNS) lookup fails?'),
+ 'multiOptions' => $form->optionalEnum(array(
+ 'null' => $form->translate('Set no value (null)'),
+ 'keep' => $form->translate('Keep the property (hostname) as is'),
+ 'fail' => $form->translate('Let the whole import run fail'),
+ )),
+ 'required' => true,
+ ));
+ }
+
+ public function getName()
+ {
+ return mt('director', 'Get host by address (Reverse DNS lookup)');
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+ $host = gethostbyaddr($value);
+ if ($host === false) {
+ switch ($this->getSetting('on_failure')) {
+ case 'null':
+ return null;
+ case 'keep':
+ return $value;
+ case 'fail':
+ default:
+ throw new InvalidPropertyException(
+ 'Reverse Host lookup failed for "%s"',
+ $value
+ );
+ }
+ }
+
+ return $host;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierGetHostByName.php b/library/Director/PropertyModifier/PropertyModifierGetHostByName.php
new file mode 100644
index 0000000..36884e8
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierGetHostByName.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierGetHostByName extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'on_failure', array(
+ 'label' => 'On failure',
+ 'description' => $form->translate('What should we do if the host (DNS) lookup fails?'),
+ 'multiOptions' => $form->optionalEnum(array(
+ 'null' => $form->translate('Set no value (null)'),
+ 'keep' => $form->translate('Keep the property (hostname) as is'),
+ 'fail' => $form->translate('Let the whole import run fail'),
+ )),
+ 'required' => true,
+ ));
+ }
+
+ public function getName()
+ {
+ return mt('director', 'Get host by name (DNS lookup)');
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ $host = gethostbyname($value);
+ if (strlen(@inet_pton($host)) !== 4) {
+ switch ($this->getSetting('on_failure')) {
+ case 'null':
+ return null;
+ case 'keep':
+ return $value;
+ case 'fail':
+ default:
+ throw new InvalidPropertyException(
+ 'Host lookup failed for "%s"',
+ $value
+ );
+ }
+ }
+
+ return $host;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php b/library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php
new file mode 100644
index 0000000..d57b427
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierGetPropertyFromOtherImportSource.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Objects\ImportRowModifier;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierGetPropertyFromOtherImportSource extends PropertyModifierHook
+{
+ protected $importSource;
+
+ private $importedData;
+
+ public function getName()
+ {
+ return 'Get a property from another Import Source';
+ }
+
+ /**
+ * @inheritdoc
+ * @throws \Zend_Form_Exception
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ if (! $form instanceof DirectorObjectForm) {
+ throw new \RuntimeException('This property modifier works only with a DirectorObjectForm');
+ }
+ $db = $form->getDb();
+ $form->addElement('select', 'import_source_id', [
+ 'label' => $form->translate('Import Source'),
+ 'description' => $form->translate(
+ 'Another Import Source. We\'re going to look up the row with the'
+ . ' key matching the value in the chosen column'
+ ),
+ 'required' => true,
+ 'multiOptions' => $form->optionalEnum($db->enumImportSource()),
+ 'class' => 'autosubmit',
+ ]);
+
+ if ($form->hasBeenSent()) {
+ $sourceId = $form->getSentValue('import_source_id');
+ } else {
+ $object = $form->getObject();
+ if ($object instanceof ImportRowModifier) {
+ $sourceId = $object->getSetting('import_source_id');
+ } else {
+ $sourceId = null;
+ }
+ }
+ $extra = [];
+ if ($sourceId) {
+ $extra = [
+ 'class' => 'director-suggest',
+ 'data-suggestion-context' => 'importsourceproperties!' . (int) $sourceId,
+ ];
+ }
+ $form->addElement('text', 'foreign_property', [
+ 'label' => $form->translate('Property'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'The property to get from the row we found in the chosen Import Source'
+ ),
+ ] + $extra);
+ }
+
+ /**
+ * @param $settings
+ * @return PropertyModifierHook
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function setSettings(array $settings)
+ {
+ if (isset($settings['import_source'])) {
+ $settings['import_source_id'] = ImportSource::load(
+ $settings['import_source'],
+ $this->getDb()
+ )->get('id');
+ unset($settings['import_source']);
+ }
+
+ return parent::setSettings($settings);
+ }
+
+ public function transform($value)
+ {
+ $data = $this->getImportedData();
+
+ if (isset($data[$value])) {
+ return $data[$value]->{$this->getSetting('foreign_property')};
+ } else {
+ return null;
+ }
+ }
+
+ public function exportSettings()
+ {
+ $settings = parent::exportSettings();
+ $settings->import_source = $this->getImportSource()->getObjectName();
+ unset($settings->import_source_id);
+
+ return $settings;
+ }
+
+ protected function & getImportedData()
+ {
+ if ($this->importedData === null) {
+ $this->importedData = $this->getImportSource()
+ ->fetchLastRun(true)
+ ->fetchRows([$this->getSetting('foreign_property')]);
+ }
+
+ return $this->importedData;
+ }
+
+ protected function getImportSource()
+ {
+ if ($this->importSource === null) {
+ $this->importSource = ImportSource::loadWithAutoIncId(
+ $this->getSetting('import_source_id'),
+ $this->getDb()
+ );
+ }
+
+ return $this->importSource;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierJoin.php b/library/Director/PropertyModifier/PropertyModifierJoin.php
new file mode 100644
index 0000000..daa6fdb
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierJoin.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierJoin extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'glue', array(
+ 'label' => $form->translate('Glue'),
+ 'required' => false,
+ 'description' => $form->translate(
+ 'One or more characters that will be used to glue an input array to a string. Can be left empty'
+ )
+ ));
+ }
+
+ public function hasArraySupport()
+ {
+ return true;
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ return implode($this->getSetting('glue'), $value);
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierJsonDecode.php b/library/Director/PropertyModifier/PropertyModifierJsonDecode.php
new file mode 100644
index 0000000..f6b9af8
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierJsonDecode.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Exception\JsonException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierJsonDecode extends PropertyModifierHook
+{
+ /**
+ * @param QuickForm $form
+ * @return QuickForm|void
+ * @throws \Zend_Form_Exception
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'on_failure', array(
+ 'label' => 'On failure',
+ 'description' => $form->translate(
+ 'What should we do in case we are unable to decode the given string?'
+ ),
+ 'multiOptions' => $form->optionalEnum(array(
+ 'null' => $form->translate('Set no value (null)'),
+ 'keep' => $form->translate('Keep the JSON string as is'),
+ 'fail' => $form->translate('Let the whole import run fail'),
+ )),
+ 'required' => true,
+ ));
+ }
+
+ public function getName()
+ {
+ return 'Decode a JSON string';
+ }
+
+ /**
+ * @param $value
+ * @return mixed|null
+ * @throws InvalidPropertyException
+ */
+ public function transform($value)
+ {
+ if (null === $value) {
+ return $value;
+ }
+
+ $decoded = @json_decode($value);
+ if ($decoded === null && JSON_ERROR_NONE !== json_last_error()) {
+ switch ($this->getSetting('on_failure')) {
+ case 'null':
+ return null;
+ case 'keep':
+ return $value;
+ case 'fail':
+ default:
+ throw new InvalidPropertyException(
+ 'JSON decoding failed with "%s" for %s',
+ JsonException::getJsonErrorMessage(json_last_error()),
+ substr($value, 0, 128)
+ );
+ }
+ }
+
+ return $decoded;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierLConfCustomVar.php b/library/Director/PropertyModifier/PropertyModifierLConfCustomVar.php
new file mode 100644
index 0000000..35db6c8
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierLConfCustomVar.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+class PropertyModifierLConfCustomVar extends PropertyModifierHook
+{
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ $vars = (object) array();
+ $this->extractLConfVars($value, $vars);
+
+ return $vars;
+ }
+
+ public function getName()
+ {
+ return 'Transform LConf CustomVars to Hash';
+ }
+
+ public function hasArraySupport()
+ {
+ return true;
+ }
+
+ protected function extractLConfVars($value, $vars)
+ {
+ if (is_string($value)) {
+ $this->extractLConfVar($value, $vars);
+ } elseif (is_array($value)) {
+ foreach ($value as $val) {
+ $this->extractLConfVar($val, $vars);
+ }
+ }
+ }
+
+ protected function extractLConfVar($value, $vars)
+ {
+ list($key, $val) = preg_split('/ /', $value, 2);
+ $key = ltrim($key, '_');
+ $vars->$key = $val;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierListToObject.php b/library/Director/PropertyModifier/PropertyModifierListToObject.php
new file mode 100644
index 0000000..9889c8f
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierListToObject.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use InvalidArgumentException;
+use ipl\Html\Error;
+
+class PropertyModifierListToObject extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'key_property', [
+ 'label' => $form->translate('Key Property'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'Each Array in the list must contain this property. It\'s value'
+ . ' will be used as the key/object property name for the row.'
+ )
+ ]);
+ $form->addElement('select', 'on_duplicate', [
+ 'label' => 'On duplicate key',
+ 'description' => $form->translate('What should we do, if the same key occurs twice?'),
+ 'multiOptions' => $form->optionalEnum([
+ 'fail' => $form->translate('Let the whole import run fail'),
+ 'keep_first' => $form->translate('Keep the first row with that key'),
+ 'keep_last' => $form->translate('Keep the last row with that key'),
+ ]),
+ 'required' => true,
+ ]);
+ }
+
+ public function getName()
+ {
+ return 'Transform Array/Object list into single Object';
+ }
+
+ public function hasArraySupport()
+ {
+ return true;
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+ if (! \is_array($value)) {
+ throw new InvalidArgumentException(
+ 'Array expected, got ' . Error::getPhpTypeName($value)
+ );
+ }
+ $keyProperty = $this->getSetting('key_property');
+ $onDuplicate = $this->getSetting('on_duplicate');
+ $result = (object) [];
+ foreach ($value as $key => $row) {
+ if (\is_object($row)) {
+ $row = (array) $row;
+ }
+ if (! \is_array($row)) {
+ throw new InvalidArgumentException(
+ "List of Arrays expected expected. Array entry '$key' is "
+ . Error::getPhpTypeName($value)
+ );
+ }
+
+ if (! \array_key_exists($keyProperty, $row)) {
+ throw new InvalidArgumentException(
+ "Key property '$keyProperty' is required, but missing on row '$key'"
+ );
+ }
+
+ $property = $row[$keyProperty];
+ if (isset($result->$property)) {
+ switch ($onDuplicate) {
+ case 'fail':
+ throw new InvalidArgumentException(
+ "Duplicate row with $keyProperty=$property found on row '$key'"
+ );
+ case 'keep_first':
+ // Do nothing
+ break;
+ case 'keep_last':
+ $result->$property = (object) $row;
+ break;
+ }
+ } else {
+ $result->$property = (object) $row;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierLowercase.php b/library/Director/PropertyModifier/PropertyModifierLowercase.php
new file mode 100644
index 0000000..1fdbb4d
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierLowercase.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+class PropertyModifierLowercase extends PropertyModifierHook
+{
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ return \mb_strtolower($value, 'UTF-8');
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierMakeBoolean.php b/library/Director/PropertyModifier/PropertyModifierMakeBoolean.php
new file mode 100644
index 0000000..ed91bcf
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierMakeBoolean.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierMakeBoolean extends PropertyModifierHook
+{
+ protected static $validStrings = array(
+ '0' => false,
+ 'false' => false,
+ 'n' => false,
+ 'no' => false,
+ '1' => true,
+ 'true' => true,
+ 'y' => true,
+ 'yes' => true,
+ );
+
+ public function getName()
+ {
+ return 'Convert to a boolean value';
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'on_invalid', array(
+ 'label' => 'Invalid properties',
+ 'required' => true,
+ 'description' => $form->translate(
+ 'This modifier transforms 0/"0"/false/"false"/"n"/"no" to false'
+ . ' and 1, "1", true, "true", "y" and "yes" to true, both in a'
+ . ' case insensitive way. What should happen if the given value'
+ . ' does not match any of those?'
+ . ' You could return a null value, or default to false or true.'
+ . ' You might also consider interrupting the whole import process'
+ . ' as of invalid source data'
+ ),
+ 'multiOptions' => $form->optionalEnum(array(
+ 'null' => $form->translate('Set null'),
+ 'true' => $form->translate('Set true'),
+ 'false' => $form->translate('Set false'),
+ 'fail' => $form->translate('Let the import fail'),
+ )),
+ ));
+ }
+
+ public function transform($value)
+ {
+ if ($value === false || $value === true || $value === null) {
+ return $value;
+ }
+
+ if ($value === 0) {
+ return false;
+ }
+
+ if ($value === 1) {
+ return true;
+ }
+
+ if (is_string($value)) {
+ $value = strtolower($value);
+
+ if (array_key_exists($value, self::$validStrings)) {
+ return self::$validStrings[$value];
+ }
+ }
+
+ switch ($this->getSetting('on_invalid')) {
+ case 'null':
+ return null;
+
+ case 'false':
+ return false;
+
+ case 'true':
+ return true;
+
+ case 'fail':
+ default:
+ throw new InvalidPropertyException(
+ '"%s" cannot be converted to a boolean value',
+ $value
+ );
+ }
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierMap.php b/library/Director/PropertyModifier/PropertyModifierMap.php
new file mode 100644
index 0000000..a6cb422
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierMap.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierMap extends PropertyModifierHook
+{
+ private $cache;
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'datalist_id', array(
+ 'label' => 'Lookup list',
+ 'required' => true,
+ 'description' => $form->translate(
+ 'Please choose a data list that can be used for map lookups'
+ ),
+ 'multiOptions' => $form->optionalEnum($form->getDb()->enumDatalist()),
+ ));
+
+ $form->addElement('select', 'on_missing', array(
+ 'label' => 'Missing entries',
+ 'required' => true,
+ 'description' => $form->translate(
+ 'What should happen if the lookup key does not exist in the data list?'
+ . ' You could return a null value, keep the unmodified imported value'
+ . ' or interrupt the import process'
+ ),
+ 'multiOptions' => $form->optionalEnum(array(
+ 'null' => $form->translate('Set null'),
+ 'keep' => $form->translate('Return lookup key unmodified'),
+ 'fail' => $form->translate('Let the import fail'),
+ )),
+ ));
+
+ // TODO: ignore case
+ }
+
+ public function transform($value)
+ {
+ $this->loadCache();
+ if (array_key_exists($value, $this->cache)) {
+ return $this->cache[$value];
+ }
+
+ switch ($this->getSetting('on_missing')) {
+ case 'null':
+ return null;
+
+ case 'keep':
+ return $value;
+
+ case 'fail':
+ default:
+ throw new InvalidPropertyException(
+ '"%s" cannot be found in the "%s" data list',
+ $value,
+ $this->getDatalistName()
+ );
+ }
+ }
+
+ protected function getDatalistName()
+ {
+ $db = $this->getDb()->getDbAdapter();
+ $query = $db->select()->from(
+ 'director_datalist',
+ 'list_name'
+ )->where(
+ 'id = ?',
+ $this->getSetting('datalist_id')
+ );
+ $result = $db->fetchOne($query);
+
+ return $result;
+ }
+
+ protected function loadCache($force = false)
+ {
+ if ($this->cache === null || $force) {
+ $this->cache = array();
+ $db = $this->getDb()->getDbAdapter();
+ $select = $db->select()->from(
+ 'director_datalist_entry',
+ array('entry_name', 'entry_value')
+ )->where('list_id = ?', $this->getSetting('datalist_id'))
+ ->order('entry_value');
+
+ $this->cache = $db->fetchPairs($select);
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierNegateBoolean.php b/library/Director/PropertyModifier/PropertyModifierNegateBoolean.php
new file mode 100644
index 0000000..e60d692
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierNegateBoolean.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use function ipl\Stdlib\get_php_type;
+
+class PropertyModifierNegateBoolean extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Negate a boolean value';
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return true;
+ }
+ if (! is_bool($value)) {
+ throw new \InvalidArgumentException('Boolean expected, got ' . get_php_type($value));
+ }
+
+ return ! $value;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierParseURL.php b/library/Director/PropertyModifier/PropertyModifierParseURL.php
new file mode 100644
index 0000000..ce7c81b
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierParseURL.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierParseURL extends PropertyModifierHook
+{
+
+ /**
+ * Array with possible components that can be returned from URL.
+ */
+ protected static $components = [
+ 'scheme' => PHP_URL_SCHEME,
+ 'host' => PHP_URL_HOST,
+ 'port' => PHP_URL_PORT,
+ 'path' => PHP_URL_PATH,
+ 'query' => PHP_URL_QUERY,
+ 'fragment' => PHP_URL_FRAGMENT,
+ ];
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'url_component', [
+ 'label' => $form->translate('URL component'),
+ 'description' => $form->translate('URL component'),
+ 'multiOptions' => $form->optionalEnum(static::enumComponents()),
+ 'required' => true,
+ ]);
+
+ $form->addElement('select', 'on_failure', [
+ 'label' => $form->translate('On failure'),
+ 'description' => $form->translate(
+ 'What should we do if the URL could not get parsed or component not found?'
+ ),
+ 'multiOptions' => $form->optionalEnum([
+ 'null' => $form->translate('Set no value (null)'),
+ 'keep' => $form->translate('Keep the property as is'),
+ 'fail' => $form->translate('Let the whole import run fail'),
+ ]),
+ 'required' => true,
+ ]);
+ }
+
+ protected static function enumComponents()
+ {
+ $components = array_keys(self::$components);
+ return array_combine($components, $components);
+ }
+
+ public function getName()
+ {
+ return 'Parse a URL and return its components';
+ }
+
+ public function transform($value)
+ {
+ $component = self::$components[$this->getSetting('url_component')];
+ $response = parse_url($value, $component);
+
+ // if component not found $response will be null, false if seriously malformed URL
+ if ($response === null || $response === false) {
+ switch ($this->getSetting('on_failure')) {
+ case 'null':
+ return null;
+ case 'keep':
+ return $value;
+ case 'fail':
+ default:
+ throw new InvalidPropertyException(
+ 'Parsing URL "%s" failed.',
+ $value
+ );
+ }
+ }
+
+ return $response;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierRegexReplace.php b/library/Director/PropertyModifier/PropertyModifierRegexReplace.php
new file mode 100644
index 0000000..59cb245
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierRegexReplace.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierRegexReplace extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'pattern', array(
+ 'label' => 'Regex pattern',
+ 'description' => $form->translate(
+ 'The pattern you want to search for. This can be a regular expression like /^www\d+\./'
+ ),
+ 'required' => true,
+ ));
+
+ $form->addElement('text', 'replacement', array(
+ 'label' => 'Replacement',
+ 'description' => $form->translate(
+ 'The string that should be used as a preplacement'
+ ),
+ ));
+ }
+
+ public function getName()
+ {
+ return 'Regular expression based replacement';
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ return preg_replace(
+ $this->getSetting('pattern'),
+ $this->getSetting('replacement'),
+ $value
+ );
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierRegexSplit.php b/library/Director/PropertyModifier/PropertyModifierRegexSplit.php
new file mode 100644
index 0000000..829810f
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierRegexSplit.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierRegexSplit extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'pattern', array(
+ 'label' => $form->translate('Pattern'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'Regular expression pattern to split the string (e.g. /\s+/ or /[,;]/)'
+ )
+ ));
+
+ $form->addElement('select', 'when_empty', array(
+ 'label' => $form->translate('When empty'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'What should happen when the given string is empty?'
+ ),
+ 'value' => 'empty_array',
+ 'multiOptions' => $form->optionalEnum(array(
+ 'empty_array' => $form->translate('return an empty array'),
+ 'null' => $form->translate('return NULL'),
+ ))
+ ));
+ }
+
+ public function transform($value)
+ {
+ if (! strlen(trim($value))) {
+ if ($this->getSetting('when_empty', 'empty_array') === 'empty_array') {
+ return array();
+ } else {
+ return null;
+ }
+ }
+
+ return preg_split(
+ $this->getSetting('pattern'),
+ $value,
+ -1,
+ PREG_SPLIT_NO_EMPTY
+ );
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php b/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php
new file mode 100644
index 0000000..1485d5d
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierRejectOrSelect extends PropertyModifierHook
+{
+ /** @var FilterExpression */
+ private $filterExpression;
+
+ public function getName()
+ {
+ return mt('director', 'Reject or keep rows based on property value');
+ }
+
+ /**
+ * @inheritdoc
+ * @throws \Zend_Form_Exception
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'filter_method', [
+ 'label' => $form->translate('Filter method'),
+ 'required' => true,
+ 'value' => 'wildcard',
+ 'multiOptions' => $form->optionalEnum([
+ 'wildcard' => $form->translate('Simple match with wildcards (*)'),
+ 'regex' => $form->translate('Regular Expression'),
+ 'is_true' => $form->translate('Match boolean TRUE'),
+ 'is_false' => $form->translate('Match boolean FALSE'),
+ 'is_null' => $form->translate('Match NULL value columns'),
+ ]),
+ 'class' => 'autosubmit',
+ ]);
+
+ $method = $form->getSetting('filter_method');
+ switch ($method) {
+ case 'wildcard':
+ $form->addElement('text', 'filter_string', [
+ 'label' => $form->translate('Filter'),
+ 'description' => $form->translate(
+ 'The string/pattern you want to search for, use wildcard'
+ . ' matches like www.* or *linux*'
+ ),
+ 'required' => true,
+ ]);
+ break;
+ case 'regex':
+ $form->addElement('text', 'filter_string', [
+ 'label' => $form->translate('Filter'),
+ 'description' => $form->translate(
+ 'The string/pattern you want to search for, use regular'
+ . ' expression like /^www\d+\./'
+ ),
+ 'required' => true,
+ ]);
+ break;
+ }
+
+ $form->addElement('select', 'policy', [
+ 'label' => $form->translate('Policy'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'What should happen with the row, when this property matches the given expression?'
+ ),
+ 'value' => 'reject',
+ 'multiOptions' => [
+ 'reject' => $form->translate('Reject the whole row'),
+ 'keep' => $form->translate('Keep only matching rows'),
+ ],
+ ]);
+ }
+
+ public function matchesRegexp($string, $expression)
+ {
+ return preg_match($expression, $string);
+ }
+
+ public function isNull($string, $expression)
+ {
+ return $string === null;
+ }
+
+ public function isTrue($string, $expression)
+ {
+ return $string === true;
+ }
+
+ public function isFalse($string, $expression)
+ {
+ return $string === false;
+ }
+
+ public function matchesWildcard($string, $expression)
+ {
+ return $this->filterExpression->matches(
+ (object) ['value' => $string]
+ );
+ }
+
+ public function transform($value)
+ {
+ $method = $this->getSetting('filter_method');
+ $filter = $this->getSetting('filter_string');
+ $policy = $this->getSetting('policy');
+
+ switch ($method) {
+ case 'wildcard':
+ $func = 'matchesWildcard';
+ $this->filterExpression = new FilterExpression('value', '=', $filter);
+ break;
+ case 'regex':
+ $func = 'matchesRegexp';
+ break;
+ case 'is_null':
+ $func = 'isNull';
+ break;
+ case 'is_true':
+ $func = 'isTrue';
+ break;
+ case 'is_false':
+ $func = 'isFalse';
+ break;
+ default:
+ throw new ConfigurationError(
+ '%s is not a valid value for an ArrayFilter filter_method',
+ var_export($method, 1)
+ );
+ }
+
+ if ($this->$func($value, $filter)) {
+ if ($policy === 'reject') {
+ $this->rejectRow();
+ }
+ } else {
+ if ($policy === 'keep') {
+ $this->rejectRow();
+ }
+ }
+
+ return $value;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierRenameColumn.php b/library/Director/PropertyModifier/PropertyModifierRenameColumn.php
new file mode 100644
index 0000000..12524d5
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierRenameColumn.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+class PropertyModifierRenameColumn extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Rename a Property/Column';
+ }
+
+ public function requiresRow()
+ {
+ return true;
+ }
+
+ public function hasArraySupport()
+ {
+ return true;
+ }
+
+ public function transform($value)
+ {
+ $row = $this->getRow();
+ $property = $this->getPropertyName();
+ if ($row) {
+ unset($row->$property);
+ }
+ // $this->rejectRow();
+ return $value;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierReplace.php b/library/Director/PropertyModifier/PropertyModifierReplace.php
new file mode 100644
index 0000000..54e6616
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierReplace.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierReplace extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'string', array(
+ 'label' => 'Search string',
+ 'description' => $form->translate('The string you want to search for'),
+ 'required' => true,
+ ));
+
+ $form->addElement('text', 'replacement', array(
+ 'label' => 'Replacement',
+ 'description' => $form->translate('Your replacement string'),
+ ));
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ return str_replace(
+ $this->getSetting('string'),
+ $this->getSetting('replacement'),
+ $value
+ );
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierReplaceNull.php b/library/Director/PropertyModifier/PropertyModifierReplaceNull.php
new file mode 100644
index 0000000..d6f9fd3
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierReplaceNull.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierReplaceNull extends PropertyModifierHook
+{
+
+ public function getName()
+ {
+ return 'Replace null value with String';
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'string', [
+ 'label' => 'Replacement String',
+ 'description' => $form->translate('Your replacement string'),
+ 'required' => true,
+ ]);
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return $this->getSetting('string');
+ } else {
+ return $value;
+ }
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierSimpleGroupBy.php b/library/Director/PropertyModifier/PropertyModifierSimpleGroupBy.php
new file mode 100644
index 0000000..6c1452f
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierSimpleGroupBy.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierSimpleGroupBy extends PropertyModifierHook
+{
+ private $keptRows = [];
+
+ public function getName()
+ {
+ return mt('director', 'Group by a column, aggregate others');
+ }
+
+ public function requiresRow()
+ {
+ return true;
+ }
+
+ public function transform($value)
+ {
+ $row = $this->getRow();
+ $aggregationColumns = preg_split(
+ '/\s*,\s*/',
+ $this->getSetting('aggregation_columns'),
+ -1,
+ PREG_SPLIT_NO_EMPTY
+ );
+ if (isset($this->keptRows[$value])) {
+ foreach ($aggregationColumns as $column) {
+ if (isset($row->$column)) {
+ $this->keptRows[$value]->{$column} = array_unique(array_merge(
+ $this->keptRows[$value]->{$column},
+ [$row->$column]
+ ));
+ sort($this->keptRows[$value]->{$column});
+ }
+ }
+ $this->rejectRow();
+ } else {
+ foreach ($aggregationColumns as $column) {
+ if (isset($row->$column)) {
+ $row->$column = [$row->$column];
+ } else {
+ $row->$column = [];
+ }
+ }
+
+ $this->keptRows[$value] = $row;
+ }
+
+ return $value;
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'aggregation_columns', [
+ 'label' => $form->translate('Aggregation Columns'),
+ 'description' => $form->translate(
+ 'Comma-separated list of columns that should be aggregated (transformed into an Array).'
+ . ' For all other columns only the first value will be kept.'
+ ),
+ 'required' => true,
+ ]);
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierSkipDuplicates.php b/library/Director/PropertyModifier/PropertyModifierSkipDuplicates.php
new file mode 100644
index 0000000..bf9bd31
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierSkipDuplicates.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+class PropertyModifierSkipDuplicates extends PropertyModifierHook
+{
+ private $seen = [];
+
+ public function getName()
+ {
+ return mt('director', 'Skip row if this value appears more than once');
+ }
+
+ public function transform($value)
+ {
+ if (isset($this->seen[$value])) {
+ $this->rejectRow();
+ }
+
+ $this->seen[$value] = true;
+
+ return $value;
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierSplit.php b/library/Director/PropertyModifier/PropertyModifierSplit.php
new file mode 100644
index 0000000..4a6fef6
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierSplit.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierSplit extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'delimiter', array(
+ 'label' => $form->translate('Delimiter'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'One or more characters that should be used to split this string'
+ )
+ ));
+
+ $form->addElement('select', 'when_empty', array(
+ 'label' => $form->translate('When empty'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'What should happen when the given string is empty?'
+ ),
+ 'value' => 'empty_array',
+ 'multiOptions' => $form->optionalEnum(array(
+ 'empty_array' => $form->translate('return an empty array'),
+ 'null' => $form->translate('return NULL'),
+ ))
+ ));
+ }
+
+ public function transform($value)
+ {
+ if (! strlen(trim($value))) {
+ if ($this->getSetting('when_empty', 'empty_array') === 'empty_array') {
+ return array();
+ } else {
+ return null;
+ }
+ }
+
+ return preg_split(
+ '/' . preg_quote($this->getSetting('delimiter'), '/') . '/',
+ $value,
+ -1,
+ PREG_SPLIT_NO_EMPTY
+ );
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierStripDomain.php b/library/Director/PropertyModifier/PropertyModifierStripDomain.php
new file mode 100644
index 0000000..34fb6ba
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierStripDomain.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierStripDomain extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'domain', array(
+ 'label' => 'Domain name',
+ 'description' => $form->translate('The domain name you want to be stripped'),
+ 'required' => true,
+ ));
+ }
+
+ public function getName()
+ {
+ return 'Strip a domain name';
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ $domain = preg_quote(ltrim($this->getSetting('domain'), '.'), '/');
+
+ return preg_replace(
+ '/\.' . $domain . '$/',
+ '',
+ $value
+ );
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierSubstring.php b/library/Director/PropertyModifier/PropertyModifierSubstring.php
new file mode 100644
index 0000000..37a2f92
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierSubstring.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierSubstring extends PropertyModifierHook
+{
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('text', 'start', array(
+ 'label' => 'Start',
+ 'required' => true,
+ 'description' => sprintf(
+ $form->translate(
+ 'Please see %s for detailled instructions of how start and end work'
+ ),
+ 'http://php.net/manual/en/function.substr.php'
+ )
+ ));
+
+ $form->addElement('text', 'length', array(
+ 'label' => 'End',
+ 'description' => sprintf(
+ $form->translate(
+ 'Please see %s for detailled instructions of how start and end work'
+ ),
+ 'http://php.net/manual/en/function.substr.php'
+ )
+ ));
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ $length = $this->getSetting('length');
+ if (is_numeric($length)) {
+ return substr(
+ $value,
+ (int) $this->getSetting('start'),
+ (int) $length
+ );
+ } else {
+ return substr(
+ $value,
+ (int) $this->getSetting('start')
+ );
+ }
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierToInt.php b/library/Director/PropertyModifier/PropertyModifierToInt.php
new file mode 100644
index 0000000..dca7db9
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierToInt.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Data\InvalidDataException;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+class PropertyModifierToInt extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Cast a string value to an Integer';
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ if (is_int($value)) {
+ return $value;
+ }
+
+ if (is_string($value)) {
+ return (int) $value;
+ }
+
+ throw new InvalidDataException('String, integer or null', $value);
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierTrim.php b/library/Director/PropertyModifier/PropertyModifierTrim.php
new file mode 100644
index 0000000..64a655b
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierTrim.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use InvalidArgumentException;
+
+class PropertyModifierTrim extends PropertyModifierHook
+{
+ const VALID_METHODS = ['trim', 'ltrim', 'rtrim'];
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'trim_method', [
+ 'label' => $form->translate('Trim Method'),
+ 'description' => $form->translate('Where to trim the string(s)'),
+ 'value' => 'trim',
+ 'multiOptions' => $form->optionalEnum([
+ 'trim' => $form->translate('Beginning and Ending'),
+ 'ltrim' => $form->translate('Beginning only'),
+ 'rtrim' => $form->translate('Ending only'),
+ ]),
+ 'required' => true,
+ ]);
+
+ $form->addElement('text', 'character_mask', [
+ 'label' => $form->translate('Character Mask'),
+ 'description' => $form->translate(
+ 'Specify the characters that trim should remove.'
+ . 'Default is: " \t\n\r\0\x0B"'
+ ),
+ ]);
+ }
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ $mask = $this->getSetting('character_mask');
+ $method = $this->getSetting('trim_method');
+ if (in_array($method, self::VALID_METHODS)) {
+ if ($mask) {
+ return $method($value, $mask);
+ } else {
+ return $method($value);
+ }
+ }
+
+ throw new InvalidArgumentException("'$method' is not a valid trim method");
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierURLEncode.php b/library/Director/PropertyModifier/PropertyModifierURLEncode.php
new file mode 100644
index 0000000..34f3588
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierURLEncode.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+class PropertyModifierURLEncode extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'URL-encode a string';
+ }
+
+
+ public function transform($value)
+ {
+ return rawurlencode($value);
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php b/library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php
new file mode 100644
index 0000000..a2fff40
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierUpperCaseFirst.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class PropertyModifierUpperCaseFirst extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Uppercase the first character of each word in a string';
+ }
+
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'lowerfirst', array(
+ 'label' => $form->translate('Use lowercase first'),
+ 'required' => true,
+ 'description' => $form->translate(
+ 'Should all the other characters be lowercased first?'
+ ),
+ 'value' => 'y',
+ 'multiOptions' => array(
+ 'y' => $form->translate('Yes'),
+ 'n' => $form->translate('No'),
+ ),
+ ));
+ }
+
+
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ if ($this->getSetting('lowerfirst', 'y') === 'y') {
+ return ucwords(strtolower($value));
+ } else {
+ return ucwords($value);
+ }
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierUppercase.php b/library/Director/PropertyModifier/PropertyModifierUppercase.php
new file mode 100644
index 0000000..e3d3d59
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierUppercase.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+class PropertyModifierUppercase extends PropertyModifierHook
+{
+ public function transform($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ return \mb_strtoupper($value, 'UTF-8');
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierUuidBinToHex.php b/library/Director/PropertyModifier/PropertyModifierUuidBinToHex.php
new file mode 100644
index 0000000..a1e5d9c
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierUuidBinToHex.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Ramsey\Uuid\Uuid;
+
+class PropertyModifierUuidBinToHex extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return mt('director', 'UUID: from binary to hex');
+ }
+
+ public function transform($value)
+ {
+ return Uuid::fromBytes($value)->toString();
+ }
+}
diff --git a/library/Director/PropertyModifier/PropertyModifierXlsNumericIp.php b/library/Director/PropertyModifier/PropertyModifierXlsNumericIp.php
new file mode 100644
index 0000000..969068e
--- /dev/null
+++ b/library/Director/PropertyModifier/PropertyModifierXlsNumericIp.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+
+class PropertyModifierXlsNumericIp extends PropertyModifierHook
+{
+ public function getName()
+ {
+ return 'Fix IP formatted as a number in MS Excel';
+ }
+
+ public function transform($value)
+ {
+ if (ctype_digit($value) && strlen($value) > 9 && strlen($value) <= 12) {
+ return preg_replace(
+ '/^(\d{1,3})(\d{3})(\d{3})(\d{3})/',
+ '\1.\2.\3.\4',
+ $value
+ );
+ } else {
+ return $value;
+ }
+ }
+}
diff --git a/library/Director/ProvidedHook/CubeLinks.php b/library/Director/ProvidedHook/CubeLinks.php
new file mode 100644
index 0000000..2cb9559
--- /dev/null
+++ b/library/Director/ProvidedHook/CubeLinks.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Director\ProvidedHook;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Hook\ActionsHook;
+use Icinga\Module\Cube\Ido\IdoHostStatusCube;
+use Icinga\Web\View;
+
+class CubeLinks extends ActionsHook
+{
+ /**
+ * @inheritdoc
+ */
+ public function prepareActionLinks(Cube $cube, View $view)
+ {
+ if (! $cube instanceof IdoHostStatusCube) {
+ return;
+ }
+
+ $cube->finalizeInnerQuery();
+ $query = $cube->innerQuery()
+ ->reset('columns')
+ ->columns(array('host' => 'o.name1'))
+ ->reset('group');
+
+ $hosts = $cube->db()->fetchCol($query);
+
+ $count = count($hosts);
+ if ($count === 1) {
+ $url = 'director/host/edit';
+ $params = array('name' => $hosts[0]);
+
+ $title = $view->translate('Modify a host');
+ $description = sprintf(
+ $view->translate('This allows you to modify properties for "%s"'),
+ $hosts[0]
+ );
+ } else {
+ $params = null;
+
+ $filter = Filter::matchAny();
+ foreach ($hosts as $host) {
+ $filter->addFilter(
+ Filter::matchAny(Filter::expression('name', '=', $host))
+ );
+ }
+
+ $url = 'director/hosts/edit?' . $filter->toQueryString();
+
+ $title = sprintf($view->translate('Modify %d hosts'), $count);
+ $description = $view->translate(
+ 'This allows you to modify properties for all chosen hosts at once'
+ );
+ }
+
+ $this->addActionLink(
+ $this->makeUrl($url, $params),
+ $title,
+ $description,
+ 'wrench'
+ );
+ }
+}
diff --git a/library/Director/ProvidedHook/IcingaDbCubeLinks.php b/library/Director/ProvidedHook/IcingaDbCubeLinks.php
new file mode 100644
index 0000000..234f61f
--- /dev/null
+++ b/library/Director/ProvidedHook/IcingaDbCubeLinks.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Icinga\Module\Director\ProvidedHook;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Cube\Hook\IcingaDbActionsHook;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Cube\IcingaDb\IcingaDbHostStatusCube;
+
+class IcingaDbCubeLinks extends IcingaDbActionsHook
+{
+ /**
+ * @inheritDoc
+ * @param IcingaDbCube $cube
+ * @throws ProgrammingError
+ */
+ public function createActionLinks(IcingaDbCube $cube)
+ {
+ if (! $cube instanceof IcingaDbHostStatusCube) {
+ return;
+ }
+
+ $filterChain = $cube->getObjectsFilter();
+
+ if ($filterChain->count() === 1) {
+ $url = 'director/host/edit?';
+ $params = ['name' => $filterChain->getIterator()->current()->getValue()];
+
+ $title = t('Modify a host');
+ $description = sprintf(
+ t('This allows you to modify properties for "%s"'),
+ $filterChain->getIterator()->current()->getValue()
+ );
+ } else {
+ $params = null;
+
+ $urlFilter = Filter::matchAny();
+ foreach ($filterChain as $filter) {
+ $urlFilter->addFilter(
+ Filter::matchAny(
+ Filter::expression(
+ 'name',
+ '=',
+ $filter->getValue()
+ )
+ )
+ );
+ }
+
+ $url = 'director/hosts/edit?' . $urlFilter->toQueryString();
+
+ $title = sprintf(t('Modify %d hosts'), $filterChain->count());
+ $description = t(
+ 'This allows you to modify properties for all chosen hosts at once'
+ );
+ }
+
+ $this->addActionLink(
+ $this->makeUrl($url, $params),
+ $title,
+ $description,
+ 'wrench'
+ );
+ }
+}
diff --git a/library/Director/ProvidedHook/Monitoring/HostActions.php b/library/Director/ProvidedHook/Monitoring/HostActions.php
new file mode 100644
index 0000000..2e3fba0
--- /dev/null
+++ b/library/Director/ProvidedHook/Monitoring/HostActions.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Icinga\Module\Director\ProvidedHook\Monitoring;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Monitoring;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Util;
+use Icinga\Module\Monitoring\Hook\HostActionsHook;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Web\Url;
+
+class HostActions extends HostActionsHook
+{
+ public function getActionsForHost(Host $host)
+ {
+ try {
+ return $this->getThem($host);
+ } catch (Exception $e) {
+ return array();
+ }
+ }
+
+ protected function getThem(Host $host)
+ {
+ $actions = array();
+ $db = $this->db();
+ if (! $db) {
+ return $actions;
+ }
+ $hostname = $host->host_name;
+ if (Util::hasPermission('director/inspect')) {
+ $actions[mt('director', 'Inspect')] = Url::fromPath(
+ 'director/inspect/object',
+ array('type' => 'host', 'plural' => 'hosts', 'name' => $hostname)
+ );
+ }
+
+ $allowEdit = false;
+ if (Util::hasPermission('director/hosts') && IcingaHost::exists($hostname, $db)) {
+ $allowEdit = true;
+ }
+ $auth = Auth::getInstance();
+ if (Util::hasPermission('director/monitoring/hosts')) {
+ $monitoring = new Monitoring();
+ if ($monitoring->isAvailable() && $monitoring->authCanEditHost($auth, $hostname)) {
+ $allowEdit = IcingaHost::exists($hostname, $db);
+ }
+ }
+
+ if ($allowEdit) {
+ $actions[mt('director', 'Modify')] = Url::fromPath(
+ 'director/host/edit',
+ array('name' => $hostname)
+ );
+ }
+
+ return $actions;
+ }
+
+ protected function db()
+ {
+ $resourceName = Config::module('director')->get('db', 'resource');
+ if (! $resourceName) {
+ return false;
+ }
+
+ return Db::fromResourceName($resourceName);
+ }
+}
diff --git a/library/Director/ProvidedHook/Monitoring/ServiceActions.php b/library/Director/ProvidedHook/Monitoring/ServiceActions.php
new file mode 100644
index 0000000..b2e303a
--- /dev/null
+++ b/library/Director/ProvidedHook/Monitoring/ServiceActions.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Icinga\Module\Director\ProvidedHook\Monitoring;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Monitoring;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Util;
+use Icinga\Module\Monitoring\Hook\ServiceActionsHook;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+
+class ServiceActions extends ServiceActionsHook
+{
+ public function getActionsForService(Service $service)
+ {
+ try {
+ return $this->getThem($service);
+ } catch (Exception $e) {
+ return [];
+ }
+ }
+
+ /**
+ * @param Service $service
+ * @return array
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ protected function getThem(Service $service)
+ {
+ $actions = [];
+ $db = $this->db();
+ if (! $db) {
+ return [];
+ }
+
+ $hostname = $service->host_name;
+ $serviceName = $service->service_description;
+ if (Util::hasPermission('director/inspect')) {
+ $actions[mt('director', 'Inspect')] = Url::fromPath('director/inspect/object', [
+ 'type' => 'service',
+ 'plural' => 'services',
+ 'name' => sprintf(
+ '%s!%s',
+ $hostname,
+ $serviceName
+ )
+ ]);
+ }
+
+ $title = null;
+ if (Util::hasPermission('director/hosts')) {
+ $title = mt('director', 'Modify');
+ } elseif (Util::hasPermission('director/monitoring/services')) {
+ $monitoring = new Monitoring();
+ if ($monitoring->isAvailable()
+ && $monitoring->authCanEditService(Auth::getInstance(), $hostname, $serviceName)
+ ) {
+ $title = mt('director', 'Modify');
+ }
+ } elseif (Util::hasPermission('director/monitoring/services-ro')) {
+ $title = mt('director', 'Configuration');
+ }
+
+ if ($title && IcingaHost::exists($hostname, $db)) {
+ $actions[$title] = Url::fromPath('director/host/findservice', [
+ 'name' => $hostname,
+ 'service' => $serviceName
+ ]);
+ }
+
+ return $actions;
+ }
+
+ protected function db()
+ {
+ $resourceName = Config::module('director')->get('db', 'resource');
+ if (! $resourceName) {
+ return false;
+ }
+
+ return Db::fromResourceName($resourceName);
+ }
+}
diff --git a/library/Director/Repository/IcingaTemplateRepository.php b/library/Director/Repository/IcingaTemplateRepository.php
new file mode 100644
index 0000000..ed3b1d0
--- /dev/null
+++ b/library/Director/Repository/IcingaTemplateRepository.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Icinga\Module\Director\Repository;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Resolver\TemplateTree;
+
+class IcingaTemplateRepository
+{
+ use RepositoryByObjectHelper;
+
+ /** @var TemplateTree */
+ protected $tree;
+
+ protected $loadedById = [];
+
+ /**
+ * @return TemplateTree
+ */
+ public function tree()
+ {
+ if ($this->tree === null) {
+ $this->tree = new TemplateTree($this->type, $this->connection);
+ }
+
+ return $this->tree;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @param bool $recursive
+ * @return IcingaObject[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getTemplatesFor(IcingaObject $object, $recursive = false)
+ {
+ if ($recursive) {
+ $ids = $this->tree()->listAncestorIdsFor($object);
+ } else {
+ $ids = $this->tree()->listParentIdsFor($object);
+ }
+
+ return $this->getTemplatesForIds($ids, $object);
+ }
+
+ /**
+ * @param array $ids
+ * @param IcingaObject $object
+ * @return IcingaObject[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getTemplatesForIds(array $ids, IcingaObject $object)
+ {
+ $templates = [];
+ foreach ($ids as $id) {
+ if (! array_key_exists($id, $this->loadedById)) {
+ // TODO: load only missing ones at once
+ $this->loadedById[$id] = $object::loadWithAutoIncId(
+ $id,
+ $this->connection
+ );
+ }
+
+ $templates[$id] = $this->loadedById[$id];
+ }
+
+ return $templates;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @param bool $recursive
+ * @return IcingaObject[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getTemplatesIndexedByNameFor(
+ IcingaObject $object,
+ $recursive = false
+ ) {
+ $templates = [];
+ foreach ($this->getTemplatesFor($object, $recursive) as $template) {
+ $templates[$template->getObjectName()] = $template;
+ }
+
+ return $templates;
+ }
+
+ public function persistImportNames()
+ {
+ }
+
+ public function storeChances(Db $db)
+ {
+ }
+
+ public function listAllowedTemplateNames()
+ {
+ $type = $this->type;
+ $db = $this->connection->getDbAdapter();
+ $table = 'icinga_' . $this->type;
+
+ $query = $db->select()
+ ->from($table, 'object_name')
+ ->order('object_name');
+
+ if ($type !== 'command') {
+ $query->where('object_type = ?', 'template');
+ }
+
+ if (in_array($type, ['host', 'service'])) {
+ $query->where('template_choice_id IS NULL');
+ }
+
+ return $db->fetchCol($query);
+ }
+
+ public static function clear()
+ {
+ static::clearInstances();
+ }
+}
diff --git a/library/Director/Repository/RepositoryByObjectHelper.php b/library/Director/Repository/RepositoryByObjectHelper.php
new file mode 100644
index 0000000..0d1dda3
--- /dev/null
+++ b/library/Director/Repository/RepositoryByObjectHelper.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Icinga\Module\Director\Repository;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+use RuntimeException;
+
+trait RepositoryByObjectHelper
+{
+ protected $type;
+
+ /** @var Db */
+ protected $connection;
+
+ /** @var Auth */
+ protected static $auth;
+
+ /** @var static[] */
+ protected static $instances = [];
+
+ protected function __construct($type, Db $connection)
+ {
+ $this->type = $type;
+ $this->connection = $connection;
+ }
+
+ /**
+ * @param string $type
+ * @return bool
+ */
+ public static function hasInstanceForType($type)
+ {
+ return array_key_exists($type, self::$instances);
+ }
+
+ /**
+ * @param string $type
+ * @param Db $connection
+ * @return static
+ */
+ public static function instanceByType($type, Db $connection)
+ {
+ if (! static::hasInstanceForType($type)) {
+ self::$instances[$type] = new static($type, $connection);
+ }
+
+ return self::$instances[$type];
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return bool
+ */
+ public static function hasInstanceForObject(IcingaObject $object)
+ {
+ return static::hasInstanceForType($object->getShortTableName());
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @param Db|null $connection
+ * @return static
+ */
+ public static function instanceByObject(IcingaObject $object, Db $connection = null)
+ {
+ if (null === $connection) {
+ $connection = $object->getConnection();
+ }
+
+ if (! $connection) {
+ throw new RuntimeException(sprintf(
+ 'Cannot use repository for %s "%s" as it has no DB connection',
+ $object->getShortTableName(),
+ $object->getObjectName()
+ ));
+ }
+
+ return static::instanceByType(
+ $object->getShortTableName(),
+ $connection
+ );
+ }
+
+ protected static function auth()
+ {
+ if (self::$auth === null) {
+ self::$auth = Auth::getInstance();
+ }
+
+ return self::$auth;
+ }
+
+ protected static function clearInstances()
+ {
+ self::$instances = [];
+ }
+}
diff --git a/library/Director/Resolver/CommandUsage.php b/library/Director/Resolver/CommandUsage.php
new file mode 100644
index 0000000..7e3e0c5
--- /dev/null
+++ b/library/Director/Resolver/CommandUsage.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace Icinga\Module\Director\Resolver;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use InvalidArgumentException;
+
+class CommandUsage
+{
+ use TranslationHelper;
+
+ /** @var IcingaCommand */
+ protected $command;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /**
+ * CommandUsageTable constructor.
+ * @param IcingaCommand $command
+ */
+ public function __construct(IcingaCommand $command)
+ {
+ if ($command->isTemplate()) {
+ throw new InvalidArgumentException(
+ 'CommandUsageTable expects object or external_object, got a template'
+ );
+ }
+
+ $this->command = $command;
+ $this->db = $command->getDb();
+ }
+
+ /**
+ * @return array
+ */
+ public function getLinks()
+ {
+ $name = $this->command->getObjectName();
+ $links = [];
+ $map = [
+ 'host' => ['check_command', 'event_command'],
+ 'service' => ['check_command', 'event_command'],
+ 'notification' => ['command'],
+ ];
+ $types = [
+ 'host' => [
+ 'object' => $this->translate('%d Host(s)'),
+ 'template' => $this->translate('%d Host Template(s)'),
+ ],
+ 'service' => [
+ 'object' => $this->translate('%d Service(s)'),
+ 'template' => $this->translate('%d Service Template(s)'),
+ 'apply' => $this->translate('%d Service Apply Rule(s)'),
+ ],
+ 'notification' => [
+ 'object' => $this->translate('%d Notification(s)'),
+ 'template' => $this->translate('%d Notification Template(s)'),
+ 'apply' => $this->translate('%d Notification Apply Rule(s)'),
+ ],
+ ];
+
+ $urlSuffix = [
+ 'object' => '',
+ 'template' => '/templates',
+ 'apply' => '/applyrules',
+ ];
+
+ foreach ($map as $type => $relations) {
+ $res = $this->fetchFor($type, $relations, array_keys($types[$type]));
+ foreach ($types[$type] as $objectType => $caption) {
+ if ($res->$objectType > 0) {
+ $suffix = $urlSuffix[$objectType];
+ $links[] = Link::create(
+ sprintf($caption, $res->$objectType),
+ "director/${type}s$suffix",
+ ['command' => $name]
+ );
+ }
+ }
+ }
+
+ return $links;
+ }
+
+ protected function fetchFor($table, $rels, $objectTypes)
+ {
+ $id = $this->command->getAutoincId();
+
+ $columns = [];
+ foreach ($objectTypes as $type) {
+ $columns[$type] = "COALESCE(SUM(CASE WHEN object_type = '$type' THEN 1 ELSE 0 END), 0)";
+ }
+ $query = $this->db->select()->from("icinga_$table", $columns);
+
+ foreach ($rels as $rel) {
+ $query->orWhere("${rel}_id = ?", $id);
+ }
+
+ return $this->db->fetchRow($query);
+ }
+}
diff --git a/library/Director/Resolver/HostServiceBlacklist.php b/library/Director/Resolver/HostServiceBlacklist.php
new file mode 100644
index 0000000..606855a
--- /dev/null
+++ b/library/Director/Resolver/HostServiceBlacklist.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Icinga\Module\Director\Resolver;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaService;
+
+class HostServiceBlacklist
+{
+ /** @var Db */
+ protected $db;
+
+ protected $table = 'icinga_host_service_blacklist';
+
+ protected $mappings;
+
+ public function __construct(Db $db)
+ {
+ $this->db = $db;
+ }
+
+ protected function loadMappings()
+ {
+ $db = $this->db->getDbAdapter();
+ $query = $db->select()->from(['hsb' => $this->table], [
+ 'host_name' => 'h.object_name',
+ 'service_id' => 'hsb.service_id'
+ ])->join(
+ ['h' => 'icinga_host'],
+ 'hsb.host_id = h.id',
+ []
+ );
+
+ $result = [];
+ foreach ($db->fetchAll($query) as $row) {
+ if (array_key_exists($row->service_id, $result)) {
+ $result[$row->service_id][] = $row->host_name;
+ } else {
+ $result[$row->service_id] = [$row->host_name];
+ }
+ }
+
+ return $result;
+ }
+
+ public function preloadMappings()
+ {
+ $this->mappings = $this->loadMappings();
+
+ return $this;
+ }
+
+ public function getBlacklistedHostnamesForService(IcingaService $service)
+ {
+ if ($this->mappings === null) {
+ return $this->fetchMappingsForService($service);
+ } else {
+ return $this->getPreLoadedMappingsForService($service);
+ }
+ }
+
+ public function fetchMappingsForService(IcingaService $service)
+ {
+ if (! $service->hasBeenLoadedFromDb() || $service->get('id') === null) {
+ return [];
+ }
+
+ $db = $this->db->getDbAdapter();
+ $query = $db->select()->from(['hsb' => $this->table], [
+ 'host_name' => 'h.object_name',
+ 'service_id' => 'hsb.service_id'
+ ])->join(
+ ['h' => 'icinga_host'],
+ 'hsb.host_id = h.id',
+ []
+ )->where('hsb.service_id = ?', $service->get('id'));
+
+ return $db->fetchCol($query);
+ }
+
+ public function getPreLoadedMappingsForService(IcingaService $service)
+ {
+ if ($this->mappings !== null
+ && array_key_exists($service->get('id'), $this->mappings)
+ ) {
+ return $this->mappings[$service->get('id')];
+ }
+
+ return [];
+ }
+}
diff --git a/library/Director/Resolver/IcingaHostObjectResolver.php b/library/Director/Resolver/IcingaHostObjectResolver.php
new file mode 100644
index 0000000..210645f
--- /dev/null
+++ b/library/Director/Resolver/IcingaHostObjectResolver.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Icinga\Module\Director\Resolver;
+
+use Zend_Db_Adapter_Abstract as ZfDB;
+
+class IcingaHostObjectResolver extends IcingaObjectResolver
+{
+ /** @var ZfDB */
+ protected $db;
+
+ protected $nameMaps;
+
+ protected $baseTable = 'icinga_host';
+
+ protected $ignoredProperties = [
+ 'id',
+ 'object_type',
+ 'disabled',
+ 'has_agent',
+ 'master_should_connect',
+ 'accept_config',
+ 'api_key',
+ 'template_choice_id',
+ ];
+
+ protected $relatedTables = [
+ 'check_command_id' => 'icinga_command',
+ 'event_command_id' => 'icinga_command',
+ 'check_period_id' => 'icinga_timeperiod',
+ 'command_endpoint_id' => 'icinga_endpoint',
+ 'zone_id' => 'icinga_zone',
+ ];
+
+ protected $booleans = [
+ 'enable_notifications',
+ 'enable_active_checks',
+ 'enable_passive_checks',
+ 'enable_event_handler',
+ 'enable_flapping',
+ 'enable_perfdata',
+ 'volatile',
+ ];
+}
diff --git a/library/Director/Resolver/IcingaObjectResolver.php b/library/Director/Resolver/IcingaObjectResolver.php
new file mode 100644
index 0000000..540e2c2
--- /dev/null
+++ b/library/Director/Resolver/IcingaObjectResolver.php
@@ -0,0 +1,558 @@
+<?php
+
+namespace Icinga\Module\Director\Resolver;
+
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Data\AssignFilterHelper;
+use Icinga\Module\Director\Objects\DynamicApplyMatches;
+use Zend_Db_Adapter_Abstract as ZfDB;
+
+class IcingaObjectResolver
+{
+ /** @var ZfDB */
+ protected $db;
+
+ protected $nameMaps;
+
+ protected $baseTable = 'not_configured';
+
+ protected $ignoredProperties = [];
+
+ protected $relatedTables = [];
+
+ protected $booleans = [];
+
+ /**
+ * @var array[]
+ */
+ protected $templates;
+
+ /**
+ * @var array[]
+ */
+ protected $resolvedTemplateProperties;
+
+ /**
+ * @var array
+ */
+ protected $inheritancePaths;
+
+ protected $flatImports = [];
+
+ protected $templateVars;
+
+ protected $resolvedTemplateVars = [];
+
+ protected $groupMemberShips;
+
+ protected $resolvedGroupMemberShips;
+
+ public function __construct(ZfDb $db)
+ {
+ // TODO: loop detection. Not critical right now, as this isn't the main resolver
+ Benchmark::measure('Object Resolver for ' . $this->baseTable . ' warming up');
+ $this->db = $db;
+ // Fetch: ignore disabled?
+ $this->prepareNameMaps();
+ $this->templates = [];
+ foreach ($this->fetchPlainObjects($this->baseTable, 'template') as $template) {
+ $id = $template->id;
+ $this->stripIgnoredProperties($template);
+ $this->stripNullProperties($template);
+ $this->templates[$id] = (array) $template;
+ }
+ $this->templateVars = $this->fetchTemplateVars();
+ $this->inheritancePaths = $this->fetchInheritancePaths($this->baseTable, 'host_id');
+ foreach ($this->inheritancePaths as $path) {
+ $this->getResolvedImports($path);
+ }
+
+ // Using already resolved data, so this is unused right now:
+ // $this->groupMemberShips = $this->fetchAllGroups();
+ $this->resolvedGroupMemberShips = $this->fetchAllResolvedGroups();
+
+ foreach ($this->inheritancePaths as $path) {
+ if (! isset($this->resolvedTemplateProperties[$path])) {
+ $properties = (object) $this->getResolvedProperties($path);
+ $this->replaceRelatedNames($properties);
+ $this->convertBooleans($properties);
+ $this->resolvedTemplateProperties[$path] = $properties;
+ $this->resolvedTemplateVars[$path] = $this->getResolvedVars($path);
+ }
+ }
+
+ Benchmark::measure('Object Resolver for ' . $this->baseTable . ' is ready');
+
+ // Notes:
+ // default != null:
+ // most icinga objects: disabled => n
+ // Icinga(ScheduledDowntime|TimePeriod)Range: range_type => include, merge_behaviour => set
+ // IcingaTemplateChoice: min_required => 0, max_allowed => 1
+ // IcingaZone: is_global => n
+ // ImportSource: import_state => unknown
+ // SyncRule: sync_state => unknown
+ }
+
+ protected static function addUniqueMembers(&$list, $newMembers)
+ {
+ foreach (\array_reverse($newMembers) as $member) {
+ $pos = \array_search($member, $list);
+ if ($pos !== false) {
+ unset($list[$pos]);
+ }
+
+ \array_unshift($list, $member);
+ }
+ }
+
+ public function fetchResolvedObjects()
+ {
+ $objects = [];
+ $allVars = $this->fetchNonTemplateVars();
+ foreach ($this->fetchPlainObjects($this->baseTable, 'object') as $object) {
+ $id = $object->id; // id will be stripped
+ $objects[$id] = $this->enrichObject($object, $allVars);
+ }
+
+ return $objects;
+ }
+
+ public function fetchObjectsMatchingFilter(Filter $filter)
+ {
+ $filter = clone($filter);
+ DynamicApplyMatches::setType($this->getType());
+ DynamicApplyMatches::fixFilterColumns($filter);
+ $helper = new AssignFilterHelper($filter);
+ $objects = [];
+ $allVars = $this->fetchNonTemplateVars();
+ foreach ($this->fetchPlainObjects($this->baseTable, 'object') as $object) {
+ $id = $object->id; // id will be stripped
+ $object = $this->enrichObject($object, $allVars);
+ if ($helper->matches($object)) {
+ $objects[$id] = $object;
+ }
+ }
+
+ return $objects;
+ }
+
+ protected function enrichObject($object, $allVars)
+ {
+ $id = $object->id;
+ $this->stripIgnoredProperties($object);
+ if (isset($allVars[$id])) {
+ $vars = $allVars[$id];
+ } else {
+ $vars = [];
+ }
+ $vars += $this->getInheritedVarsById($id);
+
+ // There is no merge, +/-, not yet. Unused, as we use resolved groups:
+ // if (isset($this->groupMemberShips[$id])) {
+ // $groups = $this->groupMemberShips[$id];
+ // } else {
+ // $groups = $this->getInheritedGroupsById($id);
+ // }
+ if (isset($this->resolvedGroupMemberShips[$id])) {
+ $groups = $this->resolvedGroupMemberShips[$id];
+ } else {
+ $groups = [];
+ }
+
+ foreach ($this->getInheritedPropertiesById($id) as $property => $value) {
+ if (! isset($object->$property)) {
+ $object->$property = $value;
+ }
+ }
+ $this->replaceRelatedNames($object);
+ $this->convertBooleans($object);
+ $this->stripNullProperties($object);
+ if (! empty($vars)) {
+ $object->vars = (object) $vars;
+ static::flattenVars($object);
+ }
+ if (! empty($groups)) {
+ $object->groups = $groups;
+ }
+
+ $templates = $this->getTemplateNamesById($id);
+ if (! empty($templates)) {
+ $object->templates = \array_reverse($templates);
+ }
+
+ return $object;
+ }
+
+ /**
+ * @param string $baseTable e.g. icinga_host
+ * @param string $relColumn e.g. host_id
+ * @return array
+ */
+ protected function fetchInheritancePaths($baseTable, $relColumn)
+ {
+ if ($this->db instanceof \Zend_Db_Adapter_Pdo_Pgsql) {
+ $groupColumn = "ARRAY_TO_STRING(ARRAY_AGG(parent_$relColumn ORDER BY weight), ',')";
+ } else {
+ $groupColumn = "GROUP_CONCAT(parent_$relColumn ORDER BY weight SEPARATOR ',')";
+ }
+ $query = $this->db->select()
+ ->from([
+ 'oi' => "${baseTable}_inheritance"
+ ], [
+ $relColumn,
+ $groupColumn
+ ])
+ ->group($relColumn)
+ // Ordering by length increases the possibility to have less cycles afterwards
+ ->order("LENGTH($groupColumn)");
+
+ return $this->db->fetchPairs($query);
+ }
+
+ protected function getInheritedPropertiesById($objectId)
+ {
+ if (isset($this->inheritancePaths[$objectId])) {
+ return $this->getResolvedProperties($this->inheritancePaths[$objectId]);
+ } else {
+ return [];
+ }
+ }
+
+ protected function getInheritedVarsById($objectId)
+ {
+ if (isset($this->inheritancePaths[$objectId])) {
+ return $this->getResolvedVars($this->inheritancePaths[$objectId]);
+ } else {
+ return [];
+ }
+ }
+
+ protected function getInheritedGroupsById($objectId)
+ {
+ if (isset($this->inheritancePaths[$objectId])) {
+ return $this->getResolvedGroups($this->inheritancePaths[$objectId]);
+ } else {
+ return [];
+ }
+ }
+
+ protected function getTemplateNamesByID($objectId)
+ {
+ if (isset($this->inheritancePaths[$objectId])) {
+ return $this->translateTemplateIdsToNames(
+ $this->getResolvedImports($this->inheritancePaths[$objectId])
+ );
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * @param $path
+ * @return array[]
+ */
+ protected function getResolvedProperties($path)
+ {
+ $result = [];
+ // + adds only non existing members, so let's reverse our templates
+ foreach ($this->getResolvedImports($path) as $templateId) {
+ $result += $this->templates[$templateId];
+ }
+ unset($result['object_name']);
+
+ return $result;
+ }
+
+ protected function getResolvedVars($path)
+ {
+ $result = [];
+ foreach ($this->getResolvedImports($path) as $templateId) {
+ $result += $this->getTemplateVars($templateId);
+ }
+
+ return $result;
+ }
+
+ protected function getTemplateVars($templateId)
+ {
+ if (isset($this->templateVars[$templateId])) {
+ return $this->templateVars[$templateId];
+ } else {
+ return [];
+ }
+ }
+
+ protected function getResolvedGroups($path)
+ {
+ $pos = \strpos($path, ',');
+ if ($pos === false) {
+ if (isset($this->groupMemberShips[$path])) {
+ return $this->groupMemberShips[$path];
+ } else {
+ return [];
+ }
+ } else {
+ $first = \substr($path, 0, $pos);
+ $parentPath = \substr($path, $pos + 1);
+ $currentGroups = $this->getResolvedVars($first);
+
+ // There is no merging +/-, not yet
+ if (empty($currentGroups)) {
+ return $this->getResolvedVars($parentPath);
+ } else {
+ return $currentGroups;
+ }
+ }
+ }
+
+ /**
+ * Hint: this ships most important (last) imports first
+ *
+ * @param $path
+ * @return array
+ */
+ protected function getResolvedImports($path)
+ {
+ if (! isset($this->flatImports[$path])) {
+ $this->flatImports[$path] = $this->calculateFlatImports($path);
+ }
+
+ return $this->flatImports[$path];
+ }
+
+ protected function calculateFlatImports($path)
+ {
+ $imports = \preg_split('/,/', $path);
+ $ancestors = [];
+ foreach ($imports as $template) {
+ if (isset($this->inheritancePaths[$template])) {
+ $this->addUniqueMembers(
+ $ancestors,
+ $this->calculateFlatImports($this->inheritancePaths[$template])
+ );
+ }
+ $this->addUniqueMembers($ancestors, [$template]);
+ }
+
+ return $ancestors;
+ }
+
+ protected function fetchPlainObjects($table, $objectType = null)
+ {
+ $query = $this->db->select()
+ ->from(['o' => $table])
+ ->order('o.object_name');
+
+ if ($objectType !== null) {
+ $query->where('o.object_type = ?', $objectType);
+ }
+
+ return $this->db->fetchAll($query);
+ }
+
+
+ /**
+ * @param \stdClass $object
+ */
+ protected function replaceRelatedNames($object)
+ {
+ foreach ($this->nameMaps as $property => $map) {
+ if (\property_exists($object, $property)) {
+ // Hint: substr strips _id
+ if ($object->$property === null) {
+ $object->{\substr($property, 0, -3)} = null;
+ } else {
+ $object->{\substr($property, 0, -3)} = $map[$object->$property];
+ }
+ unset($object->$property);
+ }
+ }
+ }
+
+ protected function translateTemplateIdsToNames($ids)
+ {
+ $names = [];
+ foreach ($ids as $id) {
+ if (isset($this->templates[$id])) {
+ $names[] = $this->templates[$id]['object_name'];
+ } else {
+ throw new \RuntimeException("There is no template with ID $id");
+ }
+ }
+
+ return $names;
+ }
+
+ protected function stripIgnoredProperties($object)
+ {
+ foreach ($this->ignoredProperties as $key) {
+ unset($object->$key);
+ }
+ }
+
+ public function prepareNameMaps()
+ {
+ // TODO: fetch from dummy Object? How to ignore irrelevant ones like choices?
+ $relatedNames = [];
+ foreach ($this->relatedTables as $key => $relatedTable) {
+ $relatedNames[$key] = $this->fetchRelationMap($this->baseTable, $relatedTable, $key);
+ }
+
+ $this->nameMaps = $relatedNames;
+ }
+
+ protected function convertBooleans($object)
+ {
+ foreach ($this->booleans as $property) {
+ if (\property_exists($object, $property) && $object->$property !== null) {
+ // Hint: substr strips _id
+ $object->$property = $object->$property === 'y';
+ }
+ }
+ }
+
+ protected function stripNullProperties($object)
+ {
+ foreach (\array_keys((array) $object) as $property) {
+ if ($object->$property === null) {
+ unset($object->$property);
+ }
+ }
+ }
+
+ protected function fetchRelationMap($sourceTable, $destinationTable, $property)
+ {
+ $query = $this->db->select()
+ ->from(['d' => $destinationTable], ['d.id', 'd.object_name'])
+ ->join(['o' => $sourceTable], "d.id = o.$property", [])
+ ->order('d.object_name');
+
+ return $this->db->fetchPairs($query);
+ }
+
+ protected function fetchTemplateVars()
+ {
+ $query = $this->prepareVarsQuery()->where('o.object_type = ?', 'template');
+ return $this->fetchAndCombineVars($query);
+ }
+
+ protected function fetchNonTemplateVars()
+ {
+ $query = $this->prepareVarsQuery()->where('o.object_type != ?', 'template');
+ return $this->fetchAndCombineVars($query);
+ }
+
+ protected function fetchAndCombineVars($query)
+ {
+ $vars = [];
+ foreach ($this->db->fetchAll($query) as $var) {
+ $id = $var->object_id;
+ if (! isset($vars[$id])) {
+ $vars[$id] = [];
+ }
+ if ($var->format === 'json') {
+ $vars[$id][$var->varname] = \json_decode($var->varvalue);
+ } else {
+ $vars[$id][$var->varname] = $var->varvalue;
+ }
+ }
+
+ return $vars;
+ }
+
+ protected function fetchAllGroups()
+ {
+ $query = $this->prepareGroupsQuery();
+ return $this->fetchAndCombineGroups($query);
+ }
+
+ protected function fetchAllResolvedGroups()
+ {
+ $query = $this->prepareGroupsQuery(true);
+ return $this->fetchAndCombineGroups($query);
+ }
+
+ protected function fetchAndCombineGroups($query)
+ {
+ $groups = [];
+ foreach ($this->db->fetchAll($query) as $group) {
+ $id = $group->object_id;
+ if (isset($groups[$id])) {
+ $groups[$id][$group->group_id] = $group->group_name;
+ } else {
+ $groups[$id] = [
+ $group->group_id => $group->group_name
+ ];
+ }
+ }
+
+ return $groups;
+ }
+
+ protected function prepareGroupsQuery($resolved = false)
+ {
+ $type = $this->getType();
+ $groupsTable = $this->baseTable . 'group';
+ $groupMembershipTable = "${groupsTable}_$type";
+ if ($resolved) {
+ $groupMembershipTable .= '_resolved';
+ }
+ $oRef = "${type}_id";
+ $gRef = "${type}group_id";
+
+ return $this->db->select()
+ ->from(['gm' => $groupMembershipTable], [
+ 'object_id' => $oRef,
+ 'group_id' => $gRef,
+ 'group_name' => 'g.object_name'
+ ])
+ ->join(['g' => $groupsTable], "g.id = gm.$gRef", [])
+ ->order("gm.$oRef")
+ ->order('g.object_name');
+ }
+
+ protected function prepareVarsQuery()
+ {
+ $table = $this->baseTable . '_var';
+ $ref = $this->getType() . '_id';
+ return $this->db->select()
+ ->from(['v' => $table], [
+ 'object_id' => $ref,
+ 'v.varname',
+ 'v.varvalue',
+ 'v.format',
+ // 'v.checksum',
+ ])
+ ->join(['o' => $this->baseTable], "o.id = v.$ref", [])
+ ->order('o.id')
+ ->order('v.varname');
+ }
+
+ protected function getType()
+ {
+ return \preg_replace('/^icinga_/', '', $this->baseTable);
+ }
+
+ /**
+ * Helper, flattens all vars of a given object
+ *
+ * The object itself will be modified, and the 'vars' property will be
+ * replaced with corresponding 'vars.whatever' properties
+ *
+ * @param $object
+ * @param string $key
+ */
+ protected static function flattenVars(\stdClass $object, $key = 'vars')
+ {
+ if (\property_exists($object, $key)) {
+ foreach ($object->vars as $k => $v) {
+ if (\is_object($v)) {
+ static::flattenVars($v, $k);
+ }
+ $object->{$key . '.' . $k} = $v;
+ }
+ unset($object->$key);
+ }
+ }
+}
diff --git a/library/Director/Resolver/OverriddenVarsResolver.php b/library/Director/Resolver/OverriddenVarsResolver.php
new file mode 100644
index 0000000..4541244
--- /dev/null
+++ b/library/Director/Resolver/OverriddenVarsResolver.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Icinga\Module\Director\Resolver;
+
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaHost;
+
+class OverriddenVarsResolver
+{
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var string */
+ protected $overrideVarName;
+
+ public function __construct(Db $connection)
+ {
+ $this->overrideVarName = $connection->settings()->get('override_services_varname');
+ $this->db = $connection->getDbAdapter();
+ }
+
+ public function fetchForHost(IcingaHost $host)
+ {
+ $overrides = [];
+ $parents = $host->listFlatResolvedImportNames();
+ if (empty($parents)) {
+ return $overrides;
+ }
+ $query = $this->db->select()
+ ->from(['hv' => 'icinga_host_var'], [
+ 'host_name' => 'h.object_name',
+ 'varvalue' => 'hv.varvalue',
+ ])
+ ->join(
+ ['h' => 'icinga_host'],
+ 'h.id = hv.host_id',
+ []
+ )
+ ->where('hv.varname = ?', $this->overrideVarName)
+ ->where('h.object_name IN (?)', $parents);
+
+ foreach ($this->db->fetchAll($query) as $row) {
+ if ($row->varvalue === null) {
+ continue;
+ }
+ foreach (Json::decode($row->varvalue) as $serviceName => $vars) {
+ $overrides[$serviceName][$row->host_name] = $vars;
+ }
+ }
+
+ return $overrides;
+ }
+
+ public function fetchForServiceName(IcingaHost $host, $serviceName)
+ {
+ $overrides = $this->fetchForHost($host);
+ if (isset($overrides[$serviceName])) {
+ return $overrides[$serviceName];
+ }
+
+ return [];
+ }
+
+ public function fetchVarForServiceName(IcingaHost $host, $serviceName, $varName)
+ {
+ $overrides = $this->fetchForHost($host);
+ if (isset($overrides[$serviceName][$varName])) {
+ return $overrides[$serviceName][$varName];
+ }
+
+ return null;
+ }
+}
diff --git a/library/Director/Resolver/OverrideHelper.php b/library/Director/Resolver/OverrideHelper.php
new file mode 100644
index 0000000..f911a4f
--- /dev/null
+++ b/library/Director/Resolver/OverrideHelper.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Icinga\Module\Director\Resolver;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use InvalidArgumentException;
+
+class OverrideHelper
+{
+ public static function applyOverriddenVars(IcingaHost $host, $serviceName, $properties)
+ {
+ static::assertVarsForOverrides($properties);
+ $current = $host->getOverriddenServiceVars($serviceName);
+ foreach ($properties as $key => $value) {
+ if ($key === 'vars') {
+ foreach ($value as $k => $v) {
+ $current->$k = $v;
+ }
+ } else {
+ $current->{substr($key, 5)} = $value;
+ }
+ }
+ $host->overrideServiceVars($serviceName, $current);
+ }
+
+ public static function assertVarsForOverrides($properties)
+ {
+ if (empty($properties)) {
+ return;
+ }
+
+ foreach ($properties as $key => $value) {
+ if ($key !== 'vars' && substr($key, 0, 5) !== 'vars.') {
+ throw new InvalidArgumentException("Only Custom Variables can be set based on Variable Overrides");
+ }
+ }
+ }
+}
diff --git a/library/Director/Resolver/TemplateTree.php b/library/Director/Resolver/TemplateTree.php
new file mode 100644
index 0000000..f8d8fed
--- /dev/null
+++ b/library/Director/Resolver/TemplateTree.php
@@ -0,0 +1,491 @@
+<?php
+
+namespace Icinga\Module\Director\Resolver;
+
+use Icinga\Application\Benchmark;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Objects\IcingaObject;
+use InvalidArgumentException;
+use RuntimeException;
+
+class TemplateTree
+{
+ protected $connection;
+
+ protected $db;
+
+ protected $parents;
+
+ protected $children;
+
+ protected $rootNodes;
+
+ protected $tree;
+
+ protected $type;
+
+ protected $objectMaps;
+
+ protected $names;
+
+ protected $templateNameToId;
+
+ public function __construct($type, Db $connection)
+ {
+ $this->type = $type;
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function listParentNamesFor(IcingaObject $object)
+ {
+ $id = (int) $object->getProperty('id');
+ $this->requireTree();
+
+ if (array_key_exists($id, $this->parents)) {
+ return array_values($this->parents[$id]);
+ }
+
+ $this->requireObjectMaps();
+
+ $parents = [];
+ if (array_key_exists($id, $this->objectMaps)) {
+ foreach ($this->objectMaps[$id] as $pid) {
+ if (array_key_exists($pid, $this->names)) {
+ $parents[] = $this->names[$pid];
+ } else {
+ throw new RuntimeException(sprintf(
+ 'Got invalid parent id %d for %s "%s"',
+ $pid,
+ $this->type,
+ $object->getObjectName()
+ ));
+ }
+ }
+ }
+
+ return $parents;
+ }
+
+ protected function loadObjectMaps()
+ {
+ $this->requireTree();
+
+ $map = [];
+ $db = $this->db;
+ $type = $this->type;
+ $table = "icinga_${type}_inheritance";
+
+ $query = $db->select()->from(
+ ['i' => $table],
+ [
+ 'object' => "i.${type}_id",
+ 'parent' => "i.parent_${type}_id",
+ ]
+ )->order('i.weight');
+
+ foreach ($db->fetchAll($query) as $row) {
+ $id = (int) $row->object;
+ if (! array_key_exists($id, $map)) {
+ $map[$id] = [];
+ }
+ $map[$id][] = (int) $row->parent;
+ }
+
+ $this->objectMaps = $map;
+ }
+
+ public function listParentIdsFor(IcingaObject $object)
+ {
+ return array_keys($this->getParentsFor($object));
+ }
+
+ public function listAncestorIdsFor(IcingaObject $object)
+ {
+ return array_keys($this->getAncestorsFor($object));
+ }
+
+ public function listChildIdsFor(IcingaObject $object)
+ {
+ return array_keys($this->getChildrenFor($object));
+ }
+
+ public function listDescendantIdsFor(IcingaObject $object)
+ {
+ return array_keys($this->getDescendantsFor($object));
+ }
+
+ public function getParentsFor(IcingaObject $object)
+ {
+ // can not use hasBeenLoadedFromDb() when in onStore()
+ $id = $object->getProperty('id');
+ if ($id !== null) {
+ return $this->getParentsById($object->getProperty('id'));
+ } else {
+ throw new RuntimeException(
+ 'Loading parents for unstored objects has not been implemented yet'
+ );
+ // return $this->getParentsForUnstoredObject($object);
+ }
+ }
+
+ public function getAncestorsFor(IcingaObject $object)
+ {
+ if ($object->hasBeenModified()
+ && $object->gotImports()
+ && $object->imports()->hasBeenModified()
+ ) {
+ return $this->getAncestorsForUnstoredObject($object);
+ } else {
+ return $this->getAncestorsById($object->getProperty('id'));
+ }
+ }
+
+ protected function getAncestorsForUnstoredObject(IcingaObject $object)
+ {
+ $this->requireTree();
+ $ancestors = [];
+ foreach ($object->imports() as $import) {
+ $name = $import->getObjectName();
+ if ($import->hasBeenLoadedFromDb()) {
+ $pid = (int) $import->get('id');
+ } else {
+ if (! array_key_exists($name, $this->templateNameToId)) {
+ continue;
+ }
+ $pid = $this->templateNameToId[$name];
+ }
+
+ $this->getAncestorsById($pid, $ancestors);
+
+ // Hint: inheritance order matters
+ if (false !== ($key = array_search($name, $ancestors))) {
+ // Note: this used to be just unset($ancestors[$key]), and that
+ // broke Apply Rules inheriting from Templates with the same name
+ // in a way that related fields no longer showed up (#1602)
+ // This new if relaxes this and doesn't unset in case the name
+ // matches the original object name. However, I'm still unsure why
+ // this was required at all.
+ if ($name !== $object->getObjectName()) {
+ unset($ancestors[$key]);
+ }
+ }
+ $ancestors[$pid] = $name;
+ }
+
+ return $ancestors;
+ }
+
+ protected function requireObjectMaps()
+ {
+ if ($this->objectMaps === null) {
+ $this->loadObjectMaps();
+ }
+ }
+
+ public function getParentsById($id)
+ {
+ $this->requireTree();
+
+ if (array_key_exists($id, $this->parents)) {
+ return $this->parents[$id];
+ }
+
+ $this->requireObjectMaps();
+ if (array_key_exists($id, $this->objectMaps)) {
+ $parents = [];
+ foreach ($this->objectMaps[$id] as $pid) {
+ $parents[$pid] = $this->names[$pid];
+ }
+
+ return $parents;
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * @param $id
+ * @param $list
+ * @throws NestingError
+ */
+ protected function assertNotInList($id, &$list)
+ {
+ if (array_key_exists($id, $list)) {
+ $list = array_keys($list);
+ array_push($list, $id);
+
+ if (is_int($id)) {
+ throw new NestingError(
+ 'Loop detected: %s',
+ implode(' -> ', $this->getNamesForIds($list, true))
+ );
+ } else {
+ throw new NestingError(
+ 'Loop detected: %s',
+ implode(' -> ', $list)
+ );
+ }
+ }
+ }
+
+ protected function getNamesForIds($ids, $ignoreErrors = false)
+ {
+ $names = [];
+ foreach ($ids as $id) {
+ $names[] = $this->getNameForId($id, $ignoreErrors);
+ }
+
+ return $names;
+ }
+
+ protected function getNameForId($id, $ignoreErrors = false)
+ {
+ if (! array_key_exists($id, $this->names)) {
+ if ($ignoreErrors) {
+ return "id=$id";
+ } else {
+ throw new InvalidArgumentException("Got no name for $id");
+ }
+ }
+
+ return $this->names[$id];
+ }
+
+ /**
+ * @param $id
+ * @param array $ancestors
+ * @param array $path
+ * @return array
+ * @throws NestingError
+ */
+ public function getAncestorsById($id, &$ancestors = [], $path = [])
+ {
+ $path[$id] = true;
+ foreach ($this->getParentsById($id) as $pid => $name) {
+ $this->assertNotInList($pid, $path);
+ $path[$pid] = true;
+
+ $this->getAncestorsById($pid, $ancestors, $path);
+ unset($path[$pid]);
+
+ // Hint: inheritance order matters
+ if (false !== ($key = array_search($name, $ancestors))) {
+ unset($ancestors[$key]);
+ }
+ $ancestors[$pid] = $name;
+ }
+ unset($path[$id]);
+
+ return $ancestors;
+ }
+
+ public function getChildrenFor(IcingaObject $object)
+ {
+ // can not use hasBeenLoadedFromDb() when in onStore()
+ $id = $object->getProperty('id');
+ if ($id !== null) {
+ return $this->getChildrenById($id);
+ } else {
+ throw new RuntimeException(
+ 'Loading children for unstored objects has not been implemented yet'
+ );
+ // return $this->getChildrenForUnstoredObject($object);
+ }
+ }
+
+ public function getChildrenById($id)
+ {
+ $this->requireTree();
+
+ if (array_key_exists($id, $this->children)) {
+ return $this->children[$id];
+ } else {
+ return [];
+ }
+ }
+
+ public function getDescendantsFor(IcingaObject $object)
+ {
+ // can not use hasBeenLoadedFromDb() when in onStore()
+ $id = $object->getProperty('id');
+ if ($id !== null) {
+ return $this->getDescendantsById($id);
+ } else {
+ throw new RuntimeException(
+ 'Loading descendants for unstored objects has not been implemented yet'
+ );
+ // return $this->getDescendantsForUnstoredObject($object);
+ }
+ }
+
+ public function getDescendantsById($id, &$children = [], &$path = [])
+ {
+ $path[$id] = true;
+ foreach ($this->getChildrenById($id) as $pid => $name) {
+ $this->assertNotInList($pid, $path);
+ $path[$pid] = true;
+ $this->getDescendantsById($pid, $children, $path);
+ unset($path[$pid]);
+ $children[$pid] = $name;
+ }
+ unset($path[$id]);
+
+ return $children;
+ }
+
+ public function getTree($parentId = null)
+ {
+ if ($this->tree === null) {
+ $this->prepareTree();
+ }
+
+ if ($parentId === null) {
+ return $this->returnFullTree();
+ } else {
+ throw new RuntimeException(
+ 'Partial tree fetching has not been implemented yet'
+ );
+ // return $this->partialTree($parentId);
+ }
+ }
+
+ protected function returnFullTree()
+ {
+ $result = $this->rootNodes;
+ foreach ($result as $id => &$node) {
+ $this->addChildrenById($id, $node);
+ }
+
+ return $result;
+ }
+
+ protected function addChildrenById($pid, array &$base)
+ {
+ foreach ($this->getChildrenById($pid) as $id => $name) {
+ $base['children'][$id] = [
+ 'name' => $name,
+ 'children' => []
+ ];
+ $this->addChildrenById($id, $base['children'][$id]);
+ }
+ }
+
+ protected function prepareTree()
+ {
+ Benchmark::measure(sprintf('Prepare "%s" Template Tree', $this->type));
+ $templates = $this->fetchTemplates();
+ $parents = [];
+ $rootNodes = [];
+ $children = [];
+ $names = [];
+ foreach ($templates as $row) {
+ $id = (int) $row->id;
+ $pid = (int) $row->parent_id;
+ $names[$id] = $row->name;
+ if (! array_key_exists($id, $parents)) {
+ $parents[$id] = [];
+ }
+
+ if ($row->parent_id === null) {
+ $rootNodes[$id] = [
+ 'name' => $row->name,
+ 'children' => []
+ ];
+ continue;
+ }
+
+ $names[$pid] = $row->parent_name;
+ $parents[$id][$pid] = $row->parent_name;
+
+ if (! array_key_exists($pid, $children)) {
+ $children[$pid] = [];
+ }
+
+ $children[$pid][$id] = $row->name;
+ }
+
+ $this->parents = $parents;
+ $this->children = $children;
+ $this->rootNodes = $rootNodes;
+ $this->names = $names;
+ $this->templateNameToId = array_flip($names);
+ Benchmark::measure(sprintf('"%s" Template Tree ready', $this->type));
+ }
+
+ public function fetchObjects()
+ {
+ //??
+ }
+
+ protected function requireTree()
+ {
+ if ($this->parents === null) {
+ $this->prepareTree();
+ }
+ }
+
+ public function fetchTemplates()
+ {
+ $db = $this->db;
+ $type = $this->type;
+ $table = "icinga_$type";
+
+ if ($type === 'command') {
+ $joinCondition = $db->quoteInto(
+ "p.id = i.parent_${type}_id",
+ 'template'
+ );
+ } else {
+ $joinCondition = $db->quoteInto(
+ "p.id = i.parent_${type}_id AND p.object_type = ?",
+ 'template'
+ );
+ }
+
+ $query = $db->select()->from(
+ ['o' => $table],
+ [
+ 'id' => 'o.id',
+ 'name' => 'o.object_name',
+ 'object_type' => 'o.object_type',
+ 'parent_id' => 'p.id',
+ 'parent_name' => 'p.object_name',
+ ]
+ )->joinLeft(
+ ['i' => $table . '_inheritance'],
+ 'o.id = i.' . $type . '_id',
+ []
+ )->joinLeft(
+ ['p' => $table],
+ $joinCondition,
+ []
+ )->order('o.id')->order('i.weight');
+
+ if ($type !== 'command') {
+ $query->where(
+ 'o.object_type = ?',
+ 'template'
+ );
+ }
+
+ return $db->fetchAll($query);
+ }
+}
+
+/**
+ *
+SELECT o.id, o.object_name AS name, o.object_type, p.id AS parent_id,
+ p.object_name AS parent_name FROM icinga_service AS o
+RIGHT JOIN icinga_service_inheritance AS i ON o.id = i.service_id
+RIGHT JOIN icinga_service AS p ON p.id = i.parent_service_id
+ WHERE (p.object_type = 'template') AND (o.object_type = 'template')
+ ORDER BY o.id ASC, i.weight ASC
+
+ */
diff --git a/library/Director/RestApi/IcingaObjectHandler.php b/library/Director/RestApi/IcingaObjectHandler.php
new file mode 100644
index 0000000..7329be3
--- /dev/null
+++ b/library/Director/RestApi/IcingaObjectHandler.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Icinga\Module\Director\RestApi;
+
+use Exception;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Core\CoreApi;
+use Icinga\Module\Director\Data\Exporter;
+use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Resolver\OverrideHelper;
+use InvalidArgumentException;
+use RuntimeException;
+
+class IcingaObjectHandler extends RequestHandler
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ /** @var CoreApi */
+ protected $api;
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+
+ public function setApi(CoreApi $api)
+ {
+ $this->api = $api;
+ return $this;
+ }
+
+ /**
+ * @return IcingaObject
+ * @throws ProgrammingError
+ */
+ protected function requireObject()
+ {
+ if ($this->object === null) {
+ throw new ProgrammingError('Object is required');
+ }
+
+ return $this->object;
+ }
+
+ /**
+ * @return IcingaObject
+ */
+ protected function loadOptionalObject()
+ {
+ return $this->object;
+ }
+
+ protected function requireJsonBody()
+ {
+ $data = json_decode($this->request->getRawBody());
+
+ if ($data === null) {
+ $this->response->setHttpResponseCode(400);
+ throw new IcingaException(
+ 'Invalid JSON: %s',
+ $this->getLastJsonError()
+ );
+ }
+
+ return $data;
+ }
+
+ protected function getType()
+ {
+ return $this->request->getControllerName();
+ }
+
+ protected function processApiRequest()
+ {
+ try {
+ $this->handleApiRequest();
+ } catch (NotFoundError $e) {
+ $this->sendJsonError($e, 404);
+ return;
+ } catch (DuplicateKeyException $e) {
+ $this->sendJsonError($e, 422);
+ return;
+ } catch (Exception $e) {
+ $this->sendJsonError($e);
+ }
+
+ if ($this->request->getActionName() !== 'index') {
+ throw new NotFoundError('Not found');
+ }
+ }
+
+ protected function handleApiRequest()
+ {
+ $request = $this->request;
+ $db = $this->db;
+
+ // TODO: I hate doing this:
+ if ($this->request->getActionName() === 'ticket') {
+ $host = $this->requireObject();
+
+ if ($host->getResolvedProperty('has_agent') !== 'y') {
+ throw new NotFoundError('The host "%s" is not an agent', $host->getObjectName());
+ }
+
+ $this->sendJson($this->api->getTicket($host->getObjectName()));
+
+ // TODO: find a better way to shut down. Currently, this avoids
+ // "not found" errors:
+ exit;
+ }
+
+ switch ($request->getMethod()) {
+ case 'DELETE':
+ $object = $this->requireObject();
+ $object->delete();
+ $this->sendJson($object->toPlainObject(false, true));
+ break;
+
+ case 'POST':
+ case 'PUT':
+ $data = (array) $this->requireJsonBody();
+ $params = $this->request->getUrl()->getParams();
+ $allowsOverrides = $params->get('allowOverrides');
+ $type = $this->getType();
+ if ($object = $this->loadOptionalObject()) {
+ if ($request->getMethod() === 'POST') {
+ $object->setProperties($data);
+ } else {
+ $data = array_merge([
+ 'object_type' => $object->get('object_type'),
+ 'object_name' => $object->getObjectName()
+ ], $data);
+ $object->replaceWith(IcingaObject::createByType($type, $data, $db));
+ }
+ $this->persistChanges($object);
+ $this->sendJson($object->toPlainObject(false, true));
+ } elseif ($allowsOverrides && $type === 'service') {
+ if ($request->getMethod() === 'PUT') {
+ throw new InvalidArgumentException('Overrides are not (yet) available for HTTP PUT');
+ }
+ $this->setServiceProperties($params->getRequired('host'), $params->getRequired('name'), $data);
+ } else {
+ $object = IcingaObject::createByType($type, $data, $db);
+ $this->persistChanges($object);
+ $this->sendJson($object->toPlainObject(false, true));
+ }
+ break;
+
+ case 'GET':
+ $object = $this->requireObject();
+ $exporter = new Exporter($this->db);
+ RestApiParams::applyParamsToExporter($exporter, $this->request, $object->getShortTableName());
+ $this->sendJson($exporter->export($object));
+ break;
+
+ default:
+ $request->getResponse()->setHttpResponseCode(400);
+ throw new IcingaException('Unsupported method ' . $request->getMethod());
+ }
+ }
+
+ protected function persistChanges(IcingaObject $object)
+ {
+ if ($object->hasBeenModified()) {
+ $status = $object->hasBeenLoadedFromDb() ? 200 : 201;
+ $object->store();
+ $this->response->setHttpResponseCode($status);
+ } else {
+ $this->response->setHttpResponseCode(304);
+ }
+ }
+
+ protected function setServiceProperties($hostname, $serviceName, $properties)
+ {
+ $host = IcingaHost::load($hostname, $this->db);
+ $service = ServiceFinder::find($host, $serviceName);
+ if ($service === false) {
+ throw new NotFoundError('Not found');
+ }
+ if ($service->requiresOverrides()) {
+ unset($properties['host']);
+ OverrideHelper::applyOverriddenVars($host, $serviceName, $properties);
+ $this->persistChanges($host);
+ $this->sendJson($host->toPlainObject(false, true));
+ } else {
+ throw new RuntimeException('Found a single service, which should have been found (and dealt with) before');
+ }
+ }
+}
diff --git a/library/Director/RestApi/IcingaObjectsHandler.php b/library/Director/RestApi/IcingaObjectsHandler.php
new file mode 100644
index 0000000..471987a
--- /dev/null
+++ b/library/Director/RestApi/IcingaObjectsHandler.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace Icinga\Module\Director\RestApi;
+
+use Exception;
+use gipfl\Json\JsonString;
+use Icinga\Application\Benchmark;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Data\Exporter;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Table\ApplyRulesTable;
+use Icinga\Module\Director\Web\Table\ObjectsTable;
+use Zend_Db_Select as ZfSelect;
+
+class IcingaObjectsHandler extends RequestHandler
+{
+ /** @var ObjectsTable */
+ protected $table;
+
+ public function processApiRequest()
+ {
+ try {
+ $this->streamJsonResult();
+ } catch (Exception $e) {
+ $this->sendJsonError($e);
+ }
+ }
+
+ /**
+ * @param ObjectsTable|ApplyRulesTable $table
+ * @return $this
+ */
+ public function setTable($table)
+ {
+ $this->table = $table;
+ return $this;
+ }
+
+ /**
+ * @return ObjectsTable
+ * @throws ProgrammingError
+ */
+ protected function getTable()
+ {
+ if ($this->table === null) {
+ throw new ProgrammingError('Table is required');
+ }
+
+ return $this->table;
+ }
+
+ /**
+ * @throws ProgrammingError
+ * @throws \Zend_Db_Select_Exception
+ * @throws \Zend_Db_Statement_Exception
+ */
+ protected function streamJsonResult()
+ {
+ $this->response->setHeader('Content-Type', 'application/json', true);
+ $this->response->sendHeaders();
+ $connection = $this->db;
+ Benchmark::measure('Ready to stream JSON result');
+ $db = $connection->getDbAdapter();
+ $table = $this->getTable();
+ $exporter = new Exporter($connection);
+ $type = $table->getType();
+ RestApiParams::applyParamsToExporter($exporter, $this->request, $type);
+ $query = $table
+ ->getQuery()
+ ->reset(ZfSelect::COLUMNS)
+ ->columns('*')
+ ->reset(ZfSelect::LIMIT_COUNT)
+ ->reset(ZfSelect::LIMIT_OFFSET);
+ if ($type === 'service' && $table instanceof ApplyRulesTable) {
+ $exporter->showIds();
+ }
+ echo '{ "objects": [ ';
+ $cnt = 0;
+ $objects = [];
+
+ $dummy = IcingaObject::createByType($type, [], $connection);
+ $dummy->prefetchAllRelatedTypes();
+
+ Benchmark::measure('Pre-fetching related objects');
+ PrefetchCache::initialize($this->db);
+ Benchmark::measure('Ready to query');
+ $stmt = $db->query($query);
+ $this->response->sendHeaders();
+ if (! ob_get_level()) {
+ ob_start();
+ }
+
+ $first = true;
+ $flushes = 0;
+ while ($row = $stmt->fetch()) {
+ /** @var IcingaObject $object */
+ if ($first) {
+ Benchmark::measure('Fetching first row');
+ }
+ $object = $dummy::fromDbRow($row, $connection);
+ $objects[] = JsonString::encode($exporter->export($object), JSON_PRETTY_PRINT);
+ if ($first) {
+ Benchmark::measure('Got first row');
+ $first = false;
+ }
+ $cnt++;
+ if ($cnt === 100) {
+ if ($flushes > 0) {
+ echo ', ';
+ }
+ echo implode(', ', $objects);
+ $cnt = 0;
+ $objects = [];
+ $flushes++;
+ ob_end_flush();
+ ob_start();
+ }
+ }
+
+ if ($cnt > 0) {
+ if ($flushes > 0) {
+ echo ', ';
+ }
+ echo implode(', ', $objects);
+ }
+
+ if ($this->request->getUrl()->getParams()->get('benchmark')) {
+ echo "],\n";
+ Benchmark::measure('All done');
+ echo '"benchmark_string": ' . json_encode(Benchmark::renderToText());
+ } else {
+ echo '] ';
+ }
+
+ echo "}\n";
+ if (ob_get_level()) {
+ ob_end_flush();
+ }
+
+ // TODO: can we improve this?
+ exit;
+ }
+}
diff --git a/library/Director/RestApi/RequestHandler.php b/library/Director/RestApi/RequestHandler.php
new file mode 100644
index 0000000..6f66889
--- /dev/null
+++ b/library/Director/RestApi/RequestHandler.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Icinga\Module\Director\RestApi;
+
+use Exception;
+use gipfl\Json\JsonString;
+use Icinga\Module\Director\Db;
+use Icinga\Web\Request;
+use Icinga\Web\Response;
+
+abstract class RequestHandler
+{
+ /** @var Request */
+ protected $request;
+
+ /** @var Response */
+ protected $response;
+
+ /** @var Db */
+ protected $db;
+
+ public function __construct(Request $request, Response $response, Db $db)
+ {
+ $this->request = $request;
+ $this->response = $response;
+ $this->db = $db;
+ }
+
+ abstract protected function processApiRequest();
+
+ public function dispatch()
+ {
+ $this->processApiRequest();
+ }
+
+ public function sendJson($object)
+ {
+ $this->response->setHeader('Content-Type', 'application/json', true);
+ $this->response->sendHeaders();
+ echo JsonString::encode($object, JSON_PRETTY_PRINT) . "\n";
+ }
+
+ public function sendJsonError($error, $code = null)
+ {
+ $response = $this->response;
+ if ($code === null) {
+ if ($response->getHttpResponseCode() === 200) {
+ $response->setHttpResponseCode(500);
+ }
+ } else {
+ $response->setHttpResponseCode((int) $code);
+ }
+
+ if ($error instanceof Exception) {
+ $message = $error->getMessage();
+ } else {
+ $message = $error;
+ }
+
+ $response->sendHeaders();
+ $result = ['error' => $message];
+ if ($this->request->getUrl()->getParam('showStacktrace')) {
+ $result['trace'] = $error->getTraceAsString();
+ }
+ $this->sendJson((object) $result);
+ }
+
+ // TODO: just return json_last_error_msg() for PHP >= 5.5.0
+ protected function getLastJsonError()
+ {
+ switch (json_last_error()) {
+ case JSON_ERROR_DEPTH:
+ return 'The maximum stack depth has been exceeded';
+ case JSON_ERROR_CTRL_CHAR:
+ return 'Control character error, possibly incorrectly encoded';
+ case JSON_ERROR_STATE_MISMATCH:
+ return 'Invalid or malformed JSON';
+ case JSON_ERROR_SYNTAX:
+ return 'Syntax error';
+ case JSON_ERROR_UTF8:
+ return 'Malformed UTF-8 characters, possibly incorrectly encoded';
+ default:
+ return 'An error occured when parsing a JSON string';
+ }
+ }
+}
diff --git a/library/Director/RestApi/RestApiClient.php b/library/Director/RestApi/RestApiClient.php
new file mode 100644
index 0000000..2ebc4d4
--- /dev/null
+++ b/library/Director/RestApi/RestApiClient.php
@@ -0,0 +1,311 @@
+<?php
+
+namespace Icinga\Module\Director\RestApi;
+
+use Icinga\Module\Director\Core\Json;
+use InvalidArgumentException;
+use RuntimeException;
+
+class RestApiClient
+{
+ /** @var resource */
+ private $curl;
+
+ /** @var string HTTP or HTTPS */
+ private $scheme;
+
+ /** @var string */
+ private $host;
+
+ /** @var int */
+ private $port;
+
+ /** @var string */
+ private $user;
+
+ /** @var string */
+ private $pass;
+
+ /** @var bool */
+ private $verifySslPeer = true;
+
+ /** @var bool */
+ private $verifySslHost = true;
+
+ /** @var string */
+ private $proxy;
+
+ /** @var string */
+ private $proxyType;
+
+ /** @var string */
+ private $proxyUser;
+
+ /** @var string */
+ private $proxyPass;
+
+ /** @var array */
+ private $proxyTypes = [
+ 'HTTP' => CURLPROXY_HTTP,
+ 'SOCKS5' => CURLPROXY_SOCKS5,
+ ];
+
+ /**
+ * RestApiClient constructor.
+ *
+ * Please note that only the host is required, user and pass are optional
+ *
+ * @param string $host
+ * @param string|null $user
+ * @param string|null $pass
+ */
+ public function __construct($host, $user = null, $pass = null)
+ {
+ $this->host = $host;
+ $this->user = $user;
+ $this->pass = $pass;
+ }
+
+ /**
+ * Use a proxy
+ *
+ * @param $url
+ * @param string $type Either HTTP or SOCKS5
+ * @return $this
+ */
+ public function setProxy($url, $type = 'HTTP')
+ {
+ $this->proxy = $url;
+ if (\is_int($type)) {
+ $this->proxyType = $type;
+ } else {
+ $this->proxyType = $this->proxyTypes[$type];
+ }
+ return $this;
+ }
+
+ /**
+ * @param string $user
+ * @param string $pass
+ * @return $this
+ */
+ public function setProxyAuth($user, $pass)
+ {
+ $this->proxyUser = $user;
+ $this->proxyPass = $pass;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getScheme()
+ {
+ if ($this->scheme === null) {
+ return 'HTTPS';
+ } else {
+ return $this->scheme;
+ }
+ }
+
+ public function setScheme($scheme)
+ {
+ $scheme = \strtoupper($scheme);
+ if (! \in_array($scheme, ['HTTP', 'HTTPS'])) {
+ throw new InvalidArgumentException("Got invalid scheme: $scheme");
+ }
+
+ $this->scheme = $scheme;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPort()
+ {
+ if ($this->port === null) {
+ return $this->getScheme() === 'HTTPS' ? 443 : 80;
+ } else {
+ return $this->port;
+ }
+ }
+
+ /**
+ * @param int|string|null $port
+ * @return $this
+ */
+ public function setPort($port)
+ {
+ if ($port === null) {
+ $this->port = null;
+ return $this;
+ }
+ $port = (int) ($port);
+ if ($port < 1 || $port > 65535) {
+ throw new InvalidArgumentException("Got invalid port: $port");
+ }
+
+ $this->port = $port;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isDefaultPort()
+ {
+ return $this->port === null
+ || $this->getScheme() === 'HTTPS' && $this->getPort() === 443
+ || $this->getScheme() === 'HTTP' && $this->getPort() === 80;
+ }
+
+ /**
+ * @param bool $disable
+ * @return $this
+ */
+ public function disableSslPeerVerification($disable = true)
+ {
+ $this->verifySslPeer = ! $disable;
+ return $this;
+ }
+
+ /**
+ * @param bool $disable
+ * @return $this
+ */
+ public function disableSslHostVerification($disable = true)
+ {
+ $this->verifySslHost = ! $disable;
+ return $this;
+ }
+
+ /**
+ * @param string $url
+ * @return string
+ */
+ public function url($url)
+ {
+ return \sprintf(
+ '%s://%s%s/%s',
+ \strtolower($this->getScheme()),
+ $this->host,
+ $this->isDefaultPort() ? '' : ':' . $this->getPort(),
+ ltrim($url, '/')
+ );
+ }
+
+ /**
+ * @param string $url
+ * @param mixed $body
+ * @param array $headers
+ * @return mixed
+ */
+ public function get($url, $body = null, $headers = [])
+ {
+ return $this->request('get', $url, $body, $headers);
+ }
+
+ /**
+ * @param $url
+ * @param null $body
+ * @param array $headers
+ * @return mixed
+ */
+ public function post($url, $body = null, $headers = [])
+ {
+ return $this->request('post', $url, Json::encode($body), $headers);
+ }
+
+ /**
+ * @param $method
+ * @param $url
+ * @param null $body
+ * @param array $headers
+ * @return mixed
+ */
+ protected function request($method, $url, $body = null, $headers = [])
+ {
+ $sendHeaders = ['Host: ' . $this->host];
+ foreach ($headers as $key => $val) {
+ $sendHeaders[] = "$key: $val";
+ }
+
+ if (! \in_array('Accept', $headers)) {
+ $sendHeaders[] = 'Accept: application/json';
+ }
+
+ $url = $this->url($url);
+ $opts = [
+ CURLOPT_URL => $url,
+ CURLOPT_HTTPHEADER => $sendHeaders,
+ CURLOPT_CUSTOMREQUEST => \strtoupper($method),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CONNECTTIMEOUT => 5,
+ ];
+
+ if ($this->getScheme() === 'HTTPS') {
+ $opts[CURLOPT_SSL_VERIFYPEER] = $this->verifySslPeer;
+ $opts[CURLOPT_SSL_VERIFYHOST] = $this->verifySslHost ? 2 : 0;
+ }
+
+ if ($this->user !== null) {
+ $opts[CURLOPT_USERPWD] = \sprintf('%s:%s', $this->user, $this->pass);
+ }
+
+ if ($this->proxy) {
+ $opts[CURLOPT_PROXY] = $this->proxy;
+ $opts[CURLOPT_PROXYTYPE] = $this->proxyType;
+
+ if ($this->proxyUser) {
+ $opts['CURLOPT_PROXYUSERPWD'] = \sprintf(
+ '%s:%s',
+ $this->proxyUser,
+ $this->proxyPass
+ );
+ }
+ }
+
+ if ($body !== null) {
+ $opts[CURLOPT_POSTFIELDS] = $body;
+ }
+
+ $curl = $this->curl();
+ \curl_setopt_array($curl, $opts);
+
+ $res = \curl_exec($curl);
+ if ($res === false) {
+ throw new RuntimeException('CURL ERROR: ' . \curl_error($curl));
+ }
+
+ $statusCode = \curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ if ($statusCode === 401) {
+ throw new RuntimeException(
+ 'Unable to authenticate, please check your REST API credentials'
+ );
+ }
+
+ if ($statusCode >= 400) {
+ throw new RuntimeException(
+ "Got $statusCode: " . \var_export($res, 1)
+ );
+ }
+
+ return Json::decode($res);
+ }
+
+ /**
+ * @return resource
+ */
+ protected function curl()
+ {
+ if ($this->curl === null) {
+ $this->curl = \curl_init(\sprintf('https://%s:%d', $this->host, $this->port));
+ if (! $this->curl) {
+ throw new RuntimeException('CURL INIT ERROR: ' . \curl_error($this->curl));
+ }
+ }
+
+ return $this->curl;
+ }
+}
diff --git a/library/Director/RestApi/RestApiParams.php b/library/Director/RestApi/RestApiParams.php
new file mode 100644
index 0000000..c237ac5
--- /dev/null
+++ b/library/Director/RestApi/RestApiParams.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Icinga\Module\Director\RestApi;
+
+use Icinga\Module\Director\Data\Exporter;
+use Icinga\Web\Request;
+use InvalidArgumentException;
+
+class RestApiParams
+{
+ public static function applyParamsToExporter(Exporter $exporter, Request $request, $shortObjectType = null)
+ {
+ $params = $request->getUrl()->getParams();
+ $resolved = (bool) $params->get('resolved', false);
+ $withNull = $params->shift('withNull');
+ if ($params->get('withServices')) {
+ if ($shortObjectType !== 'host') {
+ throw new InvalidArgumentException('withServices is available for Hosts only');
+ }
+ $exporter->enableHostServices();
+ }
+ $properties = $params->shift('properties');
+ if ($properties !== null && strlen($properties)) {
+ $exporter->filterProperties(preg_split('/\s*,\s*/', $properties, -1, PREG_SPLIT_NO_EMPTY));
+ }
+ $exporter->resolveObjects($resolved);
+ $exporter->showDefaults($withNull);
+ }
+}
diff --git a/library/Director/Restriction/FilterByNameRestriction.php b/library/Director/Restriction/FilterByNameRestriction.php
new file mode 100644
index 0000000..8c3b256
--- /dev/null
+++ b/library/Director/Restriction/FilterByNameRestriction.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Director\Restriction;
+
+use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Zend_Db_Select as ZfSelect;
+
+class FilterByNameRestriction extends ObjectRestriction
+{
+ protected $type;
+
+ /** @var Filter */
+ protected $filter;
+
+ public function __construct(Db $connection, Auth $auth, $type)
+ {
+ parent::__construct($connection, $auth);
+ $this->setType($type);
+ }
+
+ protected function setType($type)
+ {
+ $this->type = $type;
+ $this->setNameForType($type);
+ }
+
+ protected function setNameForType($type)
+ {
+ $this->name = "director/${type}/filter-by-name";
+ }
+
+ public function allows(IcingaObject $object)
+ {
+ if (! $this->isRestricted()) {
+ return true;
+ }
+
+ return $this->getFilter()->matches([
+ (object) ['object_name' => $object->getObjectName()]
+ ]);
+ }
+
+ public function getFilter()
+ {
+ if ($this->filter === null) {
+ $this->filter = MatchingFilter::forUser(
+ $this->auth->getUser(),
+ $this->name,
+ 'object_name'
+ );
+ }
+
+ return $this->filter;
+ }
+
+ protected function filterQuery(ZfSelect $query, $tableAlias = 'o')
+ {
+ FilterRenderer::applyToQuery($this->getFilter(), $query);
+ }
+}
diff --git a/library/Director/Restriction/HostgroupRestriction.php b/library/Director/Restriction/HostgroupRestriction.php
new file mode 100644
index 0000000..1a6792b
--- /dev/null
+++ b/library/Director/Restriction/HostgroupRestriction.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace Icinga\Module\Director\Restriction;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Db\IcingaObjectFilterHelper;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaHostGroup;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Zend_Db_Select as ZfSelect;
+
+class HostgroupRestriction extends ObjectRestriction
+{
+ protected $name = 'director/filter/hostgroups';
+
+ public function allows(IcingaObject $object)
+ {
+ if ($object instanceof IcingaHost) {
+ return $this->allowsHost($object);
+ } elseif ($object instanceof IcingaHostGroup) {
+ return $this->allowsHostGroup($object);
+ } else {
+ return $this;
+ }
+ }
+
+ protected function filterQuery(ZfSelect $query, $tableAlias = 'o')
+ {
+ $table = $this->getQueryTableByAlias($query, $tableAlias);
+ switch ($table) {
+ case 'icinga_host':
+ $this->filterHostsQuery($query, $tableAlias);
+ break;
+ case 'icinga_service':
+ // TODO: Alias is hardcoded
+ $this->filterHostsQuery($query, 'h');
+ break;
+ case 'icinga_hostgroup':
+ $this->filterHostGroupsQuery($query, $tableAlias);
+ break;
+ // Hint: other tables are ignored, so please take care!
+ }
+
+ return $query;
+ }
+
+ /**
+ * Whether access to the given host is allowed
+ *
+ * @param IcingaHost $host
+ * @return bool
+ */
+ public function allowsHost(IcingaHost $host)
+ {
+ if (! $this->isRestricted()) {
+ return true;
+ }
+
+ // Hint: branched hosts have no id
+ if (! $host->hasBeenLoadedFromDb() || $host->hasModifiedGroups() || $host->get('id') === null) {
+ foreach ($this->listRestrictedHostgroups() as $group) {
+ if ($host->hasGroup($group)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ $query = $this->db->select()->from(
+ ['o' => 'icinga_host'],
+ ['id']
+ )->where('o.id = ?', $host->id);
+
+ $this->filterHostsQuery($query);
+ return (int) $this->db->fetchOne($query) === (int) $host->get('id');
+ }
+
+ /**
+ * Whether access to the given hostgroup is allowed
+ *
+ * @param IcingaHostGroup $hostgroup
+ * @return bool
+ */
+ public function allowsHostGroup(IcingaHostGroup $hostgroup)
+ {
+ if (! $this->isRestricted()) {
+ return true;
+ }
+
+ $query = $this->db->select()->from(
+ ['h' => 'icinga_hostgroup'],
+ ['id']
+ )->where('id = ?', $hostgroup->id);
+
+ $this->filterHostGroupsQuery($query);
+ return (int) $this->db->fetchOne($query) === (int) $hostgroup->get('id');
+ }
+
+ /**
+ * Apply the restriction to the given Hosts Query
+ *
+ * We assume that the query wants to fetch hosts and that therefore the
+ * icinga_host table already exists in the given query, using the $tableAlias
+ * alias.
+ *
+ * @param ZfSelect $query
+ * @param string $tableAlias
+ */
+ public function filterHostsQuery(ZfSelect $query, $tableAlias = 'o')
+ {
+ if (! $this->isRestricted()) {
+ return;
+ }
+
+ IcingaObjectFilterHelper::filterByResolvedHostgroups(
+ $query,
+ 'host',
+ $this->listRestrictedHostgroups(),
+ $tableAlias
+ );
+ }
+
+ /**
+ * Apply the restriction to the given Hosts Query
+ *
+ * We assume that the query wants to fetch hosts and that therefore the
+ * icinga_host table already exists in the given query, using the $tableAlias
+ * alias.
+ *
+ * @param ZfSelect $query
+ * @param string $tableAlias
+ */
+ protected function filterHostGroupsQuery(ZfSelect $query, $tableAlias = 'o')
+ {
+ if (! $this->isRestricted()) {
+ return;
+ }
+ $groups = $this->listRestrictedHostgroups();
+
+ if (empty($groups)) {
+ $query->where('(1 = 0)');
+ } else {
+ $query->where("${tableAlias}.object_name IN (?)", $groups);
+ }
+ }
+
+ /**
+ * Give a list of allowed Hostgroups
+ *
+ * When not restricted, null is returned. This might eventually also give
+ * an empty list, and therefore not allow any access at all
+ *
+ * @return array|null
+ */
+ protected function listRestrictedHostgroups()
+ {
+ if ($restrictions = $this->auth->getRestrictions($this->getName())) {
+ $groups = array();
+ foreach ($restrictions as $restriction) {
+ foreach ($this->gracefullySplitOnComma($restriction) as $group) {
+ $groups[$group] = $group;
+ }
+ }
+
+ return array_keys($groups);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/library/Director/Restriction/MatchingFilter.php b/library/Director/Restriction/MatchingFilter.php
new file mode 100644
index 0000000..162840c
--- /dev/null
+++ b/library/Director/Restriction/MatchingFilter.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Restriction;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\User;
+
+class MatchingFilter
+{
+ public static function forPatterns(array $restrictions, $columnName)
+ {
+ $filters = [];
+ foreach ($restrictions as $restriction) {
+ foreach (preg_split('/\|/', $restriction) as $pattern) {
+ $filters[] = Filter::expression(
+ $columnName,
+ '=',
+ $pattern
+ );
+ }
+ }
+
+ if (count($filters) === 1) {
+ return $filters[0];
+ } else {
+ return Filter::matchAny($filters);
+ }
+ }
+
+ public static function forUser(
+ User $user,
+ $restrictionName,
+ $columnName
+ ) {
+ return static::forPatterns(
+ $user->getRestrictions($restrictionName),
+ $columnName
+ );
+ }
+}
diff --git a/library/Director/Restriction/ObjectRestriction.php b/library/Director/Restriction/ObjectRestriction.php
new file mode 100644
index 0000000..9161ebb
--- /dev/null
+++ b/library/Director/Restriction/ObjectRestriction.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Icinga\Module\Director\Restriction;
+
+use Icinga\Authentication\Auth;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Zend_Db_Select as ZfSelect;
+
+abstract class ObjectRestriction
+{
+ /** @var string */
+ protected $name;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var Auth */
+ protected $auth;
+
+ public function __construct(Db $connection, Auth $auth)
+ {
+ $this->db = $connection->getDbAdapter();
+ $this->auth = $auth;
+ }
+
+ abstract public function allows(IcingaObject $object);
+
+ /**
+ * Apply the restriction to the given Hosts Query
+ *
+ * We assume that the query wants to fetch hosts and that therefore the
+ * icinga_host table already exists in the given query, using the $tableAlias
+ * alias.
+ *
+ * @param ZfSelect $query
+ * @param string $tableAlias
+ */
+ abstract protected function filterQuery(ZfSelect $query, $tableAlias = 'o');
+
+ public function applyToQuery(ZfSelect $query, $tableAlias = 'o')
+ {
+ if ($this->isRestricted()) {
+ $this->filterQuery($query, $tableAlias);
+ }
+
+ return $query;
+ }
+
+ public function getName()
+ {
+ if ($this->name === null) {
+ throw new ProgrammingError('ObjectRestriction has no name');
+ }
+
+ return $this->name;
+ }
+
+ public function isRestricted()
+ {
+ $restrictions = $this->auth->getRestrictions($this->getName());
+ return ! empty($restrictions);
+ }
+
+ protected function getQueryTableByAlias(ZfSelect $query, $tableAlias)
+ {
+ $from = $query->getPart(ZfSelect::FROM);
+ if (! array_key_exists($tableAlias, $from)) {
+ throw new ProgrammingError(
+ 'Cannot restrict query with alias "%s", got %s',
+ $tableAlias,
+ json_encode($from)
+ );
+ }
+
+ return $from[$tableAlias]['tableName'];
+ }
+
+ protected function gracefullySplitOnComma($string)
+ {
+ return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY);
+ }
+}
diff --git a/library/Director/Settings.php b/library/Director/Settings.php
new file mode 100644
index 0000000..d3e0987
--- /dev/null
+++ b/library/Director/Settings.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+class Settings
+{
+ protected $connection;
+
+ protected $db;
+
+ protected $cache;
+
+ protected $defaults = [
+ 'default_global_zone' => 'director-global',
+ 'icinga_package_name' => 'director',
+ 'config_format' => 'v2',
+ 'override_services_varname' => '_override_servicevars',
+ 'override_services_templatename' => 'host var overrides (Director)',
+ 'disable_all_jobs' => 'n', // 'y'
+ 'enable_audit_log' => 'n',
+ 'deployment_mode_v1' => 'active-passive',
+ 'deployment_path_v1' => null,
+ 'activation_script_v1' => null,
+ 'self-service/agent_name' => 'fqdn',
+ 'self-service/transform_hostname' => '0',
+ 'self-service/resolve_parent_host' => '0',
+ 'self-service/global_zones' => ['director-global'],
+ 'ignore_bug7530' => 'n',
+ 'feature_custom_endpoint' => 'n',
+ // 'experimental_features' => null, // 'allow'
+ // 'master_zone' => null,
+ ];
+
+ protected $jsonEncode = [
+ 'self-service/global_zones',
+ 'self-service/installer_hashes',
+ ];
+
+ public function __construct(Db $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ }
+
+ /**
+ * @return Db
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function getDb()
+ {
+ return $this->db;
+ }
+
+ /**
+ * @param $key
+ * @param null $default
+ * @return mixed|null
+ */
+ public function get($key, $default = null)
+ {
+ if (null === ($value = $this->getStoredValue($key, $default))) {
+ return $this->getDefaultValue($key);
+ } else {
+ return $value;
+ }
+ }
+
+ /**
+ * @param $key
+ * @param null $default
+ * @return mixed|null
+ */
+ public function getStoredValue($key, $default = null)
+ {
+ if (null === ($value = $this->getSetting($key))) {
+ return $default;
+ } else {
+ if (in_array($key, $this->jsonEncode)) {
+ $value = json_decode($value);
+ }
+ return $value;
+ }
+ }
+
+ public function getDefaultValue($key)
+ {
+ if (array_key_exists($key, $this->defaults)) {
+ return $this->defaults[$key];
+ } else {
+ return null;
+ }
+ }
+
+ public function getStoredOrDefaultValue($key)
+ {
+ $value = $this->getStoredValue($key);
+ if ($value === null) {
+ return $this->getDefaultValue($key);
+ } else {
+ return $value;
+ }
+ }
+
+ /**
+ * @param $name
+ * @param $value
+ * @return $this
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function set($name, $value)
+ {
+ $db = $this->db;
+
+ if ($value === null) {
+ $db->delete(
+ 'director_setting',
+ $db->quoteInto('setting_name = ?', $name)
+ );
+
+ unset($this->cache[$name]);
+
+ return $this;
+ }
+
+ if (in_array($name, $this->jsonEncode)) {
+ $value = json_encode(array_values($value));
+ }
+
+ if ($this->getSetting($name) === $value) {
+ return $this;
+ }
+
+ $updated = $db->update(
+ 'director_setting',
+ array('setting_value' => $value),
+ $db->quoteInto('setting_name = ?', $name)
+ );
+
+ if ($updated === 0) {
+ $db->insert(
+ 'director_setting',
+ array(
+ 'setting_name' => $name,
+ 'setting_value' => $value,
+ )
+ );
+ }
+
+ if ($this->cache !== null) {
+ $this->cache[$name] = $value;
+ }
+
+ return $this;
+ }
+
+ public function clearCache()
+ {
+ $this->cache = null;
+ return $this;
+ }
+
+ protected function getSetting($name, $default = null)
+ {
+ if ($this->cache === null) {
+ $this->refreshCache();
+ }
+
+ if (array_key_exists($name, $this->cache)) {
+ return $this->cache[$name];
+ }
+
+ return $default;
+ }
+
+ protected function refreshCache()
+ {
+ $db = $this->db;
+
+ $query = $db->select()->from(
+ array('s' => 'director_setting'),
+ array('setting_name', 'setting_value')
+ );
+
+ $this->cache = (array) $db->fetchPairs($query);
+ }
+
+ /**
+ * @param $key
+ * @return mixed|null
+ */
+ public function __get($key)
+ {
+ return $this->get($key);
+ }
+
+ /**
+ * @param $key
+ * @param $value
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function __set($key, $value)
+ {
+ $this->set($key, $value);
+ }
+
+ public function __destruct()
+ {
+ $this->clearCache();
+ unset($this->db);
+ unset($this->connection);
+ }
+}
diff --git a/library/Director/StartupLogRenderer.php b/library/Director/StartupLogRenderer.php
new file mode 100644
index 0000000..bc7b3ea
--- /dev/null
+++ b/library/Director/StartupLogRenderer.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+use ipl\Html\Html;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\ValidHtml;
+
+class StartupLogRenderer implements ValidHtml
+{
+ /** @var DirectorDeploymentLog */
+ protected $deployment;
+
+ public function __construct(DirectorDeploymentLog $deployment)
+ {
+ $this->deployment = $deployment;
+ }
+
+ public function render()
+ {
+ $deployment = $this->deployment;
+ $log = Html::escape($deployment->get('startup_log'));
+ $lines = array();
+ $severity = 'information';
+ $sevPattern = '/^(debug|notice|information|warning|critical)\/(\w+)/';
+ $settings = new Settings($this->deployment->getConnection());
+ $package = $settings->get('icinga_package_name');
+ $pathPattern = '~(/[\w/]+/api/packages/' . $package . '/[^/]+/)';
+ $filePatternHint = $pathPattern . '([^:]+\.conf)(: (\d+))~';
+ $filePatternDetail = $pathPattern . '([^:]+\.conf)(\((\d+)\))~';
+ $markPattern = null;
+ // len [stage] + 1
+ $markReplace = ' ^';
+
+ foreach (preg_split('/\n/', $log) as $line) {
+ if (preg_match('/^\[([\d\s\:\+\-]+)\]\s/', $line, $m)) {
+ $time = $m[1];
+ // TODO: we might use new DateTime($time) and show a special "timeAgo"
+ // format - but for now this should suffice.
+ $line = substr($line, strpos($line, ']') + 2);
+ } else {
+ $time = null;
+ }
+
+ if (preg_match($sevPattern, $line, $m)) {
+ $severity = $m[1];
+ $line = preg_replace(
+ $sevPattern,
+ '<span class="loglevel \1">\1</span>/<span class="application">\2</span>',
+ $line
+ );
+ }
+
+ if ($markPattern !== null) {
+ $line = preg_replace($markPattern, $markReplace, $line);
+ }
+ $line = preg_replace('/([\^]{2,})/', '<span class="error-hint">\1</span>', $line);
+ $markPattern = null;
+
+ $self = $this;
+ if (preg_match($filePatternHint, $line, $m)) {
+ $line = preg_replace_callback(
+ $filePatternHint,
+ function ($matches) use ($severity, $self) {
+ return $self->logLink($matches, $severity);
+ },
+ $line
+ );
+ $line = preg_replace('/\(in/', "\n (in", $line);
+ $line = preg_replace('/\), new declaration/', "),\n new declaration", $line);
+ } elseif (preg_match($filePatternDetail, $line, $m)) {
+ $markIndent = strlen($m[1]);
+ $markPattern = '/\s{' . $markIndent . '}\^/';
+
+ $line = preg_replace_callback(
+ $filePatternDetail,
+ function ($matches) use ($severity, $self) {
+ return $self->logLink($matches, $severity);
+ },
+ $line
+ );
+ }
+
+ if ($time === null) {
+ $lines[] .= $line;
+ } else {
+ $lines[] .= "[$time] $line";
+ }
+ }
+ return implode("\n", $lines);
+ }
+
+ protected function logLink($match, $severity)
+ {
+ $stageDir = $match[1];
+ $filename = $match[2];
+ $suffix = $match[3];
+ if (preg_match('/(\d+).*/', $suffix, $m)) {
+ $lineNumber = $m[1];
+ } else {
+ $lineNumber = null;
+ }
+
+ $deployment = $this->deployment;
+ $params = array(
+ 'config_checksum' => $deployment->getConfigHexChecksum(),
+ 'deployment_id' => $deployment->get('id'),
+ 'file_path' => $filename,
+ 'backTo' => 'deployment'
+ );
+ if ($lineNumber !== null) {
+ $params['highlight'] = $lineNumber;
+ $params['highlightSeverity'] = $severity;
+ }
+
+ return Link::create(
+ '[stage]/' . $filename,
+ 'director/config/file',
+ $params,
+ [
+ 'title' => $stageDir . $filename
+ ]
+ ) . $suffix;
+ }
+}
diff --git a/library/Director/Test/BaseTestCase.php b/library/Director/Test/BaseTestCase.php
new file mode 100644
index 0000000..611805b
--- /dev/null
+++ b/library/Director/Test/BaseTestCase.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Config;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Migrations;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaZone;
+use PHPUnit_Framework_TestCase;
+
+abstract class BaseTestCase extends PHPUnit_Framework_TestCase
+{
+ private static $app;
+
+ /** @var Db */
+ private static $db;
+
+ public function setUp()
+ {
+ $this->app();
+ }
+
+ protected function skipForMissingDb()
+ {
+ if ($this->hasDb()) {
+ return false;
+ }
+
+ $this->markTestSkipped('Test db resource has not been configured');
+
+ return true;
+ }
+
+ protected function hasDb()
+ {
+ return $this->getDbResourceName() !== null;
+ }
+
+ protected static function getDbResourceName()
+ {
+ if (array_key_exists('DIRECTOR_TESTDB_RES', $_SERVER)) {
+ return $_SERVER['DIRECTOR_TESTDB_RES'];
+ } else {
+ return Config::module('director')->get('testing', 'db_resource');
+ }
+ }
+
+ /**
+ * @return Db
+ * @throws ConfigurationError
+ */
+ protected static function getDb()
+ {
+ if (self::$db === null) {
+ $resourceName = self::getDbResourceName();
+ if (! $resourceName) {
+ throw new ConfigurationError(
+ 'Could not run DB-based tests, please configure a testing db resource'
+ );
+ }
+ $dbConfig = ResourceFactory::getResourceConfig($resourceName);
+ if (array_key_exists('DIRECTOR_TESTDB', $_SERVER)) {
+ $dbConfig->dbname = $_SERVER['DIRECTOR_TESTDB'];
+ }
+ if (array_key_exists('DIRECTOR_TESTDB_HOST', $_SERVER)) {
+ $dbConfig->host = $_SERVER['DIRECTOR_TESTDB_HOST'];
+ }
+ if (array_key_exists('DIRECTOR_TESTDB_USER', $_SERVER)) {
+ $dbConfig->username = $_SERVER['DIRECTOR_TESTDB_USER'];
+ }
+ if (array_key_exists('DIRECTOR_TESTDB_PASSWORD', $_SERVER)) {
+ $dbConfig->password = $_SERVER['DIRECTOR_TESTDB_PASSWORD'];
+ }
+ self::$db = new Db($dbConfig);
+ $migrations = new Migrations(self::$db);
+ $migrations->applyPendingMigrations();
+ IcingaZone::create([
+ 'object_name' => 'director-global',
+ 'object_type' => 'external_object',
+ 'is_global' => 'y'
+ ])->store(self::$db);
+ }
+
+ return self::$db;
+ }
+
+ protected function newObject($type, $name, $properties = array())
+ {
+ if (! array_key_exists('object_type', $properties)) {
+ $properties['object_type'] = 'object';
+ }
+ $properties['object_name'] = $name;
+
+ return IcingaObject::createByType($type, $properties, $this->getDb());
+ }
+
+ protected function app()
+ {
+ if (self::$app === null) {
+ self::$app = Icinga::app();
+ }
+
+ return self::$app;
+ }
+
+ /**
+ * Call a protected function for a class during testing
+ *
+ * @param $obj
+ * @param $name
+ * @param array $args
+ *
+ * @return mixed
+ * @throws \ReflectionException
+ */
+ public static function callMethod($obj, $name, array $args)
+ {
+ $class = new \ReflectionClass($obj);
+ $method = $class->getMethod($name);
+ $method->setAccessible(true);
+ return $method->invokeArgs($obj, $args);
+ }
+}
diff --git a/library/Director/Test/Bootstrap.php b/library/Director/Test/Bootstrap.php
new file mode 100644
index 0000000..56bd85a
--- /dev/null
+++ b/library/Director/Test/Bootstrap.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+use Icinga\Application\Cli;
+
+class Bootstrap
+{
+ public static function cli($basedir = null)
+ {
+ error_reporting(E_ALL | E_STRICT);
+ if ($basedir === null) {
+ $basedir = dirname(dirname(dirname(__DIR__)));
+ }
+ $testsDir = $basedir . '/test';
+ require_once 'Icinga/Application/Cli.php';
+
+ if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) {
+ $configDir = $_SERVER['ICINGAWEB_CONFIGDIR'];
+ } else {
+ $configDir = $testsDir . '/config';
+ }
+
+ Cli::start($testsDir, $configDir)
+ ->getModuleManager()
+ ->loadModule('director', $basedir);
+ }
+}
diff --git a/library/Director/Test/IcingaObjectTestCase.php b/library/Director/Test/IcingaObjectTestCase.php
new file mode 100644
index 0000000..a37fced
--- /dev/null
+++ b/library/Director/Test/IcingaObjectTestCase.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+
+/**
+ * Icinga Object test helper class
+ */
+abstract class IcingaObjectTestCase extends BaseTestCase
+{
+ protected $table;
+ protected $testObjectName = '___TEST___';
+
+ /** @var IcingaObject */
+ protected $subject = null;
+
+ protected $createdObjects = array();
+
+ /**
+ * Creates a fresh object to play with and prepares for tearDown()
+ *
+ * @param string $type table to load from
+ * @param string $object_name of the object
+ * @param array $properties
+ * @param bool $storeIt
+ *
+ * @return IcingaObject
+ */
+ protected function createObject($object_name, $type = null, $properties = array(), $storeIt = true)
+ {
+ if ($type === null) {
+ $type = $this->table;
+ }
+ $properties['object_name'] = '___TEST___' . $type . '_' . $object_name;
+ $obj = IcingaObject::createByType($type, $properties, $this->getDb());
+
+ if ($storeIt === true) {
+ $obj->store();
+ $this->prepareObjectTearDown($obj);
+ }
+
+ return $obj;
+ }
+
+ /**
+ * Helper method for loading an object
+ *
+ * @param string $name
+ * @param null $type
+ * @return IcingaObject
+ */
+ protected function loadObject($name, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->table;
+ }
+ $realName = '___TEST___' . $type . '_' . $name;
+ return IcingaObject::loadByType($type, $realName, $this->getDb());
+ }
+
+ /**
+ * Store the object in a list for deletion on tearDown()
+ *
+ * @param IcingaObject $object
+ *
+ * @return $this
+ */
+ protected function prepareObjectTearDown(IcingaObject $object)
+ {
+ $this->assertTrue($object->hasBeenLoadedFromDb());
+ $this->createdObjects[] = $object;
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function tearDown()
+ {
+ if ($this->hasDb()) {
+ /** @var IcingaObject $object */
+ foreach (array_reverse($this->createdObjects) as $object) {
+ $object->delete();
+ }
+
+ if ($this->subject !== null) {
+ $this->subject->delete();
+ }
+ }
+ }
+}
diff --git a/library/Director/Test/ImportSourceDummy.php b/library/Director/Test/ImportSourceDummy.php
new file mode 100644
index 0000000..4ac1d09
--- /dev/null
+++ b/library/Director/Test/ImportSourceDummy.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+use Icinga\Module\Director\Hook\ImportSourceHook;
+
+class ImportSourceDummy extends ImportSourceHook
+{
+ protected static $rows = array();
+
+ /**
+ * Returns an array containing importable objects
+ *
+ * @return array
+ */
+ public function fetchData()
+ {
+ return self::$rows;
+ }
+
+ /**
+ * Returns a list of all available columns
+ *
+ * @return array
+ */
+ public function listColumns()
+ {
+ $keys = array();
+ foreach (self::$rows as $row) {
+ $keys = array_merge($keys, array_keys($row));
+ }
+ return $keys;
+ }
+
+ public static function clearRows()
+ {
+ self::$rows = array();
+ }
+
+ public static function setRows($rows)
+ {
+ static::clearRows();
+ foreach ($rows as $row) {
+ static::addRow($row);
+ }
+ }
+
+ public static function addRow($row)
+ {
+ self::$rows[] = (object) $row;
+ }
+}
diff --git a/library/Director/Test/SyncTest.php b/library/Director/Test/SyncTest.php
new file mode 100644
index 0000000..7614ff9
--- /dev/null
+++ b/library/Director/Test/SyncTest.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\Import\Sync;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\SyncProperty;
+use Icinga\Module\Director\Objects\SyncRule;
+
+abstract class SyncTest extends BaseTestCase
+{
+ protected $objectType;
+
+ protected $keyColumn;
+
+ /** @var ImportSource */
+ protected $source;
+
+ /** @var SyncRule */
+ protected $rule;
+
+ /** @var SyncProperty[] */
+ protected $properties = array();
+
+ /** @var Sync */
+ protected $sync;
+
+ public function setUp()
+ {
+ $this->source = ImportSource::create(array(
+ 'source_name' => 'testimport',
+ 'provider_class' => 'Icinga\\Module\\Director\\Test\\ImportSourceDummy',
+ 'key_column' => $this->keyColumn,
+ ));
+ $this->source->store($this->getDb());
+
+ $this->rule = SyncRule::create(array(
+ 'rule_name' => 'testrule',
+ 'object_type' => $this->objectType,
+ 'update_policy' => 'merge',
+ 'purge_existing' => 'n'
+ ));
+ $this->rule->store($this->getDb());
+
+ $this->sync = new Sync($this->rule);
+ }
+
+ public function tearDown()
+ {
+ // properties should be deleted automatically
+ if ($this->rule !== null && $this->rule->hasBeenLoadedFromDb()) {
+ $this->rule->delete();
+ }
+
+ if ($this->source !== null && $this->source->hasBeenLoadedFromDb()) {
+ $this->source->delete();
+ }
+
+ // find objects created by this class and delete them
+ $db = $this->getDb();
+ $dummy = IcingaObject::createByType($this->objectType, array(), $db);
+ $query = $db->getDbAdapter()->select()
+ ->from($dummy->getTableName())
+ ->where('object_name LIKE ?', 'SYNCTEST_%');
+
+ /** @var IcingaObject $object */
+ foreach (IcingaObject::loadAllByType($this->objectType, $db, $query) as $object) {
+ $object->delete();
+ }
+
+ // make sure cache is clean for other tests
+ PrefetchCache::forget();
+ DbObject::clearAllPrefetchCaches();
+ }
+
+ /**
+ * @param array $rows
+ *
+ * @throws IcingaException
+ */
+ protected function runImport($rows)
+ {
+ ImportSourceDummy::setRows($rows);
+ $this->source->runImport();
+ if ($this->source->get('import_state') !== 'in-sync') {
+ throw new IcingaException('Import failed: %s', $this->source->get('last_error_message'));
+ }
+ }
+
+ protected function setUpProperty($properties = array())
+ {
+ $properties = array_merge(array(
+ 'rule_id' => $this->rule->id,
+ 'source_id' => $this->source->id,
+ 'merge_policy' => 'override',
+ ), $properties);
+
+ $this->properties[] = $property = SyncProperty::create($properties);
+ $property->store($this->getDb());
+ }
+}
diff --git a/library/Director/Test/TestProcess.php b/library/Director/Test/TestProcess.php
new file mode 100644
index 0000000..b2399b7
--- /dev/null
+++ b/library/Director/Test/TestProcess.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+use Closure;
+
+class TestProcess
+{
+ protected $command;
+
+ protected $identifier;
+
+ protected $exitCode;
+
+ protected $output;
+
+ protected $onSuccess;
+
+ protected $onFailure;
+
+ protected $expectedExitCode = 0;
+
+ public function __construct($command, $identifier = null)
+ {
+ $this->command = $command;
+ $this->identifier = $identifier;
+ }
+
+ public function getIdentifier()
+ {
+ return $this->identifier;
+ }
+
+ public function expectExitCode($code)
+ {
+ $this->expectedExitCode = $code;
+ return $this;
+ }
+
+ public function onSuccess($func)
+ {
+ $this->onSuccess = $this->makeClosure($func);
+ return $this;
+ }
+
+ public function onFailure($func)
+ {
+ $this->onSuccess = $this->makeClosure($func);
+ return $this;
+ }
+
+ protected function makeClosure($func)
+ {
+ if ($func instanceof Closure) {
+ return $func;
+ }
+
+ if (is_array($func)) {
+ return function ($process) use ($func) {
+ return $func[0]->{$func[1]}($process);
+ };
+ }
+ }
+
+ public function onFailureThrow($message, $class = 'Exception')
+ {
+ return $this->onFailure(function () use ($message, $class) {
+ throw new $class($message);
+ });
+ }
+
+ public function run()
+ {
+ exec($this->command, $this->output, $this->exitCode);
+
+ if ($this->succeeded()) {
+ $this->triggerSuccess();
+ } else {
+ $this->triggerFailure();
+ }
+ }
+
+ public function succeeded()
+ {
+ return $this->exitCode === $this->expectedExitCode;
+ }
+
+ public function failed()
+ {
+ return $this->exitCode !== $this->expectedExitCode;
+ }
+
+ protected function triggerSuccess()
+ {
+ if (($func = $this->onSuccess) !== null) {
+ $func($this);
+ }
+ }
+
+ protected function triggerFailure()
+ {
+ if (($func = $this->onFailure) !== null) {
+ $func($this);
+ }
+ }
+
+ public function getExitCode()
+ {
+ return $this->exitCode;
+ }
+
+ public function getOutput()
+ {
+ return implode("\n", $this->output) . "\n";
+ }
+}
diff --git a/library/Director/Test/TestSuite.php b/library/Director/Test/TestSuite.php
new file mode 100644
index 0000000..131b974
--- /dev/null
+++ b/library/Director/Test/TestSuite.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+use Icinga\Application\Icinga;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+
+abstract class TestSuite
+{
+ private $basedir;
+
+ abstract public function run();
+
+ public static function newTempfile()
+ {
+ return tempnam(sys_get_temp_dir(), 'DirectorTest-');
+ }
+
+ public function process($command, $identifier = null)
+ {
+ return new TestProcess($command, $identifier);
+ }
+
+ protected function filesByExtension($base, $extensions)
+ {
+ $files = array();
+
+ if (! is_array($extensions)) {
+ $extensions = array($extensions);
+ }
+
+ $basedir = $this->getBaseDir() . '/' . $base;
+ $dir = new RecursiveDirectoryIterator($basedir);
+ $iterator = new RecursiveIteratorIterator(
+ $dir,
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ foreach ($iterator as $file) {
+ if (! $file->isFile()) {
+ continue;
+ }
+
+ if (in_array($file->getExtension(), $extensions)) {
+ $files[] = $file->getPathname();
+ }
+ }
+
+ return $files;
+ }
+
+ public function getBaseDir($file = null)
+ {
+ if ($this->basedir === null) {
+ $this->basedir = Icinga::app()
+ ->getModuleManager()
+ ->getModule('director')
+ ->getBaseDir();
+ }
+
+ if ($file === null) {
+ return $this->basedir;
+ } else {
+ return $this->basedir . '/' . $file;
+ }
+ }
+}
diff --git a/library/Director/Test/TestSuiteLint.php b/library/Director/Test/TestSuiteLint.php
new file mode 100644
index 0000000..41941eb
--- /dev/null
+++ b/library/Director/Test/TestSuiteLint.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+use Icinga\Application\Logger;
+
+class TestSuiteLint extends TestSuite
+{
+ protected $checked;
+
+ protected $failed;
+
+ public function run()
+ {
+ $this->checked = $this->failed = array();
+
+ foreach ($this->listFiles() as $file) {
+ $checked[] = $file;
+ $cmd = "php -l '$file'";
+ $this->result[$file] = $this
+ ->process($cmd, $file)
+ ->onFailure(array($this, 'failedCheck'))
+ ->run();
+ }
+ }
+
+ public function failedCheck($process)
+ {
+ Logger::error($process->getOutput());
+ $this->failed[] = $process->getIdentifier();
+ }
+
+ public function hasFailures()
+ {
+ return ! empty($this->failed);
+ }
+
+ protected function listFiles()
+ {
+ $basedir = $this->getBaseDir();
+ $files = array(
+ $basedir . '/run.php',
+ $basedir . '/configuration.php'
+ );
+
+ foreach ($this->filesByExtension('library/Director', 'php') as $file) {
+ $files[] = $file;
+ }
+
+ foreach ($this->filesByExtension('application', array('php', 'phtml')) as $file) {
+ $files[] = $file;
+ }
+
+ return $files;
+ }
+}
diff --git a/library/Director/Test/TestSuiteStyle.php b/library/Director/Test/TestSuiteStyle.php
new file mode 100644
index 0000000..babd43c
--- /dev/null
+++ b/library/Director/Test/TestSuiteStyle.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+class TestSuiteStyle extends TestSuite
+{
+ public function run()
+ {
+ $out = static::newTempFile();
+ $check = array(
+ 'library/Director/',
+ 'application/',
+ 'configuration.php',
+ 'run.php',
+ );
+
+ /*
+ $options = array();
+ if ($this->isVerbose) {
+ $options[] = '-v';
+ }
+ */
+
+ /*
+ $phpcs = exec('which phpcs');
+ if (!file_exists($phpcs)) {
+ $this->fail(
+ 'PHP_CodeSniffer not found. Please install PHP_CodeSniffer to be able to run code style tests.'
+ );
+ }
+ */
+
+ $cmd = sprintf(
+ "phpcs -p --standard=PSR2 --extensions=php --encoding=utf-8 -w -s --report-checkstyle=%s '%s'",
+ $out,
+ implode("' '", $check)
+ );
+
+ $proc = $this
+ ->process($cmd);
+
+ // ->onFailure(array($this, 'failedCheck'))
+ $proc->run();
+
+ echo $proc->getOutput();
+
+ echo file_get_contents($out);
+ unlink($out);
+ // /usr/bin/phpcs --standard=PSR2 --extensions=php --encoding=utf-8 application/
+ // library/Director/ --report=full
+
+ /*
+ $options[] = '--log-junit';
+ $options[] = $reportPath . '/phpunit_results.xml';
+ $options[] = '--coverage-html';
+ $options[] = $reportPath . '/php_html_coverage';
+ */
+ return;
+
+ `$cmd`;
+ echo $cmd . "\n";
+ echo $out ."\n";
+ echo file_get_contents($out);
+ unlink($out);
+ }
+}
diff --git a/library/Director/Test/TestSuiteUnit.php b/library/Director/Test/TestSuiteUnit.php
new file mode 100644
index 0000000..8156eba
--- /dev/null
+++ b/library/Director/Test/TestSuiteUnit.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Test;
+
+abstract class TestSuiteUnit
+{
+ public function run()
+ {
+ }
+ public function __construct()
+ {
+ $this->testdoxFile = $this->newTempfile();
+ }
+
+ public function __destruct()
+ {
+ if ($this->testdoxFile && file_exists($this->testdoxFile)) {
+ unlink($this->testDoxfile);
+ }
+ }
+
+ public function getPhpunitCommand()
+ {
+ // return phpunit --bootstrap test/bootstrap.php --testdox-text /tmp/testdox.txt .
+ }
+}
diff --git a/library/Director/Test/Web/Form/TestDirectorObjectForm.php b/library/Director/Test/Web/Form/TestDirectorObjectForm.php
new file mode 100644
index 0000000..0722e78
--- /dev/null
+++ b/library/Director/Test/Web/Form/TestDirectorObjectForm.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Director\Test\Web;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class TestDirectorObjectForm extends DirectorObjectForm
+{
+ protected function getActionFromRequest()
+ {
+ $this->setAction('director/test/url');
+ return $this;
+ }
+
+ public function regenerateCsrfToken()
+ {
+ return $this;
+ }
+}
diff --git a/library/Director/TranslationDummy.php b/library/Director/TranslationDummy.php
new file mode 100644
index 0000000..937cc0b
--- /dev/null
+++ b/library/Director/TranslationDummy.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+use gipfl\Translation\TranslationHelper;
+
+class TranslationDummy
+{
+ use TranslationHelper;
+
+ protected function dummyForTranslation()
+ {
+ $this->translate('Host');
+ $this->translate('Service');
+ $this->translate('Zone');
+ $this->translate('Command');
+ $this->translate('User');
+ $this->translate('Notification');
+ }
+}
diff --git a/library/Director/Util.php b/library/Director/Util.php
new file mode 100644
index 0000000..22ad5fc
--- /dev/null
+++ b/library/Director/Util.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Icinga\Module\Director;
+
+use Icinga\Authentication\Auth;
+use Icinga\Data\ResourceFactory;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Exception\ProgrammingError;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+
+class Util
+{
+ protected static $auth;
+
+ protected static $allowedResources;
+
+ /**
+ * PBKDF2 - Password-Based Cryptography Specification (RFC2898)
+ *
+ * This method strictly follows examples in php.net's documentation
+ * comments. Hint: RFC6070 would be a good source for related tests
+ *
+ * @param string $alg Desired hash algorythm (sha1, sha256...)
+ * @param string $secret Shared secret, password
+ * @param string $salt Hash salt
+ * @param int $iterations How many iterations to perform. Please use at
+ * least 1000+. More iterations make it slower
+ * but more secure.
+ * @param int $length Desired key length
+ * @param bool $raw Returns the binary key if true, hex string otherwise
+ *
+ * @throws NotImplementedError when asking for an unsupported algorightm
+ * @throws ProgrammingError when passing invalid parameters
+ *
+ * @return string A $length byte long key, derived from secret and salt
+ */
+ public static function pbkdf2($alg, $secret, $salt, $iterations, $length, $raw = false)
+ {
+ if (! in_array($alg, hash_algos(), true)) {
+ throw new NotImplementedError('No such hash algorithm found: "%s"', $alg);
+ }
+
+ if ($iterations <= 0 || $length <= 0) {
+ throw new ProgrammingError('Positive iterations and length required');
+ }
+
+ $hashLength = strlen(hash($alg, '', true));
+ $blocks = ceil($length / $hashLength);
+
+ $out = '';
+
+ for ($i = 1; $i <= $blocks; $i++) {
+ // $i encoded as 4 bytes, big endian.
+ $last = $salt . pack('N', $i);
+ // first iteration
+ $last = $xorsum = hash_hmac($alg, $last, $secret, true);
+ // perform the other $iterations - 1 iterations
+ for ($j = 1; $j < $iterations; $j++) {
+ $xorsum ^= ($last = hash_hmac($alg, $last, $secret, true));
+ }
+ $out .= $xorsum;
+ }
+
+ if ($raw) {
+ return substr($out, 0, $length);
+ }
+
+ return bin2hex(substr($out, 0, $length));
+ }
+
+ public static function auth()
+ {
+ if (self::$auth === null) {
+ self::$auth = Auth::getInstance();
+ }
+ return self::$auth;
+ }
+
+ public static function hasPermission($name)
+ {
+ return self::auth()->hasPermission($name);
+ }
+
+ public static function getRestrictions($name)
+ {
+ return self::auth()->getRestrictions($name);
+ }
+
+ public static function resourceIsAllowed($name)
+ {
+ if (self::$allowedResources === null) {
+ $restrictions = self::getRestrictions('director/resources/use');
+ $list = array();
+ foreach ($restrictions as $restriction) {
+ foreach (preg_split('/\s*,\s*/', $restriction, -1, PREG_SPLIT_NO_EMPTY) as $key) {
+ $list[$key] = $key;
+ }
+ }
+
+ self::$allowedResources = $list;
+ } else {
+ $list = self::$allowedResources;
+ }
+
+ if (empty($list) || array_key_exists($name, $list)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function enumDbResources()
+ {
+ return self::enumResources('db');
+ }
+
+ public static function enumLdapResources()
+ {
+ return self::enumResources('ldap');
+ }
+
+ protected static function enumResources($type)
+ {
+ $resources = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $resource) {
+ if ($resource->get('type') === $type && self::resourceIsAllowed($name)) {
+ $resources[$name] = $name;
+ }
+ }
+
+ return $resources;
+ }
+
+ public static function addDbResourceFormElement(QuickForm $form, $name)
+ {
+ static::addResourceFormElement($form, $name, 'db');
+ }
+
+ public static function addLdapResourceFormElement(QuickForm $form, $name)
+ {
+ static::addResourceFormElement($form, $name, 'ldap');
+ }
+
+ protected static function addResourceFormElement(QuickForm $form, $name, $type)
+ {
+ $list = self::enumResources($type);
+
+ $form->addElement('select', $name, array(
+ 'label' => 'Resource name',
+ 'multiOptions' => $form->optionalEnum($list),
+ 'required' => true,
+ ));
+
+ if (empty($list)) {
+ if (self::hasPermission('config/application/resources')) {
+ $form->addHtmlHint(Html::sprintf(
+ $form->translate('Please click %s to create new resources'),
+ Link::create(
+ $form->translate('here'),
+ 'config/resource',
+ null,
+ ['data-base-target' => '_main']
+ )
+ ));
+ $msg = sprintf($form->translate('No %s resource available'), $type);
+ } else {
+ $msg = $form->translate('Please ask an administrator to grant you access to resources');
+ }
+
+ $form->getElement($name)->addError($msg);
+ }
+ }
+}
diff --git a/library/Director/Web/ActionBar/AutomationObjectActionBar.php b/library/Director/Web/ActionBar/AutomationObjectActionBar.php
new file mode 100644
index 0000000..247677f
--- /dev/null
+++ b/library/Director/Web/ActionBar/AutomationObjectActionBar.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Director\Web\ActionBar;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\ActionBar;
+use Icinga\Web\Request;
+
+class AutomationObjectActionBar extends ActionBar
+{
+ use TranslationHelper;
+
+ /** @var Request */
+ protected $request;
+
+ protected $label;
+
+ public function __construct(Request $request)
+ {
+ $this->request = $request;
+ }
+
+ protected function assemble()
+ {
+ $request = $this->request;
+ $action = $request->getActionName();
+ $controller = $request->getControllerName();
+ $params = ['id' => $request->getParam('id')];
+ $links = [
+ 'index' => Link::create(
+ $this->translate('Overview'),
+ "director/$controller",
+ $params,
+ ['class' => 'icon-info']
+ ),
+ 'edit' => Link::create(
+ $this->translate('Modify'),
+ "director/$controller/edit",
+ $params,
+ ['class' => 'icon-edit']
+ ),
+ 'clone' => Link::create(
+ $this->translate('Clone'),
+ "director/$controller/clone",
+ $params,
+ ['class' => 'icon-paste']
+ ),
+ /*
+ // TODO: enable once handled in the controller
+ 'export' => Link::create(
+ $this->translate('Download JSON'),
+ $this->request->getUrl()->with('format', 'json'),
+ null,
+ [
+ 'data-base-target' => '_blank',
+ ]
+ )
+ */
+
+ ];
+ unset($links[$action]);
+ $this->add($links);
+ }
+}
diff --git a/library/Director/Web/ActionBar/ChoicesActionBar.php b/library/Director/Web/ActionBar/ChoicesActionBar.php
new file mode 100644
index 0000000..7b59d2c
--- /dev/null
+++ b/library/Director/Web/ActionBar/ChoicesActionBar.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Director\Web\ActionBar;
+
+use gipfl\IcingaWeb2\Link;
+
+class ChoicesActionBar extends DirectorBaseActionBar
+{
+ protected function assemble()
+ {
+ $type = $this->type;
+ $this->add(
+ $this->getBackToDashboardLink()
+ )->add(
+ Link::create(
+ $this->translate('Add'),
+ "director/templatechoice/$type",
+ ['type' => 'object'],
+ [
+ 'title' => $this->translate('Create a new template choice'),
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next'
+ ]
+ )
+ );
+ }
+}
diff --git a/library/Director/Web/ActionBar/DirectorBaseActionBar.php b/library/Director/Web/ActionBar/DirectorBaseActionBar.php
new file mode 100644
index 0000000..8612a0d
--- /dev/null
+++ b/library/Director/Web/ActionBar/DirectorBaseActionBar.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Web\ActionBar;
+
+use Icinga\Module\Director\Dashboard\Dashboard;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\ActionBar;
+use gipfl\IcingaWeb2\Url;
+
+class DirectorBaseActionBar extends ActionBar
+{
+ use TranslationHelper;
+
+ /** @var Url */
+ protected $url;
+
+ /** @var string */
+ protected $type;
+
+ public function __construct($type, Url $url)
+ {
+ $this->type = $type;
+ $this->url = $url;
+ }
+
+ protected function getBackToDashboardLink()
+ {
+ $name = $this->getPluralBaseType();
+ if (! Dashboard::exists($name)) {
+ return null;
+ }
+
+ return Link::create(
+ $this->translate('back'),
+ 'director/dashboard',
+ ['name' => $name],
+ [
+ 'title' => sprintf(
+ $this->translate('Go back to "%s" Dashboard'),
+ $this->translate(ucfirst($this->type))
+ ),
+ 'class' => 'icon-left-big',
+ 'data-base-target' => '_main'
+ ]
+ );
+ }
+
+ protected function getBaseType()
+ {
+ if (substr($this->type, -5) === 'Group') {
+ return substr($this->type, 0, -5);
+ } else {
+ return $this->type;
+ }
+ }
+
+ protected function getPluralType()
+ {
+ return $this->type . 's';
+ }
+
+ protected function getPluralBaseType()
+ {
+ return $this->getBaseType() . 's';
+ }
+}
diff --git a/library/Director/Web/ActionBar/ObjectsActionBar.php b/library/Director/Web/ActionBar/ObjectsActionBar.php
new file mode 100644
index 0000000..5f86949
--- /dev/null
+++ b/library/Director/Web/ActionBar/ObjectsActionBar.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Director\Web\ActionBar;
+
+use gipfl\IcingaWeb2\Link;
+
+class ObjectsActionBar extends DirectorBaseActionBar
+{
+ protected function assemble()
+ {
+ $type = $this->type;
+ $this->add(
+ $this->getBackToDashboardLink()
+ )->add(
+ Link::create(
+ $this->translate('Add'),
+ "director/$type/add",
+ ['type' => 'object'],
+ [
+ 'title' => $this->translate('Create a new object'),
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next'
+ ]
+ )
+ );
+ }
+}
diff --git a/library/Director/Web/ActionBar/TemplateActionBar.php b/library/Director/Web/ActionBar/TemplateActionBar.php
new file mode 100644
index 0000000..53e65ed
--- /dev/null
+++ b/library/Director/Web/ActionBar/TemplateActionBar.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Icinga\Module\Director\Web\ActionBar;
+
+use gipfl\IcingaWeb2\Link;
+
+class TemplateActionBar extends DirectorBaseActionBar
+{
+ protected function assemble()
+ {
+ $type = str_replace('_', '-', $this->type);
+ $plType = preg_replace('/cys$/', 'cies', $type . 's');
+ $renderTree = $this->url->getParam('render') === 'tree';
+ $renderParams = $renderTree ? null : ['render' => 'tree'];
+ $this->add(
+ $this->getBackToDashboardLink()
+ )->add(
+ Link::create(
+ $this->translate('Add'),
+ "director/$type/add",
+ ['type' => 'template'],
+ [
+ 'title' => $this->translate('Create a new Template'),
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next'
+ ]
+ )
+ )->add(
+ Link::create(
+ $renderTree ? $this->translate('Table') : $this->translate('Tree'),
+ "director/$plType/templates",
+ $renderParams,
+ [
+ 'class' => 'icon-' . ($renderTree ? 'doc-text' : 'sitemap'),
+ 'title' => $renderTree
+ ? $this->translate('Switch to Tree view')
+ : $this->translate('Switch to Table view')
+ ]
+ )
+ );
+ }
+}
diff --git a/library/Director/Web/Controller/ActionController.php b/library/Director/Web/Controller/ActionController.php
new file mode 100644
index 0000000..6282a16
--- /dev/null
+++ b/library/Director/Web/Controller/ActionController.php
@@ -0,0 +1,253 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use gipfl\Translation\StaticTranslator;
+use Icinga\Application\Benchmark;
+use Icinga\Data\Paginatable;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Monitoring;
+use Icinga\Module\Director\Web\Controller\Extension\CoreApi;
+use Icinga\Module\Director\Web\Controller\Extension\DirectorDb;
+use Icinga\Module\Director\Web\Controller\Extension\RestApi;
+use Icinga\Module\Director\Web\Window;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Controller;
+use Icinga\Web\UrlParams;
+use InvalidArgumentException;
+use gipfl\IcingaWeb2\Translator;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Widget\ControlsAndContent;
+use gipfl\IcingaWeb2\Controller\Extension\ControlsAndContentHelper;
+use gipfl\IcingaWeb2\Zf1\SimpleViewRenderer;
+use GuzzleHttp\Psr7\ServerRequest;
+use Psr\Http\Message\ServerRequestInterface;
+
+abstract class ActionController extends Controller implements ControlsAndContent
+{
+ use DirectorDb;
+ use CoreApi;
+ use RestApi;
+ use ControlsAndContentHelper;
+
+ protected $isApified = false;
+
+ /** @var UrlParams Hint for IDE, somehow does not work in web */
+ protected $params;
+
+ /** @var Monitoring */
+ private $monitoring;
+
+ /**
+ * @throws SecurityException
+ * @throws \Icinga\Exception\AuthenticationException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function init()
+ {
+ if (! $this->getRequest()->isApiRequest()
+ && $this->Config()->get('frontend', 'disabled', 'no') === 'yes'
+ ) {
+ throw new NotFoundError('Not found');
+ }
+ $this->initializeTranslator();
+ Benchmark::measure('Director base Controller init()');
+ $this->checkForRestApiRequest();
+ $this->checkDirectorPermissions();
+ $this->checkSpecialDirectorPermissions();
+ }
+
+ protected function initializeTranslator()
+ {
+ StaticTranslator::set(new Translator('director'));
+ }
+
+ public function getAuth()
+ {
+ return $this->Auth();
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ * @return Window
+ */
+ public function Window()
+ {
+ // @codingStandardsIgnoreEnd
+ if ($this->window === null) {
+ $this->window = new Window(
+ $this->_request->getHeader('X-Icinga-WindowId', Window::UNDEFINED)
+ );
+ }
+ return $this->window;
+ }
+
+ /**
+ * @throws SecurityException
+ */
+ protected function checkDirectorPermissions()
+ {
+ $this->assertPermission('director/admin');
+ }
+
+ /**
+ * @throws SecurityException
+ */
+ protected function checkSpecialDirectorPermissions()
+ {
+ if ($this->params->get('format') === 'sql') {
+ $this->assertPermission('director/showsql');
+ }
+ }
+
+ /**
+ * Assert that the current user has one of the given permission
+ *
+ * @param array $permissions Permission name list
+ *
+ * @return $this
+ * @throws SecurityException If the current user lacks the given permission
+ */
+ protected function assertOneOfPermissions($permissions)
+ {
+ $auth = $this->Auth();
+
+ foreach ($permissions as $permission) {
+ if ($auth->hasPermission($permission)) {
+ return $this;
+ }
+ }
+
+ throw new SecurityException(
+ 'Got none of the following permissions: %s',
+ implode(', ', $permissions)
+ );
+ }
+
+ /**
+ * @param int $interval
+ * @return $this
+ */
+ public function setAutorefreshInterval($interval)
+ {
+ if (! $this->getRequest()->isApiRequest()) {
+ try {
+ parent::setAutorefreshInterval($interval);
+ } catch (ProgrammingError $e) {
+ throw new InvalidArgumentException($e->getMessage());
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return ServerRequestInterface
+ */
+ protected function getServerRequest()
+ {
+ return ServerRequest::fromGlobals();
+ }
+
+ protected function applyPaginationLimits(Paginatable $paginatable, $limit = 25, $offset = null)
+ {
+ $limit = $this->params->get('limit', $limit);
+ $page = $this->params->get('page', $offset);
+
+ $paginatable->limit($limit, $page > 0 ? ($page - 1) * $limit : 0);
+
+ return $paginatable;
+ }
+
+ protected function addAddLink($title, $url, $urlParams = null, $target = '_next')
+ {
+ $this->actions()->add(Link::create(
+ $this->translate('Add'),
+ $url,
+ $urlParams,
+ [
+ 'class' => 'icon-plus',
+ 'title' => $title,
+ 'data-base-target' => $target
+ ]
+ ));
+
+ return $this;
+ }
+
+ protected function addBackLink($url, $urlParams = null)
+ {
+ $this->actions()->add(new Link(
+ $this->translate('back'),
+ $url,
+ $urlParams,
+ ['class' => 'icon-left-big']
+ ));
+
+ return $this;
+ }
+
+ protected function sendUnsupportedMethod()
+ {
+ $method = strtoupper($this->getRequest()->getMethod()) ;
+ $response = $this->getResponse();
+ $this->sendJsonError($response, sprintf(
+ 'Method %s is not supported',
+ $method
+ ), 422); // TODO: check response code
+ }
+
+ /**
+ * @param string $permission
+ * @return $this
+ * @throws SecurityException
+ */
+ public function assertPermission($permission)
+ {
+ parent::assertPermission($permission);
+ return $this;
+ }
+
+ public function postDispatch()
+ {
+ Benchmark::measure('Director postDispatch');
+ if ($this->view->content || $this->view->controls) {
+ $viewRenderer = new SimpleViewRenderer();
+ $viewRenderer->replaceZendViewRenderer();
+ $this->view = $viewRenderer->view;
+ // Hint -> $this->view->compact is the only way since v2.8.0
+ if ($this->view->compact || $this->getOriginalUrl()->getParam('view') === 'compact') {
+ if ($this->view->controls) {
+ $this->controls()->getAttributes()->add('style', 'display: none;');
+ }
+ }
+ } else {
+ $viewRenderer = null;
+ }
+
+ $cType = $this->getResponse()->getHeader('Content-Type', true);
+ if ($this->getRequest()->isApiRequest() || ($cType !== null && $cType !== 'text/html')) {
+ $this->_helper->layout()->disableLayout();
+ if ($viewRenderer) {
+ $viewRenderer->disable();
+ } else {
+ $this->_helper->viewRenderer->setNoRender(true);
+ }
+ }
+
+ parent::postDispatch(); // TODO: Change the autogenerated stub
+ }
+
+ /**
+ * @return Monitoring
+ */
+ protected function monitoring()
+ {
+ if ($this->monitoring === null) {
+ $this->monitoring = new Monitoring;
+ }
+
+ return $this->monitoring;
+ }
+}
diff --git a/library/Director/Web/Controller/BranchHelper.php b/library/Director/Web/Controller/BranchHelper.php
new file mode 100644
index 0000000..ac2a480
--- /dev/null
+++ b/library/Director/Web/Controller/BranchHelper.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchStore;
+use Icinga\Module\Director\Db\Branch\BranchSupport;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Widget\NotInBranchedHint;
+
+trait BranchHelper
+{
+ /** @var Branch */
+ protected $branch;
+
+ /** @var BranchStore */
+ protected $branchStore;
+
+ /**
+ * @return false|\Ramsey\Uuid\UuidInterface
+ */
+ protected function getBranchUuid()
+ {
+ return $this->getBranch()->getUuid();
+ }
+
+ protected function getBranch()
+ {
+ if ($this->branch === null) {
+ /** @var ActionController $this */
+ $this->branch = Branch::forRequest($this->getRequest(), $this->getBranchStore(), $this->Auth());
+ }
+
+ return $this->branch;
+ }
+
+ /**
+ * @return BranchStore
+ */
+ protected function getBranchStore()
+ {
+ if ($this->branchStore === null) {
+ $this->branchStore = new BranchStore($this->db());
+ }
+
+ return $this->branchStore;
+ }
+
+ protected function hasBranch()
+ {
+ return $this->getBranchUuid() !== null;
+ }
+
+ protected function enableStaticObjectLoader($table)
+ {
+ if (BranchSupport::existsForTableName($table)) {
+ IcingaObject::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch()));
+ }
+ }
+
+ /**
+ * @param string $subject
+ * @return bool
+ */
+ protected function showNotInBranch($subject)
+ {
+ if ($this->getBranch()->isBranch()) {
+ $this->content()->add(new NotInBranchedHint($subject, $this->getBranch(), $this->Auth()));
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/CoreApi.php b/library/Director/Web/Controller/Extension/CoreApi.php
new file mode 100644
index 0000000..75cba50
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/CoreApi.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\Core\CoreApi as Api;
+
+trait CoreApi
+{
+ /** @var Api */
+ private $api;
+
+ /**
+ * @return Api|null
+ */
+ public function getApiIfAvailable()
+ {
+ if ($this->api === null) {
+ if ($this->db()->hasDeploymentEndpoint()) {
+ $endpoint = $this->db()->getDeploymentEndpoint();
+ $this->api = $endpoint->api();
+ }
+ }
+
+ return $this->api;
+ }
+
+ /**
+ * @param string $endpointName
+ * @return Api
+ */
+ public function api($endpointName = null)
+ {
+ if ($this->api === null) {
+ if ($endpointName === null) {
+ $endpoint = $this->db()->getDeploymentEndpoint();
+ } else {
+ $endpoint = IcingaEndpoint::load($endpointName, $this->db());
+ }
+
+ $this->api = $endpoint->api();
+ }
+
+ return $this->api;
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/DirectorDb.php b/library/Director/Web/Controller/Extension/DirectorDb.php
new file mode 100644
index 0000000..03bec81
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/DirectorDb.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Web\Controller\ActionController;
+use Icinga\Module\Director\Web\Window;
+use RuntimeException;
+
+trait DirectorDb
+{
+ /** @var Db */
+ private $db;
+
+ protected function getDbResourceName()
+ {
+ if ($name = $this->getDbResourceNameFromRequest()) {
+ return $name;
+ } elseif ($name = $this->getPreferredDbResourceName()) {
+ return $name;
+ } else {
+ return $this->getFirstDbResourceName();
+ }
+ }
+
+ protected function getDbResourceNameFromRequest()
+ {
+ $param = 'dbResourceName';
+ // We shouldn't access _POST and _GET. However, this trait is used
+ // in various places - and our Request is going to be replaced anyways.
+ // So, let's not over-engineer things, this is quick & dirty:
+ if (isset($_POST[$param])) {
+ $name = $_POST[$param];
+ } elseif (isset($_GET[$param])) {
+ $name = $_GET[$param];
+ } else {
+ return null;
+ }
+
+ if (in_array($name, $this->listAllowedDbResourceNames())) {
+ return $name;
+ } else {
+ return null;
+ }
+ }
+
+ protected function getPreferredDbResourceName()
+ {
+ return $this->getWindowSessionValue('db_resource');
+ }
+
+ protected function getFirstDbResourceName()
+ {
+ $names = $this->listAllowedDbResourceNames();
+ if (empty($names)) {
+ return null;
+ } else {
+ return array_shift($names);
+ }
+ }
+
+ protected function listAllowedDbResourceNames()
+ {
+ /** @var \Icinga\Authentication\Auth $auth */
+ $auth = $this->Auth();
+
+ $available = $this->listAvailableDbResourceNames();
+ if ($resourceNames = $auth->getRestrictions('director/db_resource')) {
+ $names = [];
+ foreach ($resourceNames as $rNames) {
+ foreach ($this->splitList($rNames) as $name) {
+ if (array_key_exists($name, $available)) {
+ $names[] = $name;
+ }
+ }
+ }
+
+ return $names;
+ } else {
+ return $available;
+ }
+ }
+
+ /**
+ * @param string $string
+ * @return array
+ */
+ protected function splitList($string)
+ {
+ return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ protected function isMultiDbSetup()
+ {
+ return count($this->listAvailableDbResourceNames()) > 1;
+ }
+
+ /**
+ * @return array
+ */
+ protected function listAvailableDbResourceNames()
+ {
+ /** @var \Icinga\Application\Config $config */
+ $config = $this->Config();
+ $resources = $config->get('db', 'resources');
+ if ($resources === null) {
+ $resource = $config->get('db', 'resource');
+ if ($resource === null) {
+ return [];
+ } else {
+ return [$resource => $resource];
+ }
+ } else {
+ $resources = $this->splitList($resources);
+ $resources = array_combine($resources, $resources);
+ // natsort doesn't work!?
+ ksort($resources, SORT_NATURAL);
+ if ($resource = $config->get('db', 'resource')) {
+ unset($resources[$resource]);
+ $resources = [$resource => $resource] + $resources;
+ }
+
+ return $resources;
+ }
+ }
+
+ protected function getWindowSessionValue($value, $default = null)
+ {
+ /** @var Window $window */
+ $window = $this->Window();
+ /** @var \Icinga\Web\Session\SessionNamespace $session */
+ $session = $window->getSessionNamespace('director');
+
+ return $session->get($value, $default);
+ }
+
+ /**
+ *
+ * @return Db
+ */
+ public function db()
+ {
+ if ($this->db === null) {
+ $resourceName = $this->getDbResourceName();
+ if ($resourceName) {
+ $this->db = Db::fromResourceName($resourceName);
+ } elseif ($this instanceof ActionController) {
+ if ($this->getRequest()->isApiRequest()) {
+ throw new RuntimeException('Icinga Director is not correctly configured');
+ } else {
+ $this->redirectNow('director');
+ }
+ } else {
+ throw new RuntimeException('Icinga Director is not correctly configured');
+ }
+ }
+
+ return $this->db;
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/ObjectRestrictions.php b/library/Director/Web/Controller/Extension/ObjectRestrictions.php
new file mode 100644
index 0000000..bedb3f1
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/ObjectRestrictions.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Restriction\HostgroupRestriction;
+use Icinga\Module\Director\Restriction\ObjectRestriction;
+
+trait ObjectRestrictions
+{
+ /** @var ObjectRestriction[] */
+ private $objectRestrictions;
+
+ /**
+ * @return ObjectRestriction[]
+ */
+ public function getObjectRestrictions()
+ {
+ if ($this->objectRestrictions === null) {
+ $this->objectRestrictions = $this->loadObjectRestrictions($this->db(), $this->Auth());
+ }
+
+ return $this->objectRestrictions;
+ }
+
+ /**
+ * @return ObjectRestriction[]
+ */
+ protected function loadObjectRestrictions(Db $db, Auth $auth)
+ {
+ return [
+ new HostgroupRestriction($db, $auth)
+ ];
+ }
+
+ public function allowsObject(IcingaObject $object)
+ {
+ foreach ($this->getObjectRestrictions() as $restriction) {
+ if (! $restriction->allows($object)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/RestApi.php b/library/Director/Web/Controller/Extension/RestApi.php
new file mode 100644
index 0000000..3158f49
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/RestApi.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Icinga\Exception\AuthenticationException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Exception\JsonException;
+use Icinga\Web\Response;
+use InvalidArgumentException;
+use Zend_Controller_Response_Exception;
+
+trait RestApi
+{
+ protected function isApified()
+ {
+ if (property_exists($this, 'isApified')) {
+ return $this->isApified;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ protected function sendNotFoundForRestApi()
+ {
+ /** @var \Icinga\Web\Request $request */
+ $request = $this->getRequest();
+ if ($request->isApiRequest()) {
+ $this->sendJsonError($this->getResponse(), 'Not found', 404);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ protected function sendNotFoundUnlessRestApi()
+ {
+ /** @var \Icinga\Web\Request $request */
+ $request = $this->getRequest();
+ if ($request->isApiRequest()) {
+ return false;
+ } else {
+ $this->sendJsonError($this->getResponse(), 'Not found', 404);
+ return true;
+ }
+ }
+
+ /**
+ * @throws AuthenticationException
+ */
+ protected function assertApiPermission()
+ {
+ if (! $this->hasPermission('director/api')) {
+ throw new AuthenticationException('You are not allowed to access this API');
+ }
+ }
+
+ /**
+ * @throws AuthenticationException
+ * @throws NotFoundError
+ */
+ protected function checkForRestApiRequest()
+ {
+ /** @var \Icinga\Web\Request $request */
+ $request = $this->getRequest();
+ if ($request->isApiRequest()) {
+ $this->assertApiPermission();
+ if (! $this->isApified()) {
+ throw new NotFoundError('No such API endpoint found');
+ }
+ }
+ }
+
+ /**
+ * @param Response $response
+ * @param $object
+ */
+ protected function sendJson(Response $response, $object)
+ {
+ $response->setHeader('Content-Type', 'application/json', true);
+ echo json_encode($object, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
+ }
+
+ /**
+ * @param Response $response
+ * @param string $message
+ * @param int|null $code
+ */
+ protected function sendJsonError(Response $response, $message, $code = null)
+ {
+ if ($code !== null) {
+ try {
+ $response->setHttpResponseCode((int) $code);
+ } catch (Zend_Controller_Response_Exception $e) {
+ throw new InvalidArgumentException($e->getMessage(), 0, $e);
+ }
+ }
+
+ $this->sendJson($response, (object) ['error' => $message]);
+ }
+
+ /**
+ * @return string
+ */
+ protected function getLastJsonError()
+ {
+ return JsonException::getJsonErrorMessage(json_last_error());
+ }
+}
diff --git a/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php b/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php
new file mode 100644
index 0000000..bc51548
--- /dev/null
+++ b/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php
@@ -0,0 +1,236 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller\Extension;
+
+use Exception;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Forms\IcingaDeleteObjectForm;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Web\Request;
+use Icinga\Web\Response;
+
+class SingleObjectApiHandler
+{
+ use DirectorDb;
+
+ /** @var IcingaObject */
+ private $object;
+
+ /** @var string */
+ private $type;
+
+ /** @var Request */
+ private $request;
+
+ /** @var Response */
+ private $response;
+
+ /** @var \Icinga\Web\UrlParams */
+ private $params;
+
+ public function __construct($type, Request $request, Response $response)
+ {
+ $this->type = $type;
+ $this->request = $request;
+ $this->response = $response;
+ $this->params = $request->getUrl()->getParams();
+ }
+
+ public function runFailSafe()
+ {
+ try {
+ $this->loadObject();
+ $this->run();
+ } catch (NotFoundError $e) {
+ $this->sendJsonError($e->getMessage(), 404);
+ } catch (Exception $e) {
+ $response = $this->response;
+ if ($response->getHttpResponseCode() === 200) {
+ $response->setHttpResponseCode(500);
+ }
+
+ $this->sendJsonError($e->getMessage());
+ }
+ }
+
+ protected function retrieveObject()
+ {
+ $this->requireObject();
+ $this->sendJson(
+ $this->object->toPlainObject(
+ $this->params->shift('resolved'),
+ ! $this->params->shift('withNull'),
+ $this->params->shift('properties')
+ )
+ );
+ }
+
+ protected function deleteObject()
+ {
+ $this->requireObject();
+ $obj = $this->object->toPlainObject(false, true);
+ $form = new IcingaDeleteObjectForm();
+ $form->setObject($this->object)
+ ->setRequest($this->request)
+ ->onSuccess();
+
+ $this->sendJson($obj);
+ }
+
+ protected function storeObject()
+ {
+ $data = json_decode($this->request->getRawBody());
+
+ if ($data === null) {
+ $this->response->setHttpResponseCode(400);
+ throw new IcingaException(
+ 'Invalid JSON: %s' . $this->request->getRawBody(),
+ $this->getLastJsonError()
+ );
+ } else {
+ $data = (array) $data;
+ }
+
+ if ($object = $this->object) {
+ if ($this->request->getMethod() === 'POST') {
+ $object->setProperties($data);
+ } else {
+ $data = array_merge([
+ 'object_type' => $object->object_type,
+ 'object_name' => $object->object_name
+ ], $data);
+ $object->replaceWith(
+ IcingaObject::createByType($this->type, $data, $db)
+ );
+ }
+ } else {
+ $object = IcingaObject::createByType($this->type, $data, $db);
+ }
+
+ if ($object->hasBeenModified()) {
+ $status = $object->hasBeenLoadedFromDb() ? 200 : 201;
+ $object->store();
+ $this->response->setHttpResponseCode($status);
+ } else {
+ $this->response->setHttpResponseCode(304);
+ }
+
+ $this->sendJson($object->toPlainObject(false, true));
+ }
+
+ public function run()
+ {
+ switch ($this->request->getMethod()) {
+ case 'DELETE':
+ $this->deleteObject();
+ break;
+
+ case 'POST':
+ case 'PUT':
+ $this->storeObject();
+ break;
+
+ case 'GET':
+ $this->retrieveObject();
+ break;
+
+ default:
+ $this->response->setHttpResponseCode(400);
+ throw new IcingaException(
+ 'Unsupported method: %s',
+ $this->request->getMethod()
+ );
+ }
+ }
+
+ protected function requireObject()
+ {
+ if (! $this->object) {
+ $this->response->setHttpResponseCode(404);
+ if (! $this->params->get('name')) {
+ throw new NotFoundError('You need to pass a "name" parameter to access a specific object');
+ } else {
+ throw new NotFoundError('No such object available');
+ }
+ }
+ }
+
+ // TODO: just return json_last_error_msg() for PHP >= 5.5.0
+ protected function getLastJsonError()
+ {
+ switch (json_last_error()) {
+ case JSON_ERROR_DEPTH:
+ return 'The maximum stack depth has been exceeded';
+ case JSON_ERROR_CTRL_CHAR:
+ return 'Control character error, possibly incorrectly encoded';
+ case JSON_ERROR_STATE_MISMATCH:
+ return 'Invalid or malformed JSON';
+ case JSON_ERROR_SYNTAX:
+ return 'Syntax error';
+ case JSON_ERROR_UTF8:
+ return 'Malformed UTF-8 characters, possibly incorrectly encoded';
+ default:
+ return 'An error occured when parsing a JSON string';
+ }
+ }
+
+ protected function sendJson($object)
+ {
+ $this->response->setHeader('Content-Type', 'application/json', true);
+ $this->_helper->layout()->disableLayout();
+ $this->_helper->viewRenderer->setNoRender(true);
+ echo json_encode($object, JSON_PRETTY_PRINT) . "\n";
+ }
+
+ protected function sendJsonError($message, $code = null)
+ {
+ $response = $this->response;
+
+ if ($code !== null) {
+ $response->setHttpResponseCode((int) $code);
+ }
+
+ $this->sendJson((object) ['error' => $message]);
+ }
+
+ protected function loadObject()
+ {
+ if ($this->object === null) {
+ if ($name = $this->params->get('name')) {
+ $this->object = IcingaObject::loadByType(
+ $this->type,
+ $name,
+ $this->db()
+ );
+
+ if (! $this->allowsObject($this->object)) {
+ $this->object = null;
+ throw new NotFoundError('No such object available');
+ }
+ } elseif ($id = $this->params->get('id')) {
+ $this->object = IcingaObject::loadByType(
+ $this->type,
+ (int) $id,
+ $this->db()
+ );
+ } elseif ($this->request->isApiRequest()) {
+ if ($this->request->isGet()) {
+ $this->response->setHttpResponseCode(422);
+
+ throw new InvalidPropertyException(
+ 'Cannot load object, missing parameters'
+ );
+ }
+ }
+ }
+
+ return $this->object;
+ }
+
+ protected function allowsObject(IcingaObject $object)
+ {
+ return true;
+ }
+}
diff --git a/library/Director/Web/Controller/ObjectController.php b/library/Director/Web/Controller/ObjectController.php
new file mode 100644
index 0000000..0c06937
--- /dev/null
+++ b/library/Director/Web/Controller/ObjectController.php
@@ -0,0 +1,733 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchedObject;
+use Icinga\Module\Director\Db\Branch\UuidLookup;
+use Icinga\Module\Director\Deployment\DeploymentInfo;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Forms\DeploymentLinkForm;
+use Icinga\Module\Director\Forms\IcingaCloneObjectForm;
+use Icinga\Module\Director\Forms\IcingaObjectFieldForm;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaObjectGroup;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\RestApi\IcingaObjectHandler;
+use Icinga\Module\Director\Web\Controller\Extension\ObjectRestrictions;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\ObjectPreview;
+use Icinga\Module\Director\Web\Table\ActivityLogTable;
+use Icinga\Module\Director\Web\Table\BranchActivityTable;
+use Icinga\Module\Director\Web\Table\GroupMemberTable;
+use Icinga\Module\Director\Web\Table\IcingaObjectDatafieldTable;
+use Icinga\Module\Director\Web\Tabs\ObjectTabs;
+use Icinga\Module\Director\Web\Widget\BranchedObjectHint;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Html;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
+
+abstract class ObjectController extends ActionController
+{
+ use ObjectRestrictions;
+ use BranchHelper;
+
+ /** @var IcingaObject */
+ protected $object;
+
+ /** @var bool This controller handles REST API requests */
+ protected $isApified = true;
+
+ /** @var array Allowed object types we are allowed to edit anyways */
+ protected $allowedExternals = array(
+ 'apiuser',
+ 'endpoint'
+ );
+
+ protected $type;
+
+ /** @var string|null */
+ protected $objectBaseUrl;
+
+ public function init()
+ {
+ parent::init();
+ $this->enableStaticObjectLoader($this->getTableName());
+ if ($this->getRequest()->isApiRequest()) {
+ $this->initializeRestApi();
+ } else {
+ $this->initializeWebRequest();
+ }
+ }
+
+ protected function initializeRestApi()
+ {
+ $handler = new IcingaObjectHandler($this->getRequest(), $this->getResponse(), $this->db());
+ try {
+ $this->loadOptionalObject();
+ } catch (NotFoundError $e) {
+ // Silently ignore the error, the handler will complain
+ $handler->sendJsonError($e, 404);
+ // TODO: nice shutdown
+ exit;
+ }
+
+ $handler->setApi($this->api());
+ if ($this->object) {
+ $handler->setObject($this->object);
+ }
+ $handler->dispatch();
+ // Hint: also here, hard exit. There is too much magic going on.
+ // Letting this bubble up smoothly would be "correct", but proved
+ // to be too fragile. Web 2, all kinds of pre/postDispatch magic,
+ // different view renderers - hard exit is the only safe bet right
+ // now.
+ exit;
+ }
+
+ protected function initializeWebRequest()
+ {
+ $this->loadOptionalObject();
+ if ($this->getRequest()->getActionName() === 'add') {
+ $this->addSingleTab(
+ sprintf($this->translate('Add %s'), ucfirst($this->getType())),
+ null,
+ 'add'
+ );
+ } else {
+ $this->tabs(new ObjectTabs(
+ $this->getRequest()->getControllerName(),
+ $this->getAuth(),
+ $this->object
+ ));
+ }
+ if ($this->object !== null) {
+ $this->addDeploymentLink();
+ }
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function indexAction()
+ {
+ if (! $this->getRequest()->isApiRequest()) {
+ $this->redirectToPreviewForExternals()
+ ->editAction();
+ }
+ }
+
+ public function addAction()
+ {
+ $this->tabs()->activate('add');
+ $url = sprintf('director/%ss', $this->getPluralType());
+
+ $imports = $this->params->get('imports');
+ $form = $this->loadObjectForm()
+ ->presetImports($imports)
+ ->setSuccessUrl($url);
+
+ if ($oType = $this->params->get('type', 'object')) {
+ $form->setPreferredObjectType($oType);
+ }
+ if ($oType === 'template') {
+ if ($this->showNotInBranch($this->translate('Creating Templates'))) {
+ $this->addTitle($this->translate('Create a new Template'));
+ return;
+ }
+
+ $this->addTemplate();
+ } else {
+ $this->addObject();
+ }
+ $branch = $this->getBranch();
+ if ($branch->isBranch() && ! $this->getRequest()->isApiRequest()) {
+ $this->content()->add(new BranchedObjectHint($branch, $this->Auth()));
+ }
+
+ $form->handleRequest();
+ $this->content()->add($form);
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function editAction()
+ {
+ $object = $this->requireObject();
+ $this->tabs()->activate('modify');
+ $this->addObjectTitle();
+ // Hint: Service Sets are 'templates' (as long as not being assigned to a host
+ if ($this->getTableName() !== 'icinga_service_set'
+ && $object->isTemplate()
+ && $this->showNotInBranch($this->translate('Modifying Templates'))
+ ) {
+ return;
+ }
+ if ($object->isApplyRule() && $this->showNotInBranch($this->translate('Modifying Apply Rules'))) {
+ return;
+ }
+
+ $this->addObjectForm($object)
+ ->addActionClone()
+ ->addActionUsage()
+ ->addActionBasket();
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function renderAction()
+ {
+ $this->assertTypePermission()
+ ->assertPermission('director/showconfig');
+ $this->tabs()->activate('render');
+ $preview = new ObjectPreview($this->requireObject(), $this->getRequest());
+ if ($this->object->isExternal()) {
+ $this->addActionClone();
+ }
+ $this->addActionBasket();
+ $preview->renderTo($this);
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function cloneAction()
+ {
+ $this->assertTypePermission();
+ $object = $this->requireObject();
+ $this->addTitle($this->translate('Clone: %s'), $object->getObjectName())
+ ->addBackToObjectLink();
+
+ if ($object->isTemplate() && $this->showNotInBranch($this->translate('Cloning Templates'))) {
+ return;
+ }
+
+ if ($object->isTemplate() && $this->showNotInBranch($this->translate('Cloning Apply Rules'))) {
+ return;
+ }
+
+ $form = IcingaCloneObjectForm::load()
+ ->setBranch($this->getBranch())
+ ->setObject($object)
+ ->setObjectBaseUrl($this->getObjectBaseUrl())
+ ->handleRequest();
+
+ if ($object->isExternal()) {
+ $this->tabs()->activate('render');
+ } else {
+ $this->tabs()->activate('modify');
+ }
+ $this->content()->add($form);
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function fieldsAction()
+ {
+ $this->assertPermission('director/admin');
+ $object = $this->requireObject();
+ $type = $this->getType();
+
+ $this->addTitle(
+ $this->translate('Custom fields: %s'),
+ $object->getObjectName()
+ );
+ $this->tabs()->activate('fields');
+ if ($this->showNotInBranch($this->translate('Managing Fields'))) {
+ return;
+ }
+
+ try {
+ $this->addFieldsFormAndTable($object, $type);
+ } catch (NestingError $e) {
+ $this->content()->add(Hint::error($e->getMessage()));
+ }
+ }
+
+ protected function addFieldsFormAndTable($object, $type)
+ {
+ $form = IcingaObjectFieldForm::load()
+ ->setDb($this->db())
+ ->setIcingaObject($object);
+
+ if ($id = $this->params->get('field_id')) {
+ $form->loadObject([
+ "${type}_id" => $object->id,
+ 'datafield_id' => $id
+ ]);
+
+ $this->actions()->add(Link::create(
+ $this->translate('back'),
+ $this->url()->without('field_id'),
+ null,
+ ['class' => 'icon-left-big']
+ ));
+ }
+ $form->handleRequest();
+ $this->content()->add($form);
+ $table = new IcingaObjectDatafieldTable($object);
+ $table->getAttributes()->set('data-base-target', '_self');
+ $table->renderTo($this);
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function historyAction()
+ {
+ $this
+ ->assertTypePermission()
+ ->assertPermission('director/audit')
+ ->setAutorefreshInterval(10)
+ ->tabs()->activate('history');
+
+ $name = $this->requireObject()->getObjectName();
+ $this->addTitle($this->translate('Activity Log: %s'), $name);
+
+ $db = $this->db();
+ $objectTable = $this->object->getTableName();
+ $table = (new ActivityLogTable($db))
+ ->setLastDeployedId($db->getLastDeploymentActivityLogId())
+ ->filterObject($objectTable, $name);
+ if ($host = $this->params->get('host')) {
+ $table->filterHost($host);
+ }
+ $this->showOptionalBranchActivity($table);
+ $table->renderTo($this);
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function membershipAction()
+ {
+ $object = $this->requireObject();
+ if (! $object instanceof IcingaObjectGroup) {
+ throw new NotFoundError('Not Found');
+ }
+
+ $this
+ ->addTitle($this->translate('Group membership: %s'), $object->getObjectName())
+ ->setAutorefreshInterval(15)
+ ->tabs()->activate('membership');
+
+ $type = substr($this->getType(), 0, -5);
+ GroupMemberTable::create($type, $this->db())
+ ->setGroup($object)
+ ->renderTo($this);
+ }
+
+ /**
+ * @return $this
+ * @throws NotFoundError
+ */
+ protected function addObjectTitle()
+ {
+ $object = $this->requireObject();
+ $name = $object->getObjectName();
+ if ($object->isTemplate()) {
+ $this->addTitle($this->translate('Template: %s'), $name);
+ } else {
+ $this->addTitle($name);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws NotFoundError
+ */
+ protected function addActionUsage()
+ {
+ $type = $this->getType();
+ $object = $this->requireObject();
+ if ($object->isTemplate() && $type !== 'serviceSet') {
+ $this->actions()->add([
+ Link::create(
+ $this->translate('Usage'),
+ "director/${type}template/usage",
+ ['name' => $object->getObjectName()],
+ ['class' => 'icon-sitemap']
+ )
+ ]);
+ }
+
+ return $this;
+ }
+
+ protected function addActionClone()
+ {
+ $this->actions()->add(Link::create(
+ $this->translate('Clone'),
+ $this->getObjectBaseUrl() . '/clone',
+ $this->object->getUrlParams(),
+ array('class' => 'icon-paste')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addActionBasket()
+ {
+ if ($this->hasBasketSupport()) {
+ $object = $this->object;
+ if ($object instanceof ExportInterface) {
+ if ($object instanceof IcingaCommand) {
+ if ($object->isExternal()) {
+ $type = 'ExternalCommand';
+ } elseif ($object->isTemplate()) {
+ $type = 'CommandTemplate';
+ } else {
+ $type = 'Command';
+ }
+ } elseif ($object instanceof IcingaServiceSet) {
+ $type = 'ServiceSet';
+ } elseif ($object->isTemplate()) {
+ $type = ucfirst($this->getType()) . 'Template';
+ } elseif ($object->isGroup()) {
+ $type = ucfirst($this->getType());
+ } else {
+ // Command? Sure?
+ $type = ucfirst($this->getType());
+ }
+ $this->actions()->add(Link::create(
+ $this->translate('Add to Basket'),
+ 'director/basket/add',
+ [
+ 'type' => $type,
+ 'names' => $object->getUniqueIdentifier()
+ ],
+ ['class' => 'icon-tag']
+ ));
+ }
+ }
+
+ return $this;
+ }
+
+ protected function addTemplate()
+ {
+ $this->assertPermission('director/admin');
+ $this->addTitle(
+ $this->translate('Add new Icinga %s template'),
+ $this->getTranslatedType()
+ );
+ }
+
+ protected function addObject()
+ {
+ $this->assertTypePermission();
+ $imports = $this->params->get('imports');
+ if (is_string($imports) && strlen($imports)) {
+ $this->addTitle(
+ $this->translate('Add %s: %s'),
+ $this->getTranslatedType(),
+ $imports
+ );
+ } else {
+ $this->addTitle(
+ $this->translate('Add new Icinga %s'),
+ $this->getTranslatedType()
+ );
+ }
+ }
+
+ protected function redirectToPreviewForExternals()
+ {
+ if ($this->object
+ && $this->object->isExternal()
+ && ! in_array($this->object->getShortTableName(), $this->allowedExternals)
+ ) {
+ $this->redirectNow(
+ $this->getRequest()->getUrl()->setPath(sprintf('director/%s/render', $this->getType()))
+ );
+ }
+
+ return $this;
+ }
+
+ protected function getType()
+ {
+ if ($this->type === null) {
+ // Strip final 's' and upcase an eventual 'group'
+ $this->type = preg_replace(
+ array('/group$/', '/period$/', '/argument$/', '/apiuser$/', '/set$/'),
+ array('Group', 'Period', 'Argument', 'ApiUser', 'Set'),
+ $this->getRequest()->getControllerName()
+ );
+ }
+
+ return $this->type;
+ }
+
+ protected function getPluralType()
+ {
+ return $this->getType() . 's';
+ }
+
+ protected function getTranslatedType()
+ {
+ return $this->translate(ucfirst($this->getType()));
+ }
+
+ protected function assertTypePermission()
+ {
+ $type = strtolower($this->getPluralType());
+ // TODO: Check getPluralType usage, fix it there.
+ if ($type === 'scheduleddowntimes') {
+ $type = 'scheduled-downtimes';
+ }
+
+ return $this->assertPermission("director/$type");
+ }
+
+ protected function loadOptionalObject()
+ {
+ if ($this->params->get('uuid') || null !== $this->params->get('name') || $this->params->get('id')) {
+ $this->loadObject();
+ }
+ }
+
+ /**
+ * @return ?UuidInterface
+ * @throws InvalidPropertyException
+ * @throws NotFoundError
+ */
+ protected function getUuidFromUrl()
+ {
+ $key = null;
+ if ($uuid = $this->params->get('uuid')) {
+ $key = Uuid::fromString($uuid);
+ } elseif ($id = $this->params->get('id')) {
+ $key = (int) $id;
+ } elseif (null !== ($name = $this->params->get('name'))) {
+ $key = $name;
+ }
+ if ($key === null) {
+ $request = $this->getRequest();
+ if ($request->isApiRequest() && $request->isGet()) {
+ $this->getResponse()->setHttpResponseCode(422);
+
+ throw new InvalidPropertyException(
+ 'Cannot load object, missing parameters'
+ );
+ }
+
+ return null;
+ }
+
+ return $this->requireUuid($key);
+ }
+
+ protected function loadObject()
+ {
+ if ($this->object) {
+ throw new ProgrammingError('Loading an object twice is not very efficient');
+ }
+
+ $this->object = $this->loadSpecificObject($this->getTableName(), $this->getUuidFromUrl(), true);
+ }
+
+ protected function loadSpecificObject($tableName, $key, $showHint = false)
+ {
+ $branch = $this->getBranch();
+ $branchedObject = BranchedObject::load($this->db(), $tableName, $key, $branch);
+ $object = $branchedObject->getBranchedDbObject($this->db());
+ assert($object instanceof IcingaObject);
+ $object->setBeingLoadedFromDb();
+ if (! $this->allowsObject($object)) {
+ throw new NotFoundError('No such object available');
+ }
+ if ($showHint && $branch->isBranch() && $object->isObject() && ! $this->getRequest()->isApiRequest()) {
+ $this->content()->add(new BranchedObjectHint($branch, $this->Auth(), $branchedObject));
+ }
+
+ return $object;
+ }
+
+ protected function requireUuid($key)
+ {
+ if (! $key instanceof UuidInterface) {
+ $key = UuidLookup::findUuidForKey($key, $this->getTableName(), $this->db(), $this->getBranch());
+ if ($key === null) {
+ throw new NotFoundError('No such object available');
+ }
+ }
+
+ return $key;
+ }
+
+ protected function getTableName()
+ {
+ return DbObjectTypeRegistry::tableNameByType($this->getType());
+ }
+
+ protected function addDeploymentLink()
+ {
+ try {
+ $info = new DeploymentInfo($this->db());
+ $info->setObject($this->object);
+
+ if (! $this->getRequest()->isApiRequest()) {
+ if ($this->getBranch()->isBranch()) {
+ $this->actions()->add($this->linkToMergeBranch($this->getBranch()));
+ } else {
+ $this->actions()->add(
+ DeploymentLinkForm::create(
+ $this->db(),
+ $info,
+ $this->Auth(),
+ $this->api()
+ )->handleRequest()
+ );
+ }
+ }
+ } catch (IcingaException $e) {
+ // pass (deployment may not be set up yet)
+ }
+ }
+
+ protected function linkToMergeBranch(Branch $branch)
+ {
+ $link = Branch::requireHook()->linkToBranch($branch, $this->Auth(), $this->translate('Merge'));
+ if ($link instanceof Link) {
+ $link->addAttributes(['class' => 'icon-flapping']);
+ }
+
+ return $link;
+ }
+
+ protected function addBackToObjectLink()
+ {
+ $params = [
+ 'uuid' => $this->object->getUniqueId()->toString(),
+ ];
+
+ if ($this->object instanceof IcingaService) {
+ if (($host = $this->object->get('host')) !== null) {
+ $params['host'] = $host;
+ } elseif (($set = $this->object->get('service_set')) !== null) {
+ $params['set'] = $set;
+ }
+ }
+
+ $this->actions()->add(Link::create(
+ $this->translate('back'),
+ $this->getObjectBaseUrl(),
+ $params,
+ ['class' => 'icon-left-big']
+ ));
+
+ return $this;
+ }
+
+ protected function addObjectForm(IcingaObject $object = null)
+ {
+ $form = $this->loadObjectForm($object);
+ $this->content()->add($form);
+ $form->handleRequest();
+ return $this;
+ }
+
+ protected function loadObjectForm(IcingaObject $object = null)
+ {
+ /** @var DirectorObjectForm $class */
+ $class = sprintf(
+ 'Icinga\\Module\\Director\\Forms\\Icinga%sForm',
+ ucfirst($this->getType())
+ );
+
+ $form = $class::load()
+ ->setDb($this->db())
+ ->setAuth($this->Auth());
+
+ if ($object !== null) {
+ $form->setObject($object);
+ }
+ if (true || $form->supportsBranches()) {
+ $form->setBranch($this->getBranch());
+ }
+
+ $this->onObjectFormLoaded($form);
+
+ return $form;
+ }
+
+ protected function getObjectBaseUrl()
+ {
+ return $this->objectBaseUrl ?: 'director/' . strtolower($this->getType());
+ }
+
+ protected function hasBasketSupport()
+ {
+ return $this->object->isTemplate() || $this->object->isGroup();
+ }
+
+ protected function onObjectFormLoaded(DirectorObjectForm $form)
+ {
+ }
+
+ /**
+ * @return IcingaObject
+ * @throws NotFoundError
+ */
+ protected function requireObject()
+ {
+ if (! $this->object) {
+ $this->getResponse()->setHttpResponseCode(404);
+ if (null === $this->params->get('name')) {
+ throw new NotFoundError('You need to pass a "name" parameter to access a specific object');
+ } else {
+ throw new NotFoundError('No such object available');
+ }
+ }
+
+ return $this->object;
+ }
+
+ protected function showOptionalBranchActivity($activityTable)
+ {
+ $branch = $this->getBranch();
+ if ($branch->isBranch() && (int) $this->params->get('page', '1') === 1) {
+ $table = new BranchActivityTable($branch->getUuid(), $this->db(), $this->object->getUniqueId());
+ if (count($table) > 0) {
+ $this->content()->add(Hint::info(Html::sprintf($this->translate(
+ 'The following modifications are visible in this %s only...'
+ ), Branch::requireHook()->linkToBranch(
+ $branch,
+ $this->Auth(),
+ $this->translate('configuration branch')
+ ))));
+ $this->content()->add($table);
+ if (count($activityTable) === 0) {
+ return;
+ }
+ $this->content()->add(Html::tag('br'));
+ $this->content()->add(Hint::ok($this->translate(
+ '...and the modifications below are already in the main branch:'
+ )));
+ $this->content()->add(Html::tag('br'));
+ }
+ }
+ }
+}
diff --git a/library/Director/Web/Controller/ObjectsController.php b/library/Director/Web/Controller/ObjectsController.php
new file mode 100644
index 0000000..8c10b44
--- /dev/null
+++ b/library/Director/Web/Controller/ObjectsController.php
@@ -0,0 +1,548 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Forms\IcingaMultiEditForm;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\RestApi\IcingaObjectsHandler;
+use Icinga\Module\Director\Web\ActionBar\ObjectsActionBar;
+use Icinga\Module\Director\Web\ActionBar\TemplateActionBar;
+use Icinga\Module\Director\Web\Form\FormLoader;
+use Icinga\Module\Director\Web\Table\ApplyRulesTable;
+use Icinga\Module\Director\Web\Table\ObjectSetTable;
+use Icinga\Module\Director\Web\Table\ObjectsTable;
+use Icinga\Module\Director\Web\Table\TemplatesTable;
+use Icinga\Module\Director\Web\Tabs\ObjectsTabs;
+use Icinga\Module\Director\Web\Tree\TemplateTreeRenderer;
+use gipfl\IcingaWeb2\Link;
+use Icinga\Module\Director\Web\Widget\AdditionalTableActions;
+use Icinga\Module\Director\Web\Widget\BranchedObjectsHint;
+use InvalidArgumentException;
+use Ramsey\Uuid\Uuid;
+
+abstract class ObjectsController extends ActionController
+{
+ use BranchHelper;
+
+ protected $isApified = true;
+
+ /** @var ObjectsTable */
+ protected $table;
+
+ protected function checkDirectorPermissions()
+ {
+ if ($this->getRequest()->getActionName() !== 'sets') {
+ $this->assertPermission('director/' . $this->getPluralBaseType());
+ }
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ */
+ protected function addObjectsTabs()
+ {
+ $tabName = $this->getRequest()->getActionName();
+ if (substr($this->getType(), -5) === 'Group') {
+ $tabName = 'groups';
+ }
+ $this->tabs(new ObjectsTabs(
+ $this->getBaseType(),
+ $this->Auth(),
+ $this->getBaseObjectUrl()
+ ))->activate($tabName);
+
+ return $this;
+ }
+
+ /**
+ * @return IcingaObjectsHandler
+ * @throws NotFoundError
+ */
+ protected function apiRequestHandler()
+ {
+ $request = $this->getRequest();
+ $table = $this->getTable();
+ if ($request->getControllerName() === 'services'
+ && $host = $this->params->get('host')
+ ) {
+ $host = IcingaHost::load($host, $this->db());
+ $table->getQuery()->where('o.host_id = ?', $host->get('id'));
+ }
+
+ if ($request->getActionName() === 'templates') {
+ $table->filterObjectType('template');
+ } elseif ($request->getActionName() === 'applyrules') {
+ $table->filterObjectType('apply');
+ }
+ $search = $this->params->get('q');
+ if ($search !== null && \strlen($search) > 0) {
+ $table->search($search);
+ }
+
+ return (new IcingaObjectsHandler(
+ $request,
+ $this->getResponse(),
+ $this->db()
+ ))->setTable($table);
+ }
+
+ /**
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws NotFoundError
+ */
+ public function indexAction()
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $type = $this->getType();
+ if ($this->params->get('format') === 'json') {
+ $filename = sprintf(
+ "director-${type}_%s.json",
+ date('YmdHis')
+ );
+ $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true);
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $this
+ ->addObjectsTabs()
+ ->setAutorefreshInterval(10)
+ ->addTitle($this->translate(ucfirst($this->getPluralType())))
+ ->actions(new ObjectsActionBar($this->getBaseObjectUrl(), $this->url()));
+
+ $this->content()->add(new BranchedObjectsHint($this->getBranch(), $this->Auth()));
+
+ if ($type === 'command' && $this->params->get('type') === 'external_object') {
+ $this->tabs()->activate('external');
+ }
+
+ // Hint: might be used in controllers extending this
+ $this->table = $this->eventuallyFilterCommand($this->getTable());
+
+ $this->table->renderTo($this);
+ (new AdditionalTableActions($this->getAuth(), $this->url(), $this->table))
+ ->appendTo($this->actions());
+ }
+
+ /**
+ * @return ObjectsTable
+ */
+ protected function getTable()
+ {
+ $table = ObjectsTable::create($this->getType(), $this->db())
+ ->setAuth($this->getAuth())
+ ->setBranchUuid($this->getBranchUuid())
+ ->setBaseObjectUrl($this->getBaseObjectUrl());
+
+ return $table;
+ }
+
+ /**
+ * @return ApplyRulesTable
+ * @throws NotFoundError
+ */
+ protected function getApplyRulesTable()
+ {
+ $table = new ApplyRulesTable($this->db());
+ $table->setType($this->getType())
+ ->setBaseObjectUrl($this->getBaseObjectUrl());
+ $this->eventuallyFilterCommand($table);
+
+ return $table;
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function edittemplatesAction()
+ {
+ $this->commonForEdit();
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function editAction()
+ {
+ $this->commonForEdit();
+ }
+
+ /**
+ * @throws NotFoundError
+ */
+ public function commonForEdit()
+ {
+ $type = ucfirst($this->getType());
+
+ if (empty($this->multiEdit)) {
+ throw new NotFoundError('Cannot edit multiple "%s" instances', $type);
+ }
+
+ $objects = $this->loadMultiObjectsFromParams();
+ if (empty($objects)) {
+ throw new NotFoundError('No "%s" instances have been loaded', $type);
+ }
+ $formName = 'icinga' . $type;
+ $form = IcingaMultiEditForm::load()
+ ->setBranch($this->getBranch())
+ ->setObjects($objects)
+ ->pickElementsFrom($this->loadForm($formName), $this->multiEdit);
+ if ($type === 'Service') {
+ $form->setListUrl('director/services');
+ } elseif ($type === 'Host') {
+ $form->setListUrl('director/hosts');
+ }
+
+ $form->handleRequest();
+
+ $this
+ ->addSingleTab($this->translate('Multiple objects'))
+ ->addTitle(
+ $this->translate('Modify %d objects'),
+ count($objects)
+ )->content()->add($form);
+ }
+
+ /**
+ * Loads the TemplatesTable or the TemplateTreeRenderer
+ *
+ * Passing render=tree switches to the tree view.
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws \Icinga\Security\SecurityException
+ * @throws NotFoundError
+ */
+ public function templatesAction()
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+ $type = $this->getType();
+
+ if ($this->params->get('format') === 'json') {
+ $filename = sprintf(
+ "director-${type}-templates_%s.json",
+ date('YmdHis')
+ );
+ $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true);
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $shortType = IcingaObject::createByType($type)->getShortTableName();
+ $this
+ ->assertPermission('director/admin')
+ ->addObjectsTabs()
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('All your %s Templates'),
+ $this->translate(ucfirst($type))
+ )
+ ->actions(new TemplateActionBar($shortType, $this->url()));
+
+ if ($this->params->get('render') === 'tree') {
+ TemplateTreeRenderer::showType($shortType, $this, $this->db());
+ } else {
+ $table = TemplatesTable::create($shortType, $this->db());
+ $this->eventuallyFilterCommand($table);
+ $table->renderTo($this);
+ (new AdditionalTableActions($this->getAuth(), $this->url(), $table))
+ ->appendTo($this->actions());
+ }
+ }
+
+ /**
+ * @return $this
+ * @throws \Icinga\Security\SecurityException
+ */
+ protected function assertApplyRulePermission()
+ {
+ return $this->assertPermission('director/admin');
+ }
+
+ /**
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws \Icinga\Security\SecurityException
+ * @throws NotFoundError
+ */
+ public function applyrulesAction()
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $type = $this->getType();
+
+ if ($this->params->get('format') === 'json') {
+ $filename = sprintf(
+ "director-${type}-applyrules_%s.json",
+ date('YmdHis')
+ );
+ $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true);
+ $this->apiRequestHandler()->dispatch();
+ return;
+ }
+
+ $tType = $this->translate(ucfirst($type));
+ $this
+ ->assertApplyRulePermission()
+ ->addObjectsTabs()
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('All your %s Apply Rules'),
+ $tType
+ );
+ $baseUrl = 'director/' . $this->getBaseObjectUrl();
+ $this->actions()
+ //->add($this->getBackToDashboardLink())
+ ->add(
+ Link::create(
+ $this->translate('Add'),
+ "${baseUrl}/add",
+ ['type' => 'apply'],
+ [
+ 'title' => sprintf(
+ $this->translate('Create a new %s Apply Rule'),
+ $tType
+ ),
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next'
+ ]
+ )
+ );
+
+ $table = $this->getApplyRulesTable();
+ $table->renderTo($this);
+ (new AdditionalTableActions($this->getAuth(), $this->url(), $table))
+ ->appendTo($this->actions());
+ }
+
+ /**
+ * @throws NotFoundError
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws \Icinga\Security\SecurityException
+ */
+ public function setsAction()
+ {
+ $type = $this->getType();
+ $tType = $this->translate(ucfirst($type));
+ $this
+ ->assertPermission('director/' . $this->getBaseType() . 'sets')
+ ->addObjectsTabs()
+ ->requireSupportFor('Sets')
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('Icinga %s Sets'),
+ $tType
+ );
+
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Add'),
+ "director/${type}set/add",
+ null,
+ [
+ 'title' => sprintf(
+ $this->translate('Create a new %s Set'),
+ $tType
+ ),
+ 'class' => 'icon-plus',
+ 'data-base-target' => '_next'
+ ]
+ )
+ );
+
+ ObjectSetTable::create($type, $this->db(), $this->getAuth())
+ ->setBranch($this->getBranch())
+ ->renderTo($this);
+ }
+
+ /**
+ * @return array
+ * @throws NotFoundError
+ */
+ protected function loadMultiObjectsFromParams()
+ {
+ $filter = Filter::fromQueryString($this->params->toString());
+ $type = $this->getType();
+ $objects = array();
+ $db = $this->db();
+ $class = DbObjectTypeRegistry::classByType($type);
+ $table = DbObjectTypeRegistry::tableNameByType($type);
+ $store = new DbObjectStore($db, $this->getBranch());
+
+ /** @var $filter FilterChain */
+ foreach ($filter->filters() as $sub) {
+ /** @var $sub FilterChain */
+ foreach ($sub->filters() as $ex) {
+ /** @var $ex FilterChain|FilterExpression */
+ $col = $ex->getColumn();
+ if ($ex->isExpression()) {
+ if ($col === 'name') {
+ $name = $ex->getExpression();
+ if ($type === 'service') {
+ $key = [
+ 'object_type' => 'template',
+ 'object_name' => $name
+ ];
+ } else {
+ $key = $name;
+ }
+ $objects[$name] = $class::load($key, $db);
+ } elseif ($col === 'id') {
+ $name = $ex->getExpression();
+ $objects[$name] = $class::load($name, $db);
+ } elseif ($col === 'uuid') {
+ $object = $store->load($table, Uuid::fromString($ex->getExpression()));
+ $objects[$object->getObjectName()] = $object;
+ } else {
+ throw new InvalidArgumentException("'$col' is no a valid key component for '$type'");
+ }
+ }
+ }
+ }
+
+ return $objects;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return \Icinga\Module\Director\Web\Form\QuickForm
+ */
+ public function loadForm($name)
+ {
+ $form = FormLoader::load($name, $this->Module());
+ if ($this->getRequest()->isApiRequest()) {
+ // TODO: Ask form for API support?
+ $form->setApiRequest();
+ }
+
+ return $form;
+ }
+
+ /**
+ * @param ZfQueryBasedTable $table
+ * @return ZfQueryBasedTable
+ * @throws NotFoundError
+ */
+ protected function eventuallyFilterCommand(ZfQueryBasedTable $table)
+ {
+ if ($this->params->get('command')) {
+ $command = IcingaCommand::load($this->params->get('command'), $this->db());
+ switch ($this->getBaseType()) {
+ case 'host':
+ case 'service':
+ $table->getQuery()->where(
+ $this->db()->getDbAdapter()->quoteInto(
+ '(o.check_command_id = ? OR o.event_command_id = ?)',
+ $command->getAutoincId()
+ )
+ );
+ break;
+ case 'notification':
+ $table->getQuery()->where(
+ 'o.command_id = ?',
+ $command->getAutoincId()
+ );
+ break;
+ }
+ }
+
+ return $table;
+ }
+
+ /**
+ * @param $feature
+ * @return $this
+ * @throws NotFoundError
+ */
+ protected function requireSupportFor($feature)
+ {
+ if ($this->supports($feature) !== true) {
+ throw new NotFoundError(
+ '%s does not support %s',
+ $this->getType(),
+ $feature
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $feature
+ * @return bool
+ */
+ protected function supports($feature)
+ {
+ $func = "supports$feature";
+ return IcingaObject::createByType($this->getType())->$func();
+ }
+
+ /**
+ * @return string
+ */
+ protected function getBaseType()
+ {
+ $type = $this->getType();
+ if (substr($type, -5) === 'Group') {
+ return substr($type, 0, -5);
+ } else {
+ return $type;
+ }
+ }
+
+ protected function getBaseObjectUrl()
+ {
+ return $this->getType();
+ }
+
+ /**
+ * @return string
+ */
+ protected function getType()
+ {
+ // Strip final 's' and upcase an eventual 'group'
+ return preg_replace(
+ array('/group$/', '/period$/', '/argument$/', '/apiuser$/', '/dependencie$/', '/set$/'),
+ array('Group', 'Period', 'Argument', 'ApiUser', 'dependency', 'Set'),
+ str_replace(
+ 'template',
+ '',
+ substr($this->getRequest()->getControllerName(), 0, -1)
+ )
+ );
+ }
+
+ /**
+ * @return string
+ */
+ protected function getPluralType()
+ {
+ return preg_replace('/cys$/', 'cies', $this->getType() . 's');
+ }
+
+ /**
+ * @return string
+ */
+ protected function getPluralBaseType()
+ {
+ return preg_replace('/cys$/', 'cies', $this->getBaseType() . 's');
+ }
+}
diff --git a/library/Director/Web/Controller/TemplateController.php b/library/Director/Web/Controller/TemplateController.php
new file mode 100644
index 0000000..c368a82
--- /dev/null
+++ b/library/Director/Web/Controller/TemplateController.php
@@ -0,0 +1,243 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Controller;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Controller\Extension\DirectorDb;
+use Icinga\Module\Director\Web\Table\ApplyRulesTable;
+use Icinga\Module\Director\Web\Table\ObjectsTable;
+use Icinga\Module\Director\Web\Table\TemplatesTable;
+use Icinga\Module\Director\Web\Table\TemplateUsageTable;
+use Icinga\Module\Director\Web\Tabs\ObjectTabs;
+use Icinga\Module\Director\Web\Widget\UnorderedList;
+use ipl\Html\FormattedString;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\CompatController;
+
+abstract class TemplateController extends CompatController
+{
+ use DirectorDb;
+
+ /** @var IcingaObject */
+ protected $template;
+
+ public function objectsAction()
+ {
+ $template = $this->requireTemplate();
+ $plural = $this->getTranslatedPluralType();
+ $this
+ ->addSingleTab($plural)
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('%s based on %s'),
+ $plural,
+ $template->getObjectName()
+ )->addBackToUsageLink($template);
+
+ ObjectsTable::create($this->getType(), $this->db())
+ ->setAuth($this->Auth())
+ ->setBaseObjectUrl($this->getBaseObjectUrl())
+ ->filterTemplate($template, $this->getInheritance())
+ ->renderTo($this);
+ }
+
+ public function applyrulesAction()
+ {
+ $type = $this->getType();
+ $template = $this->requireTemplate();
+ $this
+ ->addSingleTab(sprintf($this->translate('Applied %s'), $this->getTranslatedPluralType()))
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('Notification Apply Rules based on %s'),
+ $template->getObjectName()
+ )->addBackToUsageLink($template);
+
+ ApplyRulesTable::create($type, $this->db())
+ ->setBaseObjectUrl($this->getBaseObjectUrl())
+ ->filterTemplate($template, $this->params->get('inheritance', 'direct'))
+ ->renderTo($this);
+ }
+
+ public function templatesAction()
+ {
+ $template = $this->requireTemplate();
+ $typeName = $this->getTranslatedType();
+ $this
+ ->addSingleTab(sprintf($this->translate('%s Templates'), $typeName))
+ ->setAutorefreshInterval(10)
+ ->addTitle(
+ $this->translate('%s templates based on %s'),
+ $typeName,
+ $template->getObjectName()
+ )->addBackToUsageLink($template);
+
+ TemplatesTable::create($this->getType(), $this->db())
+ ->filterTemplate($template, $this->getInheritance())
+ ->renderTo($this);
+ }
+
+ protected function getInheritance()
+ {
+ return $this->params->get('inheritance', 'direct');
+ }
+
+ protected function addBackToUsageLink(IcingaObject $template)
+ {
+ $type = $this->getType();
+ $this->actions()->add(
+ Link::create(
+ $this->translate('Back'),
+ "director/${type}template/usage",
+ ['name' => $template->getObjectName()],
+ ['class' => 'icon-left-big']
+ )
+ );
+
+ return $this;
+ }
+
+ public function usageAction()
+ {
+ $template = $this->requireTemplate();
+ if (! $template->isTemplate() && $template instanceof IcingaCommand) {
+ $this->redirectNow($this->url()->setPath('director/command'));
+ }
+ $templateName = $template->getObjectName();
+
+ $type = $this->getType();
+ $this->tabs(new ObjectTabs($type, $this->Auth(), $template))->activate('modify');
+ $this
+ ->addTitle($this->translate('Template: %s'), $templateName)
+ ->setAutorefreshInterval(10);
+
+ $this->actions()->add([
+ Link::create(
+ $this->translate('Modify'),
+ "director/$type/edit",
+ ['uuid' => $template->getUniqueId()->toString()],
+ ['class' => 'icon-edit']
+ )
+ ]);
+ if ($template instanceof ExportInterface) {
+ $this->actions()->add(Link::create(
+ $this->translate('Add to Basket'),
+ 'director/basket/add',
+ [
+ 'type' => ucfirst($this->getType()) . 'Template',
+ 'names' => $template->getUniqueIdentifier()
+ ],
+ ['class' => 'icon-tag']
+ ));
+ }
+
+ $list = new UnorderedList([], [
+ 'class' => 'vertical-action-list'
+ ]);
+
+ $auth = $this->Auth();
+
+ if ($type !== 'notification') {
+ $list->addItem(new FormattedString(
+ $this->translate('Create a new %s inheriting from this template'),
+ [Link::create(
+ $this->translate('Object'),
+ "director/$type/add",
+ ['imports' => $templateName, 'type' => 'object']
+ )]
+ ));
+ }
+ if ($auth->hasPermission('director/admin')) {
+ $list->addItem(new FormattedString(
+ $this->translate('Create a new %s inheriting from this one'),
+ [Link::create(
+ $this->translate('Template'),
+ "director/$type/add",
+ ['imports' => $templateName, 'type' => 'template']
+ )]
+ ));
+ }
+ if ($template->supportsApplyRules()) {
+ $list->addItem(new FormattedString(
+ $this->translate('Create a new %s inheriting from this template'),
+ [Link::create(
+ $this->translate('Apply Rule'),
+ "director/$type/add",
+ ['imports' => $templateName, 'type' => 'apply']
+ )]
+ ));
+ }
+
+ $typeName = $this->getTranslatedType();
+ $this->content()->add(Html::sprintf(
+ $this->translate(
+ 'This is the "%s" %s Template. Based on this, you might want to:'
+ ),
+ $typeName,
+ $templateName
+ ))->add(
+ $list
+ )->add(
+ Html::tag('h2', null, $this->translate('Current Template Usage'))
+ );
+
+ try {
+ $this->content()->add(
+ TemplateUsageTable::forTemplate($template)
+ );
+ } catch (NestingError $e) {
+ $this->content()->add(Hint::error($e->getMessage()));
+ }
+ }
+
+ protected function getType()
+ {
+ return $this->template()->getShortTableName();
+ }
+
+ protected function getPluralType()
+ {
+ return preg_replace(
+ '/cys$/',
+ 'cies',
+ $this->template()->getShortTableName() . 's'
+ );
+ }
+
+ protected function getTranslatedType()
+ {
+ return $this->translate(ucfirst($this->getType()));
+ }
+
+ protected function getTranslatedPluralType()
+ {
+ return $this->translate(ucfirst($this->getPluralType()));
+ }
+
+ protected function getBaseObjectUrl()
+ {
+ return $this->getType();
+ }
+
+ /**
+ * @return IcingaObject
+ */
+ protected function template()
+ {
+ if ($this->template === null) {
+ $this->template = $this->requireTemplate();
+ }
+
+ return $this->template;
+ }
+
+ /**
+ * @return IcingaObject
+ */
+ abstract protected function requireTemplate();
+}
diff --git a/library/Director/Web/Form/ClickHereForm.php b/library/Director/Web/Form/ClickHereForm.php
new file mode 100644
index 0000000..abba9d7
--- /dev/null
+++ b/library/Director/Web/Form/ClickHereForm.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\Web\InlineForm;
+
+class ClickHereForm extends InlineForm
+{
+ use TranslationHelper;
+
+ protected $hasBeenClicked = false;
+
+ protected function assemble()
+ {
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('here'),
+ 'class' => 'link-button'
+ ]);
+ }
+
+ public function hasBeenClicked()
+ {
+ return $this->hasBeenClicked;
+ }
+
+ public function onSuccess()
+ {
+ $this->hasBeenClicked = true;
+ }
+}
diff --git a/library/Director/Web/Form/CloneImportSourceForm.php b/library/Director/Web/Form/CloneImportSourceForm.php
new file mode 100644
index 0000000..0849dd4
--- /dev/null
+++ b/library/Director/Web/Form/CloneImportSourceForm.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Module\Director\Data\Exporter;
+use ipl\Html\Form;
+use ipl\Html\FormDecorator\DdDtDecorator;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Objects\ImportSource;
+
+class CloneImportSourceForm extends Form
+{
+ use TranslationHelper;
+
+ /** @var ImportSource */
+ protected $source;
+
+ /** @var ImportSource|null */
+ protected $newSource;
+
+ public function __construct(ImportSource $source)
+ {
+ $this->setDefaultElementDecorator(new DdDtDecorator());
+ $this->source = $source;
+ }
+
+ protected function assemble()
+ {
+ $this->addElement('text', 'source_name', [
+ 'label' => $this->translate('New name'),
+ 'value' => $this->source->get('source_name'),
+ ]);
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('Clone')
+ ]);
+ }
+
+ /**
+ * @return \Icinga\Module\Director\Db
+ */
+ protected function getTargetDb()
+ {
+ return $this->source->getConnection();
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ $db = $this->getTargetDb();
+ $export = (new Exporter($db))->export($this->source);
+ $newName = $this->getElement('source_name')->getValue();
+ $export->source_name = $newName;
+ unset($export->originalId);
+ if (ImportSource::existsWithName($newName, $db)) {
+ $this->getElement('source_name')->addMessage('Name already exists');
+ }
+ $this->newSource = ImportSource::import($export, $db);
+ $this->newSource->store();
+ }
+
+ public function getSuccessUrl()
+ {
+ if ($this->newSource === null) {
+ return parent::getSuccessUrl();
+ } else {
+ return Url::fromPath('director/importsource', ['id' => $this->newSource->get('id')]);
+ }
+ }
+}
diff --git a/library/Director/Web/Form/CloneSyncRuleForm.php b/library/Director/Web/Form/CloneSyncRuleForm.php
new file mode 100644
index 0000000..f90b593
--- /dev/null
+++ b/library/Director/Web/Form/CloneSyncRuleForm.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Module\Director\Data\Exporter;
+use ipl\Html\Form;
+use ipl\Html\FormDecorator\DdDtDecorator;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class CloneSyncRuleForm extends Form
+{
+ use TranslationHelper;
+
+ /** @var SyncRule */
+ protected $rule;
+
+ /** @var SyncRule|null */
+ protected $newRule;
+
+ public function __construct(SyncRule $rule)
+ {
+ $this->setDefaultElementDecorator(new DdDtDecorator());
+ $this->rule = $rule;
+ }
+
+ protected function assemble()
+ {
+ $this->addElement('text', 'rule_name', [
+ 'label' => $this->translate('New name'),
+ 'value' => $this->rule->get('rule_name'),
+ ]);
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('Clone')
+ ]);
+ }
+
+ /**
+ * @return \Icinga\Module\Director\Db
+ */
+ protected function getTargetDb()
+ {
+ return $this->rule->getConnection();
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ $db = $this->getTargetDb();
+ $exporter = new Exporter($db);
+
+ $export = $exporter->export($this->rule);
+ $newName = $this->getValue('rule_name');
+ $export->rule_name = $newName;
+ unset($export->originalId);
+
+ if (SyncRule::existsWithName($newName, $db)) {
+ $this->getElement('rule_name')->addMessage('Name already exists');
+ }
+ $this->newRule = SyncRule::import($export, $db);
+ $this->newRule->store();
+ }
+
+ public function getSuccessUrl()
+ {
+ if ($this->newRule === null) {
+ return parent::getSuccessUrl();
+ } else {
+ return Url::fromPath('director/syncrule', ['id' => $this->newRule->get('id')]);
+ }
+ }
+}
diff --git a/library/Director/Web/Form/CsrfToken.php b/library/Director/Web/Form/CsrfToken.php
new file mode 100644
index 0000000..24edf88
--- /dev/null
+++ b/library/Director/Web/Form/CsrfToken.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+class CsrfToken
+{
+ /**
+ * Check whether the given token is valid
+ *
+ * @param string $token Token
+ *
+ * @return bool
+ */
+ public static function isValid($token)
+ {
+ if (strpos($token, '|') === false) {
+ return false;
+ }
+
+ list($seed, $token) = explode('|', $elementValue);
+
+ if (!is_numeric($seed)) {
+ return false;
+ }
+
+ return $token === hash('sha256', self::getSessionId() . $seed);
+ }
+
+ /**
+ * Create a new token
+ *
+ * @return string
+ */
+ public static function generate()
+ {
+ $seed = mt_rand();
+ $token = hash('sha256', self::getSessionId() . $seed);
+
+ return sprintf('%s|%s', $seed, $token);
+ }
+
+ /**
+ * Get current session id
+ *
+ * TODO: we should do this through our App or Session object
+ *
+ * @return string
+ */
+ protected static function getSessionId()
+ {
+ return session_id();
+ }
+}
diff --git a/library/Director/Web/Form/DbSelectorForm.php b/library/Director/Web/Form/DbSelectorForm.php
new file mode 100644
index 0000000..52fe5ea
--- /dev/null
+++ b/library/Director/Web/Form/DbSelectorForm.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use gipfl\IcingaWeb2\Url;
+use Icinga\Web\Response;
+use ipl\Html\Form;
+use Icinga\Web\Window;
+
+class DbSelectorForm extends Form
+{
+ protected $defaultAttributes = [
+ 'class' => 'db-selector'
+ ];
+
+ protected $allowedNames;
+
+ /** @var Window */
+ protected $window;
+
+ protected $response;
+
+ public function __construct(Response $response, Window $window, $allowedNames)
+ {
+ $this->response = $response;
+ $this->window = $window;
+ $this->allowedNames = $allowedNames;
+ }
+
+ protected function assemble()
+ {
+ $this->addElement('hidden', 'DbSelector', [
+ 'value' => 'sent'
+ ]);
+ $this->addElement('select', 'db_resource', [
+ 'options' => $this->allowedNames,
+ 'class' => 'autosubmit',
+ 'value' => $this->getSession()->get('db_resource')
+ ]);
+ }
+
+ /**
+ * A base class should handle this, based on hidden fields
+ *
+ * @return bool
+ */
+ public function hasBeenSubmitted()
+ {
+ return $this->hasBeenSent() && $this->getRequestParam('DbSelector') === 'sent';
+ }
+
+ public function onSuccess()
+ {
+ $this->getSession()->set('db_resource', $this->getElement('db_resource')->getValue());
+ $this->response->redirectAndExit(Url::fromRequest($this->getRequest()));
+ }
+
+ protected function getRequestParam($name, $default = null)
+ {
+ $request = $this->getRequest();
+ if ($request === null) {
+ return $default;
+ }
+ if ($request->getMethod() === 'POST') {
+ $params = $request->getParsedBody();
+ } elseif ($this->getMethod() === 'GET') {
+ parse_str($request->getUri()->getQuery(), $params);
+ } else {
+ $params = [];
+ }
+
+ if (array_key_exists($name, $params)) {
+ return $params[$name];
+ }
+
+ return $default;
+ }
+ /**
+ * @return \Icinga\Web\Session\SessionNamespace
+ */
+ protected function getSession()
+ {
+ return $this->window->getSessionNamespace('director');
+ }
+}
diff --git a/library/Director/Web/Form/Decorator/ViewHelperRaw.php b/library/Director/Web/Form/Decorator/ViewHelperRaw.php
new file mode 100644
index 0000000..a3aefbf
--- /dev/null
+++ b/library/Director/Web/Form/Decorator/ViewHelperRaw.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Decorator;
+
+use Zend_Form_Decorator_ViewHelper as ViewHelper;
+use Zend_Form_Element as Element;
+
+class ViewHelperRaw extends ViewHelper
+{
+ public function getValue($element)
+ {
+ return $element->getUnfilteredValue();
+ }
+}
diff --git a/library/Director/Web/Form/DirectorForm.php b/library/Director/Web/Form/DirectorForm.php
new file mode 100644
index 0000000..145be5b
--- /dev/null
+++ b/library/Director/Web/Form/DirectorForm.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Director\Db;
+
+abstract class DirectorForm extends QuickForm
+{
+ /** @var Db */
+ protected $db;
+
+ /**
+ * @param Db $db
+ * @return $this
+ */
+ public function setDb(Db $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * @return Db
+ */
+ public function getDb()
+ {
+ return $this->db;
+ }
+
+ /**
+ * @return static
+ */
+ public static function load()
+ {
+ return new static([
+ 'icingaModule' => Icinga::App()->getModuleManager()->getModule('director')
+ ]);
+ }
+
+ public function addBoolean($key, $options, $default = null)
+ {
+ if ($default === null) {
+ return $this->addElement('OptionalYesNo', $key, $options);
+ } else {
+ $this->addElement('YesNo', $key, $options);
+ return $this->getElement($key)->setValue($default);
+ }
+ }
+
+ protected function optionalBoolean($key, $label, $description)
+ {
+ return $this->addBoolean($key, array(
+ 'label' => $label,
+ 'description' => $description
+ ));
+ }
+}
diff --git a/library/Director/Web/Form/DirectorObjectForm.php b/library/Director/Web/Form/DirectorObjectForm.php
new file mode 100644
index 0000000..b70bd7b
--- /dev/null
+++ b/library/Director/Web/Form/DirectorObjectForm.php
@@ -0,0 +1,1734 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Exception;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Hook\IcingaObjectFormHook;
+use Icinga\Module\Director\IcingaConfig\StateFilterSet;
+use Icinga\Module\Director\IcingaConfig\TypeFilterSet;
+use Icinga\Module\Director\Objects\IcingaTemplateChoice;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Util;
+use Icinga\Module\Director\Web\Form\Element\ExtensibleSet;
+use Icinga\Module\Director\Web\Form\Validate\NamePattern;
+use Zend_Form_Element as ZfElement;
+use Zend_Form_Element_Select as ZfSelect;
+
+abstract class DirectorObjectForm extends DirectorForm
+{
+ const GROUP_ORDER_OBJECT_DEFINITION = 20;
+ const GROUP_ORDER_RELATED_OBJECTS = 25;
+ const GROUP_ORDER_ASSIGN = 30;
+ const GROUP_ORDER_CHECK_EXECUTION = 40;
+ const GROUP_ORDER_CUSTOM_FIELDS = 50;
+ const GROUP_ORDER_CUSTOM_FIELD_CATEGORIES = 60;
+ const GROUP_ORDER_EVENT_FILTERS = 700;
+ const GROUP_ORDER_EXTRA_INFO = 750;
+ const GROUP_ORDER_CLUSTERING = 800;
+ const GROUP_ORDER_BUTTONS = 1000;
+
+ /** @var IcingaObject */
+ protected $object;
+
+ /** @var Branch */
+ protected $branch;
+
+ protected $objectName;
+
+ protected $className;
+
+ protected $deleteButtonName;
+
+ protected $displayGroups = [];
+
+ protected $resolvedImports;
+
+ protected $listUrl;
+
+ /** @var Auth */
+ private $auth;
+
+ private $choiceElements = [];
+
+ protected $preferredObjectType;
+
+ /** @var IcingaObjectFieldLoader */
+ protected $fieldLoader;
+
+ private $allowsExperimental;
+
+ private $presetImports;
+
+ private $earlyProperties = array(
+ // 'imports',
+ 'check_command',
+ 'check_command_id',
+ 'has_agent',
+ 'command',
+ 'command_id',
+ 'event_command',
+ 'event_command_id',
+ );
+
+ public function setPreferredObjectType($type)
+ {
+ $this->preferredObjectType = $type;
+ return $this;
+ }
+
+ public function setAuth(Auth $auth)
+ {
+ $this->auth = $auth;
+ return $this;
+ }
+
+ public function getAuth()
+ {
+ if ($this->auth === null) {
+ $this->auth = Auth::getInstance();
+ }
+ return $this->auth;
+ }
+
+ protected function eventuallyAddNameRestriction($restrictionName)
+ {
+ $restrictions = $this->getAuth()->getRestrictions($restrictionName);
+ if (! empty($restrictions)) {
+ $this->getElement('object_name')->addValidator(
+ new NamePattern($restrictions)
+ );
+ }
+
+ return $this;
+ }
+
+ public function presetImports($imports)
+ {
+ if (! empty($imports)) {
+ if (is_array($imports)) {
+ $this->presetImports = $imports;
+ } else {
+ $this->presetImports = array($imports);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return DbObject|DbObjectWithSettings|IcingaObject
+ */
+ protected function object()
+ {
+ if ($this->object === null) {
+ $values = $this->getValues();
+ /** @var DbObject|IcingaObject $class */
+ $class = $this->getObjectClassname();
+ if ($this->preferredObjectType) {
+ $values['object_type'] = $this->preferredObjectType;
+ }
+ if ($this->presetImports) {
+ $values['imports'] = $this->presetImports;
+ }
+
+ $this->object = $class::create($values, $this->db);
+ } else {
+ if (! $this->object->hasConnection()) {
+ $this->object->setConnection($this->db);
+ }
+ }
+
+ return $this->object;
+ }
+
+ protected function extractChoicesFromPost($post)
+ {
+ $imports = [];
+
+ foreach ($this->choiceElements as $other) {
+ $name = $other->getName();
+ if (array_key_exists($name, $post)) {
+ $value = $post[$name];
+ if (is_string($value)) {
+ $imports[] = $value;
+ } elseif (is_array($value)) {
+ foreach ($value as $chosen) {
+ $imports[] = $chosen;
+ }
+ }
+ }
+ }
+
+ return $imports;
+ }
+
+ protected function assertResolvedImports()
+ {
+ if ($this->resolvedImports !== null) {
+ return $this->resolvedImports;
+ }
+
+ $object = $this->object;
+
+ if (! $object instanceof IcingaObject) {
+ return $this->setResolvedImports(false);
+ }
+ if (! $object->supportsImports()) {
+ return $this->setResolvedImports(false);
+ }
+
+ if ($this->hasBeenSent()) {
+ // prefill special properties, required to resolve fields and similar
+ $post = $this->getRequest()->getPost();
+
+ $key = 'imports';
+ if ($el = $this->getElement($key)) {
+ if (array_key_exists($key, $post)) {
+ $imports = $post[$key];
+ if (! is_array($imports)) {
+ $imports = array($imports);
+ }
+ $imports = array_filter(array_values(array_merge(
+ $imports,
+ $this->extractChoicesFromPost($post)
+ )), 'strlen');
+
+ /** @var ZfElement $el */
+ $this->populate([$key => $imports]);
+ $el->setValue($imports);
+ if (! $this->tryToSetObjectPropertyFromElement($object, $el, $key)) {
+ return $this->resolvedImports = false;
+ }
+ }
+ } elseif ($this->presetImports) {
+ $imports = array_values(array_merge(
+ $this->presetImports,
+ $this->extractChoicesFromPost($post)
+ ));
+ if (! $this->eventuallySetImports($imports)) {
+ return $this->resolvedImports = false;
+ }
+ } else {
+ if (! empty($this->choiceElements)) {
+ if (! $this->eventuallySetImports($this->extractChoicesFromPost($post))) {
+ return $this->resolvedImports = false;
+ }
+ }
+ }
+
+ foreach ($this->earlyProperties as $key) {
+ if ($el = $this->getElement($key)) {
+ if (array_key_exists($key, $post)) {
+ $this->populate([$key => $post[$key]]);
+ $this->tryToSetObjectPropertyFromElement($object, $el, $key);
+ }
+ }
+ }
+ }
+
+ try {
+ $object->listAncestorIds();
+ } catch (NestingError $e) {
+ $this->addUniqueErrorMessage($e->getMessage());
+ return $this->resolvedImports = false;
+ } catch (Exception $e) {
+ $this->addException($e, 'imports');
+ return $this->resolvedImports = false;
+ }
+
+ return $this->setResolvedImports();
+ }
+
+ protected function eventuallySetImports($imports)
+ {
+ try {
+ $this->object()->set('imports', $imports);
+ return true;
+ } catch (Exception $e) {
+ $this->addException($e, 'imports');
+ return false;
+ }
+ }
+
+ protected function tryToSetObjectPropertyFromElement(
+ IcingaObject $object,
+ ZfElement $element,
+ $key
+ ) {
+ $old = null;
+ try {
+ $old = $object->get($key);
+ $object->set($key, $element->getValue());
+ $object->resolveUnresolvedRelatedProperties();
+
+ if ($key === 'imports') {
+ $object->imports()->getObjects();
+ }
+ return true;
+ } catch (Exception $e) {
+ if ($old !== null) {
+ $object->set($key, $old);
+ }
+ $this->addException($e, $key);
+ return false;
+ }
+ }
+
+ public function setResolvedImports($resolved = true)
+ {
+ return $this->resolvedImports = $resolved;
+ }
+
+ public function isObject()
+ {
+ return $this->getSentOrObjectValue('object_type') === 'object';
+ }
+
+ public function isTemplate()
+ {
+ return $this->getSentOrObjectValue('object_type') === 'template';
+ }
+
+ // TODO: move to a subform
+ protected function handleRanges(IcingaObject $object, &$values)
+ {
+ if (! $object->supportsRanges()) {
+ return;
+ }
+
+ $key = 'ranges';
+ $object = $this->object();
+
+ /* Sample:
+
+ array(
+ 'monday' => 'eins',
+ 'tuesday' => '00:00-24:00',
+ 'sunday' => 'zwei',
+ );
+
+ */
+ if (array_key_exists($key, $values)) {
+ $object->ranges()->set($values[$key]);
+ unset($values[$key]);
+ }
+
+ foreach ($object->ranges()->getRanges() as $key => $value) {
+ $this->addRange($key, $value);
+ }
+ }
+
+ protected function addToCheckExecutionDisplayGroup($elements)
+ {
+ return $this->addElementsToGroup(
+ $elements,
+ 'check_execution',
+ self::GROUP_ORDER_CHECK_EXECUTION,
+ $this->translate('Check execution')
+ );
+ }
+
+ public function addElementsToGroup($elements, $group, $order, $legend = null)
+ {
+ if (! is_array($elements)) {
+ $elements = array($elements);
+ }
+
+ // These are optional elements, they might exist or not. We still want
+ // to see exception for other ones
+ $skipLegally = array('check_period_id');
+
+ $skip = array();
+ foreach ($elements as $k => $v) {
+ if (is_string($v)) {
+ $el = $this->getElement($v);
+ if (!$el && in_array($v, $skipLegally)) {
+ $skip[] = $k;
+ continue;
+ }
+
+ $elements[$k] = $el;
+ }
+ }
+
+ foreach ($skip as $k) {
+ unset($elements[$k]);
+ }
+
+ if (! array_key_exists($group, $this->displayGroups)) {
+ $this->addDisplayGroup($elements, $group, array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => $order,
+ 'legend' => $legend ?: $group,
+ ));
+ $this->displayGroups[$group] = $this->getDisplayGroup($group);
+ } else {
+ $this->displayGroups[$group]->addElements($elements);
+ }
+
+ return $this->displayGroups[$group];
+ }
+
+ protected function handleProperties(DbObject $object, &$values)
+ {
+ if ($this->hasBeenSent()) {
+ foreach ($values as $key => $value) {
+ try {
+ if ($key === 'imports' && ! empty($this->choiceElements)) {
+ if (! is_array($value)) {
+ $value = [$value];
+ }
+ foreach ($this->choiceElements as $element) {
+ $chosen = $element->getValue();
+ if (is_string($chosen)) {
+ $value[] = $chosen;
+ } elseif (is_array($chosen)) {
+ foreach ($chosen as $choice) {
+ $value[] = $choice;
+ }
+ }
+ }
+ }
+ $object->set($key, $value);
+ if ($object instanceof IcingaObject) {
+ if ($this->resolvedImports !== false) {
+ $object->imports()->getObjects();
+ }
+ }
+ } catch (Exception $e) {
+ $this->addException($e, $key);
+ }
+ }
+ }
+ }
+
+ protected function loadInheritedProperties()
+ {
+ if ($this->assertResolvedImports()) {
+ try {
+ $this->showInheritedProperties($this->object());
+ } catch (Exception $e) {
+ $this->addException($e);
+ }
+ }
+ }
+
+ protected function showInheritedProperties(IcingaObject $object)
+ {
+ $inherited = $object->getInheritedProperties();
+ $origins = $object->getOriginsProperties();
+
+ foreach ($inherited as $k => $v) {
+ if ($v !== null && $k !== 'object_name') {
+ $el = $this->getElement($k);
+ if ($el) {
+ $this->setInheritedValue($el, $inherited->$k, $origins->$k);
+ } elseif (substr($k, -3) === '_id') {
+ $k = substr($k, 0, -3);
+ $el = $this->getElement($k);
+ if ($el) {
+ $this->setInheritedValue(
+ $el,
+ $object->getRelatedObjectName($k, $v),
+ $origins->{"${k}_id"}
+ );
+ }
+ }
+ }
+ }
+ }
+
+ protected function prepareFields($object)
+ {
+ if ($this->assertResolvedImports()) {
+ $this->fieldLoader = new IcingaObjectFieldLoader($object);
+ $this->fieldLoader->prepareElements($this);
+ }
+
+ return $this;
+ }
+
+ protected function setCustomVarValues($values)
+ {
+ if ($this->fieldLoader) {
+ $this->fieldLoader->setValues($values, 'var_');
+ }
+
+ return $this;
+ }
+
+ protected function addFields()
+ {
+ if ($this->fieldLoader) {
+ $this->fieldLoader->addFieldsToForm($this);
+ $this->onAddedFields();
+ }
+ }
+
+ protected function onAddedFields()
+ {
+ }
+
+ // TODO: remove, used in sets I guess
+ protected function fieldLoader($object)
+ {
+ if ($this->fieldLoader === null) {
+ $this->fieldLoader = new IcingaObjectFieldLoader($object);
+ }
+
+ return $this->fieldLoader;
+ }
+
+ protected function isNew()
+ {
+ return $this->object === null || ! $this->object->hasBeenLoadedFromDb();
+ }
+
+ protected function setButtons()
+ {
+ if ($this->isNew()) {
+ $this->setSubmitLabel(
+ $this->translate('Add')
+ );
+ } else {
+ $this->setSubmitLabel(
+ $this->translate('Store')
+ );
+ $this->addDeleteButton();
+ }
+ }
+
+ /**
+ * @param bool $importsFirst
+ * @return $this
+ */
+ protected function groupMainProperties($importsFirst = false)
+ {
+ if ($importsFirst) {
+ $elements = [
+ 'imports',
+ 'object_type',
+ 'object_name',
+ ];
+ } else {
+ $elements = [
+ 'object_type',
+ 'object_name',
+ 'imports',
+ ];
+ }
+ $elements = array_merge($elements, [
+ 'display_name',
+ 'host',
+ 'host_id',
+ 'address',
+ 'address6',
+ 'groups',
+ 'inherited_groups',
+ 'applied_groups',
+ 'users',
+ 'user_groups',
+ 'apply_to',
+ 'command_id', // Notification
+ 'notification_interval',
+ 'period_id',
+ 'times_begin',
+ 'times_end',
+ 'email',
+ 'pager',
+ 'enable_notifications',
+ 'disable_checks', //Dependencies
+ 'disable_notifications',
+ 'ignore_soft_states',
+ 'apply_for',
+ 'create_live',
+ 'disabled',
+ ]);
+
+ // Add template choices to the main section
+ /** @var \Zend_Form_Element $el */
+ foreach ($this->getElements() as $key => $el) {
+ if (substr($el->getName(), 0, 6) === 'choice') {
+ $elements[] = $key;
+ }
+ }
+
+ $this->addDisplayGroup($elements, 'object_definition', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_OBJECT_DEFINITION,
+ 'legend' => $this->translate('Main properties')
+ ));
+
+ return $this;
+ }
+
+ protected function setSentValue($name, $value)
+ {
+ if ($this->hasBeenSent()) {
+ $request = $this->getRequest();
+ if ($value !== null && $request->isPost() && $request->getPost($name) !== null) {
+ $request->setPost($name, $value);
+ }
+ }
+
+ $this->setElementValue($name, $value);
+ }
+
+ public function setElementValue($name, $value = null)
+ {
+ $el = $this->getElement($name);
+ if (! $el) {
+ // Not showing an error, as most object properties do not exist. Not
+ // yet, because IMO this should be checked.
+ // $this->addError(sprintf($this->translate('Form element "%s" does not exist'), $name));
+ return;
+ }
+
+ if ($value !== null) {
+ $el->setValue($value);
+ }
+ }
+
+ public function setInheritedValue(ZfElement $el, $inherited, $inheritedFrom)
+ {
+ if ($inherited === null) {
+ return;
+ }
+
+ $txtInherited = sprintf($this->translate(' (inherited from "%s")'), $inheritedFrom);
+ if ($el instanceof ZfSelect) {
+ $multi = $el->getMultiOptions();
+ if (is_bool($inherited)) {
+ $inherited = $inherited ? 'y' : 'n';
+ }
+ if (is_scalar($inherited) && array_key_exists($inherited, $multi)) {
+ $multi[null] = $multi[$inherited] . $txtInherited;
+ } else {
+ $multi[null] = $this->stringifyInheritedValue($inherited) . $txtInherited;
+ }
+ $el->setMultiOptions($multi);
+ } elseif ($el instanceof ExtensibleSet) {
+ $el->setAttrib('inherited', $inherited);
+ $el->setAttrib('inheritedFrom', $inheritedFrom);
+ } else {
+ if (is_string($inherited) || is_int($inherited)) {
+ $el->setAttrib('placeholder', $inherited . $txtInherited);
+ }
+ }
+
+ // We inherited a value, so no need to require the field
+ $el->setRequired(false);
+ }
+
+ protected function stringifyInheritedValue($value)
+ {
+ return is_scalar($value) ? $value : substr(json_encode($value), 0, 40);
+ }
+
+ public function setListUrl($url)
+ {
+ $this->listUrl = $url;
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object();
+ if ($object->hasBeenModified()) {
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->setHttpResponseCode(201);
+ }
+
+ $msg = sprintf(
+ $object->hasBeenLoadedFromDb()
+ ? $this->translate('The %s has successfully been stored')
+ : $this->translate('A new %s has successfully been created'),
+ $this->translate($this->getObjectShortClassName())
+ );
+ $this->getDbObjectStore()->store($object);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+
+ $this->setObjectSuccessUrl();
+ $this->beforeSuccessfulRedirect();
+ $this->redirectOnSuccess($msg);
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ $object = $this->object();
+
+ if ($object instanceof IcingaObject) {
+ $params = $object->getUrlParams();
+ $url = Url::fromPath($this->getAction());
+ if ($url->hasParam('dbResourceName')) {
+ $params['dbResourceName'] = $url->getParam('dbResourceName');
+ }
+ $this->setSuccessUrl(
+ 'director/' . strtolower($this->getObjectShortClassName()),
+ $params
+ );
+ } elseif ($object->hasProperty('id')) {
+ $this->setSuccessUrl($this->getSuccessUrl()->with('id', $object->getProperty('id')));
+ }
+ }
+
+ protected function beforeSuccessfulRedirect()
+ {
+ }
+
+ public function hasElement($name)
+ {
+ return $this->getElement($name) !== null;
+ }
+
+ public function getObject()
+ {
+ return $this->object;
+ }
+
+ public function hasObject()
+ {
+ return $this->object !== null;
+ }
+
+ public function isIcingaObject()
+ {
+ if ($this->object !== null) {
+ return $this->object instanceof IcingaObject;
+ }
+
+ /** @var DbObject $class */
+ $class = $this->getObjectClassname();
+ $instance = $class::create();
+
+ return $instance instanceof IcingaObject;
+ }
+
+ public function isMultiObjectForm()
+ {
+ return false;
+ }
+
+ public function setObject(DbObject $object)
+ {
+ $this->object = $object;
+ if ($this->db === null) {
+ /** @var Db $connection */
+ $connection = $object->getConnection();
+ $this->setDb($connection);
+ }
+
+ return $this;
+ }
+
+ protected function getObjectClassname()
+ {
+ if ($this->className === null) {
+ return 'Icinga\\Module\\Director\\Objects\\'
+ . substr(join('', array_slice(explode('\\', get_class($this)), -1)), 0, -4);
+ }
+
+ return $this->className;
+ }
+
+ protected function getObjectShortClassName()
+ {
+ if ($this->objectName === null) {
+ $className = substr(strrchr(get_class($this), '\\'), 1);
+ if (substr($className, 0, 6) === 'Icinga') {
+ return substr($className, 6, -4);
+ } else {
+ return substr($className, 0, -4);
+ }
+ }
+
+ return $this->objectName;
+ }
+
+ protected function removeFromSet(&$set, $key)
+ {
+ unset($set[$key]);
+ }
+
+ protected function moveUpInSet(&$set, $key)
+ {
+ list($set[$key - 1], $set[$key]) = array($set[$key], $set[$key - 1]);
+ }
+
+ protected function moveDownInSet(&$set, $key)
+ {
+ list($set[$key + 1], $set[$key]) = array($set[$key], $set[$key + 1]);
+ }
+
+ protected function beforeSetup()
+ {
+ if (!$this->hasBeenSent()) {
+ return;
+ }
+
+ $post = $values = $this->getRequest()->getPost();
+
+ foreach ($post as $key => $value) {
+ if (preg_match('/^(.+?)_(\d+)__(MOVE_DOWN|MOVE_UP|REMOVE)$/', $key, $m)) {
+ $values[$m[1]] = array_filter($values[$m[1]], 'strlen');
+ switch ($m[3]) {
+ case 'MOVE_UP':
+ $this->moveUpInSet($values[$m[1]], $m[2]);
+ break;
+ case 'MOVE_DOWN':
+ $this->moveDownInSet($values[$m[1]], $m[2]);
+ break;
+ case 'REMOVE':
+ $this->removeFromSet($values[$m[1]], $m[2]);
+ break;
+ }
+
+ $this->getRequest()->setPost($m[1], $values[$m[1]]);
+ }
+ }
+ }
+
+ protected function onRequest()
+ {
+ if ($this->object !== null) {
+ $this->setDefaultsFromObject($this->object);
+ }
+ $this->prepareFields($this->object());
+ IcingaObjectFormHook::callOnSetup($this);
+ if ($this->hasBeenSent()) {
+ $this->handlePost();
+ }
+ try {
+ $this->loadInheritedProperties();
+ $this->addFields();
+ $this->callOnRequestCallables();
+ } catch (Exception $e) {
+ $this->addUniqueException($e);
+
+ return;
+ }
+
+ if ($this->shouldBeDeleted()) {
+ $this->deleteObject($this->object());
+ }
+ }
+
+ protected function handlePost()
+ {
+ $object = $this->object();
+
+ $post = $this->getRequest()->getPost();
+ $this->populate($post);
+ $values = $this->getValues();
+
+ if ($object instanceof IcingaObject) {
+ $this->setCustomVarValues($post);
+ }
+
+ $this->handleProperties($object, $values);
+
+ // TODO: get rid of this
+ if ($object instanceof IcingaObject) {
+ $this->handleRanges($object, $values);
+ }
+ }
+
+ protected function setDefaultsFromObject(DbObject $object)
+ {
+ /** @var ZfElement $element */
+ foreach ($this->getElements() as $element) {
+ $key = $element->getName();
+ if ($object->hasProperty($key)) {
+ $value = $object->get($key);
+ if ($object instanceof IcingaObject) {
+ if ($object->propertyIsRelatedSet($key)) {
+ if (! count((array) $value)) {
+ continue;
+ }
+ }
+ }
+
+ if ($value !== null && $value !== []) {
+ $element->setValue($value);
+ }
+ }
+ }
+ }
+
+ protected function deleteObject($object)
+ {
+ if ($object instanceof IcingaObject && $object->hasProperty('object_name')) {
+ $msg = sprintf(
+ '%s "%s" has been removed',
+ $this->translate($this->getObjectShortClassName()),
+ $object->getObjectName()
+ );
+ } else {
+ $msg = sprintf(
+ '%s has been removed',
+ $this->translate($this->getObjectShortClassName())
+ );
+ }
+
+ if ($this->listUrl) {
+ $url = $this->listUrl;
+ } elseif ($object instanceof IcingaObject && $object->hasProperty('object_name')) {
+ $url = $object->getOnDeleteUrl();
+ } else {
+ $url = $this->getSuccessUrl()->without(
+ array('field_id', 'argument_id', 'range', 'range_type')
+ );
+ }
+
+ if ($this->getDbObjectStore()->delete($object)) {
+ $this->setSuccessUrl($url);
+ }
+ $this->redirectOnSuccess($msg);
+ }
+
+ /**
+ * @return DbObjectStore
+ */
+ protected function getDbObjectStore()
+ {
+ $store = new DbObjectStore($this->getDb(), $this->branch);
+ return $store;
+ }
+
+ protected function addDeleteButton($label = null)
+ {
+ $object = $this->object;
+
+ if ($label === null) {
+ $label = $this->translate('Delete');
+ }
+
+ $el = $this->createElement('submit', $label)
+ ->setLabel($label)
+ ->setDecorators(array('ViewHelper'));
+ //->removeDecorator('Label');
+
+ $this->deleteButtonName = $el->getName();
+
+ if ($object instanceof IcingaObject && $object->isTemplate()) {
+ if ($cnt = $object->countDirectDescendants()) {
+ $el->setAttrib('disabled', 'disabled');
+ $el->setAttrib(
+ 'title',
+ sprintf(
+ $this->translate('This template is still in use by %d other objects'),
+ $cnt
+ )
+ );
+ }
+ } elseif ($object instanceof IcingaCommand && $object->isInUse()) {
+ $el->setAttrib('disabled', 'disabled');
+ $el->setAttrib(
+ 'title',
+ sprintf(
+ $this->translate('This Command is still in use by %d other objects'),
+ $object->countDirectUses()
+ )
+ );
+ }
+
+ $this->addElement($el);
+
+ return $this;
+ }
+
+ public function hasDeleteButton()
+ {
+ return $this->deleteButtonName !== null;
+ }
+
+ public function shouldBeDeleted()
+ {
+ if (! $this->hasDeleteButton()) {
+ return false;
+ }
+
+ $name = $this->deleteButtonName;
+ return $this->getSentValue($name) === $this->getElement($name)->getLabel();
+ }
+
+ public function abortDeletion()
+ {
+ if ($this->hasDeleteButton()) {
+ $this->setSentValue($this->deleteButtonName, 'ABORTED');
+ }
+ }
+
+ public function getSentOrResolvedObjectValue($name, $default = null)
+ {
+ return $this->getSentOrObjectValue($name, $default, true);
+ }
+
+ public function getSentOrObjectValue($name, $default = null, $resolved = false)
+ {
+ // TODO: check whether getSentValue is still needed since element->getValue
+ // is in place (currently for form element default values only)
+
+ if (!$this->hasObject()) {
+ if ($this->hasBeenSent()) {
+ return $this->getSentValue($name, $default);
+ } else {
+ if ($name === 'object_type' && $this->preferredObjectType) {
+ return $this->preferredObjectType;
+ }
+ if ($name === 'imports' && $this->presetImports) {
+ return $this->presetImports;
+ }
+ if ($this->valueIsEmpty($val = $this->getValue($name))) {
+ return $default;
+ } else {
+ return $val;
+ }
+ }
+ }
+
+ if ($this->hasBeenSent()) {
+ if (!$this->valueIsEmpty($value = $this->getSentValue($name))) {
+ return $value;
+ }
+ }
+
+ $object = $this->getObject();
+
+ if ($object->hasProperty($name)) {
+ if ($resolved && $object->supportsImports()) {
+ if ($this->assertResolvedImports()) {
+ $objectProperty = $object->getResolvedProperty($name);
+ } else {
+ $objectProperty = $object->$name;
+ }
+ } else {
+ $objectProperty = $object->$name;
+ }
+ } else {
+ $objectProperty = null;
+ }
+
+ if ($objectProperty !== null) {
+ return $objectProperty;
+ }
+
+ if (($el = $this->getElement($name)) && !$this->valueIsEmpty($val = $el->getValue())) {
+ return $val;
+ }
+
+ return $default;
+ }
+
+ public function loadObject($id)
+ {
+ if ($this->branch && $this->branch->isBranch()) {
+ throw new \RuntimeException('Calling loadObject from form in a branch');
+ }
+ /** @var DbObject $class */
+ $class = $this->getObjectClassname();
+ if (is_int($id)) {
+ $this->object = $class::loadWithAutoIncId($id, $this->db);
+ if ($this->object->getKeyName() === 'id') {
+ $this->addHidden('id', $id);
+ }
+ } else {
+ $this->object = $class::load($id, $this->db);
+ }
+
+
+ return $this;
+ }
+
+ protected function addRange($key, $range)
+ {
+ $this->addElement('text', 'range_' . $key, array(
+ 'label' => 'ranges.' . $key,
+ 'value' => $range->range_value
+ ));
+ }
+
+ /**
+ * @param Db $db
+ * @return $this
+ */
+ public function setDb(Db $db)
+ {
+ if ($this->object !== null) {
+ $this->object->setConnection($db);
+ }
+
+ parent::setDb($db);
+ return $this;
+ }
+
+ public function optionallyAddFromEnum($enum)
+ {
+ return array(
+ null => $this->translate('- click to add more -')
+ ) + $enum;
+ }
+
+ protected function addObjectTypeElement()
+ {
+ if (!$this->isNew()) {
+ return $this;
+ }
+
+ if ($this->preferredObjectType) {
+ $this->addHidden('object_type', $this->preferredObjectType);
+ return $this;
+ }
+
+ $object = $this->object();
+
+ if ($object->supportsImports()) {
+ $templates = $this->enumAllowedTemplates();
+
+ if (empty($templates) && $this->getObjectShortClassName() !== 'Command') {
+ $types = array('template' => $this->translate('Template'));
+ } else {
+ $types = array(
+ 'object' => $this->translate('Object'),
+ 'template' => $this->translate('Template'),
+ );
+ }
+ } else {
+ $types = array('object' => $this->translate('Object'));
+ }
+
+ if ($this->object()->supportsApplyRules()) {
+ $types['apply'] = $this->translate('Apply rule');
+ }
+
+ $this->addElement('select', 'object_type', array(
+ 'label' => $this->translate('Object type'),
+ 'description' => $this->translate(
+ 'What kind of object this should be. Templates allow full access'
+ . ' to any property, they are your building blocks for "real" objects.'
+ . ' External objects should usually not be manually created or modified.'
+ . ' They allow you to work with objects locally defined on your Icinga nodes,'
+ . ' while not rendering and deploying them with the Director. Apply rules allow'
+ . ' to assign services, notifications and groups to other objects.'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($types),
+ 'class' => 'autosubmit'
+ ));
+
+ return $this;
+ }
+
+ protected function hasObjectType()
+ {
+ if (!$this->object()->hasProperty('object_type')) {
+ return false;
+ }
+
+ return ! $this->valueIsEmpty($this->getSentOrObjectValue('object_type'));
+ }
+
+ protected function addZoneElement($all = false)
+ {
+ if ($all || $this->isTemplate()) {
+ $zones = $this->db->enumZones();
+ } else {
+ $zones = $this->db->enumNonglobalZones();
+ }
+
+ $this->addElement('select', 'zone_id', array(
+ 'label' => $this->translate('Cluster Zone'),
+ 'description' => $this->translate(
+ 'Icinga cluster zone. Allows to manually override Directors decisions'
+ . ' of where to deploy your config to. You should consider not doing so'
+ . ' unless you gained deep understanding of how an Icinga Cluster stack'
+ . ' works'
+ ),
+ 'multiOptions' => $this->optionalEnum($zones)
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @param $type
+ * @return $this
+ */
+ protected function addChoices($type)
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $connection = $this->getDb();
+ $choiceType = 'TemplateChoice' . ucfirst($type);
+ $table = "icinga_$type";
+ $choices = IcingaObject::loadAllByType($choiceType, $connection);
+ $chosenTemplates = $this->getSentOrObjectValue('imports');
+ $db = $connection->getDbAdapter();
+ if (empty($chosenTemplates)) {
+ $importedIds = [];
+ } else {
+ $importedIds = $db->fetchCol(
+ $db->select()->from($table, 'id')
+ ->where('object_name in (?)', (array)$chosenTemplates)
+ ->where('object_type = ?', 'template')
+ );
+ }
+
+ foreach ($choices as $choice) {
+ $required = $choice->get('required_template_id');
+ if ($required === null || in_array($required, $importedIds, false)) {
+ $this->addChoiceElement($choice);
+ }
+ }
+
+ return $this;
+ }
+
+ protected function addChoiceElement(IcingaTemplateChoice $choice)
+ {
+ $imports = $this->object()->listImportNames();
+ $element = $choice->createFormElement($this, $imports);
+ $this->addElement($element);
+ $this->choiceElements[$element->getName()] = $element;
+ return $this;
+ }
+
+ /**
+ * @param bool $required
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addImportsElement($required = null)
+ {
+ if ($this->presetImports) {
+ return $this;
+ }
+
+ if (in_array($this->getObjectShortClassName(), ['TimePeriod', 'ScheduledDowntime'])) {
+ $required = false;
+ } else {
+ $required = $required !== null ? $required : !$this->isTemplate();
+ }
+ $enum = $this->enumAllowedTemplates();
+ if (empty($enum)) {
+ if ($required) {
+ if ($this->hasBeenSent()) {
+ $this->addError($this->translate('No template has been chosen'));
+ } else {
+ if ($this->hasPermission('director/admin')) {
+ $html = $this->translate('Please define a related template first');
+ } else {
+ $html = $this->translate('No related template has been provided yet');
+ }
+ $this->addHtml('<p class="warning">' . $html . '</p>');
+ }
+ }
+ return $this;
+ }
+
+ $db = $this->getDb()->getDbAdapter();
+ $object = $this->object;
+ if ($object->supportsChoices()) {
+ $choiceNames = $db->fetchCol(
+ $db->select()->from(
+ $this->object()->getTableName(),
+ 'object_name'
+ )->where('template_choice_id IS NOT NULL')
+ );
+ } else {
+ $choiceNames = [];
+ }
+
+ $type = $object->getShortTableName();
+ $this->addElement('extensibleSet', 'imports', array(
+ 'label' => $this->translate('Imports'),
+ 'description' => $this->translate(
+ 'Importable templates, add as many as you want. Please note that order'
+ . ' matters when importing properties from multiple templates: last one'
+ . ' wins'
+ ),
+ 'required' => $required,
+ 'spellcheck' => 'false',
+ 'hideOptions' => $choiceNames,
+ 'suggest' => "${type}templates",
+ // 'multiOptions' => $this->optionallyAddFromEnum($enum),
+ 'sorted' => true,
+ 'value' => $this->presetImports,
+ 'class' => 'autosubmit'
+ ));
+
+ return $this;
+ }
+
+ protected function addDisabledElement()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addBoolean(
+ 'disabled',
+ array(
+ 'label' => $this->translate('Disabled'),
+ 'description' => $this->translate('Disabled objects will not be deployed')
+ ),
+ 'n'
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addGroupDisplayNameElement()
+ {
+ $this->addElement('text', 'display_name', array(
+ 'label' => $this->translate('Display Name'),
+ 'description' => $this->translate(
+ 'An alternative display name for this group. If you wonder how this'
+ . ' could be helpful just leave it blank'
+ )
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @param bool $force
+ *
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addCheckCommandElements($force = false)
+ {
+ if (! $force && ! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('text', 'check_command', array(
+ 'label' => $this->translate('Check command'),
+ 'description' => $this->translate('Check command definition'),
+ // 'multiOptions' => $this->optionalEnum($this->db->enumCheckcommands()),
+ 'class' => 'autosubmit director-suggest', // This influences fields
+ 'data-suggestion-context' => 'checkcommandnames',
+ 'value' => $this->getObject()->get('check_command')
+ ));
+ $this->getDisplayGroup('object_definition')
+ // ->addElement($this->getElement('check_command_id'))
+ ->addElement($this->getElement('check_command'));
+
+ $eventCommands = $this->db->enumEventcommands();
+
+ if (! empty($eventCommands)) {
+ $this->addElement('select', 'event_command_id', array(
+ 'label' => $this->translate('Event command'),
+ 'description' => $this->translate('Event command definition'),
+ 'multiOptions' => $this->optionalEnum($eventCommands),
+ 'class' => 'autosubmit',
+ ));
+ $this->addToCheckExecutionDisplayGroup('event_command_id');
+ }
+
+ return $this;
+ }
+
+ protected function addCheckExecutionElements($force = false)
+ {
+ if (! $force && ! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'text',
+ 'check_interval',
+ array(
+ 'label' => $this->translate('Check interval'),
+ 'description' => $this->translate('Your regular check interval')
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'retry_interval',
+ array(
+ 'label' => $this->translate('Retry interval'),
+ 'description' => $this->translate(
+ 'Retry interval, will be applied after a state change unless the next hard state is reached'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'max_check_attempts',
+ array(
+ 'label' => $this->translate('Max check attempts'),
+ 'description' => $this->translate(
+ 'Defines after how many check attempts a new hard state is reached'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'check_timeout',
+ array(
+ 'label' => $this->translate('Check timeout'),
+ 'description' => $this->translate(
+ "Check command timeout in seconds. Overrides the CheckCommand's timeout attribute"
+ )
+ )
+ );
+
+ $periods = $this->db->enumTimeperiods();
+
+ if (!empty($periods)) {
+ $this->addElement(
+ 'select',
+ 'check_period_id',
+ array(
+ 'label' => $this->translate('Check period'),
+ 'description' => $this->translate(
+ 'The name of a time period which determines when this'
+ . ' object should be monitored. Not limited by default.'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+ }
+
+ $this->optionalBoolean(
+ 'enable_active_checks',
+ $this->translate('Execute active checks'),
+ $this->translate('Whether to actively check this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_passive_checks',
+ $this->translate('Accept passive checks'),
+ $this->translate('Whether to accept passive check results for this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_notifications',
+ $this->translate('Send notifications'),
+ $this->translate('Whether to send notifications for this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_event_handler',
+ $this->translate('Enable event handler'),
+ $this->translate('Whether to enable event handlers this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_perfdata',
+ $this->translate('Process performance data'),
+ $this->translate('Whether to process performance data provided by this object')
+ );
+
+ $this->optionalBoolean(
+ 'enable_flapping',
+ $this->translate('Enable flap detection'),
+ $this->translate('Whether flap detection is enabled on this object')
+ );
+
+ $this->addElement(
+ 'text',
+ 'flapping_threshold_high',
+ array(
+ 'label' => $this->translate('Flapping threshold (high)'),
+ 'description' => $this->translate(
+ 'Flapping upper bound in percent for a service to be considered flapping'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'flapping_threshold_low',
+ array(
+ 'label' => $this->translate('Flapping threshold (low)'),
+ 'description' => $this->translate(
+ 'Flapping lower bound in percent for a service to be considered not flapping'
+ )
+ )
+ );
+
+ $this->optionalBoolean(
+ 'volatile',
+ $this->translate('Volatile'),
+ $this->translate('Whether this check is volatile.')
+ );
+
+ $elements = array(
+ 'check_interval',
+ 'retry_interval',
+ 'max_check_attempts',
+ 'check_timeout',
+ 'check_period_id',
+ 'enable_active_checks',
+ 'enable_passive_checks',
+ 'enable_notifications',
+ 'enable_event_handler',
+ 'enable_perfdata',
+ 'enable_flapping',
+ 'flapping_threshold_high',
+ 'flapping_threshold_low',
+ 'volatile'
+ );
+ $this->addToCheckExecutionDisplayGroup($elements);
+
+ return $this;
+ }
+
+ protected function enumAllowedTemplates()
+ {
+ $object = $this->object();
+ $tpl = $this->db->enumIcingaTemplates($object->getShortTableName());
+ if (empty($tpl)) {
+ return [];
+ }
+
+ $id = $object->get('id');
+
+ if (array_key_exists($id, $tpl)) {
+ unset($tpl[$id]);
+ }
+
+ return array_combine($tpl, $tpl);
+ }
+
+ protected function addExtraInfoElements()
+ {
+ $this->addElement('textarea', 'notes', array(
+ 'label' => $this->translate('Notes'),
+ 'description' => $this->translate(
+ 'Additional notes for this object'
+ ),
+ 'rows' => 2,
+ 'columns' => 60,
+ ));
+
+ $this->addElement('text', 'notes_url', array(
+ 'label' => $this->translate('Notes URL'),
+ 'description' => $this->translate(
+ 'An URL pointing to additional notes for this object'
+ ),
+ ));
+
+ $this->addElement('text', 'action_url', array(
+ 'label' => $this->translate('Action URL'),
+ 'description' => $this->translate(
+ 'An URL leading to additional actions for this object. Often used'
+ . ' with Icinga Classic, rarely with Icinga Web 2 as it provides'
+ . ' far better possibilities to integrate addons'
+ ),
+ ));
+
+ $this->addElement('text', 'icon_image', array(
+ 'label' => $this->translate('Icon image'),
+ 'description' => $this->translate(
+ 'An URL pointing to an icon for this object. Try "tux.png" for icons'
+ . ' relative to public/img/icons or "cloud" (no extension) for items'
+ . ' from the Icinga icon font'
+ ),
+ ));
+
+ $this->addElement('text', 'icon_image_alt', array(
+ 'label' => $this->translate('Icon image alt'),
+ 'description' => $this->translate(
+ 'Alternative text to be shown in case above icon is missing'
+ ),
+ ));
+
+ $elements = array(
+ 'notes',
+ 'notes_url',
+ 'action_url',
+ 'icon_image',
+ 'icon_image_alt',
+ );
+
+ $this->addDisplayGroup($elements, 'extrainfo', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_EXTRA_INFO,
+ 'legend' => $this->translate('Additional properties')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * Add an assign_filter form element
+ *
+ * Forms should use this helper method for objects using the typical
+ * assign_filter column
+ *
+ * @param array $properties Form element properties
+ *
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addAssignFilter($properties)
+ {
+ if (!$this->object || !$this->object->supportsAssignments()) {
+ return $this;
+ }
+
+ $this->addFilterElement('assign_filter', $properties);
+ $el = $this->getElement('assign_filter');
+
+ $this->addDisplayGroup(array($el), 'assign', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_ASSIGN,
+ 'legend' => $this->translate('Assign where')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * Add a dataFilter element with fitting decorators
+ *
+ * TODO: Evaluate whether parts or all of this could be moved to the element
+ * class.
+ *
+ * @param string $name Element name
+ * @param array $properties Form element properties
+ *
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addFilterElement($name, $properties)
+ {
+ $this->addElement('dataFilter', $name, $properties);
+ $el = $this->getElement($name);
+
+ $ddClass = 'full-width';
+ if (array_key_exists('required', $properties) && $properties['required']) {
+ $ddClass .= ' required';
+ }
+
+ $el->clearDecorators()
+ ->addDecorator('ViewHelper')
+ ->addDecorator('Errors')
+ ->addDecorator('Description', array('tag' => 'p', 'class' => 'description'))
+ ->addDecorator('HtmlTag', array(
+ 'tag' => 'dd',
+ 'class' => $ddClass,
+ ));
+
+ return $this;
+ }
+
+ protected function addEventFilterElements($elements = array('states','types'))
+ {
+ if (in_array('states', $elements)) {
+ $this->addElement('extensibleSet', 'states', array(
+ 'label' => $this->translate('States'),
+ 'multiOptions' => $this->optionallyAddFromEnum($this->enumStates()),
+ 'description' => $this->translate(
+ 'The host/service states you want to get notifications for'
+ ),
+ ));
+ }
+
+ if (in_array('types', $elements)) {
+ $this->addElement('extensibleSet', 'types', array(
+ 'label' => $this->translate('Transition types'),
+ 'multiOptions' => $this->optionallyAddFromEnum($this->enumTypes()),
+ 'description' => $this->translate(
+ 'The state transition types you want to get notifications for'
+ ),
+ ));
+ }
+
+ $this->addDisplayGroup($elements, 'event_filters', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_EVENT_FILTERS,
+ 'legend' => $this->translate('State and transition type filters')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @param string $permission
+ * @return bool
+ */
+ public function hasPermission($permission)
+ {
+ return Util::hasPermission($permission);
+ }
+
+ public function setBranch(Branch $branch)
+ {
+ $this->branch = $branch;
+
+ return $this;
+ }
+
+ protected function allowsExperimental()
+ {
+ // NO, it is NOT a good idea to use this. You'll break your monitoring
+ // and nobody will help you.
+ if ($this->allowsExperimental === null) {
+ $this->allowsExperimental = $this->db->settings()->get(
+ 'experimental_features'
+ ) === 'allow';
+ }
+
+ return $this->allowsExperimental;
+ }
+
+ protected function enumStates()
+ {
+ $set = new StateFilterSet();
+ return $set->enumAllowedValues();
+ }
+
+ protected function enumTypes()
+ {
+ $set = new TypeFilterSet();
+ return $set->enumAllowedValues();
+ }
+}
diff --git a/library/Director/Web/Form/Element/Boolean.php b/library/Director/Web/Form/Element/Boolean.php
new file mode 100644
index 0000000..b2402c7
--- /dev/null
+++ b/library/Director/Web/Form/Element/Boolean.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Zend_Form_Element_Select as ZfSelect;
+
+/**
+ * Input control for booleans
+ */
+class Boolean extends ZfSelect
+{
+ public $options = array(
+ null => '- please choose -',
+ 'y' => 'Yes',
+ 'n' => 'No',
+ );
+
+ public function getValue()
+ {
+ $value = $this->getUnfilteredValue();
+
+ if ($value === 'y' || $value === true) {
+ return true;
+ } elseif ($value === 'n' || $value === false) {
+ return false;
+ }
+
+ return null;
+ }
+
+ public function isValid($value, $context = null)
+ {
+ if ($value === 'y' || $value === 'n') {
+ $this->setValue($value);
+ return true;
+ }
+
+ return parent::isValid($value, $context);
+ }
+
+ /**
+ * @param string $value
+ * @param string $key
+ * @codingStandardsIgnoreStart
+ */
+ protected function _filterValue(&$value, &$key)
+ {
+ // @codingStandardsIgnoreEnd
+ if ($value === true) {
+ $value = 'y';
+ } elseif ($value === false) {
+ $value = 'n';
+ } elseif ($value === '') {
+ $value = null;
+ }
+
+ parent::_filterValue($value, $key);
+ }
+
+ public function setValue($value)
+ {
+ if ($value === true) {
+ $value = 'y';
+ } elseif ($value === false) {
+ $value = 'n';
+ } elseif ($value === '') {
+ $value = null;
+ }
+
+ return parent::setValue($value);
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ protected function _translateOption($option, $value)
+ {
+ // @codingStandardsIgnoreEnd
+ if (!isset($this->_translated[$option]) && !empty($value)) {
+ $this->options[$option] = mt('director', $value);
+ if ($this->options[$option] === $value) {
+ return false;
+ }
+ $this->_translated[$option] = true;
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/library/Director/Web/Form/Element/DataFilter.php b/library/Director/Web/Form/Element/DataFilter.php
new file mode 100644
index 0000000..adae07d
--- /dev/null
+++ b/library/Director/Web/Form/Element/DataFilter.php
@@ -0,0 +1,361 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Web\Form\IconHelper;
+use Exception;
+
+/**
+ * Input control for extensible sets
+ */
+class DataFilter extends FormElement
+{
+ /**
+ * Default form view helper to use for rendering
+ * @var string
+ */
+ public $helper = 'formDataFilter';
+
+ private $addTo;
+
+ private $removeFilter;
+
+ private $stripFilter;
+
+ /** @var FilterChain */
+ private $filter;
+
+ public function getValue()
+ {
+ $value = parent::getValue();
+ if ($value !== null && $this->isEmpty($value)) {
+ $value = null;
+ }
+
+ return $value;
+ }
+
+ protected function isEmpty(Filter $filter)
+ {
+ return $filter->isEmpty() || $this->isEmptyExpression($filter);
+ }
+
+ protected function isEmptyExpression(Filter $filter)
+ {
+ return $filter instanceof FilterExpression &&
+ $filter->getColumn() === '' &&
+ $filter->getExpression() === '""'; // -> json_encode('')
+ }
+
+ /**
+ * @inheritdoc
+ * @codingStandardsIgnoreStart
+ */
+ protected function _filterValue(&$value, &$key)
+ {
+ // @codingStandardsIgnoreEnd
+ try {
+ if ($value instanceof Filter) {
+ // OK
+ } elseif (is_string($value)) {
+ $value = Filter::fromQueryString($value);
+ } elseif (is_array($value) || is_null($value)) {
+ $value = $this->arrayToFilter($value);
+ } else {
+ throw new ProgrammingError(
+ 'Value to be filtered has to be Filter, string, array or null'
+ );
+ }
+ } catch (Exception $e) {
+ $value = null;
+ // TODO: getFile, getLine
+ // Hint: cannot addMessage at it would loop through getValue
+ $this->addErrorMessage($e->getMessage());
+ $this->_isErrorForced = true;
+ }
+ }
+
+ /**
+ * This method transforms filter form data into a filter
+ * and reacts on pressed buttons
+ *
+ * @param array|null $array
+ *
+ * @return FilterChain|null
+ */
+ protected function arrayToFilter($array)
+ {
+ if ($array === null) {
+ return null;
+ }
+
+ $this->filter = null;
+ foreach ($array as $id => $entry) {
+ $filterId = $this->idToFilterId($id);
+ $sub = $this->entryToFilter($entry);
+ $this->checkEntryForActions($filterId, $entry);
+ $parentId = $this->parentIdFor($filterId);
+
+ if ($this->filter === null) {
+ $this->filter = $sub;
+ } else {
+ $this->filter->getById($parentId)->addFilter($sub);
+ }
+ }
+
+ $this->removeFilterIfRequested()
+ ->stripFilterIfRequested()
+ ->addNewFilterIfRequested()
+ ->fixNotsWithMultipleChildren();
+
+ return $this->filter;
+ }
+
+ protected function removeFilterIfRequested()
+ {
+ if ($this->removeFilter !== null) {
+ if ($this->filter->getById($this->removeFilter)->isRootNode()) {
+ $this->filter = $this->emptyExpression();
+ } else {
+ $this->filter->removeId($this->removeFilter);
+ }
+ }
+
+ return $this;
+ }
+
+
+ protected function stripFilterIfRequested()
+ {
+ if ($this->stripFilter !== null) {
+ $strip = $this->stripFilter;
+ $subId = $strip . '-1';
+ if ($this->filter->getId() === $strip) {
+ $this->filter = $this->filter->getById($subId);
+ } else {
+ $this->filter->replaceById($strip, $this->filter->getById($subId));
+ }
+ }
+
+ return $this;
+ }
+
+ protected function addNewFilterIfRequested()
+ {
+ if ($this->addTo !== null) {
+ $parent = $this->filter->getById($this->addTo);
+
+ if ($parent instanceof FilterChain) {
+ if ($parent->isEmpty()) {
+ $parent->addFilter($this->emptyExpression());
+ } else {
+ $parent->addFilter($this->emptyExpression());
+ }
+ } elseif ($parent instanceof FilterExpression) {
+ $replacement = Filter::matchAll(clone($parent));
+ if ($parent->isRootNode()) {
+ $this->filter = $replacement;
+ } else {
+ $this->filter->replaceById($parent->getId(), $replacement);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ protected function fixNotsWithMultipleChildren()
+ {
+ $this->filter = $this->fixNotsWithMultipleChildrenForFilter($this->filter);
+ return $this;
+ }
+
+ protected function fixNotsWithMultipleChildrenForFilter(Filter $filter)
+ {
+ if ($filter instanceof FilterChain) {
+ if ($filter->getOperatorName() === 'NOT') {
+ if ($filter->count() > 1) {
+ $filter = $this->notToNotAnd($filter);
+ }
+ }
+ /** @var Filter $sub */
+ foreach ($filter->filters() as $sub) {
+ $filter->replaceById(
+ $sub->getId(),
+ $this->fixNotsWithMultipleChildrenForFilter($sub)
+ );
+ }
+ }
+
+ return $filter;
+ }
+
+ protected function notToNotAnd(FilterChain $not)
+ {
+ $and = Filter::matchAll();
+ foreach ($not->filters() as $sub) {
+ $and->addFilter(clone($sub));
+ }
+
+ return Filter::not($and);
+ }
+
+ protected function emptyExpression()
+ {
+ return Filter::expression('', '=', '');
+ }
+
+ protected function parentIdFor($id)
+ {
+ if (false === ($pos = strrpos($id, '-'))) {
+ return '0';
+ } else {
+ return substr($id, 0, $pos);
+ }
+ }
+
+ protected function idToFilterId($id)
+ {
+ if (! preg_match('/^id_(new_)?(\d+(?:-\d+)*)$/', $id, $m)) {
+ die('nono' . $id);
+ }
+
+ return $m[2];
+ }
+
+ protected function checkEntryForActions($filterId, $entry)
+ {
+ switch ($this->entryAction($entry)) {
+ case 'cancel':
+ $this->removeFilter = $filterId;
+ break;
+
+ case 'minus':
+ $this->stripFilter = $filterId;
+ break;
+
+ case 'plus':
+ case 'angle-double-right':
+ $this->addTo = $filterId;
+ break;
+ }
+ }
+
+ /**
+ * Transforms a single submitted form component from an array
+ * into a Filter object
+ *
+ * @param array $entry The array as submitted through the form
+ *
+ * @return Filter
+ */
+ protected function entryToFilter($entry)
+ {
+ if (array_key_exists('operator', $entry)) {
+ return Filter::chain($entry['operator']);
+ } else {
+ return $this->entryToFilterExpression($entry);
+ }
+ }
+
+ protected function entryToFilterExpression($entry)
+ {
+ if ($entry['sign'] === 'true') {
+ return Filter::expression(
+ $entry['column'],
+ '=',
+ json_encode(true)
+ );
+ } elseif ($entry['sign'] === 'false') {
+ return Filter::expression(
+ $entry['column'],
+ '=',
+ json_encode(false)
+ );
+ } elseif ($entry['sign'] === 'in') {
+ if (array_key_exists('value', $entry)) {
+ if (is_array($entry['value'])) {
+ $value = array_filter($entry['value'], 'strlen');
+ } elseif (empty($entry['value'])) {
+ $value = array();
+ } else {
+ $value = array($entry['value']);
+ }
+ } else {
+ $value = array();
+ }
+ return Filter::expression(
+ $entry['column'],
+ '=',
+ json_encode($value)
+ );
+ } elseif ($entry['sign'] === 'contains') {
+ $value = array_key_exists('value', $entry) ? $entry['value'] : null;
+
+ return Filter::expression(
+ json_encode($value),
+ '=',
+ $entry['column']
+ );
+ } else {
+ $value = array_key_exists('value', $entry) ? $entry['value'] : null;
+
+ return Filter::expression(
+ $entry['column'],
+ $entry['sign'],
+ json_encode($value)
+ );
+ }
+ }
+
+ protected function entryAction($entry)
+ {
+ if (array_key_exists('action', $entry)) {
+ return IconHelper::instance()->characterIconName($entry['action']);
+ }
+
+ return null;
+ }
+
+ protected function hasIncompleteExpressions(Filter $filter)
+ {
+ if ($filter instanceof FilterChain) {
+ foreach ($filter->filters() as $sub) {
+ if ($this->hasIncompleteExpressions($sub)) {
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ /** @var FilterExpression $filter */
+ if ($filter->isRootNode() && $this->isEmptyExpression($filter)) {
+ return false;
+ }
+
+ return $filter->getColumn() === '';
+ }
+ }
+
+ public function isValid($value, $context = null)
+ {
+ if (! $value instanceof Filter) {
+ // TODO: try, return false on E
+ $filter = $this->arrayToFilter($value);
+ $this->setValue($filter);
+ } else {
+ $filter = $value;
+ }
+
+ if ($this->hasIncompleteExpressions($filter)) {
+ $this->addError('The configured filter is incomplete');
+ return false;
+ }
+
+ return parent::isValid($value);
+ }
+}
diff --git a/library/Director/Web/Form/Element/ExtensibleSet.php b/library/Director/Web/Form/Element/ExtensibleSet.php
new file mode 100644
index 0000000..f3c968f
--- /dev/null
+++ b/library/Director/Web/Form/Element/ExtensibleSet.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use InvalidArgumentException;
+
+/**
+ * Input control for extensible sets
+ */
+class ExtensibleSet extends FormElement
+{
+ /**
+ * Default form view helper to use for rendering
+ * @var string
+ */
+ public $helper = 'formIplExtensibleSet';
+
+ // private $multiOptions;
+
+ public function getValue()
+ {
+ $value = parent::getValue();
+ if (is_string($value) || is_numeric($value)) {
+ $value = [$value];
+ } elseif ($value === null) {
+ return $value;
+ }
+ if (! is_array($value)) {
+ throw new InvalidArgumentException(sprintf(
+ 'ExtensibleSet expects to work with Arrays, got %s',
+ var_export($value, 1)
+ ));
+ }
+ $value = array_filter($value, 'strlen');
+
+ if (empty($value)) {
+ return null;
+ }
+
+ return $value;
+ }
+
+ /**
+ * We do not want one message per entry
+ *
+ * @codingStandardsIgnoreStart
+ */
+ protected function _getErrorMessages()
+ {
+ return $this->_errorMessages;
+ // @codingStandardsIgnoreEnd
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ protected function _filterValue(&$value, &$key)
+ {
+ // @codingStandardsIgnoreEnd
+ if (is_array($value)) {
+ $value = array_filter($value, 'strlen');
+ } elseif (is_string($value) && !strlen($value)) {
+ $value = null;
+ }
+
+ parent::_filterValue($value, $key);
+ }
+
+ public function isValid($value, $context = null)
+ {
+ if ($value === null) {
+ $value = [];
+ }
+
+ $value = array_filter($value, 'strlen');
+ $this->setValue($value);
+ if ($this->isRequired() && empty($value)) {
+ // TODO: translate
+ $this->addError('You are required to choose at least one element');
+ return false;
+ }
+
+ if ($this->hasErrors()) {
+ return false;
+ }
+
+ return parent::isValid($value, $context);
+ }
+}
diff --git a/library/Director/Web/Form/Element/FormElement.php b/library/Director/Web/Form/Element/FormElement.php
new file mode 100644
index 0000000..c327859
--- /dev/null
+++ b/library/Director/Web/Form/Element/FormElement.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Zend_Form_Element_Xhtml;
+
+class FormElement extends Zend_Form_Element_Xhtml
+{
+}
diff --git a/library/Director/Web/Form/Element/InstanceSummary.php b/library/Director/Web/Form/Element/InstanceSummary.php
new file mode 100644
index 0000000..722ad26
--- /dev/null
+++ b/library/Director/Web/Form/Element/InstanceSummary.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Html;
+
+/**
+ * Used by the
+ */
+class InstanceSummary extends FormElement
+{
+ public $helper = 'formSimpleNote';
+
+ /**
+ * Always ignore this element
+ * @codingStandardsIgnoreStart
+ *
+ * @var boolean
+ */
+ protected $_ignore = true;
+ // @codingStandardsIgnoreEnd
+
+ private $instances;
+
+ /** @var array will be set via options */
+ protected $linkParams;
+
+ public function setValue($value)
+ {
+ $this->instances = $value;
+ return $this;
+ }
+
+ public function getValue()
+ {
+ return Html::tag('span', [
+ Html::tag('italic', 'empty'),
+ ' ',
+ Link::create('Manage Instances', 'director/data/dictionary', $this->linkParams, [
+ 'data-base-target' => '_next',
+ 'class' => 'icon-forward'
+ ])
+ ]);
+ }
+
+ public function isValid($value, $context = null)
+ {
+ return true;
+ }
+}
diff --git a/library/Director/Web/Form/Element/OptionalYesNo.php b/library/Director/Web/Form/Element/OptionalYesNo.php
new file mode 100644
index 0000000..7ef6d7f
--- /dev/null
+++ b/library/Director/Web/Form/Element/OptionalYesNo.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+/**
+ * Input control for booleans, gives y/n
+ */
+class OptionalYesNo extends Boolean
+{
+ public function getValue()
+ {
+ $value = $this->getUnfilteredValue();
+
+ if ($value === 'y' || $value === true) {
+ return 'y';
+ } elseif ($value === 'n' || $value === false) {
+ return 'n';
+ }
+
+ return null;
+ }
+}
diff --git a/library/Director/Web/Form/Element/SimpleNote.php b/library/Director/Web/Form/Element/SimpleNote.php
new file mode 100644
index 0000000..3097e11
--- /dev/null
+++ b/library/Director/Web/Form/Element/SimpleNote.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Icinga\Module\Director\PlainObjectRenderer;
+use ipl\Html\ValidHtml;
+
+class SimpleNote extends FormElement
+{
+ public $helper = 'formSimpleNote';
+
+ /**
+ * Always ignore this element
+ * @codingStandardsIgnoreStart
+ *
+ * @var boolean
+ */
+ protected $_ignore = true;
+ // @codingStandardsIgnoreEnd
+
+ public function isValid($value, $context = null)
+ {
+ return true;
+ }
+
+ public function setValue($value)
+ {
+ if (is_object($value) && ! $value instanceof ValidHtml) {
+ $value = 'Unexpected object: ' . PlainObjectRenderer::render($value);
+ }
+
+ return parent::setValue($value);
+ }
+}
diff --git a/library/Director/Web/Form/Element/StoredPassword.php b/library/Director/Web/Form/Element/StoredPassword.php
new file mode 100644
index 0000000..fa0545b
--- /dev/null
+++ b/library/Director/Web/Form/Element/StoredPassword.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Zend_Form_Element_Text as ZfText;
+
+/**
+ * StoredPassword
+ *
+ * This is a special form field and it might look a little bit weird at first
+ * sight. It's main use-case are stored cleartext passwords a user should be
+ * allowed to change.
+ *
+ * While this might sound simple, it's quite tricky if you try to fulfill the
+ * following requirements:
+ *
+ * - the current password should not be rendered to the HTML page (unless the
+ * user decides to change it)
+ * - it must be possible to visually distinct whether a password has been set
+ * - it should be impossible to "see" the length of the stored password
+ * - a changed password must be persisted
+ * - forms might be subject to multiple submissions in case other fields fail.
+ * If the user changed the password during the first submission attempt, the
+ * new string should not be lost.
+ * - all this must happen within the bounds of ZF1 form elements and related
+ * view helpers. This means that there is no related context available - and
+ * we do not know whether the form has been submitted and whether the current
+ * values have been populated from DB
+ *
+ * @package Icinga\Module\Director\Web\Form\Element
+ */
+class StoredPassword extends ZfText
+{
+ const UNCHANGED = '__UNCHANGED_VALUE__';
+
+ public $helper = 'formStoredPassword';
+
+ public function setValue($value)
+ {
+ if (\is_array($value) && isset($value['_value'], $value['_sent'])
+ && $value['_sent'] === 'y'
+ ) {
+ $value = $sentValue = $value['_value'];
+ if ($sentValue !== self::UNCHANGED) {
+ $this->setAttrib('sentValue', $sentValue);
+ }
+ } else {
+ $sentValue = null;
+ }
+
+ if ($value === self::UNCHANGED) {
+ return $this;
+ } else {
+ // Workaround for issue with modified DataTypes. This is Director-specific
+ if (\is_array($value)) {
+ $value = \json_encode($value);
+ }
+
+ return parent::setValue((string) $value);
+ }
+ }
+}
diff --git a/library/Director/Web/Form/Element/Text.php b/library/Director/Web/Form/Element/Text.php
new file mode 100644
index 0000000..eeb36f1
--- /dev/null
+++ b/library/Director/Web/Form/Element/Text.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+use Zend_Form_Element_Text as ZfText;
+
+class Text extends ZfText
+{
+ public function setValue($value)
+ {
+ if (\is_array($value)) {
+ $value = \json_encode($value);
+ }
+ return parent::setValue((string) $value);
+ }
+}
diff --git a/library/Director/Web/Form/Element/YesNo.php b/library/Director/Web/Form/Element/YesNo.php
new file mode 100644
index 0000000..3e8aaa7
--- /dev/null
+++ b/library/Director/Web/Form/Element/YesNo.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Element;
+
+/**
+ * Input control for booleans, gives y/n
+ */
+class YesNo extends OptionalYesNo
+{
+ public $options = array(
+ 'y' => 'Yes',
+ 'n' => 'No',
+ );
+}
diff --git a/library/Director/Web/Form/Filter/QueryColumnsFromSql.php b/library/Director/Web/Form/Filter/QueryColumnsFromSql.php
new file mode 100644
index 0000000..6f6d475
--- /dev/null
+++ b/library/Director/Web/Form/Filter/QueryColumnsFromSql.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Filter;
+
+use Exception;
+use Icinga\Data\Db\DbConnection;
+use Icinga\Module\Director\Forms\ImportSourceForm;
+use Zend_Filter_Interface;
+
+class QueryColumnsFromSql implements Zend_Filter_Interface
+{
+ /** @var ImportSourceForm */
+ private $form;
+
+ public function __construct(ImportSourceForm $form)
+ {
+ $this->form = $form;
+ }
+
+ public function filter($value)
+ {
+ $form = $this->form;
+ if (empty($value) || $form->hasChangedSetting('query')) {
+ try {
+ return implode(
+ ', ',
+ $this->getQueryColumns($form->getSentOrObjectSetting('query'))
+ );
+ } catch (Exception $e) {
+ $this->form->addUniqueException($e);
+ return '';
+ }
+ } else {
+ return $value;
+ }
+ }
+
+ protected function getQueryColumns($query)
+ {
+ $resourceName = $this->form->getSentOrObjectSetting('resource');
+ if (! $resourceName) {
+ return [];
+ }
+ $db = DbConnection::fromResourceName($resourceName)->getDbAdapter();
+
+ return array_keys((array) current($db->fetchAll($query)));
+ }
+}
diff --git a/library/Director/Web/Form/FormLoader.php b/library/Director/Web/Form/FormLoader.php
new file mode 100644
index 0000000..ea82857
--- /dev/null
+++ b/library/Director/Web/Form/FormLoader.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\ProgrammingError;
+use RuntimeException;
+
+class FormLoader
+{
+ public static function load($name, Module $module = null)
+ {
+ if ($module === null) {
+ try {
+ $basedir = Icinga::app()->getApplicationDir('forms');
+ } catch (ProgrammingError $e) {
+ throw new RuntimeException($e->getMessage(), 0, $e);
+ }
+ $ns = '\\Icinga\\Web\\Forms\\';
+ } else {
+ $basedir = $module->getFormDir();
+ $ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Forms\\';
+ }
+ if (preg_match('~^[a-z0-9/]+$~i', $name)) {
+ $parts = preg_split('~/~', $name);
+ $class = ucfirst(array_pop($parts)) . 'Form';
+ $file = sprintf('%s/%s/%s.php', rtrim($basedir, '/'), implode('/', $parts), $class);
+ if (file_exists($file)) {
+ require_once($file);
+ $class = $ns . $class;
+ $options = array();
+ if ($module !== null) {
+ $options['icingaModule'] = $module;
+ }
+
+ return new $class($options);
+ }
+ }
+
+ throw new RuntimeException(sprintf('Cannot load %s (%s), no such form', $name, $file));
+ }
+}
diff --git a/library/Director/Web/Form/IcingaObjectFieldLoader.php b/library/Director/Web/Form/IcingaObjectFieldLoader.php
new file mode 100644
index 0000000..c900edf
--- /dev/null
+++ b/library/Director/Web/Form/IcingaObjectFieldLoader.php
@@ -0,0 +1,628 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Hook\HostFieldHook;
+use Icinga\Module\Director\Hook\ServiceFieldHook;
+use Icinga\Module\Director\Objects\DirectorDatafieldCategory;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\ObjectApplyMatches;
+use Icinga\Web\Hook;
+use stdClass;
+use Zend_Db_Select as ZfSelect;
+use Zend_Form_Element as ZfElement;
+
+class IcingaObjectFieldLoader
+{
+ protected $form;
+
+ /** @var IcingaObject */
+ protected $object;
+
+ /** @var \Icinga\Module\Director\Db */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var DirectorDatafield[] */
+ protected $fields;
+
+ protected $elements;
+
+ protected $forceNull = array();
+
+ /** @var array Map element names to variable names 'elName' => 'varName' */
+ protected $nameMap = array();
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->object = $object;
+ $this->connection = $object->getConnection();
+ $this->db = $this->connection->getDbAdapter();
+ }
+
+ public function addFieldsToForm(DirectorObjectForm $form)
+ {
+ if ($this->fields || $this->object->supportsFields()) {
+ $this->attachFieldsToForm($form);
+ }
+
+ return $this;
+ }
+
+ public function loadFieldsForMultipleObjects($objects)
+ {
+ $fields = array();
+ foreach ($objects as $object) {
+ foreach ($this->prepareObjectFields($object) as $varname => $field) {
+ $varname = $field->get('varname');
+ if (array_key_exists($varname, $fields)) {
+ if ($field->get('datatype') !== $fields[$varname]->datatype) {
+ unset($fields[$varname]);
+ }
+
+ continue;
+ }
+
+ $fields[$varname] = $field;
+ }
+ }
+
+ $this->fields = $fields;
+
+ return $this;
+ }
+
+ /**
+ * Set a list of values
+ *
+ * Works in a fail-safe way, when a field does not exist the value will be
+ * silently ignored
+ *
+ * @param array $values key/value pairs with variable names and their value
+ * @param string $prefix An optional prefix that would be stripped from keys
+ *
+ * @return IcingaObjectFieldLoader
+ *
+ * @throws IcingaException
+ */
+ public function setValues($values, $prefix = null)
+ {
+ if (! $this->object->supportsCustomVars()) {
+ return $this;
+ }
+
+ if ($prefix === null) {
+ $len = null;
+ } else {
+ $len = strlen($prefix);
+ }
+ $vars = $this->object->vars();
+
+ foreach ($values as $key => $value) {
+ if ($len !== null) {
+ if (substr($key, 0, $len) === $prefix) {
+ $key = substr($key, $len);
+ } else {
+ continue;
+ }
+ }
+
+ $varName = $this->getElementVarName($prefix . $key);
+ if ($varName === null) {
+ // throw new IcingaException(
+ // 'Cannot set variable value for "%s", got no such element',
+ // $key
+ // );
+
+ // Silently ignore additional fields. One might have switched
+ // template or command
+ continue;
+ }
+
+ $el = $this->getElement($varName);
+ if ($el === null) {
+ // throw new IcingaException('No such element %s', $key);
+ // Same here.
+ continue;
+ }
+
+ $el->setValue($value);
+ $value = $el->getValue();
+ if ($value === '' || $value === array()) {
+ $value = null;
+ }
+
+ $vars->set($varName, $value);
+ }
+
+ // Hint: this does currently not happen, as removeFilteredFields did not
+ // take place yet. This has been added to be on the safe side when
+ // cleaning things up one future day
+ foreach ($this->forceNull as $key) {
+ $vars->set($key, null);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the fields for our object
+ *
+ * @return DirectorDatafield[]
+ */
+ public function getFields()
+ {
+ if ($this->fields === null) {
+ $this->fields = $this->prepareObjectFields($this->object);
+ }
+
+ return $this->fields;
+ }
+
+ /**
+ * Get the form elements for our fields
+ *
+ * @param DirectorObjectForm $form Optional
+ *
+ * @return ZfElement[]
+ */
+ public function getElements(DirectorObjectForm $form = null)
+ {
+ if ($this->elements === null) {
+ $this->elements = $this->createElements($form);
+ $this->setValuesFromObject($this->object);
+ }
+
+ return $this->elements;
+ }
+
+ /**
+ * Prepare the form elements for our fields
+ *
+ * @param DirectorObjectForm $form Optional
+ *
+ * @return self
+ */
+ public function prepareElements(DirectorObjectForm $form = null)
+ {
+ if ($this->object->supportsFields()) {
+ $this->getElements($form);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Attach our form fields to the given form
+ *
+ * This will also create a 'Custom properties' display group
+ *
+ * @param DirectorObjectForm $form
+ */
+ protected function attachFieldsToForm(DirectorObjectForm $form)
+ {
+ if ($this->fields === null) {
+ return;
+ }
+ $elements = $this->removeFilteredFields($this->getElements($form));
+
+ foreach ($elements as $element) {
+ $form->addElement($element);
+ }
+
+ $this->attachGroupElements($elements, $form);
+ }
+
+ /**
+ * @param ZfElement[] $elements
+ * @param DirectorObjectForm $form
+ */
+ protected function attachGroupElements(array $elements, DirectorObjectForm $form)
+ {
+ $categories = [];
+ $categoriesFetchedById = [];
+ foreach ($this->fields as $key => $field) {
+ if ($id = $field->get('category_id')) {
+ if (isset($categoriesFetchedById[$id])) {
+ $category = $categoriesFetchedById[$id];
+ } else {
+ $category = DirectorDatafieldCategory::loadWithAutoIncId($id, $form->getDb());
+ $categoriesFetchedById[$id] = $category;
+ }
+ } elseif ($field->hasCategory()) {
+ $category = $field->getCategory();
+ } else {
+ continue;
+ }
+ $categories[$key] = $category;
+ }
+ $prioIdx = \array_flip(\array_keys($categories));
+
+ foreach ($elements as $key => $element) {
+ if (isset($categories[$key])) {
+ $category = $categories[$key];
+ $form->addElementsToGroup(
+ [$element],
+ 'custom_fields:' . $category->get('category_name'),
+ DirectorObjectForm::GROUP_ORDER_CUSTOM_FIELD_CATEGORIES + $prioIdx[$key],
+ $category->get('category_name')
+ );
+ } else {
+ $form->addElementsToGroup(
+ [$element],
+ 'custom_fields',
+ DirectorObjectForm::GROUP_ORDER_CUSTOM_FIELDS,
+ $form->translate('Custom properties')
+ );
+ }
+ }
+ }
+
+ /**
+ * @param ZfElement[] $elements
+ * @return ZfElement[]
+ */
+ protected function removeFilteredFields(array $elements)
+ {
+ $filters = array();
+ foreach ($this->fields as $key => $field) {
+ if ($filter = $field->var_filter) {
+ $filters[$key] = Filter::fromQueryString($filter);
+ }
+ }
+
+ $kill = array();
+ $columns = array();
+ $object = $this->object;
+ if ($object instanceof IcingaHost) {
+ $prefix = 'host.vars.';
+ } elseif ($object instanceof IcingaService) {
+ $prefix = 'service.vars.';
+ } else {
+ return $elements;
+ }
+
+ $object->invalidateResolveCache();
+ $vars = $object::fromPlainObject(
+ $object->toPlainObject(true),
+ $object->getConnection()
+ )->getVars();
+
+ $prefixedVars = (object) array();
+ foreach ($vars as $k => $v) {
+ $prefixedVars->{$prefix . $k} = $v;
+ }
+
+ foreach ($filters as $key => $filter) {
+ ObjectApplyMatches::fixFilterColumns($filter);
+ /** @var $filter FilterChain|FilterExpression */
+ foreach ($filter->listFilteredColumns() as $column) {
+ $column = substr($column, strlen($prefix));
+ $columns[$column] = $column;
+ }
+ if (! $filter->matches($prefixedVars)) {
+ $kill[] = $key;
+ }
+ }
+
+ $vars = $object->vars();
+ foreach ($kill as $key) {
+ unset($elements[$key]);
+ $this->forceNull[$key] = $key;
+ // Hint: this should happen later on, currently execution order is
+ // a little bit weird
+ $vars->set($key, null);
+ }
+
+ foreach ($columns as $col) {
+ if (array_key_exists($col, $elements)) {
+ $el = $elements[$col];
+ $existingClass = $el->getAttrib('class');
+ if ($existingClass !== null && strlen($existingClass)) {
+ $el->setAttrib('class', $existingClass . ' autosubmit');
+ } else {
+ $el->setAttrib('class', 'autosubmit');
+ }
+ }
+ }
+
+ return $elements;
+ }
+
+ protected function getElementVarName($name)
+ {
+ if (array_key_exists($name, $this->nameMap)) {
+ return $this->nameMap[$name];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the form element for a specific field by it's variable name
+ *
+ * @param string $name
+ * @return null|ZfElement
+ */
+ protected function getElement($name)
+ {
+ $elements = $this->getElements();
+ if (array_key_exists($name, $elements)) {
+ return $this->elements[$name];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the form elements based on the given form
+ *
+ * @param DirectorObjectForm $form
+ *
+ * @return ZfElement[]
+ */
+ protected function createElements(DirectorObjectForm $form)
+ {
+ $elements = array();
+
+ foreach ($this->getFields() as $name => $field) {
+ $el = $field->getFormElement($form);
+ $elName = $el->getName();
+ if (array_key_exists($elName, $this->nameMap)) {
+ $form->addErrorMessage(sprintf(
+ 'Form element name collision, "%s" resolves to "%s", but this is also used for "%s"',
+ $name,
+ $elName,
+ $this->nameMap[$elName]
+ ));
+ }
+ $this->nameMap[$elName] = $name;
+ $elements[$name] = $el;
+ }
+
+ return $elements;
+ }
+
+ /**
+ * @param IcingaObject $object
+ */
+ protected function setValuesFromObject(IcingaObject $object)
+ {
+ foreach ($object->getVars() as $k => $v) {
+ if ($v !== null && $el = $this->getElement($k)) {
+ $el->setValue($v);
+ }
+ }
+ }
+
+ protected function mergeFields($listOfFields)
+ {
+ // TODO: Merge field for different object, mostly sets
+ }
+
+ /**
+ * Create the fields for our object
+ *
+ * @param IcingaObject $object
+ * @return DirectorDatafield[]
+ */
+ protected function prepareObjectFields($object)
+ {
+ $fields = $this->loadResolvedFieldsForObject($object);
+ if ($object->hasRelation('check_command')) {
+ try {
+ /** @var IcingaCommand $command */
+ $command = $object->getResolvedRelated('check_command');
+ } catch (Exception $e) {
+ // Ignore failures
+ $command = null;
+ }
+
+ if ($command) {
+ $cmdLoader = new static($command);
+ $cmdFields = $cmdLoader->getFields();
+ foreach ($cmdFields as $varname => $field) {
+ if (! array_key_exists($varname, $fields)) {
+ $fields[$varname] = $field;
+ }
+ }
+ }
+
+ // TODO -> filters!
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Create the fields for our object
+ *
+ * Follows the inheritance logic, resolves all fields and keeps the most
+ * specific ones. Returns a list of fields indexed by variable name
+ *
+ * @param IcingaObject $object
+ *
+ * @return DirectorDatafield[]
+ */
+ protected function loadResolvedFieldsForObject(IcingaObject $object)
+ {
+ $result = $this->loadDataFieldsForObject(
+ $object
+ );
+
+ $fields = array();
+ foreach ($result as $objectId => $varFields) {
+ foreach ($varFields as $var => $field) {
+ $fields[$var] = $field;
+ }
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @param IcingaObject[] $objectList List of objects
+ *
+ * @return array
+ */
+ protected function getIdsForObjectList($objectList)
+ {
+ $ids = [];
+ foreach ($objectList as $object) {
+ if ($object->hasBeenLoadedFromDb()) {
+ $ids[] = $object->get('id');
+ }
+ }
+
+ return $ids;
+ }
+
+ public function fetchFieldDetailsForObject(IcingaObject $object)
+ {
+ $ids = $object->listAncestorIds();
+ if ($id = $object->getProperty('id')) {
+ $ids[] = $id;
+ }
+ return $this->fetchFieldDetailsForIds($ids);
+ }
+
+ /***
+ * @param $objectIds
+ *
+ * @return \stdClass[]
+ */
+ protected function fetchFieldDetailsForIds($objectIds)
+ {
+ if (empty($objectIds)) {
+ return [];
+ }
+
+ $query = $this->prepareSelectForIds($objectIds);
+ return $this->db->fetchAll($query);
+ }
+
+ /**
+ * @param array $ids
+ *
+ * @return ZfSelect
+ */
+ protected function prepareSelectForIds(array $ids)
+ {
+ $object = $this->object;
+
+ $idColumn = 'f.' . $object->getShortTableName() . '_id';
+
+ $query = $this->db->select()->from(
+ array('df' => 'director_datafield'),
+ array(
+ 'object_id' => $idColumn,
+ 'icinga_type' => "('" . $object->getShortTableName() . "')",
+ 'var_filter' => 'f.var_filter',
+ 'is_required' => 'f.is_required',
+ 'id' => 'df.id',
+ 'category_id' => 'df.category_id',
+ 'varname' => 'df.varname',
+ 'caption' => 'df.caption',
+ 'description' => 'df.description',
+ 'datatype' => 'df.datatype',
+ 'format' => 'df.format',
+ )
+ )->join(
+ array('f' => $object->getTableName() . '_field'),
+ 'df.id = f.datafield_id',
+ array()
+ )->where($idColumn . ' IN (?)', $ids)
+ ->order('CASE WHEN var_filter IS NULL THEN 0 ELSE 1 END ASC')
+ ->order('df.caption ASC');
+
+ return $query;
+ }
+
+ /**
+ * Fetches fields for a given object
+ *
+ * Gives a list indexed by object id, with each entry being a list of that
+ * objects DirectorDatafield instances indexed by variable name
+ *
+ * @param IcingaObject $object
+ *
+ * @return array
+ */
+ public function loadDataFieldsForObject(IcingaObject $object)
+ {
+ $res = $this->fetchFieldDetailsForObject($object);
+
+ $result = [];
+ foreach ($res as $r) {
+ $id = $r->object_id;
+ unset($r->object_id);
+ if (! array_key_exists($id, $result)) {
+ $result[$id] = new stdClass;
+ }
+
+ $result[$id]->{$r->varname} = DirectorDatafield::fromDbRow(
+ $r,
+ $this->connection
+ );
+ }
+
+ foreach ($this->loadHookedDataFieldForObject($object) as $id => $fields) {
+ if (array_key_exists($id, $result)) {
+ foreach ($fields as $varName => $field) {
+ $result[$id]->$varName = $field;
+ }
+ } else {
+ $result[$id] = $fields;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return array
+ */
+ protected function loadHookedDataFieldForObject(IcingaObject $object)
+ {
+ $fields = [];
+ if ($object instanceof IcingaHost || $object instanceof IcingaService) {
+ $fields = $this->addHookedFields($object);
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @return mixed
+ */
+ protected function addHookedFields(IcingaObject $object)
+ {
+ $fields = [];
+ /** @var HostFieldHook|ServiceFieldHook $hook */
+ $type = ucfirst($object->getShortTableName());
+ foreach (Hook::all("Director\\${type}Field") as $hook) {
+ if ($hook->wants($object)) {
+ $id = $object->get('id');
+ $spec = $hook->getFieldSpec($object);
+ if (!array_key_exists($id, $fields)) {
+ $fields[$id] = new stdClass();
+ }
+ $fields[$id]->{$spec->getVarName()} = $spec->toDataField($object);
+ }
+ }
+ return $fields;
+ }
+}
diff --git a/library/Director/Web/Form/IconHelper.php b/library/Director/Web/Form/IconHelper.php
new file mode 100644
index 0000000..3add09b
--- /dev/null
+++ b/library/Director/Web/Form/IconHelper.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Icon helper class
+ *
+ * Should help to reduce redundant icon-lookup code. Currently with hardcoded
+ * icons only, could easily provide support for all of them as follows:
+ *
+ * $confFile = Icinga::app()
+ * ->getApplicationDir('fonts/fontello-ifont/config.json');
+ *
+ * $font = json_decode(file_get_contents($confFile));
+ * // 'icon-' is to be found in $font->css_prefix_text
+ * foreach ($font->glyphs as $icon) {
+ * // $icon->css (= 'help') -> 0x . dechex($icon->code)
+ * }
+ */
+class IconHelper
+{
+ private $icons = array(
+ 'minus' => 'e806',
+ 'trash' => 'e846',
+ 'plus' => 'e805',
+ 'cancel' => 'e804',
+ 'help' => 'e85b',
+ 'angle-double-right' => 'e87b',
+ 'up-big' => 'e825',
+ 'down-big' => 'e828',
+ 'down-open' => 'e821',
+ );
+
+ private $mappedUtf8Icons;
+
+ private $reversedUtf8Icons;
+
+ private static $instance;
+
+ public function __construct()
+ {
+ $this->prepareIconMappings();
+ }
+
+ public static function instance()
+ {
+ if (self::$instance === null) {
+ self::$instance = new static;
+ }
+
+ return self::$instance;
+ }
+
+ public function characterIconName($character)
+ {
+ if (array_key_exists($character, $this->reversedUtf8Icons)) {
+ return $this->reversedUtf8Icons[$character];
+ } else {
+ throw new ProgrammingError('There is no mapping for the given character');
+ }
+ }
+
+ protected function hexToCharacter($hex)
+ {
+ return json_decode('"\u' . $hex . '"');
+ }
+
+ public function iconCharacter($name)
+ {
+ if (array_key_exists($name, $this->mappedUtf8Icons)) {
+ return $this->mappedUtf8Icons[$name];
+ } else {
+ return $this->mappedUtf8Icons['help'];
+ }
+ }
+
+ protected function prepareIconMappings()
+ {
+ $this->mappedUtf8Icons = array();
+ $this->reversedUtf8Icons = array();
+ foreach ($this->icons as $name => $hex) {
+ $character = $this->hexToCharacter($hex);
+ $this->mappedUtf8Icons[$name] = $character;
+ $this->reversedUtf8Icons[$character] = $name;
+ }
+ }
+}
diff --git a/library/Director/Web/Form/IplElement/ExtensibleSetElement.php b/library/Director/Web/Form/IplElement/ExtensibleSetElement.php
new file mode 100644
index 0000000..a4dbb20
--- /dev/null
+++ b/library/Director/Web/Form/IplElement/ExtensibleSetElement.php
@@ -0,0 +1,570 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\IplElement;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\IcingaConfig\ExtensibleSet as Set;
+use Icinga\Module\Director\Web\Form\IconHelper;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\Translation\TranslationHelper;
+
+class ExtensibleSetElement extends BaseHtmlElement
+{
+ use TranslationHelper;
+
+ protected $tag = 'ul';
+
+ /** @var Set */
+ protected $set;
+
+ private $id;
+
+ private $name;
+
+ private $value;
+
+ private $description;
+
+ private $multiOptions;
+
+ private $validOptions;
+
+ private $chosenOptionCount = 0;
+
+ private $suggestionContext;
+
+ private $sorted = false;
+
+ private $disabled = false;
+
+ private $remainingAttribs;
+
+ private $hideOptions = [];
+
+ private $inherited;
+
+ private $inheritedFrom;
+
+ protected $defaultAttributes = [
+ 'class' => 'extensible-set'
+ ];
+
+ protected function __construct($name)
+ {
+ $this->name = $this->id = $name;
+ }
+
+ public function hideOptions($options)
+ {
+ $this->hideOptions = array_merge($this->hideOptions, $options);
+ return $this;
+ }
+
+ private function setMultiOptions($options)
+ {
+ $this->multiOptions = $options;
+ $this->validOptions = $this->flattenOptions($options);
+ }
+
+ protected function isValidOption($option)
+ {
+ if ($this->validOptions === null) {
+ if ($this->suggestionContext === null) {
+ return true;
+ } else {
+ // TODO: ask suggestionContext, if any
+ return true;
+ }
+ } else {
+ return in_array($option, $this->validOptions);
+ }
+ }
+
+ private function disable($disable = true)
+ {
+ $this->disabled = (bool) $disable;
+ }
+
+ private function isDisabled()
+ {
+ return $this->disabled;
+ }
+
+ private function isSorted()
+ {
+ return $this->sorted;
+ }
+
+ public function setValue($value)
+ {
+ if ($value instanceof Set) {
+ $value = $value->toPlainObject();
+ }
+
+ if (is_array($value)) {
+ $value = array_filter($value, 'strlen');
+ }
+
+ if (null !== $value && ! is_array($value)) {
+ throw new ProgrammingError(
+ 'Got unexpected value, no array: %s',
+ var_export($value, 1)
+ );
+ }
+
+ $this->value = $value;
+ return $this;
+ }
+
+ protected function extractZfInfo(&$attribs = null)
+ {
+ if ($attribs === null) {
+ return;
+ }
+
+ foreach (['id', 'name', 'descriptions'] as $key) {
+ if (array_key_exists($key, $attribs)) {
+ $this->$key = $attribs[$key];
+ unset($attribs[$key]);
+ }
+ }
+ if (array_key_exists('disable', $attribs)) {
+ $this->disable($attribs['disable']);
+ unset($attribs['disable']);
+ }
+ if (array_key_exists('value', $attribs)) {
+ $this->setValue($attribs['value']);
+ unset($attribs['value']);
+ }
+ if (array_key_exists('inherited', $attribs)) {
+ $this->inherited = $attribs['inherited'];
+ unset($attribs['inherited']);
+ }
+ if (array_key_exists('inheritedFrom', $attribs)) {
+ $this->inheritedFrom = $attribs['inheritedFrom'];
+ unset($attribs['inheritedFrom']);
+ }
+
+ if (array_key_exists('multiOptions', $attribs)) {
+ $this->setMultiOptions($attribs['multiOptions']);
+ unset($attribs['multiOptions']);
+ }
+
+ if (array_key_exists('hideOptions', $attribs)) {
+ $this->hideOptions($attribs['hideOptions']);
+ unset($attribs['hideOptions']);
+ }
+
+ if (array_key_exists('sorted', $attribs)) {
+ $this->sorted = (bool) $attribs['sorted'];
+ unset($attribs['sorted']);
+ }
+
+ if (array_key_exists('description', $attribs)) {
+ $this->description = $attribs['description'];
+ unset($attribs['description']);
+ }
+
+ if (array_key_exists('suggest', $attribs)) {
+ $this->suggestionContext = $attribs['suggest'];
+ unset($attribs['suggest']);
+ }
+
+ if (! empty($attribs)) {
+ $this->remainingAttribs = $attribs;
+ }
+ }
+
+ /**
+ * Generates an 'extensible set' element.
+ *
+ * @codingStandardsIgnoreEnd
+ *
+ * @param string|array $name If a string, the element name. If an
+ * array, all other parameters are ignored, and the array elements
+ * are used in place of added parameters.
+ *
+ * @param mixed $value The element value.
+ *
+ * @param array $attribs Attributes for the element tag.
+ *
+ * @return string The element XHTML.
+ */
+ public static function fromZfDingens($name, $value = null, $attribs = null)
+ {
+ $el = new static($name);
+ $el->extractZfInfo($attribs);
+ $el->setValue($value);
+ return $el->render();
+ }
+
+ protected function assemble()
+ {
+ $this->addChosenOptions();
+ $this->addAddMore();
+
+ if ($this->isSorted()) {
+ $this->getAttributes()->add('class', 'sortable');
+ }
+ if (null !== $this->description) {
+ $this->addDescription($this->description);
+ }
+ }
+
+ private function eventuallyAddAutosuggestion(BaseHtmlElement $element)
+ {
+ if ($this->suggestionContext !== null) {
+ $attrs = $element->getAttributes();
+ $attrs->add('class', 'director-suggest');
+ $attrs->set([
+ 'data-suggestion-context' => $this->suggestionContext,
+ ]);
+ }
+
+ return $element;
+ }
+
+ private function hasAvailableMultiOptions()
+ {
+ return count($this->multiOptions) > 1 || strlen(key($this->multiOptions));
+ }
+
+ private function addAddMore()
+ {
+ $cnt = $this->chosenOptionCount;
+
+ if ($this->multiOptions) {
+ if (! $this->hasAvailableMultiOptions()) {
+ return;
+ }
+ $field = Html::tag('select', ['class' => 'autosubmit']);
+ $more = $this->inherited === null
+ ? $this->translate('- add more -')
+ : $this->getInheritedInfo();
+ $field->add(Html::tag('option', [
+ 'value' => '',
+ 'tabindex' => '-1'
+ ], $more));
+
+ foreach ($this->multiOptions as $key => $label) {
+ if ($key === null) {
+ $key = '';
+ }
+ if (is_array($label)) {
+ $optGroup = Html::tag('optgroup', ['label' => $key]);
+ foreach ($label as $grpKey => $grpLabel) {
+ $optGroup->add(
+ Html::tag('option', ['value' => $grpKey], $grpLabel)
+ );
+ }
+ $field->add($optGroup);
+ } else {
+ $option = Html::tag('option', ['value' => $key], $label);
+ $field->add($option);
+ }
+ }
+ } else {
+ $field = Html::tag('input', [
+ 'type' => 'text',
+ 'placeholder' => $this->inherited === null
+ ? $this->translate('Add a new one...')
+ : $this->getInheritedInfo(),
+ ]);
+ }
+ $field->addAttributes([
+ 'id' => $this->id . $this->suffix($cnt),
+ 'name' => $this->name . '[]',
+ ]);
+ $this->eventuallyAddAutosuggestion(
+ $this->addRemainingAttributes(
+ $this->eventuallyDisable($field)
+ )
+ );
+ if ($cnt !== 0) { // TODO: was === 0?!
+ $field->getAttributes()->add('class', 'extend-set');
+ }
+
+ if ($this->suggestionContext === null) {
+ $this->add(Html::tag('li', null, [
+ $this->createAddNewButton(),
+ $field
+ ]));
+ } else {
+ $this->add(Html::tag('li', null, [
+ $this->newInlineButtons(
+ $this->renderDropDownButton()
+ ),
+ $field
+ ]));
+ }
+ }
+
+ private function getInheritedInfo()
+ {
+ if ($this->inheritedFrom === null) {
+ return \sprintf(
+ $this->translate('%s (inherited)'),
+ $this->stringifyInheritedValue()
+ );
+ } else {
+ return \sprintf(
+ $this->translate('%s (inherited from %s)'),
+ $this->stringifyInheritedValue(),
+ $this->inheritedFrom
+ );
+ }
+ }
+
+ private function stringifyInheritedValue()
+ {
+ if (\is_array($this->inherited)) {
+ return \implode(', ', $this->inherited);
+ } else {
+ return \sprintf(
+ $this->translate('%s (not an Array!)'),
+ \var_export($this->inherited, 1)
+ );
+ }
+ }
+
+ private function createAddNewButton()
+ {
+ return $this->newInlineButtons(
+ $this->eventuallyDisable($this->renderAddButton())
+ );
+ }
+
+ private function addChosenOptions()
+ {
+ if (null === $this->value) {
+ return;
+ }
+ $total = count($this->value);
+
+ foreach ($this->value as $val) {
+ if (in_array($val, $this->hideOptions)) {
+ continue;
+ }
+
+ if ($this->multiOptions !== null) {
+ if ($this->isValidOption($val)) {
+ $this->multiOptions = $this->removeOption(
+ $this->multiOptions,
+ $val
+ );
+ // TODO:
+ // $this->removeOption($val);
+ }
+ }
+
+ $text = Html::tag('input', [
+ 'type' => 'text',
+ 'name' => $this->name . '[]',
+ 'id' => $this->id . $this->suffix($this->chosenOptionCount),
+ 'value' => $val
+ ]);
+ $text->getAttributes()->set([
+ 'autocomplete' => 'off',
+ 'autocorrect' => 'off',
+ 'autocapitalize' => 'off',
+ 'spellcheck' => 'false',
+ ]);
+
+ $this->addRemainingAttributes($this->eventuallyDisable($text));
+ $this->add(Html::tag('li', null, [
+ $this->getOptionButtons($this->chosenOptionCount, $total),
+ $text
+ ]));
+ $this->chosenOptionCount++;
+ }
+ }
+
+ private function addRemainingAttributes(BaseHtmlElement $element)
+ {
+ if ($this->remainingAttribs !== null) {
+ $element->getAttributes()->add($this->remainingAttribs);
+ }
+
+ return $element;
+ }
+
+ private function eventuallyDisable(BaseHtmlElement $element)
+ {
+ if ($this->isDisabled()) {
+ $this->disableElement($element);
+ }
+
+ return $element;
+ }
+
+ private function disableElement(BaseHtmlElement $element)
+ {
+ $element->getAttributes()->set('disabled', 'disabled');
+ return $element;
+ }
+
+ private function disableIf(BaseHtmlElement $element, $condition)
+ {
+ if ($condition) {
+ $this->disableElement($element);
+ }
+
+ return $element;
+ }
+
+ private function getOptionButtons($cnt, $total)
+ {
+ if ($this->isDisabled()) {
+ return [];
+ }
+ $first = $cnt === 0;
+ $last = $cnt === $total - 1;
+ $name = $this->name;
+ $buttons = $this->newInlineButtons();
+ if ($this->isSorted()) {
+ $buttons->add([
+ $this->disableIf($this->renderDownButton($name, $cnt), $last),
+ $this->disableIf($this->renderUpButton($name, $cnt), $first)
+ ]);
+ }
+
+ $buttons->add($this->renderDeleteButton($name, $cnt));
+
+ return $buttons;
+ }
+
+ protected function newInlineButtons($content = null)
+ {
+ return Html::tag('span', ['class' => 'inline-buttons'], $content);
+ }
+
+ protected function addDescription($description)
+ {
+ $this->add(
+ Html::tag('p', ['class' => 'description'], $description)
+ );
+ }
+
+ private function flattenOptions($options)
+ {
+ $flat = array();
+
+ foreach ($options as $key => $option) {
+ if (is_array($option)) {
+ foreach ($option as $k => $o) {
+ $flat[] = $k;
+ }
+ } else {
+ $flat[] = $key;
+ }
+ }
+
+ return $flat;
+ }
+
+ private function removeOption($options, $option)
+ {
+ $unset = array();
+ foreach ($options as $key => & $value) {
+ if (is_array($value)) {
+ $value = $this->removeOption($value, $option);
+ if (empty($value)) {
+ $unset[] = $key;
+ }
+ } elseif ($key === $option) {
+ $unset[] = $key;
+ }
+ }
+
+ foreach ($unset as $key) {
+ unset($options[$key]);
+ }
+
+ return $options;
+ }
+
+ private function suffix($cnt)
+ {
+ if ($cnt === 0) {
+ return '';
+ } else {
+ return '_' . $cnt;
+ }
+ }
+
+ private function renderDropDownButton()
+ {
+ return $this->createRelatedAction(
+ 'drop-down',
+ $this->name,
+ $this->translate('Show available options'),
+ 'down-open'
+ );
+ }
+
+ private function renderAddButton()
+ {
+ return $this->createRelatedAction(
+ 'add',
+ // This would interfere with how PHP resolves _POST arrays. So we
+ // use a fake name for now, that way the button will be ignored and
+ // behave similar to an auto-submission
+ 'X_' . $this->name,
+ $this->translate('Add a new entry'),
+ 'plus'
+ );
+ }
+
+ private function renderDeleteButton($name, $cnt)
+ {
+ return $this->createRelatedAction(
+ 'remove',
+ $name . '_' . $cnt,
+ $this->translate('Remove this entry'),
+ 'cancel'
+ );
+ }
+
+ private function renderUpButton($name, $cnt)
+ {
+ return $this->createRelatedAction(
+ 'move-up',
+ $name . '_' . $cnt,
+ $this->translate('Move up'),
+ 'up-big'
+ );
+ }
+
+ private function renderDownButton($name, $cnt)
+ {
+ return $this->createRelatedAction(
+ 'move-down',
+ $name . '_' . $cnt,
+ $this->translate('Move down'),
+ 'down-big'
+ );
+ }
+
+ protected function makeActionName($name, $action)
+ {
+ return $name . '__' . str_replace('-', '_', strtoupper($action));
+ }
+
+ protected function createRelatedAction(
+ $action,
+ $name,
+ $title,
+ $icon
+ ) {
+ $input = Html::tag('input', [
+ 'type' => 'submit',
+ 'class' => ['related-action', 'action-' . $action],
+ 'name' => $this->makeActionName($name, $action),
+ 'value' => IconHelper::instance()->iconCharacter($icon),
+ 'title' => $title
+ ]);
+
+ return $input;
+ }
+}
diff --git a/library/Director/Web/Form/QuickBaseForm.php b/library/Director/Web/Form/QuickBaseForm.php
new file mode 100644
index 0000000..8d25ffb
--- /dev/null
+++ b/library/Director/Web/Form/QuickBaseForm.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use ipl\Html\Html;
+use ipl\Html\ValidHtml;
+use Zend_Form;
+
+abstract class QuickBaseForm extends Zend_Form implements ValidHtml
+{
+ /**
+ * The Icinga module this form belongs to. Usually only set if the
+ * form is initialized through the FormLoader
+ *
+ * @var Module
+ */
+ protected $icingaModule;
+
+ protected $icingaModuleName;
+
+ private $hintCount = 0;
+
+ public function __construct($options = null)
+ {
+ $this->callZfConstructor($this->handleOptions($options))
+ ->initializePrefixPaths();
+ }
+
+ protected function callZfConstructor($options = null)
+ {
+ parent::__construct($options);
+ return $this;
+ }
+
+ protected function initializePrefixPaths()
+ {
+ $this->addPrefixPathsForDirector();
+ if ($this->icingaModule && $this->icingaModuleName !== 'director') {
+ $this->addPrefixPathsForModule($this->icingaModule);
+ }
+ }
+
+ protected function addPrefixPathsForDirector()
+ {
+ $module = Icinga::app()
+ ->getModuleManager()
+ ->loadModule('director')
+ ->getModule('director');
+
+ $this->addPrefixPathsForModule($module);
+ }
+
+ public function addPrefixPathsForModule(Module $module)
+ {
+ $basedir = sprintf(
+ '%s/%s/Web/Form',
+ $module->getLibDir(),
+ ucfirst($module->getName())
+ );
+
+ $this->addPrefixPath(
+ __NAMESPACE__ . '\\Element\\',
+ $basedir . '/Element',
+ static::ELEMENT
+ );
+
+ return $this;
+ }
+
+ public function addHidden($name, $value = null)
+ {
+ $this->addElement('hidden', $name);
+ $el = $this->getElement($name);
+ $el->setDecorators(array('ViewHelper'));
+ if ($value !== null) {
+ $this->setDefault($name, $value);
+ $el->setValue($value);
+ }
+
+ return $this;
+ }
+
+ // TODO: Should be an element
+ public function addHtmlHint($html, $options = [])
+ {
+ return $this->addHtml(
+ Html::tag('div', ['class' => 'hint'], $html),
+ $options
+ );
+ }
+
+ public function addHtml($html, $options = [])
+ {
+ if ($html instanceof ValidHtml) {
+ $html = $html->render();
+ }
+
+ if (array_key_exists('name', $options)) {
+ $name = $options['name'];
+ unset($options['name']);
+ } else {
+ $name = '_HINT' . ++$this->hintCount;
+ }
+
+ $this->addElement('simpleNote', $name, $options);
+ $this->getElement($name)
+ ->setValue($html)
+ ->setIgnore(true)
+ ->setDecorators(array('ViewHelper'));
+
+ return $this;
+ }
+
+ public function optionalEnum($enum, $nullLabel = null)
+ {
+ if ($nullLabel === null) {
+ $nullLabel = $this->translate('- please choose -');
+ }
+
+ return array(null => $nullLabel) + $enum;
+ }
+
+ protected function handleOptions($options = null)
+ {
+ if ($options === null) {
+ return $options;
+ }
+
+ if (array_key_exists('icingaModule', $options)) {
+ /** @var Module icingaModule */
+ $this->icingaModule = $options['icingaModule'];
+ $this->icingaModuleName = $this->icingaModule->getName();
+ unset($options['icingaModule']);
+ }
+
+ return $options;
+ }
+
+ public function setIcingaModule(Module $module)
+ {
+ $this->icingaModule = $module;
+ return $this;
+ }
+
+ protected function loadForm($name, Module $module = null)
+ {
+ if ($module === null) {
+ $module = $this->icingaModule;
+ }
+
+ return FormLoader::load($name, $module);
+ }
+
+ protected function valueIsEmpty($value)
+ {
+ if ($value === null) {
+ return true;
+ }
+
+ if (is_array($value)) {
+ return empty($value);
+ }
+
+ return strlen($value) === 0;
+ }
+
+ public function translate($string)
+ {
+ if ($this->icingaModuleName === null) {
+ return t($string);
+ } else {
+ return mt($this->icingaModuleName, $string);
+ }
+ }
+}
diff --git a/library/Director/Web/Form/QuickForm.php b/library/Director/Web/Form/QuickForm.php
new file mode 100644
index 0000000..91c8f00
--- /dev/null
+++ b/library/Director/Web/Form/QuickForm.php
@@ -0,0 +1,641 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Notification;
+use Icinga\Web\Request;
+use Icinga\Web\Response;
+use Icinga\Web\Url;
+use InvalidArgumentException;
+use Exception;
+use RuntimeException;
+
+/**
+ * QuickForm wants to be a base class for simple forms
+ */
+abstract class QuickForm extends QuickBaseForm
+{
+ const ID = '__FORM_NAME';
+
+ const CSRF = '__FORM_CSRF';
+
+ /**
+ * The name of this form
+ */
+ protected $formName;
+
+ /**
+ * Whether the form has been sent
+ */
+ protected $hasBeenSent;
+
+ /**
+ * Whether the form has been sent
+ */
+ protected $hasBeenSubmitted;
+
+ /**
+ * The submit caption, element - still tbd
+ */
+ // protected $submit;
+
+ /**
+ * Our request
+ */
+ protected $request;
+
+ protected $successUrl;
+
+ protected $successMessage;
+
+ protected $submitLabel;
+
+ protected $submitButtonName;
+
+ protected $deleteButtonName;
+
+ protected $fakeSubmitButtonName;
+
+ /**
+ * Whether form elements have already been created
+ */
+ protected $didSetup = false;
+
+ protected $isApiRequest = false;
+
+ protected $successCallbacks = [];
+
+ protected $calledSuccessCallbacks = false;
+
+ protected $onRequestCallbacks = [];
+
+ protected $calledOnRequestCallbacks = false;
+
+ public function __construct($options = null)
+ {
+ parent::__construct($options);
+
+ $this->setMethod('post');
+ $this->getActionFromRequest()
+ ->createIdElement()
+ ->regenerateCsrfToken()
+ ->setPreferredDecorators();
+ }
+
+ protected function getActionFromRequest()
+ {
+ $this->setAction(Url::fromRequest());
+ return $this;
+ }
+
+ protected function setPreferredDecorators()
+ {
+ $current = $this->getAttrib('class');
+ $current .= ' director-form';
+ if ($current) {
+ $this->setAttrib('class', "$current autofocus");
+ } else {
+ $this->setAttrib('class', 'autofocus');
+ }
+ $this->setDecorators(
+ array(
+ 'Description',
+ array('FormErrors', array('onlyCustomFormErrors' => true)),
+ 'FormElements',
+ 'Form'
+ )
+ );
+
+ return $this;
+ }
+
+ protected function addSubmitButton($label, $options = [])
+ {
+ $el = $this->createElement('submit', $label, $options)
+ ->setLabel($label)
+ ->setDecorators(array('ViewHelper'));
+ $this->submitButtonName = $el->getName();
+ $this->setSubmitLabel($label);
+ $this->addElement($el);
+ }
+
+ protected function addStandaloneSubmitButton($label, $options = [])
+ {
+ $this->addSubmitButton($label, $options);
+ $this->addDisplayGroup([$this->submitButtonName], 'buttons', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'p')),
+ ),
+ 'order' => 1000,
+ ));
+ }
+
+ protected function addSubmitButtonIfSet()
+ {
+ if (false === ($label = $this->getSubmitLabel())) {
+ return;
+ }
+
+ if ($this->submitButtonName && $el = $this->getElement($this->submitButtonName)) {
+ return;
+ }
+
+ $this->addSubmitButton($label);
+
+ $fakeEl = $this->createElement('submit', '_FAKE_SUBMIT', array(
+ 'role' => 'none',
+ 'tabindex' => '-1',
+ ))
+ ->setLabel($label)
+ ->setDecorators(array('ViewHelper'));
+ $this->fakeSubmitButtonName = $fakeEl->getName();
+ $this->addElement($fakeEl);
+
+ $this->addDisplayGroup(
+ array($this->fakeSubmitButtonName),
+ 'fake_button',
+ array(
+ 'decorators' => array('FormElements'),
+ 'order' => 1,
+ )
+ );
+
+ $this->addButtonDisplayGroup();
+ }
+
+ protected function addButtonDisplayGroup()
+ {
+ $grp = array(
+ $this->submitButtonName,
+ $this->deleteButtonName
+ );
+ $this->addDisplayGroup($grp, 'buttons', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'DtDdWrapper',
+ ),
+ 'order' => 1000,
+ ));
+ }
+
+ protected function addSimpleDisplayGroup($elements, $name, $options)
+ {
+ if (! array_key_exists('decorators', $options)) {
+ $options['decorators'] = array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ );
+ }
+
+ return $this->addDisplayGroup($elements, $name, $options);
+ }
+
+ protected function createIdElement()
+ {
+ if ($this->isApiRequest()) {
+ return $this;
+ }
+ $this->detectName();
+ $this->addHidden(self::ID, $this->getName());
+ $this->getElement(self::ID)->setIgnore(true);
+ return $this;
+ }
+
+ public function getSentValue($name, $default = null)
+ {
+ $request = $this->getRequest();
+ if ($request->isPost() && $this->hasBeenSent()) {
+ return $request->getPost($name);
+ } else {
+ return $default;
+ }
+ }
+
+ public function getSubmitLabel()
+ {
+ if ($this->submitLabel === null) {
+ return $this->translate('Submit');
+ }
+
+ return $this->submitLabel;
+ }
+
+ public function setSubmitLabel($label)
+ {
+ $this->submitLabel = $label;
+ return $this;
+ }
+
+ public function setApiRequest($isApiRequest = true)
+ {
+ $this->isApiRequest = $isApiRequest;
+ return $this;
+ }
+
+ public function isApiRequest()
+ {
+ if ($this->isApiRequest === null) {
+ if ($this->request === null) {
+ throw new RuntimeException(
+ 'Early access to isApiRequest(). This is not possible, sorry'
+ );
+ }
+
+ return $this->getRequest()->isApiRequest();
+ } else {
+ return $this->isApiRequest;
+ }
+ }
+
+ public function regenerateCsrfToken()
+ {
+ if ($this->isApiRequest()) {
+ return $this;
+ }
+ if (! $element = $this->getElement(self::CSRF)) {
+ $this->addHidden(self::CSRF, CsrfToken::generate());
+ $element = $this->getElement(self::CSRF);
+ }
+ $element->setIgnore(true);
+
+ return $this;
+ }
+
+ public function removeCsrfToken()
+ {
+ $this->removeElement(self::CSRF);
+ return $this;
+ }
+
+ public function setSuccessUrl($url, $params = null)
+ {
+ if (! $url instanceof Url) {
+ $url = Url::fromPath($url);
+ }
+ if ($params !== null) {
+ $url->setParams($params);
+ }
+ $this->successUrl = $url;
+ return $this;
+ }
+
+ public function getSuccessUrl()
+ {
+ $url = $this->successUrl ?: $this->getAction();
+ if (! $url instanceof Url) {
+ $url = Url::fromPath($url);
+ }
+
+ return $url;
+ }
+
+ protected function beforeSetup()
+ {
+ }
+
+ public function setup()
+ {
+ }
+
+ protected function onSetup()
+ {
+ }
+
+ public function setAction($action)
+ {
+ if ($action instanceof Url) {
+ $action = $action->getAbsoluteUrl('&');
+ }
+
+ return parent::setAction($action);
+ }
+
+ public function hasBeenSubmitted()
+ {
+ if ($this->hasBeenSubmitted === null) {
+ $req = $this->getRequest();
+ if ($req->isApiRequest()) {
+ return $this->hasBeenSubmitted = true;
+ }
+ if ($req->isPost()) {
+ if (! $this->hasSubmitButton()) {
+ return $this->hasBeenSubmitted = $this->hasBeenSent();
+ }
+
+ $this->hasBeenSubmitted = $this->pressedButton(
+ $this->fakeSubmitButtonName,
+ $this->getSubmitLabel()
+ ) || $this->pressedButton(
+ $this->submitButtonName,
+ $this->getSubmitLabel()
+ );
+ } else {
+ $this->hasBeenSubmitted = false;
+ }
+ }
+
+ return $this->hasBeenSubmitted;
+ }
+
+ protected function hasSubmitButton()
+ {
+ return $this->submitButtonName !== null;
+ }
+
+ protected function pressedButton($name, $label)
+ {
+ $req = $this->getRequest();
+ if (! $req->isPost()) {
+ return false;
+ }
+
+ $req = $this->getRequest();
+ $post = $req->getPost();
+
+ return array_key_exists($name, $post)
+ && $post[$name] === $label;
+ }
+
+ protected function beforeValidation($data = array())
+ {
+ }
+
+ public function prepareElements()
+ {
+ if (! $this->didSetup) {
+ $this->beforeSetup();
+ $this->setup();
+ $this->onSetup();
+ $this->didSetup = true;
+ }
+
+ return $this;
+ }
+
+ public function handleRequest(Request $request = null)
+ {
+ if ($request === null) {
+ $request = $this->getRequest();
+ } else {
+ $this->setRequest($request);
+ }
+
+ $this->prepareElements();
+ $this->addSubmitButtonIfSet();
+
+ if ($this->hasBeenSent()) {
+ $post = $request->getPost();
+ if ($this->hasBeenSubmitted()) {
+ $this->beforeValidation($post);
+ if ($this->isValid($post)) {
+ try {
+ $this->onSuccess();
+ $this->callOnSuccessCallables();
+ } catch (Exception $e) {
+ $this->addException($e);
+ $this->onFailure();
+ }
+ } else {
+ $this->onFailure();
+ }
+ } else {
+ $this->setDefaults($post);
+ }
+ }
+
+ return $this;
+ }
+
+ public function addException(Exception $e, $elementName = null)
+ {
+ $msg = $this->getErrorMessageForException($e);
+ if ($el = $this->getElement($elementName)) {
+ $el->addError($msg);
+ } else {
+ $this->addError($msg);
+ }
+ }
+
+ public function addUniqueErrorMessage($msg)
+ {
+ if (! in_array($msg, $this->getErrorMessages())) {
+ $this->addErrorMessage($msg);
+ }
+
+ return $this;
+ }
+
+ public function addUniqueException(Exception $e)
+ {
+ $msg = $this->getErrorMessageForException($e);
+
+ if (! in_array($msg, $this->getErrorMessages())) {
+ $this->addErrorMessage($msg);
+ }
+
+ return $this;
+ }
+
+ protected function getErrorMessageForException(Exception $e)
+ {
+ $file = preg_split('/[\/\\\]/', $e->getFile(), -1, PREG_SPLIT_NO_EMPTY);
+ $file = array_pop($file);
+ return sprintf(
+ '%s (%s:%d)',
+ $e->getMessage(),
+ $file,
+ $e->getLine()
+ );
+ }
+
+ public function onSuccess()
+ {
+ $this->redirectOnSuccess();
+ }
+
+ /**
+ * @param callable $callable
+ * @return $this
+ */
+ public function callOnRequest($callable)
+ {
+ if (! is_callable($callable)) {
+ throw new InvalidArgumentException(
+ 'callOnRequest() expects a callable'
+ );
+ }
+ $this->onRequestCallbacks[] = $callable;
+
+ return $this;
+ }
+
+ protected function callOnRequestCallables()
+ {
+ if (! $this->calledOnRequestCallbacks) {
+ $this->calledOnRequestCallbacks = true;
+ foreach ($this->onRequestCallbacks as $callable) {
+ $callable($this);
+ }
+ }
+ }
+
+ /**
+ * @param callable $callable
+ * @return $this
+ */
+ public function callOnSuccess($callable)
+ {
+ if (! is_callable($callable)) {
+ throw new InvalidArgumentException(
+ 'callOnSuccess() expects a callable'
+ );
+ }
+ $this->successCallbacks[] = $callable;
+
+ return $this;
+ }
+
+ protected function callOnSuccessCallables()
+ {
+ if (! $this->calledSuccessCallbacks) {
+ $this->calledSuccessCallbacks = true;
+ foreach ($this->successCallbacks as $callable) {
+ $callable($this);
+ }
+ }
+ }
+
+ public function setSuccessMessage($message)
+ {
+ $this->successMessage = $message;
+ return $this;
+ }
+
+ public function getSuccessMessage($message = null)
+ {
+ if ($message !== null) {
+ return $message;
+ }
+ if ($this->successMessage === null) {
+ return t('Form has successfully been sent');
+ }
+ return $this->successMessage;
+ }
+
+ public function redirectOnSuccess($message = null)
+ {
+ if ($this->isApiRequest()) {
+ // TODO: Set the status line message?
+ $this->successMessage = $this->getSuccessMessage($message);
+ $this->callOnSuccessCallables();
+ return;
+ }
+
+ $url = $this->getSuccessUrl();
+ $this->callOnSuccessCallables();
+ $this->notifySuccess($this->getSuccessMessage($message));
+ $this->redirectAndExit($url);
+ }
+
+ public function onFailure()
+ {
+ }
+
+ public function notifySuccess($message = null)
+ {
+ if ($message === null) {
+ $message = t('Form has successfully been sent');
+ }
+ Notification::success($message);
+ return $this;
+ }
+
+ public function notifyError($message)
+ {
+ Notification::error($message);
+ return $this;
+ }
+
+ protected function redirectAndExit($url)
+ {
+ /** @var Response $response */
+ $response = Icinga::app()->getFrontController()->getResponse();
+ $response->redirectAndExit($url);
+ }
+
+ protected function setHttpResponseCode($code)
+ {
+ Icinga::app()->getFrontController()->getResponse()->setHttpResponseCode($code);
+ return $this;
+ }
+
+ protected function onRequest()
+ {
+ $this->callOnRequestCallables();
+ }
+
+ public function setRequest(Request $request)
+ {
+ if ($this->request !== null) {
+ throw new RuntimeException('Unable to set request twice');
+ }
+
+ $this->request = $request;
+ $this->prepareElements();
+ $this->onRequest();
+ $this->callOnRequestCallables();
+
+ return $this;
+ }
+
+ /**
+ * @return Request
+ */
+ public function getRequest()
+ {
+ if ($this->request === null) {
+ /** @var Request $request */
+ $request = Icinga::app()->getFrontController()->getRequest();
+ $this->setRequest($request);
+ }
+ return $this->request;
+ }
+
+ public function hasBeenSent()
+ {
+ if ($this->hasBeenSent === null) {
+
+ /** @var Request $req */
+ if ($this->request === null) {
+ $req = Icinga::app()->getFrontController()->getRequest();
+ } else {
+ $req = $this->request;
+ }
+
+ if ($req->isApiRequest()) {
+ $this->hasBeenSent = true;
+ } elseif ($req->isPost()) {
+ $post = $req->getPost();
+ $this->hasBeenSent = array_key_exists(self::ID, $post) &&
+ $post[self::ID] === $this->getName();
+ } else {
+ $this->hasBeenSent = false;
+ }
+ }
+
+ return $this->hasBeenSent;
+ }
+
+ protected function detectName()
+ {
+ if ($this->formName !== null) {
+ $this->setName($this->formName);
+ } else {
+ $this->setName(get_class($this));
+ }
+ }
+}
diff --git a/library/Director/Web/Form/QuickSubForm.php b/library/Director/Web/Form/QuickSubForm.php
new file mode 100644
index 0000000..2487d35
--- /dev/null
+++ b/library/Director/Web/Form/QuickSubForm.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form;
+
+abstract class QuickSubForm extends QuickBaseForm
+{
+ /**
+ * Whether or not form elements are members of an array
+ * @codingStandardsIgnoreStart
+ * @var bool
+ */
+ protected $_isArray = true;
+ // @codingStandardsIgnoreEnd
+
+ /**
+ * Load the default decorators
+ *
+ * @return $this
+ */
+ public function loadDefaultDecorators()
+ {
+ if ($this->loadDefaultDecoratorsIsDisabled()) {
+ return $this;
+ }
+
+ $decorators = $this->getDecorators();
+ if (empty($decorators)) {
+ $this->addDecorator('FormElements')
+ ->addDecorator('HtmlTag', array('tag' => 'dl'))
+ ->addDecorator('Fieldset')
+ ->addDecorator('DtDdWrapper');
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Director/Web/Form/Validate/IsDataListEntry.php b/library/Director/Web/Form/Validate/IsDataListEntry.php
new file mode 100644
index 0000000..5762d2e
--- /dev/null
+++ b/library/Director/Web/Form/Validate/IsDataListEntry.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Validate;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDatalistEntry;
+use Zend_Validate_Abstract;
+
+class IsDataListEntry extends Zend_Validate_Abstract
+{
+ const INVALID = 'intInvalid';
+
+ /** @var Db */
+ private $db;
+
+ /** @var int */
+ private $dataListId;
+
+ public function __construct($dataListId, Db $db)
+ {
+ $this->db = $db;
+ $this->dataListId = (int) $dataListId;
+ }
+
+ public function isValid($value)
+ {
+ if (is_array($value)) {
+ foreach ($value as $name) {
+ if (! $this->isListEntry($name)) {
+ $this->_error(self::INVALID, $value);
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ if ($this->isListEntry($value)) {
+ return true;
+ } else {
+ $this->_error(self::INVALID, $value);
+
+ return false;
+ }
+ }
+
+ protected function isListEntry($name)
+ {
+ return DirectorDatalistEntry::exists([
+ 'list_id' => $this->dataListId,
+ 'entry_name' => $name,
+ ], $this->db);
+ }
+}
diff --git a/library/Director/Web/Form/Validate/NamePattern.php b/library/Director/Web/Form/Validate/NamePattern.php
new file mode 100644
index 0000000..fac44d9
--- /dev/null
+++ b/library/Director/Web/Form/Validate/NamePattern.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Form\Validate;
+
+use Icinga\Module\Director\Restriction\MatchingFilter;
+use Zend_Validate_Abstract;
+
+class NamePattern extends Zend_Validate_Abstract
+{
+ const INVALID = 'intInvalid';
+
+ private $filter;
+
+ public function __construct($pattern)
+ {
+ if (! is_array($pattern)) {
+ $pattern = [$pattern];
+ }
+
+ $this->filter = MatchingFilter::forPatterns($pattern, 'value');
+
+ $this->_messageTemplates[self::INVALID] = sprintf(
+ 'Does not match %s',
+ (string) $this->filter
+ );
+ }
+
+ public function isValid($value)
+ {
+ if ($this->filter->matches((object) ['value' => $value])) {
+ return true;
+ } else {
+ $this->_error(self::INVALID, $value);
+
+ return false;
+ }
+ }
+}
diff --git a/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php b/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php
new file mode 100644
index 0000000..1aabada
--- /dev/null
+++ b/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Navigation\Renderer;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Web;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchStore;
+use Icinga\Module\Director\Db\Migrations;
+use Icinga\Module\Director\KickstartHelper;
+use Icinga\Module\Director\Web\Controller\Extension\DirectorDb;
+use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
+use Icinga\Module\Director\Web\Window;
+
+class ConfigHealthItemRenderer extends BadgeNavigationItemRenderer
+{
+ use DirectorDb;
+
+ private $directorState = self::STATE_OK;
+
+ private $message;
+
+ private $count = 0;
+
+ private $window;
+
+ protected function hasProblems()
+ {
+ try {
+ $this->checkHealth();
+ } catch (Exception $e) {
+ $this->directorState = self::STATE_UNKNOWN;
+ $this->count = 1;
+ $this->message = $e->getMessage();
+ }
+
+ return $this->count > 0;
+ }
+
+ public function getState()
+ {
+ return $this->directorState;
+ }
+
+ public function getCount()
+ {
+ if ($this->hasProblems()) {
+ return $this->count;
+ } else {
+ return 0;
+ }
+ }
+
+ public function getTitle()
+ {
+ return $this->message;
+ }
+
+ protected function checkHealth()
+ {
+ $db = $this->db();
+ if (! $db) {
+ $this->directorState = self::STATE_PENDING;
+ $this->count = 1;
+ $this->message = $this->translate(
+ 'No database has been configured for Icinga Director'
+ );
+
+ return;
+ }
+
+ $migrations = new Migrations($db);
+ if (!$migrations->hasSchema()) {
+ $this->count = 1;
+ $this->directorState = self::STATE_CRITICAL;
+ $this->message = $this->translate(
+ 'Director database schema has not been created yet'
+ );
+ return;
+ }
+
+ if ($migrations->hasPendingMigrations()) {
+ $this->count = $migrations->countPendingMigrations();
+ $this->directorState = self::STATE_PENDING;
+ $this->message = sprintf(
+ $this->translate('There are %d pending database migrations'),
+ $this->count
+ );
+ return;
+ }
+
+ $kickstart = new KickstartHelper($db);
+ if ($kickstart->isRequired()) {
+ $this->directorState = self::STATE_PENDING;
+ $this->count = 1;
+ $this->message = $this->translate(
+ 'No API user configured, you might run the kickstart helper'
+ );
+
+ return;
+ }
+
+ $branch = Branch::detect(new BranchStore($this->db()));
+ if ($branch->isBranch()) {
+ $count = $branch->getActivityCount();
+ if ($count > 0) {
+ $this->directorState = self::STATE_PENDING;
+ $this->count = $count;
+ $this->message = sprintf(
+ $this->translate('%s config changes are available in your configuration branch'),
+ $count
+ );
+ }
+
+ return;
+ }
+
+ $pendingChanges = $db->countActivitiesSinceLastDeployedConfig();
+
+ if ($pendingChanges > 0) {
+ $this->directorState = self::STATE_WARNING;
+ $this->count = $pendingChanges;
+ $this->message = sprintf(
+ $this->translate(
+ '%s config changes happend since the last deployed configuration'
+ ),
+ $pendingChanges
+ );
+ }
+ }
+
+ protected function translate($message)
+ {
+ return mt('director', $message);
+ }
+
+ protected function db()
+ {
+ try {
+ $resourceName = Config::module('director')->get('db', 'resource');
+ if ($resourceName) {
+ // Window might have switched to another DB:
+ return Db::fromResourceName($this->getDbResourceName());
+ } else {
+ return false;
+ }
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * TODO: the following methods are for the DirectorDb trait, we need
+ * something better in future. It is required to show Health
+ * related to the DB chosen in the current Window
+ *
+ * @codingStandardsIgnoreStart
+ * @return Auth
+ */
+ protected function Auth()
+ {
+ return Auth::getInstance();
+ }
+
+ /**
+ * @return Window
+ */
+ public function Window()
+ {
+ if ($this->window === null) {
+ try {
+ /** @var $app Web */
+ $app = Icinga::app();
+ $this->window = new Window(
+ $app->getRequest()->getHeader('X-Icinga-WindowId')
+ );
+ } catch (Exception $e) {
+ $this->window = new Window(Window::UNDEFINED);
+ }
+ }
+ return $this->window;
+ }
+
+ /**
+ * @return Config
+ */
+ protected function Config()
+ {
+ // @codingStandardsIgnoreEnd
+ return Config::module('director');
+ }
+}
diff --git a/library/Director/Web/ObjectPreview.php b/library/Director/Web/ObjectPreview.php
new file mode 100644
index 0000000..e7648e1
--- /dev/null
+++ b/library/Director/Web/ObjectPreview.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace Icinga\Module\Director\Web;
+
+use gipfl\Web\Widget\Hint;
+use ipl\Html\Text;
+use Icinga\Module\Director\Exception\NestingError;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Web\Request;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\ControlsAndContent;
+
+class ObjectPreview
+{
+ use TranslationHelper;
+
+ /** @var IcingaObject */
+ protected $object;
+
+ /** @var Request */
+ protected $request;
+
+ public function __construct(IcingaObject $object, Request $request)
+ {
+ $this->object = $object;
+ $this->request = $request;
+ }
+
+ /**
+ * @param ControlsAndContent $cc
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function renderTo(ControlsAndContent $cc)
+ {
+ $object = $this->object;
+ $url = $this->request->getUrl();
+ $params = $url->getParams();
+ $cc->addTitle(
+ $this->translate('Config preview: %s'),
+ $object->getObjectName()
+ );
+
+ if ($params->shift('resolved')) {
+ $object = $object::fromPlainObject(
+ $object->toPlainObject(true),
+ $object->getConnection()
+ );
+
+ $cc->actions()->add(Link::create(
+ $this->translate('Show normal'),
+ $url->without('resolved'),
+ null,
+ ['class' => 'icon-resize-small state-warning']
+ ));
+ } else {
+ try {
+ if ($object->supportsImports() && $object->imports()->count() > 0) {
+ $cc->actions()->add(Link::create(
+ $this->translate('Show resolved'),
+ $url->with('resolved', true),
+ null,
+ ['class' => 'icon-resize-full']
+ ));
+ }
+ } catch (NestingError $e) {
+ // No resolve link with nesting errors
+ }
+ }
+
+ $content = $cc->content();
+ if ($object->isDisabled()) {
+ $content->add(Hint::error(
+ $this->translate('This object will not be deployed as it has been disabled')
+ ));
+ }
+ if ($object->isExternal()) {
+ $content->add(Html::tag('p', null, $this->translate((
+ 'This is an external object. It has been imported from Icinga 2 through the'
+ . ' Core API and cannot be managed with the Icinga Director. It is however'
+ . ' perfectly valid to create objects using this or referring to this object.'
+ . ' You might also want to define related Fields to make work based on this'
+ . ' object more enjoyable.'
+ ))));
+ }
+ $config = $object->toSingleIcingaConfig();
+
+ foreach ($config->getFiles() as $filename => $file) {
+ if (! $object->isExternal()) {
+ $content->add(Html::tag('h2', null, $filename));
+ }
+
+ $classes = array();
+ if ($object->isDisabled()) {
+ $classes[] = 'disabled';
+ } elseif ($object->isExternal()) {
+ $classes[] = 'logfile';
+ }
+
+ $type = $object->getShortTableName();
+
+ $plain = Html::wantHtml($file->getContent())->render();
+ $plain = preg_replace_callback(
+ '/^(\s+import\s+\&quot\;)(.+)(\&quot\;)/m',
+ [$this, 'linkImport'],
+ $plain
+ );
+
+ if ($type !== 'command') {
+ $plain = preg_replace_callback(
+ '/^(\s+(?:check_|event_)?command\s+=\s+\&quot\;)(.+)(\&quot\;)/m',
+ [$this, 'linkCommand'],
+ $plain
+ );
+ }
+
+ $plain = preg_replace_callback(
+ '/^(\s+host_name\s+=\s+\&quot\;)(.+)(\&quot\;)/m',
+ [$this, 'linkHost'],
+ $plain
+ );
+ $text = Text::create($plain)->setEscaped();
+
+ $content->add(Html::tag('pre', ['class' => $classes], $text));
+ }
+ }
+
+ /**
+ * @api internal
+ * @param $match
+ * @return string
+ */
+ public function linkImport($match)
+ {
+ $blacklist = [
+ 'plugin-notification-command',
+ 'plugin-check-command',
+ ];
+ if (in_array($match[2], $blacklist)) {
+ return $match[1] . $match[2] . $match[3];
+ }
+
+ $urlObjectType = $this->object->getShortTableName();
+ if ($urlObjectType === 'service_set') {
+ $urlObjectType = 'service';
+ }
+ return $match[1] . Link::create(
+ $match[2],
+ sprintf("director/$urlObjectType"),
+ ['name' => $match[2]]
+ )->render() . $match[3];
+ }
+
+ /**
+ * @api internal
+ * @param $match
+ * @return string
+ */
+ public function linkCommand($match)
+ {
+ return $match[1] . Link::create(
+ $match[2],
+ sprintf('director/command'),
+ ['name' => $match[2]]
+ )->render() . $match[3];
+ }
+
+ /**
+ * @api internal
+ * @param $match
+ * @return string
+ */
+ public function linkHost($match)
+ {
+ return $match[1] . Link::create(
+ $match[2],
+ sprintf('director/host'),
+ ['name' => $match[2]]
+ )->render() . $match[3];
+ }
+}
diff --git a/library/Director/Web/SelfService.php b/library/Director/Web/SelfService.php
new file mode 100644
index 0000000..33756b7
--- /dev/null
+++ b/library/Director/Web/SelfService.php
@@ -0,0 +1,311 @@
+<?php
+
+namespace Icinga\Module\Director\Web;
+
+use Exception;
+use gipfl\Web\Widget\Hint;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Core\CoreApi;
+use Icinga\Module\Director\Forms\IcingaForgetApiKeyForm;
+use Icinga\Module\Director\Forms\IcingaGenerateApiKeyForm;
+use Icinga\Application\Icinga;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\IcingaConfig\AgentWizard;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Util;
+use Icinga\Module\Director\Web\Widget\Documentation;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\ActionBar;
+use gipfl\IcingaWeb2\Widget\ControlsAndContent;
+
+class SelfService
+{
+ use TranslationHelper;
+
+ /** @var IcingaHost */
+ protected $host;
+
+ /** @var CoreApi */
+ protected $api;
+
+ public function __construct(IcingaHost $host, CoreApi $api)
+ {
+ $this->host = $host;
+ $this->api = $api;
+ }
+
+ /**
+ * @param ControlsAndContent $controller
+ */
+ public function renderTo(ControlsAndContent $controller)
+ {
+ $host = $this->host;
+ if ($host->isTemplate()) {
+ $this->showSelfServiceTemplateInstructions($controller);
+ } elseif ($key = $host->getProperty('api_key')) {
+ $this->showRegisteredAgentInstructions($key, $controller);
+ } elseif ($key = $host->getSingleResolvedProperty('api_key')) {
+ $this->showNewAgentInstructions($controller);
+ } else {
+ $this->showLegacyAgentInstructions($controller);
+ }
+ }
+
+ /**
+ * @param string $key
+ * @param ControlsAndContent $c
+ */
+ protected function showRegisteredAgentInstructions($key, ControlsAndContent $c)
+ {
+ $c->addTitle($this->translate('Registered Agent'));
+ $c->content()->add([
+ Html::tag('p', null, $this->translate(
+ 'This host has been registered via the Icinga Director Self Service'
+ . " API. In case you re-installed the host or somehow lost it's"
+ . ' secret key, you might want to dismiss the current key. This'
+ . ' would allow you to register the same host again.'
+ )),
+ Html::tag('p', null, [$this->translate('Api Key:'), ' ', Html::tag('strong', null, $key)]),
+ Hint::warning($this->translate(
+ 'It is not a good idea to do so as long as your Agent still has'
+ . ' a valid Self Service API key!'
+ )),
+ IcingaForgetApiKeyForm::load()->setHost($this->host)->handleRequest()
+ ]);
+ }
+
+ /**
+ * @param ControlsAndContent $cc
+ */
+ protected function showSelfServiceTemplateInstructions(ControlsAndContent $cc)
+ {
+ $host = $this->host;
+ $key = $host->getProperty('api_key');
+ $hasKey = $key !== null;
+ if ($hasKey) {
+ $cc->addTitle($this->translate('Shared for Self Service API'));
+ } else {
+ $cc->addTitle($this->translate('Share this Template for Self Service API'));
+ }
+
+ $c = $cc->content();
+ /** @var ActionBar $actions */
+ $actions = $cc->actions();
+ $actions->setBaseTarget('_next')->add(Link::create(
+ $this->translate('Settings'),
+ 'director/settings/self-service',
+ null,
+ [
+ 'title' => $this->translate('Global Self Service Setting'),
+ 'class' => 'icon-services',
+ ]
+ ));
+
+ $actions->add($this->getDocumentationLink());
+
+ if ($hasKey) {
+ $c->add([
+ Html::tag('p', [
+ $this->translate('Api Key:'), ' ', Html::tag('strong', null, $key)
+ ]),
+ $this->getWindowsInstructions($host, $key),
+ Html::tag('h2', null, $this->translate('Generate a new key')),
+ Hint::warning($this->translate(
+ 'This will invalidate the former key'
+ )),
+ ]);
+ }
+
+ $c->add([
+ // Html::tag('p', null, $this->translate('..')),
+ IcingaGenerateApiKeyForm::load()->setHost($host)->handleRequest()
+ ]);
+ if ($hasKey) {
+ $c->add([
+ Html::tag('h2', null, $this->translate('Stop sharing this Template')),
+ Html::tag('p', null, $this->translate(
+ 'You can stop sharing a Template at any time. This will'
+ . ' immediately invalidate the former key.'
+ ) . ' ' . $this->translate(
+ 'Generated Host keys will continue to work, but you\'ll no'
+ . ' longer be able to register new Hosts with this key'
+ )),
+ IcingaForgetApiKeyForm::load()->setHost($host)->handleRequest()
+ ]);
+ }
+ }
+
+ protected function getWindowsInstructions($host, $key)
+ {
+ $wizard = new AgentWizard($host);
+
+ return [
+ Html::tag('h2', $this->translate('Icinga for Windows')),
+ Html::tag('p', Html::sprintf(
+ $this->translate('In case you\'re using %s, please run this Script:'),
+ Html::tag('a', [
+ 'href' => 'https://icinga.com/docs/windows/latest/',
+ 'target' => '_blank',
+ ], $this->translate('Icinga for Windows'))
+ )),
+ Html::tag(
+ 'pre',
+ ['class' => 'logfile'],
+ $wizard->renderIcinga4WindowsWizardCommand($key)
+ ),
+ Html::tag('h3', $this->translate('Icinga 2 Powershell Module')),
+ Html::tag('p', Html::sprintf(
+ $this->translate('In case you\'re using the legacy %s, please run:'),
+ Html::tag('a', [
+ 'href' => 'https://github.com/Icinga/icinga2-powershell-module',
+ 'target' => '_blank',
+ ], $this->translate('Icinga 2 Powershell Module'))
+ )),
+ Html::tag(
+ 'pre',
+ ['class' => 'logfile'],
+ $wizard->renderPowershellModuleInstaller($key)
+ ),
+ ];
+ }
+
+ protected function getDocumentationLink()
+ {
+ return Documentation::link(
+ $this->translate('Documentation'),
+ 'director',
+ '74-Self-Service-API',
+ $this->translate('Self Service API')
+ );
+ }
+
+ /**
+ * @param ControlsAndContent $cc
+ */
+ protected function showNewAgentInstructions(ControlsAndContent $cc)
+ {
+ $content = $cc->content();
+ $host = $this->host;
+ $key = $host->getSingleResolvedProperty('api_key');
+ $cc->addTitle($this->translate('Configure this Agent via Self Service API'));
+ $cc->actions()->add($this->getDocumentationLink());
+ $content->add(Html::tag('p', [
+ $this->translate('Inherited Template Api Key:'), ' ', Html::tag('strong', null, $key)
+ ]));
+ $content->add($this->getWindowsInstructions($host, $key));
+ }
+
+ /**
+ * @param ControlsAndContent $cc
+ */
+ protected function showLegacyAgentInstructions(ControlsAndContent $cc)
+ {
+ $host = $this->host;
+ $c = $cc->content();
+ $docBaseUrl = 'https://docs.icinga.com/icinga2/latest/doc/module/icinga2/chapter/distributed-monitoring';
+ $sectionSetup = 'distributed-monitoring-setup-satellite-client';
+ $sectionTopDown = 'distributed-monitoring-top-down';
+ $c->add(Html::tag('p')->add(Html::sprintf(
+ 'Please check the %s for more related information.'
+ . ' The Director-assisted setup corresponds to configuring a %s environment.',
+ Html::tag(
+ 'a',
+ ['href' => $docBaseUrl . '#' . $sectionSetup],
+ $this->translate('Icinga 2 Client documentation')
+ ),
+ Html::tag(
+ 'a',
+ ['href' => $docBaseUrl . '#' . $sectionTopDown],
+ $this->translate('Top Down')
+ )
+ )));
+
+ $cc->addTitle('Agent deployment instructions');
+
+ try {
+ $ticket = $this->api->getTicket($host->getEndpointName());
+ $wizard = new AgentWizard($host);
+ $wizard->setTicket($ticket);
+ } catch (Exception $e) {
+ $c->add(Hint::error(sprintf(
+ $this->translate(
+ 'A ticket for this agent could not have been requested from'
+ . ' your deployment endpoint: %s'
+ ),
+ $e->getMessage()
+ )));
+
+ return;
+ }
+
+ $class = ['class' => 'agent-deployment-instructions'];
+ $c->add([
+ Html::tag('h2', null, $this->translate('For manual configuration')),
+ Html::tag('p', null, [$this->translate('Ticket'), ': ', Html::tag('code', null, $ticket)]),
+ Html::tag('h2', null, $this->translate('Windows Kickstart Script')),
+ Link::create(
+ $this->translate('Download'),
+ $cc->url()->with('download', 'windows-kickstart'),
+ null,
+ ['class' => 'icon-download', 'target' => '_blank']
+ ),
+ Html::tag('pre', $class, $wizard->renderWindowsInstaller()),
+ Html::tag('p', null, $this->translate(
+ 'This requires the Icinga Agent to be installed. It generates and signs'
+ . ' it\'s certificate and it also generates a minimal icinga2.conf to get'
+ . ' your agent connected to it\'s parents'
+ )),
+ Html::tag('h2', null, $this->translate('Linux commandline')),
+ Link::create(
+ $this->translate('Download'),
+ $cc->url()->with('download', 'linux'),
+ null,
+ ['class' => 'icon-download', 'target' => '_blank']
+ ),
+ Html::tag('p', null, $this->translate('Just download and run this script on your Linux Client Machine:')),
+ Html::tag('pre', $class, $wizard->renderLinuxInstaller())
+ ]);
+ }
+
+ /**
+ * @param $os
+ * @throws NotFoundError
+ */
+ public function handleLegacyAgentDownloads($os)
+ {
+ $wizard = new AgentWizard($this->host);
+ $wizard->setTicket($this->api->getTicket($this->host->getEndpointName()));
+
+ switch ($os) {
+ case 'windows-kickstart':
+ $ext = 'ps1';
+ $script = preg_replace('/\n/', "\r\n", $wizard->renderWindowsInstaller());
+ break;
+ case 'linux':
+ $ext = 'bash';
+ $script = $wizard->renderLinuxInstaller();
+ break;
+ default:
+ throw new NotFoundError('There is no kickstart helper for %s', $os);
+ }
+
+ header('Content-type: application/octet-stream');
+ header('Content-Disposition: attachment; filename=icinga2-agent-kickstart.' . $ext);
+ echo $script;
+ exit;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function hasDocsModuleLoaded()
+ {
+ try {
+ return Icinga::app()->getModuleManager()->hasLoaded('doc');
+ } catch (ProgrammingError $e) {
+ return false;
+ }
+ }
+}
diff --git a/library/Director/Web/Table/ActivityLogTable.php b/library/Director/Web/Table/ActivityLogTable.php
new file mode 100644
index 0000000..5460bc2
--- /dev/null
+++ b/library/Director/Web/Table/ActivityLogTable.php
@@ -0,0 +1,294 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\Format\LocalTimeFormat;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Icinga\Module\Director\Util;
+use ipl\Html\Html;
+use ipl\Html\HtmlElement;
+
+class ActivityLogTable extends ZfQueryBasedTable
+{
+ protected $filters = [];
+
+ protected $lastDeployedId;
+
+ protected $extraParams = [];
+
+ protected $columnCount;
+
+ protected $hasObjectFilter = false;
+
+ protected $searchColumns = [
+ 'author',
+ 'object_name',
+ 'object_type',
+ ];
+
+ /** @var LocalTimeFormat */
+ protected $timeFormat;
+
+ protected $ranges = [];
+
+ /** @var ?object */
+ protected $currentRange = null;
+ /** @var ?HtmlElement */
+ protected $currentRangeCell = null;
+ /** @var int */
+ protected $rangeRows = 0;
+ protected $continueRange = false;
+ protected $currentRow;
+
+ public function __construct($db)
+ {
+ parent::__construct($db);
+ $this->timeFormat = new LocalTimeFormat();
+ }
+
+ public function assemble()
+ {
+ $this->getAttributes()->add('class', 'activity-log');
+ }
+
+ public function setLastDeployedId($id)
+ {
+ $this->lastDeployedId = $id;
+ return $this;
+ }
+
+ protected function fetchQueryRows()
+ {
+ $rows = parent::fetchQueryRows();
+ // Hint -> DESC, that's why they are inverted
+ if (empty($rows)) {
+ return $rows;
+ }
+ $last = $rows[0]->id;
+ $first = $rows[count($rows) - 1]->id;
+ $db = $this->db();
+ $this->ranges = $db->fetchAll(
+ $db->select()
+ ->from('director_activity_log_remark')
+ ->where('first_related_activity <= ?', $last)
+ ->where('last_related_activity >= ?', $first)
+ );
+
+ return $rows;
+ }
+
+
+ public function renderRow($row)
+ {
+ $this->currentRow = $row;
+ $this->splitByDay($row->ts_change_time);
+ $action = 'action-' . $row->action. ' ';
+ if ($row->id > $this->lastDeployedId) {
+ $action .= 'undeployed';
+ } else {
+ $action .= 'deployed';
+ }
+
+ $columns = [
+ $this::td($this->makeLink($row))->setSeparator(' '),
+ ];
+ if (! $this->hasObjectFilter) {
+ $columns[] = $this->makeRangeInfo($row->id);
+ }
+ $columns[] = $this::td($this->timeFormat->getTime($row->ts_change_time));
+
+ return $this::tr($columns)->addAttributes(['class' => $action]);
+ }
+
+ /**
+ * Hint: cloned from parent class and modified
+ * @param int $timestamp
+ */
+ protected function renderDayIfNew($timestamp)
+ {
+ $day = $this->getDateFormatter()->getFullDay($timestamp);
+
+ if ($this->lastDay !== $day) {
+ $this->nextHeader()->add(
+ $this::th($day, [
+ 'colspan' => $this->hasObjectFilter ? 2 : 3,
+ 'class' => 'table-header-day'
+ ])
+ );
+
+ $this->lastDay = $day;
+ if ($this->currentRangeCell) {
+ if ($this->currentRange->first_related_activity <= $this->currentRow->id) {
+ $this->currentRangeCell->addAttributes(['class' => 'continuing']);
+ $this->continueRange = true;
+ } else {
+ $this->continueRange = false;
+ }
+ }
+ $this->currentRangeCell = null;
+ $this->currentRange = null;
+ $this->rangeRows = 0;
+ $this->nextBody();
+ }
+ }
+
+ protected function makeRangeInfo($id)
+ {
+ $range = $this->getRangeForId($id);
+ if ($range === null) {
+ if ($this->currentRangeCell) {
+ $this->currentRangeCell->getAttributes()->remove('class', 'continuing');
+ }
+ $this->currentRange = null;
+ $this->currentRangeCell = null;
+ $this->rangeRows = 0;
+ return $this::td();
+ }
+
+ if ($range === $this->currentRange) {
+ $this->growCurrentRange();
+ return null;
+ }
+ $this->startRange($range);
+
+ return $this->currentRangeCell;
+ }
+
+ protected function startRange($range)
+ {
+ $this->currentRangeCell = $this::td($this->renderRangeComment($range), [
+ 'colspan' => $this->rangeRows = 1,
+ 'class' => 'comment-cell'
+ ]);
+ if ($this->continueRange) {
+ $this->currentRangeCell->addAttributes(['class' => 'continued']);
+ $this->continueRange = false;
+ }
+ $this->currentRange = $range;
+ }
+
+ protected function renderRangeComment($range)
+ {
+ // The only purpose of this container is to avoid hovered rows from influencing
+ // the comments background color, as we're using the alpha channel to lighten it
+ // This can be replaced once we get theme-safe colors for such messages
+ return Html::tag('div', [
+ 'class' => 'range-comment-container',
+ ], Link::create($this->continueRange ? '' : $range->remark, '#', null, [
+ 'title' => $range->remark,
+ 'class' => 'range-comment'
+ ]));
+ }
+
+ protected function growCurrentRange()
+ {
+ $this->rangeRows++;
+ $this->currentRangeCell->setAttribute('rowspan', $this->rangeRows);
+ }
+
+ protected function getRangeForId($id)
+ {
+ foreach ($this->ranges as $range) {
+ if ($id >= $range->first_related_activity && $id <= $range->last_related_activity) {
+ return $range;
+ }
+ }
+
+ return null;
+ }
+
+ protected function makeLink($row)
+ {
+ $type = $row->object_type;
+ $name = $row->object_name;
+ if (substr($type, 0, 7) === 'icinga_') {
+ $type = substr($type, 7);
+ }
+
+ if (Util::hasPermission('director/showconfig')) {
+ // Later on replacing, service_set -> serviceset
+
+ // multi column key :(
+ if ($type === 'service' || $this->hasObjectFilter) {
+ $object = "\"$name\"";
+ } else {
+ $object = Link::create(
+ "\"$name\"",
+ 'director/' . str_replace('_', '', $type),
+ ['name' => $name],
+ ['title' => $this->translate('Jump to this object')]
+ );
+ }
+
+ return [
+ '[' . $row->author . ']',
+ Link::create(
+ $row->action,
+ 'director/config/activity',
+ array_merge(['id' => $row->id], $this->extraParams),
+ ['title' => $this->translate('Show details related to this change')]
+ ),
+ str_replace('_', ' ', $type),
+ $object
+ ];
+ } else {
+ return sprintf(
+ '[%s] %s %s "%s"',
+ $row->author,
+ $row->action,
+ $type,
+ $name
+ );
+ }
+ }
+
+ public function filterObject($type, $name)
+ {
+ $this->hasObjectFilter = true;
+ $this->filters[] = ['l.object_type = ?', $type];
+ $this->filters[] = ['l.object_name = ?', $name];
+
+ return $this;
+ }
+
+ public function filterHost($name)
+ {
+ $db = $this->db();
+ $filter = '%"host":' . json_encode($name) . '%';
+ $this->filters[] = ['('
+ . $db->quoteInto('l.old_properties LIKE ?', $filter)
+ . ' OR '
+ . $db->quoteInto('l.new_properties LIKE ?', $filter)
+ . ')', null];
+
+ return $this;
+ }
+
+ public function getColumns()
+ {
+ return [
+ 'author' => 'l.author',
+ 'action' => 'l.action_name',
+ 'object_name' => 'l.object_name',
+ 'object_type' => 'l.object_type',
+ 'id' => 'l.id',
+ 'change_time' => 'l.change_time',
+ 'ts_change_time' => 'UNIX_TIMESTAMP(l.change_time)',
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $query = $this->db()->select()->from(
+ ['l' => 'director_activity_log'],
+ $this->getColumns()
+ )->order('change_time DESC')->order('id DESC')->limit(100);
+
+ foreach ($this->filters as $filter) {
+ $query->where($filter[0], $filter[1]);
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/ApplyRulesTable.php b/library/Director/Web/Table/ApplyRulesTable.php
new file mode 100644
index 0000000..a861bac
--- /dev/null
+++ b/library/Director/Web/Table/ApplyRulesTable.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\Db\IcingaObjectFilterHelper;
+use Icinga\Module\Director\IcingaConfig\AssignRenderer;
+use Icinga\Module\Director\Objects\IcingaObject;
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer;
+use Ramsey\Uuid\Uuid;
+use Zend_Db_Select as ZfSelect;
+
+class ApplyRulesTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'o.object_name',
+ 'o.assign_filter',
+ ];
+
+ private $type;
+
+ /** @var IcingaObject */
+ protected $dummyObject;
+
+ protected $baseObjectUrl;
+
+ protected $linkWithName = false;
+
+ public static function create($type, Db $db)
+ {
+ $table = new static($db);
+ $table->setType($type);
+ return $table;
+ }
+
+ public function setType($type)
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function setBaseObjectUrl($url)
+ {
+ $this->baseObjectUrl = $url;
+
+ return $this;
+ }
+
+ public function createLinksWithNames($linksWithName = true)
+ {
+ $this->linkWithName = (bool) $linksWithName;
+
+ return $this;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return ['Name', 'assign where'/*, 'Actions'*/];
+ }
+
+ public function renderRow($row)
+ {
+ $row->uuid = DbUtil::binaryResult($row->uuid);
+ if ($this->linkWithName) {
+ $params = ['name' => $row->object_name];
+ } else {
+ $params = ['uuid' => Uuid::fromBytes($row->uuid)->toString()];
+ }
+ $url = Url::fromPath("director/{$this->baseObjectUrl}/edit", $params);
+
+ $assignWhere = $this->renderApplyFilter($row->assign_filter);
+
+ if (! empty($row->apply_for)) {
+ $assignWhere = sprintf('apply for %s / %s', $row->apply_for, $assignWhere);
+ }
+
+ $tr = static::tr([
+ static::td(Link::create($row->object_name, $url)),
+ static::td($assignWhere),
+ // NOT (YET) static::td($this->createActionLinks($row))->setSeparator(' ')
+ ]);
+
+ if ($row->disabled === 'y') {
+ $tr->getAttributes()->add('class', 'disabled');
+ }
+
+ return $tr;
+ }
+
+ /**
+ * Should be triggered from renderRow, still unused.
+ *
+ * @param IcingaObject $template
+ * @param string $inheritance
+ * @return $this
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ public function filterTemplate(
+ IcingaObject $template,
+ $inheritance = IcingaObjectFilterHelper::INHERIT_DIRECT
+ ) {
+ IcingaObjectFilterHelper::filterByTemplate(
+ $this->getQuery(),
+ $template,
+ 'o',
+ $inheritance
+ );
+
+ return $this;
+ }
+
+ protected function renderApplyFilter($assignFilter)
+ {
+ try {
+ $string = AssignRenderer::forFilter(
+ Filter::fromQueryString($assignFilter)
+ )->renderAssign();
+ // Do not prefix it
+ $string = preg_replace('/^assign where /', '', $string);
+ } catch (IcingaException $e) {
+ // ignore errors in filter rendering
+ $string = 'Error in Filter rendering: ' . $e->getMessage();
+ }
+
+ return $string;
+ }
+
+ public function createActionLinks($row)
+ {
+ $params = ['uuid' => Uuid::fromBytes($row->uuid)->toString()];
+ $baseUrl = 'director/' . $this->baseObjectUrl;
+ $links = [];
+ $links[] = Link::create(
+ Icon::create('sitemap'),
+ "${baseUrl}template/applytargets",
+ ['id' => $row->id],
+ ['title' => $this->translate('Show affected Objects')]
+ );
+
+ $links[] = Link::create(
+ Icon::create('edit'),
+ "$baseUrl/edit",
+ $params,
+ ['title' => $this->translate('Modify this Apply Rule')]
+ );
+
+ $links[] = Link::create(
+ Icon::create('doc-text'),
+ "$baseUrl/render",
+ $params,
+ ['title' => $this->translate('Apply Rule rendering preview')]
+ );
+
+ $links[] = Link::create(
+ Icon::create('history'),
+ "$baseUrl/history",
+ $params,
+ ['title' => $this->translate('Apply rule history')]
+ );
+
+ return $links;
+ }
+
+ protected function applyRestrictions(ZfSelect $query)
+ {
+ $auth = Auth::getInstance();
+ $type = $this->type;
+ // TODO: Centralize this logic
+ if ($type === 'scheduledDowntime') {
+ $type = 'scheduled-downtime';
+ }
+ $restrictions = $auth->getRestrictions("director/$type/apply/filter-by-name");
+ if (empty($restrictions)) {
+ return $query;
+ }
+
+ $filter = Filter::matchAny();
+ foreach ($restrictions as $restriction) {
+ $filter->addFilter(Filter::where('o.object_name', $restriction));
+ }
+
+ return FilterRenderer::applyToQuery($filter, $query);
+ }
+
+
+ /**
+ * @return IcingaObject
+ */
+ protected function getDummyObject()
+ {
+ if ($this->dummyObject === null) {
+ $type = $this->type;
+ $this->dummyObject = IcingaObject::createByType($type);
+ }
+ return $this->dummyObject;
+ }
+
+ public function prepareQuery()
+ {
+ $table = $this->getDummyObject()->getTableName();
+ $columns = [
+ 'id' => 'o.id',
+ 'uuid' => 'o.uuid',
+ 'object_name' => 'o.object_name',
+ 'disabled' => 'o.disabled',
+ 'assign_filter' => 'o.assign_filter',
+ 'apply_for' => '(NULL)',
+ ];
+
+ if ($table === 'icinga_service') {
+ $columns['apply_for'] = 'o.apply_for';
+ }
+ $query = $this->db()->select()->from(
+ ['o' => $table],
+ $columns
+ )->where(
+ "object_type = 'apply'"
+ )->order('o.object_name');
+
+ if ($this->type === 'service') {
+ $query->where('service_set_id IS NULL');
+ }
+
+ return $this->applyRestrictions($query);
+ }
+}
diff --git a/library/Director/Web/Table/BasketSnapshotTable.php b/library/Director/Web/Table/BasketSnapshotTable.php
new file mode 100644
index 0000000..08f808a
--- /dev/null
+++ b/library/Director/Web/Table/BasketSnapshotTable.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use RuntimeException;
+
+class BasketSnapshotTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ protected $searchColumns = [
+ 'basket_name',
+ 'summary'
+ ];
+
+ /** @var Basket */
+ protected $basket;
+
+ public function setBasket(Basket $basket)
+ {
+ $this->basket = $basket;
+ $this->searchColumns = [];
+
+ return $this;
+ }
+
+ public function renderRow($row)
+ {
+ $this->splitByDay($row->ts_create_seconds);
+ $link = $this->linkToSnapshot($this->renderSummary($row->summary), $row);
+
+ if ($this->basket === null) {
+ $columns = [
+ [
+ new Link(
+ Html::tag('strong', $row->basket_name),
+ 'director/basket',
+ ['name' => $row->basket_name]
+ ),
+ Html::tag('br'),
+ $link,
+ ],
+ DateFormatter::formatTime($row->ts_create / 1000),
+ ];
+ } else {
+ $columns = [
+ $link,
+ DateFormatter::formatTime($row->ts_create / 1000),
+ ];
+ }
+ return $this::row($columns);
+ }
+
+ protected function renderSummary($summary)
+ {
+ $summary = Json::decode($summary);
+ if ($summary === null) {
+ return '-';
+ }
+ $result = [];
+ if (! is_object($summary) && ! is_array($summary)) {
+ throw new RuntimeException(sprintf(
+ 'Got invalid basket summary: %s ',
+ var_export($summary, 1)
+ ));
+ }
+
+ foreach ($summary as $type => $count) {
+ $result[] = sprintf(
+ '%dx %s',
+ $count,
+ $type
+ );
+ }
+
+ if (empty($result)) {
+ return '-';
+ }
+
+ return implode(', ', $result);
+ }
+
+ protected function linkToSnapshot($caption, $row)
+ {
+ return new Link($caption, 'director/basket/snapshot', [
+ 'checksum' => bin2hex($this->wantBinaryValue($row->content_checksum)),
+ 'ts' => $row->ts_create,
+ 'name' => $row->basket_name,
+ ]);
+ }
+
+ public function prepareQuery()
+ {
+ $query = $this->db()->select()->from([
+ 'b' => 'director_basket'
+ ], [
+ 'b.uuid',
+ 'b.basket_name',
+ 'bs.ts_create',
+ 'ts_create_seconds' => '(bs.ts_create / 1000)',
+ 'bs.content_checksum',
+ 'bc.summary',
+ ])->join(
+ ['bs' => 'director_basket_snapshot'],
+ 'bs.basket_uuid = b.uuid',
+ []
+ )->join(
+ ['bc' => 'director_basket_content'],
+ 'bc.checksum = bs.content_checksum',
+ []
+ )->order('bs.ts_create DESC');
+
+ if ($this->basket !== null) {
+ $query->where('b.uuid = ?', $this->quoteBinary($this->basket->get('uuid')));
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/BasketTable.php b/library/Director/Web/Table/BasketTable.php
new file mode 100644
index 0000000..25e37e0
--- /dev/null
+++ b/library/Director/Web/Table/BasketTable.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class BasketTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'basket_name',
+ ];
+
+ public function renderRow($row)
+ {
+ $tr = $this::row([
+ new Link(
+ $row->basket_name,
+ 'director/basket',
+ ['name' => $row->basket_name]
+ ),
+ $row->cnt_snapshots
+ ]);
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Basket'),
+ $this->translate('Snapshots'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from([
+ 'b' => 'director_basket'
+ ], [
+ 'b.uuid',
+ 'b.basket_name',
+ 'cnt_snapshots' => 'COUNT(bs.basket_uuid)',
+ ])->joinLeft(
+ ['bs' => 'director_basket_snapshot'],
+ 'bs.basket_uuid = b.uuid',
+ []
+ )->group('b.uuid')->order('b.basket_name');
+ }
+}
diff --git a/library/Director/Web/Table/BranchActivityTable.php b/library/Director/Web/Table/BranchActivityTable.php
new file mode 100644
index 0000000..e7131ef
--- /dev/null
+++ b/library/Director/Web/Table/BranchActivityTable.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\Format\LocalTimeFormat;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\BranchActivity;
+use Icinga\Module\Director\Util;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Ramsey\Uuid\UuidInterface;
+
+class BranchActivityTable extends ZfQueryBasedTable
+{
+ protected $extraParams = [];
+
+ /** @var UuidInterface */
+ protected $branchUuid;
+
+ /** @var ?UuidInterface */
+ protected $objectUuid;
+
+ /** @var LocalTimeFormat */
+ protected $timeFormat;
+
+ protected $linkToObject = true;
+
+ public function __construct(UuidInterface $branchUuid, $db, UuidInterface $objectUuid = null)
+ {
+ $this->branchUuid = $branchUuid;
+ $this->objectUuid = $objectUuid;
+ $this->timeFormat = new LocalTimeFormat();
+ parent::__construct($db);
+ }
+
+ public function assemble()
+ {
+ $this->getAttributes()->add('class', 'activity-log');
+ }
+
+ public function renderRow($row)
+ {
+ $ts = (int) floor(BranchActivity::fixFakeTimestamp($row->timestamp_ns) / 1000000);
+ $this->splitByDay($ts);
+ $activity = BranchActivity::fromDbRow($row);
+ return $this::tr([
+ $this::td($this->makeBranchLink($activity))->setSeparator(' '),
+ $this::td($this->timeFormat->getTime($ts))
+ ])->addAttributes(['class' => ['action-' . $activity->getAction(), 'branched']]);
+ }
+
+ public function disableObjectLink()
+ {
+ $this->linkToObject = false;
+ return $this;
+ }
+
+ protected function linkObject(BranchActivity $activity)
+ {
+ if (! $this->linkToObject) {
+ return $activity->getObjectName();
+ }
+ // $type, UuidInterface $uuid
+ // Later on replacing, service_set -> serviceset
+ $type = preg_replace('/^icinga_/', '', $activity->getObjectTable());
+ return Link::create(
+ $activity->getObjectName(),
+ 'director/' . str_replace('_', '', $type),
+ ['uuid' => $activity->getObjectUuid()->toString()],
+ ['title' => $this->translate('Jump to this object')]
+ );
+ }
+
+ protected function makeBranchLink(BranchActivity $activity)
+ {
+ $type = preg_replace('/^icinga_/', '', $activity->getObjectTable());
+
+ if (Util::hasPermission('director/showconfig')) {
+ // Later on replacing, service_set -> serviceset
+ return [
+ '[' . $activity->getAuthor() . ']',
+ Link::create(
+ $activity->getAction(),
+ 'director/branch/activity',
+ array_merge(['ts' => $activity->getTimestampNs()], $this->extraParams),
+ ['title' => $this->translate('Show details related to this change')]
+ ),
+ str_replace('_', ' ', $type),
+ $this->linkObject($activity)
+ ];
+ } else {
+ return sprintf(
+ '[%s] %s %s "%s"',
+ $activity->getAuthor(),
+ $activity->getAction(),
+ $type,
+ $activity->getObjectName()
+ );
+ }
+ }
+
+ public function prepareQuery()
+ {
+ /** @var Db $connection */
+ $connection = $this->connection();
+ $query = $this->db()->select()->from(['ba' => 'director_branch_activity'], 'ba.*')
+ ->join(['b' => 'director_branch'], 'b.uuid = ba.branch_uuid', ['b.owner'])
+ ->where('branch_uuid = ?', $connection->quoteBinary($this->branchUuid->getBytes()))
+ ->order('timestamp_ns DESC');
+ if ($this->objectUuid) {
+ $query->where('ba.object_uuid = ?', $connection->quoteBinary($this->objectUuid->getBytes()));
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php b/library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php
new file mode 100644
index 0000000..3d5dbcb
--- /dev/null
+++ b/library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Data\SimpleQueryPaginationAdapter;
+use gipfl\IcingaWeb2\Table\QueryBasedTable;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use gipfl\IcingaWeb2\Link;
+
+class BranchedIcingaCommandArgumentTable extends QueryBasedTable
+{
+ /** @var IcingaCommand */
+ protected $command;
+
+ /** @var Branch */
+ protected $branch;
+
+ protected $searchColumns = [
+ 'ca.argument_name',
+ 'ca.argument_value',
+ ];
+
+ public function __construct(IcingaCommand $command, Branch $branch)
+ {
+ $this->command = $command;
+ $this->branch = $branch;
+ $this->getAttributes()->set('data-base-target', '_self');
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create($row->argument_name, 'director/command/arguments', [
+ 'argument' => $row->argument_name,
+ 'uuid' => $this->command->getUniqueId()->toString(),
+ ]),
+ $row->argument_value
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Argument'),
+ $this->translate('Value'),
+ ];
+ }
+
+ protected function getPaginationAdapter()
+ {
+ return new SimpleQueryPaginationAdapter($this->getQuery());
+ }
+
+ public function getQuery()
+ {
+ return $this->prepareQuery();
+ }
+
+ protected function fetchQueryRows()
+ {
+ return $this->getQuery()->fetchAll();
+ }
+
+ protected function prepareQuery()
+ {
+ $list = [];
+ foreach ($this->command->arguments()->toPlainObject() as $name => $argument) {
+ $new = (object) [];
+ $new->argument_name = $name;
+ $new->argument_value = isset($argument->value) ? $argument->value : null;
+ $list[] = $new;
+ }
+
+ return (new ArrayDatasource($list))->select();
+ }
+}
diff --git a/library/Director/Web/Table/ChoicesTable.php b/library/Director/Web/Table/ChoicesTable.php
new file mode 100644
index 0000000..4ba2460
--- /dev/null
+++ b/library/Director/Web/Table/ChoicesTable.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+
+class ChoicesTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = ['o.object_name'];
+
+ protected $type;
+
+ /**
+ * @param $type
+ * @param Db $db
+ * @return static
+ */
+ public static function create($type, Db $db)
+ {
+ $class = __NAMESPACE__ . '\\ChoicesTable' . ucfirst($type);
+ if (! class_exists($class)) {
+ $class = __CLASS__;
+ }
+
+ /** @var static $table */
+ $table = new $class($db);
+ $table->type = $type;
+ return $table;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('Name')];
+ }
+
+ public function renderRow($row)
+ {
+ $type = $this->getType();
+ $url = Url::fromPath("director/templatechoice/${type}", [
+ 'name' => $row->object_name
+ ]);
+
+ return $this::row([
+ Link::create($row->object_name, $url)
+ ]);
+ }
+
+ protected function prepareQuery()
+ {
+ $type = $this->getType();
+ $table = "icinga_${type}_template_choice";
+ return $this->db()
+ ->select()
+ ->from(['o' => $table], 'object_name')
+ ->order('o.object_name');
+ }
+}
diff --git a/library/Director/Web/Table/ConfigFileDiffTable.php b/library/Director/Web/Table/ConfigFileDiffTable.php
new file mode 100644
index 0000000..1d14d5e
--- /dev/null
+++ b/library/Director/Web/Table/ConfigFileDiffTable.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Util;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class ConfigFileDiffTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ protected $leftChecksum;
+
+ protected $rightChecksum;
+
+ /**
+ * @param $leftSum
+ * @param $rightSum
+ * @param Db $connection
+ * @return static
+ */
+ public static function load($leftSum, $rightSum, Db $connection)
+ {
+ $table = new static($connection);
+ $table->getAttributes()->add('class', 'config-diff');
+ return $table->setLeftChecksum($leftSum)
+ ->setRightChecksum($rightSum);
+ }
+
+ public function renderRow($row)
+ {
+ $tr = $this::row([
+ $this->getFileFiffLink($row),
+ $row->file_path,
+ ]);
+
+ $tr->getAttributes()->add('class', 'file-' . $row->file_action);
+ return $tr;
+ }
+
+ protected function getFileFiffLink($row)
+ {
+ $params = array('file_path' => $row->file_path);
+
+ if ($row->file_checksum_left === $row->file_checksum_right) {
+ $params['config_checksum'] = $row->config_checksum_right;
+ } elseif ($row->file_checksum_left === null) {
+ $params['config_checksum'] = $row->config_checksum_right;
+ } elseif ($row->file_checksum_right === null) {
+ $params['config_checksum'] = $row->config_checksum_left;
+ } else {
+ $params['left'] = $row->config_checksum_left;
+ $params['right'] = $row->config_checksum_right;
+ return Link::create(
+ $row->file_action,
+ 'director/config/filediff',
+ $params
+ );
+ }
+
+ return Link::create($row->file_action, 'director/config/file', $params);
+ }
+
+ public function setLeftChecksum($checksum)
+ {
+ $this->leftChecksum = $checksum;
+ return $this;
+ }
+
+ public function setRightChecksum($checksum)
+ {
+ $this->rightChecksum = $checksum;
+ return $this;
+ }
+
+ public function getTitles()
+ {
+ return array(
+ $this->translate('Action'),
+ $this->translate('File'),
+ );
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+
+ $left = $db->select()
+ ->from(
+ array('cfl' => 'director_generated_config_file'),
+ array(
+ 'file_path' => 'COALESCE(cfl.file_path, cfr.file_path)',
+ 'config_checksum_left' => $this->dbHexFunc('cfl.config_checksum'),
+ 'config_checksum_right' => $this->dbHexFunc('cfr.config_checksum'),
+ 'file_checksum_left' => $this->dbHexFunc('cfl.file_checksum'),
+ 'file_checksum_right' => $this->dbHexFunc('cfr.file_checksum'),
+ 'file_action' => '(CASE WHEN cfr.config_checksum IS NULL'
+ . " THEN 'removed' WHEN cfl.file_checksum = cfr.file_checksum"
+ . " THEN 'unmodified' ELSE 'modified' END)",
+ )
+ )->joinLeft(
+ array('cfr' => 'director_generated_config_file'),
+ $db->quoteInto(
+ 'cfl.file_path = cfr.file_path AND cfr.config_checksum = ?',
+ $this->quoteBinary(hex2bin($this->rightChecksum))
+ ),
+ array()
+ )->where(
+ 'cfl.config_checksum = ?',
+ $this->quoteBinary(hex2bin($this->leftChecksum))
+ );
+
+ $right = $db->select()
+ ->from(
+ array('cfl' => 'director_generated_config_file'),
+ array(
+ 'file_path' => 'COALESCE(cfr.file_path, cfl.file_path)',
+ 'config_checksum_left' => $this->dbHexFunc('cfl.config_checksum'),
+ 'config_checksum_right' => $this->dbHexFunc('cfr.config_checksum'),
+ 'file_checksum_left' => $this->dbHexFunc('cfl.file_checksum'),
+ 'file_checksum_right' => $this->dbHexFunc('cfr.file_checksum'),
+ 'file_action' => "('created')",
+ )
+ )->joinRight(
+ array('cfr' => 'director_generated_config_file'),
+ $db->quoteInto(
+ 'cfl.file_path = cfr.file_path AND cfl.config_checksum = ?',
+ $this->quoteBinary(hex2bin($this->leftChecksum))
+ ),
+ array()
+ )->where(
+ 'cfr.config_checksum = ?',
+ $this->quoteBinary(hex2bin($this->rightChecksum))
+ )->where('cfl.file_checksum IS NULL');
+
+ return $db->select()->union(array($left, $right))->order('file_path');
+ }
+}
diff --git a/library/Director/Web/Table/CoreApiFieldsTable.php b/library/Director/Web/Table/CoreApiFieldsTable.php
new file mode 100644
index 0000000..24a6521
--- /dev/null
+++ b/library/Director/Web/Table/CoreApiFieldsTable.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Table;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Url;
+
+class CoreApiFieldsTable extends Table
+{
+ use TranslationHelper;
+
+ protected $defaultAttributes = [
+ 'class' => ['common-table'/*, 'table-row-selectable'*/],
+ //'data-base-target' => '_next',
+ ];
+
+ protected $fields;
+
+ /** @var Url */
+ protected $url;
+
+ public function __construct($fields, Url $url)
+ {
+ $this->url = $url;
+ $this->fields = $fields;
+ }
+
+ public function assemble()
+ {
+ if (empty($this->fields)) {
+ return;
+ }
+ $this->add(Html::tag('thead', Html::tag('tr', Html::wrapEach($this->getColumnsToBeRendered(), 'th'))));
+ foreach ($this->fields as $name => $field) {
+ $tr = $this::tr([
+ $this::td($name),
+ $this::td(Link::create(
+ $field->type,
+ $this->url->with('type', $field->type)
+ )),
+ $this::td($field->id)
+ // $this::td($field->array_rank),
+ // $this::td($this->renderKeyValue($field->attributes))
+ ]);
+ $this->addAttributeColumns($tr, $field->attributes);
+ $this->add($tr);
+ }
+ }
+
+ protected function addAttributeColumns(BaseHtmlElement $tr, $attrs)
+ {
+ $tr->add([
+ $this->makeBooleanColumn($attrs->state),
+ $this->makeBooleanColumn($attrs->config),
+ $this->makeBooleanColumn($attrs->required),
+ $this->makeBooleanColumn(isset($attrs->deprecated) ? $attrs->deprecated : null),
+ $this->makeBooleanColumn($attrs->no_user_modify),
+ $this->makeBooleanColumn($attrs->no_user_view),
+ $this->makeBooleanColumn($attrs->navigation),
+ ]);
+ }
+
+ protected function makeBooleanColumn($value)
+ {
+ if ($value === null) {
+ return $this::td('-');
+ }
+
+ return $this::td($value ? Html::tag('strong', 'true') : 'false');
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Name'),
+ $this->translate('Type'),
+ $this->translate('Id'),
+ // $this->translate('Array Rank'),
+ // $this->translate('Attributes')
+ $this->translate('State'),
+ $this->translate('Config'),
+ $this->translate('Required'),
+ $this->translate('Deprecated'),
+ $this->translate('Protected'),
+ $this->translate('Hidden'),
+ $this->translate('Nav'),
+ ];
+ }
+
+ protected function renderKeyValue($values)
+ {
+ $parts = [];
+ foreach ((array) $values as $key => $value) {
+ if (is_bool($value)) {
+ $value = $value ? 'true' : 'false';
+ }
+ $parts[] = "$key: $value";
+ }
+
+ return implode(', ', $parts);
+ }
+}
diff --git a/library/Director/Web/Table/CoreApiObjectsTable.php b/library/Director/Web/Table/CoreApiObjectsTable.php
new file mode 100644
index 0000000..c2cefea
--- /dev/null
+++ b/library/Director/Web/Table/CoreApiObjectsTable.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Html;
+use ipl\Html\Table;
+use gipfl\Translation\TranslationHelper;
+
+class CoreApiObjectsTable extends Table
+{
+ use TranslationHelper;
+
+ protected $defaultAttributes = [
+ 'class' => ['common-table', 'table-row-selectable'],
+ 'data-base-target' => '_next',
+ ];
+
+ /** @var IcingaEndpoint */
+ protected $endpoint;
+
+ protected $objects;
+
+ protected $type;
+
+ public function __construct($objects, IcingaEndpoint $endpoint, $type)
+ {
+ $this->objects = $objects;
+ $this->endpoint = $endpoint;
+ $this->type = $type;
+ }
+
+ public function assemble()
+ {
+ if (empty($this->objects)) {
+ return;
+ }
+ $this->add(Html::tag('thead', Html::tag('tr', Html::wrapEach($this->getColumnsToBeRendered(), 'th'))));
+ foreach ($this->objects as $name) {
+ $this->add($this::tr($this::td(Link::create(
+ str_replace('!', ': ', $name),
+ 'director/inspect/object',
+ [
+ 'name' => $name,
+ 'type' => $this->type->name,
+ 'plural' => $this->type->plural_name,
+ 'endpoint' => $this->endpoint->getObjectName()
+ ]
+ ))));
+ }
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Name'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/CoreApiPrototypesTable.php b/library/Director/Web/Table/CoreApiPrototypesTable.php
new file mode 100644
index 0000000..78fd964
--- /dev/null
+++ b/library/Director/Web/Table/CoreApiPrototypesTable.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use ipl\Html\Table;
+use gipfl\Translation\TranslationHelper;
+
+class CoreApiPrototypesTable extends Table
+{
+ use TranslationHelper;
+
+ protected $defaultAttributes = ['class' => ['common-table']];
+
+ protected $prototypes;
+
+ protected $typeName;
+
+ public function __construct($prototypes, $typeName)
+ {
+ $this->prototypes = $prototypes;
+ $this->typeName = $typeName;
+ }
+
+ public function assemble()
+ {
+ if (empty($this->prototypes)) {
+ return;
+ }
+ $this->add(Html::tag('thead', Html::tag('tr', Html::wrapEach($this->getColumnsToBeRendered(), 'th'))));
+ $type = $this->typeName;
+ foreach ($this->prototypes as $name) {
+ $this->add($this::tr($this::td("$type.$name()")));
+ }
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Name'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/CustomvarTable.php b/library/Director/Web/Table/CustomvarTable.php
new file mode 100644
index 0000000..f9a3844
--- /dev/null
+++ b/library/Director/Web/Table/CustomvarTable.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Zend_Db_Adapter_Abstract as ZfDbAdapter;
+use Zend_Db_Select as ZfDbSelect;
+
+class CustomvarTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = array(
+ 'varname',
+ );
+
+ public function renderRow($row)
+ {
+ $tr = $this::row([
+ new Link(
+ $row->varname,
+ 'director/customvar/variants',
+ ['name' => $row->varname]
+ )
+ ]);
+
+ foreach ($this->getObjectTypes() as $type) {
+ $tr->add($this::td(Html::tag('nobr', null, sprintf(
+ $this->translate('%d / %d'),
+ $row->{"cnt_$type"},
+ $row->{"distinct_$type"}
+ ))));
+ }
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return array(
+ $this->translate('Variable name'),
+ $this->translate('Distinct Commands'),
+ $this->translate('Hosts'),
+ $this->translate('Services'),
+ $this->translate('Service Sets'),
+ $this->translate('Notifications'),
+ $this->translate('Users'),
+ );
+ }
+
+ protected function getObjectTypes()
+ {
+ return ['command', 'host', 'service', 'service_set', 'notification', 'user'];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ $varsColumns = ['varname' => 'v.varname'];
+ $varsTypes = $this->getObjectTypes();
+ foreach ($varsTypes as $type) {
+ $varsColumns["cnt_$type"] = '(0)';
+ $varsColumns["distinct_$type"] = '(0)';
+ }
+ $varsQueries = [];
+ foreach ($varsTypes as $type) {
+ $varsQueries[] = $this->makeVarSub($type, $varsColumns, $db);
+ }
+
+ $union = $db->select()->union($varsQueries, ZfDbSelect::SQL_UNION_ALL);
+
+ $columns = ['varname' => 'u.varname'];
+ foreach ($varsTypes as $column) {
+ $columns["cnt_$column"] = "SUM(u.cnt_$column)";
+ $columns["distinct_$column"] = "SUM(u.distinct_$column)";
+ }
+ return $db->select()->from(
+ array('u' => $union),
+ $columns
+ )->group('u.varname')->order('u.varname ASC')->limit(100);
+ }
+
+ /**
+ * @param string $type
+ * @param array $columns
+ * @param ZfDbAdapter $db
+ * @return ZfDbSelect
+ */
+ protected function makeVarSub($type, array $columns, ZfDbAdapter $db)
+ {
+ $columns["cnt_$type"] = 'COUNT(*)';
+ $columns["distinct_$type"] = 'COUNT(DISTINCT varvalue)';
+ return $db->select()->from(
+ ['v' => "icinga_${type}_var"],
+ $columns
+ )->join(
+ ['o' => "icinga_${type}"],
+ "o.id = v.${type}_id",
+ []
+ )->where('o.object_type != ?', 'external_object')->group('varname');
+ }
+}
diff --git a/library/Director/Web/Table/CustomvarVariantsTable.php b/library/Director/Web/Table/CustomvarVariantsTable.php
new file mode 100644
index 0000000..80fca70
--- /dev/null
+++ b/library/Director/Web/Table/CustomvarVariantsTable.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\PlainObjectRenderer;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Zend_Db_Adapter_Abstract as ZfDbAdapter;
+use Zend_Db_Select as ZfDbSelect;
+
+class CustomvarVariantsTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = ['varvalue'];
+
+ protected $varName;
+
+ public static function create(Db $db, $varName)
+ {
+ $table = new static($db);
+ $table->varName = $varName;
+ $table->getAttributes()->set('class', 'common-table');
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ if ($row->format === 'json') {
+ $value = PlainObjectRenderer::render(json_decode($row->varvalue));
+ } else {
+ $value = $row->varvalue;
+ }
+ $tr = $this::row([
+ /* new Link(
+ $value,
+ 'director/customvar/value',
+ ['name' => $row->varvalue]
+ )*/
+ $value
+ ]);
+
+ foreach ($this->getObjectTypes() as $type) {
+ $cnt = (int) $row->{"cnt_$type"};
+ if ($cnt === 0) {
+ $cnt = '-';
+ }
+ $tr->add($this::td($cnt));
+ }
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return array(
+ $this->translate('Variable Value'),
+ $this->translate('Commands'),
+ $this->translate('Hosts'),
+ $this->translate('Services'),
+ $this->translate('Service Sets'),
+ $this->translate('Notifications'),
+ $this->translate('Users'),
+ );
+ }
+
+ protected function getObjectTypes()
+ {
+ return ['command', 'host', 'service', 'service_set', 'notification', 'user'];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ $varsColumns = ['varvalue' => 'v.varvalue'];
+ $varsTypes = $this->getObjectTypes();
+ foreach ($varsTypes as $type) {
+ $varsColumns["cnt_$type"] = '(0)';
+ }
+ $varsQueries = [];
+ foreach ($varsTypes as $type) {
+ $varsQueries[] = $this->makeVarSub($type, $varsColumns, $db);
+ }
+
+ $union = $db->select()->union($varsQueries, ZfDbSelect::SQL_UNION_ALL);
+
+ $columns = [
+ 'varvalue' => 'u.varvalue',
+ 'format' => 'u.format',
+ ];
+ foreach ($varsTypes as $column) {
+ $columns["cnt_$column"] = "SUM(u.cnt_$column)";
+ }
+ return $db->select()->from(['u' => $union], $columns)
+ ->group('u.varvalue')->group('u.format')
+ ->order('u.varvalue ASC')
+ ->order('u.format ASC')
+ ->limit(100);
+ }
+
+ /**
+ * @param string $type
+ * @param array $columns
+ * @param ZfDbAdapter $db
+ * @return ZfDbSelect
+ */
+ protected function makeVarSub($type, array $columns, ZfDbAdapter $db)
+ {
+ $columns["cnt_$type"] = 'COUNT(*)';
+ $columns['format'] = 'v.format';
+ return $db->select()->from(
+ ['v' => "icinga_${type}_var"],
+ $columns
+ )->join(
+ ['o' => "icinga_${type}"],
+ "o.id = v.${type}_id",
+ []
+ )->where(
+ 'v.varname = ?',
+ $this->varName
+ )->where(
+ 'o.object_type != ?',
+ 'external_object'
+ )->group('varvalue')->group('v.format');
+ }
+}
diff --git a/library/Director/Web/Table/DatafieldCategoryTable.php b/library/Director/Web/Table/DatafieldCategoryTable.php
new file mode 100644
index 0000000..6f07939
--- /dev/null
+++ b/library/Director/Web/Table/DatafieldCategoryTable.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use ipl\Html\Html;
+
+class DatafieldCategoryTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'dfc.category_name',
+ 'dfc.description',
+ ];
+
+ public function getColumns()
+ {
+ return array(
+ 'id' => 'dfc.id',
+ 'category_name' => 'dfc.category_name',
+ 'description' => 'dfc.description',
+ 'assigned_fields' => 'COUNT(df.id)',
+ );
+ }
+
+ public function renderRow($row)
+ {
+ $main = [Link::create(
+ $row->category_name,
+ 'director/datafieldcategory/edit',
+ ['name' => $row->category_name]
+ )];
+
+ if ($row->description !== null && strlen($row->description)) {
+ $main[] = Html::tag('br');
+ $main[] = Html::tag('small', $row->description);
+ }
+ return $this::tr([
+ $this::td($main),
+ $this::td($row->assigned_fields)
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Category Name'),
+ $this->translate('# Used'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ return $db->select()->from(
+ ['dfc' => 'director_datafield_category'],
+ $this->getColumns()
+ )->joinLeft(
+ ['df' => 'director_datafield'],
+ 'df.category_id = dfc.id',
+ []
+ )->group('dfc.id')->group('dfc.category_name')->order('category_name ASC');
+ }
+}
diff --git a/library/Director/Web/Table/DatafieldTable.php b/library/Director/Web/Table/DatafieldTable.php
new file mode 100644
index 0000000..4b321d7
--- /dev/null
+++ b/library/Director/Web/Table/DatafieldTable.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Zend_Db_Adapter_Abstract as ZfDbAdapter;
+use Zend_Db_Select as ZfDbSelect;
+
+class DatafieldTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'df.varname',
+ 'df.caption',
+ ];
+
+ public function getColumns()
+ {
+ return [
+ 'id' => 'df.id',
+ 'varname' => 'df.varname',
+ 'caption' => 'df.caption',
+ 'description' => 'df.description',
+ 'datatype' => 'df.datatype',
+ 'category' => 'dfc.category_name',
+ 'assigned_fields' => 'SUM(used_fields.cnt)',
+ 'assigned_vars' => 'SUM(used_vars.cnt)',
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ return $this::tr([
+ $this::td(Link::create(
+ $row->caption,
+ 'director/datafield/edit',
+ ['id' => $row->id]
+ )),
+ $this::td($row->varname),
+ $this::td($row->category),
+ $this::td($row->assigned_fields),
+ $this::td($row->assigned_vars)
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Label'),
+ $this->translate('Field name'),
+ $this->translate('Category'),
+ $this->translate('# Used'),
+ $this->translate('# Vars'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ $fieldTypes = ['command', 'host', 'notification', 'service', 'user'];
+ $varsTypes = ['command', 'host', 'notification', 'service', 'service_set', 'user'];
+
+ $fieldsQueries = [];
+ foreach ($fieldTypes as $type) {
+ $fieldsQueries[] = $this->makeDatafieldSub($type, $db);
+ }
+
+ $varsQueries = [];
+ foreach ($varsTypes as $type) {
+ $varsQueries[] = $this->makeVarSub($type, $db);
+ }
+
+ return $db->select()->from(
+ ['df' => 'director_datafield'],
+ $this->getColumns()
+ )->joinLeft(
+ ['dfc' => 'director_datafield_category'],
+ 'df.category_id = dfc.id',
+ []
+ )->joinLeft(
+ ['used_fields' => $db->select()->union($fieldsQueries, ZfDbSelect::SQL_UNION_ALL)],
+ 'used_fields.datafield_id = df.id',
+ []
+ )->joinLeft(
+ ['used_vars' => $db->select()->union($varsQueries, ZfDbSelect::SQL_UNION_ALL)],
+ 'used_vars.varname = df.varname',
+ []
+ )->group('df.id')->group('df.varname')->group('dfc.category_name')->order('caption ASC');
+ }
+
+ /**
+ * @param $type
+ * @param ZfDbAdapter $db
+ *
+ * @return ZfDbSelect
+ */
+ protected function makeDatafieldSub($type, ZfDbAdapter $db)
+ {
+ return $db->select()->from("icinga_${type}_field", [
+ 'cnt' => 'COUNT(*)',
+ 'datafield_id'
+ ])->group('datafield_id');
+ }
+
+ /**
+ * @param $type
+ * @param ZfDbAdapter $db
+ *
+ * @return ZfDbSelect
+ */
+ protected function makeVarSub($type, ZfDbAdapter $db)
+ {
+ return $db->select()->from("icinga_${type}_var", [
+ 'cnt' => 'COUNT(*)',
+ 'varname'
+ ])->group('varname');
+ }
+}
diff --git a/library/Director/Web/Table/DatalistEntryTable.php b/library/Director/Web/Table/DatalistEntryTable.php
new file mode 100644
index 0000000..70167c7
--- /dev/null
+++ b/library/Director/Web/Table/DatalistEntryTable.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class DatalistEntryTable extends ZfQueryBasedTable
+{
+ protected $datalist;
+
+ protected $searchColumns = [
+ 'entry_name',
+ 'entry_value'
+ ];
+
+ public function setList(DirectorDatalist $list)
+ {
+ $this->datalist = $list;
+
+ return $this;
+ }
+
+ public function getList()
+ {
+ return $this->datalist;
+ }
+
+ public function getColumns()
+ {
+ return [
+ 'list_name' => 'l.list_name',
+ 'list_id' => 'le.list_id',
+ 'entry_name' => 'le.entry_name',
+ 'entry_value' => 'le.entry_value',
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ return $this::tr([
+ $this::td(Link::create($row->entry_name, 'director/data/listentry/edit', [
+ 'list' => $row->list_name,
+ 'entry_name' => $row->entry_name,
+ ])),
+ $this::td($row->entry_value)
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ 'entry_name' => $this->translate('Key'),
+ 'entry_value' => $this->translate('Label'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['le' => 'director_datalist_entry'],
+ $this->getColumns()
+ )->join(
+ ['l' => 'director_datalist'],
+ 'l.id = le.list_id',
+ []
+ )->where(
+ 'le.list_id = ?',
+ $this->getList()->id
+ )->order('le.entry_name ASC');
+ }
+}
diff --git a/library/Director/Web/Table/DatalistTable.php b/library/Director/Web/Table/DatalistTable.php
new file mode 100644
index 0000000..7b35fe0
--- /dev/null
+++ b/library/Director/Web/Table/DatalistTable.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class DatalistTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = ['list_name'];
+
+ public function getColumns()
+ {
+ return [
+ 'id' => 'l.id',
+ 'list_name' => 'l.list_name',
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ return $this::tr($this::td(Link::create(
+ $row->list_name,
+ 'director/data/listentry',
+ array('list' => $row->list_name)
+ )));
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('List name')];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['l' => 'director_datalist'],
+ $this->getColumns()
+ )->order('list_name ASC');
+ }
+}
diff --git a/library/Director/Web/Table/DbHelper.php b/library/Director/Web/Table/DbHelper.php
new file mode 100644
index 0000000..573f946
--- /dev/null
+++ b/library/Director/Web/Table/DbHelper.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Expr as Expr;
+
+trait DbHelper
+{
+ public function dbHexFunc($column)
+ {
+ if ($this->isPgsql()) {
+ return sprintf("LOWER(ENCODE(%s, 'hex'))", $column);
+ } else {
+ return sprintf("LOWER(HEX(%s))", $column);
+ }
+ }
+
+ public function quoteBinary($binary)
+ {
+ if ($binary === '') {
+ return '';
+ }
+
+ if (is_array($binary)) {
+ return array_map([$this, 'quoteBinary'], $binary);
+ }
+
+ if ($this->isPgsql()) {
+ return new Expr("'\\x" . bin2hex($binary) . "'");
+ }
+
+ return new Expr('0x' . bin2hex($binary));
+ }
+
+ public function isPgsql()
+ {
+ return $this->db() instanceof \Zend_Db_Adapter_Pdo_Pgsql;
+ }
+
+ public function isMysql()
+ {
+ return $this->db() instanceof \Zend_Db_Adapter_Pdo_Mysql;
+ }
+
+ public function wantBinaryValue($value)
+ {
+ if (is_resource($value)) {
+ return stream_get_contents($value);
+ }
+
+ return $value;
+ }
+
+ public function getChecksum($checksum)
+ {
+ return bin2hex($this->wantBinaryValue($checksum));
+ }
+
+ public function getShortChecksum($checksum)
+ {
+ if ($checksum === null) {
+ return null;
+ }
+
+ return substr($this->getChecksum($checksum), 0, 7);
+ }
+}
diff --git a/library/Director/Web/Table/Dependency/DependencyInfoTable.php b/library/Director/Web/Table/Dependency/DependencyInfoTable.php
new file mode 100644
index 0000000..28aa856
--- /dev/null
+++ b/library/Director/Web/Table/Dependency/DependencyInfoTable.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table\Dependency;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Director\Application\DependencyChecker;
+use Icinga\Web\Url;
+
+class DependencyInfoTable
+{
+ protected $module;
+
+ protected $checker;
+
+ public function __construct(DependencyChecker $checker, Module $module)
+ {
+ $this->module = $module;
+ $this->checker = $checker;
+ }
+
+ protected function linkToModule($name, $icon)
+ {
+ return Html::link(
+ Html::escape($name),
+ Html::webUrl('config/module', ['name' => $name]),
+ [
+ 'class' => "icon-$icon"
+ ]
+ );
+ }
+
+ public function render()
+ {
+ $html = '<table class="common-table table-row-selectable">
+<thead>
+<tr>
+ <th>' . Html::escape($this->translate('Module name')) . '</th>
+ <th>' . Html::escape($this->translate('Required')) . '</th>
+ <th>' . Html::escape($this->translate('Installed')) . '</th>
+</tr>
+</thead>
+<tbody data-base-target="_next">
+';
+ foreach ($this->checker->getDependencies($this->module) as $dependency) {
+ $name = $dependency->getName();
+ $isLibrary = substr($name, 0, 11) === 'icinga-php-';
+ $rowAttributes = $isLibrary ? ['data-base-target' => '_self'] : null;
+ if ($dependency->isSatisfied()) {
+ if ($dependency->isSatisfied()) {
+ $icon = 'ok';
+ } else {
+ $icon = 'cancel';
+ }
+ $link = $isLibrary ? $this->noLink($name, $icon) : $this->linkToModule($name, $icon);
+ $installed = $dependency->getInstalledVersion();
+ } elseif ($dependency->isInstalled()) {
+ $installed = sprintf('%s (%s)', $dependency->getInstalledVersion(), $this->translate('disabled'));
+ $link = $this->linkToModule($name, 'cancel');
+ } else {
+ $installed = $this->translate('missing');
+ $repository = $isLibrary ? $name : "icingaweb2-module-$name";
+ $link = sprintf(
+ '%s (%s)',
+ $this->noLink($name, 'cancel'),
+ Html::linkToGitHub(Html::escape($this->translate('more')), 'Icinga', $repository)
+ );
+ }
+
+ $html .= $this->htmlRow([
+ $link,
+ Html::escape($dependency->getRequirement()),
+ Html::escape($installed)
+ ], $rowAttributes);
+ }
+
+ return $html . '</tbody>
+</table>
+';
+ }
+
+ protected function noLink($label, $icon)
+ {
+ return Html::link(Html::escape($label), Url::fromRequest()->with('rnd', rand(1, 100000)), [
+ 'class' => "icon-$icon"
+ ]);
+ }
+
+ protected function translate($string)
+ {
+ return \mt('director', $string);
+ }
+
+ protected function htmlRow(array $cols, $rowAttributes)
+ {
+ $content = '';
+ foreach ($cols as $escapedContent) {
+ $content .= Html::tag('td', null, $escapedContent);
+ }
+ return Html::tag('tr', $rowAttributes, $content);
+ }
+}
diff --git a/library/Director/Web/Table/Dependency/Html.php b/library/Director/Web/Table/Dependency/Html.php
new file mode 100644
index 0000000..092f799
--- /dev/null
+++ b/library/Director/Web/Table/Dependency/Html.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table\Dependency;
+
+use Icinga\Web\Url;
+use InvalidArgumentException;
+
+/**
+ * Minimal HTML helper, as we might be forced to run without ipl
+ */
+class Html
+{
+ public static function tag($tag, $attributes = [], $escapedContent = null)
+ {
+ $result = "<$tag";
+ if (! empty($attributes)) {
+ foreach ($attributes as $name => $value) {
+ if (! preg_match('/^[a-z][a-z0-9:-]*$/i', $name)) {
+ throw new InvalidArgumentException("Invalid attribute name: '$name'");
+ }
+
+ $result .= " $name=\"" . self::escapeAttributeValue($value) . '"';
+ }
+ }
+
+ return "$result>$escapedContent</$tag>";
+ }
+
+ public static function webUrl($path, $params)
+ {
+ return Url::fromPath($path, $params);
+ }
+
+ public static function link($escapedLabel, $url, $attributes = [])
+ {
+ return static::tag('a', [
+ 'href' => $url,
+ ] + $attributes, $escapedLabel);
+ }
+
+ public static function linkToGitHub($escapedLabel, $namespace, $repository)
+ {
+ return static::link(
+ $escapedLabel,
+ 'https://github.com/' . urlencode($namespace) . '/' . urlencode($repository),
+ [
+ 'target' => '_blank',
+ 'rel' => 'noreferrer',
+ 'class' => 'icon-forward'
+ ]
+ );
+ }
+
+ protected static function escapeAttributeValue($value)
+ {
+ $value = str_replace('"', '&quot;', $value);
+ // Escape ambiguous ampersands
+ return preg_replace_callback('/&[0-9A-Z]+;/i', function ($match) {
+ $subject = $match[0];
+
+ if (htmlspecialchars_decode($subject, ENT_COMPAT | ENT_HTML5) === $subject) {
+ // Ambiguous ampersand
+ return str_replace('&', '&amp;', $subject);
+ }
+
+ return $subject;
+ }, $value);
+ }
+
+ public static function escape($any)
+ {
+ return htmlspecialchars($any);
+ }
+}
diff --git a/library/Director/Web/Table/DependencyTemplateUsageTable.php b/library/Director/Web/Table/DependencyTemplateUsageTable.php
new file mode 100644
index 0000000..d7537c5
--- /dev/null
+++ b/library/Director/Web/Table/DependencyTemplateUsageTable.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+class DependencyTemplateUsageTable extends TemplateUsageTable
+{
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'applyrules' => $this->translate('Apply Rules'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'applyrules' => $this->getSummaryLine('apply'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/DeploymentLogTable.php b/library/Director/Web/Table/DeploymentLogTable.php
new file mode 100644
index 0000000..2d5cb94
--- /dev/null
+++ b/library/Director/Web/Table/DeploymentLogTable.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Icinga\Date\DateFormatter;
+
+class DeploymentLogTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ protected $activeStageName;
+
+ public function setActiveStageName($name)
+ {
+ $this->activeStageName = $name;
+ return $this;
+ }
+
+ public function assemble()
+ {
+ $this->getAttributes()->add('class', 'deployment-log');
+ }
+
+ public function renderRow($row)
+ {
+ $this->splitByDay($row->start_time);
+
+ $shortSum = $this->getShortChecksum($row->config_checksum);
+ $tr = $this::tr([
+ $this::td(Link::create(
+ $shortSum === null ? $row->peer_identity : [$row->peer_identity, " ($shortSum)"],
+ 'director/deployment',
+ ['id' => $row->id]
+ )),
+ $this::td(DateFormatter::formatTime($row->start_time))
+ ])->addAttributes(['class' => $this->getMyRowClasses($row)]);
+
+ return $tr;
+ }
+
+ protected function getMyRowClasses($row)
+ {
+ if ($row->startup_succeeded === 'y') {
+ $classes = ['succeeded'];
+ } elseif ($row->startup_succeeded === 'n') {
+ $classes = ['failed'];
+ } elseif ($row->stage_collected === null) {
+ $classes = ['pending'];
+ } elseif ($row->dump_succeeded === 'y') {
+ $classes = ['sent'];
+ } else {
+ // TODO: does this ever be stored?
+ $classes = ['notsent'];
+ }
+
+ if ($this->activeStageName !== null
+ && $row->stage_name === $this->activeStageName
+ ) {
+ $classes[] = 'running';
+ }
+
+ return $classes;
+ }
+
+ public function getColumns()
+ {
+ $columns = [
+ 'id' => 'l.id',
+ 'peer_identity' => 'l.peer_identity',
+ 'start_time' => 'UNIX_TIMESTAMP(l.start_time)',
+ 'stage_collected' => 'l.stage_collected',
+ 'dump_succeeded' => 'l.dump_succeeded',
+ 'stage_name' => 'l.stage_name',
+ 'startup_succeeded' => 'l.startup_succeeded',
+ 'config_checksum' => 'l.config_checksum',
+ ];
+
+ return $columns;
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ array('l' => 'director_deployment_log'),
+ $this->getColumns()
+ )->order('l.start_time DESC')->limit(100);
+ }
+}
diff --git a/library/Director/Web/Table/FilterableByUsage.php b/library/Director/Web/Table/FilterableByUsage.php
new file mode 100644
index 0000000..5e8695f
--- /dev/null
+++ b/library/Director/Web/Table/FilterableByUsage.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+interface FilterableByUsage
+{
+ public function showOnlyUsed();
+
+ public function showOnlyUnUsed();
+}
diff --git a/library/Director/Web/Table/GeneratedConfigFileTable.php b/library/Director/Web/Table/GeneratedConfigFileTable.php
new file mode 100644
index 0000000..97f7091
--- /dev/null
+++ b/library/Director/Web/Table/GeneratedConfigFileTable.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class GeneratedConfigFileTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ protected $searchColumns = ['file_path'];
+
+ protected $deploymentId;
+
+ protected $activeFile;
+
+ /** @var IcingaConfig */
+ protected $config;
+
+ public static function load(IcingaConfig $config, Db $db)
+ {
+ $table = new static($db);
+ $table->config = $config;
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ $counts = implode(' / ', [
+ $row->cnt_object,
+ $row->cnt_template,
+ $row->cnt_apply
+ ]);
+
+ $tr = $this::row([
+ $this->getFileLink($row),
+ $counts,
+ $row->size
+ ]);
+
+ if ($row->file_path === $this->activeFile) {
+ $tr->getAttributes()->add('class', 'active');
+ }
+
+ return $tr;
+ }
+
+ public function setActiveFilename($filename)
+ {
+ $this->activeFile = $filename;
+ return $this;
+ }
+
+ protected function getFileLink($row)
+ {
+ $params = [
+ 'config_checksum' => $row->config_checksum,
+ 'file_path' => $row->file_path
+ ];
+
+ if ($this->deploymentId) {
+ $params['deployment_id'] = $this->deploymentId;
+ }
+
+ return Link::create($row->file_path, 'director/config/file', $params);
+ }
+
+ public function setDeploymentId($id)
+ {
+ if ($id) {
+ $this->deploymentId = (int) $id;
+ }
+
+ return $this;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('File'),
+ $this->translate('Object/Tpl/Apply'),
+ $this->translate('Size'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $columns = [
+ 'file_path' => 'cf.file_path',
+ 'size' => 'LENGTH(f.content)',
+ 'cnt_object' => 'f.cnt_object',
+ 'cnt_template' => 'f.cnt_template',
+ 'cnt_apply' => 'f.cnt_apply',
+ 'cnt_all' => "f.cnt_object || ' / ' || f.cnt_template || ' / ' || f.cnt_apply",
+ 'checksum' => 'LOWER(HEX(f.checksum))',
+ 'config_checksum' => 'LOWER(HEX(cf.config_checksum))',
+ ];
+
+ if ($this->isPgsql()) {
+ $columns['checksum'] = "LOWER(ENCODE(f.checksum, 'hex'))";
+ $columns['config_checksum'] = "LOWER(ENCODE(cf.config_checksum, 'hex'))";
+ }
+
+ return $this->db()->select()->from(
+ ['cf' => 'director_generated_config_file'],
+ $columns
+ )->join(
+ ['f' => 'director_generated_file'],
+ 'cf.file_checksum = f.checksum',
+ []
+ )->where(
+ 'config_checksum = ?',
+ $this->quoteBinary($this->config->getChecksum())
+ )->order('cf.file_path ASC');
+ }
+}
diff --git a/library/Director/Web/Table/GroupMemberTable.php b/library/Director/Web/Table/GroupMemberTable.php
new file mode 100644
index 0000000..b0814ad
--- /dev/null
+++ b/library/Director/Web/Table/GroupMemberTable.php
@@ -0,0 +1,201 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Table\Extension\MultiSelect;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\AssignRenderer;
+use Icinga\Module\Director\Objects\IcingaObjectGroup;
+use Exception;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+
+class GroupMemberTable extends ZfQueryBasedTable
+{
+ use MultiSelect;
+
+ protected $searchColumns = [
+ 'o.object_name',
+ // membership_type
+ ];
+
+ protected $type;
+
+ /** @var IcingaObjectGroup */
+ protected $group;
+
+ /**
+ * @param $type
+ * @param Db $db
+ * @return static
+ */
+ public static function create($type, Db $db)
+ {
+ $class = __NAMESPACE__ . '\\GroupMemberTable' . ucfirst($type);
+ if (! class_exists($class)) {
+ $class = __CLASS__;
+ }
+
+ /** @var static $table */
+ $table = new $class($db);
+ $table->type = $type;
+ return $table;
+ }
+ public function assemble()
+ {
+ if ($this->type === 'host') {
+ $this->enableMultiSelect(
+ 'director/hosts/edit',
+ 'director/hosts',
+ ['name']
+ );
+ }
+ }
+
+ public function setGroup(IcingaObjectGroup $group)
+ {
+ $this->group = $group;
+ return $this;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ if ($this->group === null) {
+ return [
+ $this->translate('Group'),
+ $this->translate('Member'),
+ $this->translate('via')
+ ];
+ } else {
+ return [
+ $this->translate('Member'),
+ $this->translate('via')
+ ];
+ }
+ }
+
+ public function renderRow($row)
+ {
+ $type = $this->getType();
+ if ($row->object_type === 'apply') {
+ $params = [
+ 'id' => $row->id
+ ];
+ } elseif (isset($row->host_id)) {
+ // I would prefer to see host=<name> and set=<name>, but joining
+ // them here is pointless. We should use DeferredHtml for these,
+ // remember hosts/sets we need and fetch them in a single query at
+ // rendering time. For now, this works fine - just... the URLs are
+ // not so nice
+ $params = [
+ 'name' => $row->object_name,
+ 'host_id' => $row->host_id
+ ];
+ } elseif (isset($row->service_set_id)) {
+ $params = [
+ 'name' => $row->object_name,
+ 'set_id' => $row->service_set_id
+ ];
+ } else {
+ $params = [
+ 'name' => $row->object_name
+ ];
+ }
+
+ $url = Url::fromPath("director/${type}", $params);
+
+ $tr = $this::tr();
+
+ if ($this->group === null) {
+ $tr->add($this::td($row->group_name));
+ }
+ $link = Link::create($row->object_name, $url);
+ if ($row->object_type === 'apply') {
+ $link = [
+ $link,
+ ' (where ',
+ $this->renderApplyFilter($row->assign_filter),
+ ')'
+ ];
+ }
+
+ $tr->add([
+ $this::td($link),
+ $this::td($row->membership_type)
+ ]);
+
+ return $tr;
+ }
+
+ protected function renderApplyFilter($assignFilter)
+ {
+ try {
+ $string = AssignRenderer::forFilter(
+ Filter::fromQueryString($assignFilter)
+ )->renderAssign();
+ // Do not prefix it
+ $string = preg_replace('/^assign where /', '', $string);
+ } catch (Exception $e) {
+ // ignore errors in filter rendering
+ $string = 'Error in Filter rendering: ' . $e->getMessage();
+ }
+
+ return $string;
+ }
+
+ protected function prepareQuery()
+ {
+ // select h.object_name, hg.object_name,
+ // CASE WHEN hgh.host_id IS NULL THEN 'apply' ELSE 'direct' END AS assi
+ // from icinga_hostgroup_host_resolved hgr join icinga_host h on h.id = hgr.host_id
+ // join icinga_hostgroup hg on hgr.hostgroup_id = hg.id
+ // left join icinga_hostgroup_host hgh on hgh.host_id = h.id and hgh.hostgroup_id = hg.id;
+
+ $type = $this->getType();
+ $columns = [
+ 'o.id',
+ 'o.object_type',
+ 'o.object_name',
+ 'membership_type' => "CASE WHEN go.${type}_id IS NULL THEN 'apply' ELSE 'direct' END"
+ ];
+
+ if ($this->group === null) {
+ $columns = ['group_name' => 'g.object_name'] + $columns;
+ }
+ if ($type === 'service') {
+ $columns[] = 'o.assign_filter';
+ $columns[] = 'o.host_id';
+ $columns[] = 'o.service_set_id';
+ }
+
+ $query = $this->db()->select()->from(
+ ['gro' => "icinga_${type}group_${type}_resolved"],
+ $columns
+ )->join(
+ ['o' => "icinga_${type}"],
+ "o.id = gro.${type}_id",
+ []
+ )->join(
+ ['g' => "icinga_${type}group"],
+ "gro.${type}group_id = g.id",
+ []
+ )->joinLeft(
+ ['go' => "icinga_${type}group_${type}"],
+ "go.${type}_id = o.id AND go.${type}group_id = g.id",
+ []
+ )->order('o.object_name');
+
+ if ($this->group !== null) {
+ $query->where('g.id = ?', $this->group->get('id'));
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/HostTemplateUsageTable.php b/library/Director/Web/Table/HostTemplateUsageTable.php
new file mode 100644
index 0000000..2d1ee2f
--- /dev/null
+++ b/library/Director/Web/Table/HostTemplateUsageTable.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+class HostTemplateUsageTable extends TemplateUsageTable
+{
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'objects' => $this->translate('Objects'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'objects' => $this->getSummaryLine('object'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/IcingaAppliedServiceTable.php b/library/Director/Web/Table/IcingaAppliedServiceTable.php
new file mode 100644
index 0000000..b669296
--- /dev/null
+++ b/library/Director/Web/Table/IcingaAppliedServiceTable.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaService;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaAppliedServiceTable extends ZfQueryBasedTable
+{
+ protected $service;
+
+ protected $searchColumns = array(
+ 'service',
+ );
+
+ public function setService(IcingaService $service)
+ {
+ $this->service = $service;
+ return $this;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ new Link($row->service, 'director/service', ['id' => $row->id])
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('Servicename')];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ array('s' => 'icinga_service'),
+ array()
+ )->joinLeft(
+ array('si' => 'icinga_service_inheritance'),
+ 's.id = si.service_id',
+ array()
+ )->where(
+ 'si.parent_service_id = ?',
+ $this->service->id
+ )->where('s.object_type = ?', 'apply');
+ }
+}
diff --git a/library/Director/Web/Table/IcingaCommandArgumentTable.php b/library/Director/Web/Table/IcingaCommandArgumentTable.php
new file mode 100644
index 0000000..37cbc78
--- /dev/null
+++ b/library/Director/Web/Table/IcingaCommandArgumentTable.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Data\Json;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchModificationStore;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaCommandArgumentTable extends ZfQueryBasedTable
+{
+ /** @var IcingaCommand */
+ protected $command;
+
+ /** @var Branch */
+ protected $branch;
+
+ protected $searchColumns = [
+ 'ca.argument_name',
+ 'ca.argument_value',
+ ];
+
+ public function __construct(IcingaCommand $command, Branch $branch)
+ {
+ $this->command = $command;
+ $this->branch = $branch;
+ parent::__construct($command->getConnection());
+ $this->getAttributes()->set('data-base-target', '_self');
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create($row->argument_name, 'director/command/arguments', [
+ 'argument' => $row->argument_name,
+ 'name' => $this->command->getObjectName()
+ ]),
+ $row->argument_value
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Argument'),
+ $this->translate('Value'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ if ($this->branch->isBranch()) {
+ return (new ArrayDatasource((array) $this->command->arguments()->toPlainObject()))->select();
+ /** @var Db $connection */
+ $connection = $this->connection();
+ $store = new BranchModificationStore($connection, 'command');
+ $modification = $store->loadOptionalModificationByName(
+ $this->command->getObjectName(),
+ $this->branch->getUuid()
+ );
+ if ($modification) {
+ $props = $modification->getProperties()->jsonSerialize();
+ if (isset($props->arguments)) {
+ return new ArrayDatasource((array) $this->command->arguments()->toPlainObject());
+ }
+ }
+ }
+ $id = $this->command->get('id');
+ if ($id === null) {
+ return new ArrayDatasource([]);
+ }
+ return $this->db()->select()->from(
+ ['ca' => 'icinga_command_argument'],
+ [
+ 'id' => 'ca.id',
+ 'argument_name' => "COALESCE(ca.argument_name, '(none)')",
+ 'argument_value' => 'ca.argument_value',
+ ]
+ )->where(
+ 'ca.command_id = ?',
+ $id
+ )->order('ca.sort_order')->order('ca.argument_name')->limit(100);
+ }
+}
diff --git a/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php b/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php
new file mode 100644
index 0000000..0d2f8e8
--- /dev/null
+++ b/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\CustomVariable\CustomVariableDictionary;
+use Icinga\Module\Director\Objects\IcingaHost;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+
+class IcingaHostAppliedForServiceTable extends SimpleQueryBasedTable
+{
+ protected $title;
+
+ protected $host;
+
+ /** @var CustomVariableDictionary */
+ protected $cv;
+
+ protected $searchColumns = [
+ 'service',
+ ];
+
+ /** @var bool */
+ protected $readonly = false;
+
+ /** @var string|null */
+ protected $highlightedService;
+
+ /**
+ * @param IcingaHost $host
+ * @param CustomVariableDictionary $dict
+ * @return static
+ */
+ public static function load(IcingaHost $host, CustomVariableDictionary $dict)
+ {
+ $table = (new static())->setHost($host)->setDictionary($dict);
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ public function setDictionary(CustomVariableDictionary $dict)
+ {
+ $this->cv = $dict;
+ return $this;
+ }
+
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ /**
+ * Show no related links
+ *
+ * @param bool $readonly
+ * @return $this
+ */
+ public function setReadonly($readonly = true)
+ {
+ $this->readonly = (bool) $readonly;
+
+ return $this;
+ }
+
+ public function highlightService($service)
+ {
+ $this->highlightedService = $service;
+
+ return $this;
+ }
+
+ public function renderRow($row)
+ {
+ if ($this->readonly) {
+ if ($this->highlightedService === $row->service) {
+ $link = Html::tag('span', ['class' => 'icon-right-big'], $row->service);
+ } else {
+ $link = $row->service;
+ }
+ } else {
+ $link = Link::create($row->service, 'director/host/appliedservice', [
+ 'name' => $this->host->object_name,
+ 'service' => $row->service,
+ ]);
+ }
+
+ return $this::row([$link]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->title ?: $this->translate('Service name'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $data = [];
+ foreach ($this->cv->getValue() as $key => $var) {
+ $data[] = (object) array(
+ 'service' => $key,
+ );
+ }
+
+ return (new ArrayDatasource($data))->select();
+ }
+}
diff --git a/library/Director/Web/Table/IcingaHostAppliedServicesTable.php b/library/Director/Web/Table/IcingaHostAppliedServicesTable.php
new file mode 100644
index 0000000..415903b
--- /dev/null
+++ b/library/Director/Web/Table/IcingaHostAppliedServicesTable.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\IcingaConfig\AssignRenderer;
+use Icinga\Module\Director\Objects\HostApplyMatches;
+use Icinga\Module\Director\Objects\IcingaHost;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+
+class IcingaHostAppliedServicesTable extends SimpleQueryBasedTable
+{
+ protected $title;
+
+ /** @var IcingaHost */
+ protected $host;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var bool */
+ protected $readonly = false;
+
+ /** @var string|null */
+ protected $highlightedService;
+
+ private $allApplyRules;
+
+ /**
+ * @param IcingaHost $host
+ * @return static
+ */
+ public static function load(IcingaHost $host)
+ {
+ $table = (new static())->setHost($host);
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->title];
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ $this->db = $host->getDb();
+ return $this;
+ }
+
+ /**
+ * Show no related links
+ *
+ * @param bool $readonly
+ * @return $this
+ */
+ public function setReadonly($readonly = true)
+ {
+ $this->readonly = (bool) $readonly;
+
+ return $this;
+ }
+
+ public function highlightService($service)
+ {
+ $this->highlightedService = $service;
+
+ return $this;
+ }
+
+ public function renderRow($row)
+ {
+ $classes = [];
+ if ($row->blacklisted === 'y') {
+ $classes[] = 'strike-links';
+ }
+ if ($row->disabled === 'y') {
+ $classes[] = 'disabled';
+ }
+
+ $attributes = empty($classes) ? null : ['class' => $classes];
+
+ if ($this->readonly) {
+ if ($this->highlightedService === $row->name) {
+ $link = Html::tag('a', ['class' => 'icon-right-big'], $row->name);
+ } else {
+ $link = Html::tag('a', $row->name);
+ }
+ } else {
+ $applyFor = '';
+ if (! empty($row->apply_for)) {
+ $applyFor = sprintf('(apply for %s) ', $row->apply_for);
+ }
+
+ $link = Link::create(sprintf(
+ $this->translate('%s %s(%s)'),
+ $row->name,
+ $applyFor,
+ $this->renderApplyFilter($row->filter)
+ ), 'director/host/appliedservice', [
+ 'name' => $this->host->getObjectName(),
+ 'service_id' => $row->id,
+ ]);
+ }
+
+ return $this::row([$link], $attributes);
+ }
+
+ /**
+ * @param Filter $assignFilter
+ *
+ * @return string
+ */
+ protected function renderApplyFilter(Filter $assignFilter)
+ {
+ try {
+ $string = AssignRenderer::forFilter($assignFilter)->renderAssign();
+ } catch (IcingaException $e) {
+ $string = 'Error in Filter rendering: ' . $e->getMessage();
+ }
+
+ return $string;
+ }
+
+ /**
+ * @return \Icinga\Data\SimpleQuery
+ */
+ public function prepareQuery()
+ {
+ $services = [];
+ $matcher = HostApplyMatches::prepare($this->host);
+ foreach ($this->getAllApplyRules() as $rule) {
+ if ($matcher->matchesFilter($rule->filter)) {
+ $services[] = $rule;
+ }
+ }
+
+ $ds = new ArrayDatasource($services);
+ return $ds->select()->columns([
+ 'id' => 'id',
+ 'uuid' => 'uuid',
+ 'name' => 'name',
+ 'filter' => 'filter',
+ 'disabled' => 'disabled',
+ 'blacklisted' => 'blacklisted',
+ 'assign_filter' => 'assign_filter',
+ 'apply_for' => 'apply_for',
+ ]);
+ }
+
+ /***
+ * @return array
+ */
+ protected function getAllApplyRules()
+ {
+ if ($this->allApplyRules === null) {
+ $this->allApplyRules = $this->fetchAllApplyRules();
+ foreach ($this->allApplyRules as $rule) {
+ $rule->filter = Filter::fromQueryString($rule->assign_filter);
+ }
+ }
+
+ return $this->allApplyRules;
+ }
+
+ /**
+ * @return array
+ */
+ protected function fetchAllApplyRules()
+ {
+ $db = $this->db;
+ $hostId = $this->host->get('id');
+ $query = $db->select()->from(
+ ['s' => 'icinga_service'],
+ [
+ 'id' => 's.id',
+ 'uuid' => 's.uuid',
+ 'name' => 's.object_name',
+ 'assign_filter' => 's.assign_filter',
+ 'apply_for' => 's.apply_for',
+ 'disabled' => 's.disabled',
+ 'blacklisted' => $hostId ? "CASE WHEN hsb.service_id IS NULL THEN 'n' ELSE 'y' END" : "('n')",
+ ]
+ )->where('object_type = ? AND assign_filter IS NOT NULL', 'apply')
+ ->order('s.object_name');
+ if ($hostId) {
+ $query->joinLeft(
+ ['hsb' => 'icinga_host_service_blacklist'],
+ $db->quoteInto('s.id = hsb.service_id AND hsb.host_id = ?', $hostId),
+ []
+ );
+ }
+
+ return $db->fetchAll($query);
+ }
+}
diff --git a/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php b/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php
new file mode 100644
index 0000000..8d225bf
--- /dev/null
+++ b/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Data\SimpleQueryPaginationAdapter;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\QueryBasedTable;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\SimpleQuery;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Resolver\IcingaHostObjectResolver;
+
+class IcingaHostsMatchingFilterTable extends QueryBasedTable
+{
+ protected $searchColumns = [
+ 'object_name',
+ ];
+
+ /** @var ArrayDatasource */
+ protected $dataSource;
+
+ public static function load(Filter $filter, Db $db)
+ {
+ $table = new static();
+ $table->dataSource = new ArrayDatasource(
+ (new IcingaHostObjectResolver($db->getDbAdapter()))
+ ->fetchObjectsMatchingFilter($filter)
+ );
+
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->object_name,
+ 'director/host',
+ ['name' => $row->object_name]
+ )
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Hostname'),
+ ];
+ }
+
+ protected function getPaginationAdapter()
+ {
+ return new SimpleQueryPaginationAdapter($this->getQuery());
+ }
+
+ public function getQuery()
+ {
+ return $this->prepareQuery();
+ }
+
+ protected function fetchQueryRows()
+ {
+ return $this->dataSource->fetchAll($this->getQuery());
+ }
+
+ protected function prepareQuery()
+ {
+ return new SimpleQuery($this->dataSource, ['object_name']);
+ }
+}
diff --git a/library/Director/Web/Table/IcingaObjectDatafieldTable.php b/library/Director/Web/Table/IcingaObjectDatafieldTable.php
new file mode 100644
index 0000000..f97692e
--- /dev/null
+++ b/library/Director/Web/Table/IcingaObjectDatafieldTable.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader;
+use Icinga\Web\Url;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+
+class IcingaObjectDatafieldTable extends SimpleQueryBasedTable
+{
+ protected $object;
+
+ /** @var int */
+ protected $objectId;
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->object = $object;
+ $this->objectId = (int) $object->id;
+ return $this;
+ }
+
+ protected $searchColumns = array(
+ 'varname',
+ 'caption'
+ );
+
+ public function getColumns()
+ {
+ return array(
+ 'object_id',
+ 'var_filter',
+ 'is_required',
+ 'id',
+ 'varname',
+ 'caption',
+ 'description',
+ 'datatype',
+ 'format',
+ );
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return array(
+ 'caption' => $this->translate('Label'),
+ 'varname' => $this->translate('Field name'),
+ 'is_required' => $this->translate('Mandatory'),
+ );
+ }
+
+ public function renderRow($row)
+ {
+ $definedOnThis = (int) $row->object_id === $this->objectId;
+ if ($definedOnThis) {
+ $caption = new Link(
+ $row->caption,
+ Url::fromRequest()->with('field_id', $row->id)
+ );
+ } else {
+ $caption = $row->caption;
+ }
+
+ $row = $this::row([
+ $caption,
+ $row->varname,
+ $row->is_required
+ ]);
+
+ if (! $definedOnThis) {
+ $row->getAttributes()->add('class', 'disabled');
+ }
+
+ return $row;
+ }
+
+ public function prepareQuery()
+ {
+ $loader = new IcingaObjectFieldLoader($this->object);
+ $fields = $loader->fetchFieldDetailsForObject($this->object);
+ $ds = new ArrayDatasource($fields);
+ return $ds->select();
+ }
+}
diff --git a/library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php b/library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php
new file mode 100644
index 0000000..cd8f8b1
--- /dev/null
+++ b/library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaScheduledDowntime;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaScheduledDowntimeRangeTable extends ZfQueryBasedTable
+{
+ /** @var IcingaScheduledDowntime */
+ protected $downtime;
+
+ protected $searchColumns = [
+ 'range_key',
+ 'range_value',
+ ];
+
+ /**
+ * @param IcingaScheduledDowntime $downtime
+ * @return static
+ */
+ public static function load(IcingaScheduledDowntime $downtime)
+ {
+ $table = new static($downtime->getConnection());
+ $table->downtime = $downtime;
+ $table->getAttributes()->set('data-base-target', '_self');
+
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->range_key,
+ 'director/scheduled-downtime/ranges',
+ [
+ 'name' => $this->downtime->getObjectName(),
+ 'range' => $row->range_key,
+ 'range_type' => 'include'
+ ]
+ ),
+ $row->range_value
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Day(s)'),
+ $this->translate('Timeperiods'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['r' => 'icinga_scheduled_downtime_range'],
+ [
+ 'scheduled_downtime_id' => 'r.scheduled_downtime_id',
+ 'range_key' => 'r.range_key',
+ 'range_value' => 'r.range_value',
+ ]
+ )->where('r.scheduled_downtime_id = ?', $this->downtime->id);
+ }
+}
diff --git a/library/Director/Web/Table/IcingaServiceSetHostTable.php b/library/Director/Web/Table/IcingaServiceSetHostTable.php
new file mode 100644
index 0000000..9fc3c61
--- /dev/null
+++ b/library/Director/Web/Table/IcingaServiceSetHostTable.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaServiceSetHostTable extends ZfQueryBasedTable
+{
+ protected $set;
+
+ protected $searchColumns = array(
+ 'host',
+ );
+
+ public static function load(IcingaServiceSet $set)
+ {
+ $table = new static($set->getConnection());
+ $table->set = $set;
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->host,
+ 'director/host',
+ ['name' => $row->host]
+ )
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Hostname'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['h' => 'icinga_host'],
+ [
+ 'id' => 'h.id',
+ 'host' => 'h.object_name',
+ 'object_type' => 'h.object_type',
+ ]
+ )->joinLeft(
+ ['ssh' => 'icinga_service_set'],
+ 'ssh.host_id = h.id',
+ []
+ )->joinLeft(
+ ['ssih' => 'icinga_service_set_inheritance'],
+ 'ssih.service_set_id = ssh.id',
+ []
+ )->where(
+ 'ssih.parent_service_set_id = ?',
+ $this->set->id
+ )->order('h.object_name');
+ }
+}
diff --git a/library/Director/Web/Table/IcingaServiceSetServiceTable.php b/library/Director/Web/Table/IcingaServiceSetServiceTable.php
new file mode 100644
index 0000000..c205e66
--- /dev/null
+++ b/library/Director/Web/Table/IcingaServiceSetServiceTable.php
@@ -0,0 +1,259 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Data\Db\ServiceSetQueryBuilder;
+use Icinga\Module\Director\Db;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use Icinga\Module\Director\Forms\RemoveLinkForm;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+
+class IcingaServiceSetServiceTable extends ZfQueryBasedTable
+{
+ use TableWithBranchSupport;
+
+ /** @var IcingaServiceSet */
+ protected $set;
+
+ protected $title;
+
+ /** @var IcingaHost */
+ protected $host;
+
+ /** @var IcingaHost */
+ protected $affectedHost;
+
+ protected $searchColumns = [
+ 'service',
+ ];
+
+ /** @var bool */
+ protected $readonly = false;
+
+ /** @var string|null */
+ protected $highlightedService;
+
+ /**
+ * @param IcingaServiceSet $set
+ * @return static
+ */
+ public static function load(IcingaServiceSet $set)
+ {
+ $table = new static($set->getConnection());
+ $table->set = $set;
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ /**
+ * @param string $title
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @return $this
+ */
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @return $this
+ */
+ public function setAffectedHost(IcingaHost $host)
+ {
+ $this->affectedHost = $host;
+ return $this;
+ }
+
+ /**
+ * Show no related links
+ *
+ * @param bool $readonly
+ * @return $this
+ */
+ public function setReadonly($readonly = true)
+ {
+ $this->readonly = (bool) $readonly;
+
+ return $this;
+ }
+
+ public function highlightService($service)
+ {
+ $this->highlightedService = $service;
+
+ return $this;
+ }
+
+ /**
+ * @param $row
+ * @return BaseHtmlElement
+ */
+ protected function getServiceLink($row)
+ {
+ if ($this->readonly) {
+ if ($this->highlightedService === $row->service) {
+ return Html::tag('span', ['class' => 'ro-service icon-right-big'], $row->service);
+ }
+
+ return Html::tag('span', ['class' => 'ro-service'], $row->service);
+ }
+
+ if ($this->affectedHost) {
+ $params = [
+ 'uuid' => $this->affectedHost->getUniqueId()->toString(),
+ 'service' => $row->service,
+ 'set' => $row->service_set
+ ];
+ $url = 'director/host/servicesetservice';
+ } else {
+ $params = [
+ 'name' => $row->service,
+ 'set' => $row->service_set
+ ];
+ $url = 'director/service';
+ }
+
+ return Link::create(
+ $row->service,
+ $url,
+ $params
+ );
+ }
+
+ public function renderRow($row)
+ {
+ $tr = $this::row([
+ $this->getServiceLink($row)
+ ]);
+ $classes = $this->getRowClasses($row);
+ if ($row->disabled === 'y') {
+ $classes[] = 'disabled';
+ }
+ if ($row->blacklisted === 'y') {
+ $classes[] = 'strike-links';
+ }
+ if (! empty($classes)) {
+ $tr->getAttributes()->add('class', $classes);
+ }
+
+ return $tr;
+ }
+
+ protected function getRowClasses($row)
+ {
+ if ($row->branch_uuid !== null) {
+ return ['branch_modified'];
+ }
+ return [];
+ }
+
+ protected function getTitle()
+ {
+ return $this->title ?: $this->translate('Servicename');
+ }
+
+ protected function renderTitleColumns()
+ {
+ if (! $this->host || ! $this->affectedHost) {
+ return Html::tag('th', $this->getTitle());
+ }
+
+ if ($this->readonly) {
+ $link = $this->createFakeRemoveLinkForReadonlyView();
+ } elseif ($this->affectedHost->get('id') !== $this->host->get('id')) {
+ $link = $this->linkToHost($this->host);
+ } else {
+ $link = $this->createRemoveLinkForm();
+ }
+
+ return $this::th([$this->getTitle(), $link]);
+ }
+
+ /**
+ * @return \Zend_Db_Select
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function prepareQuery()
+ {
+ $connection = $this->connection();
+ assert($connection instanceof Db);
+ $builder = new ServiceSetQueryBuilder($connection, $this->branchUuid);
+ return $builder->selectServicesForSet($this->set)->limit(100);
+ }
+
+ protected function createFakeRemoveLinkForReadonlyView()
+ {
+ return Html::tag('span', [
+ 'class' => 'icon-paste',
+ 'style' => 'float: right; font-weight: normal',
+ ], $this->host->getObjectName());
+ }
+
+ protected function linkToHost(IcingaHost $host)
+ {
+ $hostname = $host->getObjectName();
+ return Link::create($hostname, 'director/host/services', ['name' => $hostname], [
+ 'class' => 'icon-paste',
+ 'style' => 'float: right; font-weight: normal',
+ 'data-base-target' => '_next',
+ 'title' => sprintf(
+ $this->translate('This set has been inherited from %s'),
+ $hostname
+ )
+ ]);
+ }
+
+ protected function createRemoveLinkForm()
+ {
+ $deleteLink = new RemoveLinkForm(
+ $this->translate('Remove'),
+ sprintf(
+ $this->translate('Remove "%s" from this host'),
+ $this->getTitle()
+ ),
+ Url::fromPath('director/host/services', [
+ 'name' => $this->host->getObjectName()
+ ]),
+ ['title' => $this->getTitle()]
+ );
+ $deleteLink->runOnSuccess(function () {
+ $conn = $this->set->getConnection();
+ $db = $conn->getDbAdapter();
+ $query = $db->select()->from(['ss' => 'icinga_service_set'], 'ss.id')
+ ->join(['ssih' => 'icinga_service_set_inheritance'], 'ssih.service_set_id = ss.id', [])
+ ->where('ssih.parent_service_set_id = ?', $this->set->get('id'))
+ ->where('ss.host_id = ?', $this->host->get('id'));
+ IcingaServiceSet::loadWithAutoIncId(
+ $db->fetchOne($query),
+ $conn
+ )->delete();
+ });
+ $deleteLink->handleRequest();
+ return $deleteLink;
+ }
+
+ public function removeQueryLimit()
+ {
+ $query = $this->getQuery();
+ $query->reset($query::LIMIT_OFFSET);
+ $query->reset($query::LIMIT_COUNT);
+
+ return $this;
+ }
+}
diff --git a/library/Director/Web/Table/IcingaTimePeriodRangeTable.php b/library/Director/Web/Table/IcingaTimePeriodRangeTable.php
new file mode 100644
index 0000000..5870e67
--- /dev/null
+++ b/library/Director/Web/Table/IcingaTimePeriodRangeTable.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaTimePeriod;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaTimePeriodRangeTable extends ZfQueryBasedTable
+{
+ protected $period;
+
+ protected $searchColumns = array(
+ 'range_key',
+ 'range_value',
+ );
+
+ public static function load(IcingaTimePeriod $period)
+ {
+ $table = new static($period->getConnection());
+ $table->period = $period;
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->range_key,
+ 'director/timeperiod/ranges',
+ array(
+ 'name' => $this->period->object_name,
+ 'range' => $row->range_key,
+ 'range_type' => 'include'
+ )
+ ),
+ $row->range_value
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Day(s)'),
+ $this->translate('Timeperiods'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['r' => 'icinga_timeperiod_range'],
+ [
+ 'timeperiod_id' => 'r.timeperiod_id',
+ 'range_key' => 'r.range_key',
+ 'range_value' => 'r.range_value',
+ ]
+ )->where('r.timeperiod_id = ?', $this->period->id);
+ }
+}
diff --git a/library/Director/Web/Table/ImportedrowsTable.php b/library/Director/Web/Table/ImportedrowsTable.php
new file mode 100644
index 0000000..d5c9811
--- /dev/null
+++ b/library/Director/Web/Table/ImportedrowsTable.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use ipl\Html\ValidHtml;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Objects\ImportRun;
+use Icinga\Module\Director\PlainObjectRenderer;
+
+class ImportedrowsTable extends SimpleQueryBasedTable
+{
+ protected $columns;
+
+ /** @var ImportRun */
+ protected $importRun;
+
+ protected $keyColumn;
+
+ public static function load(ImportRun $run)
+ {
+ $table = new static();
+ $table->setImportRun($run);
+ return $table;
+ }
+
+ public function setImportRun(ImportRun $run)
+ {
+ $this->importRun = $run;
+ return $this;
+ }
+
+ public function setColumns($columns)
+ {
+ $this->columns = $columns;
+ return $this;
+ }
+
+ protected function getKeyColumn()
+ {
+ if ($this->keyColumn === null) {
+ $this->keyColumn = $this->importRun->importSource()->get('key_column');
+ }
+
+ return $this->keyColumn;
+ }
+
+ public function getColumns()
+ {
+ if ($this->columns === null) {
+ $cols = $this->importRun->listColumnNames();
+
+ $keyColumn = $this->getKeyColumn();
+ if ($keyColumn !== null && ($pos = array_search($keyColumn, $cols)) !== false) {
+ unset($cols[$pos]);
+ array_unshift($cols, $keyColumn);
+ }
+ } else {
+ $cols = $this->columns;
+ }
+
+ return array_combine($cols, $cols);
+ }
+
+ public function renderRow($row)
+ {
+ // Find a better place!
+ if ($row === null) {
+ return null;
+ }
+ $tr = $this::tr();
+
+ foreach ($this->getColumnsToBeRendered() as $column) {
+ $td = $this::td();
+ if (property_exists($row, $column)) {
+ if (is_string($row->$column) || $row->$column instanceof ValidHtml) {
+ $td->setContent($row->$column);
+ } else {
+ $html = Html::tag('pre', null, PlainObjectRenderer::render($row->$column));
+ $td->setContent($html);
+ }
+ }
+ $tr->add($td);
+ }
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return $this->getColumns();
+ }
+
+ public function prepareQuery()
+ {
+ $ds = new ArrayDatasource(
+ $this->importRun->fetchRows($this->columns)
+ );
+
+ return $ds->select()->order($this->getKeyColumn());
+ }
+}
diff --git a/library/Director/Web/Table/ImportrunTable.php b/library/Director/Web/Table/ImportrunTable.php
new file mode 100644
index 0000000..e6c8a38
--- /dev/null
+++ b/library/Director/Web/Table/ImportrunTable.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\ImportSource;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class ImportrunTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ /** @var ImportSource */
+ protected $source;
+
+ protected $searchColumns = [
+ 'source_name',
+ ];
+
+ public static function load(ImportSource $source)
+ {
+ $table = new static($source->getConnection());
+ $table->source = $source;
+ return $table;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Source name'),
+ $this->translate('Timestamp'),
+ $this->translate('Imported rows'),
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->source_name,
+ 'director/importrun',
+ ['id' => $row->id]
+ ),
+ $row->start_time,
+ $row->cnt_rows
+ ]);
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ $columns = array(
+ 'id' => 'r.id',
+ 'source_id' => 's.id',
+ 'source_name' => 's.source_name',
+ 'start_time' => 'r.start_time',
+ 'rowset' => 'LOWER(HEX(rs.checksum))',
+ 'cnt_rows' => 'COUNT(rsr.row_checksum)',
+ );
+
+ if ($this->isPgsql()) {
+ $columns['rowset'] = "LOWER(ENCODE(rs.checksum, 'hex'))";
+ }
+
+ // TODO: Store row count to rowset
+ $query = $db->select()->from(
+ ['s' => 'import_source'],
+ $columns
+ )->join(
+ ['r' => 'import_run'],
+ 'r.source_id = s.id',
+ []
+ )->joinLeft(
+ ['rs' => 'imported_rowset'],
+ 'rs.checksum = r.rowset_checksum',
+ []
+ )->joinLeft(
+ ['rsr' => 'imported_rowset_row'],
+ 'rs.checksum = rsr.rowset_checksum',
+ []
+ )->group('r.id')->group('s.id')->group('rs.checksum')
+ ->order('r.start_time DESC');
+
+ if ($this->source) {
+ $query->where('r.source_id = ?', $this->source->get('id'));
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/ImportsourceHookTable.php b/library/Director/Web/Table/ImportsourceHookTable.php
new file mode 100644
index 0000000..5ddb6f3
--- /dev/null
+++ b/library/Director/Web/Table/ImportsourceHookTable.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\ValidHtml;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Import\SyncUtils;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\PlainObjectRenderer;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+
+class ImportsourceHookTable extends SimpleQueryBasedTable
+{
+ /** @var ImportSource */
+ protected $source;
+
+ protected $columnCache;
+
+ /** @var ImportSourceHook */
+ protected $sourceHook;
+
+ protected function assemble()
+ {
+ $this->getAttributes()->add('class', 'raw-data-table collapsed');
+ }
+
+ public function getColumns()
+ {
+ if ($this->columnCache === null) {
+ $this->columnCache = SyncUtils::getRootVariables(array_merge(
+ $this->sourceHook()->listColumns(),
+ $this->source->listModifierTargetProperties()
+ ));
+
+ sort($this->columnCache);
+
+ // prioritize key column
+ $keyColumn = $this->source->get('key_column');
+ if ($keyColumn !== null && ($pos = array_search($keyColumn, $this->columnCache)) !== false) {
+ unset($this->columnCache[$pos]);
+ array_unshift($this->columnCache, $keyColumn);
+ }
+ }
+
+ return $this->columnCache;
+ }
+
+ public function setImportSource(ImportSource $source)
+ {
+ $this->source = $source;
+ return $this;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return $this->getColumns();
+ }
+
+ public function renderRow($row)
+ {
+ // Find a better place!
+ if ($row === null) {
+ return null;
+ }
+ if (\is_array($row)) {
+ $row = (object) $row;
+ }
+ $tr = $this::tr();
+
+ foreach ($this->getColumnsToBeRendered() as $column) {
+ $td = $this::td();
+ if (\property_exists($row, $column)) {
+ if (\is_string($row->$column) || $row->$column instanceof ValidHtml) {
+ $td->setContent($row->$column);
+ } else {
+ $html = Html::tag('pre', null, PlainObjectRenderer::render($row->$column));
+ $td->setContent($html);
+ }
+ }
+ $tr->add($td);
+ }
+
+ return $tr;
+ }
+
+ protected function sourceHook()
+ {
+ if ($this->sourceHook === null) {
+ $this->sourceHook = ImportSourceHook::forImportSource(
+ $this->source
+ );
+ }
+
+ return $this->sourceHook;
+ }
+
+ public function prepareQuery()
+ {
+ $data = $this->sourceHook()->fetchData();
+ $this->source->applyModifiers($data);
+
+ $ds = new ArrayDatasource($data);
+ return $ds->select();
+ }
+}
diff --git a/library/Director/Web/Table/ImportsourceTable.php b/library/Director/Web/Table/ImportsourceTable.php
new file mode 100644
index 0000000..1a93ef5
--- /dev/null
+++ b/library/Director/Web/Table/ImportsourceTable.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class ImportsourceTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'source_name',
+ 'description',
+ ];
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Source name'),
+ ];
+ }
+
+ protected function assemble()
+ {
+ $this->getAttributes()->add('class', 'syncstate');
+ parent::assemble();
+ }
+
+ public function renderRow($row)
+ {
+ $caption = [Link::create(
+ $row->source_name,
+ 'director/importsource',
+ ['id' => $row->id]
+ )];
+ if ($row->description !== null) {
+ $caption[] = ': ' . $row->description;
+ }
+
+ if ($row->import_state === 'failing' && $row->last_error_message) {
+ $caption[] = ' (' . $row->last_error_message . ')';
+ }
+
+ $tr = $this::row([$caption]);
+ $tr->getAttributes()->add('class', $row->import_state);
+
+ return $tr;
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['s' => 'import_source'],
+ [
+ 'id' => 's.id',
+ 'source_name' => 's.source_name',
+ 'provider_class' => 's.provider_class',
+ 'import_state' => 's.import_state',
+ 'last_error_message' => 's.last_error_message',
+ 'description' => 's.description',
+ ]
+ )->order('source_name ASC');
+ }
+}
diff --git a/library/Director/Web/Table/JobTable.php b/library/Director/Web/Table/JobTable.php
new file mode 100644
index 0000000..81ba07b
--- /dev/null
+++ b/library/Director/Web/Table/JobTable.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class JobTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'job_name',
+ ];
+
+ protected function assemble()
+ {
+ $this->getAttributes()->add('class', 'jobs');
+ parent::assemble();
+ }
+
+ public function renderRow($row)
+ {
+ $caption = [Link::create(
+ $row->job_name,
+ 'director/job',
+ ['id' => $row->id]
+ )];
+
+ if ($row->last_attempt_succeeded === 'n' && $row->last_error_message) {
+ $caption[] = ' (' . $row->last_error_message . ')';
+ }
+
+ $tr = $this::row([$caption]);
+ $tr->getAttributes()->add('class', $this->getJobClasses($row));
+
+ return $tr;
+ }
+
+ protected function getJobClasses($row)
+ {
+ if ($row->unixts_last_attempt === null) {
+ return 'pending';
+ }
+
+ if ($row->unixts_last_attempt + $row->run_interval < time()) {
+ return 'pending';
+ }
+
+ if ($row->last_attempt_succeeded === 'y') {
+ return 'ok';
+ } elseif ($row->last_attempt_succeeded === 'n') {
+ return 'critical';
+ } else {
+ return 'unknown';
+ }
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Job name'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['j' => 'director_job'],
+ [
+ 'id' => 'j.id',
+ 'job_name' => 'j.job_name',
+ 'job_class' => 'j.job_class',
+ 'disabled' => 'j.disabled',
+ 'run_interval' => 'j.run_interval',
+ 'last_attempt_succeeded' => 'j.last_attempt_succeeded',
+ 'ts_last_attempt' => 'j.ts_last_attempt',
+ 'unixts_last_attempt' => 'UNIX_TIMESTAMP(j.ts_last_attempt)',
+ 'ts_last_error' => 'j.ts_last_error',
+ 'last_error_message' => 'j.last_error_message',
+ ]
+ )->order('job_name');
+ }
+}
diff --git a/library/Director/Web/Table/NotificationTemplateUsageTable.php b/library/Director/Web/Table/NotificationTemplateUsageTable.php
new file mode 100644
index 0000000..da411a3
--- /dev/null
+++ b/library/Director/Web/Table/NotificationTemplateUsageTable.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+class NotificationTemplateUsageTable extends TemplateUsageTable
+{
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'applyrules' => $this->translate('Apply Rules'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'applyrules' => $this->getSummaryLine('apply', 'o.host_id IS NULL'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/ObjectSetTable.php b/library/Director/Web/Table/ObjectSetTable.php
new file mode 100644
index 0000000..2773841
--- /dev/null
+++ b/library/Director/Web/Table/ObjectSetTable.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Db\DbSelectParenthesis;
+use Icinga\Module\Director\Restriction\FilterByNameRestriction;
+use ipl\Html\Html;
+use Ramsey\Uuid\Uuid;
+
+class ObjectSetTable extends ZfQueryBasedTable
+{
+ use TableWithBranchSupport;
+
+ protected $searchColumns = [
+ 'os.object_name',
+ 'os.description',
+ 'os.assign_filter',
+ 'o.object_name',
+ ];
+
+ private $type;
+
+ /** @var Auth */
+ private $auth;
+
+ public static function create($type, Db $db, Auth $auth)
+ {
+ $table = new static($db);
+ $table->type = $type;
+ $table->auth = $auth;
+ return $table;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('Name')];
+ }
+
+ public function renderRow($row)
+ {
+ $type = $this->getType();
+ $params = [
+ 'uuid' => Uuid::fromBytes(Db\DbUtil::binaryResult($row->uuid))->toString(),
+ ];
+
+ $url = Url::fromPath("director/${type}set", $params);
+
+ $classes = $this->getRowClasses($row);
+ $tr = static::tr([
+ static::td([
+ Link::create(sprintf(
+ $this->translate('%s (%d members)'),
+ $row->object_name,
+ $row->count_services
+ ), $url),
+ $row->description ? [Html::tag('br'), Html::tag('i', $row->description)] : null
+ ])
+ ]);
+ if (! empty($classes)) {
+ $tr->getAttributes()->add('class', $classes);
+ }
+
+ return $tr;
+ }
+
+ protected function getRowClasses($row)
+ {
+ if ($row->branch_uuid !== null) {
+ return ['branch_modified'];
+ }
+ return [];
+ }
+
+ protected function prepareQuery()
+ {
+ $type = $this->getType();
+
+ $table = "icinga_${type}_set";
+ $columns = [
+ 'id' => 'os.id',
+ 'uuid' => 'os.uuid',
+ 'branch_uuid' => '(NULL)',
+ 'object_name' => 'os.object_name',
+ 'object_type' => 'os.object_type',
+ 'assign_filter' => 'os.assign_filter',
+ 'description' => 'os.description',
+ 'count_services' => 'COUNT(DISTINCT o.uuid)',
+ ];
+ if ($this->branchUuid) {
+ $columns['branch_uuid'] = 'bos.branch_uuid';
+ $columns = $this->branchifyColumns($columns);
+ $this->stripSearchColumnAliases();
+ }
+
+ $query = $this->db()->select()->from(
+ ['os' => $table],
+ $columns
+ )->joinLeft(
+ ['o' => "icinga_${type}"],
+ "o.${type}_set_id = os.id",
+ []
+ );
+
+ $nameFilter = new FilterByNameRestriction(
+ $this->connection(),
+ $this->auth,
+ "${type}_set"
+ );
+ $nameFilter->applyToQuery($query, 'os');
+ /** @var Db $conn */
+ $conn = $this->connection();
+ if ($this->branchUuid) {
+ $right = clone($query);
+
+ $query->joinLeft(
+ ['bos' => "branched_$table"],
+ // TODO: PgHexFunc
+ $this->db()->quoteInto(
+ 'bos.uuid = os.uuid AND bos.branch_uuid = ?',
+ $conn->quoteBinary($this->branchUuid->getBytes())
+ ),
+ []
+ )->where("(bos.branch_deleted IS NULL OR bos.branch_deleted = 'n')");
+ $right->joinRight(
+ ['bos' => "branched_$table"],
+ 'bos.uuid = os.uuid',
+ []
+ )
+ ->where('os.uuid IS NULL')
+ ->where('bos.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes()));
+ $query->group('COALESCE(os.uuid, bos.uuid)');
+ $right->group('COALESCE(os.uuid, bos.uuid)');
+ if ($conn->isPgsql()) {
+ // This is ugly, might want to modify the query - even a subselect looks better
+ $query->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid');
+ $right->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid');
+ }
+
+ $query = $this->db()->select()->union([
+ 'l' => new DbSelectParenthesis($query),
+ 'r' => new DbSelectParenthesis($right),
+ ]);
+ $query = $this->db()->select()->from(['u' => $query]);
+ $query->order('object_name')->limit(100);
+
+ $query
+ ->group('uuid')
+ ->where('object_type = ?', 'template')
+ ->order('object_name');
+ if ($conn->isPgsql()) {
+ // BS. Drop count? Sub-select? Better query?
+ $query
+ ->group('uuid')
+ ->group('id')
+ ->group('branch_uuid')
+ ->group('object_name')
+ ->group('object_type')
+ ->group('assign_filter')
+ ->group('description')
+ ->group('count_services');
+ };
+ } else {
+ // Disabled for now, check for correctness:
+ // $query->joinLeft(
+ // ['osi' => "icinga_${type}_set_inheritance"],
+ // "osi.parent_${type}_set_id = os.id",
+ // []
+ // )->joinLeft(
+ // ['oso' => "icinga_${type}_set"],
+ // "oso.id = oso.${type}_set_id",
+ // []
+ // );
+ // 'count_hosts' => 'COUNT(DISTINCT oso.id)',
+
+ $query
+ ->group('os.uuid')
+ ->where('os.object_type = ?', 'template')
+ ->order('os.object_name');
+ if ($conn->isPgsql()) {
+ // BS. Drop count? Sub-select? Better query?
+ $query
+ ->group('os.uuid')
+ ->group('os.id')
+ ->group('os.object_name')
+ ->group('os.object_type')
+ ->group('os.assign_filter')
+ ->group('os.description');
+ };
+ }
+
+ return $query;
+ }
+
+ /**
+ * @return Db
+ */
+ public function connection()
+ {
+ return parent::connection();
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTable.php b/library/Director/Web/Table/ObjectsTable.php
new file mode 100644
index 0000000..792cb6d
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTable.php
@@ -0,0 +1,315 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\DbSelectParenthesis;
+use Icinga\Module\Director\Db\IcingaObjectFilterHelper;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Restriction\FilterByNameRestriction;
+use Icinga\Module\Director\Restriction\HostgroupRestriction;
+use Icinga\Module\Director\Restriction\ObjectRestriction;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+use Ramsey\Uuid\Uuid;
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTable extends ZfQueryBasedTable
+{
+ use TableWithBranchSupport;
+
+ /** @var ObjectRestriction[] */
+ protected $objectRestrictions;
+
+ protected $columns = [
+ 'object_name' => 'o.object_name',
+ 'object_type' => 'o.object_type',
+ 'disabled' => 'o.disabled',
+ 'uuid' => 'o.uuid',
+ ];
+
+ protected $searchColumns = ['o.object_name'];
+
+ protected $showColumns = ['object_name' => 'Name'];
+
+ protected $filterObjectType = 'object';
+
+ protected $type;
+
+ protected $baseObjectUrl;
+
+ /** @var IcingaObject */
+ protected $dummyObject;
+
+ protected $leftSubQuery;
+
+ protected $rightSubQuery;
+
+ /** @var Auth */
+ private $auth;
+
+ /**
+ * @param $type
+ * @param Db $db
+ * @return static
+ */
+ public static function create($type, Db $db)
+ {
+ $class = __NAMESPACE__ . '\\ObjectsTable' . ucfirst($type);
+ if (! class_exists($class)) {
+ $class = __CLASS__;
+ }
+
+ /** @var static $table */
+ $table = new $class($db);
+ $table->type = $type;
+ return $table;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * @param string $url
+ * @return $this
+ */
+ public function setBaseObjectUrl($url)
+ {
+ $this->baseObjectUrl = $url;
+
+ return $this;
+ }
+
+ /**
+ * @return Auth
+ */
+ public function getAuth()
+ {
+ return $this->auth;
+ }
+
+ public function setAuth(Auth $auth)
+ {
+ $this->auth = $auth;
+ return $this;
+ }
+
+ public function filterObjectType($type)
+ {
+ $this->filterObjectType = $type;
+ return $this;
+ }
+
+ public function addObjectRestriction(ObjectRestriction $restriction)
+ {
+ $this->objectRestrictions[$restriction->getName()] = $restriction;
+ return $this;
+ }
+
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return $this->showColumns;
+ }
+
+ public function filterTemplate(
+ IcingaObject $template,
+ $inheritance = Db\IcingaObjectFilterHelper::INHERIT_DIRECT
+ ) {
+ IcingaObjectFilterHelper::filterByTemplate(
+ $this->getQuery(),
+ $template,
+ 'o',
+ $inheritance
+ );
+
+ return $this;
+ }
+
+ protected function getMainLinkLabel($row)
+ {
+ return $row->object_name;
+ }
+
+ protected function renderObjectNameColumn($row)
+ {
+ $type = $this->baseObjectUrl;
+ $url = Url::fromPath("director/${type}", [
+ 'uuid' => Uuid::fromBytes($row->uuid)->toString()
+ ]);
+
+ return static::td(Link::create($this->getMainLinkLabel($row), $url));
+ }
+
+ protected function renderExtraColumns($row)
+ {
+ $columns = $this->getColumnsToBeRendered();
+ unset($columns['object_name']);
+ $cols = [];
+ foreach ($columns as $key => & $label) {
+ $cols[] = static::td($row->$key);
+ }
+
+ return $cols;
+ }
+
+ public function renderRow($row)
+ {
+ if (isset($row->uuid) && is_resource($row->uuid)) {
+ $row->uuid = stream_get_contents($row->uuid);
+ }
+ $tr = static::tr([
+ $this->renderObjectNameColumn($row),
+ $this->renderExtraColumns($row)
+ ]);
+
+ $classes = $this->getRowClasses($row);
+ if ($row->disabled === 'y') {
+ $classes[] = 'disabled';
+ }
+ if (! empty($classes)) {
+ $tr->getAttributes()->add('class', $classes);
+ }
+
+ return $tr;
+ }
+
+ protected function getRowClasses($row)
+ {
+ // TODO: remove isset, to figure out where it is missing
+ if (isset($row->branch_uuid) && $row->branch_uuid !== null) {
+ return ['branch_modified'];
+ }
+ return [];
+ }
+
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ if ($right) {
+ $right->where(
+ 'bo.object_type = ?',
+ $this->filterObjectType
+ );
+ }
+ return $query->where(
+ 'o.object_type = ?',
+ $this->filterObjectType
+ );
+ }
+
+ protected function applyRestrictions(ZfSelect $query)
+ {
+ foreach ($this->getRestrictions() as $restriction) {
+ $restriction->applyToQuery($query);
+ }
+
+ return $query;
+ }
+
+ protected function getRestrictions()
+ {
+ if ($this->objectRestrictions === null) {
+ $this->objectRestrictions = $this->loadRestrictions();
+ }
+
+ return $this->objectRestrictions;
+ }
+
+ protected function loadRestrictions()
+ {
+ /** @var Db $db */
+ $db = $this->connection();
+ $auth = $this->getAuth();
+
+ return [
+ new HostgroupRestriction($db, $auth),
+ new FilterByNameRestriction($db, $auth, $this->getDummyObject()->getShortTableName())
+ ];
+ }
+
+ /**
+ * @return IcingaObject
+ */
+ protected function getDummyObject()
+ {
+ if ($this->dummyObject === null) {
+ $type = $this->getType();
+ $this->dummyObject = IcingaObject::createByType($type);
+ }
+ return $this->dummyObject;
+ }
+
+ protected function prepareQuery()
+ {
+ $table = $this->getDummyObject()->getTableName();
+ if ($this->branchUuid) {
+ $this->columns['branch_uuid'] = 'bo.branch_uuid';
+ }
+
+ $columns = $this->getColumns();
+ if ($this->branchUuid) {
+ $columns = $this->branchifyColumns($columns);
+ $this->stripSearchColumnAliases();
+ }
+ $query = $this->db()->select()->from(['o' => $table], $columns);
+
+ if ($this->branchUuid) {
+ $right = clone($query);
+ // Hint: Right part has only those with object = null
+ // This means that restrictions on $right would hide all
+ // new rows. Dedicated restriction logic for the branch-only
+ // part of thw union are not required, we assume that restrictions
+ // for new objects have been checked once they have been created
+ $query = $this->applyRestrictions($query);
+ /** @var Db $conn */
+ $conn = $this->connection();
+ $query->joinLeft(
+ ['bo' => "branched_$table"],
+ // TODO: PgHexFunc
+ $this->db()->quoteInto(
+ 'bo.uuid = o.uuid AND bo.branch_uuid = ?',
+ $conn->quoteBinary($this->branchUuid->getBytes())
+ ),
+ []
+ )->where("(bo.branch_deleted IS NULL OR bo.branch_deleted = 'n')");
+ $this->applyObjectTypeFilter($query, $right);
+ $right->joinRight(
+ ['bo' => "branched_$table"],
+ 'bo.uuid = o.uuid',
+ []
+ )
+ ->where('o.uuid IS NULL')
+ ->where('bo.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes()));
+ $this->leftSubQuery = $query;
+ $this->rightSubQuery = $right;
+ $query = $this->db()->select()->union([
+ 'l' => new DbSelectParenthesis($query),
+ 'r' => new DbSelectParenthesis($right),
+ ]);
+ $query = $this->db()->select()->from(['u' => $query]);
+ $query->order('object_name')->limit(100);
+ } else {
+ $this->applyObjectTypeFilter($query);
+ $query->order('o.object_name')->limit(100);
+ }
+
+ return $query;
+ }
+
+ public function removeQueryLimit()
+ {
+ $query = $this->getQuery();
+ $query->reset($query::LIMIT_OFFSET);
+ $query->reset($query::LIMIT_COUNT);
+
+ return $this;
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableApiUser.php b/library/Director/Web/Table/ObjectsTableApiUser.php
new file mode 100644
index 0000000..2287c2f
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableApiUser.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableApiUser extends ObjectsTable
+{
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query->where("o.object_type IN ('object', 'external_object')");
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableCommand.php b/library/Director/Web/Table/ObjectsTableCommand.php
new file mode 100644
index 0000000..ebd89da
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableCommand.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableCommand extends ObjectsTable implements FilterableByUsage
+{
+ // TODO: Notifications separately?
+ protected $searchColumns = [
+ 'o.object_name',
+ 'o.command',
+ ];
+
+ protected $columns = [
+ 'uuid' => 'o.uuid',
+ 'object_name' => 'o.object_name',
+ 'object_type' => 'o.object_type',
+ 'disabled' => 'o.disabled',
+ 'command' => 'o.command',
+ ];
+
+ protected $showColumns = [
+ 'object_name' => 'Command',
+ 'command' => 'Command line'
+ ];
+
+ private $objectType;
+
+ public function setType($type)
+ {
+ $this->getQuery()->where('object_type = ?', $type);
+
+ return $this;
+ }
+
+ public function showOnlyUsed()
+ {
+ $this->getQuery()->where(
+ '('
+ . 'EXISTS (SELECT check_command_id FROM icinga_host WHERE check_command_id = o.id)'
+ . ' OR EXISTS (SELECT check_command_id FROM icinga_service WHERE check_command_id = o.id)'
+ . ' OR EXISTS (SELECT event_command_id FROM icinga_host WHERE event_command_id = o.id)'
+ . ' OR EXISTS (SELECT event_command_id FROM icinga_service WHERE event_command_id = o.id)'
+ . ' OR EXISTS (SELECT command_id FROM icinga_notification WHERE command_id = o.id)'
+ . ')'
+ );
+ }
+
+ public function showOnlyUnUsed()
+ {
+ $this->getQuery()->where(
+ '('
+ . 'NOT EXISTS (SELECT check_command_id FROM icinga_host WHERE check_command_id = o.id)'
+ . ' AND NOT EXISTS (SELECT check_command_id FROM icinga_service WHERE check_command_id = o.id)'
+ . ' AND NOT EXISTS (SELECT event_command_id FROM icinga_host WHERE event_command_id = o.id)'
+ . ' AND NOT EXISTS (SELECT event_command_id FROM icinga_service WHERE event_command_id = o.id)'
+ . ' AND NOT EXISTS (SELECT command_id FROM icinga_notification WHERE command_id = o.id)'
+ . ')'
+ );
+ }
+
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableEndpoint.php b/library/Director/Web/Table/ObjectsTableEndpoint.php
new file mode 100644
index 0000000..f73b38b
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableEndpoint.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Icon;
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableEndpoint extends ObjectsTable
+{
+ protected $searchColumns = [
+ 'o.object_name',
+ ];
+
+ protected $deploymentEndpoint;
+
+ public function getColumnsToBeRendered()
+ {
+ return array(
+ 'object_name' => $this->translate('Endpoint'),
+ 'host' => $this->translate('Host'),
+ 'zone' => $this->translate('Zone'),
+ 'object_type' => $this->translate('Type'),
+ );
+ }
+
+ public function getColumns()
+ {
+ return [
+ 'uuid' => 'o.uuid',
+ 'object_name' => 'o.object_name',
+ 'object_type' => 'o.object_type',
+ 'disabled' => 'o.disabled',
+ 'host' => "(CASE WHEN o.host IS NULL THEN NULL ELSE"
+ . " CONCAT(o.host || ':' || COALESCE(o.port, 5665)) END)",
+ 'zone' => 'z.object_name',
+ ];
+ }
+
+ protected function getMainLinkLabel($row)
+ {
+ if ($row->object_name === $this->deploymentEndpoint) {
+ return [
+ $row->object_name,
+ ' ',
+ Icon::create('upload', [
+ 'title' => $this->translate(
+ 'This is your Config master and will receive our Deployments'
+ )
+ ])
+ ];
+ } else {
+ return $row->object_name;
+ }
+ }
+
+ public function getRowClasses($row)
+ {
+ if ($row->object_name === $this->deploymentEndpoint) {
+ return array_merge(array('deployment-endpoint'), parent::getRowClasses($row));
+ } else {
+ return null;
+ }
+ }
+
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query->where("o.object_type IN ('object', 'external_object')");
+ }
+
+ public function prepareQuery()
+ {
+ if ($this->deploymentEndpoint === null) {
+ /** @var \Icinga\Module\Director\Db $c */
+ $c = $this->connection();
+ if ($c->hasDeploymentEndpoint()) {
+ $this->deploymentEndpoint = $c->getDeploymentEndpointName();
+ }
+ }
+
+ return parent::prepareQuery()->joinLeft(
+ ['z' => 'icinga_zone'],
+ 'o.zone_id = z.id',
+ []
+ );
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableHost.php b/library/Director/Web/Table/ObjectsTableHost.php
new file mode 100644
index 0000000..5128e04
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableHost.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Table\Extension\MultiSelect;
+
+class ObjectsTableHost extends ObjectsTable
+{
+ use MultiSelect;
+
+ protected $type = 'host';
+
+ protected $searchColumns = [
+ 'o.object_name',
+ 'o.display_name',
+ 'o.address',
+ ];
+
+ protected $columns = [
+ 'object_name' => 'o.object_name',
+ 'display_name' => 'o.display_name',
+ 'address' => 'o.address',
+ 'disabled' => 'o.disabled',
+ 'uuid' => 'o.uuid',
+ ];
+
+ protected $showColumns = [
+ 'object_name' => 'Hostname',
+ 'address' => 'Address'
+ ];
+
+ public function assemble()
+ {
+ $this->enableMultiSelect(
+ 'director/hosts/edit',
+ 'director/hosts',
+ ['uuid']
+ );
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableHostTemplateChoice.php b/library/Director/Web/Table/ObjectsTableHostTemplateChoice.php
new file mode 100644
index 0000000..929e050
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableHostTemplateChoice.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableHostTemplateChoice extends ObjectsTable
+{
+ protected $columns = [
+ 'object_name' => 'o.object_name',
+ 'templates' => 'GROUP_CONCAT(t.object_name)'
+ ];
+
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query;
+ }
+
+ protected function prepareQuery()
+ {
+ return parent::prepareQuery()->joinLeft(
+ ['t' => 'icinga_host'],
+ 't.template_choice_id = o.id',
+ []
+ )->group('o.id');
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableService.php b/library/Director/Web/Table/ObjectsTableService.php
new file mode 100644
index 0000000..2d4ad41
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableService.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\Objects\IcingaHost;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Table\Extension\MultiSelect;
+use gipfl\IcingaWeb2\Link;
+use Ramsey\Uuid\Uuid;
+
+class ObjectsTableService extends ObjectsTable
+{
+ use MultiSelect;
+
+ /** @var IcingaHost */
+ protected $host;
+
+ protected $type = 'service';
+
+ protected $title;
+
+ /** @var IcingaHost */
+ protected $inheritedBy;
+
+ /** @var bool */
+ protected $readonly = false;
+
+ /** @var string|null */
+ protected $highlightedService;
+
+ protected $columns = [
+ 'object_name' => 'o.object_name',
+ 'disabled' => 'o.disabled',
+ 'host' => 'h.object_name',
+ 'host_id' => 'h.id',
+ 'host_object_type' => 'h.object_type',
+ 'host_disabled' => 'h.disabled',
+ 'id' => 'o.id',
+ 'uuid' => 'o.uuid',
+ 'blacklisted' => "CASE WHEN hsb.service_id IS NULL THEN 'n' ELSE 'y' END",
+ ];
+
+ protected $searchColumns = [
+ 'o.object_name',
+ 'h.object_name'
+ ];
+
+ public function assemble()
+ {
+ $this->enableMultiSelect(
+ 'director/services/edit',
+ 'director/services',
+ ['uuid']
+ );
+ }
+
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ $this->getAttributes()->set('data-base-target', '_self');
+ return $this;
+ }
+
+ public function setInheritedBy(IcingaHost $host)
+ {
+ $this->inheritedBy = $host;
+ return $this;
+ }
+
+ /**
+ * Show no related links
+ *
+ * @param bool $readonly
+ * @return $this
+ */
+ public function setReadonly($readonly = true)
+ {
+ $this->readonly = (bool) $readonly;
+
+ return $this;
+ }
+
+ public function highlightService($service)
+ {
+ $this->highlightedService = $service;
+
+ return $this;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ if ($this->title) {
+ return [$this->title];
+ }
+ if ($this->host) {
+ return [$this->translate('Servicename')];
+ }
+ return [
+ 'host' => $this->translate('Host'),
+ 'object_name' => $this->translate('Service Name'),
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ $caption = $row->host === null
+ ? Html::tag('span', ['class' => 'error'], '- none -')
+ : $row->host;
+
+ $hostField = static::td($caption);
+ if ($row->host === null) {
+ $hostField->getAttributes()->add('class', 'error');
+ }
+ if ($this->host) {
+ $tr = static::tr([
+ static::td($this->getServiceLink($row))
+ ]);
+ } else {
+ $tr = static::tr([
+ $hostField,
+ static::td($this->getServiceLink($row))
+ ]);
+ }
+
+ $attributes = $tr->getAttributes();
+ $classes = $this->getRowClasses($row);
+ if ($row->host_disabled === 'y' || $row->disabled === 'y') {
+ $classes[] = 'disabled';
+ }
+ if ($row->blacklisted === 'y') {
+ $classes[] = 'strike-links';
+ }
+ $attributes->add('class', $classes);
+
+ return $tr;
+ }
+
+ protected function getInheritedServiceLink($row, $target)
+ {
+ $params = [
+ 'name' => $target->object_name,
+ 'service' => $row->object_name,
+ 'inheritedFrom' => $row->host,
+ ];
+
+ return Link::create(
+ $row->object_name,
+ 'director/host/inheritedservice',
+ $params
+ );
+ }
+
+ protected function getServiceLink($row)
+ {
+ if ($this->readonly) {
+ if ($this->highlightedService === $row->object_name) {
+ return Html::tag('span', ['class' => 'icon-right-big'], $row->object_name);
+ } else {
+ return $row->object_name;
+ }
+ }
+
+ $params = [
+ 'uuid' => Uuid::fromBytes(DbUtil::binaryResult($row->uuid))->toString(),
+ ];
+ if ($row->host !== null) {
+ $params['host'] = $row->host;
+ }
+ if ($target = $this->inheritedBy) {
+ return $this->getInheritedServiceLink($row, $target);
+ }
+
+ return Link::create(
+ $row->object_name,
+ 'director/service/edit',
+ $params
+ );
+ }
+
+ public function prepareQuery()
+ {
+ $query = parent::prepareQuery();
+ if ($this->branchUuid) {
+ $queries = [$this->leftSubQuery, $this->rightSubQuery];
+ } else {
+ $queries = [$query];
+ }
+
+ foreach ($queries as $subQuery) {
+ $subQuery->joinLeft(
+ ['h' => 'icinga_host'],
+ 'o.host_id = h.id',
+ []
+ )->joinLeft(
+ ['hsb' => 'icinga_host_service_blacklist'],
+ 'hsb.service_id = o.id AND hsb.host_id = o.host_id',
+ []
+ )->where('o.service_set_id IS NULL')
+ ->order('o.object_name')->order('h.object_name');
+
+ if ($this->host) {
+ if ($this->branchUuid) {
+ $subQuery->where('COALESCE(h.object_name, bo.host) = ?', $this->host->getObjectName());
+ } else {
+ $subQuery->where('h.id = ?', $this->host->get('id'));
+ }
+ }
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableZone.php b/library/Director/Web/Table/ObjectsTableZone.php
new file mode 100644
index 0000000..602cf0a
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableZone.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableZone extends ObjectsTable
+{
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/PropertymodifierTable.php b/library/Director/Web/Table/PropertymodifierTable.php
new file mode 100644
index 0000000..bf9e4a3
--- /dev/null
+++ b/library/Director/Web/Table/PropertymodifierTable.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Error;
+use Exception;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\ImportSource;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\Extension\ZfSortablePriority;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+
+class PropertymodifierTable extends ZfQueryBasedTable
+{
+ use ZfSortablePriority;
+
+ protected $searchColumns = [
+ 'property_name',
+ 'target_property',
+ ];
+
+ /** @var ImportSource */
+ protected $source;
+
+ /** @var Url */
+ protected $url;
+
+ protected $keyColumn = 'id';
+
+ protected $priorityColumn = 'priority';
+
+ protected $readOnly = false;
+
+ public static function load(ImportSource $source, Url $url)
+ {
+ $table = new static($source->getConnection());
+ $table->source = $source;
+ $table->url = $url;
+ return $table;
+ }
+
+ public function setReadOnly($readOnly = true)
+ {
+ $this->readOnly = $readOnly;
+ return $this;
+ }
+
+ public function render()
+ {
+ if ($this->readOnly) {
+ return parent::render();
+ }
+ return $this->renderWithSortableForm();
+ }
+
+ protected function assemble()
+ {
+ $this->getAttributes()->set('data-base-target', '_self');
+ }
+
+ public function getColumns()
+ {
+ return array(
+ 'id' => 'm.id',
+ 'source_id' => 'm.source_id',
+ 'property_name' => 'm.property_name',
+ 'target_property' => 'm.target_property',
+ 'description' => 'm.description',
+ 'provider_class' => 'm.provider_class',
+ 'priority' => 'm.priority',
+ );
+ }
+
+ public function renderRow($row)
+ {
+ $caption = $row->property_name;
+ if ($row->target_property !== null) {
+ $caption .= ' -> ' . $row->target_property;
+ }
+ if ($row->description === null) {
+ $class = $row->provider_class;
+ try {
+ /** @var ImportSourceHook $hook */
+ $hook = new $class;
+ $caption .= ': ' . $hook->getName();
+ } catch (Exception $e) {
+ $caption = $this->createErrorCaption($caption, $e);
+ } catch (Error $e) {
+ $caption = $this->createErrorCaption($caption, $e);
+ }
+ } else {
+ $caption .= ': ' . $row->description;
+ }
+
+ $renderedRow = $this::row([
+ Link::create($caption, 'director/importsource/editmodifier', [
+ 'id' => $row->id,
+ 'source_id' => $row->source_id,
+ ]),
+ ]);
+ if ($this->readOnly) {
+ return $renderedRow;
+ }
+
+ return $this->addSortPriorityButtons(
+ $renderedRow,
+ $row
+ );
+ }
+
+ /**
+ * @param $caption
+ * @param Exception|Error $e
+ * @return array
+ */
+ protected function createErrorCaption($caption, $e)
+ {
+ return [
+ $caption,
+ ': ',
+ $this::tag('span', ['class' => 'error'], $e->getMessage())
+ ];
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ if ($this->readOnly) {
+ return [$this->translate('Property')];
+ }
+ return [
+ $this->translate('Property'),
+ $this->getSortPriorityTitle()
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['m' => 'import_row_modifier'],
+ $this->getColumns()
+ )->where('m.source_id = ?', $this->source->get('id'))
+ ->order('priority');
+ }
+}
diff --git a/library/Director/Web/Table/QuickTable.php b/library/Director/Web/Table/QuickTable.php
new file mode 100644
index 0000000..ff3edcc
--- /dev/null
+++ b/library/Director/Web/Table/QuickTable.php
@@ -0,0 +1,547 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Filter\FilterNot;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Selectable;
+use Icinga\Data\Paginatable;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\PlainObjectRenderer;
+use Icinga\Web\Request;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Web\View;
+use Icinga\Web\Widget;
+use Icinga\Web\Widget\Paginator;
+use ipl\Html\ValidHtml;
+use stdClass;
+use Zend_Db_Select as ZfDbSelect;
+
+abstract class QuickTable implements Paginatable, ValidHtml
+{
+ protected $view;
+
+ /** @var Db */
+ protected $connection;
+
+ protected $limit;
+
+ protected $offset;
+
+ /** @var Filter */
+ protected $filter;
+
+ protected $enforcedFilters = array();
+
+ protected $searchColumns = array();
+
+ protected function getRowClasses($row)
+ {
+ return array();
+ }
+
+ protected function getRowClassesString($row)
+ {
+ return $this->createClassAttribute($this->getRowClasses($row));
+ }
+
+ protected function createClassAttribute($classes)
+ {
+ $str = $this->createClassesString($classes);
+ if (strlen($str) > 0) {
+ return ' class="' . $str . '"';
+ } else {
+ return '';
+ }
+ }
+
+ private function createClassesString($classes)
+ {
+ if (is_string($classes)) {
+ $classes = array($classes);
+ }
+
+ if (empty($classes)) {
+ return '';
+ } else {
+ return implode(' ', $classes);
+ }
+ }
+
+ protected function getMultiselectProperties()
+ {
+ /* array(
+ * 'url' => 'director/hosts/edit',
+ * 'sourceUrl' => 'director/hosts',
+ * 'keys' => 'name'
+ * ) */
+
+ return array();
+ }
+
+ protected function renderMultiselectAttributes()
+ {
+ $props = $this->getMultiselectProperties();
+
+ if (empty($props)) {
+ return '';
+ }
+
+ $prefix = 'data-icinga-multiselect-';
+ $view = $this->view();
+ $parts = array();
+ $multi = array(
+ 'url' => $view->href($props['url']),
+ 'controllers' => $view->href($props['sourceUrl']),
+ 'data' => implode(',', $props['keys']),
+ );
+
+ foreach ($multi as $k => $v) {
+ $parts[] = $prefix . $k . '="' . $v . '"';
+ }
+
+ return ' ' . implode(' ', $parts);
+ }
+
+ protected function renderRow($row)
+ {
+ $htm = " <tr" . $this->getRowClassesString($row) . ">\n";
+ $firstCol = true;
+
+ foreach ($this->getTitles() as $key => $title) {
+ // Support missing columns
+ if (property_exists($row, $key)) {
+ $val = $row->$key;
+ } else {
+ $val = null;
+ }
+
+ $value = null;
+
+ if ($firstCol) {
+ if ($val !== null && $url = $this->getActionUrl($row)) {
+ $value = $this->view()->qlink($val, $this->getActionUrl($row));
+ }
+ $firstCol = false;
+ }
+
+ if ($value === null) {
+ if ($val === null) {
+ $value = '-';
+ } elseif (is_array($val) || $val instanceof stdClass || is_bool($val)) {
+ $value = '<pre>'
+ . $this->view()->escape(PlainObjectRenderer::render($val))
+ . '</pre>';
+ } else {
+ $value = $this->view()->escape($val);
+ }
+ }
+
+ $htm .= ' <td>' . $value . "</td>\n";
+ }
+
+ if ($this->hasAdditionalActions()) {
+ $htm .= ' <td class="actions">' . $this->renderAdditionalActions($row) . "</td>\n";
+ }
+
+ return $htm . " </tr>\n";
+ }
+
+ abstract protected function getTitles();
+
+ protected function getActionUrl($row)
+ {
+ return false;
+ }
+
+ public function setConnection(Selectable $connection)
+ {
+ $this->connection = $connection;
+ return $this;
+ }
+
+ /**
+ * @return ZfDbSelect
+ */
+ abstract protected function getBaseQuery();
+
+ public function fetchData()
+ {
+ $db = $this->db();
+ $query = $this->getBaseQuery()->columns($this->getColumns());
+
+ if ($this->hasLimit() || $this->hasOffset()) {
+ $query->limit($this->getLimit(), $this->getOffset());
+ }
+
+ $this->applyFiltersToQuery($query);
+
+ return $db->fetchAll($query);
+ }
+
+ protected function applyFiltersToQuery(ZfDbSelect $query)
+ {
+ $filter = null;
+ $enforced = $this->enforcedFilters;
+ if ($this->filter && ! $this->filter->isEmpty()) {
+ $filter = $this->filter;
+ } elseif (! empty($enforced)) {
+ $filter = array_shift($enforced);
+ }
+ if ($filter) {
+ foreach ($enforced as $f) {
+ $filter = $filter->andFilter($f);
+ }
+ $query->where($this->renderFilter($filter));
+ }
+
+ return $query;
+ }
+
+ public function getPaginator()
+ {
+ $paginator = new Paginator();
+ $paginator->setQuery($this);
+
+ return $paginator;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ $db = $this->db();
+ $query = clone($this->getBaseQuery());
+ $query->reset('order')->columns(array('COUNT(*)'));
+ $this->applyFiltersToQuery($query);
+
+ return $db->fetchOne($query);
+ }
+
+ public function limit($count = null, $offset = null)
+ {
+ $this->limit = $count;
+ $this->offset = $offset;
+
+ return $this;
+ }
+
+ public function hasLimit()
+ {
+ return $this->limit !== null;
+ }
+
+ public function getLimit()
+ {
+ return $this->limit;
+ }
+
+ public function hasOffset()
+ {
+ return $this->offset !== null;
+ }
+
+ public function getOffset()
+ {
+ return $this->offset;
+ }
+
+ public function hasAdditionalActions()
+ {
+ return method_exists($this, 'renderAdditionalActions');
+ }
+
+ /** @return Db */
+ protected function connection()
+ {
+ // TODO: Fail if missing? Require connection in constructor?
+ return $this->connection;
+ }
+
+ protected function db()
+ {
+ return $this->connection()->getDbAdapter();
+ }
+
+ protected function renderTitles($row)
+ {
+ $view = $this->view();
+ $htm = "<thead>\n <tr>\n";
+
+ foreach ($row as $title) {
+ $htm .= ' <th>' . $view->escape($title) . "</th>\n";
+ }
+
+ if ($this->hasAdditionalActions()) {
+ $htm .= ' <th class="actions">' . $view->translate('Actions') . "</th>\n";
+ }
+
+ return $htm . " </tr>\n</thead>\n";
+ }
+
+ protected function url($url, $params)
+ {
+ return Url::fromPath($url, $params);
+ }
+
+ protected function listTableClasses()
+ {
+ $classes = array('simple', 'common-table', 'table-row-selectable');
+ $multi = $this->getMultiselectProperties();
+ if (! empty($multi)) {
+ $classes[] = 'multiselect';
+ }
+
+ return $classes;
+ }
+
+ public function render()
+ {
+ $data = $this->fetchData();
+
+ $htm = '<table'
+ . $this->createClassAttribute($this->listTableClasses())
+ . $this->renderMultiselectAttributes()
+ . '>' . "\n"
+ . $this->renderTitles($this->getTitles())
+ . $this->beginTableBody();
+ foreach ($data as $row) {
+ $htm .= $this->renderRow($row);
+ }
+ return $htm . $this->endTableBody() . $this->endTable();
+ }
+
+ protected function beginTableBody()
+ {
+ return "<tbody>\n";
+ }
+
+ protected function endTableBody()
+ {
+ return "</tbody>\n";
+ }
+
+ protected function endTable()
+ {
+ return "</table>\n";
+ }
+
+ /**
+ * @return View
+ */
+ protected function view()
+ {
+ if ($this->view === null) {
+ $this->view = Icinga::app()->getViewRenderer()->view;
+ }
+ return $this->view;
+ }
+
+
+ public function setView($view)
+ {
+ $this->view = $view;
+ }
+
+ public function __toString()
+ {
+ return $this->render();
+ }
+
+ protected function getSearchColumns()
+ {
+ return $this->searchColumns;
+ }
+
+ abstract public function getColumns();
+
+ public function getFilterColumns()
+ {
+ $keys = array_keys($this->getColumns());
+ return array_combine($keys, $keys);
+ }
+
+ public function setFilter($filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ public function enforceFilter($filter, $expression = null)
+ {
+ if (! $filter instanceof Filter) {
+ $filter = Filter::where($filter, $expression);
+ }
+ $this->enforcedFilters[] = $filter;
+ return $this;
+ }
+
+ public function getFilterEditor(Request $request)
+ {
+ $filterEditor = Widget::create('filterEditor')
+ ->setColumns(array_keys($this->getColumns()))
+ ->setSearchColumns($this->getSearchColumns())
+ ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', '_dev')
+ ->ignoreParams('page')
+ ->handleRequest($request);
+
+ $filter = $filterEditor->getFilter();
+ $this->setFilter($filter);
+
+ return $filterEditor;
+ }
+
+ protected function mapFilterColumn($col)
+ {
+ $cols = $this->getColumns();
+ return $cols[$col];
+ }
+
+ protected function renderFilter(Filter $filter, $level = 0)
+ {
+ $str = '';
+ if ($filter instanceof FilterChain) {
+ if ($filter instanceof FilterAnd) {
+ $op = ' AND ';
+ } elseif ($filter instanceof FilterOr) {
+ $op = ' OR ';
+ } elseif ($filter instanceof FilterNot) {
+ $op = ' AND ';
+ $str .= ' NOT ';
+ } else {
+ throw new QueryException(
+ 'Cannot render filter: %s',
+ $filter
+ );
+ }
+ $parts = array();
+ if (! $filter->isEmpty()) {
+ foreach ($filter->filters() as $f) {
+ $filterPart = $this->renderFilter($f, $level + 1);
+ if ($filterPart !== '') {
+ $parts[] = $filterPart;
+ }
+ }
+ if (! empty($parts)) {
+ if ($level > 0) {
+ $str .= ' (' . implode($op, $parts) . ') ';
+ } else {
+ $str .= implode($op, $parts);
+ }
+ }
+ }
+ } else {
+ /** @var FilterExpression $filter */
+ $str .= $this->whereToSql(
+ $this->mapFilterColumn($filter->getColumn()),
+ $filter->getSign(),
+ $filter->getExpression()
+ );
+ }
+
+ return $str;
+ }
+
+ protected function escapeForSql($value)
+ {
+ // bindParam? bindValue?
+ if (is_array($value)) {
+ $ret = array();
+ foreach ($value as $val) {
+ $ret[] = $this->escapeForSql($val);
+ }
+ return implode(', ', $ret);
+ } else {
+ //if (preg_match('/^\d+$/', $value)) {
+ // return $value;
+ //} else {
+ return $this->db()->quote($value);
+ //}
+ }
+ }
+
+ protected function escapeWildcards($value)
+ {
+ return preg_replace('/\*/', '%', $value);
+ }
+
+ protected function valueToTimestamp($value)
+ {
+ // We consider integers as valid timestamps. Does not work for URL params
+ if (! is_string($value) || ctype_digit($value)) {
+ return $value;
+ }
+ $value = strtotime($value);
+ if (! $value) {
+ /*
+ NOTE: It's too late to throw exceptions, we might finish in __toString
+ throw new QueryException(sprintf(
+ '"%s" is not a valid time expression',
+ $value
+ ));
+ */
+ }
+ return $value;
+ }
+
+ protected function timestampForSql($value)
+ {
+ // TODO: do this db-aware
+ return $this->escapeForSql(date('Y-m-d H:i:s', $value));
+ }
+
+ /**
+ * Check for timestamp fields
+ *
+ * TODO: This is not here to do automagic timestamp stuff. One may
+ * override this function for custom voodoo, IdoQuery right now
+ * does. IMO we need to split whereToSql functionality, however
+ * I'd prefer to wait with this unless we understood how other
+ * backends will work. We probably should also rename this
+ * function to isTimestampColumn().
+ *
+ * @param string $field Field Field name to checked
+ * @return bool Whether this field expects timestamps
+ */
+ public function isTimestamp($field)
+ {
+ return false;
+ }
+
+ public function whereToSql($col, $sign, $expression)
+ {
+ if ($this->isTimestamp($col)) {
+ $expression = $this->valueToTimestamp($expression);
+ }
+
+ if (is_array($expression) && $sign === '=') {
+ // TODO: Should we support this? Doesn't work for blub*
+ return $col . ' IN (' . $this->escapeForSql($expression) . ')';
+ } elseif ($sign === '=' && strpos($expression, '*') !== false) {
+ if ($expression === '*') {
+ // We'll ignore such filters as it prevents index usage and because "*" means anything, anything means
+ // all whereas all means that whether we use a filter to match anything or no filter at all makes no
+ // difference, except for performance reasons...
+ return '';
+ }
+
+ return $col . ' LIKE ' . $this->escapeForSql($this->escapeWildcards($expression));
+ } elseif ($sign === '!=' && strpos($expression, '*') !== false) {
+ if ($expression === '*') {
+ // We'll ignore such filters as it prevents index usage and because "*" means nothing, so whether we're
+ // using a real column with a valid comparison here or just an expression which cannot be evaluated to
+ // true makes no difference, except for performance reasons...
+ return $this->escapeForSql(0);
+ }
+
+ return $col . ' NOT LIKE ' . $this->escapeForSql($this->escapeWildcards($expression));
+ } else {
+ return $col . ' ' . $sign . ' ' . $this->escapeForSql($expression);
+ }
+ }
+}
diff --git a/library/Director/Web/Table/ReadOnlyFormAvpTable.php b/library/Director/Web/Table/ReadOnlyFormAvpTable.php
new file mode 100644
index 0000000..c3b44f3
--- /dev/null
+++ b/library/Director/Web/Table/ReadOnlyFormAvpTable.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\PlainObjectRenderer;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Zend_Form_Element as ZfElement;
+use Zend_Form_DisplayGroup as ZfDisplayGroup;
+
+class ReadOnlyFormAvpTable
+{
+ protected $form;
+
+ public function __construct(QuickForm $form)
+ {
+ $this->form = $form;
+ }
+
+ protected function renderDisplayGroups(QuickForm $form)
+ {
+ $html = '';
+
+ foreach ($form->getDisplayGroups() as $group) {
+ $elements = $this->filterGroupElements($group);
+
+ if (empty($elements)) {
+ continue;
+ }
+
+ $html .= '<tr><th colspan="2" style="text-align: right">' . $group->getLegend() . '</th></tr>';
+ $html .= $this->renderElements($elements);
+ }
+
+ return $html;
+ }
+
+ /**
+ * @param ZfDisplayGroup $group
+ * @return ZfElement[]
+ */
+ protected function filterGroupElements(ZfDisplayGroup $group)
+ {
+ $blacklist = array('disabled', 'assign_filter');
+ $elements = array();
+ /** @var ZfElement $element */
+ foreach ($group->getElements() as $element) {
+ if ($element->getValue() === null) {
+ continue;
+ }
+
+ if ($element->getType() === 'Zend_Form_Element_Hidden') {
+ continue;
+ }
+
+ if (in_array($element->getName(), $blacklist)) {
+ continue;
+ }
+
+
+ $elements[] = $element;
+ }
+
+ return $elements;
+ }
+
+ protected function renderElements($elements)
+ {
+ $html = '';
+ foreach ($elements as $element) {
+ $html .= $this->renderElement($element);
+ }
+
+ return $html;
+ }
+
+ /**
+ * @param ZfElement $element
+ *
+ * @return string
+ */
+ protected function renderElement(ZfElement $element)
+ {
+ $value = $element->getValue();
+ return '<tr><th>'
+ . $this->escape($element->getLabel())
+ . '</th><td>'
+ . $this->renderValue($value)
+ . '</td></tr>';
+ }
+
+ protected function renderValue($value)
+ {
+ if (is_string($value)) {
+ return $this->escape($value);
+ } elseif (is_array($value)) {
+ return $this->escape(implode(', ', $value));
+ }
+ return $this->escape(PlainObjectRenderer::render($value));
+ }
+
+ protected function escape($string)
+ {
+ return htmlspecialchars($string);
+ }
+
+ public function render()
+ {
+ $this->form->initializeForObject();
+ return '<table class="name-value-table">' . "\n"
+ . $this->renderDisplayGroups($this->form)
+ . '</table>';
+ }
+}
diff --git a/library/Director/Web/Table/ServiceTemplateUsageTable.php b/library/Director/Web/Table/ServiceTemplateUsageTable.php
new file mode 100644
index 0000000..82f9643
--- /dev/null
+++ b/library/Director/Web/Table/ServiceTemplateUsageTable.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+class ServiceTemplateUsageTable extends TemplateUsageTable
+{
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'objects' => $this->translate('Objects'),
+ 'applyrules' => $this->translate('Apply Rules'),
+ // 'setmembers' => $this->translate('Set Members'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'objects' => $this->getSummaryLine('object'),
+ 'applyrules' => $this->getSummaryLine('apply', 'o.service_set_id IS NULL'),
+ // TODO: re-enable
+ // 'setmembers' => $this->getSummaryLine('apply', 'o.service_set_id IS NOT NULL'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/SyncRunTable.php b/library/Director/Web/Table/SyncRunTable.php
new file mode 100644
index 0000000..e08aad7
--- /dev/null
+++ b/library/Director/Web/Table/SyncRunTable.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\Format\LocalTimeFormat;
+use Icinga\Module\Director\Objects\SyncRule;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class SyncRunTable extends ZfQueryBasedTable
+{
+ /** @var SyncRule */
+ protected $rule;
+
+ protected $timeFormat;
+
+ public function __construct(SyncRule $rule)
+ {
+ parent::__construct($rule->getConnection());
+ $this->timeFormat = new LocalTimeFormat();
+ $this->getAttributes()
+ ->set('data-base-target', '_self')
+ ->add('class', 'history');
+ $this->rule = $rule;
+ }
+
+ public function renderRow($row)
+ {
+ $time = strtotime($row->start_time);
+ $this->renderDayIfNew($time);
+ return $this::tr([
+ $this::td($this->makeSummary($row)),
+ $this::td(new Link(
+ $this->timeFormat->getTime($time),
+ 'director/syncrule/history',
+ [
+ 'id' => $row->rule_id,
+ 'run_id' => $row->id,
+ ]
+ ))
+ ]);
+ }
+
+ protected function makeSummary($row)
+ {
+ $parts = [];
+ if ($row->objects_created > 0) {
+ $parts[] = sprintf(
+ $this->translate('%d created'),
+ $row->objects_created
+ );
+ }
+ if ($row->objects_modified > 0) {
+ $parts[] = sprintf(
+ $this->translate('%d modified'),
+ $row->objects_modified
+ );
+ }
+ if ($row->objects_deleted > 0) {
+ $parts[] = sprintf(
+ $this->translate('%d deleted'),
+ $row->objects_deleted
+ );
+ }
+
+ return implode(', ', $parts);
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ array('sr' => 'sync_run'),
+ [
+ 'id' => 'sr.id',
+ 'rule_id' => 'sr.rule_id',
+ 'rule_name' => 'sr.rule_name',
+ 'start_time' => 'sr.start_time',
+ 'duration_ms' => 'sr.duration_ms',
+ 'objects_deleted' => 'sr.objects_deleted',
+ 'objects_created' => 'sr.objects_created',
+ 'objects_modified' => 'sr.objects_modified',
+ 'last_former_activity' => 'sr.last_former_activity',
+ 'last_related_activity' => 'sr.last_related_activity',
+ ]
+ )->where(
+ 'sr.rule_id = ?',
+ $this->rule->get('id')
+ )->order('start_time DESC');
+ }
+}
diff --git a/library/Director/Web/Table/SyncpropertyTable.php b/library/Director/Web/Table/SyncpropertyTable.php
new file mode 100644
index 0000000..79461ce
--- /dev/null
+++ b/library/Director/Web/Table/SyncpropertyTable.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\SyncRule;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\Extension\ZfSortablePriority;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class SyncpropertyTable extends ZfQueryBasedTable
+{
+ use ZfSortablePriority;
+
+ /** @var SyncRule */
+ protected $rule;
+
+ protected $searchColumns = [
+ 'source_expression',
+ 'destination_field',
+ ];
+
+ protected $keyColumn = 'id';
+
+ protected $priorityColumn = 'priority';
+
+ public static function create(SyncRule $rule)
+ {
+ $table = new static($rule->getConnection());
+ $table->getAttributes()->set('data-base-target', '_self');
+ $table->rule = $rule;
+ return $table;
+ }
+
+ public function render()
+ {
+ return $this->renderWithSortableForm();
+ }
+
+ public function renderRow($row)
+ {
+ return $this->addSortPriorityButtons(
+ $this::row([
+ $row->source_name,
+ $row->source_expression,
+ new Link(
+ $row->destination_field,
+ 'director/syncrule/editproperty',
+ [
+ 'id' => $row->id,
+ 'rule_id' => $row->rule_id,
+ ]
+ ),
+ ]),
+ $row
+ );
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Source name'),
+ $this->translate('Source field'),
+ $this->translate('Destination'),
+ $this->getSortPriorityTitle()
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['p' => 'sync_property'],
+ [
+ 'id' => 'p.id',
+ 'rule_id' => 'p.rule_id',
+ 'rule_name' => 'r.rule_name',
+ 'source_id' => 'p.source_id',
+ 'source_name' => 's.source_name',
+ 'source_expression' => 'p.source_expression',
+ 'destination_field' => 'p.destination_field',
+ 'priority' => 'p.priority',
+ 'filter_expression' => 'p.filter_expression',
+ 'merge_policy' => 'p.merge_policy'
+ ]
+ )->join(
+ ['r' => 'sync_rule'],
+ 'r.id = p.rule_id',
+ []
+ )->join(
+ ['s' => 'import_source'],
+ 's.id = p.source_id',
+ []
+ )->where(
+ 'p.rule_id = ?',
+ $this->rule->get('id')
+ )->order('p.priority');
+ }
+}
diff --git a/library/Director/Web/Table/SyncruleTable.php b/library/Director/Web/Table/SyncruleTable.php
new file mode 100644
index 0000000..4a8e4e5
--- /dev/null
+++ b/library/Director/Web/Table/SyncruleTable.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class SyncruleTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'rule_name',
+ 'description',
+ ];
+
+ protected function assemble()
+ {
+ $this->getAttributes()->add('class', 'syncstate');
+ parent::assemble();
+ }
+
+ public function renderRow($row)
+ {
+ $caption = [Link::create(
+ $row->rule_name,
+ 'director/syncrule',
+ ['id' => $row->id]
+ )];
+ if ($row->description !== null) {
+ $caption[] = ': ' . $row->description;
+ }
+
+ if ($row->sync_state === 'failing' && $row->last_error_message) {
+ $caption[] = ' (' . $row->last_error_message . ')';
+ }
+
+ $tr = $this::row([$caption, $row->object_type]);
+ $tr->getAttributes()->add('class', $row->sync_state);
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Rule name'),
+ $this->translate('Object type'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['s' => 'sync_rule'],
+ [
+ 'id' => 's.id',
+ 'rule_name' => 's.rule_name',
+ 'sync_state' => 's.sync_state',
+ 'object_type' => 's.object_type',
+ 'update_policy' => 's.update_policy',
+ 'purge_existing' => 's.purge_existing',
+ 'filter_expression' => 's.filter_expression',
+ 'last_error_message' => 's.last_error_message',
+ 'description' => 's.description',
+ ]
+ )->order('rule_name');
+ }
+}
diff --git a/library/Director/Web/Table/TableLoader.php b/library/Director/Web/Table/TableLoader.php
new file mode 100644
index 0000000..f7e378b
--- /dev/null
+++ b/library/Director/Web/Table/TableLoader.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\ProgrammingError;
+
+class TableLoader
+{
+ /** @return QuickTable */
+ public static function load($name, Module $module = null)
+ {
+ if ($module === null) {
+ $basedir = Icinga::app()->getApplicationDir('tables');
+ $ns = '\\Icinga\\Web\\Tables\\';
+ } else {
+ $basedir = $module->getBaseDir() . '/application/tables';
+ $ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Tables\\';
+ }
+ if (preg_match('~^[a-z0-9/]+$~i', $name)) {
+ $parts = preg_split('~/~', $name);
+ $class = ucfirst(array_pop($parts)) . 'Table';
+ $file = sprintf('%s/%s/%s.php', rtrim($basedir, '/'), implode('/', $parts), $class);
+ if (file_exists($file)) {
+ require_once($file);
+ /** @var QuickTable $class */
+ $class = $ns . $class;
+ return new $class();
+ }
+ }
+ throw new ProgrammingError(sprintf('Cannot load %s (%s), no such table', $name, $file));
+ }
+}
diff --git a/library/Director/Web/Table/TableWithBranchSupport.php b/library/Director/Web/Table/TableWithBranchSupport.php
new file mode 100644
index 0000000..7c5b15c
--- /dev/null
+++ b/library/Director/Web/Table/TableWithBranchSupport.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db\Branch\Branch;
+use Ramsey\Uuid\UuidInterface;
+
+trait TableWithBranchSupport
+{
+
+ /** @var UuidInterface|null */
+ protected $branchUuid;
+
+ /**
+ * Convenience method, only UUID is required
+ *
+ * @param Branch|null $branch
+ * @return $this
+ */
+ public function setBranch(Branch $branch = null)
+ {
+ if ($branch && $branch->isBranch()) {
+ $this->setBranchUuid($branch->getUuid());
+ }
+
+ return $this;
+ }
+
+ public function setBranchUuid(UuidInterface $uuid = null)
+ {
+ $this->branchUuid = $uuid;
+
+ return $this;
+ }
+
+ protected function branchifyColumns($columns)
+ {
+ $result = [
+ 'uuid' => 'COALESCE(o.uuid, bo.uuid)'
+ ];
+ $ignore = ['o.id', 'os.id', 'o.service_set_id', 'os.host_id'];
+ foreach ($columns as $alias => $column) {
+ if (substr($column, 0, 2) === 'o.' && ! in_array($column, $ignore)) {
+ // bo.column, o.column
+ $column = "COALESCE(b$column, $column)";
+ }
+ if (substr($column, 0, 3) === 'os.' && ! in_array($column, $ignore)) {
+ // bo.column, o.column
+ $column = "COALESCE(b$column, $column)";
+ }
+
+ // Used in Service Tables:
+ if ($column === 'h.object_name' && $alias = 'host') {
+ $column = "COALESCE(bo.host, $column)";
+ }
+
+ $result[$alias] = $column;
+ }
+
+ return $result;
+ }
+
+ protected function stripSearchColumnAliases()
+ {
+ foreach ($this->searchColumns as &$column) {
+ $column = preg_replace('/^[a-z]+\./', '', $column);
+ }
+ }
+}
diff --git a/library/Director/Web/Table/TemplateUsageTable.php b/library/Director/Web/Table/TemplateUsageTable.php
new file mode 100644
index 0000000..66e56ea
--- /dev/null
+++ b/library/Director/Web/Table/TemplateUsageTable.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Resolver\TemplateTree;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Table;
+use gipfl\Translation\TranslationHelper;
+
+class TemplateUsageTable extends Table
+{
+ use TranslationHelper;
+
+ protected $defaultAttributes = ['class' => 'pivot'];
+
+ protected $objectType;
+
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'objects' => $this->translate('Objects'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'objects' => $this->getSummaryLine('object'),
+ ];
+ }
+
+ /**
+ * @param IcingaObject $template
+ * @return TemplateUsageTable
+ */
+ public static function forTemplate(IcingaObject $template)
+ {
+ $type = ucfirst($template->getShortTableName());
+ $class = __NAMESPACE__ . "\\${type}TemplateUsageTable";
+ if (class_exists($class)) {
+ return new $class($template);
+ } else {
+ return new static($template);
+ }
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ '',
+ $this->translate('Direct'),
+ $this->translate('Indirect'),
+ $this->translate('Total')
+ ];
+ }
+
+ protected function __construct(IcingaObject $template)
+ {
+
+ if ($template->get('object_type') !== 'template') {
+ throw new ProgrammingError(
+ 'TemplateUsageTable expects a template, got %s',
+ $template->get('object_type')
+ );
+ }
+
+ $this->objectType = $objectType = $template->getShortTableName();
+ $types = $this->getTypes();
+ $usage = $this->getUsageSummary($template);
+
+ $used = false;
+ $rows = [];
+ foreach ($types as $type => $typeTitle) {
+ $tr = Table::tr(Table::th($typeTitle));
+ foreach (['direct', 'indirect', 'total'] as $inheritance) {
+ $count = $usage->$inheritance->$type;
+ if (! $used && $count > 0) {
+ $used = true;
+ }
+ $tr->add(
+ Table::td(
+ Link::create(
+ $count,
+ "director/${objectType}template/$type",
+ [
+ 'name' => $template->getObjectName(),
+ 'inheritance' => $inheritance
+ ]
+ )
+ )
+ );
+ }
+ $rows[] = $tr;
+ }
+
+ if ($used) {
+ $this->add($rows);
+ } else {
+ $this->add($this->translate('This template is not in use'));
+ }
+ }
+
+ protected function getUsageSummary(IcingaObject $template)
+ {
+ $id = $template->getAutoincId();
+ $connection = $template->getConnection();
+ $db = $connection->getDbAdapter();
+ $oType = $this->objectType;
+ $tree = new TemplateTree($oType, $connection);
+ $ids = $tree->listDescendantIdsFor($template);
+ if (empty($ids)) {
+ $ids = [0];
+ }
+
+ $baseQuery = $db->select()->from(
+ ['o' => 'icinga_' . $oType],
+ $this->getTypeSummaryDefinitions()
+ )->joinLeft(
+ ['oi' => "icinga_${oType}_inheritance"],
+ "oi.${oType}_id = o.id",
+ []
+ );
+
+ $query = clone($baseQuery);
+ $direct = $db->fetchRow(
+ $query->where("oi.parent_${oType}_id = ?", $id)
+ );
+ $query = clone($baseQuery);
+ $indirect = $db->fetchRow(
+ $query->where("oi.parent_${oType}_id IN (?)", $ids)
+ );
+ //$indirect->templates = count($ids) - 1;
+ $total = [];
+ $types = array_keys($this->getTypes());
+ foreach ($types as $type) {
+ $total[$type] = $direct->$type + $indirect->$type;
+ }
+
+ return (object) [
+ 'direct' => $direct,
+ 'indirect' => $indirect,
+ 'total' => (object) $total
+ ];
+ }
+
+ protected function getSummaryLine($type, $extra = null)
+ {
+ if ($extra !== null) {
+ $extra = " AND $extra";
+ }
+ return "COALESCE(SUM(CASE WHEN o.object_type = '${type}'${extra} THEN 1 ELSE 0 END), 0)";
+ }
+}
diff --git a/library/Director/Web/Table/TemplatesTable.php b/library/Director/Web/Table/TemplatesTable.php
new file mode 100644
index 0000000..be195b2
--- /dev/null
+++ b/library/Director/Web/Table/TemplatesTable.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\IcingaObjectFilterHelper;
+use Icinga\Module\Director\Objects\IcingaObject;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\Extension\MultiSelect;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer;
+use Ramsey\Uuid\Uuid;
+use Zend_Db_Select as ZfSelect;
+
+class TemplatesTable extends ZfQueryBasedTable implements FilterableByUsage
+{
+ use MultiSelect;
+
+ protected $searchColumns = ['o.object_name'];
+
+ private $type;
+
+ public static function create($type, Db $db)
+ {
+ $table = new static($db);
+ $table->type = strtolower($type);
+ return $table;
+ }
+
+ protected function assemble()
+ {
+ $type = $this->type;
+ $this->enableMultiSelect(
+ "director/${type}s/edittemplates",
+ "director/${type}template",
+ ['name']
+ );
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('Template Name')];
+ }
+
+ public function renderRow($row)
+ {
+ $name = $row->object_name;
+ $type = str_replace('_', '-', $this->getType());
+ $caption = $row->is_used === 'y' ? $name : [
+ $name,
+ Html::tag(
+ 'span',
+ ['style' => 'font-style: italic'],
+ $this->translate(' - not in use -')
+ )
+ ];
+
+ $url = Url::fromPath("director/${type}template/usage", [
+ 'name' => $name
+ ]);
+
+ return $this::row([
+ new Link($caption, $url),
+ [
+ new Link(new Icon('plus'), "director/$type/add", [
+ 'type' => 'object',
+ 'imports' => $name
+ ]),
+ new Link(new Icon('history'), "director/$type/history", [
+ 'uuid' => Uuid::fromBytes(Db\DbUtil::binaryResult($row->uuid))->toString(),
+ ])
+ ]
+ ]);
+ }
+
+ public function filterTemplate(
+ IcingaObject $template,
+ $inheritance = IcingaObjectFilterHelper::INHERIT_DIRECT
+ ) {
+ IcingaObjectFilterHelper::filterByTemplate(
+ $this->getQuery(),
+ $template,
+ 'o',
+ $inheritance
+ );
+
+ return $this;
+ }
+
+ public function showOnlyUsed()
+ {
+ $type = $this->getType();
+ $this->getQuery()->where(
+ "(EXISTS (SELECT ${type}_id FROM icinga_${type}_inheritance"
+ . " WHERE parent_${type}_id = o.id))"
+ );
+ }
+
+ public function showOnlyUnUsed()
+ {
+ $type = $this->getType();
+ $this->getQuery()->where(
+ "(NOT EXISTS (SELECT ${type}_id FROM icinga_${type}_inheritance"
+ . " WHERE parent_${type}_id = o.id))"
+ );
+ }
+
+ protected function applyRestrictions(ZfSelect $query)
+ {
+ $auth = Auth::getInstance();
+ $type = $this->type;
+ $restrictions = $auth->getRestrictions("director/$type/template/filter-by-name");
+ if (empty($restrictions)) {
+ return $query;
+ }
+
+ $filter = Filter::matchAny();
+ foreach ($restrictions as $restriction) {
+ $filter->addFilter(Filter::where('o.object_name', $restriction));
+ }
+
+ return FilterRenderer::applyToQuery($filter, $query);
+ }
+
+ protected function prepareQuery()
+ {
+ $type = $this->getType();
+ $used = "CASE WHEN EXISTS(SELECT 1 FROM icinga_${type}_inheritance oi"
+ . " WHERE oi.parent_${type}_id = o.id) THEN 'y' ELSE 'n' END";
+
+ $columns = [
+ 'object_name' => 'o.object_name',
+ 'uuid' => 'o.uuid',
+ 'id' => 'o.id',
+ 'is_used' => $used,
+ ];
+ $query = $this->db()->select()->from(
+ ['o' => "icinga_${type}"],
+ $columns
+ )->where(
+ "o.object_type = 'template'"
+ )->order('o.object_name');
+
+ return $this->applyRestrictions($query);
+ }
+}
diff --git a/library/Director/Web/Tabs/DataTabs.php b/library/Director/Web/Tabs/DataTabs.php
new file mode 100644
index 0000000..ac29310
--- /dev/null
+++ b/library/Director/Web/Tabs/DataTabs.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tabs;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class DataTabs extends Tabs
+{
+ use TranslationHelper;
+
+ public function __construct()
+ {
+ // We are not a BaseElement, not yet
+ $this->assemble();
+ }
+
+ protected function assemble()
+ {
+ $this->add('datafield', [
+ 'label' => $this->translate('Data fields'),
+ 'url' => 'director/data/fields'
+ ])->add('datafieldcategory', [
+ 'label' => $this->translate('Data field categories'),
+ 'url' => 'director/data/fieldcategories'
+ ])->add('datalist', [
+ 'label' => $this->translate('Data lists'),
+ 'url' => 'director/data/lists'
+ ])->add('customvars', [
+ 'label' => $this->translate('Custom Variables'),
+ 'url' => 'director/data/vars'
+ ]);
+ }
+}
diff --git a/library/Director/Web/Tabs/ImportTabs.php b/library/Director/Web/Tabs/ImportTabs.php
new file mode 100644
index 0000000..e6c6807
--- /dev/null
+++ b/library/Director/Web/Tabs/ImportTabs.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tabs;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class ImportTabs extends Tabs
+{
+ use TranslationHelper;
+
+ public function __construct()
+ {
+ $this->assemble();
+ }
+
+ protected function assemble()
+ {
+ $this->add('importsource', [
+ 'label' => $this->translate('Import source'),
+ 'url' => 'director/importsources'
+ ])->add('syncrule', [
+ 'label' => $this->translate('Sync rule'),
+ 'url' => 'director/syncrules'
+ ])->add('jobs', [
+ 'label' => $this->translate('Jobs'),
+ 'url' => 'director/jobs'
+ ]);
+ }
+}
diff --git a/library/Director/Web/Tabs/ImportsourceTabs.php b/library/Director/Web/Tabs/ImportsourceTabs.php
new file mode 100644
index 0000000..74dedb3
--- /dev/null
+++ b/library/Director/Web/Tabs/ImportsourceTabs.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tabs;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class ImportsourceTabs extends Tabs
+{
+ use TranslationHelper;
+
+ protected $id;
+
+ public function __construct($id = null)
+ {
+ $this->id = $id;
+ $this->assemble();
+ }
+
+ public function activateMainWithPostfix($postfix)
+ {
+ $mainTab = 'index';
+ $tab = $this->get($mainTab);
+ $tab->setLabel($tab->getLabel() . ": $postfix");
+ $this->activate($mainTab);
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ if ($id = $this->id) {
+ $params = ['id' => $id];
+ $this->add('index', [
+ 'url' => 'director/importsource',
+ 'urlParams' => $params,
+ 'label' => $this->translate('Import source'),
+ ])->add('modifier', [
+ 'url' => 'director/importsource/modifier',
+ 'urlParams' => ['source_id' => $id],
+ 'label' => $this->translate('Modifiers'),
+ ])->add('history', [
+ 'url' => 'director/importsource/history',
+ 'urlParams' => $params,
+ 'label' => $this->translate('History'),
+ ])->add('preview', [
+ 'url' => 'director/importsource/preview',
+ 'urlParams' => $params,
+ 'label' => $this->translate('Preview'),
+ ]);
+ } else {
+ $this->add('add', [
+ 'url' => 'director/importsource/add',
+ 'label' => $this->translate('New import source'),
+ ])->activate('add');
+ }
+ }
+}
diff --git a/library/Director/Web/Tabs/InfraTabs.php b/library/Director/Web/Tabs/InfraTabs.php
new file mode 100644
index 0000000..8a65c4e
--- /dev/null
+++ b/library/Director/Web/Tabs/InfraTabs.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tabs;
+
+use Icinga\Authentication\Auth;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class InfraTabs extends Tabs
+{
+ use TranslationHelper;
+
+ /** @var Auth */
+ protected $auth;
+
+ public function __construct(Auth $auth)
+ {
+ $this->auth = $auth;
+ // We are not a BaseElement, not yet
+ $this->assemble();
+ }
+
+ protected function assemble()
+ {
+ $auth = $this->auth;
+
+ if ($auth->hasPermission('director/audit')) {
+ $this->add('activitylog', [
+ 'label' => $this->translate('Activity Log'),
+ 'url' => 'director/config/activities'
+ ]);
+ }
+
+ if ($auth->hasPermission('director/deploy')) {
+ $this->add('deploymentlog', [
+ 'label' => $this->translate('Deployments'),
+ 'url' => 'director/config/deployments'
+ ]);
+ }
+
+ if ($auth->hasPermission('director/admin')) {
+ $this->add('infrastructure', [
+ 'label' => $this->translate('Infrastructure'),
+ 'url' => 'director/dashboard',
+ 'urlParams' => ['name' => 'infrastructure']
+ ]);
+ }
+ }
+}
diff --git a/library/Director/Web/Tabs/MainTabs.php b/library/Director/Web/Tabs/MainTabs.php
new file mode 100644
index 0000000..5ea2e9b
--- /dev/null
+++ b/library/Director/Web/Tabs/MainTabs.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tabs;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\Tabs;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Web\Widget\Daemon\BackgroundDaemonState;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Health;
+use Icinga\Module\Director\Web\Widget\HealthCheckPluginOutput;
+
+class MainTabs extends Tabs
+{
+ use TranslationHelper;
+
+ protected $auth;
+
+ protected $dbResourceName;
+
+ public function __construct(Auth $auth, $dbResourceName)
+ {
+ $this->auth = $auth;
+ $this->dbResourceName = $dbResourceName;
+ $this->add('main', [
+ 'label' => $this->translate('Overview'),
+ 'url' => 'director'
+ ]);
+ if ($this->auth->hasPermission('director/admin')) {
+ $this->add('health', [
+ 'label' => $this->translate('Health'),
+ 'url' => 'director/health'
+ ])->add('daemon', [
+ 'label' => $this->translate('Daemon'),
+ 'url' => 'director/daemon'
+ ]);
+ }
+ }
+
+ public function render()
+ {
+ if ($this->auth->hasPermission('director/admin')) {
+ if ($this->getActiveName() !== 'health') {
+ $state = $this->getHealthState();
+ if ($state->isProblem()) {
+ $this->get('health')->setTagParams([
+ 'class' => 'state-' . strtolower($state->getName())
+ ]);
+ }
+ }
+
+ if ($this->getActiveName() !== 'daemon') {
+ try {
+ $daemon = new BackgroundDaemonState(Db::fromResourceName($this->dbResourceName));
+ if ($daemon->isRunning()) {
+ $state = 'ok';
+ } else {
+ $state = 'critical';
+ }
+ } catch (\Exception $e) {
+ $state = 'unknown';
+ }
+ if ($state !== 'ok') {
+ $this->get('daemon')->setTagParams([
+ 'class' => 'state-' . $state
+ ]);
+ }
+ }
+ }
+
+ return parent::render();
+ }
+
+ /**
+ * @return \Icinga\Module\Director\CheckPlugin\PluginState
+ */
+ protected function getHealthState()
+ {
+ $health = new Health();
+ $health->setDbResourceName($this->dbResourceName);
+ $output = new HealthCheckPluginOutput($health);
+
+ return $output->getState();
+ }
+}
diff --git a/library/Director/Web/Tabs/ObjectTabs.php b/library/Director/Web/Tabs/ObjectTabs.php
new file mode 100644
index 0000000..cbd3f15
--- /dev/null
+++ b/library/Director/Web/Tabs/ObjectTabs.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tabs;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Objects\IcingaObject;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class ObjectTabs extends Tabs
+{
+ use TranslationHelper;
+
+ /** @var string */
+ private $type;
+
+ /** @var Auth */
+ private $auth;
+
+ /** @var IcingaObject $object */
+ private $object;
+
+ private $allowedExternals = [
+ 'apiuser',
+ 'endpoint'
+ ];
+
+ public function __construct($type, Auth $auth, IcingaObject $object = null)
+ {
+ $this->type = $type;
+ $this->auth = $auth;
+ $this->object = $object;
+ // We are not a BaseElement, not yet
+ $this->assemble();
+ }
+
+ protected function assemble()
+ {
+ if (null === $this->object) {
+ $this->addTabsForNewObject();
+ } else {
+ $this->addTabsForExistingObject();
+ }
+ }
+
+ protected function addTabsForNewObject()
+ {
+ $type = $this->type;
+ $this->add('add', array(
+ 'url' => sprintf('director/%s/add', $type),
+ 'label' => sprintf($this->translate('Add %s'), ucfirst($type)),
+ ));
+ }
+
+ protected function addTabsForExistingObject()
+ {
+ $type = $this->type;
+ $auth = $this->auth;
+ $object = $this->object;
+ $params = $object->getUrlParams();
+
+ if (! $object->isExternal()
+ || in_array($object->getShortTableName(), $this->allowedExternals)
+ ) {
+ $this->add('modify', array(
+ 'url' => sprintf('director/%s', $type),
+ 'urlParams' => $params,
+ 'label' => $this->translate(ucfirst($type))
+ ));
+ }
+ if ($object->getShortTableName() === 'host') {
+ $this->add('services', [
+ 'url' => 'director/host/services',
+ 'urlParams' => $params,
+ 'label' => $this->translate('Services')
+ ]);
+ }
+
+ if ($auth->hasPermission('director/showconfig')) {
+ if ($object->getShortTableName() !== 'service'
+ || $object->get('service_set_id') === null
+ ) {
+ $this->add('render', array(
+ 'url' => sprintf('director/%s/render', $type),
+ 'urlParams' => $params,
+ 'label' => $this->translate('Preview'),
+ ));
+ }
+ }
+
+ if ($auth->hasPermission('director/audit')) {
+ $this->add('history', array(
+ 'url' => sprintf('director/%s/history', $type),
+ 'urlParams' => $params,
+ 'label' => $this->translate('History')
+ ));
+ }
+
+ if ($auth->hasPermission('director/admin') && $this->hasFields()) {
+ $this->add('fields', array(
+ 'url' => sprintf('director/%s/fields', $type),
+ 'urlParams' => $params,
+ 'label' => $this->translate('Fields')
+ ));
+ }
+
+ // TODO: remove table check once we resolve all group types
+ if ($object->isGroup() &&
+ ($object->getShortTableName() === 'hostgroup' || $object->getShortTableName() === 'servicegroup')
+ ) {
+ $this->add('membership', [
+ 'url' => sprintf('director/%s/membership', $type),
+ 'urlParams' => $params,
+ 'label' => $this->translate('Members')
+ ]);
+ }
+
+ if ($object->supportsRanges()) {
+ $this->add('ranges', [
+ 'url' => "director/${type}/ranges",
+ 'urlParams' => $params,
+ 'label' => $this->translate('Ranges')
+ ]);
+ }
+
+ if ($object->getShortTableName() === 'endpoint'
+ && $object->get('apiuser_id')
+ ) {
+ $this->add('inspect', [
+ 'url' => 'director/inspect/types',
+ 'urlParams' => ['endpoint' => $object->getObjectName()],
+ 'label' => $this->translate('Inspect')
+ ]);
+ $this->add('packages', [
+ 'url' => 'director/inspect/packages',
+ 'urlParams' => ['endpoint' => $object->getObjectName()],
+ 'label' => $this->translate('Packages')
+ ]);
+ }
+
+ if ($object->getShortTableName() === 'host' && $auth->hasPermission('director/hosts')) {
+ $this->add('agent', [
+ 'url' => 'director/host/agent',
+ 'urlParams' => $params,
+ 'label' => $this->translate('Agent')
+ ]);
+ }
+ }
+
+ protected function hasFields()
+ {
+ if (! ($object = $this->object)) {
+ return false;
+ }
+
+ return $object->hasBeenLoadedFromDb()
+ && $object->supportsFields()
+ && ($object->isTemplate() || $this->type === 'command');
+ }
+}
diff --git a/library/Director/Web/Tabs/ObjectsTabs.php b/library/Director/Web/Tabs/ObjectsTabs.php
new file mode 100644
index 0000000..4f9e5a8
--- /dev/null
+++ b/library/Director/Web/Tabs/ObjectsTabs.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tabs;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Objects\IcingaObject;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class ObjectsTabs extends Tabs
+{
+ use TranslationHelper;
+
+ public function __construct($type, Auth $auth, $typeUrl)
+ {
+ $object = IcingaObject::createByType($type);
+ if ($object->isGroup()) {
+ $object = IcingaObject::createByType(substr($typeUrl, 0, -5));
+ }
+ $shortName = $object->getShortTableName();
+
+ $plType = strtolower(preg_replace('/cys$/', 'cies', $shortName . 's'));
+ $plType = str_replace('_', '-', $plType);
+ if ($auth->hasPermission("director/${plType}")) {
+ $this->add('index', array(
+ 'url' => sprintf('director/%s', $plType),
+ 'label' => $this->translate(ucfirst($plType)),
+ ));
+ }
+
+ if ($object->getShortTableName() === 'command') {
+ $this->add('external', array(
+ 'url' => sprintf('director/%s', strtolower($plType)),
+ 'urlParams' => ['type' => 'external_object'],
+ 'label' => $this->translate('External'),
+ ));
+ }
+
+ if ($auth->hasPermission('director/admin') || (
+ $object->getShortTableName() === 'notification'
+ && $auth->hasPermission('director/notifications')
+ ) || (
+ $object->getShortTableName() === 'scheduled_downtime'
+ && $auth->hasPermission('director/scheduled-downtimes')
+ )) {
+ if ($object->supportsApplyRules()) {
+ $this->add('applyrules', array(
+ 'url' => sprintf('director/%s/applyrules', $plType),
+ 'label' => $this->translate('Apply')
+ ));
+ }
+ }
+
+ if ($auth->hasPermission('director/admin') && $type !== 'zone') {
+ if ($object->supportsImports()) {
+ $this->add('templates', array(
+ 'url' => sprintf('director/%s/templates', $plType),
+ 'label' => $this->translate('Templates'),
+ ));
+ }
+
+ if ($object->supportsGroups()) {
+ $this->add('groups', array(
+ 'url' => sprintf('director/%sgroups', $typeUrl),
+ 'label' => $this->translate('Groups')
+ ));
+ }
+ }
+
+ if ($auth->hasPermission('director/admin')) {
+ if ($object->supportsChoices()) {
+ $this->add('choices', array(
+ 'url' => sprintf('director/templatechoices/%s', $shortName),
+ 'label' => $this->translate('Choices')
+ ));
+ }
+ }
+ if ($object->supportsSets() && $auth->hasPermission("director/${typeUrl}sets")) {
+ $this->add('sets', array(
+ 'url' => sprintf('director/%s/sets', $plType),
+ 'label' => $this->translate('Sets')
+ ));
+ }
+ }
+}
diff --git a/library/Director/Web/Tabs/SyncRuleTabs.php b/library/Director/Web/Tabs/SyncRuleTabs.php
new file mode 100644
index 0000000..d64ff81
--- /dev/null
+++ b/library/Director/Web/Tabs/SyncRuleTabs.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tabs;
+
+use Icinga\Module\Director\Objects\SyncRule;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class SyncRuleTabs extends Tabs
+{
+ use TranslationHelper;
+
+ protected $rule;
+
+ public function __construct(SyncRule $rule = null)
+ {
+ $this->rule = $rule;
+ // We are not a BaseElement, not yet
+ $this->assemble();
+ }
+
+ protected function assemble()
+ {
+ if ($this->rule) {
+ $id = $this->rule->get('id');
+ $this->add('show', [
+ 'url' => 'director/syncrule',
+ 'urlParams' => ['id' => $id],
+ 'label' => $this->translate('Sync rule'),
+ ])->add('preview', [
+ 'url' => 'director/syncrule/preview',
+ 'urlParams' => ['id' => $id],
+ 'label' => $this->translate('Preview'),
+ ])->add('edit', [
+ 'url' => 'director/syncrule/edit',
+ 'urlParams' => ['id' => $id],
+ 'label' => $this->translate('Modify'),
+ ])->add('property', [
+ 'label' => $this->translate('Properties'),
+ 'url' => 'director/syncrule/property',
+ 'urlParams' => ['rule_id' => $id]
+ ])->add('history', [
+ 'label' => $this->translate('History'),
+ 'url' => 'director/syncrule/history',
+ 'urlParams' => ['id' => $id]
+ ]);
+ } else {
+ $this->add('add', [
+ 'url' => 'director/syncrule/add',
+ 'label' => $this->translate('Sync rule'),
+ ]);
+ }
+ }
+}
diff --git a/library/Director/Web/Tree/InspectTreeRenderer.php b/library/Director/Web/Tree/InspectTreeRenderer.php
new file mode 100644
index 0000000..54a177f
--- /dev/null
+++ b/library/Director/Web/Tree/InspectTreeRenderer.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tree;
+
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+
+class InspectTreeRenderer extends BaseHtmlElement
+{
+ use TranslationHelper;
+
+ protected $tag = 'ul';
+
+ protected $defaultAttributes = [
+ 'class' => 'tree',
+ 'data-base-target' => '_next',
+ ];
+
+ protected $tree;
+
+ /** @var IcingaEndpoint */
+ protected $endpoint;
+
+ public function __construct(IcingaEndpoint $endpoint)
+ {
+ $this->endpoint = $endpoint;
+ }
+
+ protected function getNodes()
+ {
+ $rootNodes = array();
+ $types = $this->endpoint->api()->getTypes();
+ foreach ($types as $name => $type) {
+ if (property_exists($type, 'base')) {
+ $base = $type->base;
+ if (! property_exists($types[$base], 'children')) {
+ $types[$base]->children = array();
+ }
+
+ $types[$base]->children[$name] = $type;
+ } else {
+ $rootNodes[$name] = $type;
+ }
+ }
+
+ return $rootNodes;
+ }
+
+ public function assemble()
+ {
+ $this->add($this->renderNodes($this->getNodes()));
+ }
+
+ protected function renderNodes($nodes, $showLinks = false, $level = 0)
+ {
+ $result = [];
+ foreach ($nodes as $child) {
+ $result[] = $this->renderNode($child, $showLinks, $level + 1);
+ }
+
+ if ($level === 0) {
+ return $result;
+ } else {
+ return Html::tag('ul', null, $result);
+ }
+ }
+
+ protected function renderNode($node, $forceLinks = false, $level = 0)
+ {
+ $name = $node->name;
+ $showLinks = $forceLinks || $name === 'ConfigObject';
+ $hasChildren = property_exists($node, 'children');
+ $li = Html::tag('li');
+ if (! $hasChildren) {
+ $li->getAttributes()->add('class', 'collapsed');
+ }
+
+ if ($hasChildren) {
+ $li->add(Html::tag('span', ['class' => 'handle']));
+ }
+
+ $class = $node->abstract ? 'icon-sitemap' : 'icon-doc-text';
+ $li->add(Link::create($name, 'director/inspect/type', [
+ 'endpoint' => $this->endpoint->getObjectName(),
+ 'type' => $name
+ ], ['class' => $class]));
+
+ if ($hasChildren) {
+ $li->add($this->renderNodes($node->children, $showLinks, $level + 1));
+ }
+
+ return $li;
+ }
+}
diff --git a/library/Director/Web/Tree/TemplateTreeRenderer.php b/library/Director/Web/Tree/TemplateTreeRenderer.php
new file mode 100644
index 0000000..e238ded
--- /dev/null
+++ b/library/Director/Web/Tree/TemplateTreeRenderer.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Tree;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Resolver\TemplateTree;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\ControlsAndContent;
+
+class TemplateTreeRenderer extends BaseHtmlElement
+{
+ use TranslationHelper;
+
+ protected $tag = 'ul';
+
+ protected $defaultAttributes = [
+ 'class' => 'tree',
+ 'data-base-target' => '_next',
+ ];
+
+ protected $tree;
+
+ public function __construct(TemplateTree $tree)
+ {
+ $this->tree = $tree;
+ }
+
+ public static function showType($type, ControlsAndContent $controller, Db $db)
+ {
+ $controller->content()->add(
+ new static(new TemplateTree($type, $db))
+ );
+ }
+
+ public function renderContent()
+ {
+ $this->add(
+ $this->dumpTree(
+ array(
+ 'name' => $this->translate('Templates'),
+ 'children' => $this->tree->getTree()
+ )
+ )
+ );
+
+ return parent::renderContent();
+ }
+
+ protected function dumpTree($tree, $level = 0)
+ {
+ $hasChildren = ! empty($tree['children']);
+ $type = $this->tree->getType();
+
+ $li = Html::tag('li');
+ if (! $hasChildren) {
+ $li->getAttributes()->add('class', 'collapsed');
+ }
+
+ if ($hasChildren) {
+ $li->add(Html::tag('span', ['class' => 'handle']));
+ }
+
+ if ($level === 0) {
+ $li->add(Html::tag('a', [
+ 'name' => $tree['name'],
+ 'class' => 'icon-globe'
+ ], $tree['name']));
+ } else {
+ $li->add(Link::create(
+ $tree['name'],
+ "director/${type}template/usage",
+ array('name' => $tree['name']),
+ array('class' => 'icon-' .$type)
+ ));
+ }
+
+ if ($hasChildren) {
+ $li->add(
+ $ul = Html::tag('ul')
+ );
+ foreach ($tree['children'] as $child) {
+ $ul->add($this->dumpTree($child, $level + 1));
+ }
+ }
+
+ return $li;
+ }
+}
diff --git a/library/Director/Web/Widget/AbstractList.php b/library/Director/Web/Widget/AbstractList.php
new file mode 100644
index 0000000..ad1b9e3
--- /dev/null
+++ b/library/Director/Web/Widget/AbstractList.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\HtmlElement;
+
+class AbstractList extends BaseHtmlElement
+{
+ protected $contentSeparator = "\n";
+
+ /**
+ * AbstractList constructor.
+ * @param array $items
+ * @param null $attributes
+ */
+ public function __construct(array $items = [], $attributes = null)
+ {
+ foreach ($items as $item) {
+ $this->addItem($item);
+ }
+
+ if ($attributes !== null) {
+ $this->addAttributes($attributes);
+ }
+ }
+
+ /**
+ * @param Html|array|string $content
+ * @param Attributes|array $attributes
+ *
+ * @return $this
+ */
+ public function addItem($content, $attributes = null)
+ {
+ return $this->add(HtmlElement::create('li', $attributes, $content));
+ }
+}
diff --git a/library/Director/Web/Widget/ActivityLogInfo.php b/library/Director/Web/Widget/ActivityLogInfo.php
new file mode 100644
index 0000000..8454b26
--- /dev/null
+++ b/library/Director/Web/Widget/ActivityLogInfo.php
@@ -0,0 +1,634 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\Json\JsonString;
+use Icinga\Module\Director\Objects\DirectorActivityLog;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlElement;
+use Icinga\Date\DateFormatter;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Forms\RestoreObjectForm;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Url;
+use gipfl\IcingaWeb2\Widget\NameValueTable;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class ActivityLogInfo extends HtmlDocument
+{
+ use TranslationHelper;
+
+ protected $defaultTab;
+
+ /** @var Db */
+ protected $db;
+
+ /** @var string */
+ protected $type;
+
+ /** @var string */
+ protected $typeName;
+
+ /** @var string */
+ protected $name;
+
+ protected $entry;
+
+ protected $oldProperties;
+
+ protected $newProperties;
+
+ protected $oldObject;
+
+ /** @var Tabs */
+ protected $tabs;
+
+ /** @var int */
+ protected $id;
+
+ public function __construct(Db $db, $type = null, $name = null)
+ {
+ $this->db = $db;
+ if ($type !== null) {
+ $this->setType($type);
+ }
+ $this->name = $name;
+ }
+
+ public function setType($type)
+ {
+ $this->type = $type;
+ $this->typeName = $this->translate(
+ ucfirst(preg_replace('/^icinga_/', '', $type)) // really?
+ );
+
+ return $this;
+ }
+
+ /**
+ * @param Url $url
+ * @return HtmlElement
+ * @throws \Icinga\Exception\IcingaException
+ */
+ public function getPagination(Url $url)
+ {
+ /** @var Url $url */
+ $url = $url->without('checksum')->without('show');
+ $div = Html::tag('div', [
+ 'class' => 'pagination-control',
+ 'style' => 'float: right; width: 5em'
+ ]);
+
+ $ul = Html::tag('ul', ['class' => 'nav tab-nav']);
+ $li = Html::tag('li', ['class' => 'nav-item']);
+ $ul->add($li);
+ $neighbors = $this->getNeighbors();
+ $iconLeft = new Icon('angle-double-left');
+ $iconRight = new Icon('angle-double-right');
+ if ($neighbors->prev) {
+ $li->add(new Link($iconLeft, $url->with('id', $neighbors->prev)));
+ } else {
+ $li->add(Html::tag('span', ['class' => 'disabled'], $iconLeft));
+ }
+
+ $li = Html::tag('li', ['class' => 'nav-item']);
+ $ul->add($li);
+ if ($neighbors->next) {
+ $li->add(new Link($iconRight, $url->with('id', $neighbors->next)));
+ } else {
+ $li->add(Html::tag('span', ['class' => 'disabled'], $iconRight));
+ }
+
+ return $div->add($ul);
+ }
+
+ /**
+ * @param $tabName
+ * @return $this
+ * @throws \Icinga\Exception\Http\HttpNotFoundException
+ * @throws \Icinga\Exception\IcingaException
+ */
+ public function showTab($tabName)
+ {
+ if ($tabName === null) {
+ $tabName = $this->defaultTab;
+ }
+
+ $this->getTabs()->activate($tabName);
+ $this->add($this->getInfoTable());
+ if ($tabName === 'old') {
+ // $title = sprintf('%s former config', $this->entry->object_name);
+ $diffs = IcingaConfigDiff::getDiffs($this->oldConfig(), $this->emptyConfig());
+ } elseif ($tabName === 'new') {
+ // $title = sprintf('%s new config', $this->entry->object_name);
+ $diffs = IcingaConfigDiff::getDiffs($this->emptyConfig(), $this->newConfig());
+ } else {
+ $diffs = IcingaConfigDiff::getDiffs($this->oldConfig(), $this->newConfig());
+ }
+
+ $this->addDiffs($diffs);
+
+ return $this;
+ }
+
+ protected function emptyConfig()
+ {
+ return new IcingaConfig($this->db);
+ }
+
+ /**
+ * @param $diffs
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function addDiffs($diffs)
+ {
+ foreach ($diffs as $file => $diff) {
+ $this->add(Html::tag('h3', null, $file))->add($diff);
+ }
+ }
+
+ /**
+ * @return RestoreObjectForm
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function getRestoreForm()
+ {
+ return RestoreObjectForm::load()
+ ->setDb($this->db)
+ ->setObject($this->oldObject())
+ ->handleRequest();
+ }
+
+ public function setChecksum($checksum)
+ {
+ if ($checksum !== null) {
+ $this->entry = $this->db->fetchActivityLogEntry($checksum);
+ $this->id = (int) $this->entry->id;
+ }
+
+ return $this;
+ }
+
+ public function setId($id)
+ {
+ if ($id !== null) {
+ $this->entry = $this->db->fetchActivityLogEntryById($id);
+ $this->id = (int) $id;
+ }
+
+ return $this;
+ }
+
+ public function getNeighbors()
+ {
+ return $this->db->getActivitylogNeighbors(
+ $this->id,
+ $this->type,
+ $this->name
+ );
+ }
+
+ public function getCurrentObject()
+ {
+ return IcingaObject::loadByType(
+ $this->type,
+ $this->name,
+ $this->db
+ );
+ }
+
+ /**
+ * @return bool
+ * @deprecated No longer used?
+ */
+ public function objectStillExists()
+ {
+ return IcingaObject::existsByType(
+ $this->type,
+ $this->objectKey(),
+ $this->db
+ );
+ }
+
+ protected function oldProperties()
+ {
+ if ($this->oldProperties === null) {
+ if (property_exists($this->entry, 'old_properties')) {
+ $this->oldProperties = JsonString::decodeOptional($this->entry->old_properties);
+ }
+ if ($this->oldProperties === null) {
+ $this->oldProperties = new \stdClass;
+ }
+ }
+
+ return $this->oldProperties;
+ }
+
+ protected function newProperties()
+ {
+ if ($this->newProperties === null) {
+ if (property_exists($this->entry, 'new_properties')) {
+ $this->newProperties = JsonString::decodeOptional($this->entry->new_properties);
+ }
+ if ($this->newProperties === null) {
+ $this->newProperties = new \stdClass;
+ }
+ }
+
+ return $this->newProperties;
+ }
+
+ protected function getEntryProperty($key)
+ {
+ $entry = $this->entry;
+
+ if (property_exists($entry, $key)) {
+ return $entry->{$key};
+ } elseif (property_exists($this->newProperties(), $key)) {
+ return $this->newProperties->{$key};
+ } elseif (property_exists($this->oldProperties(), $key)) {
+ return $this->oldProperties->{$key};
+ } else {
+ return null;
+ }
+ }
+
+ protected function objectLinkParams()
+ {
+ $entry = $this->entry;
+
+ $params = ['name' => $entry->object_name];
+
+ if ($entry->object_type === 'icinga_service') {
+ if (($set = $this->getEntryProperty('service_set')) !== null) {
+ $params['set'] = $set;
+ return $params;
+ } elseif (($host = $this->getEntryProperty('host')) !== null) {
+ $params['host'] = $host;
+ return $params;
+ } else {
+ return $params;
+ }
+ } elseif ($entry->object_type === 'icinga_service_set') {
+ return $params;
+ } else {
+ return $params;
+ }
+ }
+
+ protected function getActionExtraHtml()
+ {
+ $entry = $this->entry;
+
+ $info = '';
+ $host = null;
+
+ if ($entry->object_type === 'icinga_service') {
+ if (($set = $this->getEntryProperty('service_set')) !== null) {
+ $info = Html::sprintf(
+ '%s "%s"',
+ $this->translate('on service set'),
+ Link::create(
+ $set,
+ 'director/serviceset',
+ ['name' => $set],
+ ['data-base-target' => '_next']
+ )
+ );
+ } else {
+ $host = $this->getEntryProperty('host');
+ }
+ } elseif ($entry->object_type === 'icinga_service_set') {
+ $host = $this->getEntryProperty('host');
+ }
+
+ if ($host !== null) {
+ $info = Html::sprintf(
+ '%s "%s"',
+ $this->translate('on host'),
+ Link::create(
+ $host,
+ 'director/host',
+ ['name' => $host],
+ ['data-base-target' => '_next']
+ )
+ );
+ }
+
+ return $info;
+ }
+
+ /**
+ * @return array
+ * @deprecated No longer used?
+ */
+ protected function objectKey()
+ {
+ $entry = $this->entry;
+ if ($entry->object_type === 'icinga_service' || $entry->object_type === 'icinga_service_set') {
+ // TODO: this is not correct. Activity needs to get (multi) key support
+ return ['name' => $entry->object_name];
+ }
+
+ return $entry->object_name;
+ }
+
+ /**
+ * @param Url|null $url
+ * @return Tabs
+ */
+ public function getTabs(Url $url = null)
+ {
+ if ($this->tabs === null) {
+ $this->tabs = $this->createTabs($url);
+ }
+
+ return $this->tabs;
+ }
+
+ /**
+ * @param Url $url
+ * @return Tabs
+ */
+ public function createTabs(Url $url)
+ {
+ $entry = $this->entry;
+ $tabs = new Tabs();
+ if ($entry->action_name === DirectorActivityLog::ACTION_MODIFY) {
+ $tabs->add('diff', [
+ 'label' => $this->translate('Diff'),
+ 'url' => $url->without('show')->with('id', $entry->id)
+ ]);
+
+ $this->defaultTab = 'diff';
+ }
+
+ if (in_array($entry->action_name, [
+ DirectorActivityLog::ACTION_CREATE,
+ DirectorActivityLog::ACTION_MODIFY,
+ ])) {
+ $tabs->add('new', [
+ 'label' => $this->translate('New object'),
+ 'url' => $url->with(['id' => $entry->id, 'show' => 'new'])
+ ]);
+
+ if ($this->defaultTab === null) {
+ $this->defaultTab = 'new';
+ }
+ }
+
+ if (in_array($entry->action_name, [
+ DirectorActivityLog::ACTION_DELETE,
+ DirectorActivityLog::ACTION_MODIFY,
+ ])) {
+ $tabs->add('old', [
+ 'label' => $this->translate('Former object'),
+ 'url' => $url->with(['id' => $entry->id, 'show' => 'old'])
+ ]);
+
+ if ($this->defaultTab === null) {
+ $this->defaultTab = 'old';
+ }
+ }
+
+ return $tabs;
+ }
+
+ /**
+ * @return IcingaObject
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function oldObject()
+ {
+ if ($this->oldObject === null) {
+ $this->oldObject = $this->createObject(
+ $this->entry->object_type,
+ $this->entry->old_properties
+ );
+ }
+
+ return $this->oldObject;
+ }
+
+ /**
+ * @return IcingaObject
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function newObject()
+ {
+ return $this->createObject(
+ $this->entry->object_type,
+ $this->entry->new_properties
+ );
+ }
+
+ protected function objectToConfig(IcingaObject $object)
+ {
+ if ($object instanceof IcingaService) {
+ return $this->previewService($object);
+ } else {
+ return $object->toSingleIcingaConfig();
+ }
+ }
+
+ protected function previewService(IcingaService $service)
+ {
+ if (($set = $service->get('service_set')) !== null) {
+ // simulate rendering of service in set
+ $set = IcingaServiceSet::load($set, $this->db);
+
+ $service->set('service_set_id', null);
+ if (($assign = $set->get('assign_filter')) !== null) {
+ $service->set('object_type', 'apply');
+ $service->set('assign_filter', $assign);
+ }
+ }
+
+ return $service->toSingleIcingaConfig();
+ }
+
+ /**
+ * @return IcingaConfig
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function newConfig()
+ {
+ return $this->objectToConfig($this->newObject());
+ }
+
+ /**
+ * @return IcingaConfig
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function oldConfig()
+ {
+ return $this->objectToConfig($this->oldObject());
+ }
+
+ protected function getLinkToObject()
+ {
+ // TODO: This logic is redundant and should be centralized
+ $entry = $this->entry;
+ $name = $entry->object_name;
+ $controller = preg_replace('/^icinga_/', '', $entry->object_type);
+
+ if ($controller === 'service_set') {
+ $controller = 'serviceset';
+ } elseif ($controller === 'scheduled_downtime') {
+ $controller = 'scheduled-downtime';
+ }
+
+ return Link::create(
+ $name,
+ 'director/' . $controller,
+ $this->objectLinkParams(),
+ ['data-base-target' => '_next']
+ );
+ }
+
+ /**
+ * @return NameValueTable
+ * @throws \Icinga\Exception\IcingaException
+ */
+ public function getInfoTable()
+ {
+ $entry = $this->entry;
+ $table = new NameValueTable();
+ $table->addNameValuePairs([
+ $this->translate('Author') => $entry->author,
+ $this->translate('Date') => DateFormatter::formatDateTime(
+ $entry->change_time_ts
+ ),
+
+ ]);
+ if (null === $this->name) {
+ $table->addNameValueRow(
+ $this->translate('Action'),
+ Html::sprintf(
+ '%s %s "%s" %s',
+ $entry->action_name,
+ $entry->object_type,
+ $this->getLinkToObject(),
+ $this->getActionExtraHtml()
+ )
+ );
+ } else {
+ $table->addNameValueRow(
+ $this->translate('Action'),
+ $entry->action_name
+ );
+ }
+
+ if ($comment = $this->getOptionalRangeComment()) {
+ $table->addNameValueRow(
+ $this->translate('Remark'),
+ $comment
+ );
+ }
+
+ if ($this->hasBeenEnabled()) {
+ $table->addNameValueRow(
+ $this->translate('Rendering'),
+ $this->translate('This object has been enabled')
+ );
+ } elseif ($this->hasBeenDisabled()) {
+ $table->addNameValueRow(
+ $this->translate('Rendering'),
+ $this->translate('This object has been disabled')
+ );
+ }
+
+ $table->addNameValueRow(
+ $this->translate('Checksum'),
+ $entry->checksum
+ );
+ if ($this->entry->old_properties) {
+ $table->addNameValueRow(
+ $this->translate('Actions'),
+ $this->getRestoreForm()
+ );
+ }
+
+ return $table;
+ }
+
+ public function hasBeenEnabled()
+ {
+ return false;
+ }
+
+ public function hasBeenDisabled()
+ {
+ return false;
+ }
+
+ /**
+ * @return string
+ * @throws ProgrammingError
+ */
+ public function getTitle()
+ {
+ switch ($this->entry->action_name) {
+ case DirectorActivityLog::ACTION_CREATE:
+ $msg = $this->translate('%s "%s" has been created');
+ break;
+ case DirectorActivityLog::ACTION_DELETE:
+ $msg = $this->translate('%s "%s" has been deleted');
+ break;
+ case DirectorActivityLog::ACTION_MODIFY:
+ $msg = $this->translate('%s "%s" has been modified');
+ break;
+ default:
+ throw new ProgrammingError(
+ 'Unable to deal with "%s" activity',
+ $this->entry->action_name
+ );
+ }
+
+ return sprintf($msg, $this->typeName, $this->entry->object_name);
+ }
+
+ protected function getOptionalRangeComment()
+ {
+ if ($this->id) {
+ $db = $this->db->getDbAdapter();
+ return $db->fetchOne(
+ $db->select()
+ ->from('director_activity_log_remark', 'remark')
+ ->where('first_related_activity <= ?', $this->id)
+ ->where('last_related_activity >= ?', $this->id)
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * @param $type
+ * @param $props
+ * @return IcingaObject
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function createObject($type, $props)
+ {
+ $props = json_decode($props);
+ $newProps = ['object_name' => $props->object_name];
+ if (property_exists($props, 'object_type')) {
+ $newProps['object_type'] = $props->object_type;
+ }
+
+ return IcingaObject::createByType(
+ $type,
+ $newProps,
+ $this->db
+ )->setProperties((array) $props);
+ }
+}
diff --git a/library/Director/Web/Widget/AdditionalTableActions.php b/library/Director/Web/Widget/AdditionalTableActions.php
new file mode 100644
index 0000000..978f399
--- /dev/null
+++ b/library/Director/Web/Widget/AdditionalTableActions.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Web\Table\FilterableByUsage;
+
+class AdditionalTableActions
+{
+ use TranslationHelper;
+
+ /** @var Auth */
+ protected $auth;
+
+ /** @var Url */
+ protected $url;
+
+ /** @var ZfQueryBasedTable */
+ protected $table;
+
+ public function __construct(Auth $auth, Url $url, ZfQueryBasedTable $table)
+ {
+ $this->auth = $auth;
+ $this->url = $url;
+ $this->table = $table;
+ }
+
+ public function appendTo(HtmlDocument $parent)
+ {
+ $links = [];
+ if ($this->hasPermission('director/admin')) {
+ $links[] = $this->createDownloadJsonLink();
+ }
+ if ($this->hasPermission('director/showsql')) {
+ $links[] = $this->createShowSqlToggle();
+ }
+
+ if ($this->table instanceof FilterableByUsage) {
+ $parent->add($this->showUsageFilter($this->table));
+ }
+
+ if (! empty($links)) {
+ $parent->add($this->moreOptions($links));
+ }
+
+ return $this;
+ }
+
+ protected function createDownloadJsonLink()
+ {
+ return Link::create(
+ $this->translate('Download as JSON'),
+ $this->url->with('format', 'json'),
+ null,
+ ['target' => '_blank']
+ );
+ }
+
+ protected function createShowSqlToggle()
+ {
+ if ($this->url->getParam('format') === 'sql') {
+ $link = Link::create(
+ $this->translate('Hide SQL'),
+ $this->url->without('format')
+ );
+ } else {
+ $link = Link::create(
+ $this->translate('Show SQL'),
+ $this->url->with('format', 'sql')
+ );
+ }
+
+ return $link;
+ }
+
+ protected function showUsageFilter(FilterableByUsage $table)
+ {
+ $active = $this->url->getParam('usage', 'all');
+ $links = [
+ Link::create($this->translate('all'), $this->url->without('usage')),
+ Link::create($this->translate('used'), $this->url->with('usage', 'used')),
+ Link::create($this->translate('unused'), $this->url->with('usage', 'unused')),
+ ];
+
+ if ($active === 'used') {
+ $table->showOnlyUsed();
+ } elseif ($active === 'unused') {
+ $table->showOnlyUnUsed();
+ }
+
+ $options = $this->ul(
+ $this->li([
+ Link::create(
+ sprintf($this->translate('Usage (%s)'), $active),
+ '#',
+ null,
+ [
+ 'class' => 'icon-sitemap'
+ ]
+ ),
+ $subUl = Html::tag('ul')
+ ]),
+ ['class' => 'nav']
+ );
+
+ foreach ($links as $link) {
+ $subUl->add($this->li($link));
+ }
+
+ return $options;
+ }
+
+ protected function moreOptions($links)
+ {
+ $options = $this->ul(
+ $this->li([
+ // TODO: extend link for dropdown-toggle from Web 2, doesn't
+ // seem to work: [..], null, ['class' => 'dropdown-toggle']
+ Link::create(Icon::create('down-open'), '#'),
+ $subUl = Html::tag('ul')
+ ]),
+ ['class' => 'nav']
+ );
+
+ foreach ($links as $link) {
+ $subUl->add($this->li($link));
+ }
+
+ return $options;
+ }
+
+ protected function ulLi($content)
+ {
+ return $this->ul($this->li($content));
+ }
+
+ protected function ul($content, $attributes = null)
+ {
+ return Html::tag('ul', $attributes, $content);
+ }
+
+ protected function li($content)
+ {
+ return Html::tag('li', null, $content);
+ }
+
+ protected function hasPermission($permission)
+ {
+ return $this->auth->hasPermission($permission);
+ }
+}
diff --git a/library/Director/Web/Widget/BackgroundDaemonDetails.php b/library/Director/Web/Widget/BackgroundDaemonDetails.php
new file mode 100644
index 0000000..b4c33dd
--- /dev/null
+++ b/library/Director/Web/Widget/BackgroundDaemonDetails.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Widget\NameValueTable;
+use gipfl\Translation\TranslationHelper;
+use gipfl\Web\Widget\Hint;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Director\Daemon\RunningDaemonInfo;
+use Icinga\Util\Format;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\Table;
+
+class BackgroundDaemonDetails extends BaseHtmlElement
+{
+ use TranslationHelper;
+
+ protected $tag = 'div';
+
+ /** @var RunningDaemonInfo */
+ protected $info;
+
+ /** @var \stdClass TODO: get rid of this */
+ protected $daemon;
+
+ public function __construct(RunningDaemonInfo $info, $daemon)
+ {
+ $this->info = $info;
+ $this->daemon = $daemon;
+ }
+
+ protected function assemble()
+ {
+ $info = $this->info;
+ if ($info->hasBeenStopped()) {
+ $this->add(Hint::error(Html::sprintf(
+ $this->translate(
+ 'Daemon has been stopped %s, was running with PID %s as %s@%s'
+ ),
+ // $info->getHexUuid(),
+ $this->timeAgo($info->getTimestampStopped() / 1000),
+ Html::tag('strong', (string) $info->getPid()),
+ Html::tag('strong', $info->getUsername()),
+ Html::tag('strong', $info->getFqdn())
+ )));
+ } elseif ($info->isOutdated()) {
+ $this->add(Hint::error(Html::sprintf(
+ $this->translate(
+ 'Daemon keep-alive is outdated, was last seen running with PID %s as %s@%s %s'
+ ),
+ // $info->getHexUuid(),
+ Html::tag('strong', (string) $info->getPid()),
+ Html::tag('strong', $info->getUsername()),
+ Html::tag('strong', $info->getFqdn()),
+ $this->timeAgo($info->getLastUpdate() / 1000)
+ )));
+ } else {
+ $this->add(Hint::ok(Html::sprintf(
+ $this->translate(
+ 'Daemon is running with PID %s as %s@%s, last refresh happened %s'
+ ),
+ // $info->getHexUuid(),
+ Html::tag('strong', (string)$info->getPid()),
+ Html::tag('strong', $info->getUsername()),
+ Html::tag('strong', $info->getFqdn()),
+ $this->timeAgo($info->getLastUpdate() / 1000)
+ )));
+ $details = new NameValueTable();
+ $details->addNameValuePairs([
+ $this->translate('Startup Time') => DateFormatter::formatDateTime($info->getTimestampStarted() / 1000),
+ $this->translate('PID') => $info->getPid(),
+ $this->translate('Username') => $info->getUsername(),
+ $this->translate('FQDN') => $info->getFqdn(),
+ $this->translate('Running with systemd') => $info->isRunningWithSystemd()
+ ? $this->translate('yes')
+ : $this->translate('no'),
+ $this->translate('Binary') => $info->getBinaryPath()
+ . ($info->binaryRealpathDiffers() ? ' -> ' . $info->getBinaryRealpath() : ''),
+ $this->translate('PHP Binary') => $info->getPhpBinaryPath()
+ . ($info->phpBinaryRealpathDiffers() ? ' -> ' . $info->getPhpBinaryRealpath() : ''),
+ $this->translate('PHP Version') => $info->getPhpVersion(),
+ $this->translate('PHP Integer') => $info->has64bitIntegers()
+ ? '64bit'
+ : Html::sprintf(
+ '%sbit (%s)',
+ $info->getPhpIntegerSize() * 8,
+ Html::tag('span', ['class' => 'error'], $this->translate('unsupported'))
+ ),
+ ]);
+ $this->add($details);
+
+ $this->add(Html::tag('h2', $this->translate('Process List')));
+ if (\is_string($this->daemon->process_info)) {
+ // from DB:
+ $processes = \json_decode($this->daemon->process_info);
+ } else {
+ // via RPC:
+ $processes = $this->daemon->process_info;
+ }
+ $table = new Table();
+ $table->add(Html::tag('thead', Html::tag('tr', Html::wrapEach([
+ 'PID',
+ 'Command',
+ 'Memory'
+ ], 'th'))));
+ $table->setAttribute('class', 'common-table');
+ foreach ($processes as $pid => $process) {
+ $table->add($table::row([
+ [
+ Icon::create($process->running ? 'ok' : 'warning-empty'),
+ ' ',
+ $pid
+ ],
+ Html::tag('pre', $process->command),
+ $process->memory === false ? 'n/a' : Format::bytes($process->memory->rss)
+ ]));
+ }
+ $this->add($table);
+ }
+ }
+
+ protected function timeAgo($time)
+ {
+ return Html::tag('span', [
+ 'class' => 'time-ago',
+ 'title' => DateFormatter::formatDateTime($time)
+ ], DateFormatter::timeAgo($time));
+ }
+}
diff --git a/library/Director/Web/Widget/BranchedObjectHint.php b/library/Director/Web/Widget/BranchedObjectHint.php
new file mode 100644
index 0000000..ec16094
--- /dev/null
+++ b/library/Director/Web/Widget/BranchedObjectHint.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\Web\Widget\Hint;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchedObject;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+
+class BranchedObjectHint extends HtmlDocument
+{
+ use TranslationHelper;
+
+ public function __construct(Branch $branch, Auth $auth, BranchedObject $object = null)
+ {
+ if (! $branch->isBranch()) {
+ return;
+ }
+ $hook = Branch::requireHook();
+
+ $name = $branch->getName();
+ if (substr($name, 0, 1) === '/') {
+ $label = $this->translate('this configuration branch');
+ } else {
+ $label = $name;
+ }
+ $link = $hook->linkToBranch($branch, $auth, $label);
+ if ($object === null) {
+ $this->add(Hint::info(Html::sprintf($this->translate(
+ 'This object will be created in %s. It will not be part of any deployment'
+ . ' unless being merged'
+ ), $link)));
+ return;
+ }
+
+ if (! $object->hasBeenTouchedByBranch()) {
+ $this->add(Hint::info(Html::sprintf($this->translate(
+ 'Your changes will be stored in %s. The\'ll not be part of any deployment'
+ . ' unless being merged'
+ ), $link)));
+ return;
+ }
+
+ if ($object->hasBeenDeletedByBranch()) {
+ throw new NotFoundError('No such object available');
+ // Alternative, requires hiding other actions:
+ // $this->add(Hint::info(Html::sprintf(
+ // $this->translate('This object has been deleted in %s'),
+ // $link
+ // )));
+ } elseif ($object->hasBeenCreatedByBranch()) {
+ $this->add(Hint::info(Html::sprintf(
+ $this->translate('This object has been created in %s'),
+ $link
+ )));
+ } else {
+ $this->add(Hint::info(Html::sprintf(
+ $this->translate('This object has modifications visible only in %s'),
+ // TODO: Also link to object modifications
+ // $hook->linkToBranchedObject($this->translate('modifications'), $branch, $object, $auth),
+ $link
+ )));
+ }
+ }
+}
diff --git a/library/Director/Web/Widget/BranchedObjectsHint.php b/library/Director/Web/Widget/BranchedObjectsHint.php
new file mode 100644
index 0000000..d689178
--- /dev/null
+++ b/library/Director/Web/Widget/BranchedObjectsHint.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\Web\Widget\Hint;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db\Branch\Branch;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+
+class BranchedObjectsHint extends HtmlDocument
+{
+ use TranslationHelper;
+
+ public function __construct(Branch $branch, Auth $auth)
+ {
+ if (! $branch->isBranch()) {
+ return;
+ }
+ $hook = Branch::requireHook();
+ $this->add(Hint::info(Html::sprintf(
+ $this->translate('Showing a branched view, with potential changes being visible only in this %s'),
+ $hook->linkToBranch($branch, $auth, $this->translate('configuration branch'))
+ )));
+ }
+}
diff --git a/library/Director/Web/Widget/Daemon/BackgroundDaemonState.php b/library/Director/Web/Widget/Daemon/BackgroundDaemonState.php
new file mode 100644
index 0000000..03e76b2
--- /dev/null
+++ b/library/Director/Web/Widget/Daemon/BackgroundDaemonState.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget\Daemon;
+
+use Icinga\Module\Director\Daemon\RunningDaemonInfo;
+use Icinga\Module\Director\Db;
+
+class BackgroundDaemonState
+{
+ protected $db;
+
+ /** @var RunningDaemonInfo[] */
+ protected $instances;
+
+ public function __construct(Db $db)
+ {
+ $this->db = $db;
+ }
+
+ public function isRunning()
+ {
+ foreach ($this->getInstances() as $instance) {
+ if ($instance->isRunning()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function getInstances()
+ {
+ if ($this->instances === null) {
+ $this->instances = $this->fetchInfo();
+ }
+
+ return $this->instances;
+ }
+
+ /**
+ * @return RunningDaemonInfo[]
+ */
+ protected function fetchInfo()
+ {
+ $db = $this->db->getDbAdapter();
+ $daemons = $db->fetchAll(
+ $db->select()->from('director_daemon_info')->order('fqdn')->order('username')->order('pid')
+ );
+
+ $result = [];
+ foreach ($daemons as $info) {
+ $result[] = new RunningDaemonInfo($info);
+ }
+
+ return $result;
+ }
+}
diff --git a/library/Director/Web/Widget/DeployedConfigInfoHeader.php b/library/Director/Web/Widget/DeployedConfigInfoHeader.php
new file mode 100644
index 0000000..0e841f3
--- /dev/null
+++ b/library/Director/Web/Widget/DeployedConfigInfoHeader.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use Icinga\Module\Director\Db\Branch\Branch;
+use ipl\Html\HtmlDocument;
+use Icinga\Module\Director\Core\DeploymentApiInterface;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Forms\DeployConfigForm;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\NameValueTable;
+
+class DeployedConfigInfoHeader extends HtmlDocument
+{
+ use TranslationHelper;
+
+ /** @var IcingaConfig */
+ protected $config;
+
+ /** @var int */
+ protected $deploymentId;
+
+ /** @var Db */
+ protected $db;
+
+ /** @var DeploymentApiInterface */
+ protected $api;
+
+ /** @var Branch */
+ protected $branch;
+
+ public function __construct(
+ IcingaConfig $config,
+ Db $db,
+ DeploymentApiInterface $api,
+ Branch $branch,
+ $deploymentId = null
+ ) {
+ $this->config = $config;
+ $this->db = $db;
+ $this->api = $api;
+ $this->branch = $branch;
+ if ($deploymentId) {
+ $this->deploymentId = (int) $deploymentId;
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Zend_Form_Exception
+ */
+ protected function assemble()
+ {
+ $config = $this->config;
+ if ($this->branch->isBranch()) {
+ $deployForm = null;
+ } else {
+ $deployForm = DeployConfigForm::load()
+ ->setDb($this->db)
+ ->setApi($this->api)
+ ->setChecksum($config->getHexChecksum())
+ ->setDeploymentId($this->deploymentId)
+ ->setAttrib('class', 'inline')
+ ->handleRequest();
+ }
+
+ $links = new NameValueTable();
+ $links->addNameValueRow(
+ $this->translate('Actions'),
+ [
+ $deployForm,
+ Html::tag('br'),
+ Link::create(
+ $this->translate('Last related activity'),
+ 'director/config/activity',
+ ['checksum' => $config->getLastActivityHexChecksum()],
+ ['class' => 'icon-clock', 'data-base-target' => '_next']
+ ),
+ Html::tag('br'),
+ Link::create(
+ $this->translate('Diff with other config'),
+ 'director/config/diff',
+ ['left' => $config->getHexChecksum()],
+ ['class' => 'icon-flapping', 'data-base-target' => '_self']
+ )
+ ]
+ )->addNameValueRow(
+ $this->translate('Statistics'),
+ sprintf(
+ $this->translate('%d files rendered in %0.2fs'),
+ count($config->getFiles()),
+ $config->getDuration() / 1000
+ )
+ );
+
+ $this->add($links);
+ }
+}
diff --git a/library/Director/Web/Widget/DeploymentInfo.php b/library/Director/Web/Widget/DeploymentInfo.php
new file mode 100644
index 0000000..110200f
--- /dev/null
+++ b/library/Director/Web/Widget/DeploymentInfo.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use ipl\Html\HtmlDocument;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\DirectorDeploymentLog;
+use Icinga\Module\Director\StartupLogRenderer;
+use Icinga\Util\Format;
+use Icinga\Web\Request;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\NameValueTable;
+use gipfl\IcingaWeb2\Widget\Tabs;
+
+class DeploymentInfo extends HtmlDocument
+{
+ use TranslationHelper;
+
+ /** @var DirectorDeploymentLog */
+ protected $deployment;
+
+ /** @var IcingaConfig */
+ protected $config;
+
+ /**
+ * DeploymentInfo constructor.
+ * @param DirectorDeploymentLog $deployment
+ */
+ public function __construct(DirectorDeploymentLog $deployment)
+ {
+ $this->deployment = $deployment;
+ if ($deployment->get('config_checksum') !== null) {
+ $this->config = IcingaConfig::load(
+ $deployment->get('config_checksum'),
+ $deployment->getConnection()
+ );
+ }
+ }
+
+ /**
+ * @param Auth $auth
+ * @param Request $request
+ * @return Tabs
+ */
+ public function getTabs(Auth $auth, Request $request)
+ {
+ $dep = $this->deployment;
+ $tabs = new Tabs();
+ $tabs->add('deployment', array(
+ 'label' => $this->translate('Deployment'),
+ 'url' => $request->getUrl()
+ ))->activate('deployment');
+
+ if ($dep->config_checksum !== null && $auth->hasPermission('director/showconfig')) {
+ $tabs->add('config', array(
+ 'label' => $this->translate('Config'),
+ 'url' => 'director/config/files',
+ 'urlParams' => array(
+ 'checksum' => $this->config->getHexChecksum(),
+ 'deployment_id' => $dep->id
+ )
+ ));
+ }
+
+ return $tabs;
+ }
+
+ protected function createInfoTable()
+ {
+ $dep = $this->deployment;
+ $table = new NameValueTable();
+ $table->addNameValuePairs([
+ $this->translate('Deployment time') => $dep->start_time,
+ $this->translate('Sent to') => $dep->peer_identity,
+ ]);
+ if ($this->config !== null) {
+ $table->addNameValuePairs([
+ $this->translate('Configuration') => $this->getConfigDetails(),
+ $this->translate('Duration') => $this->getDurationInfo(),
+ ]);
+ }
+ $table->addNameValuePairs([
+ $this->translate('Stage name') => $dep->stage_name,
+ $this->translate('Startup') => $this->getStartupInfo()
+ ]);
+
+ return $table;
+ }
+
+ protected function getDurationInfo()
+ {
+ return sprintf(
+ $this->translate('Rendered in %0.2fs, deployed in %0.2fs'),
+ $this->config->getDuration() / 1000,
+ $this->deployment->duration_dump / 1000
+ );
+ }
+
+ protected function getConfigDetails()
+ {
+ $cfg = $this->config;
+ $dep = $this->deployment;
+
+ return [
+ Link::create(
+ sprintf($this->translate('%d files'), $cfg->getFileCount()),
+ 'director/config/files',
+ [
+ 'checksum' => $cfg->getHexChecksum(),
+ 'deployment_id' => $dep->id
+ ]
+ ),
+ ', ',
+ sprintf(
+ $this->translate('%d objects, %d templates, %d apply rules'),
+ $cfg->getObjectCount(),
+ $cfg->getTemplateCount(),
+ $cfg->getApplyCount()
+ ),
+ ', ',
+ Format::bytes($cfg->getSize())
+ ];
+ }
+
+ protected function getStartupInfo()
+ {
+ $dep = $this->deployment;
+ if ($dep->startup_succeeded === null) {
+ if ($dep->stage_collected === null) {
+ return [$this->translate('Unknown, still waiting for config check outcome'), new Icon('spinner')];
+ } else {
+ return [$this->translate('Unknown, failed to collect related information'), new Icon('help')];
+ }
+ } elseif ($dep->startup_succeeded === 'y') {
+ return $this->colored('green', [$this->translate('Succeeded'), new Icon('ok')]);
+ } else {
+ return $this->colored('red', [$this->translate('Failed'), new Icon('cancel')]);
+ }
+ }
+
+ protected function colored($color, array $content)
+ {
+ return Html::tag('div', ['style' => "color: $color;"], $content)->setSeparator(' ');
+ }
+
+ public function render()
+ {
+ $this->add($this->createInfoTable());
+ if ($this->deployment->get('startup_succeeded') !== null) {
+ $this->addStartupLog();
+ }
+
+ return parent::render();
+ }
+
+ protected function addStartupLog()
+ {
+ $this->add(Html::tag('h2', null, $this->translate('Startup Log')));
+ $this->add(
+ Html::tag('pre', [
+ 'class' => 'logfile'
+ ], new StartupLogRenderer($this->deployment))
+ );
+ }
+}
diff --git a/library/Director/Web/Widget/Documentation.php b/library/Director/Web/Widget/Documentation.php
new file mode 100644
index 0000000..8665e30
--- /dev/null
+++ b/library/Director/Web/Widget/Documentation.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use ipl\Html\Html;
+
+class Documentation
+{
+ use TranslationHelper;
+
+ /** @var ApplicationBootstrap */
+ protected $app;
+
+ /** @var Auth */
+ protected $auth;
+
+ public function __construct(ApplicationBootstrap $app, Auth $auth)
+ {
+ $this->app = $app;
+ $this->auth = $auth;
+ }
+
+ public static function link($label, $module, $chapter, $title = null)
+ {
+ $doc = new static(Icinga::app(), Auth::getInstance());
+ return $doc->getModuleLink($label, $module, $chapter, $title);
+ }
+
+ public function getModuleLink($label, $module, $chapter, $title = null)
+ {
+ if ($title !== null) {
+ $title = sprintf(
+ $this->translate('Click to read our documentation: %s'),
+ $title
+ );
+ }
+ $linkToGitHub = false;
+ $baseParams = [
+ 'class' => 'icon-book',
+ 'title' => $title,
+ ];
+ if ($this->hasAccessToDocumentationModule()) {
+ return Link::create(
+ $label,
+ $this->getDirectorDocumentationUrl($chapter),
+ null,
+ ['data-base-target' => '_next'] + $baseParams
+ );
+ }
+
+ $baseParams['target'] = '_blank';
+ if ($linkToGitHub) {
+ return Html::tag('a', [
+ 'href' => $this->githubDocumentationUrl($module, $chapter),
+ ] + $baseParams, $label);
+ }
+
+ return Html::tag('a', [
+ 'href' => $this->icingaDocumentationUrl($module, $chapter),
+ ] + $baseParams, $label);
+ }
+
+ protected function getDirectorDocumentationUrl($chapter)
+ {
+ return 'doc/module/director/chapter/'
+ . \preg_replace('/^\d+-/', '', \rawurlencode($chapter));
+ }
+
+ protected function githubDocumentationUrl($module, $chapter)
+ {
+ return sprintf(
+ "https://github.com/Icinga/icingaweb2-module-%s/blob/master/doc/%s.md",
+ \rawurlencode($module),
+ \rawurlencode($chapter)
+ );
+ }
+
+ protected function icingaDocumentationUrl($module, $chapter)
+ {
+ return sprintf(
+ 'https://icinga.com/docs/%s/latest/doc/%s/',
+ \rawurlencode($module),
+ \rawurlencode($chapter)
+ );
+ }
+
+ protected function hasAccessToDocumentationModule()
+ {
+ return $this->app->getModuleManager()->hasLoaded('doc')
+ && $this->auth->hasPermission('module/doc');
+ }
+}
diff --git a/library/Director/Web/Widget/HealthCheckPluginOutput.php b/library/Director/Web/Widget/HealthCheckPluginOutput.php
new file mode 100644
index 0000000..83ac102
--- /dev/null
+++ b/library/Director/Web/Widget/HealthCheckPluginOutput.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlString;
+use gipfl\Translation\TranslationHelper;
+use Icinga\Module\Director\CheckPlugin\PluginState;
+use Icinga\Module\Director\Health;
+
+class HealthCheckPluginOutput extends HtmlDocument
+{
+ use TranslationHelper;
+
+ /** @var Health */
+ protected $health;
+
+ /** @var PluginState */
+ protected $state;
+
+ public function __construct(Health $health)
+ {
+ $this->state = new PluginState('OK');
+ $this->health = $health;
+ $this->process();
+ }
+
+ protected function process()
+ {
+ $checks = $this->health->getAllChecks();
+
+ foreach ($checks as $check) {
+ $this->add([
+ $title = Html::tag('h1', $check->getName()),
+ $ul = Html::tag('ul', ['class' => 'health-check-result'])
+ ]);
+
+ $problems = $check->getProblemSummary();
+ if (! empty($problems)) {
+ $badges = Html::tag('span', ['class' => 'title-badges']);
+ foreach ($problems as $state => $count) {
+ $badges->add(Html::tag('span', [
+ 'class' => ['badge', 'state-' . strtolower($state)],
+ 'title' => sprintf(
+ $this->translate('%s: %d'),
+ $this->translate($state),
+ $count
+ ),
+ ], $count));
+ }
+ $title->add($badges);
+ }
+
+ foreach ($check->getResults() as $result) {
+ $state = $result->getState()->getName();
+ $ul->add(Html::tag('li', [
+ 'class' => 'state state-' . strtolower($state)
+ ], $this->highlightNames($result->getOutput()))->setSeparator(' '));
+ }
+ $this->state->raise($check->getState());
+ }
+ }
+
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ protected function colorizeState($state)
+ {
+ return Html::tag('span', ['class' => 'badge state-' . strtolower($state)], $state);
+ }
+
+ protected function highlightNames($string)
+ {
+ $string = Html::escape($string);
+ return new HtmlString(preg_replace_callback(
+ "/'([^']+)'/",
+ [$this, 'highlightName'],
+ $string
+ ));
+ }
+
+ protected function highlightName($match)
+ {
+ return '"' . Html::tag('strong', $match[1]) . '"';
+ }
+
+ protected function getColorized($match)
+ {
+ return $this->colorizeState($match[1]);
+ }
+}
diff --git a/library/Director/Web/Widget/IcingaConfigDiff.php b/library/Director/Web/Widget/IcingaConfigDiff.php
new file mode 100644
index 0000000..800f1d9
--- /dev/null
+++ b/library/Director/Web/Widget/IcingaConfigDiff.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\Diff\HtmlRenderer\SideBySideDiff;
+use gipfl\Diff\PhpDiff;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Html\ValidHtml;
+
+class IcingaConfigDiff extends HtmlDocument
+{
+ public function __construct(IcingaConfig $left, IcingaConfig $right)
+ {
+ foreach (static::getDiffs($left, $right) as $filename => $diff) {
+ $this->add([
+ Html::tag('h3', $filename),
+ $diff
+ ]);
+ }
+ }
+
+ /**
+ * @param IcingaConfig $oldConfig
+ * @param IcingaConfig $newConfig
+ * @return ValidHtml[]
+ */
+ public static function getDiffs(IcingaConfig $oldConfig, IcingaConfig $newConfig)
+ {
+ $oldFileNames = $oldConfig->getFileNames();
+ $newFileNames = $newConfig->getFileNames();
+
+ $fileNames = array_merge($oldFileNames, $newFileNames);
+
+ $diffs = [];
+ foreach ($fileNames as $filename) {
+ if (in_array($filename, $oldFileNames)) {
+ $left = $oldConfig->getFile($filename)->getContent();
+ } else {
+ $left = '';
+ }
+
+ if (in_array($filename, $newFileNames)) {
+ $right = $newConfig->getFile($filename)->getContent();
+ } else {
+ $right = '';
+ }
+ if ($left === $right) {
+ continue;
+ }
+
+ $diffs[$filename] = new SideBySideDiff(new PhpDiff($left, $right));
+ }
+
+ return $diffs;
+ }
+}
diff --git a/library/Director/Web/Widget/IcingaObjectInspection.php b/library/Director/Web/Widget/IcingaObjectInspection.php
new file mode 100644
index 0000000..61f3567
--- /dev/null
+++ b/library/Director/Web/Widget/IcingaObjectInspection.php
@@ -0,0 +1,254 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\NameValueTable;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\PlainObjectRenderer;
+use Icinga\Module\Director\Web\Table\DbHelper;
+use stdClass;
+
+class IcingaObjectInspection extends BaseHtmlElement
+{
+ use DbHelper;
+ use TranslationHelper;
+
+ protected $tag = 'div';
+
+ /** @var Db */
+ protected $db;
+
+ /** @var stdClass */
+ protected $object;
+
+ public function __construct(stdClass $object, Db $db)
+ {
+ $this->object = $object;
+ $this->db = $db;
+ }
+
+ /**
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function assemble()
+ {
+ $attrs = $this->object->attrs;
+ if (isset($attrs->source_location)) {
+ $this->renderSourceLocation($attrs->source_location);
+ }
+ if (isset($attrs->last_check_result)) {
+ $this->renderLastCheckResult($attrs->last_check_result);
+ }
+
+ $this->renderObjectAttributes($attrs);
+ // $this->add(Html::tag('pre', null, PlainObjectRenderer::render($this->object)));
+ }
+
+ /**
+ * @param $result
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function renderLastCheckResult($result)
+ {
+ $this->add(Html::tag('h2', null, $this->translate('Last Check Result')));
+ $this->renderCheckResultDetails($result);
+ if (property_exists($result, 'command')) {
+ $this->renderExecutedCommand($result->command);
+ }
+ }
+
+ /**
+ * @param array|string $command
+ *
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function renderExecutedCommand($command)
+ {
+ if (is_array($command)) {
+ $command = implode(' ', array_map('escapeshellarg', $command));
+ }
+ $this->add([
+ Html::tag('h3', null, 'Executed Command'),
+ $this->formatConsole($command)
+ ]);
+ }
+
+ protected function renderCheckResultDetails($result)
+ {
+ }
+
+ /**
+ * @param $attrs
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function renderObjectAttributes($attrs)
+ {
+ $blacklist = [
+ 'last_check_result',
+ 'source_location',
+ 'templates',
+ ];
+
+ $linked = [
+ 'check_command',
+ 'groups',
+ ];
+
+ $info = new NameValueTable();
+ foreach ($attrs as $key => $value) {
+ if (in_array($key, $blacklist)) {
+ continue;
+ }
+ if ($key === 'groups') {
+ $info->addNameValueRow($key, $this->linkGroups($value));
+ } elseif (in_array($key, $linked)) {
+ $info->addNameValueRow($key, $this->renderLinkedObject($key, $value));
+ } else {
+ $info->addNameValueRow($key, PlainObjectRenderer::render($value));
+ }
+ }
+
+ $this->add([
+ Html::tag('h2', null, 'Object Properties'),
+ $info
+ ]);
+ }
+
+ /**
+ * @param $key
+ * @param $objectName
+ * @return Link|Link[]
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ protected function renderLinkedObject($key, $objectName)
+ {
+ $keys = [
+ 'check_command' => ['CheckCommand', 'CheckCommands'],
+ 'event_command' => ['EventCommand', 'EventCommands'],
+ 'notification_command' => ['NotificationCommand', 'NotificationCommands'],
+ ];
+ $type = $keys[$key];
+
+ if ($key === 'groups') {
+ return $this->linkGroups($objectName);
+ } else {
+ $singular = $type[0];
+ $plural = $type[1];
+
+ return Link::create($objectName, 'director/inspect/object', [
+ 'type' => $singular,
+ 'plural' => $plural,
+ 'name' => $objectName
+ ]);
+ }
+ }
+
+ /**
+ * @param $groups
+ * @return Link[]
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ protected function linkGroups($groups)
+ {
+ if ($groups === null) {
+ return [];
+ }
+
+ $singular = $this->object->type . 'Group';
+ $plural = $singular . "s";
+
+ $links = [];
+
+ foreach ($groups as $name) {
+ $links[] = Link::create($name, 'director/inspect/object', [
+ 'type' => $singular,
+ 'plural' => $plural,
+ 'name' => $name
+ ]);
+ }
+
+ return $links;
+ }
+
+ /**
+ * @param stdClass $source
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function renderSourceLocation(stdClass $source)
+ {
+ $findRelative = 'api/packages/director';
+ $this->add(Html::tag('h2')->add('Source Location'));
+ $pos = strpos($source->path, $findRelative);
+
+ if (false === $pos) {
+ $this->add(Html::tag('p', null, Html::sprintf(
+ 'The configuration for this object has not been rendered by'
+ . ' Icinga Director. You can find it on line %s in %s.',
+ Html::tag('strong', null, $source->first_line),
+ Html::tag('strong', null, $source->path)
+ )));
+ } else {
+ $relativePath = substr($source->path, $pos + strlen($findRelative) + 1);
+ $parts = explode('/', $relativePath);
+ $stageName = array_shift($parts);
+ $relativePath = implode('/', $parts);
+ $source->director_relative = $relativePath;
+ $deployment = $this->loadDeploymentForStage($stageName);
+
+ $this->add(Html::tag('p')->add(Html::sprintf(
+ 'The configuration for this object has been rendered by Icinga'
+ . ' Director %s to %s',
+ DateFormatter::timeAgo(strtotime($deployment->start_time, false)),
+ $this->linkToSourceLocation($deployment, $source)
+ )));
+ }
+ }
+
+ protected function loadDeploymentForStage($stageName)
+ {
+ $db = $this->db->getDbAdapter();
+ $query = $db->select()->from(
+ ['dl' => 'director_deployment_log'],
+ ['id', 'start_time', 'config_checksum']
+ )->where('stage_name = ?', $stageName)->order('id DESC')->limit(1);
+
+ return $db->fetchRow($query);
+ }
+
+ /**
+ * @param $deployment
+ * @param $source
+ * @return Link
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ protected function linkToSourceLocation($deployment, $source)
+ {
+ $filename = $source->director_relative;
+
+ return Link::create(
+ sprintf('%s:%s', $filename, $source->first_line),
+ 'director/config/file',
+ [
+ 'config_checksum' => $this->getChecksum($deployment->config_checksum),
+ 'deployment_id' => $deployment->id,
+ 'backTo' => 'deployment',
+ 'file_path' => $filename,
+ 'highlight' => $source->first_line,
+ 'highlightSeverity' => 'ok'
+ ]
+ );
+ }
+
+ protected function formatConsole($output)
+ {
+ return Html::tag('pre', ['class' => 'logfile'], $output);
+ }
+}
diff --git a/library/Director/Web/Widget/ImportSourceDetails.php b/library/Director/Web/Widget/ImportSourceDetails.php
new file mode 100644
index 0000000..32eef7f
--- /dev/null
+++ b/library/Director/Web/Widget/ImportSourceDetails.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\Web\Widget\Hint;
+use ipl\Html\HtmlDocument;
+use Icinga\Module\Director\Forms\ImportCheckForm;
+use Icinga\Module\Director\Forms\ImportRunForm;
+use Icinga\Module\Director\Objects\ImportSource;
+use ipl\Html\Html;
+use gipfl\Translation\TranslationHelper;
+
+class ImportSourceDetails extends HtmlDocument
+{
+ use TranslationHelper;
+
+ protected $source;
+
+ public function __construct(ImportSource $source)
+ {
+ $this->source = $source;
+ }
+
+ protected function assemble()
+ {
+ $source = $this->source;
+ $description = $source->get('description');
+ if ($description !== null && strlen($description)) {
+ $this->add(Html::tag('p', null, $description));
+ }
+
+ switch ($source->get('import_state')) {
+ case 'unknown':
+ $this->add(Hint::warning($this->translate(
+ "It's currently unknown whether we are in sync with this Import Source."
+ . ' You should either check for changes or trigger a new Import Run.'
+ )));
+ break;
+ case 'in-sync':
+ $this->add(Hint::ok(sprintf(
+ $this->translate(
+ 'This Import Source was last found to be in sync at %s.'
+ ),
+ $source->last_attempt
+ )));
+ // TODO: check whether...
+ // - there have been imports since then, differing from former ones
+ // - there have been activities since then
+ break;
+ case 'pending-changes':
+ $this->add(Hint::warning($this->translate(
+ 'There are pending changes for this Import Source. You should trigger a new'
+ . ' Import Run.'
+ )));
+ break;
+ case 'failing':
+ $this->add(Hint::error(sprintf(
+ $this->translate(
+ 'This Import Source failed when last checked at %s: %s'
+ ),
+ $source->last_attempt,
+ $source->last_error_message
+ )));
+ break;
+ default:
+ $this->add(Hint::error(sprintf(
+ $this->translate('This Import Source has an invalid state: %s'),
+ $source->get('import_state')
+ )));
+ }
+
+ $this->add(
+ ImportCheckForm::load()
+ ->setImportSource($source)
+ ->handleRequest()
+ );
+ $this->add(
+ ImportRunForm::load()
+ ->setImportSource($source)
+ ->handleRequest()
+ );
+ }
+}
diff --git a/library/Director/Web/Widget/InspectPackages.php b/library/Director/Web/Widget/InspectPackages.php
new file mode 100644
index 0000000..f9b8864
--- /dev/null
+++ b/library/Director/Web/Widget/InspectPackages.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use ipl\Html\Html;
+use ipl\Html\Table;
+
+class InspectPackages
+{
+ use TranslationHelper;
+
+ /** @var Db */
+ protected $db;
+
+ /** @var string */
+ protected $baseUrl;
+
+ public function __construct(Db $db, $baseUrl)
+ {
+ $this->db = $db;
+ $this->baseUrl = $baseUrl;
+ }
+
+ public function getContent(IcingaEndpoint $endpoint = null, $package = null, $stage = null, $file = null)
+ {
+ if ($endpoint === null) {
+ return $this->getRootEndpoints();
+ } elseif ($package === null) {
+ return $this->getPackages($endpoint);
+ } elseif ($stage === null) {
+ return $this->getStages($endpoint, $package);
+ } elseif ($file === null) {
+ return $this->getFiles($endpoint, $package, $stage);
+ } else {
+ return $this->getFile($endpoint, $package, $stage, $file);
+ }
+ }
+
+ public function getTitle(IcingaEndpoint $endpoint = null, $package = null, $stage = null, $file = null)
+ {
+ if ($endpoint === null) {
+ return $this->translate('Endpoint in your Root Zone');
+ } elseif ($package === null) {
+ return \sprintf($this->translate('Packages on Endpoint: %s'), $endpoint->getObjectName());
+ } elseif ($stage === null) {
+ return \sprintf($this->translate('Stages in Package: %s'), $package);
+ } elseif ($file === null) {
+ return \sprintf($this->translate('Files in Stage: %s'), $stage);
+ } else {
+ return \sprintf($this->translate('File Content: %s'), $file);
+ }
+ }
+
+ public function getBreadCrumb(IcingaEndpoint $endpoint = null, $package = null, $stage = null)
+ {
+ $parts = [
+ 'endpoint' => $endpoint === null ? null : $endpoint->getObjectName(),
+ 'package' => $package,
+ 'stage' => $stage,
+ ];
+
+ $params = [];
+ // No root zone link for now:
+ // $result = [Link::create($this->translate('Root Zone'), $this->baseUrl)];
+ $result = [Html::tag('a', ['href' => '#'], $this->translate('Root Zone'))];
+ foreach ($parts as $name => $value) {
+ if ($value === null) {
+ break;
+ }
+ $params[$name] = $value;
+ $result[] = Link::create($value, $this->baseUrl, $params);
+ }
+
+ return Html::tag('ul', ['class' => 'breadcrumb'], Html::wrapEach($result, 'li'));
+ }
+
+ protected function getRootEndpoints()
+ {
+ $table = $this->prepareTable();
+ foreach ($this->db->getEndpointNamesInDeploymentZone() as $name) {
+ $table->add(Table::row([
+ Link::create($name, $this->baseUrl, [
+ 'endpoint' => $name,
+ ])
+ ]));
+ }
+
+ return $table;
+ }
+
+ protected function getPackages(IcingaEndpoint $endpoint)
+ {
+ $table = $this->prepareTable();
+ $api = $endpoint->api();
+ foreach ($api->getPackages() as $package) {
+ $table->add(Table::row([
+ Link::create($package->name, $this->baseUrl, [
+ 'endpoint' => $endpoint->getObjectName(),
+ 'package' => $package->name,
+ ])
+ ]));
+ }
+
+ return $table;
+ }
+
+ protected function getStages(IcingaEndpoint $endpoint, $packageName)
+ {
+ $table = $this->prepareTable();
+ $api = $endpoint->api();
+ foreach ($api->getPackages() as $package) {
+ if ($package->name !== $packageName) {
+ continue;
+ }
+ foreach ($package->stages as $stage) {
+ $label = [$stage];
+ if ($stage === $package->{'active-stage'}) {
+ $label[] = Html::tag('small', [' (', $this->translate('active'), ')']);
+ }
+
+ $table->add(Table::row([
+ Link::create($label, $this->baseUrl, [
+ 'endpoint' => $endpoint->getObjectName(),
+ 'package' => $package->name,
+ 'stage' => $stage
+ ])
+ ]));
+ }
+ }
+
+ return $table;
+ }
+
+ protected function getFiles(IcingaEndpoint $endpoint, $package, $stage)
+ {
+ $table = $this->prepareTable();
+ $table->getAttributes()->set('data-base-target', '_next');
+ foreach ($endpoint->api()->listStageFiles($stage, $package) as $filename) {
+ $table->add($table->row([
+ Link::create($filename, $this->baseUrl, [
+ 'endpoint' => $endpoint->getObjectName(),
+ 'package' => $package,
+ 'stage' => $stage,
+ 'file' => $filename
+ ])
+ ]));
+ }
+
+ return $table;
+ }
+
+ protected function getFile(IcingaEndpoint $endpoint, $package, $stage, $file)
+ {
+ return Html::tag('pre', $endpoint->api()->getStagedFile($stage, $file, $package));
+ }
+
+ protected function prepareTable($headerCols = [])
+ {
+ $table = new Table();
+ $table->addAttributes([
+ 'class' => ['common-table', 'table-row-selectable'],
+ 'data-base-target' => '_self'
+ ]);
+ if (! empty($headerCols)) {
+ $table->add($table::row($headerCols, null, 'th'));
+ }
+
+ return $table;
+ }
+}
diff --git a/library/Director/Web/Widget/JobDetails.php b/library/Director/Web/Widget/JobDetails.php
new file mode 100644
index 0000000..3a530a2
--- /dev/null
+++ b/library/Director/Web/Widget/JobDetails.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Date\DateFormatter;
+use ipl\Html\HtmlDocument;
+use Icinga\Module\Director\Objects\DirectorJob;
+use ipl\Html\Html;
+use gipfl\Translation\TranslationHelper;
+
+class JobDetails extends HtmlDocument
+{
+ use TranslationHelper;
+
+ /**
+ * JobDetails constructor.
+ * @param DirectorJob $job
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function __construct(DirectorJob $job)
+ {
+ $runInterval = $job->get('run_interval');
+ if ($job->hasBeenDisabled()) {
+ $this->add(Hint::error(sprintf(
+ $this->translate(
+ 'This job would run every %ds. It has been disabled and will'
+ . ' therefore not be executed as scheduled'
+ ),
+ $runInterval
+ )));
+ } else {
+ //$class = $job->job(); echo $class::getDescription()
+ $msg = $job->isPending()
+ ? sprintf(
+ $this->translate('This job runs every %ds and is currently pending'),
+ $runInterval
+ )
+ : sprintf(
+ $this->translate('This job runs every %ds.'),
+ $runInterval
+ );
+ $this->add(Html::tag('p', null, $msg));
+ }
+
+ $tsLastAttempt = $job->get('ts_last_attempt');
+ if ($tsLastAttempt) {
+ $ts = \strtotime($tsLastAttempt);
+ $timeAgo = Html::tag('span', [
+ 'class' => 'time-ago',
+ 'title' => DateFormatter::formatDateTime($ts)
+ ], DateFormatter::timeAgo($ts));
+ if ($job->get('last_attempt_succeeded') === 'y') {
+ $this->add(Hint::ok(Html::sprintf(
+ $this->translate('The last attempt succeeded %s'),
+ $timeAgo
+ )));
+ } else {
+ $this->add(Hint::error(Html::sprintf(
+ $this->translate('The last attempt failed %s: %s'),
+ $timeAgo,
+ $job->get('last_error_message')
+ )));
+ }
+ } else {
+ $this->add(Hint::warning($this->translate('This job has not been executed yet')));
+ }
+ }
+}
diff --git a/library/Director/Web/Widget/ListItem.php b/library/Director/Web/Widget/ListItem.php
new file mode 100644
index 0000000..ec326cc
--- /dev/null
+++ b/library/Director/Web/Widget/ListItem.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\ValidHtml;
+
+class ListItem extends BaseHtmlElement
+{
+ protected $contentSeparator = "\n";
+
+ /**
+ * @param ValidHtml|array|string $content
+ * @param Attributes|array $attributes
+ *
+ * @return $this
+ */
+ public function addItem($content, $attributes = null)
+ {
+ return $this->add(
+ Html::tag('li', $attributes, $content)
+ );
+ }
+}
diff --git a/library/Director/Web/Widget/NotInBranchedHint.php b/library/Director/Web/Widget/NotInBranchedHint.php
new file mode 100644
index 0000000..222934b
--- /dev/null
+++ b/library/Director/Web/Widget/NotInBranchedHint.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\Web\Widget\Hint;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db\Branch\Branch;
+use ipl\Html\Html;
+
+class NotInBranchedHint extends Hint
+{
+ use TranslationHelper;
+
+ public function __construct($forbiddenAction, Branch $branch, Auth $auth)
+ {
+ parent::__construct(Html::sprintf(
+ $this->translate('%s is not available while being in a Configuration Branch: %s'),
+ $forbiddenAction,
+ Branch::requireHook()->linkToBranch($branch, $auth, $branch->getName())
+ ), 'info');
+ }
+}
diff --git a/library/Director/Web/Widget/OrderedList.php b/library/Director/Web/Widget/OrderedList.php
new file mode 100644
index 0000000..8f888de
--- /dev/null
+++ b/library/Director/Web/Widget/OrderedList.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+class OrderedList extends AbstractList
+{
+ protected $tag = 'ol';
+}
diff --git a/library/Director/Web/Widget/ShowConfigFile.php b/library/Director/Web/Widget/ShowConfigFile.php
new file mode 100644
index 0000000..77d32cf
--- /dev/null
+++ b/library/Director/Web/Widget/ShowConfigFile.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use ipl\Html\HtmlDocument;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigFile;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+
+class ShowConfigFile extends HtmlDocument
+{
+ use TranslationHelper;
+
+ protected $file;
+
+ protected $highlight;
+
+ protected $highlightSeverity;
+
+ public function __construct(
+ IcingaConfigFile $file,
+ $highlight = null,
+ $highlightSeverity = null
+ ) {
+ $this->file = $file;
+ $this->highlight = $highlight;
+ $this->highlightSeverity = $highlightSeverity;
+ }
+
+ /**
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function assemble()
+ {
+ $source = $this->linkObjects(Html::escape($this->file->getContent()));
+ if ($this->highlight) {
+ $source = $this->highlight(
+ $source,
+ $this->highlight,
+ $this->highlightSeverity
+ );
+ }
+
+ $this->add(Html::tag(
+ 'pre',
+ ['class' => 'generated-config'],
+ new HtmlString($source)
+ ));
+ }
+
+ /**
+ * @param $match
+ * @return string
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ protected function linkObject($match)
+ {
+ if ($match[2] === 'Service') {
+ return $match[0];
+ }
+ $controller = $match[2];
+
+ if ($match[2] === 'CheckCommand') {
+ $controller = 'command';
+ }
+
+ $name = $this->decode($match[3]);
+ return sprintf(
+ '%s %s &quot;%s&quot; {',
+ $match[1],
+ $match[2],
+ Link::create(
+ $name,
+ 'director/' . $controller,
+ ['name' => $name],
+ ['data-base-target' => '_next']
+ )
+ );
+ }
+
+ protected function decode($str)
+ {
+ return htmlspecialchars_decode($str, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5);
+ }
+
+ protected function linkObjects($config)
+ {
+ $pattern = '/^(object|template)\s([A-Z][A-Za-z]*?)\s&quot;(.+?)&quot;\s{/m';
+
+ return preg_replace_callback(
+ $pattern,
+ [$this, 'linkObject'],
+ $config
+ );
+ }
+
+ protected function highlight($what, $line, $severity)
+ {
+ $lines = explode("\n", $what);
+ $lines[$line - 1] = '<span class="highlight ' . $severity . '">' . $lines[$line - 1] . '</span>';
+ return implode("\n", $lines);
+ }
+}
diff --git a/library/Director/Web/Widget/SyncRunDetails.php b/library/Director/Web/Widget/SyncRunDetails.php
new file mode 100644
index 0000000..408e8f6
--- /dev/null
+++ b/library/Director/Web/Widget/SyncRunDetails.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+use Icinga\Module\Director\Objects\DirectorActivityLog;
+use ipl\Html\HtmlDocument;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\SyncRun;
+use gipfl\IcingaWeb2\Link;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Widget\NameValueTable;
+use function sprintf;
+
+class SyncRunDetails extends NameValueTable
+{
+ use TranslationHelper;
+
+ const URL_ACTIVITIES = 'director/config/activities';
+
+ /** @var SyncRun */
+ protected $run;
+
+ public function __construct(SyncRun $run)
+ {
+ $this->run = $run;
+ $this->getAttributes()->add('data-base-target', '_next'); // eigentlich nur runSummary
+ $this->addNameValuePairs([
+ $this->translate('Start time') => $run->get('start_time'),
+ $this->translate('Duration') => sprintf('%.2fs', $run->get('duration_ms') / 1000),
+ $this->translate('Activity') => $this->runSummary($run)
+ ]);
+ }
+
+ /**
+ * @param SyncRun $run
+ * @return array
+ */
+ protected function runSummary(SyncRun $run)
+ {
+ $html = [];
+ $total = $run->countActivities();
+ if ($total === 0) {
+ $html[] = $this->translate('No changes have been made');
+ } else {
+ if ($total === 1) {
+ $html[] = $this->translate('One object has been modified');
+ } else {
+ $html[] = sprintf(
+ $this->translate('%s objects have been modified'),
+ $total
+ );
+ }
+
+ /** @var Db $db */
+ $db = $run->getConnection();
+ $formerId = $db->fetchActivityLogIdByChecksum($run->get('last_former_activity'));
+ if ($formerId === null) {
+ return $html;
+ }
+ $lastId = $db->fetchActivityLogIdByChecksum($run->get('last_related_activity'));
+
+ if ($formerId !== $lastId) {
+ $idRangeEx = sprintf(
+ 'id>%d&id<=%d',
+ $formerId,
+ $lastId
+ );
+ } else {
+ $idRangeEx = null;
+ }
+
+ $links = new HtmlDocument();
+ $links->setSeparator(', ');
+ $links->add([
+ $this->activitiesLink(
+ 'objects_created',
+ $this->translate('%d created'),
+ DirectorActivityLog::ACTION_CREATE,
+ $idRangeEx
+ ),
+ $this->activitiesLink(
+ 'objects_modified',
+ $this->translate('%d modified'),
+ DirectorActivityLog::ACTION_MODIFY,
+ $idRangeEx
+ ),
+ $this->activitiesLink(
+ 'objects_deleted',
+ $this->translate('%d deleted'),
+ DirectorActivityLog::ACTION_DELETE,
+ $idRangeEx
+ ),
+ ]);
+
+ if ($idRangeEx && count($links) > 1) {
+ $links->add(new Link(
+ $this->translate('Show all actions'),
+ self::URL_ACTIVITIES,
+ ['idRangeEx' => $idRangeEx]
+ ));
+ }
+
+ if (! $links->isEmpty()) {
+ $html[] = ': ';
+ $html[] = $links;
+ }
+ }
+
+ return $html;
+ }
+
+ protected function activitiesLink($key, $label, $action, $rangeFilter)
+ {
+ $count = $this->run->get($key);
+ if ($count > 0) {
+ if ($rangeFilter) {
+ return new Link(
+ sprintf($label, $count),
+ self::URL_ACTIVITIES,
+ ['action' => $action, 'idRangeEx' => $rangeFilter]
+ );
+ }
+
+ return sprintf($label, $count);
+ }
+
+ return null;
+ }
+}
diff --git a/library/Director/Web/Widget/UnorderedList.php b/library/Director/Web/Widget/UnorderedList.php
new file mode 100644
index 0000000..f01dbe3
--- /dev/null
+++ b/library/Director/Web/Widget/UnorderedList.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Widget;
+
+class UnorderedList extends AbstractList
+{
+ protected $tag = 'ul';
+}
diff --git a/library/Director/Web/Window.php b/library/Director/Web/Window.php
new file mode 100644
index 0000000..3415dd3
--- /dev/null
+++ b/library/Director/Web/Window.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Icinga\Module\Director\Web;
+
+use Icinga\Web\Window as WebWindow;
+
+class Window extends WebWindow
+{
+ public function __construct($id)
+ {
+ parent::__construct(\preg_replace('/_.+$/', '', $id));
+ }
+}
diff --git a/module.info b/module.info
new file mode 100644
index 0000000..631d881
--- /dev/null
+++ b/module.info
@@ -0,0 +1,6 @@
+Name: Icinga Director
+Version: 1.10.2
+Depends: reactbundle (>=0.9.0), ipl (>=0.5.0), incubator (>=0.18.0)
+Description: Director - Config tool for Icinga 2
+ Icinga Director is a configuration tool that has been designed to make
+ Icinga 2 configuration easy and understandable.
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..9c9ddcd
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<ruleset name="PHP_CodeSniffer">
+ <description>Sniff our code a while</description>
+
+ <file>configuration.php</file>
+ <file>run.php</file>
+ <file>run-php5.3.php</file>
+ <file>application/</file>
+ <file>library/Director/</file>
+ <file>test/</file>
+
+ <arg value="wps"/>
+ <arg name="colors"/>
+ <arg name="report-width" value="auto"/>
+ <arg name="report-full"/>
+ <arg name="report-gitblame"/>
+ <arg name="report-summary"/>
+ <arg name="encoding" value="UTF-8"/>
+
+ <rule ref="PSR2"/>
+</ruleset>
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..99dde47
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit backupGlobals="false"
+ backupStaticAttributes="false"
+ colors="true"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ processIsolation="false"
+ stopOnFailure="false"
+ bootstrap="test/bootstrap.php"
+ >
+ <testsuites>
+ <testsuite name="Director PHP Unit tests">
+ <directory suffix=".php">test/php</directory>
+ </testsuite>
+ </testsuites>
+</phpunit>
diff --git a/public/css/module.less b/public/css/module.less
new file mode 100644
index 0000000..1f11251
--- /dev/null
+++ b/public/css/module.less
@@ -0,0 +1,1835 @@
+div.action-bar a:focus, .tabs a:focus {
+ outline: none;
+ text-decoration: underline;
+
+ &:before {
+ text-decoration: none;
+ }
+}
+
+table.common-table td {
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+ vertical-align: middle;
+
+ p {
+ word-break: break-word;
+ }
+}
+
+#layout.minimal-layout table.common-table td {
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+table.common-table thead th {
+ border-bottom: 1px solid @text-color;
+}
+table.common-table tbody td {
+ border-bottom: 1px solid @gray-lighter;
+}
+
+a:before {
+ text-decoration: none;
+}
+
+form.director-form {
+ max-width: 68em;
+}
+
+form.director-form:focus {
+ outline: none;
+}
+
+div.action-bar a, div.action-bar form i {
+ color: @icinga-blue;
+}
+
+div.action-bar > a {
+ margin-right: 1em;
+}
+
+.controls > .pagination-control li > a {
+ padding: 0.5em 0 0.5em 0;
+}
+
+.controls > .pagination-control > ul {
+ float: right;
+}
+
+div.action-bar {
+ .pagination-control {
+ float: none;
+ clear: none;
+ display: inline-block;
+ line-height: inherit;
+ margin: 0;
+ vertical-align: middle;
+ }
+
+ form.director-form input {
+ margin: 0;
+ }
+ input {
+ max-width: unset;
+ }
+ select {
+ line-height: 1.25em;
+ }
+}
+
+div.action-bar ul {
+ padding: 0;
+ margin: 0;
+
+ li {
+ list-style-type: none;
+ a { display: block; }
+ }
+}
+
+div.action-bar > ul {
+ display: inline-block;
+}
+
+div.action-bar > ul ul {
+ padding: 0.5em 0em 1em 0em;
+ min-width: 10em;
+ position: absolute;
+ display: none;
+ color: @text-color-inverted;
+ background-color: @icinga-blue;
+
+ a {
+ display: block;
+ padding: 0.3em 2em 0.3em 2em;
+ margin: 0;
+ outline: none;
+ color: @text-color-inverted;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ li.active a {
+ font-weight: bold;
+ }
+}
+
+div.action-bar > ul > li:hover ul {
+ display: block;
+}
+div.action-bar > ul > li > a {
+ padding: 0.2em 0.5em;
+}
+div.action-bar > ul > li:hover {
+ background-color: @icinga-blue;
+ & > a {
+ color: @text-color-inverted;
+ text-decoration: none;
+ }
+}
+
+#layout.twocols div.action-bar .pagination-control {
+ li {
+ display: none;
+ }
+
+ li:nth-child(1), li.active, li:last-child {
+ display: list-item;
+ }
+
+ li.active a {
+ border-bottom: none;
+ }
+}
+
+.content a {
+ color: @icinga-blue;
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+p {
+ max-width: 56em;
+}
+
+table.common-table {
+ max-width: 68em;
+
+ a {
+ color: inherit;
+ text-decoration: none;
+ }
+
+ th {
+ padding-top: 0.5em;
+ }
+ td {
+ vertical-align: top;
+ }
+
+ pre {
+ margin: 0;
+ padding: 0.2em;
+ max-height: 10em;
+ background: none;
+ overflow: auto;
+ word-break: keep-all;
+ white-space: pre;
+ display: inline-block;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+}
+
+table.history {
+ td:last-of-type {
+ text-align: right;
+ }
+}
+
+span.disabled {
+ cursor: no-drop;
+ color: @gray-light;
+}
+
+.controls span.action-links {
+ display: block;
+ margin-bottom: 0.5em;
+ a {
+ color: @icinga-blue;
+ margin-right: 1em;
+ }
+ form.director-form {
+ display: inline;
+ margin-right: 1em;
+ }
+}
+
+.action-bar {
+ form.director-form {
+ display: inline;
+ margin-right: 1em;
+ }
+}
+
+pre.disabled {
+ color: @disabled-gray;
+}
+
+form.director-form i.link-color::before {
+ color: @icinga-blue;
+}
+
+/* TODO: remove this, but autosubmit looks ugly otherwise */
+form.director-form input[disabled] {
+ background: inherit;
+}
+/* END OF TODO */
+
+pre {
+ background: none;
+}
+
+pre.logfile {
+ font-size: 0.875em;
+ padding: 1em;
+ background: @gray-lighter;
+ color: @gray;
+ overflow: auto;
+ white-space: pre;
+
+ a {
+ color: @link-color;
+ }
+
+ .loglevel, .application {
+ font-weight: bold;
+ }
+
+ .critical {
+ color: @color-critical;
+ }
+
+ .warning {
+ color: @color-warning;
+ }
+
+ .information {
+ color: @color-ok;
+ }
+
+ .notice {
+ // color: @color-ok;
+ }
+
+ .debug {
+ color: @color-pending;
+ }
+
+ .error-hint {
+ color: @text-color;
+ font-weight: 900;
+ }
+}
+
+pre.generated-config {
+
+ a {
+ color: @link-color;
+ font-weight: bold;
+ }
+
+ .highlight {
+ border-bottom: 1px dashed @gray-light;
+ &::before {
+ // icon: right-big
+ font-family: 'ifont';
+ content: '\e826';
+ margin-left: -1em;
+ padding-top: 0em;
+ float: left;
+ }
+
+ &.critical::before {
+ color: @color-critical;
+ }
+ &.warning::before {
+ color: @color-warning;
+ }
+ &.ok::before {
+ color: @color-ok;
+ }
+ }
+}
+
+pre.agent-deployment-instructions {
+ color: @text-color;
+ height: 14em;
+ overflow: scroll;
+}
+
+table.avp th {
+ font-size: inherit;
+}
+
+.content form.director-form {
+ margin-top: 0.5em;
+ margin-bottom: 2em;
+}
+
+.content form.director-form.inline {
+ margin: 0;
+
+ i.icon::before {
+ color: @icinga-blue;
+ }
+}
+
+.invisible {
+ position: absolute;
+ left: -100%;
+}
+
+form.director-form input[type=file] {
+ padding-right: 1em;
+}
+
+
+form.director-form input[type=submit] {
+ .button();
+ border-width: 1px;
+ margin-top: 0.5em;
+
+ &:disabled {
+ border-color: @gray-light;
+ background-color: @gray-light;
+ color: @disabled-gray;
+ }
+}
+
+form.director-form input[type=submit]:first-of-type {
+ border-width: 2px;
+}
+
+form.director-form input[type=submit].link-button {
+ color: @icinga-blue;
+ background: none;
+ border: none;
+ font-weight: normal;
+ padding: 0;
+ margin: 0;
+
+ text-align: left;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+form.director-form p.description {
+ padding: 1em 1em;
+ margin: 0;
+ font-style: italic;
+ width: 100%;
+}
+
+form.director-form {
+ input[type=text], input[type=button], select, select option, textarea {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ }
+}
+
+form.director-form ul.form-errors {
+ list-style-type: none;
+ margin-bottom: 0.5em;
+ padding: 0;
+
+ ul.errors {
+ list-style-type: none;
+ padding: 0;
+ }
+
+ ul.errors li {
+ background: @color-critical;
+ font-weight: bold;
+ padding: 0.5em 1em;
+ color: @text-color-inverted;
+ }
+}
+
+form.director-form {
+ select::-ms-expand, input::-ms-expand, textarea::-ms-expand { /* for IE 11 */
+ display: none;
+ }
+
+ select {
+ border: 1px solid @gray-light;
+ cursor: pointer;
+ background: none;
+ }
+
+ input[type=text], input[type=password], textarea, select {
+ max-width: 36em;
+ min-width: 20em;
+ width: 100%;
+ line-height: 2em;
+ height: 2.4em;
+ padding-left: 0.5em;
+ border-style: solid;
+ border-color: transparent;
+ border-bottom-color: @gray-lighter;
+ border-width: 1px 1px 1px 3px;
+ background-color: @low-sat-blue;
+
+ &.search {
+ background: transparent url("../img/icons/search.png") no-repeat scroll 0.5em center / 1em 1em;
+ padding-left: 2em;
+ }
+ }
+
+ textarea {
+ max-width: 100%;
+ }
+
+ select[multiple] {
+ height: auto;
+ }
+
+ select option {
+ height: 2em;
+ padding-top: 0.3em;
+ }
+
+ select[multiple=multiple] {
+ height: auto;
+ }
+
+ label {
+ line-height: 2em;
+ }
+}
+
+form.director-form dl {
+ margin: 0;
+ padding: 0;
+}
+
+.strike-links a, table.common-table .strike-links a {
+ text-decoration: line-through;
+ &:hover {
+ text-decoration: line-through;
+ }
+}
+.strike-links span.ro-service {
+ text-decoration: line-through;
+}
+
+// TODO: figure out whether form.editor and filter-related CSS is still required
+div.filter > form.search, div.filter > a {
+ // Duplicated by quicksearch
+ display: none;
+}
+
+div.filter form.editor > ul.tree ul li.active {
+ background-color: @tr-hover-color;
+}
+
+div.filter form.editor {
+ padding-top: 1em;
+ select, input[type=text] {
+ line-height: unset;
+ height: auto;
+ }
+}
+
+form.director-form.editor {
+ select, input[type=text] {
+ background: @low-sat-blue;
+ max-width: unset;
+ min-width: unset;
+ width: auto;
+ }
+ .tree li a {
+ padding: 0;
+ }
+}
+
+ul.extensible-set {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ display: inline-block;
+ width: 100%;
+ max-width: 36em;
+ min-width: 20em;
+ border-bottom: 1px solid @gray-lighter;
+
+ input[type=text], input[type=password], textarea, select {
+ border-color: transparent;
+ }
+
+ li {
+ display: inline;
+ }
+
+ select {
+ width: 100%;
+ }
+
+ input[type=text] {
+ background-color: @low-sat-blue;
+ .rounded-corners(0.5em);
+ border: 1px solid transparent;
+ padding: 0.1em 0.3em;
+ margin: 0.2em 0.2em;
+ width: 30%;
+ min-width: 4em;
+ text-overflow: ellipsis;
+ }
+
+ span.inline-buttons {
+ position: absolute;
+ z-index: 10;
+ right: 0.225em;
+ top: -0.275em;
+ input[type=submit] {
+ font-family: 'ifont';
+ width: 2em;
+ height: 2em;
+ font-size: 1em;
+ margin-left: 0.2em;
+ padding: 1px 0 1px 0;
+ }
+ }
+
+ select.extend-set, input.extend-set {
+ display: none;
+ }
+}
+
+form.director-form {
+ #_FAKE_SUBMIT {
+ position: absolute;
+ left: -100%;
+ }
+}
+
+form.director-form dd.active ul.extensible-set, ul.extensible-set.sortable {
+
+ li {
+ display: list-item;
+ position: relative;
+ clear: both;
+ }
+
+ input[type=text], select {
+ width: 100%;
+ }
+
+ input[type=text] {
+ background-color: @low-sat-blue;
+ .rounded-corners(0);
+ border: 1px solid @gray-light;
+ padding: 0.25em 0.5em;
+ margin: 0;
+ }
+}
+
+form.director-form dd.active ul.extensible-set {
+ border: 1px solid @icinga-blue;
+
+ input[type=submit]:first-of-type {
+ border-width: 1px;
+ }
+
+ select.extend-set, input.extend-set {
+ display: inline;
+ }
+}
+
+form.director-form {
+ select::-moz-focus-inner {
+ border: 0;
+ }
+
+ select:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #000;
+ }
+
+ select, input[type=text], textarea {
+ &:hover {
+ border-style: dotted solid dotted solid;
+ border-color: @gray-light;
+ }
+
+ &:focus, &:focus:hover {
+ border-style: solid;
+ border-color: @icinga-blue;
+ outline: none;
+ }
+ }
+
+ select option {
+ padding-left: 0.5em;
+ }
+
+ select option[value=""] {
+ color: @disabled-gray;
+ background-color: @low-sat-blue;
+ }
+}
+
+a {
+ &.state-critical {
+ color: @color-critical;
+ }
+
+ &.state-warning {
+ color: @color-warning;
+ }
+
+ &.state-ok {
+ color: @color-ok;
+ }
+
+ &.state-unknown {
+ color: @color-unknown;
+ }
+
+ &.state-pending {
+ color: @color-pending;
+ }
+}
+ul.tabs a.state-critical {
+ background-color: @color-critical;
+ font-weight: bold;
+ color: @text-color-inverted;
+}
+ul.tabs a.state-warning {
+ background-color: @color-warning;
+ font-weight: bold;
+ color: @text-color-inverted;
+}
+ul.tabs a.state-ok {
+ background-color: @color-ok;
+ font-weight: bold;
+ color: @text-color-inverted;
+}
+ul.tabs a.state-unknown {
+ background-color: @color-unknown;
+ font-weight: bold;
+ color: @text-color-inverted;
+}
+
+a:hover::before {
+ text-decoration: none;
+}
+
+ul.main-actions {
+ margin: 0;
+ padding: 0;
+ min-width: 38em;
+ max-width: 64em;
+
+ li {
+ list-style-type: none;
+
+ text-align: left;
+ display: inline-block;
+ padding: 0;
+ clear: both;
+ width: 18em;
+ min-width: 16em;
+ vertical-align: top;
+
+ a {
+ i {
+ font-size: 3em;
+ display: block;
+ float: left;
+ line-height: 1em;
+ margin-right: 0.3em;
+ color: @text-color-light;
+ }
+
+ &.state-critical i {
+ color: @color-critical;
+ }
+
+ &.state-warning i {
+ color: @color-warning;
+ }
+
+ &.state-ok i {
+ color: @color-ok;
+ }
+
+ &.state-unknown i {
+ color: @color-unknown;
+ }
+
+ &.state-pending i {
+ color: @color-pending;
+ }
+
+ border-left: 0.5em solid transparent;
+ padding: 1em;
+ color: @text-color;
+ font-weight: bold;
+ display: block;
+ text-decoration: none;
+ min-height: 12em;
+
+ overflow: hidden;
+
+ &.active {
+ border-color: @icinga-blue;
+ background-color: @tr-active-color;
+ }
+
+ &:hover {
+ background-color: @tr-hover-color;
+ text-decoration: none;
+ }
+
+ &:active, &:focus {
+ background-color: @tr-hover-color;
+ outline: none;
+ }
+ }
+
+ p {
+ font-weight: normal;
+ margin-bottom: 0.5em;
+ padding-left: 4.5em;
+ color: @text-color-light;
+ }
+ }
+}
+
+#layout.compact-layout.twocols ul.main-actions,
+#layout.minimal-layout ul.main-actions {
+ max-width: unset;
+ min-width: unset;
+ li {
+ width: 100%;
+ a {
+ height: auto;
+ min-height: unset;
+ }
+ > a > i {
+ font-size: 3em;
+ }
+
+ > a > p {
+ padding-left: 4.5em;
+ }
+ margin-bottom: 0.5em;
+ }
+}
+
+#layout.minimal-layout div.content form.director-form {
+ dt, dd {
+ display: block;
+ width: auto;
+ }
+ fieldset.collapsed {
+ dd, dt, ul, div {
+ display: none;
+ }
+ }
+ dt label {
+ color: @text-color;
+ }
+ fieldset {
+ min-width: unset;
+ }
+
+ input[type=text], input[type=password], textarea, select {
+ max-width: unset;
+ min-width: unset;
+ border-color: @gray-light;
+ }
+ dd.active {
+ input[type=text], input[type=password], textarea, select {
+ border-color: @icinga-blue;
+ }
+ }
+ ul.extensible-set {
+ max-width: unset;
+ }
+ dd ul.extensible-set {
+ border: 1px solid @gray-light;
+ }
+ dd.active ul.extensible-set {
+ border: 1px solid @icinga-blue;
+
+ input[type=submit]:first-of-type {
+ border-width: 1px;
+ }
+ }
+
+ dd.active ul.extensible-set, ul.extensible-set.sortable {
+ input[type=text], select {
+ width: 100%;
+ }
+
+ input[type=text] {
+ background-color: @low-sat-blue;
+ border: 1px solid @gray-light;
+ }
+ }
+
+}
+
+form.director-form fieldset {
+ margin: 0;
+ padding: 0 0 1.5em 0;
+ border: none;
+
+ legend {
+ margin: 0em 0 0.5em 0;
+ font-size: 1em;
+ border-bottom: 1px solid @gray-light;
+ font-weight: bold;
+ display: block;
+ width: 100%;
+ padding-left: 1em;
+ line-height: 2em;
+ cursor: pointer;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+
+ &:hover {
+ border-color: @text-color;
+ }
+
+ &::before {
+ // icon: down-dir
+ font-family: 'ifont';
+ content: '\e81d';
+ margin-left: -1em;
+ padding-top: 0em;
+ float: left;
+ color: inherit;
+ }
+ }
+
+ &.collapsed {
+ legend {
+ margin: 0;
+ }
+
+ dd, dt, ul, div {
+ display: none;
+ }
+
+ legend::before {
+ // icon: right-dir
+ content: '\e820';
+ }
+
+ margin-bottom: 0.2em;
+ padding-bottom: 0;
+ }
+}
+
+
+/* BEGIN Forms */
+form.director-form dt label {
+ width: auto;
+ font-weight: normal;
+ font-size: inherit;
+
+ &.required {
+ &::after {
+ content: '*'
+ }
+ }
+
+ &:hover {
+ text-decoration: underline;
+ cursor: pointer;
+ }
+}
+
+form.director-form fieldset {
+ min-width: 36em;
+}
+
+form.director-form dd input.related-action[type='submit'] {
+ display: none;
+}
+
+form.director-form dd.active li.active input.related-action[type='submit'] {
+ display: inline-block;
+}
+
+form.director-form {
+ p.description {
+ color: @gray;
+ font-style: italic;
+ padding: 0.25em 0.5em;
+ display: none;
+ }
+
+ dd.active {
+ p.description {
+ font-style: normal;
+ display: block;
+ height: auto;
+ color: @text-color;
+ }
+ }
+}
+
+form.director-form.db-selector {
+ padding: 0;
+ margin: 0;
+ select {
+ float: right;
+ text-align: center;
+ max-width: 20em;
+ margin-top: 0.5em;
+ min-width: 14em;
+ width: auto;
+ }
+}
+
+// Adjustments for the legacy layout to keep backwards compatibility
+#layout.twocols > #main &#col1 {
+ width: 50%;
+}
+#layout.twocols > #main &#col2 {
+ width: 50%;
+}
+#layout.twocols > #main &#col1 + #col2 {
+ width: 50%;
+}
+
+// Adjustments for the flexbox layout
+#layout.twocols > #content-wrapper &#col2,
+#layout.twocols > #content-wrapper &#col1 + #col2 {
+ flex-grow: 1;
+}
+
+form.director-form dd {
+ padding: 0.3em 0.5em;
+ margin: 0;
+}
+
+form.director-form dt {
+ padding: 0.5em 0.5em;
+ margin: 0;
+}
+
+form.director-form dt.active, form.director-form dd.active {
+ background-color: @tr-active-color;
+}
+
+form.director-form dt {
+ display: inline-block;
+ vertical-align: top;
+ min-width: 12em;
+ min-height: 2.5em;
+ width: 30%;
+ &.errors label {
+ color: @color-critical;
+ }
+}
+
+form.director-form .errors label {
+ color: @color-critical;
+}
+
+form.director-form dd {
+ display: inline-block;
+ width: 63%;
+ min-height: 2.5em;
+ vertical-align: top;
+ margin: 0;
+ &.errors {
+ input[type=text], select {
+ border-color: @color-critical;
+ }
+ }
+
+ &.full-width {
+ padding: 0.5em;
+ width: 100%;
+ }
+}
+
+form.director-form dd:after {
+ display: block;
+ content: '';
+}
+
+form.director-form textarea {
+ height: auto;
+}
+
+form.director-form dd ul.errors {
+ list-style-type: none;
+ padding-left: 0.3em;
+
+ li {
+ color: @color-critical;
+ padding: 0.3em;
+ }
+}
+
+form.director-form div.hint {
+ padding: 1em;
+ background-color: @tr-hover-color;
+ margin: 1em 0;
+ max-width: 65em;
+ font-size: 1em;
+
+ pre {
+ font-style: normal;
+ background-color: @body-bg-color;
+ margin: 0;
+ padding: 1em;
+ }
+}
+
+/* END of Forms */
+
+ul.health-check-result {
+ list-style-type: none;
+ padding: 0;
+ margin-bottom: 2em;
+ li {
+ line-height: 2em;
+ }
+ .badge {
+ font-weight: bold;
+ }
+}
+
+.title-badges {
+ .badge {
+ font-size: 0.6em;
+ margin-left: 0.5em;
+ }
+}
+
+li.state {
+ border-left: 0.5em solid transparent;
+ margin-bottom: 0.5em;
+ padding-left: 1em;
+ &.state-ok {
+ border-color: @color-ok;
+ }
+ &.state-warning {
+ border-color: @color-warning;
+ }
+ &.state-critical {
+ border-color: @color-critical;
+ }
+ &.state-unknown {
+ border-color: @color-unknown;
+ }
+ &.state-pending {
+ border-color: @color-pending;
+ }
+}
+
+span.error {
+ color: @color-critical;
+
+ a {
+ color: @color-critical;
+ }
+}
+
+p.legacy-error {
+ color: @text-color-inverted;
+ padding: 1em 2em;
+ background-color: @color-critical;
+
+}
+
+table th.actions, table td.actions {
+ text-align: right;
+}
+
+table tr.disabled td {
+ color: @gray-light;
+ font-style: italic;
+}
+
+/* Simple table, test */
+table.syncstate {
+ tr td:first-child {
+ padding-left: 2em;
+ &::before {
+ font-family: 'ifont';
+ // icon-help:
+ content: '\e85b';
+ float: left;
+ font-weight: bold;
+ margin-left: -1.5em;
+ line-height: 1.5em;
+ }
+ }
+
+ tr.in-sync td:first-child::before {
+ content: '\e803';
+ color: @color-ok;
+ }
+
+ tr.pending-changes td:first-child::before {
+ content: '\e864';
+ color: @color-warning;
+ }
+
+ tr.failing td:first-child::before {
+ content: '\e804';
+ color: @color-critical;
+ }
+}
+
+table.jobs {
+ tr td:first-child {
+ padding-left: 2em;
+ &::before {
+ font-family: 'ifont';
+ // icon-help:
+ content: '\e85b';
+ float: left;
+ font-weight: bold;
+ margin-left: -1.5em;
+ line-height: 1.5em;
+ }
+ }
+
+ tr.ok td:first-child::before {
+ content: '\e803';
+ color: @color-ok;
+ }
+
+ tr.warning td:first-child::before {
+ content: '\e864';
+ color: @color-warning;
+ }
+
+ tr.pending td:first-child::before {
+ content: '\e864';
+ color: @color-pending;
+ }
+
+ tr.critical td:first-child::before {
+ content: '\e804';
+ color: @color-critical;
+ }
+}
+
+table.icinga-objects {
+ tr td:first-child {
+ padding-left: 2em;
+ &::before {
+ font-family: 'ifont';
+ // icon-wrench:
+ content: '\e83d';
+ float: left;
+ font-weight: bold;
+ margin-left: -1.5em;
+ line-height: 1.5em;
+ }
+ }
+
+ tr.icinga-object-external td:first-child::before {
+ color: @gray;
+ // icon-pin
+ content: '\e879';
+ }
+
+ tr.icinga-object td:first-child::before {
+ color: @text-color;
+ // icon-thumbs-up
+ // content: '\e867';
+ // icon-ok
+ content: '\e803';
+ }
+
+ tr.icinga-template td:first-child::before {
+ color: @gray-light;
+ // icon-paste
+ content: '\e817';
+ }
+
+ tr.icinga-apply td:first-child::before {
+ color: @text-color;
+ // resize-full-alt
+ content: '\e829';
+ }
+
+}
+
+div.content.compact table.icinga-objects thead {
+ display: none;
+}
+
+table.deployment-log {
+
+ tr td:nth-child(2), tr th:nth-child(2) {
+ text-align: right;
+ padding-right: 1em;
+ }
+
+ tr th:first-child {
+ padding-left: 2em;
+ }
+
+ tr td:first-child {
+ padding-left: 2em;
+ &::before {
+ font-family: 'ifont';
+ // icon-help:
+ content: '\e85b';
+ float: left;
+ font-weight: bold;
+ margin-left: -1.5em;
+ line-height: 1.5em;
+ }
+ }
+
+ tr.succeeded td:first-child::before {
+ // icon-ok
+ color: @color-ok;
+ content: '\e803';
+ }
+
+ tr.pending td:first-child::before {
+ color: @gray;
+ // icon-spinner
+ content: '\e874';
+ .animate(spin 2s infinite linear);
+ }
+
+ tr.failed td:first-child::before {
+ // icon-ok
+ color: @color-critical;
+ content: '\e804';
+ }
+
+ tr.running td, tr.running td a {
+ font-weight: bold;
+ }
+}
+
+th.table-header-day {
+ text-align: right;
+}
+
+table.activity-log {
+ td {
+ max-height: 2em;
+ }
+ tr th:first-child {
+ padding-left: 2em;
+ }
+
+ tr td:last-child {
+ text-align: right;
+ white-space: nowrap;
+ width:10%;
+ }
+
+ tr td:first-child {
+ padding-left: 2em;
+ &::before {
+ font-family: 'ifont';
+ // icon-help:
+ content: '\e85b';
+ float: left;
+ font-weight: bold;
+ margin-left: -1.5em;
+ line-height: 1.5em;
+ }
+ }
+
+ tr.action-create td:first-child::before {
+ // icon-plus
+ color: @color-pending;
+ content: '\e805';
+ }
+
+ tr.action-modify td:first-child::before {
+ // icon-wrench
+ color: @color-ok;
+ content: '\e83d';
+ }
+
+ tr.action-delete td:first-child::before {
+ // icon-cancel
+ color: @color-critical;
+ content: '\e804';
+ }
+
+ tr.undeployed td, tr.undeployed a {
+ color: @gray;
+ }
+
+ tr.undeployed {
+ background-color: @gray-lightest;
+ &.active {
+ background-color: @gray-lighter;
+ }
+ &[href]:hover {
+ background-color: @gray-light;
+ td, a {
+ color: @text-color;
+ }
+ }
+ }
+
+ tr.branched {
+ background-color: @gray-lightest;
+ color: @color-pending;
+ }
+
+ tr.undeployed td:first-child::before {
+ color: @gray;
+ }
+
+ div.range-comment-container {
+ width: 100%;
+ position: absolute;
+ height: 100%;
+ background: @body-bg-color;
+ border-radius: 1em;
+ }
+ a.range-comment {
+ width: 100%;
+ height: 100%;
+ display: block;
+ border-radius: 1em;
+ padding: 0.2em 1em;
+ vertical-align: middle;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ overflow-y:auto;
+ overflow-x:hidden;
+ word-break: break-word;
+ &:hover {
+ cursor: default;
+ text-decoration: none;
+ }
+ background: fade(@color-warning-handled, 20%);
+ &:hover {
+ background: fade(@color-warning-handled, 60%);
+ }
+ }
+ td.comment-cell {
+ padding: 0;
+ min-width: 10em;
+ width: 40%;
+ position: relative;
+ &.continuing div.range-comment-container {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ a.range-comment {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+ &.continued div.range-comment-container {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ a.range-comment {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ }
+ }
+}
+
+tr.branch_modified {
+ color: @color-pending;
+}
+
+table.config-diff {
+
+ tr th:first-child {
+ padding-left: 2em;
+ }
+
+ tr td:first-child {
+ padding-left: 2em;
+ &::before {
+ font-family: 'ifont';
+ // icon-help:
+ content: '\e85b';
+ float: left;
+ font-weight: bold;
+ margin-left: -1.5em;
+ line-height: 1.5em;
+ }
+ }
+
+ tr.file-unmodified td:first-child::before {
+ // icon-ok
+ color: @color-ok;
+ content: '\e803';
+ }
+
+ tr.file-created td:first-child::before {
+ // icon-plus
+ color: @color-pending;
+ content: '\e805';
+ }
+
+ tr.file-removed td:first-child::before {
+ // icon-cancel
+ color: @color-critical;
+ content: '\e804';
+ }
+
+ tr.file-modified td:first-child::before {
+ // icon-flapping
+ color: @color-warning;
+ content: '\e85d';
+ }
+}
+
+input[type=submit].icon-button {
+ font-family: 'ifont';
+ font-weight: normal;
+ background: none;
+ border: none;
+ padding: 0.2em 0.4em 0.2em 0.4em;
+ margin: 0 0 0 0.2em;
+
+ &:hover {
+ background-color: @icinga-blue;
+ }
+
+ &:disabled {
+ background-color: unset;
+ color: @gray-light;
+ cursor: default;
+ }
+}
+
+/** BEGIN breadcrumb **/
+
+// Hint: .badges is unused right now
+.breadcrumb {
+ list-style: none;
+ overflow: hidden;
+ padding: 0;
+
+ .badges {
+ display: inline-block;
+ padding: 0 0 0 0.5em;
+ .badge {
+ line-height: 1.25em;
+ font-size: 0.8em;
+ border: 1px solid @text-color;
+ margin: -0.25em 1px 0 0;
+ }
+ }
+}
+
+.breadcrumb {
+ > .critical a { color: @text-color-inverted; background: @color-critical; }
+ > .critical.handled a { color: @text-color-inverted; background: @color-critical-handled; }
+ > .unknown a { color: @text-color-inverted; background: @color-unknown; }
+ > .unknown.handled a { color: @text-color-inverted; background: @color-unknown-handled; }
+ > .warning a { color: @text-color-inverted; background: @color-warning; }
+ > .warning.handled a { color: @text-color-inverted; background: @color-warning-handled; }
+ > .ok a { color: @text-color-inverted; background: @color-ok; }
+}
+
+.breadcrumb {
+ > .critical a:after { border-left-color: @color-critical; }
+ > .critical.handled a:after { border-left-color: @color-critical-handled; }
+ > .unknown a:after { border-left-color: @color-unknown; }
+ > .unknown.handled a:after { border-left-color: @color-unknown-handled; }
+ > .warning a:after { border-left-color: @color-warning; }
+ > .warning.handled a:after { border-left-color: @color-warning-handled; }
+ > .ok a:after { border-left-color: @color-ok; }
+}
+
+.breadcrumb:after {
+ content:'';
+ display:block;
+ clear: both;
+}
+.breadcrumb li {
+ float: left;
+ cursor: pointer;
+ user-select: none;
+ background: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+
+}
+.breadcrumb li a {
+ // color: white;
+ color: @icinga-blue;
+ margin: 0;
+ // font-size: 1.2em;
+ text-decoration: none;
+ padding-left: 2em;
+ // line-height: 1.5em;
+ // background: @icinga-blue;
+ border: 1px none @icinga-blue;
+ border-right-style: solid;
+ border-left-style: solid;
+ position: relative;
+ display: block;
+ float: left;
+ &:focus {
+ outline: none;
+ }
+ &:hover {
+ text-decoration: none;
+ }
+}
+.action-bar .breadcrumb li a {
+ padding-left: 2em;
+}
+
+.breadcrumb li a:before, .breadcrumb li a:after {
+ content: " ";
+ display: block;
+ width: 0;
+ height: 0;
+ border-top: 1.3em solid transparent;
+ border-bottom: 1.2em solid transparent;
+ position: absolute;
+ margin-top: -1.2em;
+ top: 50%;
+ left: 100%;
+}
+
+.breadcrumb li a:before {
+ border-left: 1.2em solid @icinga-blue;
+ margin-left: 1px;
+ z-index: 1;
+}
+
+.breadcrumb li a:after {
+ border-left: 1.2em solid @body-bg-color;
+ z-index: 2;
+}
+
+.breadcrumb li:first-child a {
+ padding-left: 1em;
+ padding-right: 0.5em;
+}
+.breadcrumb li:last-child a {
+ color: @text-color;
+}
+
+.breadcrumb li:not(:last-child) a:hover { background: @icinga-blue; color: @text-color-on-icinga-blue; }
+.breadcrumb li:not(:last-child) a:hover:after { border-left-color: @icinga-blue; }
+.breadcrumb li:last-child:hover, .breadcrumb li:last-child a:hover { background: transparent; text-decoration: underline; }
+
+.breadcrumb li a:focus {
+ text-decoration: underline;
+}
+/** END of breadcrumb **/
+
+
+
+ul.filter-root {
+ margin-top: 0;
+ width: 100%;
+ padding-left: 0.5em;
+ list-style-type: none;
+ ul {
+ list-style-type: none;
+ padding-left: 1.5em;
+ }
+
+ ul.filter {
+ padding-left: 1.5em;
+ list-style-type: none;
+ width: 100%;
+ }
+
+ li.filter-chain, div.filter-expression {
+ width: 100%;
+ padding: 0.3em 0.5em;
+ min-width: 30em;
+ }
+
+ ul li.filter-chain::before, ul .filter-expression::before {
+ font-family: 'ifont';
+ // Formerly: icon-down-open: e821
+ // icon-right-small:
+ content: '\e877';
+ float: left;
+ margin-left: -1.5em;
+ margin-top: 0.5em;
+ }
+
+ ul.extensible-set {
+ padding-left: 0;
+ border: none;
+ display: inline-block;
+ vertical-align: top;
+ li::before {
+ content: none;
+ }
+ }
+
+ .filter-chain > input[type=submit].icon-button, .filter-expression > input[type=submit].icon-button {
+ display: none;
+ font-family: 'ifont';
+ font-weight: normal;
+ background: none;
+ border: none;
+ padding: 0.2em 0.4em 0.2em 0.4em;
+ margin: 0 0 0 0.2em;
+ }
+
+ .active input[type=submit].icon-button,
+ li:hover input[type=submit].icon-button,
+ div:hover input[type=submit].icon-button
+ {
+ display: inline;
+ }
+}
+
+.errors > ul.filter-root {
+ input[type=text], select {
+ border-color: transparent;
+ border-bottom-color: @gray-lighter;
+ }
+
+ select.column, select.operator {
+ border-left-color: @color-critical;
+ }
+}
+
+
+form.director-form li.filter-chain > select.operator {
+ min-width: 5em;
+ max-width: 5em;
+ width: 5em;
+}
+
+form.director-form div.filter-expression {
+ .column {
+ min-width: 7em;
+ max-width: 30em;
+ width: auto;
+ }
+
+ .sign {
+ min-width: 4em;
+ max-width: 4em;
+ width: 4em;
+ margin: 0 0.3em;
+ &.wide {
+ min-width: 6em;
+ max-width: 8em;
+ width: 8em;
+ }
+ }
+
+ div.expression-wrapper {
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ div.expression-wrapper > input[type=text],
+ div.expression-wrapper > select {
+ min-width: 7em;
+ width: 10em;
+ max-width: 10em;
+ }
+}
+
+ul.director-suggestions {
+/*
+ min-width: 18.5em;
+ max-width: 34.65em;
+ width: 100%;
+ */
+ width: 20em;
+ max-height: 25em;
+ overflow-y: auto;
+ overflow-x: hidden;
+ border: 1px solid @icinga-blue;
+ position: absolute;
+ z-index: 2000;
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+ background-color: @low-sat-blue;
+ li {
+ margin: 0;
+ padding: 0.5em 1em;
+ }
+
+ li:hover {
+ background-color: @tr-hover-color;
+ cursor: pointer;
+ }
+
+ li.active {
+ color: @text-color;
+ &:hover {
+ color: @text-color;
+ }
+ }
+
+ table.benchmark {
+ font-size: 0.8em;
+ font-family: @font-family-fixed;
+ }
+}
+
+table.pivot {
+ width: 100%;
+ table-layout: fixed;
+
+ thead th {
+ text-align: center;
+ }
+
+ tbody th {
+ text-align: right;
+ width: 25%;
+ }
+ tbody td {
+ text-align: center;
+ }
+ tbody td > a {
+ display: block;
+ font-size: 2em;
+ line-height: 1.5em;
+ text-decoration: none;
+ color: @icinga-blue;
+ &:hover {
+ background: @tr-active-color;
+ text-decoration: none;
+ }
+ }
+}
+
+.tree li a {
+ display: inline-block;
+ padding-left: 2.4em;
+ line-height: 2em;
+ text-decoration: none;
+ color: @text-color;
+ outline: 0;
+ background-repeat: no-repeat;
+ background-position: 0.8em 0.4em;
+}
+
+ul.tree li > .handle {
+ background-image: none;
+ &:before {
+ content: '\e806';
+ font-family: 'ifont';
+ position: absolute;
+ font-size: 0.6em;
+ margin-left: 0.25em;
+ margin-top: 0.9em;
+ }
+}
+ul.tree li.collapsed > .handle {
+ background-image: none;
+ &:before {
+ content: '\e805';
+ }
+}
+.tree li a {
+ padding-left: 1em;
+}
+
+div.sql-dump {
+ background-color: @gray-lightest;
+ border: 1px solid @gray-light;
+ padding: 1em;
+}
+
+div.exception {
+ margin: 1em;
+}
+
+h2.action-create::before {
+ color: @color-pending;
+}
+h2.action-modify::before {
+ color: @color-ok;
+}
+h2.action-delete::before {
+ color: @color-critical;
+}
+
+/* Special components */
+table.table-basket-changes {
+ min-width: 18em;
+ max-width: 100%;
+ th {
+ width: 80%;
+ font-weight: normal;
+ text-align: left;
+ min-width: 10em;
+ }
+}
diff --git a/public/img/globe.png b/public/img/globe.png
new file mode 100644
index 0000000..48e5b6b
--- /dev/null
+++ b/public/img/globe.png
Binary files differ
diff --git a/public/img/leaf.gif b/public/img/leaf.gif
new file mode 100644
index 0000000..445769d
--- /dev/null
+++ b/public/img/leaf.gif
Binary files differ
diff --git a/public/img/script.png b/public/img/script.png
new file mode 100644
index 0000000..28f7652
--- /dev/null
+++ b/public/img/script.png
Binary files differ
diff --git a/public/img/server.png b/public/img/server.png
new file mode 100644
index 0000000..ee0c771
--- /dev/null
+++ b/public/img/server.png
Binary files differ
diff --git a/public/img/service.png b/public/img/service.png
new file mode 100644
index 0000000..0f1c2fd
--- /dev/null
+++ b/public/img/service.png
Binary files differ
diff --git a/public/img/tree.png b/public/img/tree.png
new file mode 100644
index 0000000..298343e
--- /dev/null
+++ b/public/img/tree.png
Binary files differ
diff --git a/public/js/module.js b/public/js/module.js
new file mode 100644
index 0000000..07fe265
--- /dev/null
+++ b/public/js/module.js
@@ -0,0 +1,840 @@
+
+(function (Icinga) {
+
+ var Director = function (module) {
+ this.module = module;
+
+ this.initialize();
+
+ this.openedFieldsets = {};
+
+ this.module.icinga.logger.debug('Director module loaded');
+ };
+
+ Director.prototype = {
+
+ initialize: function () {
+ /**
+ * Tell Icinga about our event handlers
+ */
+ this.module.on('rendered', this.rendered);
+ this.module.on('beforerender', this.beforeRender);
+ this.module.on('click', 'fieldset > legend', this.toggleFieldset);
+ // Disabled
+ // this.module.on('click', 'div.controls ul.tabs a', this.detailTabClick);
+ this.module.on('click', 'input.related-action', this.extensibleSetAction);
+ this.module.on('click', 'ul.filter-root input[type=submit]', this.setAutoSubmitted);
+ this.module.on('focus', 'form input, form textarea, form select', this.formElementFocus);
+ this.module.on('keyup', '.director-suggest', this.autoSuggest);
+ this.module.on('keydown', '.director-suggest', this.suggestionKeyDown);
+ this.module.on('dblclick', '.director-suggest', this.suggestionDoubleClick);
+ this.module.on('focus', '.director-suggest', this.enterSuggestionField);
+ this.module.on('focusout', '.director-suggest', this.leaveSuggestionField);
+ this.module.on('mousedown', '.director-suggestions li', this.clickSuggestion);
+ this.module.on('dblclick', 'ul.tabs a', this.tabWantsFullscreen);
+ this.module.on('change', 'form input.autosubmit, form select.autosubmit', this.setAutoSubmitted);
+ this.module.icinga.logger.debug('Director module initialized');
+ },
+
+ tabWantsFullscreen: function (ev) {
+ var icinga = this.module.icinga;
+ var $a, $container, id;
+
+ if (icinga.ui.isOneColLayout()) {
+ return;
+ }
+
+ $a = $(ev.currentTarget);
+ if ($a.hasClass('refresh-container-control')) {
+ return;
+ }
+ $container = $a.closest('.container');
+ id = $container.attr('id');
+
+ icinga.loader.stopPendingRequestsFor($container);
+ if (id === 'col2') {
+ icinga.ui.moveToLeft();
+ }
+
+ icinga.ui.layout1col();
+ icinga.history.pushCurrentState();
+ ev.preventDefault();
+ ev.stopPropagation();
+ },
+
+ /**
+ * Autocomplete/suggestion eventhandler
+ *
+ * Triggered when pressing a key in a form element with suggestions
+ * @param ev
+ */
+ suggestionKeyDown: function (ev) {
+ var $el = $(ev.currentTarget);
+ var key = ev.which;
+
+ if (key === 13) {
+ /**
+ * RETURN key pressed. In case there are any suggestions:
+ * - let's choose the active one (if set)
+ * - stop the event
+ *
+ * This let's return bubble up in case there is no suggestion list shown
+ */
+ if (this.hasActiveSuggestion($el)) {
+ this.chooseActiveSuggestion($el);
+ ev.stopPropagation();
+ ev.preventDefault();
+ } else {
+ this.removeSuggestionList($el);
+ if ($el.closest('.extensible-set')) {
+ $el.trigger('change');
+ } else {
+ $el.closest('form').submit();
+ }
+ }
+ } else if (key === 27) {
+ // ESC key pressed. Remove suggestions if any
+ this.removeSuggestionList($el);
+ } else if (key === 39) {
+ /**
+ * RIGHT ARROW key pressed. In case there are any suggestions:
+ * - let's choose the active one (if set)
+ * - stop the event only if an element has been chosen
+ *
+ * This allows to use the right arrow key normally in all other situations
+ */
+ if (this.hasSuggestions($el)) {
+ if (this.chooseActiveSuggestion($el)) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+ }
+ } else if (key === 38 ) {
+ /**
+ * UP ARROW key pressed. In any case:
+ * - stop the event
+ * - activate the previous suggestion if any
+ */
+ ev.stopPropagation();
+ ev.preventDefault();
+ this.activatePrevSuggestion($el);
+ } else if (key === 40 ) { // down
+ /**
+ * DOWN ARROW key pressed. In any case:
+ * - stop the event
+ * - activate the next suggestion if any
+ */
+ ev.stopPropagation();
+ ev.preventDefault();
+ this.activateNextSuggestion($el);
+ }
+ },
+
+ suggestionDoubleClick: function (ev) {
+ var $el = $(ev.currentTarget);
+ this.getSuggestionList($el);
+ },
+
+ /**
+ * Autocomplete/suggestion eventhandler
+ *
+ * Triggered when releasing a key in a form element with suggestions
+ *
+ * @param ev
+ */
+ autoSuggest: function (ev) {
+ // Ignore special keys, most of them have already been handled on 'keydown'
+ var key = ev.which;
+ if (key === 9 || // TAB
+ key === 13 || // RETURN
+ key === 27 || // ESC
+ key === 37 || // LEFT ARROW
+ key === 38 || // UP ARROW
+ key === 39 ) { // RIGHT ARROW
+ return;
+ }
+
+ var $el = $(ev.currentTarget);
+ if (key === 40) { // DOWN ARROW
+ this.getSuggestionList($el);
+ } else {
+ this.getSuggestionList($el, true);
+ }
+ },
+
+ /**
+ * Activate the next related suggestion if any
+ *
+ * This walks down the suggestion list, takes care about scrolling and restarts from
+ * top once reached the bottom
+ *
+ * @param $el
+ */
+ activateNextSuggestion: function ($el) {
+ var $list = this.getSuggestionList($el);
+ var $next;
+ var $active = $list.find('li.active');
+ if ($active.length) {
+ $next = $active.next('li');
+ if ($next.length === 0) {
+ $next = $list.find('li').first();
+ }
+ } else {
+ $next = $list.find('li').first();
+ }
+ if ($next.length) {
+ // Will not happen when list is empty or last element is active
+ $list.find('li.active').removeClass('active');
+ $next.addClass('active');
+ $list.scrollTop($next.offset().top - $list.offset().top - 64 + $list.scrollTop());
+ }
+ },
+
+ /**
+ * Activate the previous related suggestion if any
+ *
+ * This walks up through the suggestion list and takes care about scrolling.
+ * Puts the focus back on the input field once reached the top and restarts
+ * from bottom when moving up from there
+ *
+ * @param $el
+ */
+ activatePrevSuggestion: function ($el) {
+ var $list = this.getSuggestionList($el);
+ var $prev;
+ var $active = $list.find('li.active');
+ if ($active.length) {
+ $prev = $active.prev('li');
+ } else {
+ $prev = $list.find('li').last();
+ }
+ $list.find('li.active').removeClass('active');
+
+ if ($prev.length) {
+ $prev.addClass('active');
+ $list.scrollTop($prev.offset().top - $list.offset().top - 64 + $list.scrollTop());
+ } else {
+ $el.focus();
+ $el.val($el.val());
+ }
+ },
+
+ /**
+ * Whether a related suggestion list element exists
+ *
+ * @param $input
+ * @returns {boolean}
+ */
+ hasSuggestionList: function ($input) {
+ var $ul = $input.siblings('ul.director-suggestions');
+ return $ul.length > 0;
+ },
+
+ /**
+ * Whether any related suggestions are currently being shown
+ *
+ * @param $input
+ * @returns {boolean}
+ */
+ hasSuggestions: function ($input) {
+ var $ul = $input.siblings('ul.director-suggestions');
+ return $ul.length > 0 && $ul.is(':visible');
+ },
+
+ /**
+ * Get a suggestion list. Optionally force refresh
+ *
+ * @param $input
+ * @param $forceRefresh
+ *
+ * @returns {jQuery}
+ */
+ getSuggestionList: function ($input, $forceRefresh) {
+ var $ul = $input.siblings('ul.director-suggestions');
+ if ($ul.length) {
+ if ($forceRefresh) {
+ return this.refreshSuggestionList($ul, $input);
+ } else {
+ return $ul;
+ }
+ } else {
+ $ul = $('<ul class="director-suggestions"></ul>');
+ $input.parent().css({
+ position: 'relative'
+ });
+ $ul.insertAfter($input);
+ var suggestionWidth = (parseInt($input.css('width')) * 2) + 'px';
+ $ul.css({width: suggestionWidth});
+ return this.refreshSuggestionList($ul, $input);
+ }
+ },
+
+ /**
+ * Refresh a given suggestion list
+ *
+ * @param $suggestions
+ *
+ * @param $el
+ * @returns {jQuery}
+ */
+ refreshSuggestionList: function ($suggestions, $el) {
+ // Not sure whether we need this Accept-header
+ var headers = { 'X-Icinga-Accept': 'text/html' };
+ var icinga = this.module.icinga;
+
+ // Ask for a new window id in case we don't already have one
+ if (icinga.ui.hasWindowId()) {
+ headers['X-Icinga-WindowId'] = icinga.ui.getWindowId();
+ } else {
+ headers['X-Icinga-WindowId'] = 'undefined';
+ }
+
+ // var onResponse = function (data, textStatus, req) {
+ var onResponse = function (data) {
+ $suggestions.html(data);
+ var $li = $suggestions.find('li');
+ if ($li.length) {
+ $suggestions.show();
+ } else {
+ $suggestions.hide();
+ }
+ };
+
+ var req = $.ajax({
+ type: 'POST',
+ url: this.module.icinga.config.baseUrl + '/director/suggest',
+ data: {
+ value: $el.val(),
+ context: $el.data('suggestion-context'),
+ for_host: $el.data('suggestion-for-host')
+ },
+ headers: headers
+ });
+ req.done(onResponse);
+
+ return $suggestions;
+ },
+
+ /**
+ * Click handler for proposed suggestions
+ *
+ * @param ev
+ */
+ clickSuggestion: function (ev) {
+ this.chooseSuggestion($(ev.currentTarget));
+ },
+
+ /**
+ * Choose a specific suggestion
+
+ * @param $suggestion
+ */
+ chooseSuggestion: function ($suggestion) {
+ var $el = $suggestion.closest('ul').siblings('.director-suggest');
+ var val = $suggestion.text();
+
+ // extract label and key from key
+ var re = /^(.+) \[(\w+)]$/;
+
+ var withLabel = val.match(re);
+ if (withLabel) {
+ val = withLabel[2];
+ }
+
+ if (val.match(/\.$/)) {
+ $el.val(val);
+ this.getSuggestionList($el, true);
+ } else {
+ $el.focus();
+ $el.val(val);
+ $el.trigger('change');
+ this.getSuggestionList($el).remove();
+ }
+ },
+
+ /**
+ * Choose the current active suggestion related to a given element
+ *
+ * Returns true in case there was any, false otherwise
+ *
+ * @param $el
+ * @returns {boolean}
+ */
+ chooseActiveSuggestion: function ($el) {
+ var $list = this.getSuggestionList($el);
+ var $active = $list.find('li.active');
+ if ($active.length === 0) {
+ $active = $list.find('li:hover');
+ }
+ if ($active.length) {
+ this.chooseSuggestion($active);
+ return true;
+ } else {
+ $list.remove();
+ return false;
+ }
+ },
+
+ hasActiveSuggestion: function ($el) {
+ if (this.hasSuggestions($el)) {
+ var $list = this.getSuggestionList($el);
+ var $active = $list.find('li.active');
+ if ($active.length === 0) {
+ $active = $list.find('li:hover');
+ }
+ return $active.length > 0;
+ } else {
+ return false;
+ }
+ },
+
+ /**
+ * Remove related suggestion list if any
+ *
+ * @param $el
+ */
+ removeSuggestionList: function ($el) {
+ if (this.hasSuggestionList($el)) {
+ this.getSuggestionList($el).remove();
+ }
+ },
+
+ /**
+ * Show suggestions when arriving to an empty auto-completion field
+ *
+ * @param ev
+ */
+ enterSuggestionField: function (ev) {
+ // Has been disabled long time ago, as we do not want to open
+ // extensible Sets on focus. Should we re-enable this and just
+ // blacklist extensible sets?
+ //
+ // var $el = $(ev.currentTarget);
+ // if ($el.val() === '' || $el.val().match(/\.$/)) {
+ // this.getSuggestionList($el)
+ // }
+ },
+
+ /**
+ * Close suggestions when leaving the related form element
+ *
+ * @param ev
+ */
+ leaveSuggestionField: function (ev) {
+// return;
+ var _this = this;
+ setTimeout(function () {
+ _this.removeSuggestionList($(ev.currentTarget));
+ }, 100);
+ },
+
+ /**
+ * Sets an autosubmit flag on the container related to an event
+ *
+ * This will be used in beforeRender to determine whether the request has been triggered by an
+ * auto-submission
+ *
+ * @param ev
+ */
+ setAutoSubmitted: function (ev) {
+ $(ev.currentTarget).closest('.container').data('directorAutosubmit', 'yes');
+ },
+
+ /**
+ * Caused problems with differing tabs, should not be used
+ *
+ * @deprecated
+ */
+ detailTabClick: function (ev) {
+ var $a = $(ev.currentTarget);
+ if ($a.closest('#col2').length === 0) {
+ return;
+ }
+
+ this.alignDetailLinks();
+ },
+
+ /**
+ * Caused problems with differing tabs, should not be used
+ *
+ * @deprecated
+ */
+ alignDetailLinks: function () {
+ var self = this;
+ var $a = $('#col2').find('div.controls ul.tabs li.active a');
+ if ($a.length !== 1) {
+ return;
+ }
+
+ var $leftTable = $('#col1').find('> div.content').find('table.icinga-objects');
+ if ($leftTable.length !== 1) {
+ return;
+ }
+
+ var tabPath = self.pathFromHref($a);
+
+ $leftTable.find('tr').each(function (idx, tr) {
+ var $tr = $(tr);
+ if ($tr.is('[href]')) {
+ self.setHrefPath($tr, tabPath);
+ } else {
+ // Unfortunately we currently run BEFORE the action table
+ // handler
+ var $a = $tr.find('a[href].rowaction');
+ if ($a.length === 0) {
+ $a = $tr.find('a[href]').first();
+ }
+
+ if ($a.length) {
+ self.setHrefPath($a, tabPath);
+ }
+ }
+ });
+
+ $leftTable.find('tr[href]').each(function (idx, tr) {
+ var $tr = $(tr);
+ self.setHrefPath($tr, tabPath);
+ });
+ },
+
+ pathFromHref: function ($el) {
+ return this.module.icinga.utils.parseUrl($el.attr('href')).path
+ },
+
+ setHrefPath: function ($el, path) {
+ var a = this.module.icinga.utils.getUrlHelper();
+ a.href = $el.attr('href');
+ a.pathname = path;
+ $el.attr('href', a.href);
+ },
+
+ extensibleSetAction: function (ev) {
+ var iid, $li, $prev, $next;
+ var el = ev.currentTarget;
+ if (el.name.match(/__MOVE_UP$/)) {
+ $li = $(el).closest('li');
+ $prev = $li.prev();
+ // TODO: document what's going on here.
+ if ($li.find('input[type=text].autosubmit')) {
+ iid = $prev.find('input[type=text]').attr('id');
+ if (iid) {
+ $li.closest('.container').data('activeExtensibleEntry', iid);
+ } else {
+ return true;
+ }
+ }
+ if ($prev.length) {
+ $prev.before($li.detach());
+ this.fixRelatedActions($li.closest('ul'));
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ return false;
+ } else if (el.name.match(/__MOVE_DOWN$/)) {
+ $li = $(el).closest('li');
+ $next = $li.next();
+ // TODO: document what's going on here.
+ if ($li.find('input[type=text].autosubmit')) {
+ iid = $next.find('input[type=text]').attr('id');
+ if (iid) {
+ $li.closest('.container').data('activeExtensibleEntry', iid);
+ } else {
+ return true;
+ }
+ }
+ if ($next.length && ! $next.find('.extend-set').length) {
+ $next.after($li.detach());
+ this.fixRelatedActions($li.closest('ul'));
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ return false;
+ } else if (el.name.match(/__REMOVE$/)) {
+ $li = $(el).closest('li');
+ if ($li.find('.autosubmit').length) {
+ // Autosubmit element, let the server handle this
+ return true;
+ }
+
+ $li.remove();
+ this.fixRelatedActions($li.closest('ul'));
+ ev.preventDefault();
+ ev.stopPropagation();
+ return false;
+ } else if (el.name.match(/__DROP_DOWN$/)) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ var $el = $(ev.currentTarget).closest('li').find('input[type=text]');
+ this.getSuggestionList($el);
+ return false;
+ }
+ },
+
+ fixRelatedActions: function ($ul) {
+ var $uls = $ul.find('li');
+ var last = $uls.length - 1;
+ if ($ul.find('.extend-set').length) {
+ last--;
+ }
+
+ $uls.each(function (idx, li) {
+ var $li = $(li);
+ if (idx === 0) {
+ $li.find('.action-move-up').attr('disabled', 'disabled');
+ if (last === 0) {
+ $li.find('.action-move-down').attr('disabled', 'disabled');
+ } else {
+ $li.find('.action-move-down').removeAttr('disabled');
+ }
+ } else if (idx === last) {
+ $li.find('.action-move-up').removeAttr('disabled');
+ $li.find('.action-move-down').attr('disabled', 'disabled');
+ } else {
+ $li.find('.action-move-up').removeAttr('disabled');
+ $li.find('.action-move-down').removeAttr('disabled');
+ }
+ });
+ },
+
+ formElementFocus: function (ev) {
+ var $input = $(ev.currentTarget);
+ if ($input.closest('form.editor').length) {
+ return;
+ }
+ var $set = $input.closest('.extensible-set');
+ if ($set.length) {
+ var $textInputs = $('input[type=text]', $set);
+ if ($textInputs.length > 1) {
+ $textInputs.not(':first').attr('tabIndex', '-1');
+ }
+ }
+
+ var $dd = $input.closest('dd');
+ if ($dd.attr('id') && $dd.attr('id').match(/button/)) {
+ return;
+ }
+ var $li = $input.closest('li');
+ var $dt = $dd.prev();
+ var $form = $dd.closest('form');
+
+ $form.find('dt, dd, li').removeClass('active');
+ $li.addClass('active');
+ $dt.addClass('active');
+ $dd.addClass('active');
+ },
+
+ highlightFormErrors: function ($container) {
+ $container.find('dd ul.errors').each(function (idx, ul) {
+ var $ul = $(ul);
+ var $dd = $ul.closest('dd');
+ var $dt = $dd.prev();
+
+ $dt.addClass('errors');
+ $dd.addClass('errors');
+ });
+ },
+
+ toggleFieldset: function (ev) {
+ ev.stopPropagation();
+ var $fieldset = $(ev.currentTarget).closest('fieldset');
+ $fieldset.toggleClass('collapsed');
+ this.fixFieldsetInfo($fieldset);
+ this.openedFieldsets[$fieldset.attr('id')] = ! $fieldset.hasClass('collapsed');
+ },
+
+ beforeRender: function (ev) {
+ var $container = $(ev.currentTarget);
+ var id = $container.attr('id');
+ var requests = this.module.icinga.loader.requests;
+ if (typeof requests[id] !== 'undefined' && requests[id].autorefresh) {
+ $container.data('director-autorefreshed', 'yes');
+ } else {
+ $container.removeData('director-autorefreshed');
+ }
+
+ // Remove the temporary directorAutosubmit flag and set or remove
+ // the directorAutosubmitted property accordingly
+ if ($container.data('directorAutosubmit') === 'yes') {
+ $container.removeData('directorAutosubmit');
+ $container.data('directorAutosubmitted', 'yes');
+ } else {
+ $container.removeData('directorAutosubmitted');
+ }
+ },
+
+ /**
+ * Whether the given container has been autosubmitted
+ *
+ * @param $container
+ * @returns {boolean}
+ */
+ containerIsAutoSubmitted: function ($container) {
+ return $container.data('directorAutosubmitted') === 'yes';
+ },
+
+ /**
+ * Whether the given container has been autorefreshed
+ *
+ * @param $container
+ * @returns {boolean}
+ */
+ containerIsAutorefreshed: function ($container) {
+ return $container.data('director-autorefreshed') === 'yes';
+ },
+
+ rendered: function (ev) {
+ var iid;
+ var icinga = this.module.icinga;
+ var $container = $(ev.currentTarget);
+ if ($container.children('div.controls').first().data('directorWindowId') === '_UNDEFINED_') {
+ var $url = $container.data('icingaUrl');
+ if (typeof $url !== 'undefined') {
+ icinga.loader.loadUrl($url, $container).autorefresh = true;
+ }
+
+ $container.children('div.controls').children().hide();
+ $container.children('div.content').hide();
+ return;
+ }
+ this.restoreContainerFieldsets($container);
+ this.backupAllExtensibleSetDefaultValues($container);
+ this.highlightFormErrors($container);
+ this.scrollHighlightIntoView($container);
+ this.scrollActiveRowIntoView($container);
+ this.highlightActiveDashlet($container);
+ iid = $container.data('activeExtensibleEntry');
+ if (iid) {
+ $('#' + iid).focus();
+ $container.removeData('activeExtensibleEntry');
+ }
+ // Disabled for now
+ // this.alignDetailLinks();
+ if (! this.containerIsAutorefreshed($container) && ! this.containerIsAutoSubmitted($container)) {
+ this.putFocusOnFirstFormElement($container);
+ }
+
+ // Turn off autocomplete for all suggested fields
+ $container.find('input.director-suggest').each(this.disableAutocomplete);
+ },
+
+ highlightActiveDashlet: function ($container) {
+ if (this.module.icinga.ui.isOneColLayout()) {
+ return;
+ }
+
+ var url, $actions, $match;
+ var id = $container.attr('id');
+ if (id === 'col1') {
+ url = $('#col2').data('icingaUrl');
+ $actions = $('.main-actions', $container);
+ } else if (id === 'col2') {
+ url = $container.data('icingaUrl');
+ $actions = $('.main-actions', $('#col1'));
+ }
+ if (! $actions.length) {
+ return;
+ }
+
+ $match = $('li a[href*="' + url + '"]', $actions);
+ if ($match.length) {
+ $('li a.active', $actions).removeClass('active');
+ $match.first().addClass('active');
+ }
+ },
+
+ restoreContainerFieldsets: function ($container) {
+ var self = this;
+ $container.find('form').each(self.restoreFieldsets.bind(self));
+ },
+
+ putFocusOnFirstFormElement: function ($container) {
+ $container.find('form.autofocus').find('label').first().focus();
+ },
+
+ scrollHighlightIntoView: function ($container) {
+ var $hl = $container.find('.highlight');
+ var $content = $container.find('> div.content');
+
+ if ($hl.length) {
+ $container.animate({
+ scrollTop: $hl.offset().top - $content.offset().top
+ }, 700);
+ }
+ },
+
+ scrollActiveRowIntoView: function ($container) {
+ var $tr = $container.find('table.table-row-selectable > tbody > tr.active');
+ var $content = $container.find('> div.content');
+ if ($tr.length) {
+ $container.animate({
+ scrollTop: $tr.offset().top - $content.offset().top
+ }, 500);
+ }
+ },
+
+ backupAllExtensibleSetDefaultValues: function ($container) {
+ var self = this;
+ $container.find('.extensible-set').each(function (idx, eSet) {
+ $(eSet).find('input[type=text]').each(self.backupDefaultValue);
+ $(eSet).find('select').each(self.backupDefaultValue);
+ });
+ },
+
+ backupDefaultValue: function (idx, el) {
+ $(el).data('originalvalue', el.value);
+ },
+
+ restoreFieldsets: function (idx, form) {
+ var $form = $(form);
+ var self = this;
+ var $sets = $('fieldset', $form);
+
+ $sets.each(function (idx, fieldset) {
+ var $fieldset = $(fieldset);
+ if ($fieldset.attr('id') === 'fieldset-assign') {
+ return;
+ }
+ if ($fieldset.find('.required').length === 0 && (! self.fieldsetWasOpened($fieldset))) {
+ $fieldset.addClass('collapsed');
+ self.fixFieldsetInfo($fieldset);
+ }
+ });
+
+ if ($sets.length === 1) {
+ $sets.first().removeClass('collapsed');
+ }
+ },
+
+ fieldsetWasOpened: function ($fieldset) {
+ var id = $fieldset.attr('id');
+ if (typeof this.openedFieldsets[id] === 'undefined') {
+ return false;
+ }
+ return this.openedFieldsets[id];
+ },
+
+ fixFieldsetInfo: function ($fieldset) {
+ if ($fieldset.hasClass('collapsed')) {
+ if ($fieldset.find('legend span.element-count').length === 0) {
+ var cnt = $fieldset.find('dt, li').not('.extensible-set li').length;
+ if (cnt > 0) {
+ $fieldset.find('legend').append($('<span class="element-count"> (' + cnt + ')</span>'));
+ }
+ }
+ } else {
+ $fieldset.find('legend span.element-count').remove();
+ }
+ },
+
+ disableAutocomplete: function () {
+ $(this)
+ .attr('autocomplete', 'off')
+ .attr('autocorrect', 'off')
+ .attr('autocapitalize', 'off')
+ .attr('spellcheck', 'false');
+ }
+ };
+
+ Icinga.availableModules.director = Director;
+
+}(Icinga));
diff --git a/register-hooks.php b/register-hooks.php
new file mode 100644
index 0000000..62fd5f5
--- /dev/null
+++ b/register-hooks.php
@@ -0,0 +1,146 @@
+<?php
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Director\DataType\DataTypeArray;
+use Icinga\Module\Director\DataType\DataTypeBoolean;
+use Icinga\Module\Director\DataType\DataTypeDatalist;
+use Icinga\Module\Director\DataType\DataTypeDirectorObject;
+use Icinga\Module\Director\DataType\DataTypeDictionary;
+use Icinga\Module\Director\DataType\DataTypeNumber;
+use Icinga\Module\Director\DataType\DataTypeSqlQuery;
+use Icinga\Module\Director\DataType\DataTypeString;
+use Icinga\Module\Director\Import\ImportSourceCoreApi;
+use Icinga\Module\Director\Import\ImportSourceDirectorObject;
+use Icinga\Module\Director\Import\ImportSourceLdap;
+use Icinga\Module\Director\Import\ImportSourceRestApi;
+use Icinga\Module\Director\Import\ImportSourceSql;
+use Icinga\Module\Director\Job\ConfigJob;
+use Icinga\Module\Director\Job\HousekeepingJob;
+use Icinga\Module\Director\Job\ImportJob;
+use Icinga\Module\Director\Job\SyncJob;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierArrayElementByPosition;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierArrayFilter;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierArrayToRow;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierArrayUnique;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierBitmask;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierCombine;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierDictionaryToRow;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierDnsRecords;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierExtractFromDN;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierFromAdSid;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierFromLatin1;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierGetHostByAddr;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierGetHostByName;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierGetPropertyFromOtherImportSource;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierJoin;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierJsonDecode;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierLConfCustomVar;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierListToObject;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierLowercase;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierMakeBoolean;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierMap;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierNegateBoolean;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierParseURL;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierRegexReplace;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierRegexSplit;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierRejectOrSelect;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierRenameColumn;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierReplace;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierReplaceNull;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierSimpleGroupBy;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierSkipDuplicates;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierSplit;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierStripDomain;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierSubstring;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierToInt;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierTrim;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierUppercase;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierUpperCaseFirst;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierURLEncode;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierUuidBinToHex;
+use Icinga\Module\Director\PropertyModifier\PropertyModifierXlsNumericIp;
+use Icinga\Module\Director\ProvidedHook\CubeLinks;
+use Icinga\Module\Director\ProvidedHook\IcingaDbCubeLinks;
+
+/** @var Module $this */
+if ($this->getConfig()->get('frontend', 'disabled', 'no') !== 'yes') {
+ $this->provideHook('monitoring/HostActions');
+ $this->provideHook('monitoring/ServiceActions');
+ $this->provideHook('cube/Actions', CubeLinks::class);
+ $this->provideHook('cube/IcingaDbActions', IcingaDbCubeLinks::class);
+}
+
+$directorHooks = [
+ 'director/DataType' => [
+ DataTypeArray::class,
+ DataTypeBoolean::class,
+ DataTypeDatalist::class,
+ DataTypeDictionary::class,
+ DataTypeNumber::class,
+ DataTypeDirectorObject::class,
+ DataTypeSqlQuery::class,
+ DataTypeString::class
+ ],
+ 'director/ImportSource' => [
+ ImportSourceDirectorObject::class,
+ ImportSourceSql::class,
+ ImportSourceLdap::class,
+ ImportSourceCoreApi::class,
+ ImportSourceRestApi::class
+ ],
+ 'director/Job' => [
+ ConfigJob::class,
+ HousekeepingJob::class,
+ ImportJob::class,
+ SyncJob::class,
+ ],
+ 'director/PropertyModifier' => [
+ PropertyModifierArrayElementByPosition::class,
+ PropertyModifierArrayFilter::class,
+ PropertyModifierArrayToRow::class,
+ PropertyModifierArrayUnique::class,
+ PropertyModifierBitmask::class,
+ PropertyModifierCombine::class,
+ PropertyModifierDictionaryToRow::class,
+ PropertyModifierDnsRecords::class,
+ PropertyModifierExtractFromDN::class,
+ PropertyModifierFromAdSid::class,
+ PropertyModifierFromLatin1::class,
+ PropertyModifierGetHostByAddr::class,
+ PropertyModifierGetHostByName::class,
+ PropertyModifierGetPropertyFromOtherImportSource::class,
+ PropertyModifierJoin::class,
+ PropertyModifierJsonDecode::class,
+ PropertyModifierLConfCustomVar::class,
+ PropertyModifierListToObject::class,
+ PropertyModifierLowercase::class,
+ PropertyModifierMakeBoolean::class,
+ PropertyModifierMap::class,
+ PropertyModifierNegateBoolean::class,
+ PropertyModifierParseURL::class,
+ PropertyModifierRegexReplace::class,
+ PropertyModifierRegexSplit::class,
+ PropertyModifierRejectOrSelect::class,
+ PropertyModifierRenameColumn::class,
+ PropertyModifierReplace::class,
+ PropertyModifierReplaceNull::class,
+ PropertyModifierSimpleGroupBy::class,
+ PropertyModifierSkipDuplicates::class,
+ PropertyModifierSplit::class,
+ PropertyModifierStripDomain::class,
+ PropertyModifierSubstring::class,
+ PropertyModifierToInt::class,
+ PropertyModifierTrim::class,
+ PropertyModifierUppercase::class,
+ PropertyModifierUpperCaseFirst::class,
+ PropertyModifierURLEncode::class,
+ PropertyModifierUuidBinToHex::class,
+ PropertyModifierXlsNumericIp::class,
+ ]
+];
+
+foreach ($directorHooks as $type => $classNames) {
+ foreach ($classNames as $className) {
+ $this->provideHook($type, $className);
+ }
+}
diff --git a/run-missingdeps.php b/run-missingdeps.php
new file mode 100644
index 0000000..888692d
--- /dev/null
+++ b/run-missingdeps.php
@@ -0,0 +1,23 @@
+<?php
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Url;
+
+if (Icinga::app()->isCli()) {
+ throw new IcingaException(
+ "Missing dependencies, please check "
+ );
+} else {
+ $request = Icinga::app()->getRequest();
+ $path = $request->getPathInfo();
+ if (! preg_match('#^/director#', $path)) {
+ return;
+ }
+ if (preg_match('#^/director/phperror/dependencies#', $path)) {
+ return;
+ }
+
+ header('Location: ' . Url::fromPath('director/phperror/dependencies'));
+ exit;
+}
diff --git a/run-php5.3.php b/run-php5.3.php
new file mode 100644
index 0000000..f79cec5
--- /dev/null
+++ b/run-php5.3.php
@@ -0,0 +1,26 @@
+<?php
+
+use Icinga\Application\Icinga;
+
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Url;
+
+if (Icinga::app()->isCli()) {
+ throw new IcingaException(
+ "PHP version 5.6.x is required for Director >= 1.7.0, you're running %s."
+ . ' Please either upgrade PHP or downgrade Icinga Director',
+ PHP_VERSION
+ );
+} else {
+ $request = Icinga::app()->getRequest();
+ $path = $request->getPathInfo();
+ if (! preg_match('#^/director#', $path)) {
+ return;
+ }
+ if (preg_match('#^/director/phperror/error#', $path)) {
+ return;
+ }
+
+ header('Location: ' . Url::fromPath('director/phperror/error'));
+ exit;
+}
diff --git a/run.php b/run.php
new file mode 100644
index 0000000..8f821ed
--- /dev/null
+++ b/run.php
@@ -0,0 +1,18 @@
+<?php
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Director\Application\DependencyChecker;
+
+if (version_compare(PHP_VERSION, '5.6.0') < 0) {
+ include __DIR__ . '/run-php5.3.php';
+ return;
+}
+
+/** @var Module $this */
+$checker = new DependencyChecker($this->app);
+if (! $checker->satisfiesDependencies($this)) {
+ include __DIR__ . '/run-missingdeps.php';
+ return;
+}
+
+include __DIR__ . '/register-hooks.php';
diff --git a/schema/mysql-legacy-changes/upgrade_1.sql b/schema/mysql-legacy-changes/upgrade_1.sql
new file mode 100644
index 0000000..e9b1008
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_1.sql
@@ -0,0 +1,2 @@
+ALTER TABLE icinga_user ADD COLUMN object_type ENUM('object', 'template') NOT NULL AFTER zone_id;
+
diff --git a/schema/mysql-legacy-changes/upgrade_10.sql b/schema/mysql-legacy-changes/upgrade_10.sql
new file mode 100644
index 0000000..2709186
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_10.sql
@@ -0,0 +1,14 @@
+ALTER TABLE icinga_hostgroup_parent DROP FOREIGN KEY icinga_hostgroup_parent_parent;
+ALTER TABLE icinga_hostgroup_parent ADD CONSTRAINT icinga_hostgroup_parent_parent
+ FOREIGN KEY parent (parent_hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_usergroup_parent DROP FOREIGN KEY icinga_usergroup_parent_parent;
+ALTER TABLE icinga_usergroup_parent ADD CONSTRAINT icinga_usergroup_parent_parent
+ FOREIGN KEY parent (parent_usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
diff --git a/schema/mysql-legacy-changes/upgrade_11.sql b/schema/mysql-legacy-changes/upgrade_11.sql
new file mode 100644
index 0000000..8aaf71a
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_11.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_zone ADD is_global ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
diff --git a/schema/mysql-legacy-changes/upgrade_12.sql b/schema/mysql-legacy-changes/upgrade_12.sql
new file mode 100644
index 0000000..5ee2525
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_12.sql
@@ -0,0 +1,17 @@
+CREATE TABLE icinga_service_inheritance (
+ service_id INT(10) UNSIGNED NOT NULL,
+ parent_service_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (service_id, parent_service_id),
+ UNIQUE KEY unique_order (service_id, weight),
+ CONSTRAINT icinga_service_inheritance_service
+ FOREIGN KEY host (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_inheritance_parent_service
+ FOREIGN KEY host (parent_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_13.sql b/schema/mysql-legacy-changes/upgrade_13.sql
new file mode 100644
index 0000000..96b0aad
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_13.sql
@@ -0,0 +1,18 @@
+CREATE TABLE icinga_user_inheritance (
+ user_id INT(10) UNSIGNED NOT NULL,
+ parent_user_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (user_id, parent_user_id),
+ UNIQUE KEY unique_order (user_id, weight),
+ CONSTRAINT icinga_user_inheritance_user
+ FOREIGN KEY host (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_inheritance_parent_user
+ FOREIGN KEY host (parent_user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
diff --git a/schema/mysql-legacy-changes/upgrade_14.sql b/schema/mysql-legacy-changes/upgrade_14.sql
new file mode 100644
index 0000000..506f779
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_14.sql
@@ -0,0 +1,18 @@
+CREATE TABLE icinga_timeperiod_inheritance (
+ timeperiod_id INT(10) UNSIGNED NOT NULL,
+ parent_timeperiod_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (timeperiod_id, parent_timeperiod_id),
+ UNIQUE KEY unique_order (timeperiod_id, weight),
+ CONSTRAINT icinga_timeperiod_inheritance_timeperiod
+ FOREIGN KEY host (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_timeperiod_inheritance_parent_timeperiod
+ FOREIGN KEY host (parent_timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
diff --git a/schema/mysql-legacy-changes/upgrade_15.sql b/schema/mysql-legacy-changes/upgrade_15.sql
new file mode 100644
index 0000000..c5f7dd7
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_15.sql
@@ -0,0 +1,17 @@
+CREATE TABLE icinga_hostgroup_inheritance (
+ hostgroup_id INT(10) UNSIGNED NOT NULL,
+ parent_hostgroup_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (hostgroup_id, parent_hostgroup_id),
+ UNIQUE KEY unique_order (hostgroup_id, weight),
+ CONSTRAINT icinga_hostgroup_inheritance_hostgroup
+ FOREIGN KEY host (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_inheritance_parent_hostgroup
+ FOREIGN KEY host (parent_hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_16.sql b/schema/mysql-legacy-changes/upgrade_16.sql
new file mode 100644
index 0000000..0a41a68
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_16.sql
@@ -0,0 +1,17 @@
+CREATE TABLE icinga_servicegroup_inheritance (
+ servicegroup_id INT(10) UNSIGNED NOT NULL,
+ parent_servicegroup_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (servicegroup_id, parent_servicegroup_id),
+ UNIQUE KEY unique_order (servicegroup_id, weight),
+ CONSTRAINT icinga_servicegroup_inheritance_servicegroup
+ FOREIGN KEY host (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_inheritance_parent_servicegroup
+ FOREIGN KEY host (parent_servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_17.sql b/schema/mysql-legacy-changes/upgrade_17.sql
new file mode 100644
index 0000000..58f7e88
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_17.sql
@@ -0,0 +1,17 @@
+CREATE TABLE icinga_command_inheritance (
+ command_id INT(10) UNSIGNED NOT NULL,
+ parent_command_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (command_id, parent_command_id),
+ UNIQUE KEY unique_order (command_id, weight),
+ CONSTRAINT icinga_command_inheritance_command
+ FOREIGN KEY command (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_command_inheritance_parent_command
+ FOREIGN KEY command (parent_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_18.sql b/schema/mysql-legacy-changes/upgrade_18.sql
new file mode 100644
index 0000000..5d3eb85
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_18.sql
@@ -0,0 +1,17 @@
+CREATE TABLE icinga_zone_inheritance (
+ zone_id INT(10) UNSIGNED NOT NULL,
+ parent_zone_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (zone_id, parent_zone_id),
+ UNIQUE KEY unique_order (zone_id, weight),
+ CONSTRAINT icinga_zone_inheritance_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_zone_inheritance_parent_zone
+ FOREIGN KEY zone (parent_zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_19.sql b/schema/mysql-legacy-changes/upgrade_19.sql
new file mode 100644
index 0000000..2980ccc
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_19.sql
@@ -0,0 +1,17 @@
+CREATE TABLE icinga_usergroup_inheritance (
+ usergroup_id INT(10) UNSIGNED NOT NULL,
+ parent_usergroup_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (usergroup_id, parent_usergroup_id),
+ UNIQUE KEY unique_order (usergroup_id, weight),
+ CONSTRAINT icinga_usergroup_inheritance_usergroup
+ FOREIGN KEY usergroup (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_usergroup_inheritance_parent_usergroup
+ FOREIGN KEY usergroup (parent_usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_2.sql b/schema/mysql-legacy-changes/upgrade_2.sql
new file mode 100644
index 0000000..04f3733
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_2.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_command_argument ADD UNIQUE KEY unique_idx (command_id, argument_name);
diff --git a/schema/mysql-legacy-changes/upgrade_20.sql b/schema/mysql-legacy-changes/upgrade_20.sql
new file mode 100644
index 0000000..91a86a2
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_20.sql
@@ -0,0 +1,17 @@
+CREATE TABLE icinga_endpoint_inheritance (
+ endpoint_id INT(10) UNSIGNED NOT NULL,
+ parent_endpoint_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (endpoint_id, parent_endpoint_id),
+ UNIQUE KEY unique_order (endpoint_id, weight),
+ CONSTRAINT icinga_endpoint_inheritance_endpoint
+ FOREIGN KEY endpoint (endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_endpoint_inheritance_parent_endpoint
+ FOREIGN KEY endpoint (parent_endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_21.sql b/schema/mysql-legacy-changes/upgrade_21.sql
new file mode 100644
index 0000000..7c72b86
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_21.sql
@@ -0,0 +1,15 @@
+DROP TABLE director_datalist_value;
+
+CREATE TABLE director_datalist_entry (
+ list_id INT(10) UNSIGNED NOT NULL,
+ entry_name VARCHAR(255) DEFAULT NULL,
+ entry_value TEXT DEFAULT NULL,
+ format enum ('string', 'expression', 'json'),
+ PRIMARY KEY (list_id, entry_name),
+ CONSTRAINT director_datalist_value_datalist
+ FOREIGN KEY datalist (list_id)
+ REFERENCES director_datalist (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
diff --git a/schema/mysql-legacy-changes/upgrade_22.sql b/schema/mysql-legacy-changes/upgrade_22.sql
new file mode 100644
index 0000000..418145c
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_22.sql
@@ -0,0 +1,43 @@
+CREATE TABLE icinga_host_field (
+ host_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ fieldname VARCHAR(64) NOT NULL,
+ caption VARCHAR(255) NOT NULL,
+ datatype_id INT(10) UNSIGNED NOT NULL,
+-- datatype_param? multiple ones?
+ default_value TEXT DEFAULT NULL,
+ format enum ('string', 'json', 'expression'),
+ PRIMARY KEY (host_id, fieldname),
+ KEY search_idx (fieldname),
+ CONSTRAINT icinga_host_field_host
+ FOREIGN KEY host(host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_field_datatype
+ FOREIGN KEY datatype (datatype_id)
+ REFERENCES director_datatype (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_field (
+ service_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ fieldname VARCHAR(64) NOT NULL,
+ caption VARCHAR(255) NOT NULL,
+ datatype_id INT(10) UNSIGNED NOT NULL,
+ -- datatype_param? multiple ones?
+ default_value TEXT DEFAULT NULL,
+ format enum ('string', 'json', 'expression'),
+ PRIMARY KEY (service_id, fieldname),
+ KEY search_idx (fieldname),
+ CONSTRAINT icinga_service_field_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_field_datatype
+ FOREIGN KEY datatype (datatype_id)
+ REFERENCES director_datatype (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_23.sql b/schema/mysql-legacy-changes/upgrade_23.sql
new file mode 100644
index 0000000..2368a17
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_23.sql
@@ -0,0 +1,51 @@
+DROP TABLE director_datatype;
+
+CREATE TABLE director_datafield (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ varname VARCHAR(64) NOT NULL,
+ caption VARCHAR(255) NOT NULL,
+ description TEXT DEFAULT NULL,
+ datatype varchar(255) NOT NULL,
+-- datatype_param? multiple ones?
+ format enum ('string', 'json', 'expression'),
+ PRIMARY KEY (id),
+ KEY search_idx (varname)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+DROP TABLE icinga_host_field;
+
+CREATE TABLE icinga_host_field (
+ host_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') DEFAULT NULL,
+ PRIMARY KEY (host_id, datafield_id),
+ CONSTRAINT icinga_host_field_host
+ FOREIGN KEY host(host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+DROP TABLE icinga_service_field;
+
+CREATE TABLE icinga_service_field (
+ service_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') DEFAULT NULL,
+ PRIMARY KEY (service_id, datafield_id),
+ CONSTRAINT icinga_service_field_service
+ FOREIGN KEY service(service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file
diff --git a/schema/mysql-legacy-changes/upgrade_24.sql b/schema/mysql-legacy-changes/upgrade_24.sql
new file mode 100644
index 0000000..4c380b1
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_24.sql
@@ -0,0 +1,91 @@
+CREATE TABLE import_source (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ source_name VARCHAR(64) NOT NULL,
+ provider_class VARCHAR(72) NOT NULL,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE import_source_setting (
+ source_id INT(10) UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value VARCHAR(255) NOT NULL,
+ PRIMARY KEY (source_id, setting_name),
+ CONSTRAINT import_source_settings_source
+ FOREIGN KEY source (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE imported_rowset (
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (checksum)
+) ENGINE=InnoDB;
+
+CREATE TABLE import_run (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ source_id INT(10) UNSIGNED NOT NULL,
+ imported_rowset_checksum VARBINARY(20) DEFAULT NULL,
+ start_time DATETIME NOT NULL,
+ end_time DATETIME DEFAULT NULL,
+ succeeded ENUM('y', 'n') DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT import_run_source
+ FOREIGN KEY import_source (source_id)
+ REFERENCES import_source (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT import_run_rowset
+ FOREIGN KEY rowset (imported_rowset_checksum)
+ REFERENCES imported_rowset (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE imported_row (
+ checksum VARBINARY(20) NOT NULL COMMENT 'sha1(object_name;property_checksum;...)',
+ object_name VARCHAR(255) NOT NULL,
+ PRIMARY KEY (checksum)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE imported_rowset_row (
+ rowset_checksum VARBINARY(20) NOT NULL,
+ row_checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (rowset_checksum, row_checksum),
+ CONSTRAINT imported_rowset_row_rowset
+ FOREIGN KEY rowset_row_rowset (rowset_checksum)
+ REFERENCES imported_rowset (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT imported_rowset_row_row
+ FOREIGN KEY rowset_row_rowset (row_checksum)
+ REFERENCES imported_row (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE imported_property (
+ checksum VARBINARY(20) NOT NULL,
+ property_name VARCHAR(64) NOT NULL,
+ property_value TEXT NOT NULL,
+ format enum ('string', 'expression', 'json'),
+ PRIMARY KEY (checksum),
+ KEY search_idx (property_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE imported_row_property (
+ row_checksum VARBINARY(20) NOT NULL,
+ property_checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (row_checksum, property_checksum),
+ CONSTRAINT imported_row_property_row
+ FOREIGN KEY row_checksum (row_checksum)
+ REFERENCES imported_row (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT imported_row_property_property
+ FOREIGN KEY property_checksum (property_checksum)
+ REFERENCES imported_property (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
diff --git a/schema/mysql-legacy-changes/upgrade_25.sql b/schema/mysql-legacy-changes/upgrade_25.sql
new file mode 100644
index 0000000..f4d2d90
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_25.sql
@@ -0,0 +1,2 @@
+ALTER TABLE import_source ADD COLUMN key_column VARCHAR(64) NOT NULL AFTER source_name;
+ALTER TABLE import_source ADD INDEX search_idx (key_column);
diff --git a/schema/mysql-legacy-changes/upgrade_26.sql b/schema/mysql-legacy-changes/upgrade_26.sql
new file mode 100644
index 0000000..d7f0c46
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_26.sql
@@ -0,0 +1,4 @@
+ALTER TABLE import_run DROP FOREIGN KEY import_run_rowset;
+ALTER TABLE import_run CHANGE imported_rowset_checksum rowset_checksum varbinary(20) DEFAULT NULL;
+ALTER TABLE import_run ADD CONSTRAINT import_run_rowset FOREIGN KEY rowset (rowset_checksum) REFERENCES imported_rowset (checksum) ON DELETE RESTRICT ON UPDATE CASCADE;
+
diff --git a/schema/mysql-legacy-changes/upgrade_27.sql b/schema/mysql-legacy-changes/upgrade_27.sql
new file mode 100644
index 0000000..684e45c
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_27.sql
@@ -0,0 +1,58 @@
+
+CREATE TABLE sync_rule (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ rule_name VARCHAR(255) NOT NULL,
+ object_type ENUM('host', 'user') NOT NULL,
+ update_policy ENUM('merge', 'override', 'ignore') NOT NULL,
+ purge_existing ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ filter_expression TEXT NOT NULL,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE sync_property (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ rule_id INT(10) UNSIGNED NOT NULL,
+ source_id INT(10) UNSIGNED NOT NULL,
+ source_expression VARCHAR(255) NOT NULL,
+ destination_field VARCHAR(64),
+ priority SMALLINT UNSIGNED NOT NULL,
+ filter_expression TEXT DEFAULT NULL,
+ merge_policy ENUM('override', 'merge') NOT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT sync_property_rule
+ FOREIGN KEY sync_rule (rule_id)
+ REFERENCES sync_rule (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT sync_property_source
+ FOREIGN KEY import_source (source_id)
+ REFERENCES import_source (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE sync_modifier (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ property_id INT(10) UNSIGNED NOT NULL,
+ provider_class VARCHAR(72) NOT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT sync_modifier_property
+ FOREIGN KEY sync_property (property_id)
+ REFERENCES sync_property (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE sync_modifier_param (
+ modifier_id INT UNSIGNED NOT NULL,
+ param_name VARCHAR(64) NOT NULL,
+ param_value TEXT DEFAULT NULL,
+ PRIMARY KEY (modifier_id, param_name),
+ CONSTRAINT sync_modifier_param_modifier
+ FOREIGN KEY modifier (modifier_id)
+ REFERENCES sync_modifier (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
diff --git a/schema/mysql-legacy-changes/upgrade_28.sql b/schema/mysql-legacy-changes/upgrade_28.sql
new file mode 100644
index 0000000..2451311
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_28.sql
@@ -0,0 +1 @@
+ALTER TABLE sync_rule MODIFY filter_expression TEXT DEFAULT NULL;
diff --git a/schema/mysql-legacy-changes/upgrade_29.sql b/schema/mysql-legacy-changes/upgrade_29.sql
new file mode 100644
index 0000000..e841a7f
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_29.sql
@@ -0,0 +1,16 @@
+DROP TABLE sync_modifier_param;
+DROP TABLE sync_modifier;
+
+CREATE TABLE import_row_modifier (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ property_id INT(10) UNSIGNED NOT NULL,
+ provider_class VARCHAR(72) NOT NULL,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE import_row_modifier_settings (
+ modifier_id INT UNSIGNED NOT NULL,
+ settings_name VARCHAR(64) NOT NULL,
+ settings_value TEXT DEFAULT NULL,
+ PRIMARY KEY (modifier_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_3.sql b/schema/mysql-legacy-changes/upgrade_3.sql
new file mode 100644
index 0000000..1a093d6
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_3.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_hostgroup ADD COLUMN object_type ENUM('object', 'template') NOT NULL AFTER display_name; \ No newline at end of file
diff --git a/schema/mysql-legacy-changes/upgrade_30.sql b/schema/mysql-legacy-changes/upgrade_30.sql
new file mode 100644
index 0000000..1cd60b8
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_30.sql
@@ -0,0 +1,8 @@
+DROP TABLE import_row_modifier_settings;
+
+CREATE TABLE import_row_modifier_setting (
+ modifier_id INT UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT DEFAULT NULL,
+ PRIMARY KEY (modifier_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_31.sql b/schema/mysql-legacy-changes/upgrade_31.sql
new file mode 100644
index 0000000..d8031ed
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_31.sql
@@ -0,0 +1,2 @@
+ALTER TABLE director_generated_file MODIFY content MEDIUMTEXT DEFAULT NULL;
+
diff --git a/schema/mysql-legacy-changes/upgrade_32.sql b/schema/mysql-legacy-changes/upgrade_32.sql
new file mode 100644
index 0000000..4d584de
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_32.sql
@@ -0,0 +1 @@
+ALTER TABLE import_source_setting MODIFY setting_value TEXT DEFAULT NULL;
diff --git a/schema/mysql-legacy-changes/upgrade_33.sql b/schema/mysql-legacy-changes/upgrade_33.sql
new file mode 100644
index 0000000..19f7e45
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_33.sql
@@ -0,0 +1,11 @@
+CREATE TABLE director_datafield_setting (
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT NOT NULL,
+ PRIMARY KEY (datafield_id, setting_name),
+ CONSTRAINT datafield_id_settings
+ FOREIGN KEY datafield (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-legacy-changes/upgrade_34.sql b/schema/mysql-legacy-changes/upgrade_34.sql
new file mode 100644
index 0000000..21af3d0
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_34.sql
@@ -0,0 +1,2 @@
+ALTER TABLE icinga_host_field MODIFY is_required ENUM('y', 'n') NOT NULL;
+ALTER TABLE icinga_service_field MODIFY is_required ENUM('y', 'n') NOT NULL;
diff --git a/schema/mysql-legacy-changes/upgrade_35.sql b/schema/mysql-legacy-changes/upgrade_35.sql
new file mode 100644
index 0000000..166a06f
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_35.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_command_argument DROP COLUMN value_format, ADD COLUMN argument_format ENUM('string', 'expression', 'json') NOT NULL DEFAULT 'string' AFTER argument_value;
+ALTER TABLE icinga_command_argument DEFAULT COLLATE utf8_bin;
+ALTER TABLE icinga_command_argument MODIFY COLUMN argument_name VARCHAR(64) DEFAULT NULL COLLATE utf8_bin;
+
+
+
diff --git a/schema/mysql-legacy-changes/upgrade_36.sql b/schema/mysql-legacy-changes/upgrade_36.sql
new file mode 100644
index 0000000..8905f10
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_36.sql
@@ -0,0 +1 @@
+ALTER TABLE sync_rule MODIFY COLUMN object_type enum('host', 'host_template', 'service', 'service_template', 'command', 'command_template', 'user', 'user_template', 'hostgroup', 'servicegroup', 'usergroup', 'datalistEntry') NOT NULL;
diff --git a/schema/mysql-legacy-changes/upgrade_37.sql b/schema/mysql-legacy-changes/upgrade_37.sql
new file mode 100644
index 0000000..2533370
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_37.sql
@@ -0,0 +1,17 @@
+CREATE TABLE icinga_command_field (
+ command_id INT(10) UNSIGNED NOT NULL,
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') NOT NULL,
+ PRIMARY KEY (command_id, datafield_id),
+ CONSTRAINT icinga_command_field_command_argument
+ FOREIGN KEY host(command_id)
+ REFERENCES icinga_command_argument (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_command_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
diff --git a/schema/mysql-legacy-changes/upgrade_38.sql b/schema/mysql-legacy-changes/upgrade_38.sql
new file mode 100644
index 0000000..8760334
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_38.sql
@@ -0,0 +1,4 @@
+ALTER TABLE icinga_host
+ ADD COLUMN display_name VARCHAR(255) DEFAULT NULL,
+ ADD INDEX search_idx (display_name);
+
diff --git a/schema/mysql-legacy-changes/upgrade_39.sql b/schema/mysql-legacy-changes/upgrade_39.sql
new file mode 100644
index 0000000..81ee33d
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_39.sql
@@ -0,0 +1,5 @@
+ALTER TABLE director_deployment_log
+ MODIFY COLUMN startup_log TEXT DEFAULT NULL,
+ ADD COLUMN stage_name VARCHAR(64) DEFAULT NULL AFTER duration_dump,
+ ADD COLUMN stage_collected ENUM('y', 'n') DEFAULT NULL AFTER stage_name;
+
diff --git a/schema/mysql-legacy-changes/upgrade_4.sql b/schema/mysql-legacy-changes/upgrade_4.sql
new file mode 100644
index 0000000..4f005f2
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_4.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_servicegroup ADD COLUMN object_type ENUM('object', 'template') NOT NULL AFTER display_name; \ No newline at end of file
diff --git a/schema/mysql-legacy-changes/upgrade_40.sql b/schema/mysql-legacy-changes/upgrade_40.sql
new file mode 100644
index 0000000..55bcbed
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_40.sql
@@ -0,0 +1,9 @@
+
+ALTER TABLE director_deployment_log ADD COLUMN config_checksum VARBINARY(20) DEFAULT NULL AFTER config_id;
+ALTER TABLE director_deployment_log DROP COLUMN config_id;
+ALTER TABLE director_deployment_log ADD CONSTRAINT config_checksum
+ FOREIGN KEY config_checksum (config_checksum)
+ REFERENCES director_generated_config (checksum)
+ ON DELETE SET NULL
+ ON UPDATE RESTRICT;
+
diff --git a/schema/mysql-legacy-changes/upgrade_41.sql b/schema/mysql-legacy-changes/upgrade_41.sql
new file mode 100644
index 0000000..0ddb9b6
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_41.sql
@@ -0,0 +1,19 @@
+DROP TABLE icinga_command_field;
+
+CREATE TABLE icinga_command_field (
+ command_id INT(10) UNSIGNED NOT NULL,
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') NOT NULL,
+ PRIMARY KEY (command_id, datafield_id),
+ CONSTRAINT icinga_command_field_command
+ FOREIGN KEY command_id (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_command_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
diff --git a/schema/mysql-legacy-changes/upgrade_42.sql b/schema/mysql-legacy-changes/upgrade_42.sql
new file mode 100644
index 0000000..849ffda
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_42.sql
@@ -0,0 +1,7 @@
+ALTER TABLE import_run DROP FOREIGN KEY import_run_source;
+ALTER TABLE import_run ADD CONSTRAINT import_run_source
+ FOREIGN KEY import_source (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE RESTRICT;
+
diff --git a/schema/mysql-legacy-changes/upgrade_43.sql b/schema/mysql-legacy-changes/upgrade_43.sql
new file mode 100644
index 0000000..4e6a810
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_43.sql
@@ -0,0 +1,13 @@
+CREATE TABLE icinga_service_assignment (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ service_id INT(10) UNSIGNED NOT NULL,
+ filter_string TEXT NOT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_service_assignment
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+
diff --git a/schema/mysql-legacy-changes/upgrade_44.sql b/schema/mysql-legacy-changes/upgrade_44.sql
new file mode 100644
index 0000000..a874f67
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_44.sql
@@ -0,0 +1,2 @@
+ALTER TABLE imported_property MODIFY property_value MEDIUMTEXT NULL DEFAULT NULL;
+
diff --git a/schema/mysql-legacy-changes/upgrade_45.sql b/schema/mysql-legacy-changes/upgrade_45.sql
new file mode 100644
index 0000000..c819424
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_45.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_command_argument MODIFY argument_format ENUM('string','expression','json') NULL DEFAULT NULL;
diff --git a/schema/mysql-legacy-changes/upgrade_46.sql b/schema/mysql-legacy-changes/upgrade_46.sql
new file mode 100644
index 0000000..cc7c5fc
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_46.sql
@@ -0,0 +1,2 @@
+ALTER TABLE icinga_command_argument MODIFY argument_name VARCHAR(64) COLLATE utf8_bin DEFAULT NULL COMMENT '-x, --host';
+
diff --git a/schema/mysql-legacy-changes/upgrade_47.sql b/schema/mysql-legacy-changes/upgrade_47.sql
new file mode 100644
index 0000000..cdaedad
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_47.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_command_argument DROP INDEX sort_idx, ADD INDEX sort_idx (command_id, sort_order);
diff --git a/schema/mysql-legacy-changes/upgrade_48.sql b/schema/mysql-legacy-changes/upgrade_48.sql
new file mode 100644
index 0000000..de72ece
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_48.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_zone MODIFY object_type ENUM('object', 'template', 'external_object') NOT NULL;
diff --git a/schema/mysql-legacy-changes/upgrade_49.sql b/schema/mysql-legacy-changes/upgrade_49.sql
new file mode 100644
index 0000000..35a9dc9
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_49.sql
@@ -0,0 +1 @@
+ALTER TABLE director_deployment_log MODIFY stage_name VARCHAR(96) DEFAULT NULL;
diff --git a/schema/mysql-legacy-changes/upgrade_5.sql b/schema/mysql-legacy-changes/upgrade_5.sql
new file mode 100644
index 0000000..4c17ce3
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_5.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_usergroup ADD COLUMN object_type ENUM('object', 'template') NOT NULL AFTER zone_id; \ No newline at end of file
diff --git a/schema/mysql-legacy-changes/upgrade_50.sql b/schema/mysql-legacy-changes/upgrade_50.sql
new file mode 100644
index 0000000..6d6a7a3
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_50.sql
@@ -0,0 +1 @@
+ALTER TABLE director_deployment_log MODIFY startup_log MEDIUMTEXT DEFAULT NULL;
diff --git a/schema/mysql-legacy-changes/upgrade_51.sql b/schema/mysql-legacy-changes/upgrade_51.sql
new file mode 100644
index 0000000..13df6d5
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_51.sql
@@ -0,0 +1,2 @@
+ALTER TABLE sync_rule MODIFY object_type enum('host', 'host_template', 'service', 'service_template', 'command', 'command_template', 'user', 'user_template', 'hostgroup', 'servicegroup', 'usergroup', 'datalistEntry', 'endpoint', 'zone') NOT NULL;
+
diff --git a/schema/mysql-legacy-changes/upgrade_52.sql b/schema/mysql-legacy-changes/upgrade_52.sql
new file mode 100644
index 0000000..fad0d56
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_52.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_endpoint
+ MODIFY object_type ENUM('object', 'template', 'external_object') NOT NULL,
+ ADD COLUMN host VARCHAR(255) DEFAULT NULL COMMENT 'IP address / hostname of remote node' AFTER object_name,
+ DROP column address;
+
diff --git a/schema/mysql-legacy-changes/upgrade_53.sql b/schema/mysql-legacy-changes/upgrade_53.sql
new file mode 100644
index 0000000..d6a54cb
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_53.sql
@@ -0,0 +1,9 @@
+ALTER TABLE icinga_service
+ ADD COLUMN host_id INT(10) UNSIGNED DEFAULT NULL AFTER display_name,
+ ADD UNIQUE KEY object_key (object_name, host_id),
+ ADD CONSTRAINT icinga_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE;
+
diff --git a/schema/mysql-legacy-changes/upgrade_54.sql b/schema/mysql-legacy-changes/upgrade_54.sql
new file mode 100644
index 0000000..1b65963
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_54.sql
@@ -0,0 +1,2 @@
+ALTER TABLE director_generated_config_file MODIFY file_path VARCHAR(128) NOT NULL COMMENT 'e.g. zones/nafta/hosts.conf';
+
diff --git a/schema/mysql-legacy-changes/upgrade_55.sql b/schema/mysql-legacy-changes/upgrade_55.sql
new file mode 100644
index 0000000..4ad6feb
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_55.sql
@@ -0,0 +1,8 @@
+ALTER TABLE icinga_zone
+ DROP FOREIGN KEY icinga_zone_parent_zone,
+ CHANGE parent_zone_id parent_id INT(10) UNSIGNED DEFAULT NULL,
+ ADD CONSTRAINT icinga_zone_parent
+ FOREIGN KEY parent_zone (parent_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
diff --git a/schema/mysql-legacy-changes/upgrade_56.sql b/schema/mysql-legacy-changes/upgrade_56.sql
new file mode 100644
index 0000000..75683de
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_56.sql
@@ -0,0 +1,13 @@
+ALTER TABLE director_generated_file
+ ADD COLUMN cnt_object INT(10) UNSIGNED NOT NULL DEFAULT 0,
+ ADD COLUMN cnt_template INT(10) UNSIGNED NOT NULL DEFAULT 0;
+
+UPDATE director_generated_file
+SET cnt_object = ROUND(
+ (LENGTH(content) - LENGTH( REPLACE(content, 'object ', '') ) )
+ / LENGTH('object ')
+), cnt_template = ROUND(
+ (LENGTH(content) - LENGTH( REPLACE(content, 'template ', '') ) )
+ / LENGTH('template ')
+);
+
diff --git a/schema/mysql-legacy-changes/upgrade_57.sql b/schema/mysql-legacy-changes/upgrade_57.sql
new file mode 100644
index 0000000..3b2f756
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_57.sql
@@ -0,0 +1,20 @@
+
+CREATE TABLE icinga_apiuser (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ object_type ENUM('object', 'template', 'external_object') NOT NULL,
+ password VARCHAR(255) DEFAULT NULL,
+ client_dn VARCHAR(64) DEFAULT NULL,
+ permissions TEXT DEFAULT NULL COMMENT 'JSON-encoded permissions',
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+ALTER TABLE icinga_endpoint
+ ADD COLUMN apiuser_id INT(10) UNSIGNED DEFAULT NULL,
+ ADD CONSTRAINT icinga_apiuser
+ FOREIGN KEY apiuser (apiuser_id)
+ REFERENCES icinga_apiuser (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+
diff --git a/schema/mysql-legacy-changes/upgrade_58.sql b/schema/mysql-legacy-changes/upgrade_58.sql
new file mode 100644
index 0000000..5771f6a
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_58.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_host
+ ADD COLUMN has_agent ENUM('y', 'n') DEFAULT NULL,
+ ADD COLUMN master_should_connect ENUM('y', 'n') DEFAULT NULL,
+ ADD COLUMN accept_config ENUM('y', 'n') DEFAULT NULL;
+
diff --git a/schema/mysql-legacy-changes/upgrade_59.sql b/schema/mysql-legacy-changes/upgrade_59.sql
new file mode 100644
index 0000000..8c760e2
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_59.sql
@@ -0,0 +1,3 @@
+ALTER TABLE icinga_service
+ ADD COLUMN use_agent ENUM('y', 'n') NOT NULL DEFAULT 'n';
+
diff --git a/schema/mysql-legacy-changes/upgrade_6.sql b/schema/mysql-legacy-changes/upgrade_6.sql
new file mode 100644
index 0000000..0834b10
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_6.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_endpoint
+ MODIFY zone_id INT(10) UNSIGNED DEFAULT NULL,
+ MODIFY port SMALLINT UNSIGNED DEFAULT NULL COMMENT '5665 if not set',
+ MODIFY log_duration VARCHAR(32) DEFAULT NULL COMMENT '1d if not set';
+
diff --git a/schema/mysql-legacy-changes/upgrade_60.sql b/schema/mysql-legacy-changes/upgrade_60.sql
new file mode 100644
index 0000000..5b0da8f
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_60.sql
@@ -0,0 +1,2 @@
+ALTER TABLE icinga_service MODIFY COLUMN use_agent ENUM('y', 'n') DEFAULT NULL;
+
diff --git a/schema/mysql-legacy-changes/upgrade_61.sql b/schema/mysql-legacy-changes/upgrade_61.sql
new file mode 100644
index 0000000..50d62fd
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_61.sql
@@ -0,0 +1,2 @@
+ALTER TABLE icinga_host DROP KEY object_name, ADD UNIQUE KEY object_name (object_name);
+
diff --git a/schema/mysql-legacy-changes/upgrade_62.sql b/schema/mysql-legacy-changes/upgrade_62.sql
new file mode 100644
index 0000000..a3aba5f
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_62.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_command MODIFY command TEXT DEFAULT NULL;
diff --git a/schema/mysql-legacy-changes/upgrade_7.sql b/schema/mysql-legacy-changes/upgrade_7.sql
new file mode 100644
index 0000000..38780b2
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_7.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_service DROP INDEX object_name; \ No newline at end of file
diff --git a/schema/mysql-legacy-changes/upgrade_8.sql b/schema/mysql-legacy-changes/upgrade_8.sql
new file mode 100644
index 0000000..a1c9d46
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_8.sql
@@ -0,0 +1,36 @@
+DROP TABLE director_generated_config_file;
+
+DROP TABLE director_generated_config;
+
+CREATE TABLE director_generated_config (
+ checksum VARBINARY(20) NOT NULL COMMENT 'SHA1(last_activity_checksum;file_path=checksum;file_path=checksum;...)',
+ director_version VARCHAR(64) DEFAULT NULL,
+ director_db_version INT(10) DEFAULT NULL,
+ duration INT(10) UNSIGNED DEFAULT NULL COMMENT 'Config generation duration (ms)',
+ last_activity_checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (checksum),
+ CONSTRAINT director_generated_config_activity
+ FOREIGN KEY activity_checksum (last_activity_checksum)
+ REFERENCES director_activity_log (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_generated_config_file (
+ config_checksum VARBINARY(20) NOT NULL,
+ file_checksum VARBINARY(20) NOT NULL,
+ file_path VARCHAR(64) NOT NULL COMMENT 'e.g. zones/nafta/hosts.conf',
+ CONSTRAINT director_generated_config_file_config
+ FOREIGN KEY config (config_checksum)
+ REFERENCES director_generated_config (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT director_generated_config_file_file
+ FOREIGN KEY checksum (file_checksum)
+ REFERENCES director_generated_file (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT,
+ PRIMARY KEY (config_checksum, file_path),
+ INDEX search_idx (file_checksum)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
diff --git a/schema/mysql-legacy-changes/upgrade_9.sql b/schema/mysql-legacy-changes/upgrade_9.sql
new file mode 100644
index 0000000..4a7b93a
--- /dev/null
+++ b/schema/mysql-legacy-changes/upgrade_9.sql
@@ -0,0 +1 @@
+ALTER TABLE icinga_command MODIFY timeout SMALLINT UNSIGNED DEFAULT NULL; \ No newline at end of file
diff --git a/schema/mysql-migrations/upgrade_100.sql b/schema/mysql-migrations/upgrade_100.sql
new file mode 100644
index 0000000..56f8b54
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_100.sql
@@ -0,0 +1,6 @@
+ALTER TABLE import_row_modifier
+ ADD COLUMN target_property VARCHAR(255) DEFAULT NULL AFTER property_name;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (100, NOW());
diff --git a/schema/mysql-migrations/upgrade_101.sql b/schema/mysql-migrations/upgrade_101.sql
new file mode 100644
index 0000000..bbe1817
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_101.sql
@@ -0,0 +1,7 @@
+ALTER TABLE icinga_host
+ ADD COLUMN api_key VARCHAR(40) DEFAULT NULL AFTER accept_config,
+ ADD UNIQUE KEY api_key (api_key);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (101, NOW());
diff --git a/schema/mysql-migrations/upgrade_102.sql b/schema/mysql-migrations/upgrade_102.sql
new file mode 100644
index 0000000..5607f1e
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_102.sql
@@ -0,0 +1,13 @@
+UPDATE director_deployment_log SET startup_log = LEFT(startup_log, 20480) || '
+
+[..] shortened '
+|| (LENGTH(startup_log) - 40960)
+|| ' bytes by Director on schema upgrade [..]
+
+' || RIGHT(startup_log, 20480) WHERE LENGTH(startup_log) > 61440;
+
+OPTIMIZE TABLE director_deployment_log;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (102, NOW());
diff --git a/schema/mysql-migrations/upgrade_103.sql b/schema/mysql-migrations/upgrade_103.sql
new file mode 100644
index 0000000..64d222b
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_103.sql
@@ -0,0 +1,12 @@
+UPDATE icinga_command_argument
+ SET
+ argument_name = '(no key)',
+ skip_key = 'y'
+ WHERE argument_name IS NULL;
+
+ALTER TABLE icinga_command_argument
+ MODIFY argument_name VARCHAR(64) COLLATE utf8_bin NOT NULL COMMENT '-x, --host';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (103, NOW());
diff --git a/schema/mysql-migrations/upgrade_104.sql b/schema/mysql-migrations/upgrade_104.sql
new file mode 100644
index 0000000..673360a
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_104.sql
@@ -0,0 +1,19 @@
+ALTER TABLE icinga_timeperiod_range
+ ADD COLUMN range_key VARCHAR(255) NOT NULL COMMENT 'monday, ...',
+ ADD COLUMN range_value VARCHAR(255) NOT NULL COMMENT '00:00-24:00, ...';
+
+UPDATE icinga_timeperiod_range
+ SET range_key = timeperiod_key,
+ range_value = timeperiod_value;
+
+ALTER TABLE icinga_timeperiod_range
+ DROP PRIMARY KEY,
+ ADD PRIMARY KEY (timeperiod_id, range_type, range_key);
+
+ALTER TABLE icinga_timeperiod_range
+ DROP COLUMN timeperiod_key,
+ DROP COLUMN timeperiod_value;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (104, NOW());
diff --git a/schema/mysql-migrations/upgrade_105.sql b/schema/mysql-migrations/upgrade_105.sql
new file mode 100644
index 0000000..da4efa8
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_105.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_service
+ ADD COLUMN use_var_overrides ENUM('y', 'n') DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (105, NOW());
diff --git a/schema/mysql-migrations/upgrade_107.sql b/schema/mysql-migrations/upgrade_107.sql
new file mode 100644
index 0000000..fb35f07
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_107.sql
@@ -0,0 +1,9 @@
+ALTER TABLE director_job
+ MODIFY last_error_message TEXT DEFAULT NULL;
+
+ALTER TABLE sync_rule
+ MODIFY last_error_message TEXT DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (107, NOW());
diff --git a/schema/mysql-migrations/upgrade_108.sql b/schema/mysql-migrations/upgrade_108.sql
new file mode 100644
index 0000000..8a6ef39
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_108.sql
@@ -0,0 +1,18 @@
+ALTER TABLE icinga_command_var
+ MODIFY COLUMN varname VARCHAR(255) NOT NULL COLLATE utf8_bin;
+
+ALTER TABLE icinga_host_var
+ MODIFY COLUMN varname VARCHAR(255) NOT NULL COLLATE utf8_bin;
+
+ALTER TABLE icinga_service_var
+ MODIFY COLUMN varname VARCHAR(255) NOT NULL COLLATE utf8_bin;
+
+ALTER TABLE icinga_user_var
+ MODIFY COLUMN varname VARCHAR(255) NOT NULL COLLATE utf8_bin;
+
+ALTER TABLE icinga_notification_var
+ MODIFY COLUMN varname VARCHAR(255) NOT NULL COLLATE utf8_bin;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (108, NOW());
diff --git a/schema/mysql-migrations/upgrade_109.sql b/schema/mysql-migrations/upgrade_109.sql
new file mode 100644
index 0000000..1989fa7
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_109.sql
@@ -0,0 +1,16 @@
+CREATE TABLE icinga_hostgroup_assignment (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ hostgroup_id INT(10) UNSIGNED NOT NULL,
+ filter_string TEXT NOT NULL,
+ assign_type ENUM('assign', 'ignore') NOT NULL DEFAULT 'assign',
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_hostgroup_assignment
+ FOREIGN KEY hostgroup (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (109, NOW());
diff --git a/schema/mysql-migrations/upgrade_110.sql b/schema/mysql-migrations/upgrade_110.sql
new file mode 100644
index 0000000..800f7ab
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_110.sql
@@ -0,0 +1,104 @@
+UPDATE icinga_host_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_host_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_service_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_service_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+
+UPDATE icinga_command_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_command_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_user_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_user_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_notification_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_notification_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (110, NOW());
diff --git a/schema/mysql-migrations/upgrade_112.sql b/schema/mysql-migrations/upgrade_112.sql
new file mode 100644
index 0000000..5336d35
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_112.sql
@@ -0,0 +1,6 @@
+ALTER TABLE director_datalist_entry
+ MODIFY COLUMN entry_name VARCHAR(255) COLLATE utf8_bin NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (112, NOW());
diff --git a/schema/mysql-migrations/upgrade_114.sql b/schema/mysql-migrations/upgrade_114.sql
new file mode 100644
index 0000000..24d3430
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_114.sql
@@ -0,0 +1,55 @@
+CREATE TABLE icinga_service_set (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ object_name VARCHAR(128) NOT NULL,
+ object_type ENUM('object', 'template', 'external_object') NOT NULL,
+ host_id INT(10) UNSIGNED DEFAULT NULL,
+ description TEXT NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY object_key (object_name, host_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_set_service (
+ service_set_id INT(10) UNSIGNED NOT NULL,
+ service_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (service_set_id, service_id),
+ CONSTRAINT service_set_set
+ FOREIGN KEY service_set (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT service_set_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_set_assignment (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ service_set_id INT(10) UNSIGNED NOT NULL,
+ filter_string TEXT NOT NULL,
+ assign_type ENUM('assign', 'ignore') NOT NULL DEFAULT 'assign',
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_service_set_assignment
+ FOREIGN KEY service_set (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_service_set_var (
+ service_set_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ varvalue TEXT DEFAULT NULL,
+ format ENUM('string', 'expression', 'json') NOT NULL DEFAULT 'string',
+ PRIMARY KEY (service_set_id, varname),
+ CONSTRAINT icinga_service_set_var_service
+ FOREIGN KEY command (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (114, NOW());
diff --git a/schema/mysql-migrations/upgrade_115.sql b/schema/mysql-migrations/upgrade_115.sql
new file mode 100644
index 0000000..45f6236
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_115.sql
@@ -0,0 +1,23 @@
+CREATE TABLE icinga_service_set_inheritance (
+ service_set_id INT(10) UNSIGNED NOT NULL,
+ parent_service_set_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (service_set_id, parent_service_set_id),
+ UNIQUE KEY unique_order (service_set_id, weight),
+ CONSTRAINT icinga_service_set_inheritance_set
+ FOREIGN KEY host (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_set_inheritance_parent
+ FOREIGN KEY host (parent_service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+ALTER TABLE icinga_service_set MODIFY description TEXT DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (115, NOW());
diff --git a/schema/mysql-migrations/upgrade_116.sql b/schema/mysql-migrations/upgrade_116.sql
new file mode 100644
index 0000000..a252d42
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_116.sql
@@ -0,0 +1,18 @@
+ALTER TABLE sync_rule MODIFY object_type enum(
+ 'host',
+ 'service',
+ 'command',
+ 'user',
+ 'hostgroup',
+ 'servicegroup',
+ 'usergroup',
+ 'datalistEntry',
+ 'endpoint',
+ 'zone',
+ 'timePeriod',
+ 'serviceSet'
+) NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (116, NOW());
diff --git a/schema/mysql-migrations/upgrade_117.sql b/schema/mysql-migrations/upgrade_117.sql
new file mode 100644
index 0000000..e0ab4f3
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_117.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_notification_field (
+ notification_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') NOT NULL,
+ PRIMARY KEY (notification_id, datafield_id),
+ CONSTRAINT icinga_notification_field_notification
+ FOREIGN KEY notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (117, NOW());
diff --git a/schema/mysql-migrations/upgrade_119.sql b/schema/mysql-migrations/upgrade_119.sql
new file mode 100644
index 0000000..c5fcf54
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_119.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_service
+ ADD COLUMN apply_for VARCHAR(255) DEFAULT NULL AFTER use_agent;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (119, NOW());
diff --git a/schema/mysql-migrations/upgrade_120.sql b/schema/mysql-migrations/upgrade_120.sql
new file mode 100644
index 0000000..4020dea
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_120.sql
@@ -0,0 +1,184 @@
+ALTER TABLE icinga_service ADD COLUMN assign_filter TEXT DEFAULT NULL;
+
+UPDATE icinga_service s JOIN (
+
+ SELECT
+ service_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa.filter_string
+ ELSE GROUP_CONCAT(sa.filter_string SEPARATOR '&') END AS filter_string
+ FROM (
+ SELECT
+ sa_not.service_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa_not.filter_string
+ ELSE '(' || GROUP_CONCAT(sa_not.filter_string SEPARATOR '&') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.service_id,
+ '!' || sa.filter_string AS filter_string
+ FROM icinga_service_assignment sa
+ WHERE assign_type = 'ignore'
+ ) sa_not
+ GROUP BY service_id
+
+ UNION ALL
+
+ SELECT
+ sa_yes.service_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa_yes.filter_string
+ ELSE '(' || GROUP_CONCAT(sa_yes.filter_string SEPARATOR '|') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.service_id,
+ sa.filter_string AS filter_string
+ FROM icinga_service_assignment sa
+ WHERE assign_type = 'assign'
+ ) sa_yes
+ GROUP BY service_id
+
+ ) sa GROUP BY service_id
+
+) flat_assign ON s.id = flat_assign.service_id SET s.assign_filter = flat_assign.filter_string;
+
+DROP TABLE icinga_service_assignment;
+
+ALTER TABLE icinga_service_set ADD COLUMN assign_filter TEXT DEFAULT NULL;
+
+UPDATE icinga_service_set s JOIN (
+
+ SELECT
+ service_set_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa.filter_string
+ ELSE GROUP_CONCAT(sa.filter_string SEPARATOR '&') END AS filter_string
+ FROM (
+ SELECT
+ sa_not.service_set_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa_not.filter_string
+ ELSE '(' || GROUP_CONCAT(sa_not.filter_string SEPARATOR '&') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.service_set_id,
+ '!' || sa.filter_string AS filter_string
+ FROM icinga_service_set_assignment sa
+ WHERE assign_type = 'ignore'
+ ) sa_not
+ GROUP BY service_set_id
+
+ UNION ALL
+
+ SELECT
+ sa_yes.service_set_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa_yes.filter_string
+ ELSE '(' || GROUP_CONCAT(sa_yes.filter_string SEPARATOR '|') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.service_set_id,
+ sa.filter_string AS filter_string
+ FROM icinga_service_set_assignment sa
+ WHERE assign_type = 'assign'
+ ) sa_yes
+ GROUP BY service_set_id
+
+ ) sa GROUP BY service_set_id
+
+) flat_assign ON s.id = flat_assign.service_set_id SET s.assign_filter = flat_assign.filter_string;
+
+DROP TABLE icinga_service_set_assignment;
+
+
+ALTER TABLE icinga_notification ADD COLUMN assign_filter TEXT DEFAULT NULL;
+
+UPDATE icinga_notification s JOIN (
+
+ SELECT
+ notification_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa.filter_string
+ ELSE GROUP_CONCAT(sa.filter_string SEPARATOR '&') END AS filter_string
+ FROM (
+ SELECT
+ sa_not.notification_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa_not.filter_string
+ ELSE '(' || GROUP_CONCAT(sa_not.filter_string SEPARATOR '&') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.notification_id,
+ '!' || sa.filter_string AS filter_string
+ FROM icinga_notification_assignment sa
+ WHERE assign_type = 'ignore'
+ ) sa_not
+ GROUP BY notification_id
+
+ UNION ALL
+
+ SELECT
+ sa_yes.notification_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa_yes.filter_string
+ ELSE '(' || GROUP_CONCAT(sa_yes.filter_string SEPARATOR '|') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.notification_id,
+ sa.filter_string AS filter_string
+ FROM icinga_notification_assignment sa
+ WHERE assign_type = 'assign'
+ ) sa_yes
+ GROUP BY notification_id
+
+ ) sa GROUP BY notification_id
+
+) flat_assign ON s.id = flat_assign.notification_id SET s.assign_filter = flat_assign.filter_string;
+
+DROP TABLE icinga_notification_assignment;
+
+ALTER TABLE icinga_hostgroup ADD COLUMN assign_filter TEXT DEFAULT NULL;
+
+UPDATE icinga_hostgroup s JOIN (
+
+ SELECT
+ hostgroup_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa.filter_string
+ ELSE GROUP_CONCAT(sa.filter_string SEPARATOR '&') END AS filter_string
+ FROM (
+ SELECT
+ sa_not.hostgroup_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa_not.filter_string
+ ELSE '(' || GROUP_CONCAT(sa_not.filter_string SEPARATOR '&') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.hostgroup_id,
+ '!' || sa.filter_string AS filter_string
+ FROM icinga_hostgroup_assignment sa
+ WHERE assign_type = 'ignore'
+ ) sa_not
+ GROUP BY hostgroup_id
+
+ UNION ALL
+
+ SELECT
+ sa_yes.hostgroup_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN sa_yes.filter_string
+ ELSE '(' || GROUP_CONCAT(sa_yes.filter_string SEPARATOR '|') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.hostgroup_id,
+ sa.filter_string AS filter_string
+ FROM icinga_hostgroup_assignment sa
+ WHERE assign_type = 'assign'
+ ) sa_yes
+ GROUP BY hostgroup_id
+
+ ) sa GROUP BY hostgroup_id
+
+) flat_assign ON s.id = flat_assign.hostgroup_id SET s.assign_filter = flat_assign.filter_string;
+
+DROP TABLE icinga_hostgroup_assignment;
+
+
+ALTER TABLE icinga_servicegroup ADD COLUMN assign_filter TEXT DEFAULT NULL;
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (120, NOW());
diff --git a/schema/mysql-migrations/upgrade_121.sql b/schema/mysql-migrations/upgrade_121.sql
new file mode 100644
index 0000000..24c307a
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_121.sql
@@ -0,0 +1,8 @@
+ALTER TABLE icinga_service
+ ADD COLUMN service_set_id INT(10) UNSIGNED DEFAULT NULL AFTER host_id;
+
+DROP TABLE icinga_service_set_service;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (121, NOW());
diff --git a/schema/mysql-migrations/upgrade_122.sql b/schema/mysql-migrations/upgrade_122.sql
new file mode 100644
index 0000000..6a94e05
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_122.sql
@@ -0,0 +1,12 @@
+ALTER TABLE director_generated_file
+ ADD COLUMN cnt_apply INT(10) UNSIGNED NOT NULL DEFAULT 0;
+
+UPDATE director_generated_file
+SET cnt_apply = ROUND(
+ (LENGTH(content) - LENGTH( REPLACE(content, 'apply ', '') ) )
+ / LENGTH('apply ')
+);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (122, NOW());
diff --git a/schema/mysql-migrations/upgrade_123.sql b/schema/mysql-migrations/upgrade_123.sql
new file mode 100644
index 0000000..024ed72
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_123.sql
@@ -0,0 +1,30 @@
+-- cleanup dangling service_set before we add foreign key
+DELETE ss FROM icinga_service_set AS ss
+ LEFT JOIN icinga_host AS h ON h.id = ss.host_id
+ WHERE ss.object_type = 'object'
+ AND ss.host_id IS NOT NULL
+ AND h.id IS NULL;
+
+-- cleanup dangling services to service_set
+DELETE s FROM icinga_service AS s
+ LEFT JOIN icinga_service_set AS ss ON ss.id = s.service_set_id
+ WHERE s.object_type IN ('object', 'apply')
+ AND s.service_set_id IS NOT NULL
+ AND ss.id IS NULL;
+
+
+ALTER TABLE icinga_service_set
+ ADD FOREIGN KEY icinga_service_set_host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_service
+ ADD FOREIGN KEY icinga_service_service_set (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (123, NOW());
diff --git a/schema/mysql-migrations/upgrade_124.sql b/schema/mysql-migrations/upgrade_124.sql
new file mode 100644
index 0000000..c7e218f
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_124.sql
@@ -0,0 +1,3 @@
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (124, NOW());
diff --git a/schema/mysql-migrations/upgrade_125.sql b/schema/mysql-migrations/upgrade_125.sql
new file mode 100644
index 0000000..b1ffea1
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_125.sql
@@ -0,0 +1,18 @@
+ALTER TABLE icinga_command_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+ALTER TABLE icinga_host_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+ALTER TABLE icinga_notification_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+ALTER TABLE icinga_service_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+ALTER TABLE icinga_user_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (125, NOW());
diff --git a/schema/mysql-migrations/upgrade_126.sql b/schema/mysql-migrations/upgrade_126.sql
new file mode 100644
index 0000000..d655eaa
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_126.sql
@@ -0,0 +1,217 @@
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT * FROM information_schema.table_constraints
+ WHERE
+ table_schema = DATABASE()
+ AND table_name = 'icinga_service_set'
+ AND constraint_name = 'icinga_service_set_host'
+ )),
+ 'ALTER TABLE icinga_service_set DROP FOREIGN KEY icinga_service_set_host',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT * FROM information_schema.table_constraints
+ WHERE
+ table_schema = DATABASE()
+ AND table_name = 'icinga_service_set'
+ AND constraint_name = 'icinga_service_set_ibfk_1'
+ )),
+ 'ALTER TABLE icinga_service_set DROP FOREIGN KEY icinga_service_set_ibfk_1',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT * FROM information_schema.table_constraints
+ WHERE
+ table_schema = DATABASE()
+ AND table_name = 'icinga_service_set'
+ AND constraint_name = 'icinga_service_set_ibfk_2'
+ )),
+ 'ALTER TABLE icinga_service_set DROP FOREIGN KEY icinga_service_set_ibfk_2',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT * FROM information_schema.table_constraints
+ WHERE
+ table_schema = DATABASE()
+ AND table_name = 'icinga_service_set'
+ AND constraint_name = 'icinga_service_set_ibfk_3'
+ )),
+ 'ALTER TABLE icinga_service_set DROP FOREIGN KEY icinga_service_set_ibfk_3',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT * FROM information_schema.table_constraints
+ WHERE
+ table_schema = DATABASE()
+ AND table_name = 'icinga_service'
+ AND constraint_name = 'icinga_service_service_set'
+ )),
+ 'ALTER TABLE icinga_service DROP FOREIGN KEY icinga_service_service_set',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT * FROM information_schema.table_constraints
+ WHERE
+ table_schema = DATABASE()
+ AND table_name = 'icinga_service'
+ AND constraint_name = 'icinga_service_ibfk_1'
+ )),
+ 'ALTER TABLE icinga_service DROP FOREIGN KEY icinga_service_ibfk_1',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT 1
+ FROM information_schema.statistics
+ WHERE table_schema = SCHEMA()
+ AND table_name = 'icinga_service'
+ AND index_name = 'icinga_service_service_set'
+ )),
+ 'ALTER TABLE icinga_service DROP INDEX icinga_service_service_set',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT 1
+ FROM information_schema.statistics
+ WHERE table_schema = SCHEMA()
+ AND table_name = 'icinga_service_set'
+ AND index_name = 'icinga_service_set_ibfk_1'
+ )),
+ 'ALTER TABLE icinga_service_set DROP INDEX icinga_service_set_ibfk_1',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT 1
+ FROM information_schema.statistics
+ WHERE table_schema = SCHEMA()
+ AND table_name = 'icinga_service_set'
+ AND index_name = 'icinga_service_set_host'
+ )),
+ 'ALTER TABLE icinga_service_set DROP INDEX icinga_service_set_host',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT 1
+ FROM information_schema.statistics
+ WHERE table_schema = SCHEMA()
+ AND table_name = 'icinga_service'
+ AND index_name = 'icinga_service_ibfk_1'
+ )),
+ 'ALTER TABLE icinga_service_set DROP INDEX icinga_service_ibfk_1',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+SET @stmt = (SELECT IF(
+ (SELECT EXISTS(
+ SELECT 1
+ FROM information_schema.statistics
+ WHERE table_schema = SCHEMA()
+ AND table_name = 'icinga_service_set'
+ AND index_name = 'icinga_service_set_ibfk_2'
+ )),
+ 'ALTER TABLE icinga_service_set DROP INDEX icinga_service_set_ibfk_2',
+ 'SELECT 1'
+));
+
+PREPARE stmt FROM @stmt;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET @stmt = NULL;
+
+
+ALTER TABLE icinga_service_set
+ ADD CONSTRAINT icinga_service_set_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_service
+ ADD CONSTRAINT icinga_service_service_set
+ FOREIGN KEY service_set (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (126, NOW());
diff --git a/schema/mysql-migrations/upgrade_127.sql b/schema/mysql-migrations/upgrade_127.sql
new file mode 100644
index 0000000..575675e
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_127.sql
@@ -0,0 +1,152 @@
+ALTER TABLE icinga_command_var
+ ADD COLUMN checksum VARBINARY(20) DEFAULT NULL AFTER command_id,
+ ADD INDEX search_idx (varname),
+ ADD INDEX checksum (checksum);
+
+ALTER TABLE icinga_host_var
+ ADD COLUMN checksum VARBINARY(20) DEFAULT NULL AFTER host_id,
+ ADD INDEX checksum (checksum);
+
+ALTER TABLE icinga_notification_var
+ ADD COLUMN checksum VARBINARY(20) DEFAULT NULL AFTER notification_id,
+ ADD INDEX checksum (checksum);
+
+ALTER TABLE icinga_service_set_var
+ ADD COLUMN checksum VARBINARY(20) DEFAULT NULL AFTER service_set_id,
+ ADD INDEX search_idx (varname),
+ ADD INDEX checksum (checksum);
+
+ALTER TABLE icinga_service_var
+ ADD COLUMN checksum VARBINARY(20) DEFAULT NULL AFTER service_id,
+ ADD INDEX checksum (checksum);
+
+ALTER TABLE icinga_user_var
+ ADD COLUMN checksum VARBINARY(20) DEFAULT NULL AFTER user_id,
+ ADD INDEX checksum (checksum);
+
+CREATE TABLE icinga_var (
+ checksum VARBINARY(20) NOT NULL,
+ rendered_checksum VARBINARY(20) NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ varvalue TEXT NOT NULL,
+ rendered TEXT NOT NULL,
+ PRIMARY KEY (checksum),
+ INDEX search_idx (varname)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_flat_var (
+ var_checksum VARBINARY(20) NOT NULL,
+ flatname_checksum VARBINARY(20) NOT NULL,
+ flatname VARCHAR(512) NOT NULL COLLATE utf8_bin,
+ flatvalue TEXT NOT NULL,
+ PRIMARY KEY (var_checksum, flatname_checksum),
+ INDEX search_varname (flatname (191)),
+ INDEX search_varvalue (flatvalue (128)),
+ CONSTRAINT flat_var_var
+ FOREIGN KEY checksum (var_checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_command_resolved_var (
+ command_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (command_id, checksum),
+ INDEX search_varname (varname),
+ CONSTRAINT command_resolved_var_command
+ FOREIGN KEY command (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT command_resolved_var_checksum
+ FOREIGN KEY checksum (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_host_resolved_var (
+ host_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (host_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY host_resolved_var_host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY host_resolved_var_checksum (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification_resolved_var (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (notification_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY notification_resolved_var_notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY notification_resolved_var_checksum (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_set_resolved_var (
+ service_set_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (service_set_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY service_set_resolved_var_service_set (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY service_set_resolved_var_checksum(checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_resolved_var (
+ service_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (service_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY service_resolve_var_service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY service_resolve_var_checksum(checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_user_resolved_var (
+ user_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (user_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY user_resolve_var_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY user_resolve_var_checksum(checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (127, NOW());
diff --git a/schema/mysql-migrations/upgrade_128.sql b/schema/mysql-migrations/upgrade_128.sql
new file mode 100644
index 0000000..30e809c
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_128.sql
@@ -0,0 +1,6 @@
+ALTER TABLE director_activity_log
+ ADD INDEX search_author (author);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (128, NOW());
diff --git a/schema/mysql-migrations/upgrade_129.sql b/schema/mysql-migrations/upgrade_129.sql
new file mode 100644
index 0000000..b47601c
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_129.sql
@@ -0,0 +1,6 @@
+ALTER TABLE director_datafield
+ MODIFY COLUMN varname VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (129, NOW());
diff --git a/schema/mysql-migrations/upgrade_130.sql b/schema/mysql-migrations/upgrade_130.sql
new file mode 100644
index 0000000..c862d0d
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_130.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_hostgroup
+ MODIFY object_type enum('object', 'template', 'external_object') NOT NULL;
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (130, NOW());
diff --git a/schema/mysql-migrations/upgrade_131.sql b/schema/mysql-migrations/upgrade_131.sql
new file mode 100644
index 0000000..282e060
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_131.sql
@@ -0,0 +1,19 @@
+CREATE TABLE icinga_hostgroup_host_resolved (
+ hostgroup_id INT(10) UNSIGNED NOT NULL,
+ host_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (hostgroup_id, host_id),
+ CONSTRAINT icinga_hostgroup_host_resolved_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_host_resolved_hostgroup
+ FOREIGN KEY hostgroup (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (131, NOW());
diff --git a/schema/mysql-migrations/upgrade_132.sql b/schema/mysql-migrations/upgrade_132.sql
new file mode 100644
index 0000000..76be96f
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_132.sql
@@ -0,0 +1,21 @@
+CREATE TABLE icinga_host_template_choice (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ object_name VARCHAR(64) NOT NULL,
+ description TEXT DEFAULT NULL,
+ min_required SMALLINT UNSIGNED NOT NULL DEFAULT 0,
+ max_allowed SMALLINT UNSIGNED NOT NULL DEFAULT 1,
+ PRIMARY KEY (id),
+ UNIQUE KEY (object_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+ALTER TABLE icinga_host
+ ADD COLUMN template_choice_id INT(10) UNSIGNED DEFAULT NULL,
+ ADD CONSTRAINT icinga_host_template_choice
+ FOREIGN KEY choice (template_choice_id)
+ REFERENCES icinga_host_template_choice (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (132, NOW());
diff --git a/schema/mysql-migrations/upgrade_133.sql b/schema/mysql-migrations/upgrade_133.sql
new file mode 100644
index 0000000..9f1a474
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_133.sql
@@ -0,0 +1,21 @@
+CREATE TABLE icinga_service_template_choice (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ object_name VARCHAR(64) NOT NULL,
+ description TEXT DEFAULT NULL,
+ min_required SMALLINT UNSIGNED NOT NULL DEFAULT 0,
+ max_allowed SMALLINT UNSIGNED NOT NULL DEFAULT 1,
+ PRIMARY KEY (id),
+ UNIQUE KEY (object_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+ALTER TABLE icinga_service
+ ADD COLUMN template_choice_id INT(10) UNSIGNED DEFAULT NULL,
+ ADD CONSTRAINT icinga_service_template_choice
+ FOREIGN KEY choice (template_choice_id)
+ REFERENCES icinga_service_template_choice (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (133, NOW());
diff --git a/schema/mysql-migrations/upgrade_134.sql b/schema/mysql-migrations/upgrade_134.sql
new file mode 100644
index 0000000..b652e5b
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_134.sql
@@ -0,0 +1,19 @@
+ALTER TABLE icinga_host
+ DROP FOREIGN KEY icinga_host_template_choice,
+ ADD CONSTRAINT icinga_host_template_choice_v2
+ FOREIGN KEY template_choice (template_choice_id)
+ REFERENCES icinga_host_template_choice (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_service
+ DROP FOREIGN KEY icinga_service_template_choice,
+ ADD CONSTRAINT icinga_service_template_choice_v2
+ FOREIGN KEY template_choice (template_choice_id)
+ REFERENCES icinga_service_template_choice (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (134, NOW());
diff --git a/schema/mysql-migrations/upgrade_135.sql b/schema/mysql-migrations/upgrade_135.sql
new file mode 100644
index 0000000..6a1d687
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_135.sql
@@ -0,0 +1,9 @@
+ALTER TABLE icinga_host
+ ADD COLUMN check_timeout SMALLINT UNSIGNED DEFAULT NULL AFTER retry_interval;
+
+ALTER TABLE icinga_service
+ ADD COLUMN check_timeout SMALLINT UNSIGNED DEFAULT NULL AFTER retry_interval;
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (135, NOW());
diff --git a/schema/mysql-migrations/upgrade_136.sql b/schema/mysql-migrations/upgrade_136.sql
new file mode 100644
index 0000000..d308062
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_136.sql
@@ -0,0 +1,6 @@
+ALTER TABLE director_datalist_entry
+ ADD COLUMN allowed_roles VARCHAR(255) DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (136, NOW());
diff --git a/schema/mysql-migrations/upgrade_137.sql b/schema/mysql-migrations/upgrade_137.sql
new file mode 100644
index 0000000..535d2bc
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_137.sql
@@ -0,0 +1,9 @@
+ALTER TABLE import_source
+ ADD COLUMN description TEXT DEFAULT NULL;
+
+ALTER TABLE sync_rule
+ ADD COLUMN description TEXT DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (137, NOW());
diff --git a/schema/mysql-migrations/upgrade_138.sql b/schema/mysql-migrations/upgrade_138.sql
new file mode 100644
index 0000000..8561c00
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_138.sql
@@ -0,0 +1,6 @@
+ALTER TABLE import_row_modifier
+ ADD COLUMN description TEXT DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (138, NOW());
diff --git a/schema/mysql-migrations/upgrade_139.sql b/schema/mysql-migrations/upgrade_139.sql
new file mode 100644
index 0000000..817244b
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_139.sql
@@ -0,0 +1,7 @@
+UPDATE import_row_modifier SET priority = id;
+
+ALTER TABLE import_row_modifier ADD UNIQUE INDEX idx_prio (source_id, priority);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (139, NOW());
diff --git a/schema/mysql-migrations/upgrade_140.sql b/schema/mysql-migrations/upgrade_140.sql
new file mode 100644
index 0000000..996e9ef
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_140.sql
@@ -0,0 +1,5 @@
+UPDATE sync_property SET priority = 10000 - priority;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (140, NOW());
diff --git a/schema/mysql-migrations/upgrade_141.sql b/schema/mysql-migrations/upgrade_141.sql
new file mode 100644
index 0000000..a382208
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_141.sql
@@ -0,0 +1,7 @@
+UPDATE icinga_service_set
+ SET object_type = 'template'
+ WHERE object_type = 'object' AND host_id IS NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (141, NOW());
diff --git a/schema/mysql-migrations/upgrade_143.sql b/schema/mysql-migrations/upgrade_143.sql
new file mode 100644
index 0000000..7d07385
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_143.sql
@@ -0,0 +1,21 @@
+ALTER TABLE icinga_host_template_choice
+ ADD COLUMN required_template_id INT(10) UNSIGNED DEFAULT NULL,
+ ADD COLUMN allowed_roles VARCHAR(255) DEFAULT NULL,
+ ADD CONSTRAINT host_template_choice_required_template
+ FOREIGN KEY required_template (required_template_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_service_template_choice
+ ADD COLUMN required_template_id INT(10) UNSIGNED DEFAULT NULL,
+ ADD COLUMN allowed_roles VARCHAR(255) DEFAULT NULL,
+ ADD CONSTRAINT service_template_choice_required_template
+ FOREIGN KEY required_template (required_template_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (143, NOW());
diff --git a/schema/mysql-migrations/upgrade_144.sql b/schema/mysql-migrations/upgrade_144.sql
new file mode 100644
index 0000000..fff6f8f
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_144.sql
@@ -0,0 +1,91 @@
+CREATE TABLE icinga_dependency (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ parent_host_id INT(10) UNSIGNED DEFAULT NULL,
+ parent_service_id INT(10) UNSIGNED DEFAULT NULL,
+ child_host_id INT(10) UNSIGNED DEFAULT NULL,
+ child_service_id INT(10) UNSIGNED DEFAULT NULL,
+ disable_checks ENUM('y', 'n') DEFAULT NULL,
+ disable_notifications ENUM('y', 'n') DEFAULT NULL,
+ ignore_soft_states ENUM('y', 'n') DEFAULT NULL,
+ period_id INT(10) UNSIGNED DEFAULT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ parent_service_by_name VARCHAR(255) DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_dependency_parent_host
+ FOREIGN KEY parent_host (parent_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_parent_service
+ FOREIGN KEY parent_service (parent_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_child_host
+ FOREIGN KEY child_host (child_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_child_service
+ FOREIGN KEY child_service (child_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_period
+ FOREIGN KEY period (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_dependency_inheritance (
+ dependency_id INT(10) UNSIGNED NOT NULL,
+ parent_dependency_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (dependency_id, parent_dependency_id),
+ UNIQUE KEY unique_order (dependency_id, weight),
+ CONSTRAINT icinga_dependency_inheritance_dependency
+ FOREIGN KEY dependency (dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_inheritance_parent_dependency
+ FOREIGN KEY parent_dependency (parent_dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_dependency_states_set (
+ dependency_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'OK',
+ 'Warning',
+ 'Critical',
+ 'Unknown',
+ 'Up',
+ 'Down'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (dependency_id, property, merge_behaviour),
+ CONSTRAINT icinga_dependency_states_set_dependency
+ FOREIGN KEY icinga_dependency (dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (144, NOW());
diff --git a/schema/mysql-migrations/upgrade_145.sql b/schema/mysql-migrations/upgrade_145.sql
new file mode 100644
index 0000000..a0e1853
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_145.sql
@@ -0,0 +1,7 @@
+ALTER TABLE import_row_modifier
+ ADD INDEX source_id (source_id),
+ DROP INDEX idx_prio;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (145, NOW());
diff --git a/schema/mysql-migrations/upgrade_146.sql b/schema/mysql-migrations/upgrade_146.sql
new file mode 100644
index 0000000..0520219
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_146.sql
@@ -0,0 +1,14 @@
+ALTER TABLE icinga_host
+ DROP COLUMN flapping_threshold,
+ ADD COLUMN flapping_threshold_high SMALLINT UNSIGNED DEFAULT NULL,
+ ADD COLUMN flapping_threshold_low SMALLINT UNSIGNED DEFAULT NULL;
+
+ALTER TABLE icinga_service
+ DROP COLUMN flapping_threshold,
+ ADD COLUMN flapping_threshold_high SMALLINT UNSIGNED DEFAULT NULL,
+ ADD COLUMN flapping_threshold_low SMALLINT UNSIGNED DEFAULT NULL;
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (146, NOW());
diff --git a/schema/mysql-migrations/upgrade_147.sql b/schema/mysql-migrations/upgrade_147.sql
new file mode 100644
index 0000000..d609cda
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_147.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_host_service_blacklist (
+ host_id INT(10) UNSIGNED NOT NULL,
+ service_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (host_id, service_id),
+ CONSTRAINT icinga_host_service_bl_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_service_bl_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (147, NOW());
diff --git a/schema/mysql-migrations/upgrade_148.sql b/schema/mysql-migrations/upgrade_148.sql
new file mode 100644
index 0000000..2d15c82
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_148.sql
@@ -0,0 +1,10 @@
+ALTER TABLE import_source
+ MODIFY provider_class VARCHAR(128) NOT NULL;
+
+ALTER TABLE import_row_modifier
+ MODIFY provider_class VARCHAR(128) NOT NULL;
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (148, NOW());
diff --git a/schema/mysql-migrations/upgrade_149.sql b/schema/mysql-migrations/upgrade_149.sql
new file mode 100644
index 0000000..5940311
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_149.sql
@@ -0,0 +1,11 @@
+ALTER TABLE icinga_usergroup
+ ADD COLUMN zone_id INT(10) UNSIGNED DEFAULT NULL,
+ ADD CONSTRAINT icinga_usergroup_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (149, NOW());
diff --git a/schema/mysql-migrations/upgrade_150.sql b/schema/mysql-migrations/upgrade_150.sql
new file mode 100644
index 0000000..92a7a6d
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_150.sql
@@ -0,0 +1,17 @@
+UPDATE icinga_user u
+SET period_id = NULL
+WHERE NOT EXISTS (
+ SELECT id FROM icinga_timeperiod
+ WHERE id = u.period_id
+) AND u.period_id IS NOT NULL;
+
+ALTER TABLE icinga_user
+ ADD CONSTRAINT icinga_user_period
+ FOREIGN KEY period (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (150, NOW());
diff --git a/schema/mysql-migrations/upgrade_151.sql b/schema/mysql-migrations/upgrade_151.sql
new file mode 100644
index 0000000..a811fd4
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_151.sql
@@ -0,0 +1,38 @@
+ALTER TABLE icinga_timeperiod
+ ADD COLUMN prefer_includes ENUM('y', 'n') DEFAULT NULL;
+
+CREATE TABLE icinga_timeperiod_include (
+ timeperiod_id INT(10) UNSIGNED NOT NULL,
+ include_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (timeperiod_id, include_id),
+ CONSTRAINT icinga_timeperiod_include
+ FOREIGN KEY timeperiod (include_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT,
+ CONSTRAINT icinga_timeperiod_include_timeperiod
+ FOREIGN KEY include (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE icinga_timeperiod_exclude (
+ timeperiod_id INT(10) UNSIGNED NOT NULL,
+ exclude_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (timeperiod_id, exclude_id),
+ CONSTRAINT icinga_timeperiod_exclude
+ FOREIGN KEY timeperiod (exclude_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT,
+ CONSTRAINT icinga_timeperiod_exclude_timeperiod
+ FOREIGN KEY exclude (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (151, NOW());
diff --git a/schema/mysql-migrations/upgrade_152.sql b/schema/mysql-migrations/upgrade_152.sql
new file mode 100644
index 0000000..91e8eea
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_152.sql
@@ -0,0 +1,9 @@
+ALTER TABLE import_source
+ ADD UNIQUE INDEX source_name (source_name);
+
+ALTER TABLE sync_rule
+ ADD UNIQUE INDEX rule_name (rule_name);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (152, NOW());
diff --git a/schema/mysql-migrations/upgrade_153.sql b/schema/mysql-migrations/upgrade_153.sql
new file mode 100644
index 0000000..fa85130
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_153.sql
@@ -0,0 +1,42 @@
+CREATE TABLE director_basket (
+ uuid VARBINARY(16) NOT NULL,
+ basket_name VARCHAR(64) NOT NULL,
+ owner_type ENUM(
+ 'user',
+ 'usergroup',
+ 'role'
+ ) NOT NULL,
+ owner_value VARCHAR(255) NOT NULL,
+ objects MEDIUMTEXT NOT NULL, -- json-encoded
+ PRIMARY KEY (uuid),
+ UNIQUE INDEX basket_name (basket_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
+
+CREATE TABLE director_basket_content (
+ checksum VARBINARY(20) NOT NULL,
+ summary VARCHAR(255) NOT NULL, -- json
+ content MEDIUMTEXT NOT NULL, -- json
+ PRIMARY KEY (checksum)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
+
+CREATE TABLE director_basket_snapshot (
+ basket_uuid VARBINARY(16) NOT NULL,
+ ts_create BIGINT(20) NOT NULL,
+ content_checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (basket_uuid, ts_create),
+ INDEX sort_idx (ts_create),
+ CONSTRAINT basked_snapshot_basket
+ FOREIGN KEY director_basket_snapshot (basket_uuid)
+ REFERENCES director_basket (uuid)
+ ON DELETE CASCADE
+ ON UPDATE RESTRICT,
+ CONSTRAINT basked_snapshot_content
+ FOREIGN KEY content_checksum (content_checksum)
+ REFERENCES director_basket_content (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (153, NOW());
diff --git a/schema/mysql-migrations/upgrade_154.sql b/schema/mysql-migrations/upgrade_154.sql
new file mode 100644
index 0000000..08274b0
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_154.sql
@@ -0,0 +1,12 @@
+
+UPDATE icinga_command_argument
+SET argument_format = NULL
+WHERE argument_value IS NULL;
+
+UPDATE icinga_command_argument
+SET set_if_format = NULL
+WHERE set_if IS NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (154, NOW());
diff --git a/schema/mysql-migrations/upgrade_155.sql b/schema/mysql-migrations/upgrade_155.sql
new file mode 100644
index 0000000..eab331f
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_155.sql
@@ -0,0 +1,19 @@
+CREATE TABLE icinga_servicegroup_service_resolved (
+ servicegroup_id INT(10) UNSIGNED NOT NULL,
+ service_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (servicegroup_id, service_id),
+ CONSTRAINT icinga_servicegroup_service_resolved_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_service_resolved_servicegroup
+ FOREIGN KEY servicegroup (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (155, NOW());
diff --git a/schema/mysql-migrations/upgrade_156.sql b/schema/mysql-migrations/upgrade_156.sql
new file mode 100644
index 0000000..cd13edf
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_156.sql
@@ -0,0 +1,7 @@
+ALTER TABLE icinga_command
+ DROP INDEX object_name,
+ADD UNIQUE INDEX object_name (object_name);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (156, NOW());
diff --git a/schema/mysql-migrations/upgrade_157.sql b/schema/mysql-migrations/upgrade_157.sql
new file mode 100644
index 0000000..f093a88
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_157.sql
@@ -0,0 +1,6 @@
+ALTER TABLE director_basket_content
+ MODIFY COLUMN summary VARCHAR(500) NOT NULL;
+
+ INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (157, NOW());
diff --git a/schema/mysql-migrations/upgrade_159.sql b/schema/mysql-migrations/upgrade_159.sql
new file mode 100644
index 0000000..4de3cc6
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_159.sql
@@ -0,0 +1,6 @@
+ALTER TABLE director_generated_file
+ MODIFY COLUMN content LONGTEXT NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (159, NOW());
diff --git a/schema/mysql-migrations/upgrade_160.sql b/schema/mysql-migrations/upgrade_160.sql
new file mode 100644
index 0000000..3afbf47
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_160.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_command
+ ADD COLUMN is_string enum ('y', 'n') NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (160, NOW());
diff --git a/schema/mysql-migrations/upgrade_161.sql b/schema/mysql-migrations/upgrade_161.sql
new file mode 100644
index 0000000..f8134a4
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_161.sql
@@ -0,0 +1,58 @@
+CREATE TABLE icinga_scheduled_downtime (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ author VARCHAR(255) DEFAULT NULL,
+ comment TEXT DEFAULT NULL,
+ fixed ENUM('y', 'n') DEFAULT NULL,
+ duration INT(10) UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX object_name (object_name),
+ CONSTRAINT icinga_scheduled_downtime_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_scheduled_downtime_inheritance (
+ scheduled_downtime_id INT(10) UNSIGNED NOT NULL,
+ parent_scheduled_downtime_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (scheduled_downtime_id, parent_scheduled_downtime_id),
+ UNIQUE KEY unique_order (scheduled_downtime_id, weight),
+ CONSTRAINT icinga_scheduled_downtime_inheritance_downtime
+ FOREIGN KEY host (scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_scheduled_downtime_inheritance_parent_downtime
+ FOREIGN KEY host (parent_scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_scheduled_downtime_range (
+ scheduled_downtime_id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ range_key VARCHAR(255) NOT NULL COMMENT 'monday, ...',
+ range_value VARCHAR(255) NOT NULL COMMENT '00:00-24:00, ...',
+ range_type ENUM('include', 'exclude') NOT NULL DEFAULT 'include'
+ COMMENT 'include -> ranges {}, exclude ranges_ignore {} - not yet',
+ merge_behaviour ENUM('set', 'add', 'substract') NOT NULL DEFAULT 'set'
+ COMMENT 'set -> = {}, add -> += {}, substract -> -= {}',
+ PRIMARY KEY (scheduled_downtime_id, range_type, range_key),
+ CONSTRAINT icinga_scheduled_downtime_range_downtime
+ FOREIGN KEY scheduled_downtime (scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (161, NOW());
diff --git a/schema/mysql-migrations/upgrade_162.sql b/schema/mysql-migrations/upgrade_162.sql
new file mode 100644
index 0000000..7af104a
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_162.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_scheduled_downtime
+ ADD COLUMN with_services ENUM('y', 'n') NULL DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (162, NOW());
diff --git a/schema/mysql-migrations/upgrade_163.sql b/schema/mysql-migrations/upgrade_163.sql
new file mode 100644
index 0000000..610c0f6
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_163.sql
@@ -0,0 +1,38 @@
+-- when applying manually make sure to set a sensible timezone for your users
+-- otherwise the server / client timezone will be used!
+
+-- SET time_zone = '+02:00';
+
+SET sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT,ANSI_QUOTES,ERROR_FOR_DIVISION_BY_ZERO';
+
+ALTER TABLE director_activity_log
+ MODIFY change_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
+
+ALTER TABLE director_deployment_log
+ MODIFY start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ MODIFY end_time TIMESTAMP NULL DEFAULT NULL,
+ MODIFY abort_time TIMESTAMP NULL DEFAULT NULL;
+
+ALTER TABLE director_schema_migration
+ MODIFY migration_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
+
+ALTER TABLE director_job
+ MODIFY ts_last_attempt TIMESTAMP NULL DEFAULT NULL,
+ MODIFY ts_last_error TIMESTAMP NULL DEFAULT NULL;
+
+ALTER TABLE import_source
+ MODIFY last_attempt TIMESTAMP NULL DEFAULT NULL;
+
+ALTER TABLE import_run
+ MODIFY start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ MODIFY end_time TIMESTAMP NULL DEFAULT NULL;
+
+ALTER TABLE sync_rule
+ MODIFY last_attempt TIMESTAMP NULL DEFAULT NULL;
+
+ALTER TABLE sync_run
+ MODIFY start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (163, NOW());
diff --git a/schema/mysql-migrations/upgrade_164.sql b/schema/mysql-migrations/upgrade_164.sql
new file mode 100644
index 0000000..19dec3d
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_164.sql
@@ -0,0 +1,8 @@
+SET sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT,ANSI_QUOTES,ERROR_FOR_DIVISION_BY_ZERO';
+
+ALTER TABLE icinga_dependency
+ ADD COLUMN parent_host_var VARCHAR(128) DEFAULT NULL AFTER parent_host_id;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (164, NOW());
diff --git a/schema/mysql-migrations/upgrade_165.sql b/schema/mysql-migrations/upgrade_165.sql
new file mode 100644
index 0000000..dec47ce
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_165.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_host
+ MODIFY COLUMN address VARCHAR(255) DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (165, NOW());
diff --git a/schema/mysql-migrations/upgrade_166.sql b/schema/mysql-migrations/upgrade_166.sql
new file mode 100644
index 0000000..92b56f3
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_166.sql
@@ -0,0 +1,21 @@
+ALTER TABLE sync_rule MODIFY object_type enum(
+ 'host',
+ 'service',
+ 'command',
+ 'user',
+ 'hostgroup',
+ 'servicegroup',
+ 'usergroup',
+ 'datalistEntry',
+ 'endpoint',
+ 'zone',
+ 'timePeriod',
+ 'serviceSet',
+ 'scheduledDowntime',
+ 'notification',
+ 'dependency'
+) NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (166, NOW());
diff --git a/schema/mysql-migrations/upgrade_167.sql b/schema/mysql-migrations/upgrade_167.sql
new file mode 100644
index 0000000..7a33bec
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_167.sql
@@ -0,0 +1,25 @@
+CREATE TABLE director_daemon_info (
+ instance_uuid_hex VARCHAR(32) NOT NULL, -- random by daemon
+ schema_version SMALLINT UNSIGNED NOT NULL,
+ fqdn VARCHAR(255) NOT NULL,
+ username VARCHAR(64) NOT NULL,
+ pid INT UNSIGNED NOT NULL,
+ binary_path VARCHAR(128) NOT NULL,
+ binary_realpath VARCHAR(128) NOT NULL,
+ php_binary_path VARCHAR(128) NOT NULL,
+ php_binary_realpath VARCHAR(128) NOT NULL,
+ php_version VARCHAR(64) NOT NULL,
+ php_integer_size SMALLINT NOT NULL,
+ running_with_systemd ENUM('y', 'n') NOT NULL,
+ ts_started BIGINT(20) NOT NULL,
+ ts_stopped BIGINT(20) DEFAULT NULL,
+ ts_last_modification BIGINT(20) DEFAULT NULL,
+ ts_last_update BIGINT(20) DEFAULT NULL,
+ process_info MEDIUMTEXT NOT NULL,
+ PRIMARY KEY (instance_uuid_hex)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (167, NOW());
diff --git a/schema/mysql-migrations/upgrade_168.sql b/schema/mysql-migrations/upgrade_168.sql
new file mode 100644
index 0000000..27934ae
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_168.sql
@@ -0,0 +1,21 @@
+CREATE TABLE director_datafield_category (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ category_name VARCHAR(255) NOT NULL,
+ description TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY category_name (category_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+ALTER TABLE director_datafield
+ ADD COLUMN category_id INT(10) UNSIGNED DEFAULT NULL AFTER id,
+ ADD CONSTRAINT director_datafield_category
+ FOREIGN KEY category (category_id)
+ REFERENCES director_datafield_category (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+;
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (168, NOW());
diff --git a/schema/mysql-migrations/upgrade_170.sql b/schema/mysql-migrations/upgrade_170.sql
new file mode 100644
index 0000000..e259a79
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_170.sql
@@ -0,0 +1,7 @@
+
+ALTER TABLE sync_rule
+ MODIFY COLUMN update_policy ENUM('merge', 'override', 'ignore', 'update-only') NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (170, NOW());
diff --git a/schema/mysql-migrations/upgrade_171.sql b/schema/mysql-migrations/upgrade_171.sql
new file mode 100644
index 0000000..76ab309
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_171.sql
@@ -0,0 +1,3 @@
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (171, NOW());
diff --git a/schema/mysql-migrations/upgrade_172.sql b/schema/mysql-migrations/upgrade_172.sql
new file mode 100644
index 0000000..3af3571
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_172.sql
@@ -0,0 +1,11 @@
+ALTER TABLE sync_rule
+ ADD COLUMN purge_action ENUM('delete', 'disable') NULL DEFAULT NULL AFTER purge_existing;
+
+UPDATE sync_rule SET purge_action = 'delete';
+
+ALTER TABLE sync_rule
+ MODIFY COLUMN purge_action ENUM('delete', 'disable') DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (172, NOW());
diff --git a/schema/mysql-migrations/upgrade_173.sql b/schema/mysql-migrations/upgrade_173.sql
new file mode 100644
index 0000000..609f783
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_173.sql
@@ -0,0 +1,6 @@
+ALTER TABLE sync_rule
+ MODIFY COLUMN purge_action ENUM('delete', 'disable') NULL DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (173, NOW());
diff --git a/schema/mysql-migrations/upgrade_174.sql b/schema/mysql-migrations/upgrade_174.sql
new file mode 100644
index 0000000..653cb42
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_174.sql
@@ -0,0 +1,241 @@
+ALTER TABLE icinga_zone ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_zone SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_zone MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_timeperiod ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_timeperiod SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_timeperiod MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_command ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_command SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_command MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_apiuser ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_apiuser SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_apiuser MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_endpoint ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_endpoint SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_endpoint MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_host ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_host SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_host MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_service ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_service SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_service MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_hostgroup ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_hostgroup SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_hostgroup MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_servicegroup ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_servicegroup SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_servicegroup MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_user ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_user SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_user MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_usergroup ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_usergroup SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_usergroup MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_notification ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_notification SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_notification MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_dependency ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_dependency SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_dependency MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+ALTER TABLE icinga_scheduled_downtime ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_scheduled_downtime SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_scheduled_downtime MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (174, NOW());
diff --git a/schema/mysql-migrations/upgrade_175.sql b/schema/mysql-migrations/upgrade_175.sql
new file mode 100644
index 0000000..b8a010f
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_175.sql
@@ -0,0 +1,484 @@
+CREATE TABLE director_branch (
+ uuid VARBINARY(16) NOT NULL,
+ owner VARCHAR(255) NOT NULL,
+ branch_name VARCHAR(255) NOT NULL,
+ description TEXT DEFAULT NULL,
+ ts_merge_request BIGINT DEFAULT NULL,
+ PRIMARY KEY(uuid),
+ UNIQUE KEY (branch_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_branch_activity (
+ timestamp_ns BIGINT(20) NOT NULL,
+ object_uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ action ENUM ('create', 'modify', 'delete') NOT NULL,
+ object_table VARCHAR(64) NOT NULL,
+ author VARCHAR(255) NOT NULL,
+ former_properties LONGTEXT NOT NULL, -- json-encoded
+ modified_properties LONGTEXT NOT NULL,
+ PRIMARY KEY (timestamp_ns),
+ INDEX object_uuid (object_uuid),
+ INDEX branch_uuid (branch_uuid),
+ CONSTRAINT branch_activity_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_host (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ address VARCHAR(255) DEFAULT NULL,
+ address6 VARCHAR(45) DEFAULT NULL,
+ check_command VARCHAR(255) DEFAULT NULL,
+ max_check_attempts MEDIUMINT UNSIGNED DEFAULT NULL,
+ check_period VARCHAR(255) DEFAULT NULL,
+ check_interval VARCHAR(8) DEFAULT NULL,
+ retry_interval VARCHAR(8) DEFAULT NULL,
+ check_timeout SMALLINT UNSIGNED DEFAULT NULL,
+ enable_notifications ENUM('y', 'n') DEFAULT NULL,
+ enable_active_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_passive_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_event_handler ENUM('y', 'n') DEFAULT NULL,
+ enable_flapping ENUM('y', 'n') DEFAULT NULL,
+ enable_perfdata ENUM('y', 'n') DEFAULT NULL,
+ event_command VARCHAR(255) DEFAULT NULL,
+ flapping_threshold_high SMALLINT UNSIGNED DEFAULT NULL,
+ flapping_threshold_low SMALLINT UNSIGNED DEFAULT NULL,
+ volatile ENUM('y', 'n') DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ command_endpoint VARCHAR(255) DEFAULT NULL,
+ notes TEXT DEFAULT NULL,
+ notes_url VARCHAR(255) DEFAULT NULL,
+ action_url VARCHAR(255) DEFAULT NULL,
+ icon_image VARCHAR(255) DEFAULT NULL,
+ icon_image_alt VARCHAR(255) DEFAULT NULL,
+ has_agent ENUM('y', 'n') DEFAULT NULL,
+ master_should_connect ENUM('y', 'n') DEFAULT NULL,
+ accept_config ENUM('y', 'n') DEFAULT NULL,
+ api_key VARCHAR(40) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ `groups` TEXT DEFAULT NULL,
+ vars MEDIUMTEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_host_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_hostgroup (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_hostgroup_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_servicegroup (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_servicegroup_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_usergroup (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_usergroup_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_user (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ email VARCHAR(255) DEFAULT NULL,
+ pager VARCHAR(255) DEFAULT NULL,
+ enable_notifications ENUM('y', 'n') DEFAULT NULL,
+ period VARCHAR(255) DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ states TEXT DEFAULT NULL,
+ types TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ `groups` TEXT DEFAULT NULL,
+ vars MEDIUMTEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_user_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_zone (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ parent VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ is_global ENUM('y', 'n') DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_zone_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_timeperiod (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ update_method VARCHAR(64) DEFAULT NULL COMMENT 'Usually LegacyTimePeriod',
+ zone VARCHAR(255) DEFAULT NULL,
+ prefer_includes ENUM('y', 'n') DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ ranges TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_timeperiod_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_command (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ methods_execute VARCHAR(64) DEFAULT NULL,
+ command TEXT DEFAULT NULL,
+ is_string ENUM('y', 'n') NULL,
+ timeout SMALLINT UNSIGNED DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ arguments TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_command_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_apiuser (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ password VARCHAR(255) DEFAULT NULL,
+ client_dn VARCHAR(64) DEFAULT NULL,
+ permissions TEXT DEFAULT NULL COMMENT 'JSON-encoded permissions',
+
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_apiuser_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_endpoint (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ host VARCHAR(255) DEFAULT NULL,
+ port SMALLINT UNSIGNED DEFAULT NULL,
+ log_duration VARCHAR(32) DEFAULT NULL,
+ apiuser VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_endpoint_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_service (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ host VARCHAR(255) DEFAULT NULL,
+ service_set VARCHAR(255) DEFAULT NULL,
+ check_command VARCHAR(255) DEFAULT NULL,
+ max_check_attempts MEDIUMINT UNSIGNED DEFAULT NULL,
+ check_period VARCHAR(255) DEFAULT NULL,
+ check_interval VARCHAR(8) DEFAULT NULL,
+ retry_interval VARCHAR(8) DEFAULT NULL,
+ check_timeout SMALLINT UNSIGNED DEFAULT NULL,
+ enable_notifications ENUM('y', 'n') DEFAULT NULL,
+ enable_active_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_passive_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_event_handler ENUM('y', 'n') DEFAULT NULL,
+ enable_flapping ENUM('y', 'n') DEFAULT NULL,
+ enable_perfdata ENUM('y', 'n') DEFAULT NULL,
+ event_command VARCHAR(255) DEFAULT NULL,
+ flapping_threshold_high SMALLINT UNSIGNED DEFAULT NULL,
+ flapping_threshold_low SMALLINT UNSIGNED DEFAULT NULL,
+ volatile ENUM('y', 'n') DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ command_endpoint VARCHAR(255) DEFAULT NULL,
+ notes TEXT DEFAULT NULL,
+ notes_url VARCHAR(255) DEFAULT NULL,
+ action_url VARCHAR(255) DEFAULT NULL,
+ icon_image VARCHAR(255) DEFAULT NULL,
+ icon_image_alt VARCHAR(255) DEFAULT NULL,
+ use_agent ENUM('y', 'n') DEFAULT NULL,
+ apply_for VARCHAR(255) DEFAULT NULL,
+ use_var_overrides ENUM('y', 'n') DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ -- template_choice VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ `groups` TEXT DEFAULT NULL,
+ vars MEDIUMTEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_service_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_notification (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ host VARCHAR(255) DEFAULT NULL,
+ service VARCHAR(255) DEFAULT NULL,
+ times_begin INT(10) UNSIGNED DEFAULT NULL,
+ times_end INT(10) UNSIGNED DEFAULT NULL,
+ notification_interval INT(10) UNSIGNED DEFAULT NULL,
+ command VARCHAR(255) DEFAULT NULL,
+ period VARCHAR(255) DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+
+ states TEXT DEFAULT NULL,
+ types TEXT DEFAULT NULL,
+ users TEXT DEFAULT NULL,
+ usergroups TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ vars MEDIUMTEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_notification_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_scheduled_downtime (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ author VARCHAR(255) DEFAULT NULL,
+ comment TEXT DEFAULT NULL,
+ fixed ENUM('y', 'n') DEFAULT NULL,
+ duration INT(10) UNSIGNED DEFAULT NULL,
+ with_services ENUM('y', 'n') NULL DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ ranges TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_scheduled_downtime_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_dependency (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ parent_host VARCHAR(255) DEFAULT NULL,
+ parent_host_var VARCHAR(128) DEFAULT NULL,
+ parent_service VARCHAR(255) DEFAULT NULL,
+ child_host VARCHAR(255) DEFAULT NULL,
+ child_service VARCHAR(255) DEFAULT NULL,
+ disable_checks ENUM('y', 'n') DEFAULT NULL,
+ disable_notifications ENUM('y', 'n') DEFAULT NULL,
+ ignore_soft_states ENUM('y', 'n') DEFAULT NULL,
+ period VARCHAR(255) DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ parent_service_by_name VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_dependency_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (175, NOW());
diff --git a/schema/mysql-migrations/upgrade_176.sql b/schema/mysql-migrations/upgrade_176.sql
new file mode 100644
index 0000000..9913c11
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_176.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_host ADD COLUMN custom_endpoint_name VARCHAR(255) DEFAULT NULL AFTER accept_config;
+ALTER TABLE branched_icinga_host ADD COLUMN custom_endpoint_name VARCHAR(255) DEFAULT NULL AFTER accept_config;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES ('176', NOW());
diff --git a/schema/mysql-migrations/upgrade_177.sql b/schema/mysql-migrations/upgrade_177.sql
new file mode 100644
index 0000000..edceab0
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_177.sql
@@ -0,0 +1,20 @@
+ALTER TABLE icinga_service_set ADD COLUMN uuid VARBINARY(16) DEFAULT NULL AFTER id;
+SET @tmp_uuid = LOWER(CONCAT(
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'), '-',
+ '4',
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ HEX(FLOOR(RAND() * 4 + 8)),
+ LPAD(HEX(FLOOR(RAND() * 0x0fff)), 3, '0'), '-',
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0'),
+ LPAD(HEX(FLOOR(RAND() * 0xffff)), 4, '0')
+));
+UPDATE icinga_service_set SET uuid = UNHEX(LPAD(LPAD(HEX(id), 8, '0'), 32, REPLACE(@tmp_uuid, '-', ''))) WHERE uuid IS NULL;
+ALTER TABLE icinga_service_set MODIFY COLUMN uuid VARBINARY(16) NOT NULL, ADD UNIQUE INDEX uuid (uuid);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES ('177', NOW());
diff --git a/schema/mysql-migrations/upgrade_178.sql b/schema/mysql-migrations/upgrade_178.sql
new file mode 100644
index 0000000..589e604
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_178.sql
@@ -0,0 +1,20 @@
+CREATE TABLE director_activity_log_remark (
+ first_related_activity BIGINT(20) UNSIGNED NOT NULL,
+ last_related_activity BIGINT(20) UNSIGNED NOT NULL,
+ remark TEXT NOT NULL,
+ PRIMARY KEY (first_related_activity, last_related_activity),
+ CONSTRAINT activity_log_remark_begin
+ FOREIGN KEY first_related_activity (first_related_activity)
+ REFERENCES director_activity_log (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT activity_log_remark_end
+ FOREIGN KEY last_related_activity (last_related_activity)
+ REFERENCES director_activity_log (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES ('178', NOW());
diff --git a/schema/mysql-migrations/upgrade_179.sql b/schema/mysql-migrations/upgrade_179.sql
new file mode 100644
index 0000000..8368b18
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_179.sql
@@ -0,0 +1,5 @@
+ALTER TABLE director_deployment_log ADD INDEX (start_time);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES ('179', NOW());
diff --git a/schema/mysql-migrations/upgrade_180.sql b/schema/mysql-migrations/upgrade_180.sql
new file mode 100644
index 0000000..fb44365
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_180.sql
@@ -0,0 +1,26 @@
+CREATE TABLE branched_icinga_service_set (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(128) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ host VARCHAR(255) DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_service_set_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (180, NOW());
diff --git a/schema/mysql-migrations/upgrade_182.sql b/schema/mysql-migrations/upgrade_182.sql
new file mode 100644
index 0000000..bb91fda
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_182.sql
@@ -0,0 +1,12 @@
+DELETE sr.*
+ FROM sync_run sr
+ JOIN sync_rule s ON s.id = sr.rule_id
+ WHERE sr.last_former_activity = sr.last_related_activity
+ AND s.object_type != 'datalistEntry' AND sr.start_time > '2022-09-21 00:00:00';
+
+DELETE FROM sync_run
+ WHERE (objects_created + objects_deleted + objects_modified) = 0;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (182, NOW());
diff --git a/schema/mysql-migrations/upgrade_63.sql b/schema/mysql-migrations/upgrade_63.sql
new file mode 100644
index 0000000..7d23612
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_63.sql
@@ -0,0 +1,12 @@
+CREATE TABLE director_schema_migration (
+ schema_version SMALLINT UNSIGNED NOT NULL,
+ migration_time DATETIME NOT NULL,
+ PRIMARY KEY(schema_version)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+DROP TABLE director_dbversion;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 63;
+
diff --git a/schema/mysql-migrations/upgrade_64.sql b/schema/mysql-migrations/upgrade_64.sql
new file mode 100644
index 0000000..ded1a0c
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_64.sql
@@ -0,0 +1,10 @@
+CREATE TABLE director_setting (
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value VARCHAR(255) NOT NULL,
+ PRIMARY KEY(setting_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 64;
+
diff --git a/schema/mysql-migrations/upgrade_65.sql b/schema/mysql-migrations/upgrade_65.sql
new file mode 100644
index 0000000..7c23c91
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_65.sql
@@ -0,0 +1,37 @@
+ALTER TABLE icinga_zone
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_timeperiod
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_command
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_apiuser
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_endpoint
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_host
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_service
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_hostgroup
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_servicegroup
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_user
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+ALTER TABLE icinga_usergroup
+ ADD COLUMN disabled ENUM('y', 'n') NOT NULL DEFAULT 'n' AFTER object_type;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 65;
+
diff --git a/schema/mysql-migrations/upgrade_66.sql b/schema/mysql-migrations/upgrade_66.sql
new file mode 100644
index 0000000..9ce2cd8
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_66.sql
@@ -0,0 +1,37 @@
+
+-- dropping old tables, as they have never been used
+
+DROP TABLE import_row_modifier_setting;
+DROP TABLE import_row_modifier;
+
+CREATE TABLE import_row_modifier (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ source_id INT(10) UNSIGNED NOT NULL,
+ property_name VARCHAR(255) NOT NULL,
+ provider_class VARCHAR(72) NOT NULL,
+ priority SMALLINT UNSIGNED NOT NULL,
+ PRIMARY KEY (id),
+ KEY search_idx (property_name),
+ CONSTRAINT row_modifier_import_source
+ FOREIGN KEY source (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE import_row_modifier_setting (
+ row_modifier_id INT UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT DEFAULT NULL,
+ PRIMARY KEY (row_modifier_id, setting_name),
+ CONSTRAINT row_modifier_settings
+ FOREIGN KEY row_modifier (row_modifier_id)
+ REFERENCES import_row_modifier (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 66;
+
diff --git a/schema/mysql-migrations/upgrade_67.sql b/schema/mysql-migrations/upgrade_67.sql
new file mode 100644
index 0000000..4bbb1b9
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_67.sql
@@ -0,0 +1,23 @@
+CREATE TABLE sync_run (
+ id BIGINT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ rule_id INT(10) UNSIGNED DEFAULT NULL,
+ rule_name VARCHAR(255) NOT NULL,
+ start_time DATETIME NOT NULL,
+ duration_ms INT(10) UNSIGNED NOT NULL,
+ objects_deleted INT(10) UNSIGNED DEFAULT 0,
+ objects_created INT(10) UNSIGNED DEFAULT 0,
+ objects_modified INT(10) UNSIGNED DEFAULT 0,
+ first_related_activity VARBINARY(20) DEFAULT NULL,
+ last_related_activity VARBINARY(20) DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT sync_run_rule
+ FOREIGN KEY sync_rule (rule_id)
+ REFERENCES sync_rule (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 67;
+
diff --git a/schema/mysql-migrations/upgrade_68.sql b/schema/mysql-migrations/upgrade_68.sql
new file mode 100644
index 0000000..d2318b8
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_68.sql
@@ -0,0 +1,6 @@
+ALTER TABLE sync_run MODIFY duration_ms INT(10) UNSIGNED DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 68;
+
diff --git a/schema/mysql-migrations/upgrade_69.sql b/schema/mysql-migrations/upgrade_69.sql
new file mode 100644
index 0000000..7bf764e
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_69.sql
@@ -0,0 +1,9 @@
+ALTER TABLE sync_run
+ DROP COLUMN first_related_activity,
+ ADD COLUMN last_former_activity VARBINARY(20) DEFAULT NULL AFTER objects_modified;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 69;
+
+
diff --git a/schema/mysql-migrations/upgrade_70.sql b/schema/mysql-migrations/upgrade_70.sql
new file mode 100644
index 0000000..1312c02
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_70.sql
@@ -0,0 +1,13 @@
+ALTER TABLE icinga_timeperiod_range
+ DROP FOREIGN KEY icinga_timeperiod_range_timeperiod;
+
+ALTER TABLE icinga_timeperiod_range
+ ADD CONSTRAINT icinga_timeperiod_range_timeperiod
+ FOREIGN KEY timeperiod (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 70;
diff --git a/schema/mysql-migrations/upgrade_71.sql b/schema/mysql-migrations/upgrade_71.sql
new file mode 100644
index 0000000..87dbce1
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_71.sql
@@ -0,0 +1,44 @@
+CREATE TABLE icinga_notification (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ host_id INT(10) UNSIGNED DEFAULT NULL,
+ service_id INT(10) UNSIGNED DEFAULT NULL,
+ times_begin INT(10) UNSIGNED DEFAULT NULL,
+ times_end INT(10) UNSIGNED DEFAULT NULL,
+ notification_interval INT(10) UNSIGNED DEFAULT NULL,
+ command_id INT(10) UNSIGNED DEFAULT NULL,
+ period_id INT(10) UNSIGNED DEFAULT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_notification_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_command
+ FOREIGN KEY command (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_period
+ FOREIGN KEY period (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 71;
diff --git a/schema/mysql-migrations/upgrade_72.sql b/schema/mysql-migrations/upgrade_72.sql
new file mode 100644
index 0000000..82aa478
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_72.sql
@@ -0,0 +1,14 @@
+
+ALTER TABLE director_generated_config
+ ADD COLUMN first_activity_checksum VARBINARY(20) NOT NULL AFTER duration;
+
+UPDATE director_generated_config SET first_activity_checksum = last_activity_checksum;
+
+ALTER TABLE director_deployment_log
+ ADD COLUMN last_activity_checksum VARBINARY(20) NOT NULL AFTER config_checksum;
+
+UPDATE director_deployment_log l JOIN director_generated_config c ON l.config_checksum = c.checksum SET l.last_activity_checksum = c.last_activity_checksum;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 72;
diff --git a/schema/mysql-migrations/upgrade_73.sql b/schema/mysql-migrations/upgrade_73.sql
new file mode 100644
index 0000000..ecf27ae
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_73.sql
@@ -0,0 +1,50 @@
+DROP TABLE icinga_user_filter_state;
+
+CREATE TABLE icinga_user_states_set (
+ user_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'OK',
+ 'Warning',
+ 'Critical',
+ 'Unknown',
+ 'Up',
+ 'Down'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (user_id, property),
+ CONSTRAINT icinga_user_states_set_user
+ FOREIGN KEY icinga_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+DROP TABLE icinga_user_filter_type;
+
+CREATE TABLE icinga_user_filters_set (
+ user_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'DowntimeStart',
+ 'DowntimeEnd',
+ 'DowntimeRemoved',
+ 'Custom',
+ 'Acknowledgement',
+ 'Problem',
+ 'Recovery',
+ 'FlappingStart',
+ 'FlappingEnd'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (user_id, property),
+ CONSTRAINT icinga_user_filters_set_user
+ FOREIGN KEY icinga_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 73;
diff --git a/schema/mysql-migrations/upgrade_74.sql b/schema/mysql-migrations/upgrade_74.sql
new file mode 100644
index 0000000..382f937
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_74.sql
@@ -0,0 +1,14 @@
+
+ALTER TABLE icinga_service
+ DROP FOREIGN KEY icinga_host;
+
+ALTER TABLE icinga_service
+ ADD CONSTRAINT icinga_service_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 74;
diff --git a/schema/mysql-migrations/upgrade_75.sql b/schema/mysql-migrations/upgrade_75.sql
new file mode 100644
index 0000000..4afcab0
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_75.sql
@@ -0,0 +1,50 @@
+DROP TABLE icinga_user_states_set;
+
+CREATE TABLE icinga_user_states_set (
+ user_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'OK',
+ 'Warning',
+ 'Critical',
+ 'Unknown',
+ 'Up',
+ 'Down'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (user_id, property, merge_behaviour),
+ CONSTRAINT icinga_user_states_set_user
+ FOREIGN KEY icinga_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+DROP TABLE icinga_user_filters_set;
+
+CREATE TABLE icinga_user_filters_set (
+ user_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'DowntimeStart',
+ 'DowntimeEnd',
+ 'DowntimeRemoved',
+ 'Custom',
+ 'Acknowledgement',
+ 'Problem',
+ 'Recovery',
+ 'FlappingStart',
+ 'FlappingEnd'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (user_id, property, merge_behaviour),
+ CONSTRAINT icinga_user_filters_set_user
+ FOREIGN KEY icinga_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 75;
diff --git a/schema/mysql-migrations/upgrade_76.sql b/schema/mysql-migrations/upgrade_76.sql
new file mode 100644
index 0000000..a2c1d51
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_76.sql
@@ -0,0 +1,28 @@
+DROP TABLE icinga_user_filters_set;
+
+CREATE TABLE icinga_user_types_set (
+ user_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'DowntimeStart',
+ 'DowntimeEnd',
+ 'DowntimeRemoved',
+ 'Custom',
+ 'Acknowledgement',
+ 'Problem',
+ 'Recovery',
+ 'FlappingStart',
+ 'FlappingEnd'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (user_id, property, merge_behaviour),
+ CONSTRAINT icinga_user_types_set_user
+ FOREIGN KEY icinga_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 76;
diff --git a/schema/mysql-migrations/upgrade_77.sql b/schema/mysql-migrations/upgrade_77.sql
new file mode 100644
index 0000000..0b60cc5
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_77.sql
@@ -0,0 +1,78 @@
+CREATE TABLE icinga_notification_states_set (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'OK',
+ 'Warning',
+ 'Critical',
+ 'Unknown',
+ 'Up',
+ 'Down'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (notification_id, property, merge_behaviour),
+ CONSTRAINT icinga_notification_states_set_notification
+ FOREIGN KEY icinga_notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_notification_types_set (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'DowntimeStart',
+ 'DowntimeEnd',
+ 'DowntimeRemoved',
+ 'Custom',
+ 'Acknowledgement',
+ 'Problem',
+ 'Recovery',
+ 'FlappingStart',
+ 'FlappingEnd'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (notification_id, property, merge_behaviour),
+ CONSTRAINT icinga_notification_types_set_notification
+ FOREIGN KEY icinga_notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_notification_var (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) DEFAULT NULL,
+ varvalue TEXT DEFAULT NULL,
+ format enum ('string', 'json', 'expression'),
+ PRIMARY KEY (notification_id, varname),
+ key search_idx (varname),
+ CONSTRAINT icinga_notification_var_notification
+ FOREIGN KEY notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification_inheritance (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ parent_notification_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (notification_id, parent_notification_id),
+ UNIQUE KEY unique_order (notification_id, weight),
+ CONSTRAINT icinga_notification_inheritance_notification
+ FOREIGN KEY host (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_inheritance_parent_notification
+ FOREIGN KEY host (parent_notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 77;
diff --git a/schema/mysql-migrations/upgrade_78.sql b/schema/mysql-migrations/upgrade_78.sql
new file mode 100644
index 0000000..5988ff6
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_78.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_user_field (
+ user_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') NOT NULL,
+ PRIMARY KEY (user_id, datafield_id),
+ CONSTRAINT icinga_user_field_user
+ FOREIGN KEY user(user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 78;
diff --git a/schema/mysql-migrations/upgrade_82.sql b/schema/mysql-migrations/upgrade_82.sql
new file mode 100644
index 0000000..e1acff8
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_82.sql
@@ -0,0 +1,17 @@
+ALTER TABLE sync_rule
+ MODIFY COLUMN object_type enum(
+ 'host',
+ 'service',
+ 'command',
+ 'user',
+ 'hostgroup',
+ 'servicegroup',
+ 'usergroup',
+ 'datalistEntry',
+ 'endpoint',
+ 'zone'
+ ) NOT NULL;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 82;
diff --git a/schema/mysql-migrations/upgrade_84.sql b/schema/mysql-migrations/upgrade_84.sql
new file mode 100644
index 0000000..1de287a
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_84.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_usergroup DROP COLUMN zone_id;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (84, NOW());
diff --git a/schema/mysql-migrations/upgrade_85.sql b/schema/mysql-migrations/upgrade_85.sql
new file mode 100644
index 0000000..186e171
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_85.sql
@@ -0,0 +1,15 @@
+CREATE TABLE icinga_notification_assignment (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ notification_id INT(10) UNSIGNED NOT NULL,
+ filter_string TEXT NOT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_notification_assignment
+ FOREIGN KEY notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (85, NOW());
diff --git a/schema/mysql-migrations/upgrade_86.sql b/schema/mysql-migrations/upgrade_86.sql
new file mode 100644
index 0000000..58b81c0
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_86.sql
@@ -0,0 +1,35 @@
+CREATE TABLE icinga_notification_user (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ user_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (notification_id, user_id),
+ CONSTRAINT icinga_notification_user_user
+ FOREIGN KEY user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_user_notification
+ FOREIGN KEY notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification_usergroup (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ usergroup_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (notification_id, usergroup_id),
+ CONSTRAINT icinga_notification_usergroup_usergroup
+ FOREIGN KEY usergroup (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_usergroup_notification
+ FOREIGN KEY notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (86, NOW());
diff --git a/schema/mysql-migrations/upgrade_87.sql b/schema/mysql-migrations/upgrade_87.sql
new file mode 100644
index 0000000..a6da21f
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_87.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_notification
+ MODIFY COLUMN object_type ENUM('object', 'template', 'apply') NOT NULL;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 87;
diff --git a/schema/mysql-migrations/upgrade_89.sql b/schema/mysql-migrations/upgrade_89.sql
new file mode 100644
index 0000000..cf6ac20
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_89.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_command_argument
+ ADD required ENUM('y', 'n') DEFAULT NULL AFTER repeat_key;
+
+INSERT INTO director_schema_migration
+ SET migration_time = NOW(),
+ schema_version = 89;
diff --git a/schema/mysql-migrations/upgrade_90.sql b/schema/mysql-migrations/upgrade_90.sql
new file mode 100644
index 0000000..e3ef4fb
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_90.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_service_assignment ADD assign_type ENUM('assign', 'ignore') NOT NULL DEFAULT 'assign';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (90, NOW());
diff --git a/schema/mysql-migrations/upgrade_91.sql b/schema/mysql-migrations/upgrade_91.sql
new file mode 100644
index 0000000..88a551f
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_91.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_notification_assignment ADD assign_type ENUM('assign', 'ignore') NOT NULL DEFAULT 'assign';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (91, NOW());
diff --git a/schema/mysql-migrations/upgrade_92.sql b/schema/mysql-migrations/upgrade_92.sql
new file mode 100644
index 0000000..b7d2503
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_92.sql
@@ -0,0 +1,27 @@
+DELETE FROM director_datalist_entry WHERE entry_name IS NULL;
+ALTER TABLE director_datalist_entry
+ MODIFY entry_name VARCHAR(255) NOT NULL;
+
+DELETE FROM icinga_command_var WHERE varname IS NULL;
+ALTER TABLE icinga_command_var
+ MODIFY varname VARCHAR(255) NOT NULL;
+
+DELETE FROM icinga_host_var WHERE varname IS NULL;
+ALTER TABLE icinga_host_var
+ MODIFY varname VARCHAR(255) NOT NULL;
+
+DELETE FROM icinga_service_var WHERE varname IS NULL;
+ALTER TABLE icinga_service_var
+ MODIFY varname VARCHAR(255) NOT NULL;
+
+DELETE FROM icinga_user_var WHERE varname IS NULL;
+ALTER TABLE icinga_user_var
+ MODIFY varname VARCHAR(255) NOT NULL;
+
+DELETE FROM icinga_notification_var WHERE varname IS NULL;
+ALTER TABLE icinga_notification_var
+ MODIFY varname VARCHAR(255) NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (92, NOW());
diff --git a/schema/mysql-migrations/upgrade_93.sql b/schema/mysql-migrations/upgrade_93.sql
new file mode 100644
index 0000000..845d4bf
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_93.sql
@@ -0,0 +1,22 @@
+ALTER TABLE sync_rule
+ ADD COLUMN sync_state ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+ ) NOT NULL DEFAULT 'unknown',
+ ADD COLUMN last_error_message VARCHAR(255) DEFAULT NULL,
+ ADD COLUMN last_attempt DATETIME DEFAULT NULL
+;
+
+UPDATE sync_rule r
+ JOIN (
+ SELECT rule_id, MAX(start_time) AS start_time
+ FROM sync_run
+ GROUP BY rule_id
+ ) lr ON r.id = lr.rule_id
+ SET r.last_attempt = lr.start_time;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (93, NOW());
diff --git a/schema/mysql-migrations/upgrade_94.sql b/schema/mysql-migrations/upgrade_94.sql
new file mode 100644
index 0000000..5b55b37
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_94.sql
@@ -0,0 +1,29 @@
+CREATE TABLE director_job (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ job_name VARCHAR(64) NOT NULL,
+ job_class VARCHAR(72) NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ run_interval INT(10) UNSIGNED NOT NULL, -- seconds
+ last_attempt_succeeded ENUM('y', 'n') DEFAULT NULL,
+ ts_last_attempt DATETIME DEFAULT NULL,
+ ts_last_error DATETIME DEFAULT NULL,
+ last_error_message TEXT,
+ PRIMARY KEY (id),
+ UNIQUE KEY (job_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_job_setting (
+ job_id INT UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT DEFAULT NULL,
+ PRIMARY KEY (job_id, setting_name),
+ CONSTRAINT job_settings
+ FOREIGN KEY director_job (job_id)
+ REFERENCES director_job (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (94, NOW());
diff --git a/schema/mysql-migrations/upgrade_95.sql b/schema/mysql-migrations/upgrade_95.sql
new file mode 100644
index 0000000..aa49c5b
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_95.sql
@@ -0,0 +1,22 @@
+ALTER TABLE import_source
+ ADD COLUMN import_state ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+ ) NOT NULL DEFAULT 'unknown',
+ ADD COLUMN last_error_message TEXT DEFAULT NULL,
+ ADD COLUMN last_attempt DATETIME DEFAULT NULL
+;
+
+UPDATE import_source s
+ JOIN (
+ SELECT source_id, MAX(start_time) AS start_time
+ FROM import_run
+ GROUP BY source_id
+ ) ir ON s.id = ir.source_id
+ SET s.last_attempt = ir.start_time;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (95, NOW());
diff --git a/schema/mysql-migrations/upgrade_96.sql b/schema/mysql-migrations/upgrade_96.sql
new file mode 100644
index 0000000..de96582
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_96.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_notification ADD apply_to ENUM('host', 'service') DEFAULT NULL AFTER disabled;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (96, NOW());
diff --git a/schema/mysql-migrations/upgrade_97.sql b/schema/mysql-migrations/upgrade_97.sql
new file mode 100644
index 0000000..7da33b2
--- /dev/null
+++ b/schema/mysql-migrations/upgrade_97.sql
@@ -0,0 +1,11 @@
+ALTER TABLE director_job
+ ADD COLUMN timeperiod_id INT(10) UNSIGNED DEFAULT NULL AFTER run_interval,
+ ADD CONSTRAINT director_job_period
+ FOREIGN KEY timeperiod (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (97, NOW());
diff --git a/schema/mysql.sql b/schema/mysql.sql
new file mode 100644
index 0000000..02ac5d9
--- /dev/null
+++ b/schema/mysql.sql
@@ -0,0 +1,2442 @@
+--
+-- MySQL schema
+-- ============
+--
+-- You should normally not be required to care about schema handling.
+-- Director does all the migrations for you and guides you either in
+-- the frontend or provides everything you need for automated migration
+-- handling. Please find more related information in our documentation.
+
+SET sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT,ANSI_QUOTES,ERROR_FOR_DIVISION_BY_ZERO';
+
+CREATE TABLE director_daemon_info (
+ instance_uuid_hex VARCHAR(32) NOT NULL, -- random by daemon
+ schema_version SMALLINT UNSIGNED NOT NULL,
+ fqdn VARCHAR(255) NOT NULL,
+ username VARCHAR(64) NOT NULL,
+ pid INT UNSIGNED NOT NULL,
+ binary_path VARCHAR(128) NOT NULL,
+ binary_realpath VARCHAR(128) NOT NULL,
+ php_binary_path VARCHAR(128) NOT NULL,
+ php_binary_realpath VARCHAR(128) NOT NULL,
+ php_version VARCHAR(64) NOT NULL,
+ php_integer_size SMALLINT NOT NULL,
+ running_with_systemd ENUM('y', 'n') NOT NULL,
+ ts_started BIGINT(20) NOT NULL,
+ ts_stopped BIGINT(20) DEFAULT NULL,
+ ts_last_modification BIGINT(20) DEFAULT NULL,
+ ts_last_update BIGINT(20) DEFAULT NULL,
+ process_info MEDIUMTEXT NOT NULL,
+ PRIMARY KEY (instance_uuid_hex)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
+
+CREATE TABLE director_activity_log (
+ id BIGINT(20) UNSIGNED AUTO_INCREMENT NOT NULL,
+ object_type VARCHAR(64) NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ action_name ENUM('create', 'delete', 'modify') NOT NULL,
+ old_properties TEXT DEFAULT NULL COMMENT 'Property hash, JSON',
+ new_properties TEXT DEFAULT NULL COMMENT 'Property hash, JSON',
+ author VARCHAR(64) NOT NULL,
+ change_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ checksum VARBINARY(20) NOT NULL,
+ parent_checksum VARBINARY(20) DEFAULT NULL,
+ PRIMARY KEY (id),
+ INDEX sort_idx (change_time),
+ INDEX search_idx (object_name),
+ INDEX search_idx2 (object_type(32), object_name(64), change_time),
+ INDEX search_author (author),
+ INDEX checksum (checksum)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_activity_log_remark (
+ first_related_activity BIGINT(20) UNSIGNED NOT NULL,
+ last_related_activity BIGINT(20) UNSIGNED NOT NULL,
+ remark TEXT NOT NULL,
+ PRIMARY KEY (first_related_activity, last_related_activity),
+ CONSTRAINT activity_log_remark_begin
+ FOREIGN KEY first_related_activity (first_related_activity)
+ REFERENCES director_activity_log (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT activity_log_remark_end
+ FOREIGN KEY last_related_activity (last_related_activity)
+ REFERENCES director_activity_log (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_basket (
+ uuid VARBINARY(16) NOT NULL,
+ basket_name VARCHAR(64) NOT NULL,
+ owner_type ENUM(
+ 'user',
+ 'usergroup',
+ 'role'
+ ) NOT NULL,
+ owner_value VARCHAR(255) NOT NULL,
+ objects MEDIUMTEXT NOT NULL, -- json-encoded
+ PRIMARY KEY (uuid),
+ UNIQUE INDEX basket_name (basket_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
+
+CREATE TABLE director_basket_content (
+ checksum VARBINARY(20) NOT NULL,
+ summary VARCHAR(500) NOT NULL, -- json
+ content MEDIUMTEXT NOT NULL, -- json
+ PRIMARY KEY (checksum)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
+
+CREATE TABLE director_basket_snapshot (
+ basket_uuid VARBINARY(16) NOT NULL,
+ ts_create BIGINT(20) NOT NULL,
+ content_checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (basket_uuid, ts_create),
+ INDEX sort_idx (ts_create),
+ CONSTRAINT basked_snapshot_basket
+ FOREIGN KEY director_basket_snapshot (basket_uuid)
+ REFERENCES director_basket (uuid)
+ ON DELETE CASCADE
+ ON UPDATE RESTRICT,
+ CONSTRAINT basked_snapshot_content
+ FOREIGN KEY content_checksum (content_checksum)
+ REFERENCES director_basket_content (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
+
+CREATE TABLE director_generated_config (
+ checksum VARBINARY(20) NOT NULL COMMENT 'SHA1(last_activity_checksum;file_path=checksum;file_path=checksum;...)',
+ director_version VARCHAR(64) DEFAULT NULL,
+ director_db_version INT(10) DEFAULT NULL,
+ duration INT(10) UNSIGNED DEFAULT NULL COMMENT 'Config generation duration (ms)',
+ first_activity_checksum VARBINARY(20) NOT NULL,
+ last_activity_checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (checksum),
+ CONSTRAINT director_generated_config_activity
+ FOREIGN KEY activity_checksum (last_activity_checksum)
+ REFERENCES director_activity_log (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_generated_file (
+ checksum VARBINARY(20) NOT NULL COMMENT 'SHA1(content)',
+ content LONGTEXT NOT NULL,
+ cnt_object INT(10) UNSIGNED NOT NULL DEFAULT 0,
+ cnt_template INT(10) UNSIGNED NOT NULL DEFAULT 0,
+ cnt_apply INT(10) UNSIGNED NOT NULL DEFAULT 0,
+ PRIMARY KEY (checksum)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_generated_config_file (
+ config_checksum VARBINARY(20) NOT NULL,
+ file_checksum VARBINARY(20) NOT NULL,
+ file_path VARCHAR(128) NOT NULL COMMENT 'e.g. zones/nafta/hosts.conf',
+ CONSTRAINT director_generated_config_file_config
+ FOREIGN KEY config (config_checksum)
+ REFERENCES director_generated_config (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT director_generated_config_file_file
+ FOREIGN KEY checksum (file_checksum)
+ REFERENCES director_generated_file (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT,
+ PRIMARY KEY (config_checksum, file_path),
+ INDEX search_idx (file_checksum)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_deployment_log (
+ id BIGINT(20) UNSIGNED AUTO_INCREMENT NOT NULL,
+ config_checksum VARBINARY(20) DEFAULT NULL,
+ last_activity_checksum VARBINARY(20) NOT NULL,
+ peer_identity VARCHAR(64) NOT NULL,
+ start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ end_time TIMESTAMP NULL DEFAULT NULL,
+ abort_time TIMESTAMP NULL DEFAULT NULL,
+ duration_connection INT(10) UNSIGNED DEFAULT NULL
+ COMMENT 'The time it took to connect to an Icinga node (ms)',
+ duration_dump INT(10) UNSIGNED DEFAULT NULL
+ COMMENT 'Time spent dumping the config (ms)',
+ stage_name VARCHAR(96) DEFAULT NULL,
+ stage_collected ENUM('y', 'n') DEFAULT NULL,
+ connection_succeeded ENUM('y', 'n') DEFAULT NULL,
+ dump_succeeded ENUM('y', 'n') DEFAULT NULL,
+ startup_succeeded ENUM('y', 'n') DEFAULT NULL,
+ username VARCHAR(64) DEFAULT NULL COMMENT 'The user that triggered this deployment',
+ startup_log MEDIUMTEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ INDEX (start_time),
+ CONSTRAINT config_checksum
+ FOREIGN KEY config_checksum (config_checksum)
+ REFERENCES director_generated_config (checksum)
+ ON DELETE SET NULL
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_datalist (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ list_name VARCHAR(255) NOT NULL,
+ owner VARCHAR(255) NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY list_name (list_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_datalist_entry (
+ list_id INT(10) UNSIGNED NOT NULL,
+ entry_name VARCHAR(255) COLLATE utf8_bin NOT NULL,
+ entry_value TEXT DEFAULT NULL,
+ format enum ('string', 'expression', 'json'),
+ allowed_roles VARCHAR(255) DEFAULT NULL,
+ PRIMARY KEY (list_id, entry_name),
+ CONSTRAINT director_datalist_value_datalist
+ FOREIGN KEY datalist (list_id)
+ REFERENCES director_datalist (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_datafield_category (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ category_name VARCHAR(255) NOT NULL,
+ description TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY category_name (category_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_datafield (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ category_id INT(10) UNSIGNED DEFAULT NULL,
+ varname VARCHAR(64) NOT NULL COLLATE utf8_bin,
+ caption VARCHAR(255) NOT NULL,
+ description TEXT DEFAULT NULL,
+ datatype varchar(255) NOT NULL,
+-- datatype_param? multiple ones?
+ format enum ('string', 'json', 'expression'),
+ PRIMARY KEY (id),
+ KEY search_idx (varname),
+ CONSTRAINT director_datafield_category
+ FOREIGN KEY category (category_id)
+ REFERENCES director_datafield_category (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_datafield_setting (
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT NOT NULL,
+ PRIMARY KEY (datafield_id, setting_name),
+ CONSTRAINT datafield_id_settings
+ FOREIGN KEY datafield (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_schema_migration (
+ schema_version SMALLINT UNSIGNED NOT NULL,
+ migration_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY(schema_version)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_setting (
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value VARCHAR(255) NOT NULL,
+ PRIMARY KEY(setting_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_zone (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ parent_id INT(10) UNSIGNED DEFAULT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ object_type ENUM('object', 'template', 'external_object') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ is_global ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name),
+ CONSTRAINT icinga_zone_parent
+ FOREIGN KEY parent_zone (parent_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_zone_inheritance (
+ zone_id INT(10) UNSIGNED NOT NULL,
+ parent_zone_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (zone_id, parent_zone_id),
+ UNIQUE KEY unique_order (zone_id, weight),
+ CONSTRAINT icinga_zone_inheritance_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_zone_inheritance_parent_zone
+ FOREIGN KEY zone (parent_zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_timeperiod (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ update_method VARCHAR(64) DEFAULT NULL COMMENT 'Usually LegacyTimePeriod',
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ object_type ENUM('object', 'template') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ prefer_includes ENUM('y', 'n') DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name, zone_id),
+ CONSTRAINT icinga_timeperiod_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_timeperiod_inheritance (
+ timeperiod_id INT(10) UNSIGNED NOT NULL,
+ parent_timeperiod_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (timeperiod_id, parent_timeperiod_id),
+ UNIQUE KEY unique_order (timeperiod_id, weight),
+ CONSTRAINT icinga_timeperiod_inheritance_timeperiod
+ FOREIGN KEY host (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_timeperiod_inheritance_parent_timeperiod
+ FOREIGN KEY host (parent_timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_timeperiod_range (
+ timeperiod_id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ range_key VARCHAR(255) NOT NULL COMMENT 'monday, ...',
+ range_value VARCHAR(255) NOT NULL COMMENT '00:00-24:00, ...',
+ range_type ENUM('include', 'exclude') NOT NULL DEFAULT 'include'
+ COMMENT 'include -> ranges {}, exclude ranges_ignore {} - not yet',
+ merge_behaviour ENUM('set', 'add', 'substract') NOT NULL DEFAULT 'set'
+ COMMENT 'set -> = {}, add -> += {}, substract -> -= {}',
+ PRIMARY KEY (timeperiod_id, range_type, range_key),
+ CONSTRAINT icinga_timeperiod_range_timeperiod
+ FOREIGN KEY timeperiod (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_job (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ job_name VARCHAR(64) NOT NULL,
+ job_class VARCHAR(72) NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ run_interval INT(10) UNSIGNED NOT NULL, -- seconds
+ timeperiod_id INT(10) UNSIGNED DEFAULT NULL,
+ last_attempt_succeeded ENUM('y', 'n') DEFAULT NULL,
+ ts_last_attempt TIMESTAMP NULL DEFAULT NULL,
+ ts_last_error TIMESTAMP NULL DEFAULT NULL,
+ last_error_message TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY (job_name),
+ CONSTRAINT director_job_period
+ FOREIGN KEY timeperiod (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_job_setting (
+ job_id INT UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT DEFAULT NULL,
+ PRIMARY KEY (job_id, setting_name),
+ CONSTRAINT job_settings
+ FOREIGN KEY director_job (job_id)
+ REFERENCES director_job (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_command (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ object_type ENUM('object', 'template', 'external_object') NOT NULL
+ COMMENT 'external_object is an attempt to work with existing commands',
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ methods_execute VARCHAR(64) DEFAULT NULL,
+ command TEXT DEFAULT NULL,
+ is_string ENUM('y', 'n') NULL,
+ -- env text DEFAULT NULL,
+ -- vars text DEFAULT NULL,
+ timeout SMALLINT UNSIGNED DEFAULT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name),
+ CONSTRAINT icinga_command_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_command_inheritance (
+ command_id INT(10) UNSIGNED NOT NULL,
+ parent_command_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (command_id, parent_command_id),
+ UNIQUE KEY unique_order (command_id, weight),
+ CONSTRAINT icinga_command_inheritance_command
+ FOREIGN KEY command (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_command_inheritance_parent_command
+ FOREIGN KEY command (parent_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_command_argument (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ command_id INT(10) UNSIGNED NOT NULL,
+ argument_name VARCHAR(64) COLLATE utf8_bin NOT NULL COMMENT '-x, --host',
+ argument_value TEXT DEFAULT NULL,
+ argument_format ENUM('string', 'expression', 'json') NULL DEFAULT NULL,
+ key_string VARCHAR(64) DEFAULT NULL COMMENT 'Overrides name',
+ description TEXT DEFAULT NULL,
+ skip_key ENUM('y', 'n') DEFAULT NULL,
+ set_if VARCHAR(255) DEFAULT NULL, -- (string expression, must resolve to a numeric value)
+ set_if_format ENUM('string', 'expression', 'json') DEFAULT NULL,
+ sort_order SMALLINT DEFAULT NULL, -- -> order
+ repeat_key ENUM('y', 'n') DEFAULT NULL COMMENT 'Useful with array values',
+ required ENUM('y', 'n') DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY unique_idx (command_id, argument_name),
+ INDEX sort_idx (command_id, sort_order),
+ CONSTRAINT icinga_command_argument_command
+ FOREIGN KEY command (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_command_field (
+ command_id INT(10) UNSIGNED NOT NULL,
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (command_id, datafield_id),
+ CONSTRAINT icinga_command_field_command
+ FOREIGN KEY command_id (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_command_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_command_var (
+ command_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ varvalue TEXT DEFAULT NULL,
+ format ENUM('string', 'expression', 'json') NOT NULL DEFAULT 'string',
+ checksum VARBINARY(20) DEFAULT NULL,
+ PRIMARY KEY (command_id, varname),
+ INDEX search_idx (varname),
+ INDEX checksum (checksum),
+ CONSTRAINT icinga_command_var_command
+ FOREIGN KEY command (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_apiuser (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ object_type ENUM('object', 'template', 'external_object') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ password VARCHAR(255) DEFAULT NULL,
+ client_dn VARCHAR(64) DEFAULT NULL,
+ permissions TEXT DEFAULT NULL COMMENT 'JSON-encoded permissions',
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_endpoint (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ object_type ENUM('object', 'template', 'external_object') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ host VARCHAR(255) DEFAULT NULL COMMENT 'IP address / hostname of remote node',
+ port SMALLINT UNSIGNED DEFAULT NULL COMMENT '5665 if not set',
+ log_duration VARCHAR(32) DEFAULT NULL COMMENT '1d if not set',
+ apiuser_id INT(10) UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name),
+ CONSTRAINT icinga_endpoint_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_apiuser
+ FOREIGN KEY apiuser (apiuser_id)
+ REFERENCES icinga_apiuser (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_endpoint_inheritance (
+ endpoint_id INT(10) UNSIGNED NOT NULL,
+ parent_endpoint_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (endpoint_id, parent_endpoint_id),
+ UNIQUE KEY unique_order (endpoint_id, weight),
+ CONSTRAINT icinga_endpoint_inheritance_endpoint
+ FOREIGN KEY endpoint (endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_endpoint_inheritance_parent_endpoint
+ FOREIGN KEY endpoint (parent_endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_host_template_choice (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ object_name VARCHAR(64) NOT NULL,
+ description TEXT DEFAULT NULL,
+ min_required SMALLINT UNSIGNED NOT NULL DEFAULT 0,
+ max_allowed SMALLINT UNSIGNED NOT NULL DEFAULT 1,
+ required_template_id INT(10) UNSIGNED DEFAULT NULL,
+ allowed_roles VARCHAR(255) DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY (object_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_host (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ object_type ENUM('object', 'template') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ display_name VARCHAR(255) DEFAULT NULL,
+ address VARCHAR(255) DEFAULT NULL,
+ address6 VARCHAR(45) DEFAULT NULL,
+ check_command_id INT(10) UNSIGNED DEFAULT NULL,
+ max_check_attempts MEDIUMINT UNSIGNED DEFAULT NULL,
+ check_period_id INT(10) UNSIGNED DEFAULT NULL,
+ check_interval VARCHAR(8) DEFAULT NULL,
+ retry_interval VARCHAR(8) DEFAULT NULL,
+ check_timeout SMALLINT UNSIGNED DEFAULT NULL,
+ enable_notifications ENUM('y', 'n') DEFAULT NULL,
+ enable_active_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_passive_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_event_handler ENUM('y', 'n') DEFAULT NULL,
+ enable_flapping ENUM('y', 'n') DEFAULT NULL,
+ enable_perfdata ENUM('y', 'n') DEFAULT NULL,
+ event_command_id INT(10) UNSIGNED DEFAULT NULL,
+ flapping_threshold_high SMALLINT UNSIGNED default null,
+ flapping_threshold_low SMALLINT UNSIGNED default null,
+ volatile ENUM('y', 'n') DEFAULT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ command_endpoint_id INT(10) UNSIGNED DEFAULT NULL,
+ notes TEXT DEFAULT NULL,
+ notes_url VARCHAR(255) DEFAULT NULL,
+ action_url VARCHAR(255) DEFAULT NULL,
+ icon_image VARCHAR(255) DEFAULT NULL,
+ icon_image_alt VARCHAR(255) DEFAULT NULL,
+ has_agent ENUM('y', 'n') DEFAULT NULL,
+ master_should_connect ENUM('y', 'n') DEFAULT NULL,
+ accept_config ENUM('y', 'n') DEFAULT NULL,
+ custom_endpoint_name VARCHAR(255) DEFAULT NULL,
+ api_key VARCHAR(40) DEFAULT NULL,
+ template_choice_id INT(10) UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name),
+ UNIQUE INDEX api_key (api_key),
+ KEY search_idx (display_name),
+ CONSTRAINT icinga_host_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_check_period
+ FOREIGN KEY timeperiod (check_period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_check_command
+ FOREIGN KEY check_command (check_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_event_command
+ FOREIGN KEY event_command (event_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_command_endpoint
+ FOREIGN KEY command_endpoint (command_endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_template_choice
+ FOREIGN KEY choice (template_choice_id)
+ REFERENCES icinga_host_template_choice (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_host_inheritance (
+ host_id INT(10) UNSIGNED NOT NULL,
+ parent_host_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (host_id, parent_host_id),
+ UNIQUE KEY unique_order (host_id, weight),
+ CONSTRAINT icinga_host_inheritance_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_inheritance_parent_host
+ FOREIGN KEY host (parent_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_host_field (
+ host_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (host_id, datafield_id),
+ CONSTRAINT icinga_host_field_host
+ FOREIGN KEY host(host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_host_var (
+ host_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ varvalue TEXT DEFAULT NULL,
+ format enum ('string', 'json', 'expression'), -- immer string vorerst
+ checksum VARBINARY(20) DEFAULT NULL,
+ PRIMARY KEY (host_id, varname),
+ INDEX search_idx (varname),
+ INDEX checksum (checksum),
+ CONSTRAINT icinga_host_var_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+ALTER TABLE icinga_host_template_choice
+ ADD CONSTRAINT host_template_choice_required_template
+ FOREIGN KEY required_template (required_template_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+CREATE TABLE icinga_service_set (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(128) NOT NULL,
+ object_type ENUM('object', 'template', 'external_object') NOT NULL,
+ host_id INT(10) UNSIGNED DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE KEY object_key (object_name, host_id),
+ CONSTRAINT icinga_service_set_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_template_choice (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ object_name VARCHAR(64) NOT NULL,
+ description TEXT DEFAULT NULL,
+ min_required SMALLINT UNSIGNED NOT NULL DEFAULT 0,
+ max_allowed SMALLINT UNSIGNED NOT NULL DEFAULT 1,
+ required_template_id INT(10) UNSIGNED DEFAULT NULL,
+ allowed_roles VARCHAR(255) DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY (object_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service (
+ id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ object_type ENUM('object', 'template', 'apply') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ display_name VARCHAR(255) DEFAULT NULL,
+ host_id INT(10) UNSIGNED DEFAULT NULL,
+ service_set_id INT(10) UNSIGNED DEFAULT NULL,
+ check_command_id INT(10) UNSIGNED DEFAULT NULL,
+ max_check_attempts MEDIUMINT UNSIGNED DEFAULT NULL,
+ check_period_id INT(10) UNSIGNED DEFAULT NULL,
+ check_interval VARCHAR(8) DEFAULT NULL,
+ retry_interval VARCHAR(8) DEFAULT NULL,
+ check_timeout SMALLINT UNSIGNED DEFAULT NULL,
+ enable_notifications ENUM('y', 'n') DEFAULT NULL,
+ enable_active_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_passive_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_event_handler ENUM('y', 'n') DEFAULT NULL,
+ enable_flapping ENUM('y', 'n') DEFAULT NULL,
+ enable_perfdata ENUM('y', 'n') DEFAULT NULL,
+ event_command_id INT(10) UNSIGNED DEFAULT NULL,
+ flapping_threshold_high SMALLINT UNSIGNED DEFAULT NULL,
+ flapping_threshold_low SMALLINT UNSIGNED DEFAULT NULL,
+ volatile ENUM('y', 'n') DEFAULT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ command_endpoint_id INT(10) UNSIGNED DEFAULT NULL,
+ notes TEXT DEFAULT NULL,
+ notes_url VARCHAR(255) DEFAULT NULL,
+ action_url VARCHAR(255) DEFAULT NULL,
+ icon_image VARCHAR(255) DEFAULT NULL,
+ icon_image_alt VARCHAR(255) DEFAULT NULL,
+ use_agent ENUM('y', 'n') DEFAULT NULL,
+ apply_for VARCHAR(255) DEFAULT NULL,
+ use_var_overrides ENUM('y', 'n') DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ template_choice_id INT(10) UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE KEY object_key (object_name, host_id),
+ CONSTRAINT icinga_service_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_check_period
+ FOREIGN KEY timeperiod (check_period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_check_command
+ FOREIGN KEY check_command (check_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_event_command
+ FOREIGN KEY event_command (event_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_command_endpoint
+ FOREIGN KEY command_endpoint (command_endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_service_set
+ FOREIGN KEY service_set (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_template_choice
+ FOREIGN KEY choice (template_choice_id)
+ REFERENCES icinga_service_template_choice (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_inheritance (
+ service_id INT(10) UNSIGNED NOT NULL,
+ parent_service_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (service_id, parent_service_id),
+ UNIQUE KEY unique_order (service_id, weight),
+ CONSTRAINT icinga_service_inheritance_service
+ FOREIGN KEY host (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_inheritance_parent_service
+ FOREIGN KEY host (parent_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_var (
+ service_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ varvalue TEXT DEFAULT NULL,
+ format enum ('string', 'json', 'expression'),
+ checksum VARBINARY(20) DEFAULT NULL,
+ PRIMARY KEY (service_id, varname),
+ INDEX search_idx (varname),
+ INDEX checksum (checksum),
+ CONSTRAINT icinga_service_var_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_field (
+ service_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (service_id, datafield_id),
+ CONSTRAINT icinga_service_field_service
+ FOREIGN KEY service(service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+ALTER TABLE icinga_service_template_choice
+ ADD CONSTRAINT service_template_choice_required_template
+ FOREIGN KEY required_template (required_template_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+CREATE TABLE icinga_host_service (
+ host_id INT(10) UNSIGNED NOT NULL,
+ service_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (host_id, service_id),
+ CONSTRAINT icinga_host_service_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_service_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_host_service_blacklist (
+ host_id INT(10) UNSIGNED NOT NULL,
+ service_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (host_id, service_id),
+ CONSTRAINT icinga_host_service_bl_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_service_bl_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_service_set_inheritance (
+ service_set_id INT(10) UNSIGNED NOT NULL,
+ parent_service_set_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (service_set_id, parent_service_set_id),
+ UNIQUE KEY unique_order (service_set_id, weight),
+ CONSTRAINT icinga_service_set_inheritance_set
+ FOREIGN KEY host (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_set_inheritance_parent
+ FOREIGN KEY host (parent_service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_set_var (
+ service_set_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ varvalue TEXT DEFAULT NULL,
+ format ENUM('string', 'expression', 'json') NOT NULL DEFAULT 'string',
+ checksum VARBINARY(20) DEFAULT NULL,
+ PRIMARY KEY (service_set_id, varname),
+ INDEX search_idx (varname),
+ INDEX checksum (checksum),
+ CONSTRAINT icinga_service_set_var_service
+ FOREIGN KEY command (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_hostgroup (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ object_type ENUM('object', 'template', 'external_object') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ display_name VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name),
+ KEY search_idx (display_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+-- TODO: probably useless
+CREATE TABLE icinga_hostgroup_inheritance (
+ hostgroup_id INT(10) UNSIGNED NOT NULL,
+ parent_hostgroup_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (hostgroup_id, parent_hostgroup_id),
+ UNIQUE KEY unique_order (hostgroup_id, weight),
+ CONSTRAINT icinga_hostgroup_inheritance_hostgroup
+ FOREIGN KEY host (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_inheritance_parent_hostgroup
+ FOREIGN KEY host (parent_hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_servicegroup (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ display_name VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name),
+ KEY search_idx (display_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_servicegroup_inheritance (
+ servicegroup_id INT(10) UNSIGNED NOT NULL,
+ parent_servicegroup_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (servicegroup_id, parent_servicegroup_id),
+ UNIQUE KEY unique_order (servicegroup_id, weight),
+ CONSTRAINT icinga_servicegroup_inheritance_servicegroup
+ FOREIGN KEY host (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_inheritance_parent_servicegroup
+ FOREIGN KEY host (parent_servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_servicegroup_service (
+ servicegroup_id INT(10) UNSIGNED NOT NULL,
+ service_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (servicegroup_id, service_id),
+ CONSTRAINT icinga_servicegroup_service_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_service_servicegroup
+ FOREIGN KEY servicegroup (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_servicegroup_service_resolved (
+ servicegroup_id INT(10) UNSIGNED NOT NULL,
+ service_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (servicegroup_id, service_id),
+ CONSTRAINT icinga_servicegroup_service_resolved_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_service_resolved_servicegroup
+ FOREIGN KEY servicegroup (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_hostgroup_host (
+ hostgroup_id INT(10) UNSIGNED NOT NULL,
+ host_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (hostgroup_id, host_id),
+ CONSTRAINT icinga_hostgroup_host_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_host_hostgroup
+ FOREIGN KEY hostgroup (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_hostgroup_host_resolved (
+ hostgroup_id INT(10) UNSIGNED NOT NULL,
+ host_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (hostgroup_id, host_id),
+ CONSTRAINT icinga_hostgroup_host_resolved_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_host_resolved_hostgroup
+ FOREIGN KEY hostgroup (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_hostgroup_parent (
+ hostgroup_id INT(10) UNSIGNED NOT NULL,
+ parent_hostgroup_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (hostgroup_id, parent_hostgroup_id),
+ CONSTRAINT icinga_hostgroup_parent_hostgroup
+ FOREIGN KEY hostgroup (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_parent_parent
+ FOREIGN KEY parent (parent_hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_user (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ display_name VARCHAR(255) DEFAULT NULL,
+ email VARCHAR(255) DEFAULT NULL,
+ pager VARCHAR(255) DEFAULT NULL,
+ enable_notifications ENUM('y', 'n') DEFAULT NULL,
+ period_id INT(10) UNSIGNED DEFAULT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name, zone_id),
+ CONSTRAINT icinga_user_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_period
+ FOREIGN KEY period (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_user_inheritance (
+ user_id INT(10) UNSIGNED NOT NULL,
+ parent_user_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (user_id, parent_user_id),
+ UNIQUE KEY unique_order (user_id, weight),
+ CONSTRAINT icinga_user_inheritance_user
+ FOREIGN KEY host (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_inheritance_parent_user
+ FOREIGN KEY host (parent_user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_user_states_set (
+ user_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'OK',
+ 'Warning',
+ 'Critical',
+ 'Unknown',
+ 'Up',
+ 'Down'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (user_id, property, merge_behaviour),
+ CONSTRAINT icinga_user_states_set_user
+ FOREIGN KEY icinga_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_user_types_set (
+ user_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'DowntimeStart',
+ 'DowntimeEnd',
+ 'DowntimeRemoved',
+ 'Custom',
+ 'Acknowledgement',
+ 'Problem',
+ 'Recovery',
+ 'FlappingStart',
+ 'FlappingEnd'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (user_id, property, merge_behaviour),
+ CONSTRAINT icinga_user_types_set_user
+ FOREIGN KEY icinga_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_user_var (
+ user_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ varvalue TEXT DEFAULT NULL,
+ format ENUM('string', 'json', 'expression') NOT NULL DEFAULT 'string',
+ checksum VARBINARY(20) DEFAULT NULL,
+ PRIMARY KEY (user_id, varname),
+ INDEX search_idx (varname),
+ INDEX checksum (checksum),
+ CONSTRAINT icinga_user_var_user
+ FOREIGN KEY icinga_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_user_field (
+ user_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (user_id, datafield_id),
+ CONSTRAINT icinga_user_field_user
+ FOREIGN KEY user(user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_usergroup (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ object_type ENUM('object', 'template') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ display_name VARCHAR(255) DEFAULT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name),
+ KEY search_idx (display_name),
+ CONSTRAINT icinga_usergroup_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_usergroup_inheritance (
+ usergroup_id INT(10) UNSIGNED NOT NULL,
+ parent_usergroup_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (usergroup_id, parent_usergroup_id),
+ UNIQUE KEY unique_order (usergroup_id, weight),
+ CONSTRAINT icinga_usergroup_inheritance_usergroup
+ FOREIGN KEY usergroup (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_usergroup_inheritance_parent_usergroup
+ FOREIGN KEY usergroup (parent_usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_usergroup_user (
+ usergroup_id INT(10) UNSIGNED NOT NULL,
+ user_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (usergroup_id, user_id),
+ CONSTRAINT icinga_usergroup_user_user
+ FOREIGN KEY icinga_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_usergroup_user_usergroup
+ FOREIGN KEY usergroup (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_usergroup_parent (
+ usergroup_id INT(10) UNSIGNED NOT NULL,
+ parent_usergroup_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (usergroup_id, parent_usergroup_id),
+ CONSTRAINT icinga_usergroup_parent_usergroup
+ FOREIGN KEY usergroup (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_usergroup_parent_parent
+ FOREIGN KEY parent (parent_usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ host_id INT(10) UNSIGNED DEFAULT NULL,
+ service_id INT(10) UNSIGNED DEFAULT NULL,
+ times_begin INT(10) UNSIGNED DEFAULT NULL,
+ times_end INT(10) UNSIGNED DEFAULT NULL,
+ notification_interval INT(10) UNSIGNED DEFAULT NULL,
+ command_id INT(10) UNSIGNED DEFAULT NULL,
+ period_id INT(10) UNSIGNED DEFAULT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ CONSTRAINT icinga_notification_host
+ FOREIGN KEY host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_service
+ FOREIGN KEY service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_command
+ FOREIGN KEY command (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_period
+ FOREIGN KEY period (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification_var (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ varvalue TEXT DEFAULT NULL,
+ format enum ('string', 'json', 'expression'),
+ checksum VARBINARY(20) DEFAULT NULL,
+ PRIMARY KEY (notification_id, varname),
+ INDEX search_idx (varname),
+ INDEX checksum (checksum),
+ CONSTRAINT icinga_notification_var_notification
+ FOREIGN KEY notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification_field (
+ notification_id INT(10) UNSIGNED NOT NULL COMMENT 'Makes only sense for templates',
+ datafield_id INT(10) UNSIGNED NOT NULL,
+ is_required ENUM('y', 'n') NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (notification_id, datafield_id),
+ CONSTRAINT icinga_notification_field_notification
+ FOREIGN KEY notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_field_datafield
+ FOREIGN KEY datafield(datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification_inheritance (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ parent_notification_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (notification_id, parent_notification_id),
+ UNIQUE KEY unique_order (notification_id, weight),
+ CONSTRAINT icinga_notification_inheritance_notification
+ FOREIGN KEY host (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_inheritance_parent_notification
+ FOREIGN KEY host (parent_notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification_states_set (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'OK',
+ 'Warning',
+ 'Critical',
+ 'Unknown',
+ 'Up',
+ 'Down'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (notification_id, property, merge_behaviour),
+ CONSTRAINT icinga_notification_states_set_notification
+ FOREIGN KEY icinga_notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_notification_types_set (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'DowntimeStart',
+ 'DowntimeEnd',
+ 'DowntimeRemoved',
+ 'Custom',
+ 'Acknowledgement',
+ 'Problem',
+ 'Recovery',
+ 'FlappingStart',
+ 'FlappingEnd'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (notification_id, property, merge_behaviour),
+ CONSTRAINT icinga_notification_types_set_notification
+ FOREIGN KEY icinga_notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_notification_user (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ user_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (notification_id, user_id),
+ CONSTRAINT icinga_notification_user_user
+ FOREIGN KEY user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_user_notification
+ FOREIGN KEY notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification_usergroup (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ usergroup_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (notification_id, usergroup_id),
+ CONSTRAINT icinga_notification_usergroup_usergroup
+ FOREIGN KEY usergroup (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_usergroup_notification
+ FOREIGN KEY notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE import_source (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ source_name VARCHAR(64) NOT NULL,
+ key_column VARCHAR(64) NOT NULL,
+ provider_class VARCHAR(128) NOT NULL,
+ import_state ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+ ) NOT NULL DEFAULT 'unknown',
+ last_error_message TEXT DEFAULT NULL,
+ last_attempt TIMESTAMP NULL DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX source_name (source_name),
+ INDEX search_idx (key_column)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE import_source_setting (
+ source_id INT(10) UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT NOT NULL,
+ PRIMARY KEY (source_id, setting_name),
+ CONSTRAINT import_source_settings_source
+ FOREIGN KEY source (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE import_row_modifier (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ source_id INT(10) UNSIGNED NOT NULL,
+ property_name VARCHAR(255) NOT NULL,
+ target_property VARCHAR(255) DEFAULT NULL,
+ provider_class VARCHAR(128) NOT NULL,
+ priority SMALLINT UNSIGNED NOT NULL,
+ description TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ KEY search_idx (property_name),
+ CONSTRAINT row_modifier_import_source
+ FOREIGN KEY source (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE import_row_modifier_setting (
+ row_modifier_id INT UNSIGNED NOT NULL,
+ setting_name VARCHAR(64) NOT NULL,
+ setting_value TEXT DEFAULT NULL,
+ PRIMARY KEY (row_modifier_id, setting_name),
+ CONSTRAINT row_modifier_settings
+ FOREIGN KEY row_modifier (row_modifier_id)
+ REFERENCES import_row_modifier (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE imported_rowset (
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (checksum)
+) ENGINE=InnoDB;
+
+CREATE TABLE import_run (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ source_id INT(10) UNSIGNED NOT NULL,
+ rowset_checksum VARBINARY(20) DEFAULT NULL,
+ start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ end_time TIMESTAMP NULL DEFAULT NULL,
+ succeeded ENUM('y', 'n') DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT import_run_source
+ FOREIGN KEY import_source (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE RESTRICT,
+ CONSTRAINT import_run_rowset
+ FOREIGN KEY rowset (rowset_checksum)
+ REFERENCES imported_rowset (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE imported_row (
+ checksum VARBINARY(20) NOT NULL COMMENT 'sha1(object_name;property_checksum;...)',
+ object_name VARCHAR(255) NOT NULL,
+ PRIMARY KEY (checksum)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE imported_rowset_row (
+ rowset_checksum VARBINARY(20) NOT NULL,
+ row_checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (rowset_checksum, row_checksum),
+ CONSTRAINT imported_rowset_row_rowset
+ FOREIGN KEY rowset_row_rowset (rowset_checksum)
+ REFERENCES imported_rowset (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT imported_rowset_row_row
+ FOREIGN KEY rowset_row_rowset (row_checksum)
+ REFERENCES imported_row (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE imported_property (
+ checksum VARBINARY(20) NOT NULL,
+ property_name VARCHAR(64) NOT NULL,
+ property_value MEDIUMTEXT NOT NULL,
+ format enum ('string', 'expression', 'json'),
+ PRIMARY KEY (checksum),
+ KEY search_idx (property_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE imported_row_property (
+ row_checksum VARBINARY(20) NOT NULL,
+ property_checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (row_checksum, property_checksum),
+ CONSTRAINT imported_row_property_row
+ FOREIGN KEY row_checksum (row_checksum)
+ REFERENCES imported_row (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT imported_row_property_property
+ FOREIGN KEY property_checksum (property_checksum)
+ REFERENCES imported_property (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE sync_rule (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ rule_name VARCHAR(255) NOT NULL,
+ object_type enum(
+ 'host',
+ 'service',
+ 'command',
+ 'user',
+ 'hostgroup',
+ 'servicegroup',
+ 'usergroup',
+ 'datalistEntry',
+ 'endpoint',
+ 'zone',
+ 'timePeriod',
+ 'serviceSet',
+ 'scheduledDowntime',
+ 'notification',
+ 'dependency'
+ ) NOT NULL,
+ update_policy ENUM('merge', 'override', 'ignore', 'update-only') NOT NULL,
+ purge_existing ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ purge_action ENUM('delete', 'disable') NULL DEFAULT NULL,
+ filter_expression TEXT DEFAULT NULL,
+ sync_state ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+ ) NOT NULL DEFAULT 'unknown',
+ last_error_message TEXT DEFAULT NULL,
+ last_attempt TIMESTAMP NULL DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX rule_name (rule_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE sync_property (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ rule_id INT(10) UNSIGNED NOT NULL,
+ source_id INT(10) UNSIGNED NOT NULL,
+ source_expression VARCHAR(255) NOT NULL,
+ destination_field VARCHAR(64),
+ priority SMALLINT UNSIGNED NOT NULL,
+ filter_expression TEXT DEFAULT NULL,
+ merge_policy ENUM('override', 'merge') NOT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT sync_property_rule
+ FOREIGN KEY sync_rule (rule_id)
+ REFERENCES sync_rule (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT sync_property_source
+ FOREIGN KEY import_source (source_id)
+ REFERENCES import_source (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE sync_run (
+ id BIGINT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ rule_id INT(10) UNSIGNED DEFAULT NULL,
+ rule_name VARCHAR(255) NOT NULL,
+ start_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ duration_ms INT(10) UNSIGNED DEFAULT NULL,
+ objects_deleted INT(10) UNSIGNED DEFAULT 0,
+ objects_created INT(10) UNSIGNED DEFAULT 0,
+ objects_modified INT(10) UNSIGNED DEFAULT 0,
+ last_former_activity VARBINARY(20) DEFAULT NULL,
+ last_related_activity VARBINARY(20) DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT sync_run_rule
+ FOREIGN KEY sync_rule (rule_id)
+ REFERENCES sync_rule (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_var (
+ checksum VARBINARY(20) NOT NULL,
+ rendered_checksum VARBINARY(20) NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ varvalue TEXT NOT NULL,
+ rendered TEXT NOT NULL,
+ PRIMARY KEY (checksum),
+ INDEX search_idx (varname)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_flat_var (
+ var_checksum VARBINARY(20) NOT NULL,
+ flatname_checksum VARBINARY(20) NOT NULL,
+ flatname VARCHAR(512) NOT NULL COLLATE utf8_bin,
+ flatvalue TEXT NOT NULL,
+ PRIMARY KEY (var_checksum, flatname_checksum),
+ INDEX search_varname (flatname (191)),
+ INDEX search_varvalue (flatvalue (128)),
+ CONSTRAINT flat_var_var
+ FOREIGN KEY checksum (var_checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_command_resolved_var (
+ command_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (command_id, checksum),
+ INDEX search_varname (varname),
+ CONSTRAINT command_resolved_var_command
+ FOREIGN KEY command (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT command_resolved_var_checksum
+ FOREIGN KEY checksum (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_host_resolved_var (
+ host_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (host_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY host_resolved_var_host (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY host_resolved_var_checksum (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_notification_resolved_var (
+ notification_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (notification_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY notification_resolved_var_notification (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY notification_resolved_var_checksum (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_set_resolved_var (
+ service_set_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (service_set_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY service_set_resolved_var_service_set (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY service_set_resolved_var_checksum(checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_service_resolved_var (
+ service_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (service_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY service_resolve_var_service (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY service_resolve_var_checksum(checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_user_resolved_var (
+ user_id INT(10) UNSIGNED NOT NULL,
+ varname VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ checksum VARBINARY(20) NOT NULL,
+ PRIMARY KEY (user_id, checksum),
+ INDEX search_varname (varname),
+ FOREIGN KEY user_resolve_var_user (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY user_resolve_var_checksum(checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_dependency (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ parent_host_id INT(10) UNSIGNED DEFAULT NULL,
+ parent_host_var VARCHAR(128) DEFAULT NULL,
+ parent_service_id INT(10) UNSIGNED DEFAULT NULL,
+ child_host_id INT(10) UNSIGNED DEFAULT NULL,
+ child_service_id INT(10) UNSIGNED DEFAULT NULL,
+ disable_checks ENUM('y', 'n') DEFAULT NULL,
+ disable_notifications ENUM('y', 'n') DEFAULT NULL,
+ ignore_soft_states ENUM('y', 'n') DEFAULT NULL,
+ period_id INT(10) UNSIGNED DEFAULT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ parent_service_by_name VARCHAR(255) DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ CONSTRAINT icinga_dependency_parent_host
+ FOREIGN KEY parent_host (parent_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_parent_service
+ FOREIGN KEY parent_service (parent_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_child_host
+ FOREIGN KEY child_host (child_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_child_service
+ FOREIGN KEY child_service (child_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_period
+ FOREIGN KEY period (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_dependency_inheritance (
+ dependency_id INT(10) UNSIGNED NOT NULL,
+ parent_dependency_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (dependency_id, parent_dependency_id),
+ UNIQUE KEY unique_order (dependency_id, weight),
+ CONSTRAINT icinga_dependency_inheritance_dependency
+ FOREIGN KEY dependency (dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_inheritance_parent_dependency
+ FOREIGN KEY parent_dependency (parent_dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_dependency_states_set (
+ dependency_id INT(10) UNSIGNED NOT NULL,
+ property ENUM(
+ 'OK',
+ 'Warning',
+ 'Critical',
+ 'Unknown',
+ 'Up',
+ 'Down'
+ ) NOT NULL,
+ merge_behaviour ENUM('override', 'extend', 'blacklist') NOT NULL DEFAULT 'override'
+ COMMENT 'override: = [], extend: += [], blacklist: -= []',
+ PRIMARY KEY (dependency_id, property, merge_behaviour),
+ CONSTRAINT icinga_dependency_states_set_dependency
+ FOREIGN KEY icinga_dependency (dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB;
+
+CREATE TABLE icinga_timeperiod_include (
+ timeperiod_id INT(10) UNSIGNED NOT NULL,
+ include_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (timeperiod_id, include_id),
+ CONSTRAINT icinga_timeperiod_include
+ FOREIGN KEY timeperiod (include_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT,
+ CONSTRAINT icinga_timeperiod_include_timeperiod
+ FOREIGN KEY include (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE icinga_timeperiod_exclude (
+ timeperiod_id INT(10) UNSIGNED NOT NULL,
+ exclude_id INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (timeperiod_id, exclude_id),
+ CONSTRAINT icinga_timeperiod_exclude
+ FOREIGN KEY timeperiod (exclude_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT,
+ CONSTRAINT icinga_timeperiod_exclude_timeperiod
+ FOREIGN KEY exclude (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE icinga_scheduled_downtime (
+ id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ uuid VARBINARY(16) NOT NULL,
+ object_name VARCHAR(255) NOT NULL,
+ zone_id INT(10) UNSIGNED DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') NOT NULL,
+ disabled ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ author VARCHAR(255) DEFAULT NULL,
+ comment TEXT DEFAULT NULL,
+ fixed ENUM('y', 'n') DEFAULT NULL,
+ duration INT(10) UNSIGNED DEFAULT NULL,
+ with_services ENUM('y', 'n') NULL DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE INDEX uuid (uuid),
+ UNIQUE INDEX object_name (object_name),
+ CONSTRAINT icinga_scheduled_downtime_zone
+ FOREIGN KEY zone (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_scheduled_downtime_inheritance (
+ scheduled_downtime_id INT(10) UNSIGNED NOT NULL,
+ parent_scheduled_downtime_id INT(10) UNSIGNED NOT NULL,
+ weight MEDIUMINT UNSIGNED DEFAULT NULL,
+ PRIMARY KEY (scheduled_downtime_id, parent_scheduled_downtime_id),
+ UNIQUE KEY unique_order (scheduled_downtime_id, weight),
+ CONSTRAINT icinga_scheduled_downtime_inheritance_downtime
+ FOREIGN KEY host (scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_scheduled_downtime_inheritance_parent_downtime
+ FOREIGN KEY host (parent_scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE icinga_scheduled_downtime_range (
+ scheduled_downtime_id INT(10) UNSIGNED AUTO_INCREMENT NOT NULL,
+ range_key VARCHAR(255) NOT NULL COMMENT 'monday, ...',
+ range_value VARCHAR(255) NOT NULL COMMENT '00:00-24:00, ...',
+ range_type ENUM('include', 'exclude') NOT NULL DEFAULT 'include'
+ COMMENT 'include -> ranges {}, exclude ranges_ignore {} - not yet',
+ merge_behaviour ENUM('set', 'add', 'substract') NOT NULL DEFAULT 'set'
+ COMMENT 'set -> = {}, add -> += {}, substract -> -= {}',
+ PRIMARY KEY (scheduled_downtime_id, range_type, range_key),
+ CONSTRAINT icinga_scheduled_downtime_range_downtime
+ FOREIGN KEY scheduled_downtime (scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_branch (
+ uuid VARBINARY(16) NOT NULL,
+ owner VARCHAR(255) NOT NULL,
+ branch_name VARCHAR(255) NOT NULL,
+ description TEXT DEFAULT NULL,
+ ts_merge_request BIGINT DEFAULT NULL,
+ PRIMARY KEY(uuid),
+ UNIQUE KEY (branch_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE director_branch_activity (
+ timestamp_ns BIGINT(20) NOT NULL,
+ object_uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ action ENUM ('create', 'modify', 'delete') NOT NULL,
+ object_table VARCHAR(64) NOT NULL,
+ author VARCHAR(255) NOT NULL,
+ former_properties LONGTEXT NOT NULL, -- json-encoded
+ modified_properties LONGTEXT NOT NULL,
+ PRIMARY KEY (timestamp_ns),
+ INDEX object_uuid (object_uuid),
+ INDEX branch_uuid (branch_uuid),
+ CONSTRAINT branch_activity_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_host (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ address VARCHAR(255) DEFAULT NULL,
+ address6 VARCHAR(45) DEFAULT NULL,
+ check_command VARCHAR(255) DEFAULT NULL,
+ max_check_attempts MEDIUMINT UNSIGNED DEFAULT NULL,
+ check_period VARCHAR(255) DEFAULT NULL,
+ check_interval VARCHAR(8) DEFAULT NULL,
+ retry_interval VARCHAR(8) DEFAULT NULL,
+ check_timeout SMALLINT UNSIGNED DEFAULT NULL,
+ enable_notifications ENUM('y', 'n') DEFAULT NULL,
+ enable_active_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_passive_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_event_handler ENUM('y', 'n') DEFAULT NULL,
+ enable_flapping ENUM('y', 'n') DEFAULT NULL,
+ enable_perfdata ENUM('y', 'n') DEFAULT NULL,
+ event_command VARCHAR(255) DEFAULT NULL,
+ flapping_threshold_high SMALLINT UNSIGNED DEFAULT NULL,
+ flapping_threshold_low SMALLINT UNSIGNED DEFAULT NULL,
+ volatile ENUM('y', 'n') DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ command_endpoint VARCHAR(255) DEFAULT NULL,
+ notes TEXT DEFAULT NULL,
+ notes_url VARCHAR(255) DEFAULT NULL,
+ action_url VARCHAR(255) DEFAULT NULL,
+ icon_image VARCHAR(255) DEFAULT NULL,
+ icon_image_alt VARCHAR(255) DEFAULT NULL,
+ has_agent ENUM('y', 'n') DEFAULT NULL,
+ master_should_connect ENUM('y', 'n') DEFAULT NULL,
+ accept_config ENUM('y', 'n') DEFAULT NULL,
+ custom_endpoint_name VARCHAR(255) DEFAULT NULL,
+ api_key VARCHAR(40) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ `groups` TEXT DEFAULT NULL,
+ vars MEDIUMTEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_host_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_hostgroup (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_hostgroup_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_servicegroup (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_servicegroup_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_usergroup (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_usergroup_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_user (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ email VARCHAR(255) DEFAULT NULL,
+ pager VARCHAR(255) DEFAULT NULL,
+ enable_notifications ENUM('y', 'n') DEFAULT NULL,
+ period VARCHAR(255) DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ states TEXT DEFAULT NULL,
+ types TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ `groups` TEXT DEFAULT NULL,
+ vars MEDIUMTEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_user_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_zone (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ parent VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ is_global ENUM('y', 'n') DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_zone_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_timeperiod (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ update_method VARCHAR(64) DEFAULT NULL COMMENT 'Usually LegacyTimePeriod',
+ zone VARCHAR(255) DEFAULT NULL,
+ prefer_includes ENUM('y', 'n') DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ ranges TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_timeperiod_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_command (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ methods_execute VARCHAR(64) DEFAULT NULL,
+ command TEXT DEFAULT NULL,
+ is_string ENUM('y', 'n') NULL,
+ timeout SMALLINT UNSIGNED DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ arguments TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_command_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_apiuser (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ password VARCHAR(255) DEFAULT NULL,
+ client_dn VARCHAR(64) DEFAULT NULL,
+ permissions TEXT DEFAULT NULL COMMENT 'JSON-encoded permissions',
+
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_apiuser_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_endpoint (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ host VARCHAR(255) DEFAULT NULL,
+ port SMALLINT UNSIGNED DEFAULT NULL,
+ log_duration VARCHAR(32) DEFAULT NULL,
+ apiuser VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_endpoint_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_service (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ display_name VARCHAR(255) DEFAULT NULL,
+ host VARCHAR(255) DEFAULT NULL,
+ service_set VARCHAR(255) DEFAULT NULL,
+ check_command VARCHAR(255) DEFAULT NULL,
+ max_check_attempts MEDIUMINT UNSIGNED DEFAULT NULL,
+ check_period VARCHAR(255) DEFAULT NULL,
+ check_interval VARCHAR(8) DEFAULT NULL,
+ retry_interval VARCHAR(8) DEFAULT NULL,
+ check_timeout SMALLINT UNSIGNED DEFAULT NULL,
+ enable_notifications ENUM('y', 'n') DEFAULT NULL,
+ enable_active_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_passive_checks ENUM('y', 'n') DEFAULT NULL,
+ enable_event_handler ENUM('y', 'n') DEFAULT NULL,
+ enable_flapping ENUM('y', 'n') DEFAULT NULL,
+ enable_perfdata ENUM('y', 'n') DEFAULT NULL,
+ event_command VARCHAR(255) DEFAULT NULL,
+ flapping_threshold_high SMALLINT UNSIGNED DEFAULT NULL,
+ flapping_threshold_low SMALLINT UNSIGNED DEFAULT NULL,
+ volatile ENUM('y', 'n') DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ command_endpoint VARCHAR(255) DEFAULT NULL,
+ notes TEXT DEFAULT NULL,
+ notes_url VARCHAR(255) DEFAULT NULL,
+ action_url VARCHAR(255) DEFAULT NULL,
+ icon_image VARCHAR(255) DEFAULT NULL,
+ icon_image_alt VARCHAR(255) DEFAULT NULL,
+ use_agent ENUM('y', 'n') DEFAULT NULL,
+ apply_for VARCHAR(255) DEFAULT NULL,
+ use_var_overrides ENUM('y', 'n') DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ -- template_choice VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ `groups` TEXT DEFAULT NULL,
+ vars MEDIUMTEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ INDEX search_object_name (object_name),
+ INDEX search_display_name (display_name),
+ CONSTRAINT icinga_service_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_service_set (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(128) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'external_object') DEFAULT NULL,
+ host VARCHAR(255) DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_service_set_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_notification (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ host VARCHAR(255) DEFAULT NULL,
+ service VARCHAR(255) DEFAULT NULL,
+ times_begin INT(10) UNSIGNED DEFAULT NULL,
+ times_end INT(10) UNSIGNED DEFAULT NULL,
+ notification_interval INT(10) UNSIGNED DEFAULT NULL,
+ command VARCHAR(255) DEFAULT NULL,
+ period VARCHAR(255) DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+
+ states TEXT DEFAULT NULL,
+ types TEXT DEFAULT NULL,
+ users TEXT DEFAULT NULL,
+ usergroups TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ vars MEDIUMTEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_notification_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_scheduled_downtime (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ author VARCHAR(255) DEFAULT NULL,
+ comment TEXT DEFAULT NULL,
+ fixed ENUM('y', 'n') DEFAULT NULL,
+ duration INT(10) UNSIGNED DEFAULT NULL,
+ with_services ENUM('y', 'n') NULL DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ ranges TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_scheduled_downtime_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE branched_icinga_dependency (
+ uuid VARBINARY(16) NOT NULL,
+ branch_uuid VARBINARY(16) NOT NULL,
+ branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n',
+ branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n',
+
+ object_name VARCHAR(255) DEFAULT NULL,
+ object_type ENUM('object', 'template', 'apply') DEFAULT NULL,
+ disabled ENUM('y', 'n') DEFAULT NULL,
+ apply_to ENUM('host', 'service') DEFAULT NULL,
+ parent_host VARCHAR(255) DEFAULT NULL,
+ parent_host_var VARCHAR(128) DEFAULT NULL,
+ parent_service VARCHAR(255) DEFAULT NULL,
+ child_host VARCHAR(255) DEFAULT NULL,
+ child_service VARCHAR(255) DEFAULT NULL,
+ disable_checks ENUM('y', 'n') DEFAULT NULL,
+ disable_notifications ENUM('y', 'n') DEFAULT NULL,
+ ignore_soft_states ENUM('y', 'n') DEFAULT NULL,
+ period VARCHAR(255) DEFAULT NULL,
+ zone VARCHAR(255) DEFAULT NULL,
+ assign_filter TEXT DEFAULT NULL,
+ parent_service_by_name VARCHAR(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ UNIQUE INDEX branch_object_name (branch_uuid, object_name),
+ INDEX search_object_name (object_name),
+ CONSTRAINT icinga_dependency_branch
+ FOREIGN KEY branch (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (182, NOW());
diff --git a/schema/pgsql-legacy-changes/upgrade-10.sql b/schema/pgsql-legacy-changes/upgrade-10.sql
new file mode 100644
index 0000000..4fd3cc1
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-10.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_usergroup_inheritance (
+ usergroup_id integer NOT NULL,
+ parent_usergroup_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (usergroup_id, parent_usergroup_id),
+ CONSTRAINT icinga_usergroup_inheritance_usergroup
+ FOREIGN KEY (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_usergroup_inheritance_parent_usergroup
+ FOREIGN KEY (parent_usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX usergroup_inheritance_unique_order ON icinga_usergroup_inheritance (usergroup_id, weight);
+CREATE INDEX usergroup_inheritance_usergroup ON icinga_usergroup_inheritance (usergroup_id);
+CREATE INDEX usergroup_inheritance_usergroup_parent ON icinga_usergroup_inheritance (parent_usergroup_id);
diff --git a/schema/pgsql-legacy-changes/upgrade-11.sql b/schema/pgsql-legacy-changes/upgrade-11.sql
new file mode 100644
index 0000000..fce4e61
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-11.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_endpoint_inheritance (
+ endpoint_id integer NOT NULL,
+ parent_endpoint_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (endpoint_id, parent_endpoint_id),
+ CONSTRAINT icinga_endpoint_inheritance_endpoint
+ FOREIGN KEY (endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_endpoint_inheritance_parent_endpoint
+ FOREIGN KEY (parent_endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX endpoint_inheritance_unique_order ON icinga_endpoint_inheritance (endpoint_id, weight);
+CREATE INDEX endpoint_inheritance_endpoint ON icinga_endpoint_inheritance (endpoint_id);
+CREATE INDEX endpoint_inheritance_endpoint_parent ON icinga_endpoint_inheritance (parent_endpoint_id);
diff --git a/schema/pgsql-legacy-changes/upgrade-2.sql b/schema/pgsql-legacy-changes/upgrade-2.sql
new file mode 100644
index 0000000..9b73e52
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-2.sql
@@ -0,0 +1,2 @@
+ALTER TABLE icinga_zone ADD is_global enum_boolean NOT NULL DEFAULT 'n';
+
diff --git a/schema/pgsql-legacy-changes/upgrade-3.sql b/schema/pgsql-legacy-changes/upgrade-3.sql
new file mode 100644
index 0000000..77cb345
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-3.sql
@@ -0,0 +1,21 @@
+CREATE TABLE icinga_service_inheritance (
+ service_id integer NOT NULL,
+ parent_service_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (service_id, parent_service_id),
+ CONSTRAINT icinga_service_inheritance_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_inheritance_parent_service
+ FOREIGN KEY (parent_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_inheritance_unique_order ON icinga_service_inheritance (service_id, weight);
+CREATE INDEX service_inheritance_service ON icinga_service_inheritance (service_id);
+CREATE INDEX service_inheritance_service_parent ON icinga_service_inheritance (parent_service_id);
+
diff --git a/schema/pgsql-legacy-changes/upgrade-4.sql b/schema/pgsql-legacy-changes/upgrade-4.sql
new file mode 100644
index 0000000..7e81f1a
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-4.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_user_inheritance (
+ user_id integer NOT NULL,
+ parent_user_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (user_id, parent_user_id),
+ CONSTRAINT icinga_user_inheritance_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_inheritance_parent_user
+ FOREIGN KEY (parent_user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX user_inheritance_unique_order ON icinga_user_inheritance (user_id, weight);
+CREATE INDEX user_inheritance_user ON icinga_user_inheritance (user_id);
+CREATE INDEX user_inheritance_user_parent ON icinga_user_inheritance (parent_user_id); \ No newline at end of file
diff --git a/schema/pgsql-legacy-changes/upgrade-5.sql b/schema/pgsql-legacy-changes/upgrade-5.sql
new file mode 100644
index 0000000..2671177
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-5.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_timeperiod_inheritance (
+ timeperiod_id integer NOT NULL,
+ parent_timeperiod_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (timeperiod_id, parent_timeperiod_id),
+ CONSTRAINT icinga_timeperiod_inheritance_timeperiod
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_timeperiod_inheritance_parent_timeperiod
+ FOREIGN KEY (parent_timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX timeperiod_inheritance_unique_order ON icinga_timeperiod_inheritance (timeperiod_id, weight);
+CREATE INDEX timeperiod_inheritance_timeperiod ON icinga_timeperiod_inheritance (timeperiod_id);
+CREATE INDEX timeperiod_inheritance_timeperiod_parent ON icinga_timeperiod_inheritance (parent_timeperiod_id); \ No newline at end of file
diff --git a/schema/pgsql-legacy-changes/upgrade-6.sql b/schema/pgsql-legacy-changes/upgrade-6.sql
new file mode 100644
index 0000000..2bc32a8
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-6.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_hostgroup_inheritance (
+ hostgroup_id integer NOT NULL,
+ parent_hostgroup_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (hostgroup_id, parent_hostgroup_id),
+ CONSTRAINT icinga_hostgroup_inheritance_hostgroup
+ FOREIGN KEY (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_inheritance_parent_hostgroup
+ FOREIGN KEY (parent_hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX hostgroup_inheritance_unique_order ON icinga_hostgroup_inheritance (hostgroup_id, weight);
+CREATE INDEX hostgroup_inheritance_hostgroup ON icinga_hostgroup_inheritance (hostgroup_id);
+CREATE INDEX hostgroup_inheritance_hostgroup_parent ON icinga_hostgroup_inheritance (parent_hostgroup_id); \ No newline at end of file
diff --git a/schema/pgsql-legacy-changes/upgrade-7.sql b/schema/pgsql-legacy-changes/upgrade-7.sql
new file mode 100644
index 0000000..0d781f6
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-7.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_servicegroup_inheritance (
+ servicegroup_id integer NOT NULL,
+ parent_servicegroup_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (servicegroup_id, parent_servicegroup_id),
+ CONSTRAINT icinga_servicegroup_inheritance_servicegroup
+ FOREIGN KEY (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_inheritance_parent_servicegroup
+ FOREIGN KEY (parent_servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX servicegroup_inheritance_unique_order ON icinga_servicegroup_inheritance (servicegroup_id, weight);
+CREATE INDEX servicegroup_inheritance_servicegroup ON icinga_servicegroup_inheritance (servicegroup_id);
+CREATE INDEX servicegroup_inheritance_servicegroup_parent ON icinga_servicegroup_inheritance (parent_servicegroup_id);
diff --git a/schema/pgsql-legacy-changes/upgrade-8.sql b/schema/pgsql-legacy-changes/upgrade-8.sql
new file mode 100644
index 0000000..61a9abe
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-8.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_command_inheritance (
+ command_id integer NOT NULL,
+ parent_command_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (command_id, parent_command_id),
+ CONSTRAINT icinga_command_inheritance_command
+ FOREIGN KEY (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_command_inheritance_parent_command
+ FOREIGN KEY (parent_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX command_inheritance_unique_order ON icinga_command_inheritance (command_id, weight);
+CREATE INDEX command_inheritance_command ON icinga_command_inheritance (command_id);
+CREATE INDEX command_inheritance_command_parent ON icinga_command_inheritance (parent_command_id);
diff --git a/schema/pgsql-legacy-changes/upgrade-9.sql b/schema/pgsql-legacy-changes/upgrade-9.sql
new file mode 100644
index 0000000..401a864
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade-9.sql
@@ -0,0 +1,20 @@
+CREATE TABLE icinga_zone_inheritance (
+ zone_id integer NOT NULL,
+ parent_zone_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (zone_id, parent_zone_id),
+ CONSTRAINT icinga_zone_inheritance_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_zone_inheritance_parent_zone
+ FOREIGN KEY (parent_zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX zone_inheritance_unique_order ON icinga_zone_inheritance (zone_id, weight);
+CREATE INDEX zone_inheritance_zone ON icinga_zone_inheritance (zone_id);
+CREATE INDEX zone_inheritance_zone_parent ON icinga_zone_inheritance (parent_zone_id);
diff --git a/schema/pgsql-legacy-changes/upgrade_1.sql b/schema/pgsql-legacy-changes/upgrade_1.sql
new file mode 100644
index 0000000..11921c7
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade_1.sql
@@ -0,0 +1,13 @@
+ALTER TABLE icinga_hostgroup_parent DROP CONSTRAINT icinga_hostgroup_parent_parent;
+ALTER TABLE icinga_hostgroup_parent ADD CONSTRAINT icinga_hostgroup_parent_parent
+ FOREIGN KEY (parent_hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_usergroup_parent DROP CONSTRAINT icinga_usergroup_parent_parent;
+ALTER TABLE icinga_usergroup_parent ADD CONSTRAINT icinga_usergroup_parent_parent
+ FOREIGN KEY (parent_usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE; \ No newline at end of file
diff --git a/schema/pgsql-legacy-changes/upgrade_21.sql b/schema/pgsql-legacy-changes/upgrade_21.sql
new file mode 100644
index 0000000..1a4fc5a
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade_21.sql
@@ -0,0 +1,17 @@
+DROP TABLE director_datalist_value;
+
+CREATE TABLE director_datalist_entry (
+ list_id integer NOT NULL,
+ entry_name character varying(255) DEFAULT NULL,
+ entry_value text DEFAULT NULL,
+ format enum_property_format,
+ PRIMARY KEY (list_id, entry_name),
+ CONSTRAINT director_datalist_entry_datalist
+ FOREIGN KEY (list_id)
+ REFERENCES director_datalist (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX datalist_entry_datalist ON director_datalist_entry (list_id);
+
diff --git a/schema/pgsql-legacy-changes/upgrade_22.sql b/schema/pgsql-legacy-changes/upgrade_22.sql
new file mode 100644
index 0000000..d7aaf51
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade_22.sql
@@ -0,0 +1,55 @@
+CREATE TABLE icinga_host_field (
+ host_id integer NOT NULL,
+ fieldname character varying(64) NOT NULL,
+ caption character varying(255) NOT NULL,
+ datatype_id integer NOT NULL,
+-- datatype_param? multiple ones?
+ default_value text DEFAULT NULL,
+ format enum_property_format,
+ PRIMARY KEY (host_id, fieldname),
+ CONSTRAINT icinga_host_field_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_field_datatype
+ FOREIGN KEY (datatype_id)
+ REFERENCES director_datatype (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX host_field_key ON icinga_host_field (host_id, fieldname);
+CREATE INDEX host_field_search_idx ON icinga_host_field (fieldname);
+CREATE INDEX host_field_host ON icinga_host_field (host_id);
+CREATE INDEX host_field_datatype ON icinga_host_field (datatype);
+
+COMMENT ON COLUMN icinga_host_field.host_id IS 'Makes only sense for templates';
+
+
+CREATE TABLE icinga_service_field (
+ service_id integer NOT NULL,
+ fieldname character varying(64) NOT NULL,
+ caption character varying(255) NOT NULL,
+ datatype_id integer NOT NULL,
+-- datatype_param? multiple ones?
+ default_value text DEFAULT NULL,
+ format enum_property_format,
+ PRIMARY KEY (service_id, fieldname),
+ CONSTRAINT icinga_service_field_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_field_datatype
+ FOREIGN KEY datatype (datatype_id)
+ REFERENCES director_datatype (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_field_key ON icinga_service_field (service_id, fieldname);
+CREATE INDEX service_field_search_idx ON icinga_service_field (fieldname);
+CREATE INDEX service_field_service ON icinga_service_field (service_id);
+CREATE INDEX service_field_datatype ON icinga_service_field (datatype);
+COMMENT ON COLUMN icinga_service_field.service_id IS 'Makes only sense for templates';
diff --git a/schema/pgsql-legacy-changes/upgrade_23.sql b/schema/pgsql-legacy-changes/upgrade_23.sql
new file mode 100644
index 0000000..9d02ca6
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade_23.sql
@@ -0,0 +1,60 @@
+DROP TABLE director_datatype;
+
+CREATE TABLE director_datafield (
+ id serial,
+ varname character varying(255) NOT NULL,
+ caption character varying(255) NOT NULL,
+ description text DEFAULT NULL,
+ datatype character varying(255) NOT NULL,
+-- datatype_param? multiple ones?
+ format enum_property_format,
+ PRIMARY KEY (id)
+);
+
+DROP TABLE icinga_host_field;
+
+CREATE TABLE icinga_host_field (
+ host_id integer NOT NULL,
+ datafield_id integer NOT NULL,
+ is_required enum_boolean DEFAULT NULL,
+ PRIMARY KEY (host_id, datafield_id),
+ CONSTRAINT icinga_host_field_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_field_datafield
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX host_field_key ON icinga_host_field (host_id, datafield_id);
+CREATE INDEX host_field_host ON icinga_host_field (host_id);
+CREATE INDEX host_field_datafield ON icinga_host_field (datafield_id);
+COMMENT ON COLUMN icinga_host_field.host_id IS 'Makes only sense for templates';
+
+DROP TABLE icinga_service_field;
+
+CREATE TABLE icinga_service_field (
+ service_id integer NOT NULL,
+ datafield_id integer NOT NULL,
+ is_required enum_boolean DEFAULT NULL,
+ PRIMARY KEY (service_id, datafield_id),
+ CONSTRAINT icinga_service_field_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_field_datafield
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_field_key ON icinga_service_field (service_id, datafield_id);
+CREATE INDEX service_field_service ON icinga_service_field (service_id);
+CREATE INDEX service_field_datafield ON icinga_service_field (datafield_id);
+COMMENT ON COLUMN icinga_service_field.service_id IS 'Makes only sense for templates';
diff --git a/schema/pgsql-legacy-changes/upgrade_34.sql b/schema/pgsql-legacy-changes/upgrade_34.sql
new file mode 100644
index 0000000..c8e57ab
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade_34.sql
@@ -0,0 +1,189 @@
+ALTER TABLE director_generated_file ALTER COLUMN content SET DEFAULT NULL;
+ALTER TABLE icinga_host_field ALTER COLUMN is_required SET NOT NULL;
+ALTER TABLE icinga_service_field ALTER COLUMN is_required SET NOT NULL;
+
+CREATE TABLE import_source (
+ id serial,
+ source_name character varying(64) NOT NULL,
+ key_column character varying(64) NOT NULL,
+ provider_class character varying(72) NOT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE INDEX import_source_search_idx ON import_source (key_column);
+
+
+CREATE TABLE import_source_setting (
+ source_id integer NOT NULL,
+ setting_name character varying(64) NOT NULL,
+ setting_value text NOT NULL,
+ PRIMARY KEY (source_id, setting_name),
+ CONSTRAINT import_source_settings_source
+ FOREIGN KEY (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX import_source_setting_source ON import_source_setting (source_id);
+
+
+CREATE TABLE imported_rowset (
+ checksum bytea CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (checksum)
+);
+
+
+CREATE TABLE import_run (
+ id serial,
+ source_id integer NOT NULL,
+ rowset_checksum bytea CHECK(LENGTH(rowset_checksum) = 20),
+ start_time timestamp with time zone NOT NULL,
+ end_time timestamp with time zone NOT NULL,
+ succeeded enum_boolean DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT import_run_source
+ FOREIGN KEY (source_id)
+ REFERENCES import_source (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT import_run_rowset
+ FOREIGN KEY (rowset_checksum)
+ REFERENCES imported_rowset (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX import_run_import_source ON import_run (source_id);
+CREATE INDEX import_run_rowset ON import_run (rowset_checksum);
+
+
+CREATE TABLE imported_row (
+ checksum bytea CHECK(LENGTH(checksum) = 20),
+ object_name character varying(255) NOT NULL,
+ PRIMARY KEY (checksum)
+);
+
+COMMENT ON COLUMN imported_row.checksum IS 'sha1(object_name;property_checksum;...)';
+
+
+CREATE TABLE imported_rowset_row (
+ rowset_checksum bytea CHECK(LENGTH(checksum) = 20),
+ row_checksum bytea CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (rowset_checksum, row_checksum),
+ CONSTRAINT imported_rowset_row_rowset
+ FOREIGN KEY (rowset_checksum)
+ REFERENCES imported_rowset (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT imported_rowset_row_row
+ FOREIGN KEY (row_checksum)
+ REFERENCES imported_row (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX imported_rowset_row_rowset_checksum ON imported_rowset_row (rowset_checksum);
+CREATE INDEX imported_rowset_row_row_checksum ON imported_rowset_row (row_checksum);
+
+CREATE TABLE imported_property (
+ checksum bytea CHECK(LENGTH(checksum) = 20),
+ property_name character varying(64) NOT NULL,
+ property_value text NOT NULL,
+ format enum_property_format,
+ PRIMARY KEY (checksum)
+);
+
+CREATE INDEX imported_property_search_idx ON imported_property (property_name);
+
+CREATE TABLE imported_row_property (
+ row_checksum bytea CHECK(LENGTH(row_checksum) = 20),
+ property_checksum bytea CHECK(LENGTH(property_checksum) = 20),
+ PRIMARY KEY (row_checksum, property_checksum),
+ CONSTRAINT imported_row_property_row
+ FOREIGN KEY (row_checksum)
+ REFERENCES imported_row (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT imported_row_property_property
+ FOREIGN KEY (property_checksum)
+ REFERENCES imported_property (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX imported_row_property_row_checksum ON imported_row_property (row_checksum);
+CREATE INDEX imported_row_property_property_checksum ON imported_row_property (property_checksum);
+
+
+CREATE TYPE enum_sync_rule_object_type AS ENUM('host', 'user');
+CREATE TYPE enum_sync_rule_update_policy AS ENUM('merge', 'override', 'ignore');
+
+CREATE TABLE sync_rule (
+ id serial,
+ rule_name character varying(255) NOT NULL,
+ object_type enum_sync_rule_object_type NOT NULL,
+ update_policy enum_sync_rule_update_policy NOT NULL,
+ purge_existing enum_boolean NOT NULL DEFAULT 'n',
+ filter_expression text DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+
+CREATE TYPE enum_sync_property_merge_policy AS ENUM('override', 'merge');
+
+CREATE TABLE sync_property (
+ id serial,
+ rule_id integer NOT NULL,
+ source_id integer NOT NULL,
+ source_expression character varying(255) NOT NULL,
+ destination_field character varying(64),
+ priority smallint NOT NULL,
+ filter_expression text DEFAULT NULL,
+ merge_policy enum_sync_property_merge_policy NOT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT sync_property_rule
+ FOREIGN KEY (rule_id)
+ REFERENCES sync_rule (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT sync_property_source
+ FOREIGN KEY (source_id)
+ REFERENCES import_source (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX sync_property_rule ON sync_property (rule_id);
+CREATE INDEX sync_property_source ON sync_property (source_id);
+
+
+CREATE TABLE import_row_modifier (
+ id serial,
+ property_id integer NOT NULL,
+ provider_class character varying(72) NOT NULL,
+ PRIMARY KEY (id)
+);
+
+
+CREATE TABLE import_row_modifier_setting (
+ modifier_id integer NOT NULL,
+ setting_name character varying(64) NOT NULL,
+ setting_value text DEFAULT NULL,
+ PRIMARY KEY (modifier_id)
+);
+
+
+CREATE TABLE director_datafield_setting (
+ datafield_id integer NOT NULL,
+ setting_name character varying(64) NOT NULL,
+ setting_value text NOT NULL,
+ PRIMARY KEY (datafield_id, setting_name),
+ CONSTRAINT datafield_id_settings
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX director_datafield_datafield ON director_datafield_setting (datafield_id);
diff --git a/schema/pgsql-legacy-changes/upgrade_35.sql b/schema/pgsql-legacy-changes/upgrade_35.sql
new file mode 100644
index 0000000..b5e1294
--- /dev/null
+++ b/schema/pgsql-legacy-changes/upgrade_35.sql
@@ -0,0 +1,2 @@
+ALTER TABLE icinga_command_argument DROP COLUMN value_format, ADD COLUMN argument_format enum_property_format NOT NULL DEFAULT 'string';
+
diff --git a/schema/pgsql-migrations/upgrade_100.sql b/schema/pgsql-migrations/upgrade_100.sql
new file mode 100644
index 0000000..5f2e708
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_100.sql
@@ -0,0 +1,6 @@
+ALTER TABLE import_row_modifier
+ ADD COLUMN target_property character varying(255) DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (100, NOW());
diff --git a/schema/pgsql-migrations/upgrade_101.sql b/schema/pgsql-migrations/upgrade_101.sql
new file mode 100644
index 0000000..4243c01
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_101.sql
@@ -0,0 +1,9 @@
+ALTER TABLE icinga_host
+ ADD COLUMN api_key character varying(40) DEFAULT NULL;
+
+CREATE UNIQUE INDEX host_api_key ON icinga_host (api_key);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (101, NOW());
+
diff --git a/schema/pgsql-migrations/upgrade_102.sql b/schema/pgsql-migrations/upgrade_102.sql
new file mode 100644
index 0000000..4805d8a
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_102.sql
@@ -0,0 +1,13 @@
+UPDATE director_deployment_log SET startup_log = LEFT(startup_log, 20480) || '
+
+[..] shortened '
+|| (LENGTH(startup_log) - 40960)
+|| ' bytes by Director on schema upgrade [..]
+
+' || RIGHT(startup_log, 20480) WHERE LENGTH(startup_log) > 61440;
+
+VACUUM FULL director_deployment_log;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (102, NOW());
diff --git a/schema/pgsql-migrations/upgrade_103.sql b/schema/pgsql-migrations/upgrade_103.sql
new file mode 100644
index 0000000..ac001bf
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_103.sql
@@ -0,0 +1,11 @@
+UPDATE icinga_command_argument
+ SET
+ argument_name = '(no key)',
+ skip_key = 'y'
+ WHERE argument_name is null;
+
+ALTER TABLE icinga_command_argument ALTER COLUMN argument_name SET NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (103, NOW());
diff --git a/schema/pgsql-migrations/upgrade_104.sql b/schema/pgsql-migrations/upgrade_104.sql
new file mode 100644
index 0000000..f1972e7
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_104.sql
@@ -0,0 +1,25 @@
+ALTER TABLE icinga_timeperiod_range
+ ADD COLUMN range_key character varying(255) DEFAULT NULL,
+ ADD COLUMN range_value character varying(255) DEFAULT NULL;
+
+UPDATE icinga_timeperiod_range
+ SET range_key = timeperiod_key,
+ range_value = timeperiod_value;
+
+ALTER TABLE icinga_timeperiod_range
+ ALTER COLUMN range_key SET NOT NULL,
+ ALTER COLUMN range_key DROP DEFAULT,
+ ALTER COLUMN range_value SET NOT NULL,
+ ALTER COLUMN range_value DROP DEFAULT;
+
+ALTER TABLE icinga_timeperiod_range
+ DROP CONSTRAINT icinga_timeperiod_range_pkey,
+ ADD PRIMARY KEY (timeperiod_id, range_type, range_key);
+
+ALTER TABLE icinga_timeperiod_range
+ DROP COLUMN timeperiod_key,
+ DROP COLUMN timeperiod_value;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (104, NOW());
diff --git a/schema/pgsql-migrations/upgrade_105.sql b/schema/pgsql-migrations/upgrade_105.sql
new file mode 100644
index 0000000..69ea047
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_105.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_service
+ ADD COLUMN use_var_overrides enum_boolean DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (105, NOW());
diff --git a/schema/pgsql-migrations/upgrade_106.sql b/schema/pgsql-migrations/upgrade_106.sql
new file mode 100644
index 0000000..206db82
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_106.sql
@@ -0,0 +1,9 @@
+ALTER TABLE sync_property
+ ALTER COLUMN merge_policy DROP NOT NULL;
+
+ALTER TABLE sync_run
+ ALTER COLUMN rule_id DROP NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (106, NOW());
diff --git a/schema/pgsql-migrations/upgrade_107.sql b/schema/pgsql-migrations/upgrade_107.sql
new file mode 100644
index 0000000..293d038
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_107.sql
@@ -0,0 +1,9 @@
+ALTER TABLE import_source
+ ALTER COLUMN last_error_message TYPE TEXT;
+
+ALTER TABLE sync_rule
+ ALTER COLUMN last_error_message TYPE TEXT;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (107, NOW());
diff --git a/schema/pgsql-migrations/upgrade_109.sql b/schema/pgsql-migrations/upgrade_109.sql
new file mode 100644
index 0000000..f17123c
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_109.sql
@@ -0,0 +1,16 @@
+CREATE TABLE icinga_hostgroup_assignment (
+ id bigserial,
+ hostgroup_id integer NOT NULL,
+ filter_string TEXT NOT NULL,
+ assign_type enum_assign_type NOT NULL DEFAULT 'assign',
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_hostgroup_assignment
+ FOREIGN KEY (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (109, NOW());
diff --git a/schema/pgsql-migrations/upgrade_110.sql b/schema/pgsql-migrations/upgrade_110.sql
new file mode 100644
index 0000000..800f7ab
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_110.sql
@@ -0,0 +1,104 @@
+UPDATE icinga_host_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_host_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_service_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_service_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+
+UPDATE icinga_command_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_command_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_user_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_user_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_notification_var
+ SET varvalue = 'false',
+ format = 'json'
+ WHERE varvalue = 'n'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+UPDATE icinga_notification_var
+ SET varvalue = 'true',
+ format = 'json'
+ WHERE varvalue = 'y'
+ AND varname IN (
+ SELECT DISTINCT varname
+ FROM director_datafield
+ WHERE datatype LIKE '%DataTypeBoolean'
+ );
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (110, NOW());
diff --git a/schema/pgsql-migrations/upgrade_111.sql b/schema/pgsql-migrations/upgrade_111.sql
new file mode 100644
index 0000000..e5ea92b
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_111.sql
@@ -0,0 +1,11 @@
+ALTER TABLE import_run
+ DROP CONSTRAINT import_run_source,
+ ADD CONSTRAINT import_run_source
+ FOREIGN KEY (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE RESTRICT;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (111, NOW());
diff --git a/schema/pgsql-migrations/upgrade_113.sql b/schema/pgsql-migrations/upgrade_113.sql
new file mode 100644
index 0000000..6bc541a
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_113.sql
@@ -0,0 +1,6 @@
+COMMENT ON COLUMN icinga_timeperiod_range.range_key IS 'monday, ...';
+COMMENT ON COLUMN icinga_timeperiod_range.range_value IS '00:00-24:00, ...';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (113, NOW());
diff --git a/schema/pgsql-migrations/upgrade_114.sql b/schema/pgsql-migrations/upgrade_114.sql
new file mode 100644
index 0000000..6bd3f18
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_114.sql
@@ -0,0 +1,63 @@
+CREATE TABLE icinga_service_set (
+ id serial,
+ host_id integer NOT NULL,
+ object_name character varying(128) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ description text NOT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX service_set_name ON icinga_service_set (object_name, host_id);
+
+
+CREATE TABLE icinga_service_set_service (
+ service_set_id serial,
+ service_id serial,
+ PRIMARY KEY (service_set_id, service_id),
+ CONSTRAINT icinga_service_set_set
+ FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_set_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+
+CREATE TABLE icinga_service_set_assignment (
+ id serial,
+ service_set_id integer NOT NULL,
+ filter_string text NOT NULL,
+ assign_type enum_assign_type NOT NULL DEFAULT 'assign',
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_service_set_assignment
+ FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+
+CREATE TABLE icinga_service_set_var (
+ service_set_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ varvalue text DEFAULT NULL,
+ format enum_property_format NOT NULL DEFAULT 'string',
+ PRIMARY KEY (service_set_id, varname),
+ CONSTRAINT icinga_service_set_var_service_set
+ FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX service_set_var_service_set ON icinga_service_set_var (service_set_id);
+CREATE INDEX service_set_var_search_idx ON icinga_service_set_var (varname);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (114, NOW());
diff --git a/schema/pgsql-migrations/upgrade_115.sql b/schema/pgsql-migrations/upgrade_115.sql
new file mode 100644
index 0000000..0846355
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_115.sql
@@ -0,0 +1,28 @@
+CREATE TABLE icinga_service_set_inheritance (
+ service_set_id integer NOT NULL,
+ parent_service_set_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (service_set_id, parent_service_set_id),
+ CONSTRAINT icinga_service_set_inheritance_set
+ FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_set_inheritance_parent
+ FOREIGN KEY (parent_service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_set_inheritance_unique_order ON icinga_service_set_inheritance (service_set_id, weight);
+CREATE INDEX service_set_inheritance_set ON icinga_service_set_inheritance (service_set_id);
+CREATE INDEX service_set_inheritance_parent ON icinga_service_set_inheritance (parent_service_set_id);
+
+
+ALTER TABLE icinga_service_set ALTER COLUMN host_id DROP NOT NULL;
+ALTER TABLE icinga_service_set ALTER COLUMN description DROP NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (115, NOW());
diff --git a/schema/pgsql-migrations/upgrade_116.sql b/schema/pgsql-migrations/upgrade_116.sql
new file mode 100644
index 0000000..3190739
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_116.sql
@@ -0,0 +1,6 @@
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'timePeriod';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'serviceSet';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (116, NOW());
diff --git a/schema/pgsql-migrations/upgrade_117.sql b/schema/pgsql-migrations/upgrade_117.sql
new file mode 100644
index 0000000..6f3820f
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_117.sql
@@ -0,0 +1,26 @@
+CREATE TABLE icinga_notification_field (
+ notification_id integer NOT NULL,
+ datafield_id integer NOT NULL,
+ is_required enum_boolean NOT NULL,
+ PRIMARY KEY (notification_id, datafield_id),
+ CONSTRAINT icinga_notification_field_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_field_datafield
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX notification_field_key ON icinga_notification_field (notification_id, datafield_id);
+CREATE INDEX notification_field_notification ON icinga_notification_field (notification_id);
+CREATE INDEX notification_field_datafield ON icinga_notification_field (datafield_id);
+COMMENT ON COLUMN icinga_notification_field.notification_id IS 'Makes only sense for templates';
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (117, NOW());
diff --git a/schema/pgsql-migrations/upgrade_119.sql b/schema/pgsql-migrations/upgrade_119.sql
new file mode 100644
index 0000000..66ddba1
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_119.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_service
+ ADD COLUMN apply_for character varying(255) DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (119, NOW());
diff --git a/schema/pgsql-migrations/upgrade_120.sql b/schema/pgsql-migrations/upgrade_120.sql
new file mode 100644
index 0000000..94d7364
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_120.sql
@@ -0,0 +1,201 @@
+ALTER TABLE icinga_service ADD COLUMN assign_filter text DEFAULT NULL;
+
+WITH flat_assign AS (
+
+ SELECT
+ service_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa.filter_string)
+ ELSE ARRAY_TO_STRING(ARRAY_AGG(sa.filter_string), '&') END AS filter_string
+ FROM (
+ SELECT
+ sa_not.service_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa_not.filter_string)
+ ELSE '(' || ARRAY_TO_STRING(ARRAY_AGG(sa_not.filter_string), '&') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.service_id,
+ '!' || sa.filter_string AS filter_string
+ FROM icinga_service_assignment sa
+ WHERE assign_type = 'ignore'
+ ) sa_not
+ GROUP BY service_id
+
+ UNION ALL
+
+ SELECT
+ sa_yes.service_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa_yes.filter_string)
+ ELSE '(' || ARRAY_TO_STRING(ARRAY_AGG(sa_yes.filter_string), '|') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.service_id,
+ sa.filter_string AS filter_string
+ FROM icinga_service_assignment sa
+ WHERE assign_type = 'assign'
+ ) sa_yes
+ GROUP BY service_id
+
+ ) sa GROUP BY service_id
+
+) UPDATE icinga_service s
+ SET assign_filter = flat_assign.filter_string
+ FROM flat_assign
+ WHERE s.id = flat_assign.service_id;
+
+DROP TABLE icinga_service_assignment;
+
+ALTER TABLE icinga_service_set ADD COLUMN assign_filter text DEFAULT NULL;
+
+WITH flat_assign AS (
+
+ SELECT
+ service_set_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa.filter_string)
+ ELSE ARRAY_TO_STRING(ARRAY_AGG(sa.filter_string), '&') END AS filter_string
+ FROM (
+ SELECT
+ sa_not.service_set_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa_not.filter_string)
+ ELSE '(' || ARRAY_TO_STRING(ARRAY_AGG(sa_not.filter_string), '&') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.service_set_id,
+ '!' || sa.filter_string AS filter_string
+ FROM icinga_service_set_assignment sa
+ WHERE assign_type = 'ignore'
+ ) sa_not
+ GROUP BY service_set_id
+
+ UNION ALL
+
+ SELECT
+ sa_yes.service_set_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa_yes.filter_string)
+ ELSE '(' || ARRAY_TO_STRING(ARRAY_AGG(sa_yes.filter_string), '|') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.service_set_id,
+ sa.filter_string AS filter_string
+ FROM icinga_service_set_assignment sa
+ WHERE assign_type = 'assign'
+ ) sa_yes
+ GROUP BY service_set_id
+
+ ) sa GROUP BY service_set_id
+
+) UPDATE icinga_service_set s
+ SET assign_filter = flat_assign.filter_string
+ FROM flat_assign
+ WHERE s.id = flat_assign.service_set_id;
+
+DROP TABLE icinga_service_set_assignment;
+
+
+ALTER TABLE icinga_notification ADD COLUMN assign_filter text DEFAULT NULL;
+
+WITH flat_assign AS (
+
+ SELECT
+ notification_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa.filter_string)
+ ELSE ARRAY_TO_STRING(ARRAY_AGG(sa.filter_string), '&') END AS filter_string
+ FROM (
+ SELECT
+ sa_not.notification_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa_not.filter_string)
+ ELSE '(' || ARRAY_TO_STRING(ARRAY_AGG(sa_not.filter_string), '&') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.notification_id,
+ '!' || sa.filter_string AS filter_string
+ FROM icinga_notification_assignment sa
+ WHERE assign_type = 'ignore'
+ ) sa_not
+ GROUP BY notification_id
+
+ UNION ALL
+
+ SELECT
+ sa_yes.notification_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa_yes.filter_string)
+ ELSE '(' || ARRAY_TO_STRING(ARRAY_AGG(sa_yes.filter_string), '|') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.notification_id,
+ sa.filter_string AS filter_string
+ FROM icinga_notification_assignment sa
+ WHERE assign_type = 'assign'
+ ) sa_yes
+ GROUP BY notification_id
+
+ ) sa GROUP BY notification_id
+
+) UPDATE icinga_notification s
+ SET assign_filter = flat_assign.filter_string
+ FROM flat_assign
+ WHERE s.id = flat_assign.notification_id;
+
+
+DROP TABLE icinga_notification_assignment;
+
+ALTER TABLE icinga_hostgroup ADD COLUMN assign_filter text DEFAULT NULL;
+
+WITH flat_assign AS (
+
+ SELECT
+ hostgroup_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa.filter_string)
+ ELSE ARRAY_TO_STRING(ARRAY_AGG(sa.filter_string), '&') END AS filter_string
+ FROM (
+ SELECT
+ sa_not.hostgroup_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa_not.filter_string)
+ ELSE '(' || ARRAY_TO_STRING(ARRAY_AGG(sa_not.filter_string), '&') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.hostgroup_id,
+ '!' || sa.filter_string AS filter_string
+ FROM icinga_hostgroup_assignment sa
+ WHERE assign_type = 'ignore'
+ ) sa_not
+ GROUP BY hostgroup_id
+
+ UNION ALL
+
+ SELECT
+ sa_yes.hostgroup_id,
+ CASE WHEN COUNT(*) = 0 THEN NULL
+ WHEN COUNT(*) = 1 THEN MAX(sa_yes.filter_string)
+ ELSE '(' || ARRAY_TO_STRING(ARRAY_AGG(sa_yes.filter_string), '|') || ')' END AS filter_string
+ FROM ( SELECT
+ sa.hostgroup_id,
+ sa.filter_string AS filter_string
+ FROM icinga_hostgroup_assignment sa
+ WHERE assign_type = 'assign'
+ ) sa_yes
+ GROUP BY hostgroup_id
+
+ ) sa GROUP BY hostgroup_id
+
+) UPDATE icinga_hostgroup s
+ SET assign_filter = flat_assign.filter_string
+ FROM flat_assign
+ WHERE s.id = flat_assign.hostgroup_id;
+
+
+DROP TABLE icinga_hostgroup_assignment;
+
+
+ALTER TABLE icinga_servicegroup ADD COLUMN assign_filter text DEFAULT NULL;
+
+
+DROP TYPE enum_assign_type;
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (120, NOW());
diff --git a/schema/pgsql-migrations/upgrade_121.sql b/schema/pgsql-migrations/upgrade_121.sql
new file mode 100644
index 0000000..f6a1050
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_121.sql
@@ -0,0 +1,8 @@
+ALTER TABLE icinga_service
+ ADD COLUMN service_set_id integer DEFAULT NULL;
+
+DROP TABLE icinga_service_set_service;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (121, NOW());
diff --git a/schema/pgsql-migrations/upgrade_122.sql b/schema/pgsql-migrations/upgrade_122.sql
new file mode 100644
index 0000000..6f3f884
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_122.sql
@@ -0,0 +1,12 @@
+ALTER TABLE director_generated_file
+ ADD COLUMN cnt_apply SMALLINT NOT NULL DEFAULT 0;
+
+UPDATE director_generated_file
+SET cnt_apply = ROUND(
+ (LENGTH(content) - LENGTH( REPLACE(content, 'apply ', '') ) )
+ / LENGTH('apply ')
+);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (122, NOW());
diff --git a/schema/pgsql-migrations/upgrade_123.sql b/schema/pgsql-migrations/upgrade_123.sql
new file mode 100644
index 0000000..bc97854
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_123.sql
@@ -0,0 +1,34 @@
+-- cleanup dangling service_set before we add foreign key
+DELETE FROM icinga_service_set AS ss
+ WHERE NOT EXISTS (
+ SELECT 1 FROM icinga_host AS h
+ WHERE h.id = ss.host_id
+ )
+ AND object_type = 'object'
+ AND host_id IS NOT NULL;
+
+-- cleanup dangling services to service_set
+DELETE FROM icinga_service AS s
+ WHERE NOT EXISTS (
+ SELECT 1 FROM icinga_service_set AS ss
+ WHERE ss.id = s.service_set_id
+ )
+ AND object_type IN ('object', 'apply')
+ AND service_set_id IS NOT NULL;
+
+
+ALTER TABLE icinga_service_set
+ ADD CONSTRAINT icinga_service_set_host FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_service
+ ADD CONSTRAINT icinga_service_service_set FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (123, NOW());
diff --git a/schema/pgsql-migrations/upgrade_124.sql b/schema/pgsql-migrations/upgrade_124.sql
new file mode 100644
index 0000000..e8b74fa
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_124.sql
@@ -0,0 +1,21 @@
+ALTER TABLE icinga_service_set
+ DROP CONSTRAINT icinga_service_set_host;
+
+ALTER TABLE icinga_service_set
+ ADD CONSTRAINT icinga_service_set_host FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_service
+ DROP CONSTRAINT icinga_service_service_set;
+
+ALTER TABLE icinga_service
+ ADD CONSTRAINT icinga_service_service_set FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (124, NOW());
diff --git a/schema/pgsql-migrations/upgrade_125.sql b/schema/pgsql-migrations/upgrade_125.sql
new file mode 100644
index 0000000..b1ffea1
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_125.sql
@@ -0,0 +1,18 @@
+ALTER TABLE icinga_command_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+ALTER TABLE icinga_host_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+ALTER TABLE icinga_notification_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+ALTER TABLE icinga_service_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+ALTER TABLE icinga_user_field
+ ADD COLUMN var_filter TEXT DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (125, NOW());
diff --git a/schema/pgsql-migrations/upgrade_127.sql b/schema/pgsql-migrations/upgrade_127.sql
new file mode 100644
index 0000000..0cf1a12
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_127.sql
@@ -0,0 +1,197 @@
+ALTER TABLE icinga_command_var
+ ADD COLUMN checksum bytea DEFAULT NULL CHECK(LENGTH(checksum) = 20);
+CREATE INDEX command_var_search_idx ON icinga_command_var (varname);
+CREATE INDEX command_var_checksum ON icinga_command_var (checksum);
+
+
+ALTER TABLE icinga_host_var
+ ADD COLUMN checksum bytea DEFAULT NULL CHECK(LENGTH(checksum) = 20);
+CREATE INDEX host_var_checksum ON icinga_host_var (checksum);
+
+
+ALTER TABLE icinga_notification_var
+ ADD COLUMN checksum bytea DEFAULT NULL CHECK(LENGTH(checksum) = 20);
+CREATE INDEX notification_var_command ON icinga_notification_var (notification_id);
+CREATE INDEX notification_var_checksum ON icinga_notification_var (checksum);
+
+
+ALTER TABLE icinga_service_set_var
+ ADD COLUMN checksum bytea DEFAULT NULL CHECK(LENGTH(checksum) = 20);
+CREATE INDEX service_set_var_checksum ON icinga_service_set_var (checksum);
+
+
+ALTER TABLE icinga_service_var
+ ADD COLUMN checksum bytea DEFAULT NULL CHECK(LENGTH(checksum) = 20);
+CREATE INDEX service_var_checksum ON icinga_service_var (checksum);
+
+
+ALTER TABLE icinga_user_var
+ ADD COLUMN checksum bytea DEFAULT NULL CHECK(LENGTH(checksum) = 20);
+CREATE INDEX user_var_checksum ON icinga_user_var (checksum);
+
+
+CREATE TABLE icinga_var (
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ rendered_checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ varname character varying(255) NOT NULL,
+ varvalue TEXT NOT NULL,
+ rendered TEXT NOT NULL,
+ PRIMARY KEY (checksum)
+);
+
+CREATE INDEX var_search_idx ON icinga_var (varname);
+
+
+CREATE TABLE icinga_flat_var (
+ var_checksum bytea NOT NULL CHECK(LENGTH(var_checksum) = 20),
+ flatname_checksum bytea NOT NULL CHECK(LENGTH(flatname_checksum) = 20),
+ flatname character varying(512) NOT NULL,
+ flatvalue TEXT NOT NULL,
+ PRIMARY KEY (var_checksum, flatname_checksum),
+ CONSTRAINT flat_var_var
+ FOREIGN KEY (var_checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX flat_var_var_checksum ON icinga_flat_var (var_checksum);
+CREATE INDEX flat_var_search_varname ON icinga_flat_var (flatname);
+CREATE INDEX flat_var_search_varvalue ON icinga_flat_var (flatvalue);
+
+
+CREATE TABLE icinga_command_resolved_var (
+ command_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (command_id, checksum),
+ CONSTRAINT command_resolved_var_command
+ FOREIGN KEY (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT command_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX command_resolved_var_search_varname ON icinga_command_resolved_var (varname);
+CREATE INDEX command_resolved_var_command_id ON icinga_command_resolved_var (command_id);
+CREATE INDEX command_resolved_var_schecksum ON icinga_command_resolved_var (checksum);
+
+
+CREATE TABLE icinga_host_resolved_var (
+ host_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (host_id, checksum),
+ CONSTRAINT host_resolved_var_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT host_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX host_resolved_var_search_varname ON icinga_host_resolved_var (varname);
+CREATE INDEX host_resolved_var_host_id ON icinga_host_resolved_var (host_id);
+CREATE INDEX host_resolved_var_schecksum ON icinga_host_resolved_var (checksum);
+
+
+CREATE TABLE icinga_notification_resolved_var (
+ notification_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (notification_id, checksum),
+ CONSTRAINT notification_resolved_var_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT notification_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX notification_resolved_var_search_varname ON icinga_notification_resolved_var (varname);
+CREATE INDEX notification_resolved_var_notification_id ON icinga_notification_resolved_var (notification_id);
+CREATE INDEX notification_resolved_var_schecksum ON icinga_notification_resolved_var (checksum);
+
+
+CREATE TABLE icinga_service_set_resolved_var (
+ service_set_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (service_set_id, checksum),
+ CONSTRAINT service_set_resolved_var_service_set
+ FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT service_set_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX service_set_resolved_var_search_varname ON icinga_service_set_resolved_var (varname);
+CREATE INDEX service_set_resolved_var_service_set_id ON icinga_service_set_resolved_var (service_set_id);
+CREATE INDEX service_set_resolved_var_schecksum ON icinga_service_set_resolved_var (checksum);
+
+
+CREATE TABLE icinga_service_resolved_var (
+ service_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (service_id, checksum),
+ CONSTRAINT service_resolved_var_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT service_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX service_resolved_var_search_varname ON icinga_service_resolved_var (varname);
+CREATE INDEX service_resolved_var_service_id ON icinga_service_resolved_var (service_id);
+CREATE INDEX service_resolved_var_schecksum ON icinga_service_resolved_var (checksum);
+
+
+CREATE TABLE icinga_user_resolved_var (
+ user_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (user_id, checksum),
+ CONSTRAINT user_resolved_var_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT user_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX user_resolved_var_search_varname ON icinga_user_resolved_var (varname);
+CREATE INDEX user_resolved_var_user_id ON icinga_user_resolved_var (user_id);
+CREATE INDEX user_resolved_var_schecksum ON icinga_user_resolved_var (checksum);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (127, NOW());
diff --git a/schema/pgsql-migrations/upgrade_128.sql b/schema/pgsql-migrations/upgrade_128.sql
new file mode 100644
index 0000000..20dbbc6
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_128.sql
@@ -0,0 +1,5 @@
+CREATE INDEX activity_log_author ON director_activity_log (author);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (128, NOW());
diff --git a/schema/pgsql-migrations/upgrade_131.sql b/schema/pgsql-migrations/upgrade_131.sql
new file mode 100644
index 0000000..37fc5b7
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_131.sql
@@ -0,0 +1,22 @@
+CREATE TABLE icinga_hostgroup_host_resolved (
+ hostgroup_id integer NOT NULL,
+ host_id integer NOT NULL,
+ PRIMARY KEY (hostgroup_id, host_id),
+ CONSTRAINT icinga_hostgroup_host_resolved_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_host_resolved_hostgroup
+ FOREIGN KEY (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX hostgroup_host_resolved_host ON icinga_hostgroup_host_resolved (host_id);
+CREATE INDEX hostgroup_host_resolved_hostgroup ON icinga_hostgroup_host_resolved (hostgroup_id);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (131, NOW());
diff --git a/schema/pgsql-migrations/upgrade_132.sql b/schema/pgsql-migrations/upgrade_132.sql
new file mode 100644
index 0000000..8168c68
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_132.sql
@@ -0,0 +1,25 @@
+CREATE TABLE icinga_host_template_choice (
+ id serial,
+ object_name character varying(64) NOT NULL,
+ description text DEFAULT NULL,
+ min_required smallint NOT NULL DEFAULT 0,
+ max_allowed smallint NOT NULL DEFAULT 1,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX host_template_choice_object_name ON icinga_host_template_choice (object_name);
+
+
+ALTER TABLE icinga_host
+ ADD COLUMN template_choice_id int DEFAULT NULL,
+ ADD CONSTRAINT icinga_host_template_choice
+ FOREIGN KEY (template_choice_id)
+ REFERENCES icinga_host_template_choice (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+CREATE INDEX host_template_choice ON icinga_host (template_choice_id);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (132, NOW());
diff --git a/schema/pgsql-migrations/upgrade_133.sql b/schema/pgsql-migrations/upgrade_133.sql
new file mode 100644
index 0000000..2cfc722
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_133.sql
@@ -0,0 +1,25 @@
+CREATE TABLE icinga_service_template_choice (
+ id serial,
+ object_name character varying(64) NOT NULL,
+ description text DEFAULT NULL,
+ min_required smallint NOT NULL DEFAULT 0,
+ max_allowed smallint NOT NULL DEFAULT 1,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX service_template_choice_object_name ON icinga_service_template_choice (object_name);
+
+
+ALTER TABLE icinga_service
+ ADD COLUMN template_choice_id int DEFAULT NULL,
+ ADD CONSTRAINT icinga_service_template_choice
+ FOREIGN KEY (template_choice_id)
+ REFERENCES icinga_service_template_choice (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+CREATE INDEX service_template_choice ON icinga_service (template_choice_id);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (133, NOW());
diff --git a/schema/pgsql-migrations/upgrade_134.sql b/schema/pgsql-migrations/upgrade_134.sql
new file mode 100644
index 0000000..d08510f
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_134.sql
@@ -0,0 +1,19 @@
+ALTER TABLE icinga_host
+ DROP CONSTRAINT icinga_host_template_choice,
+ ADD CONSTRAINT icinga_host_template_choice_v2
+ FOREIGN KEY (template_choice_id)
+ REFERENCES icinga_host_template_choice (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_service
+ DROP CONSTRAINT icinga_service_template_choice,
+ ADD CONSTRAINT icinga_service_template_choice_v2
+ FOREIGN KEY (template_choice_id)
+ REFERENCES icinga_service_template_choice (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (134, NOW());
diff --git a/schema/pgsql-migrations/upgrade_135.sql b/schema/pgsql-migrations/upgrade_135.sql
new file mode 100644
index 0000000..63adfc0
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_135.sql
@@ -0,0 +1,9 @@
+ALTER TABLE icinga_host
+ ADD COLUMN check_timeout smallint DEFAULT NULL;
+
+ALTER TABLE icinga_service
+ ADD COLUMN check_timeout smallint DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (135, NOW());
diff --git a/schema/pgsql-migrations/upgrade_136.sql b/schema/pgsql-migrations/upgrade_136.sql
new file mode 100644
index 0000000..f1a3729
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_136.sql
@@ -0,0 +1,6 @@
+ALTER TABLE director_datalist_entry
+ ADD COLUMN allowed_roles character varying(255) DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (136, NOW());
diff --git a/schema/pgsql-migrations/upgrade_137.sql b/schema/pgsql-migrations/upgrade_137.sql
new file mode 100644
index 0000000..220c1c3
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_137.sql
@@ -0,0 +1,9 @@
+ALTER TABLE import_source
+ ADD COLUMN description text DEFAULT NULL;
+
+ALTER TABLE sync_rule
+ ADD COLUMN description text DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (137, NOW());
diff --git a/schema/pgsql-migrations/upgrade_138.sql b/schema/pgsql-migrations/upgrade_138.sql
new file mode 100644
index 0000000..84efc53
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_138.sql
@@ -0,0 +1,6 @@
+ALTER TABLE import_row_modifier
+ ADD COLUMN description text DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (138, NOW());
diff --git a/schema/pgsql-migrations/upgrade_139.sql b/schema/pgsql-migrations/upgrade_139.sql
new file mode 100644
index 0000000..705d2a9
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_139.sql
@@ -0,0 +1,9 @@
+UPDATE import_row_modifier SET priority = id;
+
+CREATE UNIQUE INDEX import_row_modifier_prio
+ ON import_row_modifier (source_id, priority);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (139, NOW());
diff --git a/schema/pgsql-migrations/upgrade_140.sql b/schema/pgsql-migrations/upgrade_140.sql
new file mode 100644
index 0000000..996e9ef
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_140.sql
@@ -0,0 +1,5 @@
+UPDATE sync_property SET priority = 10000 - priority;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (140, NOW());
diff --git a/schema/pgsql-migrations/upgrade_141.sql b/schema/pgsql-migrations/upgrade_141.sql
new file mode 100644
index 0000000..a382208
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_141.sql
@@ -0,0 +1,7 @@
+UPDATE icinga_service_set
+ SET object_type = 'template'
+ WHERE object_type = 'object' AND host_id IS NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (141, NOW());
diff --git a/schema/pgsql-migrations/upgrade_142.sql b/schema/pgsql-migrations/upgrade_142.sql
new file mode 100644
index 0000000..5b0c1f3
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_142.sql
@@ -0,0 +1,13 @@
+ALTER TABLE import_run
+ DROP CONSTRAINT import_run_source;
+
+ALTER TABLE import_run
+ ADD CONSTRAINT import_run_source
+ FOREIGN KEY (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE RESTRICT;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (142, NOW());
diff --git a/schema/pgsql-migrations/upgrade_143.sql b/schema/pgsql-migrations/upgrade_143.sql
new file mode 100644
index 0000000..3c8e9c5
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_143.sql
@@ -0,0 +1,27 @@
+ALTER TABLE icinga_host_template_choice
+ ADD COLUMN required_template_id integer DEFAULT NULL,
+ ADD COLUMN allowed_roles character varying(255) DEFAULT NULL,
+ ADD CONSTRAINT host_template_choice_required_template
+ FOREIGN KEY (required_template_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+ALTER TABLE icinga_service_template_choice
+ ADD COLUMN required_template_id integer DEFAULT NULL,
+ ADD COLUMN allowed_roles character varying(255) DEFAULT NULL,
+ ADD CONSTRAINT service_template_choice_required_template
+ FOREIGN KEY (required_template_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+CREATE INDEX host_template_choice_required_template
+ ON icinga_host_template_choice (required_template_id);
+
+CREATE INDEX service_template_choice_required_template
+ ON icinga_service_template_choice (required_template_id);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (143, NOW());
diff --git a/schema/pgsql-migrations/upgrade_144.sql b/schema/pgsql-migrations/upgrade_144.sql
new file mode 100644
index 0000000..4516f5e
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_144.sql
@@ -0,0 +1,99 @@
+CREATE TABLE icinga_dependency (
+ id serial,
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean DEFAULT 'n',
+ apply_to enum_host_service NULL DEFAULT NULL,
+ parent_host_id integer DEFAULT NULL,
+ parent_service_id integer DEFAULT NULL,
+ child_host_id integer DEFAULT NULL,
+ child_service_id integer DEFAULT NULL,
+ disable_checks enum_boolean DEFAULT NULL,
+ disable_notifications enum_boolean DEFAULT NULL,
+ ignore_soft_states enum_boolean DEFAULT NULL,
+ period_id integer DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ parent_service_by_name character varying(255),
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_dependency_parent_host
+ FOREIGN KEY (parent_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_parent_service
+ FOREIGN KEY (parent_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_child_host
+ FOREIGN KEY (child_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_child_service
+ FOREIGN KEY (child_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_period
+ FOREIGN KEY (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX dependency_parent_host ON icinga_dependency (parent_host_id);
+CREATE INDEX dependency_parent_service ON icinga_dependency (parent_service_id);
+CREATE INDEX dependency_child_host ON icinga_dependency (child_host_id);
+CREATE INDEX dependency_child_service ON icinga_dependency (child_service_id);
+CREATE INDEX dependency_period ON icinga_dependency (period_id);
+CREATE INDEX dependency_zone ON icinga_dependency (zone_id);
+
+
+CREATE TABLE icinga_dependency_inheritance (
+ dependency_id integer NOT NULL,
+ parent_dependency_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (dependency_id, parent_dependency_id),
+ CONSTRAINT icinga_dependency_inheritance_dependency
+ FOREIGN KEY (dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_inheritance_parent_dependency
+ FOREIGN KEY (parent_dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX dependency_inheritance_unique_order ON icinga_dependency_inheritance (dependency_id, weight);
+CREATE INDEX dependency_inheritance_dependency ON icinga_dependency_inheritance (dependency_id);
+CREATE INDEX dependency_inheritance_dependency_parent ON icinga_dependency_inheritance (parent_dependency_id);
+
+
+CREATE TABLE icinga_dependency_states_set (
+ dependency_id integer NOT NULL,
+ property enum_state_name NOT NULL,
+ merge_behaviour enum_set_merge_behaviour NOT NULL DEFAULT 'override',
+ PRIMARY KEY (dependency_id, property, merge_behaviour),
+ CONSTRAINT icinga_dependency_states_set_dependency
+ FOREIGN KEY (dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX dependency_states_set_dependency ON icinga_dependency_states_set (dependency_id);
+COMMENT ON COLUMN icinga_dependency_states_set.merge_behaviour IS 'override: = [], extend: += [], blacklist: -= []';
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (144, NOW());
diff --git a/schema/pgsql-migrations/upgrade_146.sql b/schema/pgsql-migrations/upgrade_146.sql
new file mode 100644
index 0000000..ce92a31
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_146.sql
@@ -0,0 +1,14 @@
+ALTER TABLE icinga_host
+ DROP COLUMN flapping_threshold,
+ ADD COLUMN flapping_threshold_high smallint DEFAULT NULL,
+ ADD COLUMN flapping_threshold_low smallint DEFAULT NULL;
+
+ALTER TABLE icinga_service
+ DROP COLUMN flapping_threshold,
+ ADD COLUMN flapping_threshold_high smallint DEFAULT NULL,
+ ADD COLUMN flapping_threshold_low smallint DEFAULT NULL;
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (146, NOW());
diff --git a/schema/pgsql-migrations/upgrade_147.sql b/schema/pgsql-migrations/upgrade_147.sql
new file mode 100644
index 0000000..c0c5741
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_147.sql
@@ -0,0 +1,23 @@
+CREATE TABLE icinga_host_service_blacklist(
+ host_id integer NOT NULL,
+ service_id integer NOT NULL,
+ PRIMARY KEY (host_id, service_id),
+ CONSTRAINT icinga_host_service__bl_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_service_bl_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX host_service_bl_host ON icinga_host_service_blacklist (host_id);
+CREATE INDEX host_service_bl_service ON icinga_host_service_blacklist (service_id);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (147, NOW());
diff --git a/schema/pgsql-migrations/upgrade_148.sql b/schema/pgsql-migrations/upgrade_148.sql
new file mode 100644
index 0000000..e0f24a7
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_148.sql
@@ -0,0 +1,10 @@
+ALTER TABLE import_source
+ ALTER COLUMN provider_class TYPE character varying(128);
+
+ALTER TABLE import_row_modifier
+ ALTER COLUMN provider_class TYPE character varying(128);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (148, NOW());
diff --git a/schema/pgsql-migrations/upgrade_149.sql b/schema/pgsql-migrations/upgrade_149.sql
new file mode 100644
index 0000000..9fe8d5e
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_149.sql
@@ -0,0 +1,14 @@
+ALTER TABLE icinga_usergroup
+ ADD COLUMN zone_id integer DEFAULT NULL,
+ ADD CONSTRAINT icinga_usergroup_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+CREATE INDEX usergroup_zone ON icinga_usergroup (zone_id);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (149, NOW());
diff --git a/schema/pgsql-migrations/upgrade_150.sql b/schema/pgsql-migrations/upgrade_150.sql
new file mode 100644
index 0000000..ca838bb
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_150.sql
@@ -0,0 +1,17 @@
+UPDATE icinga_user u
+SET period_id = NULL
+WHERE NOT EXISTS (
+ SELECT id FROM icinga_timeperiod
+ WHERE id = u.period_id
+) AND u.period_id IS NOT NULL;
+
+ALTER TABLE icinga_user
+ ADD CONSTRAINT icinga_user_period
+ FOREIGN KEY (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (150, NOW());
diff --git a/schema/pgsql-migrations/upgrade_151.sql b/schema/pgsql-migrations/upgrade_151.sql
new file mode 100644
index 0000000..a24189a
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_151.sql
@@ -0,0 +1,38 @@
+ALTER TABLE icinga_timeperiod
+ ADD COLUMN prefer_includes enum_boolean DEFAULT NULL;
+
+CREATE TABLE icinga_timeperiod_include (
+ timeperiod_id integer NOT NULL,
+ include_id integer NOT NULL,
+ PRIMARY KEY (timeperiod_id, include_id),
+ CONSTRAINT icinga_timeperiod_timeperiod_include
+ FOREIGN KEY (include_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_timeperiod_include
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE icinga_timeperiod_exclude (
+ timeperiod_id integer NOT NULL,
+ exclude_id integer NOT NULL,
+ PRIMARY KEY (timeperiod_id, exclude_id),
+ CONSTRAINT icinga_timeperiod_timeperiod_exclude
+ FOREIGN KEY (exclude_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_timeperiod_exclude
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+VALUES (151, NOW());
diff --git a/schema/pgsql-migrations/upgrade_152.sql b/schema/pgsql-migrations/upgrade_152.sql
new file mode 100644
index 0000000..b1d816c
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_152.sql
@@ -0,0 +1,7 @@
+CREATE UNIQUE INDEX import_source_name ON import_source (source_name);
+
+CREATE UNIQUE INDEX sync_rule_name ON sync_rule (rule_name);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+VALUES (152, NOW());
diff --git a/schema/pgsql-migrations/upgrade_153.sql b/schema/pgsql-migrations/upgrade_153.sql
new file mode 100644
index 0000000..f2c5dbd
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_153.sql
@@ -0,0 +1,45 @@
+CREATE TYPE enum_owner_type AS ENUM('user', 'usergroup', 'role');
+
+CREATE TABLE director_basket (
+ uuid bytea CHECK(LENGTH(uuid) = 16) NOT NULL,
+ basket_name VARCHAR(64) NOT NULL,
+ owner_type enum_owner_type NOT NULL,
+ owner_value VARCHAR(255) NOT NULL,
+ objects text NOT NULL, -- json-encoded
+ PRIMARY KEY (uuid)
+);
+
+CREATE UNIQUE INDEX basket_basket_name ON director_basket (basket_name);
+
+
+CREATE TABLE director_basket_content (
+ checksum bytea CHECK(LENGTH(checksum) = 20) NOT NULL,
+ summary VARCHAR(255) NOT NULL, -- json
+ content text NOT NULL, -- json
+ PRIMARY KEY (checksum)
+);
+
+
+CREATE TABLE director_basket_snapshot (
+ basket_uuid bytea CHECK(LENGTH(basket_uuid) = 16) NOT NULL,
+ ts_create bigint NOT NULL,
+ content_checksum bytea CHECK(LENGTH(content_checksum) = 20) NOT NULL,
+ PRIMARY KEY (basket_uuid, ts_create),
+ CONSTRAINT basked_snapshot_basket
+ FOREIGN KEY (basket_uuid)
+ REFERENCES director_basket (uuid)
+ ON DELETE CASCADE
+ ON UPDATE RESTRICT,
+ CONSTRAINT basked_snapshot_content
+ FOREIGN KEY (content_checksum)
+ REFERENCES director_basket_content (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX basket_snapshot_sort_idx ON director_basket_snapshot (ts_create);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (153, NOW());
diff --git a/schema/pgsql-migrations/upgrade_154.sql b/schema/pgsql-migrations/upgrade_154.sql
new file mode 100644
index 0000000..08274b0
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_154.sql
@@ -0,0 +1,12 @@
+
+UPDATE icinga_command_argument
+SET argument_format = NULL
+WHERE argument_value IS NULL;
+
+UPDATE icinga_command_argument
+SET set_if_format = NULL
+WHERE set_if IS NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (154, NOW());
diff --git a/schema/pgsql-migrations/upgrade_155.sql b/schema/pgsql-migrations/upgrade_155.sql
new file mode 100644
index 0000000..d967e63
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_155.sql
@@ -0,0 +1,22 @@
+CREATE TABLE icinga_servicegroup_service_resolved (
+ servicegroup_id integer NOT NULL,
+ service_id integer NOT NULL,
+ PRIMARY KEY (servicegroup_id, service_id),
+ CONSTRAINT icinga_servicegroup_service_resolved_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_service_resolved_servicegroup
+ FOREIGN KEY (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX servicegroup_service_resolved_service ON icinga_servicegroup_service_resolved (service_id);
+CREATE INDEX servicegroup_service_resolved_servicegroup ON icinga_servicegroup_service_resolved (servicegroup_id);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (155, NOW());
diff --git a/schema/pgsql-migrations/upgrade_156.sql b/schema/pgsql-migrations/upgrade_156.sql
new file mode 100644
index 0000000..aa80cb3
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_156.sql
@@ -0,0 +1,6 @@
+DROP INDEX IF EXISTS command_object_name;
+CREATE UNIQUE INDEX command_object_name ON icinga_command (object_name);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (156, NOW());
diff --git a/schema/pgsql-migrations/upgrade_157.sql b/schema/pgsql-migrations/upgrade_157.sql
new file mode 100644
index 0000000..02ce370
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_157.sql
@@ -0,0 +1,6 @@
+ALTER TABLE director_basket_content
+ ALTER COLUMN summary TYPE VARCHAR(500);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (157, NOW());
diff --git a/schema/pgsql-migrations/upgrade_158.sql b/schema/pgsql-migrations/upgrade_158.sql
new file mode 100644
index 0000000..e9d3599
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_158.sql
@@ -0,0 +1,6 @@
+DROP INDEX IF EXISTS notification_var_search_idx;
+CREATE INDEX notification_var_search_idx ON icinga_notification_var (varname);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (158, NOW());
diff --git a/schema/pgsql-migrations/upgrade_160.sql b/schema/pgsql-migrations/upgrade_160.sql
new file mode 100644
index 0000000..5258146
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_160.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_command
+ ADD COLUMN is_string enum_boolean NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (160, NOW());
diff --git a/schema/pgsql-migrations/upgrade_161.sql b/schema/pgsql-migrations/upgrade_161.sql
new file mode 100644
index 0000000..b8618d9
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_161.sql
@@ -0,0 +1,70 @@
+CREATE TABLE icinga_scheduled_downtime (
+ id serial,
+ object_name character varying(255) NOT NULL,
+ zone_id integer DEFAULT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ apply_to enum_host_service NULL DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ author character varying(255) DEFAULT NULL,
+ comment text DEFAULT NULL,
+ fixed enum_boolean DEFAULT NULL,
+ duration int DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_scheduled_downtime_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX scheduled_downtime_object_name ON icinga_scheduled_downtime (object_name);
+CREATE INDEX scheduled_downtime_zone ON icinga_scheduled_downtime (zone_id);
+
+
+CREATE TABLE icinga_scheduled_downtime_inheritance (
+ scheduled_downtime_id integer NOT NULL,
+ parent_scheduled_downtime_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (scheduled_downtime_id, parent_scheduled_downtime_id),
+ CONSTRAINT icinga_scheduled_downtime_inheritance_scheduled_downtime
+ FOREIGN KEY (scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_scheduled_downtime_inheritance_parent_scheduled_downtime
+ FOREIGN KEY (parent_scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX scheduled_downtime_inheritance_unique_order ON icinga_scheduled_downtime_inheritance (scheduled_downtime_id, weight);
+CREATE INDEX scheduled_downtime_inheritance_scheduled_downtime ON icinga_scheduled_downtime_inheritance (scheduled_downtime_id);
+CREATE INDEX scheduled_downtime_inheritance_scheduled_downtime_parent ON icinga_scheduled_downtime_inheritance (parent_scheduled_downtime_id);
+
+
+CREATE TABLE icinga_scheduled_downtime_range (
+ scheduled_downtime_id serial,
+ range_key character varying(255) NOT NULL,
+ range_value character varying(255) NOT NULL,
+ range_type enum_timeperiod_range_type NOT NULL DEFAULT 'include',
+ merge_behaviour enum_merge_behaviour NOT NULL DEFAULT 'set',
+ PRIMARY KEY (scheduled_downtime_id, range_type, range_key),
+ CONSTRAINT icinga_scheduled_downtime_range_scheduled_downtime
+ FOREIGN KEY (scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX scheduled_downtime_range_scheduled_downtime ON icinga_scheduled_downtime_range (scheduled_downtime_id);
+COMMENT ON COLUMN icinga_scheduled_downtime_range.range_key IS 'monday, ...';
+COMMENT ON COLUMN icinga_scheduled_downtime_range.range_value IS '00:00-24:00, ...';
+COMMENT ON COLUMN icinga_scheduled_downtime_range.range_type IS 'include -> ranges {}, exclude ranges_ignore {} - not yet';
+COMMENT ON COLUMN icinga_scheduled_downtime_range.merge_behaviour IS 'set -> = {}, add -> += {}, substract -> -= {}';
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (161, NOW());
diff --git a/schema/pgsql-migrations/upgrade_162.sql b/schema/pgsql-migrations/upgrade_162.sql
new file mode 100644
index 0000000..f90a14e
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_162.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_scheduled_downtime
+ ADD COLUMN with_services enum_boolean NULL DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (162, NOW());
diff --git a/schema/pgsql-migrations/upgrade_164.sql b/schema/pgsql-migrations/upgrade_164.sql
new file mode 100644
index 0000000..eaa3ef0
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_164.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_dependency
+ ADD COLUMN parent_host_var character varying(128) DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (164, NOW());
diff --git a/schema/pgsql-migrations/upgrade_165.sql b/schema/pgsql-migrations/upgrade_165.sql
new file mode 100644
index 0000000..225897f
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_165.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_host
+ ALTER COLUMN address TYPE character varying(255);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (165, NOW());
diff --git a/schema/pgsql-migrations/upgrade_166.sql b/schema/pgsql-migrations/upgrade_166.sql
new file mode 100644
index 0000000..8d2edaf
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_166.sql
@@ -0,0 +1,7 @@
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'scheduledDowntime';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'notification';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'dependency';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (166, NOW());
diff --git a/schema/pgsql-migrations/upgrade_167.sql b/schema/pgsql-migrations/upgrade_167.sql
new file mode 100644
index 0000000..25599cc
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_167.sql
@@ -0,0 +1,24 @@
+CREATE TABLE director_daemon_info (
+ instance_uuid_hex character varying(32) NOT NULL, -- random by daemon
+ schema_version SMALLINT NOT NULL,
+ fqdn character varying(255) NOT NULL,
+ username character varying(64) NOT NULL,
+ pid integer NOT NULL,
+ binary_path character varying(128) NOT NULL,
+ binary_realpath character varying(128) NOT NULL,
+ php_binary_path character varying(128) NOT NULL,
+ php_binary_realpath character varying(128) NOT NULL,
+ php_version character varying(64) NOT NULL,
+ php_integer_size SMALLINT NOT NULL,
+ running_with_systemd enum_boolean DEFAULT NULL,
+ ts_started bigint NOT NULL,
+ ts_stopped bigint DEFAULT NULL,
+ ts_last_modification bigint DEFAULT NULL,
+ ts_last_update bigint NOT NULL,
+ process_info text NOT NULL,
+ PRIMARY KEY (instance_uuid_hex)
+);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (167, NOW());
diff --git a/schema/pgsql-migrations/upgrade_168.sql b/schema/pgsql-migrations/upgrade_168.sql
new file mode 100644
index 0000000..7525a00
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_168.sql
@@ -0,0 +1,25 @@
+
+CREATE TABLE director_datafield_category (
+ id serial,
+ category_name character varying(255) NOT NULL,
+ description text DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX datafield_category_name ON director_datafield_category (category_name);
+
+
+ALTER TABLE director_datafield
+ ADD COLUMN category_id integer DEFAULT NULL,
+ ADD CONSTRAINT director_datafield_category
+ FOREIGN KEY (category_id)
+ REFERENCES director_datafield_category (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+CREATE INDEX datafield_category ON director_datafield (category_id);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (168, NOW());
diff --git a/schema/pgsql-migrations/upgrade_169.sql b/schema/pgsql-migrations/upgrade_169.sql
new file mode 100644
index 0000000..28b68bc
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_169.sql
@@ -0,0 +1,8 @@
+CREATE DOMAIN d_smallint AS integer CHECK (VALUE >= 0) CHECK (VALUE < 65536);
+
+ALTER TABLE icinga_endpoint ALTER COLUMN port TYPE d_smallint;
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (169, NOW());
diff --git a/schema/pgsql-migrations/upgrade_170.sql b/schema/pgsql-migrations/upgrade_170.sql
new file mode 100644
index 0000000..50cfb2b
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_170.sql
@@ -0,0 +1,5 @@
+ALTER TYPE enum_sync_rule_update_policy ADD VALUE 'update-only' AFTER 'ignore';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (170, NOW());
diff --git a/schema/pgsql-migrations/upgrade_171.sql b/schema/pgsql-migrations/upgrade_171.sql
new file mode 100644
index 0000000..76ab309
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_171.sql
@@ -0,0 +1,3 @@
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (171, NOW());
diff --git a/schema/pgsql-migrations/upgrade_172.sql b/schema/pgsql-migrations/upgrade_172.sql
new file mode 100644
index 0000000..49e66a2
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_172.sql
@@ -0,0 +1,13 @@
+CREATE TYPE enum_sync_rule_purge_action AS ENUM('delete', 'disable');
+
+ALTER TABLE sync_rule
+ ADD COLUMN purge_action enum_sync_rule_purge_action NULL DEFAULT NULL;
+
+UPDATE sync_rule SET purge_action = 'delete';
+
+ALTER TABLE sync_rule
+ ALTER COLUMN purge_action SET NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (172, NOW());
diff --git a/schema/pgsql-migrations/upgrade_173.sql b/schema/pgsql-migrations/upgrade_173.sql
new file mode 100644
index 0000000..fdd1b14
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_173.sql
@@ -0,0 +1,6 @@
+ALTER TABLE sync_rule
+ ALTER COLUMN purge_action SET DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (173, NOW());
diff --git a/schema/pgsql-migrations/upgrade_174.sql b/schema/pgsql-migrations/upgrade_174.sql
new file mode 100644
index 0000000..9b5c7ef
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_174.sql
@@ -0,0 +1,61 @@
+ALTER TABLE icinga_zone DROP COLUMN IF EXISTS uuid;
+
+ALTER TABLE icinga_zone ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_zone SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_zone ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_timeperiod ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_timeperiod SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_timeperiod ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_command ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_command SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_command ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_apiuser ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_apiuser SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_apiuser ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_endpoint ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_endpoint SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_endpoint ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_host ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_host SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_host ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_service ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_service SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_service ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_hostgroup ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_hostgroup SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_hostgroup ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_servicegroup ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_servicegroup SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_servicegroup ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_user ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_user SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_user ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_usergroup ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_usergroup SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_usergroup ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_notification ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_notification SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_notification ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_dependency ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_dependency SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_dependency ALTER COLUMN uuid SET NOT NULL;
+
+ALTER TABLE icinga_scheduled_downtime ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_scheduled_downtime SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_scheduled_downtime ALTER COLUMN uuid SET NOT NULL;
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (174, NOW());
diff --git a/schema/pgsql-migrations/upgrade_175.sql b/schema/pgsql-migrations/upgrade_175.sql
new file mode 100644
index 0000000..81234fe
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_175.sql
@@ -0,0 +1,512 @@
+CREATE TABLE director_branch (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ owner character varying(255) NOT NULL,
+ branch_name character varying(255) NOT NULL,
+ description text DEFAULT NULL,
+ ts_merge_request bigint DEFAULT NULL,
+ PRIMARY KEY(uuid)
+);
+CREATE UNIQUE INDEX branch_branch_name ON director_branch (branch_name);
+
+CREATE TYPE enum_branch_action AS ENUM('create', 'modify', 'delete');
+
+CREATE TABLE director_branch_activity (
+ timestamp_ns bigint NOT NULL,
+ object_uuid bytea NOT NULL CHECK(LENGTH(object_uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ action enum_branch_action NOT NULL,
+ object_table character varying(64) NOT NULL,
+ author character varying(255) NOT NULL,
+ former_properties text NOT NULL,
+ modified_properties text NOT NULL,
+ PRIMARY KEY (timestamp_ns),
+ CONSTRAINT branch_activity_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+CREATE INDEX branch_activity_object_uuid ON director_branch_activity (object_uuid);
+CREATE INDEX branch_activity_branch_uuid ON director_branch_activity (branch_uuid);
+
+
+CREATE TABLE branched_icinga_host (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name CHARACTER VARYING(255) DEFAULT NULL,
+ address character varying(255) DEFAULT NULL,
+ address6 character varying(45) DEFAULT NULL,
+ check_command character varying(255) DEFAULT NULL,
+ max_check_attempts integer DEFAULT NULL,
+ check_period character varying(255) DEFAULT NULL,
+ check_interval character varying(8) DEFAULT NULL,
+ retry_interval character varying(8) DEFAULT NULL,
+ check_timeout smallint DEFAULT NULL,
+ enable_notifications enum_boolean DEFAULT NULL,
+ enable_active_checks enum_boolean DEFAULT NULL,
+ enable_passive_checks enum_boolean DEFAULT NULL,
+ enable_event_handler enum_boolean DEFAULT NULL,
+ enable_flapping enum_boolean DEFAULT NULL,
+ enable_perfdata enum_boolean DEFAULT NULL,
+ event_command character varying(255) DEFAULT NULL,
+ flapping_threshold_high smallint default null,
+ flapping_threshold_low smallint default null,
+ volatile enum_boolean DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+ command_endpoint character varying(255) DEFAULT NULL,
+ notes text DEFAULT NULL,
+ notes_url character varying(255) DEFAULT NULL,
+ action_url character varying(255) DEFAULT NULL,
+ icon_image character varying(255) DEFAULT NULL,
+ icon_image_alt character varying(255) DEFAULT NULL,
+ has_agent enum_boolean DEFAULT NULL,
+ master_should_connect enum_boolean DEFAULT NULL,
+ accept_config enum_boolean DEFAULT NULL,
+ api_key character varying(40) DEFAULT NULL,
+ -- template_choice character varying(255) DEFAULT NULL, -- TODO: Forbid them!
+
+ imports TEXT DEFAULT NULL,
+ groups TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_host_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX host_branch_object_name ON branched_icinga_host (branch_uuid, object_name);
+CREATE INDEX branched_host_search_object_name ON branched_icinga_host (object_name);
+CREATE INDEX branched_host_search_display_name ON branched_icinga_host (display_name);
+
+
+CREATE TABLE branched_icinga_hostgroup (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_hostgroup_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX hostgroup_branch_object_name ON branched_icinga_hostgroup (branch_uuid, object_name);
+CREATE INDEX branched_hostgroup_search_object_name ON branched_icinga_hostgroup (object_name);
+CREATE INDEX branched_hostgroup_search_display_name ON branched_icinga_hostgroup (display_name);
+
+
+CREATE TABLE branched_icinga_servicegroup (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_servicegroup_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX servicegroup_branch_object_name ON branched_icinga_servicegroup (branch_uuid, object_name);
+CREATE INDEX branched_servicegroup_search_object_name ON branched_icinga_servicegroup (object_name);
+CREATE INDEX branched_servicegroup_search_display_name ON branched_icinga_servicegroup (display_name);
+
+
+CREATE TABLE branched_icinga_usergroup (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_usergroup_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX usergroup_branch_object_name ON branched_icinga_usergroup (branch_uuid, object_name);
+CREATE INDEX branched_usergroup_search_object_name ON branched_icinga_usergroup (object_name);
+CREATE INDEX branched_usergroup_search_display_name ON branched_icinga_usergroup (display_name);
+
+
+CREATE TABLE branched_icinga_user (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ email character varying(255) DEFAULT NULL,
+ pager character varying(255) DEFAULT NULL,
+ enable_notifications enum_boolean DEFAULT NULL,
+ period character varying(255) DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ groups TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_user_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX user_branch_object_name ON branched_icinga_user (branch_uuid, object_name);
+CREATE INDEX branched_user_search_object_name ON branched_icinga_user (object_name);
+CREATE INDEX branched_user_search_display_name ON branched_icinga_user (display_name);
+
+
+CREATE TABLE branched_icinga_zone (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ parent character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ is_global enum_boolean DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_zone_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX zone_branch_object_name ON branched_icinga_zone (branch_uuid, object_name);
+CREATE INDEX branched_zone_search_object_name ON branched_icinga_zone (object_name);
+
+
+CREATE TABLE branched_icinga_timeperiod (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ update_method character varying(64) DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ prefer_includes enum_boolean DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ ranges TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_timeperiod_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX timeperiod_branch_object_name ON branched_icinga_timeperiod (branch_uuid, object_name);
+CREATE INDEX branched_timeperiod_search_object_name ON branched_icinga_timeperiod (object_name);
+CREATE INDEX branched_timeperiod_search_display_name ON branched_icinga_timeperiod (display_name);
+
+
+CREATE TABLE branched_icinga_command (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean NOT NULL DEFAULT NULL,
+ methods_execute character varying(64) DEFAULT NULL,
+ command text DEFAULT NULL,
+ is_string enum_boolean DEFAULT NULL,
+-- env text DEFAULT NULL,
+ timeout smallint DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ arguments TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_command_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX command_branch_object_name ON branched_icinga_command (branch_uuid, object_name);
+CREATE INDEX branched_command_search_object_name ON branched_icinga_command (object_name);
+
+
+CREATE TABLE branched_icinga_apiuser (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name CHARACTER VARYING(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean NOT NULL DEFAULT NULL,
+ password CHARACTER VARYING(255) DEFAULT NULL,
+ client_dn CHARACTER VARYING(64) DEFAULT NULL,
+ permissions TEXT DEFAULT NULL,
+
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_apiuser_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX apiuser_branch_object_name ON branched_icinga_apiuser (branch_uuid, object_name);
+CREATE INDEX branched_apiuser_search_object_name ON branched_icinga_apiuser (object_name);
+
+
+CREATE TABLE branched_icinga_endpoint (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ zone character varying(255) DEFAULT NULL,
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean NOT NULL DEFAULT NULL,
+ host character varying(255) DEFAULT NULL,
+ port d_smallint DEFAULT NULL,
+ log_duration character varying(32) DEFAULT NULL,
+ apiuser character varying(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_endpoint_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX endpoint_branch_object_name ON branched_icinga_endpoint (branch_uuid, object_name);
+CREATE INDEX branched_endpoint_search_object_name ON branched_icinga_endpoint (object_name);
+
+
+CREATE TABLE branched_icinga_service (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ host character varying(255) DEFAULT NULL,
+ service_set character varying(255) DEFAULT NULL,
+ check_command character varying(255) DEFAULT NULL,
+ max_check_attempts integer DEFAULT NULL,
+ check_period character varying(255) DEFAULT NULL,
+ check_interval character varying(8) DEFAULT NULL,
+ retry_interval character varying(8) DEFAULT NULL,
+ check_timeout smallint DEFAULT NULL,
+ enable_notifications enum_boolean DEFAULT NULL,
+ enable_active_checks enum_boolean DEFAULT NULL,
+ enable_passive_checks enum_boolean DEFAULT NULL,
+ enable_event_handler enum_boolean DEFAULT NULL,
+ enable_flapping enum_boolean DEFAULT NULL,
+ enable_perfdata enum_boolean DEFAULT NULL,
+ event_command character varying(255) DEFAULT NULL,
+ flapping_threshold_high smallint DEFAULT NULL,
+ flapping_threshold_low smallint DEFAULT NULL,
+ volatile enum_boolean DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+ command_endpoint character varying(255) DEFAULT NULL,
+ notes text DEFAULT NULL,
+ notes_url character varying(255) DEFAULT NULL,
+ action_url character varying(255) DEFAULT NULL,
+ icon_image character varying(255) DEFAULT NULL,
+ icon_image_alt character varying(255) DEFAULT NULL,
+ use_agent enum_boolean DEFAULT NULL,
+ apply_for character varying(255) DEFAULT NULL,
+ use_var_overrides enum_boolean DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ -- template_choice_id int DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ groups TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_service_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_branch_object_name ON branched_icinga_service (branch_uuid, object_name);
+CREATE INDEX branched_service_search_object_name ON branched_icinga_service (object_name);
+CREATE INDEX branched_service_search_display_name ON branched_icinga_service (display_name);
+
+
+CREATE TABLE branched_icinga_notification (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name CHARACTER VARYING(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ apply_to enum_host_service DEFAULT NULL,
+ host character varying(255) DEFAULT NULL,
+ service character varying(255) DEFAULT NULL,
+ times_begin integer DEFAULT NULL,
+ times_end integer DEFAULT NULL,
+ notification_interval integer DEFAULT NULL,
+ command character varying(255) DEFAULT NULL,
+ period character varying(255) DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+
+ states TEXT DEFAULT NULL,
+ types TEXT DEFAULT NULL,
+ users TEXT DEFAULT NULL,
+ usergroups TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_notification_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX notification_branch_object_name ON branched_icinga_notification (branch_uuid, object_name);
+CREATE INDEX branched_notification_search_object_name ON branched_icinga_notification (object_name);
+
+
+CREATE TABLE branched_icinga_scheduled_downtime (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ apply_to enum_host_service DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ author character varying(255) DEFAULT NULL,
+ comment text DEFAULT NULL,
+ fixed enum_boolean DEFAULT NULL,
+ duration int DEFAULT NULL,
+ with_services enum_boolean DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ ranges TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_scheduled_downtime_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX scheduled_downtime_branch_object_name ON branched_icinga_scheduled_downtime (branch_uuid, object_name);
+CREATE INDEX branched_scheduled_downtime_search_object_name ON branched_icinga_scheduled_downtime (object_name);
+
+
+CREATE TABLE branched_icinga_dependency (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean DEFAULT 'n',
+ apply_to enum_host_service NULL DEFAULT NULL,
+ parent_host character varying(255) DEFAULT NULL,
+ parent_host_var character varying(128) DEFAULT NULL,
+ parent_service character varying(255) DEFAULT NULL,
+ child_host character varying(255) DEFAULT NULL,
+ child_service character varying(255) DEFAULT NULL,
+ disable_checks enum_boolean DEFAULT NULL,
+ disable_notifications enum_boolean DEFAULT NULL,
+ ignore_soft_states enum_boolean DEFAULT NULL,
+ period_id integer DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ parent_service_by_name character varying(255),
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_dependency_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX dependency_branch_object_name ON branched_icinga_dependency (branch_uuid, object_name);
+CREATE INDEX branched_dependency_search_object_name ON branched_icinga_dependency (object_name);
+
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (175, NOW());
diff --git a/schema/pgsql-migrations/upgrade_176.sql b/schema/pgsql-migrations/upgrade_176.sql
new file mode 100644
index 0000000..eadcb18
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_176.sql
@@ -0,0 +1,6 @@
+ALTER TABLE icinga_host ADD COLUMN custom_endpoint_name character varying(255) DEFAULT NULL;
+ALTER TABLE branched_icinga_host ADD COLUMN custom_endpoint_name character varying(255) DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES ('176', NOW());
diff --git a/schema/pgsql-migrations/upgrade_177.sql b/schema/pgsql-migrations/upgrade_177.sql
new file mode 100644
index 0000000..09784b1
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_177.sql
@@ -0,0 +1,8 @@
+ALTER TABLE icinga_service_set ADD COLUMN uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16);
+UPDATE icinga_service_set SET uuid = decode(replace(gen_random_uuid()::text, '-', ''), 'hex') WHERE uuid IS NULL;
+ALTER TABLE icinga_service_set ALTER COLUMN uuid SET NOT NULL;
+CREATE UNIQUE INDEX service_set_uuid ON icinga_service_set (uuid);
+
+INSERT INTO director_schema_migration
+(schema_version, migration_time)
+VALUES (177, NOW());
diff --git a/schema/pgsql-migrations/upgrade_178.sql b/schema/pgsql-migrations/upgrade_178.sql
new file mode 100644
index 0000000..8419384
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_178.sql
@@ -0,0 +1,23 @@
+CREATE TABLE director_activity_log_remark (
+ first_related_activity bigint NOT NULL,
+ last_related_activity bigint NOT NULL,
+ remark TEXT NOT NULL,
+ PRIMARY KEY (first_related_activity, last_related_activity),
+ CONSTRAINT activity_log_remark_begin
+ FOREIGN KEY (first_related_activity)
+ REFERENCES director_activity_log (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT activity_log_remark_end
+ FOREIGN KEY (last_related_activity)
+ REFERENCES director_activity_log (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX first_related_activity ON director_activity_log_remark (first_related_activity);
+CREATE INDEX last_related_activity ON director_activity_log_remark (last_related_activity);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (178, NOW());
diff --git a/schema/pgsql-migrations/upgrade_179.sql b/schema/pgsql-migrations/upgrade_179.sql
new file mode 100644
index 0000000..d050eee
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_179.sql
@@ -0,0 +1,5 @@
+CREATE INDEX start_time_idx ON director_deployment_log (start_time);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (179, NOW());
diff --git a/schema/pgsql-migrations/upgrade_180.sql b/schema/pgsql-migrations/upgrade_180.sql
new file mode 100644
index 0000000..b6ae70d
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_180.sql
@@ -0,0 +1,32 @@
+CREATE TABLE branched_icinga_service_set (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ host character varying(255) DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+
+
+ imports TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_service_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_set_branch_object_name ON branched_icinga_service_set (branch_uuid, object_name);
+CREATE INDEX branched_service_set_search_object_name ON branched_icinga_service_set (object_name);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (180, NOW());
diff --git a/schema/pgsql-migrations/upgrade_181.sql b/schema/pgsql-migrations/upgrade_181.sql
new file mode 100644
index 0000000..0fdd2a6
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_181.sql
@@ -0,0 +1,19 @@
+ALTER TABLE branched_icinga_host DROP CONSTRAINT branched_icinga_host_uuid_key;
+ALTER TABLE branched_icinga_hostgroup DROP CONSTRAINT branched_icinga_hostgroup_uuid_key;
+ALTER TABLE branched_icinga_servicegroup DROP CONSTRAINT branched_icinga_servicegroup_uuid_key;
+ALTER TABLE branched_icinga_usergroup DROP CONSTRAINT branched_icinga_usergroup_uuid_key;
+ALTER TABLE branched_icinga_user DROP CONSTRAINT branched_icinga_user_uuid_key;
+ALTER TABLE branched_icinga_zone DROP CONSTRAINT branched_icinga_zone_uuid_key;
+ALTER TABLE branched_icinga_timeperiod DROP CONSTRAINT branched_icinga_timeperiod_uuid_key;
+ALTER TABLE branched_icinga_command DROP CONSTRAINT branched_icinga_command_uuid_key;
+ALTER TABLE branched_icinga_apiuser DROP CONSTRAINT branched_icinga_apiuser_uuid_key;
+ALTER TABLE branched_icinga_endpoint DROP CONSTRAINT branched_icinga_endpoint_uuid_key;
+ALTER TABLE branched_icinga_service DROP CONSTRAINT branched_icinga_service_uuid_key;
+ALTER TABLE branched_icinga_service_set DROP CONSTRAINT branched_icinga_service_set_uuid_key;
+ALTER TABLE branched_icinga_notification DROP CONSTRAINT branched_icinga_notification_uuid_key;
+ALTER TABLE branched_icinga_scheduled_downtime DROP CONSTRAINT branched_icinga_scheduled_downtime_uuid_key;
+ALTER TABLE branched_icinga_dependency DROP CONSTRAINT branched_icinga_dependency_uuid_key;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (181, NOW());
diff --git a/schema/pgsql-migrations/upgrade_182.sql b/schema/pgsql-migrations/upgrade_182.sql
new file mode 100644
index 0000000..634d048
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_182.sql
@@ -0,0 +1,14 @@
+DELETE FROM sync_run AS sr
+ WHERE EXISTS (
+ SELECT 1 FROM sync_rule AS s
+ WHERE s.id = sr.rule_id
+ AND s.object_type != 'datalistEntry'
+ AND sr.start_time > '2022-09-21 00:00:00'
+ ) AND sr.last_former_activity = sr.last_related_activity;
+
+DELETE FROM sync_run
+ WHERE (objects_created + objects_deleted + objects_modified) = 0;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (182, NOW());
diff --git a/schema/pgsql-migrations/upgrade_77.sql b/schema/pgsql-migrations/upgrade_77.sql
new file mode 100644
index 0000000..de10121
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_77.sql
@@ -0,0 +1,72 @@
+ALTER TYPE enum_merge_behaviour ADD VALUE 'override';
+
+
+CREATE TABLE icinga_notification_states_set (
+ notification_id integer NOT NULL,
+ property enum_state_name NOT NULL,
+ merge_behaviour enum_merge_behaviour NOT NULL DEFAULT 'override',
+ PRIMARY KEY (notification_id, property, merge_behaviour),
+ CONSTRAINT icinga_notification_states_set_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+COMMENT ON COLUMN icinga_notification_states_set.merge_behaviour IS 'override: = [], extend: += [], blacklist: -= []';
+
+
+CREATE TABLE icinga_notification_types_set (
+ notification_id integer NOT NULL,
+ property enum_type_name NOT NULL,
+ merge_behaviour enum_merge_behaviour NOT NULL DEFAULT 'override',
+ PRIMARY KEY (notification_id, property, merge_behaviour),
+ CONSTRAINT icinga_notification_types_set_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+COMMENT ON COLUMN icinga_notification_types_set.merge_behaviour IS 'override: = [], extend: += [], blacklist: -= []';
+
+
+CREATE TABLE icinga_notification_var (
+ notification_id integer NOT NULL,
+ varname VARCHAR(255) DEFAULT NULL,
+ varvalue TEXT DEFAULT NULL,
+ format enum_property_format,
+ PRIMARY KEY (notification_id, varname),
+ CONSTRAINT icinga_notification_var_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX notification_var_search_idx ON icinga_notification_var (varname);
+
+
+CREATE TABLE icinga_notification_inheritance (
+ notification_id integer NOT NULL,
+ parent_notification_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (notification_id, parent_notification_id),
+ CONSTRAINT icinga_notification_inheritance_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_inheritance_parent_notification
+ FOREIGN KEY (parent_notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX notification_inheritance ON icinga_notification_inheritance (notification_id, weight);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (77, NOW());
diff --git a/schema/pgsql-migrations/upgrade_78.sql b/schema/pgsql-migrations/upgrade_78.sql
new file mode 100644
index 0000000..a95d294
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_78.sql
@@ -0,0 +1,25 @@
+CREATE TABLE icinga_user_field (
+ user_id integer NOT NULL,
+ datafield_id integer NOT NULL,
+ is_required enum_boolean NOT NULL,
+ PRIMARY KEY (user_id, datafield_id),
+ CONSTRAINT icinga_user_field_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_field_datafield
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX user_field_key ON icinga_user_field (user_id, datafield_id);
+CREATE INDEX user_field_user ON icinga_user_field (user_id);
+CREATE INDEX user_field_datafield ON icinga_user_field (datafield_id);
+COMMENT ON COLUMN icinga_user_field.user_id IS 'Makes only sense for templates';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (78, NOW());
diff --git a/schema/pgsql-migrations/upgrade_79.sql b/schema/pgsql-migrations/upgrade_79.sql
new file mode 100644
index 0000000..2b9551d
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_79.sql
@@ -0,0 +1,11 @@
+ALTER TABLE icinga_user_states_set
+ DROP CONSTRAINT icinga_user_states_set_pkey,
+ ADD PRIMARY KEY (user_id, property, merge_behaviour);
+
+ALTER TABLE icinga_user_types_set
+ DROP CONSTRAINT icinga_user_types_set_pkey,
+ ADD PRIMARY KEY (user_id, property, merge_behaviour);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (79, NOW());
diff --git a/schema/pgsql-migrations/upgrade_80.sql b/schema/pgsql-migrations/upgrade_80.sql
new file mode 100644
index 0000000..074af1c
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_80.sql
@@ -0,0 +1,11 @@
+ALTER TABLE icinga_timeperiod_range
+ DROP CONSTRAINT icinga_timeperiod_range_timeperiod,
+ ADD CONSTRAINT icinga_timeperiod_range_timeperiod
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (80, NOW());
diff --git a/schema/pgsql-migrations/upgrade_81.sql b/schema/pgsql-migrations/upgrade_81.sql
new file mode 100644
index 0000000..6a52a46
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_81.sql
@@ -0,0 +1,5 @@
+ALTER TABLE import_run ALTER COLUMN end_time DROP NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (81, NOW());
diff --git a/schema/pgsql-migrations/upgrade_82.sql b/schema/pgsql-migrations/upgrade_82.sql
new file mode 100644
index 0000000..0c6f5b8
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_82.sql
@@ -0,0 +1,12 @@
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'service' AFTER 'host';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'command' AFTER 'service';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'hostgroup';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'servicegroup';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'usergroup';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'datalistEntry';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'endpoint';
+ALTER TYPE enum_sync_rule_object_type ADD VALUE 'zone';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (82, NOW());
diff --git a/schema/pgsql-migrations/upgrade_83.sql b/schema/pgsql-migrations/upgrade_83.sql
new file mode 100644
index 0000000..2a2fecf
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_83.sql
@@ -0,0 +1,7 @@
+ALTER TABLE icinga_command ALTER COLUMN command TYPE text;
+ALTER TABLE icinga_command ALTER COLUMN command DROP DEFAULT;
+ALTER TABLE icinga_command ALTER COLUMN command SET DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (83, NOW());
diff --git a/schema/pgsql-migrations/upgrade_84.sql b/schema/pgsql-migrations/upgrade_84.sql
new file mode 100644
index 0000000..1de287a
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_84.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_usergroup DROP COLUMN zone_id;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (84, NOW());
diff --git a/schema/pgsql-migrations/upgrade_85.sql b/schema/pgsql-migrations/upgrade_85.sql
new file mode 100644
index 0000000..6aca709
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_85.sql
@@ -0,0 +1,15 @@
+CREATE TABLE icinga_notification_assignment (
+ id bigserial,
+ notification_id integer NOT NULL,
+ filter_string TEXT NOT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_notification_assignment
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (85, NOW());
diff --git a/schema/pgsql-migrations/upgrade_86.sql b/schema/pgsql-migrations/upgrade_86.sql
new file mode 100644
index 0000000..9672f3e
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_86.sql
@@ -0,0 +1,35 @@
+CREATE TABLE icinga_notification_user (
+ notification_id integer NOT NULL,
+ user_id integer NOT NULL,
+ PRIMARY KEY (notification_id, user_id),
+ CONSTRAINT icinga_notification_user_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_user_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE icinga_notification_usergroup (
+ notification_id integer NOT NULL,
+ usergroup_id integer NOT NULL,
+ PRIMARY KEY (notification_id, usergroup_id),
+ CONSTRAINT icinga_notification_usergroup_usergroup
+ FOREIGN KEY (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_usergroup_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (86, NOW());
diff --git a/schema/pgsql-migrations/upgrade_88.sql b/schema/pgsql-migrations/upgrade_88.sql
new file mode 100644
index 0000000..1f65333
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_88.sql
@@ -0,0 +1,6 @@
+ALTER TABLE director_generated_config_file
+ ALTER COLUMN file_path TYPE character varying(128);
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (88, NOW());
diff --git a/schema/pgsql-migrations/upgrade_89.sql b/schema/pgsql-migrations/upgrade_89.sql
new file mode 100644
index 0000000..58b1542
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_89.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_command_argument ADD required enum_boolean DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (89, NOW());
diff --git a/schema/pgsql-migrations/upgrade_90.sql b/schema/pgsql-migrations/upgrade_90.sql
new file mode 100644
index 0000000..e2a6f09
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_90.sql
@@ -0,0 +1,6 @@
+CREATE TYPE enum_assign_type AS ENUM('assign', 'ignore');
+ALTER TABLE icinga_service_assignment ADD assign_type enum_assign_type NOT NULL DEFAULT 'assign';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (90, NOW());
diff --git a/schema/pgsql-migrations/upgrade_91.sql b/schema/pgsql-migrations/upgrade_91.sql
new file mode 100644
index 0000000..c20a24a
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_91.sql
@@ -0,0 +1,5 @@
+ALTER TABLE icinga_notification_assignment ADD assign_type enum_assign_type NOT NULL DEFAULT 'assign';
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (91, NOW());
diff --git a/schema/pgsql-migrations/upgrade_92.sql b/schema/pgsql-migrations/upgrade_92.sql
new file mode 100644
index 0000000..670a996
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_92.sql
@@ -0,0 +1,27 @@
+DELETE FROM director_datalist_entry WHERE entry_name IS NULL;
+ALTER TABLE director_datalist_entry ALTER COLUMN entry_name DROP DEFAULT;
+ALTER TABLE director_datalist_entry ALTER COLUMN entry_name SET NOT NULL;
+
+DELETE FROM icinga_command_var WHERE varname IS NULL;
+ALTER TABLE icinga_command_var ALTER COLUMN varname DROP DEFAULT;
+ALTER TABLE icinga_command_var ALTER COLUMN varname SET NOT NULL;
+
+DELETE FROM icinga_host_var WHERE varname IS NULL;
+ALTER TABLE icinga_host_var ALTER COLUMN varname DROP DEFAULT;
+ALTER TABLE icinga_host_var ALTER COLUMN varname SET NOT NULL;
+
+DELETE FROM icinga_service_var WHERE varname IS NULL;
+ALTER TABLE icinga_service_var ALTER COLUMN varname DROP DEFAULT;
+ALTER TABLE icinga_service_var ALTER COLUMN varname SET NOT NULL;
+
+DELETE FROM icinga_user_var WHERE varname IS NULL;
+ALTER TABLE icinga_user_var ALTER COLUMN varname DROP DEFAULT;
+ALTER TABLE icinga_user_var ALTER COLUMN varname SET NOT NULL;
+
+DELETE FROM icinga_notification_var WHERE varname IS NULL;
+ALTER TABLE icinga_notification_var ALTER COLUMN varname DROP DEFAULT;
+ALTER TABLE icinga_notification_var ALTER COLUMN varname SET NOT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (92, NOW());
diff --git a/schema/pgsql-migrations/upgrade_93.sql b/schema/pgsql-migrations/upgrade_93.sql
new file mode 100644
index 0000000..680c3f6
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_93.sql
@@ -0,0 +1,24 @@
+CREATE TYPE enum_sync_state AS ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+);
+
+ALTER TABLE sync_rule
+ ADD COLUMN sync_state enum_sync_state NOT NULL DEFAULT 'unknown',
+ ADD COLUMN last_error_message character varying(255) NULL DEFAULT NULL,
+ ADD COLUMN last_attempt timestamp with time zone NULL DEFAULT NULL
+;
+
+UPDATE sync_rule
+ SET last_attempt = lr.start_time
+ FROM (
+ SELECT rule_id, MAX(start_time) AS start_time
+ FROM sync_run
+ GROUP BY rule_id
+ ) lr WHERE sync_rule.id = lr.rule_id;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (93, NOW());
diff --git a/schema/pgsql-migrations/upgrade_94.sql b/schema/pgsql-migrations/upgrade_94.sql
new file mode 100644
index 0000000..5341a8b
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_94.sql
@@ -0,0 +1,34 @@
+CREATE TABLE director_job (
+ id serial,
+ job_name character varying(64) NOT NULL,
+ job_class character varying(72) NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ run_interval integer NOT NULL, -- seconds
+ last_attempt_succeeded enum_boolean DEFAULT NULL,
+ ts_last_attempt timestamp with time zone DEFAULT NULL,
+ ts_last_error timestamp with time zone DEFAULT NULL,
+ last_error_message text NULL DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX director_job_unique_job_name ON director_job (job_name);
+
+
+CREATE TABLE director_job_setting (
+ job_id integer NOT NULL,
+ setting_name character varying(64) NOT NULL,
+ setting_value text DEFAULT NULL,
+ PRIMARY KEY (job_id, setting_name),
+ CONSTRAINT director_job_setting_job
+ FOREIGN KEY (job_id)
+ REFERENCES director_job (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX director_job_setting_job ON director_job_setting (job_id);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (94, NOW());
diff --git a/schema/pgsql-migrations/upgrade_95.sql b/schema/pgsql-migrations/upgrade_95.sql
new file mode 100644
index 0000000..21a212b
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_95.sql
@@ -0,0 +1,20 @@
+ALTER TABLE import_source
+ ADD COLUMN import_state enum_sync_state NOT NULL DEFAULT 'unknown',
+ ADD COLUMN last_error_message character varying(255) NULL DEFAULT NULL,
+ ADD COLUMN last_attempt timestamp with time zone NULL DEFAULT NULL
+;
+
+
+UPDATE import_source
+ SET last_attempt = ir.start_time
+ FROM (
+ SELECT source_id, MAX(start_time) AS start_time
+ FROM import_run
+ GROUP BY source_id
+ ) ir WHERE import_source.id = ir.source_id;
+
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (95, NOW());
diff --git a/schema/pgsql-migrations/upgrade_96.sql b/schema/pgsql-migrations/upgrade_96.sql
new file mode 100644
index 0000000..f96daa7
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_96.sql
@@ -0,0 +1,7 @@
+CREATE TYPE enum_host_service AS ENUM('host', 'service');
+
+ALTER TABLE icinga_notification ADD apply_to enum_host_service NULL DEFAULT NULL;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (96, NOW());
diff --git a/schema/pgsql-migrations/upgrade_97.sql b/schema/pgsql-migrations/upgrade_97.sql
new file mode 100644
index 0000000..5c9f1bc
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_97.sql
@@ -0,0 +1,11 @@
+ALTER TABLE director_job
+ ADD COLUMN timeperiod_id integer DEFAULT NULL,
+ ADD CONSTRAINT director_job_period
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (97, NOW());
diff --git a/schema/pgsql-migrations/upgrade_98.sql b/schema/pgsql-migrations/upgrade_98.sql
new file mode 100644
index 0000000..006ae88
--- /dev/null
+++ b/schema/pgsql-migrations/upgrade_98.sql
@@ -0,0 +1,7 @@
+CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS '
+ SELECT EXTRACT(EPOCH FROM $1)::bigint AS result
+' LANGUAGE sql;
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (98, NOW());
diff --git a/schema/pgsql.sql b/schema/pgsql.sql
new file mode 100644
index 0000000..b9b2cf8
--- /dev/null
+++ b/schema/pgsql.sql
@@ -0,0 +1,2781 @@
+--
+-- PostgreSQL schema
+-- =================
+--
+-- You should normally not be required to care about schema handling.
+-- Director does all the migrations for you and guides you either in
+-- the frontend or provides everything you need for automated migration
+-- handling. Please find more related information in our documentation.
+
+CREATE TYPE enum_activity_action AS ENUM('create', 'delete', 'modify');
+CREATE TYPE enum_boolean AS ENUM('y', 'n');
+CREATE TYPE enum_property_format AS ENUM('string', 'expression', 'json');
+CREATE TYPE enum_object_type_all AS ENUM('object', 'template', 'apply', 'external_object'); -- TODO: can we check for an invalid
+CREATE TYPE enum_object_type AS ENUM('object', 'template', 'external_object');
+CREATE TYPE enum_timeperiod_range_type AS ENUM('include', 'exclude');
+CREATE TYPE enum_merge_behaviour AS ENUM('set', 'add', 'substract', 'override');
+CREATE TYPE enum_set_merge_behaviour AS ENUM('override', 'extend', 'blacklist');
+CREATE TYPE enum_command_object_type AS ENUM('object', 'template', 'external_object');
+CREATE TYPE enum_apply_object_type AS ENUM('object', 'template', 'apply', 'external_object');
+CREATE TYPE enum_state_name AS ENUM('OK', 'Warning', 'Critical', 'Unknown', 'Up', 'Down');
+CREATE TYPE enum_type_name AS ENUM('DowntimeStart', 'DowntimeEnd', 'DowntimeRemoved', 'Custom', 'Acknowledgement', 'Problem', 'Recovery', 'FlappingStart', 'FlappingEnd');
+CREATE TYPE enum_sync_rule_object_type AS ENUM(
+ 'host',
+ 'service',
+ 'command',
+ 'user',
+ 'hostgroup',
+ 'servicegroup',
+ 'usergroup',
+ 'datalistEntry',
+ 'endpoint',
+ 'zone',
+ 'timePeriod',
+ 'serviceSet',
+ 'scheduledDowntime',
+ 'notification',
+ 'dependency'
+);
+CREATE TYPE enum_sync_rule_update_policy AS ENUM('merge', 'override', 'ignore', 'update-only');
+CREATE TYPE enum_sync_rule_purge_action AS ENUM('delete', 'disable');
+CREATE TYPE enum_sync_property_merge_policy AS ENUM('override', 'merge');
+CREATE TYPE enum_sync_state AS ENUM(
+ 'unknown',
+ 'in-sync',
+ 'pending-changes',
+ 'failing'
+);
+CREATE TYPE enum_host_service AS ENUM('host', 'service');
+CREATE TYPE enum_owner_type AS ENUM('user', 'usergroup', 'role');
+CREATE DOMAIN d_smallint AS integer CHECK (VALUE >= 0) CHECK (VALUE < 65536);
+
+CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS '
+ SELECT EXTRACT(EPOCH FROM $1)::bigint AS result
+' LANGUAGE sql;
+
+
+CREATE TABLE director_daemon_info (
+ instance_uuid_hex character varying(32) NOT NULL, -- random by daemon
+ schema_version SMALLINT NOT NULL,
+ fqdn character varying(255) NOT NULL,
+ username character varying(64) NOT NULL,
+ pid integer NOT NULL,
+ binary_path character varying(128) NOT NULL,
+ binary_realpath character varying(128) NOT NULL,
+ php_binary_path character varying(128) NOT NULL,
+ php_binary_realpath character varying(128) NOT NULL,
+ php_version character varying(64) NOT NULL,
+ php_integer_size SMALLINT NOT NULL,
+ running_with_systemd enum_boolean DEFAULT NULL,
+ ts_started bigint NOT NULL,
+ ts_stopped bigint DEFAULT NULL,
+ ts_last_modification bigint DEFAULT NULL,
+ ts_last_update bigint NOT NULL,
+ process_info text NOT NULL,
+ PRIMARY KEY (instance_uuid_hex)
+);
+
+
+CREATE TABLE director_activity_log (
+ id bigserial,
+ object_type character varying(64) NOT NULL,
+ object_name character varying(255) NOT NULL,
+ action_name enum_activity_action NOT NULL,
+ old_properties text DEFAULT NULL,
+ new_properties text DEFAULT NULL,
+ author character varying(64) NOT NULL,
+ change_time timestamp with time zone NOT NULL,
+ checksum bytea NOT NULL UNIQUE CHECK(LENGTH(checksum) = 20),
+ parent_checksum bytea DEFAULT NULL CHECK(parent_checksum IS NULL OR LENGTH(checksum) = 20),
+ PRIMARY KEY (id)
+);
+
+CREATE INDEX activity_log_sort_idx ON director_activity_log (change_time);
+CREATE INDEX activity_log_search_idx ON director_activity_log (object_name);
+CREATE INDEX activity_log_search_idx2 ON director_activity_log (object_type, object_name, change_time);
+CREATE INDEX activity_log_author ON director_activity_log (author);
+COMMENT ON COLUMN director_activity_log.old_properties IS 'Property hash, JSON';
+COMMENT ON COLUMN director_activity_log.new_properties IS 'Property hash, JSON';
+
+CREATE TABLE director_activity_log_remark (
+ first_related_activity bigint NOT NULL,
+ last_related_activity bigint NOT NULL,
+ remark TEXT NOT NULL,
+ PRIMARY KEY (first_related_activity, last_related_activity),
+ CONSTRAINT activity_log_remark_begin
+ FOREIGN KEY (first_related_activity)
+ REFERENCES director_activity_log (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT activity_log_remark_end
+ FOREIGN KEY (last_related_activity)
+ REFERENCES director_activity_log (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX first_related_activity ON director_activity_log_remark (first_related_activity);
+CREATE INDEX last_related_activity ON director_activity_log_remark (last_related_activity);
+
+
+CREATE TABLE director_basket (
+ uuid bytea CHECK(LENGTH(uuid) = 16) NOT NULL,
+ basket_name VARCHAR(64) NOT NULL,
+ owner_type enum_owner_type NOT NULL,
+ owner_value VARCHAR(255) NOT NULL,
+ objects text NOT NULL, -- json-encoded
+ PRIMARY KEY (uuid)
+);
+
+CREATE UNIQUE INDEX basket_basket_name ON director_basket (basket_name);
+
+
+CREATE TABLE director_basket_content (
+ checksum bytea CHECK(LENGTH(checksum) = 20) NOT NULL,
+ summary VARCHAR(500) NOT NULL, -- json
+ content text NOT NULL, -- json
+ PRIMARY KEY (checksum)
+);
+
+
+CREATE TABLE director_basket_snapshot (
+ basket_uuid bytea CHECK(LENGTH(basket_uuid) = 16) NOT NULL,
+ ts_create bigint NOT NULL,
+ content_checksum bytea CHECK(LENGTH(content_checksum) = 20) NOT NULL,
+ PRIMARY KEY (basket_uuid, ts_create),
+ CONSTRAINT basked_snapshot_basket
+ FOREIGN KEY (basket_uuid)
+ REFERENCES director_basket (uuid)
+ ON DELETE CASCADE
+ ON UPDATE RESTRICT,
+ CONSTRAINT basked_snapshot_content
+ FOREIGN KEY (content_checksum)
+ REFERENCES director_basket_content (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX basket_snapshot_sort_idx ON director_basket_snapshot (ts_create);
+
+
+CREATE TABLE director_generated_config (
+ checksum bytea CHECK(LENGTH(checksum) = 20),
+ director_version character varying(64) DEFAULT NULL,
+ director_db_version integer DEFAULT NULL,
+ duration integer DEFAULT NULL,
+ first_activity_checksum bytea NOT NULL CHECK(LENGTH(first_activity_checksum) = 20),
+ last_activity_checksum bytea NOT NULL CHECK(LENGTH(last_activity_checksum) = 20),
+ PRIMARY KEY (checksum),
+ CONSTRAINT director_generated_config_activity
+ FOREIGN KEY (last_activity_checksum)
+ REFERENCES director_activity_log (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX activity_checksum ON director_generated_config (last_activity_checksum);
+COMMENT ON COLUMN director_generated_config.checksum IS 'SHA1(last_activity_checksum;file_path=checksum;file_path=checksum;...)';
+COMMENT ON COLUMN director_generated_config.duration IS 'Config generation duration (ms)';
+
+
+CREATE TABLE director_generated_file (
+ checksum bytea CHECK(LENGTH(checksum) = 20),
+ content text DEFAULT NULL,
+ cnt_object SMALLINT NOT NULL DEFAULT 0,
+ cnt_template SMALLINT NOT NULL DEFAULT 0,
+ cnt_apply SMALLINT NOT NULL DEFAULT 0,
+ PRIMARY KEY (checksum)
+);
+
+COMMENT ON COLUMN director_generated_file.checksum IS 'SHA1(content)';
+
+
+CREATE TABLE director_generated_config_file (
+ config_checksum bytea CHECK(LENGTH(config_checksum) = 20),
+ file_checksum bytea CHECK(LENGTH(file_checksum) = 20),
+ file_path character varying(128) NOT NULL,
+ PRIMARY KEY (config_checksum, file_path),
+ CONSTRAINT director_generated_config_file_config
+ FOREIGN KEY (config_checksum)
+ REFERENCES director_generated_config (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT director_generated_config_file_file
+ FOREIGN KEY (file_checksum)
+ REFERENCES director_generated_file (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX config ON director_generated_config_file (config_checksum);
+CREATE INDEX checksum ON director_generated_config_file (file_checksum);
+COMMENT ON COLUMN director_generated_config_file.file_path IS 'e.g. zones/nafta/hosts.conf';
+
+
+CREATE TABLE director_deployment_log (
+ id bigserial,
+ config_checksum bytea CHECK(LENGTH(config_checksum) = 20),
+ last_activity_checksum bytea CHECK(LENGTH(config_checksum) = 20),
+ peer_identity character varying(64) NOT NULL,
+ start_time timestamp with time zone NOT NULL,
+ end_time timestamp with time zone DEFAULT NULL,
+ abort_time timestamp with time zone DEFAULT NULL,
+ duration_connection integer DEFAULT NULL,
+ duration_dump integer DEFAULT NULL,
+ stage_name CHARACTER VARYING(96),
+ stage_collected enum_boolean DEFAULT NULL,
+ connection_succeeded enum_boolean DEFAULT NULL,
+ dump_succeeded enum_boolean DEFAULT NULL,
+ startup_succeeded enum_boolean DEFAULT NULL,
+ username character varying(64) DEFAULT NULL,
+ startup_log text DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT config_checksum
+ FOREIGN KEY (config_checksum)
+ REFERENCES director_generated_config (checksum)
+ ON DELETE SET NULL
+ ON UPDATE RESTRICT
+);
+
+COMMENT ON COLUMN director_deployment_log.duration_connection IS 'The time it took to connect to an Icinga node (ms)';
+COMMENT ON COLUMN director_deployment_log.duration_dump IS 'Time spent dumping the config (ms)';
+COMMENT ON COLUMN director_deployment_log.username IS 'The user that triggered this deployment';
+
+CREATE INDEX start_time_idx ON director_deployment_log (start_time);
+
+
+CREATE TABLE director_datalist (
+ id serial,
+ list_name character varying(255) NOT NULL,
+ owner character varying(255) NOT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX datalist_list_name ON director_datalist (list_name);
+
+
+CREATE TABLE director_datalist_entry (
+ list_id integer NOT NULL,
+ entry_name character varying(255) NOT NULL,
+ entry_value text DEFAULT NULL,
+ format enum_property_format,
+ allowed_roles character varying(255) DEFAULT NULL,
+ PRIMARY KEY (list_id, entry_name),
+ CONSTRAINT director_datalist_entry_datalist
+ FOREIGN KEY (list_id)
+ REFERENCES director_datalist (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX datalist_entry_datalist ON director_datalist_entry (list_id);
+
+
+CREATE TABLE director_datafield_category (
+ id serial,
+ category_name character varying(255) NOT NULL,
+ description text DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX datafield_category_name ON director_datafield_category (category_name);
+
+
+CREATE TABLE director_datafield (
+ id serial,
+ category_id integer DEFAULT NULL,
+ varname character varying(64) NOT NULL,
+ caption character varying(255) NOT NULL,
+ description text DEFAULT NULL,
+ datatype character varying(255) NOT NULL,
+-- datatype_param? multiple ones?
+ format enum_property_format,
+ PRIMARY KEY (id),
+ CONSTRAINT director_datafield_category
+ FOREIGN KEY (category_id)
+ REFERENCES director_datafield_category (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX search_idx ON director_datafield (varname);
+CREATE INDEX datafield_category ON director_datafield (category_id);
+
+
+CREATE TABLE director_datafield_setting (
+ datafield_id integer NOT NULL,
+ setting_name character varying(64) NOT NULL,
+ setting_value text NOT NULL,
+ PRIMARY KEY (datafield_id, setting_name),
+ CONSTRAINT datafield_id_settings
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX director_datafield_datafield ON director_datafield_setting (datafield_id);
+
+
+CREATE TABLE director_schema_migration (
+ schema_version SMALLINT NOT NULL,
+ migration_time TIMESTAMP WITH TIME ZONE NOT NULL,
+ PRIMARY KEY(schema_version)
+);
+
+
+CREATE TABLE director_setting (
+ setting_name character varying(64) NOT NULL,
+ setting_value character varying(255) NOT NULL,
+ PRIMARY KEY(setting_name)
+);
+
+
+CREATE TABLE icinga_zone (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ parent_id integer DEFAULT NULL,
+ object_name character varying(255) NOT NULL UNIQUE,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ is_global enum_boolean NOT NULL DEFAULT 'n',
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_zone_parent_zone
+ FOREIGN KEY (parent_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX zone_parent ON icinga_zone (parent_id);
+
+
+CREATE TABLE icinga_zone_inheritance (
+ zone_id integer NOT NULL,
+ parent_zone_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (zone_id, parent_zone_id),
+ CONSTRAINT icinga_zone_inheritance_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_zone_inheritance_parent_zone
+ FOREIGN KEY (parent_zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX zone_inheritance_unique_order ON icinga_zone_inheritance (zone_id, weight);
+CREATE INDEX zone_inheritance_zone ON icinga_zone_inheritance (zone_id);
+CREATE INDEX zone_inheritance_zone_parent ON icinga_zone_inheritance (parent_zone_id);
+
+
+CREATE TABLE icinga_timeperiod (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) NOT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ update_method character varying(64) DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ prefer_includes enum_boolean DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_timeperiod_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX timeperiod_object_name ON icinga_timeperiod (object_name, zone_id);
+CREATE INDEX timeperiod_zone ON icinga_timeperiod (zone_id);
+COMMENT ON COLUMN icinga_timeperiod.update_method IS 'Usually LegacyTimePeriod';
+
+
+CREATE TABLE icinga_timeperiod_inheritance (
+ timeperiod_id integer NOT NULL,
+ parent_timeperiod_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (timeperiod_id, parent_timeperiod_id),
+ CONSTRAINT icinga_timeperiod_inheritance_timeperiod
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_timeperiod_inheritance_parent_timeperiod
+ FOREIGN KEY (parent_timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX timeperiod_inheritance_unique_order ON icinga_timeperiod_inheritance (timeperiod_id, weight);
+CREATE INDEX timeperiod_inheritance_timeperiod ON icinga_timeperiod_inheritance (timeperiod_id);
+CREATE INDEX timeperiod_inheritance_timeperiod_parent ON icinga_timeperiod_inheritance (parent_timeperiod_id);
+
+
+CREATE TABLE icinga_timeperiod_range (
+ timeperiod_id serial,
+ range_key character varying(255) NOT NULL,
+ range_value character varying(255) NOT NULL,
+ range_type enum_timeperiod_range_type NOT NULL DEFAULT 'include',
+ merge_behaviour enum_merge_behaviour NOT NULL DEFAULT 'set',
+ PRIMARY KEY (timeperiod_id, range_type, range_key),
+ CONSTRAINT icinga_timeperiod_range_timeperiod
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX timeperiod_range_timeperiod ON icinga_timeperiod_range (timeperiod_id);
+COMMENT ON COLUMN icinga_timeperiod_range.range_key IS 'monday, ...';
+COMMENT ON COLUMN icinga_timeperiod_range.range_value IS '00:00-24:00, ...';
+COMMENT ON COLUMN icinga_timeperiod_range.range_type IS 'include -> ranges {}, exclude ranges_ignore {} - not yet';
+COMMENT ON COLUMN icinga_timeperiod_range.merge_behaviour IS 'set -> = {}, add -> += {}, substract -> -= {}';
+
+
+CREATE TABLE director_job (
+ id serial,
+ job_name character varying(64) NOT NULL,
+ job_class character varying(72) NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ run_interval integer NOT NULL, -- seconds
+ timeperiod_id integer DEFAULT NULL,
+ last_attempt_succeeded enum_boolean DEFAULT NULL,
+ ts_last_attempt timestamp with time zone DEFAULT NULL,
+ ts_last_error timestamp with time zone DEFAULT NULL,
+ last_error_message text NULL DEFAULT NULL,
+ CONSTRAINT director_job_period
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX director_job_unique_job_name ON director_job (job_name);
+
+
+CREATE TABLE director_job_setting (
+ job_id integer NOT NULL,
+ setting_name character varying(64) NOT NULL,
+ setting_value text DEFAULT NULL,
+ PRIMARY KEY (job_id, setting_name),
+ CONSTRAINT director_job_setting_job
+ FOREIGN KEY (job_id)
+ REFERENCES director_job (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX director_job_setting_job ON director_job_setting (job_id);
+
+
+CREATE TABLE icinga_command (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ methods_execute character varying(64) DEFAULT NULL,
+ command text DEFAULT NULL,
+ is_string enum_boolean NULL,
+-- env text DEFAULT NULL,
+ timeout smallint DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_command_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX command_object_name ON icinga_command (object_name);
+CREATE INDEX command_zone ON icinga_command (zone_id);
+COMMENT ON COLUMN icinga_command.object_type IS 'external_object is an attempt to work with existing commands';
+
+
+CREATE TABLE icinga_command_inheritance (
+ command_id integer NOT NULL,
+ parent_command_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (command_id, parent_command_id),
+ CONSTRAINT icinga_command_inheritance_command
+ FOREIGN KEY (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_command_inheritance_parent_command
+ FOREIGN KEY (parent_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX command_inheritance_unique_order ON icinga_command_inheritance (command_id, weight);
+CREATE INDEX command_inheritance_command ON icinga_command_inheritance (command_id);
+CREATE INDEX command_inheritance_command_parent ON icinga_command_inheritance (parent_command_id);
+
+
+CREATE TABLE icinga_command_argument (
+ id serial,
+ command_id integer NOT NULL,
+ argument_name character varying(64) NOT NULL,
+ argument_value text DEFAULT NULL,
+ argument_format enum_property_format DEFAULT NULL,
+ key_string character varying(64) DEFAULT NULL,
+ description text DEFAULT NULL,
+ skip_key enum_boolean DEFAULT NULL,
+ set_if character varying(255) DEFAULT NULL, -- (string expression, must resolve to a numeric value)
+ set_if_format enum_property_format DEFAULT NULL,
+ sort_order smallint DEFAULT NULL, -- -> order
+ repeat_key enum_boolean DEFAULT NULL,
+ required enum_boolean DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_command_argument_command
+ FOREIGN KEY (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX command_argument_unique_idx ON icinga_command_argument (command_id, argument_name);
+CREATE INDEX command_argument_sort_idx ON icinga_command_argument (command_id, sort_order);
+CREATE INDEX command_argument_command ON icinga_command_argument (command_id);
+COMMENT ON COLUMN icinga_command_argument.argument_name IS '-x, --host';
+COMMENT ON COLUMN icinga_command_argument.key_string IS 'Overrides name';
+COMMENT ON COLUMN icinga_command_argument.repeat_key IS 'Useful with array values';
+
+
+CREATE TABLE icinga_command_field (
+ command_id integer NOT NULL,
+ datafield_id integer NOT NULL,
+ is_required enum_boolean NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (command_id, datafield_id),
+ CONSTRAINT icinga_command_field_command
+ FOREIGN KEY (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_command_field_datafield
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+
+CREATE TABLE icinga_command_var (
+ command_id integer NOT NULL,
+ checksum bytea DEFAULT NULL UNIQUE CHECK(LENGTH(checksum) = 20),
+ varname character varying(255) NOT NULL,
+ varvalue text DEFAULT NULL,
+ format enum_property_format NOT NULL DEFAULT 'string',
+ PRIMARY KEY (command_id, varname),
+ CONSTRAINT icinga_command_var_command
+ FOREIGN KEY (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX command_var_command ON icinga_command_var (command_id);
+CREATE INDEX command_var_search_idx ON icinga_command_var (varname);
+CREATE INDEX command_var_checksum ON icinga_command_var (checksum);
+
+
+CREATE TABLE icinga_apiuser (
+ id BIGSERIAL,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name CHARACTER VARYING(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ password CHARACTER VARYING(255) DEFAULT NULL,
+ client_dn CHARACTER VARYING(64) DEFAULT NULL,
+ permissions TEXT DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+COMMENT ON COLUMN icinga_apiuser.permissions IS 'JSON-encoded permissions';
+
+
+CREATE TABLE icinga_endpoint (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ zone_id integer DEFAULT NULL,
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ host character varying(255) DEFAULT NULL,
+ port d_smallint DEFAULT NULL,
+ log_duration character varying(32) DEFAULT NULL,
+ apiuser_id INTEGER DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_endpoint_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_apiuser
+ FOREIGN KEY (apiuser_id)
+ REFERENCES icinga_apiuser (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX endpoint_object_name ON icinga_endpoint (object_name);
+CREATE INDEX endpoint_zone ON icinga_endpoint (zone_id);
+COMMENT ON COLUMN icinga_endpoint.host IS 'IP address / hostname of remote node';
+COMMENT ON COLUMN icinga_endpoint.port IS '5665 if not set';
+COMMENT ON COLUMN icinga_endpoint.log_duration IS '1d if not set';
+
+
+CREATE TABLE icinga_endpoint_inheritance (
+ endpoint_id integer NOT NULL,
+ parent_endpoint_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (endpoint_id, parent_endpoint_id),
+ CONSTRAINT icinga_endpoint_inheritance_endpoint
+ FOREIGN KEY (endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_endpoint_inheritance_parent_endpoint
+ FOREIGN KEY (parent_endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX endpoint_inheritance_unique_order ON icinga_endpoint_inheritance (endpoint_id, weight);
+CREATE INDEX endpoint_inheritance_endpoint ON icinga_endpoint_inheritance (endpoint_id);
+CREATE INDEX endpoint_inheritance_endpoint_parent ON icinga_endpoint_inheritance (parent_endpoint_id);
+
+
+CREATE TABLE icinga_host_template_choice (
+ id serial,
+ object_name character varying(64) NOT NULL,
+ description text DEFAULT NULL,
+ min_required smallint NOT NULL DEFAULT 0,
+ max_allowed smallint NOT NULL DEFAULT 1,
+ required_template_id integer DEFAULT NULL,
+ allowed_roles character varying(255) DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX host_template_choice_object_name ON icinga_host_template_choice (object_name);
+CREATE INDEX host_template_choice_required_template ON icinga_host_template_choice (required_template_id);
+
+CREATE TABLE icinga_host (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ display_name CHARACTER VARYING(255) DEFAULT NULL,
+ address character varying(255) DEFAULT NULL,
+ address6 character varying(45) DEFAULT NULL,
+ check_command_id integer DEFAULT NULL,
+ max_check_attempts integer DEFAULT NULL,
+ check_period_id integer DEFAULT NULL,
+ check_interval character varying(8) DEFAULT NULL,
+ retry_interval character varying(8) DEFAULT NULL,
+ check_timeout smallint DEFAULT NULL,
+ enable_notifications enum_boolean DEFAULT NULL,
+ enable_active_checks enum_boolean DEFAULT NULL,
+ enable_passive_checks enum_boolean DEFAULT NULL,
+ enable_event_handler enum_boolean DEFAULT NULL,
+ enable_flapping enum_boolean DEFAULT NULL,
+ enable_perfdata enum_boolean DEFAULT NULL,
+ event_command_id integer DEFAULT NULL,
+ flapping_threshold_high smallint default null,
+ flapping_threshold_low smallint default null,
+ volatile enum_boolean DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ command_endpoint_id integer DEFAULT NULL,
+ notes text DEFAULT NULL,
+ notes_url character varying(255) DEFAULT NULL,
+ action_url character varying(255) DEFAULT NULL,
+ icon_image character varying(255) DEFAULT NULL,
+ icon_image_alt character varying(255) DEFAULT NULL,
+ has_agent enum_boolean DEFAULT NULL,
+ master_should_connect enum_boolean DEFAULT NULL,
+ accept_config enum_boolean DEFAULT NULL,
+ custom_endpoint_name character varying(255) DEFAULT NULL,
+ api_key character varying(40) DEFAULT NULL,
+ template_choice_id int DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_host_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_check_period
+ FOREIGN KEY (check_period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_check_command
+ FOREIGN KEY (check_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_event_command
+ FOREIGN KEY (event_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_command_endpoint
+ FOREIGN KEY (command_endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_template_choice
+ FOREIGN KEY (template_choice_id)
+ REFERENCES icinga_host_template_choice (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+);
+
+
+CREATE UNIQUE INDEX object_name_host ON icinga_host (object_name, zone_id);
+CREATE UNIQUE INDEX host_api_key ON icinga_host (api_key);
+CREATE INDEX host_zone ON icinga_host (zone_id);
+CREATE INDEX host_timeperiod ON icinga_host (check_period_id);
+CREATE INDEX host_check_command ON icinga_host (check_command_id);
+CREATE INDEX host_event_command ON icinga_host (event_command_id);
+CREATE INDEX host_command_endpoint ON icinga_host (command_endpoint_id);
+CREATE INDEX host_template_choice ON icinga_host (template_choice_id);
+
+
+CREATE TABLE icinga_host_inheritance (
+ host_id integer NOT NULL,
+ parent_host_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (host_id, parent_host_id),
+ CONSTRAINT icinga_host_inheritance_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_inheritance_parent_host
+ FOREIGN KEY (parent_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX host_inheritance_unique_order ON icinga_host_inheritance (host_id, weight);
+CREATE INDEX host_inheritance_host ON icinga_host_inheritance (host_id);
+CREATE INDEX host_inheritance_host_parent ON icinga_host_inheritance (parent_host_id);
+
+
+CREATE TABLE icinga_host_field (
+ host_id integer NOT NULL,
+ datafield_id integer NOT NULL,
+ is_required enum_boolean NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (host_id, datafield_id),
+ CONSTRAINT icinga_host_field_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_field_datafield
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX host_field_key ON icinga_host_field (host_id, datafield_id);
+CREATE INDEX host_field_host ON icinga_host_field (host_id);
+CREATE INDEX host_field_datafield ON icinga_host_field (datafield_id);
+COMMENT ON COLUMN icinga_host_field.host_id IS 'Makes only sense for templates';
+
+
+CREATE TABLE icinga_host_var (
+ host_id integer NOT NULL,
+ checksum bytea DEFAULT NULL UNIQUE CHECK(LENGTH(checksum) = 20),
+ varname character varying(255) NOT NULL,
+ varvalue text DEFAULT NULL,
+ format enum_property_format, -- immer string vorerst
+ PRIMARY KEY (host_id, varname),
+ CONSTRAINT icinga_host_var_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX host_var_search_idx ON icinga_host_var (varname);
+CREATE INDEX host_var_host ON icinga_host_var (host_id);
+CREATE INDEX host_var_checksum ON icinga_host_var (checksum);
+
+
+ALTER TABLE icinga_host_template_choice
+ ADD CONSTRAINT host_template_choice_required_template
+ FOREIGN KEY (required_template_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+
+CREATE TABLE icinga_service_set (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ host_id integer DEFAULT NULL,
+ object_name character varying(128) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ description text DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_service_set_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_set_name ON icinga_service_set (object_name, host_id);
+CREATE INDEX service_set_host ON icinga_service_set (host_id);
+
+
+CREATE TABLE icinga_service_template_choice (
+ id serial,
+ object_name character varying(64) NOT NULL,
+ description text DEFAULT NULL,
+ min_required smallint NOT NULL DEFAULT 0,
+ max_allowed smallint NOT NULL DEFAULT 1,
+ required_template_id integer DEFAULT NULL,
+ allowed_roles character varying(255) DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX service_template_choice_object_name ON icinga_service_template_choice (object_name);
+CREATE INDEX service_template_choice_required_template ON icinga_service_template_choice (required_template_id);
+
+
+CREATE TABLE icinga_service (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean DEFAULT 'n',
+ display_name character varying(255) DEFAULT NULL,
+ host_id INTEGER DEFAULT NULL,
+ service_set_id integer DEFAULT NULL,
+ check_command_id integer DEFAULT NULL,
+ max_check_attempts integer DEFAULT NULL,
+ check_period_id integer DEFAULT NULL,
+ check_interval character varying(8) DEFAULT NULL,
+ retry_interval character varying(8) DEFAULT NULL,
+ check_timeout smallint DEFAULT NULL,
+ enable_notifications enum_boolean DEFAULT NULL,
+ enable_active_checks enum_boolean DEFAULT NULL,
+ enable_passive_checks enum_boolean DEFAULT NULL,
+ enable_event_handler enum_boolean DEFAULT NULL,
+ enable_flapping enum_boolean DEFAULT NULL,
+ enable_perfdata enum_boolean DEFAULT NULL,
+ event_command_id integer DEFAULT NULL,
+ flapping_threshold_high smallint DEFAULT NULL,
+ flapping_threshold_low smallint DEFAULT NULL,
+ volatile enum_boolean DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ command_endpoint_id integer DEFAULT NULL,
+ notes text DEFAULT NULL,
+ notes_url character varying(255) DEFAULT NULL,
+ action_url character varying(255) DEFAULT NULL,
+ icon_image character varying(255) DEFAULT NULL,
+ icon_image_alt character varying(255) DEFAULT NULL,
+ use_agent enum_boolean DEFAULT NULL,
+ apply_for character varying(255) DEFAULT NULL,
+ use_var_overrides enum_boolean DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ template_choice_id int DEFAULT NULL,
+ PRIMARY KEY (id),
+-- UNIQUE INDEX object_name (object_name, zone_id),
+ CONSTRAINT icinga_service_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_check_period
+ FOREIGN KEY (check_period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_check_command
+ FOREIGN KEY (check_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_event_command
+ FOREIGN KEY (event_command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_command_endpoint
+ FOREIGN KEY (command_endpoint_id)
+ REFERENCES icinga_endpoint (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_service_set
+ FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_template_choice
+ FOREIGN KEY (template_choice_id)
+ REFERENCES icinga_service_template_choice (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX service_zone ON icinga_service (zone_id);
+CREATE INDEX service_timeperiod ON icinga_service (check_period_id);
+CREATE INDEX service_check_command ON icinga_service (check_command_id);
+CREATE INDEX service_event_command ON icinga_service (event_command_id);
+CREATE INDEX service_command_endpoint ON icinga_service (command_endpoint_id);
+CREATE INDEX service_template_choice ON icinga_service (template_choice_id);
+
+
+CREATE TABLE icinga_service_inheritance (
+ service_id integer NOT NULL,
+ parent_service_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (service_id, parent_service_id),
+ CONSTRAINT icinga_service_inheritance_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_inheritance_parent_service
+ FOREIGN KEY (parent_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_inheritance_unique_order ON icinga_service_inheritance (service_id, weight);
+CREATE INDEX service_inheritance_service ON icinga_service_inheritance (service_id);
+CREATE INDEX service_inheritance_service_parent ON icinga_service_inheritance (parent_service_id);
+
+
+CREATE TABLE icinga_service_var (
+ service_id integer NOT NULL,
+ checksum bytea DEFAULT NULL UNIQUE CHECK(LENGTH(checksum) = 20),
+ varname character varying(255) NOT NULL,
+ varvalue text DEFAULT NULL,
+ format enum_property_format,
+ PRIMARY KEY (service_id, varname),
+ CONSTRAINT icinga_service_var_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX service_var_search_idx ON icinga_service_var (varname);
+CREATE INDEX service_var_service ON icinga_service_var (service_id);
+CREATE INDEX service_var_checksum ON icinga_service_var (checksum);
+
+
+CREATE TABLE icinga_service_field (
+ service_id integer NOT NULL,
+ datafield_id integer NOT NULL,
+ is_required enum_boolean NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (service_id, datafield_id),
+ CONSTRAINT icinga_service_field_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_field_datafield
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_field_key ON icinga_service_field (service_id, datafield_id);
+CREATE INDEX service_field_service ON icinga_service_field (service_id);
+CREATE INDEX service_field_datafield ON icinga_service_field (datafield_id);
+COMMENT ON COLUMN icinga_service_field.service_id IS 'Makes only sense for templates';
+
+
+ALTER TABLE icinga_service_template_choice
+ ADD CONSTRAINT service_template_choice_required_template
+ FOREIGN KEY (required_template_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE;
+
+
+CREATE TABLE icinga_host_service (
+ host_id integer NOT NULL,
+ service_id integer NOT NULL,
+ PRIMARY KEY (host_id, service_id),
+ CONSTRAINT icinga_host_service_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_service_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX host_service_host ON icinga_host_service (host_id);
+CREATE INDEX host_service_service ON icinga_host_service (service_id);
+
+
+CREATE TABLE icinga_host_service_blacklist(
+ host_id integer NOT NULL,
+ service_id integer NOT NULL,
+ PRIMARY KEY (host_id, service_id),
+ CONSTRAINT icinga_host_service__bl_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_host_service_bl_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX host_service_bl_host ON icinga_host_service_blacklist (host_id);
+CREATE INDEX host_service_bl_service ON icinga_host_service_blacklist (service_id);
+
+
+CREATE TABLE icinga_service_set_inheritance (
+ service_set_id integer NOT NULL,
+ parent_service_set_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (service_set_id, parent_service_set_id),
+ CONSTRAINT icinga_service_set_inheritance_set
+ FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_service_set_inheritance_parent
+ FOREIGN KEY (parent_service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_set_inheritance_unique_order ON icinga_service_set_inheritance (service_set_id, weight);
+CREATE INDEX service_set_inheritance_set ON icinga_service_set_inheritance (service_set_id);
+CREATE INDEX service_set_inheritance_parent ON icinga_service_set_inheritance (parent_service_set_id);
+
+
+CREATE TABLE icinga_service_set_var (
+ service_set_id integer NOT NULL,
+ checksum bytea DEFAULT NULL UNIQUE CHECK(LENGTH(checksum) = 20),
+ varname character varying(255) NOT NULL,
+ varvalue text DEFAULT NULL,
+ format enum_property_format NOT NULL DEFAULT 'string',
+ PRIMARY KEY (service_set_id, varname),
+ CONSTRAINT icinga_service_set_var_service_set
+ FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX service_set_var_service_set ON icinga_service_set_var (service_set_id);
+CREATE INDEX service_set_var_search_idx ON icinga_service_set_var (varname);
+CREATE INDEX service_set_var_checksum ON icinga_service_set_var (checksum);
+
+
+CREATE TABLE icinga_hostgroup (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ display_name character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX hostgroup_object_name ON icinga_hostgroup (object_name);
+CREATE INDEX hostgroup_search_idx ON icinga_hostgroup (display_name);
+
+
+-- -- TODO: probably useless
+CREATE TABLE icinga_hostgroup_inheritance (
+ hostgroup_id integer NOT NULL,
+ parent_hostgroup_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (hostgroup_id, parent_hostgroup_id),
+ CONSTRAINT icinga_hostgroup_inheritance_hostgroup
+ FOREIGN KEY (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_inheritance_parent_hostgroup
+ FOREIGN KEY (parent_hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX hostgroup_inheritance_unique_order ON icinga_hostgroup_inheritance (hostgroup_id, weight);
+CREATE INDEX hostgroup_inheritance_hostgroup ON icinga_hostgroup_inheritance (hostgroup_id);
+CREATE INDEX hostgroup_inheritance_hostgroup_parent ON icinga_hostgroup_inheritance (parent_hostgroup_id);
+
+
+CREATE TABLE icinga_servicegroup (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ display_name character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX servicegroup_object_name ON icinga_servicegroup (object_name);
+CREATE INDEX servicegroup_search_idx ON icinga_servicegroup (display_name);
+
+CREATE TABLE icinga_servicegroup_service_resolved (
+ servicegroup_id integer NOT NULL,
+ service_id integer NOT NULL,
+ PRIMARY KEY (servicegroup_id, service_id),
+ CONSTRAINT icinga_servicegroup_service_resolved_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_service_resolved_servicegroup
+ FOREIGN KEY (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX servicegroup_service_resolved_service ON icinga_servicegroup_service_resolved (service_id);
+CREATE INDEX servicegroup_service_resolved_servicegroup ON icinga_servicegroup_service_resolved (servicegroup_id);
+
+
+CREATE TABLE icinga_servicegroup_inheritance (
+ servicegroup_id integer NOT NULL,
+ parent_servicegroup_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (servicegroup_id, parent_servicegroup_id),
+ CONSTRAINT icinga_servicegroup_inheritance_servicegroup
+ FOREIGN KEY (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_inheritance_parent_servicegroup
+ FOREIGN KEY (parent_servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX servicegroup_inheritance_unique_order ON icinga_servicegroup_inheritance (servicegroup_id, weight);
+CREATE INDEX servicegroup_inheritance_servicegroup ON icinga_servicegroup_inheritance (servicegroup_id);
+CREATE INDEX servicegroup_inheritance_servicegroup_parent ON icinga_servicegroup_inheritance (parent_servicegroup_id);
+
+
+CREATE TABLE icinga_servicegroup_service (
+ servicegroup_id integer NOT NULL,
+ service_id integer NOT NULL,
+ PRIMARY KEY (servicegroup_id, service_id),
+ CONSTRAINT icinga_servicegroup_service_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_servicegroup_service_servicegroup
+ FOREIGN KEY (servicegroup_id)
+ REFERENCES icinga_servicegroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX servicegroup_service_service ON icinga_servicegroup_service (service_id);
+CREATE INDEX servicegroup_service_servicegroup ON icinga_servicegroup_service (servicegroup_id);
+
+
+CREATE TABLE icinga_hostgroup_host (
+ hostgroup_id integer NOT NULL,
+ host_id integer NOT NULL,
+ PRIMARY KEY (hostgroup_id, host_id),
+ CONSTRAINT icinga_hostgroup_host_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_host_hostgroup
+ FOREIGN KEY (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX hostgroup_host_host ON icinga_hostgroup_host (host_id);
+CREATE INDEX hostgroup_host_hostgroup ON icinga_hostgroup_host (hostgroup_id);
+
+
+CREATE TABLE icinga_hostgroup_host_resolved (
+ hostgroup_id integer NOT NULL,
+ host_id integer NOT NULL,
+ PRIMARY KEY (hostgroup_id, host_id),
+ CONSTRAINT icinga_hostgroup_host_resolved_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_host_resolved_hostgroup
+ FOREIGN KEY (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX hostgroup_host_resolved_host ON icinga_hostgroup_host_resolved (host_id);
+CREATE INDEX hostgroup_host_resolved_hostgroup ON icinga_hostgroup_host_resolved (hostgroup_id);
+
+
+CREATE TABLE icinga_hostgroup_parent (
+ hostgroup_id integer NOT NULL,
+ parent_hostgroup_id integer NOT NULL,
+ PRIMARY KEY (hostgroup_id, parent_hostgroup_id),
+ CONSTRAINT icinga_hostgroup_parent_hostgroup
+ FOREIGN KEY (hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_hostgroup_parent_parent
+ FOREIGN KEY (parent_hostgroup_id)
+ REFERENCES icinga_hostgroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX hostgroup_parent_hostgroup ON icinga_hostgroup_parent (hostgroup_id);
+CREATE INDEX hostgroup_parent_parent ON icinga_hostgroup_parent (parent_hostgroup_id);
+
+
+CREATE TABLE icinga_user (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ display_name character varying(255) DEFAULT NULL,
+ email character varying(255) DEFAULT NULL,
+ pager character varying(255) DEFAULT NULL,
+ enable_notifications enum_boolean DEFAULT NULL,
+ period_id integer DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_user_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_period
+ FOREIGN KEY (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX user_object_name ON icinga_user (object_name, zone_id);
+CREATE INDEX user_zone ON icinga_user (zone_id);
+
+
+CREATE TABLE icinga_user_inheritance (
+ user_id integer NOT NULL,
+ parent_user_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (user_id, parent_user_id),
+ CONSTRAINT icinga_user_inheritance_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_inheritance_parent_user
+ FOREIGN KEY (parent_user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX user_inheritance_unique_order ON icinga_user_inheritance (user_id, weight);
+CREATE INDEX user_inheritance_user ON icinga_user_inheritance (user_id);
+CREATE INDEX user_inheritance_user_parent ON icinga_user_inheritance (parent_user_id);
+
+
+CREATE TABLE icinga_user_states_set (
+ user_id integer NOT NULL,
+ property enum_state_name NOT NULL,
+ merge_behaviour enum_set_merge_behaviour NOT NULL DEFAULT 'override',
+ PRIMARY KEY (user_id, property, merge_behaviour),
+ CONSTRAINT icinga_user_filter_state_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX user_states_set_user ON icinga_user_states_set (user_id);
+COMMENT ON COLUMN icinga_user_states_set.merge_behaviour IS 'override: = [], extend: += [], blacklist: -= []';
+
+
+CREATE TABLE icinga_user_types_set (
+ user_id integer NOT NULL,
+ property enum_type_name NOT NULL,
+ merge_behaviour enum_set_merge_behaviour NOT NULL DEFAULT 'override',
+ PRIMARY KEY (user_id, property, merge_behaviour),
+ CONSTRAINT icinga_user_filter_type_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX user_types_set_user ON icinga_user_types_set (user_id);
+COMMENT ON COLUMN icinga_user_types_set.merge_behaviour IS 'override: = [], extend: += [], blacklist: -= []';
+
+
+CREATE TABLE icinga_user_var (
+ user_id integer NOT NULL,
+ checksum bytea DEFAULT NULL UNIQUE CHECK(LENGTH(checksum) = 20),
+ varname character varying(255) NOT NULL,
+ varvalue text DEFAULT NULL,
+ format enum_property_format NOT NULL DEFAULT 'string',
+ PRIMARY KEY (user_id, varname),
+ CONSTRAINT icinga_user_var_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX user_var_search_idx ON icinga_user_var (varname);
+CREATE INDEX user_var_user ON icinga_user_var (user_id);
+CREATE INDEX user_var_checksum ON icinga_user_var (checksum);
+
+
+CREATE TABLE icinga_user_field (
+ user_id integer NOT NULL,
+ datafield_id integer NOT NULL,
+ is_required enum_boolean NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (user_id, datafield_id),
+ CONSTRAINT icinga_user_field_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_user_field_datafield
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX user_field_key ON icinga_user_field (user_id, datafield_id);
+CREATE INDEX user_field_user ON icinga_user_field (user_id);
+CREATE INDEX user_field_datafield ON icinga_user_field (datafield_id);
+COMMENT ON COLUMN icinga_user_field.user_id IS 'Makes only sense for templates';
+
+
+CREATE TABLE icinga_usergroup (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ display_name character varying(255) DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_usergroup_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX usergroup_search_idx ON icinga_usergroup (display_name);
+CREATE INDEX usergroup_object_name ON icinga_usergroup (object_name);
+CREATE INDEX usergroup_zone ON icinga_usergroup (zone_id);
+
+
+CREATE TABLE icinga_usergroup_inheritance (
+ usergroup_id integer NOT NULL,
+ parent_usergroup_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (usergroup_id, parent_usergroup_id),
+ CONSTRAINT icinga_usergroup_inheritance_usergroup
+ FOREIGN KEY (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_usergroup_inheritance_parent_usergroup
+ FOREIGN KEY (parent_usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX usergroup_inheritance_unique_order ON icinga_usergroup_inheritance (usergroup_id, weight);
+CREATE INDEX usergroup_inheritance_usergroup ON icinga_usergroup_inheritance (usergroup_id);
+CREATE INDEX usergroup_inheritance_usergroup_parent ON icinga_usergroup_inheritance (parent_usergroup_id);
+
+
+CREATE TABLE icinga_usergroup_user (
+ usergroup_id integer NOT NULL,
+ user_id integer NOT NULL,
+ PRIMARY KEY (usergroup_id, user_id),
+ CONSTRAINT icinga_usergroup_user_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_usergroup_user_usergroup
+ FOREIGN KEY (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX usergroup_user_user ON icinga_usergroup_user (user_id);
+CREATE INDEX usergroup_user_usergroup ON icinga_usergroup_user (usergroup_id);
+
+
+CREATE TABLE icinga_usergroup_parent (
+ usergroup_id integer NOT NULL,
+ parent_usergroup_id integer NOT NULL,
+ PRIMARY KEY (usergroup_id, parent_usergroup_id),
+ CONSTRAINT icinga_usergroup_parent_usergroup
+ FOREIGN KEY (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_usergroup_parent_parent
+ FOREIGN KEY (parent_usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX usergroup_parent_usergroup ON icinga_usergroup_parent (usergroup_id);
+CREATE INDEX usergroup_parent_parent ON icinga_usergroup_parent (parent_usergroup_id);
+
+
+CREATE TABLE icinga_notification (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name CHARACTER VARYING(255) DEFAULT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ apply_to enum_host_service NULL DEFAULT NULL,
+ host_id integer DEFAULT NULL,
+ service_id integer DEFAULT NULL,
+ times_begin integer DEFAULT NULL,
+ times_end integer DEFAULT NULL,
+ notification_interval integer DEFAULT NULL,
+ command_id integer DEFAULT NULL,
+ period_id integer DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_notification_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_command
+ FOREIGN KEY (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_period
+ FOREIGN KEY (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+
+CREATE TABLE icinga_notification_user (
+ notification_id integer NOT NULL,
+ user_id integer NOT NULL,
+ PRIMARY KEY (notification_id, user_id),
+ CONSTRAINT icinga_notification_user_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_user_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE icinga_notification_usergroup (
+ notification_id integer NOT NULL,
+ usergroup_id integer NOT NULL,
+ PRIMARY KEY (notification_id, usergroup_id),
+ CONSTRAINT icinga_notification_usergroup_usergroup
+ FOREIGN KEY (usergroup_id)
+ REFERENCES icinga_usergroup (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_usergroup_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+
+CREATE TABLE import_source (
+ id serial,
+ source_name character varying(64) NOT NULL,
+ key_column character varying(64) NOT NULL,
+ provider_class character varying(128) NOT NULL,
+ import_state enum_sync_state NOT NULL DEFAULT 'unknown',
+ last_error_message text NULL DEFAULT NULL,
+ last_attempt timestamp with time zone NULL DEFAULT NULL,
+ description text DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE INDEX import_source_search_idx ON import_source (key_column);
+CREATE UNIQUE INDEX import_source_name ON import_source (source_name);
+
+
+CREATE TABLE import_source_setting (
+ source_id integer NOT NULL,
+ setting_name character varying(64) NOT NULL,
+ setting_value text NOT NULL,
+ PRIMARY KEY (source_id, setting_name),
+ CONSTRAINT import_source_settings_source
+ FOREIGN KEY (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX import_source_setting_source ON import_source_setting (source_id);
+
+
+CREATE TABLE import_row_modifier (
+ id bigserial,
+ source_id integer NOT NULL,
+ property_name character varying(255) NOT NULL,
+ target_property character varying(255) DEFAULT NULL,
+ provider_class character varying(128) NOT NULL,
+ priority integer NOT NULL,
+ description text DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT row_modifier_import_source
+ FOREIGN KEY (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX import_row_modifier_search_idx ON import_row_modifier (property_name);
+CREATE UNIQUE INDEX import_row_modifier_prio
+ ON import_row_modifier (source_id, priority);
+
+
+CREATE TABLE import_row_modifier_setting (
+ row_modifier_id serial,
+ setting_name character varying(64) NOT NULL,
+ setting_value TEXT DEFAULT NULL,
+ PRIMARY KEY (row_modifier_id, setting_name),
+ CONSTRAINT row_modifier_settings
+ FOREIGN KEY (row_modifier_id)
+ REFERENCES import_row_modifier (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+
+CREATE TABLE imported_rowset (
+ checksum bytea CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (checksum)
+);
+
+
+CREATE TABLE import_run (
+ id serial,
+ source_id integer NOT NULL,
+ rowset_checksum bytea CHECK(LENGTH(rowset_checksum) = 20),
+ start_time timestamp with time zone NOT NULL,
+ end_time timestamp with time zone DEFAULT NULL,
+ succeeded enum_boolean DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT import_run_source
+ FOREIGN KEY (source_id)
+ REFERENCES import_source (id)
+ ON DELETE CASCADE
+ ON UPDATE RESTRICT,
+ CONSTRAINT import_run_rowset
+ FOREIGN KEY (rowset_checksum)
+ REFERENCES imported_rowset (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX import_run_import_source ON import_run (source_id);
+CREATE INDEX import_run_rowset ON import_run (rowset_checksum);
+
+
+CREATE TABLE imported_row (
+ checksum bytea CHECK(LENGTH(checksum) = 20),
+ object_name character varying(255) NOT NULL,
+ PRIMARY KEY (checksum)
+);
+
+COMMENT ON COLUMN imported_row.checksum IS 'sha1(object_name;property_checksum;...)';
+
+
+CREATE TABLE imported_rowset_row (
+ rowset_checksum bytea CHECK(LENGTH(rowset_checksum) = 20),
+ row_checksum bytea CHECK(LENGTH(row_checksum) = 20),
+ PRIMARY KEY (rowset_checksum, row_checksum),
+ CONSTRAINT imported_rowset_row_rowset
+ FOREIGN KEY (rowset_checksum)
+ REFERENCES imported_rowset (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT imported_rowset_row_row
+ FOREIGN KEY (row_checksum)
+ REFERENCES imported_row (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX imported_rowset_row_rowset_checksum ON imported_rowset_row (rowset_checksum);
+CREATE INDEX imported_rowset_row_row_checksum ON imported_rowset_row (row_checksum);
+
+
+CREATE TABLE imported_property (
+ checksum bytea CHECK(LENGTH(checksum) = 20),
+ property_name character varying(64) NOT NULL,
+ property_value text NOT NULL,
+ format enum_property_format,
+ PRIMARY KEY (checksum)
+);
+
+CREATE INDEX imported_property_search_idx ON imported_property (property_name);
+
+
+CREATE TABLE imported_row_property (
+ row_checksum bytea CHECK(LENGTH(row_checksum) = 20),
+ property_checksum bytea CHECK(LENGTH(property_checksum) = 20),
+ PRIMARY KEY (row_checksum, property_checksum),
+ CONSTRAINT imported_row_property_row
+ FOREIGN KEY (row_checksum)
+ REFERENCES imported_row (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT imported_row_property_property
+ FOREIGN KEY (property_checksum)
+ REFERENCES imported_property (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX imported_row_property_row_checksum ON imported_row_property (row_checksum);
+CREATE INDEX imported_row_property_property_checksum ON imported_row_property (property_checksum);
+
+
+CREATE TABLE sync_rule (
+ id serial,
+ rule_name character varying(255) NOT NULL,
+ object_type enum_sync_rule_object_type NOT NULL,
+ update_policy enum_sync_rule_update_policy NOT NULL,
+ purge_existing enum_boolean NOT NULL DEFAULT 'n',
+ purge_action enum_sync_rule_purge_action NULL DEFAULT NULL,
+ filter_expression text DEFAULT NULL,
+ sync_state enum_sync_state NOT NULL DEFAULT 'unknown',
+ last_error_message text NULL DEFAULT NULL,
+ last_attempt timestamp with time zone NULL DEFAULT NULL,
+ description text DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX sync_rule_name ON sync_rule (rule_name);
+
+CREATE TABLE sync_property (
+ id serial,
+ rule_id integer NOT NULL,
+ source_id integer NOT NULL,
+ source_expression character varying(255) NOT NULL,
+ destination_field character varying(64),
+ priority smallint NOT NULL,
+ filter_expression text DEFAULT NULL,
+ merge_policy enum_sync_property_merge_policy DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT sync_property_rule
+ FOREIGN KEY (rule_id)
+ REFERENCES sync_rule (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT sync_property_source
+ FOREIGN KEY (source_id)
+ REFERENCES import_source (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX sync_property_rule ON sync_property (rule_id);
+CREATE INDEX sync_property_source ON sync_property (source_id);
+
+
+CREATE TABLE sync_run (
+ id bigserial,
+ rule_id integer DEFAULT NULL,
+ rule_name character varying(255) NOT NULL,
+ start_time TIMESTAMP WITH TIME ZONE NOT NULL,
+ duration_ms integer DEFAULT NULL,
+ objects_deleted integer DEFAULT 0,
+ objects_created integer DEFAULT 0,
+ objects_modified integer DEFAULT 0,
+ last_former_activity bytea DEFAULT NULL CHECK(LENGTH(last_former_activity) = 20),
+ last_related_activity bytea DEFAULT NULL CHECK(LENGTH(last_related_activity) = 20),
+ PRIMARY KEY (id),
+ CONSTRAINT sync_run_rule
+ FOREIGN KEY (rule_id)
+ REFERENCES sync_rule (id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+);
+
+
+CREATE TABLE icinga_notification_states_set (
+ notification_id integer NOT NULL,
+ property enum_state_name NOT NULL,
+ merge_behaviour enum_set_merge_behaviour NOT NULL DEFAULT 'override',
+ PRIMARY KEY (notification_id, property, merge_behaviour),
+ CONSTRAINT icinga_notification_states_set_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+COMMENT ON COLUMN icinga_notification_states_set.merge_behaviour IS 'override: = [], extend: += [], blacklist: -= []';
+
+
+CREATE TABLE icinga_notification_types_set (
+ notification_id integer NOT NULL,
+ property enum_type_name NOT NULL,
+ merge_behaviour enum_set_merge_behaviour NOT NULL DEFAULT 'override',
+ PRIMARY KEY (notification_id, property, merge_behaviour),
+ CONSTRAINT icinga_notification_types_set_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+COMMENT ON COLUMN icinga_notification_types_set.merge_behaviour IS 'override: = [], extend: += [], blacklist: -= []';
+
+
+CREATE TABLE icinga_notification_var (
+ notification_id integer NOT NULL,
+ checksum bytea DEFAULT NULL UNIQUE CHECK(LENGTH(checksum) = 20),
+ varname VARCHAR(255) NOT NULL,
+ varvalue TEXT DEFAULT NULL,
+ format enum_property_format,
+ PRIMARY KEY (notification_id, varname),
+ CONSTRAINT icinga_notification_var_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX notification_var_command ON icinga_notification_var (notification_id);
+CREATE INDEX notification_var_search_idx ON icinga_notification_var (varname);
+CREATE INDEX notification_var_checksum ON icinga_notification_var (checksum);
+
+CREATE TABLE icinga_notification_field (
+ notification_id integer NOT NULL,
+ datafield_id integer NOT NULL,
+ is_required enum_boolean NOT NULL,
+ var_filter TEXT DEFAULT NULL,
+ PRIMARY KEY (notification_id, datafield_id),
+ CONSTRAINT icinga_notification_field_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_field_datafield
+ FOREIGN KEY (datafield_id)
+ REFERENCES director_datafield (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX notification_field_key ON icinga_notification_field (notification_id, datafield_id);
+CREATE INDEX notification_field_notification ON icinga_notification_field (notification_id);
+CREATE INDEX notification_field_datafield ON icinga_notification_field (datafield_id);
+COMMENT ON COLUMN icinga_notification_field.notification_id IS 'Makes only sense for templates';
+
+
+CREATE TABLE icinga_notification_inheritance (
+ notification_id integer NOT NULL,
+ parent_notification_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (notification_id, parent_notification_id),
+ CONSTRAINT icinga_notification_inheritance_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_notification_inheritance_parent_notification
+ FOREIGN KEY (parent_notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX notification_inheritance ON icinga_notification_inheritance (notification_id, weight);
+
+
+CREATE TABLE icinga_var (
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ rendered_checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ varname character varying(255) NOT NULL,
+ varvalue TEXT NOT NULL,
+ rendered TEXT NOT NULL,
+ PRIMARY KEY (checksum)
+);
+
+CREATE INDEX var_search_idx ON icinga_var (varname);
+
+
+CREATE TABLE icinga_flat_var (
+ var_checksum bytea NOT NULL CHECK(LENGTH(var_checksum) = 20),
+ flatname_checksum bytea NOT NULL CHECK(LENGTH(flatname_checksum) = 20),
+ flatname character varying(512) NOT NULL,
+ flatvalue TEXT NOT NULL,
+ PRIMARY KEY (var_checksum, flatname_checksum),
+ CONSTRAINT flat_var_var
+ FOREIGN KEY (var_checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX flat_var_var_checksum ON icinga_flat_var (var_checksum);
+CREATE INDEX flat_var_search_varname ON icinga_flat_var (flatname);
+CREATE INDEX flat_var_search_varvalue ON icinga_flat_var (flatvalue);
+
+
+CREATE TABLE icinga_command_resolved_var (
+ command_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (command_id, checksum),
+ CONSTRAINT command_resolved_var_command
+ FOREIGN KEY (command_id)
+ REFERENCES icinga_command (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT command_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX command_resolved_var_search_varname ON icinga_command_resolved_var (varname);
+CREATE INDEX command_resolved_var_command_id ON icinga_command_resolved_var (command_id);
+CREATE INDEX command_resolved_var_schecksum ON icinga_command_resolved_var (checksum);
+
+
+CREATE TABLE icinga_host_resolved_var (
+ host_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (host_id, checksum),
+ CONSTRAINT host_resolved_var_host
+ FOREIGN KEY (host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT host_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX host_resolved_var_search_varname ON icinga_host_resolved_var (varname);
+CREATE INDEX host_resolved_var_host_id ON icinga_host_resolved_var (host_id);
+CREATE INDEX host_resolved_var_schecksum ON icinga_host_resolved_var (checksum);
+
+
+CREATE TABLE icinga_notification_resolved_var (
+ notification_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (notification_id, checksum),
+ CONSTRAINT notification_resolved_var_notification
+ FOREIGN KEY (notification_id)
+ REFERENCES icinga_notification (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT notification_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX notification_resolved_var_search_varname ON icinga_notification_resolved_var (varname);
+CREATE INDEX notification_resolved_var_notification_id ON icinga_notification_resolved_var (notification_id);
+CREATE INDEX notification_resolved_var_schecksum ON icinga_notification_resolved_var (checksum);
+
+
+CREATE TABLE icinga_service_set_resolved_var (
+ service_set_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (service_set_id, checksum),
+ CONSTRAINT service_set_resolved_var_service_set
+ FOREIGN KEY (service_set_id)
+ REFERENCES icinga_service_set (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT service_set_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX service_set_resolved_var_search_varname ON icinga_service_set_resolved_var (varname);
+CREATE INDEX service_set_resolved_var_service_set_id ON icinga_service_set_resolved_var (service_set_id);
+CREATE INDEX service_set_resolved_var_schecksum ON icinga_service_set_resolved_var (checksum);
+
+
+CREATE TABLE icinga_service_resolved_var (
+ service_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (service_id, checksum),
+ CONSTRAINT service_resolved_var_service
+ FOREIGN KEY (service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT service_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX service_resolved_var_search_varname ON icinga_service_resolved_var (varname);
+CREATE INDEX service_resolved_var_service_id ON icinga_service_resolved_var (service_id);
+CREATE INDEX service_resolved_var_schecksum ON icinga_service_resolved_var (checksum);
+
+
+CREATE TABLE icinga_user_resolved_var (
+ user_id integer NOT NULL,
+ varname character varying(255) NOT NULL,
+ checksum bytea NOT NULL CHECK(LENGTH(checksum) = 20),
+ PRIMARY KEY (user_id, checksum),
+ CONSTRAINT user_resolved_var_user
+ FOREIGN KEY (user_id)
+ REFERENCES icinga_user (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT user_resolved_var_checksum
+ FOREIGN KEY (checksum)
+ REFERENCES icinga_var (checksum)
+ ON DELETE RESTRICT
+ ON UPDATE RESTRICT
+);
+
+CREATE INDEX user_resolved_var_search_varname ON icinga_user_resolved_var (varname);
+CREATE INDEX user_resolved_var_user_id ON icinga_user_resolved_var (user_id);
+CREATE INDEX user_resolved_var_schecksum ON icinga_user_resolved_var (checksum);
+
+
+CREATE TABLE icinga_dependency (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean DEFAULT 'n',
+ apply_to enum_host_service NULL DEFAULT NULL,
+ parent_host_id integer DEFAULT NULL,
+ parent_host_var character varying(128) DEFAULT NULL,
+ parent_service_id integer DEFAULT NULL,
+ child_host_id integer DEFAULT NULL,
+ child_service_id integer DEFAULT NULL,
+ disable_checks enum_boolean DEFAULT NULL,
+ disable_notifications enum_boolean DEFAULT NULL,
+ ignore_soft_states enum_boolean DEFAULT NULL,
+ period_id integer DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ parent_service_by_name character varying(255),
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_dependency_parent_host
+ FOREIGN KEY (parent_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_parent_service
+ FOREIGN KEY (parent_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_child_host
+ FOREIGN KEY (child_host_id)
+ REFERENCES icinga_host (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_child_service
+ FOREIGN KEY (child_service_id)
+ REFERENCES icinga_service (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_period
+ FOREIGN KEY (period_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX dependency_parent_host ON icinga_dependency (parent_host_id);
+CREATE INDEX dependency_parent_service ON icinga_dependency (parent_service_id);
+CREATE INDEX dependency_child_host ON icinga_dependency (child_host_id);
+CREATE INDEX dependency_child_service ON icinga_dependency (child_service_id);
+CREATE INDEX dependency_period ON icinga_dependency (period_id);
+CREATE INDEX dependency_zone ON icinga_dependency (zone_id);
+
+
+CREATE TABLE icinga_dependency_inheritance (
+ dependency_id integer NOT NULL,
+ parent_dependency_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (dependency_id, parent_dependency_id),
+ CONSTRAINT icinga_dependency_inheritance_dependency
+ FOREIGN KEY (dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_dependency_inheritance_parent_dependency
+ FOREIGN KEY (parent_dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX dependency_inheritance_unique_order ON icinga_dependency_inheritance (dependency_id, weight);
+CREATE INDEX dependency_inheritance_dependency ON icinga_dependency_inheritance (dependency_id);
+CREATE INDEX dependency_inheritance_dependency_parent ON icinga_dependency_inheritance (parent_dependency_id);
+
+
+CREATE TABLE icinga_dependency_states_set (
+ dependency_id integer NOT NULL,
+ property enum_state_name NOT NULL,
+ merge_behaviour enum_set_merge_behaviour NOT NULL DEFAULT 'override',
+ PRIMARY KEY (dependency_id, property, merge_behaviour),
+ CONSTRAINT icinga_dependency_states_set_dependency
+ FOREIGN KEY (dependency_id)
+ REFERENCES icinga_dependency (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX dependency_states_set_dependency ON icinga_dependency_states_set (dependency_id);
+COMMENT ON COLUMN icinga_dependency_states_set.merge_behaviour IS 'override: = [], extend: += [], blacklist: -= []';
+
+CREATE TABLE icinga_timeperiod_include (
+ timeperiod_id integer NOT NULL,
+ include_id integer NOT NULL,
+ PRIMARY KEY (timeperiod_id, include_id),
+ CONSTRAINT icinga_timeperiod_timeperiod_include
+ FOREIGN KEY (include_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_timeperiod_include
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE icinga_timeperiod_exclude (
+ timeperiod_id integer NOT NULL,
+ exclude_id integer NOT NULL,
+ PRIMARY KEY (timeperiod_id, exclude_id),
+ CONSTRAINT icinga_timeperiod_timeperiod_exclude
+ FOREIGN KEY (exclude_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_timeperiod_exclude
+ FOREIGN KEY (timeperiod_id)
+ REFERENCES icinga_timeperiod (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+
+CREATE TABLE icinga_scheduled_downtime (
+ id serial,
+ uuid bytea UNIQUE CHECK(LENGTH(uuid) = 16),
+ object_name character varying(255) NOT NULL,
+ zone_id integer DEFAULT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean NOT NULL DEFAULT 'n',
+ apply_to enum_host_service NULL DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ author character varying(255) DEFAULT NULL,
+ comment text DEFAULT NULL,
+ fixed enum_boolean DEFAULT NULL,
+ duration int DEFAULT NULL,
+ with_services enum_boolean NULL DEFAULT NULL,
+ PRIMARY KEY (id),
+ CONSTRAINT icinga_scheduled_downtime_zone
+ FOREIGN KEY (zone_id)
+ REFERENCES icinga_zone (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX scheduled_downtime_object_name ON icinga_scheduled_downtime (object_name);
+CREATE INDEX scheduled_downtime_zone ON icinga_scheduled_downtime (zone_id);
+
+
+CREATE TABLE icinga_scheduled_downtime_inheritance (
+ scheduled_downtime_id integer NOT NULL,
+ parent_scheduled_downtime_id integer NOT NULL,
+ weight integer DEFAULT NULL,
+ PRIMARY KEY (scheduled_downtime_id, parent_scheduled_downtime_id),
+ CONSTRAINT icinga_scheduled_downtime_inheritance_scheduled_downtime
+ FOREIGN KEY (scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT icinga_scheduled_downtime_inheritance_parent_scheduled_downtime
+ FOREIGN KEY (parent_scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE RESTRICT
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX scheduled_downtime_inheritance_unique_order ON icinga_scheduled_downtime_inheritance (scheduled_downtime_id, weight);
+CREATE INDEX scheduled_downtime_inheritance_scheduled_downtime ON icinga_scheduled_downtime_inheritance (scheduled_downtime_id);
+CREATE INDEX scheduled_downtime_inheritance_scheduled_downtime_parent ON icinga_scheduled_downtime_inheritance (parent_scheduled_downtime_id);
+
+
+CREATE TABLE icinga_scheduled_downtime_range (
+ scheduled_downtime_id serial,
+ range_key character varying(255) NOT NULL,
+ range_value character varying(255) NOT NULL,
+ range_type enum_timeperiod_range_type NOT NULL DEFAULT 'include',
+ merge_behaviour enum_merge_behaviour NOT NULL DEFAULT 'set',
+ PRIMARY KEY (scheduled_downtime_id, range_type, range_key),
+ CONSTRAINT icinga_scheduled_downtime_range_scheduled_downtime
+ FOREIGN KEY (scheduled_downtime_id)
+ REFERENCES icinga_scheduled_downtime (id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE INDEX scheduled_downtime_range_scheduled_downtime ON icinga_scheduled_downtime_range (scheduled_downtime_id);
+COMMENT ON COLUMN icinga_scheduled_downtime_range.range_key IS 'monday, ...';
+COMMENT ON COLUMN icinga_scheduled_downtime_range.range_value IS '00:00-24:00, ...';
+COMMENT ON COLUMN icinga_scheduled_downtime_range.range_type IS 'include -> ranges {}, exclude ranges_ignore {} - not yet';
+COMMENT ON COLUMN icinga_scheduled_downtime_range.merge_behaviour IS 'set -> = {}, add -> += {}, substract -> -= {}';
+
+
+CREATE TABLE director_branch (
+ uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16),
+ owner character varying(255) NOT NULL,
+ branch_name character varying(255) NOT NULL,
+ description text DEFAULT NULL,
+ ts_merge_request bigint DEFAULT NULL,
+ PRIMARY KEY(uuid)
+);
+CREATE UNIQUE INDEX branch_branch_name ON director_branch (branch_name);
+
+CREATE TYPE enum_branch_action AS ENUM('create', 'modify', 'delete');
+
+CREATE TABLE director_branch_activity (
+ timestamp_ns bigint NOT NULL,
+ object_uuid bytea NOT NULL CHECK(LENGTH(object_uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ action enum_branch_action NOT NULL,
+ object_table character varying(64) NOT NULL,
+ author character varying(255) NOT NULL,
+ former_properties text NOT NULL,
+ modified_properties text NOT NULL,
+ PRIMARY KEY (timestamp_ns),
+ CONSTRAINT branch_activity_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+CREATE INDEX branch_activity_object_uuid ON director_branch_activity (object_uuid);
+CREATE INDEX branch_activity_branch_uuid ON director_branch_activity (branch_uuid);
+
+
+CREATE TABLE branched_icinga_host (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name CHARACTER VARYING(255) DEFAULT NULL,
+ address character varying(255) DEFAULT NULL,
+ address6 character varying(45) DEFAULT NULL,
+ check_command character varying(255) DEFAULT NULL,
+ max_check_attempts integer DEFAULT NULL,
+ check_period character varying(255) DEFAULT NULL,
+ check_interval character varying(8) DEFAULT NULL,
+ retry_interval character varying(8) DEFAULT NULL,
+ check_timeout smallint DEFAULT NULL,
+ enable_notifications enum_boolean DEFAULT NULL,
+ enable_active_checks enum_boolean DEFAULT NULL,
+ enable_passive_checks enum_boolean DEFAULT NULL,
+ enable_event_handler enum_boolean DEFAULT NULL,
+ enable_flapping enum_boolean DEFAULT NULL,
+ enable_perfdata enum_boolean DEFAULT NULL,
+ event_command character varying(255) DEFAULT NULL,
+ flapping_threshold_high smallint default null,
+ flapping_threshold_low smallint default null,
+ volatile enum_boolean DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+ command_endpoint character varying(255) DEFAULT NULL,
+ notes text DEFAULT NULL,
+ notes_url character varying(255) DEFAULT NULL,
+ action_url character varying(255) DEFAULT NULL,
+ icon_image character varying(255) DEFAULT NULL,
+ icon_image_alt character varying(255) DEFAULT NULL,
+ has_agent enum_boolean DEFAULT NULL,
+ master_should_connect enum_boolean DEFAULT NULL,
+ accept_config enum_boolean DEFAULT NULL,
+ custom_endpoint_name character varying(255) DEFAULT NULL,
+ api_key character varying(40) DEFAULT NULL,
+ -- template_choice character varying(255) DEFAULT NULL, -- TODO: Forbid them!
+
+ imports TEXT DEFAULT NULL,
+ groups TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_host_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX host_branch_object_name ON branched_icinga_host (branch_uuid, object_name);
+CREATE INDEX branched_host_search_object_name ON branched_icinga_host (object_name);
+CREATE INDEX branched_host_search_display_name ON branched_icinga_host (display_name);
+
+
+CREATE TABLE branched_icinga_hostgroup (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_hostgroup_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX hostgroup_branch_object_name ON branched_icinga_hostgroup (branch_uuid, object_name);
+CREATE INDEX branched_hostgroup_search_object_name ON branched_icinga_hostgroup (object_name);
+CREATE INDEX branched_hostgroup_search_display_name ON branched_icinga_hostgroup (display_name);
+
+
+CREATE TABLE branched_icinga_servicegroup (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_servicegroup_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX servicegroup_branch_object_name ON branched_icinga_servicegroup (branch_uuid, object_name);
+CREATE INDEX branched_servicegroup_search_object_name ON branched_icinga_servicegroup (object_name);
+CREATE INDEX branched_servicegroup_search_display_name ON branched_icinga_servicegroup (display_name);
+
+
+CREATE TABLE branched_icinga_usergroup (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_usergroup_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX usergroup_branch_object_name ON branched_icinga_usergroup (branch_uuid, object_name);
+CREATE INDEX branched_usergroup_search_object_name ON branched_icinga_usergroup (object_name);
+CREATE INDEX branched_usergroup_search_display_name ON branched_icinga_usergroup (display_name);
+
+
+CREATE TABLE branched_icinga_user (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ email character varying(255) DEFAULT NULL,
+ pager character varying(255) DEFAULT NULL,
+ enable_notifications enum_boolean DEFAULT NULL,
+ period character varying(255) DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ groups TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_user_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX user_branch_object_name ON branched_icinga_user (branch_uuid, object_name);
+CREATE INDEX branched_user_search_object_name ON branched_icinga_user (object_name);
+CREATE INDEX branched_user_search_display_name ON branched_icinga_user (display_name);
+
+
+CREATE TABLE branched_icinga_zone (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ parent character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ is_global enum_boolean DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_zone_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX zone_branch_object_name ON branched_icinga_zone (branch_uuid, object_name);
+CREATE INDEX branched_zone_search_object_name ON branched_icinga_zone (object_name);
+
+
+CREATE TABLE branched_icinga_timeperiod (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ update_method character varying(64) DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ prefer_includes enum_boolean DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ ranges TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_timeperiod_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX timeperiod_branch_object_name ON branched_icinga_timeperiod (branch_uuid, object_name);
+CREATE INDEX branched_timeperiod_search_object_name ON branched_icinga_timeperiod (object_name);
+CREATE INDEX branched_timeperiod_search_display_name ON branched_icinga_timeperiod (display_name);
+
+
+CREATE TABLE branched_icinga_command (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean NOT NULL DEFAULT NULL,
+ methods_execute character varying(64) DEFAULT NULL,
+ command text DEFAULT NULL,
+ is_string enum_boolean DEFAULT NULL,
+-- env text DEFAULT NULL,
+ timeout smallint DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ arguments TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_command_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX command_branch_object_name ON branched_icinga_command (branch_uuid, object_name);
+CREATE INDEX branched_command_search_object_name ON branched_icinga_command (object_name);
+
+
+CREATE TABLE branched_icinga_apiuser (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name CHARACTER VARYING(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean NOT NULL DEFAULT NULL,
+ password CHARACTER VARYING(255) DEFAULT NULL,
+ client_dn CHARACTER VARYING(64) DEFAULT NULL,
+ permissions TEXT DEFAULT NULL,
+
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_apiuser_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX apiuser_branch_object_name ON branched_icinga_apiuser (branch_uuid, object_name);
+CREATE INDEX branched_apiuser_search_object_name ON branched_icinga_apiuser (object_name);
+
+
+CREATE TABLE branched_icinga_endpoint (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ zone character varying(255) DEFAULT NULL,
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean NOT NULL DEFAULT NULL,
+ host character varying(255) DEFAULT NULL,
+ port d_smallint DEFAULT NULL,
+ log_duration character varying(32) DEFAULT NULL,
+ apiuser character varying(255) DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_endpoint_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX endpoint_branch_object_name ON branched_icinga_endpoint (branch_uuid, object_name);
+CREATE INDEX branched_endpoint_search_object_name ON branched_icinga_endpoint (object_name);
+
+
+CREATE TABLE branched_icinga_service (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ display_name character varying(255) DEFAULT NULL,
+ host character varying(255) DEFAULT NULL,
+ service_set character varying(255) DEFAULT NULL,
+ check_command character varying(255) DEFAULT NULL,
+ max_check_attempts integer DEFAULT NULL,
+ check_period character varying(255) DEFAULT NULL,
+ check_interval character varying(8) DEFAULT NULL,
+ retry_interval character varying(8) DEFAULT NULL,
+ check_timeout smallint DEFAULT NULL,
+ enable_notifications enum_boolean DEFAULT NULL,
+ enable_active_checks enum_boolean DEFAULT NULL,
+ enable_passive_checks enum_boolean DEFAULT NULL,
+ enable_event_handler enum_boolean DEFAULT NULL,
+ enable_flapping enum_boolean DEFAULT NULL,
+ enable_perfdata enum_boolean DEFAULT NULL,
+ event_command character varying(255) DEFAULT NULL,
+ flapping_threshold_high smallint DEFAULT NULL,
+ flapping_threshold_low smallint DEFAULT NULL,
+ volatile enum_boolean DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+ command_endpoint character varying(255) DEFAULT NULL,
+ notes text DEFAULT NULL,
+ notes_url character varying(255) DEFAULT NULL,
+ action_url character varying(255) DEFAULT NULL,
+ icon_image character varying(255) DEFAULT NULL,
+ icon_image_alt character varying(255) DEFAULT NULL,
+ use_agent enum_boolean DEFAULT NULL,
+ apply_for character varying(255) DEFAULT NULL,
+ use_var_overrides enum_boolean DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ -- template_choice_id int DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ groups TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_service_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_branch_object_name ON branched_icinga_service (branch_uuid, object_name);
+CREATE INDEX branched_service_search_object_name ON branched_icinga_service (object_name);
+CREATE INDEX branched_service_search_display_name ON branched_icinga_service (display_name);
+
+
+CREATE TABLE branched_icinga_service_set (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ host character varying(255) DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+
+
+ imports TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_service_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX service_set_branch_object_name ON branched_icinga_service_set (branch_uuid, object_name);
+CREATE INDEX branched_service_set_search_object_name ON branched_icinga_service_set (object_name);
+
+
+CREATE TABLE branched_icinga_notification (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name CHARACTER VARYING(255) DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ apply_to enum_host_service DEFAULT NULL,
+ host character varying(255) DEFAULT NULL,
+ service character varying(255) DEFAULT NULL,
+ times_begin integer DEFAULT NULL,
+ times_end integer DEFAULT NULL,
+ notification_interval integer DEFAULT NULL,
+ command character varying(255) DEFAULT NULL,
+ period character varying(255) DEFAULT NULL,
+ zone character varying(255) DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+
+ states TEXT DEFAULT NULL,
+ types TEXT DEFAULT NULL,
+ users TEXT DEFAULT NULL,
+ usergroups TEXT DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ vars TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_notification_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX notification_branch_object_name ON branched_icinga_notification (branch_uuid, object_name);
+CREATE INDEX branched_notification_search_object_name ON branched_icinga_notification (object_name);
+
+
+CREATE TABLE branched_icinga_scheduled_downtime (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ object_type enum_object_type_all DEFAULT NULL,
+ disabled enum_boolean DEFAULT NULL,
+ apply_to enum_host_service DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ author character varying(255) DEFAULT NULL,
+ comment text DEFAULT NULL,
+ fixed enum_boolean DEFAULT NULL,
+ duration int DEFAULT NULL,
+ with_services enum_boolean DEFAULT NULL,
+
+ imports TEXT DEFAULT NULL,
+ ranges TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_scheduled_downtime_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX scheduled_downtime_branch_object_name ON branched_icinga_scheduled_downtime (branch_uuid, object_name);
+CREATE INDEX branched_scheduled_downtime_search_object_name ON branched_icinga_scheduled_downtime (object_name);
+
+
+CREATE TABLE branched_icinga_dependency (
+ uuid bytea NOT NULL CHECK(LENGTH(uuid) = 16),
+ branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16),
+ branch_created enum_boolean NOT NULL DEFAULT 'n',
+ branch_deleted enum_boolean NOT NULL DEFAULT 'n',
+
+ object_name character varying(255) NOT NULL,
+ object_type enum_object_type_all NOT NULL,
+ disabled enum_boolean DEFAULT 'n',
+ apply_to enum_host_service NULL DEFAULT NULL,
+ parent_host character varying(255) DEFAULT NULL,
+ parent_host_var character varying(128) DEFAULT NULL,
+ parent_service character varying(255) DEFAULT NULL,
+ child_host character varying(255) DEFAULT NULL,
+ child_service character varying(255) DEFAULT NULL,
+ disable_checks enum_boolean DEFAULT NULL,
+ disable_notifications enum_boolean DEFAULT NULL,
+ ignore_soft_states enum_boolean DEFAULT NULL,
+ period_id integer DEFAULT NULL,
+ zone_id integer DEFAULT NULL,
+ assign_filter text DEFAULT NULL,
+ parent_service_by_name character varying(255),
+
+ imports TEXT DEFAULT NULL,
+ set_null TEXT DEFAULT NULL,
+ PRIMARY KEY (branch_uuid, uuid),
+ CONSTRAINT icinga_dependency_branch
+ FOREIGN KEY (branch_uuid)
+ REFERENCES director_branch (uuid)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE UNIQUE INDEX dependency_branch_object_name ON branched_icinga_dependency (branch_uuid, object_name);
+CREATE INDEX branched_dependency_search_object_name ON branched_icinga_dependency (object_name);
+
+
+INSERT INTO director_schema_migration
+ (schema_version, migration_time)
+ VALUES (182, NOW());
diff --git a/test/bootstrap.php b/test/bootstrap.php
new file mode 100644
index 0000000..87ed869
--- /dev/null
+++ b/test/bootstrap.php
@@ -0,0 +1,16 @@
+<?php
+
+use Icinga\Module\Director\Test\Bootstrap;
+
+call_user_func(function () {
+ $basedir = dirname(__DIR__);
+ if (! class_exists('PHPUnit_Framework_TestCase')) {
+ require_once __DIR__ . '/phpunit-compat.php';
+ }
+
+ $include_path = $basedir . '/vendor' . PATH_SEPARATOR . ini_get('include_path');
+ ini_set('include_path', $include_path);
+
+ require_once $basedir . '/library/Director/Test/Bootstrap.php';
+ Bootstrap::cli($basedir);
+});
diff --git a/test/config/authentication.ini b/test/config/authentication.ini
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/config/authentication.ini
diff --git a/test/config/config.ini b/test/config/config.ini
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/config/config.ini
diff --git a/test/config/resources.ini b/test/config/resources.ini
new file mode 100644
index 0000000..1f64e52
--- /dev/null
+++ b/test/config/resources.ini
@@ -0,0 +1,13 @@
+[Director MySQL TestDB]
+type = "db"
+db = "mysql"
+host = "localhost"
+username = "root"
+charset = "utf8"
+
+[Director PostgreSQL TestDB]
+type = "db"
+db = "pgsql"
+host = "localhost"
+password = "testing"
+charset = "utf8"
diff --git a/test/php/library/Director/Application/DependencyTest.php b/test/php/library/Director/Application/DependencyTest.php
new file mode 100644
index 0000000..cc6047e
--- /dev/null
+++ b/test/php/library/Director/Application/DependencyTest.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Application;
+
+use Icinga\Module\Director\Application\Dependency;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class DependencyTest extends BaseTestCase
+{
+ public function testIsNotInstalled()
+ {
+ $dependency = new Dependency('something', '>=0.3.0');
+ $this->assertFalse($dependency->isInstalled());
+ }
+
+ public function testNotSatisfiedWhenNotInstalled()
+ {
+ $dependency = new Dependency('something', '>=0.3.0');
+ $this->assertFalse($dependency->isSatisfied());
+ }
+
+ public function testIsInstalled()
+ {
+ $dependency = new Dependency('something', '>=0.3.0');
+ $dependency->setInstalledVersion('1.10.0');
+ $this->assertTrue($dependency->isInstalled());
+ }
+
+ public function testNotEnabled()
+ {
+ $dependency = new Dependency('something', '>=0.3.0');
+ $this->assertFalse($dependency->isEnabled());
+ }
+
+ public function testIsEnabled()
+ {
+ $dependency = new Dependency('something', '>=0.3.0');
+ $dependency->setEnabled();
+ $this->assertTrue($dependency->isEnabled());
+ }
+
+ public function testNotSatisfiedWhenNotEnabled()
+ {
+ $dependency = new Dependency('something', '>=0.3.0');
+ $dependency->setInstalledVersion('1.10.0');
+ $this->assertFalse($dependency->isSatisfied());
+ }
+
+ public function testSatisfiedWhenEqual()
+ {
+ $dependency = new Dependency('something', '>=0.3.0');
+ $dependency->setInstalledVersion('0.3.0');
+ $dependency->setEnabled();
+ $this->assertTrue($dependency->isSatisfied());
+ }
+
+ public function testSatisfiedWhenGreater()
+ {
+ $dependency = new Dependency('something', '>=0.3.0');
+ $dependency->setInstalledVersion('0.10.0');
+ $dependency->setEnabled();
+ $this->assertTrue($dependency->isSatisfied());
+ }
+
+ public function testNotSatisfiedWhenSmaller()
+ {
+ $dependency = new Dependency('something', '>=20.3.0');
+ $dependency->setInstalledVersion('4.999.999');
+ $dependency->setEnabled();
+ $this->assertFalse($dependency->isSatisfied());
+ }
+}
diff --git a/test/php/library/Director/Application/FiltersWorkAsExpectedTest.php b/test/php/library/Director/Application/FiltersWorkAsExpectedTest.php
new file mode 100644
index 0000000..216a925
--- /dev/null
+++ b/test/php/library/Director/Application/FiltersWorkAsExpectedTest.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Application;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class FiltersWorkAsExpectedTest extends BaseTestCase
+{
+ public function testBasics()
+ {
+ $filter = Filter::fromQueryString('a');
+ $this->assertTrue($filter->matches((object) ['a' => '1']), '1 is not true');
+ }
+}
diff --git a/test/php/library/Director/Application/MemoryLimitTest.php b/test/php/library/Director/Application/MemoryLimitTest.php
new file mode 100644
index 0000000..8b4301d
--- /dev/null
+++ b/test/php/library/Director/Application/MemoryLimitTest.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Application;
+
+use Icinga\Module\Director\Application\MemoryLimit;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class MemoryLimitTest extends BaseTestCase
+{
+ public function testBytesValuesAreHandled()
+ {
+ $this->assertTrue(is_int(MemoryLimit::parsePhpIniByteString('1073741824')));
+ $this->assertEquals(
+ 1073741824,
+ MemoryLimit::parsePhpIniByteString('1073741824')
+ );
+ }
+
+ public function testIntegersAreAccepted()
+ {
+ $this->assertEquals(
+ MemoryLimit::parsePhpIniByteString(1073741824),
+ 1073741824
+ );
+ }
+
+ public function testNoLimitGivesMinusOne()
+ {
+ $this->assertTrue(is_int(MemoryLimit::parsePhpIniByteString('-1')));
+ $this->assertEquals(
+ -1,
+ MemoryLimit::parsePhpIniByteString('-1')
+ );
+ }
+
+ public function testInvalidStringGivesBytes()
+ {
+ $this->assertEquals(
+ 1024,
+ MemoryLimit::parsePhpIniByteString('1024MB')
+ );
+ }
+
+ public function testHandlesKiloBytes()
+ {
+ $this->assertEquals(
+ 45 * 1024,
+ MemoryLimit::parsePhpIniByteString('45K')
+ );
+ }
+
+ public function testHandlesMegaBytes()
+ {
+ $this->assertEquals(
+ 512 * 1024 * 1024,
+ MemoryLimit::parsePhpIniByteString('512M')
+ );
+ }
+
+ public function testHandlesGigaBytes()
+ {
+ $this->assertEquals(
+ 2 * 1024 * 1024 * 1024,
+ MemoryLimit::parsePhpIniByteString('2G')
+ );
+ }
+}
diff --git a/test/php/library/Director/CustomVariable/CustomVariablesTest.php b/test/php/library/Director/CustomVariable/CustomVariablesTest.php
new file mode 100644
index 0000000..c5ba9f6
--- /dev/null
+++ b/test/php/library/Director/CustomVariable/CustomVariablesTest.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\CustomVariable;
+
+use Icinga\Module\Director\CustomVariable\CustomVariables;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class CustomVariablesTest extends BaseTestCase
+{
+ protected $indent = ' ';
+
+ public function testWhetherSpecialKeyNames()
+ {
+ $vars = $this->newVars();
+ $vars->bla = 'da';
+ $vars->{'aBc'} = 'normal';
+ $vars->{'a-0'} = 'special';
+ $expected = $this->indentVarsList([
+ 'vars["a-0"] = "special"',
+ 'vars.aBc = "normal"',
+ 'vars.bla = "da"'
+ ]);
+ $this->assertEquals($expected, $vars->toConfigString());
+ }
+
+ public function testVarsCanBeUnsetAndSetAgain()
+ {
+ $vars = $this->newVars();
+ $vars->one = 'two';
+ unset($vars->one);
+ $vars->one = 'three';
+
+ $res = [];
+ foreach ($vars as $k => $v) {
+ $res[$k] = $v->getValue();
+ }
+
+ $this->assertEquals(['one' => 'three'], $res);
+ }
+
+ public function testNumericKeysAreRenderedWithArraySyntax()
+ {
+ $vars = $this->newVars();
+ $vars->{'1'} = 1;
+ $expected = $this->indentVarsList([
+ 'vars["1"] = 1'
+ ]);
+
+ $this->assertEquals(
+ $expected,
+ $vars->toConfigString(true)
+ );
+ }
+
+ public function testVariablesToExpression()
+ {
+ $vars = $this->newVars();
+ $vars->bla = 'da';
+ $vars->abc = '$val$';
+ $expected = $this->indentVarsList([
+ 'vars.abc = "$val$"',
+ 'vars.bla = "da"'
+ ]);
+ $this->assertEquals($expected, $vars->toConfigString(true));
+ }
+
+ protected function indentVarsList($vars)
+ {
+ return $this->indent . implode(
+ "\n" . $this->indent,
+ $vars
+ ) . "\n";
+ }
+
+ protected function newVars()
+ {
+ return new CustomVariables();
+ }
+}
diff --git a/test/php/library/Director/Data/AssignFilterHelperTest.php b/test/php/library/Director/Data/AssignFilterHelperTest.php
new file mode 100644
index 0000000..5fcdd95
--- /dev/null
+++ b/test/php/library/Director/Data/AssignFilterHelperTest.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Data\AssignFilterHelper;
+use Icinga\Module\Director\Objects\HostApplyMatches;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class AssignFilterHelperTest extends BaseTestCase
+{
+ protected static $exampleHost;
+
+ public static function setUpBeforeClass()
+ {
+ self::$exampleHost = (object) [
+ 'address' => '127.0.0.1',
+ 'vars.operatingsystem' => 'centos',
+ 'vars.customer' => 'ACME',
+ 'vars.roles' => ['webserver', 'mailserver'],
+ 'vars.bool_string' => 'true',
+ 'groups' => ['web-server', 'mail-server'],
+ ];
+ }
+
+ public function testSimpleApplyFilter()
+ {
+ $this->assertFilterOutcome(true, 'host.address=true', self::$exampleHost);
+ $this->assertFilterOutcome(false, 'host.address=false', self::$exampleHost);
+ $this->assertFilterOutcome(true, 'host.address=false', (object) ['address' => null]);
+ $this->assertFilterOutcome(false, 'host.address=true', (object) ['address' => null]);
+ $this->assertFilterOutcome(true, 'host.address=%22127.0.0.%2A%22', self::$exampleHost);
+ }
+
+ public function testListApplyFilter()
+ {
+ $this->assertFilterOutcome(true, 'host.vars.roles=%22*server%22', self::$exampleHost);
+ $this->assertFilterOutcome(true, 'host.groups=%22*-server%22', self::$exampleHost);
+ $this->assertFilterOutcome(false, 'host.groups=%22*-nothing%22', self::$exampleHost);
+ }
+
+ public function testComplexApplyFilter()
+ {
+ $this->assertFilterOutcome(
+ true,
+ 'host.vars.operatingsystem=%5B%22centos%22%2C%22fedora%22%5D|host.vars.osfamily=%22redhat%22',
+ self::$exampleHost
+ );
+
+ $this->assertFilterOutcome(
+ false,
+ 'host.vars.operatingsystem=%5B%22centos%22%2C%22fedora%22%5D&(!(host.vars.customer=%22acme*%22))',
+ self::$exampleHost
+ );
+
+ $this->assertFilterOutcome(
+ true,
+ '!(host.vars.bool_string="false")&host.vars.operatingsystem="centos"',
+ self::$exampleHost
+ );
+ }
+
+ /**
+ * @param bool $expected
+ * @param string $filterQuery
+ * @param object $object
+ * @param string $message
+ */
+ protected function assertFilterOutcome($expected, $filterQuery, $object, $message = null, $type = 'host')
+ {
+ $filter = Filter::fromQueryString($filterQuery);
+
+ if ($type === 'host') {
+ HostApplyMatches::fixFilterColumns($filter);
+ }
+
+ $helper = new AssignFilterHelper($filter);
+ $actual = $helper->matches($object);
+
+ if ($message === null) {
+ $message = sprintf('with filter "%s"', $filterQuery);
+ }
+
+ $this->assertEquals($expected, $actual, $message);
+ }
+}
diff --git a/test/php/library/Director/Data/RecursiveUtf8ValidatorTest.php b/test/php/library/Director/Data/RecursiveUtf8ValidatorTest.php
new file mode 100644
index 0000000..1434d4a
--- /dev/null
+++ b/test/php/library/Director/Data/RecursiveUtf8ValidatorTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Module\Director\Data\RecursiveUtf8Validator;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class RecursiveUtf8ValidatorTest extends BaseTestCase
+{
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testDetectInvalidUtf8Character()
+ {
+ RecursiveUtf8Validator::validateRows([
+ (object) [
+ 'name' => 'test 1',
+ 'value' => 'something',
+ ],
+ (object) [
+ 'name' => 'test 2',
+ 'value' => "some\xa1\xa2thing",
+ ],
+ ]);
+ }
+
+ public function testAcceptValidUtf8Characters()
+ {
+ $this->assertTrue(RecursiveUtf8Validator::validateRows([
+ (object) [
+ 'name' => 'test 1',
+ 'value' => "Some 🍻",
+ ],
+ (object) [
+ 'name' => 'test 2',
+ 'value' => [
+ (object) [
+ 'its' => true,
+ ['💩']
+ ]
+ ],
+ ],
+ ]));
+ }
+}
diff --git a/test/php/library/Director/IcingaConfig/AssignRendererTest.php b/test/php/library/Director/IcingaConfig/AssignRendererTest.php
new file mode 100644
index 0000000..b9f574e
--- /dev/null
+++ b/test/php/library/Director/IcingaConfig/AssignRendererTest.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\IcingaConfig\AssignRenderer;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class AssignRendererTest extends BaseTestCase
+{
+ public function testEqualMatchIsCorrectlyRendered()
+ {
+ $string = 'host.name="localhost"';
+ $expected = 'assign where host.name == "localhost"';
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+ }
+
+ public function testNegationIsRenderedCorrectlyOnRootLevel()
+ {
+ $string = '!(host.name="one"&host.name="two")';
+ $expected = 'assign where !(host.name == "one" && host.name == "two")';
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+ }
+
+ public function testNegationIsRenderedCorrectlyOnDeeperLevel()
+ {
+ $string = 'host.address="127.*"&!host.name="localhost"';
+ $expected = 'assign where match("127.*", host.address) && !(host.name == "localhost")';
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+ }
+
+ public function testWildcardsRenderAMatchMethod()
+ {
+ $string = 'host.address="127.0.0.*"';
+ $expected = 'assign where match("127.0.0.*", host.address)';
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+ }
+
+ public function testACombinedFilterRendersCorrectly()
+ {
+ $string = 'host.name="*internal"|(service.vars.priority<2'
+ . '&host.vars.is_clustered=true)';
+
+ $expected = 'assign where match("*internal", host.name) ||'
+ . ' (service.vars.priority < 2 && host.vars.is_clustered)';
+
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+ }
+
+ public function testSlashesAreNotEscaped()
+ {
+ $string = 'host.name=' . json_encode('a/b');
+
+ $expected = 'assign where host.name == "a/b"';
+
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+ }
+
+ public function testFakeContainsOperatorRendersCorrectly()
+ {
+ $string = json_encode('member') . '=host.groups';
+
+ $expected = 'assign where "member" in host.groups';
+
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+
+ $string = json_encode('member') . '=host.vars.some_array';
+
+ $expected = 'assign where "member" in host.vars.some_array';
+
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+ }
+
+ public function testInArrayIsRenderedCorrectly()
+ {
+ $string = 'host.name=' . json_encode(array('a' ,'b'));
+
+ $expected = 'assign where host.name in [ "a", "b" ]';
+
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+ }
+
+ public function testWhetherSlashesAreNotEscaped()
+ {
+ $string = 'host.name=' . json_encode('a/b');
+
+ $expected = 'assign where host.name == "a/b"';
+
+ $this->assertEquals(
+ $expected,
+ $this->renderer($string)->renderAssign()
+ );
+ }
+
+ protected function renderer($string)
+ {
+ return AssignRenderer::forFilter(Filter::fromQueryString($string));
+ }
+}
diff --git a/test/php/library/Director/IcingaConfig/ExtensibleSetTest.php b/test/php/library/Director/IcingaConfig/ExtensibleSetTest.php
new file mode 100644
index 0000000..34bd83a
--- /dev/null
+++ b/test/php/library/Director/IcingaConfig/ExtensibleSetTest.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Module\Director\IcingaConfig\ExtensibleSet;
+use Icinga\Module\Director\Objects\IcingaUser;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class ExtensibleSetTest extends BaseTestCase
+{
+ public function testNoValuesResultInEmptySet()
+ {
+ $set = new ExtensibleSet();
+
+ $this->assertEquals(
+ array(),
+ $set->getResolvedValues()
+ );
+ }
+
+ public function testValuesPassedToConstructorAreAccepted()
+ {
+ $values = array('Val1', 'Val2', 'Val4');
+ $set = new ExtensibleSet($values);
+
+ $this->assertEquals(
+ $values,
+ $set->getResolvedValues()
+ );
+ }
+
+ public function testConstructorAcceptsSingleValues()
+ {
+ $set = new ExtensibleSet('Bla');
+
+ $this->assertEquals(
+ array('Bla'),
+ $set->getResolvedValues()
+ );
+ }
+
+ public function testSingleValuesCanBeBlacklisted()
+ {
+ $values = array('Val1', 'Val2', 'Val4');
+ $set = new ExtensibleSet($values);
+ $set->blacklist('Val2');
+
+ $this->assertEquals(
+ array('Val1', 'Val4'),
+ $set->getResolvedValues()
+ );
+ }
+
+ public function testMultipleValuesCanBeBlacklisted()
+ {
+ $values = array('Val1', 'Val2', 'Val4');
+ $set = new ExtensibleSet($values);
+ $set->blacklist(array('Val4', 'Val1'));
+
+ $this->assertEquals(
+ array('Val2'),
+ $set->getResolvedValues()
+ );
+ }
+
+ public function testSimpleInheritanceWorksFine()
+ {
+ $values = array('Val1', 'Val2', 'Val4');
+ $parent = new ExtensibleSet($values);
+ $child = new ExtensibleSet();
+ $child->inheritFrom($parent);
+
+ $this->assertEquals(
+ $values,
+ $child->getResolvedValues()
+ );
+ }
+
+ public function testWeCanInheritFromMultipleParents()
+ {
+ $p1set = array('p1a', 'p1c');
+ $p2set = array('p2a', 'p2d');
+ $parent1 = new ExtensibleSet($p1set);
+ $parent2 = new ExtensibleSet($p2set);
+ $child = new ExtensibleSet();
+ $child->inheritFrom($parent1)->inheritFrom($parent2);
+
+ $this->assertEquals(
+ $p2set,
+ $child->getResolvedValues()
+ );
+ }
+
+ public function testOwnValuesOverrideParents()
+ {
+ $cset = array('p1a', 'p1c');
+ $pset = array('p2a', 'p2d');
+ $child = new ExtensibleSet($cset);
+ $parent = new ExtensibleSet($pset);
+ $child->inheritFrom($parent);
+
+ $this->assertEquals(
+ $cset,
+ $child->getResolvedValues()
+ );
+ }
+
+ public function testInheritedValuesCanBeBlacklisted()
+ {
+ $child = new ExtensibleSet();
+ $child->blacklist('p2');
+
+ $pset = array('p1', 'p2', 'p3');
+ $parent = new ExtensibleSet($pset);
+
+ $child->inheritFrom($parent);
+ $child->blacklist(array('not', 'yet', 'p1'));
+
+ $this->assertEquals(
+ array('p3'),
+ $child->getResolvedValues()
+ );
+
+ $child->blacklist(array('p3'));
+ $this->assertEquals(
+ array(),
+ $child->getResolvedValues()
+ );
+ }
+
+ public function testInheritedValuesCanBeExtended()
+ {
+ $pset = array('p1', 'p2', 'p3');
+
+ $child = new ExtensibleSet();
+ $child->extend('p5');
+
+ $parent = new ExtensibleSet($pset);
+ $child->inheritFrom($parent);
+
+ $this->assertEquals(
+ array('p1', 'p2', 'p3', 'p5'),
+ $child->getResolvedValues()
+ );
+ }
+
+ public function testCombinedDefinitionRendersCorrectly()
+ {
+ $set = new ExtensibleSet(array('Pre', 'Def', 'Ined'));
+ $set->blacklist(array('And', 'Not', 'Those'));
+ $set->extend('PlusThis');
+
+ $out = ' key_name = [ Pre, Def, Ined ]' . "\n"
+ . ' key_name += [ PlusThis ]' . "\n"
+ . ' key_name -= [ And, Not, Those ]' . "\n";
+
+ $this->assertEquals(
+ $out,
+ $set->renderAs('key_name')
+ );
+ }
+}
diff --git a/test/php/library/Director/IcingaConfig/IcingaConfigHelperTest.php b/test/php/library/Director/IcingaConfig/IcingaConfigHelperTest.php
new file mode 100644
index 0000000..506f3b8
--- /dev/null
+++ b/test/php/library/Director/IcingaConfig/IcingaConfigHelperTest.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class IcingaConfigHelperTest extends BaseTestCase
+{
+ public function testWhetherIntervalStringIsCorrectlyParsed()
+ {
+ $this->assertEquals(c::parseInterval('0'), 0);
+ $this->assertEquals(c::parseInterval('0s'), 0);
+ $this->assertEquals(c::parseInterval('10'), 10);
+ $this->assertEquals(c::parseInterval('70s'), 70);
+ $this->assertEquals(c::parseInterval('5m 10s'), 310);
+ $this->assertEquals(c::parseInterval('5m 60s'), 360);
+ $this->assertEquals(c::parseInterval('1h 5m 60s'), 3960);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testWhetherInvalidIntervalStringRaisesException()
+ {
+ c::parseInterval('1h 5m 60x');
+ }
+
+ public function testWhetherAnEmptyValueGivesNull()
+ {
+ $this->assertNull(c::parseInterval(''));
+ $this->assertNull(c::parseInterval(null));
+ }
+
+ public function testWhetherIntervalStringIsCorrectlyRendered()
+ {
+ $this->assertEquals(c::renderInterval(10), '10s');
+ $this->assertEquals(c::renderInterval(60), '1m');
+ $this->assertEquals(c::renderInterval(121), '121s');
+ $this->assertEquals(c::renderInterval(3600), '1h');
+ $this->assertEquals(c::renderInterval(86400), '1d');
+ $this->assertEquals(c::renderInterval(86459), '86459s');
+ }
+
+ public function testCorrectlyIdentifiesReservedWords()
+ {
+ $this->assertTrue(c::isReserved('include'), 'include is a reserved word');
+ $this->assertFalse(c::isReserved(0), '(int) 0 is not a reserved word');
+ $this->assertFalse(c::isReserved(1), '(int) 1 is not a reserved word');
+ $this->assertFalse(c::isReserved(true), '(boolean) true is not a reserved word');
+ $this->assertTrue(c::isReserved('true'), '(string) true is a reserved word');
+ }
+
+ public function testWhetherDictionaryRendersCorrectly()
+ {
+ $dict = (object) [
+ 'key1' => 'bla',
+ 'include' => 'reserved',
+ 'spe cial' => 'value',
+ '0' => 'numeric',
+ ];
+ $this->assertEquals(
+ c::renderDictionary($dict),
+ rtrim($this->loadRendered('dict1'))
+ );
+ }
+
+ protected function loadRendered($name)
+ {
+ return file_get_contents(__DIR__ . '/rendered/' . $name . '.out');
+ }
+
+ public function testRenderStringIsCorrectlyRendered()
+ {
+ $this->assertEquals(c::renderString('val1\\\val2'), '"val1\\\\\\\\val2"');
+ $this->assertEquals(c::renderString('"val1"'), '"\"val1\""');
+ $this->assertEquals(c::renderString('\$val\$'), '"\\\\$val\\\\$"');
+ $this->assertEquals(c::renderString('\t'), '"\\\\t"');
+ $this->assertEquals(c::renderString('\r'), '"\\\\r"');
+ $this->assertEquals(c::renderString('\n'), '"\\\\n"');
+ $this->assertEquals(c::renderString('\f'), '"\\\\f"');
+ }
+
+ public function testMacrosAreDetected()
+ {
+ $this->assertFalse(c::stringHasMacro('$$vars$'));
+ $this->assertFalse(c::stringHasMacro('$$'));
+ $this->assertTrue(c::stringHasMacro('$vars$$'));
+ $this->assertTrue(c::stringHasMacro('$multiple$$vars.nested.name$$vars$ is here'));
+ $this->assertTrue(c::stringHasMacro('some $vars.nested.name$ is here'));
+ $this->assertTrue(c::stringHasMacro('some $vars.nested.name$$vars.even.more$'));
+ $this->assertTrue(c::stringHasMacro('$vars.nested.name$$a$$$$not$'));
+ $this->assertTrue(c::stringHasMacro('MSSQL$$$config$'));
+ $this->assertTrue(c::stringHasMacro('MSSQL$$$config$', 'config'));
+ $this->assertTrue(c::stringHasMacro('MSSQL$$$nix$ and $config$', 'config'));
+ $this->assertFalse(c::stringHasMacro('MSSQL$$$nix$config$ and $$', 'config'));
+ $this->assertFalse(c::stringHasMacro('MSSQL$$$nix$ and $$config$', 'config'));
+ $this->assertFalse(c::stringHasMacro('MSSQL$$$config$', 'conf'));
+ }
+
+ public function testRenderStringWithVariables()
+ {
+ $this->assertEquals('"Before " + var', c::renderStringWithVariables('Before $var$'));
+ $this->assertEquals(c::renderStringWithVariables('$var$ After'), 'var + " After"');
+ $this->assertEquals(c::renderStringWithVariables('$var$'), 'var');
+ $this->assertEquals(c::renderStringWithVariables('$$var$$'), '"$$var$$"');
+ $this->assertEquals(c::renderStringWithVariables('Before $$var$$ After'), '"Before $$var$$ After"');
+ $this->assertEquals(
+ '"Before " + name1 + " " + name2 + " After"',
+ c::renderStringWithVariables('Before $name1$ $name2$ After')
+ );
+ }
+
+ public function testRenderStringWithVariablesX()
+ {
+ $this->assertEquals(
+ '"Before " + var1 + " " + var2 + " After"',
+ c::renderStringWithVariables('Before $var1$ $var2$ After')
+ );
+ $this->assertEquals(
+ 'host.vars.custom',
+ c::renderStringWithVariables('$host.vars.custom$')
+ );
+ $this->assertEquals('"$var\"$"', c::renderStringWithVariables('$var"$'));
+ $this->assertEquals(
+ '"\\\\tI am\\\\rrendering\\\\nproperly\\\\fand I " + support + " \"multiple\" " + variables + "\\\\$"',
+ c::renderStringWithVariables('\tI am\rrendering\nproperly\fand I $support$ "multiple" $variables$\$')
+ );
+ }
+}
diff --git a/test/php/library/Director/IcingaConfig/StateFilterTest.php b/test/php/library/Director/IcingaConfig/StateFilterTest.php
new file mode 100644
index 0000000..82e94d8
--- /dev/null
+++ b/test/php/library/Director/IcingaConfig/StateFilterTest.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Module\Director\IcingaConfig\StateFilterSet;
+use Icinga\Module\Director\Objects\IcingaUser;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class StateFilterSetTest extends BaseTestCase
+{
+ protected $testUserName1 = '__testuser2';
+
+ protected $testUserName2 = '__testuser2';
+
+ public function testIsEmptyForAnUnstoredUser()
+ {
+ $this->assertEquals(
+ array(),
+ StateFilterSet::forIcingaObject(
+ IcingaUser::create(),
+ 'states'
+ )->getResolvedValues()
+ );
+ }
+
+ /**
+ * @expectedException \Icinga\Exception\InvalidPropertyException
+ */
+ public function testFailsForInvalidProperties()
+ {
+ $set = new StateFilterSet('bla');
+ }
+
+ /**
+ * @expectedException \Icinga\Exception\ProgrammingError
+ */
+ public function testCannotBeStoredForAnUnstoredUser()
+ {
+ StateFilterSet::forIcingaObject(
+ $this->user1(),
+ 'states'
+ )->override(
+ array('OK', 'Down')
+ )->store();
+ }
+
+ public function testCanBeStored()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $states = $this->simpleUnstoredSetForStoredUser();
+
+ $this->assertTrue($states->store());
+ $states->getObject()->delete();
+ }
+
+ public function testWillNotBeStoredTwice()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $states = $this->simpleUnstoredSetForStoredUser();
+
+ $this->assertTrue($states->store());
+ $this->assertFalse($states->store());
+ $this->assertFalse($states->store());
+ $states->getObject()->delete();
+ }
+
+ public function testComplexDefinitionsCanBeStored()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $states = $this->complexUnstoredSetForStoredUser();
+
+ $this->assertTrue($states->store());
+ $states->getObject()->delete();
+ }
+
+ public function testComplexDefinitionsCanBeLoadedAndRenderCorrectly()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $states = $this->complexUnstoredSetForStoredUser();
+ $user = $states->getObject();
+
+ $this->assertTrue($states->store());
+
+ $states = StateFilterSet::forIcingaObject($user, 'states');
+ $expected = ' states = [ Down, OK, Up ]' . "\n"
+ . ' states += [ Warning ]' . "\n"
+ . ' states -= [ Up ]' . "\n";
+
+ $this->assertEquals(
+ $expected,
+ $states->renderAs('states')
+ );
+
+ $states->getObject()->delete();
+ }
+
+ protected function simpleUnstoredSetForStoredUser()
+ {
+ $user = $this->user1();
+ $user->store($this->getDb());
+
+ $states = StateFilterSet::forIcingaObject(
+ $user,
+ 'states'
+ )->override(
+ array('OK', 'Down')
+ );
+
+ return $states;
+ }
+
+ protected function complexUnstoredSetForStoredUser()
+ {
+ $user = $this->user2();
+ $user->store($this->getDb());
+
+ $states = StateFilterSet::forIcingaObject(
+ $user,
+ 'states'
+ )->override(
+ array('OK', 'Down', 'Up')
+ )->blacklist('Up')->extend('Warning');
+
+ return $states;
+ }
+
+ protected function user1()
+ {
+ return IcingaUser::create(array(
+ 'object_type' => 'object',
+ 'object_name' => $this->testUserName1
+ ));
+ }
+
+ protected function user2()
+ {
+ return IcingaUser::create(array(
+ 'object_type' => 'object',
+ 'object_name' => $this->testUserName2
+ ));
+ }
+
+ public function tearDown()
+ {
+ if ($this->hasDb()) {
+ $users = array(
+ $this->testUserName1,
+ $this->testUserName2
+ );
+
+ $db = $this->getDb();
+ foreach ($users as $user) {
+ if (IcingaUser::exists($user, $db)) {
+ IcingaUser::load($user, $db)->delete();
+ }
+ }
+ }
+ }
+}
diff --git a/test/php/library/Director/IcingaConfig/rendered/dict1.out b/test/php/library/Director/IcingaConfig/rendered/dict1.out
new file mode 100644
index 0000000..9f4e6bf
--- /dev/null
+++ b/test/php/library/Director/IcingaConfig/rendered/dict1.out
@@ -0,0 +1,6 @@
+{
+ "0" = numeric
+ @include = reserved
+ key1 = bla
+ "spe cial" = value
+}
diff --git a/test/php/library/Director/Import/HostSyncTest.php b/test/php/library/Director/Import/HostSyncTest.php
new file mode 100644
index 0000000..eeee7a4
--- /dev/null
+++ b/test/php/library/Director/Import/HostSyncTest.php
@@ -0,0 +1,250 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Module\Director\Objects\IcingaHostGroup;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Test\SyncTest;
+
+class HostSyncTest extends SyncTest
+{
+ protected $objectType = 'host';
+
+ protected $keyColumn = 'host';
+
+ public function testSimpleSync()
+ {
+ $this->runImport(array(
+ array(
+ 'host' => 'SYNCTEST_simple',
+ 'address' => '127.0.0.1',
+ 'os' => 'Linux'
+ )
+ ));
+
+ $this->setUpProperty(array(
+ 'source_expression' => '${host}',
+ 'destination_field' => 'object_name',
+ 'priority' => 10,
+ ));
+ $this->setUpProperty(array(
+ 'source_expression' => '${address}',
+ 'destination_field' => 'address',
+ 'priority' => 11,
+ ));
+ $this->setUpProperty(array(
+ 'source_expression' => '${os}',
+ 'destination_field' => 'vars.os',
+ 'priority' => 12,
+ ));
+
+ $this->assertTrue($this->sync->hasModifications(), 'Should have modifications pending');
+ $this->assertGreaterThan(0, $this->sync->apply(), 'Should successfully apply at least 1 update');
+ $this->assertFalse($this->sync->hasModifications(), 'Should not have modifications pending after sync');
+ }
+
+ public function testSyncWithoutData()
+ {
+ $this->runImport(array());
+
+ $this->setUpProperty(array(
+ 'source_expression' => '${host}',
+ 'destination_field' => 'object_name',
+ 'priority' => 10,
+ ));
+
+ $this->assertFalse($this->sync->hasModifications(), 'Should not have modifications pending');
+ }
+
+ public function testSyncMultipleGroups()
+ {
+ $this->requireGroups(['SYNCTEST_groupa', 'SYNCTEST_groupb']);
+ $this->runImport([[
+ 'host' => 'SYNCTEST_groups1',
+ 'g1' => 'SYNCTEST_groupa',
+ 'g2' => 'SYNCTEST_groupb'
+ ], [
+ 'host' => 'SYNCTEST_groups2',
+ 'g1' => null,
+ 'g2' => 'SYNCTEST_groupb'
+ ]]);
+
+ $this->setUpProperty(array(
+ 'source_expression' => '${host}',
+ 'destination_field' => 'object_name',
+ 'priority' => 10,
+ ));
+ $this->setUpProperty(array(
+ 'source_expression' => '${g1}',
+ 'destination_field' => 'groups',
+ 'priority' => 12,
+ ));
+ $this->setUpProperty(array(
+ 'source_expression' => '${g2}',
+ 'destination_field' => 'groups',
+ 'priority' => 11,
+ ));
+
+ $this->assertTrue($this->sync->hasModifications(), 'Should have modifications pending');
+
+ $modifications = array();
+ /** @var IcingaObject $mod */
+ foreach ($this->sync->getExpectedModifications() as $mod) {
+ $name = $mod->object_name;
+ $modifications[$name] = $mod;
+
+ switch ($name) {
+ case 'SYNCTEST_groups1':
+ $this->assertEquals([
+ 'SYNCTEST_groupa',
+ 'SYNCTEST_groupb',
+ ], $mod->get('groups'));
+ break;
+ case 'SYNCTEST_groups2':
+ $this->assertEquals([
+ 'SYNCTEST_groupb',
+ ], $mod->get('groups'));
+ break;
+ }
+ }
+
+ $this->assertGreaterThan(0, $this->sync->apply(), 'Should successfully apply at least 1 update');
+ $this->assertFalse($this->sync->hasModifications(), 'Should not have modifications pending after sync');
+ }
+
+ public function testSyncFilteredData()
+ {
+ $this->runImport(array(
+ array(
+ 'host' => 'SYNCTEST_filtered_in',
+ 'address' => '127.0.0.1',
+ 'os' => 'Linux',
+ 'sync' => 'yes'
+ ),
+ array(
+ 'host' => 'SYNCTEST_filtered_out',
+ 'address' => '127.0.0.1',
+ 'os' => null,
+ 'sync' => 'no'
+ ),
+ array(
+ 'host' => 'SYNCTEST_filtered_in_unusedfield',
+ 'address' => '127.0.0.1',
+ 'os' => null,
+ 'sync' => 'no',
+ 'othersync' => '1'
+ ),
+ array(
+ 'host' => 'SYNCTEST_filtered_in_unusedfield_propfilter',
+ 'address' => '127.0.0.1',
+ 'os' => null,
+ 'magic' => '2',
+ 'sync' => 'no',
+ 'othersync' => '1'
+ )
+ ));
+
+ $this->rule->set('filter_expression', 'sync=yes|othersync=1');
+ $this->rule->store();
+
+ $this->setUpProperty(array(
+ 'source_expression' => '${host}',
+ 'destination_field' => 'object_name',
+ 'priority' => 10,
+ ));
+ $this->setUpProperty(array(
+ 'source_expression' => '${address}',
+ 'destination_field' => 'address',
+ 'priority' => 11,
+ ));
+ $this->setUpProperty(array(
+ 'source_expression' => 'test',
+ 'destination_field' => 'vars.magic',
+ 'filter_expression' => 'magic!=',
+ 'priority' => 12,
+ ));
+
+ $modifications = array();
+ /** @var IcingaObject $mod */
+ foreach ($this->sync->getExpectedModifications() as $mod) {
+ $name = $mod->object_name;
+ $modifications[$name] = $mod;
+
+ $this->assertEquals(
+ '127.0.0.1',
+ $mod->get('address'),
+ $name . ': address should not be synced'
+ );
+ $this->assertNull($mod->get('vars.os'), $name . ': vars.os should not be synced');
+
+ if ($name === 'SYNCTEST_filtered_in_unusedfield_propfilter') {
+ $this->assertEquals(
+ 'test',
+ $mod->get('vars.magic'),
+ $name . ': vars.magic should not be synced'
+ );
+ } else {
+ $this->assertNull($mod->get('vars.magic'), $name . ': vars.magic should not be synced');
+ }
+ }
+
+ $this->assertArrayHasKey(
+ 'SYNCTEST_filtered_in',
+ $modifications,
+ 'SYNCTEST_filtered_in should be modified'
+ );
+ $this->assertArrayNotHasKey(
+ 'SYNCTEST_filtered_out',
+ $modifications,
+ 'SYNCTEST_filtered_out should be synced'
+ );
+ $this->assertArrayHasKey(
+ 'SYNCTEST_filtered_in_unusedfield',
+ $modifications,
+ 'SYNCTEST_filtered_in_unusedfield should be modified'
+ );
+ $this->assertArrayHasKey(
+ 'SYNCTEST_filtered_in_unusedfield_propfilter',
+ $modifications,
+ 'SYNCTEST_filtered_in_unusedfield_propfilter should be modified'
+ );
+ }
+
+ /**
+ * @param $names
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\IcingaException
+ */
+ protected function requireGroups($names)
+ {
+ foreach ($names as $name) {
+ if (! IcingaHostGroup::exists($name, $this->getDb())) {
+ IcingaHostGroup::create([
+ 'object_name' => $name,
+ 'object_type' => 'object',
+ ], $this->getDb())->store();
+ }
+ }
+ }
+
+ /**
+ * @param $names
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\IcingaException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function removeGroups($names)
+ {
+ foreach ($names as $name) {
+ if (IcingaHostGroup::exists($name, $this->getDb())) {
+ IcingaHostGroup::load($name, $this->getDb())->delete();
+ }
+ }
+ }
+
+ public function tearDown()
+ {
+ $this->removeGroups(['SYNCTEST_groupa', 'SYNCTEST_groupb']);
+ parent::tearDown();
+ }
+}
diff --git a/test/php/library/Director/Import/ImportSourceRestApiTest.php b/test/php/library/Director/Import/ImportSourceRestApiTest.php
new file mode 100644
index 0000000..7bbce2d
--- /dev/null
+++ b/test/php/library/Director/Import/ImportSourceRestApiTest.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Import;
+
+use Icinga\Module\Director\Import\ImportSourceRestApi;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class ImportSourceRestApiTest extends BaseTestCase
+{
+ public function testExtractProperty()
+ {
+ $examples = [
+ '' => json_decode('[{"name":"blau"}]'),
+ 'objects' => json_decode('{"objects":[{"name":"blau"}]}'),
+ 'results.objects.all' => json_decode('{"results":{"objects":{"all":[{"name":"blau"}]}}}'),
+ 'results\.objects.all' => json_decode('{"results.objects":{"all":[{"name":"blau"}]}}'),
+ ];
+
+ $source = new ImportSourceRestApi();
+
+ foreach ($examples as $property => $data) {
+ $source->setSettings(['extract_property' => $property]);
+ $result = static::callMethod($source, 'extractProperty', [$data]);
+
+ $this->assertCount(1, $result);
+ $this->assertArrayHasKey('name', (array) $result[0]);
+ }
+ }
+}
diff --git a/test/php/library/Director/Import/SyncUtilsTest.php b/test/php/library/Director/Import/SyncUtilsTest.php
new file mode 100644
index 0000000..ff40856
--- /dev/null
+++ b/test/php/library/Director/Import/SyncUtilsTest.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Module\Director\Import\SyncUtils;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class SyncUtilsTest extends BaseTestCase
+{
+ public function testVariableNamesAreExtracted()
+ {
+ $this->assertEquals(
+ array(
+ 'var.name',
+ '$Special Var'
+ ),
+ SyncUtils::extractVariableNames('This ${var.name} is ${$Special Var} are vars')
+ );
+
+ $this->assertEquals(
+ array(),
+ SyncUtils::extractVariableNames('No ${var.name vars ${$Special Var here')
+ );
+ }
+
+ public function testSpecificValuesCanBeRetrievedByName()
+ {
+ $row = (object)array(
+ 'host' => 'localhost',
+ 'ipaddress' => '127.0.0.1'
+ );
+
+ $this->assertEquals(
+ '127.0.0.1',
+ SyncUtils::getSpecificValue($row, 'ipaddress')
+ );
+ }
+
+ public function testMissingPropertiesMustBeNull()
+ {
+ $row = (object)array(
+ 'host' => 'localhost',
+ 'ipaddress' => '127.0.0.1'
+ );
+
+ $this->assertNull(
+ SyncUtils::getSpecificValue($row, 'address')
+ );
+ }
+
+ public function testNestedValuesCanBeRetrievedByPath()
+ {
+ $row = $this->getSampleRow();
+
+ $this->assertEquals(
+ '192.0.2.10',
+ SyncUtils::getSpecificValue($row, 'addresses.entries.eth0:1')
+ );
+
+ $this->assertEquals(
+ 2,
+ SyncUtils::getSpecificValue($row, 'addresses.count')
+ );
+ }
+
+ public function testRootVariablesCanBeExtracted()
+ {
+ $vars = array('test', 'nested.test', 'nested.dee.per');
+ $this->assertEquals(
+ array(
+ 'test' => 'test',
+ 'nested' => 'nested'
+ ),
+ SyncUtils::getRootVariables($vars)
+ );
+ }
+
+ public function testMultipleVariablesAreBeingReplacedCorrectly()
+ {
+ $string = '${addresses.entries.lo} and ${addresses.entries.eth0:1} are'
+ . ' ${This one?.$höüld be}${addressesmissing}';
+
+ $this->assertEquals(
+ '127.0.0.1 and 192.0.2.10 are fine',
+ SyncUtils::fillVariables(
+ $string,
+ $this->getSampleRow()
+ )
+ );
+ }
+
+ protected function getSampleRow()
+ {
+ return (object) array(
+ 'host' => 'localhost',
+ 'addresses' => (object) array(
+ 'count' => 2,
+ 'entries' => (object) array(
+ 'lo' => '127.0.0.1',
+ 'eth0:1' => '192.0.2.10',
+ )
+ ),
+ 'This one?' => (object) array(
+ '$höüld be' => 'fine'
+ )
+ );
+ }
+}
diff --git a/test/php/library/Director/Objects/HostApplyMatchesTest.php b/test/php/library/Director/Objects/HostApplyMatchesTest.php
new file mode 100644
index 0000000..b9f22ca
--- /dev/null
+++ b/test/php/library/Director/Objects/HostApplyMatchesTest.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Objects\HostApplyMatches;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class HostApplyMatchesTest extends BaseTestCase
+{
+ public function testExactMatches()
+ {
+ $matcher = HostApplyMatches::prepare($this->sampleHost());
+ $this->assertTrue(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('host.name=%22aha%22')
+ )
+ );
+ $this->assertFalse(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('host.name=%22ahaa%22')
+ )
+ );
+ }
+
+ public function testWildcardMatches()
+ {
+ $matcher = HostApplyMatches::prepare($this->sampleHost());
+ $this->assertTrue(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('host.name=%22ah*%22')
+ )
+ );
+ $this->assertTrue(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('host.name=%22*h*%22')
+ )
+ );
+ $this->assertFalse(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('host.name=%22*g*%22')
+ )
+ );
+ }
+
+ public function testStringVariableMatches()
+ {
+ $matcher = HostApplyMatches::prepare($this->sampleHost());
+ $this->assertTrue(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('host.vars.location=%22*urem*%22')
+ )
+ );
+ $this->assertTrue(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('host.vars.location=%22Nuremberg%22')
+ )
+ );
+ $this->assertFalse(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('host.vars.location=%22Nurembergg%22')
+ )
+ );
+ }
+
+ public function testArrayVariableMatches()
+ {
+ $matcher = HostApplyMatches::prepare($this->sampleHost());
+ $this->assertTrue(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('%22Amazing%22=host.vars.tags')
+ )
+ );
+ $this->assertFalse(
+ $matcher->matchesFilter(
+ Filter::fromQueryString('%22Amazingg%22=host.vars.tags')
+ )
+ );
+ }
+
+ protected function sampleHost()
+ {
+ return IcingaHost::create(array(
+ 'object_type' => 'object',
+ 'object_name' => 'aha',
+ 'vars' => array(
+ 'location' => 'Nuremberg',
+ 'tags' => array('Special', 'Amazing'),
+ )
+ ), $this->getDb());
+ }
+}
diff --git a/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php b/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php
new file mode 100644
index 0000000..cf2fb36
--- /dev/null
+++ b/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php
@@ -0,0 +1,353 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\Objects\HostGroupMembershipResolver;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class HostGroupMembershipResolverTest extends BaseTestCase
+{
+ const PREFIX = '__groupmembership';
+ const TYPE = 'host';
+
+ public function setUp()
+ {
+ IcingaTemplateRepository::clear();
+ }
+
+ public static function cleanArtifacts()
+ {
+ $db = static::getDb()->getDbAdapter();
+
+ $where = sprintf("object_name LIKE '%s%%'", self::PREFIX);
+
+ $db->delete('icinga_' . self::TYPE . 'group', $where);
+
+ $db->delete('icinga_' . self::TYPE, $where . " AND object_type = 'object'");
+ $db->delete('icinga_' . self::TYPE, $where);
+ }
+
+ public static function setUpBeforeClass()
+ {
+ static::cleanArtifacts();
+ }
+
+ public static function tearDownAfterClass()
+ {
+ static::cleanArtifacts();
+ }
+
+ /**
+ * @param string $type
+ * @param string $name
+ * @param array $props
+ *
+ * @return IcingaObject
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ protected function object($type, $name, $props = [])
+ {
+ $db = $this->getDb();
+ $fullName = self::PREFIX . $name;
+ $object = null;
+
+ try {
+ $object = IcingaObject::loadByType($type, $fullName, $db);
+
+ foreach ($props as $k => $v) {
+ $object->set($k, $v);
+ }
+
+ $object->store();
+ } catch (NotFoundError $e) {
+ $object = null;
+ }
+
+ if ($object === null) {
+ $object = IcingaObject::createByType($type, array_merge([
+ 'object_name' => $fullName,
+ 'object_type' => 'object',
+ ], $props), $this->getDb());
+
+ $object->store();
+ }
+
+ return $object;
+ }
+
+ protected function objects($type)
+ {
+ $dummy = DbObjectTypeRegistry::newObject($type);
+
+ $table = $dummy->getTableName();
+ $query = $this->getDb()->getDbAdapter()->select()
+ ->from($table)
+ ->where('object_name LIKE ?', self::PREFIX . '%');
+
+ $objects = [];
+ $l = strlen(self::PREFIX);
+
+ foreach ($dummy::loadAll($this->getDb(), $query) as $object) {
+ $key = substr($object->getObjectName(), $l);
+ $objects[$key] = $object;
+ }
+
+ return $objects;
+ }
+
+ protected function resolved()
+ {
+ $db = $this->getDb()->getDbAdapter();
+
+ $select = $db->select()
+ ->from(
+ ['r' => 'icinga_' . self::TYPE . 'group_' . self::TYPE . '_resolved'],
+ []
+ )->join(
+ ['o' => 'icinga_' . self::TYPE],
+ 'o.id = r.' . self::TYPE . '_id',
+ ['object' => 'object_name']
+ )->join(
+ ['g' => 'icinga_' . self::TYPE . 'group'],
+ 'g.id = r.' . self::TYPE . 'group_id',
+ ['groupname' => 'object_name']
+ );
+
+ $map = [];
+ $l = strlen(self::PREFIX);
+
+ foreach ($db->fetchAll($select) as $row) {
+ $o = $row->object;
+ $g = $row->groupname;
+
+ if (! substr($o, 0, $l) === self::PREFIX) {
+ continue;
+ }
+ $o = substr($o, $l);
+
+ if (! substr($g, 0, $l) === self::PREFIX) {
+ continue;
+ }
+ $g = substr($g, $l);
+
+ if (! array_key_exists($o, $map)) {
+ $map[$o] = [];
+ }
+
+ $map[$o][] = $g;
+ }
+
+ return $map;
+ }
+
+ /**
+ * Creates:
+ *
+ * - 1 template
+ * - 10 hosts importing the template with a var match_var=magic
+ *
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ public function testCreateHosts()
+ {
+ // template that sets a group later
+ $template = $this->object('host', 'template', [
+ 'object_type' => 'template',
+ ]);
+ $this->assertTrue($template->hasBeenLoadedFromDb());
+
+ // hosts to assign groups
+ for ($i = 1; $i <= 10; $i++) {
+ $host = $this->object('host', $i, [
+ 'imports' => self::PREFIX . 'template',
+ 'vars.match_var' => 'magic'
+ ]);
+ $this->assertTrue($host->hasBeenLoadedFromDb());
+ }
+ }
+
+ /**
+ * Creates:
+ *
+ * - 10 hostgroups applying on hosts with match_var=magic
+ * - 2 static hostgroups
+ *
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ public function testCreateHostgroups()
+ {
+ $filter = 'host.vars.match_var=%22magic%22';
+ for ($i = 1; $i <= 10; $i++) {
+ $hostgroup = $this->object('hostgroup', 'apply' . $i, [
+ 'assign_filter' => $filter
+ ]);
+ $this->assertTrue($hostgroup->hasBeenLoadedFromDb());
+ }
+
+ // static groups
+ for ($i = 1; $i <= 2; $i++) {
+ $hostgroup = $this->object('hostgroup', 'static' . $i);
+ $this->assertTrue($hostgroup->hasBeenLoadedFromDb());
+ }
+ }
+
+ /**
+ * Assigns static groups to:
+ *
+ * - the template
+ * - the first host
+ *
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\ConfigurationError
+ *
+ * @depends testCreateHosts
+ * @depends testCreateHostgroups
+ */
+ public function testAddStaticGroups()
+ {
+ // add group to template
+ $template = $this->object('host', 'template');
+ $template->setGroups(self::PREFIX . 'static1');
+ $template->store();
+ $this->assertFalse($template->hasBeenModified());
+
+ // add group to first host
+ $host = $this->object('host', 1);
+ $host->setGroups(self::PREFIX . 'static2');
+ $host->store();
+ $this->assertFalse($host->hasBeenModified());
+ }
+
+ /**
+ * Asserts that static groups are resolved for hosts:
+ *
+ * - all but first should have static1
+ * - first should have static2
+ *
+ * @depends testAddStaticGroups
+ */
+ public function testStaticResolvedMappings()
+ {
+ $resolved = $this->resolved();
+
+ $this->assertArrayHasKey(
+ 1,
+ $resolved,
+ 'Host 1 must have groups resolved'
+ );
+
+ $this->assertContains(
+ 'static2',
+ $resolved[1],
+ 'Host template must have static group 1'
+ );
+
+ $hosts = $this->objects('host');
+ $this->assertNotEmpty($hosts, 'Must have hosts found in DB');
+
+ foreach ($hosts as $name => $host) {
+ if ($host->object_type === 'template') {
+ continue;
+ }
+
+ $this->assertArrayHasKey(
+ $name,
+ $resolved,
+ 'All hosts must have groups resolved'
+ );
+
+ if ($name === 1) {
+ $this->assertNotContains(
+ 'static1',
+ $resolved[$name],
+ 'First host must not have static group 1'
+ );
+ } else {
+ $this->assertContains(
+ 'static1',
+ $resolved[$name],
+ 'All hosts but the first must have static group 1'
+ );
+ }
+ }
+ }
+
+ /**
+ * @depends testCreateHostgroups
+ */
+ public function testApplyResolvedMappings()
+ {
+ $resolved = $this->resolved();
+
+ $hosts = $this->objects('host');
+ $this->assertNotEmpty($hosts, 'Must have hosts found in DB');
+
+ foreach ($hosts as $name => $host) {
+ if ($host->object_type === 'template') {
+ continue;
+ }
+
+ $this->assertArrayHasKey($name, $resolved, 'Host must have groups resolved');
+
+ for ($i = 1; $i <= 10; $i++) {
+ $this->assertContains(
+ 'apply' . $i,
+ $resolved[$name],
+ 'All Host objects must have all applied groups'
+ );
+ }
+ }
+ }
+
+ /**
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\ConfigurationError
+ *
+ * @depends testAddStaticGroups
+ */
+ public function testChangeAppliedGroupsAfterStatic()
+ {
+ $filter = 'host.vars.match_var=%22magic*%22';
+
+ $hostgroup = $this->object('hostgroup', 'apply1', [
+ 'assign_filter' => $filter
+ ]);
+ $this->assertTrue($hostgroup->hasBeenLoadedFromDb());
+ $this->assertFalse($hostgroup->hasBeenModified());
+
+ $resolved = $this->resolved();
+
+ for ($i = 1; $i <= 10; $i++) {
+ $this->assertContains(
+ 'apply1',
+ $resolved[$i],
+ 'All Host objects must have apply1 group'
+ );
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Zend_Db_Adapter_Exception
+ *
+ * @depends testStaticResolvedMappings
+ * @depends testApplyResolvedMappings
+ * @depends testChangeAppliedGroupsAfterStatic
+ */
+ public function testFullRecheck()
+ {
+ $resolver = new HostGroupMembershipResolver($this->getDb());
+
+ $resolver->checkDb();
+ $this->assertEmpty($resolver->getNewMappings(), 'There should not be any new mappings');
+ $this->assertEmpty($resolver->getOutdatedMappings(), 'There should not be any outdated mappings');
+ }
+}
diff --git a/test/php/library/Director/Objects/IcingaCommandTest.php b/test/php/library/Director/Objects/IcingaCommandTest.php
new file mode 100644
index 0000000..8e564d8
--- /dev/null
+++ b/test/php/library/Director/Objects/IcingaCommandTest.php
@@ -0,0 +1,216 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class IcingaCommandTest extends BaseTestCase
+{
+ protected $testCommandName = '___TEST___command';
+
+ public function testCommandsWithArgumentsCanBeCreated()
+ {
+ $command = $this->command();
+ $command->arguments = array(
+ '-H' => '$host$'
+ );
+
+ $this->assertEquals(
+ '-H',
+ $command->arguments()->get('-H')->argument_name
+ );
+
+ $this->assertEquals(
+ '$host$',
+ $command->toPlainObject()->arguments['-H']
+ );
+
+ $command->arguments()->get('-H')->required = true;
+ }
+
+ public function testCommandsWithArgumentsCanBeModified()
+ {
+ $command = $this->command();
+ $command->arguments = array(
+ '-H' => '$host$'
+ );
+
+ $command->arguments = array(
+ '-x' => (object) array(
+ 'required' => true
+ )
+ );
+
+ $this->assertEquals(
+ null,
+ $command->arguments()->get('-H')
+ );
+
+ $this->assertEquals(
+ 'y',
+ $command->arguments()->get('-x')->get('required')
+ );
+
+ $this->assertEquals(
+ true,
+ $command->toPlainObject()->arguments['-x']->required
+ );
+ }
+
+ public function testCanBePersistedToDb()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $command = $this->newCommandWithArguments();
+
+ $this->assertEquals(
+ $command->store($db),
+ true
+ );
+
+
+ $command->delete();
+ }
+
+ public function testCanBeLoadedFromDb()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $name = $this->testCommandName;
+ $command = $this->newCommandWithArguments($db);
+ $command->store($db);
+
+ $command = IcingaCommand::load($name, $db);
+ $this->assertEquals(
+ $command->object_name,
+ $name
+ );
+
+ $command->delete();
+ }
+
+ public function testArgumentMotificationsAreDetected()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $command = $this->newCommandWithArguments($db);
+ $command->store($db);
+ $command->arguments()->set('-H', 'no-host');
+ $this->assertTrue($command->hasBeenModified());
+ $this->assertTrue($command->store());
+ $command->delete();
+ }
+
+ protected function newCommandWithArguments()
+ {
+ $command = $this->command();
+ $command->arguments = array(
+ '-H' => '$host$',
+ '-x' => (object) array(
+ 'required' => true,
+ 'value' => 'bal'
+ )
+ );
+
+ return $command;
+ }
+
+ public function testAbsolutePathsAreDetected()
+ {
+ $command = $this->command();
+ $command->command = 'C:\\test.exe';
+
+ $this->assertEquals(
+ $this->loadRendered('command1'),
+ (string) $command
+ );
+
+ $command->command = '/tmp/bla';
+
+ $this->assertEquals(
+ $this->loadRendered('command2'),
+ (string) $command
+ );
+
+ $command->command = 'tmp/bla';
+
+ $this->assertEquals(
+ $this->loadRendered('command3'),
+ (string) $command
+ );
+
+ $command->command = '\\\\network\\share\\bla.exe';
+
+ $this->assertEquals(
+ $this->loadRendered('command4'),
+ (string) $command
+ );
+
+ $command->command = 'BlahDir + \\network\\share\\bla.exe';
+
+ $this->assertEquals(
+ $this->loadRendered('command5'),
+ (string) $command
+ );
+
+ $command->command = 'network\\share\\bla.exe';
+
+ $this->assertEquals(
+ $this->loadRendered('command6'),
+ (string) $command
+ );
+ }
+
+ public function testSimpleSetIfIsRendered()
+ {
+ $command = $this->command();
+ $command->command = 'bla';
+ $command->arguments = array(
+ '-a' => (object) array(
+ 'set_if' => '$a$',
+ )
+ );
+
+ $this->assertEquals(
+ $this->loadRendered('command7'),
+ (string) $command
+ );
+ }
+
+ protected function command()
+ {
+ return IcingaCommand::create(
+ array(
+ 'object_name' => $this->testCommandName,
+ 'object_type' => 'object'
+ ),
+ $this->getDb()
+ );
+ }
+
+ protected function loadRendered($name)
+ {
+ return file_get_contents(__DIR__ . '/rendered/' . $name . '.out');
+ }
+
+ public function tearDown()
+ {
+ $db = $this->getDb();
+ if (IcingaCommand::exists($this->testCommandName, $db)) {
+ IcingaCommand::load($this->testCommandName, $db)->delete();
+ }
+ }
+}
diff --git a/test/php/library/Director/Objects/IcingaHostTest.php b/test/php/library/Director/Objects/IcingaHostTest.php
new file mode 100644
index 0000000..b4902bf
--- /dev/null
+++ b/test/php/library/Director/Objects/IcingaHostTest.php
@@ -0,0 +1,771 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Data\PropertiesFilter\ArrayCustomVariablesFilter;
+use Icinga\Module\Director\Data\PropertiesFilter\CustomVariablesFilter;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaHostGroup;
+use Icinga\Module\Director\Objects\IcingaZone;
+use Icinga\Module\Director\Test\BaseTestCase;
+use Icinga\Exception\IcingaException;
+
+class IcingaHostTest extends BaseTestCase
+{
+ protected $testHostName = '___TEST___host';
+ protected $testDatafieldName = 'test5';
+
+ public function testPropertiesCanBeSet()
+ {
+ $host = $this->host();
+ $host->display_name = 'Something else';
+ $this->assertEquals(
+ $host->display_name,
+ 'Something else'
+ );
+ }
+
+ public function testCanBeReplaced()
+ {
+ $host = $this->host();
+ $newHost = IcingaHost::create(
+ array('display_name' => 'Replaced display'),
+ $this->getDb()
+ );
+
+ $this->assertEquals(
+ count($host->vars()),
+ 4
+ );
+ $this->assertEquals(
+ $host->address,
+ '127.0.0.127'
+ );
+
+ $host->replaceWith($newHost);
+ $this->assertEquals(
+ $host->display_name,
+ 'Replaced display'
+ );
+ $this->assertEquals(
+ $host->address,
+ null
+ );
+
+ $this->assertEquals(
+ count($host->vars()),
+ 0
+ );
+ }
+
+ public function testCanBeMerged()
+ {
+ $host = $this->host();
+ $newHost = IcingaHost::create(
+ array('display_name' => 'Replaced display'),
+ $this->getDb()
+ );
+
+ $this->assertEquals(
+ count($host->vars()),
+ 4
+ );
+ $this->assertEquals(
+ $host->address,
+ '127.0.0.127'
+ );
+
+ $host->merge($newHost);
+ $this->assertEquals(
+ $host->display_name,
+ 'Replaced display'
+ );
+ $this->assertEquals(
+ $host->address,
+ '127.0.0.127'
+ );
+ $this->assertEquals(
+ count($host->vars()),
+ 4
+ );
+ }
+
+ public function testPropertiesCanBePreservedWhenBeingReplaced()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ $this->host()->store($db);
+ $host = IcingaHost::load($this->testHostName, $db);
+
+ $newHost = IcingaHost::create(
+ array(
+ 'display_name' => 'Replaced display',
+ 'address' => '1.2.2.3',
+ 'vars' => array(
+ 'test1' => 'newstring',
+ 'test2' => 18,
+ 'initially' => 'set and then preserved',
+ )
+ ),
+ $this->getDb()
+ );
+
+ $preserve = array('address', 'vars.test1', 'vars.initially');
+ $host->replaceWith($newHost, $preserve);
+ $this->assertEquals(
+ $host->address,
+ '127.0.0.127'
+ );
+
+ $this->assertEquals(
+ $host->{'vars.test2'},
+ 18
+ );
+
+ $this->assertEquals(
+ $host->vars()->test2->getValue(),
+ 18
+ );
+
+ $this->assertEquals(
+ $host->{'vars.initially'},
+ 'set and then preserved'
+ );
+
+ $this->assertFalse(
+ array_key_exists('address', $host->getModifiedProperties()),
+ 'Preserved property stays unmodified'
+ );
+
+ $newHost->set('vars.initially', 'changed later on');
+ $newHost->set('vars.test2', 19);
+
+ $host->replaceWith($newHost, $preserve);
+ $this->assertEquals(
+ $host->{'vars.initially'},
+ 'set and then preserved'
+ );
+
+ $this->assertEquals(
+ $host->get('vars.test2'),
+ 19
+ );
+
+
+ $host->delete();
+ }
+
+ public function testDistinctCustomVarsCanBeSetWithoutSideEffects()
+ {
+ $host = $this->host();
+ $host->set('vars.test2', 18);
+ $this->assertEquals(
+ $host->vars()->test1->getValue(),
+ 'string'
+ );
+ $this->assertEquals(
+ $host->vars()->test2->getValue(),
+ 18
+ );
+ $this->assertEquals(
+ $host->vars()->test3->getValue(),
+ false
+ );
+ }
+
+ public function testVarsArePersisted()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ $this->host()->store($db);
+ $host = IcingaHost::load($this->testHostName, $db);
+ $this->assertEquals(
+ $host->vars()->test1->getValue(),
+ 'string'
+ );
+ $this->assertEquals(
+ $host->vars()->test2->getValue(),
+ 17
+ );
+ $this->assertEquals(
+ $host->vars()->test3->getValue(),
+ false
+ );
+ $this->assertEquals(
+ $host->vars()->test4->getValue(),
+ (object) array(
+ 'this' => 'is',
+ 'a' => array(
+ 'dict',
+ 'ionary'
+ )
+ )
+ );
+ }
+
+ public function testRendersCorrectly()
+ {
+ $this->assertEquals(
+ (string) $this->host(),
+ $this->loadRendered('host1')
+ );
+ }
+
+ public function testGivesPlainObjectWithInvalidUnresolvedDependencies()
+ {
+ $props = $this->getDummyRelatedProperties();
+
+ $host = $this->host();
+ foreach ($props as $k => $v) {
+ $host->$k = $v;
+ }
+
+ $plain = $host->toPlainObject();
+ foreach ($props as $k => $v) {
+ $this->assertEquals($plain->$k, $v);
+ }
+ }
+
+ public function testCorrectlyStoresLazyRelations()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+ $db = $this->getDb();
+ $host = $this->host();
+ $host->zone = '___TEST___zone';
+ $this->assertEquals(
+ '___TEST___zone',
+ $host->zone
+ );
+
+ $zone = $this->newObject('zone', '___TEST___zone');
+ $zone->store($db);
+
+ $host->store($db);
+ $host->delete();
+ $zone->delete();
+ }
+
+ /**
+ * @expectedException \RuntimeException
+ */
+ public function testFailsToStoreWithMissingLazyRelations()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+ $db = $this->getDb();
+ $host = $this->host();
+ $host->zone = '___TEST___zone';
+ $host->store($db);
+ }
+
+ public function testHandlesUnmodifiedProperties()
+ {
+ $this->markTestSkipped('Currently broken, needs to be fixed');
+
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ $host = $this->host();
+ $host->store($db);
+
+ $parent = $this->newObject('host', '___TEST___parent');
+ $parent->store($db);
+ $host->imports = '___TEST___parent';
+
+ $host->store($db);
+
+ $plain = $host->getPlainUnmodifiedObject();
+ $this->assertEquals(
+ 'string',
+ $plain->vars->test1
+ );
+ $host->vars()->set('test1', 'nada');
+
+ $host->store();
+
+ $plain = $host->getPlainUnmodifiedObject();
+ $this->assertEquals(
+ 'nada',
+ $plain->vars->test1
+ );
+
+ $host->vars()->set('test1', 'string');
+ $plain = $host->getPlainUnmodifiedObject();
+ $this->assertEquals(
+ 'nada',
+ $plain->vars->test1
+ );
+
+ $plain = $host->getPlainUnmodifiedObject();
+ $test = IcingaHost::create((array) $plain);
+
+ $this->assertEquals(
+ $this->loadRendered('host3'),
+ (string) $test
+ );
+
+ $host->delete();
+ $parent->delete();
+ }
+
+ public function testRendersWithInvalidUnresolvedDependencies()
+ {
+ $newHost = $this->host();
+ $newHost->zone = 'invalid';
+ $newHost->check_command = 'unknown';
+ $newHost->event_command = 'What event?';
+ $newHost->check_period = 'Not time is a good time @ nite';
+ $newHost->command_endpoint = 'nirvana';
+
+ $this->assertEquals(
+ (string) $newHost,
+ $this->loadRendered('host2')
+ );
+ }
+
+ /**
+ * @expectedException \RuntimeException
+ */
+ public function testFailsToStoreWithInvalidUnresolvedDependencies()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $host = $this->host();
+ $host->zone = 'invalid';
+ $host->store($this->getDb());
+ }
+
+ public function testRendersToTheCorrectZone()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ $host = $this->host()->setConnection($db);
+ $masterzone = $db->getMasterZoneName();
+
+ $config = new IcingaConfig($db);
+ $host->renderToConfig($config);
+ $this->assertEquals(
+ array('zones.d/' . $masterzone . '/hosts.conf'),
+ $config->getFileNames()
+ );
+
+ $zone = $this->newObject('zone', '___TEST___zone');
+ $zone->store($db);
+
+ $config = new IcingaConfig($db);
+ $host->zone = '___TEST___zone';
+ $host->renderToConfig($config);
+ $this->assertEquals(
+ array('zones.d/___TEST___zone/hosts.conf'),
+ $config->getFileNames()
+ );
+
+ $host->has_agent = true;
+ $host->master_should_connect = true;
+ $host->accept_config = true;
+
+ $config = new IcingaConfig($db);
+ $host->renderToConfig($config);
+ $this->assertEquals(
+ array(
+ 'zones.d/___TEST___zone/hosts.conf',
+ 'zones.d/___TEST___zone/agent_endpoints.conf',
+ 'zones.d/___TEST___zone/agent_zones.conf'
+ ),
+ $config->getFileNames()
+ );
+
+ $host->object_type = 'template';
+ $host->zone_id = null;
+
+ $config = new IcingaConfig($db);
+ $host->renderToConfig($config);
+ $this->assertEquals(
+ array('zones.d/director-global/host_templates.conf'),
+ $config->getFileNames()
+ );
+ }
+
+ public function testWhetherTwoHostsCannotBeStoredWithTheSameApiKey()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ $a = IcingaHost::create(array(
+ 'object_name' => '___TEST___a',
+ 'object_type' => 'object',
+ 'api_key' => 'a'
+ ), $db);
+ $b = IcingaHost::create(array(
+ 'object_name' => '___TEST___b',
+ 'object_type' => 'object',
+ 'api_key' => 'a'
+ ), $db);
+
+ $a->store();
+ try {
+ $b->store();
+ } catch (\RuntimeException $e) {
+ $msg = $e->getMessage();
+ $matchMysql = strpos(
+ $msg,
+ "Duplicate entry 'a' for key 'api_key'"
+ ) !== false;
+
+ $matchPostgres = strpos(
+ $msg,
+ 'Unique violation'
+ ) !== false;
+
+ $this->assertTrue(
+ $matchMysql || $matchPostgres,
+ 'Exception message does not tell about unique constraint violation'
+ );
+ $a->delete();
+ }
+ }
+
+ public function testWhetherHostCanBeLoadedWithValidApiKey()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ $a = IcingaHost::create(array(
+ 'object_name' => '___TEST___a',
+ 'object_type' => 'object',
+ 'api_key' => 'a1a1a1'
+ ), $db);
+ $b = IcingaHost::create(array(
+ 'object_name' => '___TEST___b',
+ 'object_type' => 'object',
+ 'api_key' => 'b1b1b1'
+ ), $db);
+ $a->store();
+ $b->store();
+
+ $this->assertEquals(
+ IcingaHost::loadWithApiKey('b1b1b1', $db)->object_name,
+ '___TEST___b'
+ );
+
+ $a->delete();
+ $b->delete();
+ }
+
+ /**
+ * @expectedException \Icinga\Exception\NotFoundError
+ */
+ public function testWhetherInvalidApiKeyThrows404()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ IcingaHost::loadWithApiKey('No___such___key', $db);
+ }
+
+ public function testEnumProperties()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ $properties = IcingaHost::enumProperties($db);
+
+ $this->assertEquals(
+ array(
+ 'Host properties' => $this->getDefaultHostProperties()
+ ),
+ $properties
+ );
+ }
+
+ public function testEnumPropertiesWithCustomVars()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $host = $this->host();
+ $host->store($db);
+
+ $properties = IcingaHost::enumProperties($db);
+ $this->assertEquals(
+ array(
+ 'Host properties' => $this->getDefaultHostProperties(),
+ 'Custom variables' => array(
+ 'vars.test1' => 'test1',
+ 'vars.test2' => 'test2',
+ 'vars.test3' => 'test3',
+ 'vars.test4' => 'test4'
+ )
+ ),
+ $properties
+ );
+ }
+
+ public function testEnumPropertiesWithPrefix()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $host = $this->host();
+ $host->store($db);
+
+ $properties = IcingaHost::enumProperties($db, 'host.');
+ $this->assertEquals(
+ array(
+ 'Host properties' => $this->getDefaultHostProperties('host.'),
+ 'Custom variables' => array(
+ 'host.vars.test1' => 'test1',
+ 'host.vars.test2' => 'test2',
+ 'host.vars.test3' => 'test3',
+ 'host.vars.test4' => 'test4'
+ )
+ ),
+ $properties
+ );
+ }
+
+ public function testEnumPropertiesWithFilter()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ DirectorDatafield::create(array(
+ 'varname' => $this->testDatafieldName,
+ 'caption' => 'Blah',
+ 'description' => '',
+ 'datatype' => 'Icinga\Module\Director\DataType\DataTypeArray',
+ 'format' => 'json'
+ ))->store($db);
+
+ $host = $this->host();
+ $host->{'vars.test5'} = array('a', '1');
+ $host->store($db);
+
+ $properties = IcingaHost::enumProperties($db, '', new CustomVariablesFilter());
+ $this->assertEquals(
+ array(
+ 'Custom variables' => array(
+ 'vars.test1' => 'test1',
+ 'vars.test2' => 'test2',
+ 'vars.test3' => 'test3',
+ 'vars.test4' => 'test4',
+ 'vars.test5' => 'test5 (Blah)'
+ )
+ ),
+ $properties
+ );
+ }
+
+ public function testEnumPropertiesWithArrayFilter()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ DirectorDatafield::create(array(
+ 'varname' => $this->testDatafieldName,
+ 'caption' => 'Blah',
+ 'description' => '',
+ 'datatype' => 'Icinga\Module\Director\DataType\DataTypeArray',
+ 'format' => 'json'
+ ))->store($db);
+
+ $host = $this->host();
+ $host->{'vars.test5'} = array('a', '1');
+ $host->store($db);
+
+ $properties = IcingaHost::enumProperties($db, '', new ArrayCustomVariablesFilter());
+ $this->assertEquals(
+ array(
+ 'Custom variables' => array(
+ 'vars.test5' => 'test5 (Blah)'
+ )
+ ),
+ $properties
+ );
+ }
+
+ public function testMergingObjectKeepsGroupsIfNotGiven()
+ {
+ $one = IcingaHostGroup::create([
+ 'object_name' => 'one',
+ 'object_type' => 'object',
+ ]);
+ $two = IcingaHostGroup::create([
+ 'object_name' => 'two',
+ 'object_type' => 'object',
+ ]);
+ $a = IcingaHost::create([
+ 'object_name' => 'one',
+ 'object_type' => 'object',
+ 'imports' => [],
+ 'address' => '127.0.0.2',
+ 'groups' => [$one, $two]
+ ]);
+
+ $b = IcingaHost::create([
+ 'object_name' => 'one',
+ 'object_type' => 'object',
+ 'imports' => [],
+ 'address' => '127.0.0.42',
+ ]);
+
+ $a->merge($b);
+ $this->assertEquals(
+ '127.0.0.42',
+ $a->get('address')
+ );
+ $this->assertEquals(
+ ['one', 'two'],
+ $a->getGroups()
+ );
+ }
+
+ protected function getDummyRelatedProperties()
+ {
+ return array(
+ 'zone' => 'invalid',
+ 'check_command' => 'unknown',
+ 'event_command' => 'What event?',
+ 'check_period' => 'Not time is a good time @ nite',
+ 'command_endpoint' => 'nirvana',
+ );
+ }
+
+ protected function host()
+ {
+ return IcingaHost::create(array(
+ 'object_name' => $this->testHostName,
+ 'object_type' => 'object',
+ 'address' => '127.0.0.127',
+ 'display_name' => 'Whatever',
+ 'vars' => array(
+ 'test1' => 'string',
+ 'test2' => 17,
+ 'test3' => false,
+ 'test4' => (object) array(
+ 'this' => 'is',
+ 'a' => array(
+ 'dict',
+ 'ionary'
+ )
+ )
+ )
+ ), $this->getDb());
+ }
+
+ protected function getDefaultHostProperties($prefix = '')
+ {
+ return array(
+ "${prefix}name" => "name",
+ "${prefix}action_url" => "action_url",
+ "${prefix}address" => "address",
+ "${prefix}address6" => "address6",
+ "${prefix}api_key" => "api_key",
+ "${prefix}check_command" => "check_command",
+ "${prefix}check_interval" => "check_interval",
+ "${prefix}check_period" => "check_period",
+ "${prefix}check_timeout" => "check_timeout",
+ "${prefix}command_endpoint" => "command_endpoint",
+ "${prefix}display_name" => "display_name",
+ "${prefix}enable_active_checks" => "enable_active_checks",
+ "${prefix}enable_event_handler" => "enable_event_handler",
+ "${prefix}enable_flapping" => "enable_flapping",
+ "${prefix}enable_notifications" => "enable_notifications",
+ "${prefix}enable_passive_checks" => "enable_passive_checks",
+ "${prefix}enable_perfdata" => "enable_perfdata",
+ "${prefix}event_command" => "event_command",
+ "${prefix}flapping_threshold_high" => "flapping_threshold_high",
+ "${prefix}flapping_threshold_low" => "flapping_threshold_low",
+ "${prefix}icon_image" => "icon_image",
+ "${prefix}icon_image_alt" => "icon_image_alt",
+ "${prefix}max_check_attempts" => "max_check_attempts",
+ "${prefix}notes" => "notes",
+ "${prefix}notes_url" => "notes_url",
+ "${prefix}retry_interval" => "retry_interval",
+ "${prefix}volatile" => "volatile",
+ "${prefix}zone" => "zone",
+ "${prefix}groups" => "Groups",
+ "${prefix}templates" => "templates"
+ );
+ }
+ protected function loadRendered($name)
+ {
+ return file_get_contents(__DIR__ . '/rendered/' . $name . '.out');
+ }
+
+ public function tearDown()
+ {
+ if ($this->hasDb()) {
+ $db = $this->getDb();
+ $kill = array($this->testHostName, '___TEST___parent', '___TEST___a', '___TEST___b');
+ foreach ($kill as $name) {
+ if (IcingaHost::exists($name, $db)) {
+ IcingaHost::load($name, $db)->delete();
+ }
+ }
+
+ $kill = array('___TEST___zone');
+ foreach ($kill as $name) {
+ if (IcingaZone::exists($name, $db)) {
+ IcingaZone::load($name, $db)->delete();
+ }
+ }
+
+ $this->deleteDatafields();
+ }
+ }
+
+ protected function deleteDatafields()
+ {
+ $db = $this->getDb();
+ $dbAdapter = $db->getDbAdapter();
+ $kill = array($this->testDatafieldName);
+
+ foreach ($kill as $name) {
+ $query = $dbAdapter->select()
+ ->from('director_datafield')
+ ->where('varname = ?', $name);
+ foreach (DirectorDatafield::loadAll($db, $query, 'id') as $datafield) {
+ $datafield->delete();
+ }
+ }
+ }
+}
diff --git a/test/php/library/Director/Objects/IcingaNotificationTest.php b/test/php/library/Director/Objects/IcingaNotificationTest.php
new file mode 100644
index 0000000..9d9436a
--- /dev/null
+++ b/test/php/library/Director/Objects/IcingaNotificationTest.php
@@ -0,0 +1,248 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaNotification;
+use Icinga\Module\Director\Objects\IcingaUser;
+use Icinga\Module\Director\Objects\IcingaUsergroup;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class IcingaNotificationTest extends BaseTestCase
+{
+ protected $testUserName1 = '___TEST___user1';
+
+ protected $testUserName2 = '___TEST___user2';
+
+ protected $testNotificationName = '___TEST___notification';
+
+ public function testPropertiesCanBeSet()
+ {
+ $n = $this->notification();
+ $n->notification_interval = '10m';
+ $this->assertEquals(
+ $n->notification_interval,
+ 600
+ );
+ }
+
+ public function testCanBeStoredAndDeletedWithRelatedUserPassedAsString()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+ $db = $this->getDb();
+
+ $user = $this->user1();
+ $user->store($db);
+
+ $n = $this->notification();
+ $n->users = $user->object_name;
+ $this->assertTrue($n->store($db));
+ $this->assertTrue($n->delete());
+ $user->delete();
+ }
+
+ public function testCanBeStoredAndDeletedWithMultipleRelatedUsers()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+ $db = $this->getDb();
+
+ $user1 = $this->user1();
+ $user1->store($db);
+
+ $user2 = $this->user2();
+ $user2->store($db);
+
+ $n = $this->notification();
+ $n->users = array($user1->object_name, $user2->object_name);
+ $this->assertTrue($n->store($db));
+ $this->assertTrue($n->delete());
+ $user1->delete();
+ $user2->delete();
+ }
+
+ public function testGivesPlainObjectWithRelatedUsers()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+ $db = $this->getDb();
+
+ $user1 = $this->user1();
+ $user1->store($db);
+
+ $user2 = $this->user2();
+ $user2->store($db);
+
+ $n = $this->notification();
+ $n->users = array($user1->object_name, $user2->object_name);
+ $n->store($db);
+ $this->assertEquals(
+ (object) array(
+ 'object_name' => $this->testNotificationName,
+ 'object_type' => 'object',
+ 'users' => array(
+ $user1->object_name,
+ $user2->object_name
+ )
+ ),
+ $n->toPlainObject(false, true)
+ );
+
+ $n = IcingaNotification::load($n->object_name, $db);
+ $this->assertEquals(
+ (object) array(
+ 'object_name' => $this->testNotificationName,
+ 'object_type' => 'object',
+ 'users' => array(
+ $user1->object_name,
+ $user2->object_name
+ )
+ ),
+ $n->toPlainObject(false, true)
+ );
+ $this->assertEquals(
+ array(),
+ $n->toPlainObject()->user_groups
+ );
+ $n->delete();
+
+ $user1->delete();
+ $user2->delete();
+ }
+
+ public function testHandlesChangesForStoredRelations()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+ $db = $this->getDb();
+
+ $user1 = $this->user1();
+ $user1->store($db);
+
+ $user2 = $this->user2();
+ $user2->store($db);
+
+ $n = $this->notification();
+ $n->users = array($user1->object_name, $user2->object_name);
+ $n->store($db);
+
+ $n = IcingaNotification::load($n->object_name, $db);
+ $this->assertFalse($n->hasBeenModified());
+
+ $n->users = array($user2->object_name);
+ $this->assertTrue($n->hasBeenModified());
+
+ $n->store();
+
+ $n = IcingaNotification::load($n->object_name, $db);
+ $this->assertEquals(
+ array($user2->object_name),
+ $n->users
+ );
+
+ $n->users = array();
+ $n->store();
+
+ $n = IcingaNotification::load($n->object_name, $db);
+ $this->assertEquals(
+ array(),
+ $n->users
+ );
+
+ // Should be fixed with lazy loading:
+ // $n->users = array($user1->object_name, $user2->object_name);
+ // $this->assertFalse($n->hasBeenModified());
+
+ $n->delete();
+
+ $user1->delete();
+ $user2->delete();
+ }
+
+ public function testRendersConfigurationWithRelatedUsers()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+ $db = $this->getDb();
+
+ $user1 = $this->user1();
+ $user1->store($db);
+
+ $user2 = $this->user2();
+ $user2->store($db);
+
+ $n = $this->notification();
+ $n->users = array($user1->object_name, $user2->object_name);
+
+ $this->assertEquals(
+ $this->loadRendered('notification1'),
+ (string) $n
+ );
+ }
+
+ public function testLazyUsersCanBeSet()
+ {
+ $this->markTestSkipped('Setting lazy properties not yet completed');
+
+ $n = $this->notification();
+ $n->users = 'bla';
+ }
+
+ protected function user1()
+ {
+ return IcingaUser::create(array(
+ 'object_name' => $this->testUserName1,
+ 'object_type' => 'object',
+ 'email' => 'nowhere@example.com',
+ ), $this->getDb());
+ }
+
+ protected function user2()
+ {
+ return IcingaUser::create(array(
+ 'object_name' => $this->testUserName2,
+ 'object_type' => 'object',
+ 'email' => 'nowhere.else@example.com',
+ ), $this->getDb());
+ }
+
+ protected function notification()
+ {
+ return IcingaNotification::create(array(
+ 'object_name' => $this->testNotificationName,
+ 'object_type' => 'object',
+ ), $this->getDb());
+ }
+
+ protected function loadRendered($name)
+ {
+ return file_get_contents(__DIR__ . '/rendered/' . $name . '.out');
+ }
+
+ public function tearDown()
+ {
+ if ($this->hasDb()) {
+ $db = $this->getDb();
+ $kill = array($this->testNotificationName);
+ foreach ($kill as $name) {
+ if (IcingaNotification::exists($name, $db)) {
+ IcingaNotification::load($name, $db)->delete();
+ }
+ }
+
+ $kill = array($this->testUserName1, $this->testUserName2);
+ foreach ($kill as $name) {
+ if (IcingaUser::exists($name, $db)) {
+ IcingaUser::load($name, $db)->delete();
+ }
+ }
+ }
+ }
+}
diff --git a/test/php/library/Director/Objects/IcingaServiceSetTest.php b/test/php/library/Director/Objects/IcingaServiceSetTest.php
new file mode 100644
index 0000000..ad7c135
--- /dev/null
+++ b/test/php/library/Director/Objects/IcingaServiceSetTest.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Test\IcingaObjectTestCase;
+
+class IcingaServiceSetTest extends IcingaObjectTestCase
+{
+ protected $table = 'icinga_service_set';
+ protected $testObjectName = '___TEST___set';
+
+ public function setUp()
+ {
+ $this->assertNull($this->subject, 'subject must have been taken down before!');
+
+ if ($this->hasDb()) {
+ $this->subject = IcingaServiceSet::create(array(
+ 'object_name' => $this->testObjectName,
+ 'object_type' => 'template',
+ ));
+ $this->subject->store($this->getDb());
+ }
+ }
+
+ public function testUpdatingSet()
+ {
+ $set = IcingaServiceSet::load($this->testObjectName, $this->getDb());
+ $this->assertTrue($set->hasBeenLoadedFromDb());
+
+ $set->set('description', 'This is a set created by Phpunit!');
+ $this->assertTrue($set->hasBeenModified());
+ $set->store();
+
+ $set->set('assign_filter', 'host.name=foobar');
+ $this->assertTrue($set->hasBeenModified());
+ $set->store();
+
+ $this->assertFalse($set->hasBeenModified());
+ }
+
+ public function testAddingSetToHost()
+ {
+ $host = $this->createObject('for_set', 'icinga_host', array(
+ 'object_type' => 'object',
+ 'address' => '1.2.3.4',
+ ));
+
+ $set = IcingaServiceSet::create(array(
+ 'object_name' => $this->testObjectName,
+ 'object_type' => 'object',
+ ), $this->getDb()); // TODO: fails if db not set here...
+
+ $set->setImports($this->testObjectName);
+ $this->assertTrue($set->hasBeenModified());
+ $this->assertEquals(array($this->testObjectName), $set->getImports());
+
+ $set->set('host', $host->getObjectName());
+
+ $set->store();
+ $this->prepareObjectTearDown($set);
+ $this->assertFalse($set->hasBeenModified());
+ }
+
+ public function testDeletingHostWithSet()
+ {
+ $this->createObject('for_set', 'icinga_host', array(
+ 'object_type' => 'object',
+ 'address' => '1.2.3.4',
+ ), false)->store();
+
+ $host = $this->loadObject('for_set', 'icinga_host');
+ $host->delete();
+
+ $this->checkForDanglingHostSets();
+ }
+
+ public function testAddingServicesToSet()
+ {
+ $set = IcingaServiceSet::load($this->testObjectName, $this->getDb());
+
+ // TODO: setting service_set by name should work too...
+
+ $serviceA = $this->createObject('serviceA', 'icinga_service', array(
+ 'object_type' => 'apply',
+ 'service_set_id' => $set->getAutoincId(),
+ ));
+ $nameA = $serviceA->getObjectName();
+
+ $serviceB = $this->createObject('serviceB', 'icinga_service', array(
+ 'object_type' => 'apply',
+ 'service_set_id' => $set->getAutoincId(),
+ ));
+ $nameB = $serviceB->getObjectName();
+
+ $services = $set->getServiceObjects();
+
+ $this->assertCount(2, $services);
+ $this->assertArrayHasKey($nameA, $services);
+ $this->assertArrayHasKey($nameB, $services);
+ $this->assertEquals($serviceA->getAutoincId(), $services[$nameA]->getAutoincId());
+ $this->assertEquals($serviceB->getAutoincId(), $services[$nameB]->getAutoincId());
+
+ // TODO: deleting set should delete services
+
+ $this->checkForDanglingServices();
+ }
+
+ /**
+ * @expectedException \RuntimeException
+ */
+ public function testCreatingSetWithoutType()
+ {
+ $set = IcingaServiceSet::create(array(
+ 'object_name' => '___TEST__set_BAD',
+ ));
+ $set->store($this->getDb());
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testCreatingServiceSetWithoutHost()
+ {
+ $set = IcingaServiceSet::create(array(
+ 'object_name' => '___TEST__set_BAD2',
+ 'object_type' => 'object',
+ ));
+
+ $set->store($this->getDb());
+ }
+
+ public function testDeletingSet()
+ {
+ $set = IcingaServiceSet::load($this->testObjectName, $this->getDb());
+ $set->delete();
+
+ $this->assertFalse(IcingaServiceSet::exists($this->testObjectName, $this->getDb()));
+ $this->subject = null;
+ }
+
+ public function checkForDanglingServices()
+ {
+ $db = $this->getDb()->getDbAdapter();
+ $query = $db->select()
+ ->from(array('s' => 'icinga_service'), array('id'))
+ ->joinLeft(
+ array('ss' => 'icinga_service_set'),
+ 'ss.id = s.service_set_id',
+ array()
+ )
+ ->where('s.service_set_id IS NOT NULL')
+ ->where('ss.id IS NULL');
+
+ $ids = $db->fetchCol($query);
+
+ $this->assertEmpty($ids, sprintf('Found dangling service_set services in database: %s', join(', ', $ids)));
+ }
+
+ public function checkForDanglingHostSets()
+ {
+ $db = $this->getDb()->getDbAdapter();
+ $query = $db->select()
+ ->from(array('ss' => 'icinga_service_set'), array('id'))
+ ->joinLeft(
+ array('h' => 'icinga_host'),
+ 'h.id = ss.host_id',
+ array()
+ )
+ ->where('ss.host_id IS NOT NULL')
+ ->where('h.id IS NULL');
+
+ $ids = $db->fetchCol($query);
+
+ $this->assertEmpty(
+ $ids,
+ sprintf(
+ 'Found dangling service_set\'s for a host, without the host in database: %s',
+ join(', ', $ids)
+ )
+ );
+ }
+}
diff --git a/test/php/library/Director/Objects/IcingaServiceTest.php b/test/php/library/Director/Objects/IcingaServiceTest.php
new file mode 100644
index 0000000..468db67
--- /dev/null
+++ b/test/php/library/Director/Objects/IcingaServiceTest.php
@@ -0,0 +1,293 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class IcingaServiceTest extends BaseTestCase
+{
+ protected $testHostName = '___TEST___host';
+
+ protected $testServiceName = '___TEST___service';
+
+ protected $createdServices = array();
+
+ public function testUnstoredHostCanBeLazySet()
+ {
+ $service = $this->service();
+ $service->display_name = 'Something else';
+ $service->host = 'not yet';
+ $this->assertEquals(
+ 'not yet',
+ $service->host
+ );
+ }
+
+ /**
+ * @expectedException \RuntimeException
+ */
+ public function testFailsToStoreWithMissingLazyRelations()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ $service = $this->service();
+ $service->display_name = 'Something else';
+ $service->host = 'not yet';
+ $service->store($db);
+ $service->delete();
+ }
+
+ public function testAcceptsAssignRules()
+ {
+ $service = $this->service();
+ $service->object_type = 'apply';
+ $service->assign_filter = 'host.address="127.*"';
+ }
+
+ /**
+ * @expectedException \LogicException
+ */
+ public function testRefusesAssignRulesWhenNotBeingAnApply()
+ {
+ $service = $this->service();
+ $service->assign_filter = 'host.address=127.*';
+ }
+
+ public function testAcceptsAndRendersFlatAssignRules()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $service = $this->service();
+
+ // Service apply rule rendering requires access to settings:
+ $service->setConnection($db);
+ $service->object_type = 'apply';
+ $service->assign_filter = 'host.address="127.*"|host.vars.env="test"';
+
+ $this->assertEquals(
+ $this->loadRendered('service1'),
+ (string) $service
+ );
+
+ $this->assertEquals(
+ 'host.address="127.*"|host.vars.env="test"',
+ $service->assign_filter
+ );
+ }
+
+ public function testAcceptsAndRendersStructuredAssignRules()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $service = $this->service();
+ // Service apply rule rendering requires access to settings:
+ $service->setConnection($db);
+ $service->object_type = 'apply';
+ $service->assign_filter = 'host.address="127.*"|host.vars.env="test"';
+
+ $this->assertEquals(
+ $this->loadRendered('service1'),
+ (string) $service
+ );
+
+ $this->assertEquals(
+ 'host.address="127.*"|host.vars.env="test"',
+ $service->assign_filter = 'host.address="127.*"|host.vars.env="test"'
+ );
+ }
+
+ public function testPersistsAssignRules()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $service = $this->service();
+ $service->object_type = 'apply';
+ $service->assign_filter = 'host.address="127.*"|host.vars.env="test"';
+
+ $service->store($db);
+
+ $service = IcingaService::loadWithAutoIncId($service->id, $db);
+ $this->assertEquals(
+ $this->loadRendered('service1'),
+ (string) $service
+ );
+
+ $this->assertEquals(
+ 'host.address="127.*"|host.vars.env="test"',
+ $service->assign_filter
+ );
+
+ $service->delete();
+ }
+
+ public function testRendersToTheCorrectZone()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+ $service = $this->service()->setConnection($db);
+
+ $config = new IcingaConfig($db);
+ $service->renderToConfig($config);
+ $masterzone = $db->getMasterZoneName();
+ $this->assertEquals(
+ array('zones.d/' . $masterzone . '/services.conf'),
+ $config->getFileNames()
+ );
+ }
+
+ public function testVariablesInPropertiesAndCustomVariables()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $service = $this->service('___TEST___service_$not_replaced$');
+ $service->setConnection($db);
+ $service->object_type = 'apply';
+ $service->display_name = 'Service: $host.vars.replaced$';
+ $service->assign_filter = 'host.address="127.*"';
+ $service->{'vars.custom_var'} = '$host.vars.replaced$';
+
+ $this->assertEquals(
+ $this->loadRendered('service3'),
+ (string) $service
+ );
+ }
+
+ public function testVariablesAreNotReplacedForNonApplyObjects()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $host = $this->host();
+ $host->store($db);
+
+ $service = $this->service('___TEST___service_$not_replaced$');
+ $service->object_type = 'object';
+ $service->host_id = $host->get('id');
+ $service->display_name = 'Service: $host.vars.not_replaced$';
+ $service->{'vars.custom_var'} = '$host.vars.not_replaced$';
+ $service->store($db);
+
+ $service = IcingaService::loadWithAutoIncId($service->id, $db);
+ $this->assertEquals(
+ $this->loadRendered('service4'),
+ (string) $service
+ );
+ }
+
+ public function testApplyForRendersInVariousModes()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $service = $this->service()->setConnection($db);
+ $service->object_type = 'apply';
+ $service->apply_for = 'host.vars.test1';
+ $service->assign_filter = 'host.vars.env="test"';
+ $this->assertEquals(
+ $this->loadRendered('service5'),
+ (string) $service
+ );
+
+ $service->object_name = '___TEST$config$___service $host.var.bla$';
+ $this->assertEquals(
+ $this->loadRendered('service6'),
+ (string) $service
+ );
+
+ $service->object_name = '';
+ $this->assertEquals(
+ $this->loadRendered('service7'),
+ (string) $service
+ );
+ }
+
+ protected function host()
+ {
+ return IcingaHost::create(array(
+ 'object_name' => $this->testHostName,
+ 'object_type' => 'object',
+ 'address' => '127.0.0.1',
+ ));
+ }
+
+ protected function service($objectName = null)
+ {
+ if ($objectName === null) {
+ $objectName = $this->testServiceName;
+ }
+ $this->createdServices[] = $objectName;
+ return IcingaService::create(array(
+ 'object_name' => $objectName,
+ 'object_type' => 'object',
+ 'display_name' => 'Whatever service',
+ 'vars' => array(
+ 'test1' => 'string',
+ 'test2' => 17,
+ 'test3' => false,
+ 'test4' => (object) array(
+ 'this' => 'is',
+ 'a' => array(
+ 'dict',
+ 'ionary'
+ )
+ )
+ )
+ ));
+ }
+
+ protected function loadRendered($name)
+ {
+ return file_get_contents(__DIR__ . '/rendered/' . $name . '.out');
+ }
+
+ public function tearDown()
+ {
+ if ($this->hasDb()) {
+ $db = $this->getDb();
+ $kill = array($this->testHostName);
+ foreach ($kill as $name) {
+ if (IcingaHost::exists($name, $db)) {
+ IcingaHost::load($name, $db)->delete();
+ }
+ }
+
+ $kill = $this->createdServices;
+ foreach ($kill as $name) {
+ if (IcingaService::exists(array($name), $db)) {
+ IcingaService::load($name, $db)->delete();
+ }
+ }
+ }
+ }
+}
diff --git a/test/php/library/Director/Objects/IcingaTemplateResolverTest.php b/test/php/library/Director/Objects/IcingaTemplateResolverTest.php
new file mode 100644
index 0000000..09d0ead
--- /dev/null
+++ b/test/php/library/Director/Objects/IcingaTemplateResolverTest.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class IcingaTemplateResolverTest extends BaseTestCase
+{
+ /** @var IcingaHost[] */
+ private $scenario;
+
+ private $prefix = '__TEST_i1_';
+
+ public function testParentNamesCanBeRetrieved()
+ {
+ $this->assertEquals(
+ array(
+ $this->prefixed('t6'),
+ $this->prefixed('t5'),
+ $this->prefixed('t2')
+ ),
+ $this->getHost('t1')->templateResolver()->listParentNames()
+ );
+ }
+
+ public function testFullInhertancePathIdsAreCorrect()
+ {
+ $this->assertEquals(
+ $this->getIds(array('t5', 't6', 't5', 't5', 't4', 't3', 't5', 't6', 't2', 't1')),
+ $this->getHost('t1')->templateResolver()->listFullInheritancePathIds()
+ );
+ }
+
+ public function testInhertancePathIdsAreCorrect()
+ {
+ $this->assertEquals(
+ $this->getIds(array('t4', 't3', 't5', 't6', 't2', 't1')),
+ $this->getHost('t1')->templateResolver()->listInheritancePathIds()
+ );
+ }
+
+ protected function getHost($name)
+ {
+ $hosts = $this->getScenario();
+ return $hosts[$name];
+ }
+
+ protected function getId($name)
+ {
+ $hosts = $this->getScenario();
+ return $hosts[$name]->id;
+ }
+
+ protected function getIds($names)
+ {
+ $ids = array();
+ foreach ($names as $name) {
+ $ids[] = $this->getId($name);
+ }
+
+ return $ids;
+ }
+
+ protected function prefixed($name)
+ {
+ return $this->prefix . $name;
+ }
+
+ /**
+ * @return IcingaHost[]
+ */
+ protected function getScenario()
+ {
+ if ($this->scenario === null) {
+ $this->scenario = $this->createScenario();
+ }
+
+ return $this->scenario;
+ }
+
+ /**
+ * @return IcingaHost[]
+ */
+ protected function createScenario()
+ {
+ // Defionition imports (object -> imported)
+ // t1 -> t6, t5, t2
+ // t6 -> t5
+ // t3 -> t4
+ // t2 -> t3, t6
+ // t4 -> t5
+
+ // 5, 6, 5, 5, 4, 3, 5, 6, 2, 1
+
+ $t1 = $this->create('t1');
+ $t4 = $this->create('t4');
+ $t3 = $this->create('t3');
+ $t2 = $this->create('t2');
+ $t5 = $this->create('t5');
+ $t6 = $this->create('t6');
+
+ // TODO: Must work without this:
+ $t1->templateResolver()->clearCache();
+ $t1->set('imports', array($t6, $t5, $t2));
+ $t6->set('imports', array($t5));
+ $t3->set('imports', array($t4));
+ $t2->set('imports', array($t3, $t6));
+ $t4->set('imports', array($t5));
+
+ $t5->store();
+ $t4->store();
+ $t3->store();
+ $t6->store();
+ $t2->store();
+ $t1->store();
+
+ // TODO: Must work without this:
+ $t1->templateResolver()->clearCache();
+ return array(
+ 't1' => $t1,
+ 't2' => $t2,
+ 't3' => $t3,
+ 't4' => $t4,
+ 't5' => $t5,
+ 't6' => $t6,
+ );
+ }
+
+ /**
+ * @param $name
+ * @return IcingaHost
+ */
+ protected function create($name)
+ {
+ $host = IcingaHost::create(
+ array(
+ 'object_name' => $this->prefixed($name),
+ 'object_type' => 'template'
+ )
+ );
+
+ $host->store($this->getDb());
+ return $host;
+ }
+
+ public function tearDown()
+ {
+ $db = $this->getDb();
+ $kill = array('t1', 't2', 't6', 't3', 't4', 't5');
+ foreach ($kill as $short) {
+ $name = $this->prefixed($short);
+ if (IcingaHost::exists($name, $db)) {
+ IcingaHost::load($name, $db)->delete();
+ }
+ }
+ }
+}
diff --git a/test/php/library/Director/Objects/IcingaTimePeriodTest.php b/test/php/library/Director/Objects/IcingaTimePeriodTest.php
new file mode 100644
index 0000000..84496d3
--- /dev/null
+++ b/test/php/library/Director/Objects/IcingaTimePeriodTest.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Objects\IcingaTimePeriod;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class IcingaTimePeriodTest extends BaseTestCase
+{
+ protected $testPeriodName = '___TEST___timeperiod';
+
+ protected $createdNames = [];
+
+ public function testWhetherUpdatedTimeperiodsAreCorrectlyStored()
+ {
+ if ($this->skipForMissingDb()) {
+ return;
+ }
+
+ $period = $this->createTestPeriod();
+
+ $newRanges = array(
+ 'monday' => '00:00-24:00',
+ 'tuesday' => '18:00-24:00',
+ 'wednesday' => '00:00-24:00',
+ );
+ $period->ranges = $newRanges;
+ $this->assertEquals(
+ '18:00-24:00',
+ $period->toPlainObject()->ranges->tuesday
+ );
+
+ $period->store();
+
+ $period = $this->loadTestPeriod();
+ $this->assertEquals(
+ '18:00-24:00',
+ $period->ranges()->get('tuesday')->range_value
+ );
+
+ $this->assertEquals(
+ '00:00-24:00',
+ $period->toPlainObject()->ranges->wednesday
+ );
+
+ $period->ranges()->setRange('wednesday', '09:00-17:00');
+
+ $this->assertEquals(
+ '09:00-17:00',
+ $period->toPlainObject()->ranges->wednesday
+ );
+
+ $this->assertEquals(
+ '00:00-24:00',
+ $period->getPlainUnmodifiedObject()->ranges->wednesday
+ );
+ }
+
+ protected function createTestPeriod($suffix = '', $testRanges = [])
+ {
+ $db = $this->getDb();
+ $name = $this->testPeriodName . $suffix;
+
+ $this->createdNames[] = $name;
+ $object = IcingaTimePeriod::create(
+ array(
+ 'object_name' => $name,
+ 'object_type' => 'object'
+ ),
+ $db
+ );
+ $object->store();
+ $ranges = $object->ranges();
+
+ if (empty($testRanges)) {
+ $testRanges = array(
+ 'monday' => '00:00-24:00',
+ 'tuesday' => '00:00-24:00',
+ 'wednesday' => '00:00-24:00',
+ );
+ }
+
+ $ranges->set($testRanges);
+ $object->store();
+
+ return $object;
+ }
+
+ public function testIsActiveWorksForWeekdayRanges()
+ {
+ $period = $this->createTestPeriod();
+
+ $newRanges = array(
+ 'monday' => '00:00-24:00',
+ 'tuesday' => '18:00-24:00',
+ 'wednesday' => '00:00-24:00',
+ );
+ $period->ranges = $newRanges;
+
+ // Tuesday:
+ $this->assertFalse($period->isActive(strtotime('2016-05-17 10:00:00')));
+ // Wednesday:
+ $this->assertTrue($period->isActive(strtotime('2016-05-18 10:00:00')));
+ // Thursday:
+ $this->assertFalse($period->isActive(strtotime('2016-05-19 10:00:00')));
+ }
+
+ public function testPeriodWithIncludes()
+ {
+ $period = $this->createTestPeriod();
+ $include = $this->createTestPeriod('include', ['thursday' => '00:00-24:00']);
+
+ $period->set('includes', $include->object_name);
+ $period->store();
+
+ // Wednesday:
+ $this->assertTrue($period->isActive(strtotime('2016-05-18 10:00:00')));
+ // Thursday:
+ $this->assertTrue($period->isActive(strtotime('2016-05-19 10:00:00')));
+ }
+
+ public function testPeriodWithExcludes()
+ {
+ $period = $this->createTestPeriod();
+ $exclude = $this->createTestPeriod('exclude', ['wednesday' => '00:00-24:00']);
+
+ $period->set('excludes', $exclude->object_name);
+ $period->store();
+
+ // Wednesday:
+ $this->assertFalse($period->isActive(strtotime('2016-05-18 10:00:00')));
+ // Thursday:
+ $this->assertFalse($period->isActive(strtotime('2016-05-19 10:00:00')));
+ }
+
+ public function testPeriodPreferingIncludes()
+ {
+ $period = $this->createTestPeriod();
+ $include = $this->createTestPeriod('include', ['thursday' => '00:00-24:00']);
+ $exclude = $this->createTestPeriod('exclude', ['thursday' => '00:00-24:00']);
+
+ $period->set('includes', $include->object_name);
+ $period->set('excludes', $exclude->object_name);
+ $period->store();
+
+ // Wednesday:
+ $this->assertTrue($period->isActive(strtotime('2016-05-18 10:00:00')));
+ // Thursday:
+ $this->assertTrue($period->isActive(strtotime('2016-05-19 10:00:00')));
+ }
+
+ public function testPeriodPreferingExcludes()
+ {
+ $period = $this->createTestPeriod();
+ $include = $this->createTestPeriod('include', ['thursday' => '00:00-24:00']);
+ $exclude = $this->createTestPeriod('exclude', ['thursday' => '00:00-24:00']);
+
+ $period->set('prefer_includes', false);
+ $period->set('includes', $include->object_name);
+ $period->set('excludes', $exclude->object_name);
+ $period->store();
+
+ // Wednesday:
+ $this->assertTrue($period->isActive(strtotime('2016-05-18 10:00:00')));
+ // Thursday:
+ $this->assertFalse($period->isActive(strtotime('2016-05-19 10:00:00')));
+ }
+
+ protected function loadTestPeriod($suffix = '')
+ {
+ return IcingaTimePeriod::load($this->testPeriodName . $suffix, $this->getDb());
+ }
+
+ public function tearDown()
+ {
+ $db = $this->getDb();
+
+ foreach ($this->createdNames as $name) {
+ if (IcingaTimePeriod::exists($name, $db)) {
+ IcingaTimePeriod::load($name, $db)->delete();
+ }
+ }
+ }
+}
diff --git a/test/php/library/Director/Objects/rendered/command1.out b/test/php/library/Director/Objects/rendered/command1.out
new file mode 100644
index 0000000..12e156f
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/command1.out
@@ -0,0 +1,4 @@
+object CheckCommand "___TEST___command" {
+ command = [ "C:\\test.exe" ]
+}
+
diff --git a/test/php/library/Director/Objects/rendered/command2.out b/test/php/library/Director/Objects/rendered/command2.out
new file mode 100644
index 0000000..e853285
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/command2.out
@@ -0,0 +1,4 @@
+object CheckCommand "___TEST___command" {
+ command = [ "/tmp/bla" ]
+}
+
diff --git a/test/php/library/Director/Objects/rendered/command3.out b/test/php/library/Director/Objects/rendered/command3.out
new file mode 100644
index 0000000..7e7eef9
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/command3.out
@@ -0,0 +1,4 @@
+object CheckCommand "___TEST___command" {
+ command = [ PluginDir + "/tmp/bla" ]
+}
+
diff --git a/test/php/library/Director/Objects/rendered/command4.out b/test/php/library/Director/Objects/rendered/command4.out
new file mode 100644
index 0000000..3dc7ac5
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/command4.out
@@ -0,0 +1,4 @@
+object CheckCommand "___TEST___command" {
+ command = [ "\\\\network\\share\\bla.exe" ]
+}
+
diff --git a/test/php/library/Director/Objects/rendered/command5.out b/test/php/library/Director/Objects/rendered/command5.out
new file mode 100644
index 0000000..1e57577
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/command5.out
@@ -0,0 +1,4 @@
+object CheckCommand "___TEST___command" {
+ command = [ BlahDir + "\\network\\share\\bla.exe" ]
+}
+
diff --git a/test/php/library/Director/Objects/rendered/command6.out b/test/php/library/Director/Objects/rendered/command6.out
new file mode 100644
index 0000000..3f123ce
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/command6.out
@@ -0,0 +1,4 @@
+object CheckCommand "___TEST___command" {
+ command = [ PluginDir + "/network\\share\\bla.exe" ]
+}
+
diff --git a/test/php/library/Director/Objects/rendered/command7.out b/test/php/library/Director/Objects/rendered/command7.out
new file mode 100644
index 0000000..4f966f0
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/command7.out
@@ -0,0 +1,9 @@
+object CheckCommand "___TEST___command" {
+ command = [ PluginDir + "/bla" ]
+ arguments += {
+ "-a" = {
+ set_if = "$a$"
+ }
+ }
+}
+
diff --git a/test/php/library/Director/Objects/rendered/host1.out b/test/php/library/Director/Objects/rendered/host1.out
new file mode 100644
index 0000000..3637f2d
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/host1.out
@@ -0,0 +1,12 @@
+object Host "___TEST___host" {
+ display_name = "Whatever"
+ address = "127.0.0.127"
+ vars.test1 = "string"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+}
+
diff --git a/test/php/library/Director/Objects/rendered/host2.out b/test/php/library/Director/Objects/rendered/host2.out
new file mode 100644
index 0000000..74668e1
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/host2.out
@@ -0,0 +1,17 @@
+object Host "___TEST___host" {
+ display_name = "Whatever"
+ address = "127.0.0.127"
+ check_command = "unknown"
+ check_period = "Not time is a good time @ nite"
+ event_command = "What event?"
+ zone = "invalid"
+ command_endpoint = "nirvana"
+ vars.test1 = "string"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+}
+
diff --git a/test/php/library/Director/Objects/rendered/host3.out b/test/php/library/Director/Objects/rendered/host3.out
new file mode 100644
index 0000000..5661ca9
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/host3.out
@@ -0,0 +1,14 @@
+object Host "___TEST___host" {
+ import "___TEST___parent"
+
+ display_name = "Whatever"
+ address = "127.0.0.127"
+ vars.test1 = "nada"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+}
+
diff --git a/test/php/library/Director/Objects/rendered/notification1.out b/test/php/library/Director/Objects/rendered/notification1.out
new file mode 100644
index 0000000..13f06ce
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/notification1.out
@@ -0,0 +1,4 @@
+object Notification "___TEST___notification" {
+ users = [ "___TEST___user1", "___TEST___user2" ]
+}
+
diff --git a/test/php/library/Director/Objects/rendered/service1.out b/test/php/library/Director/Objects/rendered/service1.out
new file mode 100644
index 0000000..ba65b08
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/service1.out
@@ -0,0 +1,14 @@
+apply Service "___TEST___service" {
+ display_name = "Whatever service"
+ assign where match("127.*", host.address) || host.vars.env == "test"
+ vars.test1 = "string"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+
+ import DirectorOverrideTemplate
+}
+
diff --git a/test/php/library/Director/Objects/rendered/service2.out b/test/php/library/Director/Objects/rendered/service2.out
new file mode 100644
index 0000000..ea7d901
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/service2.out
@@ -0,0 +1,16 @@
+apply Service "___TEST___service" {
+ display_name = "Whatever service"
+ vars.test1 = "string"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+
+ assign where match("128.*", host.address)
+ ignore where host.name == "localhost"
+
+ import DirectorOverrideTemplate
+}
+
diff --git a/test/php/library/Director/Objects/rendered/service3.out b/test/php/library/Director/Objects/rendered/service3.out
new file mode 100644
index 0000000..0288ee6
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/service3.out
@@ -0,0 +1,15 @@
+apply Service "___TEST___service_$not_replaced$" {
+ display_name = "Service: " + host.vars.replaced
+ assign where match("127.*", host.address)
+ vars.custom_var = "$host.vars.replaced$"
+ vars.test1 = "string"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+
+ import DirectorOverrideTemplate
+}
+
diff --git a/test/php/library/Director/Objects/rendered/service4.out b/test/php/library/Director/Objects/rendered/service4.out
new file mode 100644
index 0000000..aeb280a
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/service4.out
@@ -0,0 +1,13 @@
+object Service "___TEST___service_$not_replaced$" {
+ host_name = "___TEST___host"
+ display_name = "Service: $host.vars.not_replaced$"
+ vars.custom_var = "$host.vars.not_replaced$"
+ vars.test1 = "string"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+}
+
diff --git a/test/php/library/Director/Objects/rendered/service5.out b/test/php/library/Director/Objects/rendered/service5.out
new file mode 100644
index 0000000..b05e630
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/service5.out
@@ -0,0 +1,14 @@
+apply Service "___TEST___service" for (config in host.vars.test1) {
+ display_name = "Whatever service"
+ assign where host.vars.env == "test"
+ vars.test1 = "string"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+
+ import DirectorOverrideTemplate
+}
+
diff --git a/test/php/library/Director/Objects/rendered/service6.out b/test/php/library/Director/Objects/rendered/service6.out
new file mode 100644
index 0000000..fdca11c
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/service6.out
@@ -0,0 +1,15 @@
+apply Service for (config in host.vars.test1) {
+ name = "___TEST" + config + "___service " + host.var.bla
+ display_name = "Whatever service"
+ assign where host.vars.env == "test"
+ vars.test1 = "string"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+
+ import DirectorOverrideTemplate
+}
+
diff --git a/test/php/library/Director/Objects/rendered/service7.out b/test/php/library/Director/Objects/rendered/service7.out
new file mode 100644
index 0000000..c125ccc
--- /dev/null
+++ b/test/php/library/Director/Objects/rendered/service7.out
@@ -0,0 +1,14 @@
+apply Service for (config in host.vars.test1) {
+ display_name = "Whatever service"
+ assign where host.vars.env == "test"
+ vars.test1 = "string"
+ vars.test2 = 17
+ vars.test3 = false
+ vars.test4 = {
+ a = [ "dict", "ionary" ]
+ @this = "is"
+ }
+
+ import DirectorOverrideTemplate
+}
+
diff --git a/test/php/library/Director/PropertyModifier/PropertyModifierArrayElementByPositionTest.php b/test/php/library/Director/PropertyModifier/PropertyModifierArrayElementByPositionTest.php
new file mode 100644
index 0000000..84465f3
--- /dev/null
+++ b/test/php/library/Director/PropertyModifier/PropertyModifierArrayElementByPositionTest.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\PropertyModifier\PropertyModifierArrayElementByPosition;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class PropertyModifierArrayElementByPositionTest extends BaseTestCase
+{
+ /*
+ * Allowed settings:
+ *
+ * position_type: first, last, fixed
+ * position
+ * when_missing: fail, null
+ */
+
+ public function testGivesFirstElementOfArray()
+ {
+ $this->assertEquals(
+ 'one',
+ $this->buildModifier('first')->transform(['one', 'two', 'three'])
+ );
+ }
+
+ public function testGivesFirstElementOfObject()
+ {
+ $this->assertEquals(
+ 'one',
+ $this->buildModifier('first')->transform((object) ['one', 'two', 'three'])
+ );
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testGettingFirstFailsForEmptyArray()
+ {
+ $this->buildModifier('first')->transform([]);
+ }
+
+ public function testGettingFirstGivesNullForEmptyArray()
+ {
+ $this->assertNull($this->buildModifier('first', null, 'null')->transform([]));
+ }
+
+ public function testGivesLastElementOfArray()
+ {
+ $this->assertEquals(
+ 'three',
+ $this->buildModifier('last')->transform(['one', 'two', 'three'])
+ );
+ }
+
+ public function testGivesLastElementOfObject()
+ {
+ $this->assertEquals(
+ 'three',
+ $this->buildModifier('last')->transform((object) ['one', 'two', 'three'])
+ );
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testGettingLastFailsForEmptyArray()
+ {
+ $this->buildModifier('last')->transform([]);
+ }
+
+ public function testGettingLastGivesNullForEmptyArray()
+ {
+ $this->assertNull($this->buildModifier('last', null, 'null')->transform([]));
+ }
+
+ public function testGivesSpecificElementOfArray()
+ {
+ $this->assertEquals(
+ 'two',
+ $this->buildModifier('fixed', '1')->transform(['one', 'two', 'three'])
+ );
+ }
+
+ public function testGivesSpecificElementOfObject()
+ {
+ $this->assertEquals(
+ 'two',
+ $this->buildModifier('fixed', 1)->transform((object) ['one', 'two', 'three'])
+ );
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testGettingSpecificFailsForEmptyArray()
+ {
+ $this->buildModifier('fixed', 1)->transform([]);
+ }
+
+ public function testGettingSpecificGivesNullForEmptyArray()
+ {
+ $this->assertNull($this->buildModifier('fixed', 1, 'null')->transform([]));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testGettingSpecificFailsForMissingValue()
+ {
+ $this->buildModifier('fixed', 3)->transform(['one', 'two', 'three']);
+ }
+
+ public function testGettingSpecificGivesNullForMissingValue()
+ {
+ $this->assertNull($this->buildModifier('fixed', 3, 'null')->transform(['one', 'two', 'three']));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testFailsForStrings()
+ {
+ $this->buildModifier('first')->transform('string');
+ }
+
+ public function testAnnouncesArraySupport()
+ {
+ $modifier = new PropertyModifierArrayElementByPosition();
+ $this->assertTrue($modifier->hasArraySupport());
+ }
+
+ protected function buildModifier($type, $position = null, $whenMissing = 'fail')
+ {
+ $modifier = new PropertyModifierArrayElementByPosition();
+ $modifier->setSettings([
+ 'position_type' => $type,
+ 'position' => $position,
+ 'when_missing' => $whenMissing,
+ ]);
+
+ return $modifier;
+ }
+}
diff --git a/test/php/library/Director/PropertyModifier/PropertyModifierArrayFilterTest.php b/test/php/library/Director/PropertyModifier/PropertyModifierArrayFilterTest.php
new file mode 100644
index 0000000..e50a45d
--- /dev/null
+++ b/test/php/library/Director/PropertyModifier/PropertyModifierArrayFilterTest.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\PropertyModifier\PropertyModifierArrayFilter;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class PropertyModifierArrayFilterTest extends BaseTestCase
+{
+ /**
+ * Allowed settings:
+ *
+ * filter_method: wildcard, regex
+ * filter_string
+ *
+ * policy: keep, reject
+ * when_empty: empty_array, null
+ */
+
+ /** @var array */
+ private $testArray = array(
+ 'www.example.com',
+ 'example.com',
+ 'www',
+ 'wwexample.com',
+ 'example.www',
+ '',
+ );
+
+ public function testKeepMatchingWildcards()
+ {
+ $modifier = new PropertyModifierArrayFilter();
+ $modifier->setSettings(array(
+ 'filter_method' => 'wildcard',
+ 'filter_string' => 'www*',
+ 'policy' => 'keep',
+ 'when_empty' => 'empty_array',
+ ));
+
+ $this->assertEquals(
+ array('www.example.com', 'www'),
+ $modifier->transform($this->testArray)
+ );
+ }
+
+ public function testRejectMatchingWildcards()
+ {
+ $modifier = new PropertyModifierArrayFilter();
+ $modifier->setSettings(array(
+ 'filter_method' => 'wildcard',
+ 'filter_string' => 'www*',
+ 'policy' => 'reject',
+ 'when_empty' => 'empty_array',
+ ));
+
+ $this->assertEquals(
+ array('example.com', 'wwexample.com', 'example.www', ''),
+ $modifier->transform($this->testArray)
+ );
+ }
+
+ public function testKeepMatchingRegularExpression()
+ {
+ $modifier = new PropertyModifierArrayFilter();
+ $modifier->setSettings(array(
+ 'filter_method' => 'regex',
+ 'filter_string' => '/^w{3}.*/',
+ 'policy' => 'keep',
+ 'when_empty' => 'empty_array',
+ ));
+
+ $this->assertEquals(
+ array('www.example.com', 'www'),
+ $modifier->transform($this->testArray)
+ );
+ }
+
+ public function testRejectMatchingRegularExpression()
+ {
+ $modifier = new PropertyModifierArrayFilter();
+ $modifier->setSettings(array(
+ 'filter_method' => 'regex',
+ 'filter_string' => '/^w{3}.*/',
+ 'policy' => 'reject',
+ 'when_empty' => 'empty_array',
+ ));
+
+ $this->assertEquals(
+ array('example.com', 'wwexample.com', 'example.www', ''),
+ $modifier->transform($this->testArray)
+ );
+ }
+
+ public function testGivesEmptyArrayOrNullAccordingToConfig()
+ {
+ $modifier = new PropertyModifierArrayFilter();
+ $modifier->setSettings(array(
+ 'filter_method' => 'wildcard',
+ 'filter_string' => 'no-match',
+ 'policy' => 'keep',
+ 'when_empty' => 'empty_array',
+ ));
+
+ $this->assertEquals(
+ array(),
+ $modifier->transform($this->testArray)
+ );
+
+ $modifier->setSetting('when_empty', 'null');
+ $this->assertNull(
+ $modifier->transform($this->testArray)
+ );
+ }
+
+ public function testAnnouncesArraySupport()
+ {
+ $modifier = new PropertyModifierArrayFilter();
+ $this->assertTrue($modifier->hasArraySupport());
+ }
+}
diff --git a/test/php/library/Director/PropertyModifier/PropertyModifierCombineTest.php b/test/php/library/Director/PropertyModifier/PropertyModifierCombineTest.php
new file mode 100644
index 0000000..4c42dba
--- /dev/null
+++ b/test/php/library/Director/PropertyModifier/PropertyModifierCombineTest.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\PropertyModifier\PropertyModifierCombine;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class PropertyModifierCombineTest extends BaseTestCase
+{
+ public function testBuildsTypicalHostServiceCombination()
+ {
+ $row = (object) array('host' => 'localhost', 'service' => 'ping');
+ $modifier = new PropertyModifierCombine();
+ $modifier->setSettings(array('pattern' => '${host}!${service}'));
+
+ $this->assertEquals(
+ 'localhost!ping',
+ $modifier->setRow($row)->transform('something')
+ );
+ }
+
+ public function testDoesNotFailForMissingProperties()
+ {
+ $row = (object) array('host' => 'localhost');
+ $modifier = new PropertyModifierCombine();
+ $modifier->setSettings(array('pattern' => '${host}!${service}'));
+
+ $this->assertEquals(
+ 'localhost!',
+ $modifier->setRow($row)->transform('something')
+ );
+ }
+
+ public function testDoesNotEvaluateVariablesFromDataSource()
+ {
+ $row = (object) array('host' => '${service}', 'service' => 'ping');
+ $modifier = new PropertyModifierCombine();
+ $modifier->setSettings(array('pattern' => '${host}!${service}'));
+
+ $this->assertEquals(
+ '${service}!ping',
+ $modifier->setRow($row)->transform('something')
+ );
+ }
+
+ public function testRequiresRow()
+ {
+ $modifier = new PropertyModifierCombine();
+ $this->assertTrue($modifier->requiresRow());
+ }
+}
diff --git a/test/php/library/Director/PropertyModifier/PropertyModifierListToObjectTest.php b/test/php/library/Director/PropertyModifier/PropertyModifierListToObjectTest.php
new file mode 100644
index 0000000..93d498c
--- /dev/null
+++ b/test/php/library/Director/PropertyModifier/PropertyModifierListToObjectTest.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\PropertyModifier\PropertyModifierListToObject;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class PropertyModifierListToObjectTest extends BaseTestCase
+{
+ public function testConvertsAListOfArrays()
+ {
+ $this->assertEquals(
+ $this->getOutput(),
+ $this->getNewModifier()->transform($this->getInputArrays())
+ );
+ }
+
+ public function testConvertsAListOfObjects()
+ {
+ $this->assertEquals(
+ $this->getOutput(),
+ $this->getNewModifier()->transform($this->getInputObjects())
+ );
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testFailsOnMissingKey()
+ {
+ $input = $this->getInputArrays();
+ unset($input[0]['name']);
+ $this->getNewModifier()->transform($input);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testFailsWithDuplicateRows()
+ {
+ $input = $this->getInputArrays();
+ $input[1]['name'] = 'row1';
+ $this->getNewModifier()->transform($input);
+ }
+
+ public function testKeepsFirstRowOnDuplicate()
+ {
+ $input = $this->getInputArrays();
+ $input[1]['name'] = 'row1';
+ $modifier = $this->getNewModifier()->setSetting('on_duplicate', 'keep_first');
+ $result = $modifier->transform($input);
+ $this->assertEquals(
+ (object) ['some' => 'property'],
+ $result->row1->props
+ );
+ }
+
+ public function testKeepsLastRowOnDuplicate()
+ {
+ $input = $this->getInputArrays();
+ $input[1]['name'] = 'row1';
+ $modifier = $this->getNewModifier()->setSetting('on_duplicate', 'keep_last');
+ $result = $modifier->transform($input);
+ $this->assertEquals(
+ (object) ['other' => 'property'],
+ $result->row1->props
+ );
+ }
+
+ protected function getNewModifier()
+ {
+ $modifier = new PropertyModifierListToObject();
+ $modifier->setSettings([
+ 'key_property' => 'name',
+ 'on_duplicate' => 'fail'
+ ]);
+
+ return $modifier;
+ }
+
+ protected function getInputArrays()
+ {
+ return [
+ ['name' => 'row1', 'props' => (object) ['some' => 'property']],
+ ['name' => 'row2', 'props' => (object) ['other' => 'property']],
+ ];
+ }
+
+ protected function getInputObjects()
+ {
+ return [
+ (object) ['name' => 'row1', 'props' => (object) ['some' => 'property']],
+ (object) ['name' => 'row2', 'props' => (object) ['other' => 'property']],
+ ];
+ }
+
+ protected function getOutput()
+ {
+ return (object) [
+ 'row1' => (object) ['name' => 'row1', 'props' => (object) ['some' => 'property']],
+ 'row2' => (object) ['name' => 'row2', 'props' => (object) ['other' => 'property']],
+ ];
+ }
+}
diff --git a/test/php/library/Director/PropertyModifier/PropertyModifierParseURLTest.php b/test/php/library/Director/PropertyModifier/PropertyModifierParseURLTest.php
new file mode 100644
index 0000000..a5ccb79
--- /dev/null
+++ b/test/php/library/Director/PropertyModifier/PropertyModifierParseURLTest.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\PropertyModifier;
+
+use Icinga\Module\Director\PropertyModifier\PropertyModifierParseURL;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class PropertyModifierParseURLTest extends BaseTestCase
+{
+ protected static $validurl = 'https://www.icinga.org/path/file.html?foo=bar#section';
+ protected static $invalidurl = 'http:///www.icinga.org/';
+
+
+ public function testModifierDoesNotSupportArraysItself()
+ {
+ $modifier = new PropertyModifierParseURL();
+ $this->assertFalse($modifier->hasArraySupport());
+ }
+
+ public function testEmptyPropertyReturnsNullOnfailureNull()
+ {
+ $modifier = new PropertyModifierParseURL();
+ $modifier->setSettings([
+ 'url_component' => 'query',
+ 'on_failure' => 'null',
+ ]);
+
+ $this->assertNull($modifier->transform(''));
+ }
+
+ public function testMissingComponentReturnsNullOnfailureNull()
+ {
+ $modifier = new PropertyModifierParseURL();
+ $modifier->setSettings([
+ 'url_component' => 'query',
+ 'on_failure' => 'null',
+ ]);
+
+ $this->assertNull($modifier->transform('https://www.icinga.org/path/'));
+ }
+
+ public function testMissingComponentReturnsPropertyOnfailureKeep()
+ {
+ $modifier = new PropertyModifierParseURL();
+ $modifier->setSettings([
+ 'url_component' => 'query',
+ 'on_failure' => 'keep',
+ ]);
+
+ $this->assertEquals('http://www.icinga.org/path/', $modifier->transform('http://www.icinga.org/path/'));
+ }
+
+ /**
+ * @expectedException \Icinga\Exception\InvalidPropertyException
+ */
+ public function testMissingComponentThrowsExceptionOnfailureFail()
+ {
+ $modifier = new PropertyModifierParseURL();
+ $modifier->setSettings([
+ 'url_component' => 'query',
+ 'on_failure' => 'fail',
+ ]);
+
+ $modifier->transform('http://www.icinga.org/path/');
+ }
+
+
+ public function testInvalidUrlReturnsNullOnfailureNull()
+ {
+ $modifier = new PropertyModifierParseURL();
+ $modifier->setSettings([
+ 'url_component' => 'host',
+ 'on_failure' => 'null',
+ ]);
+
+ $this->assertNull($modifier->transform(self::$invalidurl));
+ }
+
+ public function testInvalidUrlReturnsItselfOnfailureKeep()
+ {
+ $modifier = new PropertyModifierParseURL();
+ $modifier->setSettings([
+ 'url_component' => 'host',
+ 'on_failure' => 'keep',
+ ]);
+
+ $this->assertEquals(self::$invalidurl, $modifier->transform(self::$invalidurl));
+ }
+
+ /**
+ * @expectedException \Icinga\Exception\InvalidPropertyException
+ */
+ public function testInvalidUrlThrowsExceptionOnfailureFail()
+ {
+ $modifier = new PropertyModifierParseURL();
+ $modifier->setSettings([
+ 'url_component' => 'host',
+ 'on_failure' => 'fail',
+ ]);
+
+ $modifier->transform(self::$invalidurl);
+ }
+
+
+ /**
+ * @dataProvider dataURLcomponentProvider
+ */
+ public function testSuccessfullyParse($component, $result)
+ {
+ $modifier = new PropertyModifierParseURL();
+ $modifier->setSettings([
+ 'url_component' => $component,
+ 'on_failure' => 'null',
+ ]);
+
+ $this->assertEquals($result, $modifier->transform(self::$validurl));
+ }
+ public function dataURLcomponentProvider()
+ {
+ return [
+ 'scheme' => [
+ 'scheme',
+ 'https',
+ ],
+ 'host' => [
+ 'host',
+ 'www.icinga.org',
+ ],
+ 'port' => [
+ 'port',
+ '',
+ ],
+ 'path' => [
+ 'path',
+ '/path/file.html',
+ ],
+ 'query' => [
+ 'query',
+ 'foo=bar',
+ ],
+ 'fragment' => [
+ 'fragment',
+ 'section',
+ ],
+ ];
+ }
+}
diff --git a/test/php/library/Director/Resolver/TemplateTreeTest.php b/test/php/library/Director/Resolver/TemplateTreeTest.php
new file mode 100644
index 0000000..f44d081
--- /dev/null
+++ b/test/php/library/Director/Resolver/TemplateTreeTest.php
@@ -0,0 +1,259 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Objects;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Resolver\TemplateTree;
+use Icinga\Module\Director\Test\BaseTestCase;
+
+class TemplateTreeTest extends BaseTestCase
+{
+ protected $applyId;
+
+ protected function prepareHosts(Db $db)
+ {
+ $o1 = IcingaHost::create([
+ 'object_name' => 'o1',
+ 'object_type' => 'template'
+ ], $db);
+ $o2 = IcingaHost::create([
+ 'object_name' => 'o2',
+ 'object_type' => 'template'
+ ], $db);
+ $o3 = IcingaHost::create([
+ 'object_name' => 'o3',
+ 'object_type' => 'template'
+ ], $db);
+ $o4 = IcingaHost::create([
+ 'object_name' => 'o4',
+ 'object_type' => 'template',
+ 'imports' => ['o2', 'o1'],
+ ], $db);
+ $o5 = IcingaHost::create([
+ 'object_name' => 'o5',
+ 'object_type' => 'template',
+ 'imports' => ['o4'],
+ ], $db);
+ $o6 = IcingaHost::create([
+ 'object_name' => 'o6',
+ 'object_type' => 'template',
+ 'imports' => ['o4', 'o2'],
+ ], $db);
+ $o7 = IcingaHost::create([
+ 'object_name' => 'o7',
+ 'object_type' => 'object',
+ 'imports' => ['o4', 'o2'],
+ ], $db);
+
+ $o1->store();
+ $o2->store();
+ $o3->store();
+ $o4->store();
+ $o5->store();
+ $o6->store();
+ $o7->store();
+
+ return (object) [
+ 'o1' => $o1,
+ 'o2' => $o2,
+ 'o3' => $o3,
+ 'o4' => $o4,
+ 'o5' => $o5,
+ 'o6' => $o6,
+ 'o7' => $o7,
+ ];
+ }
+
+ public function testHostWithoutParentGivesAnEmptyArray()
+ {
+ $db = $this->getDb();
+ $hosts = $this->prepareHosts($db);
+ $tree = new TemplateTree('host', $db);
+ $this->assertEquals([], $tree->getParentsFor($hosts->o2));
+ $this->assertEquals([], $tree->getAncestorsFor($hosts->o2));
+ $this->assertEquals([], $tree->listAncestorIdsFor($hosts->o2));
+ }
+
+ public function testSimpleInheritanceWithMultipleParentsGivesOrderedResult()
+ {
+ $db = $this->getDb();
+ $hosts = $this->prepareHosts($db);
+ $tree = new TemplateTree('host', $db);
+ $this->assertArrayEqualsWithKeys([
+ $hosts->o2->id => 'o2',
+ $hosts->o1->id => 'o1',
+ ], $tree->getParentsFor($hosts->o4));
+ $this->assertArrayEqualsWithKeys([
+ (int) $hosts->o2->id,
+ (int) $hosts->o1->id,
+ ], $tree->listParentIdsFor($hosts->o4));
+ }
+
+ public function testMultiInheritanceIsResolved()
+ {
+ $db = $this->getDb();
+ $hosts = $this->prepareHosts($db);
+ $tree = new TemplateTree('host', $db);
+ $this->assertArrayEqualsWithKeys([
+ $hosts->o2->id => 'o2',
+ $hosts->o1->id => 'o1',
+ $hosts->o4->id => 'o4'
+ ], $tree->getAncestorsFor($hosts->o5));
+ $this->assertArrayEqualsWithKeys([
+ (int) $hosts->o2->get('id'),
+ (int) $hosts->o1->getProperty('id'),
+ $hosts->o4->getAutoincId(),
+ ], $tree->listAncestorIdsFor($hosts->o5));
+ }
+
+ public function testTemplateOrderIsCorrectWhenInheritingSameTemplateMultipleTimes()
+ {
+ $db = $this->getDb();
+ $hosts = $this->prepareHosts($db);
+ $tree = new TemplateTree('host', $db);
+ $this->assertArrayEqualsWithKeys([
+ $hosts->o1->id => 'o1',
+ $hosts->o4->id => 'o4',
+ $hosts->o2->id => 'o2'
+ ], $tree->getAncestorsFor($hosts->o6));
+ $this->assertArrayEqualsWithKeys([
+ $hosts->o1->getAutoincId(),
+ $hosts->o4->getAutoincId(),
+ $hosts->o2->getAutoincId(),
+ ], $tree->listAncestorIdsFor($hosts->o6));
+ }
+
+ protected function assertArrayEqualsWithKeys($expected, $actual)
+ {
+ $message = sprintf(
+ 'Failed asserting that %s equals %s',
+ json_encode($actual),
+ json_encode($expected)
+ );
+
+ $this->assertTrue(
+ $expected === $actual,
+ $message
+ );
+ }
+
+ protected function assertSameArrayValues($expected, $actual)
+ {
+ $message = sprintf(
+ 'Failed asserting that %s has the same values as %s',
+ json_encode($actual),
+ json_encode($expected)
+ );
+
+ sort($expected);
+ sort($actual);
+ $this->assertTrue(
+ $expected === $actual,
+ $message
+ );
+ }
+
+ public function testChildrenAreResolvedCorrectlyOverMultipleLevels()
+ {
+ $db = $this->getDb();
+ $o1 = IcingaService::create([
+ 'object_name' => 'o1',
+ 'object_type' => 'template'
+ ], $db);
+ $o2 = IcingaService::create([
+ 'object_name' => 'o2',
+ 'object_type' => 'template'
+ ], $db);
+ $o3 = IcingaService::create([
+ 'object_name' => 'o3',
+ 'object_type' => 'template'
+ ], $db);
+ $o4 = IcingaService::create([
+ 'object_name' => 'o4',
+ 'object_type' => 'template',
+ 'imports' => ['o2', 'o1'],
+ ], $db);
+ $o5 = IcingaService::create([
+ 'object_name' => 'o5',
+ 'object_type' => 'template',
+ 'imports' => ['o4'],
+ ], $db);
+ $o6 = IcingaService::create([
+ 'object_name' => 'o6',
+ 'object_type' => 'template',
+ 'imports' => ['o4', 'o2'],
+ ], $db);
+ $o7 = IcingaService::create([
+ 'object_name' => 'o7',
+ 'object_type' => 'apply',
+ 'imports' => ['o4', 'o2'],
+ ], $db);
+ $o1->store();
+ $o2->store();
+ $o3->store();
+ $o4->store();
+ $o5->store();
+ $o6->store();
+ $o7->store();
+ $this->applyId = (int) $o7->get('id');
+
+ $tree = new TemplateTree('service', $db);
+ $this->assertEquals([
+ $o4->id => 'o4',
+ $o5->id => 'o5',
+ $o6->id => 'o6',
+ ], $tree->getDescendantsFor($o2));
+ $this->assertSameArrayValues([
+ $o4->getAutoincId(),
+ (int) $o5->id,
+ (int) $o6->getProperty('id'),
+ ], $tree->listDescendantIdsFor($o2));
+ $this->assertEquals([
+ $o5->id => 'o5',
+ $o6->id => 'o6',
+ ], $tree->getChildrenFor($o4));
+ $this->assertEquals([], $tree->getChildrenFor($o5));
+ }
+
+ protected function removeHosts(Db $db)
+ {
+ $kill = array('o7', 'o6', 'o5', 'o4', 'o3', 'o2', 'o1');
+ foreach ($kill as $name) {
+ if (IcingaHost::exists($name, $db)) {
+ IcingaHost::load($name, $db)->delete();
+ }
+ }
+ }
+
+ protected function removeServices(Db $db)
+ {
+ if ($this->applyId) {
+ $key = ['id' => $this->applyId];
+ if (IcingaService::exists($key, $db)) {
+ IcingaService::load($key, $db)->delete();
+ }
+ }
+
+ $kill = array('o6', 'o5', 'o4', 'o3', 'o2', 'o1');
+ foreach ($kill as $name) {
+ $key = [
+ 'object_name' => $name,
+ 'object_type' => 'template',
+ ];
+ if (IcingaService::exists($key, $db)) {
+ IcingaService::load($key, $db)->delete();
+ }
+ }
+ }
+
+ public function tearDown()
+ {
+ if ($this->hasDb()) {
+ $db = $this->getDb();
+ $this->removeHosts($db);
+ $this->removeServices($db);
+ }
+ }
+}
diff --git a/test/php/library/Director/Restriction/MatchingFilterTest.php b/test/php/library/Director/Restriction/MatchingFilterTest.php
new file mode 100644
index 0000000..cd1b57e
--- /dev/null
+++ b/test/php/library/Director/Restriction/MatchingFilterTest.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Tests\Icinga\Module\Director\Restriction;
+
+use Icinga\Module\Director\Restriction\MatchingFilter;
+use Icinga\Module\Director\Test\BaseTestCase;
+use Icinga\User;
+
+class MatchingFilterTest extends BaseTestCase
+{
+ public function testUserWithNoRestrictionHasNoFilter()
+ {
+ $user = new User('dummy');
+ $this->assertEquals(
+ '',
+ (string) MatchingFilter::forUser($user, 'some/name', 'prop')
+ );
+ }
+
+ public function testSimpleRestrictionRendersCorrectly()
+ {
+ $this->assertEquals(
+ 'prop = a*',
+ (string) MatchingFilter::forPatterns(['a*'], 'prop')
+ );
+ }
+
+ public function testMultipleRestrictionsAreCombinedWithOr()
+ {
+ $this->assertEquals(
+ 'prop = a* | prop = *b',
+ (string) MatchingFilter::forPatterns(['a*', '*b'], 'prop')
+ );
+ }
+
+ public function testUserWithMultipleRestrictionsWorksFine()
+ {
+ $user = new User('dummy');
+ $user->setRestrictions([
+ 'some/name' => ['a*', '*b'],
+ 'some/thing' => ['else']
+ ]);
+ $this->assertEquals(
+ 'prop = a* | prop = *b',
+ (string) MatchingFilter::forUser($user, 'some/name', 'prop')
+ );
+ }
+
+ public function testSingleRestrictionAllowsForPipes()
+ {
+ $this->assertEquals(
+ 'prop = a* | prop = *b',
+ (string) MatchingFilter::forPatterns(['a*|*b'], 'prop')
+ );
+ }
+}
diff --git a/test/phpunit-compat.php b/test/phpunit-compat.php
new file mode 100644
index 0000000..2b1be3a
--- /dev/null
+++ b/test/phpunit-compat.php
@@ -0,0 +1,10 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @codingStandardsIgnoreStart
+ */
+class PHPUnit_Framework_TestCase extends TestCase
+{
+}
diff --git a/test/setup_vendor.sh b/test/setup_vendor.sh
new file mode 100755
index 0000000..c53982f
--- /dev/null
+++ b/test/setup_vendor.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+
+set -ex
+
+MODULE_HOME=${MODULE_HOME:="$(dirname "$(readlink -f "$(dirname "$0")")")"}
+PHP_VERSION="$(php -r 'echo phpversion();')"
+
+ICINGAWEB_VERSION=${ICINGAWEB_VERSION:=2.7.1}
+ICINGAWEB_GITREF=${ICINGAWEB_GITREF:=}
+
+if [ "$PHP_VERSION" '<' 7.1.0 ]; then
+ PHPCS_VERSION=${PHPCS_VERSION:=3.3.2}
+else
+ PHPCS_VERSION=${PHPCS_VERSION:=3.5.2}
+fi
+
+if [ "$PHP_VERSION" '<' 5.6.0 ]; then
+ PHPUNIT_VERSION=${PHPUNIT_VERSION:=4.8}
+else
+ PHPUNIT_VERSION=${PHPUNIT_VERSION:=5.7}
+fi
+
+cd "${MODULE_HOME}"
+
+test -d vendor || mkdir vendor
+cd vendor/
+
+# icingaweb2
+if [ -n "$ICINGAWEB_GITREF" ]; then
+ icingaweb_path="icingaweb2"
+ test ! -L "$icingaweb_path" || rm "$icingaweb_path"
+
+ if [ ! -d "$icingaweb_path" ]; then
+ git clone https://github.com/Icinga/icingaweb2.git "$icingaweb_path"
+ fi
+
+ (
+ set -e
+ cd "$icingaweb_path"
+ git fetch -p
+ git checkout -f "$ICINGAWEB_GITREF"
+ )
+else
+ icingaweb_path="icingaweb2-${ICINGAWEB_VERSION}"
+ if [ ! -e "${icingaweb_path}".tar.gz ]; then
+ wget -O "${icingaweb_path}".tar.gz https://github.com/Icinga/icingaweb2/archive/v"${ICINGAWEB_VERSION}".tar.gz
+ fi
+ if [ ! -d "${icingaweb_path}" ]; then
+ tar xf "${icingaweb_path}".tar.gz
+ fi
+
+ rm -f icingaweb2
+ ln -svf "${icingaweb_path}" icingaweb2
+fi
+ln -svf "${icingaweb_path}"/library/Icinga Icinga
+ln -svf "${icingaweb_path}"/library/vendor/Zend Zend
+
+# phpunit
+phpunit_path="phpunit-${PHPUNIT_VERSION}"
+if [ ! -e "${phpunit_path}".phar ]; then
+ wget -O "${phpunit_path}".phar https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar
+fi
+ln -svf "${phpunit_path}".phar phpunit.phar
+
+# phpcs
+phpcs_path="phpcs-${PHPCS_VERSION}"
+if [ ! -e "${phpcs_path}".phar ]; then
+ wget -O "${phpcs_path}".phar \
+ https://github.com/squizlabs/PHP_CodeSniffer/releases/download/${PHPCS_VERSION}/phpcs.phar
+fi
+ln -svf "${phpcs_path}".phar phpcs.phar
diff --git a/test/travis-prepare.sh b/test/travis-prepare.sh
new file mode 100755
index 0000000..7a303e8
--- /dev/null
+++ b/test/travis-prepare.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+set -ex
+
+: "${DIRECTOR_TESTDB:=director_test}"
+
+psql_cmd() {
+ psql -U postgres ${DIRECTOR_TESTDB} -q -c "$@"
+}
+
+if [ "$DB" = mysql ]; then
+ mysql -u root -e "DROP DATABASE IF EXISTS ${DIRECTOR_TESTDB}; CREATE DATABASE ${DIRECTOR_TESTDB};"
+elif [ "$DB" = pgsql ]; then
+ : "${DIRECTOR_TESTDB_USER:=director_test}"
+
+ psql -U postgres postgres -q -c "DROP DATABASE IF EXISTS ${DIRECTOR_TESTDB};"
+ psql -U postgres postgres -q -c "CREATE DATABASE ${DIRECTOR_TESTDB} WITH ENCODING 'UTF8';"
+ psql_cmd "CREATE USER ${DIRECTOR_TESTDB_USER} WITH PASSWORD 'testing';"
+ psql_cmd "GRANT ALL PRIVILEGES ON DATABASE ${DIRECTOR_TESTDB} TO ${DIRECTOR_TESTDB_USER};"
+ psql_cmd "CREATE EXTENSION pgcrypto;"
+else
+ echo "Unknown database set in environment!" >&2
+ env
+ exit 1
+fi