Krikri::PluginHelpers::PostgresqlConnection
PostgresqlConnection - pure logic for building a postgres:// connection URI from Ansible-style login_* params. No I/O - postgresql_db.cr/postgresql_user.cr do the actual DB.open.
Constants
unix_socket, given, takes precedence over host/port (matches
Ansible's postgresql_db/postgresql_user own login_unix_socket
precedence). crystal-pg expects a unix socket path via a "host"
query param, not the URI's own host component (verified against
its actual conninfo parsing source, not assumed) - the URI host
is left blank in that case.
Debian/Ubuntu's postgresql-common packaging (the target of every
real-host benchmark round so far) compiles libpq's default Unix
socket directory to this path - matches postgresql.conf's own
unix_socket_directories default there.
libpq's wording for a connection the server accepted and then dropped before/during startup packet exchange (both a clean close and an RST - live-verified against ansible-core 2.19.11 + community.postgresql 4.2.0: SO_LINGER-0 RST on the accepted socket reports this same text, no "Is the server running..." hint line either). Ends with its own newline, like libpq's PQerrorMessage buffer always does.
Class methods
Resolves the effective connect timeout for a module run, the two
ways community.postgresql lets one be set (it has no top-level
connect_timeout option): connect_params: {connect_timeout: N}
first, then PGCONNECT_TIMEOUT. Nil = unbounded (no DB-level
deadline).
The connection target a params hash resolves to, for diagnostics that have to name the server the plugin actually dialed (libpq's connection-error text quotes the host/port or the socket path verbatim). Applies the same defaults #build_uri does: an absent host means a Unix socket in DEFAULT_UNIX_SOCKET_DIR, an absent port means 5432.
libpq's "could not translate host name" wording for an EAI_* code: gai_strerror's text for the code itself, except EAI_SYSTEM which libpq's connectDBStart words with strerror(errno) instead (glibc's gai_strerror would say "System error").
gai_strerror(3)'s text for an EAI_* code - glibc's on this platform, which is what libpq prints verbatim.
libpq's own connection-error wording, which is what real (psycopg2 -> libpq) embeds in community.postgresql's "unable to connect to database: %s" fail_json. Live-verified against ansible-core 2.19.11 + community.postgresql 4.2.0 for every case reproduced below:
- TCP, nothing listening (ECONNREFUSED): connection to server at "127.0.0.1", port 59999 failed: Connection refused\n \tIs the server running on that host and accepting TCP/IP connections?\n
- TCP, unroutable netns (ENETUNREACH,
unshare -rn): connection to server at "127.0.0.1", port 59999 failed: Network is unreachable\n \tIs the server running on that host and accepting TCP/IP connections?\n - TCP, blackhole address, natural (unbounded) connect timeout: connection to server at "192.0.2.1", port 5432 failed: Connection timed out\n \tIs the server running on that host and accepting TCP/IP connections?\n (real words a libpq connect_timeout expiry differently - "timeout expired", no hint - when a connect_timeout is set; krikri models that wording via PostgresqlConnection's own bounded open, see the module's timeout comment.)
- TCP, server accepted the connection and then dropped it (both a clean close and an SO_LINGER-0 RST on the accepted socket): connection to server at "127.0.0.1", port 59999 failed: server closed the connection unexpectedly\n \tThis probably means the server terminated abnormally\n \tbefore or while processing the request.\n
- Unix socket, socket file/dir missing (ENOENT): connection to server on socket "/nonexistent/sockdir/.s.PGSQL.5432" failed: No such file or directory\n \tIs the server running locally and accepting connections on that socket?\n
- server answered and rejected us (its ErrorResponse carries the text, so the server-side severity is rebuilt as "<severity>: <message>\n", and NO hint line follows): connection to server at "127.0.0.1", port 35433 failed: FATAL: password authentication failed for user "postgres"\n connection to server at "127.0.0.1", port 35433 failed: FATAL: database "nosuchdb" does not exist\n
- unresolvable host (libpq's own DNS wording, no hint line): could not translate host name "nosuchhost.invalid" to address: Name or service not known\n
Reproducible vs not: the errno behind a TCP connect failure is re-derived with a direct non-blocking probe connect at error-report time (see #io_error_detail), because Crystal's Socket::ConnectError misreports the errno ("Resource temporarily unavailable" for a refused connect) and collapses ECONNRESET/ENETUNREACH/ETIMEDOUT into the same exception class. What krikri CANNOT reproduce: a temporary resolver failure, which libpq renders with gai_strerror's "Temporary failure in name resolution" (Crystal's Socket::Addrinfo::Error carries no gai code, so krikri prints the EAI_NONAME wording instead), any strerror string from a non-glibc libc (libpq prints the system's own, locale-dependent strings). The libpq connect_timeout wording ("timeout expired"), formerly in this not-reproducible list, is now modeled - see ConnectTimeoutCause / #libpq_connect_error below.
DB.open bounded by timeout (nil = plain DB.open). Only the
connection attempt runs under the deadline - the module block
runs on the connected database afterwards, in the caller's
fiber, so a slow query is never misreported as a connect
timeout. Raises DB::ConnectionRefused (cause:
ConnectTimeoutCause) when the deadline expires; a database that
finishes connecting after the deadline is closed by the fiber
that built it (see #connect_within_deadline), so neither that
fiber nor the connection leaks. crystal-db's pool builds its
initial connection eagerly inside DB::Database.new and, with
retry_attempts=0, ConnectionRefused is never retried or
swallowed there, so the only connection attempt in flight is
the one this bounds.
The wording for a probe result (see #io_error_detail for the live-verified cases). Split from the probing itself so the deadline-vs-errno decision stays unit-testable without a real blackhole route.
community.postgresql's shared ensure_required_libs() (module_utils/ postgres.py, 4.2.0) fails the task with Ansible's missing_required_lib wording when the target's Python cannot import psycopg2 - after the argspec checks but before get_conn_params() and the connect attempt. krikri's postgresql_* plugins talk the wire protocol through crystal-pg and need no psycopg2, so they used to carry on past the point real stops and report their own downstream connect failure instead (consensys.web3signer's postgresql_query task, issue #467: "unable to connect to database: ... Connection refused" where real failed with "Failed to import the required Python library (psycopg2) ..."). The dependency still decides the outcome, so it is reproduced here - same message, same ordering - like the passlib gate in htpasswd. Real passes no exception= to fail_json here, so the [ERROR] block carries the msg alone, unlike deb822_repository. (expect's own pexpect gate is gone: krikri's native pty path has no pexpect dependency, and the generator's podman pair provisions pexpect on the real side only, so probing the local interpreter there failed tasks the reference ansible carried out.)
nil when the target's Python imports psycopg2 cleanly - the module connects, exactly as real's does.
Resolves the login_host:/login_port:/login_user:/
login_unix_socket: params from a plugin's raw @params,
falling back to each one's real-Ansible deprecated alias
(host:/port:/login:/unix_socket: respectively -
login_password: has none) when the canonical login_* form
isn't set. Shared by postgresql_db.cr/postgresql_user.cr/
postgresql_privs.cr so the alias list only lives in one place.
Real bug found benchmarking robertdebock.postgres (round 43):
its own "Create postgres users" task writes port: "{{ postgres_port }}" (the alias, for a non-default port like
6543) - each plugin only ever read login_port:, so the
connection silently fell back to port 5432's Unix socket
instead, which doesn't exist on this host at all
(DB::ConnectionRefused / PQ::ConnectionError: Cannot establish connection, confirmed via direct debug instrumentation
against a live host - the same URI, built by hand with the
correct port included, connected via psql immediately).