fix client UI and action
This commit is contained in:
Binary file not shown.
@@ -142,3 +142,36 @@ func (h *Handler) ExecuteBconsoleCommand(c *gin.Context) {
|
||||
"output": output,
|
||||
})
|
||||
}
|
||||
|
||||
// ListClients lists all backup clients with optional filters
|
||||
func (h *Handler) ListClients(c *gin.Context) {
|
||||
opts := ListClientsOptions{}
|
||||
|
||||
// Parse enabled filter
|
||||
if enabledStr := c.Query("enabled"); enabledStr != "" {
|
||||
enabled := enabledStr == "true"
|
||||
opts.Enabled = &enabled
|
||||
}
|
||||
|
||||
// Parse search query
|
||||
opts.Search = c.Query("search")
|
||||
|
||||
clients, err := h.service.ListClients(c.Request.Context(), opts)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to list clients", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to list clients",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if clients == nil {
|
||||
clients = []Client{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"clients": clients,
|
||||
"total": len(clients),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,6 +78,27 @@ type ListJobsOptions struct {
|
||||
Offset int // Offset for pagination
|
||||
}
|
||||
|
||||
// Client represents a backup client
|
||||
type Client struct {
|
||||
ClientID int `json:"client_id"`
|
||||
Name string `json:"name"`
|
||||
Uname *string `json:"uname,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AutoPrune *bool `json:"auto_prune,omitempty"`
|
||||
FileRetention *int64 `json:"file_retention,omitempty"`
|
||||
JobRetention *int64 `json:"job_retention,omitempty"`
|
||||
LastBackupAt *time.Time `json:"last_backup_at,omitempty"`
|
||||
TotalJobs *int `json:"total_jobs,omitempty"`
|
||||
TotalBytes *int64 `json:"total_bytes,omitempty"`
|
||||
Status *string `json:"status,omitempty"` // "online" or "offline"
|
||||
}
|
||||
|
||||
// ListClientsOptions represents filtering options for clients
|
||||
type ListClientsOptions struct {
|
||||
Enabled *bool // Filter by enabled status (nil = all)
|
||||
Search string // Search by client name
|
||||
}
|
||||
|
||||
// SyncJobsFromBacula syncs jobs from Bacula/Bareos to the database
|
||||
// Tries to query Bacula database directly first, falls back to bconsole if database access fails
|
||||
func (s *Service) SyncJobsFromBacula(ctx context.Context) error {
|
||||
@@ -487,6 +508,285 @@ func (s *Service) ExecuteBconsoleCommand(ctx context.Context, command string) (s
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// ListClients lists all backup clients from Bacula database Client table
|
||||
// Falls back to bconsole if database connection is not available
|
||||
func (s *Service) ListClients(ctx context.Context, opts ListClientsOptions) ([]Client, error) {
|
||||
// Try database first if available
|
||||
if s.baculaDB != nil {
|
||||
clients, err := s.queryClientsFromDatabase(ctx, opts)
|
||||
if err == nil {
|
||||
s.logger.Debug("Queried clients from Bacula database", "count", len(clients))
|
||||
return clients, nil
|
||||
}
|
||||
s.logger.Warn("Failed to query clients from database, trying bconsole fallback", "error", err)
|
||||
}
|
||||
|
||||
// Fallback to bconsole
|
||||
s.logger.Info("Using bconsole fallback for list clients")
|
||||
return s.queryClientsFromBconsole(ctx, opts)
|
||||
}
|
||||
|
||||
// queryClientsFromDatabase queries clients from Bacula database
|
||||
func (s *Service) queryClientsFromDatabase(ctx context.Context, opts ListClientsOptions) ([]Client, error) {
|
||||
// First, try a simple query to check if Client table exists and has data
|
||||
simpleQuery := `SELECT COUNT(*) FROM Client`
|
||||
var count int
|
||||
err := s.baculaDB.QueryRowContext(ctx, simpleQuery).Scan(&count)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to count clients from Client table", "error", err)
|
||||
return nil, fmt.Errorf("failed to query Client table: %w", err)
|
||||
}
|
||||
s.logger.Debug("Total clients in database", "count", count)
|
||||
|
||||
if count == 0 {
|
||||
s.logger.Info("No clients found in Bacula database")
|
||||
return []Client{}, nil
|
||||
}
|
||||
|
||||
// Build query with filters
|
||||
query := `
|
||||
SELECT
|
||||
c.ClientId,
|
||||
c.Name,
|
||||
c.Uname,
|
||||
true as enabled,
|
||||
c.AutoPrune,
|
||||
c.FileRetention,
|
||||
c.JobRetention,
|
||||
MAX(j.StartTime) as last_backup_at,
|
||||
COUNT(DISTINCT j.JobId) as total_jobs,
|
||||
COALESCE(SUM(j.JobBytes), 0) as total_bytes
|
||||
FROM Client c
|
||||
LEFT JOIN Job j ON c.ClientId = j.ClientId
|
||||
WHERE 1=1
|
||||
`
|
||||
|
||||
args := []interface{}{}
|
||||
argIndex := 1
|
||||
|
||||
if opts.Enabled != nil {
|
||||
query += fmt.Sprintf(" AND true = $%d", argIndex)
|
||||
args = append(args, *opts.Enabled)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if opts.Search != "" {
|
||||
query += fmt.Sprintf(" AND c.Name ILIKE $%d", argIndex)
|
||||
args = append(args, "%"+opts.Search+"%")
|
||||
argIndex++
|
||||
}
|
||||
|
||||
query += " GROUP BY c.ClientId, c.Name, c.Uname, c.AutoPrune, c.FileRetention, c.JobRetention"
|
||||
query += " ORDER BY c.Name"
|
||||
|
||||
s.logger.Debug("Executing clients query", "query", query, "args", args)
|
||||
rows, err := s.baculaDB.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to execute clients query", "error", err, "query", query)
|
||||
return nil, fmt.Errorf("failed to query clients from Bacula database: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var clients []Client
|
||||
for rows.Next() {
|
||||
var client Client
|
||||
var uname sql.NullString
|
||||
var autoPrune sql.NullBool
|
||||
var fileRetention, jobRetention sql.NullInt64
|
||||
var lastBackupAt sql.NullTime
|
||||
var totalJobs sql.NullInt64
|
||||
var totalBytes sql.NullInt64
|
||||
|
||||
err := rows.Scan(
|
||||
&client.ClientID,
|
||||
&client.Name,
|
||||
&uname,
|
||||
&client.Enabled,
|
||||
&autoPrune,
|
||||
&fileRetention,
|
||||
&jobRetention,
|
||||
&lastBackupAt,
|
||||
&totalJobs,
|
||||
&totalBytes,
|
||||
)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to scan client row", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if uname.Valid {
|
||||
client.Uname = &uname.String
|
||||
}
|
||||
if autoPrune.Valid {
|
||||
client.AutoPrune = &autoPrune.Bool
|
||||
}
|
||||
if fileRetention.Valid {
|
||||
val := fileRetention.Int64
|
||||
client.FileRetention = &val
|
||||
}
|
||||
if jobRetention.Valid {
|
||||
val := jobRetention.Int64
|
||||
client.JobRetention = &val
|
||||
}
|
||||
if lastBackupAt.Valid {
|
||||
client.LastBackupAt = &lastBackupAt.Time
|
||||
}
|
||||
if totalJobs.Valid {
|
||||
val := int(totalJobs.Int64)
|
||||
client.TotalJobs = &val
|
||||
}
|
||||
if totalBytes.Valid {
|
||||
val := totalBytes.Int64
|
||||
client.TotalBytes = &val
|
||||
}
|
||||
|
||||
// Determine client status based on enabled and last backup
|
||||
// If client is enabled and has recent backup (within 24 hours), consider it online
|
||||
// Otherwise, mark as offline
|
||||
if client.Enabled {
|
||||
if lastBackupAt.Valid {
|
||||
timeSinceLastBackup := time.Since(lastBackupAt.Time)
|
||||
if timeSinceLastBackup < 24*time.Hour {
|
||||
status := "online"
|
||||
client.Status = &status
|
||||
} else {
|
||||
status := "offline"
|
||||
client.Status = &status
|
||||
}
|
||||
} else {
|
||||
// No backup yet, but enabled - assume online
|
||||
status := "online"
|
||||
client.Status = &status
|
||||
}
|
||||
} else {
|
||||
status := "offline"
|
||||
client.Status = &status
|
||||
}
|
||||
|
||||
clients = append(clients, client)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating client rows: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Queried clients from Bacula database", "count", len(clients))
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
// queryClientsFromBconsole queries clients using bconsole command (fallback method)
|
||||
func (s *Service) queryClientsFromBconsole(ctx context.Context, opts ListClientsOptions) ([]Client, error) {
|
||||
// Execute bconsole command to list clients
|
||||
s.logger.Debug("Executing bconsole list clients command")
|
||||
output, err := s.ExecuteBconsoleCommand(ctx, "list clients")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to execute bconsole list clients", "error", err)
|
||||
return nil, fmt.Errorf("failed to execute bconsole list clients: %w", err)
|
||||
}
|
||||
|
||||
previewLen := 200
|
||||
if len(output) < previewLen {
|
||||
previewLen = len(output)
|
||||
}
|
||||
s.logger.Debug("bconsole output", "output_length", len(output), "output_preview", output[:previewLen])
|
||||
|
||||
// Parse bconsole output
|
||||
clients := s.parseBconsoleClientsOutput(output)
|
||||
s.logger.Debug("Parsed clients from bconsole", "count", len(clients))
|
||||
|
||||
// Apply filters
|
||||
filtered := []Client{}
|
||||
for _, client := range clients {
|
||||
if opts.Enabled != nil && client.Enabled != *opts.Enabled {
|
||||
continue
|
||||
}
|
||||
if opts.Search != "" && !strings.Contains(strings.ToLower(client.Name), strings.ToLower(opts.Search)) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, client)
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// parseBconsoleClientsOutput parses bconsole "list clients" output
|
||||
func (s *Service) parseBconsoleClientsOutput(output string) []Client {
|
||||
var clients []Client
|
||||
lines := strings.Split(output, "\n")
|
||||
|
||||
inTable := false
|
||||
headerFound := false
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip connection messages and command echo
|
||||
if strings.Contains(line, "Connecting to Director") ||
|
||||
strings.Contains(line, "Enter a period") ||
|
||||
strings.Contains(line, "list clients") ||
|
||||
strings.Contains(line, "quit") ||
|
||||
strings.Contains(line, "You have messages") ||
|
||||
strings.Contains(line, "Automatically selected") ||
|
||||
strings.Contains(line, "Using Catalog") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect table header
|
||||
if !headerFound && (strings.Contains(line, "Name") || strings.Contains(line, "| Name")) {
|
||||
headerFound = true
|
||||
inTable = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect table separator
|
||||
if strings.HasPrefix(line, "+") && strings.Contains(line, "-") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip empty lines
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse table rows (format: | clientname | address |)
|
||||
if inTable && strings.Contains(line, "|") {
|
||||
parts := strings.Split(line, "|")
|
||||
if len(parts) >= 2 {
|
||||
clientName := strings.TrimSpace(parts[1])
|
||||
clientName = strings.Trim(clientName, "\"'")
|
||||
|
||||
if clientName == "" || clientName == "Name" || strings.HasPrefix(clientName, "-") {
|
||||
continue
|
||||
}
|
||||
|
||||
client := Client{
|
||||
ClientID: 0,
|
||||
Name: clientName,
|
||||
Enabled: true,
|
||||
}
|
||||
clients = append(clients, client)
|
||||
}
|
||||
} else if inTable && !strings.Contains(line, "|") {
|
||||
// Fallback format
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) > 0 {
|
||||
clientName := parts[0]
|
||||
clientName = strings.Trim(clientName, "\"'")
|
||||
if clientName != "" && clientName != "Name" && !strings.HasPrefix(clientName, "-") {
|
||||
client := Client{
|
||||
ClientID: 0,
|
||||
Name: clientName,
|
||||
Enabled: true,
|
||||
}
|
||||
clients = append(clients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
// upsertJob inserts or updates a job in the database
|
||||
func (s *Service) upsertJob(ctx context.Context, job Job) error {
|
||||
query := `
|
||||
|
||||
@@ -349,6 +349,7 @@ func NewRouter(cfg *config.Config, db *database.DB, log *logger.Logger) *gin.Eng
|
||||
backupGroup.GET("/jobs", backupHandler.ListJobs)
|
||||
backupGroup.GET("/jobs/:id", backupHandler.GetJob)
|
||||
backupGroup.POST("/jobs", requirePermission("backup", "write"), backupHandler.CreateJob)
|
||||
backupGroup.GET("/clients", backupHandler.ListClients)
|
||||
backupGroup.POST("/console/execute", requirePermission("backup", "write"), backupHandler.ExecuteBconsoleCommand)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user