diff --git a/api/classes/CAction.php b/api/classes/CAction.php index 3c0ab48..f3da592 100644 --- a/api/classes/CAction.php +++ b/api/classes/CAction.php @@ -973,6 +973,121 @@ class CAction extends CZBXAPI { return array('actionids' => $actionids); } + /** + * Show operations and action matches + * + * @param _array $options + * @param array $options['actionids'] + * @param array $options['hostids'] + * @param array $options['triggerids'] + * @return array|int data as array or false if error + * TODO: Hier könnte Option sein, ob man überhaupt die Operations wissen will oder nur die Actions; die Debuginfo sollte auch optional sein + */ + public function run(array $options) { + $result = array(); + + # The whole method only works properly for super admins + $userType = self::$userData['type']; + if ($userType != USER_TYPE_SUPER_ADMIN) { + return null; + } + + $defOptions = array( + 'actionids' => null, + 'hostids' => null, + 'triggerids' => null, + 'triggerStatus' => TRIGGER_VALUE_TRUE, + 'timeStart' => null, + 'timeEnd' => null, + 'timeAck' => null, + 'withMaintenance' => null, + // filter + 'filter' => null, + 'search' => null, + 'searchByAny' => null, + 'startSearch' => null, + 'excludeSearch' => null, + 'searchWildcardsEnabled' => null, + // output + 'output' => API_OUTPUT_REFER, + 'selectConditions' => null, + 'selectOperations' => null, + 'countOutput' => null, + 'sortfield' => '', + 'sortorder' => '', + 'limit' => null + ); + #$options = array( + # 'selectActionConditionDebugging' => API_OUTPUT_EXTEND, + # 'selectOperationDebugging' => API_OUTPUT_EXTEND + #); + $options = zbx_array_merge($defOptions, $options); + + $trigger = API::Trigger()->get(array( + 'triggerids' => $options['triggerids'], + 'expandDescription' => '1', + 'output' => API_OUTPUT_EXTEND, + 'selectGroups' => API_OUTPUT_EXTEND, + 'selectHosts' => API_OUTPUT_EXTEND, + 'selectItems' => API_OUTPUT_REFER, + 'selectFunctions' => API_OUTPUT_EXTEND, + 'selectDependencies' => API_OUTPUT_EXTEND + )); + $trigger = reset($trigger); + + //foreach ($trigger['hosts'] as $host) { + // $hostids[] = $host['hostid']; + //} + #TODO: Does it require a mktime like in maintenance.php:170? + + #TODO: groupids could also be fed into + //$maintenances = API::Maintenance()->get(array( + // 'hostids' => $hostids, + // 'selectTimeperiods' => API_OUTPUT_EXTEND, + // 'output' => API_OUTPUT_EXTEND, + // )); + + //foreach ($maintenances as $maintenance) { + // foreach ($maintenance['timeperiods'] as $timeperiod) { + // #TODO: Requires some helper function to figure out, whether an escalation step falls into a window of actual maintenance. It's probably not worth it. + // } + //} + + $actionOptions = array( + 'selectConditions' => API_OUTPUT_EXTEND, + 'selectOperations' => API_OUTPUT_EXTEND, + 'output' => API_OUTPUT_EXTEND, + 'preservekeys' => true, + 'filter' => array( + 'status' => array(ACTION_STATUS_ENABLED), + 'eventsource' => EVENT_SOURCE_TRIGGERS + ) + ); + //TODO: Allow filtering by status here? + $actions = $this->get($actionOptions); + + $options = array( + 'triggerStatus' => $options['triggerStatus'], + 'timeStart' => $options['timeStart'], + 'timeAck' => $options['timeAck'], + 'timeEnd' => $options['timeEnd'], + 'operationDebugging' => true, + 'actionDebugging' => true + ); + + $actionSimulation = new ActionSimulation($actions, $trigger, $options); + $actionSimulation->process(); + + $result = $actionSimulation->get($options); + + // removing keys (hash -> array) + if (false === isset($actionOptions['preservekeys'])) { + $result = zbx_cleanHashes($result); + } + + return $result; + } + protected function addConditions($conditions) { foreach ($conditions as $condition) { $connectionDbFields = array( diff --git a/include/actions.inc.php b/include/actions.inc.php index 4263253..f48c7e0 100644 --- a/include/actions.inc.php +++ b/include/actions.inc.php @@ -18,6 +18,49 @@ ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. **/ +// Map script id to script name +function get_global_script_name($scriptid) { + $script = API::Script()->get(array( + 'scriptids' => array($scriptid), + 'output' => API_OUTPUT_EXTEND, + 'preservekeys' => '1' + )); + + if ($script) { + $script = reset($script); + return $script['name']; + } + + return _('Unknown'); +} + +// Map evaluation type to textual representation +function action_evaltype2str($evaltype) { + $str_type[ACTION_EVAL_TYPE_AND_OR] = _('AND/OR'); + $str_type[ACTION_EVAL_TYPE_AND] = _('AND'); + $str_type[ACTION_EVAL_TYPE_OR] = _('OR'); + + if (isset($str_type[$evaltype])) { + return $str_type[$evaltype]; + } + + return _('Unknown'); +} + +// Map numerical script type to textual script type +function operation_scripttype2str($scripttype) { + $str_type[ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT] = _('Custom script'); + $str_type[ZBX_SCRIPT_TYPE_IPMI] = _('IPMI'); + $str_type[ZBX_SCRIPT_TYPE_SSH] = _('SSH'); + $str_type[ZBX_SCRIPT_TYPE_TELNET] = _('Telnet'); + $str_type[ZBX_SCRIPT_TYPE_GLOBAL_SCRIPT] = _('Global script'); + + if (isset($str_type[$scripttype])) { + return $str_type[$scripttype]; + } + + return _('Unknown'); +} function condition_operator2str($operator) { switch ($operator) { diff --git a/include/classes/actions/ActionSimulation.php b/include/classes/actions/ActionSimulation.php new file mode 100644 index 0000000..579e000 --- /dev/null +++ b/include/classes/actions/ActionSimulation.php @@ -0,0 +1,916 @@ +actions; + } + + /** + * Requires an action array with selectConditions and selectOperations + * Options are: + * - A different timestamp than now's to start from + * - Assume maintenance or not + * - Event OK/Problem + * - Only tag matching actions or evaluate operations + */ + public function __construct(array $actions, array $trigger, array $options) { + $this->actions = $actions; + $this->trigger = $trigger; + $defOptions = array( + 'triggerStatus' => TRIGGER_VALUE_TRUE, + 'maintenance' => null, + 'timeStart' => null, + 'timeAck' => null, + 'timeEnd' => null, + 'operationDebugging' => null, + 'actionDebugging' => null, + ); + + $this->options = zbx_array_merge($defOptions, $options); + + // Escalate for one hour, if not defined differently or hitting a limit of 2 hours + $durationDefault = 3600; + $durationLimit = 7200; + #TODO: Indicate whether the end of escalation was reached or not + #TODO: mktime necessary everywhere below? + + // If not set, use a default and also limit the duration + if (false===isset($this->options['timeStart'])) { + $this->options['timeStart'] = time(); + } + + if (true===isset($this->options['timeEnd'])) { + if ($this->options['timeEnd'] > $this->options['timeStart'] + $durationLimit) { + $this->options['timeEnd'] = $this->options['timeStart'] + $durationDefault; + } + } + else { + $this->options['timeEnd'] = $this->options['timeStart'] + $durationDefault; + } + + // Ignore acknowledgements outside of the event's scope + if (false===isset($this->options['timeStart'])) { + if ($this->options['timeStart'] > $this->options['timeAck'] || + $this->options['timeEnd'] < $this->options['timeAck']) { + $this->options['timeAck'] = null; + } + } + } + + /** + * Match action conditions and create and populate the object + * with escalation data, as requested in options + */ + public function process() { + if (empty($this->actions)) { + return false; + } + $this->findMatchingActions(); + #TODO: If options indicate, only finding matching actions is desired, we could stop here + $this->executeEscalation(); + } + + /** + * Add a label to matching actions or remove non-matching actions, + * depending on the supplied options. + * Actions only have to be evaluated once, because actions require trigger status changes to strike + * //@param array actionids Array of actionids to try + */ + protected function findMatchingActions() { + foreach ($this->actions as $action) { + $actionid = $action['actionid']; + #TODO: Convert to just using actionid! + $pass = ($this->checkActionConditions($action) == SUCCEED) ? true : false; + if ($this->options['actionDebugging']) { + $this->actions[$actionid]['match'] = $pass; + } + else { + if (!$pass) { + unset($this->actions[$actionid]); + } + } + } + } + + protected function operationDebug($info) { + if ($this->options['operationDebugging']) { + } + else { + } + } + + /** + * Evaluate whether the conditions of a given action match a given trigger + * @param array $action Action object to check conditions on + */ + #TODO: Convert to actionid + protected function checkActionConditions(array $action) { + // Actions without conditions always apply + if (!array_key_exists('conditions', $action)) { + return SUCCEED; + } + + $storedResult = null; + $old_type = -1; + $cond = -1; + $ret = SUCCEED; + $conditiontypes = array(); + + //Sort by conditiontype within the action, otherwise evaluation can fail + foreach ($action['conditions'] as $condition) { + $conditiontypes[] = $condition['conditiontype']; + } + + array_multisort($conditiontypes, SORT_ASC, $action['conditions']); + + $actionid = $action['actionid']; + $newEntry[$actionid] = array( + 'name' => $action['name'], + 'evaltype' => $action['evaltype'] + ); + + foreach ($action['conditions'] as $condition) { + $conditionid = $condition['conditionid']; + switch ($action['evaltype']) { + case ACTION_EVAL_TYPE_AND_OR: + // OR part of conditions + if ($old_type == $condition['conditiontype']) { + if ($this->checkTriggerCondition($condition) == SUCCEED) { + $ret = SUCCEED; + } + } + // AND part of conditions + else { + // Break if PREVIOUS AND condition is FALSE + if ($ret == FAIL) { + $storedResult = $ret; + } + if ($this->checkTriggerCondition($condition) == FAIL) { + $ret = FAIL; + } + } + + $old_type = $condition['conditiontype']; + + break; + case ACTION_EVAL_TYPE_AND: + $cond = $this->checkTriggerCondition($condition); + // break if any AND condition is FALSE + if ($cond == FAIL) { + $ret = FAIL; + // exit says, no further processing can change the result + // We still want to know what the results are + $storedResult = $ret; + } + elseif ($ret != FAIL) { + $ret = SUCCEED; + } + + break; + + case ACTION_EVAL_TYPE_OR: + $cond = $this->checkTriggerCondition($condition); + // break if any OR condition is TRUE + if ($cond == SUCCEED) { + $ret = SUCCEED; + $storedResult = $ret; + } + else { + $ret = FAIL; + } + + break; + } + } + if (true===isset($storedResult)) { + return $storedResult; } + else { + return $ret; + } + } + + /** + * Evaluate if a given trigger matches a given action condition + * @param array $condition Array that contains conditiontype, operator, value + * @return Return SUCCEED on match, FAIL on mismatch + */ + private function checkTriggerCondition(array $condition){ + + $actionid = $condition['actionid']; + $conditionid = $condition['conditionid']; + $ret = FAIL; + + if ($condition['conditiontype'] == CONDITION_TYPE_HOST_GROUP) { + + $conditionMatches = API::Trigger()->get(array( + 'groupids' => array($condition['value']), + 'triggerids' => array($this->trigger['triggerid']) + )); + + $hostgroups = API::HostGroup()->get(array( + 'triggerids' => array($this->trigger['triggerid']) + )); + $hostgroupslist = array(); + foreach ($hostgroups as $hostgroup) { + $hostgroupList[] = $hostgroup['groupid']; + } + $actualvalue = $hostgroupList; + + switch ($condition['operator']) { + case CONDITION_OPERATOR_EQUAL: + if ($conditionMatches) { + $ret = SUCCEED; + } + break; + case CONDITION_OPERATOR_NOT_EQUAL: + if (!$conditionMatches) { + $ret = SUCCEED; + } + break; + } + } + elseif ($condition['conditiontype'] == CONDITION_TYPE_HOST_TEMPLATE) { + switch ($condition['operator']) { + case CONDITION_OPERATOR_EQUAL: + case CONDITION_OPERATOR_NOT_EQUAL: + $triggerid = $this->trigger['triggerid']; + + //TODO: Convention on table aliasing + //Schutz vor 0 Ergebnissen +# use parent trigger ID for generated triggers + if ($this->trigger['flags'] == TRIGGER_FLAGS_DISCOVERED) { + $sql = 'SELECT td.parent_triggerid'. + ' FROM trigger_discovery td'. + ' WHERE triggerid='.$triggerid; + $triggerid = DBfetch(DBselect($sql)); + $triggerid = reset($triggerid); + } + + //Loop until you hit the base template + while ($ret != SUCCEED and $triggerid != 0){ + $conditionMatches = API::Trigger()->get(array( + 'triggerids' => $triggerid, + 'selectHosts' => API_OUTPUT_EXTEND, + 'output' => API_OUTPUT_EXTEND, + )); + + // trigger.get does not return trigger prototypes + if (!$conditionMatches) { + $conditionMatches = API::TriggerPrototype()->get(array( + 'triggerids' => $triggerid, + 'selectHosts' => API_OUTPUT_EXTEND, + 'output' => API_OUTPUT_EXTEND, + )); + } + + $conditionMatches = reset($conditionMatches); + if ($conditionMatches) { + + //TODO: Remove API calls above? + $triggerid = $conditionMatches['templateid']; + $templateid = API::Template()->get(array( + 'triggerids' => $triggerid, + 'output' => API_OUTPUT_EXTEND, + )); + if ($templateid) { + $templateid = reset($templateid); + //TODO: Turn to host instead? + $actualvalue = $templateid['hostid']; + } + else { + $actualvalue = null; + } + + if ($conditionMatches['hosts'][0]['hostid'] == $condition['value']) { + $ret = SUCCEED; + break; + } + } + else { + $triggerid = 0; + } + } + if ($condition['operator'] == CONDITION_OPERATOR_NOT_EQUAL) { + $ret = ($ret == SUCCEED) ? FAIL : SUCCEED; + break; + } + } + } + elseif ($condition['conditiontype'] == CONDITION_TYPE_HOST) { + + $actualvalue = $this->trigger['hosts'][0]['hostid']; + + if ($this->trigger['hosts'][0]['hostid'] == $condition['value']) { + $conditionMatches = 1; + } + else { + $conditionMatches = 0; + } + + switch ($condition['operator']) { + case CONDITION_OPERATOR_EQUAL: + if ($conditionMatches == 1){ + $ret = SUCCEED;} + break; + case CONDITION_OPERATOR_NOT_EQUAL: + if ($conditionMatches == 0){ + $ret = SUCCEED;} + break; + } + } + elseif ($condition['conditiontype'] == CONDITION_TYPE_TRIGGER) { + + $actualvalue = $this->trigger['triggerid']; + + if ($this->trigger['triggerid'] == $condition['value']) { + $conditionMatches = 1; + } + else { + $conditionMatches = 0; + } + + switch ($condition['operator']) { + case CONDITION_OPERATOR_EQUAL: + if ($conditionMatches == 1) + $ret = SUCCEED; + break; + case CONDITION_OPERATOR_NOT_EQUAL: + if ($conditionMatches == 0) + $ret = SUCCEED; + break; + } + } +#TODO: Weakness: Not all macros can be substituted easily (ITEM.VALUE, ...) + elseif ($condition['conditiontype'] == CONDITION_TYPE_TRIGGER_NAME) { + + $actualvalue = $this->trigger['description']; + + switch ($condition['operator']) { + case CONDITION_OPERATOR_LIKE: + if (strstr($this->trigger['description'], $condition['value'])) { + $ret = SUCCEED; + } + break; + case CONDITION_OPERATOR_NOT_LIKE: + if (!strstr($this->trigger['description'], $condition['value'])) { + $ret = SUCCEED; + } + break; + } + } + elseif ($condition['conditiontype'] == CONDITION_TYPE_TRIGGER_SEVERITY) { + + $actualvalue = $this->trigger['priority']; + + switch ($condition['operator']) { + case CONDITION_OPERATOR_EQUAL: + if ($this->trigger['priority'] == $condition['value']) { + $ret = SUCCEED; + } + break; + case CONDITION_OPERATOR_NOT_EQUAL: + if ($this->trigger['priority'] != $condition['value']) { + $ret = SUCCEED; + } + break; + case CONDITION_OPERATOR_MORE_EQUAL: + if ($this->trigger['priority'] >= $condition['value']) { + $ret = SUCCEED; + } + break; + case CONDITION_OPERATOR_LESS_EQUAL: + if ($this->trigger['priority'] <= $condition['value']) { + $ret = SUCCEED; + } + break; + } + } + elseif ($condition['conditiontype'] == CONDITION_TYPE_TRIGGER_VALUE) { + + $actualvalue = $this->options['triggerStatus']; + switch ($condition['operator']) { + case CONDITION_OPERATOR_EQUAL: + if ($this->options['triggerStatus'] == $condition['value']) { + $ret = SUCCEED; + } + break; + } + } + elseif ($condition['conditiontype'] == CONDITION_TYPE_TIME_PERIOD) { + $time_periods = explode(';', $condition['value']); + //TODO: Korrigieren und Format; Gehört das nicht eher in die Darstellung? + $actualvalue = zbx_date2str(_('D, H:i'), time()); + + switch ($condition['operator']) { + case CONDITION_OPERATOR_IN: + foreach ($time_periods as $time_period) { + if (checkTimePeriod($time_period, $this->options['timeStart'])) { + $ret = SUCCEED; + break; + } + } + break; + case CONDITION_OPERATOR_NOT_IN: + foreach ($time_periods as $time_period) { + if (checkTimePeriod($time_period, $this->options['timeStart'])) { + break; + } + $ret = SUCCEED; + } + break; + } + } + elseif ($condition['conditiontype'] == CONDITION_TYPE_MAINTENANCE) { + + //TODO: Integrate real maintenance periods + $actualvalue = $_REQUEST['maint']; + + $hostgroups = API::HostGroup()->get(array( + 'triggerids' => $this->trigger['triggerid'] + )); + $hostgroupslist = array(); + foreach ($hostgroups as $hostgroup) { + $hostgroupList[] = $hostgroup['groupid']; + } + $maintenaces = API::Maintenance()->get(array( + 'hostids' => $this->trigger['hosts'][0]['hostid'], + 'groupids' => $hostgroupList, + 'selectTimeperiods' => API_OUTPUT_EXTEND + )); + + switch ($condition['operator']) { + case CONDITION_OPERATOR_IN: + if ($_REQUEST['maint'] == HOST_MAINTENANCE_STATUS_ON) { + $ret = SUCCEED; + } + break; + case CONDITION_OPERATOR_NOT_IN: + if ($_REQUEST['maint'] == HOST_MAINTENANCE_STATUS_OFF) { + $ret = SUCCEED; + } + break; + } + } + elseif ($condition['conditiontype'] == CONDITION_TYPE_APPLICATION) { + $items = array_map(function($item) {return $item['itemid'];}, $this->trigger['items']); + //No output customization possible + $applications = API::Application()->get(array( + 'itemids' => $items, + 'output' => API_OUTPUT_EXTEND + )); + + + if ($applications) { + foreach ($applications as $app) { + $actualvalue[] = $app['name']; + } + } + else { + // Item has no application defined + $actualvalue = null; + } + + switch ($condition['operator']) { + case CONDITION_OPERATOR_EQUAL: + foreach ($applications as $app) { + if (strcmp($app['name'], $condition['value']) == 0){ + $ret = SUCCEED; + break; + } + } + break; + case CONDITION_OPERATOR_LIKE: + //Das scheint leer zu sein. + foreach ($applications as $app) { + if (strstr($app['name'], $condition['value'])){ + $ret = SUCCEED; + break; + } + } + break; + case CONDITION_OPERATOR_NOT_LIKE: + $ret = SUCCEED; + foreach ($applications as $app) { + if (strstr($app['name'], $condition['value'])){ + $ret = FAIL; + break; + } + } + break; + } + } + + $this->actions[$actionid]['conditions'][$conditionid]['match'] = $ret; + $this->actions[$actionid]['conditions'][$conditionid]['actualvalue'] = $actualvalue; + + return $ret; + } + + /** + * Run operations of matching actions, as escalation requires + */ + protected function executeEscalation() { + foreach ($this->actions as $action) { + #TODO: option berücksichtigen oder anders lösen + if (!isset($action['match'])) { + continue; + } + $stepTime = $this->options['timeStart']; + $step = 0; + $running = true; + // Valid actions must have at least one step defined + $stepsLeft = true; + + // Escalation step loop + while ($running && $stepsLeft) { + $stepsLeft = false; + $step += 1; + $nextStepTimes = array(); + + // Operation loop per escalation step + foreach ($action['operations'] as $operation) { + // Operation valid for this step? + if ($operation['esc_step_to'] == 0 || $operation['esc_step_to'] > $step) { + $stepsLeft = true; + } + if ($operation['esc_step_from'] <= $step && ($operation['esc_step_to'] == 0 || $operation['esc_step_to'] >= $step)) { + // Determine the time the next step will take place + if ($operation['esc_period'] == 0 || $operation['esc_period'] > $action['esc_period']) { + $nextStepTimes[] = $action['esc_period']; + } + else { + $nextStepTimes[] = $operation['esc_period']; + } + if ($stepTime > $this->options['timeEnd']) { + // Exit the loop + $running = false; + echo "Operation step time greater than end time"; + #TODO: Attach to API result instead + } + //if ($stepTime > $this->timeAck) { + //$running = false; + // Exit the loop? + //TODO Doesn't seem to be enough! + //Or >=? + //Alternativ: Als "nicht verschickt" anzeigen und als Grund "bestätigt" anführen. + //} + + else { + // if $operation[Bedingung_bestätigt] und Bestätigtzeit > Schritt_Zeit: + // } + //Operationsdetails ermitteln (Wie bisher, Schritt und Uhrzeit seit Beginn, sowie absolut) + #TODO: Da muss die operationid und actionid reichen + $this->executeOperation($operation, $stepTime, $step); + } + } + } + $stepTime += min($nextStepTimes); + } + } + } + + /** + * Taken from escalator.c, not actions.c! + * The function of the same name in actions.c is for host discovery + * //Determine the outcome of attempting to run an operation + * + * @param array $operation Operation object + * @param int $time Time for the operation to take place + * @param int $step Time for the operation to take place + */ + protected function executeOperation(array $operation, $time, $step) { + // Messaging + if ($operation['operationtype'] == OPERATION_TYPE_MESSAGE) { + $this->substituteUsergroupsForOperation($operation, $time, $step); + } + //Processes remote commands for all given actions + elseif ($operation['operationtype'] == OPERATION_TYPE_COMMAND) { + $opcommand = $operation['opcommand']; + $script = ''; + + switch ($opcommand['type']) { + case ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT: + if ($opcommand['execute_on'] == ZBX_SCRIPT_EXECUTE_ON_AGENT) { + $script .= 'Execute on Agent:'.SPACE; + } + else if ($opcommand['execute_on'] == ZBX_SCRIPT_EXECUTE_ON_SERVER) { + $script .= 'Execute on Server:'.SPACE; + } + break; + case ZBX_SCRIPT_TYPE_SSH: + //Better show no details + case ZBX_SCRIPT_TYPE_TELNET: + //Better show no details + break; + case ZBX_SCRIPT_TYPE_GLOBAL_SCRIPT: + $script .= get_global_script_name($opcommand['scriptid']); + } + + if ($opcommand['type'] != ZBX_SCRIPT_TYPE_GLOBAL_SCRIPT) { + //TODO: Macros, if necessary + $script .= $opcommand['command']; + } + + $results = array( + 'opdesc' => get_operation_desc(SHORT_DESCRIPTION, $operation), + 'time' => $time, + 'step' => $step, + 'type' => $opcommand['type'], + 'command' => $script, + ); + $this->actions[$operation['actionid']]['operations'][$operation['operationid']]['resultCommands'][] = $results; + } + } + +#TODO: Get method for sorted results? + + /** + * Check user's system and host permissions + * + * @param int $userid User id to check permissions for + * + * @return + */ + protected function checkPermission($userid) { + //User is enabled and has read permissions for this host + if (!check_perm2system($userid)) { + $permissionError = 'User is not allowed to access Zabbix'; + //TODO: Was kann das continue? + return $permissionError; + } + + if (!$this->getHostPermissions($userid, $this->trigger['hosts'][0]['hostid'])) { + $permissionError = 'Host permissions'; + } + else { + $permissionError = null; + } + + return $permissionError; + } + + /** + * Resolve user groups in operations + * TODO: Das macht eigentlich die Benachrichtigungen! + * + * @param array $operation Operation object + * + */ + protected function substituteUsergroupsForOperation(array $operation, $time, $step) { + $mediatypeid = $operation['opmessage']['mediatypeid']; +//mediatypeid passt soweit! + + // Operations on per user-group basis + if ($operation['opmessage_grp']) { + $usergroups = array_map(function($item) {return $item['usrgrpid'];}, $operation['opmessage_grp']); + + // Multiple user groups can be specified per operation + //TODO: Merge branches + foreach ($usergroups as $usergroup) { + $users = API::UserGroup()->get(array( + 'usrgrpids' => $usergroup, + 'selectUsers' => API_OUTPUT_EXTEND + )); + $users = reset($users); + foreach ($users['users'] as $user) { + //TODO: setHint? -- Geht scheinbar bei Spans + //TODO: Darstellungscode wie in include/views/configuration.action.edit.php:350? + //TODO: sortOperations()? + //TODO: Das ist einfach falsch. Er liefert pro Zeile mehr als eine Operation zurück bzw. löst die Gruppen nicht auf. + //TODO: Entweder muss man sich das alles woanders holen, oder was weiß ich! Im Prinzip ist es nur eine Darstellungsfrage. + $permissionError = $this->checkPermission($user['userid']); + + $mediainfos = $this->addMessageAlert($user['userid'], $operation['opmessage']['mediatypeid'], $time); + foreach ($mediainfos as $mediainfo) { + if ($permissionError) { + $error = $permissionError; + } + elseif (isset($mediainfo['error'])) { + $error = $mediainfo['error']; + } + else { + $error = null; + } + $results = array( + 'time' => $time, + 'step' => $step, + 'alias' => $user['alias'], + 'userid' => $user['userid'], + 'opdesc' => _('User group'), + 'mediatype' => $mediainfo['mediatypeid'], + 'sendto' => (isset($mediainfo['sendto'])) ? $mediainfo['sendto'] : null, + 'error' => $error + ); + $this->actions[$operation['actionid']]['operations'][$operation['operationid']]['resultMessages'][] = $results; + } + } + } + } + + //Operations on per-user basis + if ($operation['opmessage_usr']) { + $users = array_map(function($item) {return $item['userid'];}, $operation['opmessage_usr']); + + //Multiple users can be specified per operation + foreach ($users as $user) { + $users = API::User()->get(array( + 'userids' => $user, + 'output' => API_OUTPUT_EXTEND + )); + + foreach ($users as $user) { + + $permissionError = $this->checkPermission($user['userid']); + +#TODO: Unite if-branches for groups/user + $mediainfos = $this->addMessageAlert($user['userid'], $operation['opmessage']['mediatypeid'], $time); + foreach ($mediainfos as $mediainfo) { +#TODO: Unset or null for empty results? + if ($permissionError) { + $error = $permissionError; + } + elseif (isset($mediainfo['error'])) { + $error = $mediainfo['error']; + } + else { + $error = null; + } + + $results = array( + 'time' => $time, + 'step' => $step, + 'alias' => $user['alias'], + 'userid' => $user['userid'], + 'opdesc' => _('User'), + 'mediatype' => $mediainfo['mediatypeid'], + 'sendto' => (isset($mediainfo['sendto'])) ? $mediainfo['sendto'] : null, + 'error' => $error + ); + $this->actions[$operation['actionid']]['operations'][$operation['operationid']]['resultMessages'][] = $results; + } + } + } + } + } + +/** + * Resolves effective media types for users and user groups, + * if no specific media type was specified + * + * @param int $userid Id of the user that should be notified + * @param int $mediatypeid Id of the mediatype that should be used + * @param int $time Time where the notification should be sent at + * + * @result array Array of media details for a particular operation + */ +#TODO: Change name! +protected function addMessageAlert($userid, $mediatypeid, $time) { + // Possible reasons to fail: + // System -- per user + // Host -- per trigger + // Severity -- per trigger + // Time -- per trigger time and media settings + // Media not defined + // Medium inactive + + $result = array(); + +# Global media type status can only be obtained here(?) + $mediatypes = API::Mediatype()->get(array( + 'output' => API_OUTPUT_EXTEND, + 'preservekeys' => '1' + )); + + if ($mediatypeid == 0) { + //No specific media type given + //This API call ignores system-wide settings for media types + $usermedias = API::UserMedia()->get(array( + 'userids' => $userid, + 'output' => API_OUTPUT_EXTEND + )); + } + else { + //Specific media type given + $usermedias = API::UserMedia()->get(array( + 'userids' => $userid, + 'mediatypeids' => $mediatypeid, + 'output' => API_OUTPUT_EXTEND + )); + } + + if ($usermedias) { + //Compare severity and activity setting for each active user media intended for notification + foreach ($usermedias as $usermedia) { + $mediadetails = array(); + //TODO: Seems like this can still be empty on occasions, see different_machine, + //Two-items trigger, different machines OR; Admin SMS -- Lost context of this comment! + $mediadetails['sendto'] = $usermedia['sendto']; + $mediadetails['mediatypeid'] = $usermedia['mediatypeid']; + + #TODO: Some errors could be aggregated and only returned once + if ($mediatypes[$usermedia['mediatypeid']]['status'] == MEDIA_TYPE_STATUS_DISABLED) { + $mediadetails['error'] = 'Media type disabled system-wide'; + $result[] = $mediadetails; + continue; + } + + if ($usermedia['active'] == MEDIA_STATUS_DISABLED) { + //Debug + $mediadetails['error'] = 'Media type disabled for user'; + $result[] = $mediadetails; + continue; + } + + //Check for matching severity settings + if((1 << $this->trigger['priority'] & $usermedia['severity']) == 0) { + $mediadetails['error'] = 'Severity doesn\'t match media type settings'; + $result[] = $mediadetails; + continue; + } + + $periods = explode(';', $usermedia['period']); + $period_match = false; + foreach ($periods as $period) { + // One period must match to succeed + if (CheckTimePeriod($period, $time)) { + $period_match = true; + $result[] = $mediadetails; + break; + } + else { + $mediadetails['error'] = 'Time doesn\'t match media type settings'; + $result[] = $mediadetails; + continue; + } + } + + } + } + else { + $mediadetails = array(); + $mediadetails['error'] = 'Media type not defined for user'; + $mediadetails['mediatypeid'] = $mediatypeid; + $result[] = $mediadetails; + } + return $result; +} + + /** + * Make sure host is readable to the specified user + * + * @param int $userid userid of the user in question + * @param int $hostid hostid of the host in question + * + * @return bool + * true -- User has read access to the host + * false -- User has no read access to the host + */ + function getHostPermissions($userid, $hostid) { + + // Is this nessary? + $user = API::User()->get(array( + 'userids' => $userid, + 'output' => API_OUTPUT_EXTEND + )); + $user = reset($user); + + if ($user['type'] == USER_TYPE_SUPER_ADMIN) { + $ret = true; + return $ret; + } + + $sql = 'SELECT MIN(r.permission) AS min'. + ' FROM rights r, hosts_groups hg, users_groups ug'. + ' WHERE r.groupid=ug.usrgrpid'. + ' AND r.id=hg.groupid'. + ' AND hg.hostid='.$hostid. + ' AND ug.userid='.$userid; + $permission = DBfetch(DBselect($sql)); + if ($permission AND $permission['min'] > PERM_DENY) { + $ret = true; + } + else { + $ret = false; + } + + return $ret; + } +} diff --git a/include/classes/core/ZBase.php b/include/classes/core/ZBase.php index 325611f..e5d526d 100644 --- a/include/classes/core/ZBase.php +++ b/include/classes/core/ZBase.php @@ -120,6 +120,7 @@ class ZBase { $this->rootDir.'/include/classes/helpers/trigger', $this->rootDir.'/include/classes/tree', $this->rootDir.'/include/classes/parsers', + $this->rootDir.'/include/classes/actions', $this->rootDir.'/api/classes', $this->rootDir.'/api/rpc' ); diff --git a/include/classes/tree/CActionTree.php b/include/classes/tree/CActionTree.php new file mode 100644 index 0000000..22480e4 --- /dev/null +++ b/include/classes/tree/CActionTree.php @@ -0,0 +1,60 @@ + +tree[$rowId][$colName]) && $this->tree[$rowId]['id'] > 0) { + // $status = $this->tree[$rowId][$colName]; + + // if (in_array($status, array(TRIGGER_SEVERITY_INFORMATION, TRIGGER_SEVERITY_NOT_CLASSIFIED))) { + // $this->tree[$rowId][$colName] = new CSpan(_('OK'), 'green'); + // } + // else { + // $this->tree[$rowId][$colName] = getSeverityCaption($status); + // $class = getSeverityStyle($status); + // } + //} + + $col = parent::makeCol($rowId, $colName); + $col->addClass($class); + + return $col; + } + +} + diff --git a/include/defines.inc.php b/include/defines.inc.php index 90ee055..9d88eb6 100644 --- a/include/defines.inc.php +++ b/include/defines.inc.php @@ -408,6 +408,9 @@ define('TRIGGER_VALUE_UNKNOWN', 2); // only in "events" table define('TRIGGER_VALUE_FLAG_NORMAL', 0); define('TRIGGER_VALUE_FLAG_UNKNOWN', 1); +define('TRIGGER_FLAGS_PLAIN', 0); +define('TRIGGER_FLAGS_DISCOVERED', 4); + define('TRIGGER_VALUE_CHANGED_NO', 0); define('TRIGGER_VALUE_CHANGED_YES', 1); @@ -458,6 +461,11 @@ define('ACTION_DEFAULT_MSG_DISCOVERY', "Discovery rule: {DISCOVERY.RULE.NAME}\n\ "Device service uptime: {DISCOVERY.SERVICE.UPTIME}" ); +define('SUCCEED', 1); +define('FAIL', 0); +define('MEDIA_STATUS_ACTIVE', 0); +define('MEDIA_STATUS_DISABLED', 1); + define('ACTION_STATUS_ENABLED', 0); define('ACTION_STATUS_DISABLED', 1); diff --git a/include/menu.inc.php b/include/menu.inc.php index 78264de..63a141e 100644 --- a/include/menu.inc.php +++ b/include/menu.inc.php @@ -174,6 +174,7 @@ $ZBX_MENU = array( 'tr_logform.php', 'tr_testexpr.php', 'popup_trexpr.php', + 'popup_action.php', 'host_discovery.php', 'disc_prototypes.php', 'trigger_prototypes.php' diff --git a/include/views/configuration.triggers.actions.php b/include/views/configuration.triggers.actions.php new file mode 100644 index 0000000..3ac961f --- /dev/null +++ b/include/views/configuration.triggers.actions.php @@ -0,0 +1,430 @@ +setName('frmAction'); +$frmAction->addVar('dstfrm', $_REQUEST['dstfrm']); +$frmAction->addVar('triggerid', $_REQUEST['triggerid']); + +$description = array(); + +// Copied from include/views/configuration.triggers.list.php +$dependencies = $trigger['dependencies']; +if (count($dependencies) > 0) { + foreach ($dependencies as $dep_trigger) { + + $db_hosts = get_hosts_by_triggerid($dep_trigger['triggerid']); + while ($host = DBfetch($db_hosts)) { + $description[] = $host['name']; + $description[] = ', '; + } + array_pop($description); + $description[] = ' : '; + $description[] = $dep_trigger['description']; + $description[] = BR(); + } +} + +$triggerTable = new CTableInfo(_('No trigger -- something went wrong')); +$notificationTable = new CTableInfo(_('No notifications')); +$debug_messagingTable = new CTableInfo(_('No messaging considered')); +$commandTable = new CTableInfo(_('No remote commands')); +$debugTable = new CTableInfo(_('No active actions found')); + +$calendarIcon = new CImg('images/general/bar/cal.gif', 'calendar', 16, 12, 'pointer'); + +#TODO: Validierung, dass Ende>Anfang ist, macht im Original nur PHP +if (isset($_REQUEST['trigger_at'])) { + $year = get_request('trigger_at_year'); + $month = get_request('trigger_at_month'); + $day = get_request('trigger_at_day'); + $hours = get_request('trigger_at_hour'); + $minutes = get_request('trigger_at_minute'); +} +elseif (isset($this->data['trigger_at'])) { + $year = date('Y', $this->data['trigger_at']); + $month = date('m', $this->data['trigger_at']); + $day = date('d', $this->data['trigger_at']); + $hours = date('H', $this->data['trigger_at']); + $minutes = date('i', $this->data['trigger_at']); +} +else { + $year = ''; + $month = ''; + $day = ''; + $hours = ''; + $minutes = ''; +} + +$frmAction->addVar('trigger_at', $year.$month.$day.$hours.$minutes); + +$triggerAtDay = new CNumericBox('trigger_at_day', $day, 2); +$triggerAtDay->setAttribute('placeholder', _('dd')); +$triggerAtMonth = new CNumericBox('trigger_at_month', $month, 2); +$triggerAtMonth->setAttribute('placeholder', _('MM')); +$triggerAtYear = new CNumericBox('trigger_at_year', $year, 4); +$triggerAtYear->setAttribute('placeholder', _('yyyy')); +$triggerAtHour = new CNumericBox('trigger_at_hour', $hours, 2); +$triggerAtHour->setAttribute('placeholder', _('hh')); +$triggerAtMinute = new CNumericBox('trigger_at_minute', $minutes, 2); +$triggerAtMinute->setAttribute('placeholder', _('mm')); + +if (isset($_REQUEST['ack_at'])) { + $year = get_request('ack_at_year'); + $month = get_request('ack_at_month'); + $day = get_request('ack_at_day'); + $hours = get_request('ack_at_hour'); + $minutes = get_request('ack_at_minute'); +} +elseif (isset($this->data['ack_at'])) { + $year = date('Y', $this->data['ack_at']); + $month = date('m', $this->data['ack_at']); + $day = date('d', $this->data['ack_at']); + $hours = date('H', $this->data['ack_at']); + $minutes = date('i', $this->data['ack_at']); +} +else { + $year = ''; + $month = ''; + $day = ''; + $hours = ''; + $minutes = ''; +} +$frmAction->addVar('ack_at', $year.$month.$day.$hours.$minutes); + +$ackAtDay = new CNumericBox('ack_at_day', $day, 2); +$ackAtDay->setAttribute('placeholder', _('dd')); +$ackAtMonth = new CNumericBox('ack_at_month', $month, 2); +$ackAtMonth->setAttribute('placeholder', _('MM')); +$ackAtYear = new CNumericBox('ack_at_year', $year, 4); +$ackAtYear->setAttribute('placeholder', _('yyyy')); +$ackAtHour = new CNumericBox('ack_at_hour', $hours, 2); +$ackAtHour->setAttribute('placeholder', _('hh')); +$ackAtMinute = new CNumericBox('ack_at_minute', $minutes, 2); +$ackAtMinute->setAttribute('placeholder', _('mm')); + +if (isset($_REQUEST['res_at'])) { + $year = get_request('res_at_year'); + $month = get_request('res_at_month'); + $day = get_request('res_at_day'); + $hours = get_request('res_at_hour'); + $minutes = get_request('res_at_minute'); +} +elseif (isset($this->data['res_at'])) { + $year = date('Y', $this->data['res_at']); + $month = date('m', $this->data['res_at']); + $day = date('d', $this->data['res_at']); + $hours = date('H', $this->data['res_at']); + $minutes = date('i', $this->data['res_at']); +} +else { + $year = ''; + $month = ''; + $day = ''; + $hours = ''; + $minutes = ''; +} +$frmAction->addVar('res_at', $year.$month.$day.$hours.$minutes); + +$resAtDay = new CNumericBox('res_at_day', $day, 2); +$resAtDay->setAttribute('placeholder', _('dd')); +$resAtMonth = new CNumericBox('res_at_month', $month, 2); +$resAtMonth->setAttribute('placeholder', _('MM')); +$resAtYear = new CNumericBox('res_at_year', $year, 4); +$resAtYear->setAttribute('placeholder', _('yyyy')); +$resAtHour = new CNumericBox('res_at_hour', $hours, 2); +$resAtHour->setAttribute('placeholder', _('hh')); +$resAtMinute = new CNumericBox('res_at_minute', $minutes, 2); +$resAtMinute->setAttribute('placeholder', _('mm')); + +$ackOption = new CCheckbox('maint', $_REQUEST['maint'], null, 1); + +#TODO: Die drei könnte man mit einer Schleife anlegen +$maintenanceFormList = new CFormList('maintenanceFormList'); + +# Create three JS calendars; When one is opened, the others disappear. +$calendarIcon->addAction('onclick', 'javascript: var pos = getPosition(this); pos.top += 10; pos.left += 16; CLNDR["trigger_at"].clndr.clndrshow(pos.top, pos.left); CLNDR["ack_at"].clndr.clndrhide(); CLNDR["res_at"].clndr.clndrhide();'); +$maintenanceFormList->addRow(_('Status change at'), array($triggerAtDay, '/', $triggerAtMonth, '/', $triggerAtYear, SPACE, $triggerAtHour, ':', $triggerAtMinute, $calendarIcon)); +zbx_add_post_js('create_calendar(null, ["trigger_at_day", "trigger_at_month", "trigger_at_year", "trigger_at_hour", "trigger_at_minute"], "trigger_at", "trigger_at");'); + +$calendarIcon->addAction('onclick', 'javascript: var pos = getPosition(this); pos.top += 10; pos.left += 16; CLNDR["ack_at"].clndr.clndrshow(pos.top, pos.left); CLNDR["trigger_at"].clndr.clndrhide(); CLNDR["res_at"].clndr.clndrhide();'); +$maintenanceFormList->addRow(_('Acknowledged at'), array($ackAtDay, '/', $ackAtMonth, '/', $ackAtYear, SPACE, $ackAtHour, ':', $ackAtMinute, $calendarIcon)); +zbx_add_post_js('create_calendar(null, ["ack_at_day", "ack_at_month", "ack_at_year", "ack_at_hour", "ack_at_minute"], "ack_at", "ack_at");'); + +$calendarIcon->addAction('onclick', 'javascript: var pos = getPosition(this); pos.top += 10; pos.left += 16; CLNDR["res_at"].clndr.clndrshow(pos.top, pos.left); CLNDR["trigger_at"].clndr.clndrhide(); CLNDR["ack_at"].clndr.clndrhide();'); +$maintenanceFormList->addRow(_('Resolved at'), array($resAtDay, '/', $resAtMonth, '/', $resAtYear, SPACE, $resAtHour, ':', $resAtMinute, $calendarIcon)); +zbx_add_post_js('create_calendar(null, ["res_at_day", "res_at_month", "res_at_year", "res_at_hour", "res_at_minute"], "res_at", "res_at");'); + +# Trigger status form +$eventTypeRadioButton = new CRadioButtonList('triggerStatus', $triggerStatus); +$eventTypeRadioButton->makeVertical(); +$eventTypeRadioButton->addValue(SPACE._('Problem').SPACE, TRIGGER_VALUE_TRUE); +$eventTypeRadioButton->addValue(SPACE._('OK').SPACE, TRIGGER_VALUE_FALSE); + + +# All hosts included in the trigger +#TODO: What happens with grp items? +foreach ($trigger['hosts'] as $host) { + $hostnames[] = $host['host']; + $hostnames[] = BR(); +} + +if ($trigger['type'] == TRIGGER_MULT_EVENT_ENABLED) { + $triggerTable->addRow(array(_('General'), + bold(_('Multiple events configured for this trigger -- Showing only one!')))); +} + +# Assemble the form table +$triggerTable->addRow(array(_('Hosts'), $hostnames)); +$triggerTable->addRow(array(_('Trigger'), $trigger['description'])); +$triggerTable->addRow(array(_('Severity'), getSeverityCell($trigger['priority']))); + +#TODO: This is an example of an element that doesn't have a value: An empty array() +if ($trigger['dependencies']) { + $triggerTable->addRow(array(_('Depends on'), $description)); +} +$triggerTable->addRow(array(_('Status'), $eventTypeRadioButton)); +//TODO: Phase out, replace with real periods; The semantics are not clear right away anyway, as soon as multiple hosts are involved and one of them is in maintenance. +$triggerTable->addRow(array(_('Assume maintenance'), new CCheckbox('maint', $_REQUEST['maint'], null, 1))); +$triggerTable->addRow(array(_('Respect acknowledgement'), new CCheckbox('ack', $_REQUEST['ack'], null, 1))); +$triggerTable->addRow(array("Temporal settings", $maintenanceFormList)); +if ($description) { +} + +$mediatypes = API::Mediatype()->get(array( + 'output' => array('description'), + 'preservekeys' => '1' + )); +//TODO: Formatter functions +//TODO: Hyperlink to actions? +//TODO: Mouse-over? +//TODO: Error could be a mouse-over on "alias" +$notificationTable->setHeader(array( + _('Alias'), + _('Media type'), + _('Send To'), + _('Time passed'), + _('Time'), + _('Action name'), + _('Step'), + _('Operation'), + _('Step config'), + _('Error') + )); + +$commandTable->setHeader(array( + _('Time passed'), + _('Time'), + _('Action name'), + _('Step'), + _('Operation'), + _('Step config'), + _('Command'), + _('Type') + )); + +$debugTable->setHeader(array( + _('Action name'), + _('Eval type'), + _('Condition type'), + _('Trigger Value'), + _('Operator'), + _('Condition Value') + )); + +# Debugging tree loop +foreach ($simulationResults as $action) { + if ($action['match']) { + //That appears to be swapped, but it is not + $actionStatus = new CSpan($action['name'], 'off'); + } + else { + $actionStatus = new CSpan($action['name'], 'on'); + } + + //At least one condition is defined for the action + if (!empty($action['conditions'])) { + $lastProcessedConditionType = null; + + // Condition loop + foreach ($action['conditions'] as $condition) { + if ($condition['match']) { + $conditiontype = new CSpan(condition_type2str($condition['conditiontype']), 'off'); + } + else { + $conditiontype = new CSpan(condition_type2str($condition['conditiontype']), 'on'); + } + + //Host groups and templates are arrays + if (is_array($condition['actualvalue'])) { + $actualvalue = implode(', ', $condition['actualvalue']); + } + else { + $actualvalue = $condition['actualvalue']; + } + + //TODO: Beautify + // Maintenance uses NULL, which looks weird in the debugging table + if ($condition['conditiontype'] == CONDITION_TYPE_MAINTENANCE) { + if (is_null($condition['value'])) { + $condition['value'] = HOST_MAINTENANCE_STATUS_ON; + } + else { + $condition['value'] = HOST_MAINTENANCE_STATUS_OFF; + } + } +#TODO: Create a table per action? + + $debugTable->addRow(array( + $actionStatus, + action_evaltype2str($action['evaltype']), + $conditiontype, + $actualvalue, + condition_operator2str($condition['operator']), + $condition['value'] + )); + } + } + else { + // Actions without conditions always apply + $actionStatus = new CSpan($action['name'], 'off'); + + $debugTable->addRow(array( + $actionStatus, + '-', + '-', + '-', + '-', + '-', + )); + } +} + +// Get disabled actions +$options= array( + 'output' => API_OUTPUT_EXTEND, + 'filter' => array( + 'status' => array(ACTION_STATUS_DISABLED), + 'eventsource' => EVENT_SOURCE_TRIGGERS + ) + ); +$disabledActions = API::Action()->get($options); +foreach ($disabledActions as $action) { + $actionStatus = new CSpan($action['name'], 'on'); + $debugTable->addRow(array( + $actionStatus, + _('Disabled'), + '-', + '-', + '-', + '-', + )); +} + +// Loop through all results and populate tables +foreach ($simulationResults as $action) { + if ($action['match']) { + //The following line's off/on appears to be swapped, but it is not + $actionStatus = new CSpan($action['name'], 'off'); + + foreach ($action['operations'] as $operation) { + $esc_steps_txt = ($operation['esc_step_from'] == $operation['esc_step_to']) + ? $operation['esc_step_from'] + : $operation['esc_step_from'].' - '.$operation['esc_step_to']; + + if (array_key_exists('resultMessages', $operation)) { + //array_multisort($conditiontypes, SORT_ASC, $action['conditions']); + foreach ($operation['resultMessages'] as $message) { +#TODO: Colour lines with error or put them in a separate table again +#TODO: Sorting! + //All defined media types + if ($message['mediatype'] == 0) { + $mediatype = 'Any'; + } + #TODO: Does this case exist? + elseif ($message['mediatype'] === null) { + $mediatype = '-'; + } + else { + $mediatype = $mediatypes[$message['mediatype']]['description']; + } + + $notificationTable->addRow(array( + $message['alias'], + $mediatype, + ($message['sendto']) ? $message['sendto'] : '-', + zbx_date2age($message['time'], $timeStart), + zbx_date2str(_('D, H:i:s'), $message['time']), + bold($action['name']), + $message['step'], + $message['opdesc'], + $esc_steps_txt, + ($message['error']) ? $message['error'] : '-', + )); + } + } + + if (array_key_exists('resultCommands', $operation)) { + foreach ($operation['resultCommands'] as $command) { + $commandTable->addRow(array( + zbx_date2age($command['time'], $timeStart), + zbx_date2str(_('D, H:i:s'), $command['time']), + bold($action['name']), + $command['step'], + $command['opdesc'], + $esc_steps_txt, + $command['command'], + operation_scripttype2str($command['type']) + )); + } + } + } + } +} + +$triggerWidget = new CUIWidget('trigger', $triggerTable); +$triggerWidget->setHeader(_('Trigger details')); + +$notificationWidget = new CUIWidget('notifications', $notificationTable, 1, 1); +$notificationWidget->setHeader(_('Notifications')); + +$commandWidget = new CUIWidget('commands', $commandTable, 1, 1); +$commandWidget->setHeader(_('Remote Commands')); + +$debugWidget = new CUIWidget('action_debug', $debugTable, 1, 1); +$debugWidget->setHeader(_('Action condition matches')); + +$div = new CDiv(array($triggerWidget, $notificationWidget, $commandWidget, $debugWidget), 'column'); +$frmAction->addRow($div); + +$frmAction->addItemToBottomRow(array( + new CSubmit('re_eval', _('Re-evaluate')), + new CButton('close', _('Close'),'javascript: self.close();') + )); + +return $frmAction; diff --git a/include/views/configuration.triggers.list.php b/include/views/configuration.triggers.list.php index baa60f3..5da7a21 100644 --- a/include/views/configuration.triggers.list.php +++ b/include/views/configuration.triggers.list.php @@ -18,7 +18,6 @@ ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. **/ - $triggersWidget = new CWidget(); // append host summary to widget header @@ -87,6 +86,23 @@ if (!empty($this->data['parent_discoveryid'])) { $link->setArgument('hostid', $this->data['hostid']); $link = $link->getUrl(); +global $USER_DETAILS; +$superAdmin = in_array($USER_DETAILS['type'], array( + USER_TYPE_SUPER_ADMIN +)); + +// Only draw and populate column if at least one monitored host is included +// Hidden for templates +$monitoredHosts = false; +foreach ($this->data['triggers'] as $triggers) { + foreach ($triggers['hosts'] as $hostid => $host) { + if (HOST_STATUS_MONITORED == $host['status']) { + $monitoredHosts = true; + break 2; + } + } +} + $triggersTable = new CTableInfo(_('No triggers defined.')); $triggersTable->setHeader(array( new CCheckBox('all_triggers', null, "checkAll('".$triggersForm->getName()."', 'all_triggers', 'g_triggerid');"), @@ -95,7 +111,8 @@ $triggersTable->setHeader(array( make_sorting_header(_('Name'), 'description', $link), _('Expression'), make_sorting_header(_('Status'), 'status', $link), - empty($this->data['parent_discoveryid']) ? _('Error') : null + empty($this->data['parent_discoveryid']) ? _('Error') : null, + ($superAdmin && $monitoredHosts) ? _('Action?') : null )); foreach ($this->data['triggers'] as $tnum => $trigger) { $triggerid = $trigger['triggerid']; @@ -192,6 +209,16 @@ foreach ($this->data['triggers'] as $tnum => $trigger) { $status = new CLink(_('Enabled'), $status_link, 'enabled'); } + if ($superAdmin) { + if ($trigger['status'] == TRIGGER_STATUS_ENABLED && !$templated) { + $actionTest = new CButton('actions', _('Actions?'), 'return PopUp("popup_action.php?dstfrm=action&triggerid='.$triggerid.'", 700, 500);', 'link_menu'); + } + # Trigger disabled, column might be shown or not, depending on selected hosts + else { + $actionTest = ''; + } + } + $hosts = null; if (empty($this->data['hostid'])) { foreach ($trigger['hosts'] as $hostid => $host) { @@ -215,7 +242,8 @@ foreach ($this->data['triggers'] as $tnum => $trigger) { $description, $expressionColumn, $status, - $error + $error, + ($superAdmin && $monitoredHosts) ? $actionTest : null )); $triggers[$tnum] = $trigger; } diff --git a/popup_action.php b/popup_action.php new file mode 100644 index 0000000..85a00b3 --- /dev/null +++ b/popup_action.php @@ -0,0 +1,167 @@ + array(T_ZBX_STR, O_MAND,P_SYS, NOT_EMPTY, null), + 'triggerid'=> array(T_ZBX_INT, O_MAND,P_SYS, NOT_EMPTY, null), + 'triggerStatus'=> array(T_ZBX_INT, O_OPT, P_SYS, IN('0,1'), null), + 'maint'=> array(T_ZBX_INT, O_OPT, P_SYS, IN('0,1'), null), + 'ack'=> array(T_ZBX_INT, O_OPT, P_SYS, IN('0,1'), null), + 'close'=> array(T_ZBX_INT, O_OPT, null, IN('1'), null), + 'open'=> array(T_ZBX_INT, O_OPT, null, IN('1'), null), + 'trigger_at' => array(T_ZBX_STR, O_OPT, null, NOT_EMPTY, null), + 'trigger_at_day' => array(T_ZBX_STR, O_OPT, null, NOT_EMPTY, null), + 'trigger_at_month' => array(T_ZBX_STR, O_OPT, null, NOT_EMPTY, null), + 'trigger_at_year' => array(T_ZBX_STR, O_OPT, null, NOT_EMPTY, null), + 'trigger_at_hour' => array(T_ZBX_STR, O_OPT, null, NOT_EMPTY, null), + 'trigger_at_minute' => array(T_ZBX_STR, O_OPT, null, NOT_EMPTY, null), + 'ack_at' => array(T_ZBX_STR, O_OPT, null, null, null), + 'ack_at_day' => array(T_ZBX_STR, O_OPT, null, null, null), + 'ack_at_month' => array(T_ZBX_STR, O_OPT, null, null, null), + 'ack_at_year' => array(T_ZBX_STR, O_OPT, null, null, null), + 'ack_at_hour' => array(T_ZBX_STR, O_OPT, null, null, null), + 'ack_at_minute' => array(T_ZBX_STR, O_OPT, null, null, null), + 'res_at' => array(T_ZBX_STR, O_OPT, null, null, null), + 'res_at_day' => array(T_ZBX_STR, O_OPT, null, null, null), + 'res_at_month' => array(T_ZBX_STR, O_OPT, null, null, null), + 'res_at_year' => array(T_ZBX_STR, O_OPT, null, null, null), + 'res_at_hour' => array(T_ZBX_STR, O_OPT, null, null, null), + 'res_at_minute' => array(T_ZBX_STR, O_OPT, null, null, null), + +/* other */ + 'form'=> array(T_ZBX_STR, O_OPT, P_SYS, NULL, NULL), +); +#TODO: Try using ({res_at}), possibly connected to a checkbox + +check_fields($fields); + +#TODO: Store triggerid in _REQUEST so that reoloading the page succeeds +$triggerid = get_request('triggerid'); + +//Wenn 0 oder null +if (!array_key_exists('triggerStatus', $_REQUEST)) { + $_REQUEST['triggerStatus'] = TRIGGER_VALUE_TRUE; +} + +if (!array_key_exists('maint', $_REQUEST)) { + $_REQUEST['maint'] = 0; +} +if (!array_key_exists('ack', $_REQUEST)) { + $_REQUEST['ack'] = 0; +} + +$trigger = API::Trigger()->get(array( + 'triggerids' => $triggerid, + 'expandDescription' => '1', + 'output' => API_OUTPUT_EXTEND, + 'selectGroups' => API_OUTPUT_EXTEND, + 'selectHosts' => API_OUTPUT_EXTEND, + 'selectItems' => API_OUTPUT_REFER, + 'selectFunctions' => API_OUTPUT_EXTEND, + 'selectDependencies' => API_OUTPUT_EXTEND +)); +$trigger = reset($trigger); + +foreach ($trigger['hosts'] as $host) { + $hostids[] = $host['hostid']; +} + +#TODO: Add ack time and unify naming schema for times +$options = array( + 'triggerids' => array($triggerid), + 'timeAck' => get_request('ack_at'), + 'triggerStatus' => $_REQUEST['triggerStatus'], + 'maint' => true +); +#TODO: Review request style +if (isset($_REQUEST['trigger_at'])) { + $options['timeStart'] = mktime( + $_REQUEST['trigger_at_hour'], + $_REQUEST['trigger_at_minute'], + 0, + $_REQUEST['trigger_at_month'], + $_REQUEST['trigger_at_day'], + $_REQUEST['trigger_at_year']); +} +else { + $options['timeStart'] = time(); +} + +#TODO: Throws an error on re-evaluate, as it's empty in the first place +if (isset($_REQUEST['ack_at'])) { + $options['ack_at'] = mktime( + $_REQUEST['ack_at_hour'], + $_REQUEST['ack_at_minute'], + 0, + $_REQUEST['ack_at_month'], + $_REQUEST['ack_at_day'], + $_REQUEST['ack_at_year']); +} + +if (isset($_REQUEST['res_at'])) { + $options['timeEnd'] = mktime( + $_REQUEST['res_at_hour'], + $_REQUEST['res_at_minute'], + 0, + $_REQUEST['res_at_month'], + $_REQUEST['res_at_day'], + $_REQUEST['res_at_year']); +} +else { + // 2 hours runtime default + $options['timeEnd'] = $options['timeStart'] + 2 * SEC_PER_HOUR; +} + +$simulationResults = API::Action()->run($options); + +#TODO: That doesn't do a thing +//order_result($simulationResults, 'step', ZBX_SORT_UP); + +$data = array( + 'trigger' => $trigger, + 'trigger_at' => $options['timeStart'], + 'res_at' => $options['timeEnd'], + 'triggerStatus' => $_REQUEST['triggerStatus'], + 'simulationResults' => $simulationResults, +); +$actionSimView = new CView('configuration.triggers.actions', $data); +$actionSimView->render(); +$actionSimView->show(); +include_once dirname(__FILE__).'/include/page_footer.php';