1<?php
2/*
3** Zabbix
4** Copyright (C) 2001-2021 Zabbix SIA
5**
6** This program is free software; you can redistribute it and/or modify
7** it under the terms of the GNU General Public License as published by
8** the Free Software Foundation; either version 2 of the License, or
9** (at your option) any later version.
10**
11** This program is distributed in the hope that it will be useful,
12** but WITHOUT ANY WARRANTY; without even the implied warranty of
13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14** GNU General Public License for more details.
15**
16** You should have received a copy of the GNU General Public License
17** along with this program; if not, write to the Free Software
18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19**/
20
21
22require_once dirname(__FILE__).'/include/config.inc.php';
23require_once dirname(__FILE__).'/include/hosts.inc.php';
24
25$page['title'] = _('Availability report');
26$page['file'] = 'report2.php';
27$page['scripts'] = ['class.calendar.js', 'gtlc.js', 'multiselect.js', 'report2.js'];
28$page['type'] = detect_page_type(PAGE_TYPE_HTML);
29
30require_once dirname(__FILE__).'/include/page_header.php';
31
32// VAR	TYPE	OPTIONAL	FLAGS	VALIDATION	EXCEPTION
33$fields = [
34	'mode' =>				[T_ZBX_INT,			O_OPT,	P_SYS,			IN(implode(',', [AVAILABILITY_REPORT_BY_HOST, AVAILABILITY_REPORT_BY_TEMPLATE])),	null],
35	'hostgroupid' =>		[T_ZBX_INT,			O_OPT,	P_SYS,			DB_ID,		null],
36	'tpl_triggerid' =>		[T_ZBX_INT,			O_OPT,	P_SYS,			DB_ID,		null],
37	'triggerid' =>			[T_ZBX_INT,			O_OPT,	P_SYS|P_NZERO,	DB_ID,		null],
38	// filter
39	'filter_groups' =>		[T_ZBX_INT,			O_OPT,	P_SYS,			DB_ID,		null],
40	'filter_hostids' =>		[T_ZBX_INT,			O_OPT,	P_SYS,			DB_ID,		null],
41	'filter_templateid' =>	[T_ZBX_INT,			O_OPT,	P_SYS,			DB_ID,		null],
42	'filter_rst'=>			[T_ZBX_STR,			O_OPT,	P_SYS,			null,		null],
43	'filter_set' =>			[T_ZBX_STR,			O_OPT,	P_SYS,			null,		null],
44	'from' =>				[T_ZBX_RANGE_TIME,	O_OPT,	P_SYS,			null,		null],
45	'to' =>					[T_ZBX_RANGE_TIME,	O_OPT,	P_SYS,			null,		null]
46];
47check_fields($fields);
48validateTimeSelectorPeriod(getRequest('from'), getRequest('to'));
49
50$report_mode = getRequest('mode', CProfile::get('web.avail_report.mode', AVAILABILITY_REPORT_BY_HOST));
51CProfile::update('web.avail_report.mode', $report_mode, PROFILE_TYPE_INT);
52
53/*
54 * Permissions
55 */
56if ($report_mode == AVAILABILITY_REPORT_BY_TEMPLATE) {
57	if (getRequest('hostgroupid') && !isReadableHostGroups([getRequest('hostgroupid')])) {
58		access_deny();
59	}
60	if (getRequest('filter_groups') && !isReadableHostGroups([getRequest('filter_groups')])) {
61		access_deny();
62	}
63	if (getRequest('filter_templateid') && !isReadableTemplates([getRequest('filter_templateid')])) {
64		access_deny();
65	}
66	if (getRequest('tpl_triggerid')) {
67		$trigger = API::Trigger()->get([
68			'triggerids' => $_REQUEST['tpl_triggerid'],
69			'output' => ['triggerid'],
70			'filter' => ['flags' => null]
71		]);
72		if (!$trigger) {
73			access_deny();
74		}
75	}
76}
77else {
78	if (getRequest('filter_groupid') && !isReadableHostGroups([getRequest('filter_groupid')])) {
79		access_deny();
80	}
81	if (getRequest('filter_hostid') && !isReadableHosts([getRequest('filter_hostid')])) {
82		access_deny();
83	}
84}
85if (getRequest('triggerid') && !isReadableTriggers([getRequest('triggerid')])) {
86	access_deny();
87}
88
89/*
90 * Filter
91 */
92$key_prefix = 'web.avail_report.'.$report_mode;
93
94if (hasRequest('filter_set')) {
95	if ($report_mode == AVAILABILITY_REPORT_BY_TEMPLATE) {
96		CProfile::update($key_prefix.'.groupid', getRequest('filter_groups', 0), PROFILE_TYPE_ID);
97		CProfile::update($key_prefix.'.hostid', getRequest('filter_templateid', 0), PROFILE_TYPE_ID);
98		CProfile::update($key_prefix.'.tpl_triggerid', getRequest('tpl_triggerid', 0), PROFILE_TYPE_ID);
99		CProfile::update($key_prefix.'.hostgroupid', getRequest('hostgroupid', 0), PROFILE_TYPE_ID);
100	}
101	else {
102		CProfile::updateArray($key_prefix.'.groupids', getRequest('filter_groups', []), PROFILE_TYPE_ID);
103		CProfile::updateArray($key_prefix.'.hostids', getRequest('filter_hostids', []), PROFILE_TYPE_ID);
104	}
105}
106elseif (hasRequest('filter_rst')) {
107	if ($report_mode == AVAILABILITY_REPORT_BY_TEMPLATE) {
108		CProfile::delete($key_prefix.'.groupid');
109		CProfile::delete($key_prefix.'.hostid');
110		CProfile::delete($key_prefix.'.tpl_triggerid');
111		CProfile::delete($key_prefix.'.hostgroupid');
112	}
113	else {
114		CProfile::deleteIdx($key_prefix.'.groupids');
115		CProfile::deleteIdx($key_prefix.'.hostids');
116	}
117}
118
119// Get filter values.
120$data['filter'] = ($report_mode == AVAILABILITY_REPORT_BY_TEMPLATE)
121	? [
122		// 'Template group' field.
123		'groups' => getRequest('filter_groups', CProfile::get($key_prefix.'.groupid', 0)),
124		// 'Template' field.
125		'hostids' => getRequest('filter_templateid', CProfile::get($key_prefix.'.hostid', 0)),
126		// 'Template trigger' field.
127		'tpl_triggerid' => getRequest('tpl_triggerid', CProfile::get($key_prefix.'.tpl_triggerid', 0)),
128		// 'Host group' field.
129		'hostgroupid' => getRequest('hostgroupid', CProfile::get($key_prefix.'.hostgroupid', 0))
130	]
131	: [
132		// 'Host groups' field.
133		'groups' => CProfile::getArray($key_prefix.'.groupids', getRequest('filter_groups', [])),
134		// 'Hosts' field.
135		'hostids' => CProfile::getArray($key_prefix.'.hostids', getRequest('filter_hostids', []))
136	];
137
138// Get time selector filter values.
139$timeselector_options = [
140	'profileIdx' => 'web.avail_report.filter',
141	'profileIdx2' => 0,
142	'from' => getRequest('from'),
143	'to' => getRequest('to')
144];
145updateTimeSelectorPeriod($timeselector_options);
146
147$config = select_config();
148
149/*
150 * Header
151 */
152$triggerData = isset($_REQUEST['triggerid'])
153	? API::Trigger()->get([
154		'triggerids' => $_REQUEST['triggerid'],
155		'output' => API_OUTPUT_EXTEND,
156		'selectHosts' => API_OUTPUT_EXTEND,
157		'expandDescription' => true
158	])
159	: null;
160
161$reportWidget = (new CWidget())->setTitle(_('Availability report'));
162
163if ($triggerData) {
164	$triggerData = reset($triggerData);
165	$host = reset($triggerData['hosts']);
166
167	$triggerData['hostid'] = $host['hostid'];
168	$triggerData['hostname'] = $host['name'];
169
170	$reportWidget->setControls((new CTag('nav', true,
171		(new CList())
172			->addItem(new CLink($triggerData['hostname'], (new CUrl('report2.php'))
173				->setArgument('page', CPagerHelper::loadPage('report2.php', null))
174				->getUrl()
175			))
176			->addItem($triggerData['description'])
177		))->setAttribute('aria-label', _('Content controls'))
178	);
179
180	$table = (new CTableInfo())
181		->addRow(new CImg('chart4.php?triggerid='.$_REQUEST['triggerid']));
182
183	$reportWidget->addItem(BR())
184		->addItem($table)
185		->show();
186}
187else {
188	/**
189	 * Report list view (both data presentation modes).
190	 */
191
192	$select_mode = (new CSelect('mode'))
193		->setValue($report_mode)
194		->setFocusableElementId('mode')
195		->addOption(new CSelectOption(AVAILABILITY_REPORT_BY_HOST, _('By host')))
196		->addOption(new CSelectOption(AVAILABILITY_REPORT_BY_TEMPLATE, _('By trigger template')));
197
198	$reportWidget->setControls((new CForm('get'))
199		->cleanItems()
200		->setAttribute('aria-label', _('Main filter'))
201		->addItem((new CList())
202			->addItem([
203				new CLabel(_('Mode'), $select_mode->getFocusableElementId()),
204				(new CDiv())->addClass(ZBX_STYLE_FORM_INPUT_MARGIN),
205				$select_mode
206			])
207		)
208		->setName('report2')
209	);
210
211	/*
212	 * Filter
213	 */
214	$data['filter'] += [
215		'timeline' => getTimeSelectorPeriod($timeselector_options),
216		'active_tab' => CProfile::get('web.avail_report.filter.active', 1)
217	];
218
219	$filter_column = new CFormList();
220
221	// Make filter fields.
222	if ($report_mode == AVAILABILITY_REPORT_BY_TEMPLATE) {
223		// Sanitize $data['filter']['groups'] and prepare "Template group" select options.
224		$groups = API::HostGroup()->get([
225			'output' => ['name'],
226			'templated_hosts' => true,
227			'with_triggers' => true,
228			'preservekeys' => true
229		]);
230		$groups = enrichParentGroups($groups);
231		CArrayHelper::sort($groups, ['name']);
232
233		if (!array_key_exists($data['filter']['groups'], $groups)) {
234			$data['filter']['groups'] = 0;
235		}
236
237		// Sanitize $data['filter']['hostids'] and prepare "Template" select options.
238		$templates = API::Template()->get([
239			'output' => ['name'],
240			'groupids' => $data['filter']['groups'] ? [$data['filter']['groups']] : null,
241			'with_triggers' => true,
242			'preservekeys' => true
243		]);
244		CArrayHelper::sort($templates, ['name']);
245
246		if (!array_key_exists($data['filter']['hostids'], $templates)) {
247			$data['filter']['hostids'] = 0;
248		}
249
250		$select_filter_hostid = (new CSelect('filter_templateid'))
251			->setValue($data['filter']['hostids'])
252			->setFocusableElementId('filter-templateid')
253			->addOption(new CSelectOption(0, _('all')));
254
255		foreach ($templates as $templateid => $template) {
256			$select_filter_hostid->addOption(new CSelectOption($templateid, $template['name']));
257		}
258
259		// Sanitize $data['filter']['tpl_triggerid'] and prepare "Template Trigger" select options.
260		$triggers = API::Trigger()->get([
261			'output' => ['description'],
262			'selectHosts' => ['name'],
263			'selectItems' => ['status'],
264			'templateids' => $data['filter']['hostids']
265				? [$data['filter']['hostids']]
266				: null,
267			'groupids' => $data['filter']['groups']
268				? [$data['filter']['groups']]
269				: null,
270			'templated' => true,
271			'filter' => [
272				'status' => TRIGGER_STATUS_ENABLED,
273				'flags' => [ZBX_FLAG_DISCOVERY_NORMAL]
274			],
275			'sortfield' => 'description',
276			'preservekeys' => true
277		]);
278
279		foreach ($triggers as $triggerid => $trigger) {
280			foreach ($trigger['items'] as $item) {
281				if ($item['status'] != ITEM_STATUS_ACTIVE) {
282					unset($triggers[$triggerid]);
283
284					break;
285				}
286			}
287		}
288
289		if (!array_key_exists($data['filter']['tpl_triggerid'], $triggers)) {
290			$data['filter']['tpl_triggerid'] = 0;
291		}
292
293		$select_tpl_triggerid = (new CSelect('tpl_triggerid'))
294			->setValue($data['filter']['tpl_triggerid'])
295			->setFocusableElementId('tpl-triggerid')
296			->addOption(new CSelectOption(0, _('all')));
297
298		$tpl_triggerids = [];
299
300		foreach ($triggers as $triggerid => $trigger) {
301			$label = (($data['filter']['hostids'] == 0) ? $trigger['hosts'][0]['name'].NAME_DELIMITER : '').$trigger['description'];
302			$select_tpl_triggerid->addOption(new CSelectOption($triggerid, $label));
303
304			$tpl_triggerids[$triggerid] = true;
305		}
306
307		// Sanitize $data['filter']['hostgroupid'] and prepare "Host Group" select options.
308		$host_groups = API::HostGroup()->get([
309			'output' => ['name'],
310			'monitored_hosts' => true,
311			'preservekeys' => true
312		]);
313		$host_groups = enrichParentGroups($host_groups);
314		CArrayHelper::sort($host_groups, ['name']);
315
316		if (!array_key_exists($data['filter']['hostgroupid'], $host_groups)) {
317			$data['filter']['hostgroupid'] = 0;
318		}
319
320		$select_hostgroupid = (new CSelect('hostgroupid'))
321			->setValue($data['filter']['hostgroupid'])
322			->setFocusableElementId('hostgroupid')
323			->addOption(new CSelectOption(0, _('all')));
324
325		foreach ($host_groups as $groupid => $group) {
326			$select_hostgroupid->addOption(new CSelectOption($groupid, $group['name']));
327		}
328
329		$hostgroupids = [];
330		if ($data['filter']['hostgroupid'] != 0) {
331			$hostgroupids[$data['filter']['hostgroupid']] = true;
332			$parent = $host_groups[$data['filter']['hostgroupid']]['name'].'/';
333
334			foreach ($host_groups as $groupid => $group) {
335				if (strpos($group['name'], $parent) === 0) {
336					$hostgroupids[$groupid] = true;
337				}
338			}
339		}
340
341		// Gather all templated triggers, originating from host templates, which belong to requested template groups.
342		$templated_triggers_all = ($data['filter']['tpl_triggerid'] == 0)
343			? $tpl_triggerids
344			: [$data['filter']['tpl_triggerid'] => true];
345		$templated_triggers_new = $templated_triggers_all;
346
347		while ($templated_triggers_new) {
348			$templated_triggers_new = API::Trigger()->get([
349				'output' => ['triggerid'],
350				'templated' => true,
351				'filter' => ['templateid' => array_keys($templated_triggers_new)],
352				'preservekeys' => true
353			]);
354			$templated_triggers_new = array_diff_key($templated_triggers_new, $templated_triggers_all);
355			$templated_triggers_all += $templated_triggers_new;
356		}
357
358		if ($templated_triggers_all) {
359			// Select monitored host triggers, derived from templates and belonging to the requested groups.
360			$triggers = API::Trigger()->get([
361				'output' => ['triggerid', 'description', 'expression', 'value'],
362				'selectHosts' => ['name'],
363				'expandDescription' => true,
364				'monitored' => true,
365				'groupids' => ($data['filter']['hostgroupid'] == 0) ? null : array_keys($hostgroupids),
366				'filter' => ['templateid' => array_keys($templated_triggers_all)],
367				'limit' => $config['search_limit'] + 1
368			]);
369		}
370		else {
371			// No trigger templates means there are no derived triggers.
372			$triggers = [];
373		}
374
375		$select_filter_groupid = (new CSelect('filter_groups'))
376			->setAttribute('autofocus', 'autofocus')
377			->setValue($data['filter']['groups'])
378			->setFocusableElementId('filter-groups')
379			->addOption(new CSelectOption(0, _('all')));
380
381		foreach ($groups as $groupid => $group) {
382			$select_filter_groupid->addOption(new CSelectOption($groupid, $group['name']));
383		}
384
385		$filter_column
386			->addRow(
387				new CLabel(_('Template group'), $select_filter_groupid->getFocusableElementId()),
388				$select_filter_groupid
389			)
390			->addRow(
391				new CLabel(_('Template'), $select_filter_hostid->getFocusableElementId()),
392				$select_filter_hostid
393			)
394			->addRow(
395				new CLabel(_('Template trigger'), $select_tpl_triggerid->getFocusableElementId()),
396				$select_tpl_triggerid
397			)
398			->addRow(
399				new CLabel(_('Host group'), $select_hostgroupid->getFocusableElementId()),
400				$select_hostgroupid
401			)
402			->addVar('filter_set', '1');
403	}
404	// Report by host.
405	else {
406		// Sanitize $data['filter']['groups'] and prepare "Host groups" filter field.
407		$data['filter']['groups'] = $data['filter']['groups']
408			? CArrayHelper::renameObjectsKeys(API::HostGroup()->get([
409				'output' => ['groupid', 'name'],
410				'groupids' => $data['filter']['groups'],
411				'monitored_hosts' => true,
412				'preservekeys' => true
413			]), ['groupid' => 'id'])
414			: [];
415
416		CArrayHelper::sort($data['filter']['groups'], ['name']);
417
418		// Sanitize $data['filter']['hostids'] and prepare "Hosts" filter field.
419		$data['filter']['hostids'] = $data['filter']['hostids']
420			? CArrayHelper::renameObjectsKeys(API::Host()->get([
421				'output' => ['hostid', 'name'],
422				'hostids' => $data['filter']['hostids'],
423				'monitored_hosts' => true,
424				'with_triggers' => true,
425				'preservekeys' => true
426			]), ['hostid' => 'id'])
427			: [];
428
429		CArrayHelper::sort($data['filter']['hostids'], ['name']);
430
431		// Select monitored host triggers, derived from templates and belonging to the requested groups.
432		$groups = enrichParentGroups($data['filter']['groups']);
433
434		$triggers = API::Trigger()->get([
435			'output' => ['triggerid', 'description', 'expression', 'value'],
436			'selectHosts' => ['name'],
437			'groupids' => $groups ? array_keys($groups) : null,
438			'hostids' => $data['filter']['hostids'] ? array_keys($data['filter']['hostids']) : null,
439			'expandDescription' => true,
440			'monitored' => true,
441			'limit' => $config['search_limit'] + 1
442		]);
443
444		$filter_column
445			->addRow(
446				(new CLabel(_('Host groups'), 'filter_groups__ms')),
447				(new CMultiSelect([
448					'name' => 'filter_groups[]',
449					'object_name' => 'hostGroup',
450					'data' => $data['filter']['groups'],
451					'popup' => [
452						'parameters' => [
453							'srctbl' => 'host_groups',
454							'srcfld1' => 'groupid',
455							'dstfrm' => 'zbx_filter',
456							'dstfld1' => 'filter_groups_',
457							'with_triggers' => true,
458							'real_hosts' => 1,
459							'enrich_parent_groups' => true
460						]
461					]
462				]))->setWidth(ZBX_TEXTAREA_MEDIUM_WIDTH)
463			)
464			->addRow(
465				(new CLabel(_('Hosts'), 'filter_hostid__ms')),
466				(new CMultiSelect([
467					'name' => 'filter_hostids[]',
468					'object_name' => 'hosts',
469					'data' => $data['filter']['hostids'],
470					'popup' => [
471						'filter_preselect_fields' => [
472							'hostgroups' => 'filter_groups_'
473						],
474						'parameters' => [
475							'srctbl' => 'hosts',
476							'srcfld1' => 'hostid',
477							'dstfrm' => 'zbx_filter',
478							'dstfld1' => 'filter_hostids_',
479							'with_triggers' => true,
480							'real_hosts' => 1
481						]
482					]
483				]))->setWidth(ZBX_TEXTAREA_MEDIUM_WIDTH)
484			);
485	}
486
487	// Now just prepare needed data.
488	foreach ($triggers as &$trigger) {
489		$trigger['host_name'] = $trigger['hosts'][0]['name'];
490	}
491	unset($trigger);
492
493	$reportWidget->addItem(
494		(new CFilter(new CUrl('report2.php')))
495			->setProfile($data['filter']['timeline']['profileIdx'])
496			->setActiveTab($data['filter']['active_tab'])
497			->addFormItem((new CVar('mode', $report_mode))->removeId())
498			->addTimeSelector($data['filter']['timeline']['from'], $data['filter']['timeline']['to'], true,
499				ZBX_DATE_TIME)
500			->addFilterTab(_('Filter'), [$filter_column])
501	);
502
503	/*
504	 * Triggers
505	 */
506	$triggerTable = (new CTableInfo())->setHeader([_('Host'), _('Name'), _('Problems'), _('Ok'), _('Graph')]);
507
508	CArrayHelper::sort($triggers, ['host_name', 'description']);
509
510	// pager
511	$page_num = getRequest('page', 1);
512	CPagerHelper::savePage($page['file'], $page_num);
513	$paging = CPagerHelper::paginate($page_num, $triggers, ZBX_SORT_UP, new CUrl('report2.php'));
514
515	foreach ($triggers as $trigger) {
516		$availability = calculateAvailability($trigger['triggerid'], $data['filter']['timeline']['from_ts'],
517			$data['filter']['timeline']['to_ts']
518		);
519
520		$url = (new CUrl('report2.php'))->setArgument('triggerid', $trigger['triggerid']);
521		if ($report_mode == AVAILABILITY_REPORT_BY_TEMPLATE) {
522			$url->setArgument('filter_templateid', $data['filter']['hostids']);
523		}
524		else {
525			$url->setArgument('filter_hostids', $trigger['hosts'][0]['hostid']);
526		}
527
528		$triggerTable->addRow([
529			$trigger['host_name'],
530			new CLink($trigger['description'],
531				(new CUrl('zabbix.php'))
532					->setArgument('action', 'problem.view')
533					->setArgument('filter_triggerids', [$trigger['triggerid']])
534					->setArgument('filter_set', '1')
535			),
536			($availability['true'] < 0.00005)
537				? ''
538				: (new CSpan(sprintf('%.4f%%', $availability['true'])))->addClass(ZBX_STYLE_RED),
539			($availability['false'] < 0.00005)
540				? ''
541				: (new CSpan(sprintf('%.4f%%', $availability['false'])))->addClass(ZBX_STYLE_GREEN),
542			new CLink(_('Show'), $url)
543		]);
544	}
545
546	$obj_data = [
547		'id' => 'timeline_1',
548		'domid' => 'avail_report',
549		'loadSBox' => 0,
550		'loadImage' => 0,
551		'dynamic' => 0,
552		'mainObject' => 1
553	];
554	zbx_add_post_js(
555		'timeControl.addObject("avail_report", '.zbx_jsvalue($data['filter']).', '.zbx_jsvalue($obj_data).');'
556	);
557	zbx_add_post_js('timeControl.processObjects();');
558
559	$reportWidget
560		->addItem([$triggerTable, $paging])
561		->show();
562}
563
564require_once dirname(__FILE__).'/include/page_footer.php';
565