Автоматизация регистрации задач в Jira из Zabbix

Автоматизируем регистрацию инцидентов из Zabbix в Jira Service Management через webhook-скрипт. Разбираем настройку скрипта в Zabbix, передачу параметров события, защиту от дублей по TRIGGER_ID, переоткрытие ранее решённых задач и запись ссылки на Jira обратно в событие мониторинга.

Автоматизация регистрации задач в Jira из Zabbix

Полный скрипт webhook для Zabbix

var Jira = {
params: {},
schema: {},

setParams: function (params) {
if (typeof params !== 'object') {
return;
}

Jira.params = params;
if (typeof Jira.params.url === 'string') {
if (!Jira.params.url.endsWith('/')) {
Jira.params.url += '/';
}

Jira.params.url += 'rest/servicedeskapi/';
}
},

setProxy: function (HTTPProxy) {
Jira.HTTPProxy = HTTPProxy;
},

setTags: function(event_tags_json) {
if (!Jira.schema) {
Zabbix.log(4, '[ Jira Service Desk Webhook ] Cannot add labels because failed to retrieve field schema.');

return;
}

var block = Jira.schema.requestTypeFields.filter(function(object) {
return object.fieldId == 'labels';
});

if (block[0] && typeof event_tags_json !== 'undefined' && event_tags_json !== ''
&& event_tags_json !== '{EVENT.TAGSJSON}') {
try {
var tags = JSON.parse(event_tags_json),
label;

Jira.labels = [];

tags.forEach(function (tag) {
if (typeof tag.tag !== 'undefined' && typeof tag.value !== 'undefined' ) {
label = (tag.tag + (tag.value ? (':' + tag.value) : '')).replace(/\s/g, '_');
if (label.length < 255) {
Jira.labels.push(label);
}
}
});
}
catch (error) {
// Code is not missing here.
}
}
},

addCustomFields: function (data, fields) {
if (typeof fields === 'object' && Object.keys(fields).length) {
if (Jira.schema) {
Object.keys(fields)
.forEach(function(field) {
data.requestFieldValues[field] = fields[field];

var block = Jira.schema.requestTypeFields.filter(function(object) {
return object.fieldId == field;
});

if (typeof block[0] === 'object' && typeof block[0].jiraSchema === 'object'
&& (block[0].jiraSchema.type === 'number' || block[0].jiraSchema.type === 'datetime')) {
switch (block[0].jiraSchema.type) {
case 'number':
data.requestFieldValues[field] = parseInt(fields[field]);
break;

case 'datetime':
if (fields[field].match(/\d+[.-]\d+[.-]\d+T\d+:\d+:\d+/) !== null) {
data.requestFieldValues[field] = fields[field].replace(/\./g, '-');
}
else {
delete data.requestFieldValues[field];
}
break;
}
}
});
}
else {
Zabbix.log(4, '[ Jira Service Desk Webhook ] Cannot add custom fields' +
'because failed to retrieve field schema.');
}
}

return data;
},

request: function (method, query, data) {
['url', 'user', 'token', 'password', 'servicedesk_id', 'request_type_id'].forEach(function (field) {
if (typeof Jira.params !== 'object' || typeof Jira.params[field] === 'undefined'
|| Jira.params[field] === '' ) {
throw 'Required Jira param is not set: "' + field + '".';
}
});

var response,
url = Jira.params.url + query,
request = new HttpRequest();

request.addHeader('Content-Type: application/json');
request.addHeader('Authorization: Bearer ' + Jira.params.token);
request.addHeader('X-ExperimentalApi: opt-in');
request.addHeader('User-Agent: Zabbix');

if (typeof Jira.HTTPProxy !== 'undefined' && Jira.HTTPProxy !== '') {
request.setProxy(Jira.HTTPProxy);
}

if (typeof data !== 'undefined') {
data = JSON.stringify(data);
}

Zabbix.log(4, '[ Jira Service Desk Webhook ] Sending request: ' + url +
((typeof data === 'string') ? ('\n' + data) : ''));

switch (method) {
case 'get':
response = request.get(url, data);
break;

case 'post':
response = request.post(url, data);
break;

case 'put':
response = request.put(url, data);
break;

default:
throw 'Unsupported HTTP request method: ' + method;
}

Zabbix.log(4, '[ Jira Service Desk Webhook ] Received response with status code ' +
request.getStatus() + '\n' + response);

if (response !== null) {
try {
response = JSON.parse(response);
}
catch (error) {
Zabbix.log(4, '[ Jira Service Desk Webhook ] Failed to parse response received from Jira');
response = null;
}
}

if (request.getStatus() < 200 || request.getStatus() >= 300) {
var message = 'Request failed with status code ' + request.getStatus();

if (response !== null && typeof response.errors !== 'undefined'
&& Object.keys(response.errors).length > 0) {
message += ': ' + JSON.stringify(response.errors);
}
else if (response !== null && typeof response.errorMessage !== 'undefined'
&& Object.keys(response.errorMessage).length > 0) {
message += ': ' + JSON.stringify(response.errorMessage);
}

throw message + ' Check debug log for more information.';
}

return {
status: request.getStatus(),
response: response
};
},

getSchema: function() {
var result = Jira.request('get', 'servicedesk/' + Jira.params.servicedesk_id + '/requesttype/' +
Jira.params.request_type_id + '/field');

if (typeof Jira.schema !== 'object' && typeof Jira.schema.requestTypeFields !== 'object') {
Jira.schema = null;
}
else {
Jira.schema = result.response;
}
},

createRequest: function(summary, description, fields) {
var labels =[];
var triggerId = params.trigger_id ? params.trigger_id : "UNKNOWN";

var data = {
serviceDeskId: Jira.params.servicedesk_id,
requestTypeId: Jira.params.request_type_id,
requestFieldValues: {
summary: summary,
description: description + "\n\nTRIGGER_ID:" + triggerId
}
};

if (Jira.labels && Jira.labels.length > 0) {
data.requestFieldValues.labels = Jira.labels;
}

var result = Jira.request('post', 'request', Jira.addCustomFields(data, fields));

if (typeof result.response !== 'object' || typeof result.response.issueKey === 'undefined') {
throw 'Cannot create Jira request. Check debug log for more information.';
}

return result.response.issueKey;
},

searchByTrigger: function(trigger_id) {
var jql =
'project = NOC AND ' +
'reporter = "Zabbix" AND ' +
'description ~ "TRIGGER_ID:' + trigger_id + '" ' +
'AND created >= -7d ' +
'ORDER BY created DESC';

var url = '../api/2/search?jql=' + encodeURIComponent(jql) +
'&fields=key,resolution,status' +
'&maxResults=1';

var result = Jira.request('get', url);

if (result.response.issues && result.response.issues.length > 0) {
return result.response.issues[0];
}

return null;
}
};

try {

var SUCCESS_RESOLUTIONS = ["10100", "10800"];

var FAIL_RESOLUTIONS = [
"10101",
"10102",
"10103",
"10201",
"10400"
];

var STATUS_RESOLVED = "5";
var STATUS_CLOSED = "6";

function safeCreate(summary, description, fields) {
if (params.no_create === '1') {
Zabbix.log(4, '[ DEBUG ] Skip Jira task creation');
return null;
} else {
return Jira.createRequest(summary, description, fields);
}
}

function isResolved(issue) {
return issue.fields.status &&
issue.fields.status.id === STATUS_RESOLVED;
}

function isClosed(issue) {
return issue.fields.status &&
issue.fields.status.id === STATUS_CLOSED;
}

function isSuccessResolution(issue) {
return issue.fields.resolution &&
SUCCESS_RESOLUTIONS.indexOf(issue.fields.resolution.id) !== -1;
}

function isFailResolution(issue) {
return issue.fields.resolution &&
FAIL_RESOLUTIONS.indexOf(issue.fields.resolution.id) !== -1;
}

Jira.reopenIssue = function(key) {
var transitions = Jira.request('get', '../api/2/issue/' + key + '/transitions');
Zabbix.log(4, '[ DEBUG ] transitions: ' + JSON.stringify(transitions.response));

var transitionId = null;

transitions.response.transitions.forEach(function(t) {
if (t.name.toLowerCase().indexOf('откры') !== -1 ||
t.name.toLowerCase().indexOf('reopen') !== -1) {
transitionId = t.id;
}
});

if (!transitionId) {
throw 'Reopen transition not found';
}

Jira.request('post', '../api/2/issue/' + key + '/transitions', {
transition: { id: transitionId }
});
};

var params = JSON.parse(value),
fields = {},
jira = {},
comment = {public: true},
result = {tags: {}},
required_params = [
'alert_subject', 'alert_message', 'event_source', 'event_value',
'event_update_status', 'event_recovery_value'
];

Object.keys(params)
.forEach(function (key) {
if (key.startsWith('jira_')) {
jira[key.substring(5)] = params[key];
}
else if (key.startsWith('customfield_')) {
fields[key] = params[key];
}
else if (required_params.indexOf(key) !== -1 && params[key] === '') {
throw 'Parameter "' + key + '" cannot be empty.';
}
});

if ([0, 1, 2, 3].indexOf(parseInt(params.event_source)) === -1) {
throw 'Incorrect "event_source" parameter given: ' + params.event_source + '\nMust be 0-3.';
}

if (params.event_value !== '0' && params.event_value !== '1'
&& (params.event_source === '0' || params.event_source === '3')) {
throw 'Incorrect "event_value" parameter given: ' + params.event_value + '\nMust be 0 or 1.';
}

if (params.event_update_status !== '0' && params.event_update_status !== '1' && params.event_source === '0') {
throw 'Incorrect "event_update_status" parameter given: ' + params.event_update_status + '\nMust be 0 or 1.';
}

if (params.event_source !== '0' && params.event_recovery_value === '0') {
throw 'Recovery operations are supported only for trigger-based actions.';
}

Jira.setParams(jira);
Jira.setProxy(params.HTTPProxy);
Jira.getSchema();
Jira.setTags(params.event_tags_json);

if (params.event_source !== '0' && params.event_recovery_value !== '0') {
Jira.createRequest(params.alert_subject, params.alert_message);
}
else if (params.event_value === '1' && params.event_update_status === '0') {

var issue = null;

issue = Jira.searchByTrigger(params.trigger_id);

Zabbix.log(4, '[ DEBUG ] search result: ' + JSON.stringify(issue));
var key = null;

var zbx = new HttpRequest();
zbx.addHeader('Content-Type: application/json');

function zbxAck(msg, action) {
var payload = {
jsonrpc: "2.0",
method: "event.acknowledge",
params: {
eventids: [params.event_id],
message: msg,
action: action
},
auth: params.zabbix_token,
id: 1
};

zbx.post(params.zabbix_url + "/api_jsonrpc.php", JSON.stringify(payload));
}

if (issue) {

var resolved = isResolved(issue);
var closed = isClosed(issue);

var success = isSuccessResolution(issue);
var failed = isFailResolution(issue);

Zabbix.log(4,
'[ DEBUG ] statusId=' + issue.fields.status.id +
' resolutionId=' +
(issue.fields.resolution ? issue.fields.resolution.id : 'null') +
' resolved=' + resolved +
' closed=' + closed +
' success=' + success +
' failed=' + failed
);

if (!resolved && !closed) {
zbxAck(
"Заявка уже существует: " + issue.key + "\n" +
params.jira_url + "browse/" + issue.key,
4
);
return JSON.stringify(result);
}

if (closed) {
Zabbix.log(4, '[ DEBUG ] status=closed - create new');
}

else if (resolved && success) {

Jira.reopenIssue(issue.key);

Jira.request('post', '../api/2/issue/' + issue.key + '/comment', {
body: "Проблема не решена, тикет переоткрыт"
});

zbxAck(
"Переоткрыт тикет Jira: " + issue.key + "\n" +
params.jira_url + "browse/" + issue.key,
4
);
return JSON.stringify(result);
}

else if (resolved && failed) {

zbxAck(
"Ранее отклонено: " + issue.key,
6
);
return JSON.stringify(result);
}

}

var key = safeCreate(params.alert_subject, params.alert_message, fields);

if (key) {

var updateData = {
update: {
labels: [
{ add: "Zabbix" }
]
}
};

Jira.request('put', '../api/2/issue/' + key, updateData);

zbxAck(
"Создан тикет Jira: " + key + "\n" +
params.jira_url + "browse/" + key,
4
);

result.tags.__zbx_jira_requestkey = key;
result.tags.__zbx_jira_requestlink = params.jira_url +
(params.jira_url.endsWith('/') ? '' : '/') + 'browse/' + key;
}
}
else {
if (jira.request_key === '{EVENT.TAGS.__zbx_jira_requestkey}' || jira.request_key.trim() === '') {
throw 'Incorrect Request key given: ' + jira.request_key;
}
comment.body = params.alert_message;
Jira.request('post', 'request/' + Jira.params.request_key + '/comment', comment);
}

return JSON.stringify(result);
}
catch (error) {
Zabbix.log(3, '[ Jira Service Desk Webhook ] ERROR: ' + error);
throw 'Sending failed: ' + error;
}

Что делает этот скрипт

Этот webhook предназначен для регистрации задач в Jira Service Management из события Zabbix.

Скрипт запускается вручную из карточки события Zabbix и выполняет следующую логику:

  1. Получает параметры события Zabbix.
  2. Подключается к Jira API.
  3. Ищет уже существующую задачу по TRIGGER_ID.
  4. Если задачи нет — создаёт новую.
  5. Если задача уже есть и находится в работе — не создаёт дубль.
  6. Если задача была решена успешно — переоткрывает её.
  7. Если задача была закрыта — создаёт новую.
  8. Если задача была отклонена — не создаёт новую.
  9. Записывает ссылку на Jira-задачу обратно в событие Zabbix.

Основная идея — не плодить дубли по одному и тому же триггеру, а связать событие Zabbix с задачей Jira.


Настройка скрипта в Zabbix

Настройка выполняется в разделе:

Администрирование → Скрипты → Создать скрипт

Пример основных настроек:

ПолеЗначение
ИмяCreate jira ticket
ОбластьДействие вручную над событиями
Путь в менюServiceDesk/
ТипWebhook
Время ожидания30s
Описаниерегистрация заявок на дежурных ОСА в Jira

После настройки скрипт будет доступен из события Zabbix в меню:

ServiceDesk → Create jira ticket

Оператор открывает событие, запускает скрипт, и Zabbix создаёт или обновляет задачу в Jira.


Параметры webhook-скрипта

Параметры указываются в таблице Параметры внутри скрипта.

Основные параметры события

ПараметрЗначениеОписание
alert_messageПроблема на хосте {HOST.HOST}Описание задачи в Jira
alert_subjectПроблема: {HOST.HOST} - {EVENT.NAME}Заголовок задачи
event_id{EVENT.ID}ID события Zabbix
event_recovery_value{EVENT.RECOVERY.VALUE}Признак восстановления события
event_source0Источник события. Для триггеров используется 0
event_tags_json{EVENT.TAGSJSON}Теги события Zabbix в JSON
event_update_status{EVENT.UPDATE.STATUS}Статус обновления события
event_value{EVENT.VALUE}Значение события: 1 — проблема, 0 — восстановление
trigger_id{TRIGGER.ID}ID триггера. Используется для защиты от дублей
no_create00 — создавать задачи, 1 — тестовый режим без создания

Параметры подключения к Jira

ПараметрЗначениеОписание
jira_urlhttps://jira.example.ru/URL Jira
jira_user<PLACE LOGIN>Технический пользователь Jira
jira_password<PLACE PASSWORD>Обязательный параметр для проверки скрипта. Авторизация фактически идёт через token
jira_token<JIRA TOKEN>Токен для авторизации в Jira API
jira_servicedesk_id1ID Service Desk проекта
jira_request_type_id271ID типа заявки в Jira Service Management
jira_request_key{EVENT.TAGS.__zbx_jira_requestkey}Ключ уже созданной Jira-задачи, если он есть в тегах события

В скрипте все параметры с префиксом jira_ автоматически преобразуются во внутренние параметры Jira.

Например:

jira_url → url
jira_token → token
jira_servicedesk_id → servicedesk_id
jira_request_type_id → request_type_id

Поэтому названия параметров нужно указывать точно.


Параметры подключения к Zabbix API

ПараметрЗначениеОписание
zabbix_urlhttps://zabbix.example.ruURL Zabbix
zabbix_token<ZABBIX API TOKEN>Токен Zabbix API

Zabbix API используется для записи комментария обратно в событие.

Пример сообщения после создания задачи:

Создан тикет Jira: NOC-12345
https://jira.example.ru/browse/NOC-12345

Если задача уже существует:

Заявка уже существует: NOC-12345
https://jira.example.ru/browse/NOC-12345

Как работает защита от дублей

При создании задачи скрипт добавляет в описание Jira служебную строку:

TRIGGER_ID:<id триггера>

Например:

TRIGGER_ID:456789

При повторном запуске скрипт ищет задачу в Jira по этому TRIGGER_ID.

Поиск выполняется через JQL:

project = NOC AND reporter = "Zabbix" AND description ~ "TRIGGER_ID:<trigger_id>" AND created >= -7d ORDER BY created DESC

Если задача уже есть и она не закрыта, новая задача не создаётся.


Логика обработки задачи

Скрипт работает по следующей схеме:

Запуск скрипта из события Zabbix

Получение TRIGGER_ID

Поиск задачи в Jira

Если задачи нет — создать новую

Если задача уже в работе — не создавать дубль

Если задача решена успешно — переоткрыть

Если задача закрыта — создать новую

Если задача отклонена — ничего не делать

Важные места в скрипте

Перед использованием нужно проверить несколько значений под свою Jira.

Ключ проекта Jira

В скрипте сейчас указан проект:

project = NOC

Если задачи должны создаваться или искаться в другом проекте, нужно заменить NOC на свой ключ проекта.

Например:

project = OPS

или:

project = SD

ID статусов Jira

В скрипте используются ID статусов:

var STATUS_RESOLVED = "5";
var STATUS_CLOSED = "6";

Эти значения могут отличаться в вашей Jira.

Если ID будут неверные, скрипт может неправильно определять, закрыта задача или находится в работе.


ID резолюций Jira

Успешные резолюции:

var SUCCESS_RESOLUTIONS = ["10100", "10800"];

Отказные резолюции:

var FAIL_RESOLUTIONS = [
"10101",
"10102",
"10103",
"10201",
"10400"
];

Эти ID тоже нужно сверить со своей Jira.

Если задача была решена с успешной резолюцией и проблема появилась снова, скрипт переоткроет задачу.

Если задача была решена с отказной резолюцией, скрипт не будет создавать новую задачу.


Проверка работы

После настройки нужно открыть проблемное событие в Zabbix и запустить:

ServiceDesk → Create jira ticket

Если всё настроено корректно, в Jira появится новая задача, а в событии Zabbix будет записан комментарий со ссылкой.

Пример:

Создан тикет Jira: NOC-12345
https://jira.example.ru/browse/NOC-12345

При повторном запуске по тому же событию дубль создаваться не должен. Вместо этого в Zabbix появится сообщение:

Заявка уже существует: NOC-12345
https://jira.example.ru/browse/NOC-12345

Отладка

Основной лог для проверки работы webhook:

tail -f /var/log/zabbix/zabbix_server.log

В скрипте уже есть debug-сообщения:

Zabbix.log(4, '[ Jira Service Desk Webhook ] Sending request: ' + url)
Zabbix.log(4, '[ Jira Service Desk Webhook ] Received response with status code ' + request.getStatus())
Zabbix.log(4, '[ DEBUG ] search result: ' + JSON.stringify(issue))
Zabbix.log(3, '[ Jira Service Desk Webhook ] ERROR: ' + error)

Если задача не создаётся, в первую очередь нужно проверить:

jira_url
jira_token
jira_servicedesk_id
jira_request_type_id
jira_user
jira_password
zabbix_url
zabbix_token
trigger_id

Типовые ошибки

Required Jira param is not set

Ошибка вида:

Required Jira param is not set: "token"

означает, что в параметрах скрипта не передан обязательный параметр.

Например, если ошибка по token, нужно проверить наличие параметра:

jira_token

Если ошибка по servicedesk_id, проверить:

jira_servicedesk_id

Reopen transition not found

Ошибка:

Reopen transition not found

означает, что скрипт не нашёл переход для переоткрытия задачи.

Сейчас он ищет transition по словам:

откры
reopen

Если в Jira переход называется иначе, нужно доработать этот блок:

if (t.name.toLowerCase().indexOf('откры') !== -1 ||
t.name.toLowerCase().indexOf('reopen') !== -1) {
transitionId = t.id;
}

Что получаем в итоге

После настройки оператор может прямо из события Zabbix создать задачу в Jira.

При этом скрипт не просто создаёт заявку, а учитывает текущую ситуацию:

задачи нет → создать;
задача уже в работе → не создавать дубль;
задача решена успешно → переоткрыть;
задача закрыта → создать новую;
задача отклонена → ничего не делать.

Такой подход удобен для эксплуатации и NOC: Zabbix остаётся источником технических событий, а Jira используется как система учёта и обработки инцидентов.

Действия с записью
Назад в блог

Комментарии

Комментариев пока нет.

Войдите, чтобы оставить комментарий.