refactor: restructure library to expose public API directly

feat: add new helper methods for RAC operations
docs: add comprehensive library usage documentation
chore: move example to examples/library_usage directory
This commit is contained in:
2025-08-04 12:03:45 +03:00
parent ccbe0c669d
commit 46261f066b
7 changed files with 1152 additions and 678 deletions

View File

@@ -4,12 +4,51 @@ package benadis_rac
import (
"context"
"fmt"
"git.benadis.ru/gitops/benadis-rac/internal/config"
"git.benadis.ru/gitops/benadis-rac/internal/logger"
"git.benadis.ru/gitops/benadis-rac/internal/service"
"log/slog"
"os"
"os/exec"
"path"
"strings"
"time"
"unicode/utf8"
)
// 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)
}
// Config конфигурация для управления сервисным режимом
type Config struct {
// RAC configuration
RACPath string // Путь к исполняемому файлу RAC
ConnectionTimeout time.Duration // Таймаут подключения
CommandTimeout time.Duration // Таймаут выполнения команды
RetryCount int // Количество повторных попыток
RetryDelay time.Duration // Задержка между попытками
// Server configuration
ServerHost string // Хост сервера 1C
ServerPort int // Порт сервера 1C
RACPort int // Порт RAC
ServerName string // Имя сервера
BaseName string // Имя информационной базы
// Authentication
ClusterAdmin string // Администратор кластера
ClusterAdminPassword string // Пароль администратора кластера
DBAdmin string // Администратор ИБ
DBAdminPassword string // Пароль администратора ИБ
// Service mode settings
DeniedMessage string // Сообщение при блокировке
PermissionCode string // Код разрешения
}
// ServiceModeManager интерфейс для управления сервисным режимом
type ServiceModeManager interface {
// EnableServiceMode включает сервисный режим
@@ -24,101 +63,450 @@ type ServiceModeManager interface {
// Client клиент для работы с 1C сервисным режимом
type Client struct {
service *service.ServiceModeService
config *Config
logger Logger
}
// Config конфигурация для создания клиента
type Config struct {
ConfigPath string // Путь к файлу конфигурации
SecretPath string // Путь к файлу с секретами
ProjectPath string // Путь к файлу проекта
LogLevel string // Уровень логирования (Debug, Info, Warn, Error)
// 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
}
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: logLevel,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.SourceKey {
source := a.Value.Any().(*slog.Source)
source.File = path.Base(source.File)
}
return a
},
})
return &SlogLogger{
logger: slog.New(handler),
}
}
func (l *SlogLogger) Debug(msg string, args ...any) {
l.logger.Debug(msg, args...)
}
func (l *SlogLogger) Info(msg string, args ...any) {
l.logger.Info(msg, args...)
}
func (l *SlogLogger) Warn(msg string, args ...any) {
l.logger.Warn(msg, args...)
}
func (l *SlogLogger) Error(msg string, args ...any) {
l.logger.Error(msg, args...)
}
func (l *SlogLogger) DebugCommand(msg string, command []string) {
maskedCommand := maskPasswords(command)
l.logger.Debug(msg, "command", strings.Join(maskedCommand, " "))
}
func maskPasswords(command []string) []string {
masked := make([]string, len(command))
copy(masked, command)
passwordFlags := []string{"--cluster-pwd", "--infobase-pwd"}
for i, arg := range masked {
for _, flag := range passwordFlags {
if arg == flag && i+1 < len(masked) {
masked[i+1] = "***"
break
}
}
}
return masked
}
// NewClient создает новый клиент для управления сервисным режимом
func NewClient(cfg Config) (*Client, error) {
if cfg.ConfigPath == "" {
cfg.ConfigPath = "config.yaml"
}
if cfg.SecretPath == "" {
cfg.SecretPath = "secret.yaml"
}
if cfg.ProjectPath == "" {
cfg.ProjectPath = "project.yaml"
}
if cfg.LogLevel == "" {
cfg.LogLevel = "Info"
}
// Загружаем конфигурацию
appConfig, err := config.LoadConfig(cfg.ConfigPath, cfg.SecretPath, cfg.ProjectPath)
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
// Переопределяем уровень логирования если указан
if appConfig.Project != nil && appConfig.Project.ServiceMode != nil {
appConfig.Project.ServiceMode.LogLevel = cfg.LogLevel
}
// Валидируем конфигурацию
if err := appConfig.Validate(); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
func NewClient(config *Config) (*Client, error) {
if config == nil {
return nil, fmt.Errorf("config cannot be nil")
}
// Создаем логгер
logLevel := "Info"
if appConfig.Project != nil && appConfig.Project.ServiceMode != nil && appConfig.Project.ServiceMode.LogLevel != "" {
logLevel = appConfig.Project.ServiceMode.LogLevel
}
log := logger.NewLogger(logLevel)
// Создаем сервис
svc := service.NewServiceModeService(appConfig, log)
logger := NewLogger("info")
return &Client{
service: svc,
config: config,
logger: logger,
}, nil
}
// ManageServiceMode основная функция для управления сервисным режимом
func ManageServiceMode(ctx context.Context, config *Config, logger Logger, enable bool) error {
if config == nil {
return fmt.Errorf("config cannot be nil")
}
if logger == nil {
return fmt.Errorf("logger cannot be nil")
}
client := &Client{
config: config,
logger: logger,
}
if enable {
return client.EnableServiceMode(ctx)
}
return client.DisableServiceMode(ctx)
}
// EnableServiceMode включает сервисный режим
func (c *Client) EnableServiceMode(ctx context.Context) error {
return c.service.EnableServiceMode(ctx)
c.logger.Info("Starting service mode enablement")
// Получаем UUID кластера
clusterUUID, err := c.getClusterUUID(ctx)
if err != nil {
return fmt.Errorf("failed to get cluster UUID: %w", err)
}
// Получаем UUID информационной базы
infobaseUUID, err := c.getInfobaseUUID(ctx, clusterUUID)
if err != nil {
return fmt.Errorf("failed to get infobase UUID: %w", err)
}
// Включаем сервисный режим
if err := c.setServiceMode(ctx, clusterUUID, infobaseUUID, true); err != nil {
return fmt.Errorf("failed to enable service mode: %w", err)
}
// Завершаем все активные сессии
if err := c.terminateAllSessions(ctx, clusterUUID, infobaseUUID); err != nil {
c.logger.Warn("Failed to terminate all sessions", "error", err)
}
// Проверяем статус
if err := c.verifyServiceMode(ctx, clusterUUID, infobaseUUID, true); err != nil {
return fmt.Errorf("service mode verification failed: %w", err)
}
c.logger.Info("Service mode enabled successfully")
return nil
}
// DisableServiceMode выключает сервисный режим
func (c *Client) DisableServiceMode(ctx context.Context) error {
return c.service.DisableServiceMode(ctx)
c.logger.Info("Starting service mode disablement")
// Получаем UUID кластера
clusterUUID, err := c.getClusterUUID(ctx)
if err != nil {
return fmt.Errorf("failed to get cluster UUID: %w", err)
}
// Получаем UUID информационной базы
infobaseUUID, err := c.getInfobaseUUID(ctx, clusterUUID)
if err != nil {
return fmt.Errorf("failed to get infobase UUID: %w", err)
}
// Выключаем сервисный режим
if err := c.setServiceMode(ctx, clusterUUID, infobaseUUID, false); err != nil {
return fmt.Errorf("failed to disable service mode: %w", err)
}
// Проверяем статус
if err := c.verifyServiceMode(ctx, clusterUUID, infobaseUUID, false); err != nil {
return fmt.Errorf("service mode verification failed: %w", err)
}
c.logger.Info("Service mode disabled successfully")
return nil
}
// GetServiceModeStatus получает текущий статус сервисного режима
// Возвращает true если сервисный режим включен, false если выключен
func (c *Client) GetServiceModeStatus(ctx context.Context) (bool, error) {
return c.service.GetServiceModeStatus(ctx)
// Получаем UUID кластера
clusterUUID, err := c.getClusterUUID(ctx)
if err != nil {
return false, fmt.Errorf("failed to get cluster UUID: %w", err)
}
// Получаем UUID информационной базы
infobaseUUID, err := c.getInfobaseUUID(ctx, clusterUUID)
if err != nil {
return false, fmt.Errorf("failed to get infobase UUID: %w", err)
}
// Получаем информацию об информационной базе
args := []string{
c.config.RACPath,
"infobase", "info",
"--cluster=" + clusterUUID,
"--infobase=" + infobaseUUID,
"--cluster-user=" + c.config.ClusterAdmin,
"--cluster-pwd=" + c.config.ClusterAdminPassword,
"--infobase-user=" + c.config.DBAdmin,
"--infobase-pwd=" + c.config.DBAdminPassword,
c.getRACAddress(),
}
output, err := c.executeCommand(ctx, args)
if err != nil {
return false, fmt.Errorf("failed to get infobase info: %w", err)
}
// Проверяем статус сервисного режима
return strings.Contains(output, "sessions-deny") && strings.Contains(output, "on"), nil
}
// NewClientFromConfig создает клиент из готовой конфигурации
func NewClientFromConfig(appConfig *config.AppConfig, logLevel string) (*Client, error) {
if logLevel != "" && appConfig.Project != nil && appConfig.Project.ServiceMode != nil {
appConfig.Project.ServiceMode.LogLevel = logLevel
// Константы
const (
DefaultDeniedMessage = "Техническое обслуживание. Попробуйте позже."
DefaultPermissionCode = "service-mode"
TechnicalMaintenanceMessage = "Техническое обслуживание"
UUIDLength = 36
UUIDDashCount = 4
DefaultRACPort = 1545
DefaultConnectionTimeout = 30 * time.Second
DefaultCommandTimeout = 60 * time.Second
DefaultRetryCount = 3
DefaultRetryDelay = 5 * time.Second
)
// getRACAddress возвращает адрес RAC сервера
func (c *Client) getRACAddress() string {
port := c.config.RACPort
if port == 0 {
port = DefaultRACPort
}
// Валидируем конфигурацию
if err := appConfig.Validate(); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
}
// Создаем логгер
logLevelToUse := "Info"
if appConfig.Project != nil && appConfig.Project.ServiceMode != nil && appConfig.Project.ServiceMode.LogLevel != "" {
logLevelToUse = appConfig.Project.ServiceMode.LogLevel
}
log := logger.NewLogger(logLevelToUse)
// Создаем сервис
svc := service.NewServiceModeService(appConfig, log)
return &Client{
service: svc,
}, nil
return fmt.Sprintf("%s:%d", c.config.ServerHost, port)
}
// executeCommand выполняет RAC команду с retry логикой
func (c *Client) executeCommand(ctx context.Context, args []string) (string, error) {
var lastErr error
retryCount := c.config.RetryCount
if retryCount == 0 {
retryCount = DefaultRetryCount
}
retryDelay := c.config.RetryDelay
if retryDelay == 0 {
retryDelay = DefaultRetryDelay
}
for attempt := 1; attempt <= retryCount; attempt++ {
output, err := c.executeCommandOnce(ctx, args)
if err == nil {
return output, nil
}
lastErr = err
c.logger.Warn("Command execution failed", "attempt", attempt, "error", err)
if attempt < retryCount {
c.logger.Info("Retrying after delay", "delay", retryDelay)
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(retryDelay):
}
}
}
return "", fmt.Errorf("command failed after %d attempts: %w", retryCount, lastErr)
}
// executeCommandOnce выполняет RAC команду один раз
func (c *Client) executeCommandOnce(ctx context.Context, args []string) (string, error) {
c.logger.DebugCommand("Executing RAC command", args)
cmdTimeout := c.config.CommandTimeout
if cmdTimeout == 0 {
cmdTimeout = DefaultCommandTimeout
}
cmdCtx, cancel := context.WithTimeout(ctx, cmdTimeout)
defer cancel()
cmd := exec.CommandContext(cmdCtx, args[0], args[1:]...)
output, err := cmd.Output()
if err != nil {
c.logger.Error("RAC command failed", "error", err, "output", string(output))
return "", fmt.Errorf("RAC command failed: %w, output: %s", err, string(output))
}
utf8Output := convertToUTF8(output)
c.logger.Debug("RAC command executed successfully", "output_length", len(utf8Output))
return utf8Output, nil
}
// convertToUTF8 конвертирует байты в UTF-8 строку
func convertToUTF8(data []byte) string {
if utf8.Valid(data) {
return string(data)
}
// Пытаемся декодировать как Windows-1251
runes := make([]rune, 0, len(data))
for _, b := range data {
if b < 128 {
runes = append(runes, rune(b))
} else {
// Простая таблица конвертации для основных символов Windows-1251
switch b {
case 0xC0: // А
runes = append(runes, 'А')
case 0xC1: // Б
runes = append(runes, 'Б')
case 0xC2: // В
runes = append(runes, 'В')
case 0xC3: // Г
runes = append(runes, 'Г')
case 0xC4: // Д
runes = append(runes, 'Д')
case 0xC5: // Е
runes = append(runes, 'Е')
case 0xC6: // Ж
runes = append(runes, 'Ж')
case 0xC7: // З
runes = append(runes, 'З')
case 0xC8: // И
runes = append(runes, 'И')
case 0xC9: // Й
runes = append(runes, 'Й')
case 0xCA: // К
runes = append(runes, 'К')
case 0xCB: // Л
runes = append(runes, 'Л')
case 0xCC: // М
runes = append(runes, 'М')
case 0xCD: // Н
runes = append(runes, 'Н')
case 0xCE: // О
runes = append(runes, 'О')
case 0xCF: // П
runes = append(runes, 'П')
case 0xD0: // Р
runes = append(runes, 'Р')
case 0xD1: // С
runes = append(runes, 'С')
case 0xD2: // Т
runes = append(runes, 'Т')
case 0xD3: // У
runes = append(runes, 'У')
case 0xD4: // Ф
runes = append(runes, 'Ф')
case 0xD5: // Х
runes = append(runes, 'Х')
case 0xD6: // Ц
runes = append(runes, 'Ц')
case 0xD7: // Ч
runes = append(runes, 'Ч')
case 0xD8: // Ш
runes = append(runes, 'Ш')
case 0xD9: // Щ
runes = append(runes, 'Щ')
case 0xDA: // Ъ
runes = append(runes, 'Ъ')
case 0xDB: // Ы
runes = append(runes, 'Ы')
case 0xDC: // Ь
runes = append(runes, 'Ь')
case 0xDD: // Э
runes = append(runes, 'Э')
case 0xDE: // Ю
runes = append(runes, 'Ю')
case 0xDF: // Я
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, 'п')
case 0xF0: // р
runes = append(runes, 'р')
case 0xF1: // с
runes = append(runes, 'с')
case 0xF2: // т
runes = append(runes, 'т')
case 0xF3: // у
runes = append(runes, 'у')
case 0xF4: // ф
runes = append(runes, 'ф')
case 0xF5: // х
runes = append(runes, 'х')
case 0xF6: // ц
runes = append(runes, 'ц')
case 0xF7: // ч
runes = append(runes, 'ч')
case 0xF8: // ш
runes = append(runes, 'ш')
case 0xF9: // щ
runes = append(runes, 'щ')
case 0xFA: // ъ
runes = append(runes, 'ъ')
case 0xFB: // ы
runes = append(runes, 'ы')
case 0xFC: // ь
runes = append(runes, 'ь')
case 0xFD: // э
runes = append(runes, 'э')
case 0xFE: // ю
runes = append(runes, 'ю')
case 0xFF: // я
runes = append(runes, 'я')
default:
runes = append(runes, rune(b))
}
}
}
return string(runes)
}