Учёт DEPO: проверка джобы логином bj через sysjobs_view и sp_help_job/sp_help_jobschedule/sp_help_jobhistory — таблицы msdb роли SQLAgentReaderRole закрыты

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-05 15:17:45 +03:00
parent d119420db7
commit 9360543589
+85 -9
View File
@@ -857,26 +857,46 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad
return "подключение к msdb: " + err.Error(), false
}
defer mdb.Close()
// Таблицы msdb (sysjobs, sysjobhistory…) роли SQLAgentReaderRole закрыты — только представление
// sysjobs_view и процедуры sp_help_*: ими и читаем, чтобы проверка работала логином bj.
var (
jobEnabled, schedEnabled, subType, subInt sql.NullInt64
runDate, runTime, runStatus sql.NullInt64
runMsg sql.NullString
now time.Time
)
err = mdb.QueryRowContext(ctx, `SELECT j.enabled, s.enabled, s.freq_subday_type, s.freq_subday_interval,
h.run_date, h.run_time, h.run_status, h.message, GETDATE()
FROM msdb.dbo.sysjobs j
LEFT JOIN msdb.dbo.sysjobschedules js ON js.job_id = j.job_id
LEFT JOIN msdb.dbo.sysschedules s ON s.schedule_id = js.schedule_id
OUTER APPLY (SELECT TOP 1 run_date, run_time, run_status, message FROM msdb.dbo.sysjobhistory
WHERE job_id = j.job_id AND step_id = 0 ORDER BY instance_id DESC) h
WHERE j.name = @n`, sql.Named("n", depoJobName)).Scan(&jobEnabled, &schedEnabled, &subType, &subInt, &runDate, &runTime, &runStatus, &runMsg, &now)
err = mdb.QueryRowContext(ctx, "SELECT enabled, GETDATE() FROM msdb.dbo.sysjobs_view WHERE name = @n", sql.Named("n", depoJobName)).Scan(&jobEnabled, &now)
if err == sql.ErrNoRows {
return "джобы " + depoJobName + " на сервере нет — создайте её (вариант «создаёт bj» с логином администратора или скрипт для администратора)", false
}
if err != nil {
return "список джоб недоступен логину " + conn.User + " (" + quikErrorDetail(err) + "). Чтобы bj мог проверять джобу, администратору SQL — один раз: " + strings.ReplaceAll(depoJobReaderGrant(f), "\n", " "), false
}
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobschedule @job_name = @n", sql.Named("n", depoJobName)); err == nil {
for _, r := range rows {
if mapInt(r, "enabled") == 1 || !schedEnabled.Valid {
schedEnabled = sql.NullInt64{Int64: mapInt(r, "enabled"), Valid: true}
subType = sql.NullInt64{Int64: mapInt(r, "freq_subday_type"), Valid: true}
subInt = sql.NullInt64{Int64: mapInt(r, "freq_subday_interval"), Valid: true}
}
}
}
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_job @job_name = @n, @job_aspect = N'JOB'", sql.Named("n", depoJobName)); err == nil && len(rows) > 0 {
runDate = sql.NullInt64{Int64: mapInt(rows[0], "last_run_date"), Valid: true}
runTime = sql.NullInt64{Int64: mapInt(rows[0], "last_run_time"), Valid: true}
runStatus = sql.NullInt64{Int64: mapInt(rows[0], "last_run_outcome"), Valid: true}
}
if runStatus.Valid && runStatus.Int64 == 0 && runDate.Int64 != 0 {
// причина — в сообщении последнего выполненного шага (история идёт от новых к старым)
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobhistory @job_name = @n, @mode = N'FULL'", sql.Named("n", depoJobName)); err == nil {
for _, r := range rows {
if mapInt(r, "step_id") > 0 {
runMsg = sql.NullString{String: fmt.Sprint(r["message"]), Valid: true}
break
}
}
}
}
if jobEnabled.Int64 != 1 {
return "джоба " + depoJobName + " есть, но ВЫКЛЮЧЕНА — включите её в SQL Server Agent", false
}
@@ -969,7 +989,7 @@ func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, admin
}
defer mdb.Close()
var n int
if err := mdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM msdb.dbo.sysjobs WHERE name=@n", sql.Named("n", depoJobName)).Scan(&n); err != nil {
if err := mdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM msdb.dbo.sysjobs_view WHERE name=@n", sql.Named("n", depoJobName)).Scan(&n); err != nil {
return fmt.Errorf("нет доступа к msdb (%s). Нужны права: %s", quikErrorDetail(err), strings.ReplaceAll(depoJobAgentRightsScript(f), "\n", " "))
}
if n > 0 {
@@ -1324,3 +1344,59 @@ func depoSimRefs(guid string, secs []IncomingSecurity) map[string]string {
}
return m
}
// mssqlRowMaps — строки результата (в т.ч. процедуры sp_help_*) как «колонка в нижнем регистре → значение».
func mssqlRowMaps(ctx context.Context, db *sql.DB, q string, args ...any) ([]map[string]any, error) {
rows, err := db.QueryContext(ctx, q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
return nil, err
}
var out []map[string]any
for rows.Next() {
v := make([]any, len(cols))
p := make([]any, len(cols))
for i := range v {
p[i] = &v[i]
}
if err := rows.Scan(p...); err != nil {
return nil, err
}
m := make(map[string]any, len(cols))
for i, c := range cols {
m[strings.ToLower(c)] = v[i]
}
out = append(out, m)
}
return out, rows.Err()
}
func mapInt(m map[string]any, k string) int64 {
switch v := m[k].(type) {
case int64:
return v
case int32:
return int64(v)
case int:
return int64(v)
case int16:
return int64(v)
case uint8:
return int64(v)
case bool:
if v {
return 1
}
case []byte:
n, _ := strconv.ParseInt(string(v), 10, 64)
return n
case string:
n, _ := strconv.ParseInt(v, 10, 64)
return n
}
return 0
}