dbabrain 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. db_ops/__init__.py +8 -0
  2. db_ops/cli.py +495 -0
  3. db_ops/common/__init__.py +1 -0
  4. db_ops/common/backup/__init__.py +129 -0
  5. db_ops/common/backup/spec.py +209 -0
  6. db_ops/common/backup_scripts/oracle/oracle_rman_archivelog.sh +114 -0
  7. db_ops/common/backup_scripts/oracle/oracle_rman_database.sh +119 -0
  8. db_ops/common/backup_scripts/postgresql/pg_archive_wal.sh +116 -0
  9. db_ops/common/backup_scripts/postgresql/pg_basebackup_database.sh +152 -0
  10. db_ops/common/backup_scripts/sqlserver/mssql_backup_database.ps1 +274 -0
  11. db_ops/common/backup_scripts/sqlserver/mssql_backup_database.sh +224 -0
  12. db_ops/common/backupfiles/__init__.py +171 -0
  13. db_ops/common/backupfiles/oracle.py +113 -0
  14. db_ops/common/backupfiles/postgresql.py +54 -0
  15. db_ops/common/backupfiles/sqlserver.py +83 -0
  16. db_ops/common/cli.py +1671 -0
  17. db_ops/common/cli_backup.py +120 -0
  18. db_ops/common/cli_backup_files.py +237 -0
  19. db_ops/common/cli_catalog.py +230 -0
  20. db_ops/common/cli_delete_files.py +137 -0
  21. db_ops/common/cli_filetransfer.py +106 -0
  22. db_ops/common/cli_restore.py +106 -0
  23. db_ops/common/cli_restorestep.py +168 -0
  24. db_ops/common/config_admin.py +887 -0
  25. db_ops/common/confirm.py +341 -0
  26. db_ops/common/data_sources/__init__.py +461 -0
  27. db_ops/common/data_sources/metric_targets.py +118 -0
  28. db_ops/common/data_sources/ssh_auth.py +98 -0
  29. db_ops/common/data_sources/target_resolve.py +238 -0
  30. db_ops/common/db_catalog.py +534 -0
  31. db_ops/common/db_connect.py +408 -0
  32. db_ops/common/deletefiles.py +233 -0
  33. db_ops/common/evidence.py +254 -0
  34. db_ops/common/file_transfer.py +685 -0
  35. db_ops/common/filetransfer.py +204 -0
  36. db_ops/common/host_ops.py +1443 -0
  37. db_ops/common/host_probe.py +213 -0
  38. db_ops/common/hostcmd.py +395 -0
  39. db_ops/common/identifier_scan.py +528 -0
  40. db_ops/common/job_control.py +180 -0
  41. db_ops/common/oracle_bridge.py +527 -0
  42. db_ops/common/password_rotation.py +470 -0
  43. db_ops/common/remote_exec.py +1209 -0
  44. db_ops/common/restore/__init__.py +48 -0
  45. db_ops/common/restore/sqlserver/__init__.py +28 -0
  46. db_ops/common/restore/sqlserver/chain.py +180 -0
  47. db_ops/common/restore/sqlserver/runner.py +177 -0
  48. db_ops/common/restore/sqlserver/sql.py +90 -0
  49. db_ops/common/restore/sqlserver/timeparse.py +59 -0
  50. db_ops/common/restore_drill.py +154 -0
  51. db_ops/common/restore_scripts/oracle/oracle_rman_restore.sh +282 -0
  52. db_ops/common/restore_scripts/postgresql/pg_restore_basebackup.sh +344 -0
  53. db_ops/common/restore_scripts/sqlserver/mssql_restore.sh +240 -0
  54. db_ops/common/restorekey.py +110 -0
  55. db_ops/common/restoremetadata.py +113 -0
  56. db_ops/common/restorestep/__init__.py +83 -0
  57. db_ops/common/restorestep/oracle.py +192 -0
  58. db_ops/common/restorestep/postgresql.py +318 -0
  59. db_ops/common/restorestep/sqlserver.py +107 -0
  60. db_ops/common/secret_check.py +433 -0
  61. db_ops/common/session_trace.py +238 -0
  62. db_ops/common/sql_execution.py +719 -0
  63. db_ops/common/sql_run.py +920 -0
  64. db_ops/common/sqlserver_emergency.py +428 -0
  65. db_ops/common/sqlserver_instance.py +1591 -0
  66. db_ops/common/sqlserver_patch.py +804 -0
  67. db_ops/common/ssh.py +119 -0
  68. db_ops/common/table_load.py +499 -0
  69. db_ops/common/verifyrestore.py +129 -0
  70. db_ops/config.py +792 -0
  71. db_ops/db/__init__.py +3 -0
  72. db_ops/db/backend.py +759 -0
  73. db_ops/db/backup_restore_history.py +141 -0
  74. db_ops/db/cli.py +1194 -0
  75. db_ops/db/config_edit.py +242 -0
  76. db_ops/db/config_store.py +640 -0
  77. db_ops/db/config_sync.py +642 -0
  78. db_ops/db/declaration.py +130 -0
  79. db_ops/db/job_runs.py +60 -0
  80. db_ops/db/metric_definitions.py +30 -0
  81. db_ops/db/metric_results.py +57 -0
  82. db_ops/db/metric_store.py +1027 -0
  83. db_ops/db/ops_status.py +438 -0
  84. db_ops/db/postgres_store.py +280 -0
  85. db_ops/db/queue_message.py +137 -0
  86. db_ops/db/run_requests.py +355 -0
  87. db_ops/db/schema_export.py +148 -0
  88. db_ops/db/sla_results.py +94 -0
  89. db_ops/db/sla_store.py +367 -0
  90. db_ops/db/sqlite_to_postgres.py +1188 -0
  91. db_ops/db/store.py +2538 -0
  92. db_ops/db/telegram_queue.py +209 -0
  93. db_ops/db/web_auth_store.py +645 -0
  94. db_ops/jobs/__init__.py +3 -0
  95. db_ops/jobs/cli.py +32 -0
  96. db_ops/jobs/daemon.py +1317 -0
  97. db_ops/jobs/models.py +4 -0
  98. db_ops/jobs/status.py +149 -0
  99. db_ops/levels.py +29 -0
  100. db_ops/lib/__init__.py +24 -0
  101. db_ops/lib/backup_kinds.py +25 -0
  102. db_ops/lib/backup_level.py +43 -0
  103. db_ops/lib/backup_policy.py +283 -0
  104. db_ops/lib/backupfiles_retention.py +186 -0
  105. db_ops/lib/capacity_forecast.py +220 -0
  106. db_ops/lib/cmd_access.py +147 -0
  107. db_ops/lib/coerce.py +115 -0
  108. db_ops/lib/common_cli.py +120 -0
  109. db_ops/lib/connection_spec.py +157 -0
  110. db_ops/lib/delimited_import.py +229 -0
  111. db_ops/lib/distribution.py +220 -0
  112. db_ops/lib/event_policy.py +133 -0
  113. db_ops/lib/health_model.py +218 -0
  114. db_ops/lib/instance_bundle.py +55 -0
  115. db_ops/lib/interval_rates.py +38 -0
  116. db_ops/lib/inventory_render.py +575 -0
  117. db_ops/lib/json_io.py +100 -0
  118. db_ops/lib/listing.py +89 -0
  119. db_ops/lib/log_tail.py +230 -0
  120. db_ops/lib/metric_score.py +18 -0
  121. db_ops/lib/notify.py +239 -0
  122. db_ops/lib/notify_route.py +107 -0
  123. db_ops/lib/paths.py +219 -0
  124. db_ops/lib/policy_engine.py +376 -0
  125. db_ops/lib/powershell.py +116 -0
  126. db_ops/lib/record_form.py +300 -0
  127. db_ops/lib/report_archive.py +79 -0
  128. db_ops/lib/response.py +104 -0
  129. db_ops/lib/restore/__init__.py +31 -0
  130. db_ops/lib/restore/pitr.py +35 -0
  131. db_ops/lib/restore/plan.py +59 -0
  132. db_ops/lib/restore/spec.py +224 -0
  133. db_ops/lib/result_format.py +319 -0
  134. db_ops/lib/rows.py +41 -0
  135. db_ops/lib/secret_text.py +309 -0
  136. db_ops/lib/shell.py +64 -0
  137. db_ops/lib/sql_access.py +91 -0
  138. db_ops/lib/sql_text.py +193 -0
  139. db_ops/lib/ssh_errors.py +34 -0
  140. db_ops/lib/state_transition.py +239 -0
  141. db_ops/lib/target_flags.py +39 -0
  142. db_ops/lib/target_profile.py +512 -0
  143. db_ops/lib/task_output.py +48 -0
  144. db_ops/lib/telegram_route.py +112 -0
  145. db_ops/lib/telegram_severity.py +217 -0
  146. db_ops/lib/telegram_text.py +90 -0
  147. db_ops/lib/text_format.py +25 -0
  148. db_ops/lib/time_window.py +303 -0
  149. db_ops/lib/web_auth.py +200 -0
  150. db_ops/lib/xlsx_export.py +223 -0
  151. db_ops/lib/xlsx_import.py +371 -0
  152. db_ops/logging_ops/__init__.py +33 -0
  153. db_ops/logging_ops/formatter.py +10 -0
  154. db_ops/logging_ops/handlers.py +94 -0
  155. db_ops/logging_ops/runtime_stdout.py +73 -0
  156. db_ops/logging_ops/writer.py +113 -0
  157. db_ops/metrics/__init__.py +23 -0
  158. db_ops/metrics/cli.py +670 -0
  159. db_ops/metrics/collector.py +1498 -0
  160. db_ops/metrics/collectors/docker/009_docker_container_stats.sh +75 -0
  161. db_ops/metrics/collectors/mysql/001_mysql_instance_status.sql +6 -0
  162. db_ops/metrics/collectors/mysql/002_mysql_database_status.sql +9 -0
  163. db_ops/metrics/collectors/mysql/003_mysql_connections.sql +15 -0
  164. db_ops/metrics/collectors/mysql/004_mysql_long_running_queries.sql +10 -0
  165. db_ops/metrics/collectors/mysql/006_mysql_data_file_space.sql +14 -0
  166. db_ops/metrics/collectors/mysql/009_mysql_blocking_sessions.sql +19 -0
  167. db_ops/metrics/collectors/mysql/014_mysql_wait_stats.sql +11 -0
  168. db_ops/metrics/collectors/mysql/050_mysql_user_permissions.sql +40 -0
  169. db_ops/metrics/collectors/mysql/051_mysql_constraint_health.sql +44 -0
  170. db_ops/metrics/collectors/mysql/052_mysql_database_data_size.sql +29 -0
  171. db_ops/metrics/collectors/mysql/053_mysql_table_fragmentation.sql +56 -0
  172. db_ops/metrics/collectors/mysql/054_mysql_statistics_age.sql +53 -0
  173. db_ops/metrics/collectors/oracle/001_oracle_instance_status.sql +7 -0
  174. db_ops/metrics/collectors/oracle/002_oracle_database_status.sql +65 -0
  175. db_ops/metrics/collectors/oracle/003_oracle_sessions.sql +13 -0
  176. db_ops/metrics/collectors/oracle/004_oracle_long_running_queries.sql +15 -0
  177. db_ops/metrics/collectors/oracle/005_oracle_backup_age.sql +49 -0
  178. db_ops/metrics/collectors/oracle/006_oracle_data_file_space.sql +57 -0
  179. db_ops/metrics/collectors/oracle/007_oracle_archive_log_space.sql +89 -0
  180. db_ops/metrics/collectors/oracle/009_oracle_blocking_sessions.sql +12 -0
  181. db_ops/metrics/collectors/oracle/009_oracle_temp_usage.sql +14 -0
  182. db_ops/metrics/collectors/oracle/011_oracle_dataguard_health.sql +116 -0
  183. db_ops/metrics/collectors/oracle/014_oracle_wait_stats.sql +15 -0
  184. db_ops/metrics/collectors/oracle/016_oracle_backup_job_status.sql +49 -0
  185. db_ops/metrics/collectors/oracle/017_oracle_alertlog_recent_critical.sql +33 -0
  186. db_ops/metrics/collectors/oracle/018_oracle_redo_reuse_wait.sql +73 -0
  187. db_ops/metrics/collectors/oracle/019_oracle_io_latency.sql +19 -0
  188. db_ops/metrics/collectors/oracle/022_oracle_last_backup_result.sql +89 -0
  189. db_ops/metrics/collectors/oracle/024_oracle_sleeping_open_transaction.sql +62 -0
  190. db_ops/metrics/collectors/oracle/061_oracle_index_health.sql +90 -0
  191. db_ops/metrics/collectors/oracle/061_oracle_user_permissions.sql +38 -0
  192. db_ops/metrics/collectors/oracle/062_oracle_constraint_health.sql +69 -0
  193. db_ops/metrics/collectors/oracle/064_oracle_statistics_age.sql +56 -0
  194. db_ops/metrics/collectors/oracle/065_oracle_top_disk_read_sql.sql +28 -0
  195. db_ops/metrics/collectors/oracle/066_oracle_top_buffer_gets_sql.sql +29 -0
  196. db_ops/metrics/collectors/oracle/067_oracle_index_unusable.sql +49 -0
  197. db_ops/metrics/collectors/oracle/068_oracle_invalid_objects.sql +37 -0
  198. db_ops/metrics/collectors/oracle/069_oracle_top_segments.sql +36 -0
  199. db_ops/metrics/collectors/oracle/070_oracle_scheduler_jobs.sql +39 -0
  200. db_ops/metrics/collectors/oracle/071_oracle_index_inventory.sql +85 -0
  201. db_ops/metrics/collectors/oracle/201_oracle_backup_health.sh +160 -0
  202. db_ops/metrics/collectors/oracle/202_oracle_restore_validation.sh +81 -0
  203. db_ops/metrics/collectors/oracle/legacy_8i/001_oracle_instance_status.sql +9 -0
  204. db_ops/metrics/collectors/oracle/legacy_8i/002_oracle_database_status.sql +22 -0
  205. db_ops/metrics/collectors/oracle/legacy_8i/003_oracle_sessions.sql +26 -0
  206. db_ops/metrics/collectors/oracle/legacy_8i/004_oracle_long_running_queries.sql +17 -0
  207. db_ops/metrics/collectors/oracle/legacy_8i/006_oracle_data_file_space.sql +12 -0
  208. db_ops/metrics/collectors/oracle/legacy_8i/008_oracle_temp_usage.sql +40 -0
  209. db_ops/metrics/collectors/oracle/legacy_8i/009_oracle_blocking_sessions.sql +14 -0
  210. db_ops/metrics/collectors/oracle/legacy_8i/014_oracle_wait_stats.sql +20 -0
  211. db_ops/metrics/collectors/oracle/legacy_8i/019_oracle_io_latency.sql +15 -0
  212. db_ops/metrics/collectors/oracle/legacy_8i/101_oracle_tablespace_free.sql +79 -0
  213. db_ops/metrics/collectors/oracle/legacy_8i/102_oracle_process_limit.sql +18 -0
  214. db_ops/metrics/collectors/oracle/legacy_8i/103_oracle_shared_pool_free.sql +16 -0
  215. db_ops/metrics/collectors/oracle/legacy_8i/104_oracle_library_cache.sql +15 -0
  216. db_ops/metrics/collectors/oracle/legacy_8i/105_oracle_buffer_cache_hit.sql +18 -0
  217. db_ops/metrics/collectors/oracle/legacy_8i/106_oracle_top_disk_read_sql.sql +16 -0
  218. db_ops/metrics/collectors/oracle/legacy_8i/107_oracle_top_buffer_gets_sql.sql +31 -0
  219. db_ops/metrics/collectors/oracle/legacy_8i/108_oracle_segment_extent_limit.sql +39 -0
  220. db_ops/metrics/collectors/oracle/legacy_8i/109_oracle_statistics_age.sql +35 -0
  221. db_ops/metrics/collectors/oracle/legacy_8i/110_oracle_archive_dest.sql +67 -0
  222. db_ops/metrics/collectors/oracle/legacy_8i/111_oracle_rollback_contention.sql +36 -0
  223. db_ops/metrics/collectors/oracle/legacy_8i/112_oracle_jobs.sql +43 -0
  224. db_ops/metrics/collectors/oracle/legacy_8i/113_oracle_dead_sessions_and_threads.sql +78 -0
  225. db_ops/metrics/collectors/os/linux/001_os_cpu_usage.sh +48 -0
  226. db_ops/metrics/collectors/os/linux/002_os_memory_usage.sh +59 -0
  227. db_ops/metrics/collectors/os/linux/003_os_disk_usage.sh +48 -0
  228. db_ops/metrics/collectors/os/linux/004_os_top_processes.sh +12 -0
  229. db_ops/metrics/collectors/os/linux/005_os_service_status.sh +80 -0
  230. db_ops/metrics/collectors/os/linux/006_os_reboot_pending.sh +15 -0
  231. db_ops/metrics/collectors/os/linux/007_os_uptime.sh +13 -0
  232. db_ops/metrics/collectors/os/linux/008_os_time_sync.sh +16 -0
  233. db_ops/metrics/collectors/os/linux/009_os_tcp_port_status.sh +127 -0
  234. db_ops/metrics/collectors/os/linux/010_os_info.sh +31 -0
  235. db_ops/metrics/collectors/os/linux/011_os_network.sh +67 -0
  236. db_ops/metrics/collectors/os/linux/012_os_eventlog_critical.sh +38 -0
  237. db_ops/metrics/collectors/os/linux/013_os_process_top_cpu.sh +20 -0
  238. db_ops/metrics/collectors/os/linux/014_os_process_top_memory.sh +23 -0
  239. db_ops/metrics/collectors/os/windows/001_os_cpu_usage.ps1 +37 -0
  240. db_ops/metrics/collectors/os/windows/002_os_memory_usage.ps1 +42 -0
  241. db_ops/metrics/collectors/os/windows/003_os_disk_usage.ps1 +98 -0
  242. db_ops/metrics/collectors/os/windows/004_os_top_processes.ps1 +19 -0
  243. db_ops/metrics/collectors/os/windows/005_os_service_status.ps1 +32 -0
  244. db_ops/metrics/collectors/os/windows/006_os_reboot_pending.ps1 +16 -0
  245. db_ops/metrics/collectors/os/windows/007_os_uptime.ps1 +14 -0
  246. db_ops/metrics/collectors/os/windows/008_os_time_sync.ps1 +53 -0
  247. db_ops/metrics/collectors/os/windows/009_os_tcp_port_status.ps1 +149 -0
  248. db_ops/metrics/collectors/os/windows/010_os_info.ps1 +21 -0
  249. db_ops/metrics/collectors/os/windows/011_os_network.ps1 +73 -0
  250. db_ops/metrics/collectors/os/windows/012_os_eventlog_critical.ps1 +95 -0
  251. db_ops/metrics/collectors/os/windows/013_os_process_top_cpu.ps1 +52 -0
  252. db_ops/metrics/collectors/os/windows/014_os_process_top_memory.ps1 +31 -0
  253. db_ops/metrics/collectors/os/windows/015_os_failover_cluster.ps1 +191 -0
  254. db_ops/metrics/collectors/postgresql/001_postgresql_instance_status.sql +8 -0
  255. db_ops/metrics/collectors/postgresql/002_postgresql_database_status.sql +16 -0
  256. db_ops/metrics/collectors/postgresql/003_postgresql_connections.sql +15 -0
  257. db_ops/metrics/collectors/postgresql/004_postgresql_long_running_queries.sql +18 -0
  258. db_ops/metrics/collectors/postgresql/006_postgresql_database_size.sql +10 -0
  259. db_ops/metrics/collectors/postgresql/009_postgresql_blocking_sessions.sql +16 -0
  260. db_ops/metrics/collectors/postgresql/022_postgresql_last_backup_result.sh +129 -0
  261. db_ops/metrics/collectors/postgresql/050_postgresql_replication.sql +97 -0
  262. db_ops/metrics/collectors/postgresql/051_postgresql_identity_role.sql +6 -0
  263. db_ops/metrics/collectors/postgresql/052_postgresql_replication_slots.sql +10 -0
  264. db_ops/metrics/collectors/postgresql/053_postgresql_wal_archive.sql +6 -0
  265. db_ops/metrics/collectors/postgresql/054_postgresql_transactions.sql +7 -0
  266. db_ops/metrics/collectors/postgresql/055_postgresql_vacuum_health.sql +7 -0
  267. db_ops/metrics/collectors/postgresql/056_postgresql_xid_wraparound.sql +6 -0
  268. db_ops/metrics/collectors/postgresql/057_postgresql_database_health.sql +7 -0
  269. db_ops/metrics/collectors/postgresql/058_postgresql_invalid_indexes.sql +4 -0
  270. db_ops/metrics/collectors/postgresql/059_postgresql_role_permissions.sql +53 -0
  271. db_ops/metrics/collectors/postgresql/060_postgresql_constraint_health.sql +54 -0
  272. db_ops/metrics/collectors/postgresql/061_postgresql_database_data_size.sql +28 -0
  273. db_ops/metrics/collectors/postgresql/062_postgresql_table_bloat.sql +56 -0
  274. db_ops/metrics/collectors/postgresql/072_postgresql_index_usage.sql +183 -0
  275. db_ops/metrics/collectors/sqlserver/001_sqlserver_instance_status.sql +39 -0
  276. db_ops/metrics/collectors/sqlserver/002_sqlserver_database_status.sql +31 -0
  277. db_ops/metrics/collectors/sqlserver/003_sqlserver_connections.sql +17 -0
  278. db_ops/metrics/collectors/sqlserver/004_sqlserver_long_running_queries.sql +130 -0
  279. db_ops/metrics/collectors/sqlserver/005_sqlserver_backup_age.sql +41 -0
  280. db_ops/metrics/collectors/sqlserver/006_sqlserver_data_file_space.sql +132 -0
  281. db_ops/metrics/collectors/sqlserver/007_sqlserver_log_space.sql +96 -0
  282. db_ops/metrics/collectors/sqlserver/008_sqlserver_tempdb_space.sql +41 -0
  283. db_ops/metrics/collectors/sqlserver/009_sqlserver_blocking_sessions.sql +136 -0
  284. db_ops/metrics/collectors/sqlserver/010_sqlserver_agent_failed_jobs.sql +108 -0
  285. db_ops/metrics/collectors/sqlserver/011_sqlserver_ag_database_health.sql +29 -0
  286. db_ops/metrics/collectors/sqlserver/012_sqlserver_cpu_memory.sql +217 -0
  287. db_ops/metrics/collectors/sqlserver/013_sqlserver_disk_free_space.sql +54 -0
  288. db_ops/metrics/collectors/sqlserver/014_sqlserver_wait_stats.sql +71 -0
  289. db_ops/metrics/collectors/sqlserver/015_sqlserver_deadlock_recent.sql +39 -0
  290. db_ops/metrics/collectors/sqlserver/016_sqlserver_backup_job_status.sql +154 -0
  291. db_ops/metrics/collectors/sqlserver/017_sqlserver_errorlog_recent_critical.sql +249 -0
  292. db_ops/metrics/collectors/sqlserver/018_sqlserver_log_reuse_wait.sql +49 -0
  293. db_ops/metrics/collectors/sqlserver/019_sqlserver_io_latency.sql +107 -0
  294. db_ops/metrics/collectors/sqlserver/021_sqlserver_suspect_pages.sql +45 -0
  295. db_ops/metrics/collectors/sqlserver/022_sqlserver_last_backup_result.sql +87 -0
  296. db_ops/metrics/collectors/sqlserver/023_sqlserver_query_store_query_issues.sql +416 -0
  297. db_ops/metrics/collectors/sqlserver/024_sqlserver_sleeping_open_transaction.sql +167 -0
  298. db_ops/metrics/collectors/sqlserver/025_sqlserver_transaction_lock_holders.sql +221 -0
  299. db_ops/metrics/collectors/sqlserver/026_long_waiting_or_rollback_requests.sql +241 -0
  300. db_ops/metrics/collectors/sqlserver/050_sqlserver_configuration.sql +24 -0
  301. db_ops/metrics/collectors/sqlserver/051_sqlserver_database_config.sql +22 -0
  302. db_ops/metrics/collectors/sqlserver/052_sqlserver_page_life_expectancy.sql +11 -0
  303. db_ops/metrics/collectors/sqlserver/053_sqlserver_agent_job_inventory.sql +122 -0
  304. db_ops/metrics/collectors/sqlserver/054_sqlserver_database_checkdb.sql +131 -0
  305. db_ops/metrics/collectors/sqlserver/055_sqlserver_file_placement.sql +21 -0
  306. db_ops/metrics/collectors/sqlserver/056_sqlserver_agent_job_runtime.sql +91 -0
  307. db_ops/metrics/collectors/sqlserver/057_sqlserver_database_user_permissions.sql +118 -0
  308. db_ops/metrics/collectors/sqlserver/058_sqlserver_constraint_health.sql +135 -0
  309. db_ops/metrics/collectors/sqlserver/059_sqlserver_certificate_expiry.sql +67 -0
  310. db_ops/metrics/collectors/sqlserver/060_sqlserver_failed_logins.sql +73 -0
  311. db_ops/metrics/collectors/sqlserver/061_sqlserver_index_fragmentation.sql +67 -0
  312. db_ops/metrics/collectors/sqlserver/062_sqlserver_statistics_age.sql +62 -0
  313. db_ops/metrics/collectors/sqlserver/063_sqlserver_login_health.sql +147 -0
  314. db_ops/metrics/collectors/sqlserver/064_sqlserver_database_data_size.sql +36 -0
  315. db_ops/metrics/collectors/sqlserver/065_sqlserver_database_log_size.sql +30 -0
  316. db_ops/metrics/collectors/sqlserver/066_sqlserver_heap_fragmentation.sql +92 -0
  317. db_ops/metrics/collectors/sqlserver/067_sqlserver_vlf_count.sql +67 -0
  318. db_ops/metrics/collectors/sqlserver/068_sqlserver_index_usage.sql +304 -0
  319. db_ops/metrics/collectors/sqlserver/069_sqlserver_linked_server_status.sql +206 -0
  320. db_ops/metrics/collectors/sqlserver/070_sqlserver_query_store_coverage.sql +221 -0
  321. db_ops/metrics/collectors/sqlserver/071_sqlserver_server_principals.sql +99 -0
  322. db_ops/metrics/collectors/sqlserver/legacy_2008r2/001_sqlserver_instance_status.sql +35 -0
  323. db_ops/metrics/collectors/sqlserver/legacy_2008r2/002_sqlserver_database_status.sql +32 -0
  324. db_ops/metrics/collectors/sqlserver/legacy_2008r2/003_sqlserver_connections.sql +15 -0
  325. db_ops/metrics/collectors/sqlserver/legacy_2008r2/004_sqlserver_long_running_queries.sql +128 -0
  326. db_ops/metrics/collectors/sqlserver/legacy_2008r2/005_sqlserver_backup_age.sql +37 -0
  327. db_ops/metrics/collectors/sqlserver/legacy_2008r2/006_sqlserver_data_file_space.sql +50 -0
  328. db_ops/metrics/collectors/sqlserver/legacy_2008r2/007_sqlserver_log_space.sql +37 -0
  329. db_ops/metrics/collectors/sqlserver/legacy_2008r2/008_sqlserver_tempdb_space.sql +53 -0
  330. db_ops/metrics/collectors/sqlserver/legacy_2008r2/009_sqlserver_blocking_sessions.sql +37 -0
  331. db_ops/metrics/collectors/sqlserver/legacy_2008r2/010_sqlserver_agent_failed_jobs.sql +79 -0
  332. db_ops/metrics/collectors/sqlserver/legacy_2008r2/011_sqlserver_ag_database_health.sql +6 -0
  333. db_ops/metrics/collectors/sqlserver/legacy_2008r2/012_sqlserver_cpu_memory.sql +217 -0
  334. db_ops/metrics/collectors/sqlserver/legacy_2008r2/013_sqlserver_disk_free_space.sql +24 -0
  335. db_ops/metrics/collectors/sqlserver/legacy_2008r2/014_sqlserver_wait_stats.sql +28 -0
  336. db_ops/metrics/collectors/sqlserver/legacy_2008r2/015_sqlserver_deadlock_recent.sql +6 -0
  337. db_ops/metrics/collectors/sqlserver/legacy_2008r2/016_sqlserver_backup_job_status.sql +103 -0
  338. db_ops/metrics/collectors/sqlserver/legacy_2008r2/017_sqlserver_errorlog_recent_critical.sql +249 -0
  339. db_ops/metrics/collectors/sqlserver/legacy_2008r2/018_sqlserver_log_reuse_wait.sql +39 -0
  340. db_ops/metrics/collectors/sqlserver/legacy_2008r2/019_sqlserver_io_latency.sql +87 -0
  341. db_ops/metrics/collectors/sqlserver/legacy_2008r2/021_sqlserver_suspect_pages.sql +45 -0
  342. db_ops/metrics/collectors/sqlserver/legacy_2008r2/022_sqlserver_last_backup_result.sql +87 -0
  343. db_ops/metrics/collectors/sqlserver/legacy_2008r2/023_sqlserver_query_store_query_issues.sql +11 -0
  344. db_ops/metrics/collectors/sqlserver/legacy_2008r2/024_sqlserver_sleeping_open_transaction.sql +33 -0
  345. db_ops/metrics/collectors/sqlserver/legacy_2008r2/025_sqlserver_transaction_lock_holders.sql +154 -0
  346. db_ops/metrics/collectors/sqlserver/legacy_2008r2/026_long_waiting_or_rollback_requests.sql +241 -0
  347. db_ops/metrics/collectors/sqlserver/legacy_2008r2/054_sqlserver_database_checkdb.sql +136 -0
  348. db_ops/metrics/collectors/sqlserver/legacy_2008r2/069_sqlserver_linked_server_status.sql +189 -0
  349. db_ops/metrics/definitions.py +319 -0
  350. db_ops/metrics/executor.py +307 -0
  351. db_ops/metrics/health.py +83 -0
  352. db_ops/metrics/importance.py +96 -0
  353. db_ops/metrics/message.py +68 -0
  354. db_ops/metrics/models.py +148 -0
  355. db_ops/metrics/notify.py +28 -0
  356. db_ops/metrics/storage.py +29 -0
  357. db_ops/metrics/targets.py +293 -0
  358. db_ops/scaffold.py +495 -0
  359. db_ops/telegram/__init__.py +43 -0
  360. db_ops/telegram/api.py +316 -0
  361. db_ops/telegram/cli.py +219 -0
  362. db_ops/telegram/client.py +38 -0
  363. db_ops/telegram/command_processor.py +3239 -0
  364. db_ops/telegram/commands.py +41 -0
  365. db_ops/telegram/metrics_reports.py +93 -0
  366. db_ops/telegram/routing.py +75 -0
  367. db_ops/telegram/send_queue.py +132 -0
  368. db_ops/telegram/severity.py +32 -0
  369. db_ops/telegram/sql_commands.py +201 -0
  370. db_ops/telegram/updates.py +251 -0
  371. db_ops/telegram/workflow.py +75 -0
  372. dbabrain-0.1.0.dist-info/METADATA +297 -0
  373. dbabrain-0.1.0.dist-info/RECORD +378 -0
  374. dbabrain-0.1.0.dist-info/WHEEL +5 -0
  375. dbabrain-0.1.0.dist-info/entry_points.txt +2 -0
  376. dbabrain-0.1.0.dist-info/licenses/LICENSE +201 -0
  377. dbabrain-0.1.0.dist-info/licenses/NOTICE +57 -0
  378. dbabrain-0.1.0.dist-info/top_level.txt +1 -0
db_ops/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """DB Ops Python helpers for ORD 46."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ # Single source of truth for the db_ops version. Bump this on every fix/change;
6
+ # the Docker build (docker/build_and_package.ps1) reads it to tag the image
7
+ # (db_ops:<version>). Format: MAJOR.MINOR.PATCH (zero-padded).
8
+ __version__ = "0.1.0"
db_ops/cli.py ADDED
@@ -0,0 +1,495 @@
1
+ """The root entry point: the logging/store smoke test, and the one command that spans apps.
2
+
3
+ Everything else in this tree belongs to exactly one component. ``check-credentials`` does not —
4
+ answering "does every configured target resolve to a real login" needs the *metrics* target
5
+ loader (which attaches the resolved credential) and the *Telegram* SQL-command resolver (which
6
+ owns its own lookup rules), and no component may import two apps: apps may not import each other,
7
+ and ``common`` may not import any.
8
+
9
+ It lived in ``db_ops.common.cli`` until 2026-08-15 behind the single entry in
10
+ ``ALLOWED_UPWARD_IMPORTS``. That entry was the shared layer's only upward edge in the whole tree,
11
+ kept alive by one command that was never really shared-layer work. Here it costs no exception:
12
+ ``db_ops/cli.py`` is a root module, not a component, so it sits outside both import rules — which
13
+ is exactly what a composition root is for.
14
+
15
+ A config-only reimplementation was the obvious alternative and is the wrong one: it would answer a
16
+ *different* question than the one the apps ask at runtime, which is the failure this command
17
+ exists to catch.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import socket
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ from db_ops.config import DEFAULT_CONFIG_PATH, load_config
30
+ from db_ops.db import DbOpsStore
31
+ from db_ops.db.schema_export import export_sqlite_schema
32
+ from db_ops.db.job_runs import JobRun
33
+ from db_ops.levels import normalize_level
34
+ from db_ops.logging_ops import log_event, log_function_call, log_function_error, setup_app_logger
35
+ from db_ops.logging_ops.runtime_stdout import patch_stdout
36
+
37
+
38
+ CHECK_CREDENTIALS_USAGE = (
39
+ "usage: python -m db_ops.cli check-credentials [<json>|@<file>|-]\n"
40
+ "\n"
41
+ "Verify every configured target resolves to a named credential. Exit 1 if any does not.\n"
42
+ "\n"
43
+ "Answers in the standard response envelope; the unresolvable targets are in data.problems.\n"
44
+ 'Pass {"format": "txt"} for the old plain-text listing (problems on stderr, summary on\n'
45
+ "stdout), which is what pasted runbook lines expect.\n"
46
+ ' {"data_dir": "data"}\n'
47
+ "\n"
48
+ "Fields:\n"
49
+ " data_dir folder holding db_instances.json / users.json / sql_targets.json\n"
50
+ " (default: the folder data_sources resolves)\n"
51
+ "\n"
52
+ "Legacy form (still accepted): check-credentials <data-dir>\n"
53
+ )
54
+
55
+
56
+ def _check_credentials_command(argv: list[str]) -> int:
57
+ """Verify every configured target resolves to a named credential. Exit 1 if any does not.
58
+
59
+ A credential is required, never inferred (see
60
+ :func:`db_ops.common.data_sources.find_database_credential`), so a config edit that drops
61
+ ``default_credential_name`` / ``credential_name`` now stops that target instead of quietly
62
+ connecting as whatever entry happened to come first. Run this after editing
63
+ ``db_instances.json`` / ``users.json`` / ``sql_targets.json`` and before a deploy.
64
+ """
65
+ from db_ops.common import data_sources
66
+ from db_ops.common.cli import _optional_json_request
67
+ from db_ops.metrics import targets as metric_targets
68
+ from db_ops.telegram import sql_commands
69
+
70
+ # The request parser stays in `common`: one JSON-object contract for the whole tool, not a
71
+ # second one here that would drift on `@file` or stdin handling.
72
+ request, code = _optional_json_request(argv, CHECK_CREDENTIALS_USAGE)
73
+ if request is None and code:
74
+ return code
75
+ requested_dir = str((request or {}).get("data_dir") or "") if request is not None else (argv[0] if argv else "")
76
+
77
+ # load_metric_targets needs a concrete folder; default to the one data_sources resolves.
78
+ data_dir = Path(requested_dir) if requested_dir else data_sources.users_path().parent
79
+ problems: list[str] = []
80
+ checked = 0
81
+
82
+ for target in metric_targets.load_metric_targets(data_dir=data_dir):
83
+ # Host-only entries (no db_type) and API-bridge targets carry no DB login by design.
84
+ if not target.db_type:
85
+ continue
86
+ if str((target.sql_access or {}).get("method") or "direct").lower() == "api":
87
+ continue
88
+ checked += 1
89
+ if not target.credential:
90
+ problems.append(
91
+ f"metrics target {target.target_id}: no credential "
92
+ f"(default_credential_name={target.credential_name or '<unset>'})"
93
+ )
94
+
95
+ groups = data_sources.load_all_credentials(data_dir)
96
+ targets_file = data_dir / "sql_targets.json"
97
+ if targets_file.exists():
98
+ entries = json.loads(targets_file.read_bytes().decode("utf-8-sig")).get("sql_targets", [])
99
+ for entry in entries:
100
+ if str(entry.get("active", 1)) in ("0", "false", "False"):
101
+ continue
102
+ checked += 1
103
+ db_type = str(entry.get("db_type") or "").lower()
104
+ try:
105
+ data_sources.find_database_credential(
106
+ groups.get(db_type, []),
107
+ server_id=str(entry.get("server_id") or ""),
108
+ credential_name=str(entry.get("credential_name") or ""),
109
+ db_type=db_type,
110
+ service_name=str(entry.get("service_name") or ""),
111
+ instance_name=str(entry.get("instance_name") or ""),
112
+ )
113
+ except data_sources.CredentialNotFound as exc:
114
+ problems.append(f"sql target {entry.get('sql_id')}/{entry.get('target_no')}: {exc}")
115
+
116
+ commands_file = data_dir / "telegram_support_commands.json"
117
+ if commands_file.exists():
118
+ commands = json.loads(commands_file.read_bytes().decode("utf-8-sig"))
119
+ for command in commands.get("telegram_support_commands", []):
120
+ if str(command.get("action_type")) != "sql_execute":
121
+ continue
122
+ checked += 1
123
+ try:
124
+ sql_commands.find_credential(command.get("action_config") or {})
125
+ except Exception as exc: # noqa: BLE001 - report, do not abort the whole check.
126
+ problems.append(f"telegram command {command.get('command_text')}: {exc}")
127
+
128
+ # The answer, in the one response shape (2026-08-16). This command used to put its *finding*
129
+ # in the exit code and its detail on **stderr** — the precise split `lib/response.py`'s own
130
+ # docstring forbids, and it made this one of two commands in the tree a program could not
131
+ # consume at all. `success` is now "the check ran"; whether anything is wrong is
132
+ # `data.problems`, which is a fact about the estate rather than about this process.
133
+ from db_ops.lib import response
134
+
135
+ summary = f"checked {checked} target(s); {len(problems)} without a resolvable credential"
136
+ if str((request or {}).get("format") or "json").strip().lower() == "txt":
137
+ for problem in problems:
138
+ print(problem, file=sys.stderr)
139
+ print(summary)
140
+ return 1 if problems else 0
141
+ response.emit(response.ok(
142
+ "check-credentials", message=summary,
143
+ data={"checked": checked, "problems": problems},
144
+ metrics={"checked": checked, "problem_count": len(problems)},
145
+ ))
146
+ # The exit code still says "something is unresolvable", because a runbook and a scheduled
147
+ # caller both check `$?` — and it agrees with the response rather than replacing it.
148
+ return 1 if problems else 0
149
+
150
+
151
+ def parse_args(argv: list[str]) -> argparse.Namespace:
152
+ parser = argparse.ArgumentParser(description="DB Ops logging and Telegram alert test.")
153
+ parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Path to config JSON.")
154
+ parser.add_argument("--level", default="logging", help="logging, warning, or error.")
155
+ parser.add_argument("--message", default="", help="Message to log and optionally alert.")
156
+ parser.add_argument("--job-code", default="", help="When set, also insert this event into SQLite job_runs.")
157
+ parser.add_argument("--status", default="done", help="Job status for SQLite job_runs.")
158
+ parser.add_argument("--duration-ms", type=int, default=None, help="Job duration in milliseconds.")
159
+ parser.add_argument("--error-text", default=None, help="Error text for SQLite job_runs.")
160
+ parser.add_argument("--recent", type=int, default=0, help="Print recent SQLite job_runs and exit.")
161
+ parser.add_argument("--export-sqlite-schema", action="store_true", help="Export SQLite schema JSON documentation and exit.")
162
+ parser.add_argument("--schema-output-dir", default="runtime", help="Output directory for SQLite structure JSON.")
163
+ return parser.parse_args(argv)
164
+
165
+
166
+ #: Every app, by the name someone types. The value is the module holding that app's ``main`` —
167
+ #: imported only when it is asked for, which is what lets ``--help`` and ``--version`` work on an
168
+ #: install with no database driver at all. Importing all twelve up front would make the help text
169
+ #: depend on having pyodbc.
170
+ APPS: dict[str, str] = {
171
+ "metrics": "db_ops.metrics.cli",
172
+ "reports": "db_ops.reports.cli",
173
+ "telegram": "db_ops.telegram.cli",
174
+ "sla": "db_ops.sla.cli",
175
+ "backup-restore": "db_ops.backup_restore.cli",
176
+ "sql-tasks": "db_ops.sql_tasks.cli",
177
+ "sre": "db_ops.sre.cli",
178
+ "control": "db_ops.control.cli",
179
+ "webhost": "db_ops.webhost.cli",
180
+ "db": "db_ops.db.cli",
181
+ "common": "db_ops.common.cli",
182
+ "daemon": "db_ops.jobs.daemon",
183
+ }
184
+
185
+
186
+ INIT_USAGE = (
187
+ "usage: db-ops init [<directory>] [--force] [--app-name NAME]\n"
188
+ "\n"
189
+ "Turn a directory into a working tool root. Defaults to the current one.\n"
190
+ "\n"
191
+ "Writes the smallest tree that runs: config.json, a SQLite store declaration, an empty\n"
192
+ "inventory, a starter metric catalogue, and the Telegram and secret files, each carrying\n"
193
+ "notes that say what to put in it. Nothing is overwritten without --force, because the\n"
194
+ "files this writes are the ones you edit next.\n"
195
+ "\n"
196
+ "The store starts on SQLite in runtime/ - a first run has no PostgreSQL, and needing one\n"
197
+ "to hold the results of monitoring is a poor first request. data/store_config.json is\n"
198
+ "where you move to PostgreSQL later.\n"
199
+ )
200
+
201
+
202
+ def _init_command(argv: list[str]) -> int:
203
+ """``init`` — the first command anybody runs, and until 2026-08-22 it did not exist.
204
+
205
+ Measured on a real `pip install` into a clean virtualenv: from an empty directory the toolkit
206
+ resolved its configuration to `site-packages` and told the reader to create a file there from
207
+ an example that does not ship. Installable and unstartable.
208
+ """
209
+ from pathlib import Path
210
+
211
+ from db_ops import scaffold
212
+
213
+ target = "."
214
+ app_name = "dbabrain"
215
+ force = False
216
+ rest = list(argv)
217
+ while rest:
218
+ token = rest.pop(0)
219
+ if token in {"-h", "--help"}:
220
+ print(INIT_USAGE)
221
+ return 0
222
+ if token == "--force":
223
+ force = True
224
+ elif token == "--app-name":
225
+ app_name = rest.pop(0) if rest else app_name
226
+ elif token.startswith("-"):
227
+ print(f"init: unknown option {token}\n\n{INIT_USAGE}", file=sys.stderr)
228
+ return 2
229
+ else:
230
+ target = token
231
+
232
+ try:
233
+ result = scaffold.initialise(Path(target), app_name=app_name, force=force)
234
+ except scaffold.ScaffoldError as exc:
235
+ print(f"init refused: {exc}", file=sys.stderr)
236
+ return 1
237
+
238
+ for name in result.written:
239
+ print(f" created {name}")
240
+ for name in result.skipped:
241
+ print(f" kept {name} (already there; --force overwrites)")
242
+ print("")
243
+ print(scaffold.next_steps(result.root))
244
+ return 0
245
+
246
+
247
+ ENCRYPT_SECRET_USAGE = (
248
+ "usage: db-ops encrypt-secret [--source PATH] [--dest PATH] --key-base64 B64 | --key TEXT\n"
249
+ "\n"
250
+ "Encrypt secrets/secret_text.json into data/encrypted_secret_text.json, which is the file\n"
251
+ "the toolkit actually reads. The plaintext source is never read at run time and must never\n"
252
+ "be committed.\n"
253
+ "\n"
254
+ "Run it again after every edit to the plaintext file - adding a secret does nothing until\n"
255
+ "it is encrypted.\n"
256
+ "\n"
257
+ "Keep the passphrase. Nothing else can decrypt the store, and there is no recovery.\n"
258
+ )
259
+
260
+
261
+ def _encrypt_secret_command(argv: list[str]) -> int:
262
+ """``encrypt-secret`` — turn the plaintext secret source into the store the toolkit reads.
263
+
264
+ This lived only in ``db_ops.control.cli`` until 2026-08-22, and `control` is master-side deploy
265
+ tooling that the public distribution does not ship. So the first-run instructions named a
266
+ command that did not exist in the distribution they were written for — found by running the
267
+ documented steps against a real SQL Server rather than by reading them.
268
+
269
+ It belongs here on two counts: every install needs it, and it is the secret *store*, which
270
+ ships, rather than the deploy tooling, which does not.
271
+ """
272
+ from pathlib import Path
273
+
274
+ from db_ops.lib.paths import TOOL_ROOT
275
+ from db_ops.lib.secret_text import encrypt_secret_text_file, resolve_cli_key
276
+
277
+ source = Path(TOOL_ROOT) / "secrets" / "secret_text.json"
278
+ dest = Path(TOOL_ROOT) / "data" / "encrypted_secret_text.json"
279
+ key = key_base64 = None
280
+ rest = list(argv)
281
+ while rest:
282
+ token = rest.pop(0)
283
+ if token in {"-h", "--help"}:
284
+ print(ENCRYPT_SECRET_USAGE)
285
+ return 0
286
+ if token == "--source":
287
+ source = Path(rest.pop(0)) if rest else source
288
+ elif token == "--dest":
289
+ dest = Path(rest.pop(0)) if rest else dest
290
+ elif token == "--key":
291
+ key = rest.pop(0) if rest else None
292
+ elif token in {"--key-base64", "--key_base64"}:
293
+ key_base64 = rest.pop(0) if rest else None
294
+ else:
295
+ print(f"encrypt-secret: unknown option {token}", file=sys.stderr)
296
+ print("", file=sys.stderr)
297
+ print(ENCRYPT_SECRET_USAGE, file=sys.stderr)
298
+ return 2
299
+
300
+ try:
301
+ resolved = resolve_cli_key(key, key_base64)
302
+ except ValueError as exc:
303
+ print(f"ERROR: {exc}", file=sys.stderr)
304
+ return 2
305
+ resolved = resolved or os.environ.get("DB_OPS_SECRET_KEY", "").strip()
306
+ if not resolved:
307
+ # Refuse rather than guess. Encrypting with the wrong passphrase produces a store that
308
+ # decrypts nowhere, and the round-trip check inside encrypt_secret_text_file cannot catch
309
+ # it, because it verifies against the same key it just used.
310
+ print("ERROR: no passphrase. Pass --key-base64 or --key, or set DB_OPS_SECRET_KEY.",
311
+ file=sys.stderr)
312
+ return 2
313
+
314
+ if not source.exists():
315
+ print(f"ERROR: {source} not found. `db-ops init` creates it.", file=sys.stderr)
316
+ return 1
317
+ try:
318
+ count = encrypt_secret_text_file(source, dest, resolved)
319
+ except Exception as exc: # noqa: BLE001 - CLI failure path.
320
+ print(f"ERROR: {exc}", file=sys.stderr)
321
+ return 1
322
+ print(f"Encrypted {count} secret(s) -> {dest}")
323
+ return 0
324
+
325
+
326
+ def installed_apps() -> dict[str, str]:
327
+ """The entries of :data:`APPS` this installation actually has.
328
+
329
+ `v0.1.0` of the public distribution ships seven of the fourteen components
330
+ (:mod:`db_ops.lib.distribution`), so on an installed copy this table would otherwise advertise
331
+ seven commands that raise `ModuleNotFoundError` the moment somebody runs the line the help text
332
+ just offered them. Measured, not predicted: the first thin wheel built on 2026-08-22 listed all
333
+ twelve apps and could run five.
334
+
335
+ Detected rather than hardcoded, so one code path is right in both trees — the full checkout
336
+ lists twelve and the thin wheel lists five, and neither needs to know which it is.
337
+
338
+ `find_spec` **locates** a module without executing it, which is what keeps the dispatch lazy:
339
+ importing twelve apps to print a help message would make `--help` depend on having an ODBC
340
+ driver, so a slim install would crash while explaining how to use the tool.
341
+ """
342
+ import importlib.util
343
+
344
+ present: dict[str, str] = {}
345
+ for name, module in APPS.items():
346
+ try:
347
+ if importlib.util.find_spec(module) is not None:
348
+ present[name] = module
349
+ except (ImportError, ValueError):
350
+ # A parent package that is not installed raises rather than returning None.
351
+ continue
352
+ return present
353
+
354
+
355
+ def _usage() -> str:
356
+ from db_ops import __version__
357
+
358
+ apps = installed_apps() or APPS
359
+ width = max(len(name) for name in apps)
360
+ lines = [
361
+ # ASCII only: this prints to whatever console the operator has, and the Windows one is
362
+ # cp1252. An em dash there arrives as a question mark at best.
363
+ f"db_ops {__version__} - database operations toolkit",
364
+ "",
365
+ "usage: db-ops <app> [args...]",
366
+ "",
367
+ "apps:",
368
+ ]
369
+ lines += [f" {name.ljust(width)} python -m {module}" for name, module in sorted(apps.items())]
370
+ lines += [
371
+ "",
372
+ " init create a tool root here: config, a SQLite store, empty inventory",
373
+ " encrypt-secret encrypt secrets/secret_text.json into the store the tool reads",
374
+ " check-credentials does every configured target resolve to a real login",
375
+ "",
376
+ "Each app takes its own arguments; ask it directly, e.g. `db-ops metrics --help`.",
377
+ "Every app is also runnable as `python -m <module>`, which is what the daemon does.",
378
+ ]
379
+ return "\n".join(lines)
380
+
381
+
382
+ def main(argv: list[str] | None = None) -> int:
383
+ """Dispatch to an app, or answer for the toolkit itself.
384
+
385
+ A console script is called with no arguments, so ``argv`` defaults to the real ones; the tests
386
+ pass a list instead, which is why it is a parameter at all.
387
+
388
+ Routing only. Every app owns its own parser and its own ``main``, and this must stay a lookup
389
+ — the moment it starts interpreting an app's arguments there are two parsers for one command
390
+ and they disagree the first time one of them changes.
391
+ """
392
+ argv = list(sys.argv[1:] if argv is None else argv)
393
+
394
+ if not argv or argv[0] in ("-h", "--help", "help"):
395
+ print(_usage())
396
+ return 0
397
+ if argv[0] in ("-V", "--version"):
398
+ from db_ops import __version__
399
+
400
+ print(__version__)
401
+ return 0
402
+
403
+ # Underscores are what the module names use and what a reader may well type.
404
+ app = argv[0].replace("_", "-")
405
+ if app in APPS:
406
+ import importlib
407
+
408
+ # A name this build does not ship gets a sentence, not a traceback. `ModuleNotFoundError:
409
+ # db_ops.sre.cli` tells a reader their install is broken; it is not, and the difference
410
+ # matters because the next thing they do is reinstall.
411
+ #
412
+ # Asked through `installed_apps()` rather than with a bare `find_spec`, which *raises*
413
+ # `ModuleNotFoundError` when the parent package is the missing one — so checking for a
414
+ # missing app with it produced exactly the traceback this branch exists to prevent.
415
+ present = installed_apps()
416
+ if app not in present:
417
+ print(
418
+ f"db-ops: '{app}' is not in this build.\n"
419
+ f"This distribution ships: {', '.join(sorted(present))}.\n"
420
+ "The rest of the toolkit arrives in a later release.",
421
+ file=sys.stderr,
422
+ )
423
+ return 2
424
+ return int(importlib.import_module(present[app]).main(argv[1:]) or 0)
425
+
426
+ # `init` is deliberately first among the words: it is the only command that works before a
427
+ # tool root exists, so it must not be reachable only after one does.
428
+ if argv[0] == "init":
429
+ return _init_command(argv[1:])
430
+
431
+ if argv[0] in {"encrypt-secret", "encrypt-secret-text"}:
432
+ return _encrypt_secret_command(argv[1:])
433
+
434
+ # Subcommand first, flags second. `check-credentials` is a word, and every other form this
435
+ # entry point takes starts with `--`, so the two cannot be confused for one another.
436
+ if argv[0] == "check-credentials":
437
+ return _check_credentials_command(argv[1:])
438
+
439
+ if not argv[0].startswith("-"):
440
+ print(f"db-ops: unknown app {argv[0]!r}\n", file=sys.stderr)
441
+ print(_usage(), file=sys.stderr)
442
+ return 2
443
+
444
+ args = parse_args(argv)
445
+ logger = None
446
+ try:
447
+ level = normalize_level(args.level)
448
+ config = load_config(args.config)
449
+ patch_stdout(config.log_dir / "db_ops_runtime.log", app_name=config.app_name)
450
+ logger = setup_app_logger(config, enable_telegram_alerts=not (args.export_sqlite_schema or args.recent))
451
+ store = DbOpsStore.from_config(config)
452
+ if args.export_sqlite_schema:
453
+ log_function_call(logger, function_name="db.export_sqlite_schema")
454
+ result = export_sqlite_schema(sqlite_path=config.sqlite_path, output_dir=args.schema_output_dir)
455
+ print(result)
456
+ return 0
457
+ if args.recent:
458
+ log_function_call(logger, function_name="db.fetch_recent_job_runs")
459
+ for row in store.fetch_recent_job_runs(args.recent):
460
+ print(
461
+ f"{row['log_id']} | {row['created_at']} | {row['job_code']} | "
462
+ f"{row['level']} | {row['status']} | {row['message']}"
463
+ )
464
+ return 0
465
+
466
+ if not args.message:
467
+ raise ValueError("--message is required unless --recent is used.")
468
+
469
+ log_function_call(logger, function_name="logging_ops.log_event", text=args.message)
470
+ log_event(logger, level=level, message=args.message)
471
+ if args.job_code:
472
+ log_function_call(logger, function_name="db.insert_job_run", text=args.job_code)
473
+ log_id = store.insert_job_run(
474
+ JobRun(
475
+ job_code=args.job_code,
476
+ level=level,
477
+ status=args.status,
478
+ message=args.message,
479
+ duration_ms=args.duration_ms,
480
+ error_text=args.error_text,
481
+ host_name=socket.gethostname(),
482
+ )
483
+ )
484
+ print(f"SQLite job_runs inserted: {log_id}")
485
+ except Exception as exc: # noqa: BLE001 - command-line failure path.
486
+ if logger:
487
+ log_function_error(logger, function_name="db_ops.cli", error_text=str(exc))
488
+ print(f"ERROR: {exc}", file=sys.stderr)
489
+ return 1
490
+
491
+ return 0
492
+
493
+
494
+ if __name__ == "__main__":
495
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1 @@
1
+ """App-neutral shared utilities for db_ops components."""
@@ -0,0 +1,129 @@
1
+ """Running one backup: ship the script to where the database lives, and judge the result honestly.
2
+
3
+ The judging is the part with history behind it. ``exit 0`` means the shell finished, not that a
4
+ backup happened — a script fed to ``bash -s`` can end early with a clean status and no output, and
5
+ one did: a ``docker exec -i`` inside it read the rest of the script as its own stdin, so the shell
6
+ ran out of work and reported success having backed up nothing. A backup that lies about succeeding
7
+ is worse than one that fails, because the failure is only discovered by the restore that needed it.
8
+
9
+ So every script ends by printing ``RESULT=ok``, and success here means **that receipt was seen**.
10
+ Exit 0 without it is reported as an error naming exactly that, which is a sentence an operator can
11
+ act on rather than a silent green run.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ from typing import Any
18
+
19
+ from db_ops.common.backup.spec import (
20
+ BACKUP_LEVEL_BY_ENGINE,
21
+ LEVEL_ENV,
22
+ RECEIPT,
23
+ BackupSpec,
24
+ BackupSpecError,
25
+ backup_level_for,
26
+ parse_backup_spec,
27
+ )
28
+ from db_ops.common.hostcmd import HostCommandError, run_script
29
+
30
+ __all__ = [
31
+ "BACKUP_LEVEL_BY_ENGINE",
32
+ "LEVEL_ENV",
33
+ "RECEIPT",
34
+ "BackupSpec",
35
+ "BackupSpecError",
36
+ "backup_level_for",
37
+ "parse_backup_spec",
38
+ "plan_backup",
39
+ "run_backup",
40
+ ]
41
+
42
+ #: How much of a script's output is worth carrying in an error. Enough to see what the engine said,
43
+ #: bounded because this ends up in a Telegram message and a store row.
44
+ EXCERPT = 2000
45
+
46
+
47
+ def plan_backup(spec: BackupSpec) -> dict[str, Any]:
48
+ """What the run would do, without doing it. Also the dry-run answer.
49
+
50
+ Env **names** but never values: the whole point of the plan is that it can be shown to somebody,
51
+ and half of these are passphrases.
52
+ """
53
+ return {
54
+ "db_type": spec.db_type,
55
+ "label": spec.label,
56
+ "level": spec.level or "(the script decides)",
57
+ "script": spec.script_source,
58
+ "script_lines": len(spec.script.splitlines()),
59
+ "host": spec.host.host or "(this machine)",
60
+ "runtime": spec.host.runtime,
61
+ "container": spec.host.container,
62
+ "env_names": sorted(spec.env),
63
+ "timeout": spec.timeout,
64
+ "server_metadata": dict(spec.server_metadata) or None,
65
+ }
66
+
67
+
68
+ def run_backup(spec: BackupSpec) -> dict[str, Any]:
69
+ """Run the backup and return what happened. Never raises for a backup that merely failed."""
70
+ planned = plan_backup(spec)
71
+ started = time.monotonic()
72
+ try:
73
+ result = run_script(spec.host, spec.script, env=spec.env, timeout=spec.timeout)
74
+ except HostCommandError as exc:
75
+ # Could not run at all - a host that refused the connection. Distinct from a backup that
76
+ # ran and failed, because the fix is somewhere else entirely.
77
+ return {**planned, "status": "error", "exit_code": None, "duration_ms": _ms(started),
78
+ "stdout": "", "stderr": "", "error": str(exc), "receipt": False}
79
+
80
+ out = result.get("stdout") or ""
81
+ err = result.get("stderr") or ""
82
+ exit_code = result.get("exit_code")
83
+ receipt = RECEIPT in out
84
+ status = "done" if exit_code == 0 and receipt else "error"
85
+
86
+ if status == "done":
87
+ error = None
88
+ elif exit_code == 0 and not receipt:
89
+ error = (
90
+ f"Backup script exited 0 without printing {RECEIPT} - it did not run to completion. "
91
+ f"stdout={out.strip()[-800:]!r} stderr={err.strip()[-800:]!r}"
92
+ )
93
+ else:
94
+ error = (err.strip() or out.strip())[-EXCERPT:] or f"exit {exit_code}"
95
+
96
+ result = {**planned, "status": status, "exit_code": exit_code, "duration_ms": _ms(started),
97
+ "stdout": out, "stderr": err, "error": error, "receipt": receipt}
98
+ if spec.server_metadata:
99
+ result["server_metadata_result"] = _export_server_metadata(spec, backup_ok=status == "done")
100
+ return result
101
+
102
+
103
+ def _export_server_metadata(spec: BackupSpec, *, backup_ok: bool) -> dict[str, Any]:
104
+ """Export the instance's server-level metadata beside the backup. Read-only, and never fatal.
105
+
106
+ **A failure here does not fail the backup.** The data is the thing that must not be lost, and
107
+ losing it to a metadata step that could not read ``sys.credentials`` would be absurd — the
108
+ result is reported alongside so the gap is visible without being destructive.
109
+
110
+ Skipped when the backup itself failed: a bundle describing an instance whose backup did not
111
+ complete is a set of files that look like a matched pair and are not.
112
+ """
113
+ if not backup_ok:
114
+ return {"ok": False, "skipped": True,
115
+ "error": "backup did not complete; no metadata exported"}
116
+ from db_ops.common import sqlserver_instance
117
+
118
+ request = {"target": spec.server_metadata["target"],
119
+ "output_dir": spec.server_metadata["output_dir"]}
120
+ if spec.server_metadata.get("include"):
121
+ request["include"] = spec.server_metadata["include"]
122
+ try:
123
+ return sqlserver_instance.export_instance(request)
124
+ except Exception as exc: # noqa: BLE001 - reported next to the backup, never fatal to it.
125
+ return {"ok": False, "error": str(exc), "operation": "sqlserver-export-instance"}
126
+
127
+
128
+ def _ms(started: float) -> int:
129
+ return int((time.monotonic() - started) * 1000)