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.
- db_ops/__init__.py +8 -0
- db_ops/cli.py +495 -0
- db_ops/common/__init__.py +1 -0
- db_ops/common/backup/__init__.py +129 -0
- db_ops/common/backup/spec.py +209 -0
- db_ops/common/backup_scripts/oracle/oracle_rman_archivelog.sh +114 -0
- db_ops/common/backup_scripts/oracle/oracle_rman_database.sh +119 -0
- db_ops/common/backup_scripts/postgresql/pg_archive_wal.sh +116 -0
- db_ops/common/backup_scripts/postgresql/pg_basebackup_database.sh +152 -0
- db_ops/common/backup_scripts/sqlserver/mssql_backup_database.ps1 +274 -0
- db_ops/common/backup_scripts/sqlserver/mssql_backup_database.sh +224 -0
- db_ops/common/backupfiles/__init__.py +171 -0
- db_ops/common/backupfiles/oracle.py +113 -0
- db_ops/common/backupfiles/postgresql.py +54 -0
- db_ops/common/backupfiles/sqlserver.py +83 -0
- db_ops/common/cli.py +1671 -0
- db_ops/common/cli_backup.py +120 -0
- db_ops/common/cli_backup_files.py +237 -0
- db_ops/common/cli_catalog.py +230 -0
- db_ops/common/cli_delete_files.py +137 -0
- db_ops/common/cli_filetransfer.py +106 -0
- db_ops/common/cli_restore.py +106 -0
- db_ops/common/cli_restorestep.py +168 -0
- db_ops/common/config_admin.py +887 -0
- db_ops/common/confirm.py +341 -0
- db_ops/common/data_sources/__init__.py +461 -0
- db_ops/common/data_sources/metric_targets.py +118 -0
- db_ops/common/data_sources/ssh_auth.py +98 -0
- db_ops/common/data_sources/target_resolve.py +238 -0
- db_ops/common/db_catalog.py +534 -0
- db_ops/common/db_connect.py +408 -0
- db_ops/common/deletefiles.py +233 -0
- db_ops/common/evidence.py +254 -0
- db_ops/common/file_transfer.py +685 -0
- db_ops/common/filetransfer.py +204 -0
- db_ops/common/host_ops.py +1443 -0
- db_ops/common/host_probe.py +213 -0
- db_ops/common/hostcmd.py +395 -0
- db_ops/common/identifier_scan.py +528 -0
- db_ops/common/job_control.py +180 -0
- db_ops/common/oracle_bridge.py +527 -0
- db_ops/common/password_rotation.py +470 -0
- db_ops/common/remote_exec.py +1209 -0
- db_ops/common/restore/__init__.py +48 -0
- db_ops/common/restore/sqlserver/__init__.py +28 -0
- db_ops/common/restore/sqlserver/chain.py +180 -0
- db_ops/common/restore/sqlserver/runner.py +177 -0
- db_ops/common/restore/sqlserver/sql.py +90 -0
- db_ops/common/restore/sqlserver/timeparse.py +59 -0
- db_ops/common/restore_drill.py +154 -0
- db_ops/common/restore_scripts/oracle/oracle_rman_restore.sh +282 -0
- db_ops/common/restore_scripts/postgresql/pg_restore_basebackup.sh +344 -0
- db_ops/common/restore_scripts/sqlserver/mssql_restore.sh +240 -0
- db_ops/common/restorekey.py +110 -0
- db_ops/common/restoremetadata.py +113 -0
- db_ops/common/restorestep/__init__.py +83 -0
- db_ops/common/restorestep/oracle.py +192 -0
- db_ops/common/restorestep/postgresql.py +318 -0
- db_ops/common/restorestep/sqlserver.py +107 -0
- db_ops/common/secret_check.py +433 -0
- db_ops/common/session_trace.py +238 -0
- db_ops/common/sql_execution.py +719 -0
- db_ops/common/sql_run.py +920 -0
- db_ops/common/sqlserver_emergency.py +428 -0
- db_ops/common/sqlserver_instance.py +1591 -0
- db_ops/common/sqlserver_patch.py +804 -0
- db_ops/common/ssh.py +119 -0
- db_ops/common/table_load.py +499 -0
- db_ops/common/verifyrestore.py +129 -0
- db_ops/config.py +792 -0
- db_ops/db/__init__.py +3 -0
- db_ops/db/backend.py +759 -0
- db_ops/db/backup_restore_history.py +141 -0
- db_ops/db/cli.py +1194 -0
- db_ops/db/config_edit.py +242 -0
- db_ops/db/config_store.py +640 -0
- db_ops/db/config_sync.py +642 -0
- db_ops/db/declaration.py +130 -0
- db_ops/db/job_runs.py +60 -0
- db_ops/db/metric_definitions.py +30 -0
- db_ops/db/metric_results.py +57 -0
- db_ops/db/metric_store.py +1027 -0
- db_ops/db/ops_status.py +438 -0
- db_ops/db/postgres_store.py +280 -0
- db_ops/db/queue_message.py +137 -0
- db_ops/db/run_requests.py +355 -0
- db_ops/db/schema_export.py +148 -0
- db_ops/db/sla_results.py +94 -0
- db_ops/db/sla_store.py +367 -0
- db_ops/db/sqlite_to_postgres.py +1188 -0
- db_ops/db/store.py +2538 -0
- db_ops/db/telegram_queue.py +209 -0
- db_ops/db/web_auth_store.py +645 -0
- db_ops/jobs/__init__.py +3 -0
- db_ops/jobs/cli.py +32 -0
- db_ops/jobs/daemon.py +1317 -0
- db_ops/jobs/models.py +4 -0
- db_ops/jobs/status.py +149 -0
- db_ops/levels.py +29 -0
- db_ops/lib/__init__.py +24 -0
- db_ops/lib/backup_kinds.py +25 -0
- db_ops/lib/backup_level.py +43 -0
- db_ops/lib/backup_policy.py +283 -0
- db_ops/lib/backupfiles_retention.py +186 -0
- db_ops/lib/capacity_forecast.py +220 -0
- db_ops/lib/cmd_access.py +147 -0
- db_ops/lib/coerce.py +115 -0
- db_ops/lib/common_cli.py +120 -0
- db_ops/lib/connection_spec.py +157 -0
- db_ops/lib/delimited_import.py +229 -0
- db_ops/lib/distribution.py +220 -0
- db_ops/lib/event_policy.py +133 -0
- db_ops/lib/health_model.py +218 -0
- db_ops/lib/instance_bundle.py +55 -0
- db_ops/lib/interval_rates.py +38 -0
- db_ops/lib/inventory_render.py +575 -0
- db_ops/lib/json_io.py +100 -0
- db_ops/lib/listing.py +89 -0
- db_ops/lib/log_tail.py +230 -0
- db_ops/lib/metric_score.py +18 -0
- db_ops/lib/notify.py +239 -0
- db_ops/lib/notify_route.py +107 -0
- db_ops/lib/paths.py +219 -0
- db_ops/lib/policy_engine.py +376 -0
- db_ops/lib/powershell.py +116 -0
- db_ops/lib/record_form.py +300 -0
- db_ops/lib/report_archive.py +79 -0
- db_ops/lib/response.py +104 -0
- db_ops/lib/restore/__init__.py +31 -0
- db_ops/lib/restore/pitr.py +35 -0
- db_ops/lib/restore/plan.py +59 -0
- db_ops/lib/restore/spec.py +224 -0
- db_ops/lib/result_format.py +319 -0
- db_ops/lib/rows.py +41 -0
- db_ops/lib/secret_text.py +309 -0
- db_ops/lib/shell.py +64 -0
- db_ops/lib/sql_access.py +91 -0
- db_ops/lib/sql_text.py +193 -0
- db_ops/lib/ssh_errors.py +34 -0
- db_ops/lib/state_transition.py +239 -0
- db_ops/lib/target_flags.py +39 -0
- db_ops/lib/target_profile.py +512 -0
- db_ops/lib/task_output.py +48 -0
- db_ops/lib/telegram_route.py +112 -0
- db_ops/lib/telegram_severity.py +217 -0
- db_ops/lib/telegram_text.py +90 -0
- db_ops/lib/text_format.py +25 -0
- db_ops/lib/time_window.py +303 -0
- db_ops/lib/web_auth.py +200 -0
- db_ops/lib/xlsx_export.py +223 -0
- db_ops/lib/xlsx_import.py +371 -0
- db_ops/logging_ops/__init__.py +33 -0
- db_ops/logging_ops/formatter.py +10 -0
- db_ops/logging_ops/handlers.py +94 -0
- db_ops/logging_ops/runtime_stdout.py +73 -0
- db_ops/logging_ops/writer.py +113 -0
- db_ops/metrics/__init__.py +23 -0
- db_ops/metrics/cli.py +670 -0
- db_ops/metrics/collector.py +1498 -0
- db_ops/metrics/collectors/docker/009_docker_container_stats.sh +75 -0
- db_ops/metrics/collectors/mysql/001_mysql_instance_status.sql +6 -0
- db_ops/metrics/collectors/mysql/002_mysql_database_status.sql +9 -0
- db_ops/metrics/collectors/mysql/003_mysql_connections.sql +15 -0
- db_ops/metrics/collectors/mysql/004_mysql_long_running_queries.sql +10 -0
- db_ops/metrics/collectors/mysql/006_mysql_data_file_space.sql +14 -0
- db_ops/metrics/collectors/mysql/009_mysql_blocking_sessions.sql +19 -0
- db_ops/metrics/collectors/mysql/014_mysql_wait_stats.sql +11 -0
- db_ops/metrics/collectors/mysql/050_mysql_user_permissions.sql +40 -0
- db_ops/metrics/collectors/mysql/051_mysql_constraint_health.sql +44 -0
- db_ops/metrics/collectors/mysql/052_mysql_database_data_size.sql +29 -0
- db_ops/metrics/collectors/mysql/053_mysql_table_fragmentation.sql +56 -0
- db_ops/metrics/collectors/mysql/054_mysql_statistics_age.sql +53 -0
- db_ops/metrics/collectors/oracle/001_oracle_instance_status.sql +7 -0
- db_ops/metrics/collectors/oracle/002_oracle_database_status.sql +65 -0
- db_ops/metrics/collectors/oracle/003_oracle_sessions.sql +13 -0
- db_ops/metrics/collectors/oracle/004_oracle_long_running_queries.sql +15 -0
- db_ops/metrics/collectors/oracle/005_oracle_backup_age.sql +49 -0
- db_ops/metrics/collectors/oracle/006_oracle_data_file_space.sql +57 -0
- db_ops/metrics/collectors/oracle/007_oracle_archive_log_space.sql +89 -0
- db_ops/metrics/collectors/oracle/009_oracle_blocking_sessions.sql +12 -0
- db_ops/metrics/collectors/oracle/009_oracle_temp_usage.sql +14 -0
- db_ops/metrics/collectors/oracle/011_oracle_dataguard_health.sql +116 -0
- db_ops/metrics/collectors/oracle/014_oracle_wait_stats.sql +15 -0
- db_ops/metrics/collectors/oracle/016_oracle_backup_job_status.sql +49 -0
- db_ops/metrics/collectors/oracle/017_oracle_alertlog_recent_critical.sql +33 -0
- db_ops/metrics/collectors/oracle/018_oracle_redo_reuse_wait.sql +73 -0
- db_ops/metrics/collectors/oracle/019_oracle_io_latency.sql +19 -0
- db_ops/metrics/collectors/oracle/022_oracle_last_backup_result.sql +89 -0
- db_ops/metrics/collectors/oracle/024_oracle_sleeping_open_transaction.sql +62 -0
- db_ops/metrics/collectors/oracle/061_oracle_index_health.sql +90 -0
- db_ops/metrics/collectors/oracle/061_oracle_user_permissions.sql +38 -0
- db_ops/metrics/collectors/oracle/062_oracle_constraint_health.sql +69 -0
- db_ops/metrics/collectors/oracle/064_oracle_statistics_age.sql +56 -0
- db_ops/metrics/collectors/oracle/065_oracle_top_disk_read_sql.sql +28 -0
- db_ops/metrics/collectors/oracle/066_oracle_top_buffer_gets_sql.sql +29 -0
- db_ops/metrics/collectors/oracle/067_oracle_index_unusable.sql +49 -0
- db_ops/metrics/collectors/oracle/068_oracle_invalid_objects.sql +37 -0
- db_ops/metrics/collectors/oracle/069_oracle_top_segments.sql +36 -0
- db_ops/metrics/collectors/oracle/070_oracle_scheduler_jobs.sql +39 -0
- db_ops/metrics/collectors/oracle/071_oracle_index_inventory.sql +85 -0
- db_ops/metrics/collectors/oracle/201_oracle_backup_health.sh +160 -0
- db_ops/metrics/collectors/oracle/202_oracle_restore_validation.sh +81 -0
- db_ops/metrics/collectors/oracle/legacy_8i/001_oracle_instance_status.sql +9 -0
- db_ops/metrics/collectors/oracle/legacy_8i/002_oracle_database_status.sql +22 -0
- db_ops/metrics/collectors/oracle/legacy_8i/003_oracle_sessions.sql +26 -0
- db_ops/metrics/collectors/oracle/legacy_8i/004_oracle_long_running_queries.sql +17 -0
- db_ops/metrics/collectors/oracle/legacy_8i/006_oracle_data_file_space.sql +12 -0
- db_ops/metrics/collectors/oracle/legacy_8i/008_oracle_temp_usage.sql +40 -0
- db_ops/metrics/collectors/oracle/legacy_8i/009_oracle_blocking_sessions.sql +14 -0
- db_ops/metrics/collectors/oracle/legacy_8i/014_oracle_wait_stats.sql +20 -0
- db_ops/metrics/collectors/oracle/legacy_8i/019_oracle_io_latency.sql +15 -0
- db_ops/metrics/collectors/oracle/legacy_8i/101_oracle_tablespace_free.sql +79 -0
- db_ops/metrics/collectors/oracle/legacy_8i/102_oracle_process_limit.sql +18 -0
- db_ops/metrics/collectors/oracle/legacy_8i/103_oracle_shared_pool_free.sql +16 -0
- db_ops/metrics/collectors/oracle/legacy_8i/104_oracle_library_cache.sql +15 -0
- db_ops/metrics/collectors/oracle/legacy_8i/105_oracle_buffer_cache_hit.sql +18 -0
- db_ops/metrics/collectors/oracle/legacy_8i/106_oracle_top_disk_read_sql.sql +16 -0
- db_ops/metrics/collectors/oracle/legacy_8i/107_oracle_top_buffer_gets_sql.sql +31 -0
- db_ops/metrics/collectors/oracle/legacy_8i/108_oracle_segment_extent_limit.sql +39 -0
- db_ops/metrics/collectors/oracle/legacy_8i/109_oracle_statistics_age.sql +35 -0
- db_ops/metrics/collectors/oracle/legacy_8i/110_oracle_archive_dest.sql +67 -0
- db_ops/metrics/collectors/oracle/legacy_8i/111_oracle_rollback_contention.sql +36 -0
- db_ops/metrics/collectors/oracle/legacy_8i/112_oracle_jobs.sql +43 -0
- db_ops/metrics/collectors/oracle/legacy_8i/113_oracle_dead_sessions_and_threads.sql +78 -0
- db_ops/metrics/collectors/os/linux/001_os_cpu_usage.sh +48 -0
- db_ops/metrics/collectors/os/linux/002_os_memory_usage.sh +59 -0
- db_ops/metrics/collectors/os/linux/003_os_disk_usage.sh +48 -0
- db_ops/metrics/collectors/os/linux/004_os_top_processes.sh +12 -0
- db_ops/metrics/collectors/os/linux/005_os_service_status.sh +80 -0
- db_ops/metrics/collectors/os/linux/006_os_reboot_pending.sh +15 -0
- db_ops/metrics/collectors/os/linux/007_os_uptime.sh +13 -0
- db_ops/metrics/collectors/os/linux/008_os_time_sync.sh +16 -0
- db_ops/metrics/collectors/os/linux/009_os_tcp_port_status.sh +127 -0
- db_ops/metrics/collectors/os/linux/010_os_info.sh +31 -0
- db_ops/metrics/collectors/os/linux/011_os_network.sh +67 -0
- db_ops/metrics/collectors/os/linux/012_os_eventlog_critical.sh +38 -0
- db_ops/metrics/collectors/os/linux/013_os_process_top_cpu.sh +20 -0
- db_ops/metrics/collectors/os/linux/014_os_process_top_memory.sh +23 -0
- db_ops/metrics/collectors/os/windows/001_os_cpu_usage.ps1 +37 -0
- db_ops/metrics/collectors/os/windows/002_os_memory_usage.ps1 +42 -0
- db_ops/metrics/collectors/os/windows/003_os_disk_usage.ps1 +98 -0
- db_ops/metrics/collectors/os/windows/004_os_top_processes.ps1 +19 -0
- db_ops/metrics/collectors/os/windows/005_os_service_status.ps1 +32 -0
- db_ops/metrics/collectors/os/windows/006_os_reboot_pending.ps1 +16 -0
- db_ops/metrics/collectors/os/windows/007_os_uptime.ps1 +14 -0
- db_ops/metrics/collectors/os/windows/008_os_time_sync.ps1 +53 -0
- db_ops/metrics/collectors/os/windows/009_os_tcp_port_status.ps1 +149 -0
- db_ops/metrics/collectors/os/windows/010_os_info.ps1 +21 -0
- db_ops/metrics/collectors/os/windows/011_os_network.ps1 +73 -0
- db_ops/metrics/collectors/os/windows/012_os_eventlog_critical.ps1 +95 -0
- db_ops/metrics/collectors/os/windows/013_os_process_top_cpu.ps1 +52 -0
- db_ops/metrics/collectors/os/windows/014_os_process_top_memory.ps1 +31 -0
- db_ops/metrics/collectors/os/windows/015_os_failover_cluster.ps1 +191 -0
- db_ops/metrics/collectors/postgresql/001_postgresql_instance_status.sql +8 -0
- db_ops/metrics/collectors/postgresql/002_postgresql_database_status.sql +16 -0
- db_ops/metrics/collectors/postgresql/003_postgresql_connections.sql +15 -0
- db_ops/metrics/collectors/postgresql/004_postgresql_long_running_queries.sql +18 -0
- db_ops/metrics/collectors/postgresql/006_postgresql_database_size.sql +10 -0
- db_ops/metrics/collectors/postgresql/009_postgresql_blocking_sessions.sql +16 -0
- db_ops/metrics/collectors/postgresql/022_postgresql_last_backup_result.sh +129 -0
- db_ops/metrics/collectors/postgresql/050_postgresql_replication.sql +97 -0
- db_ops/metrics/collectors/postgresql/051_postgresql_identity_role.sql +6 -0
- db_ops/metrics/collectors/postgresql/052_postgresql_replication_slots.sql +10 -0
- db_ops/metrics/collectors/postgresql/053_postgresql_wal_archive.sql +6 -0
- db_ops/metrics/collectors/postgresql/054_postgresql_transactions.sql +7 -0
- db_ops/metrics/collectors/postgresql/055_postgresql_vacuum_health.sql +7 -0
- db_ops/metrics/collectors/postgresql/056_postgresql_xid_wraparound.sql +6 -0
- db_ops/metrics/collectors/postgresql/057_postgresql_database_health.sql +7 -0
- db_ops/metrics/collectors/postgresql/058_postgresql_invalid_indexes.sql +4 -0
- db_ops/metrics/collectors/postgresql/059_postgresql_role_permissions.sql +53 -0
- db_ops/metrics/collectors/postgresql/060_postgresql_constraint_health.sql +54 -0
- db_ops/metrics/collectors/postgresql/061_postgresql_database_data_size.sql +28 -0
- db_ops/metrics/collectors/postgresql/062_postgresql_table_bloat.sql +56 -0
- db_ops/metrics/collectors/postgresql/072_postgresql_index_usage.sql +183 -0
- db_ops/metrics/collectors/sqlserver/001_sqlserver_instance_status.sql +39 -0
- db_ops/metrics/collectors/sqlserver/002_sqlserver_database_status.sql +31 -0
- db_ops/metrics/collectors/sqlserver/003_sqlserver_connections.sql +17 -0
- db_ops/metrics/collectors/sqlserver/004_sqlserver_long_running_queries.sql +130 -0
- db_ops/metrics/collectors/sqlserver/005_sqlserver_backup_age.sql +41 -0
- db_ops/metrics/collectors/sqlserver/006_sqlserver_data_file_space.sql +132 -0
- db_ops/metrics/collectors/sqlserver/007_sqlserver_log_space.sql +96 -0
- db_ops/metrics/collectors/sqlserver/008_sqlserver_tempdb_space.sql +41 -0
- db_ops/metrics/collectors/sqlserver/009_sqlserver_blocking_sessions.sql +136 -0
- db_ops/metrics/collectors/sqlserver/010_sqlserver_agent_failed_jobs.sql +108 -0
- db_ops/metrics/collectors/sqlserver/011_sqlserver_ag_database_health.sql +29 -0
- db_ops/metrics/collectors/sqlserver/012_sqlserver_cpu_memory.sql +217 -0
- db_ops/metrics/collectors/sqlserver/013_sqlserver_disk_free_space.sql +54 -0
- db_ops/metrics/collectors/sqlserver/014_sqlserver_wait_stats.sql +71 -0
- db_ops/metrics/collectors/sqlserver/015_sqlserver_deadlock_recent.sql +39 -0
- db_ops/metrics/collectors/sqlserver/016_sqlserver_backup_job_status.sql +154 -0
- db_ops/metrics/collectors/sqlserver/017_sqlserver_errorlog_recent_critical.sql +249 -0
- db_ops/metrics/collectors/sqlserver/018_sqlserver_log_reuse_wait.sql +49 -0
- db_ops/metrics/collectors/sqlserver/019_sqlserver_io_latency.sql +107 -0
- db_ops/metrics/collectors/sqlserver/021_sqlserver_suspect_pages.sql +45 -0
- db_ops/metrics/collectors/sqlserver/022_sqlserver_last_backup_result.sql +87 -0
- db_ops/metrics/collectors/sqlserver/023_sqlserver_query_store_query_issues.sql +416 -0
- db_ops/metrics/collectors/sqlserver/024_sqlserver_sleeping_open_transaction.sql +167 -0
- db_ops/metrics/collectors/sqlserver/025_sqlserver_transaction_lock_holders.sql +221 -0
- db_ops/metrics/collectors/sqlserver/026_long_waiting_or_rollback_requests.sql +241 -0
- db_ops/metrics/collectors/sqlserver/050_sqlserver_configuration.sql +24 -0
- db_ops/metrics/collectors/sqlserver/051_sqlserver_database_config.sql +22 -0
- db_ops/metrics/collectors/sqlserver/052_sqlserver_page_life_expectancy.sql +11 -0
- db_ops/metrics/collectors/sqlserver/053_sqlserver_agent_job_inventory.sql +122 -0
- db_ops/metrics/collectors/sqlserver/054_sqlserver_database_checkdb.sql +131 -0
- db_ops/metrics/collectors/sqlserver/055_sqlserver_file_placement.sql +21 -0
- db_ops/metrics/collectors/sqlserver/056_sqlserver_agent_job_runtime.sql +91 -0
- db_ops/metrics/collectors/sqlserver/057_sqlserver_database_user_permissions.sql +118 -0
- db_ops/metrics/collectors/sqlserver/058_sqlserver_constraint_health.sql +135 -0
- db_ops/metrics/collectors/sqlserver/059_sqlserver_certificate_expiry.sql +67 -0
- db_ops/metrics/collectors/sqlserver/060_sqlserver_failed_logins.sql +73 -0
- db_ops/metrics/collectors/sqlserver/061_sqlserver_index_fragmentation.sql +67 -0
- db_ops/metrics/collectors/sqlserver/062_sqlserver_statistics_age.sql +62 -0
- db_ops/metrics/collectors/sqlserver/063_sqlserver_login_health.sql +147 -0
- db_ops/metrics/collectors/sqlserver/064_sqlserver_database_data_size.sql +36 -0
- db_ops/metrics/collectors/sqlserver/065_sqlserver_database_log_size.sql +30 -0
- db_ops/metrics/collectors/sqlserver/066_sqlserver_heap_fragmentation.sql +92 -0
- db_ops/metrics/collectors/sqlserver/067_sqlserver_vlf_count.sql +67 -0
- db_ops/metrics/collectors/sqlserver/068_sqlserver_index_usage.sql +304 -0
- db_ops/metrics/collectors/sqlserver/069_sqlserver_linked_server_status.sql +206 -0
- db_ops/metrics/collectors/sqlserver/070_sqlserver_query_store_coverage.sql +221 -0
- db_ops/metrics/collectors/sqlserver/071_sqlserver_server_principals.sql +99 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/001_sqlserver_instance_status.sql +35 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/002_sqlserver_database_status.sql +32 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/003_sqlserver_connections.sql +15 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/004_sqlserver_long_running_queries.sql +128 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/005_sqlserver_backup_age.sql +37 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/006_sqlserver_data_file_space.sql +50 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/007_sqlserver_log_space.sql +37 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/008_sqlserver_tempdb_space.sql +53 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/009_sqlserver_blocking_sessions.sql +37 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/010_sqlserver_agent_failed_jobs.sql +79 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/011_sqlserver_ag_database_health.sql +6 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/012_sqlserver_cpu_memory.sql +217 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/013_sqlserver_disk_free_space.sql +24 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/014_sqlserver_wait_stats.sql +28 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/015_sqlserver_deadlock_recent.sql +6 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/016_sqlserver_backup_job_status.sql +103 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/017_sqlserver_errorlog_recent_critical.sql +249 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/018_sqlserver_log_reuse_wait.sql +39 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/019_sqlserver_io_latency.sql +87 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/021_sqlserver_suspect_pages.sql +45 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/022_sqlserver_last_backup_result.sql +87 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/023_sqlserver_query_store_query_issues.sql +11 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/024_sqlserver_sleeping_open_transaction.sql +33 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/025_sqlserver_transaction_lock_holders.sql +154 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/026_long_waiting_or_rollback_requests.sql +241 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/054_sqlserver_database_checkdb.sql +136 -0
- db_ops/metrics/collectors/sqlserver/legacy_2008r2/069_sqlserver_linked_server_status.sql +189 -0
- db_ops/metrics/definitions.py +319 -0
- db_ops/metrics/executor.py +307 -0
- db_ops/metrics/health.py +83 -0
- db_ops/metrics/importance.py +96 -0
- db_ops/metrics/message.py +68 -0
- db_ops/metrics/models.py +148 -0
- db_ops/metrics/notify.py +28 -0
- db_ops/metrics/storage.py +29 -0
- db_ops/metrics/targets.py +293 -0
- db_ops/scaffold.py +495 -0
- db_ops/telegram/__init__.py +43 -0
- db_ops/telegram/api.py +316 -0
- db_ops/telegram/cli.py +219 -0
- db_ops/telegram/client.py +38 -0
- db_ops/telegram/command_processor.py +3239 -0
- db_ops/telegram/commands.py +41 -0
- db_ops/telegram/metrics_reports.py +93 -0
- db_ops/telegram/routing.py +75 -0
- db_ops/telegram/send_queue.py +132 -0
- db_ops/telegram/severity.py +32 -0
- db_ops/telegram/sql_commands.py +201 -0
- db_ops/telegram/updates.py +251 -0
- db_ops/telegram/workflow.py +75 -0
- dbabrain-0.1.0.dist-info/METADATA +297 -0
- dbabrain-0.1.0.dist-info/RECORD +378 -0
- dbabrain-0.1.0.dist-info/WHEEL +5 -0
- dbabrain-0.1.0.dist-info/entry_points.txt +2 -0
- dbabrain-0.1.0.dist-info/licenses/LICENSE +201 -0
- dbabrain-0.1.0.dist-info/licenses/NOTICE +57 -0
- 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)
|