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
22/**
23 * Class containing methods for operations with tasks.
24 */
25class CTask extends CApiService {
26
27	public const ACCESS_RULES = [
28		'get' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
29		'create' => ['min_user_type' => USER_TYPE_SUPER_ADMIN]
30	];
31
32	protected $tableName = 'task';
33	protected $tableAlias = 't';
34	protected $sortColumns = ['taskid'];
35
36	const RESULT_STATUS_ERROR = -1;
37	/**
38	 * Get results of requested ZBX_TM_TASK_DATA task.
39	 *
40	 * @param array         $options
41	 * @param string|array  $options['output']
42	 * @param string|array  $options['taskids']       Task IDs to select data about.
43	 * @param bool          $options['preservekeys']  Use IDs as keys in the resulting array.
44	 *
45	 * @return array | boolean
46	 */
47	public function get(array $options): array {
48		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
49			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
50		}
51
52		$output_fields = ['taskid', 'type', 'status', 'clock', 'ttl', 'proxy_hostid', 'request', 'result'];
53
54		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
55			// filter
56			'taskids' =>		['type' => API_IDS, 'flags' => API_NORMALIZE | API_ALLOW_NULL, 'default' => null],
57			// output
58			'output' =>			['type' => API_OUTPUT, 'in' => implode(',', $output_fields), 'default' => API_OUTPUT_EXTEND],
59			// flags
60			'preservekeys' =>	['type' => API_BOOLEAN, 'default' => false]
61		]];
62
63		if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
64			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
65		}
66
67		$options += [
68			'sortfield' => 'taskid',
69			'sortorder' => ZBX_SORT_DOWN,
70			'limit'		=> CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT)
71		];
72
73		$sql_parts = [
74			'select'	=> ['task' => 't.taskid'],
75			'from'		=> ['task' => 'task t'],
76			'where'		=> [
77				'type'		=> 't.type='.ZBX_TM_TASK_DATA
78			],
79			'order'     => [],
80			'group'     => []
81		];
82
83		if ($options['taskids'] !== null) {
84			$sql_parts['where']['taskid'] = dbConditionInt('t.taskid', $options['taskids']);
85		}
86
87		$db_tasks = [];
88
89		$sql_parts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
90		$sql_parts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
91
92		$result = DBselect($this->createSelectQueryFromParts($sql_parts), $options['limit']);
93
94		while ($row = DBfetch($result, false)) {
95			if ($this->outputIsRequested('request', $options['output'])) {
96				$row['request'] = json_decode($row['request_data']);
97				unset($row['request_data']);
98			}
99
100			if ($this->outputIsRequested('result', $options['output'])) {
101				if ($row['result_status'] === null) {
102					$row['result'] = null;
103				}
104				else {
105					if ($row['result_status'] == self::RESULT_STATUS_ERROR) {
106						$result_data = $row['result_info'];
107					}
108					else {
109						$result_data = $row['result_info'] ? json_decode($row['result_info']) : [];
110					}
111
112					$row['result'] = [
113						'data' => $result_data,
114						'status' => $row['result_status']
115					];
116				}
117
118				unset($row['result_info'], $row['result_status']);
119			}
120
121			$db_tasks[$row['taskid']] = $row;
122		}
123
124		if ($db_tasks) {
125			$db_tasks = $this->unsetExtraFields($db_tasks, ['taskid'], $options['output']);
126
127			if (!$options['preservekeys']) {
128				$db_tasks = array_values($db_tasks);
129			}
130		}
131
132		return $db_tasks;
133	}
134
135	/**
136	 * Create tasks.
137	 *
138	 * @param array        $tasks                               Tasks to create.
139	 * @param string|array $tasks[]['type']                     Type of task.
140	 * @param string       $tasks[]['request']['itemid']        Must be set for ZBX_TM_TASK_CHECK_NOW task.
141	 * @param array        $tasks[]['request']['historycache']  (optional) object of history cache data request.
142	 * @param array        $tasks[]['request']['valuecache']    (optional) object of value cache data request.
143	 * @param array        $tasks[]['request']['preprocessing'] (optional) object of preprocessing data request.
144	 * @param array        $tasks[]['request']['alerting']      (optional) object of alerting data request.
145	 * @param array        $tasks[]['request']['lld']           (optional) object of lld cache data request.
146	 * @param array        $tasks[]['proxy_hostid']             (optional) Proxy to get diagnostic data about.
147	 *
148	 * @return array
149	 */
150	public function create(array $tasks): array {
151		$this->validateCreate($tasks);
152
153		$tasks_by_types = [
154			ZBX_TM_DATA_TYPE_CHECK_NOW => [],
155			ZBX_TM_DATA_TYPE_DIAGINFO => []
156		];
157
158		foreach ($tasks as $index => $task) {
159			$tasks_by_types[$task['type']][$index] = $task;
160		}
161
162		$return = $this->createTasksCheckNow($tasks_by_types[ZBX_TM_DATA_TYPE_CHECK_NOW]);
163		$return += $this->createTasksDiagInfo($tasks_by_types[ZBX_TM_DATA_TYPE_DIAGINFO]);
164
165		ksort($return);
166
167		return ['taskids' => array_values($return)];
168	}
169
170	/**
171	 * Validates the input for create method.
172	 *
173	 * @param array $tasks  Tasks to validate.
174	 *
175	 * @throws APIException if the input is invalid.
176	 */
177	protected function validateCreate(array &$tasks) {
178		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'fields' => [
179			'type' =>		['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ZBX_TM_DATA_TYPE_DIAGINFO, ZBX_TM_DATA_TYPE_CHECK_NOW])],
180			'request' =>	['type' => API_MULTIPLE, 'flags' => API_REQUIRED, 'rules' => [
181								['if' => ['field' => 'type', 'in' => ZBX_TM_DATA_TYPE_DIAGINFO], 'type' => API_OBJECT, 'fields' => [
182				'historycache' =>	['type' => API_OBJECT, 'fields' => [
183					'stats' =>			['type' => API_OUTPUT, 'in' => implode(',', ['items', 'values', 'memory', 'memory.data', 'memory.index']), 'default' => API_OUTPUT_EXTEND],
184					'top' =>			['type' => API_OBJECT, 'fields' => [
185						'values' =>			['type' => API_INT32]
186					]]
187				]],
188				'valuecache' =>		['type' => API_OBJECT, 'fields' => [
189					'stats' =>			['type' => API_OUTPUT, 'in' => implode(',', ['items', 'values', 'memory', 'mode']), 'default' => API_OUTPUT_EXTEND],
190					'top' =>			['type' => API_OBJECT, 'fields' => [
191						'values' =>			['type' => API_INT32],
192						'request.values' =>	['type' => API_INT32]
193					]]
194				]],
195				'preprocessing' =>	['type' => API_OBJECT, 'fields' => [
196					'stats' =>			['type' => API_OUTPUT, 'in' => implode(',', ['values', 'preproc.values']), 'default' => API_OUTPUT_EXTEND],
197					'top' =>			['type' => API_OBJECT, 'fields' => [
198						'values' =>			['type' => API_INT32]
199					]]
200				]],
201				'alerting' =>		['type' => API_OBJECT, 'fields' => [
202					'stats' =>			['type' => API_OUTPUT, 'in' => 'alerts', 'default' => API_OUTPUT_EXTEND],
203					'top' =>			['type' => API_OBJECT, 'fields' => [
204						'media.alerts' =>	['type' => API_INT32],
205						'source.alerts' =>	['type' => API_INT32]
206					]]
207				]],
208				'lld' =>			['type' => API_OBJECT, 'fields' => [
209					'stats' =>			['type' => API_OUTPUT, 'in' => implode(',', ['rules', 'values']), 'default' => API_OUTPUT_EXTEND],
210					'top' =>			['type' => API_OBJECT, 'fields' => [
211						'values' =>			['type' => API_INT32]
212					]]
213				]]
214								]],
215								['if' => ['field' => 'type', 'in' => ZBX_TM_DATA_TYPE_CHECK_NOW], 'type' => API_OBJECT, 'fields' => [
216				'itemid' => ['type' => API_ID, 'flags' => API_REQUIRED | API_NOT_EMPTY]
217								]]
218			]],
219			'proxy_hostid' => ['type' => API_ID, 'default' => 0]
220		]];
221
222		if (!CApiInputValidator::validate($api_input_rules, $tasks, '/', $error)) {
223			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
224		}
225
226		$min_permissions = USER_TYPE_ZABBIX_ADMIN;
227		$itemids_editable = [];
228		$proxy_hostids = [];
229
230		foreach ($tasks as $task) {
231			switch ($task['type']) {
232				case ZBX_TM_DATA_TYPE_DIAGINFO:
233					$min_permissions = USER_TYPE_SUPER_ADMIN;
234
235					$proxy_hostids[$task['proxy_hostid']] = true;
236					break;
237
238				case ZBX_TM_DATA_TYPE_CHECK_NOW:
239					$itemids_editable[$task['request']['itemid']] = true;
240					break;
241			}
242		}
243
244		unset($proxy_hostids[0]);
245
246		if (self::$userData['type'] < $min_permissions) {
247			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
248		}
249
250		$this->checkProxyHostids(array_keys($proxy_hostids));
251		$this->checkEditableItems(array_keys($itemids_editable));
252	}
253
254	/**
255	 * Create ZBX_TM_TASK_CHECK_NOW tasks.
256	 *
257	 * @param array        $tasks                          Request object for tasks to create.
258	 * @param string|array $tasks[]['request']['itemid']   Item or LLD rule IDs to create tasks for.
259	 *
260	 * @throws APIException
261	 *
262	 * @return array
263	 */
264	protected function createTasksCheckNow(array $tasks): array {
265		if (!$tasks) {
266			return [];
267		}
268
269		$itemids = [];
270		$return = [];
271
272		foreach ($tasks as $index => $task) {
273			$itemids[$index] = $task['request']['itemid'];
274		}
275
276		// Check if tasks for items and LLD rules already exist.
277		$db_tasks = DBselect(
278			'SELECT t.taskid, tcn.itemid'.
279			' FROM task t, task_check_now tcn'.
280			' WHERE t.taskid=tcn.taskid'.
281				' AND t.type='.ZBX_TM_TASK_CHECK_NOW.
282				' AND t.status='.ZBX_TM_STATUS_NEW.
283				' AND '.dbConditionId('tcn.itemid', $itemids)
284		);
285
286		while ($db_task = DBfetch($db_tasks)) {
287			foreach (array_keys($itemids, $db_task['itemid']) as $index) {
288				$return[$index] = $db_task['taskid'];
289				unset($itemids[$index]);
290			}
291		}
292
293		// Create new tasks.
294		if ($itemids) {
295			$taskid = DB::reserveIds('task', count($itemids));
296			$task_rows = [];
297			$task_check_now_rows = [];
298			$time = time();
299
300			foreach ($itemids as $index => $itemid) {
301				$task_rows[] = [
302					'taskid' => $taskid,
303					'type' => ZBX_TM_TASK_CHECK_NOW,
304					'status' => ZBX_TM_STATUS_NEW,
305					'clock' => $time,
306					'ttl' => SEC_PER_HOUR
307				];
308				$task_check_now_rows[] = [
309					'taskid' => $taskid,
310					'itemid' => $itemid,
311					'parent_taskid' => $taskid
312				];
313
314				$return[$index] = $taskid;
315				$taskid = bcadd($taskid, 1, 0);
316			}
317
318			DB::insertBatch('task', $task_rows, false);
319			DB::insertBatch('task_check_now', $task_check_now_rows, false);
320		}
321
322		return $return;
323	}
324
325	/**
326	 * Create ZBX_TM_DATA_TYPE_DIAGINFO tasks.
327	 *
328	 * @param array    $tasks[]
329	 * @param array    $tasks[]['request']['historycache']  (optional) object of history cache data request.
330	 * @param array    $tasks[]['request']['valuecache']    (optional) object of value cache data request.
331	 * @param array    $tasks[]['request']['preprocessing'] (optional) object of preprocessing data request.
332	 * @param array    $tasks[]['request']['alerting']      (optional) object of alerting data request.
333	 * @param array    $tasks[]['request']['lld']           (optional) object of lld cache data request.
334	 * @param array    $tasks[]['proxy_hostid']             Proxy to get diagnostic data about.
335	 *
336	 * @throws APIException
337	 *
338	 * @return array
339	 */
340	protected function createTasksDiagInfo(array $tasks): array {
341		$task_rows = [];
342		$task_data_rows = [];
343		$return = [];
344		$taskid = DB::reserveIds('task', count($tasks));
345
346		foreach ($tasks as $index => $task) {
347			$task_rows[] = [
348				'taskid' => $taskid,
349				'type' => ZBX_TM_TASK_DATA,
350				'status' => ZBX_TM_STATUS_NEW,
351				'clock' => time(),
352				'ttl' => SEC_PER_HOUR,
353				'proxy_hostid' => $task['proxy_hostid']
354			];
355
356			$task_data_rows[] = [
357				'taskid' => $taskid,
358				'type' => $task['type'],
359				'data' => json_encode($task['request']),
360				'parent_taskid' => $taskid
361			];
362
363			$return[$index] = $taskid;
364			$taskid = bcadd($taskid, 1, 0);
365		}
366
367		DB::insertBatch('task', $task_rows, false);
368		DB::insertBatch('task_data', $task_data_rows, false);
369
370		return $return;
371	}
372
373	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sql_parts) {
374		$sql_parts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sql_parts);
375
376		if ($this->outputIsRequested('request', $options['output'])) {
377			$sql_parts['left_join'][] = ['alias' => 'req', 'table' => 'task_data', 'using' => 'parent_taskid'];
378			$sql_parts['left_table'] = ['alias' => $this->tableAlias, 'table' => $this->tableName()];
379
380			$sql_parts = $this->addQuerySelect('req.data AS request_data', $sql_parts);
381		}
382
383		if ($this->outputIsRequested('result', $options['output'])) {
384			$sql_parts['left_join'][] = ['alias' => 'resp', 'table' => 'task_result', 'using' => 'parent_taskid'];
385			$sql_parts['left_table'] = ['alias' => $this->tableAlias, 'table' => $this->tableName()];
386
387			$sql_parts = $this->addQuerySelect('resp.info AS result_info', $sql_parts);
388			$sql_parts = $this->addQuerySelect('resp.status AS result_status', $sql_parts);
389		}
390
391		return $sql_parts;
392	}
393
394	/**
395	 * Validate user permissions to items and LLD rules;
396	 * Check if requested items are allowed to make 'check now' operation;
397	 * Check if items are monitored and they belong to monitored hosts.
398	 *
399	 * @param array $itemids
400	 *
401	 * @throws Exception
402	 */
403	protected function checkEditableItems(array $itemids): void {
404		if (!$itemids) {
405			return;
406		}
407
408		// Check permissions.
409		$items = API::Item()->get([
410			'output' => ['name', 'type', 'status', 'flags'],
411			'selectHosts' => ['name', 'status'],
412			'itemids' => $itemids,
413			'editable' => true,
414			'preservekeys' => true
415		]);
416
417		$itemids_cnt = count($itemids);
418
419		if (count($items) != $itemids_cnt) {
420			$items += API::DiscoveryRule()->get([
421				'output' => ['name', 'type', 'status', 'flags'],
422				'selectHosts' => ['name', 'status'],
423				'itemids' => $itemids,
424				'editable' => true,
425				'preservekeys' => true
426			]);
427
428			if (count($items) != $itemids_cnt) {
429				self::exception(ZBX_API_ERROR_PERMISSIONS,
430					_('No permissions to referred object or it does not exist!')
431				);
432			}
433		}
434
435		// Validate item and LLD rule type and status.
436		$allowed_types = checkNowAllowedTypes();
437
438		foreach ($items as $item) {
439			if (!in_array($item['type'], $allowed_types)) {
440				self::exception(ZBX_API_ERROR_PARAMETERS,
441					_s('Cannot send request: %1$s.',
442						($item['flags'] == ZBX_FLAG_DISCOVERY_RULE)
443							? _('wrong discovery rule type')
444							: _('wrong item type')
445					)
446				);
447			}
448
449			if ($item['status'] != ITEM_STATUS_ACTIVE || $item['hosts'][0]['status'] != HOST_STATUS_MONITORED) {
450				$host_name = $item['hosts'][0]['name'];
451				$problem = ($item['flags'] == ZBX_FLAG_DISCOVERY_RULE)
452					? _s('discovery rule "%1$s" on host "%2$s" is not monitored', $item['name'], $host_name)
453					: _s('item "%1$s" on host "%2$s" is not monitored', $item['name'], $host_name);
454				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot send request: %1$s.', $problem));
455			}
456		}
457	}
458
459	/**
460	 * Function to check if specified proxies exists.
461	 *
462	 * @param array $proxy_hostids  Proxy IDs to check.
463	 *
464	 * @throws Exception if proxy doesn't exist.
465	 */
466	protected function checkProxyHostids(array $proxy_hostids): void {
467		if (!$proxy_hostids) {
468			return;
469		}
470
471		$proxies = API::Proxy()->get([
472			'countOutput' => true,
473			'proxyids' => $proxy_hostids
474		]);
475
476		if ($proxies != count($proxy_hostids)) {
477			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
478		}
479	}
480}
481