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