1<?php 2/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ 3 4namespace Icinga\Module\Monitoring\Object; 5 6use stdClass; 7use InvalidArgumentException; 8use Icinga\Authentication\Auth; 9use Icinga\Application\Config; 10use Icinga\Data\Filter\Filter; 11use Icinga\Data\Filterable; 12use Icinga\Exception\InvalidPropertyException; 13use Icinga\Exception\ProgrammingError; 14use Icinga\Module\Monitoring\Backend\MonitoringBackend; 15use Icinga\Util\GlobFilter; 16use Icinga\Web\UrlParams; 17 18/** 19 * A monitored Icinga object, i.e. host or service 20 */ 21abstract class MonitoredObject implements Filterable 22{ 23 /** 24 * Type host 25 */ 26 const TYPE_HOST = 'host'; 27 28 /** 29 * Type service 30 */ 31 const TYPE_SERVICE = 'service'; 32 33 /** 34 * Acknowledgement of the host or service if any 35 * 36 * @var object 37 */ 38 protected $acknowledgement; 39 40 /** 41 * Backend to fetch object information from 42 * 43 * @var MonitoringBackend 44 */ 45 protected $backend; 46 47 /** 48 * Comments 49 * 50 * @var array 51 */ 52 protected $comments; 53 54 /** 55 * This object's obfuscated custom variables 56 * 57 * @var array 58 */ 59 protected $customvars; 60 61 /** 62 * The host custom variables 63 * 64 * @var array 65 */ 66 protected $hostVariables; 67 68 /** 69 * The service custom variables 70 * 71 * @var array 72 */ 73 protected $serviceVariables; 74 75 /** 76 * Contact groups 77 * 78 * @var array 79 */ 80 protected $contactgroups; 81 82 /** 83 * Contacts 84 * 85 * @var array 86 */ 87 protected $contacts; 88 89 /** 90 * Downtimes 91 * 92 * @var array 93 */ 94 protected $downtimes; 95 96 /** 97 * Event history 98 * 99 * @var \Icinga\Module\Monitoring\DataView\EventHistory 100 */ 101 protected $eventhistory; 102 103 /** 104 * Filter 105 * 106 * @var Filter 107 */ 108 protected $filter; 109 110 /** 111 * Host groups 112 * 113 * @var array 114 */ 115 protected $hostgroups; 116 117 /** 118 * Prefix of the Icinga object, i.e. 'host_' or 'service_' 119 * 120 * @var string 121 */ 122 protected $prefix; 123 124 /** 125 * Properties 126 * 127 * @var object 128 */ 129 protected $properties; 130 131 /** 132 * Service groups 133 * 134 * @var array 135 */ 136 protected $servicegroups; 137 138 /** 139 * Type of the Icinga object, i.e. 'host' or 'service' 140 * 141 * @var string 142 */ 143 protected $type; 144 145 /** 146 * Stats 147 * 148 * @var object 149 */ 150 protected $stats; 151 152 /** 153 * The properties to hide from the user 154 * 155 * @var GlobFilter 156 */ 157 protected $blacklistedProperties = null; 158 159 /** 160 * Create a monitored object, i.e. host or service 161 * 162 * @param MonitoringBackend $backend Backend to fetch object information from 163 */ 164 public function __construct(MonitoringBackend $backend) 165 { 166 $this->backend = $backend; 167 } 168 169 /** 170 * Get the object's data view 171 * 172 * @return \Icinga\Module\Monitoring\DataView\DataView 173 */ 174 abstract protected function getDataView(); 175 176 /** 177 * Get all note urls configured for this monitored object 178 * 179 * @return array All note urls as a string 180 */ 181 abstract public function getNotesUrls(); 182 183 /** 184 * {@inheritdoc} 185 */ 186 public function addFilter(Filter $filter) 187 { 188 // Left out on purpose. Interface is deprecated. 189 } 190 191 /** 192 * {@inheritdoc} 193 */ 194 public function applyFilter(Filter $filter) 195 { 196 $this->getFilter()->addFilter($filter); 197 198 return $this; 199 } 200 201 /** 202 * {@inheritdoc} 203 */ 204 public function getFilter() 205 { 206 if ($this->filter === null) { 207 $this->filter = Filter::matchAll(); 208 } 209 210 return $this->filter; 211 } 212 213 /** 214 * {@inheritdoc} 215 */ 216 public function setFilter(Filter $filter) 217 { 218 // Left out on purpose. Interface is deprecated. 219 } 220 221 /** 222 * {@inheritdoc} 223 */ 224 public function where($condition, $value = null) 225 { 226 // Left out on purpose. Interface is deprecated. 227 } 228 229 /** 230 * Return whether this object matches the given filter 231 * 232 * @param Filter $filter 233 * 234 * @return bool 235 * 236 * @throws ProgrammingError In case the object cannot be found 237 * 238 * @deprecated Use $filter->matches($object) instead 239 */ 240 public function matches(Filter $filter) 241 { 242 if ($this->properties === null && $this->fetch() === false) { 243 throw new ProgrammingError( 244 'Unable to apply filter. Object %s of type %s not found.', 245 $this->getName(), 246 $this->getType() 247 ); 248 } 249 250 return $filter->matches($this); 251 } 252 253 /** 254 * Require the object's type to be one of the given types 255 * 256 * @param array $oneOf 257 * 258 * @return bool 259 * @throws InvalidArgumentException If the object's type is not one of the given types. 260 */ 261 public function assertOneOf(array $oneOf) 262 { 263 if (! in_array($this->type, $oneOf)) { 264 throw new InvalidArgumentException; 265 } 266 return true; 267 } 268 269 /** 270 * Fetch the object's properties 271 * 272 * @return bool 273 */ 274 public function fetch() 275 { 276 $properties = $this->getDataView()->applyFilter($this->getFilter())->getQuery()->fetchRow(); 277 278 if ($properties === false) { 279 return false; 280 } 281 282 if (isset($properties->host_contacts)) { 283 $this->contacts = array(); 284 foreach (preg_split('~,~', $properties->host_contacts) as $contact) { 285 $this->contacts[] = (object) array( 286 'contact_name' => $contact, 287 'contact_alias' => $contact, 288 'contact_email' => null, 289 'contact_pager' => null, 290 ); 291 } 292 } 293 294 $this->properties = $properties; 295 296 return true; 297 } 298 299 /** 300 * Fetch the object's acknowledgement 301 */ 302 public function fetchAcknowledgement() 303 { 304 if ($this->comments === null) { 305 $this->fetchComments(); 306 } 307 308 return $this; 309 } 310 311 /** 312 * Fetch the object's comments 313 * 314 * @return $this 315 */ 316 public function fetchComments() 317 { 318 $commentsView = $this->backend->select()->from('comment', array( 319 'author' => 'comment_author_name', 320 'comment' => 'comment_data', 321 'expiration' => 'comment_expiration', 322 'id' => 'comment_internal_id', 323 'name' => 'comment_name', 324 'persistent' => 'comment_is_persistent', 325 'timestamp' => 'comment_timestamp', 326 'type' => 'comment_type' 327 )); 328 if ($this->type === self::TYPE_SERVICE) { 329 $commentsView 330 ->where('service_host_name', $this->host_name) 331 ->where('service_description', $this->service_description); 332 } else { 333 $commentsView->where('host_name', $this->host_name); 334 } 335 $commentsView 336 ->where('comment_type', array('ack', 'comment')) 337 ->where('object_type', $this->type); 338 339 $comments = $commentsView->fetchAll(); 340 341 if ((bool) $this->properties->{$this->prefix . 'acknowledged'}) { 342 $ackCommentIdx = null; 343 344 foreach ($comments as $i => $comment) { 345 if ($comment->type === 'ack') { 346 $this->acknowledgement = new Acknowledgement(array( 347 'author' => $comment->author, 348 'comment' => $comment->comment, 349 'entry_time' => $comment->timestamp, 350 'expiration_time' => $comment->expiration, 351 'sticky' => (int) $this->properties->{$this->prefix . 'acknowledgement_type'} === 2 352 )); 353 $ackCommentIdx = $i; 354 break; 355 } 356 } 357 358 if ($ackCommentIdx !== null) { 359 unset($comments[$ackCommentIdx]); 360 } 361 } 362 363 $this->comments = $comments; 364 365 return $this; 366 } 367 368 /** 369 * Fetch the object's contact groups 370 * 371 * @return $this 372 */ 373 public function fetchContactgroups() 374 { 375 $contactsGroups = $this->backend->select()->from('contactgroup', array( 376 'contactgroup_name', 377 'contactgroup_alias' 378 )); 379 if ($this->type === self::TYPE_SERVICE) { 380 $contactsGroups 381 ->where('service_host_name', $this->host_name) 382 ->where('service_description', $this->service_description); 383 } else { 384 $contactsGroups->where('host_name', $this->host_name); 385 } 386 $this->contactgroups = $contactsGroups; 387 return $this; 388 } 389 390 /** 391 * Fetch the object's contacts 392 * 393 * @return $this 394 */ 395 public function fetchContacts() 396 { 397 $contacts = $this->backend->select()->from("{$this->type}contact", array( 398 'contact_name', 399 'contact_alias', 400 'contact_email', 401 'contact_pager', 402 )); 403 if ($this->type === self::TYPE_SERVICE) { 404 $contacts 405 ->where('service_host_name', $this->host_name) 406 ->where('service_description', $this->service_description); 407 } else { 408 $contacts->where('host_name', $this->host_name); 409 } 410 $this->contacts = $contacts; 411 return $this; 412 } 413 414 /** 415 * Fetch this object's obfuscated custom variables 416 * 417 * @return $this 418 */ 419 public function fetchCustomvars() 420 { 421 $blacklist = array(); 422 $blacklistPattern = ''; 423 424 if (($blacklistConfig = Config::module('monitoring')->get('security', 'protected_customvars', '')) !== '') { 425 foreach (explode(',', $blacklistConfig) as $customvar) { 426 $nonWildcards = array(); 427 foreach (explode('*', $customvar) as $nonWildcard) { 428 $nonWildcards[] = preg_quote($nonWildcard, '/'); 429 } 430 $blacklist[] = implode('.*', $nonWildcards); 431 } 432 $blacklistPattern = '/^(' . implode('|', $blacklist) . ')$/i'; 433 } 434 435 if ($this->type === self::TYPE_SERVICE) { 436 $this->fetchServiceVariables(); 437 $customvars = $this->serviceVariables; 438 } else { 439 $this->fetchHostVariables(); 440 $customvars = $this->hostVariables; 441 } 442 443 $this->customvars = $customvars; 444 $this->hideBlacklistedProperties(); 445 446 if ($blacklistPattern) { 447 $this->customvars = $this->obfuscateCustomVars($this->customvars, $blacklistPattern); 448 } 449 450 return $this; 451 } 452 453 /** 454 * Obfuscate custom variables recursively 455 * 456 * @param stdClass|array $customvars The custom variables to obfuscate 457 * @param string $blacklistPattern Which custom variables to obfuscate 458 * 459 * @return stdClass|array The obfuscated custom variables 460 */ 461 protected function obfuscateCustomVars($customvars, $blacklistPattern) 462 { 463 $obfuscatedCustomVars = array(); 464 foreach ($customvars as $name => $value) { 465 if ($blacklistPattern && preg_match($blacklistPattern, $name)) { 466 $obfuscatedCustomVars[$name] = '***'; 467 } else { 468 $obfuscatedCustomVars[$name] = $value instanceof stdClass || is_array($value) 469 ? $this->obfuscateCustomVars($value, $blacklistPattern) 470 : $value; 471 } 472 } 473 return $customvars instanceof stdClass ? (object) $obfuscatedCustomVars : $obfuscatedCustomVars; 474 } 475 476 /** 477 * Hide all blacklisted properties from the user as restricted by monitoring/blacklist/properties 478 * 479 * Currently this only affects the custom variables 480 */ 481 protected function hideBlacklistedProperties() 482 { 483 if ($this->blacklistedProperties === null) { 484 $this->blacklistedProperties = new GlobFilter( 485 Auth::getInstance()->getRestrictions('monitoring/blacklist/properties') 486 ); 487 } 488 489 $allProperties = $this->blacklistedProperties->removeMatching( 490 array($this->type => array('vars' => $this->customvars)) 491 ); 492 $this->customvars = isset($allProperties[$this->type]['vars']) ? $allProperties[$this->type]['vars'] : array(); 493 } 494 495 /** 496 * Fetch the host custom variables related to this object 497 * 498 * @return $this 499 */ 500 public function fetchHostVariables() 501 { 502 $query = $this->backend->select()->from('customvar', array( 503 'varname', 504 'varvalue', 505 'is_json' 506 )) 507 ->where('object_type', static::TYPE_HOST) 508 ->where('host_name', $this->host_name); 509 510 $this->hostVariables = array(); 511 foreach ($query as $row) { 512 if ($row->is_json) { 513 $this->hostVariables[strtolower($row->varname)] = json_decode($row->varvalue); 514 } else { 515 $this->hostVariables[strtolower($row->varname)] = $row->varvalue; 516 } 517 } 518 519 return $this; 520 } 521 522 /** 523 * Fetch the service custom variables related to this object 524 * 525 * @return $this 526 * 527 * @throws ProgrammingError In case this object is not a service 528 */ 529 public function fetchServiceVariables() 530 { 531 if ($this->type !== static::TYPE_SERVICE) { 532 throw new ProgrammingError('Cannot fetch service custom variables for non-service objects'); 533 } 534 535 $query = $this->backend->select()->from('customvar', array( 536 'varname', 537 'varvalue', 538 'is_json' 539 )) 540 ->where('object_type', static::TYPE_SERVICE) 541 ->where('host_name', $this->host_name) 542 ->where('service_description', $this->service_description); 543 544 $this->serviceVariables = array(); 545 foreach ($query as $row) { 546 if ($row->is_json) { 547 $this->serviceVariables[strtolower($row->varname)] = json_decode($row->varvalue); 548 } else { 549 $this->serviceVariables[strtolower($row->varname)] = $row->varvalue; 550 } 551 } 552 553 return $this; 554 } 555 556 /** 557 * Fetch the object's downtimes 558 * 559 * @return $this 560 */ 561 public function fetchDowntimes() 562 { 563 $downtimes = $this->backend->select()->from('downtime', array( 564 'author_name' => 'downtime_author_name', 565 'comment' => 'downtime_comment', 566 'duration' => 'downtime_duration', 567 'end' => 'downtime_end', 568 'entry_time' => 'downtime_entry_time', 569 'id' => 'downtime_internal_id', 570 'is_fixed' => 'downtime_is_fixed', 571 'is_flexible' => 'downtime_is_flexible', 572 'is_in_effect' => 'downtime_is_in_effect', 573 'name' => 'downtime_name', 574 'objecttype' => 'object_type', 575 'scheduled_end' => 'downtime_scheduled_end', 576 'scheduled_start' => 'downtime_scheduled_start', 577 'start' => 'downtime_start' 578 )) 579 ->where('object_type', $this->type) 580 ->order('downtime_is_in_effect', 'DESC') 581 ->order('downtime_scheduled_start', 'ASC'); 582 if ($this->type === self::TYPE_SERVICE) { 583 $downtimes 584 ->where('service_host_name', $this->host_name) 585 ->where('service_description', $this->service_description); 586 } else { 587 $downtimes 588 ->where('host_name', $this->host_name); 589 } 590 $this->downtimes = $downtimes->getQuery()->fetchAll(); 591 return $this; 592 } 593 594 /** 595 * Fetch the object's event history 596 * 597 * @return $this 598 */ 599 public function fetchEventhistory() 600 { 601 $eventHistory = $this->backend 602 ->select() 603 ->from( 604 'eventhistory', 605 array( 606 'id', 607 'object_type', 608 'host_name', 609 'host_display_name', 610 'service_description', 611 'service_display_name', 612 'timestamp', 613 'state', 614 'output', 615 'type' 616 ) 617 ) 618 ->where('object_type', $this->type) 619 ->where('host_name', $this->host_name); 620 621 if ($this->type === self::TYPE_SERVICE) { 622 $eventHistory->where('service_description', $this->service_description); 623 } 624 625 $this->eventhistory = $eventHistory; 626 return $this; 627 } 628 629 /** 630 * Fetch the object's host groups 631 * 632 * @return $this 633 */ 634 public function fetchHostgroups() 635 { 636 $this->hostgroups = $this->backend->select() 637 ->from('hostgroup', array('hostgroup_name', 'hostgroup_alias')) 638 ->where('host_name', $this->host_name) 639 ->applyFilter($this->getFilter()) 640 ->fetchPairs(); 641 return $this; 642 } 643 644 /** 645 * Fetch the object's service groups 646 * 647 * @return $this 648 */ 649 public function fetchServicegroups() 650 { 651 $query = $this->backend->select() 652 ->from('servicegroup', array('servicegroup_name', 'servicegroup_alias')) 653 ->where('host_name', $this->host_name); 654 655 if ($this->type === self::TYPE_SERVICE) { 656 $query->where('service_description', $this->service_description); 657 } 658 659 $this->servicegroups = $query->applyFilter($this->getFilter())->fetchPairs(); 660 return $this; 661 } 662 663 /** 664 * Fetch stats 665 * 666 * @return $this 667 */ 668 public function fetchStats() 669 { 670 $this->stats = $this->backend->select()->from('servicestatussummary', array( 671 'services_total', 672 'services_ok', 673 'services_critical', 674 'services_critical_unhandled', 675 'services_critical_handled', 676 'services_warning', 677 'services_warning_unhandled', 678 'services_warning_handled', 679 'services_unknown', 680 'services_unknown_unhandled', 681 'services_unknown_handled', 682 'services_pending', 683 )) 684 ->where('service_host_name', $this->host_name) 685 ->applyFilter($this->getFilter()) 686 ->fetchRow(); 687 return $this; 688 } 689 690 /** 691 * Get all action urls configured for this monitored object 692 * 693 * @return array All note urls as a string 694 */ 695 public function getActionUrls() 696 { 697 return $this->resolveAllStrings( 698 MonitoredObject::parseAttributeUrls($this->action_url) 699 ); 700 } 701 702 /** 703 * Get the type of the object 704 * 705 * @param bool $translate 706 * 707 * @return string 708 */ 709 public function getType($translate = false) 710 { 711 if ($translate !== false) { 712 switch ($this->type) { 713 case self::TYPE_HOST: 714 $type = mt('montiroing', 'host'); 715 break; 716 case self::TYPE_SERVICE: 717 $type = mt('monitoring', 'service'); 718 break; 719 default: 720 throw new InvalidArgumentException('Invalid type ' . $this->type); 721 } 722 } else { 723 $type = $this->type; 724 } 725 return $type; 726 } 727 728 /** 729 * Parse the content of the action_url or notes_url attributes 730 * 731 * Find all occurences of http links, separated by whitespaces and quoted 732 * by single or double-ticks. 733 * 734 * @link http://docs.icinga.com/latest/de/objectdefinitions.html 735 * 736 * @param string $urlString A string containing one or more urls 737 * @return array Array of urls as strings 738 */ 739 public static function parseAttributeUrls($urlString) 740 { 741 if (empty($urlString)) { 742 return array(); 743 } 744 $links = array(); 745 if (strpos($urlString, "' ") === false) { 746 $links[] = $urlString; 747 } else { 748 // parse notes-url format 749 foreach (explode("' ", $urlString) as $url) { 750 $url = strpos($url, "'") === 0 ? substr($url, 1) : $url; 751 $url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url; 752 $links[] = $url; 753 } 754 } 755 return $links; 756 } 757 758 /** 759 * Fetch all available data of the object 760 * 761 * @return $this 762 */ 763 public function populate() 764 { 765 $this 766 ->fetchComments() 767 ->fetchContactgroups() 768 ->fetchContacts() 769 ->fetchCustomvars() 770 ->fetchDowntimes(); 771 772 // Call fetchHostgroups or fetchServicegroups depending on the object's type 773 $fetchGroups = 'fetch' . ucfirst($this->type) . 'groups'; 774 $this->$fetchGroups(); 775 776 return $this; 777 } 778 779 /** 780 * Resolve macros in all given strings in the current object context 781 * 782 * @param array $strs An array of urls as string 783 * 784 * @return array 785 */ 786 protected function resolveAllStrings(array $strs) 787 { 788 foreach ($strs as $i => $str) { 789 $strs[$i] = Macro::resolveMacros($str, $this); 790 } 791 return $strs; 792 } 793 794 /** 795 * Set the object's properties 796 * 797 * @param object $properties 798 * 799 * @return $this 800 */ 801 public function setProperties($properties) 802 { 803 $this->properties = (object) $properties; 804 return $this; 805 } 806 807 public function __isset($name) 808 { 809 if (property_exists($this->properties, $name)) { 810 return isset($this->properties->$name); 811 } elseif (property_exists($this, $name)) { 812 return isset($this->$name); 813 } 814 return false; 815 } 816 817 public function __get($name) 818 { 819 if (property_exists($this->properties, $name)) { 820 return $this->properties->$name; 821 } elseif (property_exists($this, $name)) { 822 if ($this->$name === null) { 823 $fetchMethod = 'fetch' . ucfirst($name); 824 $this->$fetchMethod(); 825 } 826 827 return $this->$name; 828 } elseif (preg_match('/^_(host|service)_(.+)/i', $name, $matches)) { 829 if (strtolower($matches[1]) === static::TYPE_HOST) { 830 if ($this->hostVariables === null) { 831 $this->fetchHostVariables(); 832 } 833 834 $customvars = $this->hostVariables; 835 } else { 836 if ($this->serviceVariables === null) { 837 $this->fetchServiceVariables(); 838 } 839 840 $customvars = $this->serviceVariables; 841 } 842 843 $variableName = strtolower($matches[2]); 844 if (isset($customvars[$variableName])) { 845 return $customvars[$variableName]; 846 } 847 848 return null; // Unknown custom variables MUST NOT throw an error 849 } elseif (in_array($name, array('contact_name', 'contactgroup_name', 'hostgroup_name', 'servicegroup_name'))) { 850 if ($name === 'contact_name') { 851 if ($this->contacts === null) { 852 $this->fetchContacts(); 853 } 854 855 return array_map(function ($el) { 856 return $el->contact_name; 857 }, $this->contacts); 858 } elseif ($name === 'contactgroup_name') { 859 if ($this->contactgroups === null) { 860 $this->fetchContactgroups(); 861 } 862 863 return array_map(function ($el) { 864 return $el->contactgroup_name; 865 }, $this->contactgroups); 866 } elseif ($name === 'hostgroup_name') { 867 if ($this->hostgroups === null) { 868 $this->fetchHostgroups(); 869 } 870 871 return array_keys($this->hostgroups); 872 } else { // $name === 'servicegroup_name' 873 if ($this->servicegroups === null) { 874 $this->fetchServicegroups(); 875 } 876 877 return array_keys($this->servicegroups); 878 } 879 } elseif (strpos($name, $this->prefix) !== 0) { 880 $propertyName = strtolower($name); 881 $prefixedName = $this->prefix . $propertyName; 882 if (property_exists($this->properties, $prefixedName)) { 883 return $this->properties->$prefixedName; 884 } 885 886 if ($this->type === static::TYPE_HOST) { 887 if ($this->hostVariables === null) { 888 $this->fetchHostVariables(); 889 } 890 891 $customvars = $this->hostVariables; 892 } else { // $this->type === static::TYPE_SERVICE 893 if ($this->serviceVariables === null) { 894 $this->fetchServiceVariables(); 895 } 896 897 $customvars = $this->serviceVariables; 898 } 899 900 if (isset($customvars[$propertyName])) { 901 return $customvars[$propertyName]; 902 } 903 } 904 905 throw new InvalidPropertyException('Can\'t access property \'%s\'. Property does not exist.', $name); 906 } 907 908 /** 909 * @deprecated 910 */ 911 public static function fromParams(UrlParams $params) 912 { 913 if ($params->has('service') && $params->has('host')) { 914 return new Service(MonitoringBackend::instance(), $params->get('host'), $params->get('service')); 915 } elseif ($params->has('host')) { 916 return new Host(MonitoringBackend::instance(), $params->get('host')); 917 } 918 return null; 919 } 920} 921