This commit is contained in:
2025-08-04 11:03:25 +03:00
commit b1bde827de
20 changed files with 3579 additions and 0 deletions

333
internal/rac/rac.go Normal file
View File

@@ -0,0 +1,333 @@
package rac
import (
"context"
"fmt"
"os/exec"
"strings"
"time"
"unicode/utf8"
"git.benadis.ru/gitops/benadis-rac/internal/config"
"git.benadis.ru/gitops/benadis-rac/internal/constants"
"git.benadis.ru/gitops/benadis-rac/internal/logger"
)
// Client представляет клиент для работы с RAC
type Client struct {
config *config.AppConfig
logger logger.Logger
}
// NewClient создает новый RAC клиент
func NewClient(cfg *config.AppConfig, log logger.Logger) *Client {
return &Client{
config: cfg,
logger: log,
}
}
// ServiceModeParams параметры для управления сервисным режимом
type ServiceModeParams struct {
ClusterUUID string
InfobaseUUID string
Enable bool
DeniedMessage string
PermissionCode string
}
// ExecuteCommand выполняет RAC команду с retry логикой
func (c *Client) ExecuteCommand(ctx context.Context, args []string) (string, error) {
var lastErr error
retryCount := c.config.Config.RetryCount
if retryCount == 0 {
retryCount = constants.DEFAULT_RETRY_COUNT
}
retryDelay, err := c.config.Config.GetRetryDelay()
if err != nil {
c.logger.Warn(constants.LogMsgFailedToParseRetryDelay, "error", err)
retryDelay = constants.DEFAULT_RETRY_DELAY
}
for attempt := 1; attempt <= retryCount; attempt++ {
c.logger.Debug(constants.LogMsgExecutingRACCommand, "attempt", attempt, "max_attempts", retryCount)
c.logger.DebugCommand(constants.LogMsgRACCommand, append([]string{c.config.Config.RACPath}, args...))
output, err := c.executeCommandOnce(ctx, args)
if err == nil {
c.logger.Debug(constants.LogMsgRACCommandExecutedSuccessfully, "output_length", len(output))
return output, nil
}
lastErr = err
c.logger.Warn(constants.LogMsgRACCommandFailed, "attempt", attempt, "error", err)
if attempt < retryCount {
c.logger.Debug(constants.LogMsgRetryingAfterDelay, "delay", retryDelay)
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(retryDelay):
// Продолжаем
}
}
}
c.logger.Error(constants.LogMsgRACCommandFailedAfterAllRetries, "error", lastErr)
return "", fmt.Errorf(constants.ErrCommandFailed, retryCount, lastErr)
}
// executeCommandOnce выполняет RAC команду один раз
func (c *Client) executeCommandOnce(ctx context.Context, args []string) (string, error) {
cmdTimeout, err := c.config.Config.GetCommandTimeout()
if err != nil {
return "", fmt.Errorf(constants.ErrFailedToParseCommandTimeout, err)
}
ctx, cancel := context.WithTimeout(ctx, cmdTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.config.Config.RACPath, args...)
output, err := cmd.CombinedOutput()
if err != nil {
// Преобразуем вывод в корректную кодировку
outputStr := convertToUTF8(output)
return "", fmt.Errorf(constants.ErrRACCommandFailed, err, outputStr)
}
return convertToUTF8(output), nil
}
// GetClusterUUID получает UUID кластера по имени сервера
func (c *Client) GetClusterUUID(ctx context.Context) (string, error) {
c.logger.Info(constants.LogMsgGettingClusterUUID)
args := []string{
c.config.GetRACAddress(),
"cluster", "list",
}
output, err := c.ExecuteCommand(ctx, args)
if err != nil {
return "", fmt.Errorf(constants.ErrGetClusterList, err)
}
// Парсим вывод для получения UUID кластера
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "cluster") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
uuid := strings.TrimSpace(parts[1])
c.logger.Debug(constants.LogMsgFoundClusterUUID, "uuid", uuid)
return uuid, nil
}
}
}
return "", fmt.Errorf(constants.ErrClusterUUIDNotFound)
}
// GetInfobaseUUID получает UUID информационной базы по имени
func (c *Client) GetInfobaseUUID(ctx context.Context, clusterUUID string) (string, error) {
c.logger.Info(constants.LogMsgGettingInfobaseUUID, "cluster_uuid", clusterUUID)
// Получаем учетные данные кластера (пока используем пустое имя проекта для глобальных настроек)
clusterUser, clusterPwd := c.config.GetClusterCredentials("")
args := []string{
c.config.GetRACAddress(),
"infobase", "summary", "list",
"--cluster=" + clusterUUID,
"--cluster-user=" + clusterUser,
"--cluster-pwd=" + clusterPwd,
}
output, err := c.ExecuteCommand(ctx, args)
if err != nil {
return "", fmt.Errorf(constants.ErrGetInfobaseList, err)
}
// Парсим вывод для поиска информационной базы по имени
lines := strings.Split(output, "\n")
var currentInfobaseUUID string
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "infobase") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
currentInfobaseUUID = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "name") && currentInfobaseUUID != "" {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
name := strings.TrimSpace(parts[1])
if name == c.config.Project.ServiceMode.BaseName {
c.logger.Debug(constants.LogMsgFoundInfobaseUUID, "uuid", currentInfobaseUUID, "name", name)
return currentInfobaseUUID, nil
}
}
}
}
return "", fmt.Errorf(constants.ErrInfobaseUUIDNotFound, c.config.Project.ServiceMode.BaseName)
}
// convertToUTF8 преобразует байты из CP866 (DOS) или Windows-1251 в UTF-8
func convertToUTF8(data []byte) string {
// Проверяем, является ли строка уже валидным UTF-8
if utf8.Valid(data) {
return string(data)
}
// Преобразование из CP866 (DOS) в UTF-8
// Это основная кодировка для сообщений об ошибках RAC
runes := make([]rune, 0, len(data))
for _, b := range data {
if b < 128 {
// ASCII символы остаются без изменений
runes = append(runes, rune(b))
} else {
// Преобразование символов CP866 в Unicode
switch b {
// Кириллические символы CP866
case 0x80:
runes = append(runes, 'А') // А
case 0x81:
runes = append(runes, 'Б') // Б
case 0x82:
runes = append(runes, 'В') // В
case 0x83:
runes = append(runes, 'Г') // Г
case 0x84:
runes = append(runes, 'Д') // Д
case 0x85:
runes = append(runes, 'Е') // Е
case 0x86:
runes = append(runes, 'Ж') // Ж
case 0x87:
runes = append(runes, 'З') // З
case 0x88:
runes = append(runes, 'И') // И
case 0x89:
runes = append(runes, 'Й') // Й
case 0x8A:
runes = append(runes, 'К') // К
case 0x8B:
runes = append(runes, 'Л') // Л
case 0x8C:
runes = append(runes, 'М') // М
case 0x8D:
runes = append(runes, 'Н') // Н
case 0x8E:
runes = append(runes, 'О') // О
case 0x8F:
runes = append(runes, 'П') // П
case 0x90:
runes = append(runes, 'Р') // Р
case 0x91:
runes = append(runes, 'С') // С
case 0x92:
runes = append(runes, 'Т') // Т
case 0x93:
runes = append(runes, 'У') // У
case 0x94:
runes = append(runes, 'Ф') // Ф
case 0x95:
runes = append(runes, 'Х') // Х
case 0x96:
runes = append(runes, 'Ц') // Ц
case 0x97:
runes = append(runes, 'Ч') // Ч
case 0x98:
runes = append(runes, 'Ш') // Ш
case 0x99:
runes = append(runes, 'Щ') // Щ
case 0x9A:
runes = append(runes, 'Ъ') // Ъ
case 0x9B:
runes = append(runes, 'Ы') // Ы
case 0x9C:
runes = append(runes, 'Ь') // Ь
case 0x9D:
runes = append(runes, 'Э') // Э
case 0x9E:
runes = append(runes, 'Ю') // Ю
case 0x9F:
runes = append(runes, 'Я') // Я
case 0xA0:
runes = append(runes, 'а') // а
case 0xA1:
runes = append(runes, 'б') // б
case 0xA2:
runes = append(runes, 'в') // в
case 0xA3:
runes = append(runes, 'г') // г
case 0xA4:
runes = append(runes, 'д') // д
case 0xA5:
runes = append(runes, 'е') // е
case 0xA6:
runes = append(runes, 'ж') // ж
case 0xA7:
runes = append(runes, 'з') // з
case 0xA8:
runes = append(runes, 'и') // и
case 0xA9:
runes = append(runes, 'й') // й
case 0xAA:
runes = append(runes, 'к') // к
case 0xAB:
runes = append(runes, 'л') // л
case 0xAC:
runes = append(runes, 'м') // м
case 0xAD:
runes = append(runes, 'н') // н
case 0xAE:
runes = append(runes, 'о') // о
case 0xAF:
runes = append(runes, 'п') // п
case 0xE0:
runes = append(runes, 'р') // р
case 0xE1:
runes = append(runes, 'с') // с
case 0xE2:
runes = append(runes, 'т') // т
case 0xE3:
runes = append(runes, 'у') // у
case 0xE4:
runes = append(runes, 'ф') // ф
case 0xE5:
runes = append(runes, 'х') // х
case 0xE6:
runes = append(runes, 'ц') // ц
case 0xE7:
runes = append(runes, 'ч') // ч
case 0xE8:
runes = append(runes, 'ш') // ш
case 0xE9:
runes = append(runes, 'щ') // щ
case 0xEA:
runes = append(runes, 'ъ') // ъ
case 0xEB:
runes = append(runes, 'ы') // ы
case 0xEC:
runes = append(runes, 'ь') // ь
case 0xED:
runes = append(runes, 'э') // э
case 0xEE:
runes = append(runes, 'ю') // ю
case 0xEF:
runes = append(runes, 'я') // я
default:
// Для неизвестных символов оставляем как есть
runes = append(runes, rune(b))
}
}
}
return string(runes)
}

View File

@@ -0,0 +1,235 @@
package rac
import (
"context"
"fmt"
"strings"
"git.benadis.ru/gitops/benadis-rac/internal/constants"
)
// EnableServiceMode включает сервисный режим
func (c *Client) EnableServiceMode(ctx context.Context, params ServiceModeParams) error {
c.logger.Info(constants.LogMsgEnablingServiceMode, "cluster_uuid", params.ClusterUUID, "infobase_uuid", params.InfobaseUUID)
// Сначала отключаем всех пользователей
if err := c.TerminateAllSessions(ctx, params.ClusterUUID, params.InfobaseUUID); err != nil {
c.logger.Warn(constants.LogMsgFailedToTerminateAllSessions, "error", err)
// Продолжаем выполнение, так как это не критичная ошибка
}
// Получаем учетные данные
clusterUser, clusterPwd := c.config.GetClusterCredentials("")
dbUser, dbPwd := c.config.GetDBCredentials("")
// Включаем сервисный режим
args := []string{
c.config.GetRACAddress(),
"infobase", "update",
"--cluster=" + params.ClusterUUID,
"--infobase=" + params.InfobaseUUID,
"--sessions-deny=" + constants.SERVICE_MODE_ON,
"--scheduled-jobs-deny=" + constants.SERVICE_MODE_ON,
"--denied-message=" + params.DeniedMessage,
"--permission-code=" + params.PermissionCode,
"--cluster-user=" + clusterUser,
"--cluster-pwd=" + clusterPwd,
"--infobase-user=" + dbUser,
"--infobase-pwd=" + dbPwd,
}
_, err := c.ExecuteCommand(ctx, args)
if err != nil {
return fmt.Errorf(constants.ErrEnableServiceMode, err)
}
c.logger.Info(constants.LogMsgServiceModeEnabled)
// Верифицируем результат
return c.VerifyServiceMode(ctx, params.ClusterUUID, params.InfobaseUUID, true)
}
// DisableServiceMode выключает сервисный режим
func (c *Client) DisableServiceMode(ctx context.Context, params ServiceModeParams) error {
c.logger.Info(constants.LogMsgDisablingServiceMode, "cluster_uuid", params.ClusterUUID, "infobase_uuid", params.InfobaseUUID)
// Получаем учетные данные
clusterUser, clusterPwd := c.config.GetClusterCredentials("")
dbUser, dbPwd := c.config.GetDBCredentials("")
args := []string{
c.config.GetRACAddress(),
"infobase", "update",
"--cluster=" + params.ClusterUUID,
"--infobase=" + params.InfobaseUUID,
"--sessions-deny=" + constants.SERVICE_MODE_OFF,
"--scheduled-jobs-deny=" + constants.SERVICE_MODE_OFF,
"--cluster-user=" + clusterUser,
"--cluster-pwd=" + clusterPwd,
"--infobase-user=" + dbUser,
"--infobase-pwd=" + dbPwd,
}
_, err := c.ExecuteCommand(ctx, args)
if err != nil {
return fmt.Errorf(constants.ErrDisableServiceMode, err)
}
c.logger.Info(constants.LogMsgServiceModeDisabled)
// Верифицируем результат
return c.VerifyServiceMode(ctx, params.ClusterUUID, params.InfobaseUUID, false)
}
// TerminateAllSessions принудительно завершает все сессии пользователей
func (c *Client) TerminateAllSessions(ctx context.Context, clusterUUID, infobaseUUID string) error {
c.logger.Info(constants.LogMsgTerminatingAllSessions, "cluster_uuid", clusterUUID, "infobase_uuid", infobaseUUID)
// Получаем список сессий
sessions, err := c.GetSessions(ctx, clusterUUID, infobaseUUID)
if err != nil {
return fmt.Errorf(constants.ErrGetSessions, err)
}
if len(sessions) == 0 {
c.logger.Info(constants.LogMsgNoActiveSessions)
return nil
}
c.logger.Info(constants.LogMsgFoundActiveSessions, "count", len(sessions))
// Завершаем каждую сессию
for _, sessionUUID := range sessions {
if err := c.TerminateSession(ctx, clusterUUID, sessionUUID); err != nil {
c.logger.Warn(constants.LogMsgFailedToTerminateSession, "session_uuid", sessionUUID, "error", err)
// Продолжаем с другими сессиями
} else {
c.logger.Debug(constants.LogMsgSessionTerminated, "session_uuid", sessionUUID)
}
}
c.logger.Info(constants.LogMsgAllSessionsTerminated)
return nil
}
// GetSessions получает список активных сессий
func (c *Client) GetSessions(ctx context.Context, clusterUUID, infobaseUUID string) ([]string, error) {
// Получаем учетные данные кластера
clusterUser, clusterPwd := c.config.GetClusterCredentials("")
args := []string{
c.config.GetRACAddress(),
"session", "list",
"--cluster=" + clusterUUID,
"--infobase=" + infobaseUUID,
"--cluster-user=" + clusterUser,
"--cluster-pwd=" + clusterPwd,
}
output, err := c.ExecuteCommand(ctx, args)
if err != nil {
return nil, fmt.Errorf(constants.ErrGetSessionList, err)
}
var sessions []string
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// Ищем строки вида "session : <uuid>"
if strings.HasPrefix(line, "session") && strings.Contains(line, ":") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
sessionUUID := strings.TrimSpace(parts[1])
// Проверяем, что это действительно UUID (36 символов с дефисами)
if len(sessionUUID) == constants.UUIDLength && strings.Count(sessionUUID, "-") == constants.UUIDDashCount {
sessions = append(sessions, sessionUUID)
}
}
}
}
return sessions, nil
}
// TerminateSession завершает конкретную сессию
func (c *Client) TerminateSession(ctx context.Context, clusterUUID, sessionUUID string) error {
// Получаем учетные данные кластера
clusterUser, clusterPwd := c.config.GetClusterCredentials("")
args := []string{
c.config.GetRACAddress(),
"session", "terminate",
"--cluster=" + clusterUUID,
"--session=" + sessionUUID,
"--error-message=" + constants.TechnicalMaintenanceMessage,
"--cluster-user=" + clusterUser,
"--cluster-pwd=" + clusterPwd,
}
_, err := c.ExecuteCommand(ctx, args)
if err != nil {
return fmt.Errorf(constants.ErrTerminateSession, sessionUUID, err)
}
return nil
}
// VerifyServiceMode проверяет состояние сервисного режима
func (c *Client) VerifyServiceMode(ctx context.Context, clusterUUID, infobaseUUID string, expectedEnabled bool) error {
c.logger.Info(constants.LogMsgVerifyingServiceMode, "cluster_uuid", clusterUUID, "infobase_uuid", infobaseUUID, "expected_enabled", expectedEnabled)
// Получаем учетные данные
clusterUser, clusterPwd := c.config.GetClusterCredentials("")
dbUser, dbPwd := c.config.GetDBCredentials("")
args := []string{
c.config.GetRACAddress(),
"infobase", "info",
"--cluster=" + clusterUUID,
"--infobase=" + infobaseUUID,
"--cluster-user=" + clusterUser,
"--cluster-pwd=" + clusterPwd,
"--infobase-user=" + dbUser,
"--infobase-pwd=" + dbPwd,
}
output, err := c.ExecuteCommand(ctx, args)
if err != nil {
return fmt.Errorf(constants.ErrGetInfobaseInfo, err)
}
lines := strings.Split(output, "\n")
var sessionsDeny, scheduledJobsDeny string
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "sessions-deny") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
sessionsDeny = strings.TrimSpace(parts[1])
}
} else if strings.HasPrefix(line, "scheduled-jobs-deny") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
scheduledJobsDeny = strings.TrimSpace(parts[1])
}
}
}
expectedState := constants.SERVICE_MODE_OFF
if expectedEnabled {
expectedState = constants.SERVICE_MODE_ON
}
if sessionsDeny != expectedState {
return fmt.Errorf(constants.ErrSessionsDenyVerification, expectedState, sessionsDeny)
}
if scheduledJobsDeny != expectedState {
return fmt.Errorf(constants.ErrScheduledJobsDenyVerification, expectedState, scheduledJobsDeny)
}
c.logger.Info(constants.LogMsgServiceModeVerificationSuccessful, "sessions_deny", sessionsDeny, "scheduled_jobs_deny", scheduledJobsDeny)
return nil
}