Учёт 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
|
return "подключение к msdb: " + err.Error(), false
|
||||||
}
|
}
|
||||||
defer mdb.Close()
|
defer mdb.Close()
|
||||||
|
// Таблицы msdb (sysjobs, sysjobhistory…) роли SQLAgentReaderRole закрыты — только представление
|
||||||
|
// sysjobs_view и процедуры sp_help_*: ими и читаем, чтобы проверка работала логином bj.
|
||||||
var (
|
var (
|
||||||
jobEnabled, schedEnabled, subType, subInt sql.NullInt64
|
jobEnabled, schedEnabled, subType, subInt sql.NullInt64
|
||||||
runDate, runTime, runStatus sql.NullInt64
|
runDate, runTime, runStatus sql.NullInt64
|
||||||
runMsg sql.NullString
|
runMsg sql.NullString
|
||||||
now time.Time
|
now time.Time
|
||||||
)
|
)
|
||||||
err = mdb.QueryRowContext(ctx, `SELECT j.enabled, s.enabled, s.freq_subday_type, s.freq_subday_interval,
|
err = mdb.QueryRowContext(ctx, "SELECT enabled, GETDATE() FROM msdb.dbo.sysjobs_view WHERE name = @n", sql.Named("n", depoJobName)).Scan(&jobEnabled, &now)
|
||||||
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)
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return "джобы " + depoJobName + " на сервере нет — создайте её (вариант «создаёт bj» с логином администратора или скрипт для администратора)", false
|
return "джобы " + depoJobName + " на сервере нет — создайте её (вариант «создаёт bj» с логином администратора или скрипт для администратора)", false
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "список джоб недоступен логину " + conn.User + " (" + quikErrorDetail(err) + "). Чтобы bj мог проверять джобу, администратору SQL — один раз: " + strings.ReplaceAll(depoJobReaderGrant(f), "\n", " "), false
|
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 {
|
if jobEnabled.Int64 != 1 {
|
||||||
return "джоба " + depoJobName + " есть, но ВЫКЛЮЧЕНА — включите её в SQL Server Agent", false
|
return "джоба " + depoJobName + " есть, но ВЫКЛЮЧЕНА — включите её в SQL Server Agent", false
|
||||||
}
|
}
|
||||||
@@ -969,7 +989,7 @@ func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, admin
|
|||||||
}
|
}
|
||||||
defer mdb.Close()
|
defer mdb.Close()
|
||||||
var n int
|
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", " "))
|
return fmt.Errorf("нет доступа к msdb (%s). Нужны права: %s", quikErrorDetail(err), strings.ReplaceAll(depoJobAgentRightsScript(f), "\n", " "))
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
@@ -1324,3 +1344,59 @@ func depoSimRefs(guid string, secs []IncomingSecurity) map[string]string {
|
|||||||
}
|
}
|
||||||
return m
|
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