Учёт 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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user