1<?php 2 3namespace Icinga\Module\Director\Objects; 4 5use Icinga\Data\Db\DbConnection; 6use Icinga\Exception\NotFoundError; 7use Icinga\Module\Director\Data\PropertiesFilter; 8use Icinga\Module\Director\Db; 9use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; 10use Icinga\Module\Director\Exception\DuplicateKeyException; 11use Icinga\Module\Director\IcingaConfig\IcingaConfig; 12use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; 13use Icinga\Module\Director\Objects\Extension\FlappingSupport; 14use InvalidArgumentException; 15use RuntimeException; 16 17class IcingaHost extends IcingaObject implements ExportInterface 18{ 19 use FlappingSupport; 20 21 protected $table = 'icinga_host'; 22 23 protected $defaultProperties = array( 24 'id' => null, 25 'object_name' => null, 26 'object_type' => null, 27 'disabled' => 'n', 28 'display_name' => null, 29 'address' => null, 30 'address6' => null, 31 'check_command_id' => null, 32 'max_check_attempts' => null, 33 'check_period_id' => null, 34 'check_interval' => null, 35 'retry_interval' => null, 36 'check_timeout' => null, 37 'enable_notifications' => null, 38 'enable_active_checks' => null, 39 'enable_passive_checks' => null, 40 'enable_event_handler' => null, 41 'enable_flapping' => null, 42 'enable_perfdata' => null, 43 'event_command_id' => null, 44 'flapping_threshold_high' => null, 45 'flapping_threshold_low' => null, 46 'volatile' => null, 47 'zone_id' => null, 48 'command_endpoint_id' => null, 49 'notes' => null, 50 'notes_url' => null, 51 'action_url' => null, 52 'icon_image' => null, 53 'icon_image_alt' => null, 54 'has_agent' => null, 55 'master_should_connect' => null, 56 'accept_config' => null, 57 'api_key' => null, 58 'template_choice_id' => null, 59 ); 60 61 protected $relations = array( 62 'check_command' => 'IcingaCommand', 63 'event_command' => 'IcingaCommand', 64 'check_period' => 'IcingaTimePeriod', 65 'command_endpoint' => 'IcingaEndpoint', 66 'zone' => 'IcingaZone', 67 'template_choice' => 'IcingaTemplateChoiceHost', 68 ); 69 70 protected $booleans = array( 71 'enable_notifications' => 'enable_notifications', 72 'enable_active_checks' => 'enable_active_checks', 73 'enable_passive_checks' => 'enable_passive_checks', 74 'enable_event_handler' => 'enable_event_handler', 75 'enable_flapping' => 'enable_flapping', 76 'enable_perfdata' => 'enable_perfdata', 77 'volatile' => 'volatile', 78 'has_agent' => 'has_agent', 79 'master_should_connect' => 'master_should_connect', 80 'accept_config' => 'accept_config', 81 ); 82 83 protected $intervalProperties = array( 84 'check_interval' => 'check_interval', 85 'check_timeout' => 'check_timeout', 86 'retry_interval' => 'retry_interval', 87 ); 88 89 protected $supportsCustomVars = true; 90 91 protected $supportsGroups = true; 92 93 protected $supportsImports = true; 94 95 protected $supportsFields = true; 96 97 protected $supportsChoices = true; 98 99 protected $supportedInLegacy = true; 100 101 /** @var HostGroupMembershipResolver */ 102 protected $hostgroupMembershipResolver; 103 104 public static function enumProperties( 105 DbConnection $connection = null, 106 $prefix = '', 107 $filter = null 108 ) { 109 $hostProperties = array(); 110 if ($filter === null) { 111 $filter = new PropertiesFilter(); 112 } 113 $realProperties = array_merge(['templates'], static::create()->listProperties()); 114 sort($realProperties); 115 116 if ($filter->match(PropertiesFilter::$HOST_PROPERTY, 'name')) { 117 $hostProperties[$prefix . 'name'] = 'name'; 118 } 119 foreach ($realProperties as $prop) { 120 if (!$filter->match(PropertiesFilter::$HOST_PROPERTY, $prop)) { 121 continue; 122 } 123 124 if (substr($prop, -3) === '_id') { 125 if ($prop === 'template_choice_id') { 126 continue; 127 } 128 $prop = substr($prop, 0, -3); 129 } 130 131 $hostProperties[$prefix . $prop] = $prop; 132 } 133 134 $hostVars = array(); 135 136 if ($connection instanceof Db) { 137 foreach ($connection->fetchDistinctHostVars() as $var) { 138 if ($filter->match(PropertiesFilter::$CUSTOM_PROPERTY, $var->varname, $var)) { 139 if ($var->datatype) { 140 $hostVars[$prefix . 'vars.' . $var->varname] = sprintf( 141 '%s (%s)', 142 $var->varname, 143 $var->caption 144 ); 145 } else { 146 $hostVars[$prefix . 'vars.' . $var->varname] = $var->varname; 147 } 148 } 149 } 150 } 151 152 //$properties['vars.*'] = 'Other custom variable'; 153 ksort($hostVars); 154 155 156 $props = mt('director', 'Host properties'); 157 $vars = mt('director', 'Custom variables'); 158 159 $properties = array(); 160 if (!empty($hostProperties)) { 161 $properties[$props] = $hostProperties; 162 $properties[$props][$prefix . 'groups'] = 'Groups'; 163 } 164 165 if (!empty($hostVars)) { 166 $properties[$vars] = $hostVars; 167 } 168 169 return $properties; 170 } 171 172 public function getCheckCommand() 173 { 174 $id = $this->getSingleResolvedProperty('check_command_id'); 175 return IcingaCommand::loadWithAutoIncId( 176 $id, 177 $this->getConnection() 178 ); 179 } 180 181 public function hasCheckCommand() 182 { 183 return $this->getSingleResolvedProperty('check_command_id') !== null; 184 } 185 186 public function renderToConfig(IcingaConfig $config) 187 { 188 parent::renderToConfig($config); 189 190 // TODO: We might alternatively let the whole config fail in case we have 191 // used use_agent together with a legacy config 192 if (! $config->isLegacy()) { 193 $this->renderAgentZoneAndEndpoint($config); 194 } 195 } 196 197 public function renderAgentZoneAndEndpoint(IcingaConfig $config = null) 198 { 199 if (!$this->isObject()) { 200 return; 201 } 202 203 if ($this->isDisabled()) { 204 return; 205 } 206 207 if ($this->getRenderingZone($config) === self::RESOLVE_ERROR) { 208 return; 209 } 210 211 if ($this->getSingleResolvedProperty('has_agent') !== 'y') { 212 return; 213 } 214 215 $name = $this->object_name; 216 if (IcingaEndpoint::exists($name, $this->connection)) { 217 return; 218 } 219 220 $props = array( 221 'object_name' => $name, 222 'object_type' => 'object', 223 'log_duration' => 0 224 ); 225 226 if ($this->getSingleResolvedProperty('master_should_connect') === 'y') { 227 $props['host'] = $this->getSingleResolvedProperty('address'); 228 } 229 230 $props['zone_id'] = $this->getSingleResolvedProperty('zone_id'); 231 232 $endpoint = IcingaEndpoint::create($props, $this->connection); 233 234 $zone = IcingaZone::create(array( 235 'object_name' => $name, 236 ), $this->connection)->setEndpointList(array($name)); 237 238 if ($props['zone_id']) { 239 $zone->parent_id = $props['zone_id']; 240 } else { 241 $zone->parent = $this->connection->getMasterZoneName(); 242 } 243 244 $pre = 'zones.d/' . $this->getRenderingZone($config) . '/'; 245 $config->configFile($pre . 'agent_endpoints')->addObject($endpoint); 246 $config->configFile($pre . 'agent_zones')->addObject($zone); 247 } 248 249 public function getAgentListenPort() 250 { 251 $conn = $this->connection; 252 $name = $this->getObjectName(); 253 if (IcingaEndpoint::exists($name, $conn)) { 254 return IcingaEndpoint::load($name, $conn)->getResolvedPort(); 255 } else { 256 return 5665; 257 } 258 } 259 260 public function getUniqueIdentifier() 261 { 262 if ($this->isTemplate()) { 263 return $this->getObjectName(); 264 } else { 265 throw new RuntimeException( 266 'getUniqueIdentifier() is supported by Host Templates only' 267 ); 268 } 269 } 270 271 /** 272 * @return object 273 * @throws \Icinga\Exception\NotFoundError 274 */ 275 public function export() 276 { 277 // TODO: ksort in toPlainObject? 278 $props = (array) $this->toPlainObject(); 279 $props['fields'] = $this->loadFieldReferences(); 280 ksort($props); 281 282 return (object) $props; 283 } 284 285 /** 286 * @param $plain 287 * @param Db $db 288 * @param bool $replace 289 * @return IcingaHost 290 * @throws DuplicateKeyException 291 * @throws \Icinga\Exception\NotFoundError 292 */ 293 public static function import($plain, Db $db, $replace = false) 294 { 295 $properties = (array) $plain; 296 $name = $properties['object_name']; 297 if ($properties['object_type'] !== 'template') { 298 throw new InvalidArgumentException(sprintf( 299 'Can import only Templates, got "%s" for "%s"', 300 $properties['object_type'], 301 $name 302 )); 303 } 304 $key = $name; 305 306 if ($replace && static::exists($key, $db)) { 307 $object = static::load($key, $db); 308 } elseif (static::exists($key, $db)) { 309 throw new DuplicateKeyException( 310 'Service Template "%s" already exists', 311 $name 312 ); 313 } else { 314 $object = static::create([], $db); 315 } 316 317 // $object->newFields = $properties['fields']; 318 unset($properties['fields']); 319 $object->setProperties($properties); 320 321 return $object; 322 } 323 324 protected function loadFieldReferences() 325 { 326 $db = $this->getDb(); 327 328 $res = $db->fetchAll( 329 $db->select()->from([ 330 'hf' => 'icinga_host_field' 331 ], [ 332 'hf.datafield_id', 333 'hf.is_required', 334 'hf.var_filter', 335 ])->join(['df' => 'director_datafield'], 'df.id = hf.datafield_id', []) 336 ->where('host_id = ?', $this->get('id')) 337 ->order('varname ASC') 338 ); 339 340 if (empty($res)) { 341 return []; 342 } else { 343 foreach ($res as $field) { 344 $field->datafield_id = (int) $field->datafield_id; 345 } 346 return $res; 347 } 348 } 349 350 public function hasAnyOverridenServiceVars() 351 { 352 $varname = $this->getServiceOverrivesVarname(); 353 return isset($this->vars()->$varname); 354 } 355 356 public function getAllOverriddenServiceVars() 357 { 358 if ($this->hasAnyOverridenServiceVars()) { 359 $varname = $this->getServiceOverrivesVarname(); 360 return $this->vars()->$varname->getValue(); 361 } else { 362 return (object) array(); 363 } 364 } 365 366 public function hasOverriddenServiceVars($service) 367 { 368 $all = $this->getAllOverriddenServiceVars(); 369 return property_exists($all, $service); 370 } 371 372 public function getOverriddenServiceVars($service) 373 { 374 if ($this->hasOverriddenServiceVars($service)) { 375 $all = $this->getAllOverriddenServiceVars(); 376 return $all->$service; 377 } else { 378 return (object) array(); 379 } 380 } 381 382 public function overrideServiceVars($service, $vars) 383 { 384 // For PHP < 5.5.0: 385 $array = (array) $vars; 386 if (empty($array)) { 387 return $this->unsetOverriddenServiceVars($service); 388 } 389 390 $all = $this->getAllOverriddenServiceVars(); 391 $all->$service = $vars; 392 $varname = $this->getServiceOverrivesVarname(); 393 $this->vars()->$varname = $all; 394 395 return $this; 396 } 397 398 public function unsetOverriddenServiceVars($service) 399 { 400 if ($this->hasOverriddenServiceVars($service)) { 401 $all = (array) $this->getAllOverriddenServiceVars(); 402 unset($all[$service]); 403 404 $varname = $this->getServiceOverrivesVarname(); 405 if (empty($all)) { 406 unset($this->vars()->$varname); 407 } else { 408 $this->vars()->$varname = (object) $all; 409 } 410 } 411 412 return $this; 413 } 414 415 protected function notifyResolvers() 416 { 417 $resolver = $this->getHostGroupMembershipResolver(); 418 $resolver->addObject($this); 419 $resolver->refreshDb(); 420 421 return $this; 422 } 423 424 protected function getHostGroupMembershipResolver() 425 { 426 if ($this->hostgroupMembershipResolver === null) { 427 $this->hostgroupMembershipResolver = new HostGroupMembershipResolver( 428 $this->getConnection() 429 ); 430 } 431 432 return $this->hostgroupMembershipResolver; 433 } 434 435 public function setHostGroupMembershipResolver(HostGroupMembershipResolver $resolver) 436 { 437 $this->hostgroupMembershipResolver = $resolver; 438 return $this; 439 } 440 441 protected function getServiceOverrivesVarname() 442 { 443 return $this->connection->settings()->override_services_varname; 444 } 445 446 /** 447 * Internal property, will not be rendered 448 * 449 * Avoid complaints for method names with underscore: 450 * @codingStandardsIgnoreStart 451 * 452 * @return string 453 */ 454 protected function renderHas_Agent() 455 { 456 return ''; 457 } 458 459 /** 460 * Internal property, will not be rendered 461 * 462 * @return string 463 */ 464 protected function renderMaster_should_connect() 465 { 466 return ''; 467 } 468 469 /** 470 * Internal property, will not be rendered 471 * 472 * @return string 473 */ 474 protected function renderApi_key() 475 { 476 return ''; 477 } 478 479 /** 480 * Internal property, will not be rendered 481 * 482 * @return string 483 */ 484 protected function renderTemplate_choice_id() 485 { 486 return ''; 487 } 488 489 /** 490 * Internal property, will not be rendered 491 * 492 * @return string 493 */ 494 protected function renderAccept_config() 495 { 496 // @codingStandardsIgnoreEnd 497 return ''; 498 } 499 500 /** 501 * @codingStandardsIgnoreStart 502 * 503 * @return string 504 */ 505 protected function renderLegacyDisplay_Name() 506 { 507 // @codingStandardsIgnoreEnd 508 return c1::renderKeyValue('display_name', $this->display_name); 509 } 510 511 protected function renderLegacyVolatile() 512 { 513 // not available for hosts in Icinga 1.x 514 return; 515 } 516 517 protected function renderLegacyCustomExtensions() 518 { 519 $str = parent::renderLegacyCustomExtensions(); 520 521 if (($alias = $this->vars()->get('alias')) !== null) { 522 $str .= c1::renderKeyValue('alias', $alias->getValue()); 523 } 524 525 return $str; 526 } 527 528 /** 529 * @return IcingaService[] 530 */ 531 public function fetchServices() 532 { 533 $connection = $this->getConnection(); 534 $db = $connection->getDbAdapter(); 535 536 /** @var IcingaService[] $services */ 537 $services = IcingaService::loadAll( 538 $connection, 539 $db->select()->from('icinga_service') 540 ->where('host_id = ?', $this->get('id')) 541 ); 542 543 return $services; 544 } 545 546 /** 547 * @return IcingaServiceSet[] 548 */ 549 public function fetchServiceSets() 550 { 551 $connection = $this->getConnection(); 552 $db = $connection->getDbAdapter(); 553 554 /** @var IcingaServiceSet[] $sets */ 555 $sets = IcingaServiceSet::loadAll( 556 $connection, 557 $db->select()->from('icinga_service_set') 558 ->where('host_id = ?', $this->get('id')) 559 ); 560 561 return $sets; 562 } 563 564 /** 565 * @return string 566 */ 567 public function generateApiKey() 568 { 569 $key = sha1( 570 (string) microtime(false) 571 . $this->getObjectName() 572 . rand(1, 1000000) 573 ); 574 575 if ($this->dbHasApiKey($key)) { 576 $key = $this->generateApiKey(); 577 } 578 579 $this->set('api_key', $key); 580 581 return $key; 582 } 583 584 protected function dbHasApiKey($key) 585 { 586 $db = $this->getDb(); 587 $query = $db->select()->from( 588 ['o' => $this->getTableName()], 589 'o.api_key' 590 )->where('api_key = ?', $key); 591 592 return $db->fetchOne($query) === $key; 593 } 594 595 public static function loadWithApiKey($key, Db $db) 596 { 597 $query = $db->getDbAdapter() 598 ->select() 599 ->from('icinga_host') 600 ->where('api_key = ?', $key); 601 602 $result = self::loadAll($db, $query); 603 if (count($result) !== 1) { 604 throw new NotFoundError('Got invalid API key "%s"', $key); 605 } 606 607 return current($result); 608 } 609} 610