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

201
internal/config/config.go Normal file
View File

@@ -0,0 +1,201 @@
package config
import (
"fmt"
"os"
"time"
"git.benadis.ru/gitops/benadis-rac/internal/constants"
"gopkg.in/yaml.v3"
)
// Config представляет основную конфигурацию приложения
type Config struct {
RACPath string `yaml:"rac_path"`
ConnectionTimeout string `yaml:"connection_timeout"`
CommandTimeout string `yaml:"command_timeout"`
RetryCount int `yaml:"retry_count"`
RetryDelay string `yaml:"retry_delay"`
}
// GetConnectionTimeout возвращает ConnectionTimeout как time.Duration
func (c *Config) GetConnectionTimeout() (time.Duration, error) {
return time.ParseDuration(c.ConnectionTimeout)
}
// GetCommandTimeout возвращает CommandTimeout как time.Duration
func (c *Config) GetCommandTimeout() (time.Duration, error) {
return time.ParseDuration(c.CommandTimeout)
}
// GetRetryDelay возвращает RetryDelay как time.Duration
func (c *Config) GetRetryDelay() (time.Duration, error) {
return time.ParseDuration(c.RetryDelay)
}
// ProjectCredentials представляет учетные данные для конкретного проекта
type ProjectCredentials struct {
ClusterAdmin string `yaml:"cluster_admin,omitempty"`
ClusterAdminPassword string `yaml:"cluster_admin_password,omitempty"`
DBAdmin string `yaml:"db_admin,omitempty"`
DBAdminPassword string `yaml:"db_admin_password,omitempty"`
}
// Secret представляет секретную конфигурацию
type Secret struct {
ClusterAdmin string `yaml:"cluster_admin"`
ClusterAdminPassword string `yaml:"cluster_admin_password"`
DBAdmin string `yaml:"db_admin"`
DBAdminPassword string `yaml:"db_admin_password"`
Projects map[string]*ProjectCredentials `yaml:"projects,omitempty"`
}
// ServiceModeConfig представляет конфигурацию service-mode из project.yaml
type ServiceModeConfig struct {
ServerHost string `yaml:"server_host"`
ServerPort int `yaml:"server_port"`
RACPort int `yaml:"rac_port"`
LogLevel string `yaml:"log_level"`
ServerName string `yaml:"server_name"`
BaseName string `yaml:"base_name"`
Command string `yaml:"command"`
}
// ProjectConfig представляет конфигурацию проекта
type ProjectConfig struct {
ServiceMode *ServiceModeConfig `yaml:"service-mode"`
}
// AppConfig объединяет все конфигурации
type AppConfig struct {
Config *Config
Secret *Secret
Project *ProjectConfig
}
// LoadConfig загружает конфигурацию из файлов
func LoadConfig(configPath, secretPath, projectPath string) (*AppConfig, error) {
config, err := loadConfigFile(configPath)
if err != nil {
return nil, fmt.Errorf(constants.ErrLoadConfig, err)
}
secret, err := loadSecretFile(secretPath)
if err != nil {
return nil, fmt.Errorf(constants.ErrLoadSecret, err)
}
project, err := loadProjectFile(projectPath)
if err != nil {
return nil, fmt.Errorf("failed to load project config: %w", err)
}
return &AppConfig{
Config: config,
Secret: secret,
Project: project,
}, nil
}
// loadConfigFile загружает основной конфигурационный файл
func loadConfigFile(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return &config, nil
}
// loadSecretFile загружает файл с секретами
func loadSecretFile(path string) (*Secret, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read secret file: %w", err)
}
var secret Secret
if err := yaml.Unmarshal(data, &secret); err != nil {
return nil, fmt.Errorf("failed to unmarshal secret: %w", err)
}
return &secret, nil
}
// loadProjectFile загружает файл с настройками проекта
func loadProjectFile(path string) (*ProjectConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read project file: %w", err)
}
var project ProjectConfig
if err := yaml.Unmarshal(data, &project); err != nil {
return nil, fmt.Errorf("failed to unmarshal project: %w", err)
}
return &project, nil
}
// Validate проверяет корректность конфигурации
func (ac *AppConfig) Validate() error {
if ac.Config.RACPath == "" {
return fmt.Errorf(constants.ErrRACPathRequired)
}
if ac.Project == nil || ac.Project.ServiceMode == nil {
return fmt.Errorf("project service-mode configuration is required")
}
if ac.Project.ServiceMode.ServerHost == "" {
return fmt.Errorf(constants.ErrServerHostRequired)
}
if ac.Project.ServiceMode.BaseName == "" {
return fmt.Errorf(constants.ErrBaseNameRequired)
}
if ac.Secret.ClusterAdmin == "" {
return fmt.Errorf(constants.ErrClusterAdminRequired)
}
if ac.Secret.DBAdmin == "" {
return fmt.Errorf(constants.ErrDBAdminRequired)
}
return nil
}
// GetRACAddress возвращает адрес для подключения к RAC
func (ac *AppConfig) GetRACAddress() string {
return fmt.Sprintf("%s:%d", ac.Project.ServiceMode.ServerHost, ac.Project.ServiceMode.RACPort)
}
// GetClusterCredentials возвращает учетные данные кластера с учетом приоритета проекта
func (ac *AppConfig) GetClusterCredentials(projectName string) (string, string) {
// Проверяем настройки для конкретного проекта
if ac.Secret.Projects != nil {
if projectCreds, exists := ac.Secret.Projects[projectName]; exists {
if projectCreds.ClusterAdmin != "" {
return projectCreds.ClusterAdmin, projectCreds.ClusterAdminPassword
}
}
}
// Возвращаем глобальные настройки
return ac.Secret.ClusterAdmin, ac.Secret.ClusterAdminPassword
}
// GetDBCredentials возвращает учетные данные базы данных с учетом приоритета проекта
func (ac *AppConfig) GetDBCredentials(projectName string) (string, string) {
// Проверяем настройки для конкретного проекта
if ac.Secret.Projects != nil {
if projectCreds, exists := ac.Secret.Projects[projectName]; exists {
if projectCreds.DBAdmin != "" {
return projectCreds.DBAdmin, projectCreds.DBAdminPassword
}
}
}
// Возвращаем глобальные настройки
return ac.Secret.DBAdmin, ac.Secret.DBAdminPassword
}

View File

@@ -0,0 +1,209 @@
package config
import (
"os"
"testing"
"time"
)
func TestLoadConfig(t *testing.T) {
// Создаем временные файлы для тестирования
configContent := `server_host: localhost
server_port: 1540
rac_port: 1545
rac_path: "/path/to/rac"
connection_timeout: 30s
command_timeout: 60s
retry_count: 3
retry_delay: 5s
log_level: Debug
server_name: "Test Server"`
secretContent := `cluster_admin: "admin"
cluster_admin_password: "password"
db_admin: "dbadmin"
db_admin_password: "dbpassword"`
projectContent := `service-mode:
server_host: "localhost"
server_port: 1540
rac_port: 1545
base_name: "TestDB"
log_level: "Info"`
// Создаем временные файлы
configFile, err := os.CreateTemp("", "config_*.yaml")
if err != nil {
t.Fatalf("Failed to create temp config file: %v", err)
}
defer os.Remove(configFile.Name())
secretFile, err := os.CreateTemp("", "secret_*.yaml")
if err != nil {
t.Fatalf("Failed to create temp secret file: %v", err)
}
defer os.Remove(secretFile.Name())
projectFile, err := os.CreateTemp("", "project_*.yaml")
if err != nil {
t.Fatalf("Failed to create temp project file: %v", err)
}
defer os.Remove(projectFile.Name())
// Записываем содержимое
if _, err := configFile.WriteString(configContent); err != nil {
t.Fatalf("Failed to write config file: %v", err)
}
configFile.Close()
if _, err := secretFile.WriteString(secretContent); err != nil {
t.Fatalf("Failed to write secret file: %v", err)
}
secretFile.Close()
if _, err := projectFile.WriteString(projectContent); err != nil {
t.Fatalf("Failed to write project file: %v", err)
}
projectFile.Close()
// Тестируем загрузку конфигурации
appConfig, err := LoadConfig(configFile.Name(), secretFile.Name(), projectFile.Name())
if err != nil {
t.Fatalf("LoadConfig failed: %v", err)
}
// Проверяем основную конфигурацию
if appConfig.Project.ServiceMode.ServerHost != "localhost" {
t.Errorf("Expected ServerHost 'localhost', got '%s'", appConfig.Project.ServiceMode.ServerHost)
}
if appConfig.Project.ServiceMode.ServerPort != 1540 {
t.Errorf("Expected ServerPort 1540, got %d", appConfig.Project.ServiceMode.ServerPort)
}
if appConfig.Project.ServiceMode.RACPort != 1545 {
t.Errorf("Expected RACPort 1545, got %d", appConfig.Project.ServiceMode.RACPort)
}
connectionTimeout, err := appConfig.Config.GetConnectionTimeout()
if err != nil {
t.Errorf("Failed to parse ConnectionTimeout: %v", err)
}
if connectionTimeout != 30*time.Second {
t.Errorf("Expected ConnectionTimeout 30s, got %v", connectionTimeout)
}
// Проверяем секретную конфигурацию
if appConfig.Secret.ClusterAdmin != "admin" {
t.Errorf("Expected ClusterAdmin 'admin', got '%s'", appConfig.Secret.ClusterAdmin)
}
if appConfig.Secret.DBAdminPassword != "dbpassword" {
t.Errorf("Expected DBAdminPassword 'dbpassword', got '%s'", appConfig.Secret.DBAdminPassword)
}
// Проверяем проектную конфигурацию
if appConfig.Project.ServiceMode.BaseName != "TestDB" {
t.Errorf("Expected BaseName 'TestDB', got '%s'", appConfig.Project.ServiceMode.BaseName)
}
if appConfig.Project.ServiceMode.LogLevel != "Info" {
t.Errorf("Expected LogLevel 'Info', got '%s'", appConfig.Project.ServiceMode.LogLevel)
}
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
config *AppConfig
expectErr bool
}{
{
name: "valid config",
config: &AppConfig{
Config: &Config{
RACPath: "/path/to/rac",
},
Secret: &Secret{
ClusterAdmin: "admin",
DBAdmin: "dbadmin",
},
Project: &ProjectConfig{
ServiceMode: &ServiceModeConfig{
ServerHost: "localhost",
BaseName: "TestDB",
},
},
},
expectErr: false,
},
{
name: "missing server host",
config: &AppConfig{
Config: &Config{
RACPath: "/path/to/rac",
},
Secret: &Secret{
ClusterAdmin: "admin",
DBAdmin: "dbadmin",
},
Project: &ProjectConfig{
ServiceMode: &ServiceModeConfig{
ServerHost: "",
BaseName: "TestDB",
},
},
},
expectErr: true,
},
{
name: "missing cluster admin",
config: &AppConfig{
Config: &Config{
RACPath: "/path/to/rac",
},
Secret: &Secret{
ClusterAdmin: "",
DBAdmin: "dbadmin",
},
Project: &ProjectConfig{
ServiceMode: &ServiceModeConfig{
ServerHost: "localhost",
BaseName: "TestDB",
},
},
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if tt.expectErr && err == nil {
t.Error("Expected error but got none")
}
if !tt.expectErr && err != nil {
t.Errorf("Expected no error but got: %v", err)
}
})
}
}
func TestGetRACAddress(t *testing.T) {
config := &AppConfig{
Project: &ProjectConfig{
ServiceMode: &ServiceModeConfig{
ServerHost: "example.com",
RACPort: 1545,
},
},
}
expected := "example.com:1545"
actual := config.GetRACAddress()
if actual != expected {
t.Errorf("Expected '%s', got '%s'", expected, actual)
}
}

View File

@@ -0,0 +1,291 @@
package constants
import "time"
// Версия приложения
const (
// AppVersion версия приложения
AppVersion = "1.0.0"
)
// Константы для сервисного режима
const (
// DefaultDeniedMessage сообщение по умолчанию при включении сервисного режима
DefaultDeniedMessage = "Техническое обслуживание. Попробуйте позже."
// DefaultPermissionCode код разрешения для сервисного режима
DefaultPermissionCode = "service-mode"
// TechnicalMaintenanceMessage сообщение для завершения сессий
TechnicalMaintenanceMessage = "Техническое обслуживание"
)
// Константы таймаутов
const (
// DEFAULT_CONNECTION_TIMEOUT таймаут подключения по умолчанию
DEFAULT_CONNECTION_TIMEOUT = 30 * time.Second
// DEFAULT_COMMAND_TIMEOUT таймаут выполнения команды по умолчанию
DEFAULT_COMMAND_TIMEOUT = 60 * time.Second
// DEFAULT_RETRY_COUNT количество попыток повтора по умолчанию
DEFAULT_RETRY_COUNT = 3
// DEFAULT_RETRY_DELAY задержка между попытками по умолчанию
DEFAULT_RETRY_DELAY = 5 * time.Second
)
// Константы для RAC команд
const (
// RAC_INFOBASE_UPDATE команда обновления информационной базы
RAC_INFOBASE_UPDATE = "infobase"
// RAC_SESSION_LIST команда получения списка сессий
RAC_SESSION_LIST = "session"
// RAC_CLUSTER_LIST команда получения списка кластеров
RAC_CLUSTER_LIST = "cluster"
)
// Константы состояний сервисного режима
const (
// SERVICE_MODE_ON включение сервисного режима
SERVICE_MODE_ON = "on"
// SERVICE_MODE_OFF выключение сервисного режима
SERVICE_MODE_OFF = "off"
)
// Константы для CLI параметров
const (
// DefaultConfigPath путь к файлу конфигурации по умолчанию
DefaultConfigPath = "config.yaml"
// DefaultSecretPath путь к файлу секретов по умолчанию
DefaultSecretPath = "secret.yaml"
// CmdEnable команда включения сервисного режима
CmdEnable = "enable"
// CmdDisable команда отключения сервисного режима
CmdDisable = "disable"
// CmdStatus команда проверки статуса
CmdStatus = "status"
)
// Константы для сообщений ошибок
const (
// ErrCommandFailed сообщение об ошибке выполнения команды
ErrCommandFailed = "command failed after %d attempts: %w"
// ErrFailedToParseCommandTimeout сообщение об ошибке парсинга таймаута команды
ErrFailedToParseCommandTimeout = "failed to parse command timeout: %w"
// ErrRACCommandFailed сообщение об ошибке RAC команды
ErrRACCommandFailed = "RAC command failed: %w, output: %s"
// ErrGetClusterList сообщение об ошибке получения списка кластеров
ErrGetClusterList = "failed to get cluster list: %w"
// ErrClusterUUIDNotFound сообщение об ошибке поиска UUID кластера
ErrClusterUUIDNotFound = "cluster UUID not found in output"
// ErrGetInfobaseList сообщение об ошибке получения списка информационных баз
ErrGetInfobaseList = "failed to get infobase list: %w"
// ErrInfobaseUUIDNotFound сообщение об ошибке поиска UUID информационной базы
ErrInfobaseUUIDNotFound = "infobase UUID not found for name: %s"
// ErrUnknownCommand сообщение об неизвестной команде
ErrUnknownCommand = "unknown command: %s"
// ErrEnableServiceMode сообщение об ошибке включения сервисного режима
ErrEnableServiceMode = "failed to enable service mode: %w"
// ErrDisableServiceMode сообщение об ошибке отключения сервисного режима
ErrDisableServiceMode = "failed to disable service mode: %w"
// ErrGetSessions сообщение об ошибке получения списка сессий
ErrGetSessions = "failed to get sessions: %w"
// ErrGetSessionList сообщение об ошибке получения списка сессий
ErrGetSessionList = "failed to get session list: %w"
// ErrTerminateSession сообщение об ошибке завершения сессии
ErrTerminateSession = "failed to terminate session %s: %w"
// ErrGetInfobaseInfo сообщение об ошибке получения информации о базе
ErrGetInfobaseInfo = "failed to get infobase info: %w"
// ErrSessionsDenyVerification сообщение об ошибке проверки блокировки сессий
ErrSessionsDenyVerification = "sessions-deny verification failed: expected %s, got %s"
// ErrScheduledJobsDenyVerification сообщение об ошибке проверки блокировки заданий
ErrScheduledJobsDenyVerification = "scheduled-jobs-deny verification failed: expected %s, got %s"
// ErrLoadConfig сообщение об ошибке загрузки конфигурации
ErrLoadConfig = "failed to load config: %w"
// ErrLoadSecret сообщение об ошибке загрузки секретов
ErrLoadSecret = "failed to load secret: %w"
// ErrConfigValidation сообщение об ошибке валидации конфигурации
ErrConfigValidation = "config validation failed: %w"
// ErrGetClusterUUID сообщение об ошибке получения UUID кластера
ErrGetClusterUUID = "failed to get cluster UUID: %w"
// ErrGetInfobaseUUID сообщение об ошибке получения UUID информационной базы
ErrGetInfobaseUUID = "failed to get infobase UUID: %w"
// ErrDetermineServiceModeStatus сообщение об ошибке определения статуса сервисного режима
ErrDetermineServiceModeStatus = "failed to determine service mode status: %w"
// ErrReadConfigFile сообщение об ошибке чтения файла конфигурации
ErrReadConfigFile = "failed to read config file: %w"
// ErrUnmarshalConfig сообщение об ошибке парсинга конфигурации
ErrUnmarshalConfig = "failed to unmarshal config: %w"
// ErrReadSecretFile сообщение об ошибке чтения файла секретов
ErrReadSecretFile = "failed to read secret file: %w"
// ErrUnmarshalSecret сообщение об ошибке парсинга секретов
ErrUnmarshalSecret = "failed to unmarshal secret: %w"
// ErrServerHostRequired сообщение об обязательном поле server_host
ErrServerHostRequired = "server_host is required"
// ErrRACPathRequired сообщение об обязательном поле rac_path
ErrRACPathRequired = "rac_path is required"
// ErrBaseNameRequired сообщение об обязательном поле base_name
ErrBaseNameRequired = "base_name is required"
// ErrClusterAdminRequired сообщение об обязательном поле cluster_admin
ErrClusterAdminRequired = "cluster_admin is required"
// ErrDBAdminRequired сообщение об обязательном поле db_admin
ErrDBAdminRequired = "db_admin is required"
)
// Константы для сообщений логгера
const (
// LogMsgTerminatingAllSessions сообщение о завершении всех сессий
LogMsgTerminatingAllSessions = "Terminating all sessions"
// LogMsgNoActiveSessions сообщение об отсутствии активных сессий
LogMsgNoActiveSessions = "No active sessions found"
// LogMsgFoundActiveSessions сообщение о найденных активных сессиях
LogMsgFoundActiveSessions = "Found active sessions"
// LogMsgSessionTerminated сообщение о завершении сессии
LogMsgSessionTerminated = "Session terminated"
// LogMsgAllSessionsTerminated сообщение о завершении всех сессий
LogMsgAllSessionsTerminated = "All sessions termination completed"
// LogMsgVerifyingServiceMode сообщение о проверке сервисного режима
LogMsgVerifyingServiceMode = "Verifying service mode"
// LogMsgServiceModeEnabled сообщение об успешном включении сервисного режима
LogMsgServiceModeEnabled = "Service mode enabled successfully"
// LogMsgServiceModeDisabled сообщение об успешном отключении сервисного режима
LogMsgServiceModeDisabled = "Service mode disabled successfully"
// LogMsgServiceModeVerificationSuccessful сообщение об успешной проверке сервисного режима
LogMsgServiceModeVerificationSuccessful = "Service mode verification successful"
// LogMsgExecutingRACCommand сообщение о выполнении RAC команды
LogMsgExecutingRACCommand = "Executing RAC command"
// LogMsgRACCommandExecutedSuccessfully сообщение об успешном выполнении RAC команды
LogMsgRACCommandExecutedSuccessfully = "RAC command executed successfully"
// LogMsgRACCommandFailed сообщение об ошибке выполнения RAC команды
LogMsgRACCommandFailed = "RAC command failed"
// LogMsgFailedToTerminateSession сообщение об ошибке завершения сессии
LogMsgFailedToTerminateSession = "Failed to terminate session"
// LogMsgCommandExecutionFailed сообщение об ошибке выполнения команды
LogMsgCommandExecutionFailed = "Command execution failed"
// LogMsgRACCommand сообщение о RAC команде
LogMsgRACCommand = "RAC command"
// LogMsgEnablingServiceMode сообщение о включении сервисного режима
LogMsgEnablingServiceMode = "Enabling service mode"
// LogMsgDisablingServiceMode сообщение об отключении сервисного режима
LogMsgDisablingServiceMode = "Disabling service mode"
// LogMsgFailedToTerminateAllSessions сообщение об ошибке завершения всех сессий
LogMsgFailedToTerminateAllSessions = "Failed to terminate all sessions"
// LogMsgFailedToParseRetryDelay сообщение об ошибке парсинга задержки повтора
LogMsgFailedToParseRetryDelay = "Failed to parse retry delay, using default"
// LogMsgRetryingAfterDelay сообщение о повторе после задержки
LogMsgRetryingAfterDelay = "Retrying after delay"
// LogMsgRACCommandFailedAfterAllRetries сообщение об ошибке RAC команды после всех попыток
LogMsgRACCommandFailedAfterAllRetries = "RAC command failed after all retries"
// LogMsgGettingClusterUUID сообщение о получении UUID кластера
LogMsgGettingClusterUUID = "Getting cluster UUID"
// LogMsgFoundClusterUUID сообщение о найденном UUID кластера
LogMsgFoundClusterUUID = "Found cluster UUID"
// LogMsgGettingInfobaseUUID сообщение о получении UUID информационной базы
LogMsgGettingInfobaseUUID = "Getting infobase UUID"
// LogMsgFoundInfobaseUUID сообщение о найденном UUID информационной базы
LogMsgFoundInfobaseUUID = "Found infobase UUID"
)
// Константы для статусных сообщений
const (
// StatusServiceModeEnabled статус включенного сервисного режима
StatusServiceModeEnabled = "Service mode: ENABLED"
// StatusServiceModeDisabled статус отключенного сервисного режима
StatusServiceModeDisabled = "Service mode: DISABLED"
// MsgErrorLoadingConfig сообщение об ошибке загрузки конфигурации
MsgErrorLoadingConfig = "Error loading config: %v\n"
// MsgConfigValidationError сообщение об ошибке валидации конфигурации
MsgConfigValidationError = "Config validation error: %v\n"
// MsgVersionFormat формат вывода версии
MsgVersionFormat = "GitOps RAC v%s\n"
)
// Константы для magic numbers
const (
// UUIDLength длина UUID
UUIDLength = 36
// UUIDDashCount количество дефисов в UUID
UUIDDashCount = 4
// DefaultRACPort порт RAC по умолчанию
DefaultRACPort = 1545
// DefaultMainTimeout таймаут main функции
DefaultMainTimeout = 5
)
// Константы для логгера
const (
// PasswordMask маска для паролей
PasswordMask = "***"
// PasswordFlags флаги паролей через запятую для маскирования
PasswordFlags = "--cluster-pwd,--infobase-pwd"
)

110
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,110 @@
package logger
import (
"log/slog"
"os"
"path"
"strings"
"git.benadis.ru/gitops/benadis-rac/internal/constants"
)
// Logger интерфейс для логирования
type Logger interface {
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
DebugCommand(msg string, command []string)
}
// SlogLogger реализация Logger с использованием slog
type SlogLogger struct {
logger *slog.Logger
}
// NewLogger создает новый логгер
func NewLogger(level string) Logger {
var logLevel slog.Level
switch strings.ToLower(level) {
case "debug":
logLevel = slog.LevelDebug
case "info":
logLevel = slog.LevelInfo
case "warn":
logLevel = slog.LevelWarn
case "error":
logLevel = slog.LevelError
default:
logLevel = slog.LevelInfo
}
l := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: logLevel,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.SourceKey {
s := a.Value.Any().(*slog.Source)
s.File = path.Base(s.File)
}
return a
},
}))
l = l.With(slog.Group("App info",
slog.String("version", constants.AppVersion),
))
return &SlogLogger{logger: l}
}
// Debug логирует отладочное сообщение
func (l *SlogLogger) Debug(msg string, args ...any) {
l.logger.Debug(msg, args...)
}
// Info логирует информационное сообщение
func (l *SlogLogger) Info(msg string, args ...any) {
l.logger.Info(msg, args...)
}
// Warn логирует предупреждение
func (l *SlogLogger) Warn(msg string, args ...any) {
l.logger.Warn(msg, args...)
}
// Error логирует ошибку
func (l *SlogLogger) Error(msg string, args ...any) {
l.logger.Error(msg, args...)
}
// DebugCommand логирует команду с маскированием паролей
func (l *SlogLogger) DebugCommand(msg string, command []string) {
maskedCommand := maskPasswords(command)
l.logger.Debug(msg, "command", strings.Join(maskedCommand, " "))
}
// maskPasswords маскирует пароли в команде
func maskPasswords(command []string) []string {
masked := make([]string, len(command))
copy(masked, command)
// Преобразуем строку флагов в массив
passwordFlags := strings.Split(constants.PasswordFlags, ",")
for i, arg := range masked {
for _, flag := range passwordFlags {
if strings.HasPrefix(arg, flag+"=") {
// Формат: --flag=value
parts := strings.SplitN(arg, "=", 2)
if len(parts) == 2 {
masked[i] = parts[0] + "=" + constants.PasswordMask
}
} else if arg == flag && i+1 < len(masked) {
// Формат: --flag value
masked[i+1] = constants.PasswordMask
}
}
}
return masked
}

View File

@@ -0,0 +1,238 @@
package logger
import (
"testing"
)
func TestMaskPasswords(t *testing.T) {
tests := []struct {
name string
command []string
expected []string
}{
{
name: "mask cluster password with equals",
command: []string{
"rac", "localhost:1545", "infobase", "update",
"--cluster-pwd=secret123",
"--other-flag=value",
},
expected: []string{
"rac", "localhost:1545", "infobase", "update",
"--cluster-pwd=***",
"--other-flag=value",
},
},
{
name: "mask infobase password with equals",
command: []string{
"rac", "localhost:1545", "infobase", "update",
"--infobase-pwd=secret456",
},
expected: []string{
"rac", "localhost:1545", "infobase", "update",
"--infobase-pwd=***",
},
},
{
name: "mask password with space separator",
command: []string{
"rac", "localhost:1545", "infobase", "update",
"--cluster-pwd", "secret789",
"--other-flag", "value",
},
expected: []string{
"rac", "localhost:1545", "infobase", "update",
"--cluster-pwd", "***",
"--other-flag", "value",
},
},
{
name: "mask both passwords",
command: []string{
"rac", "localhost:1545", "infobase", "update",
"--cluster-pwd=cluster_secret",
"--infobase-pwd=infobase_secret",
},
expected: []string{
"rac", "localhost:1545", "infobase", "update",
"--cluster-pwd=***",
"--infobase-pwd=***",
},
},
{
name: "no passwords to mask",
command: []string{
"rac", "localhost:1545", "cluster", "list",
"--some-flag=value",
},
expected: []string{
"rac", "localhost:1545", "cluster", "list",
"--some-flag=value",
},
},
{
name: "mixed format passwords",
command: []string{
"rac", "localhost:1545", "infobase", "update",
"--cluster-pwd=secret1",
"--infobase-pwd", "secret2",
},
expected: []string{
"rac", "localhost:1545", "infobase", "update",
"--cluster-pwd=***",
"--infobase-pwd", "***",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := maskPasswords(tt.command)
if len(actual) != len(tt.expected) {
t.Errorf("Expected length %d, got %d", len(tt.expected), len(actual))
return
}
for i, expected := range tt.expected {
if actual[i] != expected {
t.Errorf("At index %d: expected '%s', got '%s'", i, expected, actual[i])
}
}
})
}
}
func TestNewLogger(t *testing.T) {
tests := []struct {
name string
level string
expected string // Мы не можем напрямую проверить уровень, но можем проверить что логгер создается
}{
{"debug level", "debug", "debug"},
{"info level", "info", "info"},
{"warn level", "warn", "warn"},
{"error level", "error", "error"},
{"unknown level defaults to info", "unknown", "info"},
{"empty level defaults to info", "", "info"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := NewLogger(tt.level)
if logger == nil {
t.Error("Expected logger to be created, got nil")
}
// Проверяем что логгер реализует интерфейс Logger
var _ Logger = logger
})
}
}
func TestSlogLoggerMethods(t *testing.T) {
// Создаем логгер для тестирования
logger := NewLogger("debug")
// Тестируем что методы не паникуют
t.Run("debug method", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Debug method panicked: %v", r)
}
}()
logger.Debug("test debug message", "key", "value")
})
t.Run("info method", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Info method panicked: %v", r)
}
}()
logger.Info("test info message", "key", "value")
})
t.Run("warn method", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Warn method panicked: %v", r)
}
}()
logger.Warn("test warn message", "key", "value")
})
t.Run("error method", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Error method panicked: %v", r)
}
}()
logger.Error("test error message", "key", "value")
})
t.Run("debug command method", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("DebugCommand method panicked: %v", r)
}
}()
command := []string{"rac", "--cluster-pwd=secret", "command"}
logger.DebugCommand("test command", command)
})
}
func TestMaskPasswordsEdgeCases(t *testing.T) {
tests := []struct {
name string
command []string
expected []string
}{
{
name: "empty command",
command: []string{},
expected: []string{},
},
{
name: "single element",
command: []string{"rac"},
expected: []string{"rac"},
},
{
name: "password flag at end without value",
command: []string{
"rac", "localhost:1545", "--cluster-pwd",
},
expected: []string{
"rac", "localhost:1545", "--cluster-pwd",
},
},
{
name: "password flag with empty value",
command: []string{
"rac", "localhost:1545", "--cluster-pwd=",
},
expected: []string{
"rac", "localhost:1545", "--cluster-pwd=***",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := maskPasswords(tt.command)
if len(actual) != len(tt.expected) {
t.Errorf("Expected length %d, got %d", len(tt.expected), len(actual))
return
}
for i, expected := range tt.expected {
if actual[i] != expected {
t.Errorf("At index %d: expected '%s', got '%s'", i, expected, actual[i])
}
}
})
}
}

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
}

124
internal/service/service.go Normal file
View File

@@ -0,0 +1,124 @@
package service
import (
"context"
"fmt"
"git.benadis.ru/gitops/benadis-rac/internal/config"
"git.benadis.ru/gitops/benadis-rac/internal/constants"
"git.benadis.ru/gitops/benadis-rac/internal/logger"
"git.benadis.ru/gitops/benadis-rac/internal/rac"
)
// ServiceModeService сервис для управления сервисным режимом
type ServiceModeService struct {
racClient *rac.Client
config *config.AppConfig
logger logger.Logger
}
// NewServiceModeService создает новый сервис
func NewServiceModeService(cfg *config.AppConfig, log logger.Logger) *ServiceModeService {
racClient := rac.NewClient(cfg, log)
return &ServiceModeService{
racClient: racClient,
config: cfg,
logger: log,
}
}
// EnableServiceMode включает сервисный режим
func (s *ServiceModeService) EnableServiceMode(ctx context.Context) error {
s.logger.Info("Starting service mode enablement")
// Получаем UUID кластера
clusterUUID, err := s.racClient.GetClusterUUID(ctx)
if err != nil {
return fmt.Errorf(constants.ErrGetClusterUUID, err)
}
// Получаем UUID информационной базы
infobaseUUID, err := s.racClient.GetInfobaseUUID(ctx, clusterUUID)
if err != nil {
return fmt.Errorf(constants.ErrGetInfobaseUUID, err)
}
// Подготавливаем параметры
params := rac.ServiceModeParams{
ClusterUUID: clusterUUID,
InfobaseUUID: infobaseUUID,
Enable: true,
DeniedMessage: constants.DefaultDeniedMessage,
PermissionCode: constants.DefaultPermissionCode,
}
// Включаем сервисный режим
if err := s.racClient.EnableServiceMode(ctx, params); err != nil {
return fmt.Errorf(constants.ErrEnableServiceMode, err)
}
s.logger.Info(constants.LogMsgServiceModeEnabled)
return nil
}
// DisableServiceMode выключает сервисный режим
func (s *ServiceModeService) DisableServiceMode(ctx context.Context) error {
s.logger.Info("Starting service mode disablement")
// Получаем UUID кластера
clusterUUID, err := s.racClient.GetClusterUUID(ctx)
if err != nil {
return fmt.Errorf(constants.ErrGetClusterUUID, err)
}
// Получаем UUID информационной базы
infobaseUUID, err := s.racClient.GetInfobaseUUID(ctx, clusterUUID)
if err != nil {
return fmt.Errorf(constants.ErrGetInfobaseUUID, err)
}
// Подготавливаем параметры
params := rac.ServiceModeParams{
ClusterUUID: clusterUUID,
InfobaseUUID: infobaseUUID,
Enable: false,
}
// Выключаем сервисный режим
if err := s.racClient.DisableServiceMode(ctx, params); err != nil {
return fmt.Errorf(constants.ErrDisableServiceMode, err)
}
s.logger.Info(constants.LogMsgServiceModeDisabled)
return nil
}
// GetServiceModeStatus получает текущий статус сервисного режима
func (s *ServiceModeService) GetServiceModeStatus(ctx context.Context) (bool, error) {
s.logger.Info("Getting service mode status")
// Получаем UUID кластера
clusterUUID, err := s.racClient.GetClusterUUID(ctx)
if err != nil {
return false, fmt.Errorf(constants.ErrGetClusterUUID, err)
}
// Получаем UUID информационной базы
infobaseUUID, err := s.racClient.GetInfobaseUUID(ctx, clusterUUID)
if err != nil {
return false, fmt.Errorf(constants.ErrGetInfobaseUUID, err)
}
// Проверяем статус (используем VerifyServiceMode для проверки включенного состояния)
err = s.racClient.VerifyServiceMode(ctx, clusterUUID, infobaseUUID, true)
if err != nil {
// Если проверка на включенное состояние не прошла, проверяем выключенное
err = s.racClient.VerifyServiceMode(ctx, clusterUUID, infobaseUUID, false)
if err != nil {
return false, fmt.Errorf(constants.ErrDetermineServiceModeStatus, err)
}
return false, nil // Сервисный режим выключен
}
return true, nil // Сервисный режим включен
}