['min_user_type' => USER_TYPE_ZABBIX_USER], 'getsli' => ['min_user_type' => USER_TYPE_ZABBIX_USER], 'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN, 'action' => CRoleHelper::ACTIONS_MANAGE_SLA], 'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN, 'action' => CRoleHelper::ACTIONS_MANAGE_SLA], 'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN, 'action' => CRoleHelper::ACTIONS_MANAGE_SLA] ]; protected $tableName = 'sla'; protected $tableAlias = 'sla'; protected $sortColumns = ['slaid', 'name', 'period', 'slo', 'effective_date', 'timezone', 'status', 'description']; /** * @param array $options * * @throws APIException * * @return array|string */ public function get(array $options = []) { $api_input_rules = ['type' => API_OBJECT, 'fields' => [ // filter 'slaids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 'evaltype' => ['type' => API_INT32, 'in' => implode(',', [TAG_EVAL_TYPE_AND_OR, TAG_EVAL_TYPE_OR]), 'default' => TAG_EVAL_TYPE_AND_OR], 'service_tags' => ['type' => API_OBJECTS, 'default' => [], 'fields' => [ 'tag' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED], 'value' => ['type' => API_STRING_UTF8], 'operator' => ['type' => API_INT32, 'in' => implode(',', [TAG_OPERATOR_LIKE, TAG_OPERATOR_EQUAL, TAG_OPERATOR_NOT_LIKE, TAG_OPERATOR_NOT_EQUAL, TAG_OPERATOR_EXISTS, TAG_OPERATOR_NOT_EXISTS])] ]], 'serviceids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 'filter' => ['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['slaid', 'name', 'period', 'slo', 'effective_date', 'timezone', 'status']], 'search' => ['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['name', 'timezone', 'description']], 'searchByAny' => ['type' => API_BOOLEAN, 'default' => false], 'startSearch' => ['type' => API_FLAG, 'default' => false], 'excludeSearch' => ['type' => API_FLAG, 'default' => false], 'searchWildcardsEnabled' => ['type' => API_BOOLEAN, 'default' => false], // output 'output' => ['type' => API_OUTPUT, 'in' => implode(',', ['slaid', 'name', 'period', 'slo', 'effective_date', 'timezone', 'status', 'description']), 'default' => API_OUTPUT_EXTEND], 'countOutput' => ['type' => API_FLAG, 'default' => false], 'selectServiceTags' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL | API_ALLOW_COUNT, 'in' => implode(',', ['tag', 'operator', 'value']), 'default' => null], 'selectSchedule' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL | API_ALLOW_COUNT, 'in' => implode(',', ['period_from', 'period_to']), 'default' => null], 'selectExcludedDowntimes' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL | API_ALLOW_COUNT, 'in' => implode(',', ['name', 'period_from', 'period_to']), 'default' => null], // sort and limit 'sortfield' => ['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', ['slaid', 'name', 'period', 'slo', 'effective_date', 'timezone', 'status', 'description']), 'uniq' => true, 'default' => []], 'sortorder' => ['type' => API_SORTORDER, 'default' => []], 'limit' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null], // flags 'editable' => ['type' => API_BOOLEAN, 'default' => false], 'preservekeys' => ['type' => API_BOOLEAN, 'default' => false] ]]; if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $accessible_slaids = self::getAccessibleSlaids($options['slaids'], $options['serviceids']); $count_output = $options['countOutput']; if ($count_output) { $options['output'] = ['slaid']; $options['countOutput'] = false; } $resource = DBselect($this->createSelectQuery('sla', $options)); $db_slas = []; while (($options['limit'] === null || count($db_slas) < $options['limit']) && $row = DBfetch($resource)) { if ($accessible_slaids !== null && !array_key_exists($row['slaid'], $accessible_slaids)) { continue; } $db_slas[$row['slaid']] = $row; } if ($count_output) { return (string) count($db_slas); } if ($db_slas) { $db_slas = $this->addRelatedObjects($options, $db_slas); $db_slas = $this->unsetExtraFields($db_slas, ['slaid'], $options['output']); if (!$options['preservekeys']) { $db_slas = array_values($db_slas); } } return $db_slas; } /** * @param array $slas * * @throws APIException * * @return array */ public function create(array $slas): array { self::validateCreate($slas); $ins_slas = []; foreach ($slas as $sla) { unset($sla['service_tags'], $sla['schedule'], $sla['excluded_downtimes']); $ins_slas[] = $sla; } $slaids = DB::insert('sla', $ins_slas); foreach ($slas as $index => &$sla) { $sla['slaid'] = $slaids[$index]; } unset($sla); self::updateServiceTags($slas); self::updateSchedule($slas); self::updateExcludedDowntimes($slas); self::addAuditLog(CAudit::ACTION_ADD, CAudit::RESOURCE_SLA, $slas); return ['slaids' => $slaids]; } /** * @param array $slas * * @throws APIException */ private static function validateCreate(array &$slas): void { $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [ 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('sla', 'name')], 'period' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ZBX_SLA_PERIOD_DAILY, ZBX_SLA_PERIOD_WEEKLY, ZBX_SLA_PERIOD_MONTHLY, ZBX_SLA_PERIOD_QUARTERLY, ZBX_SLA_PERIOD_ANNUALLY])], 'slo' => ['type' => API_FLOAT, 'flags' => API_REQUIRED, 'in' => '0:100'], 'effective_date' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_DATE], 'timezone' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'in' => implode(',', array_merge([ZBX_DEFAULT_TIMEZONE], array_keys(CTimezoneHelper::getList()))), 'length' => DB::getFieldLength('sla', 'timezone')], 'status' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SLA_STATUS_DISABLED, ZBX_SLA_STATUS_ENABLED])], 'description' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sla', 'description')], 'service_tags' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'uniq' => [['tag', 'value']], 'fields' => [ 'tag' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('sla_service_tag', 'tag')], 'operator' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SLA_SERVICE_TAG_OPERATOR_EQUAL, ZBX_SLA_SERVICE_TAG_OPERATOR_LIKE]), 'default' => DB::getDefault('sla_service_tag', 'operator')], 'value' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sla_service_tag', 'value'), 'default' => DB::getDefault('sla_service_tag', 'value')] ]], 'schedule' => ['type' => API_OBJECTS, 'uniq' => [['period_from', 'period_to']], 'fields' => [ 'period_from' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.SEC_PER_WEEK], 'period_to' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.SEC_PER_WEEK] ]], 'excluded_downtimes' => ['type' => API_OBJECTS, 'uniq' => [['period_from', 'period_to']], 'fields' => [ 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('sla_excluded_downtime', 'name')], 'period_from' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_DATE], 'period_to' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_DATE] ]] ]]; if (!CApiInputValidator::validate($api_input_rules, $slas, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } self::checkDuplicates($slas); self::checkSlo($slas); self::checkSchedule($slas); self::checkExcludedDowntimes($slas); } /** * @param array $slas * * @throws APIException * * @return array */ public function update(array $slas): array { self::validateUpdate($slas, $db_slas); $upd_slas = []; foreach ($slas as $sla) { $upd_sla = DB::getUpdatedValues('sla', $sla, $db_slas[$sla['slaid']]); if ($upd_sla) { $upd_slas[] = [ 'values' => $upd_sla, 'where' => ['slaid' => $sla['slaid']] ]; } } if ($upd_slas) { DB::update('sla', $upd_slas); } self::updateServiceTags($slas, $db_slas); self::updateSchedule($slas, $db_slas); self::updateExcludedDowntimes($slas, $db_slas); self::addAuditLog(CAudit::ACTION_UPDATE, CAudit::RESOURCE_SLA, $slas, $db_slas); return ['slaids' => array_column($slas, 'slaid')]; } /** * @param array $slas * @param array|null $db_slas * * @throws APIException */ private static function validateUpdate(array &$slas, ?array &$db_slas): void { $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['slaid'], ['name']], 'fields' => [ 'slaid' => ['type' => API_ID, 'flags' => API_REQUIRED], 'name' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('sla', 'name')], 'period' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SLA_PERIOD_DAILY, ZBX_SLA_PERIOD_WEEKLY, ZBX_SLA_PERIOD_MONTHLY, ZBX_SLA_PERIOD_QUARTERLY, ZBX_SLA_PERIOD_ANNUALLY])], 'slo' => ['type' => API_FLOAT, 'in' => '0:100'], 'effective_date' => ['type' => API_INT32, 'in' => '0:'.ZBX_MAX_DATE], 'timezone' => ['type' => API_STRING_UTF8, 'in' => implode(',', array_merge([ZBX_DEFAULT_TIMEZONE], array_keys(CTimezoneHelper::getList()))), 'length' => DB::getFieldLength('sla', 'timezone')], 'status' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SLA_STATUS_DISABLED, ZBX_SLA_STATUS_ENABLED])], 'description' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sla', 'description')], 'service_tags' => ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['tag', 'value']], 'fields' => [ 'tag' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('sla_service_tag', 'tag')], 'operator' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SLA_SERVICE_TAG_OPERATOR_EQUAL, ZBX_SLA_SERVICE_TAG_OPERATOR_LIKE]), 'default' => DB::getDefault('sla_service_tag', 'operator')], 'value' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sla_service_tag', 'value'), 'default' => DB::getDefault('sla_service_tag', 'value')] ]], 'schedule' => ['type' => API_OBJECTS, 'uniq' => [['period_from', 'period_to']], 'fields' => [ 'period_from' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.SEC_PER_WEEK], 'period_to' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.SEC_PER_WEEK] ]], 'excluded_downtimes' => ['type' => API_OBJECTS, 'uniq' => [['period_from', 'period_to']], 'fields' => [ 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('sla_excluded_downtime', 'name')], 'period_from' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_DATE], 'period_to' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '0:'.ZBX_MAX_DATE] ]] ]]; if (!CApiInputValidator::validate($api_input_rules, $slas, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $db_slas = DB::select('sla', [ 'output' => ['slaid', 'name', 'period', 'slo', 'timezone', 'status', 'description'], 'slaids' => array_column($slas, 'slaid'), 'preservekeys' => true ]); if (count($db_slas) != count($slas)) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } self::addAffectedObjects($slas, $db_slas); self::checkDuplicates($slas, $db_slas); self::checkSlo($slas, $db_slas); self::checkSchedule($slas, $db_slas); self::checkExcludedDowntimes($slas, $db_slas); } /** * @param array $slaids * * @throws APIException * * @return array */ public function delete(array $slaids): array { $this->validateDelete($slaids, $db_slas); DB::delete('sla', ['slaid' => $slaids]); self::addAuditLog(CAudit::ACTION_DELETE, CAudit::RESOURCE_SLA, $db_slas); return ['slaids' => $slaids]; } /** * @param array $slaids * @param array|null $db_slas * * @throws APIException */ private function validateDelete(array $slaids, ?array &$db_slas): void { $api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true]; if (!CApiInputValidator::validate($api_input_rules, $slaids, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $db_slas = $this->get([ 'output' => ['slaid', 'name'], 'slaids' => $slaids, 'editable' => true, 'preservekeys' => true ]); if (count($db_slas) != count($slaids)) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } } /** * @param string $table_name * @param string $table_alias * @param array $options * @param array $sql_parts * * @return array */ protected function applyQueryFilterOptions($table_name, $table_alias, array $options, array $sql_parts): array { $sql_parts = parent::applyQueryFilterOptions($table_name, $table_alias, $options, $sql_parts); if ($options['service_tags']) { $sql_parts['where'][] = CApiTagHelper::addWhereCondition($options['service_tags'], $options['evaltype'], 'sla', 'sla_service_tag', 'slaid' ); } return $sql_parts; } /** * @param array $options * @param array $result * * @return array */ protected function addRelatedObjects(array $options, array $result): array { $result = parent::addRelatedObjects($options, $result); self::addRelatedServiceTags($options, $result); self::addRelatedSchedule($options, $result); self::addRelatedExcludedDowntimes($options, $result); return $result; } /** * @param array $options * @param array $result */ private static function addRelatedServiceTags(array $options, array &$result): void { if ($options['selectServiceTags'] === null) { return; } foreach ($result as &$row) { $row['service_tags'] = []; } unset($row); if ($options['selectServiceTags'] === API_OUTPUT_COUNT) { $output = ['sla_service_tagid', 'slaid']; } elseif ($options['selectServiceTags'] === API_OUTPUT_EXTEND) { $output = ['sla_service_tagid', 'slaid', 'tag', 'operator', 'value']; } else { $output = array_unique(array_merge(['sla_service_tagid', 'slaid'], $options['selectServiceTags'])); } $sql_options = [ 'output' => $output, 'filter' => ['slaid' => array_keys($result)] ]; $db_service_tags = DBselect(DB::makeSql('sla_service_tag', $sql_options)); while ($db_service_tag = DBfetch($db_service_tags)) { $slaid = $db_service_tag['slaid']; unset($db_service_tag['sla_service_tagid'], $db_service_tag['slaid']); $result[$slaid]['service_tags'][] = $db_service_tag; } if ($options['selectServiceTags'] === API_OUTPUT_COUNT) { foreach ($result as &$row) { $row['service_tags'] = (string) count($row['service_tags']); } unset($row); } } /** * @param array $options * @param array $result */ private static function addRelatedSchedule(array $options, array &$result): void { if ($options['selectSchedule'] === null) { return; } foreach ($result as &$row) { $row['schedule'] = []; } unset($row); $sql_options = [ 'output' => ['sla_scheduleid', 'slaid', 'period_from', 'period_to'], 'filter' => ['slaid' => array_keys($result)] ]; $db_schedule = DBselect(DB::makeSql('sla_schedule', $sql_options)); while ($db_schedule_row = DBfetch($db_schedule)) { $slaid = $db_schedule_row['slaid']; unset($db_schedule_row['sla_scheduleid'], $db_schedule_row['slaid']); $result[$slaid]['schedule'][] = $db_schedule_row; } foreach ($result as &$row) { $row['schedule'] = self::normalizeSchedule($row['schedule']); if ($options['selectSchedule'] === API_OUTPUT_COUNT) { $row['schedule'] = (string) count($row['schedule']); } elseif ($options['selectSchedule'] !== API_OUTPUT_EXTEND) { $select_schedule = array_flip($options['selectSchedule']); foreach ($row['schedule'] as &$schedule_row) { $schedule_row = array_intersect_key($schedule_row, $select_schedule); } unset($schedule_row); } } unset($row); } /** * @param array $options * @param array $result */ private static function addRelatedExcludedDowntimes(array $options, array &$result): void { if ($options['selectExcludedDowntimes'] === null) { return; } foreach ($result as &$row) { $row['excluded_downtimes'] = []; } unset($row); if ($options['selectExcludedDowntimes'] === API_OUTPUT_COUNT) { $output = ['sla_excluded_downtimeid', 'slaid']; } elseif ($options['selectExcludedDowntimes'] === API_OUTPUT_EXTEND) { $output = ['sla_excluded_downtimeid', 'slaid', 'name', 'period_from', 'period_to']; } else { $output = array_unique(array_merge(['sla_excluded_downtimeid', 'slaid'], $options['selectExcludedDowntimes'] )); } $sql_options = [ 'output' => $output, 'filter' => ['slaid' => array_keys($result)], 'sortfield' => ['period_from'], 'sortorder' => [ZBX_SORT_UP] ]; $db_sla_excluded_downtimes = DBselect(DB::makeSql('sla_excluded_downtime', $sql_options)); while ($db_sla_excluded_downtime = DBfetch($db_sla_excluded_downtimes)) { $slaid = $db_sla_excluded_downtime['slaid']; unset($db_sla_excluded_downtime['sla_excluded_downtimeid'], $db_sla_excluded_downtime['slaid']); $result[$slaid]['excluded_downtimes'][] = $db_sla_excluded_downtime; } if ($options['selectExcludedDowntimes'] === API_OUTPUT_COUNT) { foreach ($result as &$row) { $row['excluded_downtimes'] = (string) count($row['excluded_downtimes']); } unset($row); } } /** * @param array $slas * @param array|null $db_slas * * @throws APIException */ private static function checkDuplicates(array $slas, array $db_slas = null): void { $names = []; foreach ($slas as $sla) { if ($db_slas === null || (array_key_exists('name', $sla) && $sla['name'] !== $db_slas[$sla['slaid']]['name'])) { $names[] = $sla['name']; } } if (!$names) { return; } $duplicate = DBfetch(DBselect( 'SELECT sla.name FROM sla WHERE '.dbConditionString('sla.name', $names), 1 )); if ($duplicate) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('SLA "%1$s" already exists.', $duplicate['name'])); } } /** * @param array $slas * @param array|null $db_slas * * @throws APIException */ private static function checkSlo(array $slas, array $db_slas = null): void { foreach ($slas as $sla) { $name = $db_slas !== null ? $db_slas[$sla['slaid']]['name'] : $sla['name']; if (array_key_exists('slo', $sla) && round($sla['slo'], 4) != $sla['slo']) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('SLA "%1$s" SLO must have no more than 4 fractional digits.', $name) ); } } } /** * @param array $slas * @param array|null $db_slas * * @throws APIException */ private static function checkSchedule(array &$slas, array $db_slas = null): void { foreach ($slas as &$sla) { if (!array_key_exists('schedule', $sla)) { continue; } $name = $db_slas !== null ? $db_slas[$sla['slaid']]['name'] : $sla['name']; foreach ($sla['schedule'] as $schedule_row) { if ($schedule_row['period_from'] >= $schedule_row['period_to']) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Start time must be less than end time for SLA "%1$s".', $name) ); } } $sla['schedule'] = self::normalizeSchedule($sla['schedule']); } unset($sla); } /** * @param array $slas * @param array|null $db_slas * * @throws APIException */ private static function checkExcludedDowntimes(array $slas, array $db_slas = null): void { foreach ($slas as $sla) { if (!array_key_exists('excluded_downtimes', $sla)) { continue; } $name = $db_slas !== null ? $db_slas[$sla['slaid']]['name'] : $sla['name']; foreach ($sla['excluded_downtimes'] as $excluded_downtime) { if ($excluded_downtime['period_from'] >= $excluded_downtime['period_to']) { self::exception(ZBX_API_ERROR_PARAMETERS, _s( 'Start time must be less than end time for excluded downtime "%2$s" of SLA "%1$s".', $name, $excluded_downtime['name'] )); } } } } /** * @param array $slas * @param array $db_slas */ private static function addAffectedObjects(array $slas, array &$db_slas): void { self::addAffectedServiceTags($slas, $db_slas); self::addAffectedSchedule($slas, $db_slas); self::addAffectedExcludedDowntimes($slas, $db_slas); } /** * @param array $slas * @param array $db_slas */ private static function addAffectedServiceTags(array $slas, array &$db_slas): void { $affected_slaids = []; foreach ($slas as $sla) { if (array_key_exists('service_tags', $sla)) { $affected_slaids[$sla['slaid']] = true; $db_slas[$sla['slaid']]['service_tags'] = []; } } $sql_options = [ 'output' => ['sla_service_tagid', 'slaid', 'tag', 'operator', 'value'], 'filter' => ['slaid' => array_keys($affected_slaids)] ]; $db_service_tags = DBselect(DB::makeSql('sla_service_tag', $sql_options)); while ($db_service_tag = DBfetch($db_service_tags)) { $slaid = $db_service_tag['slaid']; unset($db_service_tag['slaid']); $db_slas[$slaid]['service_tags'][$db_service_tag['sla_service_tagid']] = $db_service_tag; } } /** * @param array $slas * @param array $db_slas */ private static function addAffectedSchedule(array $slas, array &$db_slas): void { $affected_slaids = []; foreach ($slas as $sla) { if (array_key_exists('schedule', $sla)) { $affected_slaids[$sla['slaid']] = true; $db_slas[$sla['slaid']]['schedule'] = []; } } $sql_options = [ 'output' => ['sla_scheduleid', 'slaid', 'period_from', 'period_to'], 'filter' => ['slaid' => array_keys($affected_slaids)] ]; $db_schedule = DBselect(DB::makeSql('sla_schedule', $sql_options)); while ($db_schedule_row = DBfetch($db_schedule)) { $slaid = $db_schedule_row['slaid']; unset($db_schedule_row['slaid']); $db_slas[$slaid]['schedule'][$db_schedule_row['sla_scheduleid']] = $db_schedule_row; } } /** * @param array $slas * @param array $db_slas */ private static function addAffectedExcludedDowntimes(array $slas, array &$db_slas): void { $affected_slaids = []; foreach ($slas as $sla) { if (array_key_exists('excluded_downtimes', $sla)) { $affected_slaids[$sla['slaid']] = true; $db_slas[$sla['slaid']]['excluded_downtimes'] = []; } } $sql_options = [ 'output' => ['sla_excluded_downtimeid', 'slaid', 'name', 'period_from', 'period_to'], 'filter' => ['slaid' => array_keys($affected_slaids)] ]; $db_excluded_downtimes = DBselect(DB::makeSql('sla_excluded_downtime', $sql_options)); while ($db_excluded_downtime = DBfetch($db_excluded_downtimes)) { $slaid = $db_excluded_downtime['slaid']; unset($db_excluded_downtime['slaid']); $db_slas[$slaid]['excluded_downtimes'][$db_excluded_downtime['sla_excluded_downtimeid']] = $db_excluded_downtime; } } /** * @param array $slas * @param array|null $db_slas */ private static function updateServiceTags(array &$slas, array $db_slas = null): void { $ins_service_tags = []; $del_service_tags = []; foreach ($slas as &$sla) { if (!array_key_exists('service_tags', $sla)) { continue; } $db_service_tags = []; if ($db_slas !== null) { foreach ($db_slas[$sla['slaid']]['service_tags'] as $db_service_tag) { $db_service_tags[$db_service_tag['tag']][$db_service_tag['operator']][$db_service_tag['value']] = $db_service_tag['sla_service_tagid']; $del_service_tags[$db_service_tag['sla_service_tagid']] = true; } } foreach ($sla['service_tags'] as &$service_tag) { if (array_key_exists($service_tag['tag'], $db_service_tags) && array_key_exists($service_tag['operator'], $db_service_tags[$service_tag['tag']]) && array_key_exists($service_tag['value'], $db_service_tags[$service_tag['tag']][$service_tag['operator']] )) { $service_tag['sla_service_tagid'] = $db_service_tags[$service_tag['tag']][$service_tag['operator']][$service_tag['value']]; unset($del_service_tags[$service_tag['sla_service_tagid']]); } else { $ins_service_tags[] = ['slaid' => $sla['slaid']] + $service_tag; } } unset($service_tag); } unset($sla); if ($del_service_tags) { DB::delete('sla_service_tag', ['sla_service_tagid' => array_keys($del_service_tags)]); } if ($ins_service_tags) { $sla_service_tagids = DB::insert('sla_service_tag', $ins_service_tags); $sla_service_tagids_index = 0; foreach ($slas as &$sla) { if (!array_key_exists('service_tags', $sla)) { continue; } foreach ($sla['service_tags'] as &$service_tag) { if (array_key_exists('sla_service_tagid', $service_tag)) { continue; } $service_tag['sla_service_tagid'] = $sla_service_tagids[$sla_service_tagids_index]; $sla_service_tagids_index++; } unset($service_tag); } unset($sla); } } /** * @param array $slas * @param array|null $db_slas */ private static function updateSchedule(array &$slas, array $db_slas = null): void { $ins_schedule = []; $del_schedule = []; foreach ($slas as &$sla) { if (!array_key_exists('schedule', $sla)) { continue; } $db_schedule = []; if ($db_slas !== null) { foreach ($db_slas[$sla['slaid']]['schedule'] as $db_schedule_row) { $db_schedule[$db_schedule_row['period_from']][$db_schedule_row['period_to']] = $db_schedule_row['sla_scheduleid']; $del_schedule[$db_schedule_row['sla_scheduleid']] = true; } } foreach ($sla['schedule'] as &$schedule_row) { if (array_key_exists($schedule_row['period_from'], $db_schedule) && array_key_exists($schedule_row['period_to'], $db_schedule[$schedule_row['period_from']])) { $schedule_row['sla_scheduleid'] = $db_schedule[$schedule_row['period_from']][$schedule_row['period_to']]; unset($del_schedule[$schedule_row['sla_scheduleid']]); } else { $ins_schedule[] = ['slaid' => $sla['slaid']] + $schedule_row; } } unset($schedule_row); } unset($sla); if ($del_schedule) { DB::delete('sla_schedule', ['sla_scheduleid' => array_keys($del_schedule)]); } if ($ins_schedule) { $sla_scheduleids = DB::insert('sla_schedule', $ins_schedule); $sla_scheduleids_index = 0; foreach ($slas as &$sla) { if (!array_key_exists('schedule', $sla)) { continue; } foreach ($sla['schedule'] as &$schedule_row) { if (array_key_exists('sla_scheduleid', $schedule_row)) { continue; } $schedule_row['sla_scheduleid'] = $sla_scheduleids[$sla_scheduleids_index]; $sla_scheduleids_index++; } unset($schedule_row); } unset($sla); } } /** * @param array $slas * @param array|null $db_slas */ private static function updateExcludedDowntimes(array &$slas, array $db_slas = null): void { $ins_excluded_downtimes = []; $upd_excluded_downtimes = []; $del_excluded_downtimes = []; foreach ($slas as &$sla) { if (!array_key_exists('excluded_downtimes', $sla)) { continue; } $db_excluded_downtimes = []; if ($db_slas !== null) { foreach ($db_slas[$sla['slaid']]['excluded_downtimes'] as $db_excluded_downtime) { $db_excluded_downtimes[$db_excluded_downtime['period_from']][$db_excluded_downtime['period_to']] = $db_excluded_downtime; $del_excluded_downtimes[$db_excluded_downtime['sla_excluded_downtimeid']] = true; } } foreach ($sla['excluded_downtimes'] as &$excluded_downtime) { if (array_key_exists($excluded_downtime['period_from'], $db_excluded_downtimes) && array_key_exists($excluded_downtime['period_to'], $db_excluded_downtimes[$excluded_downtime['period_from']] )) { $excluded_downtime['sla_excluded_downtimeid'] = $db_excluded_downtimes [$excluded_downtime['period_from']][$excluded_downtime['period_to']]['sla_excluded_downtimeid']; unset($del_excluded_downtimes[$excluded_downtime['sla_excluded_downtimeid']]); $upd_excluded_downtime = DB::getUpdatedValues('sla_excluded_downtime', $excluded_downtime, $db_excluded_downtimes[$excluded_downtime['period_from']][$excluded_downtime['period_to']] ); if ($upd_excluded_downtime) { $upd_excluded_downtimes[] = [ 'values' => $upd_excluded_downtime, 'where' => ['sla_excluded_downtimeid' => $excluded_downtime['sla_excluded_downtimeid']] ]; } } else { $ins_excluded_downtimes[] = ['slaid' => $sla['slaid']] + $excluded_downtime; } } unset($excluded_downtime); } unset($sla); if ($del_excluded_downtimes) { DB::delete('sla_excluded_downtime', ['sla_excluded_downtimeid' => array_keys($del_excluded_downtimes)]); } if ($ins_excluded_downtimes) { $sla_excluded_downtimeids = DB::insert('sla_excluded_downtime', $ins_excluded_downtimes); $sla_excluded_downtimeids_index = 0; foreach ($slas as &$sla) { if (!array_key_exists('excluded_downtimes', $sla)) { continue; } foreach ($sla['excluded_downtimes'] as &$excluded_downtime) { if (array_key_exists('sla_excluded_downtimeid', $excluded_downtime)) { continue; } $excluded_downtime['sla_excluded_downtimeid'] = $sla_excluded_downtimeids[$sla_excluded_downtimeids_index]; $sla_excluded_downtimeids_index++; } unset($excluded_downtime); } unset($sla); } if ($upd_excluded_downtimes) { DB::update('sla_excluded_downtime', $upd_excluded_downtimes); } } /** * Concatenate overlapping periods and sort by starting time of periods. * Full weekly period is returned as empty array. * * @param array $schedule * * @return array */ private static function normalizeSchedule(array $schedule): array { $converted_schedule = []; foreach ($schedule as $schedule_row) { $period_from = $schedule_row['period_from']; $period_to = $schedule_row['period_to']; foreach ($converted_schedule as $converted_schedule_row) { $is_overlapping = $schedule_row['period_from'] <= $converted_schedule_row['period_to'] && $schedule_row['period_to'] >= $converted_schedule_row['period_from']; if ($is_overlapping) { $period_from = min($period_from, $converted_schedule_row['period_from']); $period_to = max($period_to, $converted_schedule_row['period_to']); } } foreach ($converted_schedule as $index => $converted_schedule_row) { if ($converted_schedule_row['period_from'] >= $period_from && $converted_schedule_row['period_to'] <= $period_to) { unset($converted_schedule[$index]); } } $converted_schedule[] = ['period_from' => $period_from, 'period_to' => $period_to]; } usort($converted_schedule, static function (array $schedule_row_a, array $schedule_row_b): int { return $schedule_row_a['period_from'] <=> $schedule_row_b['period_from']; } ); if (count($converted_schedule) == 1 && $converted_schedule[0]['period_from'] == 0 && $converted_schedule[0]['period_to'] == SEC_PER_WEEK) { return []; } return $converted_schedule; } /** * @param array|null $limit_slaids * @param array|null $limit_serviceids * * @throws APIException * * @return array|null */ private static function getAccessibleSlaids(?array $limit_slaids, ?array $limit_serviceids): ?array { $role = API::Role()->get([ 'output' => [], 'selectRules' => ['services.read.mode', 'services.write.mode', 'actions'], 'roleids' => self::$userData['roleid'] ]); if (!$role) { return []; } if ($limit_serviceids === null) { $rules = $role[0]['rules']; $manage_sla_status = 0; foreach ($rules['actions'] as $action) { if ($action['name'] === 'manage_sla') { $manage_sla_status = $action['status']; break; } } if ($rules['services.read.mode'] == ZBX_ROLE_RULE_SERVICES_ACCESS_ALL || $rules['services.write.mode'] == ZBX_ROLE_RULE_SERVICES_ACCESS_ALL || $manage_sla_status == 1) { return null; } } $accessible_services = API::Service()->get([ 'output' => [], 'serviceids' => $limit_serviceids, 'preservekeys' => true ]); if (!$accessible_services) { return []; } $services_slas_resource_sql = 'SELECT DISTINCT st.serviceid,sst.slaid'. ' FROM service_tag st, sla_service_tag sst'. ' WHERE sst.tag=st.tag'. ' AND ('. '(sst.operator='.ZBX_SLA_SERVICE_TAG_OPERATOR_EQUAL.' AND st.value=sst.value)'. ' OR (sst.operator='.ZBX_SLA_SERVICE_TAG_OPERATOR_LIKE." AND UPPER(st.value) LIKE CONCAT('%', ". "CONCAT(REPLACE(REPLACE(REPLACE(UPPER(sst.value), '%', '!%'), '_', '!_'), '!', '!!'), '%')". ") ESCAPE '!')". ')'. ($limit_slaids !== null ? ' AND '.dbConditionId('sst.slaid', $limit_slaids) : ''). ($limit_serviceids !== null ? ' AND '.dbConditionId('st.serviceid', $limit_serviceids) : ''); $services_slas_resource = DBSelect($services_slas_resource_sql); $accessible_slaids = []; while ($db_service_sla = DBfetch($services_slas_resource)) { if (array_key_exists($db_service_sla['serviceid'], $accessible_services)) { $accessible_slaids[$db_service_sla['slaid']] = true; } } return $accessible_slaids; } /** * @param array $options * * @throws Exception * @throws APIException * * @return array */ public function getSli(array $options = []): array { $api_input_rules = ['type' => API_OBJECT, 'fields' => [ 'slaid' => ['type' => API_ID, 'flags' => API_REQUIRED], 'period_from' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '0:'.ZBX_MAX_DATE, 'default' => null], 'period_to' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '0:'.ZBX_MAX_DATE, 'default' => null], 'periods' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_SLA_MAX_REPORTING_PERIODS, 'default' => null], 'serviceids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null] ]]; if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $db_sla = $this->get([ 'output' => ['period', 'slo', 'timezone', 'effective_date'], 'selectSchedule' => ['period_from', 'period_to'], 'selectExcludedDowntimes' => ['name', 'period_from', 'period_to'], 'slaids' => $options['slaid'] ]); if (!$db_sla) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } $db_sla = $db_sla[0]; $reporting_periods = self::getReportingPeriods($db_sla, $options); if ($reporting_periods) { $db_services = API::Service()->get([ 'output' => ['created_at'], 'selectStatusTimeline' => $reporting_periods, 'slaids' => $options['slaid'], 'serviceids' => $options['serviceids'], 'preservekeys' => true ]); } else { $db_services = []; } return [ 'periods' => $reporting_periods, 'serviceids' => array_keys($db_services), 'sli' => self::calculateSli($db_sla, $reporting_periods, array_values($db_services)) ]; } /** * @param array $sla * @param array $options * * @throws Exception * * @return array */ private static function getReportingPeriods(array $sla, array $options): array { $interval = new DateInterval([ ZBX_SLA_PERIOD_DAILY => 'P1D', ZBX_SLA_PERIOD_WEEKLY => 'P1W', ZBX_SLA_PERIOD_MONTHLY => 'P1M', ZBX_SLA_PERIOD_QUARTERLY => 'P3M', ZBX_SLA_PERIOD_ANNUALLY => 'P1Y' ][$sla['period']]); $timezone = new DateTimeZone($sla['timezone'] !== ZBX_DEFAULT_TIMEZONE ? $sla['timezone'] : CTimezoneHelper::getSystemTimezone() ); $effective_min = (new DateTime('@'.$sla['effective_date']))->setTimezone($timezone); self::alignDateToPeriodStart($effective_min, (int) $sla['period']); $effective_max = (new DateTime('now'))->setTimezone($timezone); self::alignDateToPeriodStart($effective_max, (int) $sla['period']); $effective_max->add($interval); if ($options['period_from'] !== null) { $period_from = (new DateTime('@'.$options['period_from']))->setTimezone($timezone); self::alignDateToPeriodStart($period_from, (int) $sla['period']); if ($period_from->getTimestamp() < 0) { $period_from->add($interval); } } else { $period_from = null; } if ($options['period_to'] !== null) { $period_to = (new DateTime('@'.$options['period_to']))->setTimezone($timezone); self::alignDateToPeriodStart($period_to, (int) $sla['period']); $period_to->add($interval); if ($period_to->getTimestamp() > ZBX_MAX_DATE) { $period_to->sub($interval); } } elseif ($period_from === null) { $period_to = $effective_max; } else { $period_to = null; } $reporting_periods = []; $do_descend = $period_to !== null; $date = $do_descend ? clone $period_to : clone $period_from; while (count($reporting_periods) < ZBX_SLA_MAX_REPORTING_PERIODS) { if ($options['periods'] !== null) { if (count($reporting_periods) == $options['periods']) { break; } } else { if (($period_from === null || $period_to === null) && count($reporting_periods) == ZBX_SLA_DEFAULT_REPORTING_PERIODS) { break; } if ($do_descend) { if ($period_from === null && $date <= $effective_min) { break; } } elseif ($date >= $effective_max) { break; } } if ($do_descend && $period_from !== null && $date <= $period_from) { break; } if ($do_descend) { $to = $date->getTimestamp(); $date->sub($interval); $from = $date->getTimestamp(); if ($from < 0) { break; } array_unshift($reporting_periods, ['period_from' => $from, 'period_to' => $to]); } else { $from = $date->getTimestamp(); $date->add($interval); $to = $date->getTimestamp(); if ($to > ZBX_MAX_DATE) { break; } $reporting_periods[] = ['period_from' => $from, 'period_to' => $to]; } } return $reporting_periods; } /** * @param DateTime $date * * @param int $sla_period */ private static function alignDateToPeriodStart(DateTime $date, int $sla_period): void { $year = (int) $date->format('Y'); $month = (int) $date->format('n'); switch ($sla_period) { case ZBX_SLA_PERIOD_WEEKLY: $date ->modify('1 day') ->modify('last Sunday'); break; case ZBX_SLA_PERIOD_MONTHLY: $date->setDate($year, $month, 1); break; case ZBX_SLA_PERIOD_QUARTERLY: $date->setDate($year, intdiv($month - 1, 3) * 3 + 1, 1); break; case ZBX_SLA_PERIOD_ANNUALLY: $date->setDate($year, 1, 1); break; } $date->setTime(0, 0); } /** * @param array $db_sla * @param array $reporting_periods * @param array $db_services * * @throws Exception * * @return array */ private static function calculateSli(array $db_sla, array $reporting_periods, array $db_services): array { if (!$reporting_periods || !$db_services) { return []; } $sli = []; $combined_excluded_downtimes = self::combineExcludedDowntimes($db_sla['excluded_downtimes']); foreach ($reporting_periods as $reporting_period_index => $reporting_period) { $scheduled_uptime_periods = self::getScheduledUptimePeriods($db_sla, $reporting_period); foreach ($db_services as $service_index => $db_service) { $cell = [ 'uptime' => 0, 'downtime' => 0, 'sli' => -1.0, 'error_budget' => 0, 'excluded_downtimes' => [] ]; $max_uptime = 0; foreach ($scheduled_uptime_periods as $scheduled_uptime_period) { $uptime_period_from = max($db_service['created_at'], $scheduled_uptime_period['period_from']); $uptime_period_to = $scheduled_uptime_period['period_to']; $uptime = $uptime_period_to - $uptime_period_from; if ($uptime <= 0) { continue; } $max_uptime += $uptime; foreach ($combined_excluded_downtimes as $combined_excluded_downtime) { $downtime = min($combined_excluded_downtime['period_to'], $uptime_period_to) - max($combined_excluded_downtime['period_from'], $uptime_period_from); if ($downtime > 0) { $max_uptime -= $downtime; } } } $last_excluded_downtimes = []; $prev_clock = $reporting_period['period_from']; $prev_value = $db_service['status_timeline'][$reporting_period_index]['start_value']; $alarms = $db_service['status_timeline'][$reporting_period_index]['alarms']; if (!$alarms || $alarms[count($alarms) - 1]['clock'] <= time()) { $alarms[] = ['clock' => time() + 1, 'value' => null]; } foreach ($alarms as $alarm) { foreach ($scheduled_uptime_periods as $scheduled_uptime_period) { $uptime_period_from = max($db_service['created_at'], $scheduled_uptime_period['period_from'], $prev_clock ); $uptime_period_to = min($scheduled_uptime_period['period_to'], $alarm['clock']); $uptime = $uptime_period_to - $uptime_period_from; if ($uptime <= 0) { continue; } foreach ($combined_excluded_downtimes as $combined_excluded_downtime) { $downtime = min($combined_excluded_downtime['period_to'], $uptime_period_to) - max($combined_excluded_downtime['period_from'], $uptime_period_from); if ($downtime > 0) { $uptime -= $downtime; } } if ($prev_value == ZBX_SEVERITY_OK) { $cell['uptime'] += $uptime; } else { $cell['downtime'] += $uptime; } foreach ($db_sla['excluded_downtimes'] as $index => $excluded_downtime) { $downtime_period_from = max($excluded_downtime['period_from'], $uptime_period_from); $downtime_period_to = min($excluded_downtime['period_to'], $uptime_period_to); if ($downtime_period_to > $downtime_period_from) { if (array_key_exists($index, $last_excluded_downtimes)) { $cell['excluded_downtimes'][$last_excluded_downtimes[$index]['cell']]['period_to'] = (int) $downtime_period_to; } else { $cell['excluded_downtimes'][] = [ 'name' => $excluded_downtime['name'], 'period_from' => (int) $downtime_period_from, 'period_to' => (int) $downtime_period_to ]; } $last_excluded_downtimes[$index] = [ 'cell' => count($cell['excluded_downtimes']) - 1, 'period_to' => $downtime_period_to ]; } else { unset($last_excluded_downtimes[$index]); } } } $prev_clock = $alarm['clock']; $prev_value = $alarm['value']; } if ($cell['uptime'] + $cell['downtime'] != 0) { $cell['sli'] = $cell['uptime'] / ($cell['uptime'] + $cell['downtime']) * 100; } if ($cell['sli'] != -1) { $available_uptime = $max_uptime - $cell['uptime'] - $cell['downtime']; $cell['error_budget'] = $db_sla['slo'] > 0 ? min($available_uptime, (int) ($cell['uptime'] / $db_sla['slo'] * 100) - $cell['uptime'] - $cell['downtime'] ) : $available_uptime; } $sli[$reporting_period_index][$service_index] = $cell; } } return $sli; } /** * @param array $excluded_downtimes * * @return array */ private static function combineExcludedDowntimes(array $excluded_downtimes): array { $combined_excluded_downtimes = []; foreach ($excluded_downtimes as $excluded_downtime) { $period_from = $excluded_downtime['period_from']; $period_to = $excluded_downtime['period_to']; foreach ($combined_excluded_downtimes as $combined_excluded_downtime) { $is_overlapping = $excluded_downtime['period_from'] <= $combined_excluded_downtime['period_to'] && $excluded_downtime['period_to'] >= $combined_excluded_downtime['period_from']; if ($is_overlapping) { $period_from = min($period_from, $combined_excluded_downtime['period_from']); $period_to = max($period_to, $combined_excluded_downtime['period_to']); } } foreach ($combined_excluded_downtimes as $index => $combined_excluded_downtime) { if ($combined_excluded_downtime['period_from'] >= $period_from && $combined_excluded_downtime['period_to'] <= $period_to) { unset($combined_excluded_downtimes[$index]); } } $combined_excluded_downtimes[] = ['period_from' => $period_from, 'period_to' => $period_to]; } return $combined_excluded_downtimes; } /** * @param array $db_sla * @param array $reporting_period * * @throws Exception * * @return array */ private static function getScheduledUptimePeriods(array $db_sla, array $reporting_period): array { if (!$db_sla['schedule']) { return [$reporting_period]; } $uptime_periods = []; $reporting_period_from = (new DateTimeImmutable('@'.$reporting_period['period_from'])) ->setTimezone(new DateTimeZone($db_sla['timezone'] !== ZBX_DEFAULT_TIMEZONE ? $db_sla['timezone'] : CTimezoneHelper::getSystemTimezone() )) ->modify('1 day') ->modify('last Sunday'); for ($week = 0;; $week++) { $week_period_from = $reporting_period_from->modify($week.' week'); foreach ($db_sla['schedule'] as $schedule_row) { $period_from = $week_period_from ->modify((int) ($schedule_row['period_from'] / SEC_PER_DAY).' day') ->setTime( (int) ($schedule_row['period_from'] / SEC_PER_HOUR) % 24, (int) ($schedule_row['period_from'] / SEC_PER_MIN) % 60, $schedule_row['period_from'] % 60 ) ->getTimestamp(); $period_to = $week_period_from ->modify((int) ($schedule_row['period_to'] / SEC_PER_DAY).' day') ->setTime( (int) ($schedule_row['period_to'] / SEC_PER_HOUR) % 24, (int) ($schedule_row['period_to'] / SEC_PER_MIN) % 60, $schedule_row['period_to'] % 60 ) ->getTimestamp(); if ($period_from < $reporting_period['period_to'] && $period_to > $reporting_period['period_from']) { $new_period_from = max($reporting_period['period_from'], $period_from); $new_period_to = min($reporting_period['period_to'], $period_to); if ($uptime_periods && $uptime_periods[count($uptime_periods) - 1]['period_to'] == $new_period_from) { $uptime_periods[count($uptime_periods) - 1]['period_to'] = $new_period_to; } else { $uptime_periods[] = ['period_from' => $new_period_from, 'period_to' => $new_period_to]; } } if ($period_to >= $reporting_period['period_to']) { break 2; } } } return $uptime_periods; } }