#!/usr/bin/env groovy
/**
* Импортируем библиотеки для Jenkins. Для этого библиотеки должны быть описаны в конфигурации Jenkins (Jenkins/Настроить Jenkins/Global Pipeline Libraries)
*/
@Library(['DeploykaHelper', 'CommonUse'])_
// Данный скрипт - весь выполняется на одном узле.
node {
// region Переменные для хранения объектов VanRunnerHelper
// Текущий объект-запускалка. Для запуска каких-либо операций будем использовать лишь его.
// Задумано чтобы избежать возможных ошибок использования какого-то "не того" объекта - подключение к рабочей БД вместо тестовой БД.
def runner
// Объект-запускалка, настроенный на рабочую БД
def runnWork
// Объект-запускалка, настроенный на тестовую БД
def runnTest
// endregion Переменные для хранения объектов VanRunnerHelper
// region Пути библиотек, которые будут дополнительно скачиваться с Git.
// Каталог для скачивания Vanessa
WORKSPACE_VANESSA_RUNNER = 'vanessa-runner'
// Полный путь к основному файлу запуска Vanessa-runner
PATH_TO_VANESSA_RUNNER = "\"${env.workspace}\\${WORKSPACE_VANESSA_RUNNER}\\main.os\""
// Каталог для скачивания сервисных обработок 1С
WORKSPACE_EPFS = 'epfs'
// Полный путь к обработке ЗавершениеРаботы.epf
PATH_TO_SERVICE_EPF = "\"${env.workspace}\\${WORKSPACE_EPFS}\\ЗавершениеРаботы.epf\""
// endregion Пути библиотек, которые будут дополнительно скачиваться с Git.
// region Данные, связанные с логированием
def logTextFull = '' // полный лог
def logTextBase = '' // сокращенный лог, куда пишутся только важные сообщения
def finalResult = false // итоговый результат, отработал скрипт или нет
LOG_START = 'Начало'
LOG_FINISH = 'Завершение'
LOG_OK = 'ОК'
LOG_ERROR = 'Ошибка'
LOG_WARNING = 'Предупреждение'
LOG_SUCCESS = 'Успешно'
LOG_INFO = 'Информация'
PRIOR_LEVEL_ORDINAL = 0
PRIOR_LEVEL_MIDDLE = 1
// endregion Данные, связанные с логированием
// region Параметры для запускалок
// путь к хранилищу
pathToRepo = envVar('PATH_TO_REPO')
// путь к утилите администрирования rac.exe
pathToRACUtility = envVar('PATH_TO_RAC')
// Используeтся ли тестовая БД
isUsesTestDB = boolFromString(envVar('IS_USES_TEST_DB', false), true)
// Используeтся ли рабочая БД
isUsesWorkDB = boolFromString(envVar('IS_USES_WORK_DB', false), true)
// endregion Параметры для запускалок
/**
* Простые функции для установки и/или использования текущего объекта-запускалки runner.
* Предназначены, в основном, для улучшения читаемости кода. Если БД не используется, то текущая запускался устанавливается =null
*/
def useTestDB = { Closure body ->
runner = isUsesTestDB ? runnTest : null
if (body!=null && runner!=null) {
body.delegate = runner
body(runner)
} else
runner
}
def useWorkDB = {Closure body ->
runner = isUsesWorkDB ? runnWork : null
if (body!=null && runner!=null) {
body.delegate = runner
body(runner)
} else
runner
}
/**
* Процедура для добавления сообщений в лог.
* Написано для потенциальной переделки без изменения интерфейса.
*/
def toLog = { String logText = null, String logCaption = null, int logPriority = null ->
// конструируем строку сообщения
String msg = "${dateNow()}"
if ( logText!=null && logCaption!=null )
msg = "$msg $logText - $logCaption"
else
if (logText!=null)
msg = "$msg $logText"
else
return null
// добавляем сообщение в логи
logTextFull = "${logTextFull}${logTextFull.size()==0 ? '' : '\n'}${msg}"
if (logPriority!=null && logPriority>PRIOR_LEVEL_ORDINAL)
logTextBase = "${logTextBase}${logTextBase.size()==0 ? '' : '\n'}${msg}"
// выводим сообщение эхом
echo "$msg"
return msg
}
/**
* Обработчик оповещений из объектов-запускалок или из других мест скрипта.
*/
def runnerNotifyHandler = {def msgText, def source, def msgKind, def msgType, def operationResult, Object... params ->
Boolean isFromRunner = source==runnTest || source==runnWork
// определяем, какие сигналы оповещений будем обрабатывать
Boolean isResult =
(isFromRunner && msgType!=source.NOTIFY_TYPE_BEFORE) ||
(isFromRunner && msgKind==source.OP_UPDATE_DB) ||
(isFromRunner && msgKind==source.OP_WAIT_FOR_CLOSE) ||
(isFromRunner && msgKind==source.OP_WAIT_FOR_CLOSE_CONTINUE) ||
(isFromRunner && msgKind==source.OP_KILL_SESSIONS) ||
(isFromRunner && operationResult!=true) ||
!isFromRunner
if (!isResult)
return true
// дописываем в сообщение лога БД (тест/прод) и текстовое представление результата
def msgCaption = operationResult==null ? LOG_INFO : ( operationResult ? LOG_OK : LOG_ERROR )
def msgMode = isFromRunner ? (source==runnWork ? ' (рабочая БД) ' : ' (тестовая БД) ') : ''
toLog("${msgText}${msgMode}", msgCaption)
// решаем, нужно ли вызывать ошибку для прерывания работы скрипта
Boolean needError = false ||
(isFromRunner && operationResult!=true && msgType==source.NOTIFY_TYPE_AFTER &&
(msgKind!=source.OP_WAIT_FOR_CLOSE && msgKind!=source.OP_UNLOAD_CONFIG_DB)) ||
(source==null && operationResult!=true)
// решаем, нужно ли выводить сообщение в логи
Boolean needRunnerLog = isFromRunner && operationResult!=true
// решаем, нужно ли выводить параметры запуска в лог
Boolean needRunnerParams = isFromRunner && operationResult!=true
// решаем, нужно ли выводить доп.информацию для лога Jenkins
Boolean needRunnerLogOnlyEcho =
(isFromRunner && msgKind==source.OP_KILL_SESSIONS) ||
(isFromRunner && msgType==source.NOTIFY_TYPE_AFTER && msgKind==source.OP_UPDATE_DB) ||
(isFromRunner && msgKind==source.OP_KILL_SESSIONS) ||
false
if (needRunnerLog)
toLog(source.resultLog)
if (needRunnerParams)
toLog(source.getLaunchString())
if (needRunnerLogOnlyEcho && !needRunnerLog)
echo source.resultLog
if (needError)
error 'Ошибка выполнения скрипта!'
}
def checkStatus = { def status, String logText = null, source = null ->
runnerNotifyHandler(logText, source, null, null, status)
}
/**
* Оповещение о результатах работы скрипта
*/
def notifyAboutResult = {
def subj = "${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}: ${finalResult==true ? 'Успешно' : 'Ошибка'}"
def notifier, handler
// уведомляем эл.почтой
handler = { mail bcc: '', body: "$logTextFull", cc: '', from: '', replyTo: '', subject: subj, to: envVar('NOTIFY_EMAIL_ADDR') }
notifier = boolFromString(envVar('NOTIFY_BY_EMAIL'), false) ? newNotifier() : null
notifier?.doNotify(handler)
// уведомляем телеграммом
handler = { build job: 'NotifyByTelegram', parameters: [string(name: 'teleText', value: "${subj}\n${logTextBase}"), string(name: 'TELE_CHAT_ID', value: "${envVar('NOTIFY_TELEGRAM_CHAT')}")] }
notifier = boolFromString(envVar('NOTIFY_BY_TELEGRAM'), false) ? newNotifier() : null
notifier?.doNotify(handler)
return true
}
/**
* Функция для закрытия всех сессий
*/
def killAllSessions = {->
def maxCount = 3 // Сделаем три попытки закрытия сеансов
def i
def runLog
def killed = false
def msg
for (i = 0; i < maxCount; i++){
try {
// на всякий случай ограничим максимальное время прибития сеансов - отведем на это 200 сек
timeout(time: 200, unit: 'SECONDS') {
runner.killSessions(true)
}
killed = true
break
} catch (e) {
// если что-то пошло не так, то выведем лог работы раннера
runLog = runner.resultLog
runLog = runLog==null ? '(пустой лог)' : runLog
msg = "Ошибка при завершении всех сеансов (попытка ${i + 1}):\n${runLog}"
toLog(msg)
}
}
// сгенерируем ошибку, если сеансы в итоге так и не завершены
if (!killed)
error('Не удалось завершить сеансы')
killed
}
/**
* Обновление конфигурации из хранилища
* waitingBeforeUpdate - опциональное замыкание, в котором выполняется задержка между запретом начала сеансов и прерыванием сеансов.
* Такая задержка нужна, в основном, на продуктиве, чтобы дождаться завершения каких-то фоновых заданий.
* Задержка реализована отдельно, чтобы не засорять основной код процедуры.
*/
def updateDbFromRepo = {def waitingBeforeUpdate = null ->
// Если текущая запускалка - пустая, то выходим ничего не делая
if (runner==null)
return true
Boolean needUpdateDB = false
// прибиваем сеансы конфигураторов
runner.killSessions(true, runner.newSessionFilter().addAppDesigner())
try {
// установим флаг, что при выполнении команды нам нужно будет вывести в лог строку запуска
runner.printCmdOnce = true
runner.loadConfigFromRepo()
// Иногда проверка обновленности сразу после загрузки из хранилища показывает, что обновление не требуется.
// Поэтому пробуем делать паузу. В sleep передаются секунды, а не милисекунды (как подсказывает Idea)!
sleep(10)
runner.launchUserInterface()
// читаем состояние конфигурации - требуется ли обновление конфигурации БД
needUpdateDB = runner.configInfo.isChanged
} catch (e) {
// todo протестировать при подключении БД к хранилищу под другим пользователем
// todo протестировать без подключения БД к хранилищу
toLog('Ошибка во время загрузки конфигурации из хранилища\n'.concat(e.getMessage()))
runner.unbindRepo()
runner.bindRepo()
needUpdateDB = true
}
if (needUpdateDB==true) { // Блок кода для обновления конфигурации БД
// запрещаем начало сессий и старт РЗ/ФЗ
runner.setSessionsEnabled(false)
runner.setBackgroundsEnabled(false)
try {
// Можно написать более сложную обработку ожидания. Например, не продолжать обновление, если не дождались завершения сеансов.
// Но сделаем пока проще - рвем все сеансы по истечении макс времени ожидания.
if (waitingBeforeUpdate==null)
toLog('Ожидание завершения сеансов не требуется')
else {
toLog('Выполняем ожидание завершения сеансов')
waitingBeforeUpdate(runner)
}
toLog('Начинаем прерывание сеансов')
killAllSessions()
runner.updateDb()
timeout(15) {
// Запускаем в режиме 1С:Предприятие с обновлением метаданных.
// На всякий случай ограничим время выполнения операции - макс 15 минут на это.
runner.launchUserInterface(true)
}
// Для проверки модифицированности, т.к. в режиме с обновлением МД результат модифицированности может быть не получен.
// Во время обновления МД может потребоваться монопольный доступ. И обработка в режиме обновления МД может "свалиться" и не дойти до записи в лог информации об обновлении
runner.launchUserInterface()
def isUpdateDone = runner.configInfo.isChanged==false
checkStatus(isUpdateDone, 'Проверка модифицированности конфигурации после обновления', runner)
if (runner.configInfo.isChanged==null)
toLog("Лог проверки запуска 1С:Предприятие:\n${runner.resultLog}")
} finally {
// в любом случае разрешаем сеансы и РЗ/ФЗ
runner.setSessionsEnabled()
runner.setBackgroundsEnabled()
}
} else {
if (needUpdateDB==false)
toLog('Обновление БД не требуется')
else {
// Это на всякий случай, чтобы видеть, что происходит что-то непонятное при определении модифицированности конфигурации
toLog("Непонятный статус определения обновленности. Лог:\n$runner.resultLog")
needUpdateDB = true
}
}
}
// Обрамляем весь код в try-finally, чтобы можно было выполнить какие-то пост-аварийные действия
try {
toLog('======', LOG_START, PRIOR_LEVEL_MIDDLE)
stage('Инициализация') {
// загружаем доп.библиотеки из Git
dir("${WORKSPACE_VANESSA_RUNNER}") {
git poll: false, credentialsId: 'git_deployka_auth', url: 'http://upr-jenkins/Bonobo.Git.Server/vanessa-runner.git'
}
dir("${WORKSPACE_EPFS}") {
git poll: false, credentialsId: 'git_deployka_auth', url: 'http://upr-jenkins/Bonobo.Git.Server/epfs_for_jenkins.git'
}
toLog('Загрузка библиотек из Git', LOG_SUCCESS, PRIOR_LEVEL_ORDINAL)
// создаем объекты-запускалки
runnWork = initVanRunner(PATH_TO_VANESSA_RUNNER, PATH_TO_SERVICE_EPF)
runnTest = initVanRunner(PATH_TO_VANESSA_RUNNER, PATH_TO_SERVICE_EPF)
// Задаем параметры для рабочей БД
useWorkDB()
withUserCredential(envVar('WORK_DB_AUTH')) { nm, pwd -> runner?.setDb(envVar('WORK_SERVER'), envVar('WORK_DATABASE'), nm, pwd) }
withUserCredential(envVar('WORK_REPO_AUTH')) { nm, pwd -> runner?.setRepo(pathToRepo, nm, pwd) }
runner?.setRAS(envVar('WORK_RAS_SERVER'), pathToRACUtility)
runner?.setDefaultEnables(true, boolFromString(envVar('WORK_BACKGROUND_PROCESSED_ENABLED'), true))
if (runner!=null) {
runner.notifyEvent = runnerNotifyHandler
// runner.mainProcessName = 'vanessa-runner'
}
// Задаем параметры для тестовой БД
useTestDB()
// Внутри скобок можно использовать встроенную переменную it, означающую объект-запускалку для тестовой БД
withUserCredential(envVar('TEST_DB_AUTH')) { nm, pwd -> runner?.setDb(envVar('TEST_SERVER'), envVar('TEST_DATABASE'), nm, pwd) }
withUserCredential(envVar('TEST_REPO_AUTH')) { nm, pwd -> runner?.setRepo(pathToRepo, nm, pwd) }
runner?.setRAS(envVar('TEST_RAS_SERVER'), pathToRACUtility)
runner?.setDefaultEnables(true, boolFromString(env.TEST_BACKGROUND_PROCESSED_ENABLED, false))
if (runner!=null) {
runner.notifyEvent = runnerNotifyHandler
// runner.mainProcessName = 'vanessa-runner'
}
// Рапортуем, что запускалки успешно созданы.
toLog('Создание программных объектов', LOG_SUCCESS, PRIOR_LEVEL_ORDINAL)
}
stage('Проверка необходимых ресурсов') {
checkStatus(folder(pathToRepo).enabled(), 'Проверка доступности каталога хранилища', this)
checkStatus(folder(envVar('PATH_TO_BACKUPS')).enabled(), 'Проверка доступности каталога бакапов', this)
useTestDB()
runner?.launchUserInterface()
runner?.askDatabaseInfo()
useWorkDB()
runner?.launchUserInterface()
runner?.askDatabaseInfo()
}
stage('Проверка наличия бакапа') {
if (boolFromString(env.FLAG_CHECK_BACKUP, false) && isUsesWorkDB) {
def now = dateNow()
def minDt = now.getSafeDate('21:30', '07:15', -20)
def maxDt = now.addMinutes(-1)
def bups = folder(env.PATH_TO_BACKUPS)
def bupsChecked = bups.hasFiles('*.bak', minDt, maxDt)
checkStatus(bupsChecked, 'Проверка наличия бакапа', this)
} else
checkStatus(true, 'Проверка бакапа пропущена')
}
stage('Обновление тестовой БД') {
useTestDB()
updateDbFromRepo()
}
stage('Обновление рабочей БД') {
useWorkDB()
updateDbFromRepo() { // для рабочей БД ждем завершения сеансов фоновых заданий
def dt1 = dateNow().addMinutes(15)
it.waitForCloseSessions(dt1, 4, it.newSessionFilter().addAppBackgroung())
}
}
stage('Выгрузка конфигурации') {
try {
def cfPath = envVar('CF_PATH', false)
if (cfPath != null && cfPath.toString() != '') {
cfFolder = folder(cfPath)
useWorkDB()
if (cfFolder.enabled() && runner!=null) {
sleep(20)
fileName = cfFolder.getDatedFileName(true, 'bf_')
runner.unloadConfigDB(fileName)
cfFolder.leaveLastUniqueFiles('*.cf')
}
} else
checkStatus(true, 'Выгрузка конфигурации пропущена')
} catch (e) {
toLog(e.getMessage(), LOG_ERROR)
}
}
finalResult = true
} catch (e) {
toLog("Что-то сбойнуло", LOG_ERROR)
throw e
} finally {
toLog('====== Работа скрипта завершена ', '======', PRIOR_LEVEL_MIDDLE)
notifyAboutResult()
}
}