1<?php
2
3namespace Tests\Icinga\Module\Director\Objects;
4
5use Icinga\Module\Director\Data\PropertiesFilter\ArrayCustomVariablesFilter;
6use Icinga\Module\Director\Data\PropertiesFilter\CustomVariablesFilter;
7use Icinga\Module\Director\IcingaConfig\IcingaConfig;
8use Icinga\Module\Director\Objects\DirectorDatafield;
9use Icinga\Module\Director\Objects\IcingaHost;
10use Icinga\Module\Director\Objects\IcingaHostGroup;
11use Icinga\Module\Director\Objects\IcingaZone;
12use Icinga\Module\Director\Test\BaseTestCase;
13use Icinga\Exception\IcingaException;
14
15class IcingaHostTest extends BaseTestCase
16{
17    protected $testHostName = '___TEST___host';
18    protected $testDatafieldName = 'test5';
19
20    public function testPropertiesCanBeSet()
21    {
22        $host = $this->host();
23        $host->display_name = 'Something else';
24        $this->assertEquals(
25            $host->display_name,
26            'Something else'
27        );
28    }
29
30    public function testCanBeReplaced()
31    {
32        $host = $this->host();
33        $newHost = IcingaHost::create(
34            array('display_name' => 'Replaced display'),
35            $this->getDb()
36        );
37
38        $this->assertEquals(
39            count($host->vars()),
40            4
41        );
42        $this->assertEquals(
43            $host->address,
44            '127.0.0.127'
45        );
46
47        $host->replaceWith($newHost);
48        $this->assertEquals(
49            $host->display_name,
50            'Replaced display'
51        );
52        $this->assertEquals(
53            $host->address,
54            null
55        );
56
57        $this->assertEquals(
58            count($host->vars()),
59            0
60        );
61    }
62
63    public function testCanBeMerged()
64    {
65        $host = $this->host();
66        $newHost = IcingaHost::create(
67            array('display_name' => 'Replaced display'),
68            $this->getDb()
69        );
70
71        $this->assertEquals(
72            count($host->vars()),
73            4
74        );
75        $this->assertEquals(
76            $host->address,
77            '127.0.0.127'
78        );
79
80        $host->merge($newHost);
81        $this->assertEquals(
82            $host->display_name,
83            'Replaced display'
84        );
85        $this->assertEquals(
86            $host->address,
87            '127.0.0.127'
88        );
89        $this->assertEquals(
90            count($host->vars()),
91            4
92        );
93    }
94
95    public function testPropertiesCanBePreservedWhenBeingReplaced()
96    {
97        if ($this->skipForMissingDb()) {
98            return;
99        }
100
101        $db = $this->getDb();
102        $this->host()->store($db);
103        $host = IcingaHost::load($this->testHostName, $db);
104
105        $newHost = IcingaHost::create(
106            array(
107                'display_name'  => 'Replaced display',
108                'address'       => '1.2.2.3',
109                'vars'          => array(
110                    'test1'     => 'newstring',
111                    'test2'     => 18,
112                    'initially' => 'set and then preserved',
113                )
114            ),
115            $this->getDb()
116        );
117
118        $preserve = array('address', 'vars.test1', 'vars.initially');
119        $host->replaceWith($newHost, $preserve);
120        $this->assertEquals(
121            $host->address,
122            '127.0.0.127'
123        );
124
125        $this->assertEquals(
126            $host->{'vars.test2'},
127            18
128        );
129
130        $this->assertEquals(
131            $host->vars()->test2->getValue(),
132            18
133        );
134
135        $this->assertEquals(
136            $host->{'vars.initially'},
137            'set and then preserved'
138        );
139
140        $this->assertFalse(
141            array_key_exists('address', $host->getModifiedProperties()),
142            'Preserved property stays unmodified'
143        );
144
145        $newHost->set('vars.initially', 'changed later on');
146        $newHost->set('vars.test2', 19);
147
148        $host->replaceWith($newHost, $preserve);
149        $this->assertEquals(
150            $host->{'vars.initially'},
151            'set and then preserved'
152        );
153
154        $this->assertEquals(
155            $host->get('vars.test2'),
156            19
157        );
158
159
160        $host->delete();
161    }
162
163    public function testDistinctCustomVarsCanBeSetWithoutSideEffects()
164    {
165        $host = $this->host();
166        $host->set('vars.test2', 18);
167        $this->assertEquals(
168            $host->vars()->test1->getValue(),
169            'string'
170        );
171        $this->assertEquals(
172            $host->vars()->test2->getValue(),
173            18
174        );
175        $this->assertEquals(
176            $host->vars()->test3->getValue(),
177            false
178        );
179    }
180
181    public function testVarsArePersisted()
182    {
183        if ($this->skipForMissingDb()) {
184            return;
185        }
186
187        $db = $this->getDb();
188        $this->host()->store($db);
189        $host = IcingaHost::load($this->testHostName, $db);
190        $this->assertEquals(
191            $host->vars()->test1->getValue(),
192            'string'
193        );
194        $this->assertEquals(
195            $host->vars()->test2->getValue(),
196            17
197        );
198        $this->assertEquals(
199            $host->vars()->test3->getValue(),
200            false
201        );
202        $this->assertEquals(
203            $host->vars()->test4->getValue(),
204            (object) array(
205                'this' => 'is',
206                'a' => array(
207                    'dict',
208                    'ionary'
209                )
210            )
211        );
212    }
213
214    public function testRendersCorrectly()
215    {
216        $this->assertEquals(
217            (string) $this->host(),
218            $this->loadRendered('host1')
219        );
220    }
221
222    public function testGivesPlainObjectWithInvalidUnresolvedDependencies()
223    {
224        $props = $this->getDummyRelatedProperties();
225
226        $host = $this->host();
227        foreach ($props as $k => $v) {
228            $host->$k = $v;
229        }
230
231        $plain = $host->toPlainObject();
232        foreach ($props as $k => $v) {
233            $this->assertEquals($plain->$k, $v);
234        }
235    }
236
237    public function testCorrectlyStoresLazyRelations()
238    {
239        if ($this->skipForMissingDb()) {
240            return;
241        }
242        $db = $this->getDb();
243        $host = $this->host();
244        $host->zone = '___TEST___zone';
245        $this->assertEquals(
246            '___TEST___zone',
247            $host->zone
248        );
249
250        $zone = $this->newObject('zone', '___TEST___zone');
251        $zone->store($db);
252
253        $host->store($db);
254        $host->delete();
255        $zone->delete();
256    }
257
258    /**
259     * @expectedException \RuntimeException
260     */
261    public function testFailsToStoreWithMissingLazyRelations()
262    {
263        if ($this->skipForMissingDb()) {
264            return;
265        }
266        $db = $this->getDb();
267        $host = $this->host();
268        $host->zone = '___TEST___zone';
269        $host->store($db);
270    }
271
272    public function testHandlesUnmodifiedProperties()
273    {
274        $this->markTestSkipped('Currently broken, needs to be fixed');
275
276        if ($this->skipForMissingDb()) {
277            return;
278        }
279
280        $db = $this->getDb();
281        $host = $this->host();
282        $host->store($db);
283
284        $parent = $this->newObject('host', '___TEST___parent');
285        $parent->store($db);
286        $host->imports = '___TEST___parent';
287
288        $host->store($db);
289
290        $plain = $host->getPlainUnmodifiedObject();
291        $this->assertEquals(
292            'string',
293            $plain->vars->test1
294        );
295        $host->vars()->set('test1', 'nada');
296
297        $host->store();
298
299        $plain = $host->getPlainUnmodifiedObject();
300        $this->assertEquals(
301            'nada',
302            $plain->vars->test1
303        );
304
305        $host->vars()->set('test1', 'string');
306        $plain = $host->getPlainUnmodifiedObject();
307        $this->assertEquals(
308            'nada',
309            $plain->vars->test1
310        );
311
312        $plain = $host->getPlainUnmodifiedObject();
313        $test = IcingaHost::create((array) $plain);
314
315        $this->assertEquals(
316            $this->loadRendered('host3'),
317            (string) $test
318        );
319
320        $host->delete();
321        $parent->delete();
322    }
323
324    public function testRendersWithInvalidUnresolvedDependencies()
325    {
326        $newHost = $this->host();
327        $newHost->zone             = 'invalid';
328        $newHost->check_command    = 'unknown';
329        $newHost->event_command    = 'What event?';
330        $newHost->check_period     = 'Not time is a good time @ nite';
331        $newHost->command_endpoint = 'nirvana';
332
333        $this->assertEquals(
334            (string) $newHost,
335            $this->loadRendered('host2')
336        );
337    }
338
339    /**
340     * @expectedException \RuntimeException
341     */
342    public function testFailsToStoreWithInvalidUnresolvedDependencies()
343    {
344        if ($this->skipForMissingDb()) {
345            return;
346        }
347
348        $host = $this->host();
349        $host->zone = 'invalid';
350        $host->store($this->getDb());
351    }
352
353    public function testRendersToTheCorrectZone()
354    {
355        if ($this->skipForMissingDb()) {
356            return;
357        }
358
359        $db = $this->getDb();
360        $host = $this->host()->setConnection($db);
361        $masterzone = $db->getMasterZoneName();
362
363        $config = new IcingaConfig($db);
364        $host->renderToConfig($config);
365        $this->assertEquals(
366            array('zones.d/' . $masterzone . '/hosts.conf'),
367            $config->getFileNames()
368        );
369
370        $zone = $this->newObject('zone', '___TEST___zone');
371        $zone->store($db);
372
373        $config = new IcingaConfig($db);
374        $host->zone = '___TEST___zone';
375        $host->renderToConfig($config);
376        $this->assertEquals(
377            array('zones.d/___TEST___zone/hosts.conf'),
378            $config->getFileNames()
379        );
380
381        $host->has_agent = true;
382        $host->master_should_connect = true;
383        $host->accept_config = true;
384
385        $config = new IcingaConfig($db);
386        $host->renderToConfig($config);
387        $this->assertEquals(
388            array(
389                'zones.d/___TEST___zone/hosts.conf',
390                'zones.d/___TEST___zone/agent_endpoints.conf',
391                'zones.d/___TEST___zone/agent_zones.conf'
392            ),
393            $config->getFileNames()
394        );
395
396        $host->object_type = 'template';
397        $host->zone_id = null;
398
399        $config = new IcingaConfig($db);
400        $host->renderToConfig($config);
401        $this->assertEquals(
402            array('zones.d/director-global/host_templates.conf'),
403            $config->getFileNames()
404        );
405    }
406
407    public function testWhetherTwoHostsCannotBeStoredWithTheSameApiKey()
408    {
409        if ($this->skipForMissingDb()) {
410            return;
411        }
412
413        $db = $this->getDb();
414        $a = IcingaHost::create(array(
415            'object_name' => '___TEST___a',
416            'object_type' => 'object',
417            'api_key' => 'a'
418        ), $db);
419        $b = IcingaHost::create(array(
420            'object_name' => '___TEST___b',
421            'object_type' => 'object',
422            'api_key' => 'a'
423        ), $db);
424
425        $a->store();
426        try {
427            $b->store();
428        } catch (\RuntimeException $e) {
429            $msg = $e->getMessage();
430            $matchMysql = strpos(
431                $msg,
432                "Duplicate entry 'a' for key 'api_key'"
433            ) !== false;
434
435            $matchPostgres = strpos(
436                $msg,
437                'Unique violation'
438            ) !== false;
439
440            $this->assertTrue(
441                $matchMysql || $matchPostgres,
442                'Exception message does not tell about unique constraint violation'
443            );
444            $a->delete();
445        }
446    }
447
448    public function testWhetherHostCanBeLoadedWithValidApiKey()
449    {
450        if ($this->skipForMissingDb()) {
451            return;
452        }
453
454        $db = $this->getDb();
455        $a = IcingaHost::create(array(
456            'object_name' => '___TEST___a',
457            'object_type' => 'object',
458            'api_key' => 'a1a1a1'
459        ), $db);
460        $b = IcingaHost::create(array(
461            'object_name' => '___TEST___b',
462            'object_type' => 'object',
463            'api_key' => 'b1b1b1'
464        ), $db);
465        $a->store();
466        $b->store();
467
468        $this->assertEquals(
469            IcingaHost::loadWithApiKey('b1b1b1', $db)->object_name,
470            '___TEST___b'
471        );
472
473        $a->delete();
474        $b->delete();
475    }
476
477    /**
478     * @expectedException \Icinga\Exception\NotFoundError
479     */
480    public function testWhetherInvalidApiKeyThrows404()
481    {
482        if ($this->skipForMissingDb()) {
483            return;
484        }
485
486        $db = $this->getDb();
487        IcingaHost::loadWithApiKey('No___such___key', $db);
488    }
489
490    public function testEnumProperties()
491    {
492        if ($this->skipForMissingDb()) {
493            return;
494        }
495
496        $db = $this->getDb();
497        $properties = IcingaHost::enumProperties($db);
498
499        $this->assertEquals(
500            array(
501                'Host properties' => $this->getDefaultHostProperties()
502            ),
503            $properties
504        );
505    }
506
507    public function testEnumPropertiesWithCustomVars()
508    {
509        if ($this->skipForMissingDb()) {
510            return;
511        }
512
513        $db = $this->getDb();
514
515        $host = $this->host();
516        $host->store($db);
517
518        $properties = IcingaHost::enumProperties($db);
519        $this->assertEquals(
520            array(
521                'Host properties' => $this->getDefaultHostProperties(),
522                'Custom variables' => array(
523                    'vars.test1' => 'test1',
524                    'vars.test2' => 'test2',
525                    'vars.test3' => 'test3',
526                    'vars.test4' => 'test4'
527                )
528            ),
529            $properties
530        );
531    }
532
533    public function testEnumPropertiesWithPrefix()
534    {
535        if ($this->skipForMissingDb()) {
536            return;
537        }
538
539        $db = $this->getDb();
540
541        $host = $this->host();
542        $host->store($db);
543
544        $properties = IcingaHost::enumProperties($db, 'host.');
545        $this->assertEquals(
546            array(
547                'Host properties' => $this->getDefaultHostProperties('host.'),
548                'Custom variables' => array(
549                    'host.vars.test1' => 'test1',
550                    'host.vars.test2' => 'test2',
551                    'host.vars.test3' => 'test3',
552                    'host.vars.test4' => 'test4'
553                )
554            ),
555            $properties
556        );
557    }
558
559    public function testEnumPropertiesWithFilter()
560    {
561        if ($this->skipForMissingDb()) {
562            return;
563        }
564
565        $db = $this->getDb();
566
567        DirectorDatafield::create(array(
568            'varname'       => $this->testDatafieldName,
569            'caption'       => 'Blah',
570            'description'   => '',
571            'datatype'      => 'Icinga\Module\Director\DataType\DataTypeArray',
572            'format'        => 'json'
573        ))->store($db);
574
575        $host = $this->host();
576        $host->{'vars.test5'} = array('a', '1');
577        $host->store($db);
578
579        $properties = IcingaHost::enumProperties($db, '', new CustomVariablesFilter());
580        $this->assertEquals(
581            array(
582                'Custom variables' => array(
583                    'vars.test1' => 'test1',
584                    'vars.test2' => 'test2',
585                    'vars.test3' => 'test3',
586                    'vars.test4' => 'test4',
587                    'vars.test5' => 'test5 (Blah)'
588                )
589            ),
590            $properties
591        );
592    }
593
594    public function testEnumPropertiesWithArrayFilter()
595    {
596        if ($this->skipForMissingDb()) {
597            return;
598        }
599
600        $db = $this->getDb();
601
602        DirectorDatafield::create(array(
603            'varname'       => $this->testDatafieldName,
604            'caption'       => 'Blah',
605            'description'   => '',
606            'datatype'      => 'Icinga\Module\Director\DataType\DataTypeArray',
607            'format'        => 'json'
608        ))->store($db);
609
610        $host = $this->host();
611        $host->{'vars.test5'} = array('a', '1');
612        $host->store($db);
613
614        $properties = IcingaHost::enumProperties($db, '', new ArrayCustomVariablesFilter());
615        $this->assertEquals(
616            array(
617                'Custom variables' => array(
618                    'vars.test5' => 'test5 (Blah)'
619                )
620            ),
621            $properties
622        );
623    }
624
625    public function testMergingObjectKeepsGroupsIfNotGiven()
626    {
627        $one = IcingaHostGroup::create([
628            'object_name' => 'one',
629            'object_type' => 'object',
630        ]);
631        $two = IcingaHostGroup::create([
632            'object_name' => 'two',
633            'object_type' => 'object',
634        ]);
635        $a = IcingaHost::create([
636            'object_name' => 'one',
637            'object_type' => 'object',
638            'imports'     => [],
639            'address'     => '127.0.0.2',
640            'groups'      => [$one, $two]
641        ]);
642
643        $b = IcingaHost::create([
644            'object_name' => 'one',
645            'object_type' => 'object',
646            'imports'     => [],
647            'address'     => '127.0.0.42',
648        ]);
649
650        $a->merge($b);
651        $this->assertEquals(
652            '127.0.0.42',
653            $a->get('address')
654        );
655        $this->assertEquals(
656            ['one', 'two'],
657            $a->getGroups()
658        );
659    }
660
661    protected function getDummyRelatedProperties()
662    {
663        return array(
664            'zone'             => 'invalid',
665            'check_command'    => 'unknown',
666            'event_command'    => 'What event?',
667            'check_period'     => 'Not time is a good time @ nite',
668            'command_endpoint' => 'nirvana',
669        );
670    }
671
672    protected function host()
673    {
674        return IcingaHost::create(array(
675            'object_name'  => $this->testHostName,
676            'object_type'  => 'object',
677            'address'      => '127.0.0.127',
678            'display_name' => 'Whatever',
679            'vars'         => array(
680                'test1' => 'string',
681                'test2' => 17,
682                'test3' => false,
683                'test4' => (object) array(
684                    'this' => 'is',
685                    'a' => array(
686                        'dict',
687                        'ionary'
688                    )
689                )
690            )
691        ), $this->getDb());
692    }
693
694    protected function getDefaultHostProperties($prefix = '')
695    {
696        return array(
697            "${prefix}name" => "name",
698            "${prefix}action_url" => "action_url",
699            "${prefix}address" => "address",
700            "${prefix}address6" => "address6",
701            "${prefix}api_key" => "api_key",
702            "${prefix}check_command" => "check_command",
703            "${prefix}check_interval" => "check_interval",
704            "${prefix}check_period" => "check_period",
705            "${prefix}check_timeout" => "check_timeout",
706            "${prefix}command_endpoint" => "command_endpoint",
707            "${prefix}display_name" => "display_name",
708            "${prefix}enable_active_checks" => "enable_active_checks",
709            "${prefix}enable_event_handler" => "enable_event_handler",
710            "${prefix}enable_flapping" => "enable_flapping",
711            "${prefix}enable_notifications" => "enable_notifications",
712            "${prefix}enable_passive_checks" => "enable_passive_checks",
713            "${prefix}enable_perfdata" => "enable_perfdata",
714            "${prefix}event_command" => "event_command",
715            "${prefix}flapping_threshold_high" => "flapping_threshold_high",
716            "${prefix}flapping_threshold_low" => "flapping_threshold_low",
717            "${prefix}icon_image" => "icon_image",
718            "${prefix}icon_image_alt" => "icon_image_alt",
719            "${prefix}max_check_attempts" => "max_check_attempts",
720            "${prefix}notes" => "notes",
721            "${prefix}notes_url" => "notes_url",
722            "${prefix}retry_interval" => "retry_interval",
723            "${prefix}volatile" => "volatile",
724            "${prefix}zone" => "zone",
725            "${prefix}groups" => "Groups",
726            "${prefix}templates" => "templates"
727        );
728    }
729    protected function loadRendered($name)
730    {
731        return file_get_contents(__DIR__ . '/rendered/' . $name . '.out');
732    }
733
734    public function tearDown()
735    {
736        if ($this->hasDb()) {
737            $db = $this->getDb();
738            $kill = array($this->testHostName, '___TEST___parent', '___TEST___a', '___TEST___b');
739            foreach ($kill as $name) {
740                if (IcingaHost::exists($name, $db)) {
741                    IcingaHost::load($name, $db)->delete();
742                }
743            }
744
745            $kill = array('___TEST___zone');
746            foreach ($kill as $name) {
747                if (IcingaZone::exists($name, $db)) {
748                    IcingaZone::load($name, $db)->delete();
749                }
750            }
751
752            $this->deleteDatafields();
753        }
754    }
755
756    protected function deleteDatafields()
757    {
758        $db = $this->getDb();
759        $dbAdapter = $db->getDbAdapter();
760        $kill = array($this->testDatafieldName);
761
762        foreach ($kill as $name) {
763            $query = $dbAdapter->select()
764                ->from('director_datafield')
765                ->where('varname = ?', $name);
766            foreach (DirectorDatafield::loadAll($db, $query, 'id') as $datafield) {
767                $datafield->delete();
768            }
769        }
770    }
771}
772